|
| 1 | +import ssl |
| 2 | +from datetime import datetime, timedelta |
| 3 | +import inspect |
| 4 | +import os |
| 5 | +import shutil |
| 6 | +import subprocess |
| 7 | +import time |
| 8 | + |
| 9 | +import pytest |
| 10 | +import websockets |
| 11 | +from websockets.sync.client import connect |
| 12 | + |
| 13 | +from .conf import TlsTestConf |
| 14 | + |
| 15 | + |
| 16 | +def mk_text_file(fpath: str, lines: int): |
| 17 | + t110 = 11 * "0123456789" |
| 18 | + with open(fpath, "w") as fd: |
| 19 | + for i in range(lines): |
| 20 | + fd.write("{0:015d}: ".format(i)) # total 128 bytes per line |
| 21 | + fd.write(t110) |
| 22 | + fd.write("\n") |
| 23 | + |
| 24 | + |
| 25 | +class TestWebSockets: |
| 26 | + |
| 27 | + @pytest.fixture(autouse=True, scope='class') |
| 28 | + def _class_scope(self, env): |
| 29 | + # Apache config that CONNECT proxies a WebSocket server for paths starting |
| 30 | + # with '/ws/' |
| 31 | + # The WebSocket server is started in pytest fixture 'ws_server' below. |
| 32 | + conf = TlsTestConf(env, extras={ |
| 33 | + 'base': [ |
| 34 | + 'Timeout 1', |
| 35 | + ], |
| 36 | + 'localhost': [ |
| 37 | + f'ProxyPass /ws/ http://127.0.0.1:{env.ws_port}/ upgrade=websocket \\', |
| 38 | + f'timeout=2 flushpackets=on', |
| 39 | + ], |
| 40 | + f'cgi.{env.http_tld}': [ |
| 41 | + f' ProxyPass /ws/ http://127.0.0.1:{env.ws_port}/ \\', |
| 42 | + f' upgrade=websocket timeout=2 flushpackets=on', |
| 43 | + f' ReadBufferSize 65535' |
| 44 | + ] |
| 45 | + }) |
| 46 | + conf.add_vhost('localhost', port=env.http_port) |
| 47 | + conf.add_tls_vhosts(['localhost'], port=env.https_port) |
| 48 | + conf.install() |
| 49 | + mk_text_file(os.path.join(env.gen_dir, "1k.txt"), 8) |
| 50 | + mk_text_file(os.path.join(env.gen_dir, "10k.txt"), 80) |
| 51 | + mk_text_file(os.path.join(env.gen_dir, "100k.txt"), 800) |
| 52 | + mk_text_file(os.path.join(env.gen_dir, "1m.txt"), 8000) |
| 53 | + mk_text_file(os.path.join(env.gen_dir, "10m.txt"), 80000) |
| 54 | + assert env.apache_restart() == 0 |
| 55 | + |
| 56 | + def ws_check_alive(self, env, timeout=5): |
| 57 | + url = f'http://localhost:{env.ws_port}/' |
| 58 | + end = datetime.now() + timedelta(seconds=timeout) |
| 59 | + while datetime.now() < end: |
| 60 | + r = env.curl_get(url, 5) |
| 61 | + if r.exit_code == 0: |
| 62 | + return True |
| 63 | + time.sleep(.1) |
| 64 | + return False |
| 65 | + |
| 66 | + def _mkpath(self, path): |
| 67 | + if not os.path.exists(path): |
| 68 | + return os.makedirs(path) |
| 69 | + |
| 70 | + def _rmrf(self, path): |
| 71 | + if os.path.exists(path): |
| 72 | + return shutil.rmtree(path) |
| 73 | + |
| 74 | + def ws_recv_text(self, ws): |
| 75 | + msg = "" |
| 76 | + while True: |
| 77 | + try: |
| 78 | + msg += ws.recv() |
| 79 | + except websockets.exceptions.ConnectionClosedOK: |
| 80 | + return msg |
| 81 | + |
| 82 | + def ws_recv_bytes(self, ws): |
| 83 | + msg = b'' |
| 84 | + while True: |
| 85 | + try: |
| 86 | + msg += ws.recv() |
| 87 | + except websockets.exceptions.ConnectionClosedOK: |
| 88 | + return msg |
| 89 | + |
| 90 | + @pytest.fixture(autouse=True, scope='class') |
| 91 | + def ws_server(self, env): |
| 92 | + # Run our python websockets server that has some special behaviour |
| 93 | + # for the different path to CONNECT to. |
| 94 | + run_dir = os.path.join(env.gen_dir, 'ws-server') |
| 95 | + err_file = os.path.join(run_dir, 'stderr') |
| 96 | + self._rmrf(run_dir) |
| 97 | + self._mkpath(run_dir) |
| 98 | + with open(err_file, 'w') as cerr: |
| 99 | + cmd = os.path.join(os.path.dirname(inspect.getfile(TestWebSockets)), |
| 100 | + 'ws_server.py') |
| 101 | + args = ['python3', cmd, '--port', str(env.ws_port)] |
| 102 | + p = subprocess.Popen(args=args, cwd=run_dir, stderr=cerr, |
| 103 | + stdout=cerr) |
| 104 | + if not self.ws_check_alive(env): |
| 105 | + p.kill() |
| 106 | + p.wait() |
| 107 | + pytest.fail(f'ws_server did not start. stderr={open(err_file).readlines()}') |
| 108 | + yield |
| 109 | + p.terminate() |
| 110 | + |
| 111 | + def test_tls_18_01_direct(self, env): |
| 112 | + with connect(f"ws://127.0.0.1:{env.ws_port}/echo") as ws: |
| 113 | + message = "Hello world!" |
| 114 | + ws.send(message) |
| 115 | + response = self.ws_recv_text(ws) |
| 116 | + assert response == message |
| 117 | + |
| 118 | + def test_tls_18_02_httpd_plain(self, env): |
| 119 | + with connect(f"ws://localhost:{env.http_port}/ws/echo/") as ws: |
| 120 | + message = "Hello world!" |
| 121 | + ws.send(message) |
| 122 | + response = self.ws_recv_text(ws) |
| 123 | + assert response == message |
| 124 | + |
| 125 | + @pytest.mark.parametrize("fname", ["1k.txt", "10k.txt", "100k.txt", "1m.txt", "10m.txt"]) |
| 126 | + def test_tls_18_03_file(self, env, fname): |
| 127 | + expected = open(os.path.join(env.gen_dir, fname), 'rb').read() |
| 128 | + with connect(f"ws://localhost:{env.http_port}/ws/file/{fname}") as ws: |
| 129 | + response = self.ws_recv_bytes(ws) |
| 130 | + assert response == expected |
| 131 | + |
| 132 | + @pytest.mark.parametrize("fname", ["1k.txt", "10k.txt", "100k.txt", "1m.txt", "10m.txt"]) |
| 133 | + def test_tls_18_04_tls_file(self, env, fname): |
| 134 | + expected = open(os.path.join(env.gen_dir, fname), 'rb').read() |
| 135 | + ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT) |
| 136 | + ssl_ctx.check_hostname = False |
| 137 | + ssl_ctx.verify_mode = ssl.VerifyMode.CERT_NONE |
| 138 | + with connect(f"wss://localhost:{env.https_port}/ws/file/{fname}", |
| 139 | + ssl_context=ssl_ctx) as ws: |
| 140 | + response = self.ws_recv_bytes(ws) |
| 141 | + assert response == expected |
0 commit comments