diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cowboy.erl | 2 | ||||
-rw-r--r-- | src/cowboy_http.erl | 12 | ||||
-rw-r--r-- | src/cowboy_http2.erl | 49 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 31 | ||||
-rw-r--r-- | src/cowboy_router.erl | 25 | ||||
-rw-r--r-- | src/cowboy_static.erl | 19 | ||||
-rw-r--r-- | src/cowboy_stream_h.erl | 66 |
7 files changed, 172 insertions, 32 deletions
diff --git a/src/cowboy.erl b/src/cowboy.erl index d9c14ea..3387224 100644 --- a/src/cowboy.erl +++ b/src/cowboy.erl @@ -44,7 +44,7 @@ cowboy_protocol:opts()) -> {ok, pid()} | {error, any()}. start_clear(Ref, NbAcceptors, TransOpts0, ProtoOpts) when is_integer(NbAcceptors), NbAcceptors > 0 -> - TransOpts = TransOpts0,%[connection_type(ProtoOpts)|TransOpts0], + TransOpts = [connection_type(ProtoOpts)|TransOpts0], ranch:start_listener(Ref, NbAcceptors, ranch_tcp, TransOpts, cowboy_clear, ProtoOpts). -spec start_tls(ranch:ref(), non_neg_integer(), ranch_ssl:opts(), opts()) -> {ok, pid()} | {error, any()}. diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index 4608839..e93045d 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -900,15 +900,15 @@ maybe_terminate(State=#state{last_streamid=StreamID}, StreamID, _Tail, fin) -> maybe_terminate(State, StreamID, _Tail, fin) -> stream_terminate(State, StreamID, normal). -stream_reset(State=#state{socket=Socket, transport=Transport}, StreamID, - StreamError={internal_error, _, _}) -> +stream_reset(State, StreamID, StreamError={internal_error, _, _}) -> %% @todo headers %% @todo Don't send this if there are no streams left. - Transport:send(Socket, cow_http:response(500, 'HTTP/1.1', [ - {<<"content-length">>, <<"0">>} - ])), +% Transport:send(Socket, cow_http:response(500, 'HTTP/1.1', [ +% {<<"content-length">>, <<"0">>} +% ])), %% @todo update IsFin local - stream_terminate(State#state{out_state=done}, StreamID, StreamError). +% stream_terminate(State#state{out_state=done}, StreamID, StreamError). + stream_terminate(State, StreamID, StreamError). stream_terminate(State=#state{socket=Socket, transport=Transport, handler=Handler, out_streamid=OutStreamID, out_state=OutState, diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index c5c7e0c..94581ac 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -365,18 +365,50 @@ commands(State=#state{socket=Socket, transport=Transport, encode_state=EncodeSta [{response, StatusCode, Headers0, Body}|Tail]) -> Headers = Headers0#{<<":status">> => integer_to_binary(StatusCode)}, {HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0), - Transport:send(Socket, [ - cow_http2:headers(StreamID, nofin, HeaderBlock), - cow_http2:data(StreamID, fin, Body) - ]), - commands(State#state{encode_state=EncodeState}, StreamID, Tail); + Response = cow_http2:headers(StreamID, nofin, HeaderBlock), + case Body of + {sendfile, O, B, P} -> + Transport:send(Socket, Response), + commands(State#state{encode_state=EncodeState}, StreamID, + [{sendfile, fin, O, B, P}|Tail]); + _ -> + Transport:send(Socket, [ + Response, + cow_http2:data(StreamID, fin, Body) + ]), + commands(State#state{encode_state=EncodeState}, StreamID, Tail) + end; %% Send a response body chunk. %% %% @todo WINDOW_UPDATE stuff require us to buffer some data. +%% +%% When the body is sent using sendfile, the current solution is not +%% very good. The body could be too large, blocking the connection. +%% Also sendfile technically only works over TCP, so it's not that +%% useful for HTTP/2. At the very least the sendfile call should be +%% split into multiple calls and flow control should be used to make +%% sure we only send as fast as the client can receive and don't block +%% anything. commands(State=#state{socket=Socket, transport=Transport}, StreamID, [{data, IsFin, Data}|Tail]) -> Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data)), commands(State, StreamID, Tail); +%% Send a file. +%% +%% @todo This implementation is terrible. A good implementation would +%% need to check that Bytes is exact (or we need to document that we +%% trust it to be exact), and would need to send the file asynchronously +%% in many data frames. Perhaps a sendfile call should result in a +%% process being created specifically for this purpose. Or perhaps +%% the protocol should be "dumb" and the stream handler be the one +%% to ensure the file is sent in chunks (which would require a better +%% flow control at the stream handler level). One thing for sure, the +%% implementation necessarily varies between HTTP/1.1 and HTTP/2. +commands(State=#state{socket=Socket, transport=Transport}, StreamID, + [{sendfile, IsFin, Offset, Bytes, Path}|Tail]) -> + Transport:send(Socket, cow_http2:data_header(StreamID, IsFin, Bytes)), + Transport:sendfile(Socket, Path, Offset, Bytes), + commands(State, StreamID, Tail); %% Send a push promise. %% %% @todo We need to keep track of what promises we made so that we don't @@ -400,6 +432,10 @@ commands(State, StreamID, [{flow, _Size}|Tail]) -> %% Supervise a child process. commands(State=#state{children=Children}, StreamID, [{spawn, Pid, _Shutdown}|Tail]) -> %% @todo Shutdown commands(State#state{children=[{Pid, StreamID}|Children]}, StreamID, Tail); +%% Error handling. +commands(State, StreamID, [Error = {internal_error, _, _}|Tail]) -> + %% @todo Only reset when the stream still exists. + commands(stream_reset(State, StreamID, Error), StreamID, Tail); %% Upgrade to a new protocol. %% %% @todo Implementation. @@ -447,8 +483,7 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, decode_st Host = Authority, %% @todo Port = todo, %% @todo - Path = PathWithQs, %% @todo - Qs = todo, %% @todo + {Path, Qs} = cow_http:parse_fullpath(PathWithQs), Req = #{ ref => Ref, diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index 914b273..55b4e22 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -198,7 +198,6 @@ %% End of REST callbacks. Whew! -record(state, { - env :: cowboy_middleware:env(), method = undefined :: binary(), %% Handler. @@ -235,10 +234,11 @@ -spec upgrade(Req, Env, module(), any(), infinity, run) -> {ok, Req, Env} when Req::cowboy_req:req(), Env::cowboy_middleware:env(). -upgrade(Req, Env, Handler, HandlerState, infinity, run) -> - Method = cowboy_req:method(Req), - service_available(Req, #state{env=Env, method=Method, - handler=Handler, handler_state=HandlerState}). +upgrade(Req0, Env, Handler, HandlerState, infinity, run) -> + Method = cowboy_req:method(Req0), + {ok, Req, Result} = service_available(Req0, #state{method=Method, + handler=Handler, handler_state=HandlerState}), + {ok, Req, [{result, Result}|Env]}. service_available(Req, State) -> expect(Req, State, service_available, true, fun known_methods/2, 503). @@ -683,9 +683,12 @@ if_match_exists(Req, State) -> if_match(Req, State, EtagsList) -> try generate_etag(Req, State) of + %% Strong Etag comparison: weak Etag never matches. + {{weak, _}, Req2, State2} -> + precondition_failed(Req2, State2); {Etag, Req2, State2} -> case lists:member(Etag, EtagsList) of - true -> if_unmodified_since_exists(Req2, State2); + true -> if_none_match_exists(Req2, State2); %% Etag may be `undefined' which cannot be a member. false -> precondition_failed(Req2, State2) end @@ -738,15 +741,23 @@ if_none_match(Req, State, EtagsList) -> undefined -> precondition_failed(Req2, State2); Etag -> - case lists:member(Etag, EtagsList) of + case is_weak_match(Etag, EtagsList) of true -> precondition_is_head_get(Req2, State2); - false -> if_modified_since_exists(Req2, State2) + false -> method(Req2, State2) end end catch Class:Reason -> error_terminate(Req, State, Class, Reason, generate_etag) end. +%% Weak Etag comparison: only check the opaque tag. +is_weak_match(_, []) -> + false; +is_weak_match({_, Tag}, [{_, Tag}|_]) -> + true; +is_weak_match(Etag, [_|Tail]) -> + is_weak_match(Etag, Tail). + precondition_is_head_get(Req, State=#state{method=Method}) when Method =:= <<"HEAD">>; Method =:= <<"GET">> -> not_modified(Req, State); @@ -1156,6 +1167,6 @@ error_terminate(Req, #state{handler=Handler, handler_state=HandlerState}, {state, HandlerState} ]}). -terminate(Req, #state{env=Env, handler=Handler, handler_state=HandlerState}) -> +terminate(Req, #state{handler=Handler, handler_state=HandlerState}) -> Result = cowboy_handler:terminate(normal, Req, HandlerState, Handler), - {ok, Req, [{result, Result}|Env]}. + {ok, Req, Result}. diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl index f07b307..f2f8fb4 100644 --- a/src/cowboy_router.erl +++ b/src/cowboy_router.erl @@ -322,9 +322,9 @@ split_path(Path, Acc) -> try case binary:match(Path, <<"/">>) of nomatch when Path =:= <<>> -> - lists:reverse([cow_qs:urldecode(S) || S <- Acc]); + remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- Acc]), []); nomatch -> - lists:reverse([cow_qs:urldecode(S) || S <- [Path|Acc]]); + remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- [Path|Acc]]), []); {Pos, _} -> << Segment:Pos/binary, _:8, Rest/bits >> = Path, split_path(Rest, [Segment|Acc]) @@ -334,6 +334,27 @@ split_path(Path, Acc) -> badrequest end. +remove_dot_segments([], Acc) -> + lists:reverse(Acc); +remove_dot_segments([<<".">>|Segments], Acc) -> + remove_dot_segments(Segments, Acc); +remove_dot_segments([<<"..">>|Segments], Acc=[]) -> + remove_dot_segments(Segments, Acc); +remove_dot_segments([<<"..">>|Segments], [_|Acc]) -> + remove_dot_segments(Segments, Acc); +remove_dot_segments([S|Segments], Acc) -> + remove_dot_segments(Segments, [S|Acc]). + +-ifdef(TEST). +remove_dot_segments_test_() -> + Tests = [ + {[<<"a">>, <<"b">>, <<"c">>, <<".">>, <<"..">>, <<"..">>, <<"g">>], [<<"a">>, <<"g">>]}, + {[<<"mid">>, <<"content=5">>, <<"..">>, <<"6">>], [<<"mid">>, <<"6">>]}, + {[<<"..">>, <<"a">>], [<<"a">>]} + ], + [fun() -> R = remove_dot_segments(S, []) end || {S, R} <- Tests]. +-endif. + -spec list_match(tokens(), dispatch_match(), bindings()) -> {true, bindings(), undefined | tokens()} | false. %% Atom '...' matches any trailing path, stop right now. diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index c771771..d13db62 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -81,15 +81,32 @@ init_dir(Req, Path, Extra) when is_list(Path) -> init_dir(Req, Path, Extra) -> Dir = fullpath(filename:absname(Path)), PathInfo = cowboy_req:path_info(Req), - Filepath = filename:join([Dir|PathInfo]), + Filepath = filename:join([Dir|[escape_reserved(P, <<>>) || P <- PathInfo]]), Len = byte_size(Dir), case fullpath(Filepath) of << Dir:Len/binary, $/, _/binary >> -> init_info(Req, Filepath, Extra); + << Dir:Len/binary >> -> + init_info(Req, Filepath, Extra); _ -> {cowboy_rest, Req, error} end. +%% We escape the slash found in path segments because +%% a segment corresponds to a directory entry, and +%% therefore those slashes are expected to be part of +%% the directory name. +%% +%% Note that on most systems the slash is prohibited +%% and cannot appear in filenames, which means the +%% requested file will end up being not found. +escape_reserved(<<>>, Acc) -> + Acc; +escape_reserved(<< $/, Rest/bits >>, Acc) -> + escape_reserved(Rest, << Acc/binary, $\\, $/ >>); +escape_reserved(<< C, Rest/bits >>, Acc) -> + escape_reserved(Rest, << Acc/binary, C >>). + fullpath(Path) -> fullpath(filename:split(Path), []). fullpath([], Acc) -> diff --git a/src/cowboy_stream_h.erl b/src/cowboy_stream_h.erl index f924e28..b834c17 100644 --- a/src/cowboy_stream_h.erl +++ b/src/cowboy_stream_h.erl @@ -21,10 +21,12 @@ -export([info/3]). -export([terminate/3]). +-export([proc_lib_hack/3]). -export([execute/3]). -export([resume/5]). -record(state, { + ref = undefined :: ranch:ref(), pid = undefined :: pid(), read_body_ref = undefined :: reference(), read_body_length = 0 :: non_neg_integer(), @@ -38,12 +40,12 @@ %% @todo proper specs -spec init(_,_,_) -> _. -init(_StreamID, Req, Opts) -> +init(_StreamID, Req=#{ref := Ref}, Opts) -> Env = maps:get(env, Opts, #{}), Middlewares = maps:get(middlewares, Opts, [cowboy_router, cowboy_handler]), Shutdown = maps:get(shutdown, Opts, 5000), - Pid = proc_lib:spawn_link(?MODULE, execute, [Req, Env, Middlewares]), - {[{spawn, Pid, Shutdown}], #state{pid=Pid}}. + Pid = proc_lib:spawn_link(?MODULE, proc_lib_hack, [Req, Env, Middlewares]), + {[{spawn, Pid, Shutdown}], #state{ref=Ref, pid=Pid}}. %% If we receive data and stream is waiting for data: %% If we accumulated enough data or IsFin=fin, send it. @@ -64,8 +66,29 @@ data(_StreamID, IsFin, Data, State=#state{pid=Pid, read_body_ref=Ref, read_body_ -spec info(_,_,_) -> _. info(_StreamID, {'EXIT', Pid, normal}, State=#state{pid=Pid}) -> {[stop], State}; -info(_StreamID, Reason = {'EXIT', Pid, _}, State=#state{pid=Pid}) -> - {[{internal_error, Reason, 'Stream process crashed.'}], State}; +%% @todo Transition. +%% In the future it would be better to simplify things +%% and only catch this at the stream level. +%% +%% Maybe we don't need specific error messages +%% for every single callbacks anymore? +info(_StreamID, Exit = {'EXIT', Pid, {cowboy_handler, _}}, State=#state{pid=Pid}) -> + %% No crash report; one has already been sent. + {[ + {response, 500, #{<<"content-length">> => <<"0">>}, <<>>}, + {internal_error, Exit, 'Stream process crashed.'} + ], State}; +info(_StreamID, {'EXIT', Pid, {_Reason, [_, {cow_http_hd, _, _, _}|_]}}, State=#state{pid=Pid}) -> + %% @todo Have an option to enable/disable this specific crash report? + %%report_crash(Ref, StreamID, Pid, Reason, Stacktrace), + %% @todo Headers? Details in body? More stuff in debug only? + {[{response, 400, #{}, <<>>}, stop], State}; +info(StreamID, Exit = {'EXIT', Pid, {Reason, Stacktrace}}, State=#state{ref=Ref, pid=Pid}) -> + report_crash(Ref, StreamID, Pid, Reason, Stacktrace), + {[ + {response, 500, #{<<"content-length">> => <<"0">>}, <<>>}, + {internal_error, Exit, 'Stream process crashed.'} + ], State}; %% Request body, no body buffer but IsFin=fin. info(_StreamID, {read_body, Ref, _}, State=#state{pid=Pid, read_body_is_fin=fin, read_body_buffer= <<>>}) -> Pid ! {request_body, Ref, fin, <<>>}, @@ -89,6 +112,7 @@ info(_StreamID, SwitchProtocol = {switch_protocol, _, _, _}, State) -> {[SwitchProtocol], State}; %% Stray message. info(_StreamID, _Msg, State) -> + %% @todo Error report. %% @todo Cleanup if no reply was sent when stream ends. {[], State}. @@ -97,8 +121,40 @@ info(_StreamID, _Msg, State) -> terminate(_StreamID, _Reason, _State) -> ok. +%% We use ~999999p here instead of ~w because the latter doesn't +%% support printable strings. +report_crash(_, _, _, normal, _) -> + ok; +report_crash(_, _, _, shutdown, _) -> + ok; +report_crash(_, _, _, {shutdown, _}, _) -> + ok; +report_crash(Ref, StreamID, Pid, Reason, Stacktrace) -> + error_logger:error_msg( + "Ranch listener ~p, connection process ~p, stream ~p " + "had its request process ~p exit with reason " + "~999999p and stacktrace ~999999p~n", + [Ref, self(), StreamID, Pid, Reason, Stacktrace]). + %% Request process. +%% This hack is necessary because proc_lib does not propagate +%% stacktraces by default. This is ugly because we end up +%% having two try/catch instead of one (the one in proc_lib), +%% just to add the stacktrace information. +%% +%% @todo Remove whenever proc_lib propagates stacktraces. +-spec proc_lib_hack(_, _, _) -> _. +proc_lib_hack(Req, Env, Middlewares) -> + try + execute(Req, Env, Middlewares) + catch + _:Reason when element(1, Reason) =:= cowboy_handler -> + exit(Reason); + _:Reason -> + exit({Reason, erlang:get_stacktrace()}) + end. + %% @todo %-spec execute(cowboy_req:req(), #state{}, cowboy_middleware:env(), [module()]) % -> ok. |