soth-mitm 0.2.0

Rust intercepting proxy crate with deterministic handler/event contracts for SOTH.
Documentation
fn machine_name(machine: FlowProtocolMachine) -> &'static str {
    match machine {
        FlowProtocolMachine::Unknown => "unknown",
        FlowProtocolMachine::Tunnel => "tunnel",
        FlowProtocolMachine::Http1 => "http1",
        FlowProtocolMachine::Http2 => "http2",
        FlowProtocolMachine::StreamableHttp => "streamable_http",
        FlowProtocolMachine::WebSocket => "websocket",
        FlowProtocolMachine::Sse => "sse",
    }
}

#[test]
fn flow_state_machine_allows_connect_to_close_lifecycle() {
    let s1 = next_flow_state_for_machine(
        FlowProtocolMachine::Unknown,
        FlowLifecycleState::Accepted,
        EventType::ConnectReceived,
    )
    .expect("connect");
    let s2 = next_flow_state_for_machine(FlowProtocolMachine::Unknown, s1, EventType::ConnectDecision)
        .expect("decision");
    let s3 = next_flow_state_for_machine(
        FlowProtocolMachine::Unknown,
        s2,
        EventType::TlsHandshakeStarted,
    )
    .expect("tls started");
    let s4 = next_flow_state_for_machine(
        FlowProtocolMachine::Unknown,
        s3,
        EventType::TlsHandshakeSucceeded,
    )
    .expect("tls ok");
    let s5 = next_flow_state_for_machine(
        FlowProtocolMachine::Unknown,
        s4,
        EventType::RequestHeaders,
    )
    .expect("active");
    let s6 = next_flow_state_for_machine(
        FlowProtocolMachine::Unknown,
        s5,
        EventType::StreamClosed,
    )
    .expect("closed");
    assert_eq!(s6, FlowLifecycleState::Closed);
}

#[test]
fn flow_state_machine_rejects_policy_decision_before_connect_parse() {
    let invalid = next_flow_state_for_machine(
        FlowProtocolMachine::Unknown,
        FlowLifecycleState::Accepted,
        EventType::ConnectDecision,
    );
    assert!(invalid.is_none(), "connect_decision must follow connect");
}

#[test]
fn protocol_machine_infers_from_protocol_and_h3_event() {
    assert_eq!(
        machine_name(protocol_machine_from_event(
            ApplicationProtocol::Http1,
            EventType::RequestHeaders
        )),
        "http1"
    );
    assert_eq!(
        machine_name(protocol_machine_from_event(
            ApplicationProtocol::Tunnel,
            EventType::Http3Passthrough
        )),
        "streamable_http"
    );
}

#[test]
fn protocol_machine_promotes_from_tunnel_once() {
    let promoted =
        resolve_protocol_machine(FlowProtocolMachine::Tunnel, FlowProtocolMachine::Http2);
    assert_eq!(promoted, FlowProtocolMachine::Http2);

    let sticky = resolve_protocol_machine(promoted, FlowProtocolMachine::Http1);
    assert_eq!(sticky, FlowProtocolMachine::Http2);
}

#[test]
fn http2_machine_rejects_websocket_events() {
    let next = next_flow_state_for_machine(
        FlowProtocolMachine::Http2,
        FlowLifecycleState::ProtocolActive,
        EventType::WebSocketFrame,
    );
    assert!(
        next.is_none(),
        "http2 machine should reject websocket frame events"
    );
}

#[test]
fn invalid_transition_terminalizes_without_panic_and_allows_close() {
    use crate::types::FlowId;
    let tracker = FlowStateTracker::default();
    let flow_id = FlowId(91);

    assert_eq!(
        tracker.on_event(
            flow_id,
            ApplicationProtocol::Tunnel,
            EventType::ConnectReceived
        ),
        1
    );
    assert_eq!(
        tracker.on_event(flow_id, ApplicationProtocol::Http1, EventType::RequestHeaders),
        2
    );

    let snapshot = tracker
        .debug_snapshot(flow_id)
        .expect("flow entry should still exist before close");
    assert_eq!(snapshot.0, FlowLifecycleState::StreamClosing);
    assert_eq!(snapshot.1, FlowProtocolMachine::Http1);
    assert_eq!(snapshot.2, 1);

    assert_eq!(
        tracker.on_event(flow_id, ApplicationProtocol::Tunnel, EventType::StreamClosed),
        3
    );
    assert!(
        tracker.debug_snapshot(flow_id).is_none(),
        "flow entry must be evicted after close"
    );
}