diff options
Diffstat (limited to 'src/cowboy_http.erl')
-rw-r--r-- | src/cowboy_http.erl | 90 |
1 files changed, 63 insertions, 27 deletions
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index ac0d915..e6cceae 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -219,11 +219,10 @@ timeout(State=#state{in_state=#ps_request_line{}}, request_timeout) -> %% @todo If other streams are running, just set the connection to be closed %% and stop trying to read from the socket? terminate(State, {connection_error, timeout, 'No request-line received before timeout.'}); -timeout(State=#state{socket=Socket, transport=Transport, in_state=#ps_header{}}, request_timeout) -> +timeout(State=#state{in_state=#ps_header{}}, request_timeout) -> %% @todo If other streams are running, maybe wait for their reply before sending 408? %% -> Definitely. Either way, stop reading from the socket and make that stream the last. - Transport:send(Socket, cow_http:response(408, 'HTTP/1.1', [])), - terminate(State, {connection_error, timeout, 'Request headers not received before timeout.'}). + error_terminate(408, State, {connection_error, timeout, 'Request headers not received before timeout.'}). %% Request-line. parse(<<>>, State) -> @@ -440,11 +439,13 @@ parse_header(Buffer, State=#state{opts=Opts, in_state=PS}, Headers) -> NumHeaders = maps:size(Headers), case match_colon(Buffer, 0) of nomatch when byte_size(Buffer) > MaxLength -> - error_terminate(431, State, {connection_error, limit_reached, - 'A header name is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); + error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}}, + {connection_error, limit_reached, + 'A header name is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); nomatch when NumHeaders >= MaxHeaders -> - error_terminate(431, State, {connection_error, limit_reached, - 'The number of headers is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); + error_terminate(431, State#state{in_state=PS#ps_header{headers=Headers}}, + {connection_error, limit_reached, + 'The number of headers is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); nomatch -> {more, State#state{in_state=PS#ps_header{headers=Headers}}, Buffer}; _ -> @@ -460,9 +461,10 @@ match_colon(_, _) -> parse_hd_name(<< $:, Rest/bits >>, State, H, SoFar) -> parse_hd_before_value(Rest, State, H, SoFar); -parse_hd_name(<< C, _/bits >>, State, _, <<>>) when ?IS_WS(C) -> - error_terminate(400, State, {connection_error, protocol_error, - 'Whitespace is not allowed between the header name and the colon. (RFC7230 3.2)'}); +parse_hd_name(<< C, _/bits >>, State=#state{in_state=PS}, H, <<>>) when ?IS_WS(C) -> + error_terminate(400, State#state{in_state=PS#ps_header{headers=H}}, + {connection_error, protocol_error, + 'Whitespace is not allowed between the header name and the colon. (RFC7230 3.2)'}); parse_hd_name(<< C, Rest/bits >>, State, H, SoFar) when ?IS_WS(C) -> parse_hd_name_ws(Rest, State, H, SoFar); parse_hd_name(<< C, Rest/bits >>, State, H, SoFar) -> @@ -483,8 +485,9 @@ parse_hd_before_value(Buffer, State=#state{opts=Opts, in_state=PS}, H, N) -> MaxLength = maps:get(max_header_value_length, Opts, 4096), case match_eol(Buffer, 0) of nomatch when byte_size(Buffer) > MaxLength -> - error_terminate(431, State, {connection_error, limit_reached, - 'A header value is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); + error_terminate(431, State#state{in_state=PS#ps_header{headers=H}}, + {connection_error, limit_reached, + 'A header value is larger than configuration allows. (RFC7230 3.2.5, RFC6585 5)'}); nomatch -> {more, State#state{in_state=PS#ps_header{headers=H, name=N}}, Buffer}; _ -> @@ -538,12 +541,13 @@ horse_clean_value_ws_end() -> -endif. request(Buffer, State=#state{transport=Transport, in_streamid=StreamID, - in_state=#ps_header{version=Version}}, Headers) -> + in_state=PS=#ps_header{version=Version}}, Headers) -> case maps:get(<<"host">>, Headers, undefined) of undefined when Version =:= 'HTTP/1.1' -> %% @todo Might want to not close the connection on this and next one. - error_terminate(400, State, {stream_error, StreamID, protocol_error, - 'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'}); + error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, StreamID, protocol_error, + 'HTTP/1.1 requests must include a host header. (RFC7230 5.4)'}); undefined -> request(Buffer, State, Headers, <<>>, default_port(Transport:secure())); RawHost -> @@ -553,8 +557,9 @@ request(Buffer, State=#state{transport=Transport, in_streamid=StreamID, {Host, Port} -> request(Buffer, State, Headers, Host, Port) catch _:_ -> - error_terminate(400, State, {stream_error, StreamID, protocol_error, - 'The host header is invalid. (RFC7230 5.4)'}) + error_terminate(400, State#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, StreamID, protocol_error, + 'The host header is invalid. (RFC7230 5.4)'}) end end. @@ -565,7 +570,7 @@ default_port(_) -> 80. %% End of request parsing. request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_streamid=StreamID, - in_state=#ps_header{method=Method, path=Path, qs=Qs, version=Version}}, + in_state=PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}}, Headers, Host, Port) -> Scheme = case Transport:secure() of true -> <<"https">>; @@ -578,9 +583,9 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream Length = try cow_http_hd:parse_content_length(BinLength) catch _:_ -> - error_terminate(400, State0, {stream_error, StreamID, protocol_error, - 'The content-length header is invalid. (RFC7230 3.3.2)'}) - %% @todo Err should terminate here... + error_terminate(400, State0#state{in_state=PS#ps_header{headers=Headers}}, + {stream_error, StreamID, protocol_error, + 'The content-length header is invalid. (RFC7230 3.3.2)'}) end, {true, Length, fun cow_http_te:stream_identity/2, {0, Length}}; %% @todo Better handling of transfer decoding. @@ -622,7 +627,10 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream end, {request, Req, State, Buffer}; {true, HTTP2Settings} -> - http2_upgrade(State0, Buffer, HTTP2Settings, Req) + %% We save the headers in case the upgrade will fail + %% and we need to pass them to cowboy_stream:early_error. + http2_upgrade(State0#state{in_state=PS#ps_header{headers=Headers}}, + Buffer, HTTP2Settings, Req) end. %% HTTP/2 upgrade. @@ -667,6 +675,8 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran %% Always half-closed stream coming from this side. try cow_http_hd:parse_http2_settings(HTTP2Settings) of Settings -> + %% @todo We should invoke cowboy_stream:info for this stream, + %% with a switch_protocol tuple. Transport:send(Socket, cow_http:response(101, 'HTTP/1.1', maps:to_list(#{ <<"connection">> => <<"Upgrade">>, <<"upgrade">> => <<"h2c">> @@ -676,7 +686,7 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req) catch _:_ -> error_terminate(400, State, {connection_error, protocol_error, - 'The HTTP2-Settings header contains a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'}) + 'The HTTP2-Settings header must contain a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'}) end. %% Request body parsing. @@ -916,6 +926,7 @@ stream_terminate(State=#state{socket=Socket, transport=Transport, = lists:keytake(StreamID, #stream.id, Streams0), _ = case OutState of wait -> + %% @todo This should probably go through the stream handler info callback. Transport:send(Socket, cow_http:response(204, 'HTTP/1.1', [])); chunked when Version =:= 'HTTP/1.1' -> Transport:send(Socket, <<"0\r\n\r\n">>); @@ -1005,11 +1016,36 @@ connection_hd_is_close(Conn) -> Conns = cow_http_hd:parse_connection(iolist_to_binary(Conn)), lists:member(<<"close">>, Conns). +%% This function is only called when an error occurs on a new stream. -spec error_terminate(cowboy:http_status(), #state{}, _) -> no_return(). -error_terminate(StatusCode, State=#state{socket=Socket, transport=Transport}, Reason) -> - Transport:send(Socket, cow_http:response(StatusCode, 'HTTP/1.1', [ - {<<"content-length">>, <<"0">>} - ])), +error_terminate(StatusCode0, State=#state{ref=Ref, socket=Socket, transport=Transport, + opts=Opts, peer=Peer, in_streamid=StreamID, in_state=StreamState}, Reason) -> + PartialReq = case StreamState of + #ps_request_line{} -> + #{}; + #ps_header{method=Method, path=Path, qs=Qs, + version=Version, headers=ReqHeaders} -> #{ + ref => Ref, + peer => Peer, + method => Method, + path => Path, + qs => Qs, + version => Version, + headers => case ReqHeaders of + undefined -> #{}; + _ -> ReqHeaders + end + } + end, + {response, StatusCode, RespHeaders, RespBody} + = cowboy_stream:early_error(StreamID, Reason, PartialReq, + {response, StatusCode0, #{ + <<"content-length">> => <<"0">> + }, <<>>}, Opts), + Transport:send(Socket, [ + cow_http:response(StatusCode, 'HTTP/1.1', maps:to_list(RespHeaders)), + RespBody + ]), terminate(State, Reason). -spec terminate(_, _) -> no_return(). |