Skip to content
Open
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
27 changes: 16 additions & 11 deletions src/hio/core/http/clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,12 +409,13 @@ def parseHead(self):
# create generator
lineParser = httping.parseLine(raw=self.msg, eols=(CRLF, LF), kind="status line")
while True: # parse until we get a non-100 status
if self.closed and not self.msg: # connection closed prematurely
raise httping.PrematureClosure("Connection closed unexpectedly"
" while parsing response start line")

line = next(lineParser)
if line is None:
if self.closed:
lineParser.close()
raise httping.PrematureClosure(
"Connection closed unexpectedly while parsing response "
"start line")
(yield None)
continue
lineParser.close() # close generator
Expand All @@ -427,13 +428,15 @@ def parseHead(self):
eols=(CRLF, LF),
kind="continue header line")
while True:
if self.closed and not self.msg: # connection closed prematurely
raise httping.PrematureClosure("Connection closed unexpectedly"
" while parsing response header")
headers = next(leaderParser)
if headers is not None:
leaderParser.close()
break
if self.closed:
leaderParser.close()
raise httping.PrematureClosure(
"Connection closed unexpectedly while parsing response "
"header")
(yield None)

self.code = self.status = status
Expand All @@ -450,13 +453,15 @@ def parseHead(self):
eols=(CRLF, LF),
kind="leader header line")
while True:
if self.closed and not self.msg: # connection closed prematurely
raise httping.PrematureClosure("Connection closed unexpectedly"
" while parsing response header")
headers = next(leaderParser)
if headers is not None:
leaderParser.close()
break
if self.closed:
leaderParser.close()
raise httping.PrematureClosure(
"Connection closed unexpectedly while parsing response "
"header")
(yield None)
self.headers.update(headers)

Expand Down Expand Up @@ -575,7 +580,7 @@ def parseBody(self):

elif self.length != None: # known content length
while len(self.msg) < self.length:
if self.closed and not self.msg: # connection closed prematurely
if self.closed:
raise httping.PrematureClosure("Connection closed unexpectedly"
" while parsing response body")
(yield None)
Expand Down
18 changes: 10 additions & 8 deletions src/hio/core/http/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,13 @@ def parseHead(self):
# create generator
lineParser = httping.parseLine(raw=self.msg, eols=(CRLF, LF), kind="status line")
while True: # parse until we get full start line
if self.closed: # connection closed prematurely
raise httping.PrematureClosure("Connection closed unexpectedly "
"while parsing request start line")

line = next(lineParser)
if line is None:
if self.closed:
lineParser.close()
raise httping.PrematureClosure(
"Connection closed unexpectedly while parsing request "
"start line")
(yield None)
continue
lineParser.close() # close generator
Expand Down Expand Up @@ -134,14 +135,15 @@ def parseHead(self):
eols=(CRLF, LF),
kind="leader header line")
while True:
if self.closed: # connection closed prematurely
raise httping.PrematureClosure("Connection closed unexpectedly "
"while parsing request header")

headers = next(leaderParser)
if headers is not None:
leaderParser.close()
break
if self.closed:
leaderParser.close()
raise httping.PrematureClosure(
"Connection closed unexpectedly while parsing request "
"header")
(yield None)
self.headers.update(headers)

Expand Down
68 changes: 68 additions & 0 deletions tests/core/http/test_clienting.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,74 @@
certdirpath = os.path.join(tlsdirpath, 'tls', 'certs')


def _service_respondent(respondent, limit=8):
"""Drive a finite response parser without hiding a terminal EOF stall."""
for _ in range(limit):
respondent.parse()
if respondent.parser is None:
return
raise AssertionError("response parser did not settle")


@pytest.mark.parametrize(
"initial, buffered, expected_body",
[
(b"",
b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n",
b""),
(b"HTTP/1.1 200 OK\r\n",
b"Content-Length: 0\r\n\r\n",
b""),
(b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n",
b"test",
b"test"),
],
)
def test_respondent_consumes_complete_buffered_framing_before_eof(
initial, buffered, expected_body):
"""Complete start-line, headers, or fixed body buffered at EOF are valid."""
respondent = clienting.Respondent(msg=bytearray(initial))
respondent.parse()
assert respondent.parser is not None

respondent.msg.extend(buffered)
respondent.close()
_service_respondent(respondent)

assert respondent.closed
assert respondent.ended
assert not respondent.errored
assert respondent.headed
assert respondent.bodied
assert respondent.body == expected_body
assert not respondent.msg


@pytest.mark.parametrize(
"initial, partial",
[
(b"", b"HTTP/1.1 200"),
(b"HTTP/1.1 200 OK\r\n", b"Content-Length: 0\r\n"),
(b"HTTP/1.1 100 Continue\r\n", b"Extension: partial"),
(b"HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n", b"te"),
],
)
def test_respondent_rejects_partial_buffered_framing_at_eof(initial, partial):
"""Partial start-line, headers, or fixed body at EOF settle as failure."""
respondent = clienting.Respondent(msg=bytearray(initial))
respondent.parse()
assert respondent.parser is not None

respondent.msg.extend(partial)
respondent.close()
_service_respondent(respondent)

assert respondent.closed
assert respondent.ended
assert respondent.errored
assert "closed unexpectedly" in respondent.error.lower()


def mockEchoService(server):
"""
mock echo server service for testing
Expand Down
52 changes: 51 additions & 1 deletion tests/core/http/test_serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from hio import help
from hio.help import helping
from hio.base import tyming, doing
from hio.core import http
from hio.core import http, tcp
from hio.core.http import serving


logger = help.ogler.getLogger()
Expand All @@ -22,6 +23,55 @@
sys.modules.get(__name__).__file__)))
certdirpath = os.path.join(tlsdirpath, 'tls', 'certs')


def _service_requestant(requestant, limit=8):
"""Drive a finite request parser without hiding a terminal EOF stall."""
for _ in range(limit):
requestant.parse()
if requestant.parser is None:
return
raise AssertionError("request parser did not settle")


@pytest.mark.parametrize(
"initial, buffered, expected_body",
[
(b"",
b"GET /buffered HTTP/1.1\r\nHost: localhost\r\n\r\n",
b""),
(b"GET /buffered HTTP/1.1\r\n",
b"Host: localhost\r\n\r\n",
b""),
(b"POST /buffered HTTP/1.1\r\nHost: localhost\r\n"
b"Content-Length: 4\r\n\r\n",
b"test",
b"test"),
],
)
def test_requestant_consumes_complete_buffered_framing_before_eof(
initial, buffered, expected_body):
"""Complete start-line, headers, or fixed body buffered at EOF are valid."""
remoter = tcp.Remoter(ha=("127.0.0.1", 6101),
ca=("127.0.0.1", 6102),
cs=None)
requestant = serving.Requestant(msg=bytearray(initial), remoter=remoter)
requestant.parse()
assert requestant.parser is not None

requestant.msg.extend(buffered)
requestant.close()
_service_requestant(requestant)

assert requestant.closed
assert requestant.ended
assert not requestant.errored
assert requestant.headed
assert requestant.bodied
assert requestant.path == "/buffered"
assert requestant.body == expected_body
assert not requestant.msg


def test_bare_server_echo():
"""
Test BaserServer service request response of echo non blocking
Expand Down
Loading