comparison libervia/web/server/websockets.py @ 1518:eb00d593801d

refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 16:49:28 +0200
parents libervia/server/websockets.py@ff95501abe74
children
comparison
equal deleted inserted replaced
1517:b8ed9726525b 1518:eb00d593801d
1 #!/usr/bin/env python3
2
3 # Libervia: a Salut à Toi frontend
4 # Copyright (C) 2011-2021 Jérôme Poisson <goffi@goffi.org>
5
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU Affero General Public License for more details.
15
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19
20 import json
21 from typing import Optional
22
23 from autobahn.twisted import websocket
24 from autobahn.twisted import resource as resource
25 from autobahn.websocket import types
26 from libervia.backend.core import exceptions
27 from libervia.backend.core.i18n import _
28 from libervia.backend.core.log import getLogger
29
30 from . import session_iface
31 from .constants import Const as C
32
33 log = getLogger(__name__)
34
35 host = None
36
37
38 class LiberviaPageWSProtocol(websocket.WebSocketServerProtocol):
39
40 def __init__(self):
41 super().__init__()
42 self._init_ok: bool = False
43 self.__profile: Optional[str] = None
44 self.__session: Optional[session_iface.WebSession] = None
45
46 @property
47 def init_ok(self):
48 return self._init_ok
49
50 def send(self, data_type: str, data: dict) -> None:
51 """Send data to frontend"""
52 if not self._init_ok and data_type != "error":
53 raise exceptions.InternalError(
54 "send called when not initialized, this should not happend! Please use "
55 "WebSession.send which takes care of sending correctly the data to all "
56 "sessions."
57 )
58
59 data_root = {
60 "type": data_type,
61 "data": data
62 }
63 self.sendMessage(json.dumps(data_root, ensure_ascii=False).encode())
64
65 def close(self) -> None:
66 log.debug(f"closing websocket for profile {self.__profile}")
67
68 def error(self, error_type: str, msg: str) -> None:
69 """Send an error message to frontend and log it locally"""
70 log.warning(
71 f"websocket error {error_type}: {msg}"
72 )
73 self.send("error", {
74 "type": error_type,
75 "msg": msg,
76 })
77
78 def onConnect(self, request):
79 if "libervia-page" not in request.protocols:
80 raise types.ConnectionDeny(
81 types.ConnectionDeny.NOT_IMPLEMENTED, "No supported protocol"
82 )
83 self._init_ok = False
84 cookies = {}
85 for cookie in request.headers.get("cookie", "").split(";"):
86 k, __, v = cookie.partition("=")
87 cookies[k.strip()] = v.strip()
88 session_uid = (
89 cookies.get("TWISTED_SECURE_SESSION")
90 or cookies.get("TWISTED_SESSION")
91 or ""
92 )
93 if not session_uid:
94 raise types.ConnectionDeny(
95 types.ConnectionDeny.FORBIDDEN, "No session set"
96 )
97 try:
98 session = host.site.getSession(session_uid.encode())
99 except KeyError:
100 raise types.ConnectionDeny(
101 types.ConnectionDeny.FORBIDDEN, "Invalid session"
102 )
103
104 session.touch()
105 session_data = session.getComponent(session_iface.IWebSession)
106 if session_data.ws_socket is not None:
107 log.warning(f"Session socket is already set {session_data.ws_socket=} {self=}], force closing it")
108 try:
109 session_data.ws_socket.send(
110 "force_close", {"reason": "duplicate connection detected"}
111 )
112 except Exception as e:
113 log.warning(f"Can't force close old connection: {e}")
114 session_data.ws_socket = self
115 self.__session = session_data
116 self.__profile = session_data.profile or C.SERVICE_PROFILE
117 log.debug(f"websocket connection connected for profile {self.__profile}")
118 return "libervia-page"
119
120 def on_open(self):
121 log.debug("websocket connection opened")
122
123 def onMessage(self, payload: bytes, isBinary: bool) -> None:
124 if self.__session is None:
125 raise exceptions.InternalError("empty session, this should never happen")
126 try:
127 data_full = json.loads(payload.decode())
128 data_type = data_full["type"]
129 data = data_full["data"]
130 except ValueError as e:
131 self.error(
132 "bad_request",
133 f"Not valid JSON, ignoring data ({e}): {payload!r}"
134 )
135 return
136 except KeyError:
137 self.error(
138 "bad_request",
139 'Invalid request (missing "type" or "data")'
140 )
141 return
142
143 if data_type == "init":
144 if self._init_ok:
145 self.error(
146 "bad_request",
147 "double init"
148 )
149 self.sendClose(4400, "Bad Request")
150 return
151
152 try:
153 profile = data["profile"] or C.SERVICE_PROFILE
154 token = data["token"]
155 except KeyError:
156 self.error(
157 "bad_request",
158 "Invalid init data (missing profile or token)"
159 )
160 self.sendClose(4400, "Bad Request")
161 return
162 if ((
163 profile != self.__profile
164 or (token != self.__session.ws_token and profile != C.SERVICE_PROFILE)
165 )):
166 log.debug(
167 f"profile got {profile}, was expecting {self.__profile}, "
168 f"token got {token}, was expecting {self.__session.ws_token}, "
169 )
170 self.error(
171 "Unauthorized",
172 "Invalid profile or token"
173 )
174 self.sendClose(4401, "Unauthorized")
175 return
176 else:
177 log.debug(f"websocket connection initialized for {profile}")
178 self._init_ok = True
179 # we now send all cached data, if any
180 while True:
181 try:
182 session_kw = self.__session.ws_buffer.popleft()
183 except IndexError:
184 break
185 else:
186 self.send(**session_kw)
187
188 if not self._init_ok:
189 self.error(
190 "Unauthorized",
191 "session not authorized"
192 )
193 self.sendClose(4401, "Unauthorized")
194 return
195
196 def on_close(self, wasClean, code, reason):
197 log.debug(f"closing websocket (profile: {self.__profile}, reason: {reason})")
198 if self.__profile is None:
199 log.error("self.__profile should not be None")
200 self.__profile = C.SERVICE_PROFILE
201
202 if self.__session is None:
203 log.warning("closing a socket without attached session")
204 elif self.__session.ws_socket != self:
205 log.error("session socket is not linked to our instance")
206 else:
207 log.debug(f"reseting websocket session for {self.__profile}")
208 self.__session.ws_socket = None
209 sessions = session_iface.WebSession.get_profile_sessions(self.__profile)
210 log.debug(f"websocket connection for profile {self.__profile} closed")
211 self.__profile = None
212
213 @classmethod
214 def get_base_url(cls, secure):
215 return "ws{sec}://localhost:{port}".format(
216 sec="s" if secure else "",
217 port=host.options["port_https" if secure else "port"],
218 )
219
220 @classmethod
221 def get_resource(cls, secure):
222 factory = websocket.WebSocketServerFactory(cls.get_base_url(secure))
223 factory.protocol = cls
224 return resource.WebSocketResource(factory)