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 "",