Mercurial > libervia-web
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) |