# HG changeset patch # User Goffi # Date 1453658456 -3600 # Node ID 7dde76708892dd5b3aba83ce4376ff5d1ac46fe1 # Parent e17b15f1f260164fd5c352e6f4280ceb5a627fe3 server: URL redirections + misc: - root resource is now a special class LiberviaRootResource which handle redirections - redirections are checked only if all other childs didn't return a resource - for now, redirection handle xmpp scheme, and direct redirections for internal links - use imported module instead of imported classes directly for twisted.web hierarchy - 'test' URL is now redirected only in development versions diff -r e17b15f1f260 -r 7dde76708892 src/server/server.py --- a/src/server/server.py Sun Jan 24 18:47:41 2016 +0100 +++ b/src/server/server.py Sun Jan 24 19:00:56 2016 +0100 @@ -20,9 +20,10 @@ from twisted.application import service from twisted.internet import reactor, defer from twisted.web import server -from twisted.web.static import File -from twisted.web.resource import Resource, NoResource, EncodingResourceWrapper -from twisted.web.util import Redirect, redirectTo +from twisted.web import static +from twisted.web import resource as web_resource +from twisted.web import util as web_util +from twisted.web import http from twisted.python.components import registerAdapter from twisted.python.failure import Failure from twisted.words.protocols.jabber import jid @@ -44,6 +45,8 @@ import tempfile import shutil import uuid +import urlparse +import urllib from zope.interface import Interface, Attribute, implements from httplib import HTTPS_PORT import libervia @@ -97,12 +100,147 @@ server.Session.touch(self) -class ProtectedFile(File): - """A File class which doens't show directory listing""" +class ProtectedFile(static.File): + """A static.File class which doens't show directory listing""" def directoryListing(self): - return NoResource() + return web_resource.NoResource() + + +class LiberviaRootResource(ProtectedFile): + """Specialized resource for Libervia root + + handle redirections declared in sat.conf + """ + + def __init__(self, options, *args, **kwargs): + """ + @param options(dict): configuration options, same as Libervia.options + """ + super(LiberviaRootResource, self).__init__(*args, **kwargs) + + ## redirections + self.redirections = {} + if options['url_redirections_dict'] and not options['url_redirections_profile']: + raise ValueError(u"url_redirections_profile need to be filled if you want to use url_redirections_dict") + + for old, new in options['url_redirections_dict'].iteritems(): + if not old or not old.startswith('/'): + raise ValueError(u"redirected url must start with '/', got {}".format(old)) + old = self._normalizeURL(old) + new_url = urlparse.urlsplit(new.encode('utf-8')) + if new_url.scheme == 'xmpp': + # XMPP URI + parsed_qs = urlparse.parse_qs(new_url.geturl()) + try: + item = parsed_qs['item'][0] + if not item: + raise KeyError + except (IndexError, KeyError): + raise NotImplementedError(u"only item for PubSub URI is handler for the moment for url_redirections_dict") + location = "/blog/{profile}/{item}".format( + profile=urllib.quote(options['url_redirections_profile'].encode('utf-8')), + item = urllib.quote_plus(item), + ).decode('utf-8') + request_data = self._getRequestData(location) + elif new_url.scheme in ('', 'http', 'https'): + # direct redirection + if new_url.netloc: + raise NotImplementedError(u"netloc ({netloc}) is not implemented yet for url_redirections_dict, it is not possible to redirect to an external website".format( + netloc = new_url.netloc)) + location = urlparse.urlunsplit(('', '', new_url.path, new_url.query, new_url.fragment)) + request_data = self._getRequestData(location) + else: + raise NotImplementedError(u"{scheme}: scheme is not managed for url_redirections_dict".format(scheme=new_url.scheme)) + self.redirections[old] = request_data + del options['url_redirections_dict'] + del options['url_redirections_profile'] + + def _normalizeURL(self, url, lower=True): + """Return URL normalized for self.redirections dict + + @param url(unicode): URL to normalize + @param lower(bool): lower case of url if True + @return (str): normalized URL + """ + if lower: + url = url.lower() + return '/'.join((p for p in url.encode('utf-8').split('/') if p)) + + def _getRequestData(self, uri): + """Return data needed to redirect request + @param url(unicode): destination url + @return (tuple(list[str], str, str, dict): tuple with + splitted path as in Request.postpath + uri as in Request.uri + path as in Request.path + args as in Request.args + """ + uri = uri.encode('utf-8') + # XXX: we reuse code from twisted.web.http.py here + # as we need to have the same behaviour + x = uri.split(b'?', 1) + + if len(x) == 1: + path = uri + args = {} + else: + path, argstring = x + args = http.parse_qs(argstring, 1) + + # XXX: splitted path case must not be changed, as it may be significant + # (e.g. for blog items) + return self._normalizeURL(path, lower=False).split('/'), uri, path, args + + def getChild(self, name, request): + resource = super(LiberviaRootResource, self).getChild(name, request) + + if isinstance(resource, web_resource.NoResource): + # if nothing was found, we try our luck with redirections + # XXX: we want redirections to happen only if everything else failed + current_url = '/'.join([name] + request.postpath).lower() + try: + request_data = self.redirections[current_url] + except KeyError: + # no redirection for this url + pass + else: + path_list, uri, path, args = request_data + try: + request._redirected + except AttributeError: + pass + else: + log.warning(D_(u"recursive redirection, please fix this URL:\n{old} ==> {new}").format( + old=request.uri, + new=uri, + )) + return web_resource.NoResource() + log.debug(u"Redirecting URL {old} to {new}".format( + old=request.uri, + new=uri, + )) + # we change the request to reflect the new url + request._redirected = True # here to avoid recursive redirections + request.postpath = path_list[1:] + request.uri = uri + request.path = path + request.args = args + # and we start again to look for a child with the new url + return self.getChildWithDefault(path_list[0], request) + + return resource + + def createSimilarFile(self, path): + # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource + + f = LiberviaRootResource.__base__(path, self.defaultType, self.ignoredExts, self.registry) + # refactoring by steps, here - constructor should almost certainly take these + f.processors = self.processors + f.indexNames = self.indexNames[:] + f.childNotFound = self.childNotFound + return f class SATActionIDHandler(object): """Manage SàT action action_id lifecycle""" @@ -1012,7 +1150,7 @@ class SignalHandler(jsonrpc.JSONRPC): def __init__(self, sat_host): - Resource.__init__(self) + web_resource.Resource.__init__(self) self.register = None self.sat_host = sat_host self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred) @@ -1132,7 +1270,7 @@ return jsonrpc.JSONRPC.render(self, request) -class UploadManager(Resource): +class UploadManager(web_resource.Resource): """This class manage the upload of a file It redirect the stream to SàT core backend""" isLeaf = True @@ -1241,7 +1379,7 @@ self._cleanup = [] - root = ProtectedFile(self.html_dir) + root = LiberviaRootResource(self.options, self.html_dir) self.signal_handler = SignalHandler(self) _register = Register(self) @@ -1278,24 +1416,40 @@ self.media_dir = self.bridge.getConfig('', 'media_dir') self.local_dir = self.bridge.getConfig('', 'local_dir') + ## URLs ## def putChild(path, resource): """Add a child to the root resource""" # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) - root.putChild(path, EncodingResourceWrapper(resource, [server.GzipEncoderFactory()])) + root.putChild(path, web_resource.EncodingResourceWrapper(resource, [server.GzipEncoderFactory()])) - putChild('', Redirect(C.LIBERVIA_MAIN_PAGE)) - putChild('test', Redirect('libervia_test.html')) + # we redirect root url to libevia's dynamic part + putChild('', web_util.Redirect(C.LIBERVIA_MAIN_PAGE)) + + # JSON APIs putChild('json_signal_api', self.signal_handler) putChild('json_api', MethodHandler(self)) putChild('register_api', _register) + + # files upload putChild('upload_radiocol', _upload_radiocol) putChild('upload_avatar', _upload_avatar) + + # static pages putChild('blog', MicroBlog(self)) putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) + + # media dirs putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR))) - putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg")) # We cheat for PoC because we know we are on the same host, so we use directly upload dir - wrapped = EncodingResourceWrapper(root, [server.GzipEncoderFactory()]) + + # special + putChild('radiocol', ProtectedFile(_upload_radiocol.getTmpDir(), defaultType="audio/ogg")) # FIXME: We cheat for PoC because we know we are on the same host, so we use directly upload dir + # pyjamas tests, redirected only for dev versions + if self.version[-1] == 'D': + putChild('test', web_util.Redirect('libervia_test.html')) + + + wrapped = web_resource.EncodingResourceWrapper(root, [server.GzipEncoderFactory()]) self.site = server.Site(wrapped) self.site.sessionFactory = LiberviaSession @@ -1471,10 +1625,10 @@ sys.exit(exit_code or 0) -class RedirectToHTTPS(Resource): +class RedirectToHTTPS(web_resource.Resource): def __init__(self, old_port, new_port): - Resource.__init__(self) + web_resource.Resource.__init__(self) self.isLeaf = True self.old_port = old_port self.new_port = new_port @@ -1482,7 +1636,7 @@ def render(self, request): netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port) url = "https://" + netloc + request.uri - return redirectTo(url, request) + return web_util.redirectTo(url, request) registerAdapter(SATSession, server.Session, ISATSession)