Exemplo n.º 1
0
    def _create_checked_disconnect(self) -> Dict[str, Any]:
        if self._disconnect_emitted:
            raise falcon_errors.OperationNotAllowed(
                'The websocket.disconnect event has already been emitted, '
                'and so the app should not attempt to receive any more '
                'events, since ASGI servers will likely block indefinitely '
                'rather than re-emitting websocket.disconnect events.'
            )

        self._disconnect_emitted = True
        return {'type': EventType.WS_DISCONNECT, 'code': self._close_code}
Exemplo n.º 2
0
    def _require_accepted(self):
        if self._state == _WebSocketState.ACCEPTED:
            return

        if self._state in {_WebSocketState.CONNECT, _WebSocketState.HANDSHAKE}:
            raise falcon_errors.OperationNotAllowed(
                'WebSocket connection has not yet been accepted'
            )
        elif self._state == _WebSocketState.CLOSED:
            raise falcon_errors.WebSocketDisconnected(self._close_code)

        assert self._state == _WebSocketState.DENIED

        if self._close_code == WSCloseCode.PATH_NOT_FOUND:
            raise falcon_errors.WebSocketPathNotFound(WSCloseCode.PATH_NOT_FOUND)

        if self._close_code == WSCloseCode.SERVER_ERROR:
            raise falcon_errors.WebSocketServerError(WSCloseCode.SERVER_ERROR)

        if self._close_code == WSCloseCode.HANDLER_NOT_FOUND:
            raise falcon_errors.WebSocketHandlerNotFound(WSCloseCode.HANDLER_NOT_FOUND)

        raise falcon_errors.WebSocketDisconnected(self._close_code)
Exemplo n.º 3
0
 def _require_accepted(self):
     if self._state == _WebSocketState.HANDSHAKE:
         raise errors.OperationNotAllowed(
             'WebSocket connection has not yet been accepted')
     elif self._state == _WebSocketState.CLOSED:
         raise errors.WebSocketDisconnected(self._close_code)
Exemplo n.º 4
0
    async def accept(
        self,
        subprotocol: Optional[str] = None,
        headers: Optional[Union[Iterable[Iterable[str]], Mapping[str,
                                                                 str]]] = None,
    ):
        """Accept the incoming WebSocket connection.

        If, after examining the connection's attributes (headers, advertised
        subprotocols, etc.) the request should be accepted, the responder must
        first await this coroutine method to finalize the WebSocket handshake.
        Alternatively, the responder may deny the connection request by awaiting
        the :meth:`~.close` method.

        Keyword Arguments:
            subprotocol (str): The subprotocol the app wishes to accept,
                out of the list of protocols that the client suggested. If
                more than one of the suggested protocols is acceptable,
                the first one in the list from the client should be
                selected (see also: :attr:`~.subprotocols`).

                When left unspecified, a Sec-WebSocket-Protocol header will
                not be included in the response to the client. The
                client may choose to abandon the connection in this case,
                if it does not receive an explicit protocol selection.

            headers (Iterable[[str, str]]): An iterable of ``[name: str, value: str]``
                two-item iterables, representing a collection of HTTP headers to
                include in the handshake response. Both *name* and *value* must
                be of type ``str`` and contain only US-ASCII characters.

                Alternatively, a dict-like object may be passed that implements
                an ``items()`` method.

                Note:
                    This argument is only supported for ASGI servers that
                    implement spec version 2.1 or better. If an app needs to
                    be compatible with multiple ASGI servers, it can
                    reference the :attr:`~.supports_accept_headers` property to
                    determine if the hosting server supports this feature.

        """

        if self.closed:
            raise errors.OperationNotAllowed(
                'accept() may not be called on a closed WebSocket connection')

        if self._state != _WebSocketState.HANDSHAKE:
            raise errors.OperationNotAllowed(
                'accept() may only be called once on an open WebSocket connection'
            )

        event: Dict[str, Any] = {
            'type': EventType.WS_ACCEPT,
        }

        if subprotocol is not None:
            if not isinstance(subprotocol, str):
                raise ValueError('WebSocket subprotocol must be a string')

            event['subprotocol'] = subprotocol

        if headers:
            if not self._supports_accept_headers:
                raise errors.OperationNotAllowed(
                    'The ASGI server that is running this app '
                    'does not support accept headers.')

            header_items = getattr(headers, 'items', None)

            if callable(header_items):
                headers = header_items()

            event['headers'] = parsed_headers = [
                (name.lower().encode('ascii'), value.encode('ascii'))
                for name, value in headers  # type: ignore
            ]

            for name, __ in parsed_headers:
                if name == b'sec-websocket-protocol':
                    raise ValueError(
                        'Per the ASGI spec, the headers iterable must not '
                        'contain "sec-websocket-protocol". Instead, the '
                        'subprotocol argument can be used to indicate the '
                        'accepted protocol.')

        await self._send(event)
        self._state = _WebSocketState.ACCEPTED

        self._buffered_receiver.start()
Exemplo n.º 5
0
    async def _collect(self, event: Dict[str, Any]):
        assert event

        if self._state == _WebSocketState.CONNECT:
            raise falcon_errors.OperationNotAllowed(
                'An ASGI application must receive the first websocket.connect '
                'event before attempting to send any events.'
            )

        event_type = event['type']
        if self._state == _WebSocketState.HANDSHAKE:
            if event_type == EventType.WS_ACCEPT:
                self._state = _WebSocketState.ACCEPTED
                self._accepted_subprotocol = event.get('subprotocol')
                self._accepted_headers = event.get('headers')
                self._event_handshake_complete.set()

                # NOTE(kgriffs): Yield to other pending tasks that may be
                #   waiting on the completion of the handshake. This ensures
                #   that the simulated client connection can enter its context
                #   before the app logic continues and potentially closes the
                #   connection from that side.
                await asyncio.sleep(0)

            elif event_type == EventType.WS_CLOSE:
                self._state = _WebSocketState.DENIED

                desired_code = event.get('code', WSCloseCode.NORMAL)
                if desired_code == WSCloseCode.SERVER_ERROR or (3000 <= desired_code < 4000):
                    # NOTE(kgriffs): Pass this code through since it is a
                    #   special code we have set in the framework to trigger
                    #   different raised error types or to pass through a
                    #   raised HTTPError status code.
                    self._close_code = desired_code
                else:
                    # NOTE(kgriffs): Force the close code to this since it is
                    #   similar to what happens with a real web server (the HTTP
                    #   connection is closed with a 403 and there is no websocket
                    #   close code).
                    self._close_code = WSCloseCode.FORBIDDEN

                self._event_handshake_complete.set()

            else:
                raise falcon_errors.OperationNotAllowed(
                    'An ASGI application must send either websocket.accept or '
                    'websocket.close before sending any other event types (got '
                    '{0})'.format(event_type)
                )

        elif self._state == _WebSocketState.ACCEPTED:
            if event_type == EventType.WS_CLOSE:
                self._state = _WebSocketState.CLOSED
                self._close_code = event.get('code', WSCloseCode.NORMAL)
            else:
                assert event_type == EventType.WS_SEND
                self._collected_server_events.append(event)
        else:
            assert self.closed

            # NOTE(kgriffs): According to the ASGI spec, we are
            #   supposed to just silently eat events once the
            #   socket is disconnected.
            pass

        # NOTE(kgriffs): Give whatever is waiting on the handshake or a
        #   collected data/text event a chance to progress.
        await asyncio.sleep(0)