diff options
Diffstat (limited to 'pyload/remote')
-rw-r--r-- | pyload/remote/ClickAndLoadBackend.py | 170 | ||||
-rw-r--r-- | pyload/remote/JSONClient.py | 56 | ||||
-rw-r--r-- | pyload/remote/RemoteManager.py | 89 | ||||
-rw-r--r-- | pyload/remote/WSClient.py | 59 | ||||
-rw-r--r-- | pyload/remote/WebSocketBackend.py | 49 | ||||
-rw-r--r-- | pyload/remote/__init__.py | 0 | ||||
-rw-r--r-- | pyload/remote/apitypes.py | 536 | ||||
-rw-r--r-- | pyload/remote/apitypes_debug.py | 135 | ||||
-rw-r--r-- | pyload/remote/create_apitypes.py | 180 | ||||
-rw-r--r-- | pyload/remote/create_jstypes.py | 36 | ||||
-rw-r--r-- | pyload/remote/json_converter.py | 64 | ||||
-rw-r--r-- | pyload/remote/pyload.thrift | 538 | ||||
-rw-r--r-- | pyload/remote/ttypes.py | 534 | ||||
-rw-r--r-- | pyload/remote/wsbackend/AbstractHandler.py | 133 | ||||
-rw-r--r-- | pyload/remote/wsbackend/ApiHandler.py | 81 | ||||
-rw-r--r-- | pyload/remote/wsbackend/AsyncHandler.py | 167 | ||||
-rw-r--r-- | pyload/remote/wsbackend/Dispatcher.py | 31 | ||||
-rw-r--r-- | pyload/remote/wsbackend/Server.py | 733 | ||||
-rw-r--r-- | pyload/remote/wsbackend/__init__.py | 0 |
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 |