aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy_http2.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/cowboy_http2.erl')
-rw-r--r--src/cowboy_http2.erl90
1 files changed, 79 insertions, 11 deletions
diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl
index ffcc17f..5c71628 100644
--- a/src/cowboy_http2.erl
+++ b/src/cowboy_http2.erl
@@ -369,6 +369,11 @@ info(State=#state{handler=Handler, streams=Streams}, StreamID, Msg) ->
commands(State, Stream, []) ->
after_commands(State, Stream);
+%% Error responses are sent only if a response wasn't sent already.
+commands(State, Stream=#stream{local=idle}, [{error_response, StatusCode, Headers, Body}|Tail]) ->
+ commands(State, Stream, [{response, StatusCode, Headers, Body}|Tail]);
+commands(State, Stream, [{error_response, _, _, _}|Tail]) ->
+ commands(State, Stream, Tail);
%% Send response headers.
%%
%% @todo Kill the stream if it sent a response when one has already been sent.
@@ -376,7 +381,7 @@ commands(State, Stream, []) ->
%% @todo Same two things above apply to DATA, possibly promise too.
commands(State=#state{socket=Socket, transport=Transport, encode_state=EncodeState0},
Stream=#stream{id=StreamID, local=idle}, [{response, StatusCode, Headers0, Body}|Tail]) ->
- Headers = Headers0#{<<":status">> => integer_to_binary(StatusCode)},
+ Headers = Headers0#{<<":status">> => status(StatusCode)},
{HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0),
case Body of
<<>> ->
@@ -387,17 +392,18 @@ commands(State=#state{socket=Socket, transport=Transport, encode_state=EncodeSta
commands(State#state{encode_state=EncodeState}, Stream#stream{local=nofin},
[{sendfile, fin, O, B, P}|Tail]);
_ ->
- Transport:send(Socket, [
- cow_http2:headers(StreamID, nofin, HeaderBlock),
- cow_http2:data(StreamID, fin, Body)
- ]),
+ Transport:send(Socket, cow_http2:headers(StreamID, nofin, HeaderBlock)),
+ %% @todo 16384 is the default SETTINGS_MAX_FRAME_SIZE.
+ %% Use the length set by the server instead, if any.
+ %% @todo Would be better if we didn't have to convert to binary.
+ send_data(Socket, Transport, StreamID, fin, iolist_to_binary(Body), 16384),
commands(State#state{encode_state=EncodeState}, Stream#stream{local=fin}, Tail)
end;
%% @todo response when local!=idle
%% Send response headers and initiate chunked encoding.
commands(State=#state{socket=Socket, transport=Transport, encode_state=EncodeState0},
Stream=#stream{id=StreamID, local=idle}, [{headers, StatusCode, Headers0}|Tail]) ->
- Headers = Headers0#{<<":status">> => integer_to_binary(StatusCode)},
+ Headers = Headers0#{<<":status">> => status(StatusCode)},
{HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0),
Transport:send(Socket, cow_http2:headers(StreamID, nofin, HeaderBlock)),
commands(State#state{encode_state=EncodeState}, Stream#stream{local=nofin}, Tail);
@@ -417,7 +423,9 @@ commands(State=#state{socket=Socket, transport=Transport}, Stream=#stream{id=Str
[{data, IsFin, Data}|Tail]) ->
Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data)),
commands(State, Stream#stream{local=IsFin}, Tail);
+
%% @todo data when local!=nofin
+
%% Send a file.
%%
%% @todo This implementation is terrible. A good implementation would
@@ -441,11 +449,20 @@ commands(State=#state{socket=Socket, transport=Transport}, Stream=#stream{id=Str
%% end up with an infinite loop of promises.
commands(State0=#state{socket=Socket, transport=Transport, server_streamid=PromisedStreamID,
encode_state=EncodeState0}, Stream=#stream{id=StreamID},
- [{promise, Method, Scheme, Authority, Path, Headers0}|Tail]) ->
+ [{push, Method, Scheme, Host, Port, Path, Qs, Headers0}|Tail]) ->
+ Authority = case {Scheme, Port} of
+ {<<"http">>, 80} -> Host;
+ {<<"https">>, 443} -> Host;
+ _ -> [Host, $:, integer_to_binary(Port)]
+ end,
+ PathWithQs = case Qs of
+ <<>> -> Path;
+ _ -> [Path, $?, Qs]
+ end,
Headers = Headers0#{<<":method">> => Method,
<<":scheme">> => Scheme,
<<":authority">> => Authority,
- <<":path">> => Path},
+ <<":path">> => PathWithQs},
{HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0),
Transport:send(Socket, cow_http2:push_promise(StreamID, PromisedStreamID, HeaderBlock)),
%% @todo iolist_to_binary(HeaderBlock) isn't optimal. Need a shortcut.
@@ -484,6 +501,22 @@ after_commands(State=#state{streams=Streams0}, Stream=#stream{id=StreamID}) ->
Streams = lists:keystore(StreamID, #stream.id, Streams0, Stream),
State#state{streams=Streams}.
+status(Status) when is_integer(Status) ->
+ integer_to_binary(Status);
+status(<< H, T, U, _/bits >>) when H >= $1, H =< $9, T >= $0, T =< $9, U >= $0, U =< $9 ->
+ << H, T, U >>.
+
+%% This same function is found in gun_http2.
+send_data(Socket, Transport, StreamID, IsFin, Data, Length) ->
+ if
+ Length < byte_size(Data) ->
+ << Payload:Length/binary, Rest/bits >> = Data,
+ Transport:send(Socket, cow_http2:data(StreamID, nofin, Payload)),
+ send_data(Socket, Transport, StreamID, IsFin, Rest, Length);
+ true ->
+ Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data))
+ end.
+
terminate(#state{socket=Socket, transport=Transport, handler=Handler,
streams=Streams, children=Children}, Reason) ->
%% @todo Send GOAWAY frame; need to keep track of last good stream id; how?
@@ -511,6 +544,23 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, peer=Peer
<<":path">> := PathWithQs}, DecodeState} ->
State = State0#state{decode_state=DecodeState},
Headers = maps:without([<<":method">>, <<":scheme">>, <<":authority">>, <<":path">>], Headers0),
+ BodyLength = case Headers of
+ _ when IsFin =:= fin ->
+ 0;
+ #{<<"content-length">> := <<"0">>} ->
+ 0;
+ #{<<"content-length">> := BinLength} ->
+ Length = try
+ cow_http_hd:parse_content_length(BinLength)
+ catch _:_ ->
+ terminate(State0, {stream_error, StreamID, protocol_error,
+ ''}) %% @todo
+ %% @todo Err should terminate here...
+ end,
+ Length;
+ _ ->
+ undefined
+ end,
{Host, Port} = cow_http_hd:parse_host(Authority),
{Path, Qs} = cow_http:parse_fullpath(PathWithQs),
Req = #{
@@ -527,7 +577,8 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, peer=Peer
version => 'HTTP/2',
headers => Headers,
- has_body => IsFin =:= nofin
+ has_body => IsFin =:= nofin,
+ body_length => BodyLength
%% @todo multipart? keep state separate
%% meta values (cowboy_websocket, cowboy_rest)
@@ -609,9 +660,26 @@ stream_terminate_children([Child|Tail], StreamID, Acc) ->
headers_decode(HeaderBlock, DecodeState0) ->
{Headers, DecodeState} = cow_hpack:decode(HeaderBlock, DecodeState0),
- {maps:from_list(Headers), DecodeState}.
+ {headers_to_map(Headers, #{}), DecodeState}.
-%% @todo We will need to special-case the set-cookie header here.
+%% This function is necessary to properly handle duplicate headers
+%% and the special-case cookie header.
+headers_to_map([], Acc) ->
+ Acc;
+headers_to_map([{Name, Value}|Tail], Acc0) ->
+ Acc = case Acc0 of
+ %% The cookie header does not use proper HTTP header lists.
+ #{Name := Value0} when Name =:= <<"cookie">> -> Acc0#{Name => << Value0/binary, "; ", Value/binary >>};
+ #{Name := Value0} -> Acc0#{Name => << Value0/binary, ", ", Value/binary >>};
+ _ -> Acc0#{Name => Value}
+ end,
+ headers_to_map(Tail, Acc).
+
+%% The set-cookie header is special; we can only send one cookie per header.
+headers_encode(Headers0=#{<<"set-cookie">> := SetCookies}, EncodeState) ->
+ Headers1 = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)),
+ Headers = Headers1 ++ [{<<"set-cookie">>, Value} || Value <- SetCookies],
+ cow_hpack:encode(Headers, EncodeState);
headers_encode(Headers0, EncodeState) ->
Headers = maps:to_list(Headers0),
cow_hpack:encode(Headers, EncodeState).