_networking.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. from __future__ import annotations
  2. import string
  3. import sys
  4. from typing import Dict
  5. from urllib.error import HTTPError
  6. from urllib.parse import quote as urlquote
  7. from urllib.parse import urljoin, urlsplit
  8. from urllib.request import HTTPRedirectHandler, Request, urlopen
  9. from urllib.response import addinfourl
  10. def _make_redirect_request(request: Request, http_error: HTTPError) -> Request:
  11. """Create a new request object for a redirected request.
  12. The logic is based on [HTTPRedirectHandler](https://github.com/python/cpython/blob/b58bc8c2a9a316891a5ea1a0487aebfc86c2793a/Lib/urllib/request.py#L641-L751) from urllib.request.
  13. Args:
  14. request: The original request that resulted in the redirect.
  15. http_error: The response to the original request that indicates a
  16. redirect should occur and contains the new location.
  17. Returns:
  18. A new request object to the location indicated by the response.
  19. Raises:
  20. HTTPError: the supplied `http_error` if the redirect request
  21. cannot be created.
  22. ValueError: If the response code is None.
  23. ValueError: If the response does not contain a `Location` header
  24. or the `Location` header is not a string.
  25. HTTPError: If the scheme of the new location is not `http`,
  26. `https`, or `ftp`.
  27. HTTPError: If there are too many redirects or a redirect loop.
  28. """
  29. new_url = http_error.headers.get("Location")
  30. if new_url is None:
  31. raise http_error
  32. if not isinstance(new_url, str):
  33. raise ValueError(f"Location header {new_url!r} is not a string")
  34. new_url_parts = urlsplit(new_url)
  35. # For security reasons don't allow redirection to anything other than http,
  36. # https or ftp.
  37. if new_url_parts.scheme not in ("http", "https", "ftp", ""):
  38. raise HTTPError(
  39. new_url,
  40. http_error.code,
  41. f"{http_error.reason} - Redirection to url {new_url!r} is not allowed",
  42. http_error.headers,
  43. http_error.fp,
  44. )
  45. # http.client.parse_headers() decodes as ISO-8859-1. Recover the original
  46. # bytes and percent-encode non-ASCII bytes, and any special characters such
  47. # as the space.
  48. new_url = urlquote(new_url, encoding="iso-8859-1", safe=string.punctuation)
  49. new_url = urljoin(request.full_url, new_url)
  50. # XXX Probably want to forget about the state of the current
  51. # request, although that might interact poorly with other
  52. # handlers that also use handler-specific request attributes
  53. content_headers = ("content-length", "content-type")
  54. newheaders = {
  55. k: v for k, v in request.headers.items() if k.lower() not in content_headers
  56. }
  57. new_request = Request(
  58. new_url,
  59. headers=newheaders,
  60. origin_req_host=request.origin_req_host,
  61. unverifiable=True,
  62. )
  63. visited: Dict[str, int]
  64. if hasattr(request, "redirect_dict"):
  65. visited = request.redirect_dict
  66. if (
  67. visited.get(new_url, 0) >= HTTPRedirectHandler.max_repeats
  68. or len(visited) >= HTTPRedirectHandler.max_redirections
  69. ):
  70. raise HTTPError(
  71. request.full_url,
  72. http_error.code,
  73. HTTPRedirectHandler.inf_msg + http_error.reason,
  74. http_error.headers,
  75. http_error.fp,
  76. )
  77. else:
  78. visited = {}
  79. setattr(request, "redirect_dict", visited)
  80. setattr(new_request, "redirect_dict", visited)
  81. visited[new_url] = visited.get(new_url, 0) + 1
  82. return new_request
  83. def _urlopen(request: Request) -> addinfourl:
  84. """This is a shim for `urlopen` that handles HTTP redirects with status code
  85. 308 (Permanent Redirect).
  86. This function should be removed once all supported versions of Python
  87. handles the 308 HTTP status code.
  88. Args:
  89. request: The request to open.
  90. Returns:
  91. The response to the request.
  92. """
  93. try:
  94. return urlopen(request)
  95. except HTTPError as error:
  96. if error.code == 308 and sys.version_info < (3, 11):
  97. # HTTP response code 308 (Permanent Redirect) is not supported by python
  98. # versions older than 3.11. See <https://bugs.python.org/issue40321> and
  99. # <https://github.com/python/cpython/issues/84501> for more details.
  100. # This custom error handling should be removed once all supported
  101. # versions of Python handles 308.
  102. new_request = _make_redirect_request(request, error)
  103. return _urlopen(new_request)
  104. else:
  105. raise