cow_http3_machine.erl 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. %% Copyright (c) 2023, Loïc Hoguin <essen@ninenines.eu>
  2. %%
  3. %% Permission to use, copy, modify, and/or distribute this software for any
  4. %% purpose with or without fee is hereby granted, provided that the above
  5. %% copyright notice and this permission notice appear in all copies.
  6. %%
  7. %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  8. %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  9. %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  10. %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  11. %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  12. %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  13. %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  14. -module(cow_http3_machine).
  15. -export([init/2]).
  16. -export([init_unidi_local_streams/7]).
  17. -export([init_stream/5]).
  18. -export([set_unidi_remote_stream_type/3]).
  19. -export([frame/4]).
  20. -export([prepare_headers/5]).
  21. -record(stream, {
  22. ref :: any(), %% @todo specs
  23. id = undefined :: non_neg_integer(), %% @todo spec from quicer?
  24. dir :: unidi_local | unidi_remote | bidi,
  25. type :: undefined | req | control | push | encoder | decoder,
  26. %% Further fields are only used by bidi streams.
  27. %% @todo Perhaps have two different records?
  28. %% Request method.
  29. method = undefined :: binary(),
  30. %% Whether we finished sending data.
  31. local = idle :: idle | cow_http2:fin(),
  32. %% Whether we finished receiving data.
  33. remote = idle :: idle | cow_http2:fin(),
  34. %% Size expected and read from the request body.
  35. remote_expected_size = undefined :: undefined | non_neg_integer(),
  36. remote_read_size = 0 :: non_neg_integer(),
  37. %% Unparsed te header. Used to know if we can send trailers.
  38. %% Note that we can always send trailers to the server.
  39. te :: undefined | binary()
  40. }).
  41. -type stream() :: #stream{}.
  42. -record(http3_machine, {
  43. %% Whether the HTTP/3 endpoint is a client or a server.
  44. mode :: client | server,
  45. %% Quick pointers for commonly used streams.
  46. local_encoder_ref :: any(), %% @todo specs
  47. local_decoder_ref :: any(), %% @todo specs
  48. %% Currently active HTTP/3 streams. Streams may be initiated either
  49. %% by the client or by the server through PUSH_PROMISE frames.
  50. streams = #{} :: #{reference() => stream()},
  51. %% QPACK decoding and encoding state.
  52. decode_state = cow_qpack:init() :: cow_qpack:state(),
  53. encode_state = cow_qpack:init() :: cow_qpack:state()
  54. }).
  55. -spec init(_, _) -> _. %% @todo
  56. init(Mode, _Opts) ->
  57. {ok, <<>>, #http3_machine{mode=Mode}}.
  58. -spec init_unidi_local_streams(_, _, _, _, _ ,_ ,_) -> _. %% @todo
  59. init_unidi_local_streams(ControlRef, ControlID,
  60. EncoderRef, EncoderID, DecoderRef, DecoderID,
  61. State=#http3_machine{streams=Streams}) ->
  62. State#http3_machine{
  63. local_encoder_ref=EncoderRef,
  64. local_decoder_ref=DecoderRef,
  65. streams=Streams#{
  66. ControlRef => #stream{ref=ControlRef, id=ControlID, dir=unidi_local, type=control},
  67. EncoderRef => #stream{ref=EncoderRef, id=EncoderID, dir=unidi_local, type=encoder},
  68. DecoderRef => #stream{ref=DecoderRef, id=DecoderID, dir=unidi_local, type=decoder}
  69. }}.
  70. -spec init_stream(_, _, _, _, _) -> _. %% @todo
  71. init_stream(StreamRef, StreamID, StreamDir, StreamType,
  72. State=#http3_machine{streams=Streams}) ->
  73. State#http3_machine{streams=Streams#{StreamRef => #stream{
  74. ref=StreamRef, id=StreamID, dir=StreamDir, type=StreamType}}}.
  75. -spec set_unidi_remote_stream_type(_, _, _) -> _. %% @todo
  76. set_unidi_remote_stream_type(StreamRef, Type,
  77. State=#http3_machine{streams=Streams}) ->
  78. #{StreamRef := Stream} = Streams,
  79. State#http3_machine{streams=Streams#{StreamRef => Stream#stream{type=Type}}}.
  80. -spec frame(_, _, _, _) -> _. %% @todo
  81. frame(Frame, IsFin, StreamRef, State) ->
  82. case element(1, Frame) of
  83. headers -> headers_frame(Frame, IsFin, StreamRef, State);
  84. settings -> {ok, State} %% @todo
  85. end.
  86. headers_frame(Frame, IsFin, StreamRef, State=#http3_machine{mode=Mode}) ->
  87. case Mode of
  88. server -> server_headers_frame(Frame, IsFin, StreamRef, State)
  89. end.
  90. %% @todo We may receive HEADERS before or after DATA.
  91. server_headers_frame(Frame, IsFin, StreamRef, State=#http3_machine{streams=Streams}) ->
  92. case Streams of
  93. %% Headers.
  94. #{StreamRef := Stream=#stream{remote=idle}} ->
  95. headers_decode(Frame, IsFin, Stream, State, request);
  96. %% Trailers.
  97. %% @todo Error out if we didn't get the full body.
  98. #{StreamRef := _Stream=#stream{remote=nofin}} ->
  99. todo_trailers; %% @todo
  100. %% Additional frame received after trailers.
  101. #{StreamRef := _Stream=#stream{remote=fin}} ->
  102. todo_error %% @todo
  103. end.
  104. %% @todo Check whether connection_error or stream_error fits better.
  105. headers_decode({headers, EncodedFieldSection}, IsFin, Stream=#stream{id=StreamID},
  106. State=#http3_machine{decode_state=DecodeState0}, Type) ->
  107. try cow_qpack:decode_field_section(EncodedFieldSection, StreamID, DecodeState0) of
  108. {ok, Headers, DecData, DecodeState} ->
  109. headers_pseudo_headers(Stream,
  110. State#http3_machine{decode_state=DecodeState}, IsFin, Type, DecData, Headers);
  111. {error, Reason, Human} ->
  112. {error, {connection_error, Reason, Human}, State}
  113. catch _:_ ->
  114. {error, {connection_error, qpack_decompression_failed,
  115. 'Error while trying to decode QPACK-encoded header block. (RFC9204 6)'},
  116. State}
  117. end.
  118. %% @todo Much of the headers handling past this point is common between h2 and h3.
  119. headers_pseudo_headers(Stream, State,%=#http3_machine{local_settings=LocalSettings},
  120. IsFin, Type, DecData, Headers0) when Type =:= request ->%; Type =:= push_promise ->
  121. % IsExtendedConnectEnabled = maps:get(enable_connect_protocol, LocalSettings, false),
  122. case request_pseudo_headers(Headers0, #{}) of
  123. %% Extended CONNECT method (RFC9220).
  124. % {ok, PseudoHeaders=#{method := <<"CONNECT">>, scheme := _,
  125. % authority := _, path := _, protocol := _}, Headers}
  126. % when IsExtendedConnectEnabled ->
  127. % headers_regular_headers(Frame, State, Type, Stream, PseudoHeaders, Headers);
  128. % {ok, #{method := <<"CONNECT">>, scheme := _,
  129. % authority := _, path := _}, _}
  130. % when IsExtendedConnectEnabled ->
  131. % headers_malformed(Stream, State,
  132. % 'The :protocol pseudo-header MUST be sent with an extended CONNECT. (RFC8441 4)');
  133. {ok, #{protocol := _}, _} ->
  134. headers_malformed(Stream, State,
  135. 'The :protocol pseudo-header is only defined for the extended CONNECT. (RFC8441 4)');
  136. %% Normal CONNECT (no scheme/path).
  137. {ok, PseudoHeaders=#{method := <<"CONNECT">>, authority := _}, Headers}
  138. when map_size(PseudoHeaders) =:= 2 ->
  139. headers_regular_headers(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
  140. {ok, #{method := <<"CONNECT">>}, _} ->
  141. headers_malformed(Stream, State,
  142. 'CONNECT requests only use the :method and :authority pseudo-headers. (RFC7540 8.3)');
  143. %% Other requests.
  144. {ok, PseudoHeaders=#{method := _, scheme := _, path := _}, Headers} ->
  145. headers_regular_headers(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
  146. {ok, _, _} ->
  147. headers_malformed(Stream, State,
  148. 'A required pseudo-header was not found. (RFC7540 8.1.2.3)');
  149. {error, HumanReadable} ->
  150. headers_malformed(Stream, State, HumanReadable)
  151. end.
  152. %% @todo This function was copy pasted from cow_http2_machine. Export instead.
  153. request_pseudo_headers([{<<":method">>, _}|_], #{method := _}) ->
  154. {error, 'Multiple :method pseudo-headers were found. (RFC7540 8.1.2.3)'};
  155. request_pseudo_headers([{<<":method">>, Method}|Tail], PseudoHeaders) ->
  156. request_pseudo_headers(Tail, PseudoHeaders#{method => Method});
  157. request_pseudo_headers([{<<":scheme">>, _}|_], #{scheme := _}) ->
  158. {error, 'Multiple :scheme pseudo-headers were found. (RFC7540 8.1.2.3)'};
  159. request_pseudo_headers([{<<":scheme">>, Scheme}|Tail], PseudoHeaders) ->
  160. request_pseudo_headers(Tail, PseudoHeaders#{scheme => Scheme});
  161. request_pseudo_headers([{<<":authority">>, _}|_], #{authority := _}) ->
  162. {error, 'Multiple :authority pseudo-headers were found. (RFC7540 8.1.2.3)'};
  163. request_pseudo_headers([{<<":authority">>, Authority}|Tail], PseudoHeaders) ->
  164. request_pseudo_headers(Tail, PseudoHeaders#{authority => Authority});
  165. request_pseudo_headers([{<<":path">>, _}|_], #{path := _}) ->
  166. {error, 'Multiple :path pseudo-headers were found. (RFC7540 8.1.2.3)'};
  167. request_pseudo_headers([{<<":path">>, Path}|Tail], PseudoHeaders) ->
  168. request_pseudo_headers(Tail, PseudoHeaders#{path => Path});
  169. request_pseudo_headers([{<<":protocol">>, _}|_], #{protocol := _}) ->
  170. {error, 'Multiple :protocol pseudo-headers were found. (RFC7540 8.1.2.3)'};
  171. request_pseudo_headers([{<<":protocol">>, Protocol}|Tail], PseudoHeaders) ->
  172. request_pseudo_headers(Tail, PseudoHeaders#{protocol => Protocol});
  173. request_pseudo_headers([{<<":", _/bits>>, _}|_], _) ->
  174. {error, 'An unknown or invalid pseudo-header was found. (RFC7540 8.1.2.1)'};
  175. request_pseudo_headers(Headers, PseudoHeaders) ->
  176. {ok, PseudoHeaders, Headers}.
  177. headers_malformed(#stream{id=StreamID}, State, HumanReadable) ->
  178. {error, {stream_error, StreamID, h3_message_error, HumanReadable}, State}.
  179. %% Rejecting invalid regular headers might be a bit too strong for clients.
  180. headers_regular_headers(Stream=#stream{id=_StreamID},
  181. State, IsFin, Type, DecData, PseudoHeaders, Headers) ->
  182. case regular_headers(Headers, Type) of
  183. ok when Type =:= request ->
  184. request_expected_size(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers);
  185. % ok when Type =:= push_promise ->
  186. % push_promise_frame(Frame, State, Stream, PseudoHeaders, Headers);
  187. % ok when Type =:= response ->
  188. % response_expected_size(Frame, State, Type, Stream, PseudoHeaders, Headers);
  189. % ok when Type =:= trailers ->
  190. % trailers_frame(Frame, State, Stream, Headers);
  191. {error, HumanReadable} when Type =:= request ->
  192. headers_malformed(Stream, State, HumanReadable)%;
  193. % {error, HumanReadable} ->
  194. % stream_reset(StreamID, State, protocol_error, HumanReadable)
  195. end.
  196. %% @todo This function was copy pasted from cow_http2_machine. Export instead.
  197. %% @todo The error reasons refer to the h2 RFC but then again h3 doesn't cover it in as much details.
  198. regular_headers([{<<>>, _}|_], _) ->
  199. {error, 'Empty header names are not valid regular headers. (CVE-2019-9516)'};
  200. regular_headers([{<<":", _/bits>>, _}|_], _) ->
  201. {error, 'Pseudo-headers were found after regular headers. (RFC7540 8.1.2.1)'};
  202. regular_headers([{<<"connection">>, _}|_], _) ->
  203. {error, 'The connection header is not allowed. (RFC7540 8.1.2.2)'};
  204. regular_headers([{<<"keep-alive">>, _}|_], _) ->
  205. {error, 'The keep-alive header is not allowed. (RFC7540 8.1.2.2)'};
  206. regular_headers([{<<"proxy-authenticate">>, _}|_], _) ->
  207. {error, 'The proxy-authenticate header is not allowed. (RFC7540 8.1.2.2)'};
  208. regular_headers([{<<"proxy-authorization">>, _}|_], _) ->
  209. {error, 'The proxy-authorization header is not allowed. (RFC7540 8.1.2.2)'};
  210. regular_headers([{<<"transfer-encoding">>, _}|_], _) ->
  211. {error, 'The transfer-encoding header is not allowed. (RFC7540 8.1.2.2)'};
  212. regular_headers([{<<"upgrade">>, _}|_], _) ->
  213. {error, 'The upgrade header is not allowed. (RFC7540 8.1.2.2)'};
  214. regular_headers([{<<"te">>, Value}|_], request) when Value =/= <<"trailers">> ->
  215. {error, 'The te header with a value other than "trailers" is not allowed. (RFC7540 8.1.2.2)'};
  216. regular_headers([{<<"te">>, _}|_], Type) when Type =/= request ->
  217. {error, 'The te header is only allowed in request headers. (RFC7540 8.1.2.2)'};
  218. regular_headers([{Name, _}|Tail], Type) ->
  219. Pattern = [
  220. <<$A>>, <<$B>>, <<$C>>, <<$D>>, <<$E>>, <<$F>>, <<$G>>, <<$H>>, <<$I>>,
  221. <<$J>>, <<$K>>, <<$L>>, <<$M>>, <<$N>>, <<$O>>, <<$P>>, <<$Q>>, <<$R>>,
  222. <<$S>>, <<$T>>, <<$U>>, <<$V>>, <<$W>>, <<$X>>, <<$Y>>, <<$Z>>
  223. ],
  224. case binary:match(Name, Pattern) of
  225. nomatch -> regular_headers(Tail, Type);
  226. _ -> {error, 'Header names must be lowercase. (RFC7540 8.1.2)'}
  227. end;
  228. regular_headers([], _) ->
  229. ok.
  230. %% @todo Much of the logic can probably be put in its own function shared between h2 and h3.
  231. request_expected_size(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers) ->
  232. case [CL || {<<"content-length">>, CL} <- Headers] of
  233. [] when IsFin =:= fin ->
  234. headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, 0);
  235. [] ->
  236. headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, undefined);
  237. [<<"0">>] when IsFin =:= fin ->
  238. headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, 0);
  239. [_] when IsFin =:= fin ->
  240. headers_malformed(Stream, State,
  241. 'HEADERS frame with the END_STREAM flag contains a non-zero content-length. (RFC7540 8.1.2.6)');
  242. [BinLen] ->
  243. headers_parse_expected_size(Stream, State, IsFin, Type, DecData,
  244. PseudoHeaders, Headers, BinLen);
  245. _ ->
  246. headers_malformed(Stream, State,
  247. 'Multiple content-length headers were received. (RFC7230 3.3.2)')
  248. end.
  249. headers_parse_expected_size(Stream=#stream{id=_StreamID},
  250. State, IsFin, Type, DecData, PseudoHeaders, Headers, BinLen) ->
  251. try cow_http_hd:parse_content_length(BinLen) of
  252. Len ->
  253. headers_frame(Stream, State, IsFin, Type, DecData, PseudoHeaders, Headers, Len)
  254. catch
  255. _:_ ->
  256. HumanReadable = 'The content-length header is invalid. (RFC7230 3.3.2)',
  257. case Type of
  258. request -> headers_malformed(Stream, State, HumanReadable)%;
  259. % response -> stream_reset(StreamID, State, protocol_error, HumanReadable)
  260. end
  261. end.
  262. headers_frame(Stream0, State0=#http3_machine{local_decoder_ref=DecoderRef},
  263. IsFin, Type, DecData, PseudoHeaders, Headers, Len) ->
  264. Stream = case Type of
  265. request ->
  266. TE = case lists:keyfind(<<"te">>, 1, Headers) of
  267. {_, TE0} -> TE0;
  268. false -> undefined
  269. end,
  270. Stream0#stream{method=maps:get(method, PseudoHeaders),
  271. remote=IsFin, remote_expected_size=Len, te=TE}%;
  272. % response ->
  273. % Stream1 = case PseudoHeaders of
  274. % #{status := Status} when Status >= 100, Status =< 199 -> Stream0;
  275. % _ -> Stream0#stream{remote=IsFin, remote_expected_size=Len}
  276. % end,
  277. % {Stream1, State0}
  278. end,
  279. State = stream_store(Stream, State0),
  280. %% @todo Maybe don't return DecData if empty, but return the StreamRef with it if we must send.
  281. case DecData of
  282. <<>> ->
  283. {ok, {headers, IsFin, Headers, PseudoHeaders, Len}, State};
  284. _ ->
  285. {ok, {headers, IsFin, Headers, PseudoHeaders, Len}, {DecoderRef, DecData}, State}
  286. end.
  287. %% Functions for sending a message header or body. Note that
  288. %% this module does not send data directly, instead it returns
  289. %% a value that can then be used to send the frames.
  290. %-spec prepare_headers(cow_http2:streamid(), State, idle | cow_http2:fin(),
  291. % pseudo_headers(), cow_http:headers())
  292. % -> {ok, cow_http2:fin(), iodata(), State} when State::http2_machine().
  293. -spec prepare_headers(_, _, _, _, _) -> todo.
  294. prepare_headers(StreamRef, State=#http3_machine{encode_state=EncodeState0},
  295. IsFin0, PseudoHeaders, Headers0) ->
  296. Stream = #stream{id=StreamID, method=Method, local=idle} = stream_get(StreamRef, State),
  297. IsFin = case {IsFin0, Method} of
  298. {idle, _} -> nofin;
  299. {_, <<"HEAD">>} -> fin;
  300. _ -> IsFin0
  301. end,
  302. Headers = merge_pseudo_headers(PseudoHeaders, remove_http11_headers(Headers0)),
  303. {ok, HeaderBlock, EncData, EncodeState} = cow_qpack:encode_field_section(Headers, StreamID, EncodeState0),
  304. {ok, IsFin, HeaderBlock, EncData, stream_store(Stream#stream{local=IsFin0},
  305. State#http3_machine{encode_state=EncodeState})}.
  306. %% @todo Function copied from cow_http2_machine.
  307. remove_http11_headers(Headers) ->
  308. RemoveHeaders0 = [
  309. <<"keep-alive">>,
  310. <<"proxy-connection">>,
  311. <<"transfer-encoding">>,
  312. <<"upgrade">>
  313. ],
  314. RemoveHeaders = case lists:keyfind(<<"connection">>, 1, Headers) of
  315. false ->
  316. RemoveHeaders0;
  317. {_, ConnHd} ->
  318. %% We do not need to worry about any "close" header because
  319. %% that header name is reserved.
  320. Connection = cow_http_hd:parse_connection(ConnHd),
  321. Connection ++ [<<"connection">>|RemoveHeaders0]
  322. end,
  323. lists:filter(fun({Name, _}) ->
  324. not lists:member(Name, RemoveHeaders)
  325. end, Headers).
  326. %% @todo Function copied from cow_http2_machine.
  327. merge_pseudo_headers(PseudoHeaders, Headers0) ->
  328. lists:foldl(fun
  329. ({status, Status}, Acc) when is_integer(Status) ->
  330. [{<<":status">>, integer_to_binary(Status)}|Acc];
  331. ({Name, Value}, Acc) ->
  332. [{iolist_to_binary([$:, atom_to_binary(Name, latin1)]), Value}|Acc]
  333. end, Headers0, maps:to_list(PseudoHeaders)).
  334. %% Stream-related functions.
  335. stream_get(StreamRef, #http3_machine{streams=Streams}) ->
  336. maps:get(StreamRef, Streams, undefined).
  337. stream_store(#stream{ref=StreamRef, local=fin, remote=fin},
  338. State=#http3_machine{streams=Streams0}) ->
  339. Streams = maps:remove(StreamRef, Streams0),
  340. State#http3_machine{streams=Streams};
  341. stream_store(Stream=#stream{ref=StreamRef},
  342. State=#http3_machine{streams=Streams}) ->
  343. State#http3_machine{streams=Streams#{StreamRef => Stream}}.