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