Frame interception¶
See, edit, drop and add HTTP/2 and HTTP/3 frames on live connections, on both sides of the proxy,
while the proxy keeps every connection protocol-correct. Use this page to write a Java
FrameInterceptor; scripts get the same with on_frame.
HTTP/2 frame interception¶
A FrameInterceptor (org.microproxy.frames) sees every HTTP/2 frame on live connections and may
edit, drop and add frames. It is set once per proxy, not per exchange, because SETTINGS, PING and
GOAWAY belong to a connection:
MicroProxy.bootstrap()
.withManInTheMiddle(new CertificateAuthorityMitmManager(ca)) // HTTP/2 both ways by default
.withFrameInterceptor((frame, direction, ctx) -> switch (frame) {
case Http2Frame.Headers h when direction == FrameDirection.TO_SERVER -> h.withHeader("x-via-h2", "1");
case Http2Frame.Data d when direction == FrameDirection.FROM_SERVER && d.text().contains("secret") ->
d.withText(d.text().replace("secret", "******")); // same length: content-length still holds
case Http2Frame.Priority p -> null; // drop
default -> frame; // pass through
})
.start();
- Frames.
Http2Frameis a sealed interface of records:Data,Headers(the decoded field list, pseudo-headers included, with CONTINUATION folded in),Priority,RstStream,Settings,PushPromise,Ping,GoAway,WindowUpdateandUnknown(extension frames, which the proxy otherwise discards unread).Http3FramehasData,Headers,Settings,GoAway,CancelPush,MaxPushId,PushPromiseandUnknown. Edit them with theirwith...methods. - Directions. The proxy terminates HTTP/2 on each side, so each hop has its own frames:
FROM_CLIENT(after decoding, before the proxy acts on them),TO_CLIENT(before encoding),FROM_SERVERandTO_SERVER. A request seenFROM_CLIENTis a different set of frames from the one later seenTO_SERVER: other stream ids, header blocks and DATA boundaries. - Context.
FrameContextgives the protocol, the client connection's id, the client's address, the server (host:port, on the server side), the stream id, the exchange'sFlowContextwhen the frame belongs to one, andsend(frame), which adds a frame after this one on the same hop and in the same direction (going out it is sent next; coming in the proxy processes it as if the peer had sent it). - Coverage. Clients on intercepted TLS,
h2cand the TLS listener; connections to servers with all their multiplexed streams; full-duplex gRPC;CONNECTtunnels and WebSockets over HTTP/2, whose bytes are DATA frames. DATA the proxy sends is shown in pieces of at most 16 KiB. And live HTTP/3, on both sides, under the same rules adapted to HTTP/3 (see HTTP/3 frames). - Cost. Without an interceptor nothing changes: no conversion, no allocation, no call.
The proxy stays protocol-correct whatever the interceptor returns. What it accepts:
| Frame | May be edited | May be dropped | May be followed by (ctx.send) |
|---|---|---|---|
| DATA | the payload, to any size | yes | DATA; HEADERS (trailers), last, when the frame ends the stream; extension frames |
| HEADERS | the fields | no | HEADERS; extension frames |
| PRIORITY | everything but the stream | yes | extension frames |
| RST_STREAM | the error code | no | extension frames |
| SETTINGS | received: values, only to make the proxy stricter; sent: only extension settings | no | extension frames |
| GOAWAY | the error code and debug data | no | extension frames |
extension (Unknown) |
everything | yes | extension frames |
| SETTINGS ACK, PING, WINDOW_UPDATE | nothing (the proxy's own flow control and liveness) | no | nothing |
- Stream ids and END_STREAM are the proxy's. Frames stay on their stream (extension frames may also use stream 0). When the frame ended its stream, the last DATA or HEADERS frame of the result does; a dropped DATA frame that ended its stream is replaced by an empty DATA frame with END_STREAM. What the interceptor sets is ignored.
- Header fields must be valid (
Field.validate: lower-case token names, known pseudo-headers first, no connection-specific fields, no CR, LF or NUL in values). HPACK is never at risk: blocks are decoded once on arrival and encoded once on sending. A malformed request or response the proxy receives resets its stream, as usual. Changing a body's length while its HEADERS declarecontent-length: fix or remove the field in the HEADERS too (the interceptor sees them first), or the proxy resets the stream as malformed. - SETTINGS received may lower
HEADER_TABLE_SIZE,INITIAL_WINDOW_SIZE,MAX_FRAME_SIZE(not below 16384),MAX_CONCURRENT_STREAMSandMAX_HEADER_LIST_SIZE, or clearENABLE_PUSHandENABLE_CONNECT_PROTOCOL; the proxy then uses the edited values (fewer streams to a server, say). - Flow control. Received DATA counts against the windows, and is credited back, at its size on the wire, whatever the interceptor makes of it. DATA the proxy sends waits for window at its size after interception. Rate limits count frames as they arrive.
- Illegal results and failures. A result that breaks a rule is rejected as a whole: it is
logged (logger
org.microproxy.impl.Http2Frames,WARNING) and the original frame goes on, as it does when the interceptor throws.ctx.sendafter the call has returned throwsIllegalStateException. - Threads. The interceptor is called from many threads at once and must be thread-safe. The HEADERS that open a stream to a server are intercepted while that connection's writes are held, so stream ids stay in order: keep that call short.
Scripts get the same with on_frame.
HTTP/3 frame interception¶
The frame interceptor sees HTTP/3 frames live wherever the proxy speaks
HTTP/3: FROM_CLIENT and TO_CLIENT on the reverse proxy's HTTP/3 listener, on intercepted
transparent QUIC and on HTTP/3 intercepted inside CONNECT-UDP flows, TO_SERVER and FROM_SERVER on connections to HTTP/3 servers, with the
stream's QUIC id and, from a stream's
first frame on, its exchange's FlowContext. Http3Frame has Data, Headers, Settings,
GoAway, CancelPush, MaxPushId, PushPromise and Unknown. The rules are HTTP/2's,
adapted: HTTP/3 frames carry no stream id or END_STREAM (a message ends with its stream), so
results must keep the message's order (HEADERS, DATA, trailers; interim responses first); DATA
may be edited to any size, and QUIC's flow control counts what is actually sent; field sections
are decoded through QPACK before the interceptor sees them and encoded through the connection's
QPACK encoder after it, so QPACK state never depends on it; SETTINGS received may only make the
proxy stricter (a smaller QPACK table, field section or blocked-streams limit, no
ENABLE_CONNECT_PROTOCOL or H3_DATAGRAM turned on), SETTINGS sent may change only in extension
settings; GOAWAY, MAX_PUSH_ID and CANCEL_PUSH are read-only; extension and grease frames may be
edited, dropped and added. Scripts get the same with on_frame.
For HTTP/3 traffic relayed by other means, Http3FramePipeline runs a
frame interceptor over a request stream's or a control stream's bytes, under the same rules, and
writes them back with static-only QPACK, so no encoder stream is needed. It needs only the
http3-codec jar (Http3FramePipeline.available()).
Http3FramePipeline pipeline = Http3FramePipeline.builder(interceptor)
.direction(FrameDirection.FROM_CLIENT) // whose frames these are
.build();
pipeline.controlStream(2, clientControlStream, out); // stream type, SETTINGS, ...
pipeline.encoderStream(clientQpackEncoderStream); // only when the client's QPACK uses its dynamic table
pipeline.requestStream(0, requestStream, rewritten); // HEADERS, DATA, trailers