Example #1
0
    async def send(self, message: Message) -> None:
        """
        Awaited by the application to send ASGI `http` events.
        """
        message_type = message["type"]
        self.logger.info("%s:  '%s' event received from application.",
                         self.state, message_type)

        if (self.state is HTTPCycleState.REQUEST
                and message_type == "http.response.start"):
            self.response["statusCode"] = message["status"]
            headers: typing.Dict[str, str] = {}
            multi_value_headers: typing.Dict[str, typing.List[str]] = {}
            for key, value in message.get("headers", []):
                lower_key = key.decode().lower()
                if lower_key in multi_value_headers:
                    multi_value_headers[lower_key].append(value.decode())
                elif lower_key in headers:
                    multi_value_headers[lower_key] = [
                        headers.pop(lower_key),
                        value.decode(),
                    ]
                else:
                    headers[lower_key] = value.decode()

            self.response["headers"] = headers
            if multi_value_headers:
                self.response["multiValueHeaders"] = multi_value_headers
            self.state = HTTPCycleState.RESPONSE

        elif (self.state is HTTPCycleState.RESPONSE
              and message_type == "http.response.body"):
            body = message.get("body", b"")
            more_body = message.get("more_body", False)

            # The body must be completely read before returning the response.
            self.body += body

            if not more_body:
                body = self.body

                # Check if a binary response should be returned based on the mime type
                # or content encoding.
                mimetype, _ = cgi.parse_header(self.response["headers"].get(
                    "content-type", "text/plain"))
                if (mimetype not in self.text_mime_types
                        and not mimetype.startswith("text/")
                    ) or self.response["headers"].get("content-encoding") in [
                        "gzip", "br"
                    ]:
                    body = base64.b64encode(body)
                    self.response["isBase64Encoded"] = True

                self.response["body"] = body.decode()
                self.state = HTTPCycleState.COMPLETE
                await self.app_queue.put({"type": "http.disconnect"})

        else:
            raise UnexpectedMessage(
                f"{self.state}: Unexpected '{message_type}' event received.")
Example #2
0
    async def send(self, message: Message) -> None:
        """
        Awaited by the application to send ASGI `websocket` events.
        """
        message_type = message["type"]
        self.logger.info(
            "%s:  '%s' event received from application.", self.state, message_type
        )

        if self.state is WebSocketCycleState.HANDSHAKE and message_type in (
            "websocket.accept",
            "websocket.close",
        ):

            # API Gateway handles the WebSocket client handshake in the connect event,
            # and it cannot be negotiated by the application directly. The handshake
            # behaviour is simulated to allow the application to accept or reject the
            # the client connection. This process does not support subprotocols.
            if message_type == "websocket.accept":
                await self.app_queue.put(
                    {"type": "websocket.receive", "bytes": None, "text": self.body}
                )
            elif message_type == "websocket.close":
                self.state = WebSocketCycleState.CLOSED
                raise WebSocketClosed
        elif (
            self.state is WebSocketCycleState.RESPONSE
            and message_type == "websocket.send"
        ):

            # Message data sent from the application is posted to the WebSocket client
            # in API Gateway using an API call.
            message_text = message.get("text", "")
            self.websocket.post_to_connection(message_text.encode())
            await self.app_queue.put({"type": "websocket.disconnect", "code": "1000"})

        elif (
            self.state is WebSocketCycleState.DISCONNECTING
            and message_type == "websocket.close"
        ):
            # ASGI connection is closing, however the WebSocket client in API Gateway
            # will persist and be used in future application ASGI connections until the
            # client disconnects or the application rejects a handshake.
            self.state = WebSocketCycleState.CLOSED
        else:
            raise UnexpectedMessage(
                f"{self.state}: Unexpected '{message_type}' event received."
            )
Example #3
0
    async def send(self, message: Message) -> None:
        """
        Awaited by the application to send ASGI `lifespan` events.
        """
        message_type = message["type"]
        self.logger.info("%s:  '%s' event received from application.",
                         self.state, message_type)

        if self.state is LifespanCycleState.CONNECTING:

            if self.lifespan == "on":
                raise LifespanFailure(
                    "Lifespan connection failed during startup and lifespan is 'on'."
                )

            # If a message is sent before the startup event is received by the
            # application, then assume that lifespan is unsupported.
            self.state = LifespanCycleState.UNSUPPORTED
            raise LifespanUnsupported("Lifespan protocol appears unsupported.")

        if message_type not in (
                "lifespan.startup.complete",
                "lifespan.shutdown.complete",
                "lifespan.startup.failed",
                "lifespan.shutdown.failed",
        ):
            self.state = LifespanCycleState.FAILED
            raise UnexpectedMessage(
                f"Unexpected '{message_type}' event received.")

        if self.state is LifespanCycleState.STARTUP:
            if message_type == "lifespan.startup.complete":
                self.startup_event.set()
            elif message_type == "lifespan.startup.failed":
                self.state = LifespanCycleState.FAILED
                self.startup_event.set()
                message = message.get("message", "")
                raise LifespanFailure(f"Lifespan startup failure. {message}")

        elif self.state is LifespanCycleState.SHUTDOWN:
            if message_type == "lifespan.shutdown.complete":
                self.shutdown_event.set()
            elif message_type == "lifespan.shutdown.failed":
                self.state = LifespanCycleState.FAILED
                self.shutdown_event.set()
                message = message.get("message", "")
                raise LifespanFailure(f"Lifespan shutdown failure. {message}")
Example #4
0
    async def send(self, message: Message) -> None:
        """
        Awaited by the application to send ASGI `http` events.
        """
        message_type = message["type"]
        self.logger.info("%s:  '%s' event received from application.",
                         self.state, message_type)

        if (self.state is HTTPCycleState.REQUEST
                and message_type == "http.response.start"):
            self.response["statusCode"] = message["status"]
            self.response["headers"] = {
                k.decode().lower(): v.decode()
                for k, v in message.get("headers", [])
            }
            self.state = HTTPCycleState.RESPONSE

        elif (self.state is HTTPCycleState.RESPONSE
              and message_type == "http.response.body"):
            body = message.get("body", b"")
            more_body = message.get("more_body", False)

            # The body must be completely read before returning the response.
            self.body += body

            if not more_body:
                body = self.body
                mimetype, _ = cgi.parse_header(self.response["headers"].get(
                    "content-type", "text/plain"))

                # Check if a binary response should be returned based on the mime type
                # or content encoding.
                if (mimetype not in self.text_mime_types
                        and not mimetype.startswith("text/")) or self.response[
                            "headers"].get("content-encoding") == "gzip":
                    body = base64.b64encode(body)
                    self.response["isBase64Encoded"] = True

                self.response["body"] = body.decode()
                self.state = HTTPCycleState.COMPLETE
                await self.app_queue.put({"type": "http.disconnect"})

        else:
            raise UnexpectedMessage(
                f"{self.state}: Unexpected '{message_type}' event received.")
Example #5
0
    async def send(self, message: Message) -> None:
        """
        Awaited by the application to send ASGI `http` events.
        """
        message_type = message["type"]
        self.logger.info("%s:  '%s' event received from application.",
                         self.state, message_type)

        if (self.state is HTTPCycleState.REQUEST
                and message_type == "http.response.start"):
            self.response["statusCode"] = message["status"]
            headers: typing.Dict[str, str] = {}
            multi_value_headers: typing.Dict[str, typing.List[str]] = {}
            cookies: typing.List[str] = []
            event = self.scope["aws.event"]
            # ELB
            if "elb" in event["requestContext"]:
                for key, value in message.get("headers", []):
                    lower_key = key.decode().lower()
                    if lower_key in multi_value_headers:
                        multi_value_headers[lower_key].append(value.decode())
                    else:
                        multi_value_headers[lower_key] = [value.decode()]
                if "multiValueHeaders" not in event:
                    # If there are multiple occurrences of headers, create case-mutated
                    # variations: https://github.com/logandk/serverless-wsgi/issues/11
                    for key, values in multi_value_headers.items():
                        if len(values) > 1:
                            for value, cased_key in zip(
                                    values, all_casings(key)):
                                headers[cased_key] = value
                        elif len(values) == 1:
                            headers[key] = values[0]
                    multi_value_headers = {}
            # API Gateway
            else:
                for key, value in message.get("headers", []):
                    lower_key = key.decode().lower()
                    if event.get(
                            "version") == "2.0" and lower_key == "set-cookie":
                        cookies.append(value.decode())
                    elif lower_key in multi_value_headers:
                        multi_value_headers[lower_key].append(value.decode())
                    elif lower_key in headers:
                        multi_value_headers[lower_key] = [
                            headers.pop(lower_key),
                            value.decode(),
                        ]
                    else:
                        headers[lower_key] = value.decode()

            self.response["headers"] = headers
            if multi_value_headers:
                self.response["multiValueHeaders"] = multi_value_headers
            if cookies:
                self.response["cookies"] = cookies
            self.state = HTTPCycleState.RESPONSE

        elif (self.state is HTTPCycleState.RESPONSE
              and message_type == "http.response.body"):
            body = message.get("body", b"")
            more_body = message.get("more_body", False)

            # The body must be completely read before returning the response.
            self.body.write(body)

            if not more_body:
                body = self.body.getvalue()
                self.body.close()

                # Check if a binary response should be returned based on the mime type
                # or content encoding.
                mimetype, _ = cgi.parse_header(self.response["headers"].get(
                    "content-type", "text/plain"))
                if (mimetype not in self.text_mime_types
                        and not mimetype.startswith("text/")
                    ) or self.response["headers"].get("content-encoding") in [
                        "gzip", "br"
                    ]:
                    body = base64.b64encode(body)
                    self.response["isBase64Encoded"] = True

                self.response["body"] = body.decode()
                self.state = HTTPCycleState.COMPLETE
                await self.app_queue.put({"type": "http.disconnect"})

        else:
            raise UnexpectedMessage(
                f"{self.state}: Unexpected '{message_type}' event received.")