%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2015. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
%%
%% Description: Implements a request handler process for the HTTP server.
%%
-module(httpd_request_handler).
-behaviour(gen_server).
%% Application internal API
-export([start_link/2, start_link/3, socket_ownership_transfered/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-include("httpd.hrl").
-include("http_internal.hrl").
-include("httpd_internal.hrl").
-define(HANDSHAKE_TIMEOUT, 5000).
-record(state, {mod, %% #mod{}
manager, %% pid()
status, %% accept | busy | blocked
mfa, %% {Module, Function, Args}
max_keep_alive_request = infinity, %% integer() | infinity
response_sent = false, %% true | false
timeout, %% infinity | integer() > 0
timer, %% ref() - Request timer
headers, %% #http_request_h{}
body, %% binary()
data, %% The total data received in bits, checked after 10s
byte_limit %% Bit limit per second before kick out
}).
%%====================================================================
%% Application internal API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start() -> {ok, Pid} | ignore | {error,Error}
%% Description: Starts a httpd-request handler process. Intended to be
%% called by the httpd acceptor process.
%%--------------------------------------------------------------------
start_link(Manager, ConfigDB) ->
start_link(Manager, ConfigDB, 15000).
start_link(Manager, ConfigDB, AcceptTimeout) ->
proc_lib:start_link(?MODULE, init, [[Manager, ConfigDB,AcceptTimeout]]).
%%--------------------------------------------------------------------
%% socket_ownership_transfered(Pid, SocketType, Socket) -> void()
%%
%% Pid = pid()
%% SocketType = ip_comm | ssl
%% Socket = socket()
%%
%% Description: Send a message to the request handler process
%% confirming that the socket ownership has now sucssesfully been
%% transfered to it. Intended to be called by the httpd acceptor
%% process.
%%--------------------------------------------------------------------
socket_ownership_transfered(Pid, SocketType, Socket) ->
Pid ! {socket_ownership_transfered, SocketType, Socket}.
%%--------------------------------------------------------------------
%% Function: init(Args) -> _
%%
%% Description: Initiates the server. Obs special init that uses
%% gen_server:enter_loop/3. This is used instead of the normal
%% gen_server callback init, as a more complex init than the
%% gen_server provides is needed.
%%--------------------------------------------------------------------
init([Manager, ConfigDB, AcceptTimeout]) ->
process_flag(trap_exit, true),
%% Make sure this process terminates if the httpd manager process
%% should die!
%%link(Manager),
%% At this point the function httpd_request_handler:start/2 will return.
proc_lib:init_ack({ok, self()}),
{SocketType, Socket} = await_socket_ownership_transfer(AcceptTimeout),
%%Timeout value is in seconds we want it in milliseconds
KeepAliveTimeOut = 1000 * httpd_util:lookup(ConfigDB, keep_alive_timeout, 150),
case http_transport:negotiate(SocketType, Socket, ?HANDSHAKE_TIMEOUT) of
{error, _Error} ->
exit(shutdown); %% Can be 'normal'.
ok ->
continue_init(Manager, ConfigDB, SocketType, Socket, KeepAliveTimeOut)
end.
continue_init(Manager, ConfigDB, SocketType, Socket, TimeOut) ->
Resolve = http_transport:resolve(),
Peername = httpd_socket:peername(SocketType, Socket),
InitData = #init_data{peername = Peername, resolve = Resolve},
Mod = #mod{config_db = ConfigDB,
socket_type = SocketType,
socket = Socket,
init_data = InitData},
MaxHeaderSize = max_header_size(ConfigDB),
MaxURISize = max_uri_size(ConfigDB),
NrOfRequest = max_keep_alive_request(ConfigDB),
MaxContentLen = max_content_length(ConfigDB),
{_, Status} = httpd_manager:new_connection(Manager),
MFA = {httpd_request, parse, [[{max_uri, MaxURISize}, {max_header, MaxHeaderSize},
{max_version, ?HTTP_MAX_VERSION_STRING},
{max_method, ?HTTP_MAX_METHOD_STRING},
{max_content_length, MaxContentLen}
]]},
State = #state{mod = Mod,
manager = Manager,
status = Status,
timeout = TimeOut,
max_keep_alive_request = NrOfRequest,
mfa = MFA},
http_transport:setopts(SocketType, Socket,
[binary, {packet, 0}, {active, once}]),
NewState = data_receive_counter(activate_request_timeout(State), httpd_util:lookup(ConfigDB, minimum_bytes_per_second, false)),
gen_server:enter_loop(?MODULE, [], NewState).
%%====================================================================
%% gen_server callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call(Request, From, #state{mod = ModData} = State) ->
Error =
lists:flatten(
io_lib:format("Unexpected request: "
"~n~p"
"~nto request handler (~p) from ~p"
"~n", [Request, self(), From])),
error_log(Error, ModData),
{stop, {call_api_violation, Request, From}, State}.
%%--------------------------------------------------------------------
%% handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast(Msg, #state{mod = ModData} = State) ->
Error =
lists:flatten(
io_lib:format("Unexpected message: "
"~n~p"
"~nto request handler (~p)"
"~n", [Msg, self()])),
error_log(Error, ModData),
{noreply, State}.
%%--------------------------------------------------------------------
%% handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info({Proto, Socket, Data},
#state{mfa = {Module, Function, Args},
mod = #mod{socket_type = SockType,
socket = Socket} = ModData} = State)
when (((Proto =:= tcp) orelse
(Proto =:= ssl) orelse
(Proto =:= dummy)) andalso is_binary(Data)) ->
PROCESSED = (catch Module:Function([Data | Args])),
NewDataSize = case State#state.byte_limit of
undefined ->
undefined;
_ ->
State#state.data + byte_size(Data)
end,
case PROCESSED of
{ok, Result} ->
NewState = case NewDataSize of
undefined ->
cancel_request_timeout(State);
_ ->
set_new_data_size(cancel_request_timeout(State), NewDataSize)
end,
handle_http_msg(Result, NewState);
{error, {size_error, MaxSize, ErrCode, ErrStr}, Version} ->
NewModData = ModData#mod{http_version = Version},
httpd_response:send_status(NewModData, ErrCode, ErrStr),
Reason = io_lib:format("~p: ~p max size is ~p~n",
[ErrCode, ErrStr, MaxSize]),
error_log(Reason, NewModData),
{stop, normal, State#state{response_sent = true,
mod = NewModData}};
NewMFA ->
http_transport:setopts(SockType, Socket, [{active, once}]),
case NewDataSize of
undefined ->
{noreply, State#state{mfa = NewMFA}};
_ ->
{noreply, State#state{mfa = NewMFA, data = NewDataSize}}
end
end;
%% Error cases
handle_info({tcp_closed, _}, State) ->
{stop, normal, State};
handle_info({ssl_closed, _}, State) ->
{stop, normal, State};
handle_info({tcp_error, _, _} = Reason, State) ->
{stop, Reason, State};
handle_info({ssl_error, _, _} = Reason, State) ->
{stop, Reason, State};
%% Timeouts
handle_info(timeout, #state{mfa = {_, parse, _}} = State) ->
%% error_log("No request received on keep-alive connection "
%% "before server side timeout", ModData),
%% No response should be sent!
{stop, normal, State#state{response_sent = true}};
handle_info(timeout, #state{mod = ModData} = State) ->
httpd_response:send_status(ModData, 408, "Request timeout"),
error_log("The client did not send the whole request before the "
"server side timeout", ModData),
{stop, normal, State#state{response_sent = true}};
handle_info(check_data_first, #state{data = Data, byte_limit = Byte_Limit} = State) ->
case Data >= (Byte_Limit*3) of
true ->
erlang:send_after(1000, self(), check_data),
{noreply, State#state{data = 0}};
_ ->
{stop, normal, State#state{response_sent = true}}
end;
handle_info(check_data, #state{data = Data, byte_limit = Byte_Limit} = State) ->
case Data >= Byte_Limit of
true ->
erlang:send_after(1000, self(), check_data),
{noreply, State#state{data = 0}};
_ ->
{stop, normal, State#state{response_sent = true}}
end;
handle_info({'EXIT', _, Reason}, State) ->
{stop, Reason, State};
%% Default case
handle_info(Info, #state{mod = ModData} = State) ->
Error = lists:flatten(
io_lib:format("Unexpected info: "
"~n~p"
"~nto request handler (~p)"
"~n", [Info, self()])),
error_log(Error, ModData),
{noreply, State}.
%%--------------------------------------------------------------------
%% terminate(Reason, State) -> void()
%%
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(normal, State) ->
do_terminate(State);
terminate(Reason, #state{response_sent = false, mod = ModData} = State) ->
httpd_response:send_status(ModData, 500, none),
ReasonStr =
lists:flatten(io_lib:format("~s - ~p",
[httpd_util:reason_phrase(500), Reason])),
error_log(ReasonStr, ModData),
terminate(Reason, State#state{response_sent = true, mod = ModData});
terminate(_Reason, State) ->
do_terminate(State).
do_terminate(#state{mod = ModData} = State) ->
cancel_request_timeout(State),
httpd_socket:close(ModData#mod.socket_type, ModData#mod.socket).
%%--------------------------------------------------------------------
%% code_change(OldVsn, State, Extra) -> {ok, NewState}
%%
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
set_new_data_size(State, NewData) ->
State#state{data = NewData}.
await_socket_ownership_transfer(AcceptTimeout) ->
receive
{socket_ownership_transfered, SocketType, Socket} ->
{SocketType, Socket}
after AcceptTimeout ->
exit(accept_socket_timeout)
end.
handle_http_msg({_, _, Version, {_, _}, _},
#state{status = busy, mod = ModData} = State) ->
handle_manager_busy(State#state{mod =
ModData#mod{http_version = Version}}),
{stop, normal, State};
handle_http_msg({_, _, Version, {_, _}, _},
#state{status = blocked, mod = ModData} = State) ->
handle_manager_blocked(State#state{mod =
ModData#mod{http_version = Version}}),
{stop, normal, State};
handle_http_msg({Method, Uri, Version, {RecordHeaders, Headers}, Body},
#state{status = accept, mod = ModData} = State) ->
case httpd_request:validate(Method, Uri, Version) of
ok ->
{ok, NewModData} =
httpd_request:update_mod_data(ModData, Method, Uri,
Version, Headers),
case is_host_specified_if_required(NewModData#mod.absolute_uri,
RecordHeaders, Version) of
true ->
handle_body(State#state{headers = RecordHeaders,
body = Body,
mod = NewModData});
false ->
httpd_response:send_status(ModData#mod{http_version =
Version},
400, none),
{stop, normal, State#state{response_sent = true}}
end;
{error, {not_supported, What}} ->
httpd_response:send_status(ModData#mod{http_version = Version},
501, {Method, Uri, Version}),
Reason = io_lib:format("Not supported: ~p~n", [What]),
error_log(Reason, ModData),
{stop, normal, State#state{response_sent = true}};
{error, {bad_request, {forbidden, URI}}} ->
httpd_response:send_status(ModData#mod{http_version = Version},
403, URI),
Reason = io_lib:format("Forbidden URI: ~p~n", [URI]),
error_log(Reason, ModData),
{stop, normal, State#state{response_sent = true}};
{error, {bad_request, {malformed_syntax, URI}}} ->
httpd_response:send_status(ModData#mod{http_version = Version},
400, URI),
Reason = io_lib:format("Malformed syntax in URI: ~p~n", [URI]),
error_log(Reason, ModData),
{stop, normal, State#state{response_sent = true}};
{error, {bad_version, Ver}} ->
httpd_response:send_status(ModData#mod{http_version = "HTTP/0.9"}, 400, Ver),
Reason = io_lib:format("Malformed syntax version: ~p~n", [Ver]),
error_log(Reason, ModData),
{stop, normal, State#state{response_sent = true}}
end;
handle_http_msg({ChunkedHeaders, Body},
State = #state{headers = Headers}) ->
NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders),
handle_response(State#state{headers = NewHeaders, body = Body});
handle_http_msg(Body, State) ->
handle_response(State#state{body = Body}).
handle_manager_busy(#state{mod = #mod{config_db = ConfigDB}} = State) ->
MaxClients = httpd_util:lookup(ConfigDB, max_clients, 150),
Reason = io_lib:format("heavy load (>~w processes)", [MaxClients]),
reject_connection(State, lists:flatten(Reason)).
handle_manager_blocked(State) ->
Reason = "Server maintenance performed, try again later",
reject_connection(State, Reason).
reject_connection(#state{mod = ModData} = State, Reason) ->
httpd_response:send_status(ModData, 503, Reason),
{stop, normal, State#state{response_sent = true}}.
is_host_specified_if_required(nohost, #http_request_h{host = undefined},
"HTTP/1.1") ->
false;
is_host_specified_if_required(_, _, _) ->
true.
handle_body(#state{mod = #mod{config_db = ConfigDB}} = State) ->
MaxHeaderSize = max_header_size(ConfigDB),
MaxBodySize = max_body_size(ConfigDB),
case handle_expect(State, MaxBodySize) of
ok ->
handle_body(State, MaxHeaderSize, MaxBodySize);
Other ->
Other
end.
handle_body(#state{headers = Headers, body = Body, mod = ModData} = State,
MaxHeaderSize, MaxBodySize) ->
case Headers#http_request_h.'transfer-encoding' of
"chunked" ->
case http_chunk:decode(Body, MaxBodySize, MaxHeaderSize) of
{Module, Function, Args} ->
http_transport:setopts(ModData#mod.socket_type,
ModData#mod.socket,
[{active, once}]),
{noreply, State#state{mfa =
{Module, Function, Args}}};
{ok, {ChunkedHeaders, NewBody}} ->
NewHeaders =
http_chunk:handle_headers(Headers, ChunkedHeaders),
handle_response(State#state{headers = NewHeaders,
body = NewBody})
end;
Encoding when is_list(Encoding) ->
httpd_response:send_status(ModData, 501,
"Unknown Transfer-Encoding"),
Reason = io_lib:format("Unknown Transfer-Encoding: ~p~n",
[Encoding]),
error_log(Reason, ModData),
{stop, normal, State#state{response_sent = true}};
_ ->
Length = list_to_integer(Headers#http_request_h.'content-length'),
case ((Length =< MaxBodySize) or (MaxBodySize == nolimit)) of
true ->
case httpd_request:whole_body(Body, Length) of
{Module, Function, Args} ->
http_transport:setopts(ModData#mod.socket_type,
ModData#mod.socket,
[{active, once}]),
{noreply, State#state{mfa =
{Module, Function, Args}}};
{ok, NewBody} ->
handle_response(
State#state{headers = Headers,
body = NewBody})
end;
false ->
httpd_response:send_status(ModData, 413, "Body too long"),
error_log("Body too long", ModData),
{stop, normal, State#state{response_sent = true}}
end
end.
handle_expect(#state{headers = Headers, mod =
#mod{config_db = ConfigDB} = ModData} = State,
MaxBodySize) ->
Length = list_to_integer(Headers#http_request_h.'content-length'),
case expect(Headers, ModData#mod.http_version, ConfigDB) of
continue when (MaxBodySize > Length) orelse (MaxBodySize =:= nolimit) ->
httpd_response:send_status(ModData, 100, ""),
ok;
continue when MaxBodySize < Length ->
httpd_response:send_status(ModData, 413, "Body too long"),
error_log("Body too long", ModData),
{stop, normal, State#state{response_sent = true}};
{break, Value} ->
httpd_response:send_status(ModData, 417,
"Unexpected expect value"),
Reason = io_lib:format("Unexpected expect value: ~p~n", [Value]),
error_log(Reason, ModData),
{stop, normal, State#state{response_sent = true}};
no_expect_header ->
ok;
http_1_0_expect_header ->
httpd_response:send_status(ModData, 400,
"Only HTTP/1.1 Clients "
"may use the Expect Header"),
error_log("Client with lower version than 1.1 tried to send"
"an expect header", ModData),
{stop, normal, State#state{response_sent = true}}
end.
expect(Headers, "HTTP/1.1", _) ->
case Headers#http_request_h.expect of
"100-continue" ->
continue;
undefined ->
no_expect_header;
Other ->
{break, Other}
end;
expect(Headers, _, ConfigDB) ->
case Headers#http_request_h.expect of
undefined ->
no_expect_header;
_ ->
case httpd_util:lookup(ConfigDB, expect, continue) of
continue->
no_expect_header;
_ ->
http_1_0_expect_header
end
end.
handle_response(#state{body = Body,
mod = ModData,
headers = Headers,
max_keep_alive_request = Max} = State) when Max > 0 ->
{NewBody, Data} = httpd_request:body_data(Headers, Body),
ok = httpd_response:generate_and_send_response(
ModData#mod{entity_body = NewBody}),
handle_next_request(State#state{response_sent = true}, Data);
handle_response(#state{body = Body,
headers = Headers,
mod = ModData} = State) ->
{NewBody, _} = httpd_request:body_data(Headers, Body),
ok = httpd_response:generate_and_send_response(
ModData#mod{entity_body = NewBody}),
{stop, normal, State#state{response_sent = true}}.
handle_next_request(#state{mod = #mod{connection = true} = ModData,
max_keep_alive_request = Max} = State, Data) ->
NewModData = #mod{socket_type = ModData#mod.socket_type,
socket = ModData#mod.socket,
config_db = ModData#mod.config_db,
init_data = ModData#mod.init_data},
MaxHeaderSize = max_header_size(ModData#mod.config_db),
MaxURISize = max_uri_size(ModData#mod.config_db),
MaxContentLen = max_content_length(ModData#mod.config_db),
MFA = {httpd_request, parse, [[{max_uri, MaxURISize}, {max_header, MaxHeaderSize},
{max_version, ?HTTP_MAX_VERSION_STRING},
{max_method, ?HTTP_MAX_METHOD_STRING},
{max_content_length, MaxContentLen}
]]},
TmpState = State#state{mod = NewModData,
mfa = MFA,
max_keep_alive_request = decrease(Max),
headers = undefined,
body = undefined,
response_sent = false},
NewState = activate_request_timeout(TmpState),
case Data of
<<>> ->
http_transport:setopts(ModData#mod.socket_type,
ModData#mod.socket, [{active, once}]),
{noreply, NewState};
_ ->
handle_info({dummy, ModData#mod.socket, Data}, NewState)
end;
handle_next_request(State, _) ->
{stop, normal, State}.
activate_request_timeout(#state{timeout = Time} = State) ->
Ref = erlang:send_after(Time, self(), timeout),
State#state{timer = Ref}.
data_receive_counter(State, Byte_limit) ->
case Byte_limit of
false ->
State#state{data = 0};
Nr ->
erlang:send_after(3000, self(), check_data_first),
State#state{data = 0, byte_limit = Nr}
end.
cancel_request_timeout(#state{timer = undefined} = State) ->
State;
cancel_request_timeout(#state{timer = Timer} = State) ->
erlang:cancel_timer(Timer),
receive
timeout ->
ok
after 0 ->
ok
end,
State#state{timer = undefined}.
decrease(N) when is_integer(N) ->
N-1;
decrease(N) ->
N.
error_log(ReasonString, Info) ->
Error = lists:flatten(
io_lib:format("Error reading request: ~s", [ReasonString])),
error_log(mod_log, Info, Error),
error_log(mod_disk_log, Info, Error).
error_log(Mod, #mod{config_db = ConfigDB} = Info, String) ->
Modules = httpd_util:lookup(ConfigDB, modules,
[mod_get, mod_head, mod_log]),
case lists:member(Mod, Modules) of
true ->
Mod:error_log(Info, String);
_ ->
ok
end.
%%--------------------------------------------------------------------
%% Config access wrapper functions
%%--------------------------------------------------------------------
max_header_size(ConfigDB) ->
httpd_util:lookup(ConfigDB, max_header_size, ?HTTP_MAX_HEADER_SIZE).
max_uri_size(ConfigDB) ->
httpd_util:lookup(ConfigDB, max_uri_size, ?HTTP_MAX_URI_SIZE).
max_body_size(ConfigDB) ->
httpd_util:lookup(ConfigDB, max_body_size, nolimit).
max_keep_alive_request(ConfigDB) ->
httpd_util:lookup(ConfigDB, max_keep_alive_request, infinity).
max_content_length(ConfigDB) ->
httpd_util:lookup(ConfigDB, max_content_length, ?HTTP_MAX_CONTENT_LENGTH).