_http.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. """
  2. websocket - WebSocket client library for Python
  3. Copyright (C) 2010 Hiroki Ohtani(liris)
  4. This library is free software; you can redistribute it and/or
  5. modify it under the terms of the GNU Lesser General Public
  6. License as published by the Free Software Foundation; either
  7. version 2.1 of the License, or (at your option) any later version.
  8. This library is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. Lesser General Public License for more details.
  12. You should have received a copy of the GNU Lesser General Public
  13. License along with this library; if not, write to the Free Software
  14. Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  15. """
  16. import errno
  17. import os
  18. import socket
  19. import sys
  20. import six
  21. from ._exceptions import *
  22. from ._logging import *
  23. from ._socket import*
  24. from ._ssl_compat import *
  25. from ._url import *
  26. if six.PY3:
  27. from base64 import encodebytes as base64encode
  28. else:
  29. from base64 import encodestring as base64encode
  30. __all__ = ["proxy_info", "connect", "read_headers"]
  31. try:
  32. import socks
  33. ProxyConnectionError = socks.ProxyConnectionError
  34. HAS_PYSOCKS = True
  35. except:
  36. class ProxyConnectionError(BaseException):
  37. pass
  38. HAS_PYSOCKS = False
  39. class proxy_info(object):
  40. def __init__(self, **options):
  41. self.type = options.get("proxy_type") or "http"
  42. if not(self.type in ['http', 'socks4', 'socks5', 'socks5h']):
  43. raise ValueError("proxy_type must be 'http', 'socks4', 'socks5' or 'socks5h'")
  44. self.host = options.get("http_proxy_host", None)
  45. if self.host:
  46. self.port = options.get("http_proxy_port", 0)
  47. self.auth = options.get("http_proxy_auth", None)
  48. self.no_proxy = options.get("http_no_proxy", None)
  49. else:
  50. self.port = 0
  51. self.auth = None
  52. self.no_proxy = None
  53. def _open_proxied_socket(url, options, proxy):
  54. hostname, port, resource, is_secure = parse_url(url)
  55. if not HAS_PYSOCKS:
  56. raise WebSocketException("PySocks module not found.")
  57. ptype = socks.SOCKS5
  58. rdns = False
  59. if proxy.type == "socks4":
  60. ptype = socks.SOCKS4
  61. if proxy.type == "http":
  62. ptype = socks.HTTP
  63. if proxy.type[-1] == "h":
  64. rdns = True
  65. sock = socks.create_connection(
  66. (hostname, port),
  67. proxy_type=ptype,
  68. proxy_addr=proxy.host,
  69. proxy_port=proxy.port,
  70. proxy_rdns=rdns,
  71. proxy_username=proxy.auth[0] if proxy.auth else None,
  72. proxy_password=proxy.auth[1] if proxy.auth else None,
  73. timeout=options.timeout,
  74. socket_options=DEFAULT_SOCKET_OPTION + options.sockopt
  75. )
  76. if is_secure:
  77. if HAVE_SSL:
  78. sock = _ssl_socket(sock, options.sslopt, hostname)
  79. else:
  80. raise WebSocketException("SSL not available.")
  81. return sock, (hostname, port, resource)
  82. def connect(url, options, proxy, socket):
  83. if proxy.host and not socket and not (proxy.type == 'http'):
  84. return _open_proxied_socket(url, options, proxy)
  85. hostname, port, resource, is_secure = parse_url(url)
  86. if socket:
  87. return socket, (hostname, port, resource)
  88. addrinfo_list, need_tunnel, auth = _get_addrinfo_list(
  89. hostname, port, is_secure, proxy)
  90. if not addrinfo_list:
  91. raise WebSocketException(
  92. "Host not found.: " + hostname + ":" + str(port))
  93. sock = None
  94. try:
  95. sock = _open_socket(addrinfo_list, options.sockopt, options.timeout)
  96. if need_tunnel:
  97. sock = _tunnel(sock, hostname, port, auth)
  98. if is_secure:
  99. if HAVE_SSL:
  100. sock = _ssl_socket(sock, options.sslopt, hostname)
  101. else:
  102. raise WebSocketException("SSL not available.")
  103. return sock, (hostname, port, resource)
  104. except:
  105. if sock:
  106. sock.close()
  107. raise
  108. def _get_addrinfo_list(hostname, port, is_secure, proxy):
  109. phost, pport, pauth = get_proxy_info(
  110. hostname, is_secure, proxy.host, proxy.port, proxy.auth, proxy.no_proxy)
  111. try:
  112. # when running on windows 10, getaddrinfo without socktype returns a socktype 0.
  113. # This generates an error exception: `_on_error: exception Socket type must be stream or datagram, not 0`
  114. # or `OSError: [Errno 22] Invalid argument` when creating socket. Force the socket type to SOCK_STREAM.
  115. if not phost:
  116. addrinfo_list = socket.getaddrinfo(
  117. hostname, port, 0, socket.SOCK_STREAM, socket.SOL_TCP)
  118. return addrinfo_list, False, None
  119. else:
  120. pport = pport and pport or 80
  121. # when running on windows 10, the getaddrinfo used above
  122. # returns a socktype 0. This generates an error exception:
  123. # _on_error: exception Socket type must be stream or datagram, not 0
  124. # Force the socket type to SOCK_STREAM
  125. addrinfo_list = socket.getaddrinfo(phost, pport, 0, socket.SOCK_STREAM, socket.SOL_TCP)
  126. return addrinfo_list, True, pauth
  127. except socket.gaierror as e:
  128. raise WebSocketAddressException(e)
  129. def _open_socket(addrinfo_list, sockopt, timeout):
  130. err = None
  131. for addrinfo in addrinfo_list:
  132. family, socktype, proto = addrinfo[:3]
  133. sock = socket.socket(family, socktype, proto)
  134. sock.settimeout(timeout)
  135. for opts in DEFAULT_SOCKET_OPTION:
  136. sock.setsockopt(*opts)
  137. for opts in sockopt:
  138. sock.setsockopt(*opts)
  139. address = addrinfo[4]
  140. err = None
  141. while not err:
  142. try:
  143. sock.connect(address)
  144. except ProxyConnectionError as error:
  145. err = WebSocketProxyException(str(error))
  146. err.remote_ip = str(address[0])
  147. continue
  148. except socket.error as error:
  149. error.remote_ip = str(address[0])
  150. try:
  151. eConnRefused = (errno.ECONNREFUSED, errno.WSAECONNREFUSED)
  152. except:
  153. eConnRefused = (errno.ECONNREFUSED, )
  154. if error.errno == errno.EINTR:
  155. continue
  156. elif error.errno in eConnRefused:
  157. err = error
  158. continue
  159. else:
  160. raise error
  161. else:
  162. break
  163. else:
  164. continue
  165. break
  166. else:
  167. if err:
  168. raise err
  169. return sock
  170. def _can_use_sni():
  171. return six.PY2 and sys.version_info >= (2, 7, 9) or sys.version_info >= (3, 2)
  172. def _wrap_sni_socket(sock, sslopt, hostname, check_hostname):
  173. context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))
  174. if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
  175. cafile = sslopt.get('ca_certs', None)
  176. capath = sslopt.get('ca_cert_path', None)
  177. if cafile or capath:
  178. context.load_verify_locations(cafile=cafile, capath=capath)
  179. elif hasattr(context, 'load_default_certs'):
  180. context.load_default_certs(ssl.Purpose.SERVER_AUTH)
  181. if sslopt.get('certfile', None):
  182. context.load_cert_chain(
  183. sslopt['certfile'],
  184. sslopt.get('keyfile', None),
  185. sslopt.get('password', None),
  186. )
  187. # see
  188. # https://github.com/liris/websocket-client/commit/b96a2e8fa765753e82eea531adb19716b52ca3ca#commitcomment-10803153
  189. context.verify_mode = sslopt['cert_reqs']
  190. if HAVE_CONTEXT_CHECK_HOSTNAME:
  191. context.check_hostname = check_hostname
  192. if 'ciphers' in sslopt:
  193. context.set_ciphers(sslopt['ciphers'])
  194. if 'cert_chain' in sslopt:
  195. certfile, keyfile, password = sslopt['cert_chain']
  196. context.load_cert_chain(certfile, keyfile, password)
  197. if 'ecdh_curve' in sslopt:
  198. context.set_ecdh_curve(sslopt['ecdh_curve'])
  199. return context.wrap_socket(
  200. sock,
  201. do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
  202. suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
  203. server_hostname=hostname,
  204. )
  205. def _ssl_socket(sock, user_sslopt, hostname):
  206. sslopt = dict(cert_reqs=ssl.CERT_REQUIRED)
  207. sslopt.update(user_sslopt)
  208. certPath = os.environ.get('WEBSOCKET_CLIENT_CA_BUNDLE')
  209. if certPath and os.path.isfile(certPath) \
  210. and user_sslopt.get('ca_certs', None) is None \
  211. and user_sslopt.get('ca_cert', None) is None:
  212. sslopt['ca_certs'] = certPath
  213. elif certPath and os.path.isdir(certPath) \
  214. and user_sslopt.get('ca_cert_path', None) is None:
  215. sslopt['ca_cert_path'] = certPath
  216. check_hostname = sslopt["cert_reqs"] != ssl.CERT_NONE and sslopt.pop(
  217. 'check_hostname', True)
  218. if _can_use_sni():
  219. sock = _wrap_sni_socket(sock, sslopt, hostname, check_hostname)
  220. else:
  221. sslopt.pop('check_hostname', True)
  222. sock = ssl.wrap_socket(sock, **sslopt)
  223. if not HAVE_CONTEXT_CHECK_HOSTNAME and check_hostname:
  224. match_hostname(sock.getpeercert(), hostname)
  225. return sock
  226. def _tunnel(sock, host, port, auth):
  227. debug("Connecting proxy...")
  228. connect_header = "CONNECT %s:%d HTTP/1.1\r\n" % (host, port)
  229. connect_header += "Host: %s:%d\r\n" % (host, port)
  230. # TODO: support digest auth.
  231. if auth and auth[0]:
  232. auth_str = auth[0]
  233. if auth[1]:
  234. auth_str += ":" + auth[1]
  235. encoded_str = base64encode(auth_str.encode()).strip().decode().replace('\n', '')
  236. connect_header += "Proxy-Authorization: Basic %s\r\n" % encoded_str
  237. connect_header += "\r\n"
  238. dump("request header", connect_header)
  239. send(sock, connect_header)
  240. try:
  241. status, resp_headers, status_message = read_headers(sock)
  242. except Exception as e:
  243. raise WebSocketProxyException(str(e))
  244. if status != 200:
  245. raise WebSocketProxyException(
  246. "failed CONNECT via proxy status: %r" % status)
  247. return sock
  248. def read_headers(sock):
  249. status = None
  250. status_message = None
  251. headers = {}
  252. trace("--- response header ---")
  253. while True:
  254. line = recv_line(sock)
  255. line = line.decode('utf-8').strip()
  256. if not line:
  257. break
  258. trace(line)
  259. if not status:
  260. status_info = line.split(" ", 2)
  261. status = int(status_info[1])
  262. if len(status_info) > 2:
  263. status_message = status_info[2]
  264. else:
  265. kv = line.split(":", 1)
  266. if len(kv) == 2:
  267. key, value = kv
  268. if key.lower() == "set-cookie" and headers.get("set-cookie"):
  269. headers["set-cookie"] = headers.get("set-cookie") + "; " + value.strip()
  270. else:
  271. headers[key.lower()] = value.strip()
  272. else:
  273. raise WebSocketException("Invalid header")
  274. trace("-----------------------")
  275. return status, headers, status_message