Exemple #1
0
 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"])
Exemple #3
0
 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))