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