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

1""" 

2_http.py 

3websocket - WebSocket client library for Python 

4 

5Copyright 2022 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 

22import sys 

23 

24from ._exceptions import * 

25from ._logging import * 

26from ._socket import * 

27from ._ssl_compat import * 

28from ._url import * 

29 

30from base64 import encodebytes as base64encode 

31 

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

33 

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 

41 

42 class ProxyError(Exception): 

43 pass 

44 

45 class ProxyTimeoutError(Exception): 

46 pass 

47 

48 class ProxyConnectionError(Exception): 

49 pass 

50 

51 

52class proxy_info: 

53 

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" 

70 

71 

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

75 

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

77 

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 

91 

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) 

99 

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

101 

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

106 

107 return sock, (hostname, port, resource) 

108 

109 

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) 

116 

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

118 

119 if socket: 

120 return socket, (hostname, port_from_url, resource) 

121 

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

127 

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) 

133 

134 if is_secure: 

135 if HAVE_SSL: 

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

137 else: 

138 raise WebSocketException("SSL not available.") 

139 

140 return sock, (hostname, port_from_url, resource) 

141 except: 

142 if sock: 

143 sock.close() 

144 raise 

145 

146 

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) 

168 

169 

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) 

180 

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 

206 

207 return sock 

208 

209 

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

214 

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 ) 

228 

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) 

238 

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

246 

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 ) 

253 

254 

255def _ssl_socket(sock, user_sslopt, hostname): 

256 sslopt = dict(cert_reqs=ssl.CERT_REQUIRED) 

257 sslopt.update(user_sslopt) 

258 

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 

266 

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

268 hostname = sslopt['server_hostname'] 

269 

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

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

272 

273 return sock 

274 

275 

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) 

280 

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) 

290 

291 send(sock, connect_header) 

292 

293 try: 

294 status, resp_headers, status_message = read_headers(sock) 

295 except Exception as e: 

296 raise WebSocketProxyException(str(e)) 

297 

298 if status != 200: 

299 raise WebSocketProxyException( 

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

301 

302 return sock 

303 

304 

305def read_headers(sock): 

306 status = None 

307 status_message = None 

308 headers = {} 

309 trace("--- response header ---") 

310 

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: 

318 

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

333 

334 trace("-----------------------") 

335 

336 return status, headers, status_message