1#
2# Licensed to the Apache Software Foundation (ASF) under one
3# or more contributor license agreements. See the NOTICE file
4# distributed with this work for additional information
5# regarding copyright ownership. The ASF licenses this file
6# to you under the Apache License, Version 2.0 (the
7# "License"); you may not use this file except in compliance
8# with the License. You may obtain a copy of the License at
9#
10#   http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing,
13# software distributed under the License is distributed on an
14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15# KIND, either express or implied. See the License for the
16# specific language governing permissions and limitations
17# under the License.
18#
19
20from io import BytesIO
21import os
22import ssl
23import sys
24import warnings
25import base64
26
27from six.moves import urllib
28from six.moves import http_client
29
30from .TTransport import TTransportBase
31import six
32
33
34class THttpClient(TTransportBase):
35    """Http implementation of TTransport base."""
36
37    def __init__(self, uri_or_host, port=None, path=None, cafile=None, cert_file=None, key_file=None, ssl_context=None):
38        """THttpClient supports two different types of construction:
39
40        THttpClient(host, port, path) - deprecated
41        THttpClient(uri, [port=<n>, path=<s>, cafile=<filename>, cert_file=<filename>, key_file=<filename>, ssl_context=<context>])
42
43        Only the second supports https.  To properly authenticate against the server,
44        provide the client's identity by specifying cert_file and key_file.  To properly
45        authenticate the server, specify either cafile or ssl_context with a CA defined.
46        NOTE: if both cafile and ssl_context are defined, ssl_context will override cafile.
47        """
48        if port is not None:
49            warnings.warn(
50                "Please use the THttpClient('http{s}://host:port/path') constructor",
51                DeprecationWarning,
52                stacklevel=2)
53            self.host = uri_or_host
54            self.port = port
55            assert path
56            self.path = path
57            self.scheme = 'http'
58        else:
59            parsed = urllib.parse.urlparse(uri_or_host)
60            self.scheme = parsed.scheme
61            assert self.scheme in ('http', 'https')
62            if self.scheme == 'http':
63                self.port = parsed.port or http_client.HTTP_PORT
64            elif self.scheme == 'https':
65                self.port = parsed.port or http_client.HTTPS_PORT
66                self.certfile = cert_file
67                self.keyfile = key_file
68                self.context = ssl.create_default_context(cafile=cafile) if (cafile and not ssl_context) else ssl_context
69            self.host = parsed.hostname
70            self.path = parsed.path
71            if parsed.query:
72                self.path += '?%s' % parsed.query
73        try:
74            proxy = urllib.request.getproxies()[self.scheme]
75        except KeyError:
76            proxy = None
77        else:
78            if urllib.request.proxy_bypass(self.host):
79                proxy = None
80        if proxy:
81            parsed = urllib.parse.urlparse(proxy)
82            self.realhost = self.host
83            self.realport = self.port
84            self.host = parsed.hostname
85            self.port = parsed.port
86            self.proxy_auth = self.basic_proxy_auth_header(parsed)
87        else:
88            self.realhost = self.realport = self.proxy_auth = None
89        self.__wbuf = BytesIO()
90        self.__http = None
91        self.__http_response = None
92        self.__timeout = None
93        self.__custom_headers = None
94        self.headers = None
95
96    @staticmethod
97    def basic_proxy_auth_header(proxy):
98        if proxy is None or not proxy.username:
99            return None
100        ap = "%s:%s" % (urllib.parse.unquote(proxy.username),
101                        urllib.parse.unquote(proxy.password))
102        cr = base64.b64encode(ap.encode()).strip()
103        return "Basic " + cr
104
105    def using_proxy(self):
106        return self.realhost is not None
107
108    def open(self):
109        if self.scheme == 'http':
110            self.__http = http_client.HTTPConnection(self.host, self.port,
111                                                     timeout=self.__timeout)
112        elif self.scheme == 'https':
113            self.__http = http_client.HTTPSConnection(self.host, self.port,
114                                                      key_file=self.keyfile,
115                                                      cert_file=self.certfile,
116                                                      timeout=self.__timeout,
117                                                      context=self.context)
118        if self.using_proxy():
119            self.__http.set_tunnel(self.realhost, self.realport,
120                                   {"Proxy-Authorization": self.proxy_auth})
121
122    def close(self):
123        self.__http.close()
124        self.__http = None
125        self.__http_response = None
126
127    def isOpen(self):
128        return self.__http is not None
129
130    def setTimeout(self, ms):
131        if ms is None:
132            self.__timeout = None
133        else:
134            self.__timeout = ms / 1000.0
135
136    def setCustomHeaders(self, headers):
137        self.__custom_headers = headers
138
139    def read(self, sz):
140        return self.__http_response.read(sz)
141
142    def write(self, buf):
143        self.__wbuf.write(buf)
144
145    def flush(self):
146        if self.isOpen():
147            self.close()
148        self.open()
149
150        # Pull data out of buffer
151        data = self.__wbuf.getvalue()
152        self.__wbuf = BytesIO()
153
154        # HTTP request
155        if self.using_proxy() and self.scheme == "http":
156            # need full URL of real host for HTTP proxy here (HTTPS uses CONNECT tunnel)
157            self.__http.putrequest('POST', "http://%s:%s%s" %
158                                   (self.realhost, self.realport, self.path))
159        else:
160            self.__http.putrequest('POST', self.path)
161
162        # Write headers
163        self.__http.putheader('Content-Type', 'application/x-thrift')
164        self.__http.putheader('Content-Length', str(len(data)))
165        if self.using_proxy() and self.scheme == "http" and self.proxy_auth is not None:
166            self.__http.putheader("Proxy-Authorization", self.proxy_auth)
167
168        if not self.__custom_headers or 'User-Agent' not in self.__custom_headers:
169            user_agent = 'Python/THttpClient'
170            script = os.path.basename(sys.argv[0])
171            if script:
172                user_agent = '%s (%s)' % (user_agent, urllib.parse.quote(script))
173            self.__http.putheader('User-Agent', user_agent)
174
175        if self.__custom_headers:
176            for key, val in six.iteritems(self.__custom_headers):
177                self.__http.putheader(key, val)
178
179        # Saves the cookie sent by the server in the previous response.
180        # HTTPConnection.putheader can only be called after a request has been
181        # started, and before it's been sent.
182        if self.headers and 'Set-Cookie' in self.headers:
183            self.__http.putheader('Cookie', self.headers['Set-Cookie'])
184
185        self.__http.endheaders()
186
187        # Write payload
188        self.__http.send(data)
189
190        # Get reply to flush the request
191        self.__http_response = self.__http.getresponse()
192        self.code = self.__http_response.status
193        self.message = self.__http_response.reason
194        self.headers = self.__http_response.msg
195