annotate src/server/websockets.py @ 995:f88325b56a6a

server: dynamic pages first draft: /!\ new dependency: autobahn This patch introduce server part of dynamic pages. Dynamic pages use websockets to establish constant connection with a Libervia page, allowing to receive real time data or update it. The feature is activated by specifying "dynamic = true" in the page. Once activated, page can implement "on_data" method which will be called when data are sent by the page. To send data the other way, the page can use request.sendData. The new "registerSignal" method allows to use an "on_signal" method to be called each time given signal is received, with automatic (and optional) filtering on profile. New renderPartial and renderAndUpdate method allow to append new HTML elements to the dynamic page.
author Goffi <goffi@goffi.org>
date Wed, 03 Jan 2018 01:10:12 +0100
parents
children f2170536ba23
Ignore whitespace changes - Everywhere: Within whitespace: At end of lines:
rev   line source
995
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
1 #!/usr/bin/python
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
2 # -*- coding: utf-8 -*-
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
3
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
4 # Libervia: a Salut à Toi frontend
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
5 # Copyright (C) 2011-2017 Jérôme Poisson <goffi@goffi.org>
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
6
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
7 # This program is free software: you can redistribute it and/or modify
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
8 # it under the terms of the GNU Affero General Public License as published by
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
9 # the Free Software Foundation, either version 3 of the License, or
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
10 # (at your option) any later version.
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
11
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
12 # This program is distributed in the hope that it will be useful,
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
15 # GNU Affero General Public License for more details.
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
16
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
17 # You should have received a copy of the GNU Affero General Public License
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
19
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
20 from sat.core.i18n import _
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
21 from sat.core.log import getLogger
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
22 log = getLogger(__name__)
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
23 from sat.core import exceptions
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
24
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
25 from autobahn.twisted import websocket
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
26 from autobahn.twisted import resource as resource
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
27 from autobahn.websocket import types
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
28
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
29 import json
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
30
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
31 LIBERVIA_PROTOCOL = 'libervia_page'
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
32
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
33
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
34 class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
35 host = None
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
36 tokens_map = {}
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
37
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
38 def onConnect(self, request):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
39 prefix = LIBERVIA_PROTOCOL + u'_'
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
40 for protocol in request.protocols:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
41 if protocol.startswith(prefix):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
42 token = protocol[len(prefix):].strip()
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
43 if token:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
44 break
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
45 else:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
46 raise types.ConnectionDeny(types.ConnectionDeny.NOT_IMPLEMENTED,
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
47 u"Can't use this subprotocol")
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
48
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
49 if token not in self.tokens_map:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
50 log.warning(_(u"Can't activate page socket: unknown token"))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
51 raise types.ConnectionDeny(types.ConnectionDeny.FORBIDDEN,
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
52 u"Bad token, please reload page")
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
53 self.token = token
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
54 self.page = self.tokens_map[token]['page']
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
55 self.request = self.tokens_map[token]['request']
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
56 return protocol
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
57
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
58 def onOpen(self):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
59 log.debug(_(u"Websocket opened for {page} (token: {token})".format(
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
60 page = self.page,
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
61 token = self.token)))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
62 self.request.sendData = self.sendJSONData
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
63 self.page.onSocketOpen(self.request)
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
64
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
65 def onMessage(self, payload, isBinary):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
66 try:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
67 data_json = json.loads(payload.decode('utf8'))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
68 except ValueError as e:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
69 log.warning(_(u"Not valid JSON, ignoring data: {msg}\n{data}").format(msg=e, data=payload))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
70 return
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
71 # we request page first, to raise an AttributeError
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
72 # if it is not set (which should never happen)
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
73 page = self.page
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
74 try:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
75 cb = page.on_data
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
76 except AttributeError:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
77 log.warning(_(u'No "on_data" method set on dynamic page, ignoring data:\n{data}').format(data=data_json))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
78 else:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
79 cb(page, self.request, data_json)
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
80
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
81 def onClose(self, wasClean, code, reason):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
82 try:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
83 token = self.token
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
84 except AttributeError:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
85 log.warning(_(u'Websocket closed but no token is associated'))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
86 return
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
87
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
88 self.page.onSocketClose(self.request)
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
89
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
90 try:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
91 del self.tokens_map[token]
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
92 del self.request.sendData
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
93 except (KeyError, AttributeError):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
94 raise exceptions.InternalError(_(u"Token or sendData doesn't exist, this should never happen!"))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
95 log.debug(_(u"Websocket closed for {page} (token: {token}). {reason}".format(
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
96 page = self.page,
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
97 token = self.token,
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
98 reason = u'' if wasClean else _(u'Reason: {reason}').format(reason=reason))))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
99
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
100 def sendJSONData(self, type_, **data):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
101 assert 'type' not in data
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
102 data['type'] = type_
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
103 self.sendMessage(json.dumps(data, ensure_ascii = False).encode('utf8'))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
104
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
105 @classmethod
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
106 def getBaseURL(cls, host, secure):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
107 return u"ws{sec}://localhost:{port}".format(
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
108 sec='s' if secure else '',
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
109 port=cls.host.options['port_https' if secure else 'port'])
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
110
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
111 @classmethod
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
112 def getResource(cls, host, secure):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
113 if cls.host is None:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
114 cls.host = host
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
115 factory = websocket.WebSocketServerFactory(cls.getBaseURL(host, secure))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
116 factory.protocol = cls
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
117 return resource.WebSocketResource(factory)
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
118
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
119 @classmethod
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
120 def registerToken(cls, token, page, request):
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
121 if token in cls.tokens_map:
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
122 raise exceptions.ConflictError(_(u'This token is already registered'))
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
123 cls.tokens_map[token] = {'page': page,
f88325b56a6a server: dynamic pages first draft:
Goffi <goffi@goffi.org>
parents:
diff changeset
124 'request': request}