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)