firegex/tests/test_http_history.py
Sogoss dbc8b176b1 fix(nfproxy): fix HttpHistory parameter ordering and state immutability
- Fix parameter order sensitivity in HttpHistory._fetch_packet when history is first argument

- Delegate total_size and should_upgrade to _message instead of live _parser

- Sanitize FGEX_MAX_HISTORY_SIZE with max(0, int(val)) and dynamically sync deque maxlen
2026-07-20 16:03:43 +02:00

278 lines
8.4 KiB
Python

from firegex.nfproxy import pyfilter, ACCEPT, clear_pyfilter_registry
from firegex.nfproxy.models import (
HttpRequest,
HttpResponse,
HttpFullRequest,
HttpFullResponse,
HttpHistory,
HttpStreamHistory,
)
from firegex.nfproxy.internals import compile, handle_packet
def create_packet_info(payload: bytes, is_input: bool):
return {
"data": payload,
"raw_packet": b"\x00" * 40 + payload,
"is_input": is_input,
"is_ipv6": False,
"is_tcp": True,
"l4_size": len(payload),
}
def test_http_history_model():
req1 = None
res1 = None
history_obj = HttpHistory(requests=[req1], responses=[res1])
assert history_obj.requests == [req1]
assert history_obj.responses == [res1]
assert "HttpHistory" in repr(history_obj)
def test_http_history_stream_execution():
glob = {}
clear_pyfilter_registry()
code = """
from firegex.nfproxy import pyfilter, ACCEPT
from firegex.nfproxy.models import HttpFullRequest, HttpFullResponse, HttpHistory
recorded_history = []
@pyfilter
def filter_req(req: HttpFullRequest, history: HttpHistory):
req_urls = [r.url for r in history.requests]
resp_statuses = [r.status_code for r in history.responses]
recorded_history.append(("req", req.url, req_urls, resp_statuses))
return ACCEPT
@pyfilter
def filter_resp(resp: HttpFullResponse, history: HttpHistory):
req_urls = [r.url for r in history.requests]
resp_statuses = [r.status_code for r in history.responses]
recorded_history.append(("resp", resp.status_code, req_urls, resp_statuses))
return ACCEPT
"""
glob["__firegex_pyfilter_enabled"] = ["filter_req", "filter_resp"]
glob["__firegex_proto"] = "http"
exec(code, glob, glob)
compile(glob)
# 1. First Request
req1_data = b"GET /first HTTP/1.1\r\nHost: example.com\r\n\r\n"
glob["__firegex_packet_info"] = create_packet_info(req1_data, is_input=True)
handle_packet(glob)
# 2. First Response
resp1_data = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK"
glob["__firegex_packet_info"] = create_packet_info(resp1_data, is_input=False)
handle_packet(glob)
# 3. Second Request
req2_data = b"GET /second HTTP/1.1\r\nHost: example.com\r\n\r\n"
glob["__firegex_packet_info"] = create_packet_info(req2_data, is_input=True)
handle_packet(glob)
# 4. Second Response
resp2_data = b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n"
glob["__firegex_packet_info"] = create_packet_info(resp2_data, is_input=False)
handle_packet(glob)
recorded = glob["recorded_history"]
# 1. First Request: history is empty
assert recorded[0] == ("req", "/first", [], [])
# 2. First Response: past requests contains /first
assert recorded[1] == ("resp", "OK", ["/first"], [])
# 3. Second Request: past requests contains [/first], past responses contains [OK]
assert recorded[2] == ("req", "/second", ["/first"], ["OK"])
# 4. Second Response: past requests contains [/first, /second], past responses contains [OK]
assert recorded[3] == ("resp", "Not Found", ["/first", "/second"], ["OK"])
def test_http_history_attribute_on_request():
glob = {}
clear_pyfilter_registry()
code = """
from firegex.nfproxy import pyfilter, ACCEPT
from firegex.nfproxy.models import HttpFullRequest, HttpFullResponse
history_lengths = []
@pyfilter
def filter_req(req: HttpFullRequest):
history_lengths.append((len(req.history.requests), len(req.history.responses)))
return ACCEPT
@pyfilter
def filter_resp(resp: HttpFullResponse):
history_lengths.append((len(resp.history.requests), len(resp.history.responses)))
return ACCEPT
"""
glob["__firegex_pyfilter_enabled"] = ["filter_req", "filter_resp"]
glob["__firegex_proto"] = "http"
exec(code, glob, glob)
compile(glob)
# Req 1
glob["__firegex_packet_info"] = create_packet_info(b"GET /a HTTP/1.1\r\nHost: a\r\n\r\n", is_input=True)
handle_packet(glob)
# Resp 1
glob["__firegex_packet_info"] = create_packet_info(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", is_input=False)
handle_packet(glob)
# Req 2
glob["__firegex_packet_info"] = create_packet_info(b"GET /b HTTP/1.1\r\nHost: b\r\n\r\n", is_input=True)
handle_packet(glob)
lengths = glob["history_lengths"]
assert lengths[0] == (0, 0)
assert lengths[1] == (1, 0)
assert lengths[2] == (1, 1)
def test_http_history_invalid_max_size():
glob = {}
clear_pyfilter_registry()
code = """
from firegex.nfproxy import pyfilter, ACCEPT
from firegex.nfproxy.models import HttpFullRequest
@pyfilter
def filter_req(req: HttpFullRequest):
return ACCEPT
"""
glob["__firegex_pyfilter_enabled"] = ["filter_req"]
glob["__firegex_proto"] = "http"
glob["FGEX_MAX_HISTORY_SIZE"] = "invalid_number"
exec(code, glob, glob)
compile(glob)
glob["__firegex_packet_info"] = create_packet_info(b"GET /test HTTP/1.1\r\nHost: test\r\n\r\n", is_input=True)
# Should not raise ValueError, should default to 100
handle_packet(glob)
def test_http_history_parameter_order():
glob = {}
clear_pyfilter_registry()
code = """
from firegex.nfproxy import pyfilter, ACCEPT
from firegex.nfproxy.models import HttpFullRequest, HttpHistory
calls_log = []
@pyfilter
def filter_history_first(history: HttpHistory, req: HttpFullRequest):
calls_log.append(("history_first", req.url, len(history.requests)))
return ACCEPT
"""
glob["__firegex_pyfilter_enabled"] = ["filter_history_first"]
glob["__firegex_proto"] = "http"
exec(code, glob, glob)
compile(glob)
glob["__firegex_packet_info"] = create_packet_info(b"GET /first HTTP/1.1\r\nHost: test\r\n\r\n", is_input=True)
handle_packet(glob)
calls = glob["calls_log"]
# Must be called EXACTLY ONCE with 0 history elements for the first request
assert len(calls) == 1
assert calls[0] == ("history_first", "/first", 0)
def test_http_history_property_immutability():
glob = {}
clear_pyfilter_registry()
immutability_log = []
code = """
from firegex.nfproxy import pyfilter, ACCEPT
from firegex.nfproxy.models import HttpFullRequest, HttpHistory
@pyfilter
def filter_req(req: HttpFullRequest, history: HttpHistory):
if len(history.requests) > 0:
past = history.requests[0]
immutability_log.append({
"past_size": past.total_size,
"past_url": past.url,
"curr_size": req.total_size,
"curr_url": req.url,
})
return ACCEPT
"""
glob["__firegex_pyfilter_enabled"] = ["filter_req"]
glob["__firegex_proto"] = "http"
glob["immutability_log"] = immutability_log
exec(code, glob, glob)
compile(glob)
# 1. First short request
req1_data = b"GET /short HTTP/1.1\r\nHost: test\r\n\r\n"
glob["__firegex_packet_info"] = create_packet_info(req1_data, is_input=True)
handle_packet(glob)
# 2. Second longer request
req2_data = b"POST /large_path HTTP/1.1\r\nHost: test\r\nContent-Length: 5\r\n\r\nHELLO"
glob["__firegex_packet_info"] = create_packet_info(req2_data, is_input=True)
handle_packet(glob)
entries = glob["immutability_log"]
assert len(entries) == 1
# Historical request 1 size must equal 14 (/short), current size 39 (/large_path)
assert entries[0]["past_size"] == 14
assert entries[0]["past_url"] == "/short"
assert entries[0]["curr_size"] == 39
assert entries[0]["curr_url"] == "/large_path"
def test_http_history_negative_max_size():
glob = {}
clear_pyfilter_registry()
code = """
from firegex.nfproxy import pyfilter, ACCEPT
from firegex.nfproxy.models import HttpFullRequest
@pyfilter
def filter_req(req: HttpFullRequest):
return ACCEPT
"""
glob["__firegex_pyfilter_enabled"] = ["filter_req"]
glob["__firegex_proto"] = "http"
glob["FGEX_MAX_HISTORY_SIZE"] = "-5"
exec(code, glob, glob)
compile(glob)
glob["__firegex_packet_info"] = create_packet_info(b"GET /test HTTP/1.1\r\nHost: test\r\n\r\n", is_input=True)
# Should not raise ValueError for maxlen=-5, should clamp to 0
handle_packet(glob)
if __name__ == "__main__":
test_http_history_model()
test_http_history_stream_execution()
test_http_history_attribute_on_request()
test_http_history_invalid_max_size()
test_http_history_parameter_order()
test_http_history_property_immutability()
test_http_history_negative_max_size()
print("All HTTP history unit tests passed successfully!")