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

1""" 

2_http.py 

3websocket - WebSocket client library for Python 

4 

5Copyright 2023 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""" 

19import errno 

20import os 

21import socket 

22 

23from ._exceptions import * 

24from ._logging import * 

25from ._socket import * 

26from ._ssl_compat import * 

27from ._url import * 

28 

29from base64 import encodebytes as base64encode 

30 

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

32 

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 

40 

41 class ProxyError(Exception): 

42 pass 

43 

44 class ProxyTimeoutError(Exception): 

45 pass 

46 

47 class ProxyConnectionError(Exception): 

48 pass 

49 

50 

51class proxy_info: 

52 

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" 

69 

70 

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

74 

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

76 

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 

90 

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) 

98 

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

100 

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

105 

106 return sock, (hostname, port, resource) 

107 

108 

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) 

115 

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

117 

118 if socket: 

119 return socket, (hostname, port_from_url, resource) 

120 

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

126 

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) 

132 

133 if is_secure: 

134 if HAVE_SSL: 

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

136 else: 

137 raise WebSocketException("SSL not available.") 

138 

139 return sock, (hostname, port_from_url, resource) 

140 except: 

141 if sock: 

142 sock.close() 

143 raise 

144 

145 

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) 

167 

168 

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) 

179 

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 

205 

206 return sock 

207 

208 

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) 

218 

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 ) 

232 

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) 

242 

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

250 

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 ) 

257 

258 

259def _ssl_socket(sock, user_sslopt, hostname): 

260 sslopt = dict(cert_reqs=ssl.CERT_REQUIRED) 

261 sslopt.update(user_sslopt) 

262 

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 

270 

271 if sslopt.get('server_hostname', None): 

272 hostname = sslopt['server_hostname'] 

273 

274 check_hostname = sslopt.get('check_hostname', True) 

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

276 

277 return sock 

278 

279 

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) 

284 

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) 

294 

295 send(sock, connect_header) 

296 

297 try: 

298 status, resp_headers, status_message = read_headers(sock) 

299 except Exception as e: 

300 raise WebSocketProxyException(str(e)) 

301 

302 if status != 200: 

303 raise WebSocketProxyException( 

304 "failed CONNECT via proxy status: {status}".format(status=status)) 

305 

306 return sock 

307 

308 

309def read_headers(sock): 

310 status = None 

311 status_message = None 

312 headers = {} 

313 trace("--- response header ---") 

314 

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: 

322 

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

337 

338 trace("-----------------------") 

339 

340 return status, headers, status_message