Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/websocket/_http.py: 35%
211 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:48 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:48 +0000
1"""
2_http.py
3websocket - WebSocket client library for Python
5Copyright 2022 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
22import sys
24from ._exceptions import *
25from ._logging import *
26from ._socket import *
27from ._ssl_compat import *
28from ._url import *
30from base64 import encodebytes as base64encode
32__all__ = ["proxy_info", "connect", "read_headers"]
34try:
35 from python_socks.sync import Proxy
36 from python_socks._errors import *
37 from python_socks._types import ProxyType
38 HAVE_PYTHON_SOCKS = True
39except:
40 HAVE_PYTHON_SOCKS = False
42 class ProxyError(Exception):
43 pass
45 class ProxyTimeoutError(Exception):
46 pass
48 class ProxyConnectionError(Exception):
49 pass
52class proxy_info:
54 def __init__(self, **options):
55 self.proxy_host = options.get("http_proxy_host", None)
56 if self.proxy_host:
57 self.proxy_port = options.get("http_proxy_port", 0)
58 self.auth = options.get("http_proxy_auth", None)
59 self.no_proxy = options.get("http_no_proxy", None)
60 self.proxy_protocol = options.get("proxy_type", "http")
61 # Note: If timeout not specified, default python-socks timeout is 60 seconds
62 self.proxy_timeout = options.get("http_proxy_timeout", None)
63 if self.proxy_protocol not in ['http', 'socks4', 'socks4a', 'socks5', 'socks5h']:
64 raise ProxyError("Only http, socks4, socks5 proxy protocols are supported")
65 else:
66 self.proxy_port = 0
67 self.auth = None
68 self.no_proxy = None
69 self.proxy_protocol = "http"
72def _start_proxied_socket(url, options, proxy):
73 if not HAVE_PYTHON_SOCKS:
74 raise WebSocketException("Python Socks is needed for SOCKS proxying but is not available")
76 hostname, port, resource, is_secure = parse_url(url)
78 if proxy.proxy_protocol == "socks5":
79 rdns = False
80 proxy_type = ProxyType.SOCKS5
81 if proxy.proxy_protocol == "socks4":
82 rdns = False
83 proxy_type = ProxyType.SOCKS4
84 # socks5h and socks4a send DNS through proxy
85 if proxy.proxy_protocol == "socks5h":
86 rdns = True
87 proxy_type = ProxyType.SOCKS5
88 if proxy.proxy_protocol == "socks4a":
89 rdns = True
90 proxy_type = ProxyType.SOCKS4
92 ws_proxy = Proxy.create(
93 proxy_type=proxy_type,
94 host=proxy.proxy_host,
95 port=int(proxy.proxy_port),
96 username=proxy.auth[0] if proxy.auth else None,
97 password=proxy.auth[1] if proxy.auth else None,
98 rdns=rdns)
100 sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout)
102 if is_secure and HAVE_SSL:
103 sock = _ssl_socket(sock, options.sslopt, hostname)
104 elif is_secure:
105 raise WebSocketException("SSL not available.")
107 return sock, (hostname, port, resource)
110def connect(url, options, proxy, socket):
111 # Use _start_proxied_socket() only for socks4 or socks5 proxy
112 # Use _tunnel() for http proxy
113 # TODO: Use python-socks for http protocol also, to standardize flow
114 if proxy.proxy_host and not socket and not (proxy.proxy_protocol == "http"):
115 return _start_proxied_socket(url, options, proxy)
117 hostname, port_from_url, resource, is_secure = parse_url(url)
119 if socket:
120 return socket, (hostname, port_from_url, resource)
122 addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
123 hostname, port_from_url, is_secure, proxy)
124 if not addrinfo_list:
125 raise WebSocketException(
126 "Host not found.: " + hostname + ":" + str(port_from_url))
128 sock = None
129 try:
130 sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
131 if need_tunnel:
132 sock = _tunnel(sock, hostname, port_from_url, auth)
134 if is_secure:
135 if HAVE_SSL:
136 sock = _ssl_socket(sock, options.sslopt, hostname)
137 else:
138 raise WebSocketException("SSL not available.")
140 return sock, (hostname, port_from_url, resource)
141 except:
142 if sock:
143 sock.close()
144 raise
147def _get_addrinfo_list(hostname, port, is_secure, proxy):
148 phost, pport, pauth = get_proxy_info(
149 hostname, is_secure, proxy.proxy_host, proxy.proxy_port, proxy.auth, proxy.no_proxy)
150 try:
151 # when running on windows 10, getaddrinfo without socktype returns a socktype 0.
152 # This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
153 # or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
154 if not phost:
155 addrinfo_list = socket.getaddrinfo(
156 hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP)
157 return addrinfo_list, False, None
158 else:
159 pport = pport and pport or 80
160 # when running on windows 10, the getaddrinfo used above
161 # returns a socktype 0. This generates an error exception:
162 # _on_error: exception Socket type must be stream or datagram, not 0
163 # Force the socket type to SOCK_STREAM
164 addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP)
165 return addrinfo_list, True, pauth
166 except socket.gaierror as e:
167 raise WebSocketAddressException(e)
170def _open_socket(addrinfo_list, sockopt, timeout):
171 err = None
172 for addrinfo in addrinfo_list:
173 family, socktype, proto = addrinfo[:3]
174 sock = socket.socket(family, socktype, proto)
175 sock.settimeout(timeout)
176 for opts in DEFAULT_SOCKET_OPTION:
177 sock.setsockopt(*opts)
178 for opts in sockopt:
179 sock.setsockopt(*opts)
181 address = addrinfo[4]
182 err = None
183 while not err:
184 try:
185 sock.connect(address)
186 except socket.error as error:
187 sock.close()
188 error.remote_ip = str(address[0])
189 try:
190 eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED, errno.ENETUNREACH)
191 except AttributeError:
192 eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH)
193 if error.errno in eConnRefused:
194 err = error
195 continue
196 else:
197 raise error
198 else:
199 break
200 else:
201 continue
202 break
203 else:
204 if err:
205 raise err
207 return sock
210def _wrap_sni_socket(sock, sslopt, hostname, check_hostname):
211 context = sslopt.get('context', None)
212 if not context:
213 context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_TLS_CLIENT))
215 if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
216 cafile = sslopt.get('ca_certs', None)
217 capath = sslopt.get('ca_cert_path', None)
218 if cafile or capath:
219 context.load_verify_locations(cafile=cafile, capath=capath)
220 elif hasattr(context, 'load_default_certs'):
221 context.load_default_certs(ssl.Purpose.SERVER_AUTH)
222 if sslopt.get('certfile', None):
223 context.load_cert_chain(
224 sslopt['certfile'],
225 sslopt.get('keyfile', None),
226 sslopt.get('password', None),
227 )
229 # Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True"
230 # If both disabled, set check_hostname before verify_mode
231 # see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
232 if sslopt.get('cert_reqs', ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get('check_hostname', False):
233 context.check_hostname = False
234 context.verify_mode = ssl.CERT_NONE
235 else:
236 context.check_hostname = sslopt.get('check_hostname', True)
237 context.verify_mode = sslopt.get('cert_reqs', ssl.CERT_REQUIRED)
239 if 'ciphers' in sslopt:
240 context.set_ciphers(sslopt['ciphers'])
241 if 'cert_chain' in sslopt:
242 certfile, keyfile, password = sslopt['cert_chain']
243 context.load_cert_chain(certfile, keyfile, password)
244 if 'ecdh_curve' in sslopt:
245 context.set_ecdh_curve(sslopt['ecdh_curve'])
247 return context.wrap_socket(
248 sock,
249 do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
250 suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
251 server_hostname=hostname,
252 )
255def _ssl_socket(sock, user_sslopt, hostname):
256 sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)
257 sslopt.update(user_sslopt)
259 certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE')
260 if certPath and os.path.isfile(certPath) \
261 and user_sslopt.get('ca_certs', None) is None:
262 sslopt['ca_certs'] = certPath
263 elif certPath and os.path.isdir(certPath) \
264 and user_sslopt.get('ca_cert_path', None) is None:
265 sslopt['ca_cert_path'] = certPath
267 if sslopt.get('server_hostname', None):
268 hostname = sslopt['server_hostname']
270 check_hostname = sslopt.get('check_hostname', True)
271 sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
273 return sock
276def _tunnel(sock, host, port, auth):
277 debug("Connecting proxy...")
278 connect_header = "CONNECT {h}:{p} HTTP/1.1\r\n".format(h=host, p=port)
279 connect_header += "Host: {h}:{p}\r\n".format(h=host, p=port)
281 # TODO: support digest auth.
282 if auth and auth[0]:
283 auth_str = auth[0]
284 if auth[1]:
285 auth_str += ":" + auth[1]
286 encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '')
287 connect_header += "Proxy-Authorization: Basic {str}\r\n".format(str=encoded_str)
288 connect_header += "\r\n"
289 dump("request header", connect_header)
291 send(sock, connect_header)
293 try:
294 status, resp_headers, status_message = read_headers(sock)
295 except Exception as e:
296 raise WebSocketProxyException(str(e))
298 if status != 200:
299 raise WebSocketProxyException(
300 "failed CONNECT via proxy status: {status}".format(status=status))
302 return sock
305def read_headers(sock):
306 status = None
307 status_message = None
308 headers = {}
309 trace("--- response header ---")
311 while True:
312 line = recv_line(sock)
313 line = line.decode('utf-8').strip()
314 if not line:
315 break
316 trace(line)
317 if not status:
319 status_info = line.split(" ", 2)
320 status = int(status_info[1])
321 if len(status_info) > 2:
322 status_message = status_info[2]
323 else:
324 kv = line.split(":", 1)
325 if len(kv) == 2:
326 key, value = kv
327 if key.lower() == "set-cookie" and headers.get("set-cookie"):
328 headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
329 else:
330 headers[key.lower()] = value.strip()
331 else:
332 raise WebSocketException("Invalid header")
334 trace("-----------------------")
336 return status, headers, status_message