Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions test/modules/tls/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def __init__(self, pytestconfig=None):
CertificateSpec(domains=[self.domain_a]),
CertificateSpec(domains=[self.domain_b], key_type='secp256r1', single_file=True),
CertificateSpec(domains=[self.domain_b], key_type='rsa4096'),
CertificateSpec(domains=['localhost'], key_type='rsa4096'),
CertificateSpec(name="clientsX", sub_specs=[
CertificateSpec(name="user1", client=True, single_file=True),
CertificateSpec(name="user2", client=True, single_file=True),
Expand Down
3 changes: 2 additions & 1 deletion test/modules/tls/test_09_timeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def _function_scope(self, env):
def test_tls_09_timeout_handshake(self, env):
# in domain_b root, the StdEnvVars is switch on
s = socket.create_connection(('localhost', env.https_port))
s.send(b'1234')
# something that looks like a ClientHello
s.send(bytes.fromhex('3c37121e'))
s.settimeout(0.0)
try:
s.recv(1024)
Expand Down
201 changes: 136 additions & 65 deletions test/modules/tls/test_18_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,81 @@
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 WsServer:

def __init__(self, name, env, port, creds=None):
self.name = name
self.env = env
self.process = None
self.cerr = None
self.port = port
self.creds = creds
self.run_dir = os.path.join(env.gen_dir, self.name)
self.err_file = os.path.join(self.run_dir, 'stderr')
self._rmrf(self.run_dir)
self._mkpath(self.run_dir)

def start(self):
if not self.process:
self.cerr = open(self.err_file, 'w')
cmd = os.path.join(os.path.dirname(inspect.getfile(TestWebSockets)),
'ws_server.py')
args = ['python3', cmd, '--port', str(self.port)]
if self.creds:
args.extend([
'--cert', self.creds[0].cert_file,
'--key', self.creds[0].pkey_file,
])
self.process = subprocess.Popen(args=args, cwd=self.run_dir,
stderr=self.cerr, stdout=self.cerr)
if not self.check_alive():
self.stop()
pytest.fail(f'ws_server did not start. stderr={open(self.err_file).readlines()}')

def stop(self):
if self.process:
self.process.kill()
self.process.wait()
self.process = None
if self.cerr:
self.cerr.close()
self.cerr = None

def check_alive(self, timeout=5):
if self.creds:
url = f'https://localhost:{self.port}/'
else:
url = f'http://localhost:{self.port}/'
end = datetime.now() + timedelta(seconds=timeout)
while datetime.now() < end:
r = self.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)



class TestWebSockets:

@staticmethod
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")


@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
# Apache config that CONNECT proxies a WebSocket server for paths starting
Expand All @@ -32,44 +96,52 @@ def _class_scope(self, env):
conf = TlsTestConf(env, extras={
'base': [
'Timeout 1',
f'<Proxy https://localhost:{env.wss_port}/>',
' TLSProxyEngine on',
f' TLSProxyCA {env.ca.cert_file}',
' ProxyPreserveHost on',
'</Proxy>',
],
'localhost': [
f'ProxyPass /ws/ http://127.0.0.1:{env.ws_port}/ upgrade=websocket \\',
f'timeout=2 flushpackets=on',
f'ProxyPass /wss/ https://localhost:{env.wss_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)
TestWebSockets.mk_text_file(os.path.join(env.gen_dir, "1k.txt"), 8)
TestWebSockets.mk_text_file(os.path.join(env.gen_dir, "10k.txt"), 80)
TestWebSockets.mk_text_file(os.path.join(env.gen_dir, "100k.txt"), 800)
TestWebSockets.mk_text_file(os.path.join(env.gen_dir, "1m.txt"), 8000)
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)
@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.
ws_server = WsServer('ws-server', env, port=env.ws_port)
ws_server.start()
yield ws_server
ws_server.stop()

def _rmrf(self, path):
if os.path.exists(path):
return shutil.rmtree(path)
@pytest.fixture(autouse=True, scope='class')
def wss_server(self, env):
# Run our python websockets server that has some special behaviour
# for the different path to CONNECT to.
creds = env.get_credentials_for_name('localhost')
assert creds
ws_server = WsServer('wss-server', env, port=env.wss_port, creds=creds)
ws_server.start()
yield ws_server
ws_server.stop()

def ssl_ctx(self, env):
ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
ssl_ctx.load_verify_locations(cafile=env.ca.cert_file)
return ssl_ctx

def ws_recv_text(self, ws):
msg = ""
Expand All @@ -87,55 +159,54 @@ def ws_recv_bytes(self, ws):
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):
# verify the our plain websocket server works
def test_tls_18_01_ws_direct(self, env, ws_server):
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):
# verify that our secure websocket server works
def test_tls_18_02_wss_direct(self, env, wss_server):
pytest.skip(reason='For unknown reasons, this is flaky in CI')
with connect(f"wss://localhost:{env.wss_port}/echo",
ssl_context=self.ssl_ctx(env)) as ws:
message = "Hello world!"
ws.send(message)
response = self.ws_recv_text(ws)
assert response == message

# verify to send plain websocket message pingpong through apache
def test_tls_18_03_http_ws(self, env, ws_server):
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):
# verify to send secure websocket message pingpong through apache
def test_tls_18_04_http_wss(self, env, wss_server):
pytest.skip(reason='This fails, needing a fix like PR #9')
with connect(f"ws://localhost:{env.http_port}/wss/echo/") as ws:
message = "Hello world!"
ws.send(message)
response = self.ws_recv_text(ws)
assert response == message

# verify that getting a large file works without any TLS involved
@pytest.mark.parametrize("fname", ["1m.txt"])
def test_tls_18_05_http_ws_file(self, env, fname, ws_server):
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):
# verify getting secure websocket from the http: server
# this is "backend" mod_tls work
@pytest.mark.parametrize("fname", ["1k.txt", "10k.txt", "100k.txt", "1m.txt"])
def test_tls_18_06_http_wss_file(self, env, fname, ws_server):
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:
with connect(f"ws://localhost:{env.http_port}/wss/file/{fname}") as ws:
response = self.ws_recv_bytes(ws)
assert response == expected
17 changes: 13 additions & 4 deletions test/modules/tls/ws_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import logging
import os
import ssl
import sys
import time

Expand Down Expand Up @@ -75,10 +76,14 @@ async def on_async_conn(conn):
await conn.close(code=1000, reason='')


async def run_server(port):
log.info(f'starting server on port {port}')
async def run_server(port, cert, pkey):
log.info(f'starting server on port {port}, cert {cert}, key {pkey}')
ssl_ctx = None
if cert and pkey:
ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_SERVER)
ssl_ctx.load_cert_chain(certfile=cert, keyfile=pkey)
async with ws_server.serve(ws_handler=on_async_conn,
host="localhost", port=port):
host="localhost", port=port, ssl=ssl_ctx):
await asyncio.Future()


Expand All @@ -87,6 +92,10 @@ async def main():
description="Run a websocket echo server.")
parser.add_argument("--port", type=int,
default=0, help="port to listen on")
parser.add_argument("--cert", type=str,
default=None, help="TLS certificate")
parser.add_argument("--key", type=str,
default=None, help="TLS private key")
args = parser.parse_args()

if args.port == 0:
Expand All @@ -97,7 +106,7 @@ async def main():
format="%(asctime)s %(message)s",
level=logging.DEBUG,
)
await run_server(args.port)
await run_server(args.port, args.cert, args.key)


if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions test/pyhttpd/config.ini.in
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ https_port = 5001
proxy_port = 5003
http_port2 = 5004
ws_port = 5100
wss_port = 5101
http_tld = tests.httpd.apache.org
test_dir = @abs_srcdir@
test_src_dir = @abs_srcdir@
5 changes: 5 additions & 0 deletions test/pyhttpd/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def __init__(self, pytestconfig=None):
self._https_port = int(self.config.get('test', 'https_port'))
self._proxy_port = int(self.config.get('test', 'proxy_port'))
self._ws_port = int(self.config.get('test', 'ws_port'))
self._wss_port = int(self.config.get('test', 'wss_port'))
self._http_tld = self.config.get('test', 'http_tld')
self._test_dir = self.config.get('test', 'test_dir')
self._clients_dir = os.path.join(os.path.dirname(self._test_dir), 'clients')
Expand Down Expand Up @@ -404,6 +405,10 @@ def proxy_port(self) -> int:
def ws_port(self) -> int:
return self._ws_port

@property
def wss_port(self) -> int:
return self._wss_port

@property
def http_tld(self) -> str:
return self._http_tld
Expand Down