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