diff 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
line wrap: on
line diff
--- a/libervia/server/websockets.py	Fri Jul 12 14:58:11 2019 +0200
+++ b/libervia/server/websockets.py	Sun Jul 14 14:45:51 2019 +0200
@@ -17,21 +17,78 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+
+import json
+from twisted.internet import error
+from autobahn.twisted import websocket
+from autobahn.twisted import resource as resource
+from autobahn.websocket import types
+from sat.core import exceptions
 from sat.core.i18n import _
 from sat.core.log import getLogger
 
 log = getLogger(__name__)
-from sat.core import exceptions
-
-from autobahn.twisted import websocket
-from autobahn.twisted import resource as resource
-from autobahn.websocket import types
-
-import json
 
 LIBERVIA_PROTOCOL = "libervia_page"
 
 
+class WebsocketRequest(object):
+    """Wrapper around autobahn's ConnectionRequest and Twisted's server.Request
+
+    This is used to have a common interface in Libervia page with request object
+    """
+
+    def __init__(self, ws_protocol, connection_request, server_request):
+        """
+        @param connection_request: websocket request
+        @param serveur_request: original request of the page
+        """
+        self.ws_protocol = ws_protocol
+        self.ws_request = connection_request
+        if self.isSecure():
+            cookie_string = "TWISTED_SECURE_SESSION"
+        else:
+            cookie_string = "TWISTED_SESSION"
+        cookie_value = server_request.getCookie(cookie_string)
+        try:
+            raw_cookies = ws_protocol.http_headers['cookie']
+        except KeyError:
+            raise ValueError(u"missing expected cookie header")
+        self.cookies = {k:v for k,v in (c.split('=') for c in raw_cookies.split(';'))}
+        if self.cookies[cookie_string] != cookie_value:
+            raise exceptions.PermissionError(
+                u"Bad cookie value, this should never happen.\n"
+                u"headers: {headers}".format(headers=ws_protocol.http_headers))
+
+        self.template_data = server_request.template_data
+        self.data = server_request.data
+        self.session = server_request.getSession()
+        self._signals_registered = server_request._signals_registered
+        self._signals_cache = server_request._signals_cache
+        # signal id is needed to link original request with signal handler
+        self.signal_id = server_request._signal_id
+
+    def isSecure(self):
+        return self.ws_protocol.factory.isSecure
+
+    def getSession(self, sessionInterface=None):
+        try:
+            self.session.touch()
+        except (error.AlreadyCalled, error.AlreadyCancelled):
+            # Session has already expired.
+            self.session = None
+
+        if sessionInterface:
+            return self.session.getComponent(sessionInterface)
+
+        return self.session
+
+    def sendData(self, type_, **data):
+        assert "type" not in data
+        data["type"] = type_
+        self.ws_protocol.sendMessage(json.dumps(data, ensure_ascii=False).encode("utf8"))
+
+
 class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol):
     host = None
     tokens_map = {}
@@ -54,8 +111,9 @@
                 types.ConnectionDeny.FORBIDDEN, u"Bad token, please reload page"
             )
         self.token = token
-        self.page = self.tokens_map[token]["page"]
-        self.request = self.tokens_map[token]["request"]
+        token_map = self.tokens_map.pop(token)
+        self.page = token_map["page"]
+        self.request = WebsocketRequest(self, request, token_map["request"])
         return protocol
 
     def onOpen(self):
@@ -66,7 +124,6 @@
                 )
             )
         )
-        self.request.sendData = self.sendJSONData
         self.page.onSocketOpen(self.request)
 
     def onMessage(self, payload, isBinary):
@@ -94,21 +151,8 @@
             cb(page, self.request, data_json)
 
     def onClose(self, wasClean, code, reason):
-        try:
-            token = self.token
-        except AttributeError:
-            log.warning(_(u"Websocket closed but no token is associated"))
-            return
-
         self.page.onSocketClose(self.request)
 
-        try:
-            del self.tokens_map[token]
-            del self.request.sendData
-        except (KeyError, AttributeError):
-            raise exceptions.InternalError(
-                _(u"Token or sendData doesn't exist, this should never happen!")
-            )
         log.debug(
             _(
                 u"Websocket closed for {page} (token: {token}). {reason}".format(
@@ -121,11 +165,6 @@
             )
         )
 
-    def sendJSONData(self, type_, **data):
-        assert "type" not in data
-        data["type"] = type_
-        self.sendMessage(json.dumps(data, ensure_ascii=False).encode("utf8"))
-
     @classmethod
     def getBaseURL(cls, host, secure):
         return u"ws{sec}://localhost:{port}".format(