Mercurial > libervia-web
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) |