blob: 631e93a2a9e83b3ae4dc258cc5554f65ec80f083 [file] [log] [blame]
%%% File : ibrowse_test_server.erl
%%% Author : Chandrashekhar Mullaparthi <chandrashekhar.mullaparthi@t-mobile.co.uk>
%%% Description : A server to simulate various test scenarios
%%% Created : 17 Oct 2010 by Chandrashekhar Mullaparthi <chandrashekhar.mullaparthi@t-mobile.co.uk>
-module(ibrowse_test_server).
-export([
start_server/0,
start_server/2,
stop_server/1,
get_conn_pipeline_depth/0
]).
-ifdef(new_rand).
-define(RAND, rand).
-else.
-define(RAND, random).
-endif.
-record(request, {method, uri, version, headers = [], body = [], state}).
-define(dec2hex(X), erlang:integer_to_list(X, 16)).
-define(ACCEPT_TIMEOUT_MS, 10000).
-define(CONN_PIPELINE_DEPTH, conn_pipeline_depth).
start_server() ->
start_server(8181, tcp).
start_server(Port, Sock_type) ->
Fun = fun() ->
Proc_name = server_proc_name(Port),
case whereis(Proc_name) of
undefined ->
register(Proc_name, self()),
ets:new(?CONN_PIPELINE_DEPTH, [named_table, public, set]),
case do_listen(Sock_type, Port, [{active, false},
{reuseaddr, true},
{nodelay, true},
{packet, http}]) of
{ok, Sock} ->
do_trace("Server listening on port: ~p~n", [Port]),
accept_loop(Sock, Sock_type);
Err ->
erlang:error(
lists:flatten(
io_lib:format(
"Failed to start server on port ~p. ~p~n",
[Port, Err]))),
exit({listen_error, Err})
end;
_X ->
ok
end
end,
spawn_link(Fun),
ibrowse_socks_server:start(8282, 0), %% No auth
ibrowse_socks_server:start(8383, 2). %% Username/Password auth
stop_server(Port) ->
catch server_proc_name(Port) ! stop,
ibrowse_socks_server:stop(8282),
ibrowse_socks_server:stop(8383),
timer:sleep(2000), % wait for server to receive msg and unregister
ok.
get_conn_pipeline_depth() ->
ets:tab2list(?CONN_PIPELINE_DEPTH).
server_proc_name(Port) ->
list_to_atom("ibrowse_test_server_"++integer_to_list(Port)).
do_listen(tcp, Port, Opts) ->
gen_tcp:listen(Port, Opts);
do_listen(ssl, Port, Opts) ->
application:start(crypto),
application:start(ssl),
ssl:listen(Port, Opts).
-ifdef(OTP_RELEASE).
do_accept(tcp, Listen_sock) ->
gen_tcp:accept(Listen_sock, ?ACCEPT_TIMEOUT_MS);
do_accept(ssl, Listen_sock) ->
ssl:handshake(Listen_sock, ?ACCEPT_TIMEOUT_MS).
-else.
do_accept(tcp, Listen_sock) ->
gen_tcp:accept(Listen_sock, ?ACCEPT_TIMEOUT_MS);
do_accept(ssl, Listen_sock) ->
ssl:ssl_accept(Listen_sock, ?ACCEPT_TIMEOUT_MS).
-endif.
accept_loop(Sock, Sock_type) ->
case do_accept(Sock_type, Sock) of
{ok, Conn} ->
Pid = spawn_link(fun() -> connection(Conn, Sock_type) end),
set_controlling_process(Conn, Sock_type, Pid),
accept_loop(Sock, Sock_type);
{error, timeout} ->
receive
stop ->
ok
after 10 ->
accept_loop(Sock, Sock_type)
end;
Err ->
Err
end.
connection(Conn, Sock_type) ->
catch ets:insert(?CONN_PIPELINE_DEPTH, {self(), 0}),
try
inet:setopts(Conn, [{packet, http}, {active, true}]),
server_loop(Conn, Sock_type, #request{})
after
catch ets:delete(?CONN_PIPELINE_DEPTH, self())
end.
set_controlling_process(Sock, tcp, Pid) ->
gen_tcp:controlling_process(Sock, Pid);
set_controlling_process(Sock, ssl, Pid) ->
ssl:controlling_process(Sock, Pid).
setopts(Sock, tcp, Opts) ->
inet:setopts(Sock, Opts);
setopts(Sock, ssl, Opts) ->
ssl:setopts(Sock, Opts).
server_loop(Sock, Sock_type, #request{headers = Headers} = Req) ->
receive
{http, Sock, {http_request, HttpMethod, HttpUri, HttpVersion}} ->
catch ets:update_counter(?CONN_PIPELINE_DEPTH, self(), 1),
server_loop(Sock, Sock_type, Req#request{method = HttpMethod,
uri = HttpUri,
version = HttpVersion});
{http, Sock, {http_header, _, _, _, _} = H} ->
server_loop(Sock, Sock_type, Req#request{headers = [H | Headers]});
{http, Sock, http_eoh} ->
case process_request(Sock, Sock_type, Req) of
close_connection ->
gen_tcp:shutdown(Sock, read_write);
not_done ->
ok;
collect_body ->
server_loop(Sock, Sock_type, Req#request{state = collect_body});
_ ->
catch ets:update_counter(?CONN_PIPELINE_DEPTH, self(), -1)
end,
server_loop(Sock, Sock_type, #request{});
{http, Sock, {http_error, Packet}} when Req#request.state == collect_body ->
Req_1 = Req#request{body = list_to_binary([Packet, Req#request.body])},
case process_request(Sock, Sock_type, Req_1) of
close_connection ->
gen_tcp:shutdown(Sock, read_write);
ok ->
server_loop(Sock, Sock_type, #request{});
collect_body ->
server_loop(Sock, Sock_type, Req_1)
end;
{http, Sock, {http_error, Err}} ->
io:format("Error parsing HTTP request:~n"
"Req so far : ~p~n"
"Err : ~p", [Req, Err]),
exit({http_error, Err});
{setopts, Opts} ->
setopts(Sock, Sock_type, Opts),
server_loop(Sock, Sock_type, Req);
{tcp_closed, Sock} ->
do_trace("Client closed connection~n", []),
ok;
Other ->
io:format("Recvd unknown msg: ~p~n", [Other]),
exit({unknown_msg, Other})
after 120000 ->
do_trace("Timing out client connection~n", []),
ok
end.
do_trace(Fmt, Args) ->
do_trace(get(my_trace_flag), Fmt, Args).
do_trace(true, Fmt, Args) ->
io:format("~s -- " ++ Fmt, [ibrowse_lib:printable_date() | Args]);
do_trace(_, _, _) ->
ok.
process_request(Sock, Sock_type,
#request{method='GET',
headers = Headers,
uri = {abs_path, "/ibrowse_stream_once_chunk_pipeline_test"}} = Req) ->
Req_id = case lists:keysearch("X-Ibrowse-Request-Id", 3, Headers) of
false ->
"";
{value, {http_header, _, _, _, Req_id_1}} ->
Req_id_1
end,
Req_id_header = ["x-ibrowse-request-id: ", Req_id, "\r\n"],
do_trace("Recvd req: ~p~n", [Req]),
Body = string:join([integer_to_list(X) || X <- lists:seq(1,100)], "-"),
Chunked_body = chunk_request_body(Body, 50),
Resp_1 = [<<"HTTP/1.1 200 OK\r\n">>,
Req_id_header,
<<"Transfer-Encoding: chunked\r\n\r\n">>],
Resp_2 = Chunked_body,
do_send(Sock, Sock_type, Resp_1),
timer:sleep(100),
do_send(Sock, Sock_type, Resp_2);
process_request(Sock, Sock_type,
#request{method='GET',
headers = _Headers,
uri = {abs_path, "/ibrowse_inac_timeout_test"}} = Req) ->
do_trace("Recvd req: ~p. Sleeping for 30 secs...~n", [Req]),
timer:sleep(3000),
do_trace("...Sending response now.~n", []),
Resp = <<"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n">>,
do_send(Sock, Sock_type, Resp);
process_request(Sock, Sock_type,
#request{method='HEAD',
headers = _Headers,
uri = {abs_path, "/ibrowse_head_transfer_enc"}}) ->
Resp = <<"HTTP/1.1 400 Bad Request\r\nServer: Apache-Coyote/1.1\r\nContent-Length:5\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\n\r\nabcde">>,
do_send(Sock, Sock_type, Resp);
process_request(Sock, Sock_type,
#request{method='POST',
headers = Headers,
uri = {abs_path, "/echo_body"},
body = Body}) ->
Content_len = get_content_length(Headers),
case iolist_size(Body) == Content_len of
true ->
Resp = [<<"HTTP/1.1 200 OK\r\nContent-Length: ">>, integer_to_list(Content_len), <<"\r\nServer: ibrowse_test_server\r\n\r\n">>, Body],
do_send(Sock, Sock_type, list_to_binary(Resp));
false ->
collect_body
end;
process_request(Sock, Sock_type,
#request{method='GET',
headers = Headers,
uri = {abs_path, "/ibrowse_echo_header"}}) ->
Tag = "x-binary",
Headers_1 = [{to_lower(X), to_lower(Y)} || {http_header, _, X, _, Y} <- Headers],
X_binary_header_val = case lists:keysearch(Tag, 1, Headers_1) of
false ->
"not_found";
{value, {_, V}} ->
V
end,
Resp = [<<"HTTP/1.1 200 OK\r\n">>,
<<"Server: ibrowse_test\r\n">>,
Tag, ": ", X_binary_header_val, "\r\n",
<<"Content-Length: 0\r\n\r\n">>],
do_send(Sock, Sock_type, Resp);
process_request(Sock, Sock_type,
#request{method='HEAD',
headers = _Headers,
uri = {abs_path, "/ibrowse_head_test"}}) ->
Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\Date: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\n">>,
do_send(Sock, Sock_type, Resp);
process_request(Sock, Sock_type,
#request{method='POST',
headers = _Headers,
uri = {abs_path, "/ibrowse_303_no_body_test"}}) ->
Resp = <<"HTTP/1.1 303 See Other\r\nLocation: http://example.org\r\n\r\n">>,
do_send(Sock, Sock_type, Resp);
process_request(Sock, Sock_type,
#request{method='POST',
headers = _Headers,
uri = {abs_path, "/ibrowse_303_with_body_test"}}) ->
Resp = <<"HTTP/1.1 303 See Other\r\nLocation: http://example.org\r\nContent-Length: 5\r\n\r\nabcde">>,
do_send(Sock, Sock_type, Resp);
process_request(Sock, Sock_type,
#request{method='GET',
headers = _Headers,
uri = {abs_path, "/ibrowse_preserve_status_line"}}) ->
Resp = <<"HTTP/1.1 200 OKBlah\r\nContent-Length: 5\r\n\r\nabcde">>,
do_send(Sock, Sock_type, Resp);
process_request(Sock, Sock_type,
#request{method='GET',
headers = _Headers,
uri = {abs_path, "/ibrowse_handle_one_request_only_with_delay"}}) ->
timer:sleep(2000),
Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\n">>,
do_send(Sock, Sock_type, Resp),
close_connection;
process_request(Sock, Sock_type,
#request{method='GET',
headers = _Headers,
uri = {abs_path, "/ibrowse_handle_one_request_only"}}) ->
Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\n">>,
do_send(Sock, Sock_type, Resp),
close_connection;
process_request(Sock, Sock_type,
#request{method='GET',
headers = _Headers,
uri = {abs_path, "/ibrowse_send_file_conn_close"}}) ->
Resp = <<"HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nDate: Wed, 04 Apr 2012 16:53:49 GMT\r\nConnection: close\r\n\r\nblahblah-">>,
do_send(Sock, Sock_type, Resp),
timer:sleep(1000),
do_send(Sock, Sock_type, <<"blahblah">>),
close_connection;
process_request(_Sock, _Sock_type, #request{uri = {abs_path, "/never_respond"} } ) ->
not_done;
process_request(Sock, Sock_type, Req) ->
do_trace("Recvd req: ~p~n", [Req]),
Resp = <<"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n">>,
do_send(Sock, Sock_type, Resp),
timer:sleep(?RAND:uniform(100)).
do_send(Sock, tcp, Resp) ->
gen_tcp:send(Sock, Resp);
do_send(Sock, ssl, Resp) ->
ssl:send(Sock, Resp).
%%------------------------------------------------------------------------------
%% Utility functions
%%------------------------------------------------------------------------------
chunk_request_body(Body, _ChunkSize) when is_tuple(Body) orelse
is_function(Body) ->
Body;
chunk_request_body(Body, ChunkSize) ->
chunk_request_body(Body, ChunkSize, []).
chunk_request_body(Body, _ChunkSize, Acc) when Body == <<>>; Body == [] ->
LastChunk = "0\r\n",
lists:reverse(["\r\n", LastChunk | Acc]);
chunk_request_body(Body, ChunkSize, Acc) when is_binary(Body),
size(Body) >= ChunkSize ->
<<ChunkBody:ChunkSize/binary, Rest/binary>> = Body,
Chunk = [?dec2hex(ChunkSize),"\r\n",
ChunkBody, "\r\n"],
chunk_request_body(Rest, ChunkSize, [Chunk | Acc]);
chunk_request_body(Body, _ChunkSize, Acc) when is_binary(Body) ->
BodySize = size(Body),
Chunk = [?dec2hex(BodySize),"\r\n",
Body, "\r\n"],
LastChunk = "0\r\n",
lists:reverse(["\r\n", LastChunk, Chunk | Acc]);
chunk_request_body(Body, ChunkSize, Acc) when length(Body) >= ChunkSize ->
{ChunkBody, Rest} = split_list_at(Body, ChunkSize),
Chunk = [?dec2hex(ChunkSize),"\r\n",
ChunkBody, "\r\n"],
chunk_request_body(Rest, ChunkSize, [Chunk | Acc]);
chunk_request_body(Body, _ChunkSize, Acc) when is_list(Body) ->
BodySize = length(Body),
Chunk = [?dec2hex(BodySize),"\r\n",
Body, "\r\n"],
LastChunk = "0\r\n",
lists:reverse(["\r\n", LastChunk, Chunk | Acc]).
split_list_at(List, N) ->
split_list_at(List, N, []).
split_list_at([], _, Acc) ->
{lists:reverse(Acc), []};
split_list_at(List2, 0, List1) ->
{lists:reverse(List1), List2};
split_list_at([H | List2], N, List1) ->
split_list_at(List2, N-1, [H | List1]).
to_lower(X) when is_atom(X) ->
list_to_atom(to_lower(atom_to_list(X)));
to_lower(X) when is_list(X) ->
string:to_lower(X).
get_content_length([{http_header, _, 'Content-Length', _, V} | _]) ->
list_to_integer(V);
get_content_length([{http_header, _, _X, _, _Y} | T]) ->
get_content_length(T);
get_content_length([]) ->
undefined.