Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
wsgi.py198 linesDownload Raw Back to asgiref
1import sys2from collections import defaultdict3from tempfile import SpooledTemporaryFile4 5from asgiref.sync import AsyncToSync, sync_to_async6 7 8class WsgiToAsgi:9    """10    Wraps a WSGI application to make it into an ASGI application.11    """12 13    def __init__(self, wsgi_application, duplicate_header_limit=100):14        self.wsgi_application = wsgi_application15        self.duplicate_header_limit = duplicate_header_limit16 17    async def __call__(self, scope, receive, send):18        """19        ASGI application instantiation point.20        We return a new WsgiToAsgiInstance here with the WSGI app21        and the scope, ready to respond when it is __call__ed.22        """23        await WsgiToAsgiInstance(self.wsgi_application, self.duplicate_header_limit)(24            scope, receive, send25        )26 27 28class WsgiToAsgiInstance:29    """30    Per-socket instance of a wrapped WSGI application31    """32 33    def __init__(self, wsgi_application, duplicate_header_limit=100):34        self.wsgi_application = wsgi_application35        self.duplicate_header_limit = duplicate_header_limit36        self.response_started = False37        self.response_content_length = None38 39    async def __call__(self, scope, receive, send):40        if scope["type"] != "http":41            raise ValueError("WSGI wrapper received a non-HTTP scope")42        self.scope = scope43        with SpooledTemporaryFile(max_size=65536) as body:44            # Alright, wait for the http.request messages45            while True:46                message = await receive()47                if message["type"] != "http.request":48                    raise ValueError("WSGI wrapper received a non-HTTP-request message")49                body.write(message.get("body", b""))50                if not message.get("more_body"):51                    break52            body.seek(0)53            # Wrap send so it can be called from the subthread54            self.sync_send = AsyncToSync(send)55            # Call the WSGI app56            await self.run_wsgi_app(body)57 58    def build_environ(self, scope, body):59        """60        Builds a scope and request body into a WSGI environ object.61        """62        script_name = scope.get("root_path", "").encode("utf8").decode("latin1")63        path_info = scope["path"].encode("utf8").decode("latin1")64        if path_info.startswith(script_name):65            path_info = path_info[len(script_name) :]66        environ = {67            "REQUEST_METHOD": scope["method"],68            "SCRIPT_NAME": script_name,69            "PATH_INFO": path_info,70            "QUERY_STRING": scope["query_string"].decode("ascii"),71            "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"],72            "wsgi.version": (1, 0),73            "wsgi.url_scheme": scope.get("scheme", "http"),74            "wsgi.input": body,75            "wsgi.errors": sys.stderr,76            "wsgi.multithread": True,77            "wsgi.multiprocess": True,78            "wsgi.run_once": False,79        }80        # Get server name and port - required in WSGI, not in ASGI81        if "server" in scope:82            environ["SERVER_NAME"] = scope["server"][0]83            environ["SERVER_PORT"] = str(scope["server"][1])84        else:85            environ["SERVER_NAME"] = "localhost"86            environ["SERVER_PORT"] = "80"87 88        if scope.get("client") is not None:89            environ["REMOTE_ADDR"] = scope["client"][0]90 91        # Go through headers and make them into environ entries92        _headers = defaultdict(list)93        for name, value in self.scope.get("headers", []):94            name = name.decode("latin1")95            if name == "content-length":96                corrected_name = "CONTENT_LENGTH"97            elif name == "content-type":98                corrected_name = "CONTENT_TYPE"99            else:100                corrected_name = "HTTP_%s" % name.upper().replace("-", "_")101            # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case102            value = value.decode("latin1")103            if (104                self.duplicate_header_limit105                and len(_headers[corrected_name]) >= self.duplicate_header_limit106            ):107                raise ValueError(108                    f"Too many duplicate headers: {corrected_name} exceeds limit of"109                    f"{self.duplicate_header_limit}"110                )111            _headers[corrected_name].append(value)112        for name, values in _headers.items():113            environ[name] = ",".join(values)114        return environ115 116    def start_response(self, status, response_headers, exc_info=None):117        """118        WSGI start_response callable.119        """120        # Don't allow re-calling once response has begun121        if self.response_started:122            raise exc_info[1].with_traceback(exc_info[2])123        # Don't allow re-calling without exc_info124        if hasattr(self, "response_start") and exc_info is None:125            raise ValueError(126                "You cannot call start_response a second time without exc_info"127            )128        # Extract status code129        status_code, _ = status.split(" ", 1)130        status_code = int(status_code)131        # Extract headers132        headers = [133            (name.lower().encode("ascii"), value.encode("ascii"))134            for name, value in response_headers135        ]136        # Extract content-length137        self.response_content_length = None138        for name, value in response_headers:139            if name.lower() == "content-length":140                self.response_content_length = int(value)141        # Build and send response start message.142        self.response_start = {143            "type": "http.response.start",144            "status": status_code,145            "headers": headers,146        }147 148    @sync_to_async149    def run_wsgi_app(self, body):150        """151        Called in a subthread to run the WSGI app. We encapsulate like152        this so that the start_response callable is called in the same thread.153        """154        # Translate the scope and incoming request body into a WSGI environ155        try:156            environ = self.build_environ(self.scope, body)157        except ValueError:158            # Return 400 Bad Request if header limit exceeded159            self.sync_send(160                {161                    "type": "http.response.start",162                    "status": 400,163                    "headers": [(b"content-type", b"text/plain")],164                }165            )166            self.sync_send(167                {168                    "type": "http.response.body",169                    "body": b"Bad Request: Too many duplicate headers",170                }171            )172            return173        # Run the WSGI app174        bytes_sent = 0175        for output in self.wsgi_application(environ, self.start_response):176            # If this is the first response, include the response headers177            if not self.response_started:178                self.response_started = True179                self.sync_send(self.response_start)180            # If the application supplies a Content-Length header181            if self.response_content_length is not None:182                # The server should not transmit more bytes to the client than the header allows183                bytes_allowed = self.response_content_length - bytes_sent184                if len(output) > bytes_allowed:185                    output = output[:bytes_allowed]186            self.sync_send(187                {"type": "http.response.body", "body": output, "more_body": True}188            )189            bytes_sent += len(output)190            # The server should stop iterating over the response when enough data has been sent191            if bytes_sent == self.response_content_length:192                break193        # Close connection194        if not self.response_started:195            self.response_started = True196            self.sync_send(self.response_start)197        self.sync_send({"type": "http.response.body"})198 
codekingpro/portable-devtools · Team Ai