blob: cda5f83d4621627917f6a64fe525df88af5ebabc [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.
46 NOTE: if both cafile and ssl_context are defined, ssl_context will override 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
James E. King III6f8c99e2018-03-24 16:32:02 -040066 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
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090069 self.host = parsed.hostname
70 self.path = parsed.path
71 if parsed.query:
72 self.path += '?%s' % parsed.query
Martin Wilck1ac0a802016-04-27 09:41:03 +020073 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
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +090089 self.__wbuf = BytesIO()
90 self.__http = None
91 self.__http_response = None
92 self.__timeout = None
93 self.__custom_headers = None
Márton Csordás103a11c2021-12-16 10:08:11 +010094 self.headers = None
Mark Sleebd8b9912007-02-27 20:17:00 +000095
Martin Wilck1ac0a802016-04-27 09:41:03 +020096 @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))
くらげbf096752021-07-25 23:46:42 +0900102 cr = base64.b64encode(ap.encode()).strip()
Martin Wilck1ac0a802016-04-27 09:41:03 +0200103 return "Basic " + cr
104
105 def using_proxy(self):
106 return self.realhost is not None
107
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900108 def open(self):
109 if self.scheme == 'http':
Alexandre Detiste8f9fba82024-08-28 14:28:39 +0200110 self.__http = http.client.HTTPConnection(self.host, self.port,
James E. King III6f8c99e2018-03-24 16:32:02 -0400111 timeout=self.__timeout)
Martin Wilck1ac0a802016-04-27 09:41:03 +0200112 elif self.scheme == 'https':
Alexandre Detiste8f9fba82024-08-28 14:28:39 +0200113 self.__http = http.client.HTTPSConnection(self.host, self.port,
James E. King III6f8c99e2018-03-24 16:32:02 -0400114 timeout=self.__timeout,
115 context=self.context)
116 if self.using_proxy():
117 self.__http.set_tunnel(self.realhost, self.realport,
118 {"Proxy-Authorization": self.proxy_auth})
Mark Sleebd8b9912007-02-27 20:17:00 +0000119
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900120 def close(self):
121 self.__http.close()
122 self.__http = None
123 self.__http_response = None
David Reiss0c90f6f2008-02-06 22:18:40 +0000124
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900125 def isOpen(self):
126 return self.__http is not None
Mark Sleebd8b9912007-02-27 20:17:00 +0000127
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900128 def setTimeout(self, ms):
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900129 if ms is None:
130 self.__timeout = None
131 else:
132 self.__timeout = ms / 1000.0
David Reissff3d2492010-03-09 05:19:16 +0000133
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900134 def setCustomHeaders(self, headers):
135 self.__custom_headers = headers
Roger Meierfa392e92012-04-11 22:15:15 +0000136
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900137 def read(self, sz):
138 return self.__http_response.read(sz)
Mark Sleebd8b9912007-02-27 20:17:00 +0000139
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900140 def write(self, buf):
141 self.__wbuf.write(buf)
Mark Sleebd8b9912007-02-27 20:17:00 +0000142
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900143 def flush(self):
144 if self.isOpen():
145 self.close()
146 self.open()
David Reiss7c1f6f82009-03-24 20:10:24 +0000147
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900148 # Pull data out of buffer
149 data = self.__wbuf.getvalue()
150 self.__wbuf = BytesIO()
Mark Sleebd8b9912007-02-27 20:17:00 +0000151
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900152 # HTTP request
Martin Wilck1ac0a802016-04-27 09:41:03 +0200153 if self.using_proxy() and self.scheme == "http":
154 # need full URL of real host for HTTP proxy here (HTTPS uses CONNECT tunnel)
155 self.__http.putrequest('POST', "http://%s:%s%s" %
156 (self.realhost, self.realport, self.path))
157 else:
158 self.__http.putrequest('POST', self.path)
Mark Sleebd8b9912007-02-27 20:17:00 +0000159
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900160 # Write headers
161 self.__http.putheader('Content-Type', 'application/x-thrift')
162 self.__http.putheader('Content-Length', str(len(data)))
Martin Wilck1ac0a802016-04-27 09:41:03 +0200163 if self.using_proxy() and self.scheme == "http" and self.proxy_auth is not None:
164 self.__http.putheader("Proxy-Authorization", self.proxy_auth)
Roger Meierfa392e92012-04-11 22:15:15 +0000165
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900166 if not self.__custom_headers or 'User-Agent' not in self.__custom_headers:
167 user_agent = 'Python/THttpClient'
168 script = os.path.basename(sys.argv[0])
169 if script:
170 user_agent = '%s (%s)' % (user_agent, urllib.parse.quote(script))
171 self.__http.putheader('User-Agent', user_agent)
Roger Meier3f5a2642012-04-13 14:20:08 +0000172
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900173 if self.__custom_headers:
Alexandre Detiste8f9fba82024-08-28 14:28:39 +0200174 for key, val in self.__custom_headers.items():
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900175 self.__http.putheader(key, val)
Roger Meierfa392e92012-04-11 22:15:15 +0000176
Márton Csordás103a11c2021-12-16 10:08:11 +0100177 # Saves the cookie sent by the server in the previous response.
178 # HTTPConnection.putheader can only be called after a request has been
179 # started, and before it's been sent.
180 if self.headers and 'Set-Cookie' in self.headers:
181 self.__http.putheader('Cookie', self.headers['Set-Cookie'])
182
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900183 self.__http.endheaders()
Mark Sleebd8b9912007-02-27 20:17:00 +0000184
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900185 # Write payload
186 self.__http.send(data)
Mark Sleebd8b9912007-02-27 20:17:00 +0000187
Nobuaki Sukegawa10308cb2016-02-03 01:57:03 +0900188 # Get reply to flush the request
189 self.__http_response = self.__http.getresponse()
190 self.code = self.__http_response.status
191 self.message = self.__http_response.reason
192 self.headers = self.__http_response.msg