source: deluge/ui/web/json_api.py@ e837493

2.0.x develop extjs4-port
Last change on this file since e837493 was e837493, checked in by Damien Churchill <damoc@gmail.com>, 16 years ago

add basic session support

  • Property mode set to 100644
File size: 19.5 KB
Line 
1#
2# deluge/ui/web/json_api.py
3#
4# Copyright (C) 2009 Damien Churchill <damoxc@gmail.com>
5#
6# Deluge is free software.
7#
8# You may redistribute it and/or modify it under the terms of the
9# GNU General Public License, as published by the Free Software
10# Foundation; either version 3 of the License, or (at your option)
11# any later version.
12#
13# deluge is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16# See the GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with deluge. If not, write to:
20# The Free Software Foundation, Inc.,
21# 51 Franklin Street, Fifth Floor
22# Boston, MA 02110-1301, USA.
23#
24
25import os
26import time
27import base64
28import random
29import urllib
30import hashlib
31import logging
32import tempfile
33
34from types import FunctionType
35from twisted.internet.defer import Deferred
36from twisted.web import http, resource, server
37
38from deluge import common, component
39from deluge.configmanager import ConfigManager
40from deluge.ui import common as uicommon
41from deluge.ui.client import client, Client
42from deluge.ui.web.auth import *
43from deluge.ui.web.common import _
44json = common.json
45
46log = logging.getLogger(__name__)
47
48def export(auth_level=AUTH_LEVEL_DEFAULT):
49 """
50 Decorator function to register an object's method as an RPC. The object
51 will need to be registered with an `:class:RPCServer` to be effective.
52
53 :param func: function, the function to export
54 :param auth_level: int, the auth level required to call this method
55
56 """
57 def wrap(func, *args, **kwargs):
58 func._json_export = True
59 func._json_auth_level = auth_level
60 return func
61
62 if type(auth_level) is FunctionType:
63 func = auth_level
64 auth_level = AUTH_LEVEL_DEFAULT
65 return wrap(func)
66 else:
67 return wrap
68
69class JSONException(Exception):
70 def __init__(self, inner_exception):
71 self.inner_exception = inner_exception
72 Exception.__init__(self, str(inner_exception))
73
74class JSON(resource.Resource, component.Component):
75 """
76 A Twisted Web resource that exposes a JSON-RPC interface for web clients
77 to use.
78 """
79
80 def __init__(self):
81 resource.Resource.__init__(self)
82 component.Component.__init__(self, "JSON")
83 self._remote_methods = []
84 self._local_methods = {}
85 client.disconnect_callback = self._on_client_disconnect
86
87 def connect(self, host="localhost", port=58846, username="", password=""):
88 """
89 Connects the client to a daemon
90 """
91 d = Deferred()
92 _d = client.connect(host, port, username, password)
93
94 def on_get_methods(methods):
95 """
96 Handles receiving the method names
97 """
98 self._remote_methods = methods
99 methods = list(self._remote_methods)
100 methods.extend(self._local_methods)
101 d.callback(methods)
102
103 def on_client_connected(connection_id):
104 """
105 Handles the client successfully connecting to the daemon and
106 invokes retrieving the method names.
107 """
108 d = client.daemon.get_method_list()
109 d.addCallback(on_get_methods)
110 component.get("PluginManager").start()
111 _d.addCallback(on_client_connected)
112 return d
113
114 def _on_client_disconnect(self, *args):
115 component.get("PluginManager").stop()
116
117 def _exec_local(self, method, params):
118 """
119 Handles executing all local methods.
120 """
121 if method == "system.listMethods":
122 d = Deferred()
123 methods = list(self._remote_methods)
124 methods.extend(self._local_methods)
125 d.callback(methods)
126 return d
127 elif method in self._local_methods:
128 # This will eventually process methods that the server adds
129 # and any plugins.
130 return self._local_methods[method](*params)
131 raise JSONException("Unknown system method")
132
133 def _exec_remote(self, method, params):
134 """
135 Executes methods using the Deluge client.
136 """
137 component, method = method.split(".")
138 return getattr(getattr(client, component), method)(*params)
139
140 def _handle_request(self, request):
141 """
142 Takes some json data as a string and attempts to decode it, and process
143 the rpc object that should be contained, returning a deferred for all
144 procedure calls and the request id.
145 """
146 request_id = None
147 try:
148 request = json.loads(request)
149 except ValueError:
150 raise JSONException("JSON not decodable")
151
152 if "method" not in request or "id" not in request or \
153 "params" not in request:
154 raise JSONException("Invalid JSON request")
155
156 method, params = request["method"], request["params"]
157 request_id = request["id"]
158
159 try:
160 if method.startswith("system."):
161 return self._exec_local(method, params), request_id
162 elif method in self._local_methods:
163 return self._exec_local(method, params), request_id
164 elif method in self._remote_methods:
165 return self._exec_remote(method, params), request_id
166 except Exception, e:
167 log.exception(e)
168 d = Deferred()
169 d.callback(None)
170 return d, request_id
171
172 def _on_rpc_request_finished(self, result, response, request):
173 """
174 Sends the response of any rpc calls back to the json-rpc client.
175 """
176 response["result"] = result
177 return self._send_response(request, response)
178
179 def _on_rpc_request_failed(self, reason, response, request):
180 """
181 Handles any failures that occured while making an rpc call.
182 """
183 print type(reason)
184 request.setResponseCode(http.INTERNAL_SERVER_ERROR)
185 return ""
186
187 def _on_json_request(self, request):
188 """
189 Handler to take the json data as a string and pass it on to the
190 _handle_request method for further processing.
191 """
192 log.debug("json-request: %s", request.json)
193 response = {"result": None, "error": None, "id": None}
194 d, response["id"] = self._handle_request(request.json)
195 d.addCallback(self._on_rpc_request_finished, response, request)
196 d.addErrback(self._on_rpc_request_failed, response, request)
197 return d
198
199 def _on_json_request_failed(self, reason, request):
200 """
201 Errback handler to return a HTTP code of 500.
202 """
203 log.exception(reason)
204 request.setResponseCode(http.INTERNAL_SERVER_ERROR)
205 return ""
206
207 def _send_response(self, request, response):
208 response = json.dumps(response)
209 request.setHeader("content-type", "application/x-json")
210 request.write(response)
211 request.finish()
212
213 def render(self, request):
214 """
215 Handles all the POST requests made to the /json controller.
216 """
217
218 if request.method != "POST":
219 request.setResponseCode(http.NOT_ALLOWED)
220 return ""
221
222 try:
223 request.content.seek(0)
224 request.json = request.content.read()
225 d = self._on_json_request(request)
226 return server.NOT_DONE_YET
227 except Exception, e:
228 return self._on_json_request_failed(e, request)
229
230 def register_object(self, obj, name=None):
231 """
232 Registers an object to export it's rpc methods. These methods should
233 be exported with the export decorator prior to registering the object.
234
235 :param obj: object, the object that we want to export
236 :param name: str, the name to use, if None, it will be the class name of the object
237 """
238 name = name or obj.__class__.__name__
239 name = name.lower()
240
241 for d in dir(obj):
242 if d[0] == "_":
243 continue
244 if getattr(getattr(obj, d), '_json_export', False):
245 log.debug("Registering method: %s", name + "." + d)
246 self._local_methods[name + "." + d] = getattr(obj, d)
247
248class JSONComponent(component.Component):
249 def __init__(self, name, interval=1, depend=None):
250 super(JSONComponent, self).__init__(name, interval, depend)
251 self._json = component.get("JSON")
252 self._json.register_object(self, name)
253
254
255DEFAULT_HOST = "127.0.0.1"
256DEFAULT_PORT = 58846
257
258DEFAULT_HOSTS = {
259 "hosts": [(hashlib.sha1(str(time.time())).hexdigest(),
260 DEFAULT_HOST, DEFAULT_PORT, "", "")]
261}
262HOSTLIST_ID = 0
263HOSTLIST_NAME = 1
264HOSTLIST_PORT = 2
265HOSTLIST_USER = 3
266HOSTLIST_PASS = 4
267
268HOSTS_ID = HOSTLIST_ID
269HOSTS_NAME = HOSTLIST_NAME
270HOSTS_PORT = HOSTLIST_PORT
271HOSTS_STATUS = 3
272HOSTS_INFO = 4
273
274FILES_KEYS = ["files", "file_progress", "file_priorities"]
275
276class WebApi(JSONComponent):
277 def __init__(self):
278 super(WebApi, self).__init__("Web")
279 self.host_list = ConfigManager("hostlist.conf.1.2", DEFAULT_HOSTS)
280
281 def get_host(self, connection_id):
282 for host in self.host_list["hosts"]:
283 if host[0] == connection_id:
284 return host
285
286 @export
287 def connect(self, host_id):
288 d = Deferred()
289 def on_connected(methods):
290 d.callback(methods)
291 for host in self.host_list["hosts"]:
292 if host_id != host[0]:
293 continue
294 self._json.connect(*host[1:]).addCallback(on_connected)
295 return d
296
297 @export
298 def connected(self):
299 d = Deferred()
300 d.callback(client.connected())
301 return d
302
303 @export
304 def disconnect(self):
305 d = Deferred()
306 client.disconnect()
307 d.callback(True)
308 return d
309
310 @export
311 def update_ui(self, keys, filter_dict):
312
313 ui_info = {
314 "torrents": None,
315 "filters": None,
316 "stats": None
317 }
318
319 d = Deferred()
320
321 log.info("Updating ui with keys '%r' and filters '%r'", keys,
322 filter_dict)
323
324 def got_stats(stats):
325 ui_info["stats"] = stats
326 d.callback(ui_info)
327
328 def got_filters(filters):
329 ui_info["filters"] = filters
330 client.core.get_stats().addCallback(got_stats)
331
332 def got_torrents(torrents):
333 ui_info["torrents"] = torrents
334 client.core.get_filter_tree().addCallback(got_filters)
335 client.core.get_torrents_status(filter_dict, keys).addCallback(got_torrents)
336 return d
337
338 def _on_got_files(self, torrent, d):
339 files = torrent.get("files")
340 file_progress = torrent.get("file_progress")
341 file_priorities = torrent.get("file_priorities")
342
343 paths = []
344 info = {}
345 for index, torrent_file in enumerate(files):
346 path = torrent_file["path"]
347 paths.append(path)
348 torrent_file["progress"] = file_progress[index]
349 torrent_file["priority"] = file_priorities[index]
350 torrent_file["index"] = index
351 info[path] = torrent_file
352
353 def walk(path, item):
354 if type(item) is dict:
355 return item
356 return [info[path]["index"], info[path]["size"],
357 info[path]["progress"], info[path]["priority"]]
358
359 file_tree = uicommon.FileTree(paths)
360 file_tree.walk(walk)
361 d.callback(file_tree.get_tree())
362
363 @export
364 def get_torrent_files(self, torrent_id):
365 main_deferred = Deferred()
366 d = client.core.get_torrent_status(torrent_id, FILES_KEYS)
367 d.addCallback(self._on_got_files, main_deferred)
368 return main_deferred
369
370 @export
371 def download_torrent_from_url(self, url):
372 """
373 input:
374 url: the url of the torrent to download
375
376 returns:
377 filename: the temporary file name of the torrent file
378 """
379 tmp_file = os.path.join(tempfile.gettempdir(), url.split("/")[-1])
380 filename, headers = urllib.urlretrieve(url, tmp_file)
381 log.debug("filename: %s", filename)
382 d = Deferred()
383 d.callback(filename)
384 return d
385
386 @export
387 def get_torrent_info(self, filename):
388 """
389 Goal:
390 allow the webui to retrieve data about the torrent
391
392 input:
393 filename: the filename of the torrent to gather info about
394
395 returns:
396 {
397 "filename": the torrent file
398 "name": the torrent name
399 "size": the total size of the torrent
400 "files": the files the torrent contains
401 "info_hash" the torrents info_hash
402 }
403 """
404 d = Deferred()
405 try:
406 torrent_info = uicommon.TorrentInfo(filename.strip())
407 d.callback(torrent_info.as_dict("name", "info_hash", "files_tree"))
408 except:
409 d.callback(False)
410 return d
411
412 @export
413 def add_torrents(self, torrents):
414 """
415 input:
416 torrents [{
417 path: the path of the torrent file,
418 options: the torrent options
419 }]
420 """
421 for torrent in torrents:
422 filename = os.path.basename(torrent["path"])
423 fdump = base64.encodestring(open(torrent["path"], "r").read())
424 log.info("Adding torrent from file `%s` with options `%r`",
425 filename, torrent["options"])
426 client.core.add_torrent_file(filename, fdump, torrent["options"])
427 d = Deferred()
428 d.callback(True)
429 return d
430
431 def _create_session(self, login='admin'):
432 m = hashlib.md5()
433 m.update(login)
434 m.update(str(time.time()))
435 m.update(str(random.getrandbits(999)))
436 m.update(m.hexdigest())
437 session_id = m.hexdigest()
438
439 config = component.get("DelugeWeb").config
440 config["sessions"][session_id] = {
441 "login": login
442 }
443 return session_id
444
445 @export
446 def check_session(self, session_id):
447 d = Deferred()
448 config = component.get("DelugeWeb").config
449 d.callback(session_id in config["sessions"])
450 return d
451
452 @export
453 def delete_session(self, session_id):
454 d = Deferred()
455 config = component.get("DelugeWeb").config
456 del config["sessions"][session_id]
457 d.callback(True)
458 return d
459
460 @export
461 def login(self, password):
462 """Method to allow the webui to authenticate
463 """
464 config = component.get("DelugeWeb").config
465 m = hashlib.md5()
466 m.update(config['pwd_salt'])
467 m.update(password)
468 d = Deferred()
469 if m.hexdigest() == config['pwd_md5']:
470 # Change this to return a session id
471 d.callback(self._create_session())
472 else:
473 d.callback(False)
474 return d
475
476 @export
477 def get_hosts(self):
478 """Return the hosts in the hostlist"""
479 hosts = dict((host[HOSTLIST_ID], list(host[:])) for \
480 host in self.host_list["hosts"])
481
482 main_deferred = Deferred()
483 def run_check():
484 if all(map(lambda x: x[HOSTS_STATUS] is not None, hosts.values())):
485 main_deferred.callback(hosts.values())
486
487 def on_connect(connected, c, host_id):
488 def on_info(info, c):
489 hosts[host_id][HOSTS_STATUS] = _("Online")
490 hosts[host_id][HOSTS_INFO] = info
491 c.disconnect()
492 run_check()
493
494 def on_info_fail(reason):
495 hosts[host_id][HOSTS_STATUS] = _("Offline")
496 run_check()
497
498 if not connected:
499 hosts[host_id][HOSTS_STATUS] = _("Offline")
500 run_check()
501 return
502
503 d = c.daemon.info()
504 d.addCallback(on_info, c)
505 d.addErrback(on_info_fail)
506
507 def on_connect_failed(reason, host_id):
508 log.exception(reason)
509 hosts[host_id][HOSTS_STATUS] = _("Offline")
510 run_check()
511
512 for host in hosts.values():
513 host_id, host, port, user, password = host
514 hosts[host_id][HOSTS_STATUS:HOSTS_INFO] = (None, None)
515
516 if client.connected() and (host, port, "localclient" if not \
517 user and host in ("127.0.0.1", "localhost") else \
518 user) == client.connection_info():
519 def on_info(info):
520 hosts[host_id][HOSTS_INFO] = info
521 run_check()
522 hosts[host_id][HOSTS_STATUS] = _("Connected")
523 client.daemon.info().addCallback(on_info)
524 continue
525
526 c = Client()
527 d = c.connect(host, port, user, password)
528 d.addCallback(on_connect, c, host_id)
529 d.addErrback(on_connect_failed, host_id)
530 return main_deferred
531
532 @export
533 def stop_daemon(self, connection_id):
534 """
535 Stops a running daemon.
536
537 :param connection_id: str, the hash id of the connection
538
539 """
540 main_deferred = Deferred()
541 host = self.get_host(connection_id)
542 if not host:
543 main_deferred.callback((False, _("Daemon doesn't exist")))
544 return main_deferred
545
546 try:
547 def on_connect(connected, c):
548 if not connected:
549 main_deferred.callback((False, _("Daemon not running")))
550 return
551 c.daemon.shutdown()
552 main_deferred.callback((True, ))
553
554 def on_connect_failed(reason):
555 main_deferred.callback((False, reason))
556
557 host, port, user, password = host[1:5]
558 c = Client()
559 d = c.connect(host, port, user, password)
560 d.addCallback(on_connect, c)
561 d.addErrback(on_connect_failed)
562 except:
563 main_deferred.callback((False, "An error occured"))
564 return main_deferred
565
566 @export
567 def add_host(self, host, port, username="", password=""):
568 """
569 Adds a host to the list.
570
571 :param host: str, the hostname
572 :param port: int, the port
573 :param username: str, the username to login as
574 :param password: str, the password to login with
575
576 """
577 d = Deferred()
578 # Check to see if there is already an entry for this host and return
579 # if thats the case
580 for entry in self.host_list["hosts"]:
581 if (entry[0], entry[1], entry[2]) == (host, port, username):
582 d.callback((False, "Host already in the list"))
583
584 try:
585 port = int(port)
586 except:
587 d.callback((False, "Port is invalid"))
588 return d
589
590 # Host isn't in the list, so lets add it
591 connection_id = hashlib.sha1(str(time.time())).hexdigest()
592 self.host_list["hosts"].append([connection_id, host, port, username,
593 password])
594 self.host_list.save()
595 d.callback((True,))
596 return d
597
598 @export
599 def remove_host(self, connection_id):
600 """
601 Removes a host for the list
602
603 :param connection_Id: str, the hash id of the connection
604
605 """
606 d = Deferred()
607 host = self.get_host(connection_id)
608 if host is None:
609 d.callback(False)
610
611 self.host_list["hosts"].remove(host)
612 self.host_list.save()
613 d.callback(True)
614 return d
Note: See TracBrowser for help on using the repository browser.