diff options
-rw-r--r-- | module/Api.py | 136 | ||||
-rw-r--r-- | module/database/UserDatabase.py | 22 | ||||
-rw-r--r-- | module/plugins/Hook.py | 12 | ||||
-rw-r--r-- | module/remote/RemoteManager.py | 33 | ||||
-rw-r--r-- | module/remote/thriftbackend/Processor.py | 41 | ||||
-rw-r--r-- | module/web/api_app.py | 6 | ||||
-rw-r--r-- | module/web/json_app.py | 113 | ||||
-rw-r--r-- | module/web/pyload_app.py | 127 | ||||
-rw-r--r-- | module/web/templates/default/admin.html | 50 | ||||
-rw-r--r-- | module/web/templates/default/base.html | 4 | ||||
-rw-r--r-- | module/web/utils.py | 61 | ||||
-rw-r--r-- | module/web/webinterface.py | 1 | ||||
-rwxr-xr-x | pyLoadCore.py | 2 |
13 files changed, 277 insertions, 331 deletions
diff --git a/module/Api.py b/module/Api.py index 37336e568..f4a252ac9 100644 --- a/module/Api.py +++ b/module/Api.py @@ -26,14 +26,47 @@ from remote.thriftbackend.thriftgen.pyload.ttypes import * from remote.thriftbackend.thriftgen.pyload.Pyload import Iface from PyFile import PyFile -from database.UserDatabase import ROLE from utils import freeSpace, compare_time from common.packagetools import parseNames from network.RequestFactory import getURL +# contains function names mapped to their permissions +# unlisted functions are for admins only +permMap = {} + +# decorator only called on init, never initialized, so has no effect on runtime +def permission(bits): + class _Dec(object): + def __new__(cls, func, *args, **kwargs): + permMap[func.__name__] = bits + return func + + return _Dec + + urlmatcher = re.compile(r"((https?|ftps?|xdcc|sftp):((//)|(\\\\))+[\w\d:#@%/;$()~_?\+-=\\\.&]*)", re.IGNORECASE) +class PERMS: + ALL = 0 # requires no permission, but login + ADD = 1 # can add packages + DELETE = 2 # can delete packages + STATUS = 4 # see and change server status + LIST = 16 # see queue and collector + MODIFY = 32 # moddify some attribute of downloads + DOWNLOAD = 64 # can download from webinterface + SETTINGS = 128 # can access settings + ACCOUNTS = 256 # can access accounts + +class ROLE: + ADMIN = 0 #admin has all permissions implicit + USER = 1 + +def has_permission(userperms, perms): + # bytewise or perms before if needed + return perms == (userperms & perms) + + class Api(Iface): """ **pyLoads API** @@ -75,6 +108,7 @@ class Api(Iface): section.outline = sub["outline"] return sections + @permission(PERMS.SETTINGS) def getConfigValue(self, category, option, section="core"): """Retrieve config value. @@ -90,6 +124,7 @@ class Api(Iface): return str(value) if not isinstance(value, basestring) else value + @permission(PERMS.SETTINGS) def setConfigValue(self, category, option, value, section="core"): """Set new config value. @@ -109,6 +144,7 @@ class Api(Iface): elif section == "plugin": self.core.config.setPlugin(category, option, value) + @permission(PERMS.SETTINGS) def getConfig(self): """Retrieves complete config of core. @@ -123,6 +159,7 @@ class Api(Iface): """ return self.core.config.config + @permission(PERMS.SETTINGS) def getPluginConfig(self): """Retrieves complete config for all plugins. @@ -138,14 +175,17 @@ class Api(Iface): return self.core.config.plugin + @permission(PERMS.STATUS) def pauseServer(self): """Pause server: Tt wont start any new downloads, but nothing gets aborted.""" self.core.threadManager.pause = True + @permission(PERMS.STATUS) def unpauseServer(self): """Unpause server: New Downloads will be started.""" self.core.threadManager.pause = False + @permission(PERMS.STATUS) def togglePause(self): """Toggle pause state. @@ -154,6 +194,7 @@ class Api(Iface): self.core.threadManager.pause ^= True return self.core.threadManager.pause + @permission(PERMS.STATUS) def toggleReconnect(self): """Toggle reconnect activation. @@ -162,6 +203,7 @@ class Api(Iface): self.core.config["reconnect"]["activated"] ^= True return self.core.config["reconnect"]["activated"] + @permission(PERMS.LIST) def statusServer(self): """Some general information about the current status of pyLoad. @@ -177,10 +219,12 @@ class Api(Iface): return serverStatus + @permission(PERMS.STATUS) def freeSpace(self): """Available free space at download directory in bytes""" return freeSpace(self.core.config["general"]["download_folder"]) + @permission(PERMS.ALL) def getServerVersion(self): """pyLoad Core version """ return self.core.version @@ -194,6 +238,7 @@ class Api(Iface): pass #self.core.do_restart = True + @permission(PERMS.STATUS) def getLog(self, offset=0): """Returns most recent log entries. @@ -211,6 +256,7 @@ class Api(Iface): except: return ['No log available'] + @permission(PERMS.STATUS) def isTimeDownload(self): """Checks if pyload will start new downloads according to time in config. @@ -220,6 +266,7 @@ class Api(Iface): end = self.core.config['downloadTime']['end'].split(":") return compare_time(start, end) + @permission(PERMS.STATUS) def isTimeReconnect(self): """Checks if pyload will try to make a reconnect @@ -229,6 +276,7 @@ class Api(Iface): end = self.core.config['reconnect']['endTime'].split(":") return compare_time(start, end) and self.core.config["reconnect"]["activated"] + @permission(PERMS.LIST) def statusDownloads(self): """ Status off all currently running downloads. @@ -248,6 +296,7 @@ class Api(Iface): return data + @permission(PERMS.ADD) def addPackage(self, name, links, dest=Destination.Queue): """Adds a package, with links to desired destination. @@ -273,6 +322,7 @@ class Api(Iface): return pid + @permission(PERMS.ADD) def parseURLs(self, html=None, url=None): """Parses html content or any arbitaty text for links and returns result of `checkURLs` @@ -291,6 +341,7 @@ class Api(Iface): return self.checkURLs(urls) + @permission(PERMS.ADD) def checkURLs(self, urls): """ Gets urls and returns pluginname mapped to list of matches urls. @@ -308,6 +359,7 @@ class Api(Iface): return plugins + @permission(PERMS.ADD) def checkOnlineStatus(self, urls): """ initiates online status check @@ -329,6 +381,7 @@ class Api(Iface): return OnlineCheck(rid, result) + @permission(PERMS.ADD) def checkOnlineStatusContainer(self, urls, container, data): """ checks online status of urls and a submited container file @@ -343,6 +396,7 @@ class Api(Iface): return self.checkOnlineStatus(urls + [th.name]) + @permission(PERMS.ADD) def pollResults(self, rid): """ Polls the result available for ResultID @@ -358,6 +412,7 @@ class Api(Iface): return OnlineCheck(rid, result) + @permission(PERMS.ADD) def generatePackages(self, links): """ Parses links, generates packages names from urls @@ -367,6 +422,7 @@ class Api(Iface): result = parseNames((x, x) for x in links) return result + @permission(PERMS.ADD) def generateAndAddPackages(self, links, dest=Destination.Queue): """Generates and add packages @@ -377,6 +433,7 @@ class Api(Iface): return [self.addPackage(name, urls, dest) for name, urls in self.generatePackages(links).iteritems()] + @permission(PERMS.ADD) def checkAndAddPackages(self, links, dest=Destination.Queue): """Checks online status, retrieves names, and will add packages.\ Because of this packages are not added immediatly, only for internal use. @@ -389,6 +446,7 @@ class Api(Iface): self.core.threadManager.createResultThread(data, True) + @permission(PERMS.LIST) def getPackageData(self, pid): """Returns complete information about package, and included files. @@ -406,6 +464,7 @@ class Api(Iface): return pdata + @permission(PERMS.LIST) def getPackageInfo(self, pid): """Returns information about package, without detailed information about containing files @@ -423,6 +482,7 @@ class Api(Iface): return pdata + @permission(PERMS.LIST) def getFileData(self, fid): """Get complete information about a specific file. @@ -436,6 +496,7 @@ class Api(Iface): fdata = self._convertPyFile(info.values()[0]) return fdata + @permission(PERMS.DELETE) def deleteFiles(self, fids): """Deletes several file entries from pyload. @@ -446,6 +507,7 @@ class Api(Iface): self.core.files.save() + @permission(PERMS.DELETE) def deletePackages(self, pids): """Deletes packages and containing links. @@ -456,6 +518,7 @@ class Api(Iface): self.core.files.save() + @permission(PERMS.LIST) def getQueue(self): """Returns info about queue and packages, **not** about files, see `getQueueData` \ or `getPackageData` instead. @@ -468,6 +531,7 @@ class Api(Iface): pack["linkstotal"]) for pack in self.core.files.getInfoData(Destination.Queue).itervalues()] + @permission(PERMS.LIST) def getQueueData(self): """Return complete data about everything in queue, this is very expensive use it sparely.\ See `getQueue` for alternative. @@ -480,6 +544,7 @@ class Api(Iface): links=[self._convertPyFile(x) for x in pack["links"].itervalues()]) for pack in self.core.files.getCompleteData(Destination.Queue).itervalues()] + @permission(PERMS.LIST) def getCollector(self): """same as `getQueue` for collector. @@ -491,6 +556,7 @@ class Api(Iface): pack["linkstotal"]) for pack in self.core.files.getInfoData(Destination.Collector).itervalues()] + @permission(PERMS.LIST) def getCollectorData(self): """same as `getQueueData` for collector. @@ -503,6 +569,7 @@ class Api(Iface): for pack in self.core.files.getCompleteData(Destination.Collector).itervalues()] + @permission(PERMS.ADD) def addFiles(self, pid, links): """Adds files to specific package. @@ -514,6 +581,7 @@ class Api(Iface): self.core.log.info(_("Added %(count)d links to package #%(package)d ") % {"count": len(links), "package": pid}) self.core.files.save() + @permission(PERMS.MODIFY) def pushToQueue(self, pid): """Moves package from Collector to Queue. @@ -521,6 +589,7 @@ class Api(Iface): """ self.core.files.setPackageLocation(pid, Destination.Queue) + @permission(PERMS.MODIFY) def pullFromQueue(self, pid): """Moves package from Queue to Collector. @@ -528,6 +597,7 @@ class Api(Iface): """ self.core.files.setPackageLocation(pid, Destination.Collector) + @permission(PERMS.MODIFY) def restartPackage(self, pid): """Restarts a package, resets every containing files. @@ -535,6 +605,7 @@ class Api(Iface): """ self.core.files.restartPackage(int(pid)) + @permission(PERMS.MODIFY) def restartFile(self, fid): """Resets file status, so it will be downloaded again. @@ -542,6 +613,7 @@ class Api(Iface): """ self.core.files.restartFile(int(fid)) + @permission(PERMS.MODIFY) def recheckPackage(self, pid): """Proofes online status of all files in a package, also a default action when package is added. @@ -550,6 +622,7 @@ class Api(Iface): """ self.core.files.reCheckPackage(int(pid)) + @permission(PERMS.MODIFY) def stopAllDownloads(self): """Aborts all running downloads.""" @@ -557,6 +630,7 @@ class Api(Iface): for pyfile in pyfiles: pyfile.abortDownload() + @permission(PERMS.MODIFY) def stopDownloads(self, fids): """Aborts specific downloads. @@ -569,6 +643,7 @@ class Api(Iface): if pyfile.id in fids: pyfile.abortDownload() + @permission(PERMS.MODIFY) def setPackageName(self, pid, name): """Renames a package. @@ -579,6 +654,7 @@ class Api(Iface): pack.name = name pack.sync() + @permission(PERMS.MODIFY) def movePackage(self, destination, pid): """Set a new package location. @@ -588,6 +664,7 @@ class Api(Iface): if destination not in (0, 1): return self.core.files.setPackageLocation(pid, destination) + @permission(PERMS.MODIFY) def moveFiles(self, fids, pid): """Move multiple files to another package @@ -599,6 +676,7 @@ class Api(Iface): pass + @permission(PERMS.ADD) def uploadContainer(self, filename, data): """Uploads and adds a container file to pyLoad. @@ -611,6 +689,7 @@ class Api(Iface): self.addPackage(th.name, [th.name], Destination.Queue) + @permission(PERMS.MODIFY) def orderPackage(self, pid, position): """Gives a package a new position. @@ -619,6 +698,7 @@ class Api(Iface): """ self.core.files.reorderPackage(pid, position) + @permission(PERMS.MODIFY) def orderFile(self, fid, position): """Gives a new position to a file within its package. @@ -627,6 +707,7 @@ class Api(Iface): """ self.core.files.reorderFile(fid, position) + @permission(PERMS.MODIFY) def setPackageData(self, pid, data): """Allows to modify several package attributes. @@ -643,6 +724,7 @@ class Api(Iface): p.sync() self.core.files.save() + @permission(PERMS.DELETE) def deleteFinished(self): """Deletes all finished files and completly finished packages. @@ -651,10 +733,12 @@ class Api(Iface): deleted = self.core.files.deleteFinishedLinks() return deleted + @permission(PERMS.MODIFY) def restartFailed(self): """Restarts all failed failes.""" self.core.files.restartFailed() + @permission(PERMS.LIST) def getPackageOrder(self, destination): """Returns information about package order. @@ -672,6 +756,7 @@ class Api(Iface): order[pack["order"]] = pack["id"] return order + @permission(PERMS.LIST) def getFileOrder(self, pid): """Information about file order within package. @@ -687,6 +772,7 @@ class Api(Iface): return order + @permission(PERMS.STATUS) def isCaptchaWaiting(self): """Indicates wether a captcha task is available @@ -696,6 +782,7 @@ class Api(Iface): task = self.core.captchaManager.getTask() return not task is None + @permission(PERMS.STATUS) def getCaptchaTask(self, exclusive=False): """Returns a captcha task @@ -712,6 +799,7 @@ class Api(Iface): else: return CaptchaTask(-1) + @permission(PERMS.STATUS) def getCaptchaTaskStatus(self, tid): """Get information about captcha task @@ -722,6 +810,7 @@ class Api(Iface): t = self.core.captchaManager.getTaskByID(tid) return t.getStatus() if t else "" + @permission(PERMS.STATUS) def setCaptchaResult(self, tid, result): """Set result for a captcha task @@ -735,6 +824,7 @@ class Api(Iface): self.core.captchaManager.removeTask(task) + @permission(PERMS.STATUS) def getEvents(self, uuid): """Lists occured events, may be affected to changes in future. @@ -764,6 +854,7 @@ class Api(Iface): newEvents.append(event) return newEvents + @permission(PERMS.ACCOUNTS) def getAccounts(self, refresh): """Get information about all entered accounts. @@ -778,6 +869,7 @@ class Api(Iface): for acc in group]) return accounts + @permission(PERMS.ALL) def getAccountTypes(self): """All available account types. @@ -785,10 +877,12 @@ class Api(Iface): """ return self.core.accountManager.accounts.keys() + @permission(PERMS.ACCOUNTS) def updateAccount(self, plugin, account, password=None, options={}): """Changes pw/options for specific account.""" self.core.accountManager.updateAccount(plugin, account, password, options) + @permission(PERMS.ACCOUNTS) def removeAccount(self, plugin, account): """Remove account from pyload. @@ -797,6 +891,7 @@ class Api(Iface): """ self.core.accountManager.removeAccount(plugin, account) + @permission(PERMS.ALL) def login(self, username, password, remoteip=None): """Login into pyLoad, this **must** be called when using rpc before any methods can be used. @@ -805,26 +900,38 @@ class Api(Iface): :param remoteip: Omit this argument, its only used internal :return: bool indicating login was successful """ - if self.core.config["remote"]["nolocalauth"] and remoteip == "127.0.0.1": - return True - if self.core.startedInGui and remoteip == "127.0.0.1": - return True + return True if self.checkAuth(username, password, remoteip) else False - user = self.core.db.checkAuth(username, password) - if user and user["role"] == ROLE.ADMIN: - return True - - return False - - def checkAuth(self, username, password): + def checkAuth(self, username, password, remoteip=None): """Check authentication and returns details :param username: :param password: + :param remoteip: :return: dict with info, empty when login is incorrect """ + if self.core.config["remote"]["nolocalauth"] and remoteip == "127.0.0.1": + return "local" + if self.core.startedInGui and remoteip == "127.0.0.1": + return "local" + return self.core.db.checkAuth(username, password) + def isAuthorized(self, func, userdata): + """checks if the user is authorized for specific method + + :param func: function name + :param userdata: dictionary of user data + :return: boolean + """ + if userdata["role"] == ROLE.ADMIN or userdata == "local": + return True + elif func in permMap and has_permission(userdata["permission"], permMap[func]): + return True + else: + return False + + def getUserData(self, username, password): """see `checkAuth`""" return self.checkAuth(username, password) @@ -834,6 +941,7 @@ class Api(Iface): """returns all known user and info""" return self.core.db.getAllUserData() + @permission(PERMS.STATUS) def getServices(self): """ A dict of available services, these can be defined by hook plugins. @@ -845,6 +953,7 @@ class Api(Iface): return data + @permission(PERMS.STATUS) def hasService(self, plugin, func): """Checks wether a service is available. @@ -855,6 +964,7 @@ class Api(Iface): cont = self.core.hookManager.methods return plugin in cont and func in cont[plugin] + @permission(PERMS.STATUS) def call(self, info): """Calls a service (a method in hook plugin). @@ -877,6 +987,7 @@ class Api(Iface): except Exception, e: raise ServiceException(e.message) + @permission(PERMS.STATUS) def getAllInfo(self): """Returns all information stored by hook plugins. Values are always strings @@ -884,6 +995,7 @@ class Api(Iface): """ return self.core.hookManager.getAllInfo() + @permission(PERMS.STATUS) def getInfoByPlugin(self, plugin): """Returns information stored by a specific plugin. diff --git a/module/database/UserDatabase.py b/module/database/UserDatabase.py index f888e219e..e74399c11 100644 --- a/module/database/UserDatabase.py +++ b/module/database/UserDatabase.py @@ -16,29 +16,11 @@ @author: mkaay """ -from DatabaseBackend import DatabaseBackend -from DatabaseBackend import style - from hashlib import sha1 import random -class PERMS: - ALL = 0 # requires no permission, but login - ADD = 1 # can add packages - DELETE = 2 # can delete packages - STATUS = 4 # see and change server status - SEE_DOWNLOADS = 16 # see queue and collector / modify downloads - DOWNLOAD = 32 # can download from webinterface - SETTINGS = 64 # can access settings - ACCOUNTS = 128 # can access accounts - -class ROLE: - ADMIN = 0 #admin has all permissions implicit - USER = 1 - -def has_permission(current, perms): - # bytewise or perms before if needed - return perms == (current & perms) +from DatabaseBackend import DatabaseBackend +from DatabaseBackend import style class UserMethods(): @style.queue diff --git a/module/plugins/Hook.py b/module/plugins/Hook.py index 7e4f58c66..85fb49190 100644 --- a/module/plugins/Hook.py +++ b/module/plugins/Hook.py @@ -23,16 +23,10 @@ from traceback import print_exc class Expose(object): """ used for decoration to declare rpc services """ - def __init__(self, *args, **kwargs): - self._f = args[0] - hookManager.addRPC(self._f.__module__, self._f.func_name, self._f.func_doc) - def __get__(self, obj, klass): - self._obj = obj - return self - - def __call__(self, *args, **kwargs): - return self._f(self._obj, *args, **kwargs) + def __new__(cls, f, *args, **kwargs): + hookManager.addRPC(f.__module__, f.func_name, f.func_doc) + return f def threaded(f): def run(*args,**kwargs): diff --git a/module/remote/RemoteManager.py b/module/remote/RemoteManager.py index 792eaec4d..2ac26a677 100644 --- a/module/remote/RemoteManager.py +++ b/module/remote/RemoteManager.py @@ -19,14 +19,12 @@ from threading import Thread from traceback import print_exc -from module.database.UserDatabase import ROLE - class BackendBase(Thread): def __init__(self, manager): Thread.__init__(self) - self.manager = manager + self.m = manager self.core = manager.core - + def run(self): try: self.serve() @@ -34,18 +32,16 @@ class BackendBase(Thread): self.core.log.error(_("Remote backend error: %s") % e) if self.core.debug: print_exc() - + def setup(self, host, port): pass - + def checkDeps(self): return True - + def serve(self): pass - - def checkAuth(self, user, password, remoteip=None): - return self.manager.checkAuth(user, password, remoteip) + class RemoteManager(): available = ["ThriftBackend"] @@ -53,14 +49,13 @@ class RemoteManager(): def __init__(self, core): self.core = core self.backends = [] - - def startBackends(self): + def startBackends(self): host = self.core.config["remote"]["listenaddr"] port = self.core.config["remote"]["port"] for b in self.available: - klass = getattr(__import__("module.remote.%s" % b, globals(), locals(), [b] , -1), b) + klass = getattr(__import__("module.remote.%s" % b, globals(), locals(), [b], -1), b) backend = klass(self) if not backend.checkDeps(): continue @@ -76,15 +71,3 @@ class RemoteManager(): self.backends.append(backend) port += 1 - - def checkAuth(self, user, password, remoteip=None): - if self.core.config["remote"]["nolocalauth"] and remoteip == "127.0.0.1": - return True - if self.core.startedInGui and remoteip == "127.0.0.1": - return True - - user = self.core.db.checkAuth(user, password) - if user and user["role"] == ROLE.ADMIN: - return user - else: - return {} diff --git a/module/remote/thriftbackend/Processor.py b/module/remote/thriftbackend/Processor.py index a8fc94298..a8b87c82c 100644 --- a/module/remote/thriftbackend/Processor.py +++ b/module/remote/thriftbackend/Processor.py @@ -12,14 +12,18 @@ class Processor(Pyload.Processor): if trans not in self.authenticated: self.authenticated[trans] = False oldclose = trans.close + def wrap(): if self in self.authenticated: del self.authenticated[trans] oldclose() + trans.close = wrap authenticated = self.authenticated[trans] (name, type, seqid) = iprot.readMessageBegin() - if name not in self._processMap or (not authenticated and not name == "login"): + + # unknown method + if name not in self._processMap: iprot.skip(Pyload.TType.STRUCT) iprot.readMessageEnd() x = Pyload.TApplicationException(Pyload.TApplicationException.UNKNOWN_METHOD, 'Unknown function %s' % name) @@ -28,17 +32,46 @@ class Processor(Pyload.Processor): oprot.writeMessageEnd() oprot.trans.flush() return + + # not logged in + elif not authenticated and not name == "login": + iprot.skip(Pyload.TType.STRUCT) + iprot.readMessageEnd() + # 20 - Not logged in (in situ declared error code) + x = Pyload.TApplicationException(20, 'Not logged in') + oprot.writeMessageBegin(name, Pyload.TMessageType.EXCEPTION, seqid) + x.write(oprot) + oprot.writeMessageEnd() + oprot.trans.flush() + return + elif not authenticated and name == "login": args = Pyload.login_args() args.read(iprot) iprot.readMessageEnd() result = Pyload.login_result() - self.authenticated[trans] = self._handler.login(args.username, args.password, trans.remoteaddr[0]) - result.success = self.authenticated[trans] + # api login + self.authenticated[trans] = self._handler.checkAuth(args.username, args.password, trans.remoteaddr[0]) + + result.success = True if self.authenticated[trans] else False oprot.writeMessageBegin("login", Pyload.TMessageType.REPLY, seqid) result.write(oprot) oprot.writeMessageEnd() oprot.trans.flush() - else: + + elif self._handler.isAuthorized(name, authenticated): self._processMap[name](self, seqid, iprot, oprot) + + else: + #no permission + iprot.skip(Pyload.TType.STRUCT) + iprot.readMessageEnd() + # 21 - Not authorized + x = Pyload.TApplicationException(21, 'Not authorized') + oprot.writeMessageBegin(name, Pyload.TMessageType.EXCEPTION, seqid) + x.write(oprot) + oprot.writeMessageEnd() + oprot.trans.flush() + return + return True diff --git a/module/web/api_app.py b/module/web/api_app.py index 32b128e6a..156922d6a 100644 --- a/module/web/api_app.py +++ b/module/web/api_app.py @@ -14,7 +14,6 @@ from utils import toDict, set_session from webinterface import PYLOAD from module.common.json_layer import json_dumps -from module.database.UserDatabase import ROLE try: from ast import literal_eval @@ -46,9 +45,12 @@ def call_api(func, args=""): if 'session' in request.POST: s = s.get_by_id(request.POST['session']) - if not s or not s.get("authenticated", False) or s.get("role", -1) != ROLE.ADMIN: + if not s or not s.get("authenticated", False): return HTTPError(401, json_dumps("Unauthorized")) + if not PYLOAD.isAuthorized(func, {"role": s["role"], "permission": s["perms"]}): + return HTTPError(403, json_dumps("Forbidden")) + args = args.split("/")[1:] kwargs = {} diff --git a/module/web/json_app.py b/module/web/json_app.py index 0573eff77..6d50525bb 100644 --- a/module/web/json_app.py +++ b/module/web/json_app.py @@ -31,7 +31,7 @@ def get_sort_key(item): @route("/json/status") @route("/json/status", method="POST") -@login_required('see_downloads') +@login_required('LIST') def status(): try: status = toDict(PYLOAD.statusServer()) @@ -43,7 +43,7 @@ def status(): @route("/json/links") @route("/json/links", method="POST") -@login_required('see_downloads') +@login_required('LIST') def links(): try: links = [toDict(x) for x in PYLOAD.statusDownloads()] @@ -69,7 +69,7 @@ def links(): @route("/json/queue") -@login_required('see_downloads') +@login_required('LIST') def queue(): print "/json/queue" try: @@ -80,7 +80,7 @@ def queue(): @route("/json/pause") -@login_required('status') +@login_required('STATUS') def pause(): try: return PYLOAD.pauseServer() @@ -90,7 +90,7 @@ def pause(): @route("/json/unpause") -@login_required('status') +@login_required('STATUS') def unpause(): try: return PYLOAD.unpauseServer() @@ -100,7 +100,7 @@ def unpause(): @route("/json/cancel") -@login_required('status') +@login_required('STATUS') def cancel(): try: return PYLOAD.stopAllDownloads() @@ -109,7 +109,7 @@ def cancel(): @route("/json/packages") -@login_required('see_downloads') +@login_required('LIST') def packages(): print "/json/packages" try: @@ -128,7 +128,7 @@ def packages(): @route("/json/package/:id") @validate(id=int) -@login_required('see_downloads') +@login_required('LIST') def package(id): try: data = toDict(PYLOAD.getPackageData(id)) @@ -163,7 +163,7 @@ def package(id): @route("/json/package_order/:ids") -@login_required('add') +@login_required('ADD') def package_order(ids): try: pid, pos = ids.split("|") @@ -175,7 +175,7 @@ def package_order(ids): @route("/json/link/:id") @validate(id=int) -@login_required('see_downloads') +@login_required('LIST') def link(id): print "/json/link/%d" % id try: @@ -187,7 +187,7 @@ def link(id): @route("/json/remove_link/:id") @validate(id=int) -@login_required('delete') +@login_required('DELETE') def remove_link(id): try: PYLOAD.deleteFiles([id]) @@ -198,7 +198,7 @@ def remove_link(id): @route("/json/restart_link/:id") @validate(id=int) -@login_required('add') +@login_required('ADD') def restart_link(id): try: PYLOAD.restartFile(id) @@ -209,7 +209,7 @@ def restart_link(id): @route("/json/abort_link/:id") @validate(id=int) -@login_required('delete') +@login_required('DELETE') def abort_link(id): try: PYLOAD.stopDownloads([id]) @@ -219,7 +219,7 @@ def abort_link(id): @route("/json/link_order/:ids") -@login_required('add') +@login_required('ADD') def link_order(ids): try: pid, pos = ids.split("|") @@ -231,7 +231,7 @@ def link_order(ids): @route("/json/add_package") @route("/json/add_package", method="POST") -@login_required('add') +@login_required('ADD') def add_package(): name = request.forms.get("add_name", "New Package").strip() queue = int(request.forms['add_dest']) @@ -267,7 +267,7 @@ def add_package(): @route("/json/remove_package/:id") @validate(id=int) -@login_required('delete') +@login_required('DELETE') def remove_package(id): try: PYLOAD.deletePackages([id]) @@ -278,7 +278,7 @@ def remove_package(id): @route("/json/restart_package/:id") @validate(id=int) -@login_required('add') +@login_required('MODIFY') def restart_package(id): try: PYLOAD.restartPackage(id) @@ -290,7 +290,7 @@ def restart_package(id): @route("/json/move_package/:dest/:id") @validate(dest=int, id=int) -@login_required('add') +@login_required('MODIFY') def move_package(dest, id): try: PYLOAD.movePackage(dest, id) @@ -300,7 +300,7 @@ def move_package(dest, id): @route("/json/edit_package", method="POST") -@login_required('add') +@login_required('MODIFY') def edit_package(): try: id = int(request.forms.get("pack_id")) @@ -317,7 +317,7 @@ def edit_package(): @route("/json/set_captcha") @route("/json/set_captcha", method="POST") -@login_required('add') +@login_required('ADD') def set_captcha(): if request.environ.get('REQUEST_METHOD', "GET") == "POST": try: @@ -336,13 +336,13 @@ def set_captcha(): @route("/json/delete_finished") -@login_required('delete') +@login_required('DELETE') def delete_finished(): return {"del": PYLOAD.deleteFinished()} @route("/json/restart_failed") -@login_required('delete') +@login_required('MODIFY') def restart_failed(): restart = PYLOAD.restartFailed() @@ -351,7 +351,7 @@ def restart_failed(): @route("/json/load_config/:category/:section") -@login_required("settings") +@login_required("SETTINGS") def load_config(category, section): conf = None if category == "general": @@ -371,7 +371,7 @@ def load_config(category, section): @route("/json/save_config/:category", method="POST") -@login_required("settings") +@login_required("SETTINGS") def save_config(category): for key, value in request.POST.iteritems(): try: @@ -385,7 +385,7 @@ def save_config(category): @route("/json/add_account", method="POST") -@login_required("settings") +@login_required("ACCOUNTS") def add_account(): login = request.POST["account_login"] password = request.POST["account_password"] @@ -395,7 +395,7 @@ def add_account(): @route("/json/update_accounts", method="POST") -@login_required("settings") +@login_required("ACCOUNTS") def update_accounts(): deleted = [] #dont update deleted accs or they will be created again @@ -428,64 +428,3 @@ def change_password(): if not PYLOAD.changePassword(user, oldpw, newpw): print "Wrong password" return HTTPError() - -#@route("/json/filemanager/rename", method="POST") -#@login_required('filemanager') -def rename_dir(): - try: - path = decode(request.forms.get("path")) - old_name = path + "/" + decode(request.forms.get("old_name")) - new_name = path + "/" + decode(request.forms.get("new_name")) - - try: - #check if file exists - os.rename(old_name, new_name) - except Exception, e: - return {"response": "fail", "error": str(e) + "\n" + old_name + " => " + new_name} - - return {"response": "success"} - - except: - return HTTPError() - - -#@route("/json/filemanager/delete", method="POST") -#@login_required('filemanager') -def delete_dir(): - try: - try: - path = decode(request.forms.get("path")) - name = decode(request.forms.get("name")) - shutil.rmtree(path + "/" + name) - except Exception, e: - return {"response": "fail", "error": str(e) + "\n" + path + "/" + name} - - return {"response": "success"} - - except: - return HTTPError() - - -#@route("/json/filemanager/mkdir", method="POST") -#@login_required('filemanager') -def make_dir(): - try: - path = decode(request.forms.get("path")) - name = decode(request.forms.get("name")) - try: - #i = 1 - #full_name = path + "/" + name - #while os.path.exists(full_name) - # full_name = full_name + i - # i = i + 1 - # - #os.mkdir(full_name) - - os.mkdir(path + "/" + name) - except Exception, e: - return {"response": "fail", "error": str(e) + "\nUnable to create directory: " + path + "/" + name} - - return {"response": "success", "path": path, "name": name} - - except: - return HTTPError() diff --git a/module/web/pyload_app.py b/module/web/pyload_app.py index 49568baad..553eeaa57 100644 --- a/module/web/pyload_app.py +++ b/module/web/pyload_app.py @@ -32,11 +32,11 @@ from bottle import route, static_file, request, response, redirect, HTTPError, e from webinterface import PYLOAD, PYLOAD_DIR, PROJECT_DIR, SETUP from utils import render_to_response, parse_permissions, parse_userdata, \ - login_required, get_permission, set_permission, toDict, set_session + login_required, get_permission, set_permission, permlist, toDict, set_session from filters import relpath, unquotepath -from module.utils import formatSize, decode, fs_decode +from module.utils import formatSize, fs_decode # Helper @@ -132,7 +132,7 @@ def logout(): @route("/") @route("/home") -@login_required("see_downloads") +@login_required("LIST") def home(): try: res = [toDict(x) for x in PYLOAD.statusDownloads()] @@ -149,7 +149,7 @@ def home(): @route("/queue") -@login_required("see_downloads") +@login_required("LIST") def queue(): queue = PYLOAD.getQueue() @@ -159,7 +159,7 @@ def queue(): @route("/collector") -@login_required('see_downloads') +@login_required('LIST') def collector(): queue = PYLOAD.getCollector() @@ -169,7 +169,7 @@ def collector(): @route("/downloads") -@login_required('download') +@login_required('DOWNLOAD') def downloads(): root = fs_decode(PYLOAD.getConfigValue("general", "download_folder")) @@ -205,7 +205,7 @@ def downloads(): @route("/downloads/get/:path#.+#") -@login_required("download") +@login_required("DOWNLOAD") def get_download(path): path = unquote(path) #@TODO some files can not be downloaded @@ -221,64 +221,9 @@ def get_download(path): return HTTPError(404, "File not Found.") -#@route("/filemanager") -#@login_required('filemanager') -def filemanager(): - root = PYLOAD.getConfigValue("general", "download_folder") - - if not isdir(root): - return base([_('Download directory not found.')]) - - root_node = {'name': '/', - 'path': root, - 'files': [], - 'folders': [] - } - - for item in sorted(listdir(root)): - if isdir(join(root, item)): - root_node['folders'].append(iterate_over_dir(root, item)) - elif isfile(join(root, item)): - f = { - 'name': decode(item), - 'path': root - } - root_node['files'].append(f) - - return render_to_response('filemanager.html', {'root': root_node}, [pre_processor]) - - -def iterate_over_dir(root, dir): - out = { - 'name': decode(dir), - 'path': root, - 'files': [], - 'folders': [] - } - for item in sorted(listdir(join(root, dir))): - subroot = join(root, dir) - if isdir(join(subroot, item)): - out['folders'].append(iterate_over_dir(subroot, item)) - elif isfile(join(subroot, item)): - f = { - 'name': decode(item), - 'path': subroot - } - out['files'].append(f) - - return out - - -#@route("/filemanager/get_dir", "POST") -#@login_required('filemanager') -def folder(): - path = request.forms.get("path").decode("utf8", "ignore") - name = request.forms.get("name").decode("utf8", "ignore") - return render_to_response('folder.html', {'path': path, 'name': name}, [pre_processor]) - @route("/settings") -@login_required('settings') +@login_required('SETTINGS') def config(): conf = PYLOAD.getConfig() plugin = PYLOAD.getPluginConfig() @@ -327,7 +272,7 @@ def config(): @route("/package_ui.js") -@login_required('see_downloads') +@login_required('LIST') def package_ui(): response.headers['Expires'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + 60 * 60 * 24 * 7)) @@ -336,7 +281,7 @@ def package_ui(): @route("/filemanager_ui.js") -@login_required('see_downloads') +@login_required('LIST') def package_ui(): response.headers['Expires'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(time.time() + 60 * 60 * 24 * 7)) @@ -348,7 +293,7 @@ def package_ui(): @route("/pathchooser") @route("/filechooser/:file#.+#") @route("/pathchooser/:path#.+#") -@login_required('status') +@login_required('STATUS') def path(file="", path=""): if file: type = "file" @@ -438,7 +383,7 @@ def path(file="", path=""): @route("/logs", method="POST") @route("/logs/:item") @route("/logs/:item", method="POST") -@login_required('status') +@login_required('STATUS') def logs(item=-1): s = request.environ.get('beaker.session') @@ -523,14 +468,17 @@ def logs(item=-1): @route("/admin") @route("/admin", method="POST") -@login_required("is_admin") +@login_required("ADMIN") def admin(): user = PYLOAD.getAllUserData() + perms = permlist() + for data in user.itervalues(): data["perms"] = {} get_permission(data["perms"], data["permission"]) data["perms"]["admin"] = True if data["role"] is 0 else False + s = request.environ.get('beaker.session') if request.environ.get('REQUEST_METHOD', "GET") == "POST": for name in user: @@ -541,46 +489,19 @@ def admin(): user[name]["role"] = 1 user[name]["perms"]["admin"] = False - if request.POST.get("%s|add" % name, False): - user[name]["perms"]["add"] = True - else: - user[name]["perms"]["add"] = False - - if request.POST.get("%s|delete" % name, False): - user[name]["perms"]["delete"] = True - else: - user[name]["perms"]["delete"] = False - - if request.POST.get("%s|status" % name, False): - user[name]["perms"]["status"] = True - else: - user[name]["perms"]["status"] = False - - if request.POST.get("%s|see_downloads" % name, False): - user[name]["perms"]["see_downloads"] = True - else: - user[name]["perms"]["see_downloads"] = False - - if request.POST.get("%s|download" % name, False): - user[name]["perms"]["download"] = True - else: - user[name]["perms"]["download"] = False - - if request.POST.get("%s|settings" % name, False): - user[name]["perms"]["settings"] = True - else: - user[name]["perms"]["settings"] = False - - if request.POST.get("%s|filemanager" % name, False): - user[name]["perms"]["filemanager"] = True - else: - user[name]["perms"]["filemanager"] = False + # set all perms to false + for perm in perms: + user[name]["perms"][perm] = False + + + for perm in request.POST.getall("%s|perms" % name): + user[name]["perms"][perm] = True user[name]["permission"] = set_permission(user[name]["perms"]) PYLOAD.setUserPermission(name, user[name]["permission"], user[name]["role"]) - return render_to_response("admin.html", {"users": user}, [pre_processor]) + return render_to_response("admin.html", {"users": user, "permlist": perms}, [pre_processor]) @route("/setup") diff --git a/module/web/templates/default/admin.html b/module/web/templates/default/admin.html index 7b9a8b32d..96c5e7ef3 100644 --- a/module/web/templates/default/admin.html +++ b/module/web/templates/default/admin.html @@ -72,9 +72,8 @@ {% block content %} - {{ _("Note: You can only change permissions for webinterface.") }} {{ _("To add user or change passwords use:") }} <b>python pyLoadCore.py -u</b><br> - {{ _("Important: Admin user have always all permissions! Only Admin user can use other clients like CLI and GUI.") }} + {{ _("Important: Admin user have always all permissions!") }} <form action="" method="POST"> <table class="settable wide"> @@ -89,48 +88,27 @@ {{ _("Admin") }} </th> <th> - {{ _("Add downloads") }} - </th> - <th> - {{ _("Delete downloads") }} - </th> - <th> - {{ _("Change server status") }} - </th> - <th> - {{ _("See queue/collector") }} - </th> - <th> - {{ _("Download from webinterface") }} - </th> - <th> - {{ _("Change settings") }} - </th> - <th> - {{ _("Filemanager") }} + {{ _("Permissions") }} </th> </thead> - {% for name, data in users.iteritems ( ) %} + {% for name, data in users.iteritems() %} <tr> <td>{{ name }}</td> <td><a class="change_password" href="#" id="change_pw|{{name}}">{{ _("change") }}</a></td> <td><input name="{{ name }}|admin" type="checkbox" {% if data.perms.admin %} checked="True" {% endif %}"></td> - <td><input name="{{ name }}|add" type="checkbox" {% if data.perms.add %} checked="True" {% endif %} - "></td> - <td><input name="{{ name }}|delete" type="checkbox" {% if data.perms.delete %} - checked="True" {% endif %}"></td> - <td><input name="{{ name }}|status" type="checkbox" {% if data.perms.status %} - checked="True" {% endif %}"></td> - <td><input name="{{ name }}|see_downloads" type="checkbox" {% if data.perms.see_downloads %} - checked="True" {% endif %}"></td> - <td><input name="{{ name }}|download" type="checkbox" {% if data.perms.download %} - checked="True" {% endif %}"></td> - <td><input name="{{ name }}|settings" type="checkbox" {% if data.perms.settings %} - checked="True" {% endif %}"></td> - <td><input name="{{ name }}|filemanager" type="checkbox" {% if data.perms.filemanager %} - checked="True" {% endif %}"></td> + <td> + <select multiple="multiple" size="{{ permlist|length }}" name="{{ name }}|perms"> + {% for perm in permlist %} + {% if data.perms|getitem(perm) %} + <option selected="selected">{{ perm }}</option> + {% else %} + <option>{{ perm }}</option> + {% endif %} + {% endfor %} + </select> + </td> </tr> {% endfor %} diff --git a/module/web/templates/default/base.html b/module/web/templates/default/base.html index 4057d320c..2987cf081 100644 --- a/module/web/templates/default/base.html +++ b/module/web/templates/default/base.html @@ -257,7 +257,7 @@ function AddBox(){ <div style="clear:both;"></div>
</div>
-{% if perms.status %}
+{% if perms.STATUS %}
<ul id="page-actions2">
<li id="action_play"><a href="#" class="action play" accesskey="o" rel="nofollow">{{_("Start")}}</a></li>
<li id="action_stop"><a href="#" class="action stop" accesskey="o" rel="nofollow">{{_("Stop")}}</a></li>
@@ -266,7 +266,7 @@ function AddBox(){ </ul>
{% endif %}
-{% if perms.see_downloads %}
+{% if perms.LIST %}
<ul id="page-actions">
<li><span class="time">{{_("Download:")}}</span><a id="time" style=" background-color: {% if status.download %}#8ffc25{% else %} #fc6e26{% endif %}; padding-left: 0cm; padding-right: 0.1cm; "> {% if status.download %}{{_("on")}}{% else %}{{_("off")}}{% endif %}</a></li>
<li><span class="reconnect">{{_("Reconnect:")}}</span><a id="reconnect" style=" background-color: {% if status.reconnect %}#8ffc25{% else %} #fc6e26{% endif %}; padding-left: 0cm; padding-right: 0.1cm; "> {% if status.reconnect %}{{_("on")}}{% else %}{{_("off")}}{% endif %}</a></li>
diff --git a/module/web/utils.py b/module/web/utils.py index 39ddb361f..a89c87558 100644 --- a/module/web/utils.py +++ b/module/web/utils.py @@ -20,7 +20,7 @@ from bottle import request, HTTPError, redirect, ServerAdapter from webinterface import env, TEMPLATE -from module.database.UserDatabase import has_permission, PERMS, ROLE +from module.Api import has_permission, PERMS, ROLE def render_to_response(name, args={}, proc=[]): for p in proc: @@ -29,15 +29,11 @@ def render_to_response(name, args={}, proc=[]): t = env.get_template(TEMPLATE + "/" + name) return t.render(**args) + def parse_permissions(session): - perms = {"add": False, - "delete": False, - "status": False, - "see_downloads": False, - "download" : False, - "filemanager" : False, - "settings": False, - "is_admin": False} + perms = dict([(x, False) for x in dir(PERMS) if not x.startswith("_")]) + perms["ADMIN"] = False + perms["is_admin"] = False if not session.get("authenticated", False): return perms @@ -53,31 +49,31 @@ def parse_permissions(session): return perms +def permlist(): + return [x for x in dir(PERMS) if not x.startswith("_") and x != "ALL"] + + def get_permission(perms, p): - perms["add"] = has_permission(p, PERMS.ADD) - perms["delete"] = has_permission(p, PERMS.DELETE) - perms["status"] = has_permission(p, PERMS.STATUS) - perms["see_downloads"] = has_permission(p, PERMS.SEE_DOWNLOADS) - perms["download"] = has_permission(p, PERMS.DOWNLOAD) - perms["settings"] = has_permission(p, PERMS.SETTINGS) - perms["accounts"] = has_permission(p, PERMS.ACCOUNTS) + """Returns a dict with permission key + + :param perms: dictionary + :param p: bits + """ + for name in permlist(): + perms[name] = has_permission(p, getattr(PERMS, name)) + def set_permission(perms): + """generates permission bits from dictionary + + :param perms: dict + """ permission = 0 - if perms["add"]: - permission |= PERMS.ADD - if perms["delete"]: - permission |= PERMS.DELETE - if perms["status"]: - permission |= PERMS.STATUS - if perms["see_downloads"]: - permission |= PERMS.SEE_DOWNLOADS - if perms["download"]: - permission |= PERMS.DOWNLOAD - if perms["settings"]: - permission |= PERMS.SETTINGS - if perms["accounts"]: - permission |= PERMS.ACCOUNTS + for name in dir(PERMS): + if name.startswith("_"): continue + + if name in perms and perms[name]: + permission |= getattr(PERMS, name) return permission @@ -94,11 +90,13 @@ def set_session(request, info): return s + def parse_userdata(session): return {"name": session.get("name", "Anonymous"), "is_admin": True if session.get("role", 1) == 0 else False, "is_authenticated": session.get("authenticated", False)} + def login_required(perm=None): def _dec(func): def _view(*args, **kwargs): @@ -123,14 +121,15 @@ def login_required(perm=None): return _dec + def toDict(obj): ret = {} for att in obj.__slots__: ret[att] = getattr(obj, att) return ret -class CherryPyWSGI(ServerAdapter): +class CherryPyWSGI(ServerAdapter): def run(self, handler): from wsgiserver import CherryPyWSGIServer diff --git a/module/web/webinterface.py b/module/web/webinterface.py index f6ad35138..8f814715f 100644 --- a/module/web/webinterface.py +++ b/module/web/webinterface.py @@ -89,6 +89,7 @@ env.filters["path_make_absolute"] = path_make_absolute env.filters["decode"] = decode env.filters["type"] = lambda x: str(type(x)) env.filters["formatsize"] = formatSize +env.filters["getitem"] = lambda x,y: x.__getitem__(y) if PREFIX: env.filters["url"] = lambda x: x else: diff --git a/pyLoadCore.py b/pyLoadCore.py index bb1bd28f3..b95d6f072 100755 --- a/pyLoadCore.py +++ b/pyLoadCore.py @@ -66,6 +66,8 @@ else: sys.stdout = getwriter(enc)(sys.stdout, errors="replace") # TODO List +# - configurable auth system ldap/mysql +# - cron job like sheduler class Core(object): """pyLoad Core, one tool to rule them all... (the filehosters) :D""" |