test_http.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. # -*- coding: utf-8 -*-
  2. #
  3. import os
  4. import os.path
  5. import socket
  6. import ssl
  7. import unittest
  8. import websocket
  9. from websocket._exceptions import WebSocketProxyException, WebSocketException
  10. from websocket._http import (
  11. _get_addrinfo_list,
  12. _start_proxied_socket,
  13. _tunnel,
  14. connect,
  15. proxy_info,
  16. read_headers,
  17. HAVE_PYTHON_SOCKS,
  18. )
  19. """
  20. test_http.py
  21. websocket - WebSocket client library for Python
  22. Copyright 2024 engn33r
  23. Licensed under the Apache License, Version 2.0 (the "License");
  24. you may not use this file except in compliance with the License.
  25. You may obtain a copy of the License at
  26. http://www.apache.org/licenses/LICENSE-2.0
  27. Unless required by applicable law or agreed to in writing, software
  28. distributed under the License is distributed on an "AS IS" BASIS,
  29. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  30. See the License for the specific language governing permissions and
  31. limitations under the License.
  32. """
  33. try:
  34. from python_socks._errors import ProxyConnectionError, ProxyError, ProxyTimeoutError
  35. except:
  36. from websocket._http import ProxyConnectionError, ProxyError, ProxyTimeoutError
  37. # Skip test to access the internet unless TEST_WITH_INTERNET == 1
  38. TEST_WITH_INTERNET = os.environ.get("TEST_WITH_INTERNET", "0") == "1"
  39. TEST_WITH_PROXY = os.environ.get("TEST_WITH_PROXY", "0") == "1"
  40. # Skip tests relying on local websockets server unless LOCAL_WS_SERVER_PORT != -1
  41. LOCAL_WS_SERVER_PORT = os.environ.get("LOCAL_WS_SERVER_PORT", "-1")
  42. TEST_WITH_LOCAL_SERVER = LOCAL_WS_SERVER_PORT != "-1"
  43. class SockMock:
  44. def __init__(self):
  45. self.data = []
  46. self.sent = []
  47. def add_packet(self, data):
  48. self.data.append(data)
  49. def gettimeout(self):
  50. return None
  51. def recv(self, bufsize):
  52. if self.data:
  53. e = self.data.pop(0)
  54. if isinstance(e, Exception):
  55. raise e
  56. if len(e) > bufsize:
  57. self.data.insert(0, e[bufsize:])
  58. return e[:bufsize]
  59. def send(self, data):
  60. self.sent.append(data)
  61. return len(data)
  62. def close(self):
  63. pass
  64. class HeaderSockMock(SockMock):
  65. def __init__(self, fname):
  66. SockMock.__init__(self)
  67. import yatest.common as yc
  68. path = os.path.join(os.path.dirname(yc.source_path(__file__)), fname)
  69. with open(path, "rb") as f:
  70. self.add_packet(f.read())
  71. class OptsList:
  72. def __init__(self):
  73. self.timeout = 1
  74. self.sockopt = []
  75. self.sslopt = {"cert_reqs": ssl.CERT_NONE}
  76. class HttpTest(unittest.TestCase):
  77. def test_read_header(self):
  78. status, header, _ = read_headers(HeaderSockMock("data/header01.txt"))
  79. self.assertEqual(status, 101)
  80. self.assertEqual(header["connection"], "Upgrade")
  81. # header02.txt is intentionally malformed
  82. self.assertRaises(
  83. WebSocketException, read_headers, HeaderSockMock("data/header02.txt")
  84. )
  85. def test_tunnel(self):
  86. self.assertRaises(
  87. WebSocketProxyException,
  88. _tunnel,
  89. HeaderSockMock("data/header01.txt"),
  90. "example.com",
  91. 80,
  92. ("username", "password"),
  93. )
  94. self.assertRaises(
  95. WebSocketProxyException,
  96. _tunnel,
  97. HeaderSockMock("data/header02.txt"),
  98. "example.com",
  99. 80,
  100. ("username", "password"),
  101. )
  102. @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
  103. def test_connect(self):
  104. # Not currently testing an actual proxy connection, so just check whether proxy errors are raised. This requires internet for a DNS lookup
  105. if HAVE_PYTHON_SOCKS:
  106. # Need this check, otherwise case where python_socks is not installed triggers
  107. # websocket._exceptions.WebSocketException: Python Socks is needed for SOCKS proxying but is not available
  108. self.assertRaises(
  109. (ProxyTimeoutError, OSError),
  110. _start_proxied_socket,
  111. "wss://example.com",
  112. OptsList(),
  113. proxy_info(
  114. http_proxy_host="example.com",
  115. http_proxy_port="8080",
  116. proxy_type="socks4",
  117. http_proxy_timeout=1,
  118. ),
  119. )
  120. self.assertRaises(
  121. (ProxyTimeoutError, OSError),
  122. _start_proxied_socket,
  123. "wss://example.com",
  124. OptsList(),
  125. proxy_info(
  126. http_proxy_host="example.com",
  127. http_proxy_port="8080",
  128. proxy_type="socks4a",
  129. http_proxy_timeout=1,
  130. ),
  131. )
  132. self.assertRaises(
  133. (ProxyTimeoutError, OSError),
  134. _start_proxied_socket,
  135. "wss://example.com",
  136. OptsList(),
  137. proxy_info(
  138. http_proxy_host="example.com",
  139. http_proxy_port="8080",
  140. proxy_type="socks5",
  141. http_proxy_timeout=1,
  142. ),
  143. )
  144. self.assertRaises(
  145. (ProxyTimeoutError, OSError),
  146. _start_proxied_socket,
  147. "wss://example.com",
  148. OptsList(),
  149. proxy_info(
  150. http_proxy_host="example.com",
  151. http_proxy_port="8080",
  152. proxy_type="socks5h",
  153. http_proxy_timeout=1,
  154. ),
  155. )
  156. self.assertRaises(
  157. ProxyConnectionError,
  158. connect,
  159. "wss://example.com",
  160. OptsList(),
  161. proxy_info(
  162. http_proxy_host="127.0.0.1",
  163. http_proxy_port=9999,
  164. proxy_type="socks4",
  165. http_proxy_timeout=1,
  166. ),
  167. None,
  168. )
  169. self.assertRaises(
  170. TypeError,
  171. _get_addrinfo_list,
  172. None,
  173. 80,
  174. True,
  175. proxy_info(
  176. http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http"
  177. ),
  178. )
  179. self.assertRaises(
  180. TypeError,
  181. _get_addrinfo_list,
  182. None,
  183. 80,
  184. True,
  185. proxy_info(
  186. http_proxy_host="127.0.0.1", http_proxy_port="9999", proxy_type="http"
  187. ),
  188. )
  189. self.assertRaises(
  190. socket.timeout,
  191. connect,
  192. "wss://google.com",
  193. OptsList(),
  194. proxy_info(
  195. http_proxy_host="8.8.8.8",
  196. http_proxy_port=9999,
  197. proxy_type="http",
  198. http_proxy_timeout=1,
  199. ),
  200. None,
  201. )
  202. self.assertEqual(
  203. connect(
  204. "wss://google.com",
  205. OptsList(),
  206. proxy_info(
  207. http_proxy_host="8.8.8.8", http_proxy_port=8080, proxy_type="http"
  208. ),
  209. True,
  210. ),
  211. (True, ("google.com", 443, "/")),
  212. )
  213. # The following test fails on Mac OS with a gaierror, not an OverflowError
  214. # self.assertRaises(OverflowError, connect, "wss://example.com", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port=99999, proxy_type="socks4", timeout=2), False)
  215. @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
  216. @unittest.skipUnless(
  217. TEST_WITH_PROXY, "This test requires a HTTP proxy to be running on port 8899"
  218. )
  219. @unittest.skipUnless(
  220. TEST_WITH_LOCAL_SERVER, "Tests using local websocket server are disabled"
  221. )
  222. def test_proxy_connect(self):
  223. ws = websocket.WebSocket()
  224. ws.connect(
  225. f"ws://127.0.0.1:{LOCAL_WS_SERVER_PORT}",
  226. http_proxy_host="127.0.0.1",
  227. http_proxy_port="8899",
  228. proxy_type="http",
  229. )
  230. ws.send("Hello, Server")
  231. server_response = ws.recv()
  232. self.assertEqual(server_response, "Hello, Server")
  233. # self.assertEqual(_start_proxied_socket("wss://api.bitfinex.com/ws/2", OptsList(), proxy_info(http_proxy_host="127.0.0.1", http_proxy_port="8899", proxy_type="http"))[1], ("api.bitfinex.com", 443, '/ws/2'))
  234. self.assertEqual(
  235. _get_addrinfo_list(
  236. "api.bitfinex.com",
  237. 443,
  238. True,
  239. proxy_info(
  240. http_proxy_host="127.0.0.1",
  241. http_proxy_port="8899",
  242. proxy_type="http",
  243. ),
  244. ),
  245. (
  246. socket.getaddrinfo(
  247. "127.0.0.1", 8899, 0, socket.SOCK_STREAM, socket.SOL_TCP
  248. ),
  249. True,
  250. None,
  251. ),
  252. )
  253. self.assertEqual(
  254. connect(
  255. "wss://api.bitfinex.com/ws/2",
  256. OptsList(),
  257. proxy_info(
  258. http_proxy_host="127.0.0.1", http_proxy_port=8899, proxy_type="http"
  259. ),
  260. None,
  261. )[1],
  262. ("api.bitfinex.com", 443, "/ws/2"),
  263. )
  264. # TODO: Test SOCKS4 and SOCK5 proxies with unit tests
  265. @unittest.skipUnless(TEST_WITH_INTERNET, "Internet-requiring tests are disabled")
  266. def test_sslopt(self):
  267. ssloptions = {
  268. "check_hostname": False,
  269. "server_hostname": "ServerName",
  270. "ssl_version": ssl.PROTOCOL_TLS_CLIENT,
  271. "ciphers": "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:\
  272. TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:\
  273. ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:\
  274. ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:\
  275. DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:\
  276. ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:\
  277. ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:\
  278. DHE-RSA-AES256-SHA256:ECDHE-ECDSA-AES128-SHA256:\
  279. ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:\
  280. ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA",
  281. "ecdh_curve": "prime256v1",
  282. }
  283. ws_ssl1 = websocket.WebSocket(sslopt=ssloptions)
  284. ws_ssl1.connect("wss://api.bitfinex.com/ws/2")
  285. ws_ssl1.send("Hello")
  286. ws_ssl1.close()
  287. ws_ssl2 = websocket.WebSocket(sslopt={"check_hostname": True})
  288. ws_ssl2.connect("wss://api.bitfinex.com/ws/2")
  289. ws_ssl2.close
  290. def test_proxy_info(self):
  291. self.assertEqual(
  292. proxy_info(
  293. http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http"
  294. ).proxy_protocol,
  295. "http",
  296. )
  297. self.assertRaises(
  298. ProxyError,
  299. proxy_info,
  300. http_proxy_host="127.0.0.1",
  301. http_proxy_port="8080",
  302. proxy_type="badval",
  303. )
  304. self.assertEqual(
  305. proxy_info(
  306. http_proxy_host="example.com", http_proxy_port="8080", proxy_type="http"
  307. ).proxy_host,
  308. "example.com",
  309. )
  310. self.assertEqual(
  311. proxy_info(
  312. http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http"
  313. ).proxy_port,
  314. "8080",
  315. )
  316. self.assertEqual(
  317. proxy_info(
  318. http_proxy_host="127.0.0.1", http_proxy_port="8080", proxy_type="http"
  319. ).auth,
  320. None,
  321. )
  322. self.assertEqual(
  323. proxy_info(
  324. http_proxy_host="127.0.0.1",
  325. http_proxy_port="8080",
  326. proxy_type="http",
  327. http_proxy_auth=("my_username123", "my_pass321"),
  328. ).auth[0],
  329. "my_username123",
  330. )
  331. self.assertEqual(
  332. proxy_info(
  333. http_proxy_host="127.0.0.1",
  334. http_proxy_port="8080",
  335. proxy_type="http",
  336. http_proxy_auth=("my_username123", "my_pass321"),
  337. ).auth[1],
  338. "my_pass321",
  339. )
  340. if __name__ == "__main__":
  341. unittest.main()