changeset 858:7dde76708892

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
author Goffi <goffi@goffi.org>
date Sun, 24 Jan 2016 19:00:56 +0100
parents e17b15f1f260
children 0e9341e537d6
files src/server/server.py
diffstat 1 files changed, 171 insertions(+), 17 deletions(-) [+]
line wrap: on
line diff
--- 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)