request.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
  2. # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
  3. # (c) 2005 Ian Bicking and contributors
  4. # This module is part of the Python Paste Project and is released under
  5. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  6. """
  7. This module provides helper routines with work directly on a WSGI
  8. environment to solve common requirements.
  9. * get_cookies(environ)
  10. * parse_querystring(environ)
  11. * parse_formvars(environ, include_get_vars=True)
  12. * construct_url(environ, with_query_string=True, with_path_info=True,
  13. script_name=None, path_info=None, querystring=None)
  14. * path_info_split(path_info)
  15. * path_info_pop(environ)
  16. * resolve_relative_url(url, environ)
  17. """
  18. import cgi
  19. from six.moves.urllib import parse as urlparse
  20. from six.moves.urllib.parse import quote
  21. try:
  22. # Python 3
  23. from http.cookies import SimpleCookie, CookieError
  24. except ImportError:
  25. # Python 2
  26. from Cookie import SimpleCookie, CookieError
  27. try:
  28. from UserDict import DictMixin
  29. except ImportError:
  30. from collections import MutableMapping as DictMixin
  31. import six
  32. from paste.util.multidict import MultiDict
  33. __all__ = ['get_cookies', 'get_cookie_dict', 'parse_querystring',
  34. 'parse_formvars', 'construct_url', 'path_info_split',
  35. 'path_info_pop', 'resolve_relative_url', 'EnvironHeaders']
  36. def get_cookies(environ):
  37. """
  38. Gets a cookie object (which is a dictionary-like object) from the
  39. request environment; caches this value in case get_cookies is
  40. called again for the same request.
  41. """
  42. header = environ.get('HTTP_COOKIE', '')
  43. if 'paste.cookies' in environ:
  44. cookies, check_header = environ['paste.cookies']
  45. if check_header == header:
  46. return cookies
  47. cookies = SimpleCookie()
  48. try:
  49. cookies.load(header)
  50. except CookieError:
  51. pass
  52. environ['paste.cookies'] = (cookies, header)
  53. return cookies
  54. def get_cookie_dict(environ):
  55. """Return a *plain* dictionary of cookies as found in the request.
  56. Unlike ``get_cookies`` this returns a dictionary, not a
  57. ``SimpleCookie`` object. For incoming cookies a dictionary fully
  58. represents the information. Like ``get_cookies`` this caches and
  59. checks the cache.
  60. """
  61. header = environ.get('HTTP_COOKIE')
  62. if not header:
  63. return {}
  64. if 'paste.cookies.dict' in environ:
  65. cookies, check_header = environ['paste.cookies.dict']
  66. if check_header == header:
  67. return cookies
  68. cookies = SimpleCookie()
  69. try:
  70. cookies.load(header)
  71. except CookieError:
  72. pass
  73. result = {}
  74. for name in cookies:
  75. result[name] = cookies[name].value
  76. environ['paste.cookies.dict'] = (result, header)
  77. return result
  78. def parse_querystring(environ):
  79. """
  80. Parses a query string into a list like ``[(name, value)]``.
  81. Caches this value in case parse_querystring is called again
  82. for the same request.
  83. You can pass the result to ``dict()``, but be aware that keys that
  84. appear multiple times will be lost (only the last value will be
  85. preserved).
  86. """
  87. source = environ.get('QUERY_STRING', '')
  88. if not source:
  89. return []
  90. if 'paste.parsed_querystring' in environ:
  91. parsed, check_source = environ['paste.parsed_querystring']
  92. if check_source == source:
  93. return parsed
  94. parsed = cgi.parse_qsl(source, keep_blank_values=True,
  95. strict_parsing=False)
  96. environ['paste.parsed_querystring'] = (parsed, source)
  97. return parsed
  98. def parse_dict_querystring(environ):
  99. """Parses a query string like parse_querystring, but returns a MultiDict
  100. Caches this value in case parse_dict_querystring is called again
  101. for the same request.
  102. Example::
  103. >>> environ = {'QUERY_STRING': 'day=Monday&user=fred&user=jane'}
  104. >>> parsed = parse_dict_querystring(environ)
  105. >>> parsed['day']
  106. 'Monday'
  107. >>> parsed['user']
  108. 'fred'
  109. >>> parsed.getall('user')
  110. ['fred', 'jane']
  111. """
  112. source = environ.get('QUERY_STRING', '')
  113. if not source:
  114. return MultiDict()
  115. if 'paste.parsed_dict_querystring' in environ:
  116. parsed, check_source = environ['paste.parsed_dict_querystring']
  117. if check_source == source:
  118. return parsed
  119. parsed = cgi.parse_qsl(source, keep_blank_values=True,
  120. strict_parsing=False)
  121. multi = MultiDict(parsed)
  122. environ['paste.parsed_dict_querystring'] = (multi, source)
  123. return multi
  124. def parse_formvars(environ, include_get_vars=True):
  125. """Parses the request, returning a MultiDict of form variables.
  126. If ``include_get_vars`` is true then GET (query string) variables
  127. will also be folded into the MultiDict.
  128. All values should be strings, except for file uploads which are
  129. left as ``FieldStorage`` instances.
  130. If the request was not a normal form request (e.g., a POST with an
  131. XML body) then ``environ['wsgi.input']`` won't be read.
  132. """
  133. source = environ['wsgi.input']
  134. if 'paste.parsed_formvars' in environ:
  135. parsed, check_source = environ['paste.parsed_formvars']
  136. if check_source == source:
  137. if include_get_vars:
  138. parsed.update(parse_querystring(environ))
  139. return parsed
  140. # @@: Shouldn't bother FieldStorage parsing during GET/HEAD and
  141. # fake_out_cgi requests
  142. type = environ.get('CONTENT_TYPE', '').lower()
  143. if ';' in type:
  144. type = type.split(';', 1)[0]
  145. fake_out_cgi = type not in ('', 'application/x-www-form-urlencoded',
  146. 'multipart/form-data')
  147. # FieldStorage assumes a default CONTENT_LENGTH of -1, but a
  148. # default of 0 is better:
  149. if not environ.get('CONTENT_LENGTH'):
  150. environ['CONTENT_LENGTH'] = '0'
  151. # Prevent FieldStorage from parsing QUERY_STRING during GET/HEAD
  152. # requests
  153. old_query_string = environ.get('QUERY_STRING','')
  154. environ['QUERY_STRING'] = ''
  155. if fake_out_cgi:
  156. input = six.BytesIO(b'')
  157. old_content_type = environ.get('CONTENT_TYPE')
  158. old_content_length = environ.get('CONTENT_LENGTH')
  159. environ['CONTENT_LENGTH'] = '0'
  160. environ['CONTENT_TYPE'] = ''
  161. else:
  162. input = environ['wsgi.input']
  163. fs = cgi.FieldStorage(fp=input,
  164. environ=environ,
  165. keep_blank_values=1)
  166. environ['QUERY_STRING'] = old_query_string
  167. if fake_out_cgi:
  168. environ['CONTENT_TYPE'] = old_content_type
  169. environ['CONTENT_LENGTH'] = old_content_length
  170. formvars = MultiDict()
  171. if isinstance(fs.value, list):
  172. for name in fs.keys():
  173. values = fs[name]
  174. if not isinstance(values, list):
  175. values = [values]
  176. for value in values:
  177. if not value.filename:
  178. value = value.value
  179. formvars.add(name, value)
  180. environ['paste.parsed_formvars'] = (formvars, source)
  181. if include_get_vars:
  182. formvars.update(parse_querystring(environ))
  183. return formvars
  184. def construct_url(environ, with_query_string=True, with_path_info=True,
  185. script_name=None, path_info=None, querystring=None):
  186. """Reconstructs the URL from the WSGI environment.
  187. You may override SCRIPT_NAME, PATH_INFO, and QUERYSTRING with
  188. the keyword arguments.
  189. """
  190. url = environ['wsgi.url_scheme']+'://'
  191. if environ.get('HTTP_HOST'):
  192. host = environ['HTTP_HOST']
  193. port = None
  194. if ':' in host:
  195. host, port = host.split(':', 1)
  196. if environ['wsgi.url_scheme'] == 'https':
  197. if port == '443':
  198. port = None
  199. elif environ['wsgi.url_scheme'] == 'http':
  200. if port == '80':
  201. port = None
  202. url += host
  203. if port:
  204. url += ':%s' % port
  205. else:
  206. url += environ['SERVER_NAME']
  207. if environ['wsgi.url_scheme'] == 'https':
  208. if environ['SERVER_PORT'] != '443':
  209. url += ':' + environ['SERVER_PORT']
  210. else:
  211. if environ['SERVER_PORT'] != '80':
  212. url += ':' + environ['SERVER_PORT']
  213. if script_name is None:
  214. url += quote(environ.get('SCRIPT_NAME',''))
  215. else:
  216. url += quote(script_name)
  217. if with_path_info:
  218. if path_info is None:
  219. url += quote(environ.get('PATH_INFO',''))
  220. else:
  221. url += quote(path_info)
  222. if with_query_string:
  223. if querystring is None:
  224. if environ.get('QUERY_STRING'):
  225. url += '?' + environ['QUERY_STRING']
  226. elif querystring:
  227. url += '?' + querystring
  228. return url
  229. def resolve_relative_url(url, environ):
  230. """
  231. Resolve the given relative URL as being relative to the
  232. location represented by the environment. This can be used
  233. for redirecting to a relative path. Note: if url is already
  234. absolute, this function will (intentionally) have no effect
  235. on it.
  236. """
  237. cur_url = construct_url(environ, with_query_string=False)
  238. return urlparse.urljoin(cur_url, url)
  239. def path_info_split(path_info):
  240. """
  241. Splits off the first segment of the path. Returns (first_part,
  242. rest_of_path). first_part can be None (if PATH_INFO is empty), ''
  243. (if PATH_INFO is '/'), or a name without any /'s. rest_of_path
  244. can be '' or a string starting with /.
  245. """
  246. if not path_info:
  247. return None, ''
  248. assert path_info.startswith('/'), (
  249. "PATH_INFO should start with /: %r" % path_info)
  250. path_info = path_info.lstrip('/')
  251. if '/' in path_info:
  252. first, rest = path_info.split('/', 1)
  253. return first, '/' + rest
  254. else:
  255. return path_info, ''
  256. def path_info_pop(environ):
  257. """
  258. 'Pops' off the next segment of PATH_INFO, pushing it onto
  259. SCRIPT_NAME, and returning that segment.
  260. For instance::
  261. >>> def call_it(script_name, path_info):
  262. ... env = {'SCRIPT_NAME': script_name, 'PATH_INFO': path_info}
  263. ... result = path_info_pop(env)
  264. ... print('SCRIPT_NAME=%r; PATH_INFO=%r; returns=%r' % (
  265. ... env['SCRIPT_NAME'], env['PATH_INFO'], result))
  266. >>> call_it('/foo', '/bar')
  267. SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns='bar'
  268. >>> call_it('/foo/bar', '')
  269. SCRIPT_NAME='/foo/bar'; PATH_INFO=''; returns=None
  270. >>> call_it('/foo/bar', '/')
  271. SCRIPT_NAME='/foo/bar/'; PATH_INFO=''; returns=''
  272. >>> call_it('', '/1/2/3')
  273. SCRIPT_NAME='/1'; PATH_INFO='/2/3'; returns='1'
  274. >>> call_it('', '//1/2')
  275. SCRIPT_NAME='//1'; PATH_INFO='/2'; returns='1'
  276. """
  277. path = environ.get('PATH_INFO', '')
  278. if not path:
  279. return None
  280. while path.startswith('/'):
  281. environ['SCRIPT_NAME'] += '/'
  282. path = path[1:]
  283. if '/' not in path:
  284. environ['SCRIPT_NAME'] += path
  285. environ['PATH_INFO'] = ''
  286. return path
  287. else:
  288. segment, path = path.split('/', 1)
  289. environ['PATH_INFO'] = '/' + path
  290. environ['SCRIPT_NAME'] += segment
  291. return segment
  292. _parse_headers_special = {
  293. # This is a Zope convention, but we'll allow it here:
  294. 'HTTP_CGI_AUTHORIZATION': 'Authorization',
  295. 'CONTENT_LENGTH': 'Content-Length',
  296. 'CONTENT_TYPE': 'Content-Type',
  297. }
  298. def parse_headers(environ):
  299. """
  300. Parse the headers in the environment (like ``HTTP_HOST``) and
  301. yield a sequence of those (header_name, value) tuples.
  302. """
  303. # @@: Maybe should parse out comma-separated headers?
  304. for cgi_var, value in environ.iteritems():
  305. if cgi_var in _parse_headers_special:
  306. yield _parse_headers_special[cgi_var], value
  307. elif cgi_var.startswith('HTTP_'):
  308. yield cgi_var[5:].title().replace('_', '-'), value
  309. class EnvironHeaders(DictMixin):
  310. """An object that represents the headers as present in a
  311. WSGI environment.
  312. This object is a wrapper (with no internal state) for a WSGI
  313. request object, representing the CGI-style HTTP_* keys as a
  314. dictionary. Because a CGI environment can only hold one value for
  315. each key, this dictionary is single-valued (unlike outgoing
  316. headers).
  317. """
  318. def __init__(self, environ):
  319. self.environ = environ
  320. def _trans_name(self, name):
  321. key = 'HTTP_'+name.replace('-', '_').upper()
  322. if key == 'HTTP_CONTENT_LENGTH':
  323. key = 'CONTENT_LENGTH'
  324. elif key == 'HTTP_CONTENT_TYPE':
  325. key = 'CONTENT_TYPE'
  326. return key
  327. def _trans_key(self, key):
  328. if key == 'CONTENT_TYPE':
  329. return 'Content-Type'
  330. elif key == 'CONTENT_LENGTH':
  331. return 'Content-Length'
  332. elif key.startswith('HTTP_'):
  333. return key[5:].replace('_', '-').title()
  334. else:
  335. return None
  336. def __len__(self):
  337. return len(self.environ)
  338. def __getitem__(self, item):
  339. return self.environ[self._trans_name(item)]
  340. def __setitem__(self, item, value):
  341. # @@: Should this dictionary be writable at all?
  342. self.environ[self._trans_name(item)] = value
  343. def __delitem__(self, item):
  344. del self.environ[self._trans_name(item)]
  345. def __iter__(self):
  346. for key in self.environ:
  347. name = self._trans_key(key)
  348. if name is not None:
  349. yield name
  350. def keys(self):
  351. return list(iter(self))
  352. def __contains__(self, item):
  353. return self._trans_name(item) in self.environ
  354. def _cgi_FieldStorage__repr__patch(self):
  355. """ monkey patch for FieldStorage.__repr__
  356. Unbelievely, the default __repr__ on FieldStorage reads
  357. the entire file content instead of being sane about it.
  358. This is a simple replacement that doesn't do that
  359. """
  360. if self.file:
  361. return "FieldStorage(%r, %r)" % (
  362. self.name, self.filename)
  363. return "FieldStorage(%r, %r, %r)" % (
  364. self.name, self.filename, self.value)
  365. cgi.FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch
  366. if __name__ == '__main__':
  367. import doctest
  368. doctest.testmod()