diff --git a/test/modules/tls/env.py b/test/modules/tls/env.py
index 30910f0..8399d52 100644
--- a/test/modules/tls/env.py
+++ b/test/modules/tls/env.py
@@ -107,18 +107,6 @@ def __init__(self, pytestconfig=None):
' AddHandler cgi-script .py',
' Options +ExecCGI',
'',
- f'',
- ' ServerName localhost',
- ' DocumentRoot "htdocs"',
- '',
- f'',
- f' ServerName {self.domain_a}',
- ' DocumentRoot "htdocs/a.mod-tls.test"',
- '',
- f'',
- f' ServerName {self.domain_b}',
- ' DocumentRoot "htdocs/b.mod-tls.test"',
- '',
])
self.add_cert_specs([
CertificateSpec(domains=[self.domain_a]),
diff --git a/test/modules/tls/test_13_proxy.py b/test/modules/tls/test_13_proxy.py
index 42a1efb..6490c79 100644
--- a/test/modules/tls/test_13_proxy.py
+++ b/test/modules/tls/test_13_proxy.py
@@ -18,6 +18,8 @@ def _class_scope(self, env):
]
})
# add vhosts a+b and a ssl proxy from a to b
+ conf.add_vhost('localhost', port=env.http_port)
+ conf.add_vhost(env.domain_b, port=env.http_port, doc_root=f"htdocs/{env.domain_b}")
conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
conf.install()
assert env.apache_restart() == 0
diff --git a/test/modules/tls/test_18_ws.py b/test/modules/tls/test_18_ws.py
new file mode 100644
index 0000000..5ef90e8
--- /dev/null
+++ b/test/modules/tls/test_18_ws.py
@@ -0,0 +1,141 @@
+import ssl
+from datetime import datetime, timedelta
+import inspect
+import os
+import shutil
+import subprocess
+import time
+
+import pytest
+import websockets
+from websockets.sync.client import connect
+
+from .conf import TlsTestConf
+
+
+def mk_text_file(fpath: str, lines: int):
+ t110 = 11 * "0123456789"
+ with open(fpath, "w") as fd:
+ for i in range(lines):
+ fd.write("{0:015d}: ".format(i)) # total 128 bytes per line
+ fd.write(t110)
+ fd.write("\n")
+
+
+class TestWebSockets:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ # Apache config that CONNECT proxies a WebSocket server for paths starting
+ # with '/ws/'
+ # The WebSocket server is started in pytest fixture 'ws_server' below.
+ conf = TlsTestConf(env, extras={
+ 'base': [
+ 'Timeout 1',
+ ],
+ 'localhost': [
+ f'ProxyPass /ws/ http://127.0.0.1:{env.ws_port}/ upgrade=websocket \\',
+ f'timeout=2 flushpackets=on',
+ ],
+ f'cgi.{env.http_tld}': [
+ f' ProxyPass /ws/ http://127.0.0.1:{env.ws_port}/ \\',
+ f' upgrade=websocket timeout=2 flushpackets=on',
+ f' ReadBufferSize 65535'
+ ]
+ })
+ conf.add_vhost('localhost', port=env.http_port)
+ conf.add_tls_vhosts(['localhost'], port=env.https_port)
+ conf.install()
+ mk_text_file(os.path.join(env.gen_dir, "1k.txt"), 8)
+ mk_text_file(os.path.join(env.gen_dir, "10k.txt"), 80)
+ mk_text_file(os.path.join(env.gen_dir, "100k.txt"), 800)
+ mk_text_file(os.path.join(env.gen_dir, "1m.txt"), 8000)
+ mk_text_file(os.path.join(env.gen_dir, "10m.txt"), 80000)
+ assert env.apache_restart() == 0
+
+ def ws_check_alive(self, env, timeout=5):
+ url = f'http://localhost:{env.ws_port}/'
+ end = datetime.now() + timedelta(seconds=timeout)
+ while datetime.now() < end:
+ r = env.curl_get(url, 5)
+ if r.exit_code == 0:
+ return True
+ time.sleep(.1)
+ return False
+
+ def _mkpath(self, path):
+ if not os.path.exists(path):
+ return os.makedirs(path)
+
+ def _rmrf(self, path):
+ if os.path.exists(path):
+ return shutil.rmtree(path)
+
+ def ws_recv_text(self, ws):
+ msg = ""
+ while True:
+ try:
+ msg += ws.recv()
+ except websockets.exceptions.ConnectionClosedOK:
+ return msg
+
+ def ws_recv_bytes(self, ws):
+ msg = b''
+ while True:
+ try:
+ msg += ws.recv()
+ except websockets.exceptions.ConnectionClosedOK:
+ return msg
+
+ @pytest.fixture(autouse=True, scope='class')
+ def ws_server(self, env):
+ # Run our python websockets server that has some special behaviour
+ # for the different path to CONNECT to.
+ run_dir = os.path.join(env.gen_dir, 'ws-server')
+ err_file = os.path.join(run_dir, 'stderr')
+ self._rmrf(run_dir)
+ self._mkpath(run_dir)
+ with open(err_file, 'w') as cerr:
+ cmd = os.path.join(os.path.dirname(inspect.getfile(TestWebSockets)),
+ 'ws_server.py')
+ args = ['python3', cmd, '--port', str(env.ws_port)]
+ p = subprocess.Popen(args=args, cwd=run_dir, stderr=cerr,
+ stdout=cerr)
+ if not self.ws_check_alive(env):
+ p.kill()
+ p.wait()
+ pytest.fail(f'ws_server did not start. stderr={open(err_file).readlines()}')
+ yield
+ p.terminate()
+
+ def test_tls_18_01_direct(self, env):
+ with connect(f"ws://127.0.0.1:{env.ws_port}/echo") as ws:
+ message = "Hello world!"
+ ws.send(message)
+ response = self.ws_recv_text(ws)
+ assert response == message
+
+ def test_tls_18_02_httpd_plain(self, env):
+ with connect(f"ws://localhost:{env.http_port}/ws/echo/") as ws:
+ message = "Hello world!"
+ ws.send(message)
+ response = self.ws_recv_text(ws)
+ assert response == message
+
+ @pytest.mark.parametrize("fname", ["1k.txt", "10k.txt", "100k.txt", "1m.txt", "10m.txt"])
+ def test_tls_18_03_file(self, env, fname):
+ expected = open(os.path.join(env.gen_dir, fname), 'rb').read()
+ with connect(f"ws://localhost:{env.http_port}/ws/file/{fname}") as ws:
+ response = self.ws_recv_bytes(ws)
+ assert response == expected
+
+ @pytest.mark.parametrize("fname", ["1k.txt", "10k.txt", "100k.txt", "1m.txt", "10m.txt"])
+ def test_tls_18_04_tls_file(self, env, fname):
+ expected = open(os.path.join(env.gen_dir, fname), 'rb').read()
+ ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
+ ssl_ctx.check_hostname = False
+ ssl_ctx.verify_mode = ssl.VerifyMode.CERT_NONE
+ with connect(f"wss://localhost:{env.https_port}/ws/file/{fname}",
+ ssl_context=ssl_ctx) as ws:
+ response = self.ws_recv_bytes(ws)
+ assert response == expected
diff --git a/test/modules/tls/ws_server.py b/test/modules/tls/ws_server.py
new file mode 100755
index 0000000..99fb9cf
--- /dev/null
+++ b/test/modules/tls/ws_server.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+import argparse
+import asyncio
+import logging
+import os
+import sys
+import time
+
+import websockets.server as ws_server
+from websockets.exceptions import ConnectionClosedError
+
+log = logging.getLogger(__name__)
+
+logging.basicConfig(
+ format="[%(asctime)s] %(message)s",
+ level=logging.DEBUG,
+)
+
+
+async def echo(websocket):
+ try:
+ async for message in websocket:
+ try:
+ log.info(f'got request {message}')
+ except Exception as e:
+ log.error(f'error {e} getting path from {message}')
+ await websocket.send(message)
+ except ConnectionClosedError:
+ pass
+
+
+async def on_async_conn(conn):
+ rpath = str(conn.path)
+ pcomps = rpath[1:].split('/')
+ if len(pcomps) == 0:
+ pcomps = ['echo'] # default handler
+ log.info(f'connection for {pcomps}')
+ if pcomps[0] == 'echo':
+ log.info(f'/echo endpoint')
+ for message in await conn.recv():
+ await conn.send(message)
+ elif pcomps[0] == 'text':
+ await conn.send('hello!')
+ elif pcomps[0] == 'file':
+ if len(pcomps) < 2:
+ conn.close(code=4999, reason='unknown file')
+ return
+ fpath = os.path.join('../', pcomps[1])
+ if not os.path.exists(fpath):
+ conn.close(code=4999, reason='file not found')
+ return
+ bufsize = 0
+ if len(pcomps) > 2:
+ bufsize = int(pcomps[2])
+ if bufsize <= 0:
+ bufsize = 16*1024
+ delay_ms = 0
+ if len(pcomps) > 3:
+ delay_ms = int(pcomps[3])
+ n = 1
+ if len(pcomps) > 4:
+ n = int(pcomps[4])
+ for _ in range(n):
+ with open(fpath, 'r+b') as fd:
+ while True:
+ buf = fd.read(bufsize)
+ if buf is None or len(buf) == 0:
+ break
+ await conn.send(buf)
+ if delay_ms > 0:
+ time.sleep(delay_ms/1000)
+ else:
+ log.info(f'unknown endpoint: {rpath}')
+ await conn.close(code=4999, reason='path unknown')
+ await conn.close(code=1000, reason='')
+
+
+async def run_server(port):
+ log.info(f'starting server on port {port}')
+ async with ws_server.serve(ws_handler=on_async_conn,
+ host="localhost", port=port):
+ await asyncio.Future()
+
+
+async def main():
+ parser = argparse.ArgumentParser(prog='scorecard',
+ description="Run a websocket echo server.")
+ parser.add_argument("--port", type=int,
+ default=0, help="port to listen on")
+ args = parser.parse_args()
+
+ if args.port == 0:
+ sys.stderr.write('need --port\n')
+ sys.exit(1)
+
+ logging.basicConfig(
+ format="%(asctime)s %(message)s",
+ level=logging.DEBUG,
+ )
+ await run_server(args.port)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/test/pyhttpd/env.py b/test/pyhttpd/env.py
index aacedb7..6f2b285 100644
--- a/test/pyhttpd/env.py
+++ b/test/pyhttpd/env.py
@@ -312,6 +312,7 @@ def __init__(self, pytestconfig=None):
f"test2.{self._http_tld}",
f"test3.{self._http_tld}",
f"cgi.{self._http_tld}",
+ "localhost",
], key_type='rsa4096')]
self._verify_certs = False
diff --git a/test/requirements.txt b/test/requirements.txt
index 92e69fe..f6cb4d2 100644
--- a/test/requirements.txt
+++ b/test/requirements.txt
@@ -19,3 +19,4 @@ filelock
python-multipart
psutil
tqdm
+websockets