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