blob: 1dd1476d3a4f38e452c2c6b91ddb3b3cacb14cda [file] [log] [blame]
Mark Slee89e2bb82007-03-01 00:20:36 +00001#
David Reissea2cba82009-03-30 21:35:00 +00002# 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#
Mark Slee89e2bb82007-03-01 00:20:36 +000019
Nobuaki Sukegawa760511f2015-11-06 21:24:16 +090020from io import BytesIO
Roger Meier3f5a2642012-04-13 14:20:08 +000021import os
James E. King III6f8c99e2018-03-24 16:32:02 -040022import ssl
Roger Meier3f5a2642012-04-13 14:20:08 +000023import sys
Bryan Duxbury69720412012-01-03 17:32:30 +000024import warnings
Martin Wilck1ac0a802016-04-27 09:41:03 +020025import base64
Bryan Duxbury69720412012-01-03 17:32:30 +000026
Alexandre Detiste8f9fba82024-08-28 14:28:39 +020027import urllib.parse
28import urllib.request
29import http.client
Mark Sleebd8b9912007-02-27 20:17:00 +000030
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090031from .TTransport import TTransportBase
Bryan Duxbury69720412012-01-03 17:32:30 +000032
Mark Sleebd8b9912007-02-27 20:17:00 +000033
34class THttpClient(TTransportBase):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090035 """Http implementation of TTransport base."""
Mark Sleebd8b9912007-02-27 20:17:00 +000036
James E. King III6f8c99e2018-03-24 16:32:02 -040037 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:
David Reiss2aa28902009-03-26 06:22:18 +000039
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090040 THttpClient(host, port, path) - deprecated
James E. King III6f8c99e2018-03-24 16:32:02 -040041 THttpClient(uri, [port=<n>, path=<s>, cafile=<filename>, cert_file=<filename>, key_file=<filename>, ssl_context=<context>])
David Reiss2aa28902009-03-26 06:22:18 +000042
James E. King III6f8c99e2018-03-24 16:32:02 -040043 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.
Jeffrey Tolar7aea5242025-05-30 11:20:05 -050046 NOTE: if ssl_context is defined, it will override any provided cert_file, key_file, and cafile.
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090047 """
48 if port is not None:
49 warnings.warn(
James E. King III6f8c99e2018-03-24 16:32:02 -040050 "Please use the THttpClient('http{s}://host:port/path') constructor",
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090051 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':
Alexandre Detiste8f9fba82024-08-28 14:28:39 +020063 self.port = parsed.port or http.client.HTTP_PORT
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090064 elif self.scheme == 'https':
Alexandre Detiste8f9fba82024-08-28 14:28:39 +020065 self.port = parsed.port or http.client.HTTPS_PORT
Jeffrey Tolar7aea5242025-05-30 11:20:05 -050066 if (cafile or cert_file or key_file) and not ssl_context:
67 self.context = ssl.create_default_context(cafile=cafile)
68 self.context.load_cert_chain(certfile=cert_file, keyfile=key_file)
69 else:
70 self.context = ssl_context
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090071 self.host = parsed.hostname
72 self.path = parsed.path
73 if parsed.query:
74 self.path += '?%s' % parsed.query
Martin Wilck1ac0a802016-04-27 09:41:03 +020075 try:
76 proxy = urllib.request.getproxies()[self.scheme]
77 except KeyError:
78 proxy = None
79 else:
80 if urllib.request.proxy_bypass(self.host):
81 proxy = None
82 if proxy:
83 parsed = urllib.parse.urlparse(proxy)
84 self.realhost = self.host
85 self.realport = self.port
86 self.host = parsed.hostname
87 self.port = parsed.port
88 self.proxy_auth = self.basic_proxy_auth_header(parsed)
89 else:
90 self.realhost = self.realport = self.proxy_auth = None
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090091 self.__wbuf = BytesIO()
92 self.__http = None
93 self.__http_response = None
94 self.__timeout = None
95 self.__custom_headers = None
Márton Csordás103a11c2021-12-16 10:08:11 +010096 self.headers = None
Mark Sleebd8b9912007-02-27 20:17:00 +000097
Martin Wilck1ac0a802016-04-27 09:41:03 +020098 @staticmethod
99 def basic_proxy_auth_header(proxy):
100 if proxy is None or not proxy.username:
101 return None
102 ap = "%s:%s" % (urllib.parse.unquote(proxy.username),
103 urllib.parse.unquote(proxy.password))
くらげbf096752021-07-25 23:46:42 +0900104 cr = base64.b64encode(ap.encode()).strip()
Martin Wilck1ac0a802016-04-27 09:41:03 +0200105 return "Basic " + cr
106
107 def using_proxy(self):
108 return self.realhost is not None
109
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900110 def open(self):
111 if self.scheme == 'http':
Alexandre Detiste8f9fba82024-08-28 14:28:39 +0200112 self.__http = http.client.HTTPConnection(self.host, self.port,
James E. King III6f8c99e2018-03-24 16:32:02 -0400113 timeout=self.__timeout)
Martin Wilck1ac0a802016-04-27 09:41:03 +0200114 elif self.scheme == 'https':
Alexandre Detiste8f9fba82024-08-28 14:28:39 +0200115 self.__http = http.client.HTTPSConnection(self.host, self.port,
James E. King III6f8c99e2018-03-24 16:32:02 -0400116 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})
Mark Sleebd8b9912007-02-27 20:17:00 +0000121
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900122 def close(self):
123 self.__http.close()
124 self.__http = None
125 self.__http_response = None
David Reiss0c90f6f2008-02-06 22:18:40 +0000126
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900127 def isOpen(self):
128 return self.__http is not None
Mark Sleebd8b9912007-02-27 20:17:00 +0000129
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900130 def setTimeout(self, ms):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900131 if ms is None:
132 self.__timeout = None
133 else:
134 self.__timeout = ms / 1000.0
David Reissff3d2492010-03-09 05:19:16 +0000135
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900136 def setCustomHeaders(self, headers):
137 self.__custom_headers = headers
Roger Meierfa392e92012-04-11 22:15:15 +0000138
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900139 def read(self, sz):
140 return self.__http_response.read(sz)
Mark Sleebd8b9912007-02-27 20:17:00 +0000141
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900142 def write(self, buf):
143 self.__wbuf.write(buf)
Mark Sleebd8b9912007-02-27 20:17:00 +0000144
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900145 def flush(self):
146 if self.isOpen():
147 self.close()
148 self.open()
David Reiss7c1f6f82009-03-24 20:10:24 +0000149
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900150 # Pull data out of buffer
151 data = self.__wbuf.getvalue()
152 self.__wbuf = BytesIO()
Mark Sleebd8b9912007-02-27 20:17:00 +0000153
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900154 # HTTP request
Martin Wilck1ac0a802016-04-27 09:41:03 +0200155 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)
Mark Sleebd8b9912007-02-27 20:17:00 +0000161
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900162 # Write headers
163 self.__http.putheader('Content-Type', 'application/x-thrift')
164 self.__http.putheader('Content-Length', str(len(data)))
Martin Wilck1ac0a802016-04-27 09:41:03 +0200165 if self.using_proxy() and self.scheme == "http" and self.proxy_auth is not None:
166 self.__http.putheader("Proxy-Authorization", self.proxy_auth)
Roger Meierfa392e92012-04-11 22:15:15 +0000167
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900168 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)
Roger Meier3f5a2642012-04-13 14:20:08 +0000174
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900175 if self.__custom_headers:
Alexandre Detiste8f9fba82024-08-28 14:28:39 +0200176 for key, val in self.__custom_headers.items():
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900177 self.__http.putheader(key, val)
Roger Meierfa392e92012-04-11 22:15:15 +0000178
Márton Csordás103a11c2021-12-16 10:08:11 +0100179 # 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
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900185 self.__http.endheaders()
Mark Sleebd8b9912007-02-27 20:17:00 +0000186
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900187 # Write payload
188 self.__http.send(data)
Mark Sleebd8b9912007-02-27 20:17:00 +0000189
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900190 # 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