-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathserver.py
More file actions
110 lines (98 loc) · 4.07 KB
/
Copy pathserver.py
File metadata and controls
110 lines (98 loc) · 4.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/env python
import argparse
import functools
import json
import logging
import ssl
from socket import IPPROTO_TCP, TCP_NODELAY, MSG_PEEK
import trio
from http_helpers import http_resp, is_http_request, parse_request
from tlsfp import b_to_int, client_hello_data, hexify, make_ja3, make_ja4, parse_tls_record
PEEK_TIMEOUT = 5 # seconds to wait for the full ClientHello record
logging.basicConfig(
format='%(asctime)s %(levelname)s - %(message)s',
level=logging.INFO
)
INFO = logging.info
WARNING = logging.warning
ERROR = logging.error
DEBUG = logging.debug
def parse_args():
"""Parse cli arguments"""
parser = argparse.ArgumentParser()
parser.add_argument("--key", type=str, default="/tmp/server.key")
parser.add_argument("--cert", type=str, default="/tmp/server.crt")
parser.add_argument("--host", type=str, default="0.0.0.0")
parser.add_argument("--port", type=int, default=4433)
return parser.parse_args()
async def peek_exactly(stream, size):
"""Peek until size bytes are buffered, without consuming them"""
peek = await stream.socket.recv(size, MSG_PEEK)
while peek and len(peek) < size:
await trio.sleep(0.01) # wait for the rest to arrive
peek = await stream.socket.recv(size, MSG_PEEK)
return peek
async def peek_tls_record(stream):
"""
Peek at the first complete TLS record without consuming it from the
socket buffer. Returns None if the data is not a TLS handshake.
A ClientHello can exceed a single TCP segment (e.g. Chrome with a
post-quantum key share is ~1700 bytes), so keep peeking until the
full record length from the header is buffered.
"""
peek = await peek_exactly(stream, 5)
if len(peek) < 5 or peek[:3] != b'\x16\x03\x01':
return None
return await peek_exactly(stream, 5 + b_to_int(peek[3:5]))
async def handle(stream):
"""Handles each new connection"""
stream.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
addr = stream.socket.getpeername()
INFO(f'Connection from: {addr[0]}')
try:
with trio.fail_after(PEEK_TIMEOUT):
peek = await peek_tls_record(stream)
if not peek:
DEBUG('Not a TLS handshake request. Closing connection.')
return
DEBUG(peek)
stream = trio.SSLStream(stream, tls, server_side=True,
https_compatible=True)
await stream.do_handshake()
buf = await stream.receive_some(4096)
if buf and is_http_request(buf):
INFO(buf)
req, path, headers = parse_request(buf)
if req == 'GET':
if path == '/':
resp = http_resp(body='OK', ctype='text/plain')
elif path == '/tls':
rec = parse_tls_record(peek)
tls_data = client_hello_data(rec.data.data)
fp = {'tls_data': hexify(tls_data._asdict()),
'tls_fingerprints': {**make_ja3(tls_data),
**make_ja4(tls_data)}
}
resp = http_resp(body=json.dumps(fp), ctype='application/json')
else:
resp = http_resp(body=r'¯\_(ツ)_/¯', status_code=404)
else:
resp = http_resp(status_code=405)
await stream.send_all(resp.encode())
except trio.TooSlowError:
WARNING(f'Timed out waiting for TLS record from {addr[0]}')
except Exception as e:
WARNING(f'Error handling connection from {addr[0]}: {e!r}')
finally:
await stream.aclose()
if __name__ == "__main__":
try:
args = parse_args()
tls = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
tls.load_cert_chain(args.cert, args.key)
trio.run(functools.partial(trio.serve_tcp, handle, args.port, host=args.host))
# trio delivers Ctrl-C wrapped in a BaseExceptionGroup from the
# server nursery, so a plain `except KeyboardInterrupt` won't match
except* KeyboardInterrupt:
ERROR('Keyboard interrupt. Exiting.')
raise SystemExit(1) from None