_http.py 10 KB

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