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