123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460 |
- %% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
- %% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>
- %%
- %% Permission to use, copy, modify, and/or distribute this software for any
- %% purpose with or without fee is hereby granted, provided that the above
- %% copyright notice and this permission notice appear in all copies.
- %%
- %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- %% @doc HTTP protocol handler.
- %%
- %% The available options are:
- %% <dl>
- %% <dt>dispatch</dt><dd>The dispatch list for this protocol.</dd>
- %% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request.
- %% Defaults to 5.</dd>
- %% <dt>timeout</dt><dd>Time in milliseconds before an idle
- %% connection is closed. Defaults to 5000 milliseconds.</dd>
- %% <dt>urldecode</dt><dd>Function and options argument to use when decoding
- %% URL encoded strings. Defaults to `{fun cowboy_http:urldecode/2, crash}'.
- %% </dd>
- %% </dl>
- %%
- %% Note that there is no need to monitor these processes when using Cowboy as
- %% an application as it already supervises them under the listener supervisor.
- %%
- %% @see cowboy_dispatcher
- %% @see cowboy_http_handler
- -module(cowboy_http_protocol).
- -behaviour(cowboy_protocol).
- -export([start_link/4]). %% API.
- -export([init/4, parse_request/1, handler_loop/3]). %% FSM.
- -include("include/http.hrl").
- -include_lib("eunit/include/eunit.hrl").
- -record(state, {
- listener :: pid(),
- socket :: inet:socket(),
- transport :: module(),
- dispatch :: cowboy_dispatcher:dispatch_rules(),
- handler :: {module(), any()},
- urldecode :: {fun((binary(), T) -> binary()), T},
- req_empty_lines = 0 :: integer(),
- max_empty_lines :: integer(),
- req_keepalive = 1 :: integer(),
- max_keepalive :: integer(),
- max_line_length :: integer(),
- timeout :: timeout(),
- buffer = <<>> :: binary(),
- hibernate = false :: boolean(),
- loop_timeout = infinity :: timeout(),
- loop_timeout_ref :: undefined | reference()
- }).
- %% API.
- %% @doc Start an HTTP protocol process.
- -spec start_link(pid(), inet:socket(), module(), any()) -> {ok, pid()}.
- start_link(ListenerPid, Socket, Transport, Opts) ->
- Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport, Opts]),
- {ok, Pid}.
- %% FSM.
- %% @private
- -spec init(pid(), inet:socket(), module(), any()) -> ok | none().
- init(ListenerPid, Socket, Transport, Opts) ->
- Dispatch = proplists:get_value(dispatch, Opts, []),
- MaxEmptyLines = proplists:get_value(max_empty_lines, Opts, 5),
- MaxKeepalive = proplists:get_value(max_keepalive, Opts, infinity),
- MaxLineLength = proplists:get_value(max_line_length, Opts, 4096),
- Timeout = proplists:get_value(timeout, Opts, 5000),
- URLDecDefault = {fun cowboy_http:urldecode/2, crash},
- URLDec = proplists:get_value(urldecode, Opts, URLDecDefault),
- ok = cowboy:accept_ack(ListenerPid),
- wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport,
- dispatch=Dispatch, max_empty_lines=MaxEmptyLines,
- max_keepalive=MaxKeepalive, max_line_length=MaxLineLength,
- timeout=Timeout, urldecode=URLDec}).
- %% @private
- -spec parse_request(#state{}) -> ok | none().
- %% We limit the length of the Request-line to MaxLength to avoid endlessly
- %% reading from the socket and eventually crashing.
- parse_request(State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
- case erlang:decode_packet(http_bin, Buffer, []) of
- {ok, Request, Rest} -> request(Request, State#state{buffer=Rest});
- {more, _Length} when byte_size(Buffer) > MaxLength ->
- error_terminate(413, State);
- {more, _Length} -> wait_request(State);
- {error, _Reason} -> error_terminate(400, State)
- end.
- -spec wait_request(#state{}) -> ok | none().
- wait_request(State=#state{socket=Socket, transport=Transport,
- timeout=T, buffer=Buffer}) ->
- case Transport:recv(Socket, 0, T) of
- {ok, Data} -> parse_request(State#state{
- buffer= << Buffer/binary, Data/binary >>});
- {error, _Reason} -> terminate(State)
- end.
- -spec request({http_request, http_method(), http_uri(),
- http_version()}, #state{}) -> ok | none().
- request({http_request, _Method, _URI, Version}, State)
- when Version =/= {1, 0}, Version =/= {1, 1} ->
- error_terminate(505, State);
- request({http_request, Method, {abs_path, AbsPath}, Version},
- State=#state{socket=Socket, transport=Transport,
- urldecode={URLDecFun, URLDecArg}=URLDec}) ->
- URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end,
- {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath, URLDecode),
- ConnAtom = version_to_connection(Version),
- parse_header(#http_req{socket=Socket, transport=Transport,
- connection=ConnAtom, method=Method, version=Version,
- path=Path, raw_path=RawPath, raw_qs=Qs, urldecode=URLDec}, State);
- request({http_request, Method, '*', Version},
- State=#state{socket=Socket, transport=Transport, urldecode=URLDec}) ->
- ConnAtom = version_to_connection(Version),
- parse_header(#http_req{socket=Socket, transport=Transport,
- connection=ConnAtom, method=Method, version=Version,
- path='*', raw_path= <<"*">>, raw_qs= <<>>, urldecode=URLDec}, State);
- request({http_request, _Method, _URI, _Version}, State) ->
- error_terminate(501, State);
- request({http_error, <<"\r\n">>},
- State=#state{req_empty_lines=N, max_empty_lines=N}) ->
- error_terminate(400, State);
- request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) ->
- parse_request(State#state{req_empty_lines=N + 1});
- request(_Any, State) ->
- error_terminate(400, State).
- -spec parse_header(#http_req{}, #state{}) -> ok | none().
- parse_header(Req, State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
- case erlang:decode_packet(httph_bin, Buffer, []) of
- {ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest});
- {more, _Length} when byte_size(Buffer) > MaxLength ->
- error_terminate(413, State);
- {more, _Length} -> wait_header(Req, State);
- {error, _Reason} -> error_terminate(400, State)
- end.
- -spec wait_header(#http_req{}, #state{}) -> ok | none().
- wait_header(Req, State=#state{socket=Socket,
- transport=Transport, timeout=T, buffer=Buffer}) ->
- case Transport:recv(Socket, 0, T) of
- {ok, Data} -> parse_header(Req, State#state{
- buffer= << Buffer/binary, Data/binary >>});
- {error, timeout} -> error_terminate(408, State);
- {error, closed} -> terminate(State)
- end.
- -spec header({http_header, integer(), http_header(), any(), binary()}
- | http_eoh, #http_req{}, #state{}) -> ok | none().
- header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
- transport=Transport, host=undefined}, State) ->
- RawHost2 = cowboy_bstr:to_lower(RawHost),
- case catch cowboy_dispatcher:split_host(RawHost2) of
- {Host, RawHost3, undefined} ->
- Port = default_port(Transport:name()),
- dispatch(fun parse_header/2, Req#http_req{
- host=Host, raw_host=RawHost3, port=Port,
- headers=[{'Host', RawHost3}|Req#http_req.headers]}, State);
- {Host, RawHost3, Port} ->
- dispatch(fun parse_header/2, Req#http_req{
- host=Host, raw_host=RawHost3, port=Port,
- headers=[{'Host', RawHost3}|Req#http_req.headers]}, State);
- {'EXIT', _Reason} ->
- error_terminate(400, State)
- end;
- %% Ignore Host headers if we already have it.
- header({http_header, _I, 'Host', _R, _V}, Req, State) ->
- parse_header(Req, State);
- header({http_header, _I, 'Connection', _R, Connection},
- Req=#http_req{headers=Headers}, State) ->
- Req2 = Req#http_req{headers=[{'Connection', Connection}|Headers]},
- {ConnTokens, Req3}
- = cowboy_http_req:parse_header('Connection', Req2),
- ConnAtom = cowboy_http:connection_to_atom(ConnTokens),
- parse_header(Req3#http_req{connection=ConnAtom}, State);
- header({http_header, _I, Field, _R, Value}, Req, State) ->
- Field2 = format_header(Field),
- parse_header(Req#http_req{headers=[{Field2, Value}|Req#http_req.headers]},
- State);
- %% The Host header is required in HTTP/1.1.
- header(http_eoh, #http_req{version={1, 1}, host=undefined}, State) ->
- error_terminate(400, State);
- %% It is however optional in HTTP/1.0.
- header(http_eoh, Req=#http_req{version={1, 0}, transport=Transport,
- host=undefined}, State=#state{buffer=Buffer}) ->
- Port = default_port(Transport:name()),
- dispatch(fun handler_init/2, Req#http_req{host=[], raw_host= <<>>,
- port=Port, buffer=Buffer}, State#state{buffer= <<>>});
- header(http_eoh, Req, State=#state{buffer=Buffer}) ->
- handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>});
- header(_Any, _Req, State) ->
- error_terminate(400, State).
- -spec dispatch(fun((#http_req{}, #state{}) -> ok),
- #http_req{}, #state{}) -> ok | none().
- dispatch(Next, Req=#http_req{host=Host, path=Path},
- State=#state{dispatch=Dispatch}) ->
- %% @todo We should allow a configurable chain of handlers here to
- %% allow things like url rewriting, site-wide authentication,
- %% optional dispatching, and more. It would default to what
- %% we are doing so far.
- case cowboy_dispatcher:match(Host, Path, Dispatch) of
- {ok, Handler, Opts, Binds, HostInfo, PathInfo} ->
- Next(Req#http_req{host_info=HostInfo, path_info=PathInfo,
- bindings=Binds}, State#state{handler={Handler, Opts}});
- {error, notfound, host} ->
- error_terminate(400, State);
- {error, notfound, path} ->
- error_terminate(404, State)
- end.
- -spec handler_init(#http_req{}, #state{}) -> ok | none().
- handler_init(Req, State=#state{transport=Transport,
- handler={Handler, Opts}}) ->
- try Handler:init({Transport:name(), http}, Req, Opts) of
- {ok, Req2, HandlerState} ->
- handler_handle(HandlerState, Req2, State);
- {loop, Req2, HandlerState} ->
- handler_before_loop(HandlerState, Req2, State);
- {loop, Req2, HandlerState, hibernate} ->
- handler_before_loop(HandlerState, Req2,
- State#state{hibernate=true});
- {loop, Req2, HandlerState, Timeout} ->
- handler_before_loop(HandlerState, Req2,
- State#state{loop_timeout=Timeout});
- {loop, Req2, HandlerState, Timeout, hibernate} ->
- handler_before_loop(HandlerState, Req2,
- State#state{hibernate=true, loop_timeout=Timeout});
- {shutdown, Req2, HandlerState} ->
- handler_terminate(HandlerState, Req2, State);
- %% @todo {upgrade, transport, Module}
- {upgrade, protocol, Module} ->
- upgrade_protocol(Req, State, Module)
- catch Class:Reason ->
- error_terminate(500, State),
- error_logger:error_msg(
- "** Handler ~p terminating in init/3~n"
- " for the reason ~p:~p~n"
- "** Options were ~p~n"
- "** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()])
- end.
- -spec upgrade_protocol(#http_req{}, #state{}, atom()) -> ok | none().
- upgrade_protocol(Req, State=#state{listener=ListenerPid,
- handler={Handler, Opts}}, Module) ->
- case Module:upgrade(ListenerPid, Handler, Opts, Req) of
- {UpgradeRes, Req2} -> next_request(Req2, State, UpgradeRes);
- _Any -> terminate(State)
- end.
- -spec handler_handle(any(), #http_req{}, #state{}) -> ok | none().
- handler_handle(HandlerState, Req, State=#state{handler={Handler, Opts}}) ->
- try Handler:handle(Req, HandlerState) of
- {ok, Req2, HandlerState2} ->
- terminate_request(HandlerState2, Req2, State)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Handler ~p terminating in handle/2~n"
- " for the reason ~p:~p~n"
- "** Options were ~p~n** Handler state was ~p~n"
- "** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, Opts,
- HandlerState, Req, erlang:get_stacktrace()]),
- handler_terminate(HandlerState, Req, State),
- terminate(State)
- end.
- %% We don't listen for Transport closes because that would force us
- %% to receive data and buffer it indefinitely.
- -spec handler_before_loop(any(), #http_req{}, #state{}) -> ok | none().
- handler_before_loop(HandlerState, Req, State=#state{hibernate=true}) ->
- State2 = handler_loop_timeout(State),
- erlang:hibernate(?MODULE, handler_loop,
- [HandlerState, Req, State2#state{hibernate=false}]);
- handler_before_loop(HandlerState, Req, State) ->
- State2 = handler_loop_timeout(State),
- handler_loop(HandlerState, Req, State2).
- %% Almost the same code can be found in cowboy_http_websocket.
- -spec handler_loop_timeout(#state{}) -> #state{}.
- handler_loop_timeout(State=#state{loop_timeout=infinity}) ->
- State#state{loop_timeout_ref=undefined};
- handler_loop_timeout(State=#state{loop_timeout=Timeout,
- loop_timeout_ref=PrevRef}) ->
- _ = case PrevRef of undefined -> ignore; PrevRef ->
- erlang:cancel_timer(PrevRef) end,
- TRef = make_ref(),
- erlang:send_after(Timeout, self(), {?MODULE, timeout, TRef}),
- State#state{loop_timeout_ref=TRef}.
- -spec handler_loop(any(), #http_req{}, #state{}) -> ok | none().
- handler_loop(HandlerState, Req, State=#state{loop_timeout_ref=TRef}) ->
- receive
- {?MODULE, timeout, TRef} ->
- terminate_request(HandlerState, Req, State);
- {?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) ->
- handler_loop(HandlerState, Req, State);
- Message ->
- handler_call(HandlerState, Req, State, Message)
- end.
- -spec handler_call(any(), #http_req{}, #state{}, any()) -> ok | none().
- handler_call(HandlerState, Req, State=#state{handler={Handler, Opts}},
- Message) ->
- try Handler:info(Message, Req, HandlerState) of
- {ok, Req2, HandlerState2} ->
- terminate_request(HandlerState2, Req2, State);
- {loop, Req2, HandlerState2} ->
- handler_before_loop(HandlerState2, Req2, State);
- {loop, Req2, HandlerState2, hibernate} ->
- handler_before_loop(HandlerState2, Req2,
- State#state{hibernate=true})
- catch Class:Reason ->
- error_logger:error_msg(
- "** Handler ~p terminating in info/3~n"
- " for the reason ~p:~p~n"
- "** Options were ~p~n** Handler state was ~p~n"
- "** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, Opts,
- HandlerState, Req, erlang:get_stacktrace()])
- end.
- -spec handler_terminate(any(), #http_req{}, #state{}) -> ok.
- handler_terminate(HandlerState, Req, #state{handler={Handler, Opts}}) ->
- try
- Handler:terminate(Req#http_req{resp_state=locked}, HandlerState)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Handler ~p terminating in terminate/2~n"
- " for the reason ~p:~p~n"
- "** Options were ~p~n** Handler state was ~p~n"
- "** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Class, Reason, Opts,
- HandlerState, Req, erlang:get_stacktrace()])
- end.
- -spec terminate_request(any(), #http_req{}, #state{}) -> ok | none().
- terminate_request(HandlerState, Req, State) ->
- HandlerRes = handler_terminate(HandlerState, Req, State),
- next_request(Req, State, HandlerRes).
- -spec next_request(#http_req{}, #state{}, any()) -> ok | none().
- next_request(Req=#http_req{connection=Conn, buffer=Buffer},
- State=#state{req_keepalive=Keepalive, max_keepalive=MaxKeepalive},
- HandlerRes) ->
- BodyRes = ensure_body_processed(Req),
- RespRes = ensure_response(Req),
- case {HandlerRes, BodyRes, RespRes, Conn} of
- {ok, ok, ok, keepalive} when Keepalive < MaxKeepalive ->
- ?MODULE:parse_request(State#state{
- buffer=Buffer, req_empty_lines=0,
- req_keepalive=Keepalive + 1});
- _Closed ->
- terminate(State)
- end.
- -spec ensure_body_processed(#http_req{}) -> ok | close.
- ensure_body_processed(#http_req{body_state=done}) ->
- ok;
- ensure_body_processed(Req=#http_req{body_state=waiting}) ->
- case cowboy_http_req:body(Req) of
- {error, badarg} -> ok; %% No body.
- {error, _Reason} -> close;
- _Any -> ok
- end.
- -spec ensure_response(#http_req{}) -> ok.
- %% The handler has already fully replied to the client.
- ensure_response(#http_req{resp_state=done}) ->
- ok;
- %% No response has been sent but everything apparently went fine.
- %% Reply with 204 No Content to indicate this.
- ensure_response(Req=#http_req{resp_state=waiting}) ->
- _ = cowboy_http_req:reply(204, [], [], Req),
- ok;
- %% Close the chunked reply.
- ensure_response(#http_req{method='HEAD', resp_state=chunks}) ->
- close;
- ensure_response(#http_req{socket=Socket, transport=Transport,
- resp_state=chunks}) ->
- Transport:send(Socket, <<"0\r\n\r\n">>),
- close.
- -spec error_terminate(http_status(), #state{}) -> ok.
- error_terminate(Code, State=#state{socket=Socket, transport=Transport}) ->
- _ = cowboy_http_req:reply(Code, [], [], #http_req{
- socket=Socket, transport=Transport,
- connection=close, resp_state=waiting}),
- terminate(State).
- -spec terminate(#state{}) -> ok.
- terminate(#state{socket=Socket, transport=Transport}) ->
- Transport:close(Socket),
- ok.
- %% Internal.
- -spec version_to_connection(http_version()) -> keepalive | close.
- version_to_connection({1, 1}) -> keepalive;
- version_to_connection(_Any) -> close.
- -spec default_port(atom()) -> 80 | 443.
- default_port(ssl) -> 443;
- default_port(_) -> 80.
- %% @todo While 32 should be enough for everybody, we should probably make
- %% this configurable or something.
- -spec format_header(atom()) -> atom(); (binary()) -> binary().
- format_header(Field) when is_atom(Field) ->
- Field;
- format_header(Field) when byte_size(Field) =< 20; byte_size(Field) > 32 ->
- Field;
- format_header(Field) ->
- format_header(Field, true, <<>>).
- -spec format_header(binary(), boolean(), binary()) -> binary().
- format_header(<<>>, _Any, Acc) ->
- Acc;
- %% Replicate a bug in OTP for compatibility reasons when there's a - right
- %% after another. Proper use should always be 'true' instead of 'not Bool'.
- format_header(<< $-, Rest/bits >>, Bool, Acc) ->
- format_header(Rest, not Bool, << Acc/binary, $- >>);
- format_header(<< C, Rest/bits >>, true, Acc) ->
- format_header(Rest, false, << Acc/binary, (cowboy_bstr:char_to_upper(C)) >>);
- format_header(<< C, Rest/bits >>, false, Acc) ->
- format_header(Rest, false, << Acc/binary, (cowboy_bstr:char_to_lower(C)) >>).
- %% Tests.
- -ifdef(TEST).
- format_header_test_() ->
- %% {Header, Result}
- Tests = [
- {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>},
- {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>},
- {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>},
- {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>},
- %% These last tests ensures we're formatting headers exactly like OTP.
- %% Even though it's dumb, it's better for compatibility reasons.
- {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--version">>},
- {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>}
- ],
- [{H, fun() -> R = format_header(H) end} || {H, R} <- Tests].
- -endif.
|