Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/websocket/_http.py: 34%
211 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:34 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-25 06:34 +0000
1"""
2_http.py
3websocket - WebSocket client library for Python
5Copyright 2023 engn33r
7Licensed under the Apache License, Version 2.0 (the "License");
8you may not use this file except in compliance with the License.
9You may obtain a copy of the License at
11 http://www.apache.org/licenses/LICENSE-2.0
13Unless required by applicable law or agreed to in writing, software
14distributed under the License is distributed on an "AS IS" BASIS,
15WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16See the License for the specific language governing permissions and
17limitations under the License.
18"""
19import errno
20import os
21import socket
23from ._exceptions import *
24from ._logging import *
25from ._socket import *
26from ._ssl_compat import *
27from ._url import *
29from base64 import encodebytes as base64encode
31__all__ = ["proxy_info", "connect", "read_headers"]
33try:
34 from python_socks.sync import Proxy
35 from python_socks._errors import *
36 from python_socks._types import ProxyType
37 HAVE_PYTHON_SOCKS = True
38except:
39 HAVE_PYTHON_SOCKS = False
41 class ProxyError(Exception):
42 pass
44 class ProxyTimeoutError(Exception):
45 pass
47 class ProxyConnectionError(Exception):
48 pass
51class proxy_info:
53 def __init__(self, **options):
54 self.proxy_host = options.get("http_proxy_host", None)
55 if self.proxy_host:
56 self.proxy_port = options.get("http_proxy_port", 0)
57 self.auth = options.get("http_proxy_auth", None)
58 self.no_proxy = options.get("http_no_proxy", None)
59 self.proxy_protocol = options.get("proxy_type", "http")
60 # Note: If timeout not specified, default python-socks timeout is 60 seconds
61 self.proxy_timeout = options.get("http_proxy_timeout", None)
62 if self.proxy_protocol not in ['http', 'socks4', 'socks4a', 'socks5', 'socks5h']:
63 raise ProxyError("Only http, socks4, socks5 proxy protocols are supported")
64 else:
65 self.proxy_port = 0
66 self.auth = None
67 self.no_proxy = None
68 self.proxy_protocol = "http"
71def _start_proxied_socket(url: str, options, proxy):
72 if not HAVE_PYTHON_SOCKS:
73 raise WebSocketException("Python Socks is needed for SOCKS proxying but is not available")
75 hostname, port, resource, is_secure = parse_url(url)
77 if proxy.proxy_protocol == "socks5":
78 rdns = False
79 proxy_type = ProxyType.SOCKS5
80 if proxy.proxy_protocol == "socks4":
81 rdns = False
82 proxy_type = ProxyType.SOCKS4
83 # socks5h and socks4a send DNS through proxy
84 if proxy.proxy_protocol == "socks5h":
85 rdns = True
86 proxy_type = ProxyType.SOCKS5
87 if proxy.proxy_protocol == "socks4a":
88 rdns = True
89 proxy_type = ProxyType.SOCKS4
91 ws_proxy = Proxy.create(
92 proxy_type=proxy_type,
93 host=proxy.proxy_host,
94 port=int(proxy.proxy_port),
95 username=proxy.auth[0] if proxy.auth else None,
96 password=proxy.auth[1] if proxy.auth else None,
97 rdns=rdns)
99 sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout)
101 if is_secure and HAVE_SSL:
102 sock = _ssl_socket(sock, options.sslopt, hostname)
103 elif is_secure:
104 raise WebSocketException("SSL not available.")
106 return sock, (hostname, port, resource)
109def connect(url: str, options, proxy, socket):
110 # Use _start_proxied_socket() only for socks4 or socks5 proxy
111 # Use _tunnel() for http proxy
112 # TODO: Use python-socks for http protocol also, to standardize flow
113 if proxy.proxy_host and not socket and not (proxy.proxy_protocol == "http"):
114 return _start_proxied_socket(url, options, proxy)
116 hostname, port_from_url, resource, is_secure = parse_url(url)
118 if socket:
119 return socket, (hostname, port_from_url, resource)
121 addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
122 hostname, port_from_url, is_secure, proxy)
123 if not addrinfo_list:
124 raise WebSocketException(
125 "Host not found.: " + hostname + ":" + str(port_from_url))
127 sock = None
128 try:
129 sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
130 if need_tunnel:
131 sock = _tunnel(sock, hostname, port_from_url, auth)
133 if is_secure:
134 if HAVE_SSL:
135 sock = _ssl_socket(sock, options.sslopt, hostname)
136 else:
137 raise WebSocketException("SSL not available.")
139 return sock, (hostname, port_from_url, resource)
140 except:
141 if sock:
142 sock.close()
143 raise
146def _get_addrinfo_list(hostname, port, is_secure, proxy):
147 phost, pport, pauth = get_proxy_info(
148 hostname, is_secure, proxy.proxy_host, proxy.proxy_port, proxy.auth, proxy.no_proxy)
149 try:
150 # when running on windows 10, getaddrinfo without socktype returns a socktype 0.
151 # This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
152 # or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
153 if not phost:
154 addrinfo_list = socket.getaddrinfo(
155 hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP)
156 return addrinfo_list, False, None
157 else:
158 pport = pport and pport or 80
159 # when running on windows 10, the getaddrinfo used above
160 # returns a socktype 0. This generates an error exception:
161 # _on_error: exception Socket type must be stream or datagram, not 0
162 # Force the socket type to SOCK_STREAM
163 addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP)
164 return addrinfo_list, True, pauth
165 except socket.gaierror as e:
166 raise WebSocketAddressException(e)
169def _open_socket(addrinfo_list, sockopt, timeout):
170 err = None
171 for addrinfo in addrinfo_list:
172 family, socktype, proto = addrinfo[:3]
173 sock = socket.socket(family, socktype, proto)
174 sock.settimeout(timeout)
175 for opts in DEFAULT_SOCKET_OPTION:
176 sock.setsockopt(*opts)
177 for opts in sockopt:
178 sock.setsockopt(*opts)
180 address = addrinfo[4]
181 err = None
182 while not err:
183 try:
184 sock.connect(address)
185 except socket.error as error:
186 sock.close()
187 error.remote_ip = str(address[0])
188 try:
189 eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED, errno.ENETUNREACH)
190 except AttributeError:
191 eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH)
192 if error.errno in eConnRefused:
193 err = error
194 continue
195 else:
196 raise error
197 else:
198 break
199 else:
200 continue
201 break
202 else:
203 if err:
204 raise err
206 return sock
209def _wrap_sni_socket(sock, sslopt, hostname, check_hostname):
210 context = sslopt.get('context', None)
211 if not context:
212 context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_TLS_CLIENT))
213 # Non default context need to manually enable SSLKEYLOGFILE support by setting the keylog_filename attribute.
214 # For more details see also:
215 # * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#context-creation
216 # * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#ssl.SSLContext.keylog_filename
217 context.keylog_filename = os.environ.get("SSLKEYLOGFILE", None)
219 if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
220 cafile = sslopt.get('ca_certs', None)
221 capath = sslopt.get('ca_cert_path', None)
222 if cafile or capath:
223 context.load_verify_locations(cafile=cafile, capath=capath)
224 elif hasattr(context, 'load_default_certs'):
225 context.load_default_certs(ssl.Purpose.SERVER_AUTH)
226 if sslopt.get('certfile', None):
227 context.load_cert_chain(
228 sslopt['certfile'],
229 sslopt.get('keyfile', None),
230 sslopt.get('password', None),
231 )
233 # Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True"
234 # If both disabled, set check_hostname before verify_mode
235 # see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
236 if sslopt.get('cert_reqs', ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get('check_hostname', False):
237 context.check_hostname = False
238 context.verify_mode = ssl.CERT_NONE
239 else:
240 context.check_hostname = sslopt.get('check_hostname', True)
241 context.verify_mode = sslopt.get('cert_reqs', ssl.CERT_REQUIRED)
243 if 'ciphers' in sslopt:
244 context.set_ciphers(sslopt['ciphers'])
245 if 'cert_chain' in sslopt:
246 certfile, keyfile, password = sslopt['cert_chain']
247 context.load_cert_chain(certfile, keyfile, password)
248 if 'ecdh_curve' in sslopt:
249 context.set_ecdh_curve(sslopt['ecdh_curve'])
251 return context.wrap_socket(
252 sock,
253 do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
254 suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
255 server_hostname=hostname,
256 )
259def _ssl_socket(sock, user_sslopt, hostname):
260 sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)
261 sslopt.update(user_sslopt)
263 certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE')
264 if certPath and os.path.isfile(certPath) \
265 and user_sslopt.get('ca_certs', None) is None:
266 sslopt['ca_certs'] = certPath
267 elif certPath and os.path.isdir(certPath) \
268 and user_sslopt.get('ca_cert_path', None) is None:
269 sslopt['ca_cert_path'] = certPath
271 if sslopt.get('server_hostname', None):
272 hostname = sslopt['server_hostname']
274 check_hostname = sslopt.get('check_hostname', True)
275 sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
277 return sock
280def _tunnel(sock, host, port, auth):
281 debug("Connecting proxy...")
282 connect_header = "CONNECT {h}:{p} HTTP/1.1\r\n".format(h=host, p=port)
283 connect_header += "Host: {h}:{p}\r\n".format(h=host, p=port)
285 # TODO: support digest auth.
286 if auth and auth[0]:
287 auth_str = auth[0]
288 if auth[1]:
289 auth_str += ":" + auth[1]
290 encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '')
291 connect_header += "Proxy-Authorization: Basic {str}\r\n".format(str=encoded_str)
292 connect_header += "\r\n"
293 dump("request header", connect_header)
295 send(sock, connect_header)
297 try:
298 status, resp_headers, status_message = read_headers(sock)
299 except Exception as e:
300 raise WebSocketProxyException(str(e))
302 if status != 200:
303 raise WebSocketProxyException(
304 "failed CONNECT via proxy status: {status}".format(status=status))
306 return sock
309def read_headers(sock):
310 status = None
311 status_message = None
312 headers = {}
313 trace("--- response header ---")
315 while True:
316 line = recv_line(sock)
317 line = line.decode('utf-8').strip()
318 if not line:
319 break
320 trace(line)
321 if not status:
323 status_info = line.split(" ", 2)
324 status = int(status_info[1])
325 if len(status_info) > 2:
326 status_message = status_info[2]
327 else:
328 kv = line.split(":", 1)
329 if len(kv) == 2:
330 key, value = kv
331 if key.lower() == "set-cookie" and headers.get("set-cookie"):
332 headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
333 else:
334 headers[key.lower()] = value.strip()
335 else:
336 raise WebSocketException("Invalid header")
338 trace("-----------------------")
340 return status, headers, status_message