xref: /aosp_15_r20/prebuilts/build-tools/common/py3-stdlib/wsgiref/util.py (revision cda5da8d549138a6648c5ee6d7a49cf8f4a657be)
1"""Miscellaneous WSGI-related Utilities"""
2
3import posixpath
4
5__all__ = [
6    'FileWrapper', 'guess_scheme', 'application_uri', 'request_uri',
7    'shift_path_info', 'setup_testing_defaults',
8]
9
10
11class FileWrapper:
12    """Wrapper to convert file-like objects to iterables"""
13
14    def __init__(self, filelike, blksize=8192):
15        self.filelike = filelike
16        self.blksize = blksize
17        if hasattr(filelike,'close'):
18            self.close = filelike.close
19
20    def __iter__(self):
21        return self
22
23    def __next__(self):
24        data = self.filelike.read(self.blksize)
25        if data:
26            return data
27        raise StopIteration
28
29def guess_scheme(environ):
30    """Return a guess for whether 'wsgi.url_scheme' should be 'http' or 'https'
31    """
32    if environ.get("HTTPS") in ('yes','on','1'):
33        return 'https'
34    else:
35        return 'http'
36
37def application_uri(environ):
38    """Return the application's base URI (no PATH_INFO or QUERY_STRING)"""
39    url = environ['wsgi.url_scheme']+'://'
40    from urllib.parse import quote
41
42    if environ.get('HTTP_HOST'):
43        url += environ['HTTP_HOST']
44    else:
45        url += environ['SERVER_NAME']
46
47        if environ['wsgi.url_scheme'] == 'https':
48            if environ['SERVER_PORT'] != '443':
49                url += ':' + environ['SERVER_PORT']
50        else:
51            if environ['SERVER_PORT'] != '80':
52                url += ':' + environ['SERVER_PORT']
53
54    url += quote(environ.get('SCRIPT_NAME') or '/', encoding='latin1')
55    return url
56
57def request_uri(environ, include_query=True):
58    """Return the full request URI, optionally including the query string"""
59    url = application_uri(environ)
60    from urllib.parse import quote
61    path_info = quote(environ.get('PATH_INFO',''), safe='/;=,', encoding='latin1')
62    if not environ.get('SCRIPT_NAME'):
63        url += path_info[1:]
64    else:
65        url += path_info
66    if include_query and environ.get('QUERY_STRING'):
67        url += '?' + environ['QUERY_STRING']
68    return url
69
70def shift_path_info(environ):
71    """Shift a name from PATH_INFO to SCRIPT_NAME, returning it
72
73    If there are no remaining path segments in PATH_INFO, return None.
74    Note: 'environ' is modified in-place; use a copy if you need to keep
75    the original PATH_INFO or SCRIPT_NAME.
76
77    Note: when PATH_INFO is just a '/', this returns '' and appends a trailing
78    '/' to SCRIPT_NAME, even though empty path segments are normally ignored,
79    and SCRIPT_NAME doesn't normally end in a '/'.  This is intentional
80    behavior, to ensure that an application can tell the difference between
81    '/x' and '/x/' when traversing to objects.
82    """
83    path_info = environ.get('PATH_INFO','')
84    if not path_info:
85        return None
86
87    path_parts = path_info.split('/')
88    path_parts[1:-1] = [p for p in path_parts[1:-1] if p and p != '.']
89    name = path_parts[1]
90    del path_parts[1]
91
92    script_name = environ.get('SCRIPT_NAME','')
93    script_name = posixpath.normpath(script_name+'/'+name)
94    if script_name.endswith('/'):
95        script_name = script_name[:-1]
96    if not name and not script_name.endswith('/'):
97        script_name += '/'
98
99    environ['SCRIPT_NAME'] = script_name
100    environ['PATH_INFO']   = '/'.join(path_parts)
101
102    # Special case: '/.' on PATH_INFO doesn't get stripped,
103    # because we don't strip the last element of PATH_INFO
104    # if there's only one path part left.  Instead of fixing this
105    # above, we fix it here so that PATH_INFO gets normalized to
106    # an empty string in the environ.
107    if name=='.':
108        name = None
109    return name
110
111def setup_testing_defaults(environ):
112    """Update 'environ' with trivial defaults for testing purposes
113
114    This adds various parameters required for WSGI, including HTTP_HOST,
115    SERVER_NAME, SERVER_PORT, REQUEST_METHOD, SCRIPT_NAME, PATH_INFO,
116    and all of the wsgi.* variables.  It only supplies default values,
117    and does not replace any existing settings for these variables.
118
119    This routine is intended to make it easier for unit tests of WSGI
120    servers and applications to set up dummy environments.  It should *not*
121    be used by actual WSGI servers or applications, since the data is fake!
122    """
123
124    environ.setdefault('SERVER_NAME','127.0.0.1')
125    environ.setdefault('SERVER_PROTOCOL','HTTP/1.0')
126
127    environ.setdefault('HTTP_HOST',environ['SERVER_NAME'])
128    environ.setdefault('REQUEST_METHOD','GET')
129
130    if 'SCRIPT_NAME' not in environ and 'PATH_INFO' not in environ:
131        environ.setdefault('SCRIPT_NAME','')
132        environ.setdefault('PATH_INFO','/')
133
134    environ.setdefault('wsgi.version', (1,0))
135    environ.setdefault('wsgi.run_once', 0)
136    environ.setdefault('wsgi.multithread', 0)
137    environ.setdefault('wsgi.multiprocess', 0)
138
139    from io import StringIO, BytesIO
140    environ.setdefault('wsgi.input', BytesIO())
141    environ.setdefault('wsgi.errors', StringIO())
142    environ.setdefault('wsgi.url_scheme',guess_scheme(environ))
143
144    if environ['wsgi.url_scheme']=='http':
145        environ.setdefault('SERVER_PORT', '80')
146    elif environ['wsgi.url_scheme']=='https':
147        environ.setdefault('SERVER_PORT', '443')
148
149
150
151_hoppish = {
152    'connection', 'keep-alive', 'proxy-authenticate',
153    'proxy-authorization', 'te', 'trailers', 'transfer-encoding',
154    'upgrade'
155}.__contains__
156
157def is_hop_by_hop(header_name):
158    """Return true if 'header_name' is an HTTP/1.1 "Hop-by-Hop" header"""
159    return _hoppish(header_name.lower())
160