Mercurial > libervia-web
diff src/server/server.py @ 984:f0fc28b3bd1e
server: moved LiberviaPage code in its own module
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 17 Nov 2017 12:10:56 +0100 |
parents | bcacf970f970 |
children | 64826e69f365 |
line wrap: on
line diff
--- a/src/server/server.py Fri Nov 17 11:01:34 2017 +0100 +++ b/src/server/server.py Fri Nov 17 12:10:56 2017 +0100 @@ -39,7 +39,6 @@ from sat.tools import utils from sat.tools.common import regex from sat.tools.common import template -from sat.tools.common import uri as common_uri import re import glob @@ -52,6 +51,8 @@ import urllib from httplib import HTTPS_PORT import libervia +from libervia.server.pages import LiberviaPage +from libervia.server.utils import quote try: import OpenSSL @@ -68,11 +69,6 @@ DATA_DIR_DEFAULT = OPT_PARAMETERS_BOTH = OPT_PARAMETERS_CFG = coerceDataDir = None -def quote(value): - """shortcut to quote an unicode value for URL""" - return urllib.quote_plus(value.encode('utf-8')).replace('%40','@') - - class LiberviaSession(server.Session): sessionTimeout = C.SESSION_TIMEOUT @@ -1366,643 +1362,6 @@ return ("setAvatar", filepath, profile) -class LiberviaPage(web_resource.Resource): - isLeaf = True # we handle subpages ourself - named_pages = {} - uri_callbacks = {} - pages_redirects = {} - - def __init__(self, host, root_dir, url, name=None, redirect=None, access=None, parse_url=None, - prepare_render=None, render=None, template=None, on_data_post=None): - """initiate LiberviaPages - - LiberviaPages are the main resources of Libervia, using easy to set python files - The arguments are the variables found in page_meta.py - @param host(Libervia): the running instance of Libervia - @param root_dir(unicode): aboslute file path of the page - @param url(unicode): relative URL to the page - this URL may not be valid, as pages may require path arguments - @param name(unicode, None): if not None, a unique name to identify the page - can then be used for e.g. redirection - "/" is not allowed in names (as it can be used to construct URL paths) - @param redirect(unicode, None): if not None, this page will be redirected. A redirected - parameter is used as in self.pageRedirect. parse_url will not be skipped - using this redirect parameter is called "full redirection" - using self.pageRedirect is called "partial redirection" (because some rendering method - can still be used, e.g. parse_url) - @param access(unicode, None): permission needed to access the page - None means public access. - Pages inherit from parent pages: e.g. if a "settings" page is restricted to admins, - and if "settings/blog" is public, it still can only be accessed by admins. - see C.PAGES_ACCESS_* for details - @param parse_url(callable, None): if set it will be called to handle the URL path - after this method, the page will be rendered if noting is left in path (request.postpath) - else a the request will be transmitted to a subpage - @param prepare_render(callable, None): if set, will be used to prepare the rendering - that often means gathering data using the bridge - @param render(callable, None): if not template is set, this method will be called and - what it returns will be rendered. - This method is mutually exclusive with template and must return a unicode string. - @param template(unicode, None): path to the template to render. - This method is mutually exclusive with render - @param on_data_post(callable, None): method to call when data is posted - None if not post is handled - on_data_post can return a string with following value: - - C.POST_NO_CONFIRM: confirm flag will not be set - """ - - web_resource.Resource.__init__(self) - self.host = host - self.root_dir = root_dir - self.url = url - self.name = name - if name is not None: - if name in self.named_pages: - raise exceptions.ConflictError(_(u'a Libervia page named "{}" already exists'.format(name))) - if u'/' in name: - raise ValueError(_(u'"/" is not allowed in page names')) - if not name: - raise ValueError(_(u"a page name can't be empty")) - self.named_pages[name] = self - if access is None: - access = C.PAGES_ACCESS_PUBLIC - if access not in (C.PAGES_ACCESS_PUBLIC, C.PAGES_ACCESS_PROFILE, C.PAGES_ACCESS_NONE): - raise NotImplementedError(_(u"{} access is not implemented yet").format(access)) - self.access = access - if redirect is not None: - # only page access and name make sense in case of full redirection - # so we check that rendering methods/values are not set - if not all(lambda x: x is not None - for x in (parse_url, prepare_render, render, template)): - raise ValueError(_(u"you can't use full page redirection with other rendering method," - u"check self.pageRedirect if you need to use them")) - self.redirect = redirect - else: - self.redirect = None - self.parse_url = parse_url - self.prepare_render = prepare_render - self.template = template - self.render_method = render - self.on_data_post = on_data_post - if access == C.PAGES_ACCESS_NONE: - # none pages just return a 404, no further check is needed - return - if template is None: - if not callable(render): - log.error(_(u"render must be implemented and callable if template is not set")) - else: - if render is not None: - log.error(_(u"render can't be used at the same time as template")) - if parse_url is not None and not callable(parse_url): - log.error(_(u"parse_url must be a callable")) - - @classmethod - def importPages(cls, host, parent=None, path=None): - """Recursively import Libervia pages""" - if path is None: - path = [] - if parent is None: - root_dir = os.path.join(os.path.dirname(libervia.__file__), C.PAGES_DIR) - parent = host - else: - root_dir = parent.root_dir - for d in os.listdir(root_dir): - dir_path = os.path.join(root_dir, d) - if not os.path.isdir(dir_path): - continue - meta_path = os.path.join(dir_path, C.PAGES_META_FILE) - if os.path.isfile(meta_path): - page_data = {} - new_path = path + [d] - # we don't want to force the presence of __init__.py - # so we use execfile instead of import. - # TODO: when moved to Python 3, __init__.py is not mandatory anymore - # so we can switch to import - execfile(meta_path, page_data) - resource = LiberviaPage( - host, - dir_path, - u'/' + u'/'.join(new_path), - name=page_data.get('name'), - redirect=page_data.get('redirect'), - access=page_data.get('access'), - parse_url=page_data.get('parse_url'), - prepare_render=page_data.get('prepare_render'), - render=page_data.get('render'), - template=page_data.get('template'), - on_data_post=page_data.get('on_data_post')) - parent.putChild(d, resource) - log.info(u"Added /{path} page".format(path=u'[...]/'.join(new_path))) - if 'uri_handlers' in page_data: - if not isinstance(page_data, dict): - log.error(_(u'uri_handlers must be a dict')) - else: - for uri_tuple, cb_name in page_data['uri_handlers'].iteritems(): - if len(uri_tuple) != 2 or not isinstance(cb_name, basestring): - log.error(_(u"invalid uri_tuple")) - continue - log.info(_(u'setting {}/{} URIs handler').format(*uri_tuple)) - try: - cb = page_data[cb_name] - except KeyError: - log.error(_(u'missing {name} method to handle {1}/{2}').format( - name = cb_name, *uri_tuple)) - continue - else: - cls.registerURI(uri_tuple, cb, new_path) - - LiberviaPage.importPages(host, resource, new_path) - - @classmethod - def registerURI(cls, uri_tuple, get_uri_cb, pre_path): - """register a URI handler - - @param uri_tuple(tuple[unicode, unicode]): type or URIs handler - type/subtype as returned by tools/common/parseXMPPUri - @param get_uri_cb(callable): method which take uri_data dict as only argument - and return path with correct arguments relative to page itself - @param pre_path(list[unicode]): prefix path to reference the handler page - """ - if uri_tuple in cls.uri_callbacks: - log.info(_(u"{}/{} URIs are already handled, replacing by the new handler").format(*uri_tuple)) - cls.uri_callbacks[uri_tuple] = {u'callback': get_uri_cb, - u'pre_path': pre_path} - - def getPagePathFromURI(self, uri): - """Retrieve page URL from xmpp: URI - - @param uri(unicode): URI with a xmpp: scheme - @return (unicode,None): absolute path (starting from root "/") to page handling the URI - None is returned if not page has been registered for this URI - """ - uri_data = common_uri.parseXMPPUri(uri) - try: - callback_data = self.uri_callbacks[uri_data['type'], uri_data.get('sub_type')] - except KeyError: - return - else: - url = os.path.join(u'/', u'/'.join(callback_data['pre_path']), callback_data['callback'](self, uri_data)) - return url - - @classmethod - def getPageByName(cls, name): - """retrieve page instance from its name - - @param name(unicode): name of the page - @return (LiberviaPage): page instance - @raise KeyError: the page doesn't exist - """ - return cls.named_pages[name] - - def getPageRedirectURL(self, request, page_name=u'login', url=None): - """generate URL for a page with redirect_url parameter set - - mainly used for login page with redirection to current page - @param request(server.Request): current HTTP request - @param page_name(unicode): name of the page to go - @param url(None, unicode): url to redirect to - None to use request path (i.e. current page) - @return (unicode): URL to use - """ - return u'{root_url}?redirect_url={redirect_url}'.format( - root_url = self.getPageByName(page_name).url, - redirect_url=urllib.quote_plus(request.uri) if url is None else url.encode('utf-8')) - - def getURL(self, *args): - """retrieve URL of the page set arguments - - *args(list[unicode]): argument to add to the URL as path elements - """ - url_args = [quote(a) for a in args] - - if self.name is not None and self.name in self.pages_redirects: - # we check for redirection - redirect_data = self.pages_redirects[self.name] - args_hash = tuple(args) - for limit in xrange(len(args)+1): - current_hash = args_hash[:limit] - if current_hash in redirect_data: - url_base = redirect_data[current_hash] - remaining = args[limit:] - remaining_url = '/'.join(remaining) - return os.path.join('/', url_base, remaining_url) - - return os.path.join(self.url, *url_args) - - def getSubPageURL(self, request, page_name, *args): - """retrieve a page in direct children and build its URL according to request - - request's current path is used as base (at current parsing point, - i.e. it's more prepath than path). - Requested page is checked in children and an absolute URL is then built - by the resulting combination. - This method is useful to construct absolute URLs for children instead of - using relative path, which may not work in subpages, and are linked to the - names of directories (i.e. relative URL will break if subdirectory is renamed - while getSubPageURL won't as long as page_name is consistent). - Also, request.path is used, keeping real path used by user, - and potential redirections. - @param request(server.Request): current HTTP request - @param page_name(unicode): name of the page to retrieve - it must be a direct children of current page - @param *args(list[unicode]): arguments to add as path elements - @return unicode: absolute URL to the sub page - """ - # we get url in the following way (splitting request.path instead of using - # request.prepath) because request.prepath may have been modified by - # redirection (if redirection args have been specified), while path reflect - # the real request - - # we ignore empty path elements (i.e. double '/' or '/' at the end) - path_elts = [p for p in request.path.split('/') if p] - - if request.postpath: - if not request.postpath[-1]: - # we remove trailing slash - request.postpath = request.postpath[:-1] - if request.postpath: - # getSubPageURL must return subpage from the point where - # the it is called, so we have to remove remanining - # path elements - path_elts = path_elts[:-len(request.postpath)] - - current_url = '/' + '/'.join(path_elts).decode('utf-8') - - for path, child in self.children.iteritems(): - try: - child_name = child.name - except AttributeError: - # LiberviaPage have a name, but maybe this is an other Resource - continue - if child_name == page_name: - return os.path.join(u'/', current_url, path, *args) - raise exceptions.NotFound(_(u'requested sub page has not been found')) - - def getChildWithDefault(self, path, request): - # we handle children ourselves - raise exceptions.InternalError(u"this method should not be used with LiberviaPage") - - def nextPath(self, request): - """get next URL path segment, and update request accordingly - - will move first segment of postpath in prepath - @param request(server.Request): current HTTP request - @return (unicode): unquoted segment - @raise IndexError: there is no segment left - """ - pathElement = request.postpath.pop(0) - request.prepath.append(pathElement) - return urllib.unquote(pathElement).decode('utf-8') - - def HTTPRedirect(self, request, url): - """redirect to an URL using HTTP redirection - - @param request(server.Request): current HTTP request - @param url(unicode): url to redirect to - """ - - web_util.redirectTo(url.encode('utf-8'), request) - request.finish() - raise failure.Failure(exceptions.CancelError(u'HTTP redirection is used')) - - def redirectOrContinue(self, request, redirect_arg=u'redirect_url'): - """helper method to redirect a page to an url given as arg - - if the arg is not present, the page will continue normal workflow - @param request(server.Request): current HTTP request - @param redirect_arg(unicode): argument to use to get redirection URL - @interrupt: redirect the page to requested URL - @interrupt pageError(C.HTTP_BAD_REQUEST): empty or non local URL is used - """ - try: - url = self.getPostedData(request, 'redirect_url') - except KeyError: - pass - else: - # a redirection is requested - if not url or url[0] != u'/': - # we only want local urls - self.pageError(request, C.HTTP_BAD_REQUEST) - else: - self.HTTPRedirect(request, url) - - def pageRedirect(self, page_path, request, skip_parse_url=True): - """redirect a page to a named page - - the workflow will continue with the workflow of the named page, - skipping named page's parse_url method if it exist. - If you want to do a HTTP redirection, use HTTPRedirect - @param page_path(unicode): path to page (elements are separated by "/"): - if path starts with a "/": - path is a full path starting from root - else: - - first element is name as registered in name variable - - following element are subpages path - e.g.: "blog" redirect to page named "blog" - "blog/atom.xml" redirect to atom.xml subpage of "blog" - "/common/blog/atom.xml" redirect to the page at the fiven full path - @param request(server.Request): current HTTP request - @param skip_parse_url(bool): if True, parse_url method on redirect page will be skipped - @raise KeyError: there is no known page with this name - """ - # FIXME: render non LiberviaPage resources - path = page_path.rstrip(u'/').split(u'/') - if not path[0]: - redirect_page = self.host.root - else: - redirect_page = self.named_pages[path[0]] - - for subpage in path[1:]: - if redirect_page is self.host.root: - redirect_page = redirect_page.children[subpage] - else: - redirect_page = redirect_page.original.children[subpage] - - redirect_page.renderPage(request, skip_parse_url=True) - raise failure.Failure(exceptions.CancelError(u'page redirection is used')) - - def pageError(self, request, code=C.HTTP_NOT_FOUND): - """generate an error page and terminate the request - - @param request(server.Request): HTTP request - @param core(int): error code to use - """ - template = u'error/' + unicode(code) + '.html' - - request.setResponseCode(code) - - rendered = self.host.renderer.render( - template, - root_path = '/templates/', - error_code = code, - **request.template_data) - - self.writeData(rendered, request) - raise failure.Failure(exceptions.CancelError(u'error page is used')) - - def writeData(self, data, request): - """write data to transport and finish the request""" - if data is None: - self.pageError(request) - request.write(data.encode('utf-8')) - request.finish() - - def _subpagesHandler(self, dummy, request): - """render subpage if suitable - - this method checks if there is still an unmanaged part of the path - and check if it corresponds to a subpage. If so, it render the subpage - else it render a NoResource. - If there is no unmanaged part of the segment, current page workflow is pursued - """ - if request.postpath: - subpage = self.nextPath(request) - try: - child = self.children[subpage] - except KeyError: - self.pageError(request) - else: - child.render(request) - raise failure.Failure(exceptions.CancelError(u'subpage page is used')) - - def _prepare_render(self, dummy, request): - return defer.maybeDeferred(self.prepare_render, self, request) - - def _render_method(self, dummy, request): - return defer.maybeDeferred(self.render_method, self, request) - - def _render_template(self, dummy, request): - template_data = request.template_data - - # if confirm variable is set in case of successfuly data post - session_data = self.host.getSessionData(request, session_iface.ISATSession) - if session_data.popPageFlag(self, C.FLAG_CONFIRM): - template_data[u'confirm'] = True - - return self.host.renderer.render( - self.template, - root_path = '/templates/', - media_path = '/' + C.MEDIA_DIR, - **template_data) - - def _renderEb(self, failure_, request): - """don't raise error on CancelError""" - failure_.trap(exceptions.CancelError) - - def _internalError(self, failure_, request): - """called if an error is not catched""" - log.error(_(u"Uncatched error for HTTP request on {url}: {msg}").format( - url = request.URLPath(), - msg = failure_)) - self.pageError(request, C.HTTP_INTERNAL_ERROR) - - def _on_data_post_redirect(self, ret, request): - """called when page's on_data_post has been done successfuly - - This will do a Post/Redirect/Get pattern. - this method redirect to the same page or to request.data['post_redirect_page'] - post_redirect_page can be either a page or a tuple with page as first item, then a list of unicode arguments to append to the url. - if post_redirect_page is not used, initial request.uri (i.e. the same page as where the data have been posted) will be used for redirection. - HTTP status code "See Other" (303) is used as it is the recommanded code in this case. - @param ret(None, unicode, iterable): on_data_post return value - see LiberviaPage.__init__ on_data_post docstring - """ - if ret is None: - ret = () - elif isinstance(ret, basestring): - ret = (ret,) - else: - ret = tuple(ret) - raise NotImplementedError(_(u'iterable in on_data_post return value is not used yet')) - session_data = self.host.getSessionData(request, session_iface.ISATSession) - request_data = self.getRData(request) - if 'post_redirect_page' in request_data: - redirect_page_data = request_data['post_redirect_page'] - if isinstance(redirect_page_data, tuple): - redirect_page = redirect_page_data[0] - redirect_page_args = redirect_page_data[1:] - redirect_uri = redirect_page.getURL(*redirect_page_args) - else: - redirect_page = redirect_page_data - redirect_uri = redirect_page.url - else: - redirect_page = self - redirect_uri = request.uri - - if not C.POST_NO_CONFIRM in ret: - session_data.setPageFlag(redirect_page, C.FLAG_CONFIRM) - request.setResponseCode(C.HTTP_SEE_OTHER) - request.setHeader("location", redirect_uri) - request.finish() - raise failure.Failure(exceptions.CancelError(u'Post/Redirect/Get is used')) - - def _on_data_post(self, dummy, request): - csrf_token = self.host.getSessionData(request, session_iface.ISATSession).csrf_token - try: - given_csrf = self.getPostedData(request, u'csrf_token') - except KeyError: - given_csrf = None - if given_csrf is None or given_csrf != csrf_token: - log.warning(_(u"invalid CSRF token, hack attempt? URL: {url}, IP: {ip}").format( - url=request.uri, - ip=request.getClientIP())) - self.pageError(request, C.HTTP_UNAUTHORIZED) - d = defer.maybeDeferred(self.on_data_post, self, request) - d.addCallback(self._on_data_post_redirect, request) - return d - - def getPostedData(self, request, keys, multiple=False): - """get data from a POST request and decode it - - @param request(server.Request): request linked to the session - @param keys(unicode, iterable[unicode]): name of the value(s) to get - unicode to get one value - iterable to get more than one - @param multiple(bool): True if multiple values are possible/expected - if False, the first value is returned - @return (iterator[unicode], list[iterator[unicode], unicode, list[unicode]): values received for this(these) key(s) - @raise KeyError: one specific key has been requested, and it is missing - """ - if isinstance(keys, basestring): - keys = [keys] - get_first = True - else: - get_first = False - - ret = [] - for key in keys: - gen = (urllib.unquote(v).decode('utf-8') for v in request.args.get(key,[])) - if multiple: - ret.append(gen) - else: - try: - ret.append(next(gen)) - except StopIteration: - raise KeyError(key) - - return ret[0] if get_first else ret - - def getAllPostedData(self, request, except_=()): - """get all posted data - - @param request(server.Request): request linked to the session - @param except_(iterable[unicode]): key of values to ignore - csrf_token will always be ignored - @return (dict[unicode, list[unicode]]): post values - """ - except_ = tuple(except_) + (u'csrf_token',) - ret = {} - for key, values in request.args.iteritems(): - key = urllib.unquote(key).decode('utf-8') - if key in except_: - continue - ret[key] = [urllib.unquote(v).decode('utf-8') for v in values] - return ret - - def getProfile(self, request): - """helper method to easily get current profile - - @return (unicode, None): current profile - None if no profile session is started - """ - sat_session = self.host.getSessionData(request, session_iface.ISATSession) - return sat_session.profile - - def getRData(self, request): - """helper method to get request data dict - - this dictionnary if for the request only, it is not saved in session - It is mainly used to pass data between pages/methods called during request workflow - @return (dict): request data - """ - try: - return request.data - except AttributeError: - request.data = {} - return request.data - - def _checkAccess(self, data, request): - """Check access according to self.access - - if access is not granted, show a HTTP_UNAUTHORIZED pageError and stop request, - else return data (so it can be inserted in deferred chain - """ - if self.access == C.PAGES_ACCESS_PUBLIC: - pass - elif self.access == C.PAGES_ACCESS_PROFILE: - profile = self.getProfile(request) - if not profile: - # no session started - if not self.host.options["allow_registration"]: - # registration not allowed, access is not granted - self.pageError(request, C.HTTP_UNAUTHORIZED) - else: - # registration allowed, we redirect to login page - login_url = self.getPageRedirectURL(request) - self.HTTPRedirect(request, login_url) - - return data - - def renderPage(self, request, skip_parse_url=False): - """Main method to handle the workflow of a LiberviaPage""" - # template_data are the variables passed to template - if not hasattr(request, 'template_data'): - session_data = self.host.getSessionData(request, session_iface.ISATSession) - csrf_token = session_data.csrf_token - request.template_data = {u'csrf_token': csrf_token} - - # XXX: here is the code which need to be executed once - # at the beginning of the request hanling - if request.postpath and not request.postpath[-1]: - # we don't differenciate URLs finishing with '/' or not - del request.postpath[-1] - - d = defer.Deferred() - d.addCallback(self._checkAccess, request) - - if self.redirect is not None: - self.pageRedirect(self.redirect, request, skip_parse_url=False) - - if self.parse_url is not None and not skip_parse_url: - d.addCallback(self.parse_url, request) - - d.addCallback(self._subpagesHandler, request) - - if request.method not in (C.HTTP_METHOD_GET, C.HTTP_METHOD_POST): - # only HTTP GET and POST are handled so far - d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) - - if request.method == C.HTTP_METHOD_POST: - if self.on_data_post is None: - # if we don't have on_data_post, the page was not expecting POST - # so we return an error - d.addCallback(lambda dummy: self.pageError(request, C.HTTP_BAD_REQUEST)) - else: - d.addCallback(self._on_data_post, request) - # by default, POST follow normal behaviour after on_data_post is called - # this can be changed by a redirection or other method call in on_data_post - - if self.prepare_render: - d.addCallback(self._prepare_render, request) - - if self.template: - d.addCallback(self._render_template, request) - elif self.render_method: - d.addCallback(self._render_method, request) - - d.addCallback(self.writeData, request) - d.addErrback(self._renderEb, request) - d.addErrback(self._internalError, request) - d.callback(self) - return server.NOT_DONE_YET - - def render_GET(self, request): - return self.renderPage(request) - - def render_POST(self, request): - return self.renderPage(request) - - class Libervia(service.Service): def __init__(self, options):