Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/websocket/_http.py: 35%
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 2024 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 *
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 context.keylog_filename = os.environ.get("SSLKEYLOGFILE", None)
248 if sslopt.get("cert_reqs", ssl.CERT_NONE) != ssl.CERT_NONE:
249 cafile = sslopt.get("ca_certs", None)
250 capath = sslopt.get("ca_cert_path", None)
251 if cafile or capath:
252 context.load_verify_locations(cafile=cafile, capath=capath)
253 elif hasattr(context, "load_default_certs"):
254 context.load_default_certs(ssl.Purpose.SERVER_AUTH)
255 if sslopt.get("certfile", None):
256 context.load_cert_chain(
257 sslopt["certfile"],
258 sslopt.get("keyfile", None),
259 sslopt.get("password", None),
260 )
262 # Python 3.10 switch to PROTOCOL_TLS_CLIENT defaults to "cert_reqs = ssl.CERT_REQUIRED" and "check_hostname = True"
263 # If both disabled, set check_hostname before verify_mode
264 # see https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
265 if sslopt.get("cert_reqs", ssl.CERT_NONE) == ssl.CERT_NONE and not sslopt.get(
266 "check_hostname", False
267 ):
268 context.check_hostname = False
269 context.verify_mode = ssl.CERT_NONE
270 else:
271 context.check_hostname = sslopt.get("check_hostname", True)
272 context.verify_mode = sslopt.get("cert_reqs", ssl.CERT_REQUIRED)
274 if "ciphers" in sslopt:
275 context.set_ciphers(sslopt["ciphers"])
276 if "cert_chain" in sslopt:
277 certfile, keyfile, password = sslopt["cert_chain"]
278 context.load_cert_chain(certfile, keyfile, password)
279 if "ecdh_curve" in sslopt:
280 context.set_ecdh_curve(sslopt["ecdh_curve"])
282 return context.wrap_socket(
283 sock,
284 do_handshake_on_connect=sslopt.get("do_handshake_on_connect", True),
285 suppress_ragged_eofs=sslopt.get("suppress_ragged_eofs", True),
286 server_hostname=hostname,
287 )
290def _ssl_socket(sock: socket.socket, user_sslopt: dict, hostname):
291 sslopt: dict = {"cert_reqs": ssl.CERT_REQUIRED}
292 sslopt.update(user_sslopt)
294 cert_path = os.environ.get("WEBSOCKET_CLIENT_CA_BUNDLE")
295 if (
296 cert_path
297 and os.path.isfile(cert_path)
298 and user_sslopt.get("ca_certs", None) is None
299 ):
300 sslopt["ca_certs"] = cert_path
301 elif (
302 cert_path
303 and os.path.isdir(cert_path)
304 and user_sslopt.get("ca_cert_path", None) is None
305 ):
306 sslopt["ca_cert_path"] = cert_path
308 if sslopt.get("server_hostname", None):
309 hostname = sslopt["server_hostname"]
311 check_hostname = sslopt.get("check_hostname", True)
312 sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
314 return sock
317def _tunnel(sock: socket.socket, host, port: int, auth) -> socket.socket:
318 debug("Connecting proxy...")
319 connect_header = f"CONNECT {host}:{port} HTTP/1.1\r\n"
320 connect_header += f"Host: {host}:{port}\r\n"
322 # TODO: support digest auth.
323 if auth and auth[0]:
324 auth_str = auth[0]
325 if auth[1]:
326 auth_str += f":{auth[1]}"
327 encoded_str = base64encode(auth_str.encode()).strip().decode().replace("\n", "")
328 connect_header += f"Proxy-Authorization: Basic {encoded_str}\r\n"
329 connect_header += "\r\n"
330 dump("request header", connect_header)
332 send(sock, connect_header)
334 try:
335 status, _, _ = read_headers(sock)
336 except Exception as e:
337 raise WebSocketProxyException(str(e))
339 if status != 200:
340 raise WebSocketProxyException(f"failed CONNECT via proxy status: {status}")
342 return sock
345def read_headers(sock: socket.socket) -> tuple:
346 status = None
347 status_message = None
348 headers: dict = {}
349 trace("--- response header ---")
351 while True:
352 line = recv_line(sock)
353 line = line.decode("utf-8").strip()
354 if not line:
355 break
356 trace(line)
357 if not status:
358 status_info = line.split(" ", 2)
359 status = int(status_info[1])
360 if len(status_info) > 2:
361 status_message = status_info[2]
362 else:
363 kv = line.split(":", 1)
364 if len(kv) != 2:
365 raise WebSocketException("Invalid header")
366 key, value = kv
367 if key.lower() == "set-cookie" and headers.get("set-cookie"):
368 headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
369 else:
370 headers[key.lower()] = value.strip()
372 trace("-----------------------")
374 return status, headers, status_message