move the json resource into its own module
split the json-rpc methods and the rpc server into seperate classes make the json-rpc server a component to allow for extension
This commit is contained in:
parent
344446a626
commit
0ac64da5c6
|
@ -0,0 +1,30 @@
|
||||||
|
#
|
||||||
|
# deluge/ui/web/auth.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
AUTH_LEVEL_NONE = 0
|
||||||
|
AUTH_LEVEL_READONLY = 1
|
||||||
|
AUTH_LEVEL_NORMAL = 5
|
||||||
|
AUTH_LEVEL_ADMIN = 10
|
||||||
|
|
||||||
|
AUTH_LEVEL_DEFAULT = AUTH_LEVEL_NORMAL
|
|
@ -0,0 +1,40 @@
|
||||||
|
#
|
||||||
|
# deluge/ui/web/common.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
from mako.template import Template as MakoTemplate
|
||||||
|
from deluge import common
|
||||||
|
|
||||||
|
_ = gettext.gettext
|
||||||
|
|
||||||
|
class Template(MakoTemplate):
|
||||||
|
|
||||||
|
builtins = {
|
||||||
|
"_": _,
|
||||||
|
"version": common.get_version()
|
||||||
|
}
|
||||||
|
|
||||||
|
def render(self, *args, **data):
|
||||||
|
data.update(self.builtins)
|
||||||
|
return MakoTemplate.render(self, *args, **data)
|
|
@ -0,0 +1,413 @@
|
||||||
|
#
|
||||||
|
# deluge/ui/web/json_api.py
|
||||||
|
#
|
||||||
|
# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
|
||||||
|
#
|
||||||
|
# Deluge is free software.
|
||||||
|
#
|
||||||
|
# You may redistribute it and/or modify it under the terms of the
|
||||||
|
# GNU General Public License, as published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option)
|
||||||
|
# any later version.
|
||||||
|
#
|
||||||
|
# deluge is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
# See the GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with deluge. If not, write to:
|
||||||
|
# The Free Software Foundation, Inc.,
|
||||||
|
# 51 Franklin Street, Fifth Floor
|
||||||
|
# Boston, MA 02110-1301, USA.
|
||||||
|
#
|
||||||
|
|
||||||
|
import time
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from twisted.internet.defer import Deferred
|
||||||
|
from twisted.web import http, resource, server
|
||||||
|
|
||||||
|
from deluge import common, component
|
||||||
|
from deluge.configmanager import ConfigManager
|
||||||
|
from deluge.ui.client import client, Client
|
||||||
|
from deluge.ui.web.auth import *
|
||||||
|
from deluge.ui.web.common import _
|
||||||
|
json = common.json
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def export(auth_level=AUTH_LEVEL_DEFAULT):
|
||||||
|
"""
|
||||||
|
Decorator function to register an object's method as an RPC. The object
|
||||||
|
will need to be registered with an `:class:RPCServer` to be effective.
|
||||||
|
|
||||||
|
:param func: function, the function to export
|
||||||
|
:param auth_level: int, the auth level required to call this method
|
||||||
|
|
||||||
|
"""
|
||||||
|
def wrap(func, *args, **kwargs):
|
||||||
|
func._json_export = True
|
||||||
|
func._json_auth_level = auth_level
|
||||||
|
return func
|
||||||
|
|
||||||
|
return wrap
|
||||||
|
|
||||||
|
class JSONException(Exception):
|
||||||
|
def __init__(self, inner_exception):
|
||||||
|
self.inner_exception = inner_exception
|
||||||
|
Exception.__init__(self, str(inner_exception))
|
||||||
|
|
||||||
|
class JSON(resource.Resource, component.Component):
|
||||||
|
"""
|
||||||
|
A Twisted Web resource that exposes a JSON-RPC interface for web clients
|
||||||
|
to use.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
resource.Resource.__init__(self)
|
||||||
|
component.Component.__init__(self, "JSON")
|
||||||
|
self._remote_methods = []
|
||||||
|
self._local_methods = {}
|
||||||
|
|
||||||
|
def connect(self, host="localhost", port=58846, username="", password=""):
|
||||||
|
"""
|
||||||
|
Connects the client to a daemon
|
||||||
|
"""
|
||||||
|
d = Deferred()
|
||||||
|
_d = client.connect(host, port, username, password)
|
||||||
|
|
||||||
|
def on_get_methods(methods):
|
||||||
|
"""
|
||||||
|
Handles receiving the method names
|
||||||
|
"""
|
||||||
|
self._remote_methods = methods
|
||||||
|
methods = list(self._remote_methods)
|
||||||
|
methods.extend(self._local_methods)
|
||||||
|
d.callback(methods)
|
||||||
|
|
||||||
|
def on_client_connected(connection_id):
|
||||||
|
"""
|
||||||
|
Handles the client successfully connecting to the daemon and
|
||||||
|
invokes retrieving the method names.
|
||||||
|
"""
|
||||||
|
d = client.daemon.get_method_list()
|
||||||
|
d.addCallback(on_get_methods)
|
||||||
|
_d.addCallback(on_client_connected)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _exec_local(self, method, params):
|
||||||
|
"""
|
||||||
|
Handles executing all local methods.
|
||||||
|
"""
|
||||||
|
if method == "system.listMethods":
|
||||||
|
d = Deferred()
|
||||||
|
methods = list(self._remote_methods)
|
||||||
|
methods.extend(self._local_methods)
|
||||||
|
d.callback(methods)
|
||||||
|
return d
|
||||||
|
elif method in self._local_methods:
|
||||||
|
# This will eventually process methods that the server adds
|
||||||
|
# and any plugins.
|
||||||
|
return self._local_methods[method](*params)
|
||||||
|
raise JSONException("Unknown system method")
|
||||||
|
|
||||||
|
def _exec_remote(self, method, params):
|
||||||
|
"""
|
||||||
|
Executes methods using the Deluge client.
|
||||||
|
"""
|
||||||
|
component, method = method.split(".")
|
||||||
|
return getattr(getattr(client, component), method)(*params)
|
||||||
|
|
||||||
|
def _handle_request(self, request):
|
||||||
|
"""
|
||||||
|
Takes some json data as a string and attempts to decode it, and process
|
||||||
|
the rpc object that should be contained, returning a deferred for all
|
||||||
|
procedure calls and the request id.
|
||||||
|
"""
|
||||||
|
request_id = None
|
||||||
|
try:
|
||||||
|
request = json.loads(request)
|
||||||
|
except ValueError:
|
||||||
|
raise JSONException("JSON not decodable")
|
||||||
|
|
||||||
|
if "method" not in request or "id" not in request or \
|
||||||
|
"params" not in request:
|
||||||
|
raise JSONException("Invalid JSON request")
|
||||||
|
|
||||||
|
method, params = request["method"], request["params"]
|
||||||
|
request_id = request["id"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
if method.startswith("system."):
|
||||||
|
return self._exec_local(method, params), request_id
|
||||||
|
elif method in self._local_methods:
|
||||||
|
return self._exec_local(method, params), request_id
|
||||||
|
elif method in self._remote_methods:
|
||||||
|
return self._exec_remote(method, params), request_id
|
||||||
|
except Exception, e:
|
||||||
|
log.exception(e)
|
||||||
|
d = Deferred()
|
||||||
|
d.callback(None)
|
||||||
|
return d, request_id
|
||||||
|
|
||||||
|
def _on_rpc_request_finished(self, result, response, request):
|
||||||
|
"""
|
||||||
|
Sends the response of any rpc calls back to the json-rpc client.
|
||||||
|
"""
|
||||||
|
response["result"] = result
|
||||||
|
return self._send_response(request, response)
|
||||||
|
|
||||||
|
def _on_rpc_request_failed(self, reason, response, request):
|
||||||
|
"""
|
||||||
|
Handles any failures that occured while making an rpc call.
|
||||||
|
"""
|
||||||
|
print type(reason)
|
||||||
|
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _on_json_request(self, request):
|
||||||
|
"""
|
||||||
|
Handler to take the json data as a string and pass it on to the
|
||||||
|
_handle_request method for further processing.
|
||||||
|
"""
|
||||||
|
log.debug("json-request: %s", request.json)
|
||||||
|
response = {"result": None, "error": None, "id": None}
|
||||||
|
d, response["id"] = self._handle_request(request.json)
|
||||||
|
d.addCallback(self._on_rpc_request_finished, response, request)
|
||||||
|
d.addErrback(self._on_rpc_request_failed, response, request)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _on_json_request_failed(self, reason, request):
|
||||||
|
"""
|
||||||
|
Errback handler to return a HTTP code of 500.
|
||||||
|
"""
|
||||||
|
log.exception(reason)
|
||||||
|
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _send_response(self, request, response):
|
||||||
|
response = json.dumps(response)
|
||||||
|
request.setHeader("content-type", "application/x-json")
|
||||||
|
request.write(response)
|
||||||
|
request.finish()
|
||||||
|
|
||||||
|
def render(self, request):
|
||||||
|
"""
|
||||||
|
Handles all the POST requests made to the /json controller.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if request.method != "POST":
|
||||||
|
request.setResponseCode(http.NOT_ALLOWED)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
request.content.seek(0)
|
||||||
|
request.json = request.content.read()
|
||||||
|
d = self._on_json_request(request)
|
||||||
|
return server.NOT_DONE_YET
|
||||||
|
except Exception, e:
|
||||||
|
return self._on_json_request_failed(e, request)
|
||||||
|
|
||||||
|
def register_object(self, obj, name=None):
|
||||||
|
"""
|
||||||
|
Registers an object to export it's rpc methods. These methods should
|
||||||
|
be exported with the export decorator prior to registering the object.
|
||||||
|
|
||||||
|
:param obj: object, the object that we want to export
|
||||||
|
:param name: str, the name to use, if None, it will be the class name of the object
|
||||||
|
"""
|
||||||
|
name = name or obj.__class__.__name__
|
||||||
|
name = name.lower()
|
||||||
|
|
||||||
|
for d in dir(obj):
|
||||||
|
if d[0] == "_":
|
||||||
|
continue
|
||||||
|
if getattr(getattr(obj, d), '_json_export', False):
|
||||||
|
log.debug("Registering method: %s", name + "." + d)
|
||||||
|
self._local_methods[name + "." + d] = getattr(obj, d)
|
||||||
|
|
||||||
|
class JSONComponent(component.Component):
|
||||||
|
def __init__(self, name, interval=1, depend=None):
|
||||||
|
super(JSONComponent, self).__init__(name, interval, depend)
|
||||||
|
self._json = component.get("JSON")
|
||||||
|
self._json.register_object(self, name)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_HOST = "127.0.0.1"
|
||||||
|
DEFAULT_PORT = 58846
|
||||||
|
|
||||||
|
DEFAULT_HOSTS = {
|
||||||
|
"hosts": [(hashlib.sha1(str(time.time())).hexdigest(),
|
||||||
|
DEFAULT_HOST, DEFAULT_PORT, "", "")]
|
||||||
|
}
|
||||||
|
|
||||||
|
class WebApi(JSONComponent):
|
||||||
|
def __init__(self):
|
||||||
|
super(WebApi, self).__init__("Web")
|
||||||
|
self.host_list = ConfigManager("hostlist.conf.1.2", DEFAULT_HOSTS)
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def connect(self, host_id):
|
||||||
|
d = Deferred()
|
||||||
|
def on_connected(methods):
|
||||||
|
d.callback(methods)
|
||||||
|
for host in self.host_list["hosts"]:
|
||||||
|
if host_id != host[0]:
|
||||||
|
continue
|
||||||
|
self._json.connect(*host[1:]).addCallback(on_connected)
|
||||||
|
return d
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def connected(self):
|
||||||
|
d = Deferred()
|
||||||
|
d.callback(client.connected())
|
||||||
|
return d
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def update_ui(self, keys, filter_dict):
|
||||||
|
|
||||||
|
ui_info = {
|
||||||
|
"torrents": None,
|
||||||
|
"filters": None,
|
||||||
|
"stats": None
|
||||||
|
}
|
||||||
|
|
||||||
|
d = Deferred()
|
||||||
|
|
||||||
|
def got_stats(stats):
|
||||||
|
ui_info["stats"] = stats
|
||||||
|
d.callback(ui_info)
|
||||||
|
|
||||||
|
def got_filters(filters):
|
||||||
|
ui_info["filters"] = filters
|
||||||
|
client.core.get_stats().addCallback(got_stats)
|
||||||
|
|
||||||
|
def got_torrents(torrents):
|
||||||
|
ui_info["torrents"] = torrents
|
||||||
|
client.core.get_filter_tree().addCallback(got_filters)
|
||||||
|
client.core.get_torrents_status(filter_dict, keys).addCallback(got_torrents)
|
||||||
|
return d
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def download_torrent_from_url(self, url):
|
||||||
|
"""
|
||||||
|
input:
|
||||||
|
url: the url of the torrent to download
|
||||||
|
|
||||||
|
returns:
|
||||||
|
filename: the temporary file name of the torrent file
|
||||||
|
"""
|
||||||
|
tmp_file = os.path.join(tempfile.gettempdir(), url.split("/")[-1])
|
||||||
|
filename, headers = urllib.urlretrieve(url, tmp_file)
|
||||||
|
log.debug("filename: %s", filename)
|
||||||
|
d = Deferred()
|
||||||
|
d.callback(filename)
|
||||||
|
return d
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def get_torrent_info(self, filename):
|
||||||
|
"""
|
||||||
|
Goal:
|
||||||
|
allow the webui to retrieve data about the torrent
|
||||||
|
|
||||||
|
input:
|
||||||
|
filename: the filename of the torrent to gather info about
|
||||||
|
|
||||||
|
returns:
|
||||||
|
{
|
||||||
|
"filename": the torrent file
|
||||||
|
"name": the torrent name
|
||||||
|
"size": the total size of the torrent
|
||||||
|
"files": the files the torrent contains
|
||||||
|
"info_hash" the torrents info_hash
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
d = Deferred()
|
||||||
|
d.callback(uicommon.get_torrent_info(filename.strip()))
|
||||||
|
return d
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def add_torrents(self, torrents):
|
||||||
|
"""
|
||||||
|
input:
|
||||||
|
torrents [{
|
||||||
|
path: the path of the torrent file,
|
||||||
|
options: the torrent options
|
||||||
|
}]
|
||||||
|
"""
|
||||||
|
for torrent in torrents:
|
||||||
|
filename = os.path.basename(torrent["path"])
|
||||||
|
fdump = open(torrent["path"], "r").read()
|
||||||
|
client.add_torrent_file(filename, fdump, torrent["options"])
|
||||||
|
d = Deferred()
|
||||||
|
d.callback(True)
|
||||||
|
return d
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def login(self, password):
|
||||||
|
"""Method to allow the webui to authenticate
|
||||||
|
"""
|
||||||
|
config = component.get("DelugeWeb").config
|
||||||
|
m = hashlib.md5()
|
||||||
|
m.update(config['pwd_salt'])
|
||||||
|
m.update(password)
|
||||||
|
d = Deferred()
|
||||||
|
d.callback(m.hexdigest() == config['pwd_md5'])
|
||||||
|
return d
|
||||||
|
|
||||||
|
@export()
|
||||||
|
def get_hosts(self):
|
||||||
|
"""Return the hosts in the hostlist"""
|
||||||
|
hosts = dict((host[0], host[:]) for host in self.host_list["hosts"])
|
||||||
|
|
||||||
|
main_deferred = Deferred()
|
||||||
|
def run_check():
|
||||||
|
if all(map(lambda x: x[3] is not None, hosts.values())):
|
||||||
|
main_deferred.callback(hosts.values())
|
||||||
|
|
||||||
|
def on_connect(result, c, host_id):
|
||||||
|
def on_info(info, c):
|
||||||
|
hosts[host_id][3] = _("Online")
|
||||||
|
hosts[host_id][4] = info
|
||||||
|
c.disconnect()
|
||||||
|
run_check()
|
||||||
|
|
||||||
|
def on_info_fail(reason):
|
||||||
|
hosts[host_id][3] = _("Offline")
|
||||||
|
run_check()
|
||||||
|
|
||||||
|
if not c.connected():
|
||||||
|
hosts[host_id][3] = _("Offline")
|
||||||
|
run_check()
|
||||||
|
return
|
||||||
|
|
||||||
|
d = c.daemon.info()
|
||||||
|
d.addCallback(on_info, c)
|
||||||
|
d.addErrback(on_info_fail)
|
||||||
|
|
||||||
|
def on_connect_failed(reason, host_id):
|
||||||
|
log.exception(reason)
|
||||||
|
hosts[host_id][3] = _("Offline")
|
||||||
|
run_check()
|
||||||
|
|
||||||
|
for host in hosts.values():
|
||||||
|
host_id, host, port, user, password = host[0:5]
|
||||||
|
hosts[host_id][3:4] = (None, None)
|
||||||
|
|
||||||
|
if client.connected() and \
|
||||||
|
(host, port, user) == client.connection_info():
|
||||||
|
def on_info(info):
|
||||||
|
hosts[host_id][4] = info
|
||||||
|
run_check()
|
||||||
|
host[5] = _("Connected")
|
||||||
|
client.daemon.info().addCallback(on_info)
|
||||||
|
hosts[host_id] = host
|
||||||
|
continue
|
||||||
|
|
||||||
|
c = Client()
|
||||||
|
d = c.connect(host, port, user, password)
|
||||||
|
d.addCallback(on_connect, c, host_id)
|
||||||
|
d.addErrback(on_connect_failed, host_id)
|
||||||
|
return main_deferred
|
|
@ -36,19 +36,16 @@ import pkg_resources
|
||||||
|
|
||||||
from twisted.application import service, internet
|
from twisted.application import service, internet
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.internet.defer import Deferred
|
|
||||||
from twisted.web import http, resource, server, static
|
from twisted.web import http, resource, server, static
|
||||||
|
|
||||||
from mako.template import Template as MakoTemplate
|
|
||||||
|
|
||||||
from deluge import common, component
|
from deluge import common, component
|
||||||
from deluge.configmanager import ConfigManager
|
from deluge.configmanager import ConfigManager
|
||||||
from deluge.log import setupLogger, LOG as _log
|
from deluge.log import setupLogger, LOG as _log
|
||||||
from deluge.ui import common as uicommon
|
from deluge.ui import common as uicommon
|
||||||
from deluge.ui.client import client, Client
|
|
||||||
from deluge.ui.tracker_icons import TrackerIcons
|
from deluge.ui.tracker_icons import TrackerIcons
|
||||||
|
from deluge.ui.web.common import Template
|
||||||
|
from deluge.ui.web.json_api import JSON, WebApi
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
json = common.json
|
|
||||||
|
|
||||||
# Initialize gettext
|
# Initialize gettext
|
||||||
try:
|
try:
|
||||||
|
@ -70,8 +67,8 @@ current_dir = os.path.dirname(__file__)
|
||||||
CONFIG_DEFAULTS = {
|
CONFIG_DEFAULTS = {
|
||||||
"port": 8112,
|
"port": 8112,
|
||||||
"template": "slate",
|
"template": "slate",
|
||||||
"pwd_salt": u"2\xe8\xc7\xa6(n\x81_\x8f\xfc\xdf\x8b\xd1\x1e\xd5\x90",
|
"pwd_salt": "16f65d5c79b7e93278a28b60fed2431e",
|
||||||
"pwd_md5": u".\xe8w\\+\xec\xdb\xf2id4F\xdb\rUc",
|
"pwd_md5": "2c9baa929ca38fb5c9eb5b054474d1ce",
|
||||||
"base": "",
|
"base": "",
|
||||||
"sessions": [],
|
"sessions": [],
|
||||||
"sidebar_show_zero": False,
|
"sidebar_show_zero": False,
|
||||||
|
@ -81,354 +78,12 @@ CONFIG_DEFAULTS = {
|
||||||
"https": False
|
"https": False
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_HOST = "127.0.0.1"
|
|
||||||
DEFAULT_PORT = 58846
|
|
||||||
DEFAULT_HOSTS = {
|
|
||||||
"hosts": [[hashlib.sha1(str(time.time())).hexdigest(), DEFAULT_HOST, DEFAULT_PORT, "", ""]]
|
|
||||||
}
|
|
||||||
|
|
||||||
HOSTLIST_COL_ID = 0
|
|
||||||
HOSTLIST_COL_HOST = 1
|
|
||||||
HOSTLIST_COL_PORT = 2
|
|
||||||
HOSTLIST_COL_STATUS = 3
|
|
||||||
HOSTLIST_COL_USER = 4
|
|
||||||
HOSTLIST_COL_PASS = 5
|
|
||||||
HOSTLIST_COL_VERSION = 6
|
|
||||||
|
|
||||||
hostlist = ConfigManager("hostlist.conf.1.2", DEFAULT_HOSTS)
|
|
||||||
|
|
||||||
def rpath(path):
|
def rpath(path):
|
||||||
"""Convert a relative path into an absolute path relative to the location
|
"""Convert a relative path into an absolute path relative to the location
|
||||||
of this script.
|
of this script.
|
||||||
"""
|
"""
|
||||||
return os.path.join(current_dir, path)
|
return os.path.join(current_dir, path)
|
||||||
|
|
||||||
class Template(MakoTemplate):
|
|
||||||
|
|
||||||
builtins = {
|
|
||||||
"_": _,
|
|
||||||
"version": common.get_version()
|
|
||||||
}
|
|
||||||
|
|
||||||
def render(self, *args, **data):
|
|
||||||
data.update(self.builtins)
|
|
||||||
return MakoTemplate.render(self, *args, **data)
|
|
||||||
|
|
||||||
class JSONException(Exception):
|
|
||||||
def __init__(self, inner_exception):
|
|
||||||
self.inner_exception = inner_exception
|
|
||||||
Exception.__init__(self, str(inner_exception))
|
|
||||||
|
|
||||||
class JSON(resource.Resource, component.Component):
|
|
||||||
"""
|
|
||||||
A Twisted Web resource that exposes a JSON-RPC interface for web clients
|
|
||||||
to use.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
resource.Resource.__init__(self)
|
|
||||||
component.Component.__init__(self, "JSON")
|
|
||||||
self._remote_methods = []
|
|
||||||
self._local_methods = {
|
|
||||||
"web.update_ui": self.update_ui,
|
|
||||||
"web.download_torrent_from_url": self.download_torrent_from_url,
|
|
||||||
"web.get_torrent_info": self.get_torrent_info,
|
|
||||||
"web.add_torrents": self.add_torrents,
|
|
||||||
"web.login": self.login,
|
|
||||||
"web.get_hosts": self.get_hosts,
|
|
||||||
"web.connect": self.connect,
|
|
||||||
"web.connected": self.connected
|
|
||||||
}
|
|
||||||
|
|
||||||
def _connect(self, host="localhost", port=58846, username="", password=""):
|
|
||||||
"""
|
|
||||||
Connects the client to a daemon
|
|
||||||
"""
|
|
||||||
d = Deferred()
|
|
||||||
_d = client.connect(host, port, username, password)
|
|
||||||
|
|
||||||
def on_get_methods(methods):
|
|
||||||
"""
|
|
||||||
Handles receiving the method names
|
|
||||||
"""
|
|
||||||
self._remote_methods = methods
|
|
||||||
methods = list(self._remote_methods)
|
|
||||||
methods.extend(self._local_methods)
|
|
||||||
d.callback(methods)
|
|
||||||
|
|
||||||
def on_client_connected(connection_id):
|
|
||||||
"""
|
|
||||||
Handles the client successfully connecting to the daemon and
|
|
||||||
invokes retrieving the method names.
|
|
||||||
"""
|
|
||||||
d = client.daemon.get_method_list()
|
|
||||||
d.addCallback(on_get_methods)
|
|
||||||
_d.addCallback(on_client_connected)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def _exec_local(self, method, params):
|
|
||||||
"""
|
|
||||||
Handles executing all local methods.
|
|
||||||
"""
|
|
||||||
if method == "system.listMethods":
|
|
||||||
d = Deferred()
|
|
||||||
methods = list(self._remote_methods)
|
|
||||||
methods.extend(self._local_methods)
|
|
||||||
d.callback(methods)
|
|
||||||
return d
|
|
||||||
elif method in self._local_methods:
|
|
||||||
# This will eventually process methods that the server adds
|
|
||||||
# and any plugins.
|
|
||||||
return self._local_methods[method](*params)
|
|
||||||
raise JSONException("Unknown system method")
|
|
||||||
|
|
||||||
def _exec_remote(self, method, params):
|
|
||||||
"""
|
|
||||||
Executes methods using the Deluge client.
|
|
||||||
"""
|
|
||||||
component, method = method.split(".")
|
|
||||||
return getattr(getattr(client, component), method)(*params)
|
|
||||||
|
|
||||||
def _handle_request(self, request):
|
|
||||||
"""
|
|
||||||
Takes some json data as a string and attempts to decode it, and process
|
|
||||||
the rpc object that should be contained, returning a deferred for all
|
|
||||||
procedure calls and the request id.
|
|
||||||
"""
|
|
||||||
request_id = None
|
|
||||||
try:
|
|
||||||
request = json.loads(request)
|
|
||||||
except ValueError:
|
|
||||||
raise JSONException("JSON not decodable")
|
|
||||||
|
|
||||||
if "method" not in request or "id" not in request or \
|
|
||||||
"params" not in request:
|
|
||||||
raise JSONException("Invalid JSON request")
|
|
||||||
|
|
||||||
method, params = request["method"], request["params"]
|
|
||||||
request_id = request["id"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if method.startswith("system."):
|
|
||||||
return self._exec_local(method, params), request_id
|
|
||||||
elif method in self._local_methods:
|
|
||||||
return self._exec_local(method, params), request_id
|
|
||||||
elif method in self._remote_methods:
|
|
||||||
return self._exec_remote(method, params), request_id
|
|
||||||
except Exception, e:
|
|
||||||
log.exception(e)
|
|
||||||
d = Deferred()
|
|
||||||
d.callback(None)
|
|
||||||
return d, request_id
|
|
||||||
|
|
||||||
def _on_rpc_request_finished(self, result, response, request):
|
|
||||||
"""
|
|
||||||
Sends the response of any rpc calls back to the json-rpc client.
|
|
||||||
"""
|
|
||||||
response["result"] = result
|
|
||||||
return self._send_response(request, response)
|
|
||||||
|
|
||||||
def _on_rpc_request_failed(self, reason, response, request):
|
|
||||||
"""
|
|
||||||
Handles any failures that occured while making an rpc call.
|
|
||||||
"""
|
|
||||||
print type(reason)
|
|
||||||
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _on_json_request(self, request):
|
|
||||||
"""
|
|
||||||
Handler to take the json data as a string and pass it on to the
|
|
||||||
_handle_request method for further processing.
|
|
||||||
"""
|
|
||||||
log.debug("json-request: %s", request.json)
|
|
||||||
response = {"result": None, "error": None, "id": None}
|
|
||||||
d, response["id"] = self._handle_request(request.json)
|
|
||||||
d.addCallback(self._on_rpc_request_finished, response, request)
|
|
||||||
d.addErrback(self._on_rpc_request_failed, response, request)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def _on_json_request_failed(self, reason, request):
|
|
||||||
"""
|
|
||||||
Errback handler to return a HTTP code of 500.
|
|
||||||
"""
|
|
||||||
log.exception(reason)
|
|
||||||
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _send_response(self, request, response):
|
|
||||||
response = json.dumps(response)
|
|
||||||
request.setHeader("content-type", "application/x-json")
|
|
||||||
request.write(response)
|
|
||||||
request.finish()
|
|
||||||
|
|
||||||
def render(self, request):
|
|
||||||
"""
|
|
||||||
Handles all the POST requests made to the /json controller.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if request.method != "POST":
|
|
||||||
request.setResponseCode(http.NOT_ALLOWED)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
try:
|
|
||||||
request.content.seek(0)
|
|
||||||
request.json = request.content.read()
|
|
||||||
d = self._on_json_request(request)
|
|
||||||
return server.NOT_DONE_YET
|
|
||||||
except Exception, e:
|
|
||||||
return self._on_json_request_failed(e, request)
|
|
||||||
|
|
||||||
def connect(self, host_id):
|
|
||||||
d = Deferred()
|
|
||||||
def on_connected(methods):
|
|
||||||
d.callback(methods)
|
|
||||||
for host in hostlist["hosts"]:
|
|
||||||
if host_id != host[0]:
|
|
||||||
continue
|
|
||||||
self._connect(*host[1:]).addCallback(on_connected)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def connected(self):
|
|
||||||
d = Deferred()
|
|
||||||
d.callback(client.connected())
|
|
||||||
return d
|
|
||||||
|
|
||||||
def update_ui(self, keys, filter_dict):
|
|
||||||
|
|
||||||
ui_info = {
|
|
||||||
"torrents": None,
|
|
||||||
"filters": None,
|
|
||||||
"stats": None
|
|
||||||
}
|
|
||||||
|
|
||||||
d = Deferred()
|
|
||||||
|
|
||||||
def got_stats(stats):
|
|
||||||
ui_info["stats"] = stats
|
|
||||||
d.callback(ui_info)
|
|
||||||
|
|
||||||
def got_filters(filters):
|
|
||||||
ui_info["filters"] = filters
|
|
||||||
client.core.get_stats().addCallback(got_stats)
|
|
||||||
|
|
||||||
def got_torrents(torrents):
|
|
||||||
ui_info["torrents"] = torrents
|
|
||||||
client.core.get_filter_tree().addCallback(got_filters)
|
|
||||||
client.core.get_torrents_status(filter_dict, keys).addCallback(got_torrents)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def download_torrent_from_url(self, url):
|
|
||||||
"""
|
|
||||||
input:
|
|
||||||
url: the url of the torrent to download
|
|
||||||
|
|
||||||
returns:
|
|
||||||
filename: the temporary file name of the torrent file
|
|
||||||
"""
|
|
||||||
tmp_file = os.path.join(tempfile.gettempdir(), url.split("/")[-1])
|
|
||||||
filename, headers = urllib.urlretrieve(url, tmp_file)
|
|
||||||
log.debug("filename: %s", filename)
|
|
||||||
d = Deferred()
|
|
||||||
d.callback(filename)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def get_torrent_info(self, filename):
|
|
||||||
"""
|
|
||||||
Goal:
|
|
||||||
allow the webui to retrieve data about the torrent
|
|
||||||
|
|
||||||
input:
|
|
||||||
filename: the filename of the torrent to gather info about
|
|
||||||
|
|
||||||
returns:
|
|
||||||
{
|
|
||||||
"filename": the torrent file
|
|
||||||
"name": the torrent name
|
|
||||||
"size": the total size of the torrent
|
|
||||||
"files": the files the torrent contains
|
|
||||||
"info_hash" the torrents info_hash
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
d = Deferred()
|
|
||||||
d.callback(uicommon.get_torrent_info(filename.strip()))
|
|
||||||
return d
|
|
||||||
|
|
||||||
def add_torrents(self, torrents):
|
|
||||||
"""
|
|
||||||
input:
|
|
||||||
torrents [{
|
|
||||||
path: the path of the torrent file,
|
|
||||||
options: the torrent options
|
|
||||||
}]
|
|
||||||
"""
|
|
||||||
for torrent in torrents:
|
|
||||||
filename = os.path.basename(torrent["path"])
|
|
||||||
fdump = open(torrent["path"], "r").read()
|
|
||||||
client.add_torrent_file(filename, fdump, torrent["options"])
|
|
||||||
d = Deferred()
|
|
||||||
d.callback(True)
|
|
||||||
return d
|
|
||||||
|
|
||||||
def login(self, password):
|
|
||||||
"""Method to allow the webui to authenticate
|
|
||||||
"""
|
|
||||||
config = component.get("DelugeWeb").config
|
|
||||||
m = hashlib.md5()
|
|
||||||
m.update(config['pwd_salt'])
|
|
||||||
m.update(password)
|
|
||||||
d = Deferred()
|
|
||||||
d.callback(m.digest() == config['pwd_md5'])
|
|
||||||
return d
|
|
||||||
|
|
||||||
def get_hosts(self):
|
|
||||||
"""Return the hosts in the hostlist"""
|
|
||||||
hosts = dict((host[0], host[:]) for host in hostlist["hosts"])
|
|
||||||
|
|
||||||
main_deferred = Deferred()
|
|
||||||
def run_check():
|
|
||||||
if all(map(lambda x: x[3] is not None, hosts.values())):
|
|
||||||
main_deferred.callback(hosts.values())
|
|
||||||
|
|
||||||
def on_connect(result, c, host_id):
|
|
||||||
def on_info(info, c):
|
|
||||||
hosts[host_id][3] = _("Online")
|
|
||||||
hosts[host_id][4] = info
|
|
||||||
c.disconnect()
|
|
||||||
run_check()
|
|
||||||
|
|
||||||
def on_info_fail(reason):
|
|
||||||
hosts[host_id][3] = _("Offline")
|
|
||||||
run_check()
|
|
||||||
|
|
||||||
d = c.daemon.info()
|
|
||||||
d.addCallback(on_info, c)
|
|
||||||
d.addErrback(on_info_fail, c)
|
|
||||||
|
|
||||||
def on_connect_failed(reason, host_id):
|
|
||||||
log.exception(reason)
|
|
||||||
hosts[host_id][3] = _("Offline")
|
|
||||||
run_check()
|
|
||||||
|
|
||||||
for host in hosts.values():
|
|
||||||
host_id, host, port, user, password = host[0:5]
|
|
||||||
hosts[host_id][3:4] = (None, None)
|
|
||||||
|
|
||||||
if client.connected() and (host, port, user) == client.connection_info():
|
|
||||||
def on_info(info):
|
|
||||||
hosts[host_id][4] = info
|
|
||||||
run_check()
|
|
||||||
host[5] = _("Connected")
|
|
||||||
client.daemon.info().addCallback(on_info)
|
|
||||||
hosts[host_id] = host
|
|
||||||
continue
|
|
||||||
|
|
||||||
c = Client()
|
|
||||||
d = c.connect(host, port, user, password)
|
|
||||||
d.addCallback(on_connect, c, host_id)
|
|
||||||
d.addErrback(on_connect_failed, host_id)
|
|
||||||
return main_deferred
|
|
||||||
|
|
||||||
class GetText(resource.Resource):
|
class GetText(resource.Resource):
|
||||||
def render(self, request):
|
def render(self, request):
|
||||||
request.setHeader("content-type", "text/javascript")
|
request.setHeader("content-type", "text/javascript")
|
||||||
|
@ -570,6 +225,7 @@ class DelugeWeb(component.Component):
|
||||||
self.site = server.Site(TopLevel())
|
self.site = server.Site(TopLevel())
|
||||||
self.config = ConfigManager("web.conf", CONFIG_DEFAULTS)
|
self.config = ConfigManager("web.conf", CONFIG_DEFAULTS)
|
||||||
self.port = self.config["port"]
|
self.port = self.config["port"]
|
||||||
|
self.web_api = WebApi()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.shutdown)
|
signal.signal(signal.SIGINT, self.shutdown)
|
||||||
signal.signal(signal.SIGTERM, self.shutdown)
|
signal.signal(signal.SIGTERM, self.shutdown)
|
||||||
|
|
Loading…
Reference in New Issue