Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/websocket/_http.py: 31%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2_http.py
3websocket - WebSocket client library for Python
5Copyright 2025 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"""
20import errno
21import os
22import socket
23from base64 import encodebytes as base64encode
25from ._exceptions import (
26 WebSocketAddressException,
27 WebSocketException,
28 WebSocketProxyException,
29)
30from ._logging import debug, dump, trace
31from ._socket import DEFAULT_SOCKET_OPTION, recv_line, send
32from ._ssl_compat import HAVE_SSL, ssl
33from ._url import get_proxy_info, parse_url
35__all__ = ["proxy_info", "connect", "read_headers"]
37try:
38 from python_socks._errors import ProxyConnectionError, ProxyError, ProxyTimeoutError
39 from python_socks._types import ProxyType
40 from python_socks.sync import Proxy
42 HAVE_PYTHON_SOCKS = True
43except:
44 HAVE_PYTHON_SOCKS = False
46 class ProxyError(Exception):
47 pass
49 class ProxyTimeoutError(Exception):
50 pass
52 class ProxyConnectionError(Exception):
53 pass
56class proxy_info:
57 def __init__(self, **options):
58 self.proxy_host = options.get("http_proxy_host", None)
59 if self.proxy_host:
60 self.proxy_port = options.get("http_proxy_port", 0)
61 self.auth = options.get("http_proxy_auth", None)
62 self.no_proxy = options.get("http_no_proxy", None)
63 self.proxy_protocol = options.get("proxy_type", "http")
64 # Note: If timeout not specified, default python-socks timeout is 60 seconds
65 self.proxy_timeout = options.get("http_proxy_timeout", None)
66 if self.proxy_protocol not in [
67 "http",
68 "socks4",
69 "socks4a",
70 "socks5",
71 "socks5h",
72 ]:
73 raise ProxyError(
74 "Only http, socks4, socks5 proxy protocols are supported"
75 )
76 else:
77 self.proxy_port = 0
78 self.auth = None
79 self.no_proxy = None
80 self.proxy_protocol = "http"
83def _start_proxied_socket(url: str, options, proxy) -> tuple:
84 if not HAVE_PYTHON_SOCKS:
85 raise WebSocketException(
86 "Python Socks is needed for SOCKS proxying but is not available"
87 )
89 hostname, port, resource, is_secure = parse_url(url)
91 if proxy.proxy_protocol == "socks4":
92 rdns = False
93 proxy_type = ProxyType.SOCKS4
94 # socks4a sends DNS through proxy
95 elif proxy.proxy_protocol == "socks4a":
96 rdns = True
97 proxy_type = ProxyType.SOCKS4
98 elif proxy.proxy_protocol == "socks5":
99 rdns = False
100 proxy_type = ProxyType.SOCKS5
101 # socks5h sends DNS through proxy
102 elif proxy.proxy_protocol == "socks5h":
103 rdns = True
104 proxy_type = ProxyType.SOCKS5
106 ws_proxy = Proxy.create(
107 proxy_type=proxy_type,
108 host=proxy.proxy_host,
109 port=int(proxy.proxy_port),
110 username=proxy.auth[0] if proxy.auth else None,
111 password=proxy.auth[1] if proxy.auth else None,
112 rdns=rdns,
113 )
115 sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout)
117 if is_secure:
118 if HAVE_SSL:
119 sock = _ssl_socket(sock, options.sslopt, hostname)
120 else:
121 raise WebSocketException("SSL not available.")
123 return sock, (hostname, port, resource)
126def connect(url: str, options, proxy, socket):
127 # Use _start_proxied_socket() only for socks4 or socks5 proxy
128 # Use _tunnel() for http proxy
129 # TODO: Use python-socks for http protocol also, to standardize flow
130 if proxy.proxy_host and not socket and proxy.proxy_protocol != "http":
131 return _start_proxied_socket(url, options, proxy)
133 hostname, port_from_url, resource, is_secure = parse_url(url)
135 if socket:
136 return socket, (hostname, port_from_url, resource)
138 addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
139 hostname, port_from_url, is_secure, proxy
140 )
141 if not addrinfo_list:
142 raise WebSocketException(f"Host not found.: {hostname}:{port_from_url}")
144 sock = None
145 try:
146 sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
147 if need_tunnel:
148 sock = _tunnel(sock, hostname, port_from_url, auth)
150 if is_secure:
151 if HAVE_SSL:
152 sock = _ssl_socket(sock, options.sslopt, hostname)
153 else:
154 raise WebSocketException("SSL not available.")
156 return sock, (hostname, port_from_url, resource)
157 except:
158 if sock:
159 sock.close()
160 raise
163def _get_addrinfo_list(hostname, port: int, is_secure: bool, proxy) -> tuple:
164 phost, pport, pauth = get_proxy_info(
165 hostname,
166 is_secure,
167 proxy.proxy_host,
168 proxy.proxy_port,
169 proxy.auth,
170 proxy.no_proxy,
171 )
172 try:
173 # when running on windows 10, getaddrinfo without socktype returns a socktype 0.
174 # This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
175 # or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
176 if not phost:
177 addrinfo_list = socket.getaddrinfo(
178 hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP
179 )
180 return addrinfo_list, False, None
181 else:
182 pport = pport and pport or 80
183 # when running on windows 10, the getaddrinfo used above
184 # returns a socktype 0. This generates an error exception:
185 # _on_error: exception Socket type must be stream or datagram, not 0
186 # Force the socket type to SOCK_STREAM
187 addrinfo_list = socket.getaddrinfo(
188 phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP
189 )
190 return addrinfo_list, True, pauth
191 except socket.gaierror as e:
192 raise WebSocketAddressException(e)
195def _open_socket(addrinfo_list, sockopt, timeout):
196 err = None
197 for addrinfo in addrinfo_list:
198 family, socktype, proto = addrinfo[:3]
199 sock = socket.socket(family, socktype, proto)
200 sock.settimeout(timeout)
201 for opts in DEFAULT_SOCKET_OPTION:
202 sock.setsockopt(*opts)
203 for opts in sockopt:
204 sock.setsockopt(*opts)
206 address = addrinfo[4]
207 err = None
208 while not err:
209 try:
210 sock.connect(address)
211 except socket.error as error:
212 sock.close()
213 error.remote_ip = str(address[0])
214 try:
215 eConnRefused = (
216 errno.ECONNREFUSED,
217 errno.WSAECONNREFUSED,
218 errno.ENETUNREACH,
219 )
220 except AttributeError:
221 eConnRefused = (errno.ECONNREFUSED, errno.ENETUNREACH)
222 if error.errno not in eConnRefused:
223 raise error
224 err = error
225 continue
226 else:
227 break
228 else:
229 continue
230 break
231 else:
232 if err:
233 raise err
235 return sock
238def _wrap_sni_socket(sock: socket.socket, sslopt: dict, hostname, check_hostname):
239 context = sslopt.get("context", None)
240 if not context:
241 context = ssl.SSLContext(sslopt.get("ssl_version", ssl.PROTOCOL_TLS_CLIENT))
242 # Non default context need to manually enable SSLKEYLOGFILE support by setting the keylog_filename attribute.
243 # For more details see also:
244 # * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#context-creation
245 # * https://docs.python.org/3.8/library/ssl.html?highlight=sslkeylogfile#ssl.SSLContext.keylog_filename
246 keylog_file = os.environ.get("SSLKEYLOGFILE")
247 if keylog_file is not None:
248 context.keylog_filename = keylog_file
250 if sslopt.get("cert_reqs", ssl.CERT_NONE) != ssl.CERT_NONE:
251 cafile = sslopt.get("ca_certs", None)
252 capath = sslopt.get("ca_cert_path", None)
253 if cafile or capath:
254 try:
255 context.load_verify_locations(cafile=cafile, capath=capath)
256 except (FileNotFoundError, ssl.SSLError, ValueError) as e:
257 raise WebSocketException(f"SSL CA certificate loading failed: {e}")
258 elif hasattr(context, "load_default_certs"):
259 try:
260 context.load_default_certs(ssl.Purpose.SERVER_AUTH)
261 except ssl.SSLError as e:
262 raise WebSocketException(
263 f"SSL default certificate loading failed: {e}"
264 )
265 if sslopt.get("certfile", None):
266 try:
267 context.load_cert_chain(
268 sslopt["certfile"],
269 sslopt.get("keyfile", None),
270 sslopt.get("password", None),
271 )
272 except (FileNotFoundError, ValueError) as e:
273 raise WebSocketException(f"SSL client certificate loading failed: {e}")
274 except ssl.SSLError as e:
275 raise WebSocketException(f"SSL client certificate loading failed: {e}")
277 # Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True"
278 # If both disabled, set check_hostname before verify_mode
279 # see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
280 if sslopt.get("cert_reqs", ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get(
281 "check_hostname", False
282 ):
283 context.check_hostname = False
284 context.verify_mode = ssl.CERT_NONE
285 else:
286 context.check_hostname = sslopt.get("check_hostname", True)
287 context.verify_mode = sslopt.get("cert_reqs", ssl.CERT_REQUIRED)
289 if "ciphers" in sslopt:
290 try:
291 context.set_ciphers(sslopt["ciphers"])
292 except ssl.SSLError as e:
293 raise WebSocketException(f"SSL cipher configuration failed: {e}")
294 if "cert_chain" in sslopt:
295 try:
296 cert_chain = sslopt["cert_chain"]
297 if not isinstance(cert_chain, (tuple, list)) or len(cert_chain) != 3:
298 raise ValueError(
299 "cert_chain must be a tuple/list of (certfile, keyfile, password)"
300 )
301 certfile, keyfile, password = cert_chain
302 context.load_cert_chain(certfile, keyfile, password)
303 except ValueError:
304 raise
305 except (FileNotFoundError, ssl.SSLError) as e:
306 raise WebSocketException(
307 f"SSL client certificate configuration failed: {e}"
308 )
309 if "ecdh_curve" in sslopt:
310 try:
311 context.set_ecdh_curve(sslopt["ecdh_curve"])
312 except ValueError as e:
313 raise WebSocketException(f"SSL ECDH curve configuration failed: {e}")
315 return context.wrap_socket(
316 sock,
317 do_handshake_on_connect=sslopt.get("do_handshake_on_connect", True),
318 suppress_ragged_eofs=sslopt.get("suppress_ragged_eofs", True),
319 server_hostname=hostname,
320 )
323def _ssl_socket(sock: socket.socket, user_sslopt: dict, hostname):
324 sslopt: dict = {"cert_reqs": ssl.CERT_REQUIRED}
325 sslopt.update(user_sslopt)
327 cert_path = os.environ.get("WEBSOCKET_CLIENT_CA_BUNDLE")
328 if (
329 cert_path
330 and os.path.isfile(cert_path)
331 and user_sslopt.get("ca_certs", None) is None
332 ):
333 sslopt["ca_certs"] = cert_path
334 elif (
335 cert_path
336 and os.path.isdir(cert_path)
337 and user_sslopt.get("ca_cert_path", None) is None
338 ):
339 sslopt["ca_cert_path"] = cert_path
341 if sslopt.get("server_hostname", None):
342 hostname = sslopt["server_hostname"]
344 check_hostname = sslopt.get("check_hostname", True)
345 sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
347 return sock
350def _tunnel(sock: socket.socket, host, port: int, auth) -> socket.socket:
351 debug("Connecting proxy...")
352 connect_header = f"CONNECT {host}:{port} HTTP/1.1\r\n"
353 connect_header += f"Host: {host}:{port}\r\n"
355 # TODO: support digest auth.
356 if auth and auth[0]:
357 auth_str = auth[0]
358 if auth[1]:
359 auth_str += f":{auth[1]}"
360 encoded_str = base64encode(auth_str.encode()).strip().decode().replace("\n", "")
361 connect_header += f"Proxy-Authorization: Basic {encoded_str}\r\n"
362 connect_header += "\r\n"
363 dump("request header", connect_header)
365 send(sock, connect_header)
367 try:
368 status, _, _ = read_headers(sock)
369 except (socket.error, WebSocketException) as e:
370 raise WebSocketProxyException(str(e))
372 if status != 200:
373 raise WebSocketProxyException(f"failed CONNECT via proxy status: {status}")
375 return sock
378def read_headers(sock: socket.socket) -> tuple:
379 status = None
380 status_message = None
381 headers: dict = {}
382 trace("--- response header ---")
384 while True:
385 line = recv_line(sock)
386 line = line.decode("utf-8").strip()
387 if not line:
388 break
389 trace(line)
390 if not status:
391 status_info = line.split(" ", 2)
392 status = int(status_info[1])
393 if len(status_info) > 2:
394 status_message = status_info[2]
395 else:
396 kv = line.split(":", 1)
397 if len(kv) != 2:
398 raise WebSocketException("Invalid header")
399 key, value = kv
400 if key.lower() == "set-cookie" and headers.get("set-cookie"):
401 existing_cookie = headers.get("set-cookie")
402 if existing_cookie is not None:
403 headers["set-cookie"] = existing_cookie + "; " + value.strip()
404 else:
405 headers["set-cookie"] = value.strip()
406 else:
407 headers[key.lower()] = value.strip()
409 trace("-----------------------")
411 return status, headers, status_message