%% @doc: Elli HTTP request implementation %% %% An elli_http process blocks in elli_tcp:accept/2 until a client %% connects. It then handles requests on that connection until it's %% closed either by the client timing out or explicitly by the user. -module(elli_http). -include("elli.hrl"). -include("elli_util.hrl"). %% API -export([start_link/4]). -export([send_response/4]). -export([mk_req/7]). %% useful when testing. %% Exported for looping with a fully-qualified module name -export([accept/4, handle_request/4, chunk_loop/1, split_args/1, parse_path/1, keepalive_loop/3, keepalive_loop/5]). -spec start_link(pid(), elli_tcp:socket(), proplists:proplist(), callback()) -> pid(). start_link(Server, ListenSocket, Options, Callback) -> proc_lib:spawn_link(?MODULE, accept, [Server, ListenSocket, Options, Callback]). -spec accept(pid(), elli_tcp:socket(), proplists:proplist(), callback()) -> ok. %% @doc: Accept on the socket until a client connects. Handles the %% request, then loops if we're using keep alive or chunked %% transfer. If accept doesn't give us a socket within a configurable %% timeout, we loop to allow code upgrades of this module. accept(Server, ListenSocket, Options, Callback) -> case catch elli_tcp:accept(ListenSocket, accept_timeout(Options)) of {ok, Socket} -> t(accepted), gen_server:cast(Server, accepted), ?MODULE:keepalive_loop(Socket, Options, Callback); {error, timeout} -> ?MODULE:accept(Server, ListenSocket, Options, Callback); {error, econnaborted} -> ?MODULE:accept(Server, ListenSocket, Options, Callback); {error, {tls_alert, _}} -> ?MODULE:accept(Server, ListenSocket, Options, Callback); {error, closed} -> ok; {error, Other} -> exit({error, Other}) end. %% @doc: Handle multiple requests on the same connection, ie. "keep %% alive". keepalive_loop(Socket, Options, Callback) -> keepalive_loop(Socket, 0, <<>>, Options, Callback). keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of {keep_alive, NewBuffer} -> ?MODULE:keepalive_loop(Socket, NumRequests, NewBuffer, Options, Callback); {close, _} -> elli_tcp:close(Socket), ok end. -spec handle_request(elli_tcp:socket(), binary(), proplists:proplist(), callback()) -> {'keep_alive' | 'close', binary()}. %% @doc: Handle a HTTP request that will possibly come on the %% socket. Returns the appropriate connection token and any buffer %% containing (parts of) the next request. handle_request(S, PrevB, Opts, {Mod, Args} = Callback) -> {Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback), t(request_start), {RequestHeaders, B1} = get_headers(S, V, B0, Opts, Callback), t(headers_end), Req = mk_req(Method, RawPath, RequestHeaders, <<>>, V, S, Callback), case init(Req) of {ok, standard} -> {RequestBody, B2} = get_body(S, RequestHeaders, B1, Opts, Callback), t(body_end), Req1 = Req#req{body = RequestBody}, t(user_start), Response = execute_callback(Req1), t(user_end), handle_response(Req1, B2, Response); {ok, handover} -> Req1 = Req#req{body = B1}, t(user_start), Response = Mod:handle(Req1, Args), t(user_end), t(request_end), handle_event(Mod, request_complete, [Req1, handover, [], <<>>, get_timings()], Args), Response end. handle_response(Req, Buffer, {response, Code, UserHeaders, Body}) -> #req{callback = {Mod, Args}} = Req, Headers = [connection(Req, UserHeaders), content_length(UserHeaders, Body) | UserHeaders], send_response(Req, Code, Headers, Body), t(request_end), handle_event(Mod, request_complete, [Req, Code, Headers, Body, get_timings()], Args), {close_or_keepalive(Req, UserHeaders), Buffer}; handle_response(Req, _Buffer, {chunk, UserHeaders, Initial}) -> #req{callback = {Mod, Args}} = Req, ResponseHeaders = [{<<"Transfer-Encoding">>, <<"chunked">>}, connection(Req, UserHeaders) | UserHeaders], send_response(Req, 200, ResponseHeaders, <<"">>), Initial =:= <<"">> orelse send_chunk(Req#req.socket, Initial), ClosingEnd = case start_chunk_loop(Req#req.socket) of {error, client_closed} -> client; ok -> server end, t(request_end), handle_event(Mod, chunk_complete, [Req, 200, ResponseHeaders, ClosingEnd, get_timings()], Args), {close, <<>>}; handle_response(Req, Buffer, {file, ResponseCode, UserHeaders, Filename, Range}) -> #req{callback = {Mod, Args}} = Req, ResponseHeaders = [connection(Req, UserHeaders) | UserHeaders], send_file(Req, ResponseCode, ResponseHeaders, Filename, Range), t(request_end), handle_event(Mod, request_complete, [Req, ResponseCode, ResponseHeaders, <<>>, get_timings()], Args), {close_or_keepalive(Req, UserHeaders), Buffer}. %% @doc: Generates a HTTP response and sends it to the client send_response(Req, Code, Headers, UserBody) -> Body = case {Req#req.method, Code} of {'HEAD', _} -> <<>>; {_, 304} -> <<>>; {_, 204} -> <<>>; _ -> UserBody end, Response = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, encode_headers(Headers), <<"\r\n">>, Body], case elli_tcp:send(Req#req.socket, Response) of ok -> ok; {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> #req{callback = {Mod, Args}} = Req, handle_event(Mod, client_closed, [before_response], Args), ok end. -spec send_file(Request::#req{}, Code::response_code(), Headers::headers(), Filename::file:filename(), Range::range()) -> ok. %% @doc: Sends a HTTP response to the client where the body is the %% contents of the given file. Assumes correctly set response code %% and headers. send_file(Req, Code, Headers, Filename, {Offset, Length}) -> #req{callback = {Mod, Args}} = Req, ResponseHeaders = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, encode_headers(Headers), <<"\r\n">>], case file:open(Filename, [read, raw, binary]) of {ok, Fd} -> try elli_tcp:send(Req#req.socket, ResponseHeaders) of ok -> case elli_tcp:sendfile(Fd, Req#req.socket, Offset, Length, []) of {ok, _BytesSent} -> ok; {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> handle_event(Mod, client_closed, [before_response], Args) end; {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> handle_event(Mod, client_closed, [before_response], Args) after file:close(Fd) end; {error, FileError} -> handle_event(Mod, file_error, [FileError], Args) end, ok. send_bad_request(Socket) -> %% To send a response, we must first have received everything the %% client is sending. If this is not the case, send_bad_request/1 %% might reset the client connection. Body = <<"Bad Request">>, Response = [<<"HTTP/1.1 ">>, status(400), <<"\r\n">>, <<"Content-Length: ">>, integer_to_list(size(Body)), <<"\r\n">>, <<"\r\n">>], elli_tcp:send(Socket, Response). %% @doc: Executes the user callback, translating failure into a proper %% response. execute_callback(#req{callback = {Mod, Args}} = Req) -> try Mod:handle(Req, Args) of {ok, Headers, {file, Filename}} -> {file, 200, Headers, Filename, {0, 0}}; {ok, Headers, {file, Filename, Range}}-> {file, 200, Headers, Filename, Range}; {ok, Headers, Body} -> {response, 200, Headers, Body}; {ok, Body} -> {response, 200, [], Body}; {chunk, Headers} -> {chunk, Headers, <<"">>}; {chunk, Headers, Initial} -> {chunk, Headers, Initial}; {HttpCode, Headers, {file, Filename}} -> {file, HttpCode, Headers, Filename, {0, 0}}; {HttpCode, Headers, {file, Filename, Range}} -> {file, HttpCode, Headers, Filename, Range}; {HttpCode, Headers, Body} -> {response, HttpCode, Headers, Body}; {HttpCode, Body} -> {response, HttpCode, [], Body}; Unexpected -> handle_event(Mod, invalid_return, [Req, Unexpected], Args), {response, 500, [], <<"Internal server error">>} catch throw:{ResponseCode, Headers, Body} when is_integer(ResponseCode) -> {response, ResponseCode, Headers, Body}; throw:Exc -> handle_event(Mod, request_throw, [Req, Exc, erlang:get_stacktrace()], Args), {response, 500, [], <<"Internal server error">>}; error:Error -> handle_event(Mod, request_error, [Req, Error, erlang:get_stacktrace()], Args), {response, 500, [], <<"Internal server error">>}; exit:Exit -> handle_event(Mod, request_exit, [Req, Exit, erlang:get_stacktrace()], Args), {response, 500, [], <<"Internal server error">>} end. %% %% CHUNKED-TRANSFER %% %% @doc: The chunk loop is an intermediary between the socket and the %% user. We forward anything the user sends until the user sends an %% empty response, which signals that the connection should be %% closed. When the client closes the socket, the loop exits. start_chunk_loop(Socket) -> %% Set the socket to active so we receive the tcp_closed message %% if the client closes the connection elli_tcp:setopts(Socket, [{active, once}]), ?MODULE:chunk_loop(Socket). chunk_loop(Socket) -> {_SockType, InnerSocket} = Socket, receive {tcp_closed, InnerSocket} -> {error, client_closed}; {chunk, close} -> case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of ok -> elli_tcp:close(Socket), ok; {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> {error, client_closed} end; {chunk, close, From} -> case elli_tcp:send(Socket, <<"0\r\n\r\n">>) of ok -> elli_tcp:close(Socket), From ! {self(), ok}, ok; {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> From ! {self(), {error, closed}}, ok end; {chunk, Data} -> send_chunk(Socket, Data), ?MODULE:chunk_loop(Socket); {chunk, Data, From} -> case send_chunk(Socket, Data) of ok -> From ! {self(), ok}; {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> From ! {self(), {error, closed}} end, ?MODULE:chunk_loop(Socket) after 10000 -> ?MODULE:chunk_loop(Socket) end. send_chunk(Socket, Data) -> case iolist_size(Data) of 0 -> ok; Size -> Response = [integer_to_list(Size, 16), <<"\r\n">>, Data, <<"\r\n">>], elli_tcp:send(Socket, Response) end. %% %% RECEIVE REQUEST %% %% @doc: Retrieves the request line get_request(Socket, Buffer, Options, {Mod, Args} = Callback) -> case erlang:decode_packet(http_bin, Buffer, []) of {more, _} -> case elli_tcp:recv(Socket, 0, request_timeout(Options)) of {ok, Data} -> NewBuffer = <>, get_request(Socket, NewBuffer, Options, Callback); {error, timeout} -> handle_event(Mod, request_timeout, [], Args), elli_tcp:close(Socket), exit(normal); {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> handle_event(Mod, request_closed, [], Args), elli_tcp:close(Socket), exit(normal) end; {ok, {http_request, Method, RawPath, Version}, Rest} -> {Method, RawPath, Version, Rest}; {ok, {http_error, _}, _} -> handle_event(Mod, request_parse_error, [Buffer], Args), send_bad_request(Socket), elli_tcp:close(Socket), exit(normal); {ok, {http_response, _, _, _}, _} -> elli_tcp:close(Socket), exit(normal) end. -spec get_headers(elli_tcp:socket(), version(), binary(), proplists:proplist(), callback()) -> {headers(), any()}. get_headers(_Socket, {0, 9}, _, _, _) -> {[], <<>>}; get_headers(Socket, {1, _}, Buffer, Opts, Callback) -> get_headers(Socket, Buffer, [], 0, Opts, Callback). get_headers(Socket, _, Headers, HeadersCount, _Opts, {Mod, Args}) when HeadersCount >= 100 -> handle_event(Mod, bad_request, [{too_many_headers, Headers}], Args), send_bad_request(Socket), elli_tcp:close(Socket), exit(normal); get_headers(Socket, Buffer, Headers, HeadersCount, Opts, {Mod, Args} = Callback) -> case erlang:decode_packet(httph_bin, Buffer, []) of {ok, {http_header, _, Key, _, Value}, Rest} -> NewHeaders = [{ensure_binary(Key), Value} | Headers], get_headers(Socket, Rest, NewHeaders, HeadersCount + 1, Opts, Callback); {ok, http_eoh, Rest} -> {Headers, Rest}; {ok, {http_error, _}, Rest} -> get_headers(Socket, Rest, Headers, HeadersCount, Opts, Callback); {more, _} -> case elli_tcp:recv(Socket, 0, header_timeout(Opts)) of {ok, Data} -> get_headers(Socket, <>, Headers, HeadersCount, Opts, Callback); {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> handle_event(Mod, client_closed, [receiving_headers], Args), elli_tcp:close(Socket), exit(normal); {error, timeout} -> handle_event(Mod, client_timeout, [receiving_headers], Args), elli_tcp:close(Socket), exit(normal) end end. -spec get_body(elli_tcp:socket(), headers(), binary(), proplists:proplist(), callback()) -> {body(), binary()}. %% @doc: Fetches the full body of the request, if any is available. %% %% At the moment we don't need to handle large requests, so there is %% no need for streaming or lazily fetching the body in the user %% code. Fully receiving the body allows us to avoid the complex %% request object threading in Cowboy and the caching in Mochiweb. %% %% As we are always receiving whatever the client sends, we might have %% buffered too much and get parts of the next pipelined request. In %% that case, push it back in the buffer and handle the first request. get_body(Socket, Headers, Buffer, Opts, {Mod, Args} = Callback) -> case proplists:get_value(<<"Content-Length">>, Headers, undefined) of undefined -> {<<>>, Buffer}; ContentLengthBin -> ContentLength = ?b2i(binary:replace(ContentLengthBin, <<" ">>, <<>>, [global])), ok = check_max_size(Socket, ContentLength, Buffer, Opts, Callback), case ContentLength - byte_size(Buffer) of 0 -> {Buffer, <<>>}; N when N > 0 -> case elli_tcp:recv(Socket, N, body_timeout(Opts)) of {ok, Data} -> {<>, <<>>}; {error, Closed} when Closed =:= closed orelse Closed =:= enotconn -> handle_event(Mod, client_closed, [receiving_body], Args), ok = elli_tcp:close(Socket), exit(normal); {error, timeout} -> handle_event(Mod, client_timeout, [receiving_body], Args), ok = elli_tcp:close(Socket), exit(normal) end; _ -> <> = Buffer, {Body, Rest} end end. ensure_binary(Bin) when is_binary(Bin) -> Bin; ensure_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, latin1). check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) -> case ContentLength > max_body_size(Opts) of true -> handle_event(Mod, bad_request, [{body_size, ContentLength}], Args), %% To send a response, we must first receive anything the %% client is sending. To avoid allowing clients to use all %% our bandwidth, if the request size is too big, we %% simply close the socket. case ContentLength < max_body_size(Opts) * 2 of true -> OnSocket = ContentLength - size(Buffer), elli_tcp:recv(Socket, OnSocket, 60000), Response = [<<"HTTP/1.1 ">>, status(413), <<"\r\n">>, <<"Content-Length: 0">>, <<"\r\n\r\n">>], elli_tcp:send(Socket, Response), elli_tcp:close(Socket); false -> elli_tcp:close(Socket) end, exit(normal); false -> ok end. -spec mk_req(Method::http_method(), {PathType::atom(), RawPath::binary()}, RequestHeaders::headers(), RequestBody::body(), V::version(), Socket::elli_tcp:socket() | undefined, Callback::callback()) -> #req{}. mk_req(Method, RawPath, RequestHeaders, RequestBody, V, Socket, Callback) -> {Mod, Args} = Callback, case parse_path(RawPath) of {ok, {Path, URL, URLArgs}} -> #req{method = Method, path = URL, args = URLArgs, version = V, raw_path = Path, headers = RequestHeaders, body = RequestBody, pid = self(), socket = Socket, callback = Callback}; {error, Reason} -> handle_event(Mod, request_parse_error, [{Reason, {Method, RawPath}}], Args), send_bad_request(Socket), elli_tcp:close(Socket), exit(normal) end. %% %% HEADERS %% encode_headers([]) -> []; encode_headers([[] | H]) -> encode_headers(H); encode_headers([{K, V} | H]) -> [encode_value(K), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)]. encode_value(V) when is_integer(V) -> ?i2l(V); encode_value(V) when is_binary(V) -> V; encode_value(V) when is_list(V) -> list_to_binary(V). connection_token(#req{version = {1, 1}, headers = Headers}) -> case proplists:get_value(<<"Connection">>, Headers) of <<"close">> -> <<"close">>; <<"Close">> -> <<"close">>; _ -> <<"Keep-Alive">> end; connection_token(#req{version = {1, 0}, headers = Headers}) -> case proplists:get_value(<<"Connection">>, Headers) of <<"Keep-Alive">> -> <<"Keep-Alive">>; _ -> <<"close">> end; connection_token(#req{version = {0, 9}}) -> <<"close">>. close_or_keepalive(Req, UserHeaders) -> case proplists:get_value(<<"Connection">>, UserHeaders) of undefined -> case connection_token(Req) of <<"Keep-Alive">> -> keep_alive; <<"close">> -> close end; <<"close">> -> close; <<"Keep-Alive">> -> keep_alive end. %% @doc: Adds appropriate connection header if the user did not add %% one already. connection(Req, UserHeaders) -> case proplists:get_value(<<"Connection">>, UserHeaders) of undefined -> {<<"Connection">>, connection_token(Req)}; _ -> [] end. content_length(Headers, Body)-> case proplists:is_defined(<<"Content-Length">>, Headers) of true -> []; false -> {<<"Content-Length">>, iolist_size(Body)} end. %% %% PATH HELPERS %% parse_path({abs_path, FullPath}) -> case binary:split(FullPath, [<<"?">>]) of [URL] -> {ok, {FullPath, split_path(URL), []}}; [URL, Args] -> {ok, {FullPath, split_path(URL), split_args(Args)}} end; parse_path({absoluteURI, _Scheme, _Host, _Port, Path}) -> parse_path({abs_path, Path}); parse_path(_) -> {error, unsupported_uri}. split_path(Path) -> [P || P <- binary:split(Path, [<<"/">>], [global]), P =/= <<>>]. %% @doc: Splits the url arguments into a proplist. Lifted from %% cowboy_http:x_www_form_urlencoded/2 -spec split_args(binary()) -> list({binary(), binary() | true}). split_args(<<>>) -> []; split_args(Qs) -> Tokens = binary:split(Qs, <<"&">>, [global, trim]), [case binary:split(Token, <<"=">>) of [Token] -> {Token, true}; [Name, Value] -> {Name, Value} end || Token <- Tokens]. %% %% CALLBACK HELPERS %% init(#req{callback = {Mod, Args}} = Req) -> case erlang:function_exported(Mod, init, 2) of true -> case Mod:init(Req, Args) of ignore -> {ok, standard}; {ok, Behaviour} -> {ok, Behaviour} end; false -> {ok, standard} end. handle_event(Mod, Name, EventArgs, ElliArgs) -> try Mod:handle_event(Name, EventArgs, ElliArgs) catch EvClass:EvError -> error_logger:error_msg("~p:handle_event/3 crashed ~p:~p~n~p", [Mod, EvClass, EvError, erlang:get_stacktrace()]) end. %% %% TIMING HELPERS %% %% @doc: Record the current time in the process dictionary. This %% allows easily adding time tracing wherever, without passing along %% any variables. t(Key) -> put({time, Key}, os:timestamp()). get_timings() -> lists:flatmap(fun ({{time, Key}, Val}) -> if Key =:= accepted -> ok; true -> erase({time, Key}) end, [{Key, Val}]; (_) -> [] end, get()). %% %% OPTIONS %% accept_timeout(Opts) -> proplists:get_value(accept_timeout, Opts). request_timeout(Opts) -> proplists:get_value(request_timeout, Opts). header_timeout(Opts) -> proplists:get_value(header_timeout, Opts). body_timeout(Opts) -> proplists:get_value(body_timeout, Opts). max_body_size(Opts) -> proplists:get_value(max_body_size, Opts). %% %% HTTP STATUS CODES %% %% @doc: Response code string. Lifted from cowboy_http_req.erl status(100) -> <<"100 Continue">>; status(101) -> <<"101 Switching Protocols">>; status(102) -> <<"102 Processing">>; status(200) -> <<"200 OK">>; status(201) -> <<"201 Created">>; status(202) -> <<"202 Accepted">>; status(203) -> <<"203 Non-Authoritative Information">>; status(204) -> <<"204 No Content">>; status(205) -> <<"205 Reset Content">>; status(206) -> <<"206 Partial Content">>; status(207) -> <<"207 Multi-Status">>; status(226) -> <<"226 IM Used">>; status(300) -> <<"300 Multiple Choices">>; status(301) -> <<"301 Moved Permanently">>; status(302) -> <<"302 Found">>; status(303) -> <<"303 See Other">>; status(304) -> <<"304 Not Modified">>; status(305) -> <<"305 Use Proxy">>; status(306) -> <<"306 Switch Proxy">>; status(307) -> <<"307 Temporary Redirect">>; status(400) -> <<"400 Bad Request">>; status(401) -> <<"401 Unauthorized">>; status(402) -> <<"402 Payment Required">>; status(403) -> <<"403 Forbidden">>; status(404) -> <<"404 Not Found">>; status(405) -> <<"405 Method Not Allowed">>; status(406) -> <<"406 Not Acceptable">>; status(407) -> <<"407 Proxy Authentication Required">>; status(408) -> <<"408 Request Timeout">>; status(409) -> <<"409 Conflict">>; status(410) -> <<"410 Gone">>; status(411) -> <<"411 Length Required">>; status(412) -> <<"412 Precondition Failed">>; status(413) -> <<"413 Request Entity Too Large">>; status(414) -> <<"414 Request-URI Too Long">>; status(415) -> <<"415 Unsupported Media Type">>; status(416) -> <<"416 Requested Range Not Satisfiable">>; status(417) -> <<"417 Expectation Failed">>; status(418) -> <<"418 I'm a teapot">>; status(422) -> <<"422 Unprocessable Entity">>; status(423) -> <<"423 Locked">>; status(424) -> <<"424 Failed Dependency">>; status(425) -> <<"425 Unordered Collection">>; status(426) -> <<"426 Upgrade Required">>; status(428) -> <<"428 Precondition Required">>; status(429) -> <<"429 Too Many Requests">>; status(431) -> <<"431 Request Header Fields Too Large">>; status(500) -> <<"500 Internal Server Error">>; status(501) -> <<"501 Not Implemented">>; status(502) -> <<"502 Bad Gateway">>; status(503) -> <<"503 Service Unavailable">>; status(504) -> <<"504 Gateway Timeout">>; status(505) -> <<"505 HTTP Version Not Supported">>; status(506) -> <<"506 Variant Also Negotiates">>; status(507) -> <<"507 Insufficient Storage">>; status(510) -> <<"510 Not Extended">>; status(511) -> <<"511 Network Authentication Required">>; status(B) when is_binary(B) -> B. %% %% UNIT TESTS %% -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). get_body_test() -> Socket = socket, Headers = [{<<"Content-Length">>, <<" 42 ">>}], Buffer = binary:copy(<<".">>, 42), Opts = [], Callback = {no, op}, ?assertEqual({Buffer, <<>>}, get_body(Socket, Headers, Buffer, Opts, Callback)). -endif.