async def handshake( self, path: str, request_headers: Headers ) -> typing.Optional[HTTPResponse]: if not request_headers.get("Authorization"): return http.HTTPStatus.UNAUTHORIZED, {}, b"" # parse credentials _type, _credentials = request_headers.get("Authorization").split(" ") username, password = base64.b64decode(_credentials).decode("utf8").split(":") if not (username in self.userlist and password == self.userlist[username]): logger.warning(f"Authorization Error: {username}:{password}") return http.HTTPStatus.UNAUTHORIZED, {}, b""
def _authorize_request(self, database_client: DatabaseClient, request_headers: Headers) -> None: """ Check if the websocket connection is authorized and can proceed, otherwise raise an HTTP error """ if "Authorization" not in request_headers: raise HttpError(HttpStatus.FORBIDDEN) self.worker_identifier = request_headers.get("X-Orchestra-WorkerIdentifier", None) if self.worker_identifier is None or self.worker_identifier == "": raise HttpError(HttpStatus.BAD_REQUEST) self.worker_version = request_headers.get("X-Orchestra-WorkerVersion", None) if self.worker_version is None or self.worker_version == "": raise HttpError(HttpStatus.BAD_REQUEST) self.user_identifier = self._authorize_worker(database_client, request_headers["Authorization"])
async def handshake( self, path: str, request_headers: Headers) -> typing.Optional[HTTPResponse]: # parse credentials _type, _credentials = request_headers.get('Proxy-Authorization').split( " ") username, password = base64.b64decode(_credentials).decode( "utf8").split(":") if not (username == self.username or password == self.password): logger.warning(f"Authorization Error: {username}:{password}") return http.HTTPStatus.NOT_FOUND, {}, b""
class WebSocketBase(AbstractWebSocketClient): """Client for the Andesite WebSocket handler. Args: ws_uri: Websocket endpoint to connect to. user_id: Bot's user id. If at the time of creation this is unknown, you may pass `None`, but then it needs to be set before connecting for the first time. password: Authorization for the Andesite node. Set to `None` if the node doesn't have a password. state: State handler to use. If `False` state handling is disabled. `None` to use the default state handler (`State`). max_connect_attempts: See the `max_connect_attempts` attribute. The client automatically keeps track of the current connection id and resumes the previous connection when calling `connect`, if there is any. You can delete the `connection_id` property to disable this. See Also: `AbstractWebSocketClient` for more details including a list of events that are dispatched. Attributes: max_connect_attempts (Optional[int]): Max amount of connection attempts to start before giving up. If `None`, there is no upper limit. This value can be overwritten when calling `connect` manually. web_socket_client (Optional[WebSocketClientProtocol]): Web socket client which is used. This attribute will be set once `connect` is called. Don't use the presence of this attribute to check whether the client is connected, use the `connected` property. """ max_connect_attempts: Optional[int] web_socket_client: Optional[WebSocketClientProtocol] __closed: bool __ws_uri: str __headers: Headers __last_connection_id: Optional[str] __connect_lock: Optional[asyncio.Lock] __read_loop: Optional[asyncio.Future] _json_encoder: JSONEncoder _json_decoder: JSONDecoder def __init__(self, ws_uri: Union[str, URL], user_id: Optional[int], password: Optional[str], *, state: andesite.StateArgumentType = False, max_connect_attempts: int = None) -> None: self.__ws_uri = str(ws_uri) self.__headers = Headers() if password is not None: self.__headers["Authorization"] = password if user_id is not None: self.user_id = user_id self.__last_connection_id = None self.max_connect_attempts = max_connect_attempts self.web_socket_client = None # can't create the lock here, because if the user uses # asyncio.run and creates the client outside of it, the loop # within the lock will not be the same as the loop used by # asyncio.run (as it creates a new loop every time) self.__connect_lock = None self.__closed = False self.__read_loop = None self._json_encoder = JSONEncoder() self._json_decoder = JSONDecoder() self.state = state def __repr__(self) -> str: return f"{type(self).__name__}(ws_uri={self.__ws_uri!r}, user_id={self.user_id!r}, " \ f"password=[HIDDEN], state={self.state!r}, max_connect_attempts={self.max_connect_attempts!r})" def __str__(self) -> str: return f"{type(self).__name__}({self.__ws_uri})" @property def user_id(self) -> Optional[int]: """User id. This is only `None` if it wasn't passed to the constructor. You can set this property to a new user id. """ return self.__headers.get("User-Id") @user_id.setter def user_id(self, user_id: int) -> None: self.__headers["User-Id"] = str(user_id) @property def closed(self) -> bool: return self.__closed @property def connected(self) -> bool: if self.web_socket_client: return self.web_socket_client.open else: return False @property def connection_id(self) -> Optional[str]: return self.__last_connection_id @connection_id.deleter def connection_id(self) -> None: self.__last_connection_id = None @property def node_region(self) -> Optional[str]: client = self.web_socket_client if client: return client.response_headers.get("Andesite-Node-Region") return None @property def node_id(self) -> Optional[str]: client = self.web_socket_client if client: return client.response_headers.get("Andesite-Node-Id") return None def _get_connect_lock(self, *, loop: asyncio.AbstractEventLoop = None ) -> asyncio.Lock: """Get the connect lock. The connect lock is only created once. Subsequent calls always return the same lock. The reason for the delayed creating is that the lock is bound to an event loop, which can change between __init__ and connect. """ if self.__connect_lock is None: self.__connect_lock = asyncio.Lock(loop=loop) return self.__connect_lock async def __connect(self, max_attempts: int = None) -> None: """Internal connect method. Args: max_attempts: Max amount of connection attempts to perform before aborting. This overwrites the instance attribute `max_connect_attempts`. Raises: ValueError: If client is already connected Notes: If `max_attempts` is exceeded and the client gives up on connecting it is closed! """ if self.connected: raise ValueError("Already connected!") headers = self.__headers if "User-Id" not in headers: raise KeyError("Trying to connect but user id unknown.\n" "This is most likely the case because you didn't\n" "set the user_id in the constructor and forgot to\n" "set it before connecting!") # inject the connection id to resume previous connection if self.__last_connection_id is not None: headers["Andesite-Resume-Id"] = self.__last_connection_id else: with suppress(KeyError): del headers["Andesite-Resume-Id"] attempt: int = 1 max_attempts = max_attempts or self.max_connect_attempts while max_attempts is None or attempt <= max_attempts: client = await try_connect(self.__ws_uri, extra_headers=headers) if client: break timeout = int(math.pow(attempt, 1.5)) log.info( f"Connection unsuccessful, trying again in {timeout} seconds") await asyncio.sleep(timeout) attempt += 1 else: self.__closed = True raise ConnectionError( f"Couldn't connect to {self.__ws_uri} after {attempt} attempts" ) log.info("%s: connected", self) self.web_socket_client = client self.__start_read_loop() _ = self.event_target.emit(WebSocketConnectEvent(self)) async def connect(self, *, max_attempts: int = None) -> None: if self.closed: raise ValueError("Client is closed and cannot be reused.") async with self._get_connect_lock(): if not self.connected: await self.__connect(max_attempts) async def disconnect(self) -> None: async with self._get_connect_lock(): self.__stop_read_loop() self.__last_connection_id = None if self.connected: await self.web_socket_client.close(reason="disconnect") _ = self.event_target.emit(WebSocketDisconnectEvent( self, True)) async def reset(self) -> None: await self.disconnect() del self.connection_id self.__closed = False async def close(self) -> None: await self.disconnect() self.__closed = True async def __web_socket_reader(self) -> None: """Internal web socket read loop. This method should never be called manually, see the following methods for controlling the reader. See Also: `WebSocket._start_read_loop` to start the read loop. `WebSocket._stop_read_loop` to stop the read loop. Notes: The read loop is automatically managed by the `WebSocket.connect` and `WebSocket.disconnect` methods. """ loop = asyncio.get_event_loop() def handle_msg(raw_msg: str) -> None: try: data: Dict[str, Any] = self._json_decoder.decode(raw_msg) except JSONDecodeError as e: log.error( f"Couldn't parse received JSON data in {self}: {e}\nmsg: {raw_msg}" ) return if not isinstance(data, dict): log.warning( f"Received invalid message type in {self}. " f"Expecting object, received type {type(data).__name__}: {data}" ) return _ = self.event_target.emit(RawMsgReceiveEvent(self, data)) try: op = data.pop("op") except KeyError: log.info(f"Ignoring message without op code in {self}: {data}") return event_type = data.get("type") cls = andesite.get_update_model(op, event_type) if cls is None: log.warning( f"Ignoring message with unknown op \"{op}\" in {self}: {data}" ) return try: message: andesite.ReceiveOperation = build_from_raw(cls, data) except Exception: log.exception( f"Couldn't parse message in {self} from Andesite node to {cls}: {data}" ) return message.client = self if isinstance(message, andesite.ConnectionUpdate): log.info( f"received connection update, setting last connection id in {self}." ) self.__last_connection_id = message.id _ = self.event_target.emit(message) if self.state is not None: loop.create_task(self.state._handle_andesite_message(message)) while True: try: raw_msg = await self.web_socket_client.recv() except asyncio.CancelledError: break except ConnectionClosed: _ = self.event_target.emit( WebSocketDisconnectEvent(self, False)) log.error( f"Disconnected from websocket, trying to reconnect {self}!" ) await self.connect() continue if log.isEnabledFor(logging.DEBUG): log.debug(f"Received message in {self}: {raw_msg}") try: handle_msg(raw_msg) except Exception: log.exception("Exception in %s while handling message %s.", self, raw_msg) def __start_read_loop(self, *, loop: asyncio.AbstractEventLoop = None) -> None: """Start the web socket reader. If the reader is already running, this is a no-op. """ if self.__read_loop and not self.__read_loop.done(): return if loop is None: loop = asyncio.get_event_loop() self.__read_loop = loop.create_task(self.__web_socket_reader()) def __stop_read_loop(self) -> None: """Stop the web socket reader. If the reader is already stopped, this is a no-op. """ if not self.__read_loop: return self.__read_loop.cancel() async def send(self, guild_id: int, op: str, payload: Dict[str, Any]) -> None: if self.web_socket_client is None or self.web_socket_client.open: log.info("%s: Not connected, connecting.", self) await self.connect() payload.update(guildId=str(guild_id), op=op) log.debug("%s: sending payload: %s", self, payload) _ = self.event_target.emit(RawMsgSendEvent(self, guild_id, op, payload)) data = self._json_encoder.encode(payload) try: await self.web_socket_client.send(data) except ConnectionClosed: # let the websocket reader handle this log.warning( "%s: couldn't send message because the connection is closed: %s", self, payload) else: state = self.state if state: loop = asyncio.get_event_loop() loop.create_task( state._handle_sent_message(guild_id, op, payload))