Mercurial > libervia-web
comparison src/server/server.py @ 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 | fd438e51bda8 |
children | 0e9341e537d6 |
comparison
equal
deleted
inserted
replaced
857:e17b15f1f260 | 858:7dde76708892 |
---|---|
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 from twisted.application import service | 20 from twisted.application import service |
21 from twisted.internet import reactor, defer | 21 from twisted.internet import reactor, defer |
22 from twisted.web import server | 22 from twisted.web import server |
23 from twisted.web.static import File | 23 from twisted.web import static |
24 from twisted.web.resource import Resource, NoResource, EncodingResourceWrapper | 24 from twisted.web import resource as web_resource |
25 from twisted.web.util import Redirect, redirectTo | 25 from twisted.web import util as web_util |
26 from twisted.web import http | |
26 from twisted.python.components import registerAdapter | 27 from twisted.python.components import registerAdapter |
27 from twisted.python.failure import Failure | 28 from twisted.python.failure import Failure |
28 from twisted.words.protocols.jabber import jid | 29 from twisted.words.protocols.jabber import jid |
29 | 30 |
30 from txjsonrpc.web import jsonrpc | 31 from txjsonrpc.web import jsonrpc |
42 import os.path | 43 import os.path |
43 import sys | 44 import sys |
44 import tempfile | 45 import tempfile |
45 import shutil | 46 import shutil |
46 import uuid | 47 import uuid |
48 import urlparse | |
49 import urllib | |
47 from zope.interface import Interface, Attribute, implements | 50 from zope.interface import Interface, Attribute, implements |
48 from httplib import HTTPS_PORT | 51 from httplib import HTTPS_PORT |
49 import libervia | 52 import libervia |
50 | 53 |
51 try: | 54 try: |
95 def touch(self): | 98 def touch(self): |
96 if not self.__lock: | 99 if not self.__lock: |
97 server.Session.touch(self) | 100 server.Session.touch(self) |
98 | 101 |
99 | 102 |
100 class ProtectedFile(File): | 103 class ProtectedFile(static.File): |
101 """A File class which doens't show directory listing""" | 104 """A static.File class which doens't show directory listing""" |
102 | 105 |
103 def directoryListing(self): | 106 def directoryListing(self): |
104 return NoResource() | 107 return web_resource.NoResource() |
105 | 108 |
109 | |
110 class LiberviaRootResource(ProtectedFile): | |
111 """Specialized resource for Libervia root | |
112 | |
113 handle redirections declared in sat.conf | |
114 """ | |
115 | |
116 def __init__(self, options, *args, **kwargs): | |
117 """ | |
118 @param options(dict): configuration options, same as Libervia.options | |
119 """ | |
120 super(LiberviaRootResource, self).__init__(*args, **kwargs) | |
121 | |
122 ## redirections | |
123 self.redirections = {} | |
124 if options['url_redirections_dict'] and not options['url_redirections_profile']: | |
125 raise ValueError(u"url_redirections_profile need to be filled if you want to use url_redirections_dict") | |
126 | |
127 for old, new in options['url_redirections_dict'].iteritems(): | |
128 if not old or not old.startswith('/'): | |
129 raise ValueError(u"redirected url must start with '/', got {}".format(old)) | |
130 old = self._normalizeURL(old) | |
131 new_url = urlparse.urlsplit(new.encode('utf-8')) | |
132 if new_url.scheme == 'xmpp': | |
133 # XMPP URI | |
134 parsed_qs = urlparse.parse_qs(new_url.geturl()) | |
135 try: | |
136 item = parsed_qs['item'][0] | |
137 if not item: | |
138 raise KeyError | |
139 except (IndexError, KeyError): | |
140 raise NotImplementedError(u"only item for PubSub URI is handler for the moment for url_redirections_dict") | |
141 location = "/blog/{profile}/{item}".format( | |
142 profile=urllib.quote(options['url_redirections_profile'].encode('utf-8')), | |
143 item = urllib.quote_plus(item), | |
144 ).decode('utf-8') | |
145 request_data = self._getRequestData(location) | |
146 elif new_url.scheme in ('', 'http', 'https'): | |
147 # direct redirection | |
148 if new_url.netloc: | |
149 raise NotImplementedError(u"netloc ({netloc}) is not implemented yet for url_redirections_dict, it is not possible to redirect to an external website".format( | |
150 netloc = new_url.netloc)) | |
151 location = urlparse.urlunsplit(('', '', new_url.path, new_url.query, new_url.fragment)) | |
152 request_data = self._getRequestData(location) | |
153 else: | |
154 raise NotImplementedError(u"{scheme}: scheme is not managed for url_redirections_dict".format(scheme=new_url.scheme)) | |
155 self.redirections[old] = request_data | |
156 del options['url_redirections_dict'] | |
157 del options['url_redirections_profile'] | |
158 | |
159 def _normalizeURL(self, url, lower=True): | |
160 """Return URL normalized for self.redirections dict | |
161 | |
162 @param url(unicode): URL to normalize | |
163 @param lower(bool): lower case of url if True | |
164 @return (str): normalized URL | |
165 """ | |
166 if lower: | |
167 url = url.lower() | |
168 return '/'.join((p for p in url.encode('utf-8').split('/') if p)) | |
169 | |
170 def _getRequestData(self, uri): | |
171 """Return data needed to redirect request | |
172 | |
173 @param url(unicode): destination url | |
174 @return (tuple(list[str], str, str, dict): tuple with | |
175 splitted path as in Request.postpath | |
176 uri as in Request.uri | |
177 path as in Request.path | |
178 args as in Request.args | |
179 """ | |
180 uri = uri.encode('utf-8') | |
181 # XXX: we reuse code from twisted.web.http.py here | |
182 # as we need to have the same behaviour | |
183 x = uri.split(b'?', 1) | |
184 | |
185 if len(x) == 1: | |
186 path = uri | |
187 args = {} | |
188 else: | |
189 path, argstring = x | |
190 args = http.parse_qs(argstring, 1) | |
191 | |
192 # XXX: splitted path case must not be changed, as it may be significant | |
193 # (e.g. for blog items) | |
194 return self._normalizeURL(path, lower=False).split('/'), uri, path, args | |
195 | |
196 def getChild(self, name, request): | |
197 resource = super(LiberviaRootResource, self).getChild(name, request) | |
198 | |
199 if isinstance(resource, web_resource.NoResource): | |
200 # if nothing was found, we try our luck with redirections | |
201 # XXX: we want redirections to happen only if everything else failed | |
202 current_url = '/'.join([name] + request.postpath).lower() | |
203 try: | |
204 request_data = self.redirections[current_url] | |
205 except KeyError: | |
206 # no redirection for this url | |
207 pass | |
208 else: | |
209 path_list, uri, path, args = request_data | |
210 try: | |
211 request._redirected | |
212 except AttributeError: | |
213 pass | |
214 else: | |
215 log.warning(D_(u"recursive redirection, please fix this URL:\n{old} ==> {new}").format( | |
216 old=request.uri, | |
217 new=uri, | |
218 )) | |
219 return web_resource.NoResource() | |
220 log.debug(u"Redirecting URL {old} to {new}".format( | |
221 old=request.uri, | |
222 new=uri, | |
223 )) | |
224 # we change the request to reflect the new url | |
225 request._redirected = True # here to avoid recursive redirections | |
226 request.postpath = path_list[1:] | |
227 request.uri = uri | |
228 request.path = path | |
229 request.args = args | |
230 # and we start again to look for a child with the new url | |
231 return self.getChildWithDefault(path_list[0], request) | |
232 | |
233 return resource | |
234 | |
235 def createSimilarFile(self, path): | |
236 # XXX: this method need to be overriden to avoid recreating a LiberviaRootResource | |
237 | |
238 f = LiberviaRootResource.__base__(path, self.defaultType, self.ignoredExts, self.registry) | |
239 # refactoring by steps, here - constructor should almost certainly take these | |
240 f.processors = self.processors | |
241 f.indexNames = self.indexNames[:] | |
242 f.childNotFound = self.childNotFound | |
243 return f | |
106 | 244 |
107 class SATActionIDHandler(object): | 245 class SATActionIDHandler(object): |
108 """Manage SàT action action_id lifecycle""" | 246 """Manage SàT action action_id lifecycle""" |
109 ID_LIFETIME = 30 # after this time (in seconds), action_id will be suppressed and action result will be ignored | 247 ID_LIFETIME = 30 # after this time (in seconds), action_id will be suppressed and action result will be ignored |
110 | 248 |
1010 | 1148 |
1011 | 1149 |
1012 class SignalHandler(jsonrpc.JSONRPC): | 1150 class SignalHandler(jsonrpc.JSONRPC): |
1013 | 1151 |
1014 def __init__(self, sat_host): | 1152 def __init__(self, sat_host): |
1015 Resource.__init__(self) | 1153 web_resource.Resource.__init__(self) |
1016 self.register = None | 1154 self.register = None |
1017 self.sat_host = sat_host | 1155 self.sat_host = sat_host |
1018 self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred) | 1156 self.signalDeferred = {} # dict of deferred (key: profile, value: Deferred) |
1019 # which manages the long polling HTTP request with signals | 1157 # which manages the long polling HTTP request with signals |
1020 self.queue = {} | 1158 self.queue = {} |
1130 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) # pylint: disable=E1103 | 1268 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc')) # pylint: disable=E1103 |
1131 self.request = request | 1269 self.request = request |
1132 return jsonrpc.JSONRPC.render(self, request) | 1270 return jsonrpc.JSONRPC.render(self, request) |
1133 | 1271 |
1134 | 1272 |
1135 class UploadManager(Resource): | 1273 class UploadManager(web_resource.Resource): |
1136 """This class manage the upload of a file | 1274 """This class manage the upload of a file |
1137 It redirect the stream to SàT core backend""" | 1275 It redirect the stream to SàT core backend""" |
1138 isLeaf = True | 1276 isLeaf = True |
1139 NAME = 'path' # name use by the FileUpload | 1277 NAME = 'path' # name use by the FileUpload |
1140 | 1278 |
1239 self.html_dir = os.path.join(self.options['data_dir'], C.HTML_DIR) | 1377 self.html_dir = os.path.join(self.options['data_dir'], C.HTML_DIR) |
1240 self.themes_dir = os.path.join(self.options['data_dir'], C.THEMES_DIR) | 1378 self.themes_dir = os.path.join(self.options['data_dir'], C.THEMES_DIR) |
1241 | 1379 |
1242 self._cleanup = [] | 1380 self._cleanup = [] |
1243 | 1381 |
1244 root = ProtectedFile(self.html_dir) | 1382 root = LiberviaRootResource(self.options, self.html_dir) |
1245 | 1383 |
1246 self.signal_handler = SignalHandler(self) | 1384 self.signal_handler = SignalHandler(self) |
1247 _register = Register(self) | 1385 _register = Register(self) |
1248 _upload_radiocol = UploadManagerRadioCol(self) | 1386 _upload_radiocol = UploadManagerRadioCol(self) |
1249 _upload_avatar = UploadManagerAvatar(self) | 1387 _upload_avatar = UploadManagerAvatar(self) |
1276 'roomLeft', 'roomUserChangedNick', 'chatStateReceived']: | 1414 'roomLeft', 'roomUserChangedNick', 'chatStateReceived']: |
1277 self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin") | 1415 self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin") |
1278 self.media_dir = self.bridge.getConfig('', 'media_dir') | 1416 self.media_dir = self.bridge.getConfig('', 'media_dir') |
1279 self.local_dir = self.bridge.getConfig('', 'local_dir') | 1417 self.local_dir = self.bridge.getConfig('', 'local_dir') |
1280 | 1418 |
1419 ## URLs ## | |
1281 def putChild(path, resource): | 1420 def putChild(path, resource): |
1282 """Add a child to the root resource""" | 1421 """Add a child to the root resource""" |
1283 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) | 1422 # FIXME: check that no information is leaked (c.f. https://twistedmatrix.com/documents/current/web/howto/using-twistedweb.html#request-encoders) |
1284 root.putChild(path, EncodingResourceWrapper(resource, [server.GzipEncoderFactory()])) | 1423 root.putChild(path, web_resource.EncodingResourceWrapper(resource, [server.GzipEncoderFactory()])) |
1285 | 1424 |
1286 putChild('', Redirect(C.LIBERVIA_MAIN_PAGE)) | 1425 # we redirect root url to libevia's dynamic part |
1287 putChild('test', Redirect('libervia_test.html')) | 1426 putChild('', web_util.Redirect(C.LIBERVIA_MAIN_PAGE)) |
1427 | |
1428 # JSON APIs | |
1288 putChild('json_signal_api', self.signal_handler) | 1429 putChild('json_signal_api', self.signal_handler) |
1289 putChild('json_api', MethodHandler(self)) | 1430 putChild('json_api', MethodHandler(self)) |
1290 putChild('register_api', _register) | 1431 putChild('register_api', _register) |
1432 | |
1433 # files upload | |
1291 putChild('upload_radiocol', _upload_radiocol) | 1434 putChild('upload_radiocol', _upload_radiocol) |
1292 putChild('upload_avatar', _upload_avatar) | 1435 putChild('upload_avatar', _upload_avatar) |
1436 | |
1437 # static pages | |
1293 putChild('blog', MicroBlog(self)) | 1438 putChild('blog', MicroBlog(self)) |
1294 putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) | 1439 putChild(C.THEMES_URL, ProtectedFile(self.themes_dir)) |
1440 | |
1441 # media dirs | |
1295 putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) | 1442 putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir)) |
1296 putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR))) | 1443 putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR))) |
1297 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 | 1444 |
1298 wrapped = EncodingResourceWrapper(root, [server.GzipEncoderFactory()]) | 1445 # special |
1446 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 | |
1447 # pyjamas tests, redirected only for dev versions | |
1448 if self.version[-1] == 'D': | |
1449 putChild('test', web_util.Redirect('libervia_test.html')) | |
1450 | |
1451 | |
1452 wrapped = web_resource.EncodingResourceWrapper(root, [server.GzipEncoderFactory()]) | |
1299 self.site = server.Site(wrapped) | 1453 self.site = server.Site(wrapped) |
1300 self.site.sessionFactory = LiberviaSession | 1454 self.site.sessionFactory = LiberviaSession |
1301 | 1455 |
1302 self.bridge.getReady(lambda: self.initialised.callback(None), | 1456 self.bridge.getReady(lambda: self.initialised.callback(None), |
1303 lambda failure: self.initialised.errback(Exception(failure))) | 1457 lambda failure: self.initialised.errback(Exception(failure))) |
1469 """ | 1623 """ |
1470 self.stop() | 1624 self.stop() |
1471 sys.exit(exit_code or 0) | 1625 sys.exit(exit_code or 0) |
1472 | 1626 |
1473 | 1627 |
1474 class RedirectToHTTPS(Resource): | 1628 class RedirectToHTTPS(web_resource.Resource): |
1475 | 1629 |
1476 def __init__(self, old_port, new_port): | 1630 def __init__(self, old_port, new_port): |
1477 Resource.__init__(self) | 1631 web_resource.Resource.__init__(self) |
1478 self.isLeaf = True | 1632 self.isLeaf = True |
1479 self.old_port = old_port | 1633 self.old_port = old_port |
1480 self.new_port = new_port | 1634 self.new_port = new_port |
1481 | 1635 |
1482 def render(self, request): | 1636 def render(self, request): |
1483 netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port) | 1637 netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port) |
1484 url = "https://" + netloc + request.uri | 1638 url = "https://" + netloc + request.uri |
1485 return redirectTo(url, request) | 1639 return web_util.redirectTo(url, request) |
1486 | 1640 |
1487 | 1641 |
1488 registerAdapter(SATSession, server.Session, ISATSession) | 1642 registerAdapter(SATSession, server.Session, ISATSession) |