Mercurial > libervia-web
comparison libervia/server/websockets.py @ 1203:251eba911d4d
server (websockets): fixed websocket handling on HTTPS connections:
Original request used to retrieve a page was stored on dynamic pages, but after the end of
it, the channel was deleted, resulting in a isSecure() always returning False, and
troubles in chain leading to the the use of the wrong session object. This patch fixes
this by reworking the way original request is used, and creating a new wrapping class
allowing to keep an API similar to iweb.IRequest, with data coming from both the original
request and the websocket request.
fix 327
author | Goffi <goffi@goffi.org> |
---|---|
date | Sun, 14 Jul 2019 14:45:51 +0200 |
parents | 2af117bfe6cc |
children | d2e2bf14f2e2 |
comparison
equal
deleted
inserted
replaced
1202:3f791fbc1643 | 1203:251eba911d4d |
---|---|
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 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/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 | |
21 import json | |
22 from twisted.internet import error | |
23 from autobahn.twisted import websocket | |
24 from autobahn.twisted import resource as resource | |
25 from autobahn.websocket import types | |
26 from sat.core import exceptions | |
20 from sat.core.i18n import _ | 27 from sat.core.i18n import _ |
21 from sat.core.log import getLogger | 28 from sat.core.log import getLogger |
22 | 29 |
23 log = getLogger(__name__) | 30 log = getLogger(__name__) |
24 from sat.core import exceptions | |
25 | |
26 from autobahn.twisted import websocket | |
27 from autobahn.twisted import resource as resource | |
28 from autobahn.websocket import types | |
29 | |
30 import json | |
31 | 31 |
32 LIBERVIA_PROTOCOL = "libervia_page" | 32 LIBERVIA_PROTOCOL = "libervia_page" |
33 | |
34 | |
35 class WebsocketRequest(object): | |
36 """Wrapper around autobahn's ConnectionRequest and Twisted's server.Request | |
37 | |
38 This is used to have a common interface in Libervia page with request object | |
39 """ | |
40 | |
41 def __init__(self, ws_protocol, connection_request, server_request): | |
42 """ | |
43 @param connection_request: websocket request | |
44 @param serveur_request: original request of the page | |
45 """ | |
46 self.ws_protocol = ws_protocol | |
47 self.ws_request = connection_request | |
48 if self.isSecure(): | |
49 cookie_string = "TWISTED_SECURE_SESSION" | |
50 else: | |
51 cookie_string = "TWISTED_SESSION" | |
52 cookie_value = server_request.getCookie(cookie_string) | |
53 try: | |
54 raw_cookies = ws_protocol.http_headers['cookie'] | |
55 except KeyError: | |
56 raise ValueError(u"missing expected cookie header") | |
57 self.cookies = {k:v for k,v in (c.split('=') for c in raw_cookies.split(';'))} | |
58 if self.cookies[cookie_string] != cookie_value: | |
59 raise exceptions.PermissionError( | |
60 u"Bad cookie value, this should never happen.\n" | |
61 u"headers: {headers}".format(headers=ws_protocol.http_headers)) | |
62 | |
63 self.template_data = server_request.template_data | |
64 self.data = server_request.data | |
65 self.session = server_request.getSession() | |
66 self._signals_registered = server_request._signals_registered | |
67 self._signals_cache = server_request._signals_cache | |
68 # signal id is needed to link original request with signal handler | |
69 self.signal_id = server_request._signal_id | |
70 | |
71 def isSecure(self): | |
72 return self.ws_protocol.factory.isSecure | |
73 | |
74 def getSession(self, sessionInterface=None): | |
75 try: | |
76 self.session.touch() | |
77 except (error.AlreadyCalled, error.AlreadyCancelled): | |
78 # Session has already expired. | |
79 self.session = None | |
80 | |
81 if sessionInterface: | |
82 return self.session.getComponent(sessionInterface) | |
83 | |
84 return self.session | |
85 | |
86 def sendData(self, type_, **data): | |
87 assert "type" not in data | |
88 data["type"] = type_ | |
89 self.ws_protocol.sendMessage(json.dumps(data, ensure_ascii=False).encode("utf8")) | |
33 | 90 |
34 | 91 |
35 class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol): | 92 class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol): |
36 host = None | 93 host = None |
37 tokens_map = {} | 94 tokens_map = {} |
52 log.warning(_(u"Can't activate page socket: unknown token")) | 109 log.warning(_(u"Can't activate page socket: unknown token")) |
53 raise types.ConnectionDeny( | 110 raise types.ConnectionDeny( |
54 types.ConnectionDeny.FORBIDDEN, u"Bad token, please reload page" | 111 types.ConnectionDeny.FORBIDDEN, u"Bad token, please reload page" |
55 ) | 112 ) |
56 self.token = token | 113 self.token = token |
57 self.page = self.tokens_map[token]["page"] | 114 token_map = self.tokens_map.pop(token) |
58 self.request = self.tokens_map[token]["request"] | 115 self.page = token_map["page"] |
116 self.request = WebsocketRequest(self, request, token_map["request"]) | |
59 return protocol | 117 return protocol |
60 | 118 |
61 def onOpen(self): | 119 def onOpen(self): |
62 log.debug( | 120 log.debug( |
63 _( | 121 _( |
64 u"Websocket opened for {page} (token: {token})".format( | 122 u"Websocket opened for {page} (token: {token})".format( |
65 page=self.page, token=self.token | 123 page=self.page, token=self.token |
66 ) | 124 ) |
67 ) | 125 ) |
68 ) | 126 ) |
69 self.request.sendData = self.sendJSONData | |
70 self.page.onSocketOpen(self.request) | 127 self.page.onSocketOpen(self.request) |
71 | 128 |
72 def onMessage(self, payload, isBinary): | 129 def onMessage(self, payload, isBinary): |
73 try: | 130 try: |
74 data_json = json.loads(payload.decode("utf8")) | 131 data_json = json.loads(payload.decode("utf8")) |
92 ) | 149 ) |
93 else: | 150 else: |
94 cb(page, self.request, data_json) | 151 cb(page, self.request, data_json) |
95 | 152 |
96 def onClose(self, wasClean, code, reason): | 153 def onClose(self, wasClean, code, reason): |
97 try: | |
98 token = self.token | |
99 except AttributeError: | |
100 log.warning(_(u"Websocket closed but no token is associated")) | |
101 return | |
102 | |
103 self.page.onSocketClose(self.request) | 154 self.page.onSocketClose(self.request) |
104 | 155 |
105 try: | |
106 del self.tokens_map[token] | |
107 del self.request.sendData | |
108 except (KeyError, AttributeError): | |
109 raise exceptions.InternalError( | |
110 _(u"Token or sendData doesn't exist, this should never happen!") | |
111 ) | |
112 log.debug( | 156 log.debug( |
113 _( | 157 _( |
114 u"Websocket closed for {page} (token: {token}). {reason}".format( | 158 u"Websocket closed for {page} (token: {token}). {reason}".format( |
115 page=self.page, | 159 page=self.page, |
116 token=self.token, | 160 token=self.token, |
118 if wasClean | 162 if wasClean |
119 else _(u"Reason: {reason}").format(reason=reason), | 163 else _(u"Reason: {reason}").format(reason=reason), |
120 ) | 164 ) |
121 ) | 165 ) |
122 ) | 166 ) |
123 | |
124 def sendJSONData(self, type_, **data): | |
125 assert "type" not in data | |
126 data["type"] = type_ | |
127 self.sendMessage(json.dumps(data, ensure_ascii=False).encode("utf8")) | |
128 | 167 |
129 @classmethod | 168 @classmethod |
130 def getBaseURL(cls, host, secure): | 169 def getBaseURL(cls, host, secure): |
131 return u"ws{sec}://localhost:{port}".format( | 170 return u"ws{sec}://localhost:{port}".format( |
132 sec="s" if secure else "", | 171 sec="s" if secure else "", |