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