diff options
Diffstat (limited to 'src/cowboy_http.erl')
-rw-r--r-- | src/cowboy_http.erl | 88 |
1 files changed, 44 insertions, 44 deletions
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index 0231def..ae42e6d 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -111,8 +111,7 @@ %% The connection will be closed after this stream. last_streamid = undefined :: pos_integer(), - %% Currently active HTTP/1.1 streams. Streams may be initiated either - %% by the client or by the server through PUSH_PROMISE frames. + %% Currently active HTTP/1.1 streams. streams = [] :: [stream()], %% Children which are in the process of shutting down. @@ -202,7 +201,7 @@ loop(State=#state{parent=Parent, socket=Socket, transport=Transport, loop(State, Buffer); %% Unknown messages. Msg -> - error_logger:error_msg("Received stray message ~p.", [Msg]), + error_logger:error_msg("Received stray message ~p.~n", [Msg]), loop(State, Buffer) %% @todo Configurable timeout. This should be a global inactivity timeout %% that triggers when really nothing happens (ie something went really wrong). @@ -271,6 +270,7 @@ after_parse({request, Req=#{streamid := StreamID, headers := Headers, version := error_logger:error_msg("Exception occurred in ~s:init(~p, ~p, ~p) " "with reason ~p:~p.", [Handler, StreamID, Req, Opts, Class, Reason]), + %% @todo Bad value returned here. Crashes. ok %% @todo Status code. % stream_reset(State, StreamID, {internal_error, {Class, Reason}, @@ -288,6 +288,7 @@ after_parse({data, StreamID, IsFin, Data, State=#state{handler=Handler, catch Class:Reason -> error_logger:error_msg("Exception occurred in ~s:data(~p, ~p, ~p, ~p) with reason ~p:~p.", [Handler, StreamID, IsFin, Data, StreamState0, Class, Reason]), + %% @todo Bad value returned here. Crashes. ok %% @todo % stream_reset(State, StreamID, {internal_error, {Class, Reason}, @@ -502,6 +503,8 @@ parse_hd_value(<< $\r, $\n, Rest/bits >>, S, Headers0, Name, SoFar) -> Value = clean_value_ws_end(SoFar, byte_size(SoFar) - 1), Headers = case maps:get(Name, Headers0, undefined) of undefined -> Headers0#{Name => Value}; + %% The cookie header does not use proper HTTP header lists. + Value0 when Name =:= <<"cookie">> -> Headers0#{Name => << Value0/binary, "; ", Value/binary >>}; Value0 -> Headers0#{Name => << Value0/binary, ", ", Value/binary >>} end, parse_header(Rest, S, Headers); @@ -552,7 +555,7 @@ request(Buffer, State=#state{transport=Transport, in_streamid=StreamID, undefined -> request(Buffer, State, Headers, <<>>, default_port(Transport:secure())); RawHost -> - try parse_host(RawHost, false, <<>>) of + try cow_http_hd:parse_host(RawHost) of {Host, undefined} -> request(Buffer, State, Headers, Host, default_port(Transport:secure())); {Host, Port} -> @@ -567,20 +570,6 @@ request(Buffer, State=#state{transport=Transport, in_streamid=StreamID, default_port(true) -> 443; default_port(_) -> 80. -%% @todo Yeah probably just call the cowlib function. -%% Same code as cow_http:parse_fullhost/1, but inline because we -%% really want this to go fast. -parse_host(<< $[, Rest/bits >>, false, <<>>) -> - parse_host(Rest, true, << $[ >>); -parse_host(<<>>, false, Acc) -> - {Acc, undefined}; -parse_host(<< $:, Rest/bits >>, false, Acc) -> - {Acc, list_to_integer(binary_to_list(Rest))}; -parse_host(<< $], Rest/bits >>, true, Acc) -> - parse_host(Rest, false, << Acc/binary, $] >>); -parse_host(<< C, Rest/bits >>, E, Acc) -> - ?LOWER(parse_host, Rest, E, Acc). - %% End of request parsing. request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_streamid=StreamID, @@ -617,6 +606,9 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream scheme => Scheme, host => Host, port => Port, + +%% @todo So the path component needs to be normalized. + path => Path, qs => Qs, version => Version, @@ -734,6 +726,11 @@ parse_body(Buffer, State=#state{in_streamid=StreamID, in_state= %% Message handling. +%% @todo There is a difference in behavior between HTTP/1.1 and HTTP/2 +%% when an error or crash occurs after sending a 500 response. In HTTP/2 +%% the error will be printed, in HTTP/1.1 the error will be ignored. +%% This is due to HTTP/1.1 disabling streams differently after both +%% requests and responses have been sent. down(State=#state{children=Children0}, Pid, Msg) -> case lists:keytake(Pid, 1, Children0) of {value, {_, undefined, _}, Children} -> @@ -741,7 +738,7 @@ down(State=#state{children=Children0}, Pid, Msg) -> {value, {_, StreamID, _}, Children} -> info(State#state{children=Children}, StreamID, Msg); false -> - error_logger:error_msg("Received EXIT signal ~p for unknown process ~p.", [Msg, Pid]), + error_logger:error_msg("Received EXIT signal ~p for unknown process ~p.~n", [Msg, Pid]), State end. @@ -762,16 +759,10 @@ info(State=#state{handler=Handler, streams=Streams0}, StreamID, Msg) -> % 'Exception occurred in StreamHandler:info/3 call.'}) end; false -> - error_logger:error_msg("Received message ~p for unknown stream ~p.", [Msg, StreamID]), + error_logger:error_msg("Received message ~p for unknown stream ~p.~n", [Msg, StreamID]), State end. -%% @todo commands/3 -%% @todo stream_reset - - - - %% Commands. commands(State, _, []) -> @@ -800,20 +791,24 @@ commands(State, StreamID, [{flow, _Length}|Tail]) -> %% @todo Set the body reading length to min(Length, BodyLength) commands(State, StreamID, Tail); -%% @todo Probably a good idea to have an atomic response send (single send call for resp+body). +%% Error responses are sent only if a response wasn't sent already. +commands(State=#state{out_state=wait}, StreamID, [{error_response, StatusCode, Headers, Body}|Tail]) -> + commands(State, StreamID, [{response, StatusCode, Headers, Body}|Tail]); +commands(State, StreamID, [{error_response, _, _, _}|Tail]) -> + commands(State, StreamID, Tail); %% Send a full response. %% %% @todo Kill the stream if it sent a response when one has already been sent. %% @todo Keep IsFin in the state. %% @todo Same two things above apply to DATA, possibly promise too. -commands(State0=#state{socket=Socket, transport=Transport, streams=Streams}, StreamID, +commands(State0=#state{socket=Socket, transport=Transport, out_state=wait, streams=Streams}, StreamID, [{response, StatusCode, Headers0, Body}|Tail]) -> %% @todo I'm pretty sure the last stream in the list is the one we want %% considering all others are queued. #stream{version=Version} = lists:keyfind(StreamID, #stream.id, Streams), {State, Headers} = connection(State0, Headers0, StreamID, Version), %% @todo Ensure content-length is set. - Response = cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(Headers)), + Response = cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers)), case Body of {sendfile, O, B, P} -> Transport:send(Socket, Response), @@ -838,18 +833,22 @@ commands(State0=#state{socket=Socket, transport=Transport, streams=Streams}, Str {State0#state{last_streamid=StreamID}, Headers0} end, {State, Headers} = connection(State1, Headers1, StreamID, Version), - Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(Headers))), + Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1', headers_to_list(Headers))), commands(State#state{out_state=chunked}, StreamID, Tail); %% Send a response body chunk. %% %% @todo WINDOW_UPDATE stuff require us to buffer some data. +%% @todo We probably want to allow Data to be the {sendfile, ...} tuple also. commands(State=#state{socket=Socket, transport=Transport, streams=Streams}, StreamID, [{data, IsFin, Data}|Tail]) -> + + %% @todo We need to kill the stream if it tries to send data before headers. + %% @todo Same as above. case lists:keyfind(StreamID, #stream.id, Streams) of #stream{version='HTTP/1.1'} -> Size = iolist_size(Data), - Transport:send(Socket, [integer_to_list(Size, 16), <<"\r\n">>, Data, <<"\r\n">>]); + Transport:send(Socket, [integer_to_binary(Size, 16), <<"\r\n">>, Data, <<"\r\n">>]); #stream{version='HTTP/1.0'} -> Transport:send(Socket, Data) end, @@ -885,7 +884,20 @@ commands(State0=#state{ref=Ref, parent=Parent, socket=Socket, transport=Transpor commands(State, StreamID, [stop|Tail]) -> %% @todo Do we want to run the commands after a stop? % commands(stream_terminate(State, StreamID, stop), StreamID, Tail). - maybe_terminate(State, StreamID, Tail, fin). + + %% @todo I think that's where we need to terminate streams. + + maybe_terminate(State, StreamID, Tail, fin); +%% HTTP/1.1 does not support push; ignore. +commands(State, StreamID, [{push, _, _, _, _, _, _, _}|Tail]) -> + commands(State, StreamID, Tail). + +%% The set-cookie header is special; we can only send one cookie per header. +headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) -> + Headers1 = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)), + Headers1 ++ [{<<"set-cookie">>, Value} || Value <- SetCookies]; +headers_to_list(Headers) -> + maps:to_list(Headers). flush() -> receive _ -> flush() after 0 -> ok end. @@ -1013,18 +1025,6 @@ error_terminate(StatusCode, State=#state{socket=Socket, transport=Transport}, Re terminate(_State, _Reason) -> exit(normal). %% @todo - - - - - - - - - - - - %% System callbacks. -spec system_continue(_, _, #state{}) -> ok. |