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:
Damien Churchill 2009-03-12 18:57:35 +00:00
parent 344446a626
commit 0ac64da5c6
4 changed files with 488 additions and 349 deletions

30
deluge/ui/web/auth.py Normal file
View File

@ -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

40
deluge/ui/web/common.py Normal file
View File

@ -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)

413
deluge/ui/web/json_api.py Normal file
View File

@ -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

View File

@ -36,19 +36,16 @@ import pkg_resources
from twisted.application import service, internet
from twisted.internet import reactor
from twisted.internet.defer import Deferred
from twisted.web import http, resource, server, static
from mako.template import Template as MakoTemplate
from deluge import common, component
from deluge.configmanager import ConfigManager
from deluge.log import setupLogger, LOG as _log
from deluge.ui import common as uicommon
from deluge.ui.client import client, Client
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__)
json = common.json
# Initialize gettext
try:
@ -70,8 +67,8 @@ current_dir = os.path.dirname(__file__)
CONFIG_DEFAULTS = {
"port": 8112,
"template": "slate",
"pwd_salt": u"2\xe8\xc7\xa6(n\x81_\x8f\xfc\xdf\x8b\xd1\x1e\xd5\x90",
"pwd_md5": u".\xe8w\\+\xec\xdb\xf2id4F\xdb\rUc",
"pwd_salt": "16f65d5c79b7e93278a28b60fed2431e",
"pwd_md5": "2c9baa929ca38fb5c9eb5b054474d1ce",
"base": "",
"sessions": [],
"sidebar_show_zero": False,
@ -81,354 +78,12 @@ CONFIG_DEFAULTS = {
"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):
"""Convert a relative path into an absolute path relative to the location
of this script.
"""
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):
def render(self, request):
request.setHeader("content-type", "text/javascript")
@ -570,6 +225,7 @@ class DelugeWeb(component.Component):
self.site = server.Site(TopLevel())
self.config = ConfigManager("web.conf", CONFIG_DEFAULTS)
self.port = self.config["port"]
self.web_api = WebApi()
signal.signal(signal.SIGINT, self.shutdown)
signal.signal(signal.SIGTERM, self.shutdown)