Skip to content

Commit b16d6a1

Browse files
authored
add websocket test cases (#11)
* add websocket test cases * add websockets to python requirements
1 parent cefe4c0 commit b16d6a1

File tree

6 files changed

+249
-12
lines changed

6 files changed

+249
-12
lines changed

test/modules/tls/env.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,18 +107,6 @@ def __init__(self, pytestconfig=None):
107107
' AddHandler cgi-script .py',
108108
' Options +ExecCGI',
109109
'</Directory>',
110-
f'<VirtualHost *:{self.http_port}>',
111-
' ServerName localhost',
112-
' DocumentRoot "htdocs"',
113-
'</VirtualHost>',
114-
f'<VirtualHost *:{self.http_port}>',
115-
f' ServerName {self.domain_a}',
116-
' DocumentRoot "htdocs/a.mod-tls.test"',
117-
'</VirtualHost>',
118-
f'<VirtualHost *:{self.http_port}>',
119-
f' ServerName {self.domain_b}',
120-
' DocumentRoot "htdocs/b.mod-tls.test"',
121-
'</VirtualHost>',
122110
])
123111
self.add_cert_specs([
124112
CertificateSpec(domains=[self.domain_a]),

test/modules/tls/test_13_proxy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ def _class_scope(self, env):
1818
]
1919
})
2020
# add vhosts a+b and a ssl proxy from a to b
21+
conf.add_vhost('localhost', port=env.http_port)
22+
conf.add_vhost(env.domain_b, port=env.http_port, doc_root=f"htdocs/{env.domain_b}")
2123
conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
2224
conf.install()
2325
assert env.apache_restart() == 0

test/modules/tls/test_18_ws.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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

test/modules/tls/ws_server.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import asyncio
4+
import logging
5+
import os
6+
import sys
7+
import time
8+
9+
import websockets.server as ws_server
10+
from websockets.exceptions import ConnectionClosedError
11+
12+
log = logging.getLogger(__name__)
13+
14+
logging.basicConfig(
15+
format="[%(asctime)s] %(message)s",
16+
level=logging.DEBUG,
17+
)
18+
19+
20+
async def echo(websocket):
21+
try:
22+
async for message in websocket:
23+
try:
24+
log.info(f'got request {message}')
25+
except Exception as e:
26+
log.error(f'error {e} getting path from {message}')
27+
await websocket.send(message)
28+
except ConnectionClosedError:
29+
pass
30+
31+
32+
async def on_async_conn(conn):
33+
rpath = str(conn.path)
34+
pcomps = rpath[1:].split('/')
35+
if len(pcomps) == 0:
36+
pcomps = ['echo'] # default handler
37+
log.info(f'connection for {pcomps}')
38+
if pcomps[0] == 'echo':
39+
log.info(f'/echo endpoint')
40+
for message in await conn.recv():
41+
await conn.send(message)
42+
elif pcomps[0] == 'text':
43+
await conn.send('hello!')
44+
elif pcomps[0] == 'file':
45+
if len(pcomps) < 2:
46+
conn.close(code=4999, reason='unknown file')
47+
return
48+
fpath = os.path.join('../', pcomps[1])
49+
if not os.path.exists(fpath):
50+
conn.close(code=4999, reason='file not found')
51+
return
52+
bufsize = 0
53+
if len(pcomps) > 2:
54+
bufsize = int(pcomps[2])
55+
if bufsize <= 0:
56+
bufsize = 16*1024
57+
delay_ms = 0
58+
if len(pcomps) > 3:
59+
delay_ms = int(pcomps[3])
60+
n = 1
61+
if len(pcomps) > 4:
62+
n = int(pcomps[4])
63+
for _ in range(n):
64+
with open(fpath, 'r+b') as fd:
65+
while True:
66+
buf = fd.read(bufsize)
67+
if buf is None or len(buf) == 0:
68+
break
69+
await conn.send(buf)
70+
if delay_ms > 0:
71+
time.sleep(delay_ms/1000)
72+
else:
73+
log.info(f'unknown endpoint: {rpath}')
74+
await conn.close(code=4999, reason='path unknown')
75+
await conn.close(code=1000, reason='')
76+
77+
78+
async def run_server(port):
79+
log.info(f'starting server on port {port}')
80+
async with ws_server.serve(ws_handler=on_async_conn,
81+
host="localhost", port=port):
82+
await asyncio.Future()
83+
84+
85+
async def main():
86+
parser = argparse.ArgumentParser(prog='scorecard',
87+
description="Run a websocket echo server.")
88+
parser.add_argument("--port", type=int,
89+
default=0, help="port to listen on")
90+
args = parser.parse_args()
91+
92+
if args.port == 0:
93+
sys.stderr.write('need --port\n')
94+
sys.exit(1)
95+
96+
logging.basicConfig(
97+
format="%(asctime)s %(message)s",
98+
level=logging.DEBUG,
99+
)
100+
await run_server(args.port)
101+
102+
103+
if __name__ == "__main__":
104+
asyncio.run(main())

test/pyhttpd/env.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def __init__(self, pytestconfig=None):
312312
f"test2.{self._http_tld}",
313313
f"test3.{self._http_tld}",
314314
f"cgi.{self._http_tld}",
315+
"localhost",
315316
], key_type='rsa4096')]
316317

317318
self._verify_certs = False

test/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ filelock
1919
python-multipart
2020
psutil
2121
tqdm
22+
websockets

0 commit comments

Comments
 (0)