comparison src/server/server.py @ 449:981ed669d3b3

/!\ reorganize all the file hierarchy, move the code and launching script to src: - browser_side --> src/browser - public --> src/browser_side/public - libervia.py --> src/browser/libervia_main.py - libervia_server --> src/server - libervia_server/libervia.sh --> src/libervia.sh - twisted --> src/twisted - new module src/common - split constants.py in 3 files: - src/common/constants.py - src/browser/constants.py - src/server/constants.py - output --> html (generated by pyjsbuild during the installation) - new option/parameter "data_dir" (-d) to indicates the directory containing html and server_css - setup.py installs libervia to the following paths: - src/common --> <LIB>/libervia/common - src/server --> <LIB>/libervia/server - src/twisted --> <LIB>/twisted - html --> <SHARE>/libervia/html - server_side --> <SHARE>libervia/server_side - LIBERVIA_INSTALL environment variable takes 2 new options with prompt confirmation: - clean: remove previous installation directories - purge: remove building and previous installation directories You may need to update your sat.conf and/or launching script to update the following options/parameters: - ssl_certificate - data_dir
author souliane <souliane@mailoo.org>
date Tue, 20 May 2014 06:41:16 +0200
parents libervia_server/__init__.py@14c35f7f1ef5
children 1a0cec9b0f1e
comparison
equal deleted inserted replaced
448:14c35f7f1ef5 449:981ed669d3b3
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
3
4 # Libervia: a Salut à Toi frontend
5 # Copyright (C) 2011, 2012, 2013, 2014 Jérôme Poisson <goffi@goffi.org>
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 from twisted.application import service
21 from twisted.internet import glib2reactor
22 glib2reactor.install()
23 from twisted.internet import reactor, defer
24 from twisted.web import server
25 from twisted.web.static import File
26 from twisted.web.resource import Resource, NoResource
27 from twisted.web.util import Redirect, redirectTo
28 from twisted.python.components import registerAdapter
29 from twisted.python.failure import Failure
30 from twisted.words.protocols.jabber.jid import JID
31
32 from txjsonrpc.web import jsonrpc
33 from txjsonrpc import jsonrpclib
34
35 from sat.core.log import getLogger
36 log = getLogger(__name__)
37 from sat_frontends.bridge.DBus import DBusBridgeFrontend, BridgeExceptionNoService
38 from sat.core.i18n import _, D_
39 from sat.tools.xml_tools import paramsXML2XMLUI
40
41 import re
42 import glob
43 import os.path
44 import sys
45 import tempfile
46 import shutil
47 import uuid
48 from zope.interface import Interface, Attribute, implements
49 from xml.dom import minidom
50 from httplib import HTTPS_PORT
51
52 try:
53 import OpenSSL
54 from twisted.internet import ssl
55 ssl_available = True
56 except:
57 ssl_available = False
58
59 from libervia.server.constants import Const as C
60 from libervia.server.blog import MicroBlog
61
62
63 class ISATSession(Interface):
64 profile = Attribute("Sat profile")
65 jid = Attribute("JID associated with the profile")
66
67
68 class SATSession(object):
69 implements(ISATSession)
70 def __init__(self, session):
71 self.profile = None
72 self.jid = None
73
74
75 class LiberviaSession(server.Session):
76 sessionTimeout = C.TIMEOUT
77
78 def __init__(self, *args, **kwargs):
79 self.__lock = False
80 server.Session.__init__(self, *args, **kwargs)
81
82 def lock(self):
83 """Prevent session from expiring"""
84 self.__lock = True
85 self._expireCall.reset(sys.maxint)
86
87 def unlock(self):
88 """Allow session to expire again, and touch it"""
89 self.__lock = False
90 self.touch()
91
92 def touch(self):
93 if not self.__lock:
94 server.Session.touch(self)
95
96 class ProtectedFile(File):
97 """A File class which doens't show directory listing"""
98
99 def directoryListing(self):
100 return NoResource()
101
102 class SATActionIDHandler(object):
103 """Manage SàT action action_id lifecycle"""
104 ID_LIFETIME = 30 #after this time (in seconds), action_id will be suppressed and action result will be ignored
105
106 def __init__(self):
107 self.waiting_ids = {}
108
109 def waitForId(self, callback, action_id, profile, *args, **kwargs):
110 """Wait for an action result
111 @param callback: method to call when action gave a result back
112 @param action_id: action_id to wait for
113 @param profile: %(doc_profile)s
114 @param *args: additional argument to pass to callback
115 @param **kwargs: idem"""
116 action_tuple = (action_id, profile)
117 self.waiting_ids[action_tuple] = (callback, args, kwargs)
118 reactor.callLater(self.ID_LIFETIME, self.purgeID, action_tuple)
119
120 def purgeID(self, action_tuple):
121 """Called when an action_id has not be handled in time"""
122 if action_tuple in self.waiting_ids:
123 log.warning ("action of action_id %s [%s] has not been managed, action_id is now ignored" % action_tuple)
124 del self.waiting_ids[action_tuple]
125
126 def actionResultCb(self, answer_type, action_id, data, profile):
127 """Manage the actionResult signal"""
128 action_tuple = (action_id, profile)
129 if action_tuple in self.waiting_ids:
130 callback, args, kwargs = self.waiting_ids[action_tuple]
131 del self.waiting_ids[action_tuple]
132 callback(answer_type, action_id, data, *args, **kwargs)
133
134 class JSONRPCMethodManager(jsonrpc.JSONRPC):
135
136 def __init__(self, sat_host):
137 jsonrpc.JSONRPC.__init__(self)
138 self.sat_host=sat_host
139
140 def asyncBridgeCall(self, method_name, *args, **kwargs):
141 """Call an asynchrone bridge method and return a deferred
142 @param method_name: name of the method as a unicode
143 @return: a deferred which trigger the result
144
145 """
146 d = defer.Deferred()
147
148 def _callback(*args):
149 if not args:
150 d.callback(None)
151 else:
152 if len(args) != 1:
153 Exception("Multiple return arguments not supported")
154 d.callback(args[0])
155
156 def _errback(result):
157 d.errback(Failure(jsonrpclib.Fault(C.ERRNUM_BRIDGE_ERRBACK, unicode(result))))
158
159 kwargs["callback"] = _callback
160 kwargs["errback"] = _errback
161 getattr(self.sat_host.bridge, method_name)(*args, **kwargs)
162 return d
163
164
165 class MethodHandler(JSONRPCMethodManager):
166
167 def __init__(self, sat_host):
168 JSONRPCMethodManager.__init__(self, sat_host)
169 self.authorized_params = None
170
171 def render(self, request):
172 self.session = request.getSession()
173 profile = ISATSession(self.session).profile
174 if not profile:
175 #user is not identified, we return a jsonrpc fault
176 parsed = jsonrpclib.loads(request.content.read())
177 fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
178 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
179 return jsonrpc.JSONRPC.render(self, request)
180
181 def jsonrpc_getProfileJid(self):
182 """Return the jid of the profile"""
183 sat_session = ISATSession(self.session)
184 profile = sat_session.profile
185 sat_session.jid = JID(self.sat_host.bridge.getParamA("JabberID", "Connection", profile_key=profile))
186 return sat_session.jid.full()
187
188 def jsonrpc_disconnect(self):
189 """Disconnect the profile"""
190 sat_session = ISATSession(self.session)
191 profile = sat_session.profile
192 self.sat_host.bridge.disconnect(profile)
193
194 def jsonrpc_getContacts(self):
195 """Return all passed args."""
196 profile = ISATSession(self.session).profile
197 return self.sat_host.bridge.getContacts(profile)
198
199 def jsonrpc_addContact(self, entity, name, groups):
200 """Subscribe to contact presence, and add it to the given groups"""
201 profile = ISATSession(self.session).profile
202 self.sat_host.bridge.addContact(entity, profile)
203 self.sat_host.bridge.updateContact(entity, name, groups, profile)
204
205 def jsonrpc_delContact(self, entity):
206 """Remove contact from contacts list"""
207 profile = ISATSession(self.session).profile
208 self.sat_host.bridge.delContact(entity, profile)
209
210 def jsonrpc_updateContact(self, entity, name, groups):
211 """Update contact's roster item"""
212 profile = ISATSession(self.session).profile
213 self.sat_host.bridge.updateContact(entity, name, groups, profile)
214
215 def jsonrpc_subscription(self, sub_type, entity, name, groups):
216 """Confirm (or infirm) subscription,
217 and setup user roster in case of subscription"""
218 profile = ISATSession(self.session).profile
219 self.sat_host.bridge.subscription(sub_type, entity, profile)
220 if sub_type == 'subscribed':
221 self.sat_host.bridge.updateContact(entity, name, groups, profile)
222
223 def jsonrpc_getWaitingSub(self):
224 """Return list of room already joined by user"""
225 profile = ISATSession(self.session).profile
226 return self.sat_host.bridge.getWaitingSub(profile)
227
228 def jsonrpc_setStatus(self, presence, status):
229 """Change the presence and/or status
230 @param presence: value from ("", "chat", "away", "dnd", "xa")
231 @param status: any string to describe your status
232 """
233 profile = ISATSession(self.session).profile
234 self.sat_host.bridge.setPresence('', presence, {'': status}, profile)
235
236
237 def jsonrpc_sendMessage(self, to_jid, msg, subject, type_, options={}):
238 """send message"""
239 profile = ISATSession(self.session).profile
240 return self.asyncBridgeCall("sendMessage", to_jid, msg, subject, type_, options, profile)
241
242 def jsonrpc_sendMblog(self, type_, dest, text, extra={}):
243 """ Send microblog message
244 @param type_: one of "PUBLIC", "GROUP"
245 @param dest: destinees (list of groups, ignored for "PUBLIC")
246 @param text: microblog's text
247 """
248 profile = ISATSession(self.session).profile
249 extra['allow_comments'] = 'True'
250
251 if not type_: # auto-detect
252 type_ = "PUBLIC" if dest == [] else "GROUP"
253
254 if type_ in ("PUBLIC", "GROUP") and text:
255 if type_ == "PUBLIC":
256 #This text if for the public microblog
257 print "sending public blog"
258 return self.sat_host.bridge.sendGroupBlog("PUBLIC", [], text, extra, profile)
259 else:
260 print "sending group blog"
261 dest = dest if isinstance(dest, list) else [dest]
262 return self.sat_host.bridge.sendGroupBlog("GROUP", dest, text, extra, profile)
263 else:
264 raise Exception("Invalid data")
265
266 def jsonrpc_deleteMblog(self, pub_data, comments):
267 """Delete a microblog node
268 @param pub_data: a tuple (service, comment node identifier, item identifier)
269 @param comments: comments node identifier (for main item) or False
270 """
271 profile = ISATSession(self.session).profile
272 return self.sat_host.bridge.deleteGroupBlog(pub_data, comments if comments else '', profile)
273
274 def jsonrpc_updateMblog(self, pub_data, comments, message, extra={}):
275 """Modify a microblog node
276 @param pub_data: a tuple (service, comment node identifier, item identifier)
277 @param comments: comments node identifier (for main item) or False
278 @param message: new message
279 @param extra: dict which option name as key, which can be:
280 - allow_comments: True to accept an other level of comments, False else (default: False)
281 - rich: if present, contain rich text in currently selected syntax
282 """
283 profile = ISATSession(self.session).profile
284 if comments:
285 extra['allow_comments'] = 'True'
286 return self.sat_host.bridge.updateGroupBlog(pub_data, comments if comments else '', message, extra, profile)
287
288 def jsonrpc_sendMblogComment(self, node, text, extra={}):
289 """ Send microblog message
290 @param node: url of the comments node
291 @param text: comment
292 """
293 profile = ISATSession(self.session).profile
294 if node and text:
295 return self.sat_host.bridge.sendGroupBlogComment(node, text, extra, profile)
296 else:
297 raise Exception("Invalid data")
298
299 def jsonrpc_getMblogs(self, publisher_jid, item_ids):
300 """Get specified microblogs posted by a contact
301 @param publisher_jid: jid of the publisher
302 @param item_ids: list of microblogs items IDs
303 @return list of microblog data (dict)"""
304 profile = ISATSession(self.session).profile
305 d = self.asyncBridgeCall("getGroupBlogs", publisher_jid, item_ids, profile)
306 return d
307
308 def jsonrpc_getMblogsWithComments(self, publisher_jid, item_ids):
309 """Get specified microblogs posted by a contact and their comments
310 @param publisher_jid: jid of the publisher
311 @param item_ids: list of microblogs items IDs
312 @return list of couple (microblog data, list of microblog data)"""
313 profile = ISATSession(self.session).profile
314 d = self.asyncBridgeCall("getGroupBlogsWithComments", publisher_jid, item_ids, profile)
315 return d
316
317 def jsonrpc_getLastMblogs(self, publisher_jid, max_item):
318 """Get last microblogs posted by a contact
319 @param publisher_jid: jid of the publisher
320 @param max_item: number of items to ask
321 @return list of microblog data (dict)"""
322 profile = ISATSession(self.session).profile
323 d = self.asyncBridgeCall("getLastGroupBlogs", publisher_jid, max_item, profile)
324 return d
325
326 def jsonrpc_getMassiveLastMblogs(self, publishers_type, publishers_list, max_item):
327 """Get lasts microblogs posted by several contacts at once
328 @param publishers_type: one of "ALL", "GROUP", "JID"
329 @param publishers_list: list of publishers type (empty list of all, list of groups or list of jids)
330 @param max_item: number of items to ask
331 @return: dictionary key=publisher's jid, value=list of microblog data (dict)"""
332 profile = ISATSession(self.session).profile
333 d = self.asyncBridgeCall("getMassiveLastGroupBlogs", publishers_type, publishers_list, max_item, profile)
334 self.sat_host.bridge.massiveSubscribeGroupBlogs(publishers_type, publishers_list, profile)
335 return d
336
337 def jsonrpc_getMblogComments(self, service, node):
338 """Get all comments of given node
339 @param service: jid of the service hosting the node
340 @param node: comments node
341 """
342 profile = ISATSession(self.session).profile
343 d = self.asyncBridgeCall("getGroupBlogComments", service, node, profile)
344 return d
345
346
347 def jsonrpc_getPresenceStatuses(self):
348 """Get Presence information for connected contacts"""
349 profile = ISATSession(self.session).profile
350 return self.sat_host.bridge.getPresenceStatuses(profile)
351
352 def jsonrpc_getHistory(self, from_jid, to_jid, size, between):
353 """Return history for the from_jid/to_jid couple"""
354 sat_session = ISATSession(self.session)
355 profile = sat_session.profile
356 sat_jid = sat_session.jid
357 if not sat_jid:
358 log.error("No jid saved for this profile")
359 return {}
360 if JID(from_jid).userhost() != sat_jid.userhost() and JID(to_jid).userhost() != sat_jid.userhost():
361 log.error("Trying to get history from a different jid, maybe a hack attempt ?")
362 return {}
363 d = self.asyncBridgeCall("getHistory", from_jid, to_jid, size, between, profile)
364 def show(result_dbus):
365 result = []
366 for line in result_dbus:
367 #XXX: we have to do this stupid thing because Python D-Bus use its own types instead of standard types
368 # and txJsonRPC doesn't accept D-Bus types, resulting in a empty query
369 timestamp, from_jid, to_jid, message, mess_type, extra = line
370 result.append((float(timestamp), unicode(from_jid), unicode(to_jid), unicode(message), unicode(mess_type), dict(extra)))
371 return result
372 d.addCallback(show)
373 return d
374
375 def jsonrpc_joinMUC(self, room_jid, nick):
376 """Join a Multi-User Chat room
377 @room_jid: leave empty string to generate a unique name
378 """
379 profile = ISATSession(self.session).profile
380 try:
381 if room_jid != "":
382 room_jid = JID(room_jid).userhost()
383 except:
384 log.warning('Invalid room jid')
385 return
386 d = self.asyncBridgeCall("joinMUC", room_jid, nick, {}, profile)
387 return d
388
389 def jsonrpc_inviteMUC(self, contact_jid, room_jid):
390 """Invite a user to a Multi-User Chat room"""
391 profile = ISATSession(self.session).profile
392 try:
393 room_jid = JID(room_jid).userhost()
394 except:
395 log.warning('Invalid room jid')
396 return
397 room_id = room_jid.split("@")[0]
398 service = room_jid.split("@")[1]
399 self.sat_host.bridge.inviteMUC(contact_jid, service, room_id, {}, profile)
400
401 def jsonrpc_mucLeave(self, room_jid):
402 """Quit a Multi-User Chat room"""
403 profile = ISATSession(self.session).profile
404 try:
405 room_jid = JID(room_jid)
406 except:
407 log.warning('Invalid room jid')
408 return
409 self.sat_host.bridge.mucLeave(room_jid.userhost(), profile)
410
411 def jsonrpc_getRoomsJoined(self):
412 """Return list of room already joined by user"""
413 profile = ISATSession(self.session).profile
414 return self.sat_host.bridge.getRoomsJoined(profile)
415
416 def jsonrpc_launchTarotGame(self, other_players, room_jid=""):
417 """Create a room, invite the other players and start a Tarot game
418 @param room_jid: leave empty string to generate a unique room name
419 """
420 profile = ISATSession(self.session).profile
421 try:
422 if room_jid != "":
423 room_jid = JID(room_jid).userhost()
424 except:
425 log.warning('Invalid room jid')
426 return
427 self.sat_host.bridge.tarotGameLaunch(other_players, room_jid, profile)
428
429 def jsonrpc_getTarotCardsPaths(self):
430 """Give the path of all the tarot cards"""
431 _join = os.path.join
432 _media_dir = _join(self.sat_host.media_dir,'')
433 return map(lambda x: _join(C.MEDIA_DIR, x[len(_media_dir):]), glob.glob(_join(_media_dir, C.CARDS_DIR, '*_*.png')));
434
435 def jsonrpc_tarotGameReady(self, player, referee):
436 """Tell to the server that we are ready to start the game"""
437 profile = ISATSession(self.session).profile
438 self.sat_host.bridge.tarotGameReady(player, referee, profile)
439
440 def jsonrpc_tarotGamePlayCards(self, player_nick, referee, cards):
441 """Tell to the server the cards we want to put on the table"""
442 profile = ISATSession(self.session).profile
443 self.sat_host.bridge.tarotGamePlayCards(player_nick, referee, cards, profile)
444
445 def jsonrpc_launchRadioCollective(self, invited, room_jid=""):
446 """Create a room, invite people, and start a radio collective
447 @param room_jid: leave empty string to generate a unique room name
448 """
449 profile = ISATSession(self.session).profile
450 try:
451 if room_jid != "":
452 room_jid = JID(room_jid).userhost()
453 except:
454 log.warning('Invalid room jid')
455 return
456 self.sat_host.bridge.radiocolLaunch(invited, room_jid, profile)
457
458 def jsonrpc_getEntityData(self, jid, keys):
459 """Get cached data for an entit
460 @param jid: jid of contact from who we want data
461 @param keys: name of data we want (list)
462 @return: requested data"""
463 profile = ISATSession(self.session).profile
464 return self.sat_host.bridge.getEntityData(jid, keys, profile)
465
466 def jsonrpc_getCard(self, jid):
467 """Get VCard for entiry
468 @param jid: jid of contact from who we want data
469 @return: id to retrieve the profile"""
470 profile = ISATSession(self.session).profile
471 return self.sat_host.bridge.getCard(jid, profile)
472
473 def jsonrpc_getAccountDialogUI(self):
474 """Get the dialog for managing user account
475 @return: XML string of the XMLUI"""
476 profile = ISATSession(self.session).profile
477 return self.sat_host.bridge.getAccountDialogUI(profile)
478
479 def jsonrpc_getParamsUI(self):
480 """Return the parameters XML for profile"""
481 profile = ISATSession(self.session).profile
482 d = self.asyncBridgeCall("getParams", C.SECURITY_LIMIT, C.APP_NAME, profile)
483
484 def setAuthorizedParams(params_xml):
485 if self.authorized_params is None:
486 self.authorized_params = {}
487 for cat in minidom.parseString(params_xml.encode('utf-8')).getElementsByTagName("category"):
488 params = cat.getElementsByTagName("param")
489 params_list = [param.getAttribute("name") for param in params]
490 self.authorized_params[cat.getAttribute("name")] = params_list
491 if self.authorized_params:
492 return params_xml
493 else:
494 return None
495
496 d.addCallback(setAuthorizedParams)
497
498 d.addCallback(lambda params_xml: paramsXML2XMLUI(params_xml) if params_xml else "")
499
500 return d
501
502 def jsonrpc_asyncGetParamA(self, param, category, attribute="value"):
503 """Return the parameter value for profile"""
504 profile = ISATSession(self.session).profile
505 d = self.asyncBridgeCall("asyncGetParamA", param, category, attribute, C.SECURITY_LIMIT, profile_key=profile)
506 return d
507
508 def jsonrpc_setParam(self, name, value, category):
509 profile = ISATSession(self.session).profile
510 if category in self.authorized_params and name in self.authorized_params[category]:
511 return self.sat_host.bridge.setParam(name, value, category, C.SECURITY_LIMIT, profile)
512 else:
513 log.warning("Trying to set parameter '%s' in category '%s' without authorization!!!"
514 % (name, category))
515
516 def jsonrpc_launchAction(self, callback_id, data):
517 #FIXME: any action can be launched, this can be a huge security issue if callback_id can be guessed
518 # a security system with authorised callback_id must be implemented, similar to the one for authorised params
519 profile = ISATSession(self.session).profile
520 d = self.asyncBridgeCall("launchAction", callback_id, data, profile)
521 return d
522
523 def jsonrpc_chatStateComposing(self, to_jid_s):
524 """Call the method to process a "composing" state.
525 @param to_jid_s: contact the user is composing to
526 """
527 profile = ISATSession(self.session).profile
528 self.sat_host.bridge.chatStateComposing(to_jid_s, profile)
529
530 def jsonrpc_getNewAccountDomain(self):
531 """@return: the domain for new account creation"""
532 d = self.asyncBridgeCall("getNewAccountDomain")
533 return d
534
535 def jsonrpc_confirmationAnswer(self, confirmation_id, result, answer_data):
536 """Send the user's answer to any previous 'askConfirmation' signal"""
537 profile = ISATSession(self.session).profile
538 self.sat_host.bridge.confirmationAnswer(confirmation_id, result, answer_data, profile)
539
540 def jsonrpc_syntaxConvert(self, text, syntax_from=C.SYNTAX_XHTML, syntax_to=C.SYNTAX_CURRENT):
541 """ Convert a text between two syntaxes
542 @param text: text to convert
543 @param syntax_from: source syntax (e.g. "markdown")
544 @param syntax_to: dest syntax (e.g.: "XHTML")
545 @param safe: clean resulting XHTML to avoid malicious code if True (forced here)
546 @return: converted text """
547 profile = ISATSession(self.session).profile
548 return self.sat_host.bridge.syntaxConvert(text, syntax_from, syntax_to, True, profile)
549
550
551 class Register(JSONRPCMethodManager):
552 """This class manage the registration procedure with SàT
553 It provide an api for the browser, check password and setup the web server"""
554
555 def __init__(self, sat_host):
556 JSONRPCMethodManager.__init__(self, sat_host)
557 self.profiles_waiting={}
558 self.request=None
559
560 def getWaitingRequest(self, profile):
561 """Tell if a profile is trying to log in"""
562 if self.profiles_waiting.has_key(profile):
563 return self.profiles_waiting[profile]
564 else:
565 return None
566
567 def render(self, request):
568 """
569 Render method with some hacks:
570 - if login is requested, try to login with form data
571 - except login, every method is jsonrpc
572 - user doesn't need to be authentified for explicitely listed methods, but must be for all others
573 """
574 if request.postpath == ['login']:
575 return self.loginOrRegister(request)
576 _session = request.getSession()
577 parsed = jsonrpclib.loads(request.content.read())
578 method = parsed.get("method")
579 if method not in ['isRegistered', 'registerParams', 'getMenus']:
580 #if we don't call these methods, we need to be identified
581 profile = ISATSession(_session).profile
582 if not profile:
583 #user is not identified, we return a jsonrpc fault
584 fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
585 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
586 self.request = request
587 return jsonrpc.JSONRPC.render(self, request)
588
589 def loginOrRegister(self, request):
590 """This method is called with the POST information from the registering form.
591
592 @param request: request of the register form
593 @return: a constant indicating the state:
594 - BAD REQUEST: something is wrong in the request (bad arguments)
595 - a return value from self._loginAccount or self._registerNewAccount
596 """
597 try:
598 submit_type = request.args['submit_type'][0]
599 except KeyError:
600 return "BAD REQUEST"
601
602 if submit_type == 'register':
603 return self._registerNewAccount(request)
604 elif submit_type == 'login':
605 return self._loginAccount(request)
606 return Exception('Unknown submit type')
607
608 def _loginAccount(self, request):
609 """Try to authenticate the user with the request information.
610 @param request: request of the register form
611 @return: a constant indicating the state:
612 - BAD REQUEST: something is wrong in the request (bad arguments)
613 - AUTH ERROR: either the profile (login) or the password is wrong
614 - ALREADY WAITING: a request has already been submitted for this profile
615 - server.NOT_DONE_YET: the profile is being processed, the return
616 value will be given by self._logged or self._logginError
617 """
618 try:
619 login_ = request.args['login'][0]
620 password_ = request.args['login_password'][0]
621 except KeyError:
622 return "BAD REQUEST"
623
624 if login_.startswith('@'):
625 raise Exception('No profile_key allowed')
626
627 profile_check = self.sat_host.bridge.getProfileName(login_)
628 if not profile_check or profile_check != login_ or not password_:
629 # profiles with empty passwords are restricted to local frontends
630 return "AUTH ERROR"
631
632 if login_ in self.profiles_waiting:
633 return "ALREADY WAITING"
634
635 def auth_eb(ignore=None):
636 self.__cleanWaiting(login_)
637 log.info("Profile %s doesn't exist or the submitted password is wrong" % login_)
638 request.write("AUTH ERROR")
639 request.finish()
640
641 self.profiles_waiting[login_] = request
642 d = self.asyncBridgeCall("asyncConnect", login_, password_)
643 d.addCallbacks(lambda connected: self._logged(login_, request) if connected else None, auth_eb)
644
645 return server.NOT_DONE_YET
646
647 def _registerNewAccount(self, request):
648 """Create a new account, or return error
649 @param request: request of the register form
650 @return: a constant indicating the state:
651 - BAD REQUEST: something is wrong in the request (bad arguments)
652 - REGISTRATION: new account has been successfully registered
653 - ALREADY EXISTS: the given profile already exists
654 - INTERNAL or 'Unknown error (...)'
655 - server.NOT_DONE_YET: the profile is being processed, the return
656 value will be given later (one of those previously described)
657 """
658 try:
659 profile = login = request.args['register_login'][0]
660 password = request.args['register_password'][0]
661 email = request.args['email'][0]
662 except KeyError:
663 return "BAD REQUEST"
664 if not re.match(r'^[a-z0-9_-]+$', login, re.IGNORECASE) or \
665 not re.match(r'^.+@.+\..+', email, re.IGNORECASE) or \
666 len(password) < C.PASSWORD_MIN_LENGTH:
667 return "BAD REQUEST"
668
669 def registered(result):
670 request.write('REGISTRATION')
671 request.finish()
672
673 def registeringError(failure):
674 reason = failure.value.faultString
675 if reason == "ConflictError":
676 request.write('ALREADY EXISTS')
677 elif reason == "InternalError":
678 request.write('INTERNAL')
679 else:
680 log.error('Unknown registering error: %s' % (reason,))
681 request.write('Unknown error (%s)' % reason)
682 request.finish()
683
684 d = self.asyncBridgeCall("registerSatAccount", email, password, profile)
685 d.addCallback(registered)
686 d.addErrback(registeringError)
687 return server.NOT_DONE_YET
688
689 def __cleanWaiting(self, login):
690 """Remove login from waiting queue"""
691 try:
692 del self.profiles_waiting[login]
693 except KeyError:
694 pass
695
696 def _logged(self, profile, request):
697 """Set everything when a user just logged in
698
699 @param profile
700 @param request
701 @return: a constant indicating the state:
702 - LOGGED
703 - SESSION_ACTIVE
704 """
705 self.__cleanWaiting(profile)
706 _session = request.getSession()
707 sat_session = ISATSession(_session)
708 if sat_session.profile:
709 log.error(('/!\\ Session has already a profile, this should NEVER happen!'))
710 request.write('SESSION_ACTIVE')
711 request.finish()
712 return
713 sat_session.profile = profile
714 self.sat_host.prof_connected.add(profile)
715
716 def onExpire():
717 log.info("Session expired (profile=%s)" % (profile,))
718 try:
719 #We purge the queue
720 del self.sat_host.signal_handler.queue[profile]
721 except KeyError:
722 pass
723 #and now we disconnect the profile
724 self.sat_host.bridge.disconnect(profile)
725
726 _session.notifyOnExpire(onExpire)
727
728 request.write('LOGGED')
729 request.finish()
730
731 def _logginError(self, login, request, error_type):
732 """Something went wrong during logging in
733 @return: error
734 """
735 self.__cleanWaiting(login)
736 return error_type
737
738 def jsonrpc_isConnected(self):
739 _session = self.request.getSession()
740 profile = ISATSession(_session).profile
741 return self.sat_host.bridge.isConnected(profile)
742
743 def jsonrpc_connect(self):
744 _session = self.request.getSession()
745 profile = ISATSession(_session).profile
746 if self.profiles_waiting.has_key(profile):
747 raise jsonrpclib.Fault(1,'Already waiting') #FIXME: define some standard error codes for libervia
748 self.profiles_waiting[profile] = self.request
749 self.sat_host.bridge.connect(profile)
750 return server.NOT_DONE_YET
751
752 def jsonrpc_isRegistered(self):
753 """
754 @return: a couple (registered, message) with:
755 - registered: True if the user is already registered, False otherwise
756 - message: a security warning message if registered is False *and* the connection is unsecure, None otherwise
757 """
758 _session = self.request.getSession()
759 profile = ISATSession(_session).profile
760 if bool(profile):
761 return (True, None)
762 return (False, self.__getSecurityWarning())
763
764 def jsonrpc_registerParams(self):
765 """Register the frontend specific parameters"""
766 params = """
767 <params>
768 <individual>
769 <category name="%(category_name)s" label="%(category_label)s">
770 <param name="%(param_name)s" label="%(param_label)s" value="false" type="bool" security="0"/>
771 </category>
772 </individual>
773 </params>
774 """ % {
775 'category_name': C.ENABLE_UNIBOX_KEY,
776 'category_label': _(C.ENABLE_UNIBOX_KEY),
777 'param_name': C.ENABLE_UNIBOX_PARAM,
778 'param_label': _(C.ENABLE_UNIBOX_PARAM)
779 }
780
781 self.sat_host.bridge.paramsRegisterApp(params, C.SECURITY_LIMIT, C.APP_NAME)
782
783 def jsonrpc_getMenus(self):
784 """Return the parameters XML for profile"""
785 # XXX: we put this method in Register because we get menus before being logged
786 return self.sat_host.bridge.getMenus('', C.SECURITY_LIMIT)
787
788 def __getSecurityWarning(self):
789 """@return: a security warning message, or None if the connection is secure"""
790 if self.request.URLPath().scheme == 'https' or not self.sat_host.security_warning:
791 return None
792 text = D_("You are about to connect to an unsecured service.")
793 if self.sat_host.connection_type == 'both':
794 new_port = (':%s' % self.sat_host.port_https_ext) if self.sat_host.port_https_ext != HTTPS_PORT else ''
795 url = "https://%s" % self.request.URLPath().netloc.replace(':%s' % self.sat_host.port, new_port)
796 text += D_('<br />Secure version of this website: <a href="%(url)s">%(url)s</a>') % {'url': url}
797 return text
798
799
800 class SignalHandler(jsonrpc.JSONRPC):
801
802 def __init__(self, sat_host):
803 Resource.__init__(self)
804 self.register=None
805 self.sat_host=sat_host
806 self.signalDeferred = {}
807 self.queue = {}
808
809 def plugRegister(self, register):
810 self.register = register
811
812 def jsonrpc_getSignals(self):
813 """Keep the connection alive until a signal is received, then send it
814 @return: (signal, *signal_args)"""
815 _session = self.request.getSession()
816 profile = ISATSession(_session).profile
817 if profile in self.queue: #if we have signals to send in queue
818 if self.queue[profile]:
819 return self.queue[profile].pop(0)
820 else:
821 #the queue is empty, we delete the profile from queue
822 del self.queue[profile]
823 _session.lock() #we don't want the session to expire as long as this connection is active
824 def unlock(signal, profile):
825 _session.unlock()
826 try:
827 source_defer = self.signalDeferred[profile]
828 if source_defer.called and source_defer.result[0] == "disconnected":
829 log.info(u"[%s] disconnected" % (profile,))
830 _session.expire()
831 except IndexError:
832 log.error("Deferred result should be a tuple with fonction name first")
833
834 self.signalDeferred[profile] = defer.Deferred()
835 self.request.notifyFinish().addBoth(unlock, profile)
836 return self.signalDeferred[profile]
837
838 def getGenericCb(self, function_name):
839 """Return a generic function which send all params to signalDeferred.callback
840 function must have profile as last argument"""
841 def genericCb(*args):
842 profile = args[-1]
843 if not profile in self.sat_host.prof_connected:
844 return
845 if profile in self.signalDeferred:
846 self.signalDeferred[profile].callback((function_name,args[:-1]))
847 del self.signalDeferred[profile]
848 else:
849 if not self.queue.has_key(profile):
850 self.queue[profile] = []
851 self.queue[profile].append((function_name, args[:-1]))
852 return genericCb
853
854 def connected(self, profile):
855 assert(self.register) # register must be plugged
856 request = self.register.getWaitingRequest(profile)
857 if request:
858 self.register._logged(profile, request)
859
860 def disconnected(self, profile):
861 if not profile in self.sat_host.prof_connected:
862 log.error("'disconnected' signal received for a not connected profile")
863 return
864 self.sat_host.prof_connected.remove(profile)
865 if profile in self.signalDeferred:
866 self.signalDeferred[profile].callback(("disconnected",))
867 del self.signalDeferred[profile]
868 else:
869 if not self.queue.has_key(profile):
870 self.queue[profile] = []
871 self.queue[profile].append(("disconnected",))
872
873
874 def connectionError(self, error_type, profile):
875 assert(self.register) #register must be plugged
876 request = self.register.getWaitingRequest(profile)
877 if request: #The user is trying to log in
878 if error_type == "AUTH_ERROR":
879 _error_t = "AUTH ERROR"
880 else:
881 _error_t = "UNKNOWN"
882 self.register._logginError(profile, request, _error_t)
883
884 def render(self, request):
885 """
886 Render method wich reject access if user is not identified
887 """
888 _session = request.getSession()
889 parsed = jsonrpclib.loads(request.content.read())
890 profile = ISATSession(_session).profile
891 if not profile:
892 #user is not identified, we return a jsonrpc fault
893 fault = jsonrpclib.Fault(C.ERRNUM_LIBERVIA, "Not allowed") #FIXME: define some standard error codes for libervia
894 return jsonrpc.JSONRPC._cbRender(self, fault, request, parsed.get('id'), parsed.get('jsonrpc'))
895 self.request = request
896 return jsonrpc.JSONRPC.render(self, request)
897
898 class UploadManager(Resource):
899 """This class manage the upload of a file
900 It redirect the stream to SàT core backend"""
901 isLeaf = True
902 NAME = 'path' #name use by the FileUpload
903
904 def __init__(self, sat_host):
905 self.sat_host=sat_host
906 self.upload_dir = tempfile.mkdtemp()
907 self.sat_host.addCleanup(shutil.rmtree, self.upload_dir)
908
909 def getTmpDir(self):
910 return self.upload_dir
911
912 def _getFileName(self, request):
913 """Generate unique filename for a file"""
914 raise NotImplementedError
915
916 def _fileWritten(self, request, filepath):
917 """Called once the file is actually written on disk
918 @param request: HTTP request object
919 @param filepath: full filepath on the server
920 @return: a tuple with the name of the async bridge method
921 to be called followed by its arguments.
922 """
923 raise NotImplementedError
924
925 def render(self, request):
926 """
927 Render method with some hacks:
928 - if login is requested, try to login with form data
929 - except login, every method is jsonrpc
930 - user doesn't need to be authentified for isRegistered, but must be for all other methods
931 """
932 filename = self._getFileName(request)
933 filepath = os.path.join(self.upload_dir, filename)
934 #FIXME: the uploaded file is fully loaded in memory at form parsing time so far
935 # (see twisted.web.http.Request.requestReceived). A custom requestReceived should
936 # be written in the futur. In addition, it is not yet possible to get progression informations
937 # (see http://twistedmatrix.com/trac/ticket/288)
938
939 with open(filepath,'w') as f:
940 f.write(request.args[self.NAME][0])
941
942 def finish(d):
943 error = isinstance(d, Exception) or isinstance (d, Failure)
944 request.write('KO' if error else 'OK')
945 # TODO: would be great to re-use the original Exception class and message
946 # but it is lost in the middle of the backtrace and encapsulated within
947 # a DBusException instance --> extract the data from the backtrace?
948 request.finish()
949
950 d = JSONRPCMethodManager(self.sat_host).asyncBridgeCall(*self._fileWritten(request, filepath))
951 d.addCallbacks(lambda d: finish(d), lambda failure: finish(failure))
952 return server.NOT_DONE_YET
953
954
955 class UploadManagerRadioCol(UploadManager):
956 NAME = 'song'
957
958 def _getFileName(self, request):
959 extension = os.path.splitext(request.args['filename'][0])[1]
960 return "%s%s" % (str(uuid.uuid4()), extension) # XXX: chromium doesn't seem to play song without the .ogg extension, even with audio/ogg mime-type
961
962 def _fileWritten(self, request, filepath):
963 """Called once the file is actually written on disk
964 @param request: HTTP request object
965 @param filepath: full filepath on the server
966 @return: a tuple with the name of the async bridge method
967 to be called followed by its arguments.
968 """
969 profile = ISATSession(request.getSession()).profile
970 return ("radiocolSongAdded", request.args['referee'][0], filepath, profile)
971
972
973 class UploadManagerAvatar(UploadManager):
974 NAME = 'avatar_path'
975
976 def _getFileName(self, request):
977 return str(uuid.uuid4())
978
979 def _fileWritten(self, request, filepath):
980 """Called once the file is actually written on disk
981 @param request: HTTP request object
982 @param filepath: full filepath on the server
983 @return: a tuple with the name of the async bridge method
984 to be called followed by its arguments.
985 """
986 profile = ISATSession(request.getSession()).profile
987 return ("setAvatar", filepath, profile)
988
989
990 def coerceConnectionType(value): # called from Libervia.OPT_PARAMETERS
991 allowed_values = ('http', 'https', 'both')
992 if value not in allowed_values:
993 raise ValueError("%(given)s not in %(expected)s" % {'given': value, 'expected': str(allowed_values)})
994 return value
995
996
997 def coerceDataDir(value): # called from Libervia.OPT_PARAMETERS
998 html = os.path.join(value, C.HTML_DIR)
999 if not os.path.isfile(os.path.join(html, 'libervia.html')):
1000 raise ValueError("%s is not a Libervia's browser HTML directory" % os.path.realpath(html))
1001 server_css = os.path.join(value, C.SERVER_CSS_DIR)
1002 if not os.path.isfile(os.path.join(server_css, 'blog.css')):
1003 raise ValueError("%s is not a Libervia's server data directory" % os.path.realpath(server_css))
1004 return value
1005
1006
1007 class Libervia(service.Service):
1008
1009 DATA_DIR_DEFAULT = ''
1010 OPT_PARAMETERS = [['connection_type', 't', 'https', "'http', 'https' or 'both' (to launch both servers).", coerceConnectionType],
1011 ['port', 'p', 8080, 'The port number to listen HTTP on.', int],
1012 ['port_https', 's', 8443, 'The port number to listen HTTPS on.', int],
1013 ['port_https_ext', 'e', 0, 'The external port number used for HTTPS (0 means port_https value).', int],
1014 ['ssl_certificate', 'c', 'libervia.pem', 'PEM certificate with both private and public parts.', str],
1015 ['redirect_to_https', 'r', 1, 'Automatically redirect from HTTP to HTTPS.', int],
1016 ['security_warning', 'w', 1, 'Warn user that he is about to connect on HTTP.', int],
1017 # FIXME: twistd bugs when printing 'à' on "Unknown command" error (works on normal command listing)
1018 ['passphrase', 'k', '', u"Passphrase for the SaT profile named '%s'" % C.SERVICE_PROFILE, str],
1019 ['data_dir', 'd', DATA_DIR_DEFAULT, u'Data directory for Libervia', coerceDataDir],
1020 ]
1021
1022 def __init__(self, *args, **kwargs):
1023 if not kwargs:
1024 # During the loading of the twisted plugins, we just need the default values.
1025 # This part is not executed when the plugin is actually started.
1026 for name, value in [(option[0], option[2]) for option in self.OPT_PARAMETERS]:
1027 kwargs[name] = value
1028 self.initialised = defer.Deferred()
1029 self.connection_type = kwargs['connection_type']
1030 self.port = kwargs['port']
1031 self.port_https = kwargs['port_https']
1032 self.port_https_ext = kwargs['port_https_ext']
1033 if not self.port_https_ext:
1034 self.port_https_ext = self.port_https
1035 self.ssl_certificate = kwargs['ssl_certificate']
1036 self.redirect_to_https = kwargs['redirect_to_https']
1037 self.security_warning = kwargs['security_warning']
1038 self.passphrase = kwargs['passphrase']
1039 self.data_dir = kwargs['data_dir']
1040 if self.data_dir == Libervia.DATA_DIR_DEFAULT:
1041 coerceDataDir(self.data_dir) # this is not done when using the default value
1042 self.html_dir = os.path.join(self.data_dir, C.HTML_DIR)
1043 self.server_css_dir = os.path.join(self.data_dir, C.SERVER_CSS_DIR)
1044 self._cleanup = []
1045 root = ProtectedFile(self.html_dir)
1046 self.signal_handler = SignalHandler(self)
1047 _register = Register(self)
1048 _upload_radiocol = UploadManagerRadioCol(self)
1049 _upload_avatar = UploadManagerAvatar(self)
1050 self.signal_handler.plugRegister(_register)
1051 self.sessions = {} #key = session value = user
1052 self.prof_connected = set() #Profiles connected
1053 self.action_handler = SATActionIDHandler()
1054 ## bridge ##
1055 try:
1056 self.bridge=DBusBridgeFrontend()
1057 except BridgeExceptionNoService:
1058 print(u"Can't connect to SàT backend, are you sure it's launched ?")
1059 sys.exit(1)
1060 def backendReady(dummy):
1061 self.bridge.register("connected", self.signal_handler.connected)
1062 self.bridge.register("disconnected", self.signal_handler.disconnected)
1063 self.bridge.register("connectionError", self.signal_handler.connectionError)
1064 self.bridge.register("actionResult", self.action_handler.actionResultCb)
1065 #core
1066 for signal_name in ['presenceUpdate', 'newMessage', 'subscribe', 'contactDeleted', 'newContact', 'entityDataUpdated', 'askConfirmation', 'newAlert', 'paramUpdate']:
1067 self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name))
1068 #plugins
1069 for signal_name in ['personalEvent', 'roomJoined', 'roomUserJoined', 'roomUserLeft', 'tarotGameStarted', 'tarotGameNew', 'tarotGameChooseContrat',
1070 'tarotGameShowCards', 'tarotGameInvalidCards', 'tarotGameCardsPlayed', 'tarotGameYourTurn', 'tarotGameScore', 'tarotGamePlayers',
1071 'radiocolStarted', 'radiocolPreload', 'radiocolPlay', 'radiocolNoUpload', 'radiocolUploadOk', 'radiocolSongRejected', 'radiocolPlayers',
1072 'roomLeft', 'roomUserChangedNick', 'chatStateReceived']:
1073 self.bridge.register(signal_name, self.signal_handler.getGenericCb(signal_name), "plugin")
1074 self.media_dir = self.bridge.getConfig('', 'media_dir')
1075 self.local_dir = self.bridge.getConfig('', 'local_dir')
1076 root.putChild('', Redirect('libervia.html'))
1077 root.putChild('json_signal_api', self.signal_handler)
1078 root.putChild('json_api', MethodHandler(self))
1079 root.putChild('register_api', _register)
1080 root.putChild('upload_radiocol', _upload_radiocol)
1081 root.putChild('upload_avatar', _upload_avatar)
1082 root.putChild('blog', MicroBlog(self))
1083 root.putChild('css', ProtectedFile(self.server_css_dir))
1084 root.putChild(os.path.dirname(C.MEDIA_DIR), ProtectedFile(self.media_dir))
1085 root.putChild(os.path.dirname(C.AVATARS_DIR), ProtectedFile(os.path.join(self.local_dir, C.AVATARS_DIR)))
1086 root.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
1087 self.site = server.Site(root)
1088 self.site.sessionFactory = LiberviaSession
1089
1090 self.bridge.getReady(lambda: self.initialised.callback(None),
1091 lambda failure: self.initialised.errback(Exception(failure)))
1092 self.initialised.addCallback(backendReady)
1093 self.initialised.addErrback(lambda failure: log.error("Init error: %s" % failure))
1094
1095 def addCleanup(self, callback, *args, **kwargs):
1096 """Add cleaning method to call when service is stopped
1097 cleaning method will be called in reverse order of they insertion
1098 @param callback: callable to call on service stop
1099 @param *args: list of arguments of the callback
1100 @param **kwargs: list of keyword arguments of the callback"""
1101 self._cleanup.insert(0, (callback, args, kwargs))
1102
1103 def startService(self):
1104 """Connect the profile for Libervia and start the HTTP(S) server(s)"""
1105 def eb(e):
1106 log.error(_("Connection failed: %s") % e)
1107 self.stop()
1108
1109 def initOk(dummy):
1110 if not self.bridge.isConnected(C.SERVICE_PROFILE):
1111 self.bridge.asyncConnect(C.SERVICE_PROFILE, self.passphrase,
1112 callback=self._startService, errback=eb)
1113
1114 self.initialised.addCallback(initOk)
1115
1116 def _startService(self, dummy):
1117 """Actually start the HTTP(S) server(s) after the profile for Libervia is connected.
1118 @raise IOError: the certificate file doesn't exist
1119 @raise OpenSSL.crypto.Error: the certificate file is invalid
1120 """
1121 if self.connection_type in ('https', 'both'):
1122 if not ssl_available:
1123 raise(ImportError(_("Python module pyOpenSSL is not installed!")))
1124 try:
1125 with open(os.path.expanduser(self.ssl_certificate)) as keyAndCert:
1126 try:
1127 cert = ssl.PrivateCertificate.loadPEM(keyAndCert.read())
1128 except OpenSSL.crypto.Error as e:
1129 log.error(_("The file '%s' must contain both private and public parts of the certificate") % self.ssl_certificate)
1130 raise e
1131 except IOError as e:
1132 log.error(_("The file '%s' doesn't exist") % self.ssl_certificate)
1133 raise e
1134 reactor.listenSSL(self.port_https, self.site, cert.options())
1135 if self.connection_type in ('http', 'both'):
1136 if self.connection_type == 'both' and self.redirect_to_https:
1137 reactor.listenTCP(self.port, server.Site(RedirectToHTTPS(self.port, self.port_https_ext)))
1138 else:
1139 reactor.listenTCP(self.port, self.site)
1140
1141 def stopService(self):
1142 print "launching cleaning methods"
1143 for callback, args, kwargs in self._cleanup:
1144 callback(*args, **kwargs)
1145 self.bridge.disconnect(C.SERVICE_PROFILE)
1146
1147 def run(self):
1148 reactor.run()
1149
1150 def stop(self):
1151 reactor.stop()
1152
1153
1154 class RedirectToHTTPS(Resource):
1155
1156 def __init__(self, old_port, new_port):
1157 Resource.__init__(self)
1158 self.isLeaf = True
1159 self.old_port = old_port
1160 self.new_port = new_port
1161
1162 def render(self, request):
1163 netloc = request.URLPath().netloc.replace(':%s' % self.old_port, ':%s' % self.new_port)
1164 url = "https://" + netloc + request.uri
1165 return redirectTo(url, request)
1166
1167
1168 registerAdapter(SATSession, server.Session, ISATSession)