contrib.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. # -*- coding: utf-8 -*-
  2. """
  3. hyper/contrib
  4. ~~~~~~~~~~~~~
  5. Contains a few utilities for use with other HTTP libraries.
  6. """
  7. try:
  8. from requests.adapters import HTTPAdapter
  9. from requests.models import Response
  10. from requests.structures import CaseInsensitiveDict
  11. from requests.utils import get_encoding_from_headers
  12. from requests.cookies import extract_cookies_to_jar
  13. except ImportError: # pragma: no cover
  14. HTTPAdapter = object
  15. from hyper.common.connection import HTTPConnection
  16. from hyper.compat import urlparse
  17. from hyper.tls import init_context
  18. class HTTP20Adapter(HTTPAdapter):
  19. """
  20. A Requests Transport Adapter that uses hyper to send requests over
  21. HTTP/2. This implements some degree of connection pooling to maximise the
  22. HTTP/2 gain.
  23. """
  24. def __init__(self, *args, **kwargs):
  25. #: A mapping between HTTP netlocs and ``HTTP20Connection`` objects.
  26. self.connections = {}
  27. def get_connection(self, host, port, scheme, cert=None):
  28. """
  29. Gets an appropriate HTTP/2 connection object based on
  30. host/port/scheme/cert tuples.
  31. """
  32. secure = (scheme == 'https')
  33. if port is None: # pragma: no cover
  34. port = 80 if not secure else 443
  35. ssl_context = None
  36. if cert is not None:
  37. ssl_context = init_context(cert=cert)
  38. try:
  39. conn = self.connections[(host, port, scheme, cert)]
  40. except KeyError:
  41. conn = HTTPConnection(
  42. host,
  43. port,
  44. secure=secure,
  45. ssl_context=ssl_context)
  46. self.connections[(host, port, scheme, cert)] = conn
  47. return conn
  48. def send(self, request, stream=False, cert=None, **kwargs):
  49. """
  50. Sends a HTTP message to the server.
  51. """
  52. parsed = urlparse(request.url)
  53. conn = self.get_connection(
  54. parsed.hostname,
  55. parsed.port,
  56. parsed.scheme,
  57. cert=cert)
  58. # Build the selector.
  59. selector = parsed.path
  60. selector += '?' + parsed.query if parsed.query else ''
  61. selector += '#' + parsed.fragment if parsed.fragment else ''
  62. conn.request(
  63. request.method,
  64. selector,
  65. request.body,
  66. request.headers
  67. )
  68. resp = conn.get_response()
  69. r = self.build_response(request, resp)
  70. if not stream:
  71. r.content
  72. return r
  73. def build_response(self, request, resp):
  74. """
  75. Builds a Requests' response object. This emulates most of the logic of
  76. the standard fuction but deals with the lack of the ``.headers``
  77. property on the HTTP20Response object.
  78. Additionally, this function builds in a number of features that are
  79. purely for HTTPie. This is to allow maximum compatibility with what
  80. urllib3 does, so that HTTPie doesn't fall over when it uses us.
  81. """
  82. response = Response()
  83. response.status_code = resp.status
  84. response.headers = CaseInsensitiveDict(resp.headers.iter_raw())
  85. response.raw = resp
  86. response.reason = resp.reason
  87. response.encoding = get_encoding_from_headers(response.headers)
  88. extract_cookies_to_jar(response.cookies, request, response)
  89. response.url = request.url
  90. response.request = request
  91. response.connection = self
  92. # First horrible patch: Requests expects its raw responses to have a
  93. # release_conn method, which I don't. We should monkeypatch a no-op on.
  94. resp.release_conn = lambda: None
  95. # Next, add the things HTTPie needs. It needs the following things:
  96. #
  97. # - The `raw` object has a property called `_original_response` that is
  98. # a `httplib` response object.
  99. # - `raw._original_response` has three simple properties: `version`,
  100. # `status`, `reason`.
  101. # - `raw._original_response.version` has one of three values: `9`,
  102. # `10`, `11`.
  103. # - `raw._original_response.msg` exists.
  104. # - `raw._original_response.msg._headers` exists and is an iterable of
  105. # two-tuples.
  106. #
  107. # We fake this out. Most of this exists on our response object already,
  108. # and the rest can be faked.
  109. #
  110. # All of this exists for httpie, which I don't have any tests for,
  111. # so I'm not going to bother adding test coverage for it.
  112. class FakeOriginalResponse(object): # pragma: no cover
  113. def __init__(self, headers):
  114. self._headers = headers
  115. def get_all(self, name, default=None):
  116. values = []
  117. for n, v in self._headers:
  118. if n == name.lower():
  119. values.append(v)
  120. if not values:
  121. return default
  122. return values
  123. def getheaders(self, name):
  124. return self.get_all(name, [])
  125. response.raw._original_response = orig = FakeOriginalResponse(None)
  126. orig.version = 20
  127. orig.status = resp.status
  128. orig.reason = resp.reason
  129. orig.msg = FakeOriginalResponse(resp.headers.iter_raw())
  130. return response