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

213 statements  

1""" 

2_http.py 

3websocket - WebSocket client library for Python 

4 

5Copyright 2024 engn33r 

6 

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 

10 

11 http://www.apache.org/licenses/LICENSE-2.0 

12 

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""" 

19 

20import errno 

21import os 

22import socket 

23from base64 import encodebytes as base64encode 

24 

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 

34 

35__all__ = ["proxy_info", "connect", "read_headers"] 

36 

37try: 

38 from python_socks._errors import * 

39 from python_socks._types import ProxyType 

40 from python_socks.sync import Proxy 

41 

42 HAVE_PYTHON_SOCKS = True 

43except: 

44 HAVE_PYTHON_SOCKS = False 

45 

46 class ProxyError(Exception): 

47 pass 

48 

49 class ProxyTimeoutError(Exception): 

50 pass 

51 

52 class ProxyConnectionError(Exception): 

53 pass 

54 

55 

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" 

81 

82 

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 ) 

88 

89 hostname, port, resource, is_secure = parse_url(url) 

90 

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 

105 

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 ) 

114 

115 sock = ws_proxy.connect(hostname, port, timeout=proxy.proxy_timeout) 

116 

117 if is_secure: 

118 if HAVE_SSL: 

119 sock = _ssl_socket(sock, options.sslopt, hostname) 

120 else: 

121 raise WebSocketException("SSL not available.") 

122 

123 return sock, (hostname, port, resource) 

124 

125 

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) 

132 

133 hostname, port_from_url, resource, is_secure = parse_url(url) 

134 

135 if socket: 

136 return socket, (hostname, port_from_url, resource) 

137 

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}") 

143 

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) 

149 

150 if is_secure: 

151 if HAVE_SSL: 

152 sock = _ssl_socket(sock, options.sslopt, hostname) 

153 else: 

154 raise WebSocketException("SSL not available.") 

155 

156 return sock, (hostname, port_from_url, resource) 

157 except: 

158 if sock: 

159 sock.close() 

160 raise 

161 

162 

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) 

193 

194 

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) 

205 

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 

234 

235 return sock 

236 

237 

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) 

247 

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 ) 

261 

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) 

273 

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"]) 

281 

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 ) 

288 

289 

290def _ssl_socket(sock: socket.socket, user_sslopt: dict, hostname): 

291 sslopt: dict = {"cert_reqs": ssl.CERT_REQUIRED} 

292 sslopt.update(user_sslopt) 

293 

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 

307 

308 if sslopt.get("server_hostname", None): 

309 hostname = sslopt["server_hostname"] 

310 

311 check_hostname = sslopt.get("check_hostname", True) 

312 sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname) 

313 

314 return sock 

315 

316 

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" 

321 

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) 

331 

332 send(sock, connect_header) 

333 

334 try: 

335 status, _, _ = read_headers(sock) 

336 except Exception as e: 

337 raise WebSocketProxyException(str(e)) 

338 

339 if status != 200: 

340 raise WebSocketProxyException(f"failed CONNECT via proxy status: {status}") 

341 

342 return sock 

343 

344 

345def read_headers(sock: socket.socket) -> tuple: 

346 status = None 

347 status_message = None 

348 headers: dict = {} 

349 trace("--- response header ---") 

350 

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() 

371 

372 trace("-----------------------") 

373 

374 return status, headers, status_message