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
29 changes: 27 additions & 2 deletions scripts/nvx_tools/microvm_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3008,6 +3008,32 @@ def run_ipv6_egress_policy(
endpoint.close()


HOST_LOOPBACK_HTTP_REQUEST_LIMIT = 8192


def _read_http_request(connection: socket.socket, timeout: float) -> bytes:
"""Read a request up to its blank line, however TCP splits it.

A request can arrive in several reads, so one ``recv`` is not enough. The
read stops at the header terminator, at the peer's close, or when the
request is too large or the deadline passes.
"""
deadline = time.monotonic() + timeout
request = bytearray()
while b"\r\n\r\n" not in request:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("host-loopback HTTP request did not complete in time")
connection.settimeout(remaining)
chunk = connection.recv(4096)
if not chunk:
break
request.extend(chunk)
if len(request) > HOST_LOOPBACK_HTTP_REQUEST_LIMIT:
raise RuntimeError("host-loopback HTTP request exceeds its size limit")
return bytes(request)


def _http_server(
listener: socket.socket,
expected_path: bytes,
Expand All @@ -3018,8 +3044,7 @@ def _http_server(
try:
connection, _ = listener.accept()
with connection:
connection.settimeout(timeout)
request = connection.recv(4096)
request = _read_http_request(connection, timeout)
if not request.startswith(b"GET " + expected_path + b" HTTP/1."):
raise RuntimeError(
f"unexpected host-loopback HTTP request: {request!r}"
Expand Down
40 changes: 40 additions & 0 deletions scripts/test_microvm_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,46 @@ def _fresh_openvmm_command(*_args: object, **_kwargs: object) -> list[str]:
return ["openvmm"]


class HostLoopbackHttpFixtureTests(unittest.TestCase):
def _exchange(self, pieces: list[bytes]) -> tuple[bytes, list[Exception]]:
"""Send the request in pieces with a gap, and return the reply and fixture errors."""
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.addCleanup(listener.close)
listener.bind(("127.0.0.1", 0))
listener.listen(1)
errors: list[Exception] = []
server = threading.Thread(
target=microvm_tests._http_server,
args=(listener, b"/proxy", b"NVX-BODY", 2.0, errors),
daemon=True,
)
server.start()
response = b""
with socket.create_connection(listener.getsockname(), timeout=5) as client:
for index, piece in enumerate(pieces):
if index:
time.sleep(0.25)
client.sendall(piece)
while chunk := client.recv(4096):
response += chunk
server.join(5)
return response, errors

def test_a_single_send_request_is_answered(self) -> None:
response, errors = self._exchange(
[b"GET /proxy HTTP/1.1\r\nHost: localhost\r\n\r\n"]
)
self.assertTrue(response.startswith(b"HTTP/1.1 200 OK"), response)
self.assertEqual(errors, [])

def test_a_request_split_across_reads_is_answered(self) -> None:
response, errors = self._exchange(
[b"GET ", b"/proxy HTTP/1.1\r\nHost: localhost\r\n\r\n"]
)
self.assertTrue(response.startswith(b"HTTP/1.1 200 OK"), response)
self.assertEqual(errors, [])


class MicrovmTestParserTests(unittest.TestCase):
def test_parser_defaults_to_all_correctness_scenarios(self):
args = nvx.parse_args(["test-microvm", "--backend", "mshv"])
Expand Down