summaryrefslogtreecommitdiffstats
path: root/pyload/remote
diff options
context:
space:
mode:
Diffstat (limited to 'pyload/remote')
-rw-r--r--pyload/remote/ClickAndLoadBackend.py170
-rw-r--r--pyload/remote/JSONClient.py56
-rw-r--r--pyload/remote/RemoteManager.py89
-rw-r--r--pyload/remote/WSClient.py59
-rw-r--r--pyload/remote/WebSocketBackend.py49
-rw-r--r--pyload/remote/__init__.py0
-rw-r--r--pyload/remote/apitypes.py536
-rw-r--r--pyload/remote/apitypes_debug.py135
-rw-r--r--pyload/remote/create_apitypes.py180
-rw-r--r--pyload/remote/create_jstypes.py36
-rw-r--r--pyload/remote/json_converter.py64
-rw-r--r--pyload/remote/pyload.thrift538
-rw-r--r--pyload/remote/ttypes.py534
-rw-r--r--pyload/remote/wsbackend/AbstractHandler.py133
-rw-r--r--pyload/remote/wsbackend/ApiHandler.py81
-rw-r--r--pyload/remote/wsbackend/AsyncHandler.py167
-rw-r--r--pyload/remote/wsbackend/Dispatcher.py31
-rw-r--r--pyload/remote/wsbackend/Server.py733
-rw-r--r--pyload/remote/wsbackend/__init__.py0
19 files changed, 3591 insertions, 0 deletions
diff --git a/pyload/remote/ClickAndLoadBackend.py b/pyload/remote/ClickAndLoadBackend.py
new file mode 100644
index 000000000..ad8031587
--- /dev/null
+++ b/pyload/remote/ClickAndLoadBackend.py
@@ -0,0 +1,170 @@
+# -*- coding: utf-8 -*-
+"""
+ This program is free software; you can 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.
+
+ This program 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 this program; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: RaNaN
+"""
+import re
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+from cgi import FieldStorage
+from urllib import unquote
+from base64 import standard_b64decode
+from binascii import unhexlify
+
+try:
+ from Crypto.Cipher import AES
+except:
+ pass
+
+from RemoteManager import BackendBase
+
+core = None
+js = None
+
+class ClickAndLoadBackend(BackendBase):
+ def setup(self, host, port):
+ self.httpd = HTTPServer((host, port), CNLHandler)
+ global core, js
+ core = self.m.core
+ js = core.js
+
+ def serve(self):
+ while self.enabled:
+ self.httpd.handle_request()
+
+class CNLHandler(BaseHTTPRequestHandler):
+
+ def add_package(self, name, urls, queue=0):
+ print "name", name
+ print "urls", urls
+ print "queue", queue
+
+ def get_post(self, name, default=""):
+ if name in self.post:
+ return self.post[name]
+ else:
+ return default
+
+ def start_response(self, string):
+
+ self.send_response(200)
+
+ self.send_header("Content-Length", len(string))
+ self.send_header("Content-Language", "de")
+ self.send_header("Vary", "Accept-Language, Cookie")
+ self.send_header("Cache-Control", "no-cache, must-revalidate")
+ self.send_header("Content-type", "text/html")
+ self.end_headers()
+
+ def do_GET(self):
+ path = self.path.strip("/").lower()
+ #self.wfile.write(path+"\n")
+
+ self.map = [ (r"add$", self.add),
+ (r"addcrypted$", self.addcrypted),
+ (r"addcrypted2$", self.addcrypted2),
+ (r"flashgot", self.flashgot),
+ (r"crossdomain\.xml", self.crossdomain),
+ (r"checkSupportForUrl", self.checksupport),
+ (r"jdcheck.js", self.jdcheck),
+ (r"", self.flash) ]
+
+ func = None
+ for r, f in self.map:
+ if re.match(r"(flash(got)?/?)?"+r, path):
+ func = f
+ break
+
+ if func:
+ try:
+ resp = func()
+ if not resp: resp = "success"
+ resp += "\r\n"
+ self.start_response(resp)
+ self.wfile.write(resp)
+ except Exception,e :
+ self.send_error(500, str(e))
+ else:
+ self.send_error(404, "Not Found")
+
+ def do_POST(self):
+ form = FieldStorage(
+ fp=self.rfile,
+ headers=self.headers,
+ environ={'REQUEST_METHOD':'POST',
+ 'CONTENT_TYPE':self.headers['Content-Type'],
+ })
+
+ self.post = {}
+ for name in form.keys():
+ self.post[name] = form[name].value
+
+ return self.do_GET()
+
+ def flash(self):
+ return "JDownloader"
+
+ def add(self):
+ package = self.get_post('referer', 'ClickAndLoad Package')
+ urls = filter(lambda x: x != "", self.get_post('urls').split("\n"))
+
+ self.add_package(package, urls, 0)
+
+ def addcrypted(self):
+ package = self.get_post('referer', 'ClickAndLoad Package')
+ dlc = self.get_post('crypted').replace(" ", "+")
+
+ core.upload_container(package, dlc)
+
+ def addcrypted2(self):
+ package = self.get_post("source", "ClickAndLoad Package")
+ crypted = self.get_post("crypted")
+ jk = self.get_post("jk")
+
+ crypted = standard_b64decode(unquote(crypted.replace(" ", "+")))
+ jk = "%s f()" % jk
+ jk = js.eval(jk)
+ Key = unhexlify(jk)
+ IV = Key
+
+ obj = AES.new(Key, AES.MODE_CBC, IV)
+ result = obj.decrypt(crypted).replace("\x00", "").replace("\r","").split("\n")
+
+ result = filter(lambda x: x != "", result)
+
+ self.add_package(package, result, 0)
+
+
+ def flashgot(self):
+ autostart = int(self.get_post('autostart', 0))
+ package = self.get_post('package', "FlashGot")
+ urls = filter(lambda x: x != "", self.get_post('urls').split("\n"))
+
+ self.add_package(package, urls, autostart)
+
+ def crossdomain(self):
+ rep = "<?xml version=\"1.0\"?>\n"
+ rep += "<!DOCTYPE cross-domain-policy SYSTEM \"http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd\">\n"
+ rep += "<cross-domain-policy>\n"
+ rep += "<allow-access-from domain=\"*\" />\n"
+ rep += "</cross-domain-policy>"
+ return rep
+
+ def checksupport(self):
+ pass
+
+ def jdcheck(self):
+ rep = "jdownloader=true;\n"
+ rep += "var version='10629';\n"
+ return rep
diff --git a/pyload/remote/JSONClient.py b/pyload/remote/JSONClient.py
new file mode 100644
index 000000000..a2c07a132
--- /dev/null
+++ b/pyload/remote/JSONClient.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from urllib import urlopen, urlencode
+from httplib import UNAUTHORIZED, FORBIDDEN
+
+from json_converter import loads, dumps
+from apitypes import Unauthorized, Forbidden
+
+class JSONClient:
+ URL = "http://localhost:8001/api"
+
+ def __init__(self, url=None):
+ self.url = url or self.URL
+ self.session = None
+
+ def request(self, path, data):
+ ret = urlopen(self.url + path, urlencode(data))
+ if ret.code == 400:
+ raise loads(ret.read())
+ if ret.code == 404:
+ raise AttributeError("Unknown Method")
+ if ret.code == 500:
+ raise Exception("Remote Exception")
+ if ret.code == UNAUTHORIZED:
+ raise Unauthorized()
+ if ret.code == FORBIDDEN:
+ raise Forbidden()
+ return ret.read()
+
+ def login(self, username, password):
+ self.session = loads(self.request("/login", {'username': username, 'password': password}))
+ return self.session
+
+ def logout(self):
+ self.call("logout")
+ self.session = None
+
+ def call(self, func, *args, **kwargs):
+ # Add the current session
+ kwargs["session"] = self.session
+ path = "/" + func + "/" + "/".join(dumps(x) for x in args)
+ data = dict((k, dumps(v)) for k, v in kwargs.iteritems())
+ rep = self.request(path, data)
+ return loads(rep)
+
+ def __getattr__(self, item):
+ def call(*args, **kwargs):
+ return self.call(item, *args, **kwargs)
+
+ return call
+
+if __name__ == "__main__":
+ api = JSONClient()
+ api.login("User", "test")
+ print api.getServerVersion() \ No newline at end of file
diff --git a/pyload/remote/RemoteManager.py b/pyload/remote/RemoteManager.py
new file mode 100644
index 000000000..7aeeb8a7a
--- /dev/null
+++ b/pyload/remote/RemoteManager.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+ This program is free software; you can 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.
+
+ This program 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 this program; if not, see <http://www.gnu.org/licenses/>.
+
+ @author: mkaay
+"""
+
+from threading import Thread
+from traceback import print_exc
+
+class BackendBase(Thread):
+ def __init__(self, manager):
+ Thread.__init__(self)
+ self.m = manager
+ self.core = manager.core
+ self.enabled = True
+ self.running = False
+
+ def run(self):
+ self.running = True
+ try:
+ self.serve()
+ except Exception, e:
+ self.core.log.error(_("Remote backend error: %s") % e)
+ if self.core.debug:
+ print_exc()
+ finally:
+ self.running = False
+
+ def setup(self, host, port):
+ pass
+
+ def checkDeps(self):
+ return True
+
+ def serve(self):
+ pass
+
+ def shutdown(self):
+ pass
+
+ def stop(self):
+ self.enabled = False# set flag and call shutdowm message, so thread can react
+ self.shutdown()
+
+
+class RemoteManager():
+ available = []
+
+ def __init__(self, core):
+ self.core = core
+ self.backends = []
+
+ if self.core.remote:
+ self.available.append("WebSocketBackend")
+
+
+ def startBackends(self):
+ host = self.core.config["remote"]["listenaddr"]
+ port = self.core.config["remote"]["port"]
+
+ for b in self.available:
+ klass = getattr(__import__("pyload.remote.%s" % b, globals(), locals(), [b], -1), b)
+ backend = klass(self)
+ if not backend.checkDeps():
+ continue
+ try:
+ backend.setup(host, port)
+ self.core.log.info(_("Starting %(name)s: %(addr)s:%(port)s") % {"name": b, "addr": host, "port": port})
+ except Exception, e:
+ self.core.log.error(_("Failed loading backend %(name)s | %(error)s") % {"name": b, "error": str(e)})
+ if self.core.debug:
+ print_exc()
+ else:
+ backend.start()
+ self.backends.append(backend)
+
+ port += 1
diff --git a/pyload/remote/WSClient.py b/pyload/remote/WSClient.py
new file mode 100644
index 000000000..793a6ef28
--- /dev/null
+++ b/pyload/remote/WSClient.py
@@ -0,0 +1,59 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from websocket import create_connection
+from httplib import UNAUTHORIZED, FORBIDDEN
+
+from json_converter import loads, dumps
+from apitypes import Unauthorized, Forbidden
+
+class WSClient:
+ URL = "ws://localhost:7227/api"
+
+ def __init__(self, url=None):
+ self.url = url or self.URL
+ self.ws = None
+
+ def connect(self):
+ self.ws = create_connection(self.URL)
+
+ def close(self):
+ self.ws.close()
+
+ def login(self, username, password):
+ if not self.ws: self.connect()
+ return self.call("login", username, password)
+
+ def call(self, func, *args, **kwargs):
+ if not self.ws:
+ raise Exception("Not Connected")
+
+ if kwargs:
+ self.ws.send(dumps([func, args, kwargs]))
+ else: # omit kwargs
+ self.ws.send(dumps([func, args]))
+
+ code, result = loads(self.ws.recv())
+ if code == 400:
+ raise result
+ if code == 404:
+ raise AttributeError("Unknown Method")
+ elif code == 500:
+ raise Exception("Remote Exception: %s" % result)
+ elif code == UNAUTHORIZED:
+ raise Unauthorized()
+ elif code == FORBIDDEN:
+ raise Forbidden()
+
+ return result
+
+ def __getattr__(self, item):
+ def call(*args, **kwargs):
+ return self.call(item, *args, **kwargs)
+
+ return call
+
+if __name__ == "__main__":
+ api = WSClient()
+ api.login("User", "test")
+ print api.getServerVersion() \ No newline at end of file
diff --git a/pyload/remote/WebSocketBackend.py b/pyload/remote/WebSocketBackend.py
new file mode 100644
index 000000000..d29470067
--- /dev/null
+++ b/pyload/remote/WebSocketBackend.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+###############################################################################
+# Copyright(c) 2008-2012 pyLoad Team
+# http://www.pyload.org
+#
+# This file is part of pyLoad.
+# pyLoad is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Subjected to the terms and conditions in LICENSE
+#
+# @author: RaNaN
+###############################################################################
+
+import logging
+
+from RemoteManager import BackendBase
+
+from mod_pywebsocket import util
+def get_class_logger(o=None):
+ return logging.getLogger('log')
+
+# Monkey patch for our logger
+util.get_class_logger = get_class_logger
+
+class WebSocketBackend(BackendBase):
+ def setup(self, host, port):
+
+ from wsbackend.Server import WebSocketServer, DefaultOptions
+ from wsbackend.Dispatcher import Dispatcher
+ from wsbackend.ApiHandler import ApiHandler
+ from wsbackend.AsyncHandler import AsyncHandler
+
+ options = DefaultOptions()
+ options.server_host = host
+ options.port = port
+ options.dispatcher = Dispatcher()
+ options.dispatcher.addHandler(ApiHandler.PATH, ApiHandler(self.core.api))
+ options.dispatcher.addHandler(AsyncHandler.PATH, AsyncHandler(self.core.api))
+
+ self.server = WebSocketServer(options)
+
+
+ def serve(self):
+ self.server.serve_forever()
diff --git a/pyload/remote/__init__.py b/pyload/remote/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pyload/remote/__init__.py
diff --git a/pyload/remote/apitypes.py b/pyload/remote/apitypes.py
new file mode 100644
index 000000000..196491083
--- /dev/null
+++ b/pyload/remote/apitypes.py
@@ -0,0 +1,536 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Autogenerated by pyload
+# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
+
+class BaseObject(object):
+ __slots__ = []
+
+ def __str__(self):
+ return "<%s %s>" % (self.__class__.__name__, ", ".join("%s=%s" % (k,getattr(self,k)) for k in self.__slots__))
+
+class ExceptionObject(Exception):
+ __slots__ = []
+
+class DownloadState:
+ All = 0
+ Finished = 1
+ Unfinished = 2
+ Failed = 3
+ Unmanaged = 4
+
+class DownloadStatus:
+ NA = 0
+ Offline = 1
+ Online = 2
+ Queued = 3
+ Paused = 4
+ Finished = 5
+ Skipped = 6
+ Failed = 7
+ Starting = 8
+ Waiting = 9
+ Downloading = 10
+ TempOffline = 11
+ Aborted = 12
+ Decrypting = 13
+ Processing = 14
+ Custom = 15
+ Unknown = 16
+
+class FileStatus:
+ Ok = 0
+ Missing = 1
+ Remote = 2
+
+class InputType:
+ NA = 0
+ Text = 1
+ Int = 2
+ File = 3
+ Folder = 4
+ Textbox = 5
+ Password = 6
+ Bool = 7
+ Click = 8
+ Select = 9
+ Multiple = 10
+ List = 11
+ Table = 12
+
+class Interaction:
+ All = 0
+ Notification = 1
+ Captcha = 2
+ Query = 4
+
+class MediaType:
+ All = 0
+ Other = 1
+ Audio = 2
+ Image = 4
+ Video = 8
+ Document = 16
+ Archive = 32
+
+class PackageStatus:
+ Ok = 0
+ Paused = 1
+ Folder = 2
+ Remote = 3
+
+class Permission:
+ All = 0
+ Add = 1
+ Delete = 2
+ Modify = 4
+ Download = 8
+ Accounts = 16
+ Interaction = 32
+ Plugins = 64
+
+class Role:
+ Admin = 0
+ User = 1
+
+class AccountInfo(BaseObject):
+ __slots__ = ['plugin', 'loginname', 'owner', 'valid', 'validuntil', 'trafficleft', 'maxtraffic', 'premium', 'activated', 'shared', 'options']
+
+ def __init__(self, plugin=None, loginname=None, owner=None, valid=None, validuntil=None, trafficleft=None, maxtraffic=None, premium=None, activated=None, shared=None, options=None):
+ self.plugin = plugin
+ self.loginname = loginname
+ self.owner = owner
+ self.valid = valid
+ self.validuntil = validuntil
+ self.trafficleft = trafficleft
+ self.maxtraffic = maxtraffic
+ self.premium = premium
+ self.activated = activated
+ self.shared = shared
+ self.options = options
+
+class AddonInfo(BaseObject):
+ __slots__ = ['func_name', 'description', 'value']
+
+ def __init__(self, func_name=None, description=None, value=None):
+ self.func_name = func_name
+ self.description = description
+ self.value = value
+
+class AddonService(BaseObject):
+ __slots__ = ['func_name', 'description', 'arguments', 'media']
+
+ def __init__(self, func_name=None, description=None, arguments=None, media=None):
+ self.func_name = func_name
+ self.description = description
+ self.arguments = arguments
+ self.media = media
+
+class ConfigHolder(BaseObject):
+ __slots__ = ['name', 'label', 'description', 'long_description', 'items', 'info']
+
+ def __init__(self, name=None, label=None, description=None, long_description=None, items=None, info=None):
+ self.name = name
+ self.label = label
+ self.description = description
+ self.long_description = long_description
+ self.items = items
+ self.info = info
+
+class ConfigInfo(BaseObject):
+ __slots__ = ['name', 'label', 'description', 'category', 'user_context', 'activated']
+
+ def __init__(self, name=None, label=None, description=None, category=None, user_context=None, activated=None):
+ self.name = name
+ self.label = label
+ self.description = description
+ self.category = category
+ self.user_context = user_context
+ self.activated = activated
+
+class ConfigItem(BaseObject):
+ __slots__ = ['name', 'label', 'description', 'input', 'default_value', 'value']
+
+ def __init__(self, name=None, label=None, description=None, input=None, default_value=None, value=None):
+ self.name = name
+ self.label = label
+ self.description = description
+ self.input = input
+ self.default_value = default_value
+ self.value = value
+
+class DownloadInfo(BaseObject):
+ __slots__ = ['url', 'plugin', 'hash', 'status', 'statusmsg', 'error']
+
+ def __init__(self, url=None, plugin=None, hash=None, status=None, statusmsg=None, error=None):
+ self.url = url
+ self.plugin = plugin
+ self.hash = hash
+ self.status = status
+ self.statusmsg = statusmsg
+ self.error = error
+
+class DownloadProgress(BaseObject):
+ __slots__ = ['fid', 'pid', 'speed', 'status']
+
+ def __init__(self, fid=None, pid=None, speed=None, status=None):
+ self.fid = fid
+ self.pid = pid
+ self.speed = speed
+ self.status = status
+
+class EventInfo(BaseObject):
+ __slots__ = ['eventname', 'event_args']
+
+ def __init__(self, eventname=None, event_args=None):
+ self.eventname = eventname
+ self.event_args = event_args
+
+class FileDoesNotExists(ExceptionObject):
+ __slots__ = ['fid']
+
+ def __init__(self, fid=None):
+ self.fid = fid
+
+class FileInfo(BaseObject):
+ __slots__ = ['fid', 'name', 'package', 'owner', 'size', 'status', 'media', 'added', 'fileorder', 'download']
+
+ def __init__(self, fid=None, name=None, package=None, owner=None, size=None, status=None, media=None, added=None, fileorder=None, download=None):
+ self.fid = fid
+ self.name = name
+ self.package = package
+ self.owner = owner
+ self.size = size
+ self.status = status
+ self.media = media
+ self.added = added
+ self.fileorder = fileorder
+ self.download = download
+
+class Forbidden(ExceptionObject):
+ pass
+
+class Input(BaseObject):
+ __slots__ = ['type', 'data']
+
+ def __init__(self, type=None, data=None):
+ self.type = type
+ self.data = data
+
+class InteractionTask(BaseObject):
+ __slots__ = ['iid', 'type', 'input', 'default_value', 'title', 'description', 'plugin']
+
+ def __init__(self, iid=None, type=None, input=None, default_value=None, title=None, description=None, plugin=None):
+ self.iid = iid
+ self.type = type
+ self.input = input
+ self.default_value = default_value
+ self.title = title
+ self.description = description
+ self.plugin = plugin
+
+class InvalidConfigSection(ExceptionObject):
+ __slots__ = ['section']
+
+ def __init__(self, section=None):
+ self.section = section
+
+class LinkStatus(BaseObject):
+ __slots__ = ['url', 'name', 'plugin', 'size', 'status', 'packagename']
+
+ def __init__(self, url=None, name=None, plugin=None, size=None, status=None, packagename=None):
+ self.url = url
+ self.name = name
+ self.plugin = plugin
+ self.size = size
+ self.status = status
+ self.packagename = packagename
+
+class OnlineCheck(BaseObject):
+ __slots__ = ['rid', 'data']
+
+ def __init__(self, rid=None, data=None):
+ self.rid = rid
+ self.data = data
+
+class PackageDoesNotExists(ExceptionObject):
+ __slots__ = ['pid']
+
+ def __init__(self, pid=None):
+ self.pid = pid
+
+class PackageInfo(BaseObject):
+ __slots__ = ['pid', 'name', 'folder', 'root', 'owner', 'site', 'comment', 'password', 'added', 'tags', 'status', 'shared', 'packageorder', 'stats', 'fids', 'pids']
+
+ def __init__(self, pid=None, name=None, folder=None, root=None, owner=None, site=None, comment=None, password=None, added=None, tags=None, status=None, shared=None, packageorder=None, stats=None, fids=None, pids=None):
+ self.pid = pid
+ self.name = name
+ self.folder = folder
+ self.root = root
+ self.owner = owner
+ self.site = site
+ self.comment = comment
+ self.password = password
+ self.added = added
+ self.tags = tags
+ self.status = status
+ self.shared = shared
+ self.packageorder = packageorder
+ self.stats = stats
+ self.fids = fids
+ self.pids = pids
+
+class PackageStats(BaseObject):
+ __slots__ = ['linkstotal', 'linksdone', 'sizetotal', 'sizedone']
+
+ def __init__(self, linkstotal=None, linksdone=None, sizetotal=None, sizedone=None):
+ self.linkstotal = linkstotal
+ self.linksdone = linksdone
+ self.sizetotal = sizetotal
+ self.sizedone = sizedone
+
+class ProgressInfo(BaseObject):
+ __slots__ = ['plugin', 'name', 'statusmsg', 'eta', 'done', 'total', 'download']
+
+ def __init__(self, plugin=None, name=None, statusmsg=None, eta=None, done=None, total=None, download=None):
+ self.plugin = plugin
+ self.name = name
+ self.statusmsg = statusmsg
+ self.eta = eta
+ self.done = done
+ self.total = total
+ self.download = download
+
+class ServerStatus(BaseObject):
+ __slots__ = ['speed', 'linkstotal', 'linksqueue', 'sizetotal', 'sizequeue', 'notifications', 'paused', 'download', 'reconnect']
+
+ def __init__(self, speed=None, linkstotal=None, linksqueue=None, sizetotal=None, sizequeue=None, notifications=None, paused=None, download=None, reconnect=None):
+ self.speed = speed
+ self.linkstotal = linkstotal
+ self.linksqueue = linksqueue
+ self.sizetotal = sizetotal
+ self.sizequeue = sizequeue
+ self.notifications = notifications
+ self.paused = paused
+ self.download = download
+ self.reconnect = reconnect
+
+class ServiceDoesNotExists(ExceptionObject):
+ __slots__ = ['plugin', 'func']
+
+ def __init__(self, plugin=None, func=None):
+ self.plugin = plugin
+ self.func = func
+
+class ServiceException(ExceptionObject):
+ __slots__ = ['msg']
+
+ def __init__(self, msg=None):
+ self.msg = msg
+
+class TreeCollection(BaseObject):
+ __slots__ = ['root', 'files', 'packages']
+
+ def __init__(self, root=None, files=None, packages=None):
+ self.root = root
+ self.files = files
+ self.packages = packages
+
+class Unauthorized(ExceptionObject):
+ pass
+
+class UserData(BaseObject):
+ __slots__ = ['uid', 'name', 'email', 'role', 'permission', 'folder', 'traffic', 'dllimit', 'dlquota', 'hddquota', 'user', 'templateName']
+
+ def __init__(self, uid=None, name=None, email=None, role=None, permission=None, folder=None, traffic=None, dllimit=None, dlquota=None, hddquota=None, user=None, templateName=None):
+ self.uid = uid
+ self.name = name
+ self.email = email
+ self.role = role
+ self.permission = permission
+ self.folder = folder
+ self.traffic = traffic
+ self.dllimit = dllimit
+ self.dlquota = dlquota
+ self.hddquota = hddquota
+ self.user = user
+ self.templateName = templateName
+
+class UserDoesNotExists(ExceptionObject):
+ __slots__ = ['user']
+
+ def __init__(self, user=None):
+ self.user = user
+
+class Iface(object):
+ def addFromCollector(self, name, paused):
+ pass
+ def addLinks(self, pid, links):
+ pass
+ def addLocalFile(self, pid, name, path):
+ pass
+ def addPackage(self, name, links, password):
+ pass
+ def addPackageChild(self, name, links, password, root, paused):
+ pass
+ def addPackageP(self, name, links, password, paused):
+ pass
+ def addToCollector(self, links):
+ pass
+ def addUser(self, username, password):
+ pass
+ def callAddon(self, plugin, func, arguments):
+ pass
+ def callAddonHandler(self, plugin, func, pid_or_fid):
+ pass
+ def checkOnlineStatus(self, urls):
+ pass
+ def checkOnlineStatusContainer(self, urls, filename, data):
+ pass
+ def checkURLs(self, urls):
+ pass
+ def createPackage(self, name, folder, root, password, site, comment, paused):
+ pass
+ def deleteCollLink(self, url):
+ pass
+ def deleteCollPack(self, name):
+ pass
+ def deleteConfig(self, plugin):
+ pass
+ def deleteFiles(self, fids):
+ pass
+ def deletePackages(self, pids):
+ pass
+ def findFiles(self, pattern):
+ pass
+ def findPackages(self, tags):
+ pass
+ def freeSpace(self):
+ pass
+ def generateAndAddPackages(self, links, paused):
+ pass
+ def generateDownloadLink(self, fid, timeout):
+ pass
+ def generatePackages(self, links):
+ pass
+ def getAccountTypes(self):
+ pass
+ def getAccounts(self, refresh):
+ pass
+ def getAddonHandler(self):
+ pass
+ def getAllFiles(self):
+ pass
+ def getAllUserData(self):
+ pass
+ def getAvailablePlugins(self):
+ pass
+ def getCollector(self):
+ pass
+ def getConfig(self):
+ pass
+ def getConfigValue(self, section, option):
+ pass
+ def getCoreConfig(self):
+ pass
+ def getFileInfo(self, fid):
+ pass
+ def getFileTree(self, pid, full):
+ pass
+ def getFilteredFileTree(self, pid, full, state):
+ pass
+ def getFilteredFiles(self, state):
+ pass
+ def getInteractionTasks(self, mode):
+ pass
+ def getLog(self, offset):
+ pass
+ def getPackageContent(self, pid):
+ pass
+ def getPackageInfo(self, pid):
+ pass
+ def getPluginConfig(self):
+ pass
+ def getProgressInfo(self):
+ pass
+ def getServerStatus(self):
+ pass
+ def getServerVersion(self):
+ pass
+ def getUserData(self):
+ pass
+ def getWSAddress(self):
+ pass
+ def hasAddonHandler(self, plugin, func):
+ pass
+ def isInteractionWaiting(self, mode):
+ pass
+ def loadConfig(self, name):
+ pass
+ def login(self, username, password):
+ pass
+ def moveFiles(self, fids, pid):
+ pass
+ def movePackage(self, pid, root):
+ pass
+ def orderFiles(self, fids, pid, position):
+ pass
+ def orderPackage(self, pids, position):
+ pass
+ def parseURLs(self, html, url):
+ pass
+ def pauseServer(self):
+ pass
+ def pollResults(self, rid):
+ pass
+ def quit(self):
+ pass
+ def recheckPackage(self, pid):
+ pass
+ def removeAccount(self, account):
+ pass
+ def removeUser(self, uid):
+ pass
+ def renameCollPack(self, name, new_name):
+ pass
+ def restart(self):
+ pass
+ def restartFailed(self):
+ pass
+ def restartFile(self, fid):
+ pass
+ def restartPackage(self, pid):
+ pass
+ def saveConfig(self, config):
+ pass
+ def searchSuggestions(self, pattern):
+ pass
+ def setConfigValue(self, section, option, value):
+ pass
+ def setInteractionResult(self, iid, result):
+ pass
+ def setPackageFolder(self, pid, path):
+ pass
+ def setPassword(self, username, old_password, new_password):
+ pass
+ def stopAllDownloads(self):
+ pass
+ def stopDownloads(self, fids):
+ pass
+ def togglePause(self):
+ pass
+ def toggleReconnect(self):
+ pass
+ def unpauseServer(self):
+ pass
+ def updateAccount(self, plugin, login, password):
+ pass
+ def updateAccountInfo(self, account):
+ pass
+ def updatePackage(self, pack):
+ pass
+ def updateUserData(self, data):
+ pass
+ def uploadContainer(self, filename, data):
+ pass
+
diff --git a/pyload/remote/apitypes_debug.py b/pyload/remote/apitypes_debug.py
new file mode 100644
index 000000000..96673cc99
--- /dev/null
+++ b/pyload/remote/apitypes_debug.py
@@ -0,0 +1,135 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Autogenerated by pyload
+# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
+
+from ttypes import *
+
+enums = [
+ "DownloadState",
+ "DownloadStatus",
+ "FileStatus",
+ "InputType",
+ "Interaction",
+ "MediaType",
+ "PackageStatus",
+ "Permission",
+ "Role",
+]
+
+classes = {
+ 'AccountInfo' : [basestring, basestring, int, bool, int, int, int, bool, bool, bool, (dict, basestring, basestring)],
+ 'AddonInfo' : [basestring, basestring, basestring],
+ 'AddonService' : [basestring, basestring, (list, basestring), (None, int)],
+ 'ConfigHolder' : [basestring, basestring, basestring, basestring, (list, ConfigItem), (None, (list, AddonInfo))],
+ 'ConfigInfo' : [basestring, basestring, basestring, basestring, bool, (None, bool)],
+ 'ConfigItem' : [basestring, basestring, basestring, Input, basestring, basestring],
+ 'DownloadInfo' : [basestring, basestring, basestring, int, basestring, basestring],
+ 'DownloadProgress' : [int, int, int, int],
+ 'EventInfo' : [basestring, (list, basestring)],
+ 'FileDoesNotExists' : [int],
+ 'FileInfo' : [int, basestring, int, int, int, int, int, int, int, (None, DownloadInfo)],
+ 'Input' : [int, (None, basestring)],
+ 'InteractionTask' : [int, int, Input, (None, basestring), basestring, basestring, basestring],
+ 'InvalidConfigSection' : [basestring],
+ 'LinkStatus' : [basestring, basestring, basestring, int, int, basestring],
+ 'OnlineCheck' : [int, (None, (dict, basestring, LinkStatus))],
+ 'PackageDoesNotExists' : [int],
+ 'PackageInfo' : [int, basestring, basestring, int, int, basestring, basestring, basestring, int, (list, basestring), int, bool, int, PackageStats, (list, int), (list, int)],
+ 'PackageStats' : [int, int, int, int],
+ 'ProgressInfo' : [basestring, basestring, basestring, int, int, int, (None, DownloadProgress)],
+ 'ServerStatus' : [int, int, int, int, int, bool, bool, bool, bool],
+ 'ServiceDoesNotExists' : [basestring, basestring],
+ 'ServiceException' : [basestring],
+ 'TreeCollection' : [PackageInfo, (dict, int, FileInfo), (dict, int, PackageInfo)],
+ 'UserData' : [int, basestring, basestring, int, int, basestring, int, int, basestring, int, int, basestring],
+ 'UserDoesNotExists' : [basestring],
+}
+
+methods = {
+ 'addFromCollector': int,
+ 'addLinks': None,
+ 'addLocalFile': None,
+ 'addPackage': int,
+ 'addPackageChild': int,
+ 'addPackageP': int,
+ 'addToCollector': None,
+ 'addUser': UserData,
+ 'callAddon': None,
+ 'callAddonHandler': None,
+ 'checkOnlineStatus': OnlineCheck,
+ 'checkOnlineStatusContainer': OnlineCheck,
+ 'checkURLs': (dict, basestring, list),
+ 'createPackage': int,
+ 'deleteCollLink': None,
+ 'deleteCollPack': None,
+ 'deleteConfig': None,
+ 'deleteFiles': None,
+ 'deletePackages': None,
+ 'findFiles': TreeCollection,
+ 'findPackages': TreeCollection,
+ 'freeSpace': int,
+ 'generateAndAddPackages': (list, int),
+ 'generateDownloadLink': basestring,
+ 'generatePackages': (dict, basestring, list),
+ 'getAccountTypes': (list, basestring),
+ 'getAccounts': (list, AccountInfo),
+ 'getAddonHandler': (dict, basestring, list),
+ 'getAllFiles': TreeCollection,
+ 'getAllUserData': (dict, int, UserData),
+ 'getAvailablePlugins': (list, ConfigInfo),
+ 'getCollector': (list, LinkStatus),
+ 'getConfig': (dict, basestring, ConfigHolder),
+ 'getConfigValue': basestring,
+ 'getCoreConfig': (list, ConfigInfo),
+ 'getFileInfo': FileInfo,
+ 'getFileTree': TreeCollection,
+ 'getFilteredFileTree': TreeCollection,
+ 'getFilteredFiles': TreeCollection,
+ 'getInteractionTasks': (list, InteractionTask),
+ 'getLog': (list, basestring),
+ 'getPackageContent': TreeCollection,
+ 'getPackageInfo': PackageInfo,
+ 'getPluginConfig': (list, ConfigInfo),
+ 'getProgressInfo': (list, ProgressInfo),
+ 'getServerStatus': ServerStatus,
+ 'getServerVersion': basestring,
+ 'getUserData': UserData,
+ 'getWSAddress': basestring,
+ 'hasAddonHandler': bool,
+ 'isInteractionWaiting': bool,
+ 'loadConfig': ConfigHolder,
+ 'login': bool,
+ 'moveFiles': bool,
+ 'movePackage': bool,
+ 'orderFiles': None,
+ 'orderPackage': None,
+ 'parseURLs': (dict, basestring, list),
+ 'pauseServer': None,
+ 'pollResults': OnlineCheck,
+ 'quit': None,
+ 'recheckPackage': None,
+ 'removeAccount': None,
+ 'removeUser': None,
+ 'renameCollPack': None,
+ 'restart': None,
+ 'restartFailed': None,
+ 'restartFile': None,
+ 'restartPackage': None,
+ 'saveConfig': None,
+ 'searchSuggestions': (list, basestring),
+ 'setConfigValue': None,
+ 'setInteractionResult': None,
+ 'setPackageFolder': bool,
+ 'setPassword': bool,
+ 'stopAllDownloads': None,
+ 'stopDownloads': None,
+ 'togglePause': bool,
+ 'toggleReconnect': bool,
+ 'unpauseServer': None,
+ 'updateAccount': None,
+ 'updateAccountInfo': None,
+ 'updatePackage': None,
+ 'updateUserData': None,
+ 'uploadContainer': int,
+}
diff --git a/pyload/remote/create_apitypes.py b/pyload/remote/create_apitypes.py
new file mode 100644
index 000000000..d596f07ac
--- /dev/null
+++ b/pyload/remote/create_apitypes.py
@@ -0,0 +1,180 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+import re
+import inspect
+from os.path import abspath, dirname, join
+
+path = dirname(abspath(__file__))
+root = abspath(join(path, "..", ".."))
+
+from thrift.Thrift import TType
+from thriftgen.pyload import ttypes
+from thriftgen.pyload import Pyload
+
+# TODO: import and add version
+# from pyload import CURRENT_VERSION
+
+type_map = {
+ TType.BOOL: 'bool',
+ TType.DOUBLE: 'float',
+ TType.I16: 'int',
+ TType.I32: 'int',
+ TType.I64: 'int',
+ TType.STRING: 'basestring',
+ TType.MAP: 'dict',
+ TType.LIST: 'list',
+ TType.SET: 'set',
+ TType.VOID: 'None',
+ TType.STRUCT: 'BaseObject',
+ TType.UTF8: 'unicode',
+}
+
+def get_spec(spec, optional=False):
+ """ analyze the generated spec file and writes information into file """
+ if spec[1] == TType.STRUCT:
+ return spec[3][0].__name__
+ elif spec[1] == TType.LIST:
+ if spec[3][0] == TType.STRUCT:
+ ttype = spec[3][1][0].__name__
+ else:
+ ttype = type_map[spec[3][0]]
+ return "(list, %s)" % ttype
+ elif spec[1] == TType.MAP:
+ if spec[3][2] == TType.STRUCT:
+ ttype = spec[3][3][0].__name__
+ else:
+ ttype = type_map[spec[3][2]]
+
+ return "(dict, %s, %s)" % (type_map[spec[3][0]], ttype)
+ else:
+ return type_map[spec[1]]
+
+optional_re = "%d: +optional +[a-z0-9<>_-]+ +%s"
+
+def main():
+
+ enums = []
+ classes = []
+ tf = open(join(path, "pyload.thrift"), "rb").read()
+
+ print "generating apitypes.py"
+
+ for name in dir(ttypes):
+ klass = getattr(ttypes, name)
+
+ if name in ("TBase", "TExceptionBase") or name.startswith("_") or not (issubclass(klass, ttypes.TBase) or issubclass(klass, ttypes.TExceptionBase)):
+ continue
+
+ if hasattr(klass, "thrift_spec"):
+ classes.append(klass)
+ else:
+ enums.append(klass)
+
+
+ f = open(join(path, "apitypes.py"), "wb")
+ f.write(
+ """#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Autogenerated by pyload
+# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
+
+class BaseObject(object):
+\t__slots__ = []
+
+\tdef __str__(self):
+\t\treturn "<%s %s>" % (self.__class__.__name__, ", ".join("%s=%s" % (k,getattr(self,k)) for k in self.__slots__))
+
+class ExceptionObject(Exception):
+\t__slots__ = []
+
+""")
+
+ dev = open(join(path, "apitypes_debug.py"), "wb")
+ dev.write("""#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Autogenerated by pyload
+# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING\n
+from ttypes import *\n
+""")
+
+ dev.write("enums = [\n")
+
+ ## generate enums
+ for enum in enums:
+ name = enum.__name__
+ f.write("class %s:\n" % name)
+
+ for attr in sorted(dir(enum), key=lambda x: getattr(enum, x)):
+ if attr.startswith("_") or attr in ("read", "write"): continue
+ f.write("\t%s = %s\n" % (attr, getattr(enum, attr)))
+
+ dev.write('\t"%s",\n' % name)
+ f.write("\n")
+
+ dev.write("]\n\n")
+
+ dev.write("classes = {\n")
+
+ for klass in classes:
+ name = klass.__name__
+ base = "ExceptionObject" if issubclass(klass, ttypes.TExceptionBase) else "BaseObject"
+ f.write("class %s(%s):\n" % (name, base))
+
+ # No attributes, don't write further info
+ if not klass.__slots__:
+ f.write("\tpass\n\n")
+ continue
+
+ f.write("\t__slots__ = %s\n\n" % klass.__slots__)
+ dev.write("\t'%s' : [" % name)
+
+ #create init
+ args = ["self"] + ["%s=None" % x for x in klass.__slots__]
+ specs = []
+
+ f.write("\tdef __init__(%s):\n" % ", ".join(args))
+ for i, attr in enumerate(klass.__slots__):
+ f.write("\t\tself.%s = %s\n" % (attr, attr))
+
+ spec = klass.thrift_spec[i+1]
+ # assert correct order, so the list of types is enough for check
+ assert spec[2] == attr
+ # dirty way to check optional attribute, since it is not in the generated code
+ # can produce false positives, but these are not critical
+ optional = re.search(optional_re % (i+1, attr), tf, re.I)
+ if optional:
+ specs.append("(None, %s)" % get_spec(spec))
+ else:
+ specs.append(get_spec(spec))
+
+ f.write("\n")
+ dev.write(", ".join(specs) + "],\n")
+
+ dev.write("}\n\n")
+
+ f.write("class Iface(object):\n")
+ dev.write("methods = {\n")
+
+ for name in dir(Pyload.Iface):
+ if name.startswith("_"): continue
+
+ func = inspect.getargspec(getattr(Pyload.Iface, name))
+
+ f.write("\tdef %s(%s):\n\t\tpass\n" % (name, ", ".join(func.args)))
+
+ spec = getattr(Pyload, "%s_result" % name).thrift_spec
+ if not spec or not spec[0]:
+ dev.write("\t'%s': None,\n" % name)
+ else:
+ spec = spec[0]
+ dev.write("\t'%s': %s,\n" % (name, get_spec(spec)))
+
+ f.write("\n")
+ dev.write("}\n")
+
+ f.close()
+ dev.close()
+
+if __name__ == "__main__":
+ main() \ No newline at end of file
diff --git a/pyload/remote/create_jstypes.py b/pyload/remote/create_jstypes.py
new file mode 100644
index 000000000..90afa4c96
--- /dev/null
+++ b/pyload/remote/create_jstypes.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+from os.path import abspath, dirname, join
+
+path = dirname(abspath(__file__))
+module = join(path, "..")
+
+import apitypes
+from apitypes_debug import enums
+
+# generate js enums
+def main():
+
+ print "generating apitypes.js"
+
+ f = open(join(module, 'web', 'app', 'scripts', 'utils', 'apitypes.js'), 'wb')
+ f.write("""// Autogenerated, do not edit!
+/*jslint -W070: false*/
+define([], function() {
+\t'use strict';
+\treturn {
+""")
+
+ for name in enums:
+ enum = getattr(apitypes, name)
+ values = dict([(attr, getattr(enum, attr)) for attr in dir(enum) if not attr.startswith("_")])
+
+ f.write("\t\t%s: %s,\n" % (name, str(values)))
+
+ f.write("\t};\n});")
+ f.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pyload/remote/json_converter.py b/pyload/remote/json_converter.py
new file mode 100644
index 000000000..3e6c7f797
--- /dev/null
+++ b/pyload/remote/json_converter.py
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+try:
+ from pyload.utils import json
+except ImportError:
+ import json
+
+
+import apitypes
+from apitypes import BaseObject
+from apitypes import ExceptionObject
+
+# compact json separator
+separators = (',', ':')
+
+# json encoder that accepts api objects
+class BaseEncoder(json.JSONEncoder):
+
+ def default(self, o):
+ if isinstance(o, BaseObject) or isinstance(o, ExceptionObject):
+ ret = {"@class" : o.__class__.__name__}
+ for att in o.__slots__:
+ ret[att] = getattr(o, att)
+ return ret
+
+ return json.JSONEncoder.default(self, o)
+
+# more compact representation, only clients with information of the classes can handle it
+class BaseEncoderCompact(json.JSONEncoder):
+
+ def default(self, o):
+ if isinstance(o, BaseObject) or isinstance(o, ExceptionObject):
+ ret = {"@compact" : [o.__class__.__name__]}
+ ret["@compact"].extend(getattr(o, attr) for attr in o.__slots__)
+ return ret
+
+ return json.JSONEncoder.default(self, o)
+
+def convert_obj(dct):
+ if '@class' in dct:
+ cls = getattr(apitypes, dct['@class'])
+ del dct['@class']
+ return cls(**dct)
+ elif '@compact' in dct:
+ cls = getattr(apitypes, dct['@compact'][0])
+ return cls(*dct['@compact'][1:])
+
+ return dct
+
+def dumps(*args, **kwargs):
+ if 'compact' in kwargs:
+ kwargs['cls'] = BaseEncoderCompact
+ del kwargs['compact']
+ else:
+ kwargs['cls'] = BaseEncoder
+
+ kwargs['separators'] = separators
+ return json.dumps(*args, **kwargs)
+
+
+def loads(*args, **kwargs):
+ kwargs['object_hook'] = convert_obj
+ return json.loads(*args, **kwargs) \ No newline at end of file
diff --git a/pyload/remote/pyload.thrift b/pyload/remote/pyload.thrift
new file mode 100644
index 000000000..57d7e0a0a
--- /dev/null
+++ b/pyload/remote/pyload.thrift
@@ -0,0 +1,538 @@
+namespace java org.pyload.thrift
+
+typedef i32 FileID
+typedef i32 PackageID
+typedef i32 ResultID
+typedef i32 InteractionID
+typedef i32 UserID
+typedef i64 UTCDate
+typedef i64 ByteCount
+typedef list<string> LinkList
+typedef string PluginName
+typedef string JSONString
+
+// NA - Not Available
+enum DownloadStatus {
+ NA,
+ Offline,
+ Online,
+ Queued,
+ Paused,
+ Finished,
+ Skipped,
+ Failed,
+ Starting,
+ Waiting,
+ Downloading,
+ TempOffline,
+ Aborted,
+ Decrypting,
+ Processing,
+ Custom,
+ Unknown
+}
+
+// Download states, combination of several downloadstatuses
+// defined in Api
+enum DownloadState {
+ All,
+ Finished,
+ Unfinished,
+ Failed,
+ Unmanaged // internal state
+}
+
+enum MediaType {
+ All = 0
+ Other = 1,
+ Audio = 2,
+ Image = 4,
+ Video = 8,
+ Document = 16,
+ Archive = 32,
+}
+
+enum FileStatus {
+ Ok,
+ Missing,
+ Remote, // file is available at remote location
+}
+
+enum PackageStatus {
+ Ok,
+ Paused,
+ Folder,
+ Remote,
+}
+
+// types for user interaction
+// some may only be place holder currently not supported
+// also all input - output combination are not reasonable, see InteractionManager for further info
+// Todo: how about: time, ip, s.o.
+enum InputType {
+ NA,
+ Text,
+ Int,
+ File,
+ Folder,
+ Textbox,
+ Password,
+ Bool, // confirm like, yes or no dialog
+ Click, // for positional captchas
+ Select, // select from list
+ Multiple, // multiple choice from list of elements
+ List, // arbitary list of elements
+ Table // table like data structure
+}
+// more can be implemented by need
+
+// this describes the type of the outgoing interaction
+// ensure they can be logcial or'ed
+enum Interaction {
+ All = 0,
+ Notification = 1,
+ Captcha = 2,
+ Query = 4,
+}
+
+enum Permission {
+ All = 0, // requires no permission, but login
+ Add = 1, // can add packages
+ Delete = 2, // can delete packages
+ Modify = 4, // modify some attribute of downloads
+ Download = 8, // can download from webinterface
+ Accounts = 16, // can access accounts
+ Interaction = 32, // can interact with plugins
+ Plugins = 64 // user can configure plugins and activate addons
+}
+
+enum Role {
+ Admin = 0, //admin has all permissions implicit
+ User = 1
+}
+
+struct Input {
+ 1: InputType type,
+ 2: optional JSONString data,
+}
+
+struct DownloadProgress {
+ 1: FileID fid,
+ 2: PackageID pid,
+ 3: ByteCount speed, // per second
+ 4: DownloadStatus status,
+}
+
+struct ProgressInfo {
+ 1: PluginName plugin,
+ 2: string name,
+ 3: string statusmsg,
+ 4: i32 eta, // in seconds
+ 5: ByteCount done,
+ 6: ByteCount total, // arbitary number, size in case of files
+ 7: optional DownloadProgress download
+}
+
+// download info for specific file
+struct DownloadInfo {
+ 1: string url,
+ 2: PluginName plugin,
+ 3: string hash,
+ 4: DownloadStatus status,
+ 5: string statusmsg,
+ 6: string error,
+}
+
+struct FileInfo {
+ 1: FileID fid,
+ 2: string name,
+ 3: PackageID package,
+ 4: UserID owner,
+ 5: ByteCount size,
+ 6: FileStatus status,
+ 7: MediaType media,
+ 8: UTCDate added,
+ 9: i16 fileorder,
+ 10: optional DownloadInfo download,
+}
+
+struct PackageStats {
+ 1: i16 linkstotal,
+ 2: i16 linksdone,
+ 3: ByteCount sizetotal,
+ 4: ByteCount sizedone,
+}
+
+struct PackageInfo {
+ 1: PackageID pid,
+ 2: string name,
+ 3: string folder,
+ 4: PackageID root,
+ 5: UserID owner,
+ 6: string site,
+ 7: string comment,
+ 8: string password,
+ 9: UTCDate added,
+ 10: list<string> tags,
+ 11: PackageStatus status,
+ 12: bool shared,
+ 13: i16 packageorder,
+ 14: PackageStats stats,
+ 15: list<FileID> fids,
+ 16: list<PackageID> pids,
+}
+
+// thrift does not allow recursive datatypes, so all data is accumulated and mapped with id
+struct TreeCollection {
+ 1: PackageInfo root,
+ 2: map<FileID, FileInfo> files,
+ 3: map<PackageID, PackageInfo> packages
+}
+
+// general info about link, used for collector and online results
+struct LinkStatus {
+ 1: string url,
+ 2: string name,
+ 3: PluginName plugin,
+ 4: ByteCount size, // size <= 0 : unknown
+ 5: DownloadStatus status,
+ 6: string packagename,
+}
+
+struct ServerStatus {
+ 1: ByteCount speed,
+ 2: i16 linkstotal,
+ 3: i16 linksqueue,
+ 4: ByteCount sizetotal,
+ 5: ByteCount sizequeue,
+ 6: bool notifications,
+ 7: bool paused,
+ 8: bool download,
+ 9: bool reconnect,
+}
+
+struct InteractionTask {
+ 1: InteractionID iid,
+ 2: Interaction type,
+ 3: Input input,
+ 4: optional JSONString default_value,
+ 5: string title,
+ 6: string description,
+ 7: PluginName plugin,
+}
+
+struct AddonService {
+ 1: string func_name,
+ 2: string description,
+ 3: list<string> arguments,
+ 4: optional i16 media,
+}
+
+struct AddonInfo {
+ 1: string func_name,
+ 2: string description,
+ 3: JSONString value,
+}
+
+struct ConfigItem {
+ 1: string name,
+ 2: string label,
+ 3: string description,
+ 4: Input input,
+ 5: JSONString default_value,
+ 6: JSONString value,
+}
+
+struct ConfigHolder {
+ 1: string name, // for plugin this is the PluginName
+ 2: string label,
+ 3: string description,
+ 4: string long_description,
+ 5: list<ConfigItem> items,
+ 6: optional list<AddonInfo> info,
+}
+
+struct ConfigInfo {
+ 1: string name
+ 2: string label,
+ 3: string description,
+ 4: string category,
+ 5: bool user_context,
+ 6: optional bool activated,
+}
+
+struct EventInfo {
+ 1: string eventname,
+ 2: list<JSONString> event_args, //will contain json objects
+}
+
+struct UserData {
+ 1: UserID uid,
+ 2: string name,
+ 3: string email,
+ 4: i16 role,
+ 5: i16 permission,
+ 6: string folder,
+ 7: ByteCount traffic
+ 8: i16 dllimit
+ 9: string dlquota,
+ 10: ByteCount hddquota,
+ 11: UserID user,
+ 12: string templateName
+}
+
+struct AccountInfo {
+ 1: PluginName plugin,
+ 2: string loginname,
+ 3: UserID owner,
+ 4: bool valid,
+ 5: UTCDate validuntil,
+ 6: ByteCount trafficleft,
+ 7: ByteCount maxtraffic,
+ 8: bool premium,
+ 9: bool activated,
+ 10: bool shared,
+ 11: map<string, string> options,
+}
+
+struct OnlineCheck {
+ 1: ResultID rid, // -1 -> nothing more to get
+ 2: map<string, LinkStatus> data, // url to result
+}
+
+// exceptions
+
+exception PackageDoesNotExists {
+ 1: PackageID pid
+}
+
+exception FileDoesNotExists {
+ 1: FileID fid
+}
+
+exception UserDoesNotExists {
+ 1: string user
+}
+
+exception ServiceDoesNotExists {
+ 1: string plugin
+ 2: string func
+}
+
+exception ServiceException {
+ 1: string msg
+}
+
+exception InvalidConfigSection {
+ 1: string section
+}
+
+exception Unauthorized {
+}
+
+exception Forbidden {
+}
+
+
+service Pyload {
+
+ ///////////////////////
+ // Core Status
+ ///////////////////////
+
+ string getServerVersion(),
+ string getWSAddress(),
+ ServerStatus getServerStatus(),
+ list<ProgressInfo> getProgressInfo(),
+
+ list<string> getLog(1: i32 offset),
+ ByteCount freeSpace(),
+
+ void pauseServer(),
+ void unpauseServer(),
+ bool togglePause(),
+ bool toggleReconnect(),
+
+ void quit(),
+ void restart(),
+
+ ///////////////////////
+ // Configuration
+ ///////////////////////
+
+ map<string, ConfigHolder> getConfig(),
+ string getConfigValue(1: string section, 2: string option),
+
+ // two methods with ambigous classification, could be configuration or addon/plugin related
+ list<ConfigInfo> getCoreConfig(),
+ list<ConfigInfo> getPluginConfig(),
+ list<ConfigInfo> getAvailablePlugins(),
+
+ ConfigHolder loadConfig(1: string name),
+
+ void setConfigValue(1: string section, 2: string option, 3: string value),
+ void saveConfig(1: ConfigHolder config),
+ void deleteConfig(1: PluginName plugin),
+
+ ///////////////////////
+ // Download Preparing
+ ///////////////////////
+
+ map<PluginName, LinkList> checkURLs(1: LinkList urls),
+ map<PluginName, LinkList> parseURLs(1: string html, 2: string url),
+
+ // parses results and generates packages
+ OnlineCheck checkOnlineStatus(1: LinkList urls),
+ OnlineCheck checkOnlineStatusContainer(1: LinkList urls, 2: string filename, 3: binary data)
+
+ // poll results from previously started online check
+ OnlineCheck pollResults(1: ResultID rid),
+
+ // packagename -> urls
+ map<string, LinkList> generatePackages(1: LinkList links),
+
+ ///////////////////////
+ // Download
+ ///////////////////////
+
+ list<PackageID> generateAndAddPackages(1: LinkList links, 2: bool paused),
+
+ PackageID createPackage(1: string name, 2: string folder, 3: PackageID root, 4: string password,
+ 5: string site, 6: string comment, 7: bool paused),
+
+ PackageID addPackage(1: string name, 2: LinkList links, 3: string password),
+ // same as above with paused attribute
+ PackageID addPackageP(1: string name, 2: LinkList links, 3: string password, 4: bool paused),
+
+ // pid -1 is toplevel
+ PackageID addPackageChild(1: string name, 2: LinkList links, 3: string password, 4: PackageID root, 5: bool paused),
+
+ PackageID uploadContainer(1: string filename, 2: binary data),
+
+ void addLinks(1: PackageID pid, 2: LinkList links) throws (1: PackageDoesNotExists e),
+ void addLocalFile(1: PackageID pid, 2: string name, 3: string path) throws (1: PackageDoesNotExists e)
+
+ // these are real file operations and WILL delete files on disk
+ void deleteFiles(1: list<FileID> fids),
+ void deletePackages(1: list<PackageID> pids), // delete the whole folder recursive
+
+ // Modify Downloads
+
+ void restartPackage(1: PackageID pid),
+ void restartFile(1: FileID fid),
+ void recheckPackage(1: PackageID pid),
+ void restartFailed(),
+ void stopDownloads(1: list<FileID> fids),
+ void stopAllDownloads(),
+
+ ///////////////////////
+ // Collector
+ ///////////////////////
+
+ list<LinkStatus> getCollector(),
+
+ void addToCollector(1: LinkList links),
+ PackageID addFromCollector(1: string name, 2: bool paused),
+ void renameCollPack(1: string name, 2: string new_name),
+ void deleteCollPack(1: string name),
+ void deleteCollLink(1: string url),
+
+ ////////////////////////////
+ // File Information retrieval
+ ////////////////////////////
+
+ TreeCollection getAllFiles(),
+ TreeCollection getFilteredFiles(1: DownloadState state),
+
+ // pid -1 for root, full=False only delivers first level in tree
+ TreeCollection getFileTree(1: PackageID pid, 2: bool full),
+ TreeCollection getFilteredFileTree(1: PackageID pid, 2: bool full, 3: DownloadState state),
+
+ // same as above with full=False
+ TreeCollection getPackageContent(1: PackageID pid),
+
+ PackageInfo getPackageInfo(1: PackageID pid) throws (1: PackageDoesNotExists e),
+ FileInfo getFileInfo(1: FileID fid) throws (1: FileDoesNotExists e),
+
+ TreeCollection findFiles(1: string pattern),
+ TreeCollection findPackages(1: list<string> tags),
+ list<string> searchSuggestions(1: string pattern),
+
+ // Modify Files/Packages
+
+ // moving package while downloading is not possible, so they will return bool to indicate success
+ void updatePackage(1: PackageInfo pack) throws (1: PackageDoesNotExists e),
+ bool setPackageFolder(1: PackageID pid, 2: string path) throws (1: PackageDoesNotExists e),
+
+ // as above, this will move files on disk
+ bool movePackage(1: PackageID pid, 2: PackageID root) throws (1: PackageDoesNotExists e),
+ bool moveFiles(1: list<FileID> fids, 2: PackageID pid) throws (1: PackageDoesNotExists e),
+
+ void orderPackage(1: list<PackageID> pids, 2: i16 position),
+ void orderFiles(1: list<FileID> fids, 2: PackageID pid, 3: i16 position),
+
+ ///////////////////////
+ // User Interaction
+ ///////////////////////
+
+ // mode = interaction types binary ORed
+ bool isInteractionWaiting(1: i16 mode),
+ list<InteractionTask> getInteractionTasks(1: i16 mode),
+ void setInteractionResult(1: InteractionID iid, 2: JSONString result),
+
+ // generate a download link, everybody can download the file until timeout reached
+ string generateDownloadLink(1: FileID fid, 2: i16 timeout),
+
+ ///////////////////////
+ // Account Methods
+ ///////////////////////
+
+ list<AccountInfo> getAccounts(1: bool refresh),
+ list<string> getAccountTypes(),
+ void updateAccount(1: PluginName plugin, 2: string login, 3: string password),
+ void updateAccountInfo(1: AccountInfo account),
+ void removeAccount(1: AccountInfo account),
+
+ /////////////////////////
+ // Auth+User Information
+ /////////////////////////
+
+ bool login(1: string username, 2: string password),
+ // returns own user data
+ UserData getUserData(),
+
+ // all user, for admins only
+ map<UserID, UserData> getAllUserData(),
+
+ UserData addUser(1: string username, 2:string password),
+
+ // normal user can only update their own userdata and not all attributes
+ void updateUserData(1: UserData data),
+ void removeUser(1: UserID uid),
+
+ // works contextual, admin can change every password
+ bool setPassword(1: string username, 2: string old_password, 3: string new_password),
+
+ ///////////////////////
+ // Addon Methods
+ ///////////////////////
+
+ //map<PluginName, list<AddonInfo>> getAllInfo(),
+ //list<AddonInfo> getInfoByPlugin(1: PluginName plugin),
+
+ map<PluginName, list<AddonService>> getAddonHandler(),
+ bool hasAddonHandler(1: PluginName plugin, 2: string func),
+
+ void callAddon(1: PluginName plugin, 2: string func, 3: list<JSONString> arguments)
+ throws (1: ServiceDoesNotExists e, 2: ServiceException ex),
+
+ // special variant of callAddon that works on the media types, acccepting integer
+ void callAddonHandler(1: PluginName plugin, 2: string func, 3: PackageID pid_or_fid)
+ throws (1: ServiceDoesNotExists e, 2: ServiceException ex),
+
+
+ //scheduler
+
+ // TODO
+
+}
diff --git a/pyload/remote/ttypes.py b/pyload/remote/ttypes.py
new file mode 100644
index 000000000..1f91403d5
--- /dev/null
+++ b/pyload/remote/ttypes.py
@@ -0,0 +1,534 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Autogenerated by pyload
+# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
+
+class BaseObject(object):
+ __slots__ = []
+
+ def __str__(self):
+ return "<%s %s>" % (self.__class__.__name__, ", ".join("%s=%s" % (k,getattr(self,k)) for k in self.__slots__))
+
+class ExceptionObject(Exception):
+ __slots__ = []
+
+class DownloadState:
+ All = 0
+ Finished = 1
+ Unfinished = 2
+ Failed = 3
+ Unmanaged = 4
+
+class DownloadStatus:
+ NA = 0
+ Offline = 1
+ Online = 2
+ Queued = 3
+ Paused = 4
+ Finished = 5
+ Skipped = 6
+ Failed = 7
+ Starting = 8
+ Waiting = 9
+ Downloading = 10
+ TempOffline = 11
+ Aborted = 12
+ Decrypting = 13
+ Processing = 14
+ Custom = 15
+ Unknown = 16
+
+class FileStatus:
+ Ok = 0
+ Missing = 1
+ Remote = 2
+
+class Input:
+ NA = 0
+ Text = 1
+ Int = 2
+ File = 3
+ Folder = 4
+ Textbox = 5
+ Password = 6
+ Bool = 7
+ Click = 8
+ Select = 9
+ Multiple = 10
+ List = 11
+ Table = 12
+
+class MediaType:
+ All = 0
+ Other = 1
+ Audio = 2
+ Image = 4
+ Video = 8
+ Document = 16
+ Archive = 32
+
+class Output:
+ All = 0
+ Notification = 1
+ Captcha = 2
+ Query = 4
+
+class PackageStatus:
+ Ok = 0
+ Paused = 1
+ Folder = 2
+ Remote = 3
+
+class Permission:
+ All = 0
+ Add = 1
+ Delete = 2
+ Modify = 4
+ Download = 8
+ Accounts = 16
+ Interaction = 32
+ Plugins = 64
+
+class Role:
+ Admin = 0
+ User = 1
+
+class AccountInfo(BaseObject):
+ __slots__ = ['plugin', 'loginname', 'owner', 'valid', 'validuntil', 'trafficleft', 'maxtraffic', 'premium', 'activated', 'shared', 'options']
+
+ def __init__(self, plugin=None, loginname=None, owner=None, valid=None, validuntil=None, trafficleft=None, maxtraffic=None, premium=None, activated=None, shared=None, options=None):
+ self.plugin = plugin
+ self.loginname = loginname
+ self.owner = owner
+ self.valid = valid
+ self.validuntil = validuntil
+ self.trafficleft = trafficleft
+ self.maxtraffic = maxtraffic
+ self.premium = premium
+ self.activated = activated
+ self.shared = shared
+ self.options = options
+
+class AddonInfo(BaseObject):
+ __slots__ = ['func_name', 'description', 'value']
+
+ def __init__(self, func_name=None, description=None, value=None):
+ self.func_name = func_name
+ self.description = description
+ self.value = value
+
+class AddonService(BaseObject):
+ __slots__ = ['func_name', 'description', 'arguments', 'media']
+
+ def __init__(self, func_name=None, description=None, arguments=None, media=None):
+ self.func_name = func_name
+ self.description = description
+ self.arguments = arguments
+ self.media = media
+
+class ConfigHolder(BaseObject):
+ __slots__ = ['name', 'label', 'description', 'long_description', 'items', 'info', 'handler']
+
+ def __init__(self, name=None, label=None, description=None, long_description=None, items=None, info=None, handler=None):
+ self.name = name
+ self.label = label
+ self.description = description
+ self.long_description = long_description
+ self.items = items
+ self.info = info
+ self.handler = handler
+
+class ConfigInfo(BaseObject):
+ __slots__ = ['name', 'label', 'description', 'category', 'user_context', 'activated']
+
+ def __init__(self, name=None, label=None, description=None, category=None, user_context=None, activated=None):
+ self.name = name
+ self.label = label
+ self.description = description
+ self.category = category
+ self.user_context = user_context
+ self.activated = activated
+
+class ConfigItem(BaseObject):
+ __slots__ = ['name', 'label', 'description', 'type', 'default_value', 'value']
+
+ def __init__(self, name=None, label=None, description=None, type=None, default_value=None, value=None):
+ self.name = name
+ self.label = label
+ self.description = description
+ self.type = type
+ self.default_value = default_value
+ self.value = value
+
+class DownloadInfo(BaseObject):
+ __slots__ = ['url', 'plugin', 'hash', 'status', 'statusmsg', 'error']
+
+ def __init__(self, url=None, plugin=None, hash=None, status=None, statusmsg=None, error=None):
+ self.url = url
+ self.plugin = plugin
+ self.hash = hash
+ self.status = status
+ self.statusmsg = statusmsg
+ self.error = error
+
+class DownloadProgress(BaseObject):
+ __slots__ = ['fid', 'pid', 'speed', 'status']
+
+ def __init__(self, fid=None, pid=None, speed=None, status=None):
+ self.fid = fid
+ self.pid = pid
+ self.speed = speed
+ self.status = status
+
+class EventInfo(BaseObject):
+ __slots__ = ['eventname', 'event_args']
+
+ def __init__(self, eventname=None, event_args=None):
+ self.eventname = eventname
+ self.event_args = event_args
+
+class FileDoesNotExists(ExceptionObject):
+ __slots__ = ['fid']
+
+ def __init__(self, fid=None):
+ self.fid = fid
+
+class FileInfo(BaseObject):
+ __slots__ = ['fid', 'name', 'package', 'owner', 'size', 'status', 'media', 'added', 'fileorder', 'download']
+
+ def __init__(self, fid=None, name=None, package=None, owner=None, size=None, status=None, media=None, added=None, fileorder=None, download=None):
+ self.fid = fid
+ self.name = name
+ self.package = package
+ self.owner = owner
+ self.size = size
+ self.status = status
+ self.media = media
+ self.added = added
+ self.fileorder = fileorder
+ self.download = download
+
+class Forbidden(ExceptionObject):
+ pass
+
+class InteractionTask(BaseObject):
+ __slots__ = ['iid', 'input', 'data', 'output', 'default_value', 'title', 'description', 'plugin']
+
+ def __init__(self, iid=None, input=None, data=None, output=None, default_value=None, title=None, description=None, plugin=None):
+ self.iid = iid
+ self.input = input
+ self.data = data
+ self.output = output
+ self.default_value = default_value
+ self.title = title
+ self.description = description
+ self.plugin = plugin
+
+class InvalidConfigSection(ExceptionObject):
+ __slots__ = ['section']
+
+ def __init__(self, section=None):
+ self.section = section
+
+class LinkStatus(BaseObject):
+ __slots__ = ['url', 'name', 'plugin', 'size', 'status', 'packagename']
+
+ def __init__(self, url=None, name=None, plugin=None, size=None, status=None, packagename=None):
+ self.url = url
+ self.name = name
+ self.plugin = plugin
+ self.size = size
+ self.status = status
+ self.packagename = packagename
+
+class OnlineCheck(BaseObject):
+ __slots__ = ['rid', 'data']
+
+ def __init__(self, rid=None, data=None):
+ self.rid = rid
+ self.data = data
+
+class PackageDoesNotExists(ExceptionObject):
+ __slots__ = ['pid']
+
+ def __init__(self, pid=None):
+ self.pid = pid
+
+class PackageInfo(BaseObject):
+ __slots__ = ['pid', 'name', 'folder', 'root', 'owner', 'site', 'comment', 'password', 'added', 'tags', 'status', 'shared', 'packageorder', 'stats', 'fids', 'pids']
+
+ def __init__(self, pid=None, name=None, folder=None, root=None, owner=None, site=None, comment=None, password=None, added=None, tags=None, status=None, shared=None, packageorder=None, stats=None, fids=None, pids=None):
+ self.pid = pid
+ self.name = name
+ self.folder = folder
+ self.root = root
+ self.owner = owner
+ self.site = site
+ self.comment = comment
+ self.password = password
+ self.added = added
+ self.tags = tags
+ self.status = status
+ self.shared = shared
+ self.packageorder = packageorder
+ self.stats = stats
+ self.fids = fids
+ self.pids = pids
+
+class PackageStats(BaseObject):
+ __slots__ = ['linkstotal', 'linksdone', 'sizetotal', 'sizedone']
+
+ def __init__(self, linkstotal=None, linksdone=None, sizetotal=None, sizedone=None):
+ self.linkstotal = linkstotal
+ self.linksdone = linksdone
+ self.sizetotal = sizetotal
+ self.sizedone = sizedone
+
+class ProgressInfo(BaseObject):
+ __slots__ = ['plugin', 'name', 'statusmsg', 'eta', 'done', 'total', 'download']
+
+ def __init__(self, plugin=None, name=None, statusmsg=None, eta=None, done=None, total=None, download=None):
+ self.plugin = plugin
+ self.name = name
+ self.statusmsg = statusmsg
+ self.eta = eta
+ self.done = done
+ self.total = total
+ self.download = download
+
+class ServerStatus(BaseObject):
+ __slots__ = ['queuedDownloads', 'totalDownloads', 'speed', 'pause', 'download', 'reconnect']
+
+ def __init__(self, queuedDownloads=None, totalDownloads=None, speed=None, pause=None, download=None, reconnect=None):
+ self.queuedDownloads = queuedDownloads
+ self.totalDownloads = totalDownloads
+ self.speed = speed
+ self.pause = pause
+ self.download = download
+ self.reconnect = reconnect
+
+class ServiceDoesNotExists(ExceptionObject):
+ __slots__ = ['plugin', 'func']
+
+ def __init__(self, plugin=None, func=None):
+ self.plugin = plugin
+ self.func = func
+
+class ServiceException(ExceptionObject):
+ __slots__ = ['msg']
+
+ def __init__(self, msg=None):
+ self.msg = msg
+
+class TreeCollection(BaseObject):
+ __slots__ = ['root', 'files', 'packages']
+
+ def __init__(self, root=None, files=None, packages=None):
+ self.root = root
+ self.files = files
+ self.packages = packages
+
+class Unauthorized(ExceptionObject):
+ pass
+
+class UserData(BaseObject):
+ __slots__ = ['uid', 'name', 'email', 'role', 'permission', 'folder', 'traffic', 'dllimit', 'dlquota', 'hddquota', 'user', 'templateName']
+
+ def __init__(self, uid=None, name=None, email=None, role=None, permission=None, folder=None, traffic=None, dllimit=None, dlquota=None, hddquota=None, user=None, templateName=None):
+ self.uid = uid
+ self.name = name
+ self.email = email
+ self.role = role
+ self.permission = permission
+ self.folder = folder
+ self.traffic = traffic
+ self.dllimit = dllimit
+ self.dlquota = dlquota
+ self.hddquota = hddquota
+ self.user = user
+ self.templateName = templateName
+
+class UserDoesNotExists(ExceptionObject):
+ __slots__ = ['user']
+
+ def __init__(self, user=None):
+ self.user = user
+
+class Iface(object):
+ def addFromCollector(self, name, paused):
+ pass
+ def addLinks(self, pid, links):
+ pass
+ def addLocalFile(self, pid, name, path):
+ pass
+ def addPackage(self, name, links, password):
+ pass
+ def addPackageChild(self, name, links, password, root, paused):
+ pass
+ def addPackageP(self, name, links, password, paused):
+ pass
+ def addToCollector(self, links):
+ pass
+ def addUser(self, username, password):
+ pass
+ def callAddon(self, plugin, func, arguments):
+ pass
+ def callAddonHandler(self, plugin, func, pid_or_fid):
+ pass
+ def checkOnlineStatus(self, urls):
+ pass
+ def checkOnlineStatusContainer(self, urls, filename, data):
+ pass
+ def checkURLs(self, urls):
+ pass
+ def configurePlugin(self, plugin):
+ pass
+ def createPackage(self, name, folder, root, password, site, comment, paused):
+ pass
+ def deleteCollLink(self, url):
+ pass
+ def deleteCollPack(self, name):
+ pass
+ def deleteConfig(self, plugin):
+ pass
+ def deleteFiles(self, fids):
+ pass
+ def deletePackages(self, pids):
+ pass
+ def findFiles(self, pattern):
+ pass
+ def findPackages(self, tags):
+ pass
+ def freeSpace(self):
+ pass
+ def generateAndAddPackages(self, links, paused):
+ pass
+ def generateDownloadLink(self, fid, timeout):
+ pass
+ def generatePackages(self, links):
+ pass
+ def getAccountTypes(self):
+ pass
+ def getAccounts(self, refresh):
+ pass
+ def getAddonHandler(self):
+ pass
+ def getAllFiles(self):
+ pass
+ def getAllUserData(self):
+ pass
+ def getAutocompletion(self, pattern):
+ pass
+ def getAvailablePlugins(self):
+ pass
+ def getCollector(self):
+ pass
+ def getConfig(self):
+ pass
+ def getConfigValue(self, section, option):
+ pass
+ def getCoreConfig(self):
+ pass
+ def getEvents(self, uuid):
+ pass
+ def getFileInfo(self, fid):
+ pass
+ def getFileTree(self, pid, full):
+ pass
+ def getFilteredFileTree(self, pid, full, state):
+ pass
+ def getFilteredFiles(self, state):
+ pass
+ def getInteractionTask(self, mode):
+ pass
+ def getLog(self, offset):
+ pass
+ def getNotifications(self):
+ pass
+ def getPackageContent(self, pid):
+ pass
+ def getPackageInfo(self, pid):
+ pass
+ def getPluginConfig(self):
+ pass
+ def getProgressInfo(self):
+ pass
+ def getServerStatus(self):
+ pass
+ def getServerVersion(self):
+ pass
+ def getUserData(self):
+ pass
+ def getWSAddress(self):
+ pass
+ def hasAddonHandler(self, plugin, func):
+ pass
+ def isInteractionWaiting(self, mode):
+ pass
+ def login(self, username, password):
+ pass
+ def moveFiles(self, fids, pid):
+ pass
+ def movePackage(self, pid, root):
+ pass
+ def orderFiles(self, fids, pid, position):
+ pass
+ def orderPackage(self, pids, position):
+ pass
+ def parseURLs(self, html, url):
+ pass
+ def pauseServer(self):
+ pass
+ def pollResults(self, rid):
+ pass
+ def quit(self):
+ pass
+ def recheckPackage(self, pid):
+ pass
+ def removeAccount(self, plugin, account):
+ pass
+ def removeUser(self, uid):
+ pass
+ def renameCollPack(self, name, new_name):
+ pass
+ def restart(self):
+ pass
+ def restartFailed(self):
+ pass
+ def restartFile(self, fid):
+ pass
+ def restartPackage(self, pid):
+ pass
+ def saveConfig(self, config):
+ pass
+ def setConfigHandler(self, plugin, iid, value):
+ pass
+ def setConfigValue(self, section, option, value):
+ pass
+ def setInteractionResult(self, iid, result):
+ pass
+ def setPackageFolder(self, pid, path):
+ pass
+ def setPassword(self, username, old_password, new_password):
+ pass
+ def stopAllDownloads(self):
+ pass
+ def stopDownloads(self, fids):
+ pass
+ def togglePause(self):
+ pass
+ def toggleReconnect(self):
+ pass
+ def unpauseServer(self):
+ pass
+ def updateAccount(self, plugin, account, password):
+ pass
+ def updateAccountInfo(self, account):
+ pass
+ def updatePackage(self, pack):
+ pass
+ def updateUserData(self, data):
+ pass
+ def uploadContainer(self, filename, data):
+ pass
+
diff --git a/pyload/remote/wsbackend/AbstractHandler.py b/pyload/remote/wsbackend/AbstractHandler.py
new file mode 100644
index 000000000..8012d6cd8
--- /dev/null
+++ b/pyload/remote/wsbackend/AbstractHandler.py
@@ -0,0 +1,133 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+###############################################################################
+# Copyright(c) 2008-2012 pyLoad Team
+# http://www.pyload.org
+#
+# This file is part of pyLoad.
+# pyLoad is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Subjected to the terms and conditions in LICENSE
+#
+# @author: RaNaN
+###############################################################################
+
+from mod_pywebsocket.msgutil import send_message
+from mod_pywebsocket.util import get_class_logger
+from pyload.remote.json_converter import loads, dumps
+
+
+class AbstractHandler:
+ """
+ Abstract Handler providing common methods shared across WebSocket handlers
+ """
+ PATH = "/"
+
+ OK = 200
+ BAD_REQUEST = 400
+ UNAUTHORIZED = 401
+ FORBIDDEN = 403
+ NOT_FOUND = 404
+ ERROR = 500
+
+ def __init__(self, api):
+ self.log = get_class_logger()
+ self.api = api
+ self.core = api.core
+
+ def do_extra_handshake(self, req):
+ self.log.debug("WS Connected: %s" % req)
+ req.api = None #when api is set client is logged in
+
+ # allow login via session when webinterface is active
+ if self.core.config['webinterface']['activated']:
+ cookie = req.headers_in.getheader('Cookie')
+ s = self.load_session(cookie)
+ if s:
+ uid = s.get('uid', None)
+ req.api = self.api.withUserContext(uid)
+ self.log.debug("WS authenticated user with cookie: %d" % uid)
+
+ self.on_open(req)
+
+ def on_open(self, req):
+ pass
+
+ def load_session(self, cookies):
+ from Cookie import SimpleCookie
+ from beaker.session import Session
+ from pyload.web.webinterface import session
+
+ cookies = SimpleCookie(cookies)
+ sid = cookies.get(session.options['key'])
+ if not sid:
+ return None
+
+ s = Session({}, use_cookies=False, id=sid.value, **session.options)
+ if s.is_new:
+ return None
+
+ return s
+
+ def passive_closing_handshake(self, req):
+ self.log.debug("WS Closed: %s" % req)
+ self.on_close(req)
+
+ def on_close(self, req):
+ pass
+
+ def transfer_data(self, req):
+ raise NotImplemented
+
+ def handle_call(self, msg, req):
+ """ Parses the msg for an argument call. If func is null an response was already sent.
+
+ :return: func, args, kwargs
+ """
+ try:
+ o = loads(msg)
+ except ValueError, e: #invalid json object
+ self.log.debug("Invalid Request: %s" % e)
+ self.send_result(req, self.ERROR, "No JSON request")
+ return None, None, None
+
+ if not isinstance(o, basestring) and type(o) != list and len(o) not in range(1, 4):
+ self.log.debug("Invalid Api call: %s" % o)
+ self.send_result(req, self.ERROR, "Invalid Api call")
+ return None, None, None
+
+ # called only with name, no args
+ if isinstance(o, basestring):
+ return o, [], {}
+ elif len(o) == 1: # arguments omitted
+ return o[0], [], {}
+ elif len(o) == 2:
+ func, args = o
+ if type(args) == list:
+ return func, args, {}
+ else:
+ return func, [], args
+ else:
+ return tuple(o)
+
+ def do_login(self, req, args, kwargs):
+ user = self.api.checkAuth(*args, **kwargs)
+ if user:
+ req.api = self.api.withUserContext(user.uid)
+ return self.send_result(req, self.OK, True)
+ else:
+ return self.send_result(req, self.FORBIDDEN, "Forbidden")
+
+ def do_logout(self, req):
+ req.api = None
+ return self.send_result(req, self.OK, True)
+
+ def send_result(self, req, code, result):
+ return send_message(req, dumps([code, result]))
+
+ def send(self, req, obj):
+ return send_message(req, dumps(obj)) \ No newline at end of file
diff --git a/pyload/remote/wsbackend/ApiHandler.py b/pyload/remote/wsbackend/ApiHandler.py
new file mode 100644
index 000000000..4685121d4
--- /dev/null
+++ b/pyload/remote/wsbackend/ApiHandler.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+###############################################################################
+# Copyright(c) 2008-2012 pyLoad Team
+# http://www.pyload.org
+#
+# This file is part of pyLoad.
+# pyLoad is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Subjected to the terms and conditions in LICENSE
+#
+# @author: RaNaN
+###############################################################################
+
+from mod_pywebsocket.msgutil import receive_message
+
+from pyload.Api import ExceptionObject
+
+from AbstractHandler import AbstractHandler
+
+class ApiHandler(AbstractHandler):
+ """Provides access to the API.
+
+ Send your request as json encoded string in the following manner:
+ ["function", [*args]] or ["function", {**kwargs}]
+
+ the result will be:
+
+ [code, result]
+
+ Don't forget to login first.
+ Non json request will be ignored.
+ """
+
+ PATH = "/api"
+
+ def transfer_data(self, req):
+ while True:
+ try:
+ line = receive_message(req)
+ except TypeError, e: # connection closed
+ self.log.debug("WS Error: %s" % e)
+ return self.passive_closing_handshake(req)
+
+ self.handle_message(line, req)
+
+ def handle_message(self, msg, req):
+
+ func, args, kwargs = self.handle_call(msg, req)
+ if not func:
+ return # handle_call already sent the result
+
+ if func == 'login':
+ return self.do_login(req, args, kwargs)
+ elif func == 'logout':
+ return self.do_logout(req)
+ else:
+ if not req.api:
+ return self.send_result(req, self.FORBIDDEN, "Forbidden")
+
+ if not self.api.isAuthorized(func, req.api.user):
+ return self.send_result(req, self.UNAUTHORIZED, "Unauthorized")
+
+ try:
+ result = getattr(req.api, func)(*args, **kwargs)
+ except ExceptionObject, e:
+ return self.send_result(req, self.BAD_REQUEST, e)
+ except AttributeError:
+ return self.send_result(req, self.NOT_FOUND, "Not Found")
+ except Exception, e:
+ self.core.print_exc()
+ return self.send_result(req, self.ERROR, str(e))
+
+ # None is invalid json type
+ if result is None: result = True
+
+ return self.send_result(req, self.OK, result) \ No newline at end of file
diff --git a/pyload/remote/wsbackend/AsyncHandler.py b/pyload/remote/wsbackend/AsyncHandler.py
new file mode 100644
index 000000000..88bd371b0
--- /dev/null
+++ b/pyload/remote/wsbackend/AsyncHandler.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+###############################################################################
+# Copyright(c) 2008-2012 pyLoad Team
+# http://www.pyload.org
+#
+# This file is part of pyLoad.
+# pyLoad is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Subjected to the terms and conditions in LICENSE
+#
+# @author: RaNaN
+###############################################################################
+
+import re
+from Queue import Queue, Empty
+from threading import Lock
+from time import time
+
+from mod_pywebsocket.msgutil import receive_message
+
+from pyload.Api import EventInfo, Interaction
+from pyload.utils import lock
+from AbstractHandler import AbstractHandler
+
+class Mode:
+ STANDBY = 1
+ RUNNING = 2
+
+class AsyncHandler(AbstractHandler):
+ """
+ Handler that provides asynchronous information about server status, running downloads, occurred events.
+
+ Progress information are continuous and will be pushed in a fixed interval when available.
+ After connect you have to login and can set the interval by sending the json command ["setInterval", xy].
+ To start receiving updates call "start", afterwards no more incoming messages will be accepted!
+ """
+
+ PATH = "/async"
+ COMMAND = "start"
+
+ PROGRESS_INTERVAL = 2
+ EVENT_PATTERN = re.compile(r"^(package|file|interaction)", re.I)
+ INTERACTION = Interaction.All
+
+ def __init__(self, api):
+ AbstractHandler.__init__(self, api)
+ self.clients = []
+ self.lock = Lock()
+
+ self.core.evm.addEvent("event", self.add_event)
+
+ @lock
+ def on_open(self, req):
+ req.queue = Queue()
+ req.interval = self.PROGRESS_INTERVAL
+ req.events = self.EVENT_PATTERN
+ req.interaction = self.INTERACTION
+ req.mode = Mode.STANDBY
+ req.t = time() # time when update should be pushed
+ self.clients.append(req)
+
+ @lock
+ def on_close(self, req):
+ try:
+ del req.queue
+ self.clients.remove(req)
+ except ValueError: # ignore when not in list
+ pass
+
+ @lock
+ def add_event(self, event, *args):
+ # Convert arguments to json suited instance
+ event = EventInfo(event, [x.toInfoData() if hasattr(x, 'toInfoData') else x for x in args])
+
+ for req in self.clients:
+ # Not logged in yet
+ if not req.api: continue
+
+ # filter events that these user is no owner of
+ # TODO: events are security critical, this should be revised later
+ # TODO: permissions? interaction etc
+ if not req.api.user.isAdmin():
+ skip = False
+ for arg in args:
+ if hasattr(arg, 'owner') and arg.owner != req.api.primaryUID:
+ skip = True
+ break
+
+ # user should not get this event
+ if skip: break
+
+ if req.events.search(event.eventname):
+ self.log.debug("Pushing event %s" % event)
+ req.queue.put(event)
+
+ def transfer_data(self, req):
+ while True:
+
+ if req.mode == Mode.STANDBY:
+ try:
+ line = receive_message(req)
+ except TypeError, e: # connection closed
+ self.log.debug("WS Error: %s" % e)
+ return self.passive_closing_handshake(req)
+
+ self.mode_standby(line, req)
+ else:
+ if self.mode_running(req):
+ return self.passive_closing_handshake(req)
+
+ def mode_standby(self, msg, req):
+ """ accepts calls before pushing updates """
+ func, args, kwargs = self.handle_call(msg, req)
+ if not func:
+ return # Result was already sent
+
+ if func == 'login':
+ return self.do_login(req, args, kwargs)
+
+ elif func == 'logout':
+ return self.do_logout(req)
+
+ else:
+ if not req.api:
+ return self.send_result(req, self.FORBIDDEN, "Forbidden")
+
+ if func == "setInterval":
+ req.interval = args[0]
+ elif func == "setEvents":
+ req.events = re.compile(args[0], re.I)
+ elif func == "setInteraction":
+ req.interaction = args[0]
+ elif func == self.COMMAND:
+ req.mode = Mode.RUNNING
+
+
+ def mode_running(self, req):
+ """ Listen for events, closes socket when returning True """
+ try:
+ # block length of update interval if necessary
+ ev = req.queue.get(True, req.interval)
+ try:
+ self.send(req, ev)
+ except TypeError:
+ self.log.debug("Event %s not converted" % ev)
+ ev.event_args = []
+ # Resend the event without arguments
+ self.send(req, ev)
+
+ except Empty:
+ pass
+
+ if req.t <= time():
+ # TODO: server status is not enough
+ # modify core api to include progress? think of other needed information to show
+ # eta is quite wrong currently
+ # notifications
+ self.send(req, self.api.getServerStatus())
+ self.send(req, self.api.getProgressInfo())
+
+ # update time for next update
+ req.t = time() + req.interval \ No newline at end of file
diff --git a/pyload/remote/wsbackend/Dispatcher.py b/pyload/remote/wsbackend/Dispatcher.py
new file mode 100644
index 000000000..44cc7555e
--- /dev/null
+++ b/pyload/remote/wsbackend/Dispatcher.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+###############################################################################
+# Copyright(c) 2008-2012 pyLoad Team
+# http://www.pyload.org
+#
+# This file is part of pyLoad.
+# pyLoad is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Subjected to the terms and conditions in LICENSE
+#
+# @author: RaNaN
+###############################################################################
+
+from mod_pywebsocket import util
+from mod_pywebsocket.dispatch import Dispatcher as BaseDispatcher
+
+class Dispatcher(BaseDispatcher):
+
+ def __init__(self):
+ self._logger = util.get_class_logger(self)
+
+ self._handler_suite_map = {}
+ self._source_warnings = []
+
+ def addHandler(self, path, handler):
+ self._handler_suite_map[path] = handler \ No newline at end of file
diff --git a/pyload/remote/wsbackend/Server.py b/pyload/remote/wsbackend/Server.py
new file mode 100644
index 000000000..af5e1cf19
--- /dev/null
+++ b/pyload/remote/wsbackend/Server.py
@@ -0,0 +1,733 @@
+#!/usr/bin/env python
+#
+# Copyright 2012, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+# A copy of standalone.py with uneeded stuff removed
+# some logging methods removed
+# Added api attribute to request
+
+import BaseHTTPServer
+import CGIHTTPServer
+import SocketServer
+import httplib
+import logging
+import os
+import re
+import select
+import socket
+import sys
+import threading
+
+_HAS_SSL = False
+_HAS_OPEN_SSL = False
+
+from mod_pywebsocket import common
+from mod_pywebsocket import dispatch
+from mod_pywebsocket import handshake
+from mod_pywebsocket import http_header_util
+from mod_pywebsocket import memorizingfile
+from mod_pywebsocket import util
+
+
+_DEFAULT_LOG_MAX_BYTES = 1024 * 256
+_DEFAULT_LOG_BACKUP_COUNT = 5
+
+_DEFAULT_REQUEST_QUEUE_SIZE = 128
+
+# 1024 is practically large enough to contain WebSocket handshake lines.
+_MAX_MEMORIZED_LINES = 1024
+
+def import_ssl():
+ global _HAS_SSL, _HAS_OPEN_SSL
+ try:
+ import ssl
+ _HAS_SSL = True
+ except ImportError:
+ try:
+ import OpenSSL.SSL
+ _HAS_OPEN_SSL = True
+ except ImportError:
+ pass
+
+
+class _StandaloneConnection(object):
+ """Mimic mod_python mp_conn."""
+
+ def __init__(self, request_handler):
+ """Construct an instance.
+
+ Args:
+ request_handler: A WebSocketRequestHandler instance.
+ """
+
+ self._request_handler = request_handler
+
+ def get_local_addr(self):
+ """Getter to mimic mp_conn.local_addr."""
+
+ return (self._request_handler.server.server_name,
+ self._request_handler.server.server_port)
+ local_addr = property(get_local_addr)
+
+ def get_remote_addr(self):
+ """Getter to mimic mp_conn.remote_addr.
+
+ Setting the property in __init__ won't work because the request
+ handler is not initialized yet there."""
+
+ return self._request_handler.client_address
+ remote_addr = property(get_remote_addr)
+
+ def write(self, data):
+ """Mimic mp_conn.write()."""
+
+ return self._request_handler.wfile.write(data)
+
+ def read(self, length):
+ """Mimic mp_conn.read()."""
+
+ return self._request_handler.rfile.read(length)
+
+ def get_memorized_lines(self):
+ """Get memorized lines."""
+
+ return self._request_handler.rfile.get_memorized_lines()
+
+
+class _StandaloneRequest(object):
+ """Mimic mod_python request."""
+
+ def __init__(self, request_handler, use_tls):
+ """Construct an instance.
+
+ Args:
+ request_handler: A WebSocketRequestHandler instance.
+ """
+
+ self._logger = util.get_class_logger(self)
+
+ self._request_handler = request_handler
+ self.connection = _StandaloneConnection(request_handler)
+ self._use_tls = use_tls
+ self.headers_in = request_handler.headers
+
+ def get_uri(self):
+ """Getter to mimic request.uri."""
+
+ return self._request_handler.path
+ uri = property(get_uri)
+
+ def get_method(self):
+ """Getter to mimic request.method."""
+
+ return self._request_handler.command
+ method = property(get_method)
+
+ def get_protocol(self):
+ """Getter to mimic request.protocol."""
+
+ return self._request_handler.request_version
+ protocol = property(get_protocol)
+
+ def is_https(self):
+ """Mimic request.is_https()."""
+
+ return self._use_tls
+
+ def _drain_received_data(self):
+ """Don't use this method from WebSocket handler. Drains unread data
+ in the receive buffer.
+ """
+
+ raw_socket = self._request_handler.connection
+ drained_data = util.drain_received_data(raw_socket)
+
+ if drained_data:
+ self._logger.debug(
+ 'Drained data following close frame: %r', drained_data)
+
+
+class _StandaloneSSLConnection(object):
+ """A wrapper class for OpenSSL.SSL.Connection to provide makefile method
+ which is not supported by the class.
+ """
+
+ def __init__(self, connection):
+ self._connection = connection
+
+ def __getattribute__(self, name):
+ if name in ('_connection', 'makefile'):
+ return object.__getattribute__(self, name)
+ return self._connection.__getattribute__(name)
+
+ def __setattr__(self, name, value):
+ if name in ('_connection', 'makefile'):
+ return object.__setattr__(self, name, value)
+ return self._connection.__setattr__(name, value)
+
+ def makefile(self, mode='r', bufsize=-1):
+ return socket._fileobject(self._connection, mode, bufsize)
+
+
+def _alias_handlers(dispatcher, websock_handlers_map_file):
+ """Set aliases specified in websock_handler_map_file in dispatcher.
+
+ Args:
+ dispatcher: dispatch.Dispatcher instance
+ websock_handler_map_file: alias map file
+ """
+
+ fp = open(websock_handlers_map_file)
+ try:
+ for line in fp:
+ if line[0] == '#' or line.isspace():
+ continue
+ m = re.match('(\S+)\s+(\S+)', line)
+ if not m:
+ logging.warning('Wrong format in map file:' + line)
+ continue
+ try:
+ dispatcher.add_resource_path_alias(
+ m.group(1), m.group(2))
+ except dispatch.DispatchException, e:
+ logging.error(str(e))
+ finally:
+ fp.close()
+
+
+class WebSocketServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
+ """HTTPServer specialized for WebSocket."""
+
+ # Overrides SocketServer.ThreadingMixIn.daemon_threads
+ daemon_threads = True
+ # Overrides BaseHTTPServer.HTTPServer.allow_reuse_address
+ allow_reuse_address = True
+
+ def __init__(self, options):
+ """Override SocketServer.TCPServer.__init__ to set SSL enabled
+ socket object to self.socket before server_bind and server_activate,
+ if necessary.
+ """
+ # Removed dispatcher init here
+ self._logger = logging.getLogger("log")
+
+ self.request_queue_size = options.request_queue_size
+ self.__ws_is_shut_down = threading.Event()
+ self.__ws_serving = False
+
+ SocketServer.BaseServer.__init__(
+ self, (options.server_host, options.port), WebSocketRequestHandler)
+
+ # Expose the options object to allow handler objects access it. We name
+ # it with websocket_ prefix to avoid conflict.
+ self.websocket_server_options = options
+
+ self._create_sockets()
+ self.server_bind()
+ self.server_activate()
+
+ def _create_sockets(self):
+ self.server_name, self.server_port = self.server_address
+ self._sockets = []
+ if not self.server_name:
+ # On platforms that doesn't support IPv6, the first bind fails.
+ # On platforms that supports IPv6
+ # - If it binds both IPv4 and IPv6 on call with AF_INET6, the
+ # first bind succeeds and the second fails (we'll see 'Address
+ # already in use' error).
+ # - If it binds only IPv6 on call with AF_INET6, both call are
+ # expected to succeed to listen both protocol.
+ addrinfo_array = [
+ (socket.AF_INET6, socket.SOCK_STREAM, '', '', ''),
+ (socket.AF_INET, socket.SOCK_STREAM, '', '', '')]
+ else:
+ addrinfo_array = socket.getaddrinfo(self.server_name,
+ self.server_port,
+ socket.AF_UNSPEC,
+ socket.SOCK_STREAM,
+ socket.IPPROTO_TCP)
+ for addrinfo in addrinfo_array:
+ family, socktype, proto, canonname, sockaddr = addrinfo
+ try:
+ socket_ = socket.socket(family, socktype)
+ except Exception, e:
+ self._logger.info('Skip by failure: %r', e)
+ continue
+ if self.websocket_server_options.use_tls:
+ if _HAS_SSL:
+ if self.websocket_server_options.tls_client_auth:
+ client_cert_ = ssl.CERT_REQUIRED
+ else:
+ client_cert_ = ssl.CERT_NONE
+ socket_ = ssl.wrap_socket(socket_,
+ keyfile=self.websocket_server_options.private_key,
+ certfile=self.websocket_server_options.certificate,
+ ssl_version=ssl.PROTOCOL_SSLv23,
+ ca_certs=self.websocket_server_options.tls_client_ca,
+ cert_reqs=client_cert_)
+ if _HAS_OPEN_SSL:
+ ctx = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+ ctx.use_privatekey_file(
+ self.websocket_server_options.private_key)
+ ctx.use_certificate_file(
+ self.websocket_server_options.certificate)
+ socket_ = OpenSSL.SSL.Connection(ctx, socket_)
+ self._sockets.append((socket_, addrinfo))
+
+ def server_bind(self):
+ """Override SocketServer.TCPServer.server_bind to enable multiple
+ sockets bind.
+ """
+
+ failed_sockets = []
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ if self.allow_reuse_address:
+ socket_.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ try:
+ socket_.bind(self.server_address)
+ except Exception, e:
+ self._logger.info('Skip by failure: %r', e)
+ socket_.close()
+ failed_sockets.append(socketinfo)
+ if self.server_address[1] == 0:
+ # The operating system assigns the actual port number for port
+ # number 0. This case, the second and later sockets should use
+ # the same port number. Also self.server_port is rewritten
+ # because it is exported, and will be used by external code.
+ self.server_address = (
+ self.server_name, socket_.getsockname()[1])
+ self.server_port = self.server_address[1]
+ self._logger.info('Port %r is assigned', self.server_port)
+
+ for socketinfo in failed_sockets:
+ self._sockets.remove(socketinfo)
+
+ def server_activate(self):
+ """Override SocketServer.TCPServer.server_activate to enable multiple
+ sockets listen.
+ """
+
+ failed_sockets = []
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.debug('Listen on: %r', addrinfo)
+ try:
+ socket_.listen(self.request_queue_size)
+ except Exception, e:
+ self._logger.info('Skip by failure: %r', e)
+ socket_.close()
+ failed_sockets.append(socketinfo)
+
+ for socketinfo in failed_sockets:
+ self._sockets.remove(socketinfo)
+
+ if len(self._sockets) == 0:
+ self._logger.critical(
+ 'No sockets activated. Use info log level to see the reason.')
+
+ def server_close(self):
+ """Override SocketServer.TCPServer.server_close to enable multiple
+ sockets close.
+ """
+
+ for socketinfo in self._sockets:
+ socket_, addrinfo = socketinfo
+ self._logger.info('Close on: %r', addrinfo)
+ socket_.close()
+
+ def fileno(self):
+ """Override SocketServer.TCPServer.fileno."""
+
+ self._logger.critical('Not supported: fileno')
+ return self._sockets[0][0].fileno()
+
+ def handle_error(self, rquest, client_address):
+ """Override SocketServer.handle_error."""
+
+ self._logger.error(
+ 'Exception in processing request from: %r\n%s',
+ client_address,
+ util.get_stack_trace())
+ # Note: client_address is a tuple.
+
+ def get_request(self):
+ """Override TCPServer.get_request to wrap OpenSSL.SSL.Connection
+ object with _StandaloneSSLConnection to provide makefile method. We
+ cannot substitute OpenSSL.SSL.Connection.makefile since it's readonly
+ attribute.
+ """
+
+ accepted_socket, client_address = self.socket.accept()
+ if self.websocket_server_options.use_tls and _HAS_OPEN_SSL:
+ accepted_socket = _StandaloneSSLConnection(accepted_socket)
+ return accepted_socket, client_address
+
+ def serve_forever(self, poll_interval=0.5):
+ """Override SocketServer.BaseServer.serve_forever."""
+
+ self.__ws_serving = True
+ self.__ws_is_shut_down.clear()
+ handle_request = self.handle_request
+ if hasattr(self, '_handle_request_noblock'):
+ handle_request = self._handle_request_noblock
+ else:
+ self._logger.warning('Fallback to blocking request handler')
+ try:
+ while self.__ws_serving:
+ r, w, e = select.select(
+ [socket_[0] for socket_ in self._sockets],
+ [], [], poll_interval)
+ for socket_ in r:
+ self.socket = socket_
+ handle_request()
+ self.socket = None
+ finally:
+ self.__ws_is_shut_down.set()
+
+ def shutdown(self):
+ """Override SocketServer.BaseServer.shutdown."""
+
+ self.__ws_serving = False
+ self.__ws_is_shut_down.wait()
+
+
+class WebSocketRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
+ """CGIHTTPRequestHandler specialized for WebSocket."""
+
+ # Use httplib.HTTPMessage instead of mimetools.Message.
+ MessageClass = httplib.HTTPMessage
+
+ def setup(self):
+ """Override SocketServer.StreamRequestHandler.setup to wrap rfile
+ with MemorizingFile.
+
+ This method will be called by BaseRequestHandler's constructor
+ before calling BaseHTTPRequestHandler.handle.
+ BaseHTTPRequestHandler.handle will call
+ BaseHTTPRequestHandler.handle_one_request and it will call
+ WebSocketRequestHandler.parse_request.
+ """
+
+ # Call superclass's setup to prepare rfile, wfile, etc. See setup
+ # definition on the root class SocketServer.StreamRequestHandler to
+ # understand what this does.
+ CGIHTTPServer.CGIHTTPRequestHandler.setup(self)
+
+ self.rfile = memorizingfile.MemorizingFile(
+ self.rfile,
+ max_memorized_lines=_MAX_MEMORIZED_LINES)
+
+ def __init__(self, request, client_address, server):
+ self._logger = util.get_class_logger(self)
+
+ self._options = server.websocket_server_options
+
+ # Overrides CGIHTTPServerRequestHandler.cgi_directories.
+ self.cgi_directories = self._options.cgi_directories
+ # Replace CGIHTTPRequestHandler.is_executable method.
+ if self._options.is_executable_method is not None:
+ self.is_executable = self._options.is_executable_method
+
+ # OWN MODIFICATION
+ # This actually calls BaseRequestHandler.__init__.
+ try:
+ CGIHTTPServer.CGIHTTPRequestHandler.__init__(
+ self, request, client_address, server)
+ except socket.error, e:
+ # Broken pipe, let it pass
+ if e.errno != 32:
+ raise
+ self._logger.debug("WS: Broken pipe")
+
+
+
+ def parse_request(self):
+ """Override BaseHTTPServer.BaseHTTPRequestHandler.parse_request.
+
+ Return True to continue processing for HTTP(S), False otherwise.
+
+ See BaseHTTPRequestHandler.handle_one_request method which calls
+ this method to understand how the return value will be handled.
+ """
+
+ # We hook parse_request method, but also call the original
+ # CGIHTTPRequestHandler.parse_request since when we return False,
+ # CGIHTTPRequestHandler.handle_one_request continues processing and
+ # it needs variables set by CGIHTTPRequestHandler.parse_request.
+ #
+ # Variables set by this method will be also used by WebSocket request
+ # handling (self.path, self.command, self.requestline, etc. See also
+ # how _StandaloneRequest's members are implemented using these
+ # attributes).
+ if not CGIHTTPServer.CGIHTTPRequestHandler.parse_request(self):
+ return False
+
+ if self._options.use_basic_auth:
+ auth = self.headers.getheader('Authorization')
+ if auth != self._options.basic_auth_credential:
+ self.send_response(401)
+ self.send_header('WWW-Authenticate',
+ 'Basic realm="Pywebsocket"')
+ self.end_headers()
+ self._logger.info('Request basic authentication')
+ return True
+
+ host, port, resource = http_header_util.parse_uri(self.path)
+ if resource is None:
+ self._logger.info('Invalid URI: %r', self.path)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ server_options = self.server.websocket_server_options
+ if host is not None:
+ validation_host = server_options.validation_host
+ if validation_host is not None and host != validation_host:
+ self._logger.info('Invalid host: %r (expected: %r)',
+ host,
+ validation_host)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ if port is not None:
+ validation_port = server_options.validation_port
+ if validation_port is not None and port != validation_port:
+ self._logger.info('Invalid port: %r (expected: %r)',
+ port,
+ validation_port)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ self.path = resource
+
+ request = _StandaloneRequest(self, self._options.use_tls)
+
+ try:
+ # Fallback to default http handler for request paths for which
+ # we don't have request handlers.
+ if not self._options.dispatcher.get_handler_suite(self.path):
+ self._logger.info('No handler for resource: %r',
+ self.path)
+ self._logger.info('Fallback to CGIHTTPRequestHandler')
+ return True
+ except dispatch.DispatchException, e:
+ self._logger.info('%s', e)
+ self.send_error(e.status)
+ return False
+
+ # If any Exceptions without except clause setup (including
+ # DispatchException) is raised below this point, it will be caught
+ # and logged by WebSocketServer.
+
+ try:
+ try:
+ handshake.do_handshake(
+ request,
+ self._options.dispatcher,
+ allowDraft75=self._options.allow_draft75,
+ strict=self._options.strict)
+ except handshake.VersionException, e:
+ self._logger.info('%s', e)
+ self.send_response(common.HTTP_STATUS_BAD_REQUEST)
+ self.send_header(common.SEC_WEBSOCKET_VERSION_HEADER,
+ e.supported_versions)
+ self.end_headers()
+ return False
+ except handshake.HandshakeException, e:
+ # Handshake for ws(s) failed.
+ self._logger.info('%s', e)
+ self.send_error(e.status)
+ return False
+
+ request._dispatcher = self._options.dispatcher
+ self._options.dispatcher.transfer_data(request)
+ except handshake.AbortedByUserException, e:
+ self._logger.info('%s', e)
+ return False
+
+ def log_request(self, code='-', size='-'):
+ """Override BaseHTTPServer.log_request."""
+
+ self._logger.info('"%s" %s %s',
+ self.requestline, str(code), str(size))
+
+ def log_error(self, *args):
+ """Override BaseHTTPServer.log_error."""
+
+ # Despite the name, this method is for warnings than for errors.
+ # For example, HTTP status code is logged by this method.
+ self._logger.warning('%s - %s',
+ self.address_string(),
+ args[0] % args[1:])
+
+ def is_cgi(self):
+ """Test whether self.path corresponds to a CGI script.
+
+ Add extra check that self.path doesn't contains ..
+ Also check if the file is a executable file or not.
+ If the file is not executable, it is handled as static file or dir
+ rather than a CGI script.
+ """
+
+ if CGIHTTPServer.CGIHTTPRequestHandler.is_cgi(self):
+ if '..' in self.path:
+ return False
+ # strip query parameter from request path
+ resource_name = self.path.split('?', 2)[0]
+ # convert resource_name into real path name in filesystem.
+ scriptfile = self.translate_path(resource_name)
+ if not os.path.isfile(scriptfile):
+ return False
+ if not self.is_executable(scriptfile):
+ return False
+ return True
+ return False
+
+
+def _get_logger_from_class(c):
+ return logging.getLogger('%s.%s' % (c.__module__, c.__name__))
+
+
+def _configure_logging(options):
+ logging.addLevelName(common.LOGLEVEL_FINE, 'FINE')
+
+ logger = logging.getLogger()
+ logger.setLevel(logging.getLevelName(options.log_level.upper()))
+ if options.log_file:
+ handler = logging.handlers.RotatingFileHandler(
+ options.log_file, 'a', options.log_max, options.log_count)
+ else:
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter(
+ '[%(asctime)s] [%(levelname)s] %(name)s: %(message)s')
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+
+ deflate_log_level_name = logging.getLevelName(
+ options.deflate_log_level.upper())
+ _get_logger_from_class(util._Deflater).setLevel(
+ deflate_log_level_name)
+ _get_logger_from_class(util._Inflater).setLevel(
+ deflate_log_level_name)
+
+class DefaultOptions:
+ server_host = ''
+ port = common.DEFAULT_WEB_SOCKET_PORT
+ use_tls = False
+ private_key = ''
+ certificate = ''
+ ca_certificate = ''
+ dispatcher = None
+ request_queue_size = _DEFAULT_REQUEST_QUEUE_SIZE
+ use_basic_auth = False
+
+ allow_draft75 = False
+ strict = False
+ validation_host = None
+ validation_port = None
+ cgi_directories = ''
+ is_executable_method = False
+
+def _main(args=None):
+ """You can call this function from your own program, but please note that
+ this function has some side-effects that might affect your program. For
+ example, util.wrap_popen3_for_win use in this method replaces implementation
+ of os.popen3.
+ """
+
+ options, args = _parse_args_and_config(args=args)
+
+ os.chdir(options.document_root)
+
+ _configure_logging(options)
+
+ # TODO(tyoshino): Clean up initialization of CGI related values. Move some
+ # of code here to WebSocketRequestHandler class if it's better.
+ options.cgi_directories = []
+ options.is_executable_method = None
+ if options.cgi_paths:
+ options.cgi_directories = options.cgi_paths.split(',')
+ if sys.platform in ('cygwin', 'win32'):
+ cygwin_path = None
+ # For Win32 Python, it is expected that CYGWIN_PATH
+ # is set to a directory of cygwin binaries.
+ # For example, websocket_server.py in Chromium sets CYGWIN_PATH to
+ # full path of third_party/cygwin/bin.
+ if 'CYGWIN_PATH' in os.environ:
+ cygwin_path = os.environ['CYGWIN_PATH']
+ util.wrap_popen3_for_win(cygwin_path)
+
+ def __check_script(scriptpath):
+ return util.get_script_interp(scriptpath, cygwin_path)
+
+ options.is_executable_method = __check_script
+
+ if options.use_tls:
+ if not (_HAS_SSL or _HAS_OPEN_SSL):
+ logging.critical('TLS support requires ssl or pyOpenSSL module.')
+ sys.exit(1)
+ if not options.private_key or not options.certificate:
+ logging.critical(
+ 'To use TLS, specify private_key and certificate.')
+ sys.exit(1)
+
+ if options.tls_client_auth:
+ if not options.use_tls:
+ logging.critical('TLS must be enabled for client authentication.')
+ sys.exit(1)
+ if not _HAS_SSL:
+ logging.critical('Client authentication requires ssl module.')
+
+ if not options.scan_dir:
+ options.scan_dir = options.websock_handlers
+
+ if options.use_basic_auth:
+ options.basic_auth_credential = 'Basic ' + base64.b64encode(
+ options.basic_auth_credential)
+
+ try:
+ if options.thread_monitor_interval_in_sec > 0:
+ # Run a thread monitor to show the status of server threads for
+ # debugging.
+ ThreadMonitor(options.thread_monitor_interval_in_sec).start()
+
+ server = WebSocketServer(options)
+ server.serve_forever()
+ except Exception, e:
+ logging.critical('mod_pywebsocket: %s' % e)
+ logging.critical('mod_pywebsocket: %s' % util.get_stack_trace())
+ sys.exit(1)
+
+
+if __name__ == '__main__':
+ _main(sys.argv[1:])
+
+
+# vi:sts=4 sw=4 et
diff --git a/pyload/remote/wsbackend/__init__.py b/pyload/remote/wsbackend/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pyload/remote/wsbackend/__init__.py