diff options
author | AlexSm <[email protected]> | 2024-03-05 10:40:59 +0100 |
---|---|---|
committer | GitHub <[email protected]> | 2024-03-05 12:40:59 +0300 |
commit | 1ac13c847b5358faba44dbb638a828e24369467b (patch) | |
tree | 07672b4dd3604ad3dee540a02c6494cb7d10dc3d /contrib/tools/python3/Lib/wsgiref | |
parent | ffcca3e7f7958ddc6487b91d3df8c01054bd0638 (diff) |
Library import 16 (#2433)
Co-authored-by: robot-piglet <[email protected]>
Co-authored-by: deshevoy <[email protected]>
Co-authored-by: robot-contrib <[email protected]>
Co-authored-by: thegeorg <[email protected]>
Co-authored-by: robot-ya-builder <[email protected]>
Co-authored-by: svidyuk <[email protected]>
Co-authored-by: shadchin <[email protected]>
Co-authored-by: robot-ratatosk <[email protected]>
Co-authored-by: innokentii <[email protected]>
Co-authored-by: arkady-e1ppa <[email protected]>
Co-authored-by: snermolaev <[email protected]>
Co-authored-by: dimdim11 <[email protected]>
Co-authored-by: kickbutt <[email protected]>
Co-authored-by: abdullinsaid <[email protected]>
Co-authored-by: korsunandrei <[email protected]>
Co-authored-by: petrk <[email protected]>
Co-authored-by: miroslav2 <[email protected]>
Co-authored-by: serjflint <[email protected]>
Co-authored-by: akhropov <[email protected]>
Co-authored-by: prettyboy <[email protected]>
Co-authored-by: ilikepugs <[email protected]>
Co-authored-by: hiddenpath <[email protected]>
Co-authored-by: mikhnenko <[email protected]>
Co-authored-by: spreis <[email protected]>
Co-authored-by: andreyshspb <[email protected]>
Co-authored-by: dimaandreev <[email protected]>
Co-authored-by: rashid <[email protected]>
Co-authored-by: robot-ydb-importer <[email protected]>
Co-authored-by: r-vetrov <[email protected]>
Co-authored-by: ypodlesov <[email protected]>
Co-authored-by: zaverden <[email protected]>
Co-authored-by: vpozdyayev <[email protected]>
Co-authored-by: robot-cozmo <[email protected]>
Co-authored-by: v-korovin <[email protected]>
Co-authored-by: arikon <[email protected]>
Co-authored-by: khoden <[email protected]>
Co-authored-by: psydmm <[email protected]>
Co-authored-by: robot-javacom <[email protected]>
Co-authored-by: dtorilov <[email protected]>
Co-authored-by: sennikovmv <[email protected]>
Co-authored-by: hcpp <[email protected]>
Diffstat (limited to 'contrib/tools/python3/Lib/wsgiref')
-rw-r--r-- | contrib/tools/python3/Lib/wsgiref/__init__.py | 25 | ||||
-rw-r--r-- | contrib/tools/python3/Lib/wsgiref/handlers.py | 573 | ||||
-rw-r--r-- | contrib/tools/python3/Lib/wsgiref/headers.py | 184 | ||||
-rw-r--r-- | contrib/tools/python3/Lib/wsgiref/simple_server.py | 165 | ||||
-rw-r--r-- | contrib/tools/python3/Lib/wsgiref/types.py | 54 | ||||
-rw-r--r-- | contrib/tools/python3/Lib/wsgiref/util.py | 159 | ||||
-rw-r--r-- | contrib/tools/python3/Lib/wsgiref/validate.py | 438 |
7 files changed, 1598 insertions, 0 deletions
diff --git a/contrib/tools/python3/Lib/wsgiref/__init__.py b/contrib/tools/python3/Lib/wsgiref/__init__.py new file mode 100644 index 00000000000..59ee48fddec --- /dev/null +++ b/contrib/tools/python3/Lib/wsgiref/__init__.py @@ -0,0 +1,25 @@ +"""wsgiref -- a WSGI (PEP 3333) Reference Library + +Current Contents: + +* util -- Miscellaneous useful functions and wrappers + +* headers -- Manage response headers + +* handlers -- base classes for server/gateway implementations + +* simple_server -- a simple BaseHTTPServer that supports WSGI + +* validate -- validation wrapper that sits between an app and a server + to detect errors in either + +* types -- collection of WSGI-related types for static type checking + +To-Do: + +* cgi_gateway -- Run WSGI apps under CGI (pending a deployment standard) + +* cgi_wrapper -- Run CGI apps under WSGI + +* router -- a simple middleware component that handles URL traversal +""" diff --git a/contrib/tools/python3/Lib/wsgiref/handlers.py b/contrib/tools/python3/Lib/wsgiref/handlers.py new file mode 100644 index 00000000000..cafe872c7aa --- /dev/null +++ b/contrib/tools/python3/Lib/wsgiref/handlers.py @@ -0,0 +1,573 @@ +"""Base classes for server/gateway implementations""" + +from .util import FileWrapper, guess_scheme, is_hop_by_hop +from .headers import Headers + +import sys, os, time + +__all__ = [ + 'BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler', + 'IISCGIHandler', 'read_environ' +] + +# Weekday and month names for HTTP date/time formatting; always English! +_weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +_monthname = [None, # Dummy so we can use 1-based month numbers + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + +def format_date_time(timestamp): + year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) + return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + _weekdayname[wd], day, _monthname[month], year, hh, mm, ss + ) + +_is_request = { + 'SCRIPT_NAME', 'PATH_INFO', 'QUERY_STRING', 'REQUEST_METHOD', 'AUTH_TYPE', + 'CONTENT_TYPE', 'CONTENT_LENGTH', 'HTTPS', 'REMOTE_USER', 'REMOTE_IDENT', +}.__contains__ + +def _needs_transcode(k): + return _is_request(k) or k.startswith('HTTP_') or k.startswith('SSL_') \ + or (k.startswith('REDIRECT_') and _needs_transcode(k[9:])) + +def read_environ(): + """Read environment, fixing HTTP variables""" + enc = sys.getfilesystemencoding() + esc = 'surrogateescape' + try: + ''.encode('utf-8', esc) + except LookupError: + esc = 'replace' + environ = {} + + # Take the basic environment from native-unicode os.environ. Attempt to + # fix up the variables that come from the HTTP request to compensate for + # the bytes->unicode decoding step that will already have taken place. + for k, v in os.environ.items(): + if _needs_transcode(k): + + # On win32, the os.environ is natively Unicode. Different servers + # decode the request bytes using different encodings. + if sys.platform == 'win32': + software = os.environ.get('SERVER_SOFTWARE', '').lower() + + # On IIS, the HTTP request will be decoded as UTF-8 as long + # as the input is a valid UTF-8 sequence. Otherwise it is + # decoded using the system code page (mbcs), with no way to + # detect this has happened. Because UTF-8 is the more likely + # encoding, and mbcs is inherently unreliable (an mbcs string + # that happens to be valid UTF-8 will not be decoded as mbcs) + # always recreate the original bytes as UTF-8. + if software.startswith('microsoft-iis/'): + v = v.encode('utf-8').decode('iso-8859-1') + + # Apache mod_cgi writes bytes-as-unicode (as if ISO-8859-1) direct + # to the Unicode environ. No modification needed. + elif software.startswith('apache/'): + pass + + # Python 3's http.server.CGIHTTPRequestHandler decodes + # using the urllib.unquote default of UTF-8, amongst other + # issues. + elif ( + software.startswith('simplehttp/') + and 'python/3' in software + ): + v = v.encode('utf-8').decode('iso-8859-1') + + # For other servers, guess that they have written bytes to + # the environ using stdio byte-oriented interfaces, ending up + # with the system code page. + else: + v = v.encode(enc, 'replace').decode('iso-8859-1') + + # Recover bytes from unicode environ, using surrogate escapes + # where available (Python 3.1+). + else: + v = v.encode(enc, esc).decode('iso-8859-1') + + environ[k] = v + return environ + + +class BaseHandler: + """Manage the invocation of a WSGI application""" + + # Configuration parameters; can override per-subclass or per-instance + wsgi_version = (1,0) + wsgi_multithread = True + wsgi_multiprocess = True + wsgi_run_once = False + + origin_server = True # We are transmitting direct to client + http_version = "1.0" # Version that should be used for response + server_software = None # String name of server software, if any + + # os_environ is used to supply configuration from the OS environment: + # by default it's a copy of 'os.environ' as of import time, but you can + # override this in e.g. your __init__ method. + os_environ= read_environ() + + # Collaborator classes + wsgi_file_wrapper = FileWrapper # set to None to disable + headers_class = Headers # must be a Headers-like class + + # Error handling (also per-subclass or per-instance) + traceback_limit = None # Print entire traceback to self.get_stderr() + error_status = "500 Internal Server Error" + error_headers = [('Content-Type','text/plain')] + error_body = b"A server error occurred. Please contact the administrator." + + # State variables (don't mess with these) + status = result = None + headers_sent = False + headers = None + bytes_sent = 0 + + def run(self, application): + """Invoke the application""" + # Note to self: don't move the close()! Asynchronous servers shouldn't + # call close() from finish_response(), so if you close() anywhere but + # the double-error branch here, you'll break asynchronous servers by + # prematurely closing. Async servers must return from 'run()' without + # closing if there might still be output to iterate over. + try: + self.setup_environ() + self.result = application(self.environ, self.start_response) + self.finish_response() + except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError): + # We expect the client to close the connection abruptly from time + # to time. + return + except: + try: + self.handle_error() + except: + # If we get an error handling an error, just give up already! + self.close() + raise # ...and let the actual server figure it out. + + + def setup_environ(self): + """Set up the environment for one request""" + + env = self.environ = self.os_environ.copy() + self.add_cgi_vars() + + env['wsgi.input'] = self.get_stdin() + env['wsgi.errors'] = self.get_stderr() + env['wsgi.version'] = self.wsgi_version + env['wsgi.run_once'] = self.wsgi_run_once + env['wsgi.url_scheme'] = self.get_scheme() + env['wsgi.multithread'] = self.wsgi_multithread + env['wsgi.multiprocess'] = self.wsgi_multiprocess + + if self.wsgi_file_wrapper is not None: + env['wsgi.file_wrapper'] = self.wsgi_file_wrapper + + if self.origin_server and self.server_software: + env.setdefault('SERVER_SOFTWARE',self.server_software) + + + def finish_response(self): + """Send any iterable data, then close self and the iterable + + Subclasses intended for use in asynchronous servers will + want to redefine this method, such that it sets up callbacks + in the event loop to iterate over the data, and to call + 'self.close()' once the response is finished. + """ + try: + if not self.result_is_file() or not self.sendfile(): + for data in self.result: + self.write(data) + self.finish_content() + except: + # Call close() on the iterable returned by the WSGI application + # in case of an exception. + if hasattr(self.result, 'close'): + self.result.close() + raise + else: + # We only call close() when no exception is raised, because it + # will set status, result, headers, and environ fields to None. + # See bpo-29183 for more details. + self.close() + + + def get_scheme(self): + """Return the URL scheme being used""" + return guess_scheme(self.environ) + + + def set_content_length(self): + """Compute Content-Length or switch to chunked encoding if possible""" + try: + blocks = len(self.result) + except (TypeError,AttributeError,NotImplementedError): + pass + else: + if blocks==1: + self.headers['Content-Length'] = str(self.bytes_sent) + return + # XXX Try for chunked encoding if origin server and client is 1.1 + + + def cleanup_headers(self): + """Make any necessary header changes or defaults + + Subclasses can extend this to add other defaults. + """ + if 'Content-Length' not in self.headers: + self.set_content_length() + + def start_response(self, status, headers,exc_info=None): + """'start_response()' callable as specified by PEP 3333""" + + if exc_info: + try: + if self.headers_sent: + raise + finally: + exc_info = None # avoid dangling circular ref + elif self.headers is not None: + raise AssertionError("Headers already set!") + + self.status = status + self.headers = self.headers_class(headers) + status = self._convert_string_type(status, "Status") + self._validate_status(status) + + if __debug__: + for name, val in headers: + name = self._convert_string_type(name, "Header name") + val = self._convert_string_type(val, "Header value") + assert not is_hop_by_hop(name),\ + f"Hop-by-hop header, '{name}: {val}', not allowed" + + return self.write + + def _validate_status(self, status): + if len(status) < 4: + raise AssertionError("Status must be at least 4 characters") + if not status[:3].isdigit(): + raise AssertionError("Status message must begin w/3-digit code") + if status[3] != " ": + raise AssertionError("Status message must have a space after code") + + def _convert_string_type(self, value, title): + """Convert/check value type.""" + if type(value) is str: + return value + raise AssertionError( + "{0} must be of type str (got {1})".format(title, repr(value)) + ) + + def send_preamble(self): + """Transmit version/status/date/server, via self._write()""" + if self.origin_server: + if self.client_is_modern(): + self._write(('HTTP/%s %s\r\n' % (self.http_version,self.status)).encode('iso-8859-1')) + if 'Date' not in self.headers: + self._write( + ('Date: %s\r\n' % format_date_time(time.time())).encode('iso-8859-1') + ) + if self.server_software and 'Server' not in self.headers: + self._write(('Server: %s\r\n' % self.server_software).encode('iso-8859-1')) + else: + self._write(('Status: %s\r\n' % self.status).encode('iso-8859-1')) + + def write(self, data): + """'write()' callable as specified by PEP 3333""" + + assert type(data) is bytes, \ + "write() argument must be a bytes instance" + + if not self.status: + raise AssertionError("write() before start_response()") + + elif not self.headers_sent: + # Before the first output, send the stored headers + self.bytes_sent = len(data) # make sure we know content-length + self.send_headers() + else: + self.bytes_sent += len(data) + + # XXX check Content-Length and truncate if too many bytes written? + self._write(data) + self._flush() + + + def sendfile(self): + """Platform-specific file transmission + + Override this method in subclasses to support platform-specific + file transmission. It is only called if the application's + return iterable ('self.result') is an instance of + 'self.wsgi_file_wrapper'. + + This method should return a true value if it was able to actually + transmit the wrapped file-like object using a platform-specific + approach. It should return a false value if normal iteration + should be used instead. An exception can be raised to indicate + that transmission was attempted, but failed. + + NOTE: this method should call 'self.send_headers()' if + 'self.headers_sent' is false and it is going to attempt direct + transmission of the file. + """ + return False # No platform-specific transmission by default + + + def finish_content(self): + """Ensure headers and content have both been sent""" + if not self.headers_sent: + # Only zero Content-Length if not set by the application (so + # that HEAD requests can be satisfied properly, see #3839) + self.headers.setdefault('Content-Length', "0") + self.send_headers() + else: + pass # XXX check if content-length was too short? + + def close(self): + """Close the iterable (if needed) and reset all instance vars + + Subclasses may want to also drop the client connection. + """ + try: + if hasattr(self.result,'close'): + self.result.close() + finally: + self.result = self.headers = self.status = self.environ = None + self.bytes_sent = 0; self.headers_sent = False + + + def send_headers(self): + """Transmit headers to the client, via self._write()""" + self.cleanup_headers() + self.headers_sent = True + if not self.origin_server or self.client_is_modern(): + self.send_preamble() + self._write(bytes(self.headers)) + + + def result_is_file(self): + """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'""" + wrapper = self.wsgi_file_wrapper + return wrapper is not None and isinstance(self.result,wrapper) + + + def client_is_modern(self): + """True if client can accept status and headers""" + return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9' + + + def log_exception(self,exc_info): + """Log the 'exc_info' tuple in the server log + + Subclasses may override to retarget the output or change its format. + """ + try: + from traceback import print_exception + stderr = self.get_stderr() + print_exception( + exc_info[0], exc_info[1], exc_info[2], + self.traceback_limit, stderr + ) + stderr.flush() + finally: + exc_info = None + + def handle_error(self): + """Log current error, and send error output to client if possible""" + self.log_exception(sys.exc_info()) + if not self.headers_sent: + self.result = self.error_output(self.environ, self.start_response) + self.finish_response() + # XXX else: attempt advanced recovery techniques for HTML or text? + + def error_output(self, environ, start_response): + """WSGI mini-app to create error output + + By default, this just uses the 'error_status', 'error_headers', + and 'error_body' attributes to generate an output page. It can + be overridden in a subclass to dynamically generate diagnostics, + choose an appropriate message for the user's preferred language, etc. + + Note, however, that it's not recommended from a security perspective to + spit out diagnostics to any old user; ideally, you should have to do + something special to enable diagnostic output, which is why we don't + include any here! + """ + start_response(self.error_status,self.error_headers[:],sys.exc_info()) + return [self.error_body] + + + # Pure abstract methods; *must* be overridden in subclasses + + def _write(self,data): + """Override in subclass to buffer data for send to client + + It's okay if this method actually transmits the data; BaseHandler + just separates write and flush operations for greater efficiency + when the underlying system actually has such a distinction. + """ + raise NotImplementedError + + def _flush(self): + """Override in subclass to force sending of recent '_write()' calls + + It's okay if this method is a no-op (i.e., if '_write()' actually + sends the data. + """ + raise NotImplementedError + + def get_stdin(self): + """Override in subclass to return suitable 'wsgi.input'""" + raise NotImplementedError + + def get_stderr(self): + """Override in subclass to return suitable 'wsgi.errors'""" + raise NotImplementedError + + def add_cgi_vars(self): + """Override in subclass to insert CGI variables in 'self.environ'""" + raise NotImplementedError + + +class SimpleHandler(BaseHandler): + """Handler that's just initialized with streams, environment, etc. + + This handler subclass is intended for synchronous HTTP/1.0 origin servers, + and handles sending the entire response output, given the correct inputs. + + Usage:: + + handler = SimpleHandler( + inp,out,err,env, multithread=False, multiprocess=True + ) + handler.run(app)""" + + def __init__(self,stdin,stdout,stderr,environ, + multithread=True, multiprocess=False + ): + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.base_env = environ + self.wsgi_multithread = multithread + self.wsgi_multiprocess = multiprocess + + def get_stdin(self): + return self.stdin + + def get_stderr(self): + return self.stderr + + def add_cgi_vars(self): + self.environ.update(self.base_env) + + def _write(self,data): + result = self.stdout.write(data) + if result is None or result == len(data): + return + from warnings import warn + warn("SimpleHandler.stdout.write() should not do partial writes", + DeprecationWarning) + while data := data[result:]: + result = self.stdout.write(data) + + def _flush(self): + self.stdout.flush() + self._flush = self.stdout.flush + + +class BaseCGIHandler(SimpleHandler): + + """CGI-like systems using input/output/error streams and environ mapping + + Usage:: + + handler = BaseCGIHandler(inp,out,err,env) + handler.run(app) + + This handler class is useful for gateway protocols like ReadyExec and + FastCGI, that have usable input/output/error streams and an environment + mapping. It's also the base class for CGIHandler, which just uses + sys.stdin, os.environ, and so on. + + The constructor also takes keyword arguments 'multithread' and + 'multiprocess' (defaulting to 'True' and 'False' respectively) to control + the configuration sent to the application. It sets 'origin_server' to + False (to enable CGI-like output), and assumes that 'wsgi.run_once' is + False. + """ + + origin_server = False + + +class CGIHandler(BaseCGIHandler): + + """CGI-based invocation via sys.stdin/stdout/stderr and os.environ + + Usage:: + + CGIHandler().run(app) + + The difference between this class and BaseCGIHandler is that it always + uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and + 'wsgi.multiprocess' of 'True'. It does not take any initialization + parameters, but always uses 'sys.stdin', 'os.environ', and friends. + + If you need to override any of these parameters, use BaseCGIHandler + instead. + """ + + wsgi_run_once = True + # Do not allow os.environ to leak between requests in Google App Engine + # and other multi-run CGI use cases. This is not easily testable. + # See http://bugs.python.org/issue7250 + os_environ = {} + + def __init__(self): + BaseCGIHandler.__init__( + self, sys.stdin.buffer, sys.stdout.buffer, sys.stderr, + read_environ(), multithread=False, multiprocess=True + ) + + +class IISCGIHandler(BaseCGIHandler): + """CGI-based invocation with workaround for IIS path bug + + This handler should be used in preference to CGIHandler when deploying on + Microsoft IIS without having set the config allowPathInfo option (IIS>=7) + or metabase allowPathInfoForScriptMappings (IIS<7). + """ + wsgi_run_once = True + os_environ = {} + + # By default, IIS gives a PATH_INFO that duplicates the SCRIPT_NAME at + # the front, causing problems for WSGI applications that wish to implement + # routing. This handler strips any such duplicated path. + + # IIS can be configured to pass the correct PATH_INFO, but this causes + # another bug where PATH_TRANSLATED is wrong. Luckily this variable is + # rarely used and is not guaranteed by WSGI. On IIS<7, though, the + # setting can only be made on a vhost level, affecting all other script + # mappings, many of which break when exposed to the PATH_TRANSLATED bug. + # For this reason IIS<7 is almost never deployed with the fix. (Even IIS7 + # rarely uses it because there is still no UI for it.) + + # There is no way for CGI code to tell whether the option was set, so a + # separate handler class is provided. + def __init__(self): + environ= read_environ() + path = environ.get('PATH_INFO', '') + script = environ.get('SCRIPT_NAME', '') + if (path+'/').startswith(script+'/'): + environ['PATH_INFO'] = path[len(script):] + BaseCGIHandler.__init__( + self, sys.stdin.buffer, sys.stdout.buffer, sys.stderr, + environ, multithread=False, multiprocess=True + ) diff --git a/contrib/tools/python3/Lib/wsgiref/headers.py b/contrib/tools/python3/Lib/wsgiref/headers.py new file mode 100644 index 00000000000..fab851c5a44 --- /dev/null +++ b/contrib/tools/python3/Lib/wsgiref/headers.py @@ -0,0 +1,184 @@ +"""Manage HTTP Response Headers + +Much of this module is red-handedly pilfered from email.message in the stdlib, +so portions are Copyright (C) 2001,2002 Python Software Foundation, and were +written by Barry Warsaw. +""" + +# Regular expression that matches `special' characters in parameters, the +# existence of which force quoting of the parameter value. +import re +tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') + +def _formatparam(param, value=None, quote=1): + """Convenience function to format and return a key=value pair. + + This will quote the value if needed or if quote is true. + """ + if value is not None and len(value) > 0: + if quote or tspecials.search(value): + value = value.replace('\\', '\\\\').replace('"', r'\"') + return '%s="%s"' % (param, value) + else: + return '%s=%s' % (param, value) + else: + return param + + +class Headers: + """Manage a collection of HTTP response headers""" + + def __init__(self, headers=None): + headers = headers if headers is not None else [] + if type(headers) is not list: + raise TypeError("Headers must be a list of name/value tuples") + self._headers = headers + if __debug__: + for k, v in headers: + self._convert_string_type(k) + self._convert_string_type(v) + + def _convert_string_type(self, value): + """Convert/check value type.""" + if type(value) is str: + return value + raise AssertionError("Header names/values must be" + " of type str (got {0})".format(repr(value))) + + def __len__(self): + """Return the total number of headers, including duplicates.""" + return len(self._headers) + + def __setitem__(self, name, val): + """Set the value of a header.""" + del self[name] + self._headers.append( + (self._convert_string_type(name), self._convert_string_type(val))) + + def __delitem__(self,name): + """Delete all occurrences of a header, if present. + + Does *not* raise an exception if the header is missing. + """ + name = self._convert_string_type(name.lower()) + self._headers[:] = [kv for kv in self._headers if kv[0].lower() != name] + + def __getitem__(self,name): + """Get the first header value for 'name' + + Return None if the header is missing instead of raising an exception. + + Note that if the header appeared multiple times, the first exactly which + occurrence gets returned is undefined. Use getall() to get all + the values matching a header field name. + """ + return self.get(name) + + def __contains__(self, name): + """Return true if the message contains the header.""" + return self.get(name) is not None + + + def get_all(self, name): + """Return a list of all the values for the named field. + + These will be sorted in the order they appeared in the original header + list or were added to this instance, and may contain duplicates. Any + fields deleted and re-inserted are always appended to the header list. + If no fields exist with the given name, returns an empty list. + """ + name = self._convert_string_type(name.lower()) + return [kv[1] for kv in self._headers if kv[0].lower()==name] + + + def get(self,name,default=None): + """Get the first header value for 'name', or return 'default'""" + name = self._convert_string_type(name.lower()) + for k,v in self._headers: + if k.lower()==name: + return v + return default + + + def keys(self): + """Return a list of all the header field names. + + These will be sorted in the order they appeared in the original header + list, or were added to this instance, and may contain duplicates. + Any fields deleted and re-inserted are always appended to the header + list. + """ + return [k for k, v in self._headers] + + def values(self): + """Return a list of all header values. + + These will be sorted in the order they appeared in the original header + list, or were added to this instance, and may contain duplicates. + Any fields deleted and re-inserted are always appended to the header + list. + """ + return [v for k, v in self._headers] + + def items(self): + """Get all the header fields and values. + + These will be sorted in the order they were in the original header + list, or were added to this instance, and may contain duplicates. + Any fields deleted and re-inserted are always appended to the header + list. + """ + return self._headers[:] + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, self._headers) + + def __str__(self): + """str() returns the formatted headers, complete with end line, + suitable for direct HTTP transmission.""" + return '\r\n'.join(["%s: %s" % kv for kv in self._headers]+['','']) + + def __bytes__(self): + return str(self).encode('iso-8859-1') + + def setdefault(self,name,value): + """Return first matching header value for 'name', or 'value' + + If there is no header named 'name', add a new header with name 'name' + and value 'value'.""" + result = self.get(name) + if result is None: + self._headers.append((self._convert_string_type(name), + self._convert_string_type(value))) + return value + else: + return result + + def add_header(self, _name, _value, **_params): + """Extended header setting. + + _name is the header field to add. keyword arguments can be used to set + additional parameters for the header field, with underscores converted + to dashes. Normally the parameter will be added as key="value" unless + value is None, in which case only the key will be added. + + Example: + + h.add_header('content-disposition', 'attachment', filename='bud.gif') + + Note that unlike the corresponding 'email.message' method, this does + *not* handle '(charset, language, value)' tuples: all values must be + strings or None. + """ + parts = [] + if _value is not None: + _value = self._convert_string_type(_value) + parts.append(_value) + for k, v in _params.items(): + k = self._convert_string_type(k) + if v is None: + parts.append(k.replace('_', '-')) + else: + v = self._convert_string_type(v) + parts.append(_formatparam(k.replace('_', '-'), v)) + self._headers.append((self._convert_string_type(_name), "; ".join(parts))) diff --git a/contrib/tools/python3/Lib/wsgiref/simple_server.py b/contrib/tools/python3/Lib/wsgiref/simple_server.py new file mode 100644 index 00000000000..93d01a863ad --- /dev/null +++ b/contrib/tools/python3/Lib/wsgiref/simple_server.py @@ -0,0 +1,165 @@ +"""BaseHTTPServer that implements the Python WSGI protocol (PEP 3333) + +This is both an example of how WSGI can be implemented, and a basis for running +simple web applications on a local machine, such as might be done when testing +or debugging an application. It has not been reviewed for security issues, +however, and we strongly recommend that you use a "real" web server for +production use. + +For example usage, see the 'if __name__=="__main__"' block at the end of the +module. See also the BaseHTTPServer module docs for other API information. +""" + +from http.server import BaseHTTPRequestHandler, HTTPServer +import sys +import urllib.parse +from wsgiref.handlers import SimpleHandler +from platform import python_implementation + +__version__ = "0.2" +__all__ = ['WSGIServer', 'WSGIRequestHandler', 'demo_app', 'make_server'] + + +server_version = "WSGIServer/" + __version__ +sys_version = python_implementation() + "/" + sys.version.split()[0] +software_version = server_version + ' ' + sys_version + + +class ServerHandler(SimpleHandler): + + server_software = software_version + + def close(self): + try: + self.request_handler.log_request( + self.status.split(' ',1)[0], self.bytes_sent + ) + finally: + SimpleHandler.close(self) + + + +class WSGIServer(HTTPServer): + + """BaseHTTPServer that implements the Python WSGI protocol""" + + application = None + + def server_bind(self): + """Override server_bind to store the server name.""" + HTTPServer.server_bind(self) + self.setup_environ() + + def setup_environ(self): + # Set up base environment + env = self.base_environ = {} + env['SERVER_NAME'] = self.server_name + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['SERVER_PORT'] = str(self.server_port) + env['REMOTE_HOST']='' + env['CONTENT_LENGTH']='' + env['SCRIPT_NAME'] = '' + + def get_app(self): + return self.application + + def set_app(self,application): + self.application = application + + + +class WSGIRequestHandler(BaseHTTPRequestHandler): + + server_version = "WSGIServer/" + __version__ + + def get_environ(self): + env = self.server.base_environ.copy() + env['SERVER_PROTOCOL'] = self.request_version + env['SERVER_SOFTWARE'] = self.server_version + env['REQUEST_METHOD'] = self.command + if '?' in self.path: + path,query = self.path.split('?',1) + else: + path,query = self.path,'' + + env['PATH_INFO'] = urllib.parse.unquote(path, 'iso-8859-1') + env['QUERY_STRING'] = query + + host = self.address_string() + if host != self.client_address[0]: + env['REMOTE_HOST'] = host + env['REMOTE_ADDR'] = self.client_address[0] + + if self.headers.get('content-type') is None: + env['CONTENT_TYPE'] = self.headers.get_content_type() + else: + env['CONTENT_TYPE'] = self.headers['content-type'] + + length = self.headers.get('content-length') + if length: + env['CONTENT_LENGTH'] = length + + for k, v in self.headers.items(): + k=k.replace('-','_').upper(); v=v.strip() + if k in env: + continue # skip content length, type,etc. + if 'HTTP_'+k in env: + env['HTTP_'+k] += ','+v # comma-separate multiple headers + else: + env['HTTP_'+k] = v + return env + + def get_stderr(self): + return sys.stderr + + def handle(self): + """Handle a single HTTP request""" + + self.raw_requestline = self.rfile.readline(65537) + if len(self.raw_requestline) > 65536: + self.requestline = '' + self.request_version = '' + self.command = '' + self.send_error(414) + return + + if not self.parse_request(): # An error code has been sent, just exit + return + + handler = ServerHandler( + self.rfile, self.wfile, self.get_stderr(), self.get_environ(), + multithread=False, + ) + handler.request_handler = self # backpointer for logging + handler.run(self.server.get_app()) + + + +def demo_app(environ,start_response): + from io import StringIO + stdout = StringIO() + print("Hello world!", file=stdout) + print(file=stdout) + h = sorted(environ.items()) + for k,v in h: + print(k,'=',repr(v), file=stdout) + start_response("200 OK", [('Content-Type','text/plain; charset=utf-8')]) + return [stdout.getvalue().encode("utf-8")] + + +def make_server( + host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler +): + """Create a new WSGI server listening on `host` and `port` for `app`""" + server = server_class((host, port), handler_class) + server.set_app(app) + return server + + +if __name__ == '__main__': + with make_server('', 8000, demo_app) as httpd: + sa = httpd.socket.getsockname() + print("Serving HTTP on", sa[0], "port", sa[1], "...") + import webbrowser + webbrowser.open('http://localhost:8000/xyz?abc') + httpd.handle_request() # serve one request, then exit diff --git a/contrib/tools/python3/Lib/wsgiref/types.py b/contrib/tools/python3/Lib/wsgiref/types.py new file mode 100644 index 00000000000..ef0aead5b28 --- /dev/null +++ b/contrib/tools/python3/Lib/wsgiref/types.py @@ -0,0 +1,54 @@ +"""WSGI-related types for static type checking""" + +from collections.abc import Callable, Iterable, Iterator +from types import TracebackType +from typing import Any, Protocol, TypeAlias + +__all__ = [ + "StartResponse", + "WSGIEnvironment", + "WSGIApplication", + "InputStream", + "ErrorStream", + "FileWrapper", +] + +_ExcInfo: TypeAlias = tuple[type[BaseException], BaseException, TracebackType] +_OptExcInfo: TypeAlias = _ExcInfo | tuple[None, None, None] + +class StartResponse(Protocol): + """start_response() callable as defined in PEP 3333""" + def __call__( + self, + status: str, + headers: list[tuple[str, str]], + exc_info: _OptExcInfo | None = ..., + /, + ) -> Callable[[bytes], object]: ... + +WSGIEnvironment: TypeAlias = dict[str, Any] +WSGIApplication: TypeAlias = Callable[[WSGIEnvironment, StartResponse], + Iterable[bytes]] + +class InputStream(Protocol): + """WSGI input stream as defined in PEP 3333""" + def read(self, size: int = ..., /) -> bytes: ... + def readline(self, size: int = ..., /) -> bytes: ... + def readlines(self, hint: int = ..., /) -> list[bytes]: ... + def __iter__(self) -> Iterator[bytes]: ... + +class ErrorStream(Protocol): + """WSGI error stream as defined in PEP 3333""" + def flush(self) -> object: ... + def write(self, s: str, /) -> object: ... + def writelines(self, seq: list[str], /) -> object: ... + +class _Readable(Protocol): + def read(self, size: int = ..., /) -> bytes: ... + # Optional: def close(self) -> object: ... + +class FileWrapper(Protocol): + """WSGI file wrapper as defined in PEP 3333""" + def __call__( + self, file: _Readable, block_size: int = ..., /, + ) -> Iterable[bytes]: ... diff --git a/contrib/tools/python3/Lib/wsgiref/util.py b/contrib/tools/python3/Lib/wsgiref/util.py new file mode 100644 index 00000000000..cbbe094cba1 --- /dev/null +++ b/contrib/tools/python3/Lib/wsgiref/util.py @@ -0,0 +1,159 @@ +"""Miscellaneous WSGI-related Utilities""" + +import posixpath + +__all__ = [ + 'FileWrapper', 'guess_scheme', 'application_uri', 'request_uri', + 'shift_path_info', 'setup_testing_defaults', +] + + +class FileWrapper: + """Wrapper to convert file-like objects to iterables""" + + def __init__(self, filelike, blksize=8192): + self.filelike = filelike + self.blksize = blksize + if hasattr(filelike,'close'): + self.close = filelike.close + + def __iter__(self): + return self + + def __next__(self): + data = self.filelike.read(self.blksize) + if data: + return data + raise StopIteration + +def guess_scheme(environ): + """Return a guess for whether 'wsgi.url_scheme' should be 'http' or 'https' + """ + if environ.get("HTTPS") in ('yes','on','1'): + return 'https' + else: + return 'http' + +def application_uri(environ): + """Return the application's base URI (no PATH_INFO or QUERY_STRING)""" + url = environ['wsgi.url_scheme']+'://' + from urllib.parse import quote + + if environ.get('HTTP_HOST'): + url += environ['HTTP_HOST'] + else: + url += environ['SERVER_NAME'] + + if environ['wsgi.url_scheme'] == 'https': + if environ['SERVER_PORT'] != '443': + url += ':' + environ['SERVER_PORT'] + else: + if environ['SERVER_PORT'] != '80': + url += ':' + environ['SERVER_PORT'] + + url += quote(environ.get('SCRIPT_NAME') or '/', encoding='latin1') + return url + +def request_uri(environ, include_query=True): + """Return the full request URI, optionally including the query string""" + url = application_uri(environ) + from urllib.parse import quote + path_info = quote(environ.get('PATH_INFO',''), safe='/;=,', encoding='latin1') + if not environ.get('SCRIPT_NAME'): + url += path_info[1:] + else: + url += path_info + if include_query and environ.get('QUERY_STRING'): + url += '?' + environ['QUERY_STRING'] + return url + +def shift_path_info(environ): + """Shift a name from PATH_INFO to SCRIPT_NAME, returning it + + If there are no remaining path segments in PATH_INFO, return None. + Note: 'environ' is modified in-place; use a copy if you need to keep + the original PATH_INFO or SCRIPT_NAME. + + Note: when PATH_INFO is just a '/', this returns '' and appends a trailing + '/' to SCRIPT_NAME, even though empty path segments are normally ignored, + and SCRIPT_NAME doesn't normally end in a '/'. This is intentional + behavior, to ensure that an application can tell the difference between + '/x' and '/x/' when traversing to objects. + """ + path_info = environ.get('PATH_INFO','') + if not path_info: + return None + + path_parts = path_info.split('/') + path_parts[1:-1] = [p for p in path_parts[1:-1] if p and p != '.'] + name = path_parts[1] + del path_parts[1] + + script_name = environ.get('SCRIPT_NAME','') + script_name = posixpath.normpath(script_name+'/'+name) + if script_name.endswith('/'): + script_name = script_name[:-1] + if not name and not script_name.endswith('/'): + script_name += '/' + + environ['SCRIPT_NAME'] = script_name + environ['PATH_INFO'] = '/'.join(path_parts) + + # Special case: '/.' on PATH_INFO doesn't get stripped, + # because we don't strip the last element of PATH_INFO + # if there's only one path part left. Instead of fixing this + # above, we fix it here so that PATH_INFO gets normalized to + # an empty string in the environ. + if name=='.': + name = None + return name + +def setup_testing_defaults(environ): + """Update 'environ' with trivial defaults for testing purposes + + This adds various parameters required for WSGI, including HTTP_HOST, + SERVER_NAME, SERVER_PORT, REQUEST_METHOD, SCRIPT_NAME, PATH_INFO, + and all of the wsgi.* variables. It only supplies default values, + and does not replace any existing settings for these variables. + + This routine is intended to make it easier for unit tests of WSGI + servers and applications to set up dummy environments. It should *not* + be used by actual WSGI servers or applications, since the data is fake! + """ + + environ.setdefault('SERVER_NAME','127.0.0.1') + environ.setdefault('SERVER_PROTOCOL','HTTP/1.0') + + environ.setdefault('HTTP_HOST',environ['SERVER_NAME']) + environ.setdefault('REQUEST_METHOD','GET') + + if 'SCRIPT_NAME' not in environ and 'PATH_INFO' not in environ: + environ.setdefault('SCRIPT_NAME','') + environ.setdefault('PATH_INFO','/') + + environ.setdefault('wsgi.version', (1,0)) + environ.setdefault('wsgi.run_once', 0) + environ.setdefault('wsgi.multithread', 0) + environ.setdefault('wsgi.multiprocess', 0) + + from io import StringIO, BytesIO + environ.setdefault('wsgi.input', BytesIO()) + environ.setdefault('wsgi.errors', StringIO()) + environ.setdefault('wsgi.url_scheme',guess_scheme(environ)) + + if environ['wsgi.url_scheme']=='http': + environ.setdefault('SERVER_PORT', '80') + elif environ['wsgi.url_scheme']=='https': + environ.setdefault('SERVER_PORT', '443') + + + +_hoppish = { + 'connection', 'keep-alive', 'proxy-authenticate', + 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', + 'upgrade' +}.__contains__ + +def is_hop_by_hop(header_name): + """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header""" + return _hoppish(header_name.lower()) diff --git a/contrib/tools/python3/Lib/wsgiref/validate.py b/contrib/tools/python3/Lib/wsgiref/validate.py new file mode 100644 index 00000000000..1a1853cd63a --- /dev/null +++ b/contrib/tools/python3/Lib/wsgiref/validate.py @@ -0,0 +1,438 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) +# Licensed under the MIT license: https://opensource.org/licenses/mit-license.php +# Also licenced under the Apache License, 2.0: https://opensource.org/licenses/apache2.0.php +# Licensed to PSF under a Contributor Agreement +""" +Middleware to check for obedience to the WSGI specification. + +Some of the things this checks: + +* Signature of the application and start_response (including that + keyword arguments are not used). + +* Environment checks: + + - Environment is a dictionary (and not a subclass). + + - That all the required keys are in the environment: REQUEST_METHOD, + SERVER_NAME, SERVER_PORT, wsgi.version, wsgi.input, wsgi.errors, + wsgi.multithread, wsgi.multiprocess, wsgi.run_once + + - That HTTP_CONTENT_TYPE and HTTP_CONTENT_LENGTH are not in the + environment (these headers should appear as CONTENT_LENGTH and + CONTENT_TYPE). + + - Warns if QUERY_STRING is missing, as the cgi module acts + unpredictably in that case. + + - That CGI-style variables (that don't contain a .) have + (non-unicode) string values + + - That wsgi.version is a tuple + + - That wsgi.url_scheme is 'http' or 'https' (@@: is this too + restrictive?) + + - Warns if the REQUEST_METHOD is not known (@@: probably too + restrictive). + + - That SCRIPT_NAME and PATH_INFO are empty or start with / + + - That at least one of SCRIPT_NAME or PATH_INFO are set. + + - That CONTENT_LENGTH is a positive integer. + + - That SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should + be '/'). + + - That wsgi.input has the methods read, readline, readlines, and + __iter__ + + - That wsgi.errors has the methods flush, write, writelines + +* The status is a string, contains a space, starts with an integer, + and that integer is in range (> 100). + +* That the headers is a list (not a subclass, not another kind of + sequence). + +* That the items of the headers are tuples of strings. + +* That there is no 'status' header (that is used in CGI, but not in + WSGI). + +* That the headers don't contain newlines or colons, end in _ or -, or + contain characters codes below 037. + +* That Content-Type is given if there is content (CGI often has a + default content type, but WSGI does not). + +* That no Content-Type is given when there is no content (@@: is this + too restrictive?) + +* That the exc_info argument to start_response is a tuple or None. + +* That all calls to the writer are with strings, and no other methods + on the writer are accessed. + +* That wsgi.input is used properly: + + - .read() is called with exactly one argument + + - That it returns a string + + - That readline, readlines, and __iter__ return strings + + - That .close() is not called + + - No other methods are provided + +* That wsgi.errors is used properly: + + - .write() and .writelines() is called with a string + + - That .close() is not called, and no other methods are provided. + +* The response iterator: + + - That it is not a string (it should be a list of a single string; a + string will work, but perform horribly). + + - That .__next__() returns a string + + - That the iterator is not iterated over until start_response has + been called (that can signal either a server or application + error). + + - That .close() is called (doesn't raise exception, only prints to + sys.stderr, because we only know it isn't called when the object + is garbage collected). +""" +__all__ = ['validator'] + + +import re +import sys +import warnings + +header_re = re.compile(r'^[a-zA-Z][a-zA-Z0-9\-_]*$') +bad_header_value_re = re.compile(r'[\000-\037]') + +class WSGIWarning(Warning): + """ + Raised in response to WSGI-spec-related warnings + """ + +def assert_(cond, *args): + if not cond: + raise AssertionError(*args) + +def check_string_type(value, title): + if type (value) is str: + return value + raise AssertionError( + "{0} must be of type str (got {1})".format(title, repr(value))) + +def validator(application): + + """ + When applied between a WSGI server and a WSGI application, this + middleware will check for WSGI compliance on a number of levels. + This middleware does not modify the request or response in any + way, but will raise an AssertionError if anything seems off + (except for a failure to close the application iterator, which + will be printed to stderr -- there's no way to raise an exception + at that point). + """ + + def lint_app(*args, **kw): + assert_(len(args) == 2, "Two arguments required") + assert_(not kw, "No keyword arguments allowed") + environ, start_response = args + + check_environ(environ) + + # We use this to check if the application returns without + # calling start_response: + start_response_started = [] + + def start_response_wrapper(*args, **kw): + assert_(len(args) == 2 or len(args) == 3, ( + "Invalid number of arguments: %s" % (args,))) + assert_(not kw, "No keyword arguments allowed") + status = args[0] + headers = args[1] + if len(args) == 3: + exc_info = args[2] + else: + exc_info = None + + check_status(status) + check_headers(headers) + check_content_type(status, headers) + check_exc_info(exc_info) + + start_response_started.append(None) + return WriteWrapper(start_response(*args)) + + environ['wsgi.input'] = InputWrapper(environ['wsgi.input']) + environ['wsgi.errors'] = ErrorWrapper(environ['wsgi.errors']) + + iterator = application(environ, start_response_wrapper) + assert_(iterator is not None and iterator != False, + "The application must return an iterator, if only an empty list") + + check_iterator(iterator) + + return IteratorWrapper(iterator, start_response_started) + + return lint_app + +class InputWrapper: + + def __init__(self, wsgi_input): + self.input = wsgi_input + + def read(self, *args): + assert_(len(args) == 1) + v = self.input.read(*args) + assert_(type(v) is bytes) + return v + + def readline(self, *args): + assert_(len(args) <= 1) + v = self.input.readline(*args) + assert_(type(v) is bytes) + return v + + def readlines(self, *args): + assert_(len(args) <= 1) + lines = self.input.readlines(*args) + assert_(type(lines) is list) + for line in lines: + assert_(type(line) is bytes) + return lines + + def __iter__(self): + while line := self.readline(): + yield line + + def close(self): + assert_(0, "input.close() must not be called") + +class ErrorWrapper: + + def __init__(self, wsgi_errors): + self.errors = wsgi_errors + + def write(self, s): + assert_(type(s) is str) + self.errors.write(s) + + def flush(self): + self.errors.flush() + + def writelines(self, seq): + for line in seq: + self.write(line) + + def close(self): + assert_(0, "errors.close() must not be called") + +class WriteWrapper: + + def __init__(self, wsgi_writer): + self.writer = wsgi_writer + + def __call__(self, s): + assert_(type(s) is bytes) + self.writer(s) + +class PartialIteratorWrapper: + + def __init__(self, wsgi_iterator): + self.iterator = wsgi_iterator + + def __iter__(self): + # We want to make sure __iter__ is called + return IteratorWrapper(self.iterator, None) + +class IteratorWrapper: + + def __init__(self, wsgi_iterator, check_start_response): + self.original_iterator = wsgi_iterator + self.iterator = iter(wsgi_iterator) + self.closed = False + self.check_start_response = check_start_response + + def __iter__(self): + return self + + def __next__(self): + assert_(not self.closed, + "Iterator read after closed") + v = next(self.iterator) + if type(v) is not bytes: + assert_(False, "Iterator yielded non-bytestring (%r)" % (v,)) + if self.check_start_response is not None: + assert_(self.check_start_response, + "The application returns and we started iterating over its body, but start_response has not yet been called") + self.check_start_response = None + return v + + def close(self): + self.closed = True + if hasattr(self.original_iterator, 'close'): + self.original_iterator.close() + + def __del__(self): + if not self.closed: + sys.stderr.write( + "Iterator garbage collected without being closed") + assert_(self.closed, + "Iterator garbage collected without being closed") + +def check_environ(environ): + assert_(type(environ) is dict, + "Environment is not of the right type: %r (environment: %r)" + % (type(environ), environ)) + + for key in ['REQUEST_METHOD', 'SERVER_NAME', 'SERVER_PORT', + 'wsgi.version', 'wsgi.input', 'wsgi.errors', + 'wsgi.multithread', 'wsgi.multiprocess', + 'wsgi.run_once']: + assert_(key in environ, + "Environment missing required key: %r" % (key,)) + + for key in ['HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH']: + assert_(key not in environ, + "Environment should not have the key: %s " + "(use %s instead)" % (key, key[5:])) + + if 'QUERY_STRING' not in environ: + warnings.warn( + 'QUERY_STRING is not in the WSGI environment; the cgi ' + 'module will use sys.argv when this variable is missing, ' + 'so application errors are more likely', + WSGIWarning) + + for key in environ.keys(): + if '.' in key: + # Extension, we don't care about its type + continue + assert_(type(environ[key]) is str, + "Environmental variable %s is not a string: %r (value: %r)" + % (key, type(environ[key]), environ[key])) + + assert_(type(environ['wsgi.version']) is tuple, + "wsgi.version should be a tuple (%r)" % (environ['wsgi.version'],)) + assert_(environ['wsgi.url_scheme'] in ('http', 'https'), + "wsgi.url_scheme unknown: %r" % environ['wsgi.url_scheme']) + + check_input(environ['wsgi.input']) + check_errors(environ['wsgi.errors']) + + # @@: these need filling out: + if environ['REQUEST_METHOD'] not in ( + 'GET', 'HEAD', 'POST', 'OPTIONS', 'PATCH', 'PUT', 'DELETE', 'TRACE'): + warnings.warn( + "Unknown REQUEST_METHOD: %r" % environ['REQUEST_METHOD'], + WSGIWarning) + + assert_(not environ.get('SCRIPT_NAME') + or environ['SCRIPT_NAME'].startswith('/'), + "SCRIPT_NAME doesn't start with /: %r" % environ['SCRIPT_NAME']) + assert_(not environ.get('PATH_INFO') + or environ['PATH_INFO'].startswith('/'), + "PATH_INFO doesn't start with /: %r" % environ['PATH_INFO']) + if environ.get('CONTENT_LENGTH'): + assert_(int(environ['CONTENT_LENGTH']) >= 0, + "Invalid CONTENT_LENGTH: %r" % environ['CONTENT_LENGTH']) + + if not environ.get('SCRIPT_NAME'): + assert_('PATH_INFO' in environ, + "One of SCRIPT_NAME or PATH_INFO are required (PATH_INFO " + "should at least be '/' if SCRIPT_NAME is empty)") + assert_(environ.get('SCRIPT_NAME') != '/', + "SCRIPT_NAME cannot be '/'; it should instead be '', and " + "PATH_INFO should be '/'") + +def check_input(wsgi_input): + for attr in ['read', 'readline', 'readlines', '__iter__']: + assert_(hasattr(wsgi_input, attr), + "wsgi.input (%r) doesn't have the attribute %s" + % (wsgi_input, attr)) + +def check_errors(wsgi_errors): + for attr in ['flush', 'write', 'writelines']: + assert_(hasattr(wsgi_errors, attr), + "wsgi.errors (%r) doesn't have the attribute %s" + % (wsgi_errors, attr)) + +def check_status(status): + status = check_string_type(status, "Status") + # Implicitly check that we can turn it into an integer: + status_code = status.split(None, 1)[0] + assert_(len(status_code) == 3, + "Status codes must be three characters: %r" % status_code) + status_int = int(status_code) + assert_(status_int >= 100, "Status code is invalid: %r" % status_int) + if len(status) < 4 or status[3] != ' ': + warnings.warn( + "The status string (%r) should be a three-digit integer " + "followed by a single space and a status explanation" + % status, WSGIWarning) + +def check_headers(headers): + assert_(type(headers) is list, + "Headers (%r) must be of type list: %r" + % (headers, type(headers))) + for item in headers: + assert_(type(item) is tuple, + "Individual headers (%r) must be of type tuple: %r" + % (item, type(item))) + assert_(len(item) == 2) + name, value = item + name = check_string_type(name, "Header name") + value = check_string_type(value, "Header value") + assert_(name.lower() != 'status', + "The Status header cannot be used; it conflicts with CGI " + "script, and HTTP status is not given through headers " + "(value: %r)." % value) + assert_('\n' not in name and ':' not in name, + "Header names may not contain ':' or '\\n': %r" % name) + assert_(header_re.search(name), "Bad header name: %r" % name) + assert_(not name.endswith('-') and not name.endswith('_'), + "Names may not end in '-' or '_': %r" % name) + if bad_header_value_re.search(value): + assert_(0, "Bad header value: %r (bad char: %r)" + % (value, bad_header_value_re.search(value).group(0))) + +def check_content_type(status, headers): + status = check_string_type(status, "Status") + code = int(status.split(None, 1)[0]) + # @@: need one more person to verify this interpretation of RFC 2616 + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + NO_MESSAGE_BODY = (204, 304) + for name, value in headers: + name = check_string_type(name, "Header name") + if name.lower() == 'content-type': + if code not in NO_MESSAGE_BODY: + return + assert_(0, ("Content-Type header found in a %s response, " + "which must not return content.") % code) + if code not in NO_MESSAGE_BODY: + assert_(0, "No Content-Type header found in headers (%s)" % headers) + +def check_exc_info(exc_info): + assert_(exc_info is None or type(exc_info) is tuple, + "exc_info (%r) is not a tuple: %r" % (exc_info, type(exc_info))) + # More exc_info checks? + +def check_iterator(iterator): + # Technically a bytestring is legal, which is why it's a really bad + # idea, because it may cause the response to be returned + # character-by-character + assert_(not isinstance(iterator, (str, bytes)), + "You should not return a string as your application iterator, " + "instead return a single-item list containing a bytestring.") |