Skip to content

Commit c792b42

Browse files
committed
[Fix] server: TLS handshake skipping, better body reading logic
1 parent 2010fad commit c792b42

File tree

4 files changed

+101
-35
lines changed

4 files changed

+101
-35
lines changed

src/py/extra/http/model.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ def headername(name: str, *, headers: dict[str, str] = {}) -> str:
5858
# -----------------------------------------------------------------------------
5959

6060

61+
class TLSHandshake(NamedTuple):
62+
"""Represents a TLS handshake"""
63+
64+
# Empty for now
65+
pass
66+
67+
6168
class HTTPRequestLine(NamedTuple):
6269
"""Represents a request status line"""
6370

@@ -165,7 +172,7 @@ async def _read(
165172
elif self.remaining:
166173
# FIXME: We should probably have a timeout there
167174
try:
168-
payload = await self.reader.load()
175+
payload = await self.reader.load(size=self.remaining)
169176
except TimeoutError:
170177
warning(
171178
"Request body loading timed out",
@@ -290,7 +297,7 @@ def __init__(self, transform: BytesTransform | None = None) -> None:
290297
async def read(
291298
self, timeout: float = BODY_READER_TIMEOUT, size: int | None = None
292299
) -> bytes | None:
293-
chunk = await self._read(timeout)
300+
chunk = await self._read(timeout=timeout, size=size)
294301
if chunk is not None and self.transform:
295302
res = self.transform.feed(chunk)
296303
return res if res else None
@@ -304,15 +311,24 @@ async def _read(
304311

305312
# NOTE: This is a dangerous operation, as this way bloat the whole memory.
306313
# Instead, loading should spool the file.
307-
async def load(self, timeout: float = BODY_READER_TIMEOUT) -> bytes:
314+
async def load(
315+
self, timeout: float = BODY_READER_TIMEOUT, size: int | None = None
316+
) -> bytes:
308317
"""Loads the entire body into a bytes array."""
309318
data = bytearray()
319+
# We may have an expected size to read
320+
left = size
310321
while True:
311-
chunk = await self.read(timeout)
322+
chunk = await self.read(timeout=timeout, size=left)
312323
if not chunk:
313324
break
314325
else:
315326
data += chunk
327+
# If we had a size to read, then we update it
328+
if size is not None:
329+
left = size - len(chunk)
330+
if left <= 0:
331+
break
316332
return data
317333

318334
async def spool(

src/py/extra/http/parser.py

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,20 @@
99
HTTPBodyBlob,
1010
HTTPAtom,
1111
HTTPProcessingStatus,
12+
TLSHandshake,
1213
headername,
1314
)
1415

1516

1617
class MessageParser:
1718
"""Parses an HTTP request or response line."""
1819

19-
__slots__ = ["line", "value"]
20+
__slots__ = ["line", "value", "skipping"]
2021

2122
def __init__(self) -> None:
2223
self.line: LineParser = LineParser()
23-
self.value: HTTPRequestLine | HTTPResponseLine | None = None
24+
self.value: HTTPRequestLine | HTTPResponseLine | TLSHandshake | None = None
25+
self.skipping: int = 0
2426

2527
def flush(self) -> "HTTPRequestLine|HTTPResponseLine|None":
2628
res = self.value
@@ -30,31 +32,50 @@ def flush(self) -> "HTTPRequestLine|HTTPResponseLine|None":
3032
def reset(self) -> "MessageParser":
3133
self.line.reset()
3234
self.value = None
35+
self.skipping = 0
3336
return self
3437

3538
def feed(self, chunk: bytes, start: int = 0) -> tuple[bool | None, int]:
36-
line, read = self.line.feed(chunk, start)
37-
if line:
38-
# NOTE: This is safe
39-
l = line.decode("ascii")
40-
if l.startswith("HTTP/"):
41-
protocol, status, message = l.split(" ", 2)
42-
self.value = HTTPResponseLine(
43-
protocol,
44-
int(status),
45-
message,
46-
)
39+
n = len(chunk)
40+
available = n - start
41+
# FIXME: In the future, we may want to do something with the TLS
42+
# handshake. This is something you can trigger with Firefox.
43+
if self.skipping:
44+
# We have reamining data to read/skip, so we do that
45+
read = min(available, self.skipping)
46+
self.skipping -= read
47+
return None, read
48+
elif (n - start) >= 5 and chunk[start] == 0x16:
49+
# This is a TLS Handshake, we parse the length
50+
size = 5 + (chunk[start + 3] << 8) + chunk[start + 4]
51+
if available >= size:
52+
return None, size
4753
else:
48-
i = l.find(" ")
49-
j = l.rfind(" ")
50-
p: list[str] = l[i + 1 : j].split("?", 1)
51-
# NOTE: There may be junk before the method name
52-
self.value = HTTPRequestLine(
53-
l[0:i], p[0], p[1] if len(p) > 1 else "", l[j + 1 :]
54-
)
55-
return True, read
54+
self.skipping = size - available
55+
return None, available
5656
else:
57-
return None, read
57+
line, read = self.line.feed(chunk, start)
58+
if line:
59+
# NOTE: This is safe
60+
l = line.decode("ascii")
61+
if l.startswith("HTTP/"):
62+
protocol, status, message = l.split(" ", 2)
63+
self.value = HTTPResponseLine(
64+
protocol,
65+
int(status),
66+
message,
67+
)
68+
else:
69+
i = l.find(" ")
70+
j = l.rfind(" ")
71+
p: list[str] = l[i + 1 : j].split("?", 1)
72+
# NOTE: There may be junk before the method name
73+
self.value = HTTPRequestLine(
74+
l[0:i], p[0], p[1] if len(p) > 1 else "", l[j + 1 :]
75+
)
76+
return True, read
77+
else:
78+
return None, read
5879

5980
def __str__(self) -> str:
6081
return f"MessageParser({self.value})"

src/py/extra/server.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import socket
66
import asyncio
77
import threading
8-
from .utils.logging import exception, info, warning, event
8+
from .utils.logging import exception, info, warning, event, debug, logged
99
from .utils.codec import BytesTransform
1010
from .utils.limits import LimitType, unlimit
1111
from .model import Application, Service, mount
@@ -100,10 +100,10 @@ def __init__(
100100
async def _read(
101101
self, timeout: float = 1.0, size: int | None = None
102102
) -> bytes | None:
103-
info(
103+
logged(debug) and debug(
104104
"Reading Body",
105105
Client=f"{id(self.socket):x}",
106-
Size=size,
106+
Size=size or self.size,
107107
Timeout=timeout,
108108
)
109109
return await asyncio.wait_for(
@@ -218,9 +218,9 @@ async def OnRequest(
218218
# NOTE: With HTTP Pipelining, we may receive more than one
219219
# request in the same payload, so we need to be prepared
220220
# to answer more than one request.
221-
stream = parser.feed(buffer[:n] if n != size else buffer)
222-
# TODO: Should be debug
223-
info(
221+
chunk = buffer[:n] if n != size else buffer
222+
stream = parser.feed(chunk)
223+
debug(
224224
"Reading Requests(s)",
225225
Client=f"{id(client):x}",
226226
Read=n,
@@ -231,11 +231,10 @@ async def OnRequest(
231231

232232
try:
233233
atom = next(stream)
234-
# TODO: Should be debug
235-
info("Request Atom", Atom=atom.__class__.__name__)
234+
debug("Request Atom", Atom=atom.__class__.__name__)
236235
except StopIteration:
237236
# TODO: Should be debug
238-
info("Request End")
237+
debug("Requests End", Iteration=iteration, Count=res_count)
239238
break
240239
if atom is HTTPProcessingStatus.Complete:
241240
status = atom

src/py/extra/utils/logging.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,28 @@ def entry(
144144
)
145145

146146

147+
def debug(
148+
message: str,
149+
*,
150+
origin: str | None = None,
151+
at: float | None = None,
152+
icon: str | None = None,
153+
stack: TStack | bool | None = None,
154+
**context: TPrimitive,
155+
) -> LogEntry:
156+
return send(
157+
entry(
158+
message=message,
159+
level=LogLevel.Debug,
160+
origin=origin,
161+
at=at,
162+
context=context,
163+
icon=icon,
164+
stack=callstack(1) if stack is True else stack if stack else None,
165+
)
166+
)
167+
168+
147169
def info(
148170
message: str,
149171
*,
@@ -278,4 +300,12 @@ def exception(
278300
return exception
279301

280302

303+
def logged(item) -> bool:
304+
"""Takes one of the logging function, and tells if it is currently
305+
supported. This is used to guard against running the whole entry
306+
building when not necessary."""
307+
# TODO: Implement based on log level
308+
return True
309+
310+
281311
# EOF

0 commit comments

Comments
 (0)