-module(nhttp_h2_frame).
-moduledoc """
HTTP/2 binary frame encoding and decoding.
This module implements RFC 9113 Section 4-6 binary framing layer.
It provides zero-copy parsing using binary pattern matching and
efficient encoding using iolists.
## Decoding
```erlang
case nhttp_h2_frame:decode(Data) of
{ok, Frame, Rest} -> handle_frame(Frame), decode(Rest);
{more, MinBytes} -> wait_for_data(MinBytes);
{error, Error} -> handle_error(Error)
end.
```
## Encoding
```erlang
{ok, Frame} = nhttp_h2_frame:headers(StreamId, fin, fin, HeaderBlock),
ok = ssl:send(Socket, Frame).
```
""".
%%%-----------------------------------------------------------------------------
%% INLINE DIRECTIVES (PERFORMANCE OPTIMIZATION)
%%%-----------------------------------------------------------------------------
-compile({inline, [fin_to_end_stream/1]}).
-compile({inline, [fin_to_end_headers/1]}).
-compile({inline, [end_stream_to_fin/1]}).
-compile({inline, [end_headers_to_fin/1]}).
%%%-----------------------------------------------------------------------------
%% DECODING
%%%-----------------------------------------------------------------------------
-export([
decode/1,
decode/2,
decode_all/1,
decode_settings_payload/1
]).
%%%-----------------------------------------------------------------------------
%% ENCODING
%%%-----------------------------------------------------------------------------
-export([
continuation/3,
data/3,
goaway/3,
headers/4,
headers/5,
ping/1,
ping_ack/1,
priority/2,
push_promise/4,
rst_stream/2,
settings/1,
settings_ack/0,
window_update/1,
window_update/2
]).
%%%-----------------------------------------------------------------------------
%% UTILITIES
%%%-----------------------------------------------------------------------------
-export([
headers_with_continuation/4,
preface/0,
split_at/2
]).
%%%-----------------------------------------------------------------------------
%% TYPE EXPORTS
%%%-----------------------------------------------------------------------------
-export_type([
decode_all_result/0,
decode_error/0,
decode_result/0,
t/0
]).
%%%-----------------------------------------------------------------------------
%% TYPES
%%%-----------------------------------------------------------------------------
-type decode_all_result() ::
{ok, [t()], BytesConsumed :: non_neg_integer()}
| {error, decode_error()}.
-type decode_error() ::
{connection_error, nhttp_h2:error_code(), Reason :: binary()}
| {stream_error, nhttp_lib:stream_id(), nhttp_h2:error_code(), Reason :: binary()}.
-type decode_result() ::
{ok, t(), BytesConsumed :: pos_integer()}
| {more, MinBytes :: pos_integer()}
| {error, decode_error()}.
-type t() ::
{data, nhttp_lib:stream_id(), nhttp_h2:fin(), Payload :: binary()}
| {headers, nhttp_lib:stream_id(), nhttp_h2:fin(), nhttp_h2:fin(), HeaderBlock :: binary()}
| {headers, nhttp_lib:stream_id(), nhttp_h2:fin(), nhttp_h2:fin(), nhttp_h2:priority(),
HeaderBlock :: binary()}
| {priority, nhttp_lib:stream_id(), nhttp_h2:priority()}
| {rst_stream, nhttp_lib:stream_id(), nhttp_h2:error_code()}
| {settings, nhttp_h2:settings()}
| settings_ack
| {push_promise, nhttp_lib:stream_id(), nhttp_h2:fin(),
PromisedStreamId :: nhttp_lib:stream_id(), HeaderBlock :: binary()}
| {ping, OpaqueData :: binary()}
| {ping_ack, OpaqueData :: binary()}
| {goaway, LastStreamId :: nhttp_lib:stream_id(), nhttp_h2:error_code(), DebugData :: binary()}
| {window_update, Increment :: pos_integer()}
| {window_update, nhttp_lib:stream_id(), Increment :: pos_integer()}
| {continuation, nhttp_lib:stream_id(), nhttp_h2:fin(), HeaderBlock :: binary()}
| {unknown, Type :: non_neg_integer()}
| preface.
%%%-----------------------------------------------------------------------------
%% FRAME TYPE IDENTIFIERS (RFC 9113 SECTION 6)
%%%-----------------------------------------------------------------------------
-define(FRAME_DATA, 16#00).
-define(FRAME_HEADERS, 16#01).
-define(FRAME_PRIORITY, 16#02).
-define(FRAME_RST_STREAM, 16#03).
-define(FRAME_SETTINGS, 16#04).
-define(FRAME_PUSH_PROMISE, 16#05).
-define(FRAME_PING, 16#06).
-define(FRAME_GOAWAY, 16#07).
-define(FRAME_WINDOW_UPDATE, 16#08).
-define(FRAME_CONTINUATION, 16#09).
%%%-----------------------------------------------------------------------------
%% FLAG BITS (RFC 9113 SECTION 6)
%%%-----------------------------------------------------------------------------
-define(FLAG_END_STREAM, 16#01).
-define(FLAG_ACK, 16#01).
-define(FLAG_END_HEADERS, 16#04).
-define(FLAG_PADDED, 16#08).
-define(FLAG_PRIORITY, 16#20).
%%%-----------------------------------------------------------------------------
%% SETTINGS IDENTIFIERS (RFC 9113 SECTION 6.5.2)
%%%-----------------------------------------------------------------------------
-define(SETTINGS_HEADER_TABLE_SIZE, 16#01).
-define(SETTINGS_ENABLE_PUSH, 16#02).
-define(SETTINGS_MAX_CONCURRENT_STREAMS, 16#03).
-define(SETTINGS_INITIAL_WINDOW_SIZE, 16#04).
-define(SETTINGS_MAX_FRAME_SIZE, 16#05).
-define(SETTINGS_MAX_HEADER_LIST_SIZE, 16#06).
-define(SETTINGS_ENABLE_CONNECT_PROTOCOL, 16#08).
%%%-----------------------------------------------------------------------------
%% DEFAULT VALUES (RFC 9113 SECTION 6.5.2)
%%%-----------------------------------------------------------------------------
-define(DEFAULT_MAX_FRAME_SIZE, 16384).
%%%-----------------------------------------------------------------------------
%% LIMITS (RFC 9113)
%%%-----------------------------------------------------------------------------
-define(MIN_MAX_FRAME_SIZE, 16#4000).
-define(MAX_FRAME_SIZE_LIMIT, 16#ffffff).
-define(MAX_WINDOW_SIZE, 16#7fffffff).
%%%-----------------------------------------------------------------------------
%% FRAME STRUCTURE
%%%-----------------------------------------------------------------------------
-define(FRAME_HEADER_SIZE, 9).
%%%-----------------------------------------------------------------------------
%% CONNECTION PREFACE (RFC 9113 SECTION 3.4)
%%%-----------------------------------------------------------------------------
-define(PREFACE, <<"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n">>).
-define(PREFACE_LEN, 24).
%%%-----------------------------------------------------------------------------
%% DECODING
%%%-----------------------------------------------------------------------------
-doc "Decode a single frame from binary data. Returns {ok, Frame, BytesConsumed} where BytesConsumed is the number of bytes consumed from the input. Use split_at/2 to get the remaining buffer. Uses default max frame size (16384 bytes).".
-spec decode(Data :: binary()) -> decode_result().
decode(<<Data/binary>>) ->
decode(Data, ?DEFAULT_MAX_FRAME_SIZE).
-doc "Decode a single frame with custom max frame size. The max frame size should come from peer's SETTINGS_MAX_FRAME_SIZE.".
-spec decode(Data :: binary(), MaxFrameSize :: pos_integer()) -> decode_result().
decode(<<"PRI ", _/binary>> = Data, _MaxFrameSize) when byte_size(Data) >= ?PREFACE_LEN ->
decode_frame(Data, 0);
decode(<<"PRI ", _/binary>> = Data, _MaxFrameSize) ->
{more, ?PREFACE_LEN - byte_size(Data)};
decode(<<Data/binary>>, _MaxFrameSize) when byte_size(Data) < ?FRAME_HEADER_SIZE ->
{more, ?FRAME_HEADER_SIZE - byte_size(Data)};
decode(<<Len:24, _:48, _/binary>> = Data, _MaxFrameSize) when
byte_size(Data) < Len + ?FRAME_HEADER_SIZE
->
{more, Len + ?FRAME_HEADER_SIZE - byte_size(Data)};
decode(<<Len:24, _:48, _/binary>>, MaxFrameSize) when Len > MaxFrameSize ->
{error,
{connection_error, frame_size_error,
<<"Frame size exceeds SETTINGS_MAX_FRAME_SIZE (RFC 9113 Section 4.2)">>}};
decode(<<Data/binary>>, _MaxFrameSize) ->
decode_frame(Data, 0).
-doc "Decode all complete frames from binary data. Returns {ok, Frames, BytesConsumed} with list of decoded frames.".
-spec decode_all(binary()) -> decode_all_result().
decode_all(<<Data/binary>>) ->
decode_all_loop(Data, ?DEFAULT_MAX_FRAME_SIZE, 0, []).
-doc "Decode settings payload (for use in connection layer).".
-spec decode_settings_payload(binary()) -> {ok, nhttp_h2:settings()} | {error, decode_error()}.
decode_settings_payload(Payload) ->
decode_settings_payload(Payload, #{}).
%%%-----------------------------------------------------------------------------
%% ENCODING
%%%-----------------------------------------------------------------------------
-doc "Encode a CONTINUATION frame.".
-spec continuation(StreamId, EndHeaders, HeaderBlock) -> {ok, iodata()} when
StreamId :: nhttp_lib:stream_id(),
EndHeaders :: nhttp_h2:fin(),
HeaderBlock :: iodata().
continuation(StreamId, EndHeaders, HeaderBlock) ->
Len = iolist_size(HeaderBlock),
Flags = fin_to_end_headers(EndHeaders),
{ok, [<<Len:24, ?FRAME_CONTINUATION:8, Flags:8, 0:1, StreamId:31>>, HeaderBlock]}.
-doc "Encode a DATA frame.".
-spec data(StreamId, EndStream, Payload) -> {ok, iodata()} when
StreamId :: nhttp_lib:stream_id(),
EndStream :: nhttp_h2:fin(),
Payload :: iodata().
data(StreamId, EndStream, Payload) ->
Len = iolist_size(Payload),
Flags = fin_to_end_stream(EndStream),
{ok, [<<Len:24, ?FRAME_DATA:8, Flags:8, 0:1, StreamId:31>>, Payload]}.
-doc "Encode a GOAWAY frame.".
-spec goaway(LastStreamId, ErrorCode, DebugData) -> {ok, iodata()} when
LastStreamId :: nhttp_lib:stream_id(),
ErrorCode :: nhttp_h2:error_code(),
DebugData :: iodata().
goaway(LastStreamId, ErrorCode, DebugData) ->
Len = iolist_size(DebugData) + 8,
Code = error_code_to_int(ErrorCode),
{ok, [<<Len:24, ?FRAME_GOAWAY:8, 0:8, 0:32, 0:1, LastStreamId:31, Code:32>>, DebugData]}.
-doc "Encode a HEADERS frame without priority.".
-spec headers(StreamId, EndStream, EndHeaders, HeaderBlock) -> {ok, iodata()} when
StreamId :: nhttp_lib:stream_id(),
EndStream :: nhttp_h2:fin(),
EndHeaders :: nhttp_h2:fin(),
HeaderBlock :: iodata().
headers(StreamId, EndStream, EndHeaders, HeaderBlock) ->
Len = iolist_size(HeaderBlock),
Flags = fin_to_end_stream(EndStream) bor fin_to_end_headers(EndHeaders),
{ok, [<<Len:24, ?FRAME_HEADERS:8, Flags:8, 0:1, StreamId:31>>, HeaderBlock]}.
-doc "Encode a HEADERS frame with priority.".
-spec headers(StreamId, EndStream, EndHeaders, Priority, HeaderBlock) -> {ok, iodata()} when
StreamId :: nhttp_lib:stream_id(),
EndStream :: nhttp_h2:fin(),
EndHeaders :: nhttp_h2:fin(),
Priority :: nhttp_h2:priority(),
HeaderBlock :: iodata().
headers(StreamId, EndStream, EndHeaders, Priority, HeaderBlock) ->
#{exclusive := Exclusive, stream_dependency := DepStreamId, weight := Weight} = Priority,
Len = iolist_size(HeaderBlock) + 5,
Flags = fin_to_end_stream(EndStream) bor fin_to_end_headers(EndHeaders) bor ?FLAG_PRIORITY,
E = bool_to_bit(Exclusive),
{ok, [
<<Len:24, ?FRAME_HEADERS:8, Flags:8, 0:1, StreamId:31, E:1, DepStreamId:31,
(Weight - 1):8>>,
HeaderBlock
]}.
-doc "Encode a PING frame.".
-spec ping(OpaqueData :: binary()) -> {ok, binary()}.
ping(OpaqueData) when byte_size(OpaqueData) =:= 8 ->
{ok, <<8:24, ?FRAME_PING:8, 0:8, 0:32, OpaqueData:8/binary>>}.
-doc "Encode a PING acknowledgment.".
-spec ping_ack(OpaqueData :: binary()) -> {ok, binary()}.
ping_ack(OpaqueData) when byte_size(OpaqueData) =:= 8 ->
{ok, <<8:24, ?FRAME_PING:8, ?FLAG_ACK:8, 0:32, OpaqueData:8/binary>>}.
-doc "Encode a PRIORITY frame.".
-spec priority(StreamId, Priority) -> {ok, iodata()} when
StreamId :: nhttp_lib:stream_id(),
Priority :: nhttp_h2:priority().
priority(StreamId, Priority) ->
#{exclusive := Exclusive, stream_dependency := DepStreamId, weight := Weight} = Priority,
E = bool_to_bit(Exclusive),
{ok, <<5:24, ?FRAME_PRIORITY:8, 0:8, 0:1, StreamId:31, E:1, DepStreamId:31, (Weight - 1):8>>}.
-doc "Encode a PUSH_PROMISE frame.".
-spec push_promise(StreamId, PromisedStreamId, EndHeaders, HeaderBlock) -> {ok, iodata()} when
StreamId :: nhttp_lib:stream_id(),
PromisedStreamId :: nhttp_lib:stream_id(),
EndHeaders :: nhttp_h2:fin(),
HeaderBlock :: iodata().
push_promise(StreamId, PromisedStreamId, EndHeaders, HeaderBlock) ->
Len = iolist_size(HeaderBlock) + 4,
Flags = fin_to_end_headers(EndHeaders),
{ok, [
<<Len:24, ?FRAME_PUSH_PROMISE:8, Flags:8, 0:1, StreamId:31, 0:1, PromisedStreamId:31>>,
HeaderBlock
]}.
-doc "Encode a RST_STREAM frame.".
-spec rst_stream(StreamId, ErrorCode) -> {ok, binary()} when
StreamId :: nhttp_lib:stream_id(),
ErrorCode :: nhttp_h2:error_code().
rst_stream(StreamId, ErrorCode) ->
Code = error_code_to_int(ErrorCode),
{ok, <<4:24, ?FRAME_RST_STREAM:8, 0:8, 0:1, StreamId:31, Code:32>>}.
-doc "Encode a SETTINGS frame.".
-spec settings(Settings :: nhttp_h2:settings()) -> {ok, iodata()}.
settings(Settings) ->
Payload = maps:fold(fun encode_setting/3, [], Settings),
Len = iolist_size(Payload),
{ok, [<<Len:24, ?FRAME_SETTINGS:8, 0:8, 0:32>>, Payload]}.
-doc "Encode a SETTINGS acknowledgment.".
-spec settings_ack() -> {ok, binary()}.
settings_ack() ->
{ok, <<0:24, ?FRAME_SETTINGS:8, ?FLAG_ACK:8, 0:32>>}.
-doc "Encode a connection-level WINDOW_UPDATE frame.".
-spec window_update(Increment :: pos_integer()) -> {ok, binary()}.
window_update(Increment) when Increment > 0, Increment =< ?MAX_WINDOW_SIZE ->
{ok, <<4:24, ?FRAME_WINDOW_UPDATE:8, 0:8, 0:32, 0:1, Increment:31>>}.
-doc "Encode a stream-level WINDOW_UPDATE frame.".
-spec window_update(StreamId, Increment) -> {ok, binary()} when
StreamId :: nhttp_lib:stream_id(),
Increment :: pos_integer().
window_update(StreamId, Increment) when Increment > 0, Increment =< ?MAX_WINDOW_SIZE ->
{ok, <<4:24, ?FRAME_WINDOW_UPDATE:8, 0:8, 0:1, StreamId:31, 0:1, Increment:31>>}.
%%%-----------------------------------------------------------------------------
%% UTILITIES
%%%-----------------------------------------------------------------------------
-doc "Split header block into HEADERS + CONTINUATION frames if needed.".
-spec headers_with_continuation(StreamId, EndStream, HeaderBlock, MaxFrameSize) ->
{ok, iodata()}
when
StreamId :: nhttp_lib:stream_id(),
EndStream :: nhttp_h2:fin(),
HeaderBlock :: iodata(),
MaxFrameSize :: pos_integer().
headers_with_continuation(StreamId, EndStream, HeaderBlock, MaxFrameSize) ->
Bin = iolist_to_binary(HeaderBlock),
case Bin of
<<First:MaxFrameSize/binary, Rest/binary>> when byte_size(Rest) > 0 ->
{ok, HeadersFrame} = headers(StreamId, EndStream, nofin, First),
{ok, [HeadersFrame | continuation_frames(StreamId, Rest, MaxFrameSize)]};
_ ->
headers(StreamId, EndStream, fin, Bin)
end.
-doc "HTTP/2 connection preface magic string.".
-spec preface() -> {ok, binary()}.
preface() ->
{ok, ?PREFACE}.
-doc "Split buffer at position, returning the remainder. This is the intentional single allocation point for callers.".
-spec split_at(binary(), non_neg_integer()) -> binary().
split_at(<<Bin/binary>>, Pos) ->
<<_:Pos/binary, Rest/binary>> = Bin,
Rest.
%%%-----------------------------------------------------------------------------
%% INTERNAL FUNCTIONS
%%%-----------------------------------------------------------------------------
-spec bit_to_bool(0 | 1) -> boolean().
bit_to_bool(1) -> true;
bit_to_bool(0) -> false.
-spec bool_to_bit(boolean()) -> 0 | 1.
bool_to_bit(true) -> 1;
bool_to_bit(false) -> 0.
-spec continuation_frames(nhttp_lib:stream_id(), binary(), pos_integer()) -> iodata().
continuation_frames(StreamId, <<Data/binary>>, MaxFrameSize) ->
continuation_frames_loop(StreamId, Data, MaxFrameSize, byte_size(Data), []).
-spec continuation_frames_loop(
nhttp_lib:stream_id(), binary(), pos_integer(), non_neg_integer(), iodata()
) -> iodata().
continuation_frames_loop(StreamId, <<Data/binary>>, _MaxFrameSize, Remaining, Acc) when
Remaining =< 0
->
lists:reverse([encode_continuation_bin(StreamId, fin, Data) | Acc]);
continuation_frames_loop(StreamId, <<Data/binary>>, MaxFrameSize, Remaining, Acc) when
Remaining =< MaxFrameSize
->
lists:reverse([encode_continuation_bin(StreamId, fin, Data) | Acc]);
continuation_frames_loop(StreamId, <<Data/binary>>, MaxFrameSize, Remaining, Acc) ->
<<Chunk:MaxFrameSize/binary, Rest/binary>> = Data,
Frame = encode_continuation_bin(StreamId, nofin, Chunk),
continuation_frames_loop(StreamId, Rest, MaxFrameSize, Remaining - MaxFrameSize, [Frame | Acc]).
-spec decode_all_loop(binary(), pos_integer(), non_neg_integer(), [t()]) ->
decode_all_result().
decode_all_loop(<<Data/binary>>, MaxFrameSize, TotalConsumed, Acc) ->
case decode(Data, MaxFrameSize) of
{ok, Frame, Consumed} ->
<<_:Consumed/binary, Rest/binary>> = Data,
decode_all_loop(Rest, MaxFrameSize, TotalConsumed + Consumed, [Frame | Acc]);
{more, _} ->
{ok, lists:reverse(Acc), TotalConsumed};
{error, _} = Error ->
Error
end.
-spec decode_frame(binary(), non_neg_integer()) -> decode_result().
decode_frame(<<"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", _/binary>>, Skip) ->
{ok, preface, Skip + ?PREFACE_LEN};
decode_frame(<<"PRI ", _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error, <<"Invalid connection preface (RFC 9113 Section 3.4)">>}};
decode_frame(<<_Len:24, ?FRAME_DATA:8, _:8, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error,
<<"DATA frame MUST be associated with a stream (RFC 9113 Section 6.1)">>}};
decode_frame(<<0:24, ?FRAME_DATA:8, Flags:8, _:32, _/binary>>, _Skip) when
Flags band ?FLAG_PADDED =/= 0
->
{error,
{connection_error, frame_size_error,
<<"DATA frame with padding MUST have length > 0 (RFC 9113 Section 6.1)">>}};
decode_frame(
<<Len:24, ?FRAME_DATA:8, Flags:8, _:1, StreamId:31, PadLen:8, Rest1/binary>>, Skip
) when
Flags band ?FLAG_PADDED =/= 0
->
DataLen = Len - PadLen - 1,
maybe
true ?= PadLen < Len,
<<Data:DataLen/binary, Padding:PadLen/binary, _/binary>> = Rest1,
true ?= is_zero_padding(Padding),
EndStream = end_stream_to_fin(Flags),
{ok, {data, StreamId, EndStream, Data}, Skip + ?FRAME_HEADER_SIZE + Len}
else
false ->
{error,
{connection_error, protocol_error,
<<"Padding length exceeds frame payload (RFC 9113 Section 6.1)">>}}
end;
decode_frame(<<Len:24, ?FRAME_DATA:8, Flags:8, _:1, StreamId:31, Data:Len/binary, _/binary>>, Skip) ->
EndStream = end_stream_to_fin(Flags),
{ok, {data, StreamId, EndStream, Data}, Skip + ?FRAME_HEADER_SIZE + Len};
decode_frame(<<_:24, ?FRAME_HEADERS:8, _:8, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error,
<<"HEADERS frame MUST be associated with a stream (RFC 9113 Section 6.2)">>}};
decode_frame(<<0:24, ?FRAME_HEADERS:8, Flags:8, _:32, _/binary>>, _Skip) when
Flags band ?FLAG_PADDED =/= 0
->
{error,
{connection_error, frame_size_error,
<<"HEADERS frame with padding MUST have length > 0 (RFC 9113 Section 6.2)">>}};
decode_frame(<<Len:24, ?FRAME_HEADERS:8, Flags:8, _:32, _/binary>>, _Skip) when
Flags band ?FLAG_PRIORITY =/= 0, Len < 5
->
{error,
{connection_error, frame_size_error,
<<"HEADERS frame with priority MUST have length >= 5 (RFC 9113 Section 6.2)">>}};
decode_frame(<<Len:24, ?FRAME_HEADERS:8, Flags:8, _:32, _/binary>>, _Skip) when
Flags band ?FLAG_PADDED =/= 0, Flags band ?FLAG_PRIORITY =/= 0, Len < 6
->
{error,
{connection_error, frame_size_error,
<<"HEADERS frame with padding and priority MUST have length >= 6 (RFC 9113 Section 6.2)">>}};
decode_frame(
<<Len:24, ?FRAME_HEADERS:8, Flags:8, _:1, StreamId:31, HeaderBlock:Len/binary, _/binary>>, Skip
) when
Flags band ?FLAG_PADDED =:= 0, Flags band ?FLAG_PRIORITY =:= 0
->
EndStream = end_stream_to_fin(Flags),
EndHeaders = end_headers_to_fin(Flags),
{ok, {headers, StreamId, EndStream, EndHeaders, HeaderBlock}, Skip + ?FRAME_HEADER_SIZE + Len};
decode_frame(
<<Len0:24, ?FRAME_HEADERS:8, Flags:8, _:1, StreamId:31, PadLen:8, Rest0/binary>>, Skip
) when
Flags band ?FLAG_PADDED =/= 0, Flags band ?FLAG_PRIORITY =:= 0
->
DataLen = Len0 - PadLen - 1,
maybe
true ?= PadLen < Len0,
<<HeaderBlock:DataLen/binary, Padding:PadLen/binary, _/binary>> = Rest0,
true ?= is_zero_padding(Padding),
EndStream = end_stream_to_fin(Flags),
EndHeaders = end_headers_to_fin(Flags),
{ok, {headers, StreamId, EndStream, EndHeaders, HeaderBlock},
Skip + ?FRAME_HEADER_SIZE + Len0}
else
false ->
{error,
{connection_error, protocol_error,
<<"Padding length exceeds frame payload (RFC 9113 Section 6.2)">>}}
end;
decode_frame(
<<_Len0:24, ?FRAME_HEADERS:8, Flags:8, _:1, StreamId:31, _E:1, StreamId:31, _:8, _/binary>>,
_Skip
) when
Flags band ?FLAG_PADDED =:= 0, Flags band ?FLAG_PRIORITY =/= 0
->
{error,
{connection_error, protocol_error,
<<"HEADERS frame cannot depend on itself (RFC 9113 Section 5.3.1)">>}};
decode_frame(
<<Len0:24, ?FRAME_HEADERS:8, Flags:8, _:1, StreamId:31, E:1, DepStreamId:31, Weight:8,
Rest0/binary>>,
Skip
) when
Flags band ?FLAG_PADDED =:= 0, Flags band ?FLAG_PRIORITY =/= 0
->
DataLen = Len0 - 5,
<<HeaderBlock:DataLen/binary, _/binary>> = Rest0,
EndStream = end_stream_to_fin(Flags),
EndHeaders = end_headers_to_fin(Flags),
Priority = #{
exclusive => bit_to_bool(E), stream_dependency => DepStreamId, weight => Weight + 1
},
{ok, {headers, StreamId, EndStream, EndHeaders, Priority, HeaderBlock},
Skip + ?FRAME_HEADER_SIZE + Len0};
decode_frame(
<<_Len0:24, ?FRAME_HEADERS:8, Flags:8, _:1, StreamId:31, _:8, _:1, StreamId:31, _/binary>>,
_Skip
) when
Flags band ?FLAG_PADDED =/= 0, Flags band ?FLAG_PRIORITY =/= 0
->
{error,
{connection_error, protocol_error,
<<"HEADERS frame cannot depend on itself (RFC 9113 Section 5.3.1)">>}};
decode_frame(
<<Len0:24, ?FRAME_HEADERS:8, Flags:8, _:1, StreamId:31, PadLen:8, E:1, DepStreamId:31, Weight:8,
Rest0/binary>>,
Skip
) when
Flags band ?FLAG_PADDED =/= 0, Flags band ?FLAG_PRIORITY =/= 0
->
DataLen = Len0 - PadLen - 6,
maybe
true ?= PadLen < Len0 - 5,
<<HeaderBlock:DataLen/binary, Padding:PadLen/binary, _/binary>> = Rest0,
true ?= is_zero_padding(Padding),
EndStream = end_stream_to_fin(Flags),
EndHeaders = end_headers_to_fin(Flags),
Priority = #{
exclusive => bit_to_bool(E), stream_dependency => DepStreamId, weight => Weight + 1
},
{ok, {headers, StreamId, EndStream, EndHeaders, Priority, HeaderBlock},
Skip + ?FRAME_HEADER_SIZE + Len0}
else
false ->
{error,
{connection_error, protocol_error,
<<"Padding length exceeds frame payload (RFC 9113 Section 6.2)">>}}
end;
decode_frame(<<5:24, ?FRAME_PRIORITY:8, _:8, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error,
<<"PRIORITY frame MUST be associated with a stream (RFC 9113 Section 6.3)">>}};
decode_frame(
<<5:24, ?FRAME_PRIORITY:8, _:8, _:1, StreamId:31, _:1, StreamId:31, _:8, _/binary>>, _Skip
) ->
{error,
{stream_error, StreamId, protocol_error,
<<"PRIORITY frame cannot depend on itself (RFC 9113 Section 5.3.1)">>}};
decode_frame(
<<5:24, ?FRAME_PRIORITY:8, _:8, _:1, StreamId:31, E:1, DepStreamId:31, Weight:8, _/binary>>,
Skip
) ->
Priority = #{
exclusive => bit_to_bool(E), stream_dependency => DepStreamId, weight => Weight + 1
},
{ok, {priority, StreamId, Priority}, Skip + ?FRAME_HEADER_SIZE + 5};
decode_frame(
<<Len:24, ?FRAME_PRIORITY:8, _:8, _:1, StreamId:31, _:Len/binary, _/binary>>, _Skip
) when
Len =/= 5
->
{error,
{stream_error, StreamId, frame_size_error,
<<"PRIORITY frame MUST be 5 bytes (RFC 9113 Section 6.3)">>}};
decode_frame(<<4:24, ?FRAME_RST_STREAM:8, _:8, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error,
<<"RST_STREAM frame MUST be associated with a stream (RFC 9113 Section 6.4)">>}};
decode_frame(<<4:24, ?FRAME_RST_STREAM:8, _:8, _:1, StreamId:31, ErrorCode:32, _/binary>>, Skip) ->
{ok, {rst_stream, StreamId, int_to_error_code(ErrorCode)}, Skip + ?FRAME_HEADER_SIZE + 4};
decode_frame(<<Len:24, ?FRAME_RST_STREAM:8, _:8, _:32, _/binary>>, _Skip) when Len =/= 4 ->
{error,
{connection_error, frame_size_error,
<<"RST_STREAM frame MUST be 4 bytes (RFC 9113 Section 6.4)">>}};
decode_frame(<<_:24, ?FRAME_SETTINGS:8, _:8, _:1, StreamId:31, _/binary>>, _Skip) when
StreamId =/= 0
->
{error,
{connection_error, protocol_error,
<<"SETTINGS frame MUST NOT be associated with a stream (RFC 9113 Section 6.5)">>}};
decode_frame(<<0:24, ?FRAME_SETTINGS:8, Flags:8, _:1, 0:31, _/binary>>, Skip) when
Flags band ?FLAG_ACK =/= 0
->
{ok, settings_ack, Skip + ?FRAME_HEADER_SIZE};
decode_frame(<<Len:24, ?FRAME_SETTINGS:8, Flags:8, _:1, 0:31, _/binary>>, _Skip) when
Flags band ?FLAG_ACK =/= 0, Len =/= 0
->
{error,
{connection_error, frame_size_error,
<<"SETTINGS ACK frame MUST have length 0 (RFC 9113 Section 6.5)">>}};
decode_frame(<<Len:24, ?FRAME_SETTINGS:8, _:8, _:1, 0:31, _/binary>>, _Skip) when Len rem 6 =/= 0 ->
{error,
{connection_error, frame_size_error,
<<"SETTINGS frame length MUST be multiple of 6 (RFC 9113 Section 6.5)">>}};
decode_frame(<<Len:24, ?FRAME_SETTINGS:8, _:8, _:1, 0:31, Payload:Len/binary, _/binary>>, Skip) ->
case decode_settings_payload(Payload, #{}) of
{ok, Settings} -> {ok, {settings, Settings}, Skip + ?FRAME_HEADER_SIZE + Len};
{error, _} = Error -> Error
end;
decode_frame(<<_:24, ?FRAME_PUSH_PROMISE:8, _:8, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error,
<<"PUSH_PROMISE frame MUST be associated with a stream (RFC 9113 Section 6.6)">>}};
decode_frame(<<Len:24, ?FRAME_PUSH_PROMISE:8, _:8, _:32, _/binary>>, _Skip) when Len < 4 ->
{error,
{connection_error, frame_size_error,
<<"PUSH_PROMISE frame MUST have length >= 4 (RFC 9113 Section 6.6)">>}};
decode_frame(<<Len:24, ?FRAME_PUSH_PROMISE:8, Flags:8, _:32, _/binary>>, _Skip) when
Flags band ?FLAG_PADDED =/= 0, Len < 5
->
{error,
{connection_error, frame_size_error,
<<"PUSH_PROMISE frame with padding MUST have length >= 5 (RFC 9113 Section 6.6)">>}};
decode_frame(
<<Len0:24, ?FRAME_PUSH_PROMISE:8, Flags:8, _:1, StreamId:31, _:1, PromisedStreamId:31,
Rest0/binary>>,
Skip
) when
Flags band ?FLAG_PADDED =:= 0
->
DataLen = Len0 - 4,
<<HeaderBlock:DataLen/binary, _/binary>> = Rest0,
EndHeaders = end_headers_to_fin(Flags),
{ok, {push_promise, StreamId, EndHeaders, PromisedStreamId, HeaderBlock},
Skip + ?FRAME_HEADER_SIZE + Len0};
decode_frame(
<<Len0:24, ?FRAME_PUSH_PROMISE:8, Flags:8, _:1, StreamId:31, PadLen:8, _:1, PromisedStreamId:31,
Rest0/binary>>,
Skip
) when
Flags band ?FLAG_PADDED =/= 0
->
DataLen = Len0 - PadLen - 5,
maybe
true ?= PadLen < Len0 - 4,
<<HeaderBlock:DataLen/binary, Padding:PadLen/binary, _/binary>> = Rest0,
true ?= is_zero_padding(Padding),
EndHeaders = end_headers_to_fin(Flags),
{ok, {push_promise, StreamId, EndHeaders, PromisedStreamId, HeaderBlock},
Skip + ?FRAME_HEADER_SIZE + Len0}
else
false ->
{error,
{connection_error, protocol_error,
<<"Padding length exceeds frame payload (RFC 9113 Section 6.6)">>}}
end;
decode_frame(<<8:24, ?FRAME_PING:8, _:8, _:1, StreamId:31, _/binary>>, _Skip) when StreamId =/= 0 ->
{error,
{connection_error, protocol_error,
<<"PING frame MUST NOT be associated with a stream (RFC 9113 Section 6.7)">>}};
decode_frame(<<Len:24, ?FRAME_PING:8, _:8, _:32, _/binary>>, _Skip) when Len =/= 8 ->
{error,
{connection_error, frame_size_error,
<<"PING frame MUST be 8 bytes (RFC 9113 Section 6.7)">>}};
decode_frame(<<8:24, ?FRAME_PING:8, Flags:8, _:1, 0:31, OpaqueData:8/binary, _/binary>>, Skip) when
Flags band ?FLAG_ACK =/= 0
->
{ok, {ping_ack, OpaqueData}, Skip + ?FRAME_HEADER_SIZE + 8};
decode_frame(<<8:24, ?FRAME_PING:8, _:8, _:1, 0:31, OpaqueData:8/binary, _/binary>>, Skip) ->
{ok, {ping, OpaqueData}, Skip + ?FRAME_HEADER_SIZE + 8};
decode_frame(<<_:24, ?FRAME_GOAWAY:8, _:8, _:1, StreamId:31, _/binary>>, _Skip) when
StreamId =/= 0
->
{error,
{connection_error, protocol_error,
<<"GOAWAY frame MUST NOT be associated with a stream (RFC 9113 Section 6.8)">>}};
decode_frame(<<Len:24, ?FRAME_GOAWAY:8, _:8, _:32, _/binary>>, _Skip) when Len < 8 ->
{error,
{connection_error, frame_size_error,
<<"GOAWAY frame MUST have length >= 8 (RFC 9113 Section 6.8)">>}};
decode_frame(
<<Len0:24, ?FRAME_GOAWAY:8, _:8, _:1, 0:31, _:1, LastStreamId:31, ErrorCode:32, Rest0/binary>>,
Skip
) ->
DebugLen = Len0 - 8,
<<DebugData:DebugLen/binary, _/binary>> = Rest0,
{ok, {goaway, LastStreamId, int_to_error_code(ErrorCode), DebugData},
Skip + ?FRAME_HEADER_SIZE + Len0};
decode_frame(<<Len:24, ?FRAME_WINDOW_UPDATE:8, _:8, _:32, _/binary>>, _Skip) when Len =/= 4 ->
{error,
{connection_error, frame_size_error,
<<"WINDOW_UPDATE frame MUST be 4 bytes (RFC 9113 Section 6.9)">>}};
decode_frame(<<4:24, ?FRAME_WINDOW_UPDATE:8, _:8, _:1, 0:31, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error,
<<"WINDOW_UPDATE increment MUST NOT be 0 (RFC 9113 Section 6.9)">>}};
decode_frame(<<4:24, ?FRAME_WINDOW_UPDATE:8, _:8, _:1, 0:31, _:1, Increment:31, _/binary>>, Skip) ->
{ok, {window_update, Increment}, Skip + ?FRAME_HEADER_SIZE + 4};
decode_frame(<<4:24, ?FRAME_WINDOW_UPDATE:8, _:8, _:1, StreamId:31, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{stream_error, StreamId, protocol_error,
<<"WINDOW_UPDATE increment MUST NOT be 0 (RFC 9113 Section 6.9)">>}};
decode_frame(
<<4:24, ?FRAME_WINDOW_UPDATE:8, _:8, _:1, StreamId:31, _:1, Increment:31, _/binary>>, Skip
) ->
{ok, {window_update, StreamId, Increment}, Skip + ?FRAME_HEADER_SIZE + 4};
decode_frame(<<_:24, ?FRAME_CONTINUATION:8, _:8, _:1, 0:31, _/binary>>, _Skip) ->
{error,
{connection_error, protocol_error,
<<"CONTINUATION frame MUST be associated with a stream (RFC 9113 Section 6.10)">>}};
decode_frame(
<<Len:24, ?FRAME_CONTINUATION:8, Flags:8, _:1, StreamId:31, HeaderBlock:Len/binary, _/binary>>,
Skip
) ->
EndHeaders = end_headers_to_fin(Flags),
{ok, {continuation, StreamId, EndHeaders, HeaderBlock}, Skip + ?FRAME_HEADER_SIZE + Len};
decode_frame(<<Len:24, Type:8, _:8, _:32, _:Len/binary, _/binary>>, Skip) when
Type > ?FRAME_CONTINUATION
->
{ok, {unknown, Type}, Skip + ?FRAME_HEADER_SIZE + Len};
decode_frame(_, _Skip) ->
{more, ?FRAME_HEADER_SIZE}.
-spec decode_settings_payload(binary(), nhttp_h2:settings()) ->
{ok, nhttp_h2:settings()} | {error, decode_error()}.
decode_settings_payload(<<>>, Settings) ->
{ok, Settings};
decode_settings_payload(<<?SETTINGS_HEADER_TABLE_SIZE:16, Value:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{header_table_size => Value});
decode_settings_payload(<<?SETTINGS_ENABLE_PUSH:16, 0:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{enable_push => false});
decode_settings_payload(<<?SETTINGS_ENABLE_PUSH:16, 1:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{enable_push => true});
decode_settings_payload(<<?SETTINGS_ENABLE_PUSH:16, _:32, _/binary>>, _Settings) ->
{error,
{connection_error, protocol_error,
<<"SETTINGS_ENABLE_PUSH MUST be 0 or 1 (RFC 9113 Section 6.5.2)">>}};
decode_settings_payload(<<?SETTINGS_MAX_CONCURRENT_STREAMS:16, Value:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{max_concurrent_streams => Value});
decode_settings_payload(<<?SETTINGS_INITIAL_WINDOW_SIZE:16, Value:32, _/binary>>, _Settings) when
Value > ?MAX_WINDOW_SIZE
->
{error,
{connection_error, flow_control_error,
<<"SETTINGS_INITIAL_WINDOW_SIZE exceeds maximum (RFC 9113 Section 6.5.2)">>}};
decode_settings_payload(<<?SETTINGS_INITIAL_WINDOW_SIZE:16, Value:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{initial_window_size => Value});
decode_settings_payload(<<?SETTINGS_MAX_FRAME_SIZE:16, Value:32, _/binary>>, _Settings) when
Value < ?MIN_MAX_FRAME_SIZE
->
{error,
{connection_error, protocol_error,
<<"SETTINGS_MAX_FRAME_SIZE below minimum (RFC 9113 Section 6.5.2)">>}};
decode_settings_payload(<<?SETTINGS_MAX_FRAME_SIZE:16, Value:32, _/binary>>, _Settings) when
Value > ?MAX_FRAME_SIZE_LIMIT
->
{error,
{connection_error, protocol_error,
<<"SETTINGS_MAX_FRAME_SIZE exceeds maximum (RFC 9113 Section 6.5.2)">>}};
decode_settings_payload(<<?SETTINGS_MAX_FRAME_SIZE:16, Value:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{max_frame_size => Value});
decode_settings_payload(<<?SETTINGS_MAX_HEADER_LIST_SIZE:16, Value:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{max_header_list_size => Value});
decode_settings_payload(<<?SETTINGS_ENABLE_CONNECT_PROTOCOL:16, 0:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{enable_connect_protocol => false});
decode_settings_payload(<<?SETTINGS_ENABLE_CONNECT_PROTOCOL:16, 1:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings#{enable_connect_protocol => true});
decode_settings_payload(<<?SETTINGS_ENABLE_CONNECT_PROTOCOL:16, _:32, _/binary>>, _Settings) ->
{error,
{connection_error, protocol_error,
<<"SETTINGS_ENABLE_CONNECT_PROTOCOL MUST be 0 or 1 (RFC 8441 Section 3)">>}};
decode_settings_payload(<<_:16, _:32, Rest/binary>>, Settings) ->
decode_settings_payload(Rest, Settings).
-spec encode_continuation_bin(nhttp_lib:stream_id(), nhttp_h2:fin(), binary()) -> iodata().
encode_continuation_bin(StreamId, EndHeaders, <<HeaderBlock/binary>>) ->
Len = byte_size(HeaderBlock),
Flags = fin_to_end_headers(EndHeaders),
[<<Len:24, ?FRAME_CONTINUATION:8, Flags:8, 0:1, StreamId:31>>, HeaderBlock].
-spec encode_setting(atom(), term(), iodata()) -> iodata().
encode_setting(header_table_size, Value, Acc) ->
[<<?SETTINGS_HEADER_TABLE_SIZE:16, Value:32>> | Acc];
encode_setting(enable_push, true, Acc) ->
[<<?SETTINGS_ENABLE_PUSH:16, 1:32>> | Acc];
encode_setting(enable_push, false, Acc) ->
[<<?SETTINGS_ENABLE_PUSH:16, 0:32>> | Acc];
encode_setting(max_concurrent_streams, infinity, Acc) ->
Acc;
encode_setting(max_concurrent_streams, Value, Acc) ->
[<<?SETTINGS_MAX_CONCURRENT_STREAMS:16, Value:32>> | Acc];
encode_setting(initial_window_size, Value, Acc) ->
[<<?SETTINGS_INITIAL_WINDOW_SIZE:16, Value:32>> | Acc];
encode_setting(max_frame_size, Value, Acc) ->
[<<?SETTINGS_MAX_FRAME_SIZE:16, Value:32>> | Acc];
encode_setting(max_header_list_size, infinity, Acc) ->
Acc;
encode_setting(max_header_list_size, Value, Acc) ->
[<<?SETTINGS_MAX_HEADER_LIST_SIZE:16, Value:32>> | Acc];
encode_setting(enable_connect_protocol, true, Acc) ->
[<<?SETTINGS_ENABLE_CONNECT_PROTOCOL:16, 1:32>> | Acc];
encode_setting(enable_connect_protocol, false, Acc) ->
[<<?SETTINGS_ENABLE_CONNECT_PROTOCOL:16, 0:32>> | Acc];
encode_setting(_, _, Acc) ->
Acc.
-spec end_headers_to_fin(non_neg_integer()) -> nhttp_h2:fin().
end_headers_to_fin(Flags) when Flags band ?FLAG_END_HEADERS =/= 0 -> fin;
end_headers_to_fin(_) -> nofin.
-spec end_stream_to_fin(non_neg_integer()) -> nhttp_h2:fin().
end_stream_to_fin(Flags) when Flags band ?FLAG_END_STREAM =/= 0 -> fin;
end_stream_to_fin(_) -> nofin.
-spec error_code_to_int(nhttp_h2:error_code()) -> non_neg_integer().
error_code_to_int(no_error) -> 16#00;
error_code_to_int(protocol_error) -> 16#01;
error_code_to_int(internal_error) -> 16#02;
error_code_to_int(flow_control_error) -> 16#03;
error_code_to_int(settings_timeout) -> 16#04;
error_code_to_int(stream_closed) -> 16#05;
error_code_to_int(frame_size_error) -> 16#06;
error_code_to_int(refused_stream) -> 16#07;
error_code_to_int(cancel) -> 16#08;
error_code_to_int(compression_error) -> 16#09;
error_code_to_int(connect_error) -> 16#0a;
error_code_to_int(enhance_your_calm) -> 16#0b;
error_code_to_int(inadequate_security) -> 16#0c;
error_code_to_int(http_1_1_required) -> 16#0d.
-spec fin_to_end_headers(nhttp_h2:fin()) -> 0 | 4.
fin_to_end_headers(fin) -> ?FLAG_END_HEADERS;
fin_to_end_headers(nofin) -> 0.
-spec fin_to_end_stream(nhttp_h2:fin()) -> 0 | 1.
fin_to_end_stream(fin) -> ?FLAG_END_STREAM;
fin_to_end_stream(nofin) -> 0.
-spec int_to_error_code(non_neg_integer()) -> nhttp_h2:error_code().
int_to_error_code(16#00) -> no_error;
int_to_error_code(16#01) -> protocol_error;
int_to_error_code(16#02) -> internal_error;
int_to_error_code(16#03) -> flow_control_error;
int_to_error_code(16#04) -> settings_timeout;
int_to_error_code(16#05) -> stream_closed;
int_to_error_code(16#06) -> frame_size_error;
int_to_error_code(16#07) -> refused_stream;
int_to_error_code(16#08) -> cancel;
int_to_error_code(16#09) -> compression_error;
int_to_error_code(16#0a) -> connect_error;
int_to_error_code(16#0b) -> enhance_your_calm;
int_to_error_code(16#0c) -> inadequate_security;
int_to_error_code(16#0d) -> http_1_1_required;
int_to_error_code(_) -> internal_error.
-spec is_zero_padding(binary()) -> boolean().
is_zero_padding(Padding) ->
Padding =:= <<0:(byte_size(Padding) * 8)>>.