%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2004-2012. 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% %% %% %% %% ts:run(inets, httpc_SUITE, [batch]). %% -module(httpc_SUITE). -include_lib("common_test/include/ct.hrl"). -include("test_server_line.hrl"). -include_lib("kernel/include/file.hrl"). -include("inets_test_lib.hrl"). %% Note: This directive should only be used in test suites. -compile(export_all). %% Test server specific exports -define(IP_PORT, 8998). -define(SSL_PORT, 8999). -define(NOT_IN_USE_PORT, 8997). -define(LOCAL_HOST, {127,0,0,1}). -define(IPV6_LOCAL_HOST, "0:0:0:0:0:0:0:1"). -define(URL_START, "http://localhost:"). -define(SSL_URL_START, "https://localhost:"). -define(CR, $\r). -define(LF, $\n). -define(HTTP_MAX_HEADER_SIZE, 10240). %%-------------------------------------------------------------------- %% all(Arg) -> [Doc] | [Case] | {skip, Comment} %% Arg - doc | suite %% Doc - string() %% Case - atom() %% Name of a test case function. %% Comment - string() %% Description: Returns documentation/test cases in this test suite %% or a skip tuple if the platform is not supported. %%-------------------------------------------------------------------- suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [ http_options, http_head, http_get, http_post, http_post_streaming, http_dummy_pipe, http_inets_pipe, http_trace, http_async, http_save_to_file, http_save_to_file_async, http_headers, http_headers_dummy, http_bad_response, http_redirect, http_redirect_loop, http_internal_server_error, http_userinfo, http_cookie, http_server_does_not_exist, http_invalid_http, http_emulate_lower_versions, http_relaxed, page_does_not_exist, parse_url, options, headers_as_is, selecting_session, {group, ssl}, {group, stream}, {group, ipv6}, {group, tickets}, initial_server_connect ]. groups() -> [ {ssl, [], [ssl_head, essl_head, ssl_get, essl_get, ssl_trace, essl_trace]}, {stream, [], [http_stream, http_stream_once]}, {tickets, [], [hexed_query_otp_6191, empty_body_otp_6243, empty_response_header_otp_6830, transfer_encoding_otp_6807, no_content_204_otp_6982, missing_CR_otp_7304, {group, otp_7883}, {group, otp_8154}, {group, otp_8106}, otp_8056, otp_8352, otp_8371, otp_8739]}, {otp_7883, [], [otp_7883_1, otp_7883_2]}, {otp_8154, [], [otp_8154_1]}, {otp_8106, [], [otp_8106_pid, otp_8106_fun, otp_8106_mfa]}, {ipv6, [], [ipv6_ipcomm, ipv6_essl]} ]. init_per_group(ipv6 = _GroupName, Config) -> case inets_test_lib:has_ipv6_support() of {ok, _} -> Config; _ -> {skip, "Host does not support IPv6"} end; init_per_group(_GroupName, Config) -> Config. end_per_group(_GroupName, Config) -> Config. %%-------------------------------------------------------------------- %% Function: init_per_suite(Config) -> Config %% Config - [tuple()] %% A list of key/value pairs, holding the test case configuration. %% Description: Initiation before the whole suite %% %% Note: This function is free to add any key/value pairs to the Config %% variable, but should NOT alter/remove any existing entries. %%-------------------------------------------------------------------- init_per_suite(Config) -> ?PRINT_SYSTEM_INFO([]), PrivDir = ?config(priv_dir, Config), DataDir = ?config(data_dir, Config), ServerRoot = filename:join(PrivDir, "server_root"), DocRoot = filename:join(ServerRoot, "htdocs"), IpConfFile = integer_to_list(?IP_PORT) ++ ".conf", SslConfFile = integer_to_list(?SSL_PORT) ++ ".conf", setup_server_dirs(ServerRoot, DocRoot, DataDir), create_config(IpConfFile, ip_comm, ?IP_PORT, PrivDir, ServerRoot, DocRoot, DataDir), create_config(SslConfFile, ssl, ?SSL_PORT, PrivDir, ServerRoot, DocRoot, DataDir), Cgi = case test_server:os_type() of {win32, _} -> filename:join([ServerRoot, "cgi-bin", "cgi_echo.exe"]); _ -> filename:join([ServerRoot, "cgi-bin", "cgi_echo"]) end, {ok, FileInfo} = file:read_file_info(Cgi), ok = file:write_file_info(Cgi, FileInfo#file_info{mode = 8#00755}), [{has_ipv6_support, inets_test_lib:has_ipv6_support()}, {server_root, ServerRoot}, {doc_root, DocRoot}, {local_port, ?IP_PORT}, {local_ssl_port, ?SSL_PORT} | Config]. %%-------------------------------------------------------------------- %% Function: end_per_suite(Config) -> _ %% Config - [tuple()] %% A list of key/value pairs, holding the test case configuration. %% Description: Cleanup after the whole suite %%-------------------------------------------------------------------- end_per_suite(Config) -> PrivDir = ?config(priv_dir, Config), inets_test_lib:del_dirs(PrivDir), application:stop(inets), application:stop(ssl), ok. %%-------------------------------------------------------------------- %% Function: init_per_testcase(Case, Config) -> Config %% Case - atom() %% Name of the test case that is about to be run. %% Config - [tuple()] %% A list of key/value pairs, holding the test case configuration. %% %% Description: Initiation before each test case %% %% Note: This function is free to add any key/value pairs to the Config %% variable, but should NOT alter/remove any existing entries. %%-------------------------------------------------------------------- init_per_testcase(otp_8154_1 = Case, Config) -> init_per_testcase(Case, 5, Config); init_per_testcase(initial_server_connect = Case, Config) -> %% Try to check if crypto actually exist or not, %% this test case does not work unless it does try begin ?ENSURE_STARTED([crypto, public_key, ssl]), inets:start(), Config end catch throw:{error, {failed_starting, App, ActualError}} -> tsp("init_per_testcase(~w) -> failed starting ~w: " "~n ~p", [Case, App, ActualError]), SkipString = "Could not start " ++ atom_to_list(App), skip(SkipString); _:X -> SkipString = lists:flatten( io_lib:format("Failed starting apps: ~p", [X])), skip(SkipString) end; init_per_testcase(Case, Config) -> init_per_testcase(Case, 2, Config). init_per_testcase(Case, Timeout, Config) -> io:format(user, "~n~n*** INIT ~w:~w[~w] ***" "~n~n", [?MODULE, Case, Timeout]), PrivDir = ?config(priv_dir, Config), application:stop(inets), Dog = test_server:timetrap(inets_test_lib:minutes(Timeout)), TmpConfig = lists:keydelete(watchdog, 1, Config), IpConfFile = integer_to_list(?IP_PORT) ++ ".conf", SslConfFile = integer_to_list(?SSL_PORT) ++ ".conf", %% inets:enable_trace(max, io, httpd), %% inets:enable_trace(max, io, httpc), %% inets:enable_trace(max, io, all), NewConfig = case atom_to_list(Case) of [$s, $s, $l | _] -> ?ENSURE_STARTED([crypto, public_key, ssl]), init_per_testcase_ssl(ssl, PrivDir, SslConfFile, [{watchdog, Dog} | TmpConfig]); [$e, $s, $s, $l | _] -> ?ENSURE_STARTED([crypto, public_key, ssl]), init_per_testcase_ssl(essl, PrivDir, SslConfFile, [{watchdog, Dog} | TmpConfig]); "ipv6_" ++ _Rest -> %% Ensure needed apps (crypto, public_key and ssl) are started try ?ENSURE_STARTED([crypto, public_key, ssl]) of ok -> Profile = ipv6, %% A stand-alone profile is represented by a pid() {ok, ProfilePid} = inets:start(httpc, [{profile, Profile}, {data_dir, PrivDir}], stand_alone), ok = httpc:set_options([{ipfamily, inet6}], ProfilePid), tsp("httpc profile pid: ~p", [ProfilePid]), [{watchdog, Dog}, {profile, ProfilePid}| TmpConfig] catch throw:{error, {failed_starting, App, ActualError}} -> tsp("init_per_testcase(~w) -> failed starting ~w: " "~n ~p", [Case, App, ActualError]), SkipString = "Could not start " ++ atom_to_list(App), skip(SkipString); _:X -> SkipString = lists:flatten( io_lib:format("Failed starting apps: ~p", [X])), skip(SkipString) end; _ -> %% Try inet6fb4 on windows... %% No need? Since it is set above? %% tsp("init_per_testcase -> allways try IPv6 on windows"), %% ?RUN_ON_WINDOWS( %% fun() -> %% tsp("init_per_testcase:set_options_fun -> " %% "set-option ipfamily to inet6fb4"), %% Res = httpc:set_options([{ipfamily, inet6fb4}]), %% tsp("init_per_testcase:set_options_fun -> " %% "~n Res: ~p", [Res]), %% Res %% end), TmpConfig2 = lists:keydelete(local_server, 1, TmpConfig), %% Will start inets tsp("init_per_testcase -> try start server"), Server = start_http_server(PrivDir, IpConfFile), [{watchdog, Dog}, {local_server, Server} | TmpConfig2] end, %% %% Set default ipfamily to the same as the main server has by default %% This makes the client try w/ ipv6 before falling back to ipv4, %% as that is what the server is configured to do. %% Note that this is required for the tests to run on *BSD w/ ipv6 enabled %% as well as on Windows. The Linux behaviour of allowing ipv4 connects %% to ipv6 sockets is not required or even encouraged. tsp("init_per_testcase -> Options before ipfamily set: ~n~p", [httpc:get_options(all)]), ok = httpc:set_options([{ipfamily, inet6fb4}]), tsp("init_per_testcase -> Options after ipfamily set: ~n~p", [httpc:get_options(all)]), %% Note that the IPv6 test case(s) *must* use inet6, %% so this value will be overwritten (see "ipv6_" below). %% inets:enable_trace(max, io, httpc), %% inets:enable_trace(max, io, all), %% snmp:set_trace([gen_tcp]), tsp("init_per_testcase(~w) -> done when" "~n NewConfig: ~p" "~n~n", [Case, NewConfig]), NewConfig. init_per_testcase_ssl(Tag, PrivDir, SslConfFile, Config) -> tsp("init_per_testcase_ssl(~w) -> stop ssl", [Tag]), application:stop(ssl), Config2 = lists:keydelete(local_ssl_server, 1, Config), %% Will start inets tsp("init_per_testcase_ssl(~w) -> try start http server (including inets)", [Tag]), Server = inets_test_lib:start_http_server( filename:join(PrivDir, SslConfFile), Tag), tsp("init_per_testcase(~w) -> Server: ~p", [Tag, Server]), [{local_ssl_server, Server} | Config2]. start_http_server(ConfDir, ConfFile) -> inets_test_lib:start_http_server( filename:join(ConfDir, ConfFile) ). %%-------------------------------------------------------------------- %% Function: end_per_testcase(Case, Config) -> _ %% Case - atom() %% Name of the test case that is about to be run. %% Config - [tuple()] %% A list of key/value pairs, holding the test case configuration. %% Description: Cleanup after each test case %%-------------------------------------------------------------------- end_per_testcase(http_save_to_file = Case, Config) -> io:format(user, "~n~n*** END ~w:~w ***~n~n", [?MODULE, Case]), PrivDir = ?config(priv_dir, Config), FullPath = filename:join(PrivDir, "dummy.html"), file:delete(FullPath), finish(Config); end_per_testcase(Case, Config) -> io:format(user, "~n~n*** END ~w:~w ***~n~n", [?MODULE, Case]), dbg:stop(), % ? case atom_to_list(Case) of "ipv6_" ++ _Rest -> tsp("end_per_testcase(~w) -> stop ssl", [Case]), application:stop(ssl), tsp("end_per_testcase(~w) -> stop public_key", [Case]), application:stop(public_key), tsp("end_per_testcase(~w) -> stop crypto", [Case]), application:stop(crypto), ProfilePid = ?config(profile, Config), tsp("end_per_testcase(~w) -> stop httpc profile (~p)", [Case, ProfilePid]), unlink(ProfilePid), inets:stop(stand_alone, ProfilePid), tsp("end_per_testcase(~w) -> httpc profile (~p) stopped", [Case, ProfilePid]), ok; _ -> ok end, finish(Config). finish(Config) -> Dog = ?config(watchdog, Config), case Dog of undefined -> ok; _ -> tsp("finish -> stop watchdog (~p)", [Dog]), test_server:timetrap_cancel(Dog) end. %%------------------------------------------------------------------------- %% Test cases starts here. %%------------------------------------------------------------------------- %%------------------------------------------------------------------------- http_options(doc) -> ["Test http options request against local server."]; http_options(suite) -> []; http_options(Config) when is_list(Config) -> skip("Not supported by httpd"). http_head(doc) -> ["Test http head request against local server."]; http_head(suite) -> []; http_head(Config) when is_list(Config) -> tsp("http_head -> entry with" "~n Config: ~p", [Config]), Method = head, Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", Request = {URL, []}, HttpOpts = [], Opts = [], VerifyResult = fun({ok, {{_,200,_}, [_ | _], []}}) -> ok; ({ok, UnexpectedReply}) -> tsp("http_head:verify_fun -> Unexpected Reply: " "~n ~p", [UnexpectedReply]), tsf({unexpected_reply, UnexpectedReply}); ({error, Reason} = Error) -> tsp("http_head:verify_fun -> Error reply: " "~n Reason: ~p", [Reason]), tsf({bad_reply, Error}) end, simple_request_and_verify(Config, Method, Request, HttpOpts, Opts, VerifyResult). %%------------------------------------------------------------------------- http_get(doc) -> ["Test http get request against local server"]; http_get(suite) -> []; http_get(Config) when is_list(Config) -> tsp("http_get -> entry with" "~n Config: ~p", [Config]), case ?config(local_server, Config) of ok -> tsp("local-server running"), Method = get, Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", Request = {URL, []}, Timeout = timer:seconds(1), ConnTimeout = Timeout + timer:seconds(1), HttpOptions1 = [{timeout, Timeout}, {connect_timeout, ConnTimeout}], Options1 = [], Body = case httpc:request(Method, Request, HttpOptions1, Options1) of {ok, {{_,200,_}, [_ | _], ReplyBody = [_ | _]}} -> ReplyBody; {ok, UnexpectedReply1} -> tsf({unexpected_reply, UnexpectedReply1}); {error, _} = Error1 -> tsf({bad_reply, Error1}) end, %% eqvivivalent to httpc:request(get, {URL, []}, [], []), inets_test_lib:check_body(Body), HttpOptions2 = [], Options2 = [{body_format, binary}], case httpc:request(Method, Request, HttpOptions2, Options2) of {ok, {{_,200,_}, [_ | _], Bin}} when is_binary(Bin) -> ok; {ok, {{_,200,_}, [_ | _], BadBin}} -> tsf({body_format_not_binary, BadBin}); {ok, UnexpectedReply2} -> tsf({unexpected_reply, UnexpectedReply2}); {error, _} = Error2 -> tsf({bad_reply, Error2}) end; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_post(doc) -> ["Test http post request against local server. We do in this case " "only care about the client side of the the post. The server " "script will not actually use the post data."]; http_post(suite) -> []; http_post(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = case test_server:os_type() of {win32, _} -> ?URL_START ++ integer_to_list(Port) ++ "/cgi-bin/cgi_echo.exe"; _ -> ?URL_START ++ integer_to_list(Port) ++ "/cgi-bin/cgi_echo" end, %% Cgi-script expects the body length to be 100 Body = lists:duplicate(100, "1"), {ok, {{_,200,_}, [_ | _], [_ | _]}} = httpc:request(post, {URL, [{"expect","100-continue"}], "text/plain", Body}, [], []), {ok, {{_,504,_}, [_ | _], []}} = httpc:request(post, {URL, [{"expect","100-continue"}], "text/plain", "foobar"}, [], []); _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_post_streaming(doc) -> ["Test streaming http post request against local server. " "We only care about the client side of the the post. " "The server script will not actually use the post data."]; http_post_streaming(suite) -> []; http_post_streaming(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = case test_server:os_type() of {win32, _} -> ?URL_START ++ integer_to_list(Port) ++ "/cgi-bin/cgi_echo.exe"; _ -> ?URL_START ++ integer_to_list(Port) ++ "/cgi-bin/cgi_echo" end, %% Cgi-script expects the body length to be 100 BodyFun = fun(0) -> io:format("~w:http_post_streaming_fun -> " "zero~n", [?MODULE]), eof; (LenLeft) -> io:format("~w:http_post_streaming_fun -> " "LenLeft: ~p~n", [?MODULE, LenLeft]), {ok, lists:duplicate(10, "1"), LenLeft - 10} end, {ok, {{_,200,_}, [_ | _], [_ | _]}} = httpc:request(post, {URL, [{"expect", "100-continue"}, {"content-length", "100"}], "text/plain", {BodyFun, 100}}, [], []), {ok, {{_,504,_}, [_ | _], []}} = httpc:request(post, {URL, [{"expect", "100-continue"}, {"content-length", "10"}], "text/plain", {BodyFun, 10}}, [], []); _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_emulate_lower_versions(doc) -> ["Perform request as 0.9 and 1.0 clients."]; http_emulate_lower_versions(suite) -> []; http_emulate_lower_versions(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", {ok, Body0} = httpc:request(get, {URL, []}, [{version, "HTTP/0.9"}], []), inets_test_lib:check_body(Body0), {ok, {{"HTTP/1.0", 200, _}, [_ | _], Body1 = [_ | _]}} = httpc:request(get, {URL, []}, [{version, "HTTP/1.0"}], []), inets_test_lib:check_body(Body1), {ok, {{"HTTP/1.1", 200, _}, [_ | _], Body2 = [_ | _]}} = httpc:request(get, {URL, []}, [{version, "HTTP/1.1"}], []), inets_test_lib:check_body(Body2); _-> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_relaxed(doc) -> ["Test relaxed mode"]; http_relaxed(suite) -> []; http_relaxed(Config) when is_list(Config) -> ok = httpc:set_options([{ipv6, disabled}]), % also test the old option %% ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/missing_reason_phrase.html", {error, Reason} = httpc:request(get, {URL, []}, [{relaxed, false}], []), test_server:format("Not relaxed: ~p~n", [Reason]), {ok, {{_, 200, _}, [_ | _], [_ | _]}} = httpc:request(get, {URL, []}, [{relaxed, true}], []), DummyServerPid ! stop, ok = httpc:set_options([{ipv6, enabled}]), %% ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- http_dummy_pipe(doc) -> ["Test pipelining code."]; http_dummy_pipe(suite) -> []; http_dummy_pipe(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/foobar.html", test_pipeline(URL), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. http_inets_pipe(doc) -> ["Test pipelining code."]; http_inets_pipe(suite) -> []; http_inets_pipe(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", test_pipeline(URL); _ -> skip("Failed to start local http-server") end. test_pipeline(URL) -> p("test_pipeline -> entry with" "~n URL: ~p", [URL]), httpc:set_options([{pipeline_timeout, 50000}]), p("test_pipeline -> issue (async) request 1" "~n when profile info: ~p", [httpc:info()]), {ok, RequestIdA1} = httpc:request(get, {URL, []}, [], [{sync, false}]), tsp("RequestIdA1: ~p", [RequestIdA1]), p("test_pipeline -> RequestIdA1: ~p" "~n when profile info: ~p", [RequestIdA1, httpc:info()]), %% Make sure pipeline is initiated p("test_pipeline -> sleep some", []), test_server:sleep(4000), p("test_pipeline -> issue (async) request A2, A3 and A4" "~n when profile info: ~p", [httpc:info()]), {ok, RequestIdA2} = httpc:request(get, {URL, []}, [], [{sync, false}]), {ok, RequestIdA3} = httpc:request(get, {URL, []}, [], [{sync, false}]), {ok, RequestIdA4} = httpc:request(get, {URL, []}, [], [{sync, false}]), tsp("RequestIdAs => A2: ~p, A3: ~p and A4: ~p", [RequestIdA2, RequestIdA3, RequestIdA4]), p("test_pipeline -> RequestIds => A2: ~p, A3: ~p and A4: ~p" "~n when profile info: ~p", [RequestIdA2, RequestIdA3, RequestIdA4, httpc:info()]), p("test_pipeline -> issue (sync) request 3"), {ok, {{_,200,_}, [_ | _], [_ | _]}} = httpc:request(get, {URL, []}, [], []), p("test_pipeline -> expect reply for (async) request A1, A2, A3 and A4" "~n when profile info: ~p", [httpc:info()]), pipeline_await_async_reply([{RequestIdA1, a1, 200}, {RequestIdA2, a2, 200}, {RequestIdA3, a3, 200}, {RequestIdA4, a4, 200}], ?MINS(1)), p("test_pipeline -> sleep some" "~n when profile info: ~p", [httpc:info()]), test_server:sleep(4000), p("test_pipeline -> issue (async) request B1, B2, B3 and B4" "~n when profile info: ~p", [httpc:info()]), {ok, RequestIdB1} = httpc:request(get, {URL, []}, [], [{sync, false}]), {ok, RequestIdB2} = httpc:request(get, {URL, []}, [], [{sync, false}]), {ok, RequestIdB3} = httpc:request(get, {URL, []}, [], [{sync, false}]), {ok, RequestIdB4} = httpc:request(get, {URL, []}, [], [{sync, false}]), tsp("RequestIdBs => B1: ~p, B2: ~p, B3: ~p and B4: ~p", [RequestIdB1, RequestIdB2, RequestIdB3, RequestIdB4]), p("test_pipeline -> RequestIdBs => B1: ~p, B2: ~p, B3: ~p and B4: ~p" "~n when profile info: ~p", [RequestIdB1, RequestIdB2, RequestIdB3, RequestIdB4, httpc:info()]), p("test_pipeline -> cancel (async) request B2" "~n when profile info: ~p", [httpc:info()]), ok = httpc:cancel_request(RequestIdB2), p("test_pipeline -> " "expect *no* reply for cancelled (async) request B2 (for 3 secs)" "~n when profile info: ~p", [httpc:info()]), receive {http, {RequestIdB2, _}} -> tsf(http_cancel_request_failed) after 3000 -> ok end, p("test_pipeline -> expect reply for (async) request B1, B3 and B4" "~n when profile info: ~p", [httpc:info()]), Bodies = pipeline_await_async_reply([{RequestIdB1, b1, 200}, {RequestIdB3, b3, 200}, {RequestIdB4, b4, 200}], ?MINS(1)), [{b1, Body}|_] = Bodies, p("test_pipeline -> check reply for (async) request B1" "~n when profile info: ~p", [httpc:info()]), inets_test_lib:check_body(binary_to_list(Body)), p("test_pipeline -> ensure no unexpected incomming" "~n when profile info: ~p", [httpc:info()]), receive {http, Any} -> tsf({unexpected_message, Any}) after 500 -> ok end, p("test_pipeline -> done" "~n when profile info: ~p", [httpc:info()]), ok. pipeline_await_async_reply(ReqIds, Timeout) -> pipeline_await_async_reply(ReqIds, Timeout, []). pipeline_await_async_reply([], _, Acc) -> lists:keysort(1, Acc); pipeline_await_async_reply(ReqIds, Timeout, Acc) when Timeout > 0 -> T1 = inets_test_lib:timestamp(), p("pipeline_await_async_reply -> await replies" "~n ReqIds: ~p" "~n Timeout: ~p", [ReqIds, Timeout]), receive {http, {RequestId, {{_, Status, _}, _, Body}}} -> p("pipeline_await_async_reply -> received reply for" "~n RequestId: ~p" "~n Status: ~p", [RequestId, Status]), case lists:keysearch(RequestId, 1, ReqIds) of {value, {RequestId, N, Status}} -> p("pipeline_await_async_reply -> " "found expected request ~w", [N]), ReqIds2 = lists:keydelete(RequestId, 1, ReqIds), NewTimeout = Timeout - (inets_test_lib:timestamp()-T1), pipeline_await_async_reply(ReqIds2, NewTimeout, [{N, Body} | Acc]); {value, {RequestId, N, WrongStatus}} -> p("pipeline_await_async_reply -> " "found request ~w with wrong status", [N]), tsf({reply_with_unexpected_status, {RequestId, N, WrongStatus}}); false -> tsf({unexpected_reply, {RequestId, Status}}) end; {http, Msg} -> tsf({unexpected_reply, Msg}) after Timeout -> receive Any -> tsp("pipeline_await_async_reply -> " "received unknown data after timeout: " "~n ~p", [Any]), tsf({timeout, {unknown, Any}}) end end; pipeline_await_async_reply(ReqIds, _, Acc) -> tsp("pipeline_await_async_reply -> " "timeout: " "~n ~p" "~nwhen" "~n ~p", [ReqIds, Acc]), tsf({timeout, ReqIds, Acc}). %%------------------------------------------------------------------------- http_trace(doc) -> ["Perform a TRACE request."]; http_trace(suite) -> []; http_trace(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", case httpc:request(trace, {URL, []}, [], []) of {ok, {{_,200,_}, [_ | _], "TRACE /dummy.html" ++ _}} -> ok; {ok, {{_,200,_}, [_ | _], WrongBody}} -> tsf({wrong_body, WrongBody}); {ok, WrongReply} -> tsf({wrong_reply, WrongReply}); Error -> tsf({failed, Error}) end; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_async(doc) -> ["Test an asynchrony http request."]; http_async(suite) -> []; http_async(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", {ok, RequestId} = httpc:request(get, {URL, []}, [], [{sync, false}]), Body = receive {http, {RequestId, {{_, 200, _}, _, BinBody}}} -> BinBody; {http, Msg} -> tsf(Msg) end, inets_test_lib:check_body(binary_to_list(Body)), {ok, NewRequestId} = httpc:request(get, {URL, []}, [], [{sync, false}]), ok = httpc:cancel_request(NewRequestId), receive {http, {NewRequestId, _NewResult}} -> tsf(http_cancel_request_failed) after 3000 -> ok end; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_save_to_file(doc) -> ["Test to save the http body to a file"]; http_save_to_file(suite) -> []; http_save_to_file(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> PrivDir = ?config(priv_dir, Config), FilePath = filename:join(PrivDir, "dummy.html"), Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", {ok, saved_to_file} = httpc:request(get, {URL, []}, [], [{stream, FilePath}]), {ok, Bin} = file:read_file(FilePath), {ok, {{_,200,_}, [_ | _], Body}} = httpc:request(URL), Bin == Body; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_save_to_file_async(doc) -> ["Test to save the http body to a file"]; http_save_to_file_async(suite) -> []; http_save_to_file_async(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> PrivDir = ?config(priv_dir, Config), FilePath = filename:join(PrivDir, "dummy.html"), Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", {ok, RequestId} = httpc:request(get, {URL, []}, [], [{stream, FilePath}, {sync, false}]), receive {http, {RequestId, saved_to_file}} -> ok; {http, Msg} -> tsf(Msg) end, {ok, Bin} = file:read_file(FilePath), {ok, {{_,200,_}, [_ | _], Body}} = httpc:request(URL), Bin == Body; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_headers(doc) -> ["Use as many request headers as possible not used in proxy_headers"]; http_headers(suite) -> []; http_headers(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", DocRoot = ?config(doc_root, Config), {ok, FileInfo} = file:read_file_info(filename:join([DocRoot,"dummy.html"])), CreatedSec = calendar:datetime_to_gregorian_seconds( FileInfo#file_info.mtime), Mod = httpd_util:rfc1123_date( calendar:gregorian_seconds_to_datetime( CreatedSec-1)), Date = httpd_util:rfc1123_date({date(), time()}), {ok, {{_,200,_}, [_ | _], [_ | _]}} = httpc:request(get, {URL, [{"If-Modified-Since", Mod}, {"From","webmaster@erlang.se"}, {"Date", Date} ]}, [], []), Mod1 = httpd_util:rfc1123_date( calendar:gregorian_seconds_to_datetime( CreatedSec+1)), {ok, {{_,200,_}, [_ | _], [_ | _]}} = httpc:request(get, {URL, [{"If-UnModified-Since", Mod1} ]}, [], []), Tag = httpd_util:create_etag(FileInfo), {ok, {{_,200,_}, [_ | _], [_ | _]}} = httpc:request(get, {URL, [{"If-Match", Tag} ]}, [], []), {ok, {{_,200,_}, [_ | _], _}} = httpc:request(get, {URL, [{"If-None-Match", "NotEtag,NeihterEtag"}, {"Connection", "Close"} ]}, [], []), ok; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_headers_dummy(doc) -> ["Test the code for handling headers we do not want/can send " "to a real server. Note it is not logical to send" "all of these headers together, we only want to test that" "the code for handling headers will not crash."]; http_headers_dummy(suite) -> []; http_headers_dummy(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy_headers.html", Foo = http_chunk:encode("foobar") ++ binary_to_list(http_chunk:encode_last()), FooBar = Foo ++ "\r\n\r\nOther:inets_test\r\n\r\n", UserPasswd = base64:encode_to_string("Alladin:Sesame"), Auth = "Basic " ++ UserPasswd, %% The dummy server will ignore the headers, we only want to test %% that the client header-handling code. This would not %% be a vaild http-request! {ok, {{_,200,_}, [_ | _], [_|_]}} = httpc:request(post, {URL, [{"Via", "1.0 fred, 1.1 nowhere.com (Apache/1.1)"}, {"Warning","1#pseudonym foobar"}, {"Vary","*"}, {"Upgrade","HTTP/2.0"}, {"Pragma", "1#no-cache"}, {"Cache-Control", "no-cache"}, {"Connection", "close"}, {"Date", "Sat, 29 Oct 1994 19:43:31 GMT"}, {"Accept", " text/plain; q=0.5, text/html"}, {"Accept-Language", "en"}, {"Accept-Encoding","chunked"}, {"Accept-Charset", "ISO8859-1"}, {"Authorization", Auth}, {"Expect", "1#100-continue"}, {"User-Agent","inets"}, {"Transfer-Encoding","chunked"}, {"Range", " bytes=0-499"}, {"If-Range", "Sat, 29 Oct 1994 19:43:31 GMT"}, {"If-Match", "*"}, {"Content-Type", "text/plain"}, {"Content-Encoding", "chunked"}, {"Content-Length", "6"}, {"Content-Language", "en"}, {"Content-Location", "http://www.foobar.se"}, {"Content-MD5", "104528739076276072743283077410617235478"}, {"Content-Range", "bytes 0-499/1234"}, {"Allow", "GET"}, {"Proxy-Authorization", Auth}, {"Expires", "Sat, 29 Oct 1994 19:43:31 GMT"}, {"Upgrade", "HTTP/2.0"}, {"Last-Modified", "Sat, 29 Oct 1994 19:43:31 GMT"}, {"Trailer","1#User-Agent"} ], "text/plain", FooBar}, [], []), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- http_bad_response(doc) -> ["Test what happens when the server does not follow the protocol"]; http_bad_response(suite) -> []; http_bad_response(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/missing_crlf.html", URL1 = ?URL_START ++ integer_to_list(Port) ++ "/wrong_statusline.html", {error, timeout} = httpc:request(get, {URL, []}, [{timeout, 400}], []), {error, Reason} = httpc:request(URL1), test_server:format("Wrong Statusline: ~p~n", [Reason]), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- ssl_head(doc) -> ["Same as http_head/1 but over ssl sockets."]; ssl_head(suite) -> []; ssl_head(Config) when is_list(Config) -> ssl_head(ssl, Config). essl_head(doc) -> ["Same as http_head/1 but over ssl sockets."]; essl_head(suite) -> []; essl_head(Config) when is_list(Config) -> ssl_head(essl, Config). ssl_head(SslTag, Config) -> tsp("ssl_head -> entry with" "~n SslTag: ~p" "~n Config: ~p", [SslTag, Config]), case ?config(local_ssl_server, Config) of ok -> DataDir = ?config(data_dir, Config), Port = ?config(local_ssl_port, Config), URL = ?SSL_URL_START ++ integer_to_list(Port) ++ "/dummy.html", CertFile = filename:join(DataDir, "ssl_client_cert.pem"), SSLOptions = [{certfile, CertFile}, {keyfile, CertFile}], SSLConfig = case SslTag of ssl -> SSLOptions; essl -> {essl, SSLOptions} end, tsp("ssl_head -> make request using: " "~n URL: ~p" "~n SslTag: ~p" "~n SSLOptions: ~p", [URL, SslTag, SSLOptions]), {ok, {{_,200, _}, [_ | _], []}} = httpc:request(head, {URL, []}, [{ssl, SSLConfig}], []); {ok, _} -> skip("local http-server not started"); _ -> skip("SSL not started") end. %%------------------------------------------------------------------------- ssl_get(doc) -> ["Same as http_get/1 but over ssl sockets."]; ssl_get(suite) -> []; ssl_get(Config) when is_list(Config) -> ssl_get(ssl, Config). essl_get(doc) -> ["Same as http_get/1 but over ssl sockets."]; essl_get(suite) -> []; essl_get(Config) when is_list(Config) -> ssl_get(essl, Config). ssl_get(SslTag, Config) when is_list(Config) -> case ?config(local_ssl_server, Config) of ok -> DataDir = ?config(data_dir, Config), Port = ?config(local_ssl_port, Config), URL = ?SSL_URL_START ++ integer_to_list(Port) ++ "/dummy.html", CertFile = filename:join(DataDir, "ssl_client_cert.pem"), SSLOptions = [{certfile, CertFile}, {keyfile, CertFile}], SSLConfig = case SslTag of ssl -> SSLOptions; essl -> {essl, SSLOptions} end, tsp("ssl_get -> make request using: " "~n URL: ~p" "~n SslTag: ~p" "~n SSLOptions: ~p", [URL, SslTag, SSLOptions]), case httpc:request(get, {URL, []}, [{ssl, SSLConfig}], []) of {ok, {{_,200, _}, [_ | _], Body = [_ | _]}} -> inets_test_lib:check_body(Body), ok; {ok, {StatusLine, Headers, _Body}} -> tsp("ssl_get -> unexpected result: " "~n StatusLine: ~p" "~n Headers: ~p", [StatusLine, Headers]), tsf({unexpected_response, StatusLine, Headers}); {error, Reason} -> tsp("ssl_get -> request failed: " "~n Reason: ~p", [Reason]), tsf({request_failed, Reason}) end; {ok, _} -> skip("local http-server not started"); _ -> skip("SSL not started") end. %%------------------------------------------------------------------------- ssl_trace(doc) -> ["Same as http_trace/1 but over ssl sockets."]; ssl_trace(suite) -> []; ssl_trace(Config) when is_list(Config) -> ssl_trace(ssl, Config). essl_trace(doc) -> ["Same as http_trace/1 but over ssl sockets."]; essl_trace(suite) -> []; essl_trace(Config) when is_list(Config) -> ssl_trace(essl, Config). ssl_trace(SslTag, Config) when is_list(Config) -> case ?config(local_ssl_server, Config) of ok -> DataDir = ?config(data_dir, Config), Port = ?config(local_ssl_port, Config), URL = ?SSL_URL_START ++ integer_to_list(Port) ++ "/dummy.html", CertFile = filename:join(DataDir, "ssl_client_cert.pem"), SSLOptions = [{certfile, CertFile}, {keyfile, CertFile}], SSLConfig = case SslTag of ssl -> SSLOptions; essl -> {essl, SSLOptions} end, tsp("ssl_trace -> make request using: " "~n URL: ~p" "~n SslTag: ~p" "~n SSLOptions: ~p", [URL, SslTag, SSLOptions]), case httpc:request(trace, {URL, []}, [{ssl, SSLConfig}], []) of {ok, {{_,200, _}, [_ | _], "TRACE /dummy.html" ++ _}} -> ok; {ok, {{_,200,_}, [_ | _], WrongBody}} -> tsf({wrong_body, WrongBody}); {ok, WrongReply} -> tsf({wrong_reply, WrongReply}); Error -> tsf({failed, Error}) end; {ok, _} -> skip("local http-server not started"); _ -> skip("SSL not started") end. %%------------------------------------------------------------------------- http_redirect(doc) -> ["Test redirect with dummy server as httpd does not implement" " server redirect"]; http_redirect(suite) -> []; http_redirect(Config) when is_list(Config) -> tsp("http_redirect -> entry with" "~n Config: ~p", [Config]), case ?config(local_server, Config) of ok -> %% tsp("http_redirect -> set ipfamily option to inet"), %% ok = httpc:set_options([{ipfamily, inet}]), tsp("http_redirect -> start dummy server inet"), {DummyServerPid, Port} = dummy_server(ipv4), tsp("http_redirect -> server port = ~p", [Port]), URL300 = ?URL_START ++ integer_to_list(Port) ++ "/300.html", tsp("http_redirect -> issue request 1: " "~n ~p", [URL300]), {ok, {{_,200,_}, [_ | _], [_|_]}} = httpc:request(get, {URL300, []}, [], []), tsp("http_redirect -> issue request 2: " "~n ~p", [URL300]), {ok, {{_,300,_}, [_ | _], _}} = httpc:request(get, {URL300, []}, [{autoredirect, false}], []), URL301 = ?URL_START ++ integer_to_list(Port) ++ "/301.html", tsp("http_redirect -> issue request 3: " "~n ~p", [URL301]), {ok, {{_,200,_}, [_ | _], [_|_]}} = httpc:request(get, {URL301, []}, [], []), tsp("http_redirect -> issue request 4: " "~n ~p", [URL301]), {ok, {{_,200,_}, [_ | _], []}} = httpc:request(head, {URL301, []}, [], []), tsp("http_redirect -> issue request 5: " "~n ~p", [URL301]), {ok, {{_,301,_}, [_ | _], [_|_]}} = httpc:request(post, {URL301, [],"text/plain", "foobar"}, [], []), URL302 = ?URL_START ++ integer_to_list(Port) ++ "/302.html", tsp("http_redirect -> issue request 6: " "~n ~p", [URL302]), {ok, {{_,200,_}, [_ | _], [_|_]}} = httpc:request(get, {URL302, []}, [], []), case httpc:request(get, {URL302, []}, [], []) of {ok, Reply7} -> case Reply7 of {{_,200,_}, [_ | _], [_|_]} -> tsp("http_redirect -> " "expected reply for request 7"), ok; {StatusLine, Headers, Body} -> tsp("http_redirect -> " "unexpected reply for request 7: " "~n StatusLine: ~p" "~n Headers: ~p" "~n Body: ~p", [StatusLine, Headers, Body]), tsf({unexpected_reply, Reply7}) end; Error7 -> tsp("http_redirect -> " "unexpected result for request 7: " "~n Error7: ~p", [Error7]), tsf({unexpected_result, Error7}) end, tsp("http_redirect -> issue request 7: " "~n ~p", [URL302]), {ok, {{_,200,_}, [_ | _], []}} = httpc:request(head, {URL302, []}, [], []), tsp("http_redirect -> issue request 8: " "~n ~p", [URL302]), {ok, {{_,302,_}, [_ | _], [_|_]}} = httpc:request(post, {URL302, [],"text/plain", "foobar"}, [], []), URL307 = ?URL_START ++ integer_to_list(Port) ++ "/307.html", tsp("http_redirect -> issue request 9: " "~n ~p", [URL307]), {ok, {{_,200,_}, [_ | _], [_|_]}} = httpc:request(get, {URL307, []}, [], []), tsp("http_redirect -> issue request 10: " "~n ~p", [URL307]), {ok, {{_,200,_}, [_ | _], []}} = httpc:request(head, {URL307, []}, [], []), tsp("http_redirect -> issue request 11: " "~n ~p", [URL307]), {ok, {{_,307,_}, [_ | _], [_|_]}} = httpc:request(post, {URL307, [],"text/plain", "foobar"}, [], []), tsp("http_redirect -> stop dummy server"), DummyServerPid ! stop, tsp("http_redirect -> reset ipfamily option (to inet6fb4)"), ok = httpc:set_options([{ipfamily, inet6fb4}]), tsp("http_redirect -> done"), ok; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_redirect_loop(doc) -> ["Test redirect loop detection"]; http_redirect_loop(suite) -> []; http_redirect_loop(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/redirectloop.html", {ok, {{_,300,_}, [_ | _], _}} = httpc:request(get, {URL, []}, [], []), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- http_internal_server_error(doc) -> ["Test 50X codes"]; http_internal_server_error(suite) -> []; http_internal_server_error(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL500 = ?URL_START ++ integer_to_list(Port) ++ "/500.html", {ok, {{_,500,_}, [_ | _], _}} = httpc:request(get, {URL500, []}, [], []), URL503 = ?URL_START ++ integer_to_list(Port) ++ "/503.html", %% Used to be able to make the service available after retry. ets:new(unavailable, [named_table, public, set]), ets:insert(unavailable, {503, unavailable}), {ok, {{_,200, _}, [_ | _], [_|_]}} = httpc:request(get, {URL503, []}, [], []), ets:insert(unavailable, {503, long_unavailable}), {ok, {{_,503, _}, [_ | _], [_|_]}} = httpc:request(get, {URL503, []}, [], []), ets:delete(unavailable), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- http_userinfo(doc) -> ["Test user info e.i. http://user:passwd@host:port/"]; http_userinfo(suite) -> []; http_userinfo(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URLAuth = "http://alladin:sesame@localhost:" ++ integer_to_list(Port) ++ "/userinfo.html", {ok, {{_,200,_}, [_ | _], _}} = httpc:request(get, {URLAuth, []}, [], []), URLUnAuth = "http://alladin:foobar@localhost:" ++ integer_to_list(Port) ++ "/userinfo.html", {ok, {{_,401, _}, [_ | _], _}} = httpc:request(get, {URLUnAuth, []}, [], []), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- http_cookie(doc) -> ["Test cookies."]; http_cookie(suite) -> []; http_cookie(Config) when is_list(Config) -> ok = httpc:set_options([{cookies, enabled}, {ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URLStart = ?URL_START ++ integer_to_list(Port), URLCookie = URLStart ++ "/cookie.html", {ok, {{_,200,_}, [_ | _], [_|_]}} = httpc:request(get, {URLCookie, []}, [], []), ets:new(cookie, [named_table, public, set]), ets:insert(cookie, {cookies, true}), {ok, {{_,200,_}, [_ | _], [_|_]}} = httpc:request(get, {URLStart ++ "/", []}, [], []), ets:delete(cookie), ok = httpc:set_options([{cookies, disabled}]), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- http_server_does_not_exist(doc) -> ["Test that we get an error message back when the server " "does note exist."]; http_server_does_not_exist(suite) -> []; http_server_does_not_exist(Config) when is_list(Config) -> {error, _} = httpc:request(get, {"http://localhost:" ++ integer_to_list(?NOT_IN_USE_PORT) ++ "/", []},[], []), ok. %%------------------------------------------------------------------------- page_does_not_exist(doc) -> ["Test that we get a 404 when the page is not found."]; page_does_not_exist(suite) -> []; page_does_not_exist(Config) when is_list(Config) -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/doesnotexist.html", {ok, {{_,404,_}, [_ | _], [_ | _]}} = httpc:request(get, {URL, []}, [], []), ok. %%------------------------------------------------------------------------- http_stream(doc) -> ["Test the option stream for asynchrony requests"]; http_stream(suite) -> []; http_stream(Config) when is_list(Config) -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", {ok, {{_,200,_}, [_ | _], Body}} = httpc:request(get, {URL, []}, [], []), {ok, RequestId} = httpc:request(get, {URL, []}, [], [{sync, false}, {stream, self}]), receive {http, {RequestId, stream_start, _Headers}} -> ok; {http, Msg} -> tsf(Msg) end, StreamedBody = receive_streamed_body(RequestId, <<>>), Body == binary_to_list(StreamedBody). %%------------------------------------------------------------------------- http_stream_once(doc) -> ["Test the option stream for asynchrony requests"]; http_stream_once(suite) -> []; http_stream_once(Config) when is_list(Config) -> p("http_stream_once -> entry with" "~n Config: ~p", [Config]), p("http_stream_once -> set ipfamily to inet", []), ok = httpc:set_options([{ipfamily, inet}]), p("http_stream_once -> start dummy server", []), {DummyServerPid, Port} = dummy_server(ipv4), PortStr = integer_to_list(Port), p("http_stream_once -> once", []), once(?URL_START ++ PortStr ++ "/once.html"), p("http_stream_once -> once_chunked", []), once(?URL_START ++ PortStr ++ "/once_chunked.html"), p("http_stream_once -> dummy", []), once(?URL_START ++ PortStr ++ "/dummy.html"), p("http_stream_once -> stop dummy server", []), DummyServerPid ! stop, p("http_stream_once -> set ipfamily to inet6fb4", []), ok = httpc:set_options([{ipfamily, inet6fb4}]), p("http_stream_once -> done", []), ok. once(URL) -> p("once -> issue sync request for ~p", [URL]), {ok, {{_,200,_}, [_ | _], Body}} = httpc:request(get, {URL, []}, [], []), p("once -> issue async (self stream) request for ~p", [URL]), {ok, RequestId} = httpc:request(get, {URL, []}, [], [{sync, false}, {stream, {self, once}}]), p("once -> await stream_start reply for (async) request ~p", [RequestId]), NewPid = receive {http, {RequestId, stream_start, _Headers, Pid}} -> p("once -> received stream_start reply for (async) request ~p: ~p", [RequestId, Pid]), Pid; {http, Msg} -> tsf(Msg) end, tsp("once -> request handler: ~p", [NewPid]), p("once -> await stream reply for (async) request ~p", [RequestId]), BodyPart = receive {http, {RequestId, stream, BinBodyPart}} -> p("once -> received stream reply for (async) request ~p: " "~n~p", [RequestId, binary_to_list(BinBodyPart)]), BinBodyPart end, tsp("once -> first body part '~p' received", [binary_to_list(BodyPart)]), StreamedBody = receive_streamed_body(RequestId, BinBodyPart, NewPid), Body = binary_to_list(StreamedBody), p("once -> done when Bode: ~p", [Body]), ok. %%------------------------------------------------------------------------- parse_url(doc) -> ["Test that an url is parsed correctly"]; parse_url(suite) -> []; parse_url(Config) when is_list(Config) -> %% ipv6 {ok, {http,[],"2010:836B:4179::836B:4179",80,"/foobar.html",[]}} = http_uri:parse("http://[2010:836B:4179::836B:4179]/foobar.html"), {ok, {http,[],"[2010:836B:4179::836B:4179]",80,"/foobar.html",[]}} = http_uri:parse("http://[2010:836B:4179::836B:4179]/foobar.html", [{ipv6_host_with_brackets, true}]), {ok, {http,[],"2010:836B:4179::836B:4179",80,"/foobar.html",[]}} = http_uri:parse("http://[2010:836B:4179::836B:4179]/foobar.html", [{ipv6_host_with_brackets, false}]), {ok, {http,[],"2010:836B:4179::836B:4179",80,"/foobar.html",[]}} = http_uri:parse("http://[2010:836B:4179::836B:4179]/foobar.html", [{foo, false}]), {error, {malformed_url, _, "http://2010:836B:4179::836B:4179/foobar.html"}} = http_uri:parse("http://2010:836B:4179::836B:4179/foobar.html"), %% ipv4 {ok, {http,[],"127.0.0.1",80,"/foobar.html",[]}} = http_uri:parse("http://127.0.0.1/foobar.html"), %% host {ok, {http,[],"localhost",8888,"/foobar.html",[]}} = http_uri:parse("http://localhost:8888/foobar.html"), %% Userinfo {ok, {http,"nisse:foobar","localhost",8888,"/foobar.html",[]}} = http_uri:parse("http://nisse:foobar@localhost:8888/foobar.html"), %% Scheme error {error, no_scheme} = http_uri:parse("localhost/foobar.html"), {error, {malformed_url, _, _}} = http_uri:parse("localhost:8888/foobar.html"), %% Query {ok, {http,[],"localhost",8888,"/foobar.html","?foo=bar&foobar=42"}} = http_uri:parse("http://localhost:8888/foobar.html?foo=bar&foobar=42"), %% Esc chars {ok, {http,[],"www.somedomain.com",80,"/%2Eabc",[]}} = http_uri:parse("http://www.somedomain.com/%2Eabc"), {ok, {http,[],"www.somedomain.com",80,"/%252Eabc",[]}} = http_uri:parse("http://www.somedomain.com/%252Eabc"), {ok, {http,[],"www.somedomain.com",80,"/%25abc",[]}} = http_uri:parse("http://www.somedomain.com/%25abc"), {ok, {http,[],"www.somedomain.com",80,"/%25abc", "?foo=bar"}} = http_uri:parse("http://www.somedomain.com/%25abc?foo=bar"), ok. %%------------------------------------------------------------------------- ipv6_ipcomm() -> [{require, ipv6_hosts}]. ipv6_ipcomm(doc) -> ["Test ip_comm ipv6."]; ipv6_ipcomm(suite) -> []; ipv6_ipcomm(Config) when is_list(Config) -> HTTPOptions = [], SocketType = ip_comm, Scheme = "http", Extra = [], ipv6(SocketType, Scheme, HTTPOptions, Extra, Config). %%------------------------------------------------------------------------- ipv6_essl() -> [{require, ipv6_hosts}]. ipv6_essl(doc) -> ["Test essl ipv6."]; ipv6_essl(suite) -> []; ipv6_essl(Config) when is_list(Config) -> DataDir = ?config(data_dir, Config), CertFile = filename:join(DataDir, "ssl_client_cert.pem"), SSLOptions = [{certfile, CertFile}, {keyfile, CertFile}], SSLConfig = {essl, SSLOptions}, tsp("ssl_ipv6 -> make request using: " "~n SSLOptions: ~p", [SSLOptions]), HTTPOptions = [{ssl, SSLConfig}], SocketType = essl, Scheme = "https", Extra = SSLOptions, ipv6(SocketType, Scheme, HTTPOptions, Extra, Config). %%------------------------------------------------------------------------- ipv6(SocketType, Scheme, HTTPOptions, Extra, Config) -> %% Check if we are a IPv6 host tsp("ipv6 -> verify ipv6 support"), case inets_test_lib:has_ipv6_support(Config) of {ok, Addr} -> tsp("ipv6 -> ipv6 supported: ~p", [Addr]), {DummyServerPid, Port} = dummy_server(SocketType, ipv6, Extra), Profile = ?config(profile, Config), URL = Scheme ++ "://[" ++ http_transport:ipv6_name(Addr) ++ "]:" ++ integer_to_list(Port) ++ "/foobar.html", tsp("ipv6 -> issue request with: " "~n URL: ~p" "~n HTTPOptions: ~p", [URL, HTTPOptions]), case httpc:request(get, {URL, []}, HTTPOptions, [], Profile) of {ok, {{_,200,_}, [_ | _], [_|_]}} -> tsp("ipv6 -> expected result"), DummyServerPid ! stop, ok; {ok, Unexpected} -> tsp("ipv6 -> unexpected result: " "~n ~p", [Unexpected]), DummyServerPid ! stop, tsf({unexpected_result, Unexpected}); {error, Reason} -> tsp("ipv6 -> error: " "~n Reason: ~p", [Reason]), DummyServerPid ! stop, tsf(Reason) end, ok; _ -> tsp("ipv6 -> ipv6 not supported"), skip("Host does not support IPv6") end. %%------------------------------------------------------------------------- headers_as_is(doc) -> ["Test the option headers_as_is"]; headers_as_is(suite) -> []; headers_as_is(Config) when is_list(Config) -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", {ok, {{_,200,_}, [_|_], [_|_]}} = httpc:request(get, {URL, [{"Host", "localhost"},{"Te", ""}]}, [], [{headers_as_is, true}]), {ok, {{_,400,_}, [_|_], [_|_]}} = httpc:request(get, {URL, [{"Te", ""}]},[], [{headers_as_is, true}]), ok. %%------------------------------------------------------------------------- selecting_session(doc) -> ["Test selection of sessions - OTP-9847"]; selecting_session(suite) -> []; selecting_session(Config) when is_list(Config) -> tsp("selecting_session -> entry with" "~n Config: ~p", [Config]), tsp("selecting_session -> set ipfamily to inet"), ok = httpc:set_options([{ipfamily, inet}]), tsp("selecting_session -> start server"), {ServerPid, Port} = otp_9847_server(), PortStr = integer_to_list(Port), URL = ?URL_START ++ PortStr ++ "/index.html", tsp("selecting_session -> issue the first batch (three) requests"), lists:foreach(fun(P) -> tsp("selecting_session:fun1 -> " "send stop request to ~p", [P]), P ! stop end, reqs(URL, ServerPid, 3, 3, false)), tsp("selecting_session -> sleep some (1) to make sure nothing lingers"), ?SLEEP(5000), tsp("selecting_session -> " "instruct the server to reply to the first request"), ServerPid ! {answer, true}, receive {answer, true} -> tsp("selecting_session -> " "received ack from server to reply to the first request"), ok end, tsp("selecting_session -> issue the second batch (four) requests"), lists:foreach(fun(P) -> tsp("selecting_session:fun2 -> " "send stop request to ~p", [P]), P ! stop end, reqs(URL, ServerPid, 4, 1, true)), tsp("selecting_session -> sleep some (2) to make sure nothing lingers"), ?SLEEP(5000), tsp("selecting_session -> stop server"), ServerPid ! stop, tsp("selecting_session -> set ipfamily (back) to inet6fb4"), ok = httpc:set_options([{ipfamily, inet6fb4}]), tsp("selecting_session -> done"), ok. reqs(URL, ServerPid, NumReqs, NumHandlers, InitialSync) -> tsp("reqs -> entry with" "~n URL: ~p" "~n ServerPid: ~w" "~n NumReqs: ~w" "~n NumHandlers: ~w" "~n InitialSync: ~w", [URL, ServerPid, NumReqs, NumHandlers, InitialSync]), Handlers = reqs2(URL, NumReqs, [], InitialSync), tsp("reqs -> " "~n Handlers: ~w", [Handlers]), case length(Handlers) of NumHandlers -> tsp("reqs -> " "~n NumHandlers: ~w", [NumHandlers]), ServerPid ! num_handlers, receive {num_handlers, NumHandlers} -> tsp("reqs -> received num_handlers with" "~n NumHandlers: ~w", [NumHandlers]), Handlers; {num_handlers, WrongNumHandlers} -> tsp("reqs -> received num_handlers with" "~n WrongNumHandlers: ~w", [WrongNumHandlers]), exit({wrong_num_handlers1, WrongNumHandlers, NumHandlers}) end; WrongNumHandlers -> tsp("reqs -> " "~n WrongNumHandlers: ~w", [WrongNumHandlers]), exit({wrong_num_handlers2, WrongNumHandlers, NumHandlers}) end. reqs2(_URL, 0, Acc, _Sync) -> lists:reverse(Acc); reqs2(URL, Num, Acc, Sync) -> tsp("reqs2 -> entry with" "~n Num: ~w" "~n Sync: ~w", [Num, Sync]), case httpc:request(get, {URL, []}, [], [{sync, Sync}]) of {ok, _Reply} -> tsp("reqs2 -> successful request: ~p", [_Reply]), receive {handler, Handler, _Manager} -> %% This is when a new handler is created tsp("reqs2 -> received handler: ~p", [Handler]), case lists:member(Handler, Acc) of true -> tsp("reqs2 -> duplicate handler"), exit({duplicate_handler, Handler, Num, Acc}); false -> tsp("reqs2 -> wait for data ack"), receive {data_received, Handler} -> tsp("reqs2 -> " "received data ack from ~p", [Handler]), case Sync of true -> reqs2(URL, Num-1, [Handler|Acc], false); false -> reqs2(URL, Num-1, [Handler|Acc], Sync) end end end; {data_received, Handler} -> tsp("reqs2 -> " "received data ack from ~p", [Handler]), reqs2(URL, Num-1, Acc, false) end; {error, Reason} -> tsp("reqs2 -> request ~w failed: ~p", [Num, Reason]), exit({request_failed, Reason, Num, Acc}) end. otp_9847_server() -> TC = self(), Pid = spawn_link(fun() -> otp_9847_server_init(TC) end), receive {port, Port} -> {Pid, Port} end. otp_9847_server_init(TC) -> tsp("otp_9847_server_init -> entry with" "~n TC: ~p", [TC]), {ok, ListenSocket} = gen_tcp:listen(0, [binary, inet, {packet, 0}, {reuseaddr,true}, {active, false}]), tsp("otp_9847_server_init -> listen socket created: " "~n ListenSocket: ~p", [ListenSocket]), {ok, Port} = inet:port(ListenSocket), tsp("otp_9847_server_init -> Port: ~p", [Port]), TC ! {port, Port}, otp_9847_server_main(TC, ListenSocket, false, []). otp_9847_server_main(TC, ListenSocket, Answer, Handlers) -> tsp("otp_9847_server_main -> entry with" "~n TC: ~p" "~n ListenSocket: ~p" "~n Answer: ~p" "~n Handlers: ~p", [TC, ListenSocket, Answer, Handlers]), case gen_tcp:accept(ListenSocket, 1000) of {ok, Sock} -> tsp("otp_9847_server_main -> accepted" "~n Sock: ~p", [Sock]), {Handler, Mon, Port} = otp_9847_handler(TC, Sock, Answer), tsp("otp_9847_server_main -> handler ~p created for ~w", [Handler, Port]), gen_tcp:controlling_process(Sock, Handler), tsp("otp_9847_server_main -> control transfer"), Handler ! owner, tsp("otp_9847_server_main -> " "handler ~p informed of owner transfer", [Handler]), TC ! {handler, Handler, self()}, tsp("otp_9847_server_main -> " "TC ~p informed of handler ~p", [TC, Handler]), otp_9847_server_main(TC, ListenSocket, Answer, [{Handler, Mon, Sock, Port}|Handlers]); {error, timeout} -> tsp("otp_9847_server_main -> timeout"), receive {answer, true} -> tsp("otp_9847_server_main -> received answer request"), TC ! {answer, true}, otp_9847_server_main(TC, ListenSocket, true, Handlers); {'DOWN', _Mon, process, Pid, _Reason} -> %% Could be one of the handlers tsp("otp_9847_server_main -> received DOWN for ~p", [Pid]), otp_9847_server_main(TC, ListenSocket, Answer, lists:keydelete(Pid, 1, Handlers)); num_handlers -> tsp("otp_9847_server_main -> " "received request for number of handlers (~w)", [length(Handlers)]), TC ! {num_handlers, length(Handlers)}, otp_9847_server_main(TC, ListenSocket, Answer, Handlers); stop -> tsp("otp_9847_server_main -> received stop request"), %% Stop all handlers (just in case) Pids = [Handler || {Handler, _, _} <- Handlers], lists:foreach(fun(Pid) -> Pid ! stop end, Pids), exit(normal); Any -> tsp("otp_9847_server_main -> received" "~n Any: ~p", [Any]), exit({crap, Any}) after 0 -> tsp("otp_9847_server_main -> nothing in queue"), otp_9847_server_main(TC, ListenSocket, Answer, Handlers) end; Error -> exit(Error) end. otp_9847_handler(TC, Sock, Answer) -> tsp("otp_9847_handler -> entry with" "~n TC: ~p" "~n Sock: ~p" "~n Answer: ~p", [TC, Sock, Answer]), Self = self(), {Pid, Mon} = spawn_opt(fun() -> otp_9847_handler_init(TC, Self, Sock, Answer) end, [monitor]), receive {port, Port} -> tsp("otp_9847_handler -> received port message (from ~p)" "~n Port: ~p", [Pid, Port]), {Pid, Mon, Port} end. otp_9847_handler_init(TC, Server, Sock, Answer) -> tsp("otp_9847_handler_init -> entry with" "~n TC: ~p" "~n Server: ~p" "~n Sock: ~p" "~n Answer: ~p", [TC, Server, Sock, Answer]), {ok, Port} = inet:port(Sock), Server ! {port, Port}, receive owner -> tsp("otp_9847_handler_init -> " "received owner message - activate socket"), inet:setopts(Sock, [{active, true}]), otp_9847_handler_main(TC, Server, Sock, Answer, [?HTTP_MAX_HEADER_SIZE]) end. otp_9847_handler_main(TC, Server, Sock, Answer, ParseArgs) -> tsp("otp_9847_handler_main -> entry with" "~n TC: ~p" "~n Server: ~p" "~n Sock: ~p" "~n Answer: ~p" "~n ParseArgs: ~p", [TC, Server, Sock, Answer, ParseArgs]), receive stop -> tsp("otp_9847_handler_main -> received stop request"), exit(normal); {tcp, Sock, _Data} when Answer =:= false -> tsp("otp_9847_handler_main -> received tcp data - no answer"), TC ! {data_received, self()}, inet:setopts(Sock, [{active, true}]), %% Ignore all data otp_9847_handler_main(TC, Server, Sock, Answer, ParseArgs); {tcp, Sock, Data} when Answer =:= true -> tsp("otp_9847_handler_main -> received tcp data - answer"), TC ! {data_received, self()}, inet:setopts(Sock, [{active, true}]), NewParseArgs = otp_9847_handler_request(Sock, [Data|ParseArgs]), otp_9847_handler_main(TC, Server, Sock, Answer, NewParseArgs); {tcp_closed, Sock} -> tsp("otp_9847_handler_main -> received tcp socket closed"), exit(normal); {tcp_error, Sock, Reason} -> tsp("otp_9847_handler_main -> socket error: ~p", [Reason]), (catch gen_tcp:close(Sock)), exit(normal) %% after 30000 -> %% gen_tcp:close(Sock), %% exit(normal) end. otp_9847_handler_request(Sock, Args) -> Msg = case httpd_request:parse(Args) of {ok, {_, "/index.html" = _RelUrl, _, _, _}} -> B = "" ++ "...some body part..." ++ "", Len = integer_to_list(length(B)), "HTTP/1.1 200 ok\r\n" ++ "Content-Length:" ++ Len ++ "\r\n\r\n" ++ B end, gen_tcp:send(Sock, Msg), [?HTTP_MAX_HEADER_SIZE]. %%------------------------------------------------------------------------- options(doc) -> ["Test the option parameters."]; options(suite) -> []; options(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", {ok, {{_,200,_}, [_ | _], Bin}} = httpc:request(get, {URL, []}, [{foo, bar}], %% Ignore unknown options [{body_format, binary}, {foo, bar}]), true = is_binary(Bin), {ok, {200, [_|_]}} = httpc:request(get, {URL, []}, [{timeout, infinity}], [{full_result, false}]); _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- http_invalid_http(doc) -> ["Test parse error"]; http_invalid_http(suite) -> []; http_invalid_http(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/invalid_http.html", {error, {could_not_parse_as_http, _} = Reason} = httpc:request(get, {URL, []}, [], []), test_server:format("Parse error: ~p ~n", [Reason]), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- -define(GOOGLE, "www.google.com"). hexed_query_otp_6191(doc) -> []; hexed_query_otp_6191(suite) -> []; hexed_query_otp_6191(Config) when is_list(Config) -> Google = ?GOOGLE, GoogleSearch = "http://" ++ Google ++ "/search", Search1 = "?hl=en&q=a%D1%85%D1%83%D0%B9&btnG=Google+Search", URI1 = GoogleSearch ++ Search1, Search2 = "?hl=en&q=%25%25", URI2 = GoogleSearch ++ Search2, Search3 = "?hl=en&q=%foo", URI3 = GoogleSearch ++ Search3, Verify1 = fun({http, [], ?GOOGLE, 80, "/search", _}) -> ok; (_) -> error end, Verify2 = Verify1, Verify3 = Verify1, verify_uri(URI1, Verify1), verify_uri(URI2, Verify2), verify_uri(URI3, Verify3), ok. verify_uri(URI, Verify) -> case http_uri:parse(URI) of {ok, ParsedURI} -> case Verify(ParsedURI) of ok -> ok; error -> Reason = {unexpected_parse_result, URI, ParsedURI}, ERROR = {error, Reason}, throw(ERROR) end; {error, _} = ERROR -> throw(ERROR) end. %%------------------------------------------------------------------------- empty_body_otp_6243(doc) -> ["An empty body was not returned directly. There was a delay for several" "seconds."]; empty_body_otp_6243(suite) -> []; empty_body_otp_6243(Config) when is_list(Config) -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/empty.html", {ok, {{_,200,_}, [_ | _], []}} = httpc:request(get, {URL, []}, [{timeout, 500}], []). %%------------------------------------------------------------------------- transfer_encoding_otp_6807(doc) -> ["Transfer encoding is case insensitive"]; transfer_encoding_otp_6807(suite) -> []; transfer_encoding_otp_6807(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/capital_transfer_encoding.html", {ok, {{_,200,_}, [_|_], [_ | _]}} = httpc:request(URL), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- empty_response_header_otp_6830(doc) -> ["Test the case that the HTTP server does not send any headers"]; empty_response_header_otp_6830(suite) -> []; empty_response_header_otp_6830(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/no_headers.html", {ok, {{_,200,_}, [], [_ | _]}} = httpc:request(URL), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- no_content_204_otp_6982(doc) -> ["Test the case that the HTTP 204 no content header"]; no_content_204_otp_6982(suite) -> []; no_content_204_otp_6982(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/no_content.html", {ok, {{_,204,_}, [], []}} = httpc:request(URL), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- missing_CR_otp_7304(doc) -> ["Test the case that the HTTP server uses only LF instead of CRLF" "as delimitor"]; missing_CR_otp_7304(suite) -> []; missing_CR_otp_7304(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/missing_CR.html", {ok, {{_,200,_}, _, [_ | _]}} = httpc:request(URL), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- otp_7883_1(doc) -> ["OTP-7883-sync"]; otp_7883_1(suite) -> []; otp_7883_1(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/just_close.html", {error, socket_closed_remotely} = httpc:request(URL), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. otp_7883_2(doc) -> ["OTP-7883-async"]; otp_7883_2(suite) -> []; otp_7883_2(Config) when is_list(Config) -> ok = httpc:set_options([{ipfamily, inet}]), {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/just_close.html", Method = get, Request = {URL, []}, HttpOptions = [], Options = [{sync, false}], Profile = httpc:default_profile(), {ok, RequestId} = httpc:request(Method, Request, HttpOptions, Options, Profile), ok = receive {http, {RequestId, {error, socket_closed_remotely}}} -> ok end, DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]), ok. %%------------------------------------------------------------------------- otp_8154_1(doc) -> ["OTP-8154"]; otp_8154_1(suite) -> []; otp_8154_1(Config) when is_list(Config) -> start_inets(), ReqSeqNumServer = start_sequence_number_server(), RespSeqNumServer = start_sequence_number_server(), {ok, Server, Port} = start_slow_server(RespSeqNumServer), Clients = run_clients(105, Port, ReqSeqNumServer), %% ok = wait_for_clients(Clients), ok = wait4clients(Clients, timer:minutes(3)), Server ! shutdown, RespSeqNumServer ! shutdown, ReqSeqNumServer ! shutdown, ok. start_inets() -> inets:start(), ok. %% ----------------------------------------------------- %% A sequence number handler %% The purpose is to be able to pair requests with responses. start_sequence_number_server() -> proc_lib:spawn(fun() -> loop_sequence_number(1) end). loop_sequence_number(N) -> receive shutdown -> ok; {From, get_next} -> From ! {next_is, N}, loop_sequence_number(N + 1) end. get_next_sequence_number(SeqNumServer) -> SeqNumServer ! {self(), get_next}, receive {next_is, N} -> N end. %% ----------------------------------------------------- %% Client part %% Sends requests randomly parallel run_clients(NumClients, ServerPort, SeqNumServer) -> io:format("start clients when" "~n NumClients: ~w" "~n ServerPort: ~w" "~n SeqNumServer: ~w" "~n", [NumClients, ServerPort, SeqNumServer]), set_random_seed(), lists:map( fun(Id) -> io:format("starting client ~w~n", [Id]), Req = f("req~3..0w", [get_next_sequence_number(SeqNumServer)]), Url = f(?URL_START ++ "~w/~s", [ServerPort, Req]), Pid = proc_lib:spawn( fun() -> io:format("[~w] client started - " "issue request~n", [Id]), case httpc:request(Url) of {ok, {{_,200,_}, _, Resp}} -> io:format("[~w] 200 response: " "~p~n", [Id, Resp]), case lists:prefix(Req++"->", Resp) of true -> exit(normal); false -> exit({bad_resp,Req,Resp}) end; {ok, {{_,EC,Reason},_,Resp}} -> io:format("[~w] ~w response: " "~s~n~s~n", [Id, EC, Reason, Resp]), exit({bad_resp,Req,Resp}); Crap -> io:format("[~w] bad response: ~p", [Id, Crap]), exit({bad_resp, Req, Crap}) end end), MRef = erlang:monitor(process, Pid), timer:sleep(10 + random:uniform(1334)), {Id, Pid, MRef} end, lists:seq(1, NumClients)). %% wait_for_clients(Clients) -> %% lists:foreach( %% fun({Id, Pid, MRef}) -> %% io:format("waiting for client ~w termination~n", [Id]), %% receive %% {'DOWN', MRef, process, Pid, normal} -> %% io:format("waiting for clients: " %% "normal exit from ~w (~p)~n", %% [Id, Pid]), %% ok; %% {'DOWN', MRef, process, Pid, Reason} -> %% io:format("waiting for clients: " %% "unexpected exit from ~w (~p):" %% "~n Reason: ~p" %% "~n", [Id, Pid, Reason]), %% erlang:error(Reason) %% end %% end, %% Clients). wait4clients([], _Timeout) -> ok; wait4clients(Clients, Timeout) when Timeout > 0 -> io:format("wait4clients -> entry with" "~n length(Clients): ~w" "~n Timeout: ~w" "~n", [length(Clients), Timeout]), T = t(), receive {'DOWN', _MRef, process, Pid, normal} -> case lists:keysearch(Pid, 2, Clients) of {value, {Id, _, _}} -> io:format("receive normal exit message " "from client ~p (~p)", [Id, Pid]), NewClients = lists:keydelete(Id, 1, Clients), wait4clients(NewClients, Timeout - (t() - T)); false -> io:format("receive normal exit message " "from unknown process: ~p", [Pid]), wait4clients(Clients, Timeout - (t() - T)) end; {'DOWN', _MRef, process, Pid, Reason} -> case lists:keysearch(Pid, 2, Clients) of {value, {Id, _, _}} -> io:format("receive bad exit message " "from client ~p (~p):" "~n ~p", [Id, Pid, Reason]), erlang:error({bad_client_termination, Id, Reason}); false -> io:format("receive normal exit message " "from unknown process: ~p", [Pid]), wait4clients(Clients, Timeout - (t() - T)) end after Timeout -> erlang:error({client_timeout, Clients}) end; wait4clients(Clients, _) -> erlang:error({client_timeout, Clients}). %% Time in milli seconds t() -> {A,B,C} = erlang:now(), A*1000000000+B*1000+(C div 1000). %% ----------------------------------------------------- %% Webserver part: %% Implements a web server that sends responses one character %% at a time, with random delays between the characters. start_slow_server(SeqNumServer) -> io:format("start slow server when" "~n SeqNumServer: ~w" "~n", [SeqNumServer]), proc_lib:start( erlang, apply, [fun() -> init_slow_server(SeqNumServer) end, []]). init_slow_server(SeqNumServer) -> io:format("[webserver ~w] init slow server" "~n", [SeqNumServer]), {ok, LSock} = gen_tcp:listen(0, [binary, {packet,0}, {active,true}, {backlog, 100}]), io:format("[webserver ~w] LSock: ~p" "~n", [SeqNumServer, LSock]), {ok, {_IP, Port}} = inet:sockname(LSock), io:format("[webserver ~w] Port: ~w" "~n", [SeqNumServer, Port]), proc_lib:init_ack({ok, self(), Port}), loop_slow_server(LSock, SeqNumServer). loop_slow_server(LSock, SeqNumServer) -> io:format("[webserver ~w] entry with" "~n LSock: ~p" "~n", [SeqNumServer, LSock]), Master = self(), Acceptor = proc_lib:spawn( fun() -> client_handler(Master, LSock, SeqNumServer) end), io:format("[webserver ~w] acceptor started" "~n Acceptor: ~p" "~n", [SeqNumServer, Acceptor]), receive {accepted, Acceptor} -> io:format("[webserver ~w] accepted" "~n", [SeqNumServer]), loop_slow_server(LSock, SeqNumServer); shutdown -> gen_tcp:close(LSock), exit(Acceptor, kill) end. %% Handle one client connection client_handler(Master, LSock, SeqNumServer) -> io:format("[acceptor ~w] await accept" "~n", [SeqNumServer]), {ok, CSock} = gen_tcp:accept(LSock), io:format("[acceptor ~w] accepted" "~n CSock: ~p" "~n", [SeqNumServer, CSock]), Master ! {accepted, self()}, set_random_seed(), loop_client(1, CSock, SeqNumServer). loop_client(N, CSock, SeqNumServer) -> %% Await request, don't bother parsing it too much, %% assuming the entire request arrives in one packet. io:format("[acceptor ~w] await request" "~n N: ~p" "~n", [SeqNumServer, N]), receive {tcp, CSock, Req} -> ReqNum = parse_req_num(Req), RespSeqNum = get_next_sequence_number(SeqNumServer), Response = f("~s->resp~3..0w/~2..0w", [ReqNum, RespSeqNum, N]), Txt = f("Slow server (~p) got ~p, answering with ~p", [self(), Req, Response]), io:format("~s...~n", [Txt]), slowly_send_response(CSock, Response), case parse_connection_type(Req) of keep_alive -> io:format("~s...done~n", [Txt]), loop_client(N+1, CSock, SeqNumServer); close -> io:format("~s...done (closing)~n", [Txt]), gen_tcp:close(CSock) end end. slowly_send_response(CSock, Answer) -> Response = f("HTTP/1.1 200 OK\r\nContent-Length: ~w\r\n\r\n~s", [length(Answer), Answer]), lists:foreach( fun(Char) -> timer:sleep(random:uniform(500)), gen_tcp:send(CSock, <>) end, Response). parse_req_num(Request) -> Opts = [caseless,{capture,all_but_first,list}], {match, [ReqNum]} = re:run(Request, "GET /(.*) HTTP", Opts), ReqNum. parse_connection_type(Request) -> Opts = [caseless,{capture,all_but_first,list}], {match,[CType]} = re:run(Request, "connection: *(keep-alive|close)", Opts), case string:to_lower(CType) of "close" -> close; "keep-alive" -> keep_alive end. set_random_seed() -> {_, _, Micros} = now(), A = erlang:phash2([make_ref(), self(), Micros]), random:seed(A, A, A). f(F, A) -> lists:flatten(io_lib:format(F,A)). %%------------------------------------------------------------------------- otp_8106_pid(doc) -> ["OTP-8106 - deliver reply info using \"other\" pid"]; otp_8106_pid(suite) -> []; otp_8106_pid(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> ReceiverPid = create_receiver(pid), Receiver = ReceiverPid, otp8106(ReceiverPid, Receiver, Config), stop_receiver(ReceiverPid), ok; _ -> skip("Failed to start local http-server") end. otp_8106_fun(doc) -> ["OTP-8106 - deliver reply info using fun"]; otp_8106_fun(suite) -> []; otp_8106_fun(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> ReceiverPid = create_receiver(function), Receiver = otp_8106_deliver_fun(ReceiverPid), otp8106(ReceiverPid, Receiver, Config), stop_receiver(ReceiverPid), ok; _ -> skip("Failed to start local http-server") end. otp_8106_mfa(doc) -> ["OTP-8106 - deliver reply info using mfa callback"]; otp_8106_mfa(suite) -> []; otp_8106_mfa(Config) when is_list(Config) -> case ?config(local_server, Config) of ok -> ReceiverPid = create_receiver(mfa), Receiver = {?MODULE, otp_8106_deliver, [mfa, ReceiverPid]}, otp8106(ReceiverPid, Receiver, Config), stop_receiver(ReceiverPid), ok; _ -> skip("Failed to start local http-server") end. otp8106(ReceiverPid, Receiver, Config) -> Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", Request = {URL, []}, HTTPOptions = [], Options = [{sync, false}, {receiver, Receiver}], {ok, RequestId} = httpc:request(get, Request, HTTPOptions, Options), Body = receive {reply, ReceiverPid, {RequestId, {{_, 200, _}, _, B}}} -> B; {reply, ReceiverPid, Msg} -> tsf(Msg); {bad_reply, ReceiverPid, Msg} -> tsf(Msg) end, inets_test_lib:check_body(binary_to_list(Body)), ok. create_receiver(Type) -> Parent = self(), Receiver = fun() -> receiver(Type, Parent) end, spawn_link(Receiver). stop_receiver(Pid) -> Pid ! {stop, self()}. receiver(Type, Parent) -> receive {stop, Parent} -> exit(normal); {http, ReplyInfo} when (Type =:= pid) -> Parent ! {reply, self(), ReplyInfo}, receiver(Type, Parent); {Type, ReplyInfo} -> Parent ! {reply, self(), ReplyInfo}, receiver(Type, Parent); Crap -> Parent ! {reply, self(), {bad_reply, Crap}}, receiver(Type, Parent) end. otp_8106_deliver_fun(ReceiverPid) -> fun(ReplyInfo) -> otp_8106_deliver(ReplyInfo, function, ReceiverPid) end. otp_8106_deliver(ReplyInfo, Type, ReceiverPid) -> ReceiverPid ! {Type, ReplyInfo}, ok. %%------------------------------------------------------------------------- otp_8056(doc) -> "OTP-8056"; otp_8056(suite) -> []; otp_8056(Config) when is_list(Config) -> Method = get, Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", Request = {URL, []}, HTTPOptions = [], Options1 = [{sync, true}, {stream, {self, once}}], Options2 = [{sync, true}, {stream, self}], {error, streaming_error} = httpc:request(Method, Request, HTTPOptions, Options1), tsp("request 1 failed as expected"), {error, streaming_error} = httpc:request(Method, Request, HTTPOptions, Options2), tsp("request 2 failed as expected"), ok. %%------------------------------------------------------------------------- otp_8352(doc) -> "OTP-8352"; otp_8352(suite) -> []; otp_8352(Config) when is_list(Config) -> tsp("otp_8352 -> entry with" "~n Config: ~p", [Config]), case ?config(local_server, Config) of ok -> tsp("local-server running"), tsp("initial profile info(1): ~p", [httpc:info()]), MaxSessions = 5, MaxKeepAlive = 10, KeepAliveTimeout = timer:minutes(2), ConnOptions = [{max_sessions, MaxSessions}, {max_keep_alive_length, MaxKeepAlive}, {keep_alive_timeout, KeepAliveTimeout}], httpc:set_options(ConnOptions), Method = get, Port = ?config(local_port, Config), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", Request = {URL, []}, Timeout = timer:seconds(1), ConnTimeout = Timeout + timer:seconds(1), HttpOptions1 = [{timeout, Timeout}, {connect_timeout, ConnTimeout}], Options1 = [{socket_opts, [{tos, 87}, {recbuf, 16#FFFF}, {sndbuf, 16#FFFF}]}], case httpc:request(Method, Request, HttpOptions1, Options1) of {ok, {{_,200,_}, [_ | _], ReplyBody1 = [_ | _]}} -> %% equivaliant to httpc:request(get, {URL, []}, [], []), inets_test_lib:check_body(ReplyBody1); {ok, UnexpectedReply1} -> tsf({unexpected_reply, UnexpectedReply1}); {error, _} = Error1 -> tsf({bad_reply, Error1}) end, tsp("profile info (2): ~p", [httpc:info()]), HttpOptions2 = [], Options2 = [{socket_opts, [{tos, 84}, {recbuf, 32#1FFFF}, {sndbuf, 32#1FFFF}]}], case httpc:request(Method, Request, HttpOptions2, Options2) of {ok, {{_,200,_}, [_ | _], ReplyBody2 = [_ | _]}} -> %% equivaliant to httpc:request(get, {URL, []}, [], []), inets_test_lib:check_body(ReplyBody2); {ok, UnexpectedReply2} -> tsf({unexpected_reply, UnexpectedReply2}); {error, _} = Error2 -> tsf({bad_reply, Error2}) end, tsp("profile info (3): ~p", [httpc:info()]), ok; _ -> skip("Failed to start local http-server") end. %%------------------------------------------------------------------------- otp_8371(doc) -> ["OTP-8371"]; otp_8371(suite) -> []; otp_8371(Config) when is_list(Config) -> ok = httpc:set_options([{ipv6, disabled}]), % also test the old option {DummyServerPid, Port} = dummy_server(ipv4), URL = ?URL_START ++ integer_to_list(Port) ++ "/ensure_host_header_with_port.html", case httpc:request(get, {URL, []}, [], []) of {ok, Result} -> case Result of {{_, 200, _}, _Headers, Body} -> tsp("expected response with" "~n Body: ~p", [Body]), ok; {StatusLine, Headers, Body} -> tsp("expected response with" "~n StatusLine: ~p" "~n Headers: ~p" "~n Body: ~p", [StatusLine, Headers, Body]), tsf({unexpected_result, [{status_line, StatusLine}, {headers, Headers}, {body, Body}]}); _ -> tsf({unexpected_result, Result}) end; Error -> tsf({request_failed, Error}) end, DummyServerPid ! stop, ok = httpc:set_options([{ipv6, enabled}]), ok. %%------------------------------------------------------------------------- otp_8739(doc) -> ["OTP-8739"]; otp_8739(suite) -> []; otp_8739(Config) when is_list(Config) -> {_DummyServerPid, Port} = otp_8739_dummy_server(), URL = ?URL_START ++ integer_to_list(Port) ++ "/dummy.html", Method = get, Request = {URL, []}, HttpOptions = [{connect_timeout, 500}, {timeout, 1}], Options = [{sync, true}], case httpc:request(Method, Request, HttpOptions, Options) of {error, timeout} -> %% And now we check the size of the handler db Info = httpc:info(), tsp("Info: ~p", [Info]), {value, {handlers, Handlers}} = lists:keysearch(handlers, 1, Info), case Handlers of [] -> ok; _ -> tsf({unexpected_handlers, Handlers}) end; Unexpected -> tsf({unexpected, Unexpected}) end. otp_8739_dummy_server() -> Parent = self(), Pid = spawn_link(fun() -> otp_8739_dummy_server_init(Parent) end), receive {port, Port} -> {Pid, Port} end. otp_8739_dummy_server_init(Parent) -> {ok, ListenSocket} = gen_tcp:listen(0, [binary, inet, {packet, 0}, {reuseaddr,true}, {active, false}]), {ok, Port} = inet:port(ListenSocket), Parent ! {port, Port}, otp_8739_dummy_server_main(Parent, ListenSocket). otp_8739_dummy_server_main(_Parent, ListenSocket) -> case gen_tcp:accept(ListenSocket) of {ok, Sock} -> %% Ignore the request, and simply wait for the socket to close receive {tcp_closed, Sock} -> (catch gen_tcp:close(ListenSocket)), exit(normal); {tcp_error, Sock, Reason} -> tsp("socket error: ~p", [Reason]), (catch gen_tcp:close(ListenSocket)), exit(normal) after 10000 -> %% Just in case (catch gen_tcp:close(Sock)), (catch gen_tcp:close(ListenSocket)), exit(timeout) end; Error -> exit(Error) end. %%------------------------------------------------------------------------- initial_server_connect(doc) -> ["If this test cases times out the init of httpc_handler process is" "blocking the manager/client process (implementation dependent which) but nither" "should be blocked."]; initial_server_connect(suite) -> []; initial_server_connect(Config) when is_list(Config) -> DataDir = ?config(data_dir, Config), ok = httpc:set_options([{ipfamily, inet}]), CertFile = filename:join(DataDir, "ssl_server_cert.pem"), SSLOptions = [{certfile, CertFile}, {keyfile, CertFile}], {DummyServerPid, Port} = dummy_ssl_server_hang(self(), ipv4, SSLOptions), URL = ?SSL_URL_START ++ integer_to_list(Port) ++ "/index.html", httpc:request(get, {URL, []}, [{ssl,{essl,[]}}], [{sync, false}]), [{session_cookies,[]}] = httpc:which_cookies(), DummyServerPid ! stop, ok = httpc:set_options([{ipfamily, inet6fb4}]). %%-------------------------------------------------------------------- %% Internal functions %%-------------------------------------------------------------------- setup_server_dirs(ServerRoot, DocRoot, DataDir) -> ConfDir = filename:join(ServerRoot, "conf"), CgiDir = filename:join(ServerRoot, "cgi-bin"), ok = file:make_dir(ServerRoot), ok = file:make_dir(DocRoot), ok = file:make_dir(ConfDir), ok = file:make_dir(CgiDir), {ok, Files} = file:list_dir(DataDir), lists:foreach(fun(File) -> case lists:suffix(".html", File) of true -> inets_test_lib:copy_file(File, DataDir, DocRoot); false -> ok end end, Files), Cgi = case test_server:os_type() of {win32, _} -> "cgi_echo.exe"; _ -> "cgi_echo" end, inets_test_lib:copy_file(Cgi, DataDir, CgiDir), inets_test_lib:copy_file("mime.types", DataDir, ConfDir). create_config(FileName, ComType, Port, PrivDir, ServerRoot, DocRoot, SSLDir) -> MaxHdrSz = io_lib:format("~p", [256]), MaxHdrAct = io_lib:format("~p", [close]), SSL = case ComType of ssl -> [cline(["SSLCertificateFile ", filename:join(SSLDir, "ssl_server_cert.pem")]), cline(["SSLCertificateKeyFile ", filename:join(SSLDir, "ssl_server_cert.pem")]), cline(["SSLVerifyClient 0"])]; _ -> [] end, Mod_order = "Modules mod_alias mod_auth mod_esi mod_actions mod_cgi" " mod_include mod_dir mod_get mod_head" " mod_log mod_disk_log mod_trace", %% BindAddress = "*|inet", % Force the use of IPv4 BindAddress = "*", % This corresponds to using IpFamily inet6fb4 HttpConfig = [ cline(["BindAddress ", BindAddress]), cline(["Port ", integer_to_list(Port)]), cline(["ServerName ", "httpc_test"]), cline(["SocketType ", atom_to_list(ComType)]), cline([Mod_order]), cline(["ServerRoot ", ServerRoot]), cline(["DocumentRoot ", DocRoot]), cline(["MaxHeaderSize ",MaxHdrSz]), cline(["MaxHeaderAction ",MaxHdrAct]), cline(["DirectoryIndex ", "index.html "]), cline(["DefaultType ", "text/plain"]), cline(["ScriptAlias /cgi-bin/ ", filename:join(ServerRoot, "cgi-bin"), "/"]), SSL], ConfigFile = filename:join([PrivDir,FileName]), {ok, Fd} = file:open(ConfigFile, [write]), ok = file:write(Fd, lists:flatten(HttpConfig)), ok = file:close(Fd). cline(List) -> lists:flatten([List, "\r\n"]). receive_streamed_body(RequestId, Body) -> receive {http, {RequestId, stream, BinBodyPart}} -> receive_streamed_body(RequestId, <>); {http, {RequestId, stream_end, _Headers}} -> Body; {http, Msg} -> tsf(Msg) end. receive_streamed_body(RequestId, Body, Pid) -> httpc:stream_next(Pid), test_server:format("~p:receive_streamed_body -> requested next stream ~n", [?MODULE]), receive {http, {RequestId, stream, BinBodyPart}} -> receive_streamed_body(RequestId, <>, Pid); {http, {RequestId, stream_end, _Headers}} -> Body; {http, Msg} -> tsf(Msg) end. %% Perform a synchronous stop dummy_server_stop(Pid) -> Pid ! {stop, self()}, receive {stopped, Pid} -> ok end. dummy_server(IpV) -> dummy_server(self(), ip_comm, IpV, []). dummy_server(SocketType, IpV, Extra) -> dummy_server(self(), SocketType, IpV, Extra). dummy_server(Caller, SocketType, IpV, Extra) -> Args = [Caller, SocketType, IpV, Extra], Pid = spawn(httpc_SUITE, dummy_server_init, Args), receive {port, Port} -> {Pid, Port} end. dummy_server_init(Caller, ip_comm, IpV, _) -> BaseOpts = [binary, {packet, 0}, {reuseaddr,true}, {active, false}], {ok, ListenSocket} = case IpV of ipv4 -> tsp("ip_comm ipv4 listen", []), gen_tcp:listen(0, [inet | BaseOpts]); ipv6 -> tsp("ip_comm ipv6 listen", []), gen_tcp:listen(0, [inet6 | BaseOpts]) end, {ok, Port} = inet:port(ListenSocket), tsp("dummy_server_init(ip_comm) -> Port: ~p", [Port]), Caller ! {port, Port}, dummy_ipcomm_server_loop({httpd_request, parse, [?HTTP_MAX_HEADER_SIZE]}, [], ListenSocket); dummy_server_init(Caller, essl, IpV, SSLOptions) -> BaseOpts = [{ssl_imp, new}, {backlog, 128}, binary, {reuseaddr,true}, {active, false} | SSLOptions], dummy_ssl_server_init(Caller, BaseOpts, IpV). dummy_ssl_server_init(Caller, BaseOpts, IpV) -> {ok, ListenSocket} = case IpV of ipv4 -> tsp("dummy_ssl_server_init -> ssl ipv4 listen", []), ssl:listen(0, [inet | BaseOpts]); ipv6 -> tsp("dummy_ssl_server_init -> ssl ipv6 listen", []), ssl:listen(0, [inet6 | BaseOpts]) end, tsp("dummy_ssl_server_init -> ListenSocket: ~p", [ListenSocket]), {ok, {_, Port}} = ssl:sockname(ListenSocket), tsp("dummy_ssl_server_init -> Port: ~p", [Port]), Caller ! {port, Port}, dummy_ssl_server_loop({httpd_request, parse, [?HTTP_MAX_HEADER_SIZE]}, [], ListenSocket). dummy_ipcomm_server_loop(MFA, Handlers, ListenSocket) -> receive stop -> tsp("dummy_ipcomm_server_loop -> stop handlers", []), lists:foreach(fun(Handler) -> Handler ! stop end, Handlers); {stop, From} -> tsp("dummy_ipcomm_server_loop -> " "stop command from ~p for handlers (~p)", [From, Handlers]), Stopper = fun(Handler) -> Handler ! stop end, lists:foreach(Stopper, Handlers), From ! {stopped, self()} after 0 -> tsp("dummy_ipcomm_server_loop -> await accept", []), {ok, Socket} = gen_tcp:accept(ListenSocket), tsp("dummy_ipcomm_server_loop -> accepted: ~p", [Socket]), HandlerPid = dummy_request_handler(MFA, Socket), tsp("dummy_icomm_server_loop -> handler created: ~p", [HandlerPid]), gen_tcp:controlling_process(Socket, HandlerPid), tsp("dummy_ipcomm_server_loop -> " "control transfered to handler", []), HandlerPid ! ipcomm_controller, tsp("dummy_ipcomm_server_loop -> " "handler informed about control transfer", []), dummy_ipcomm_server_loop(MFA, [HandlerPid | Handlers], ListenSocket) end. dummy_ssl_server_loop(MFA, Handlers, ListenSocket) -> receive stop -> tsp("dummy_ssl_server_loop -> stop handlers", []), lists:foreach(fun(Handler) -> Handler ! stop end, Handlers); {stop, From} -> tsp("dummy_ssl_server_loop -> " "stop command from ~p for handlers (~p)", [From, Handlers]), Stopper = fun(Handler) -> Handler ! stop end, lists:foreach(Stopper, Handlers), From ! {stopped, self()} after 0 -> tsp("dummy_ssl_server_loop -> await accept", []), {ok, Socket} = ssl:transport_accept(ListenSocket), tsp("dummy_ssl_server_loop -> accepted: ~p", [Socket]), HandlerPid = dummy_request_handler(MFA, Socket), tsp("dummy_ssl_server_loop -> handler created: ~p", [HandlerPid]), ssl:controlling_process(Socket, HandlerPid), tsp("dummy_ssl_server_loop -> control transfered to handler", []), HandlerPid ! ssl_controller, tsp("dummy_ssl_server_loop -> " "handler informed about control transfer", []), dummy_ssl_server_loop(MFA, [HandlerPid | Handlers], ListenSocket) end. dummy_request_handler(MFA, Socket) -> tsp("spawn request handler", []), spawn(httpc_SUITE, dummy_request_handler_init, [MFA, Socket]). dummy_request_handler_init(MFA, Socket) -> SockType = receive ipcomm_controller -> tsp("dummy_request_handler_init -> " "received ip_comm controller - activate", []), inet:setopts(Socket, [{active, true}]), ip_comm; ssl_controller -> tsp("dummy_request_handler_init -> " "received ssl controller - activate", []), ssl:setopts(Socket, [{active, true}]), ssl end, dummy_request_handler_loop(MFA, SockType, Socket). dummy_request_handler_loop({Module, Function, Args}, SockType, Socket) -> tsp("dummy_request_handler_loop -> entry with" "~n Module: ~p" "~n Function: ~p" "~n Args: ~p", [Module, Function, Args]), receive {Proto, _, Data} when (Proto =:= tcp) orelse (Proto =:= ssl) -> tsp("dummy_request_handler_loop -> [~w] Data ~p", [Proto, Data]), case handle_request(Module, Function, [Data | Args], Socket, Proto) of stop when Proto =:= tcp -> gen_tcp:close(Socket); stop when Proto =:= ssl -> ssl:close(Socket); NewMFA -> dummy_request_handler_loop(NewMFA, SockType, Socket) end; stop when SockType =:= ip_comm -> gen_tcp:close(Socket); stop when SockType =:= ssl -> ssl:close(Socket) end. mk_close(tcp) -> fun(Sock) -> gen_tcp:close(Sock) end; mk_close(ssl) -> fun(Sock) -> ssl:close(Sock) end. mk_send(tcp) -> fun(Sock, Data) -> gen_tcp:send(Sock, Data) end; mk_send(ssl) -> fun(Sock, Data) -> ssl:send(Sock, Data) end. handle_request(Module, Function, Args, Socket, Proto) -> Close = mk_close(Proto), Send = mk_send(Proto), handle_request(Module, Function, Args, Socket, Close, Send). handle_request(Module, Function, Args, Socket, Close, Send) -> tsp("handle_request -> entry with" "~n Module: ~p" "~n Function: ~p" "~n Args: ~p", [Module, Function, Args]), case Module:Function(Args) of {ok, Result} -> tsp("handle_request -> ok" "~n Result: ~p", [Result]), case (catch handle_http_msg(Result, Socket, Close, Send)) of stop -> stop; <<>> -> tsp("handle_request -> empty data"), {httpd_request, parse, [[<<>>, ?HTTP_MAX_HEADER_SIZE]]}; Data -> handle_request(httpd_request, parse, [Data |[?HTTP_MAX_HEADER_SIZE]], Socket, Close, Send) end; NewMFA -> tsp("handle_request -> " "~n NewMFA: ~p", [NewMFA]), NewMFA end. handle_http_msg({_, RelUri, _, {_, Headers}, Body}, Socket, Close, Send) -> tsp("handle_http_msg -> entry with: " "~n RelUri: ~p" "~n Headers: ~p" "~n Body: ~p", [RelUri, Headers, Body]), NextRequest = case RelUri of "/dummy_headers.html" -> <<>>; "/no_headers.html" -> stop; "/just_close.html" -> stop; _ -> ContentLength = content_length(Headers), case size(Body) - ContentLength of 0 -> <<>>; _ -> <<_BodyThisReq:ContentLength/binary, Next/binary>> = Body, Next end end, tsp("handle_http_msg -> NextRequest: ~p", [NextRequest]), case (catch ets:lookup(cookie, cookies)) of [{cookies, true}]-> tsp("handle_http_msg -> check cookies ~p", []), check_cookie(Headers); _ -> ok end, DefaultResponse = "HTTP/1.1 200 ok\r\n" ++ "Content-Length:32\r\n\r\n" "foobar", Msg = case RelUri of "/just_close.html" -> close; "/no_content.html" -> "HTTP/1.0 204 No Content\r\n\r\n"; "/no_headers.html" -> "HTTP/1.0 200 OK\r\n\r\nTEST"; "/ensure_host_header_with_port.html" -> %% tsp("handle_http_msg -> validate host with port"), case ensure_host_header_with_port(Headers) of true -> B = "" ++ "host with port" ++ "", Len = integer_to_list(length(B)), "HTTP/1.1 200 ok\r\n" ++ "Content-Length:" ++ Len ++ "\r\n\r\n" ++ B; false -> B = "" ++ "Internal Server Error - host without port" ++ "", Len = integer_to_list(length(B)), "HTTP/1.1 500 Internal Server Error\r\n" ++ "Content-Length:" ++ Len ++ "\r\n\r\n" ++ B end; "/300.html" -> NewUri = ?URL_START ++ integer_to_list(?IP_PORT) ++ "/dummy.html", "HTTP/1.1 300 Multiple Choices\r\n" ++ "Location:" ++ NewUri ++ "\r\n" ++ "Content-Length:0\r\n\r\n"; "/301.html" -> NewUri = ?URL_START ++ integer_to_list(?IP_PORT) ++ "/dummy.html", "HTTP/1.1 301 Moved Permanently\r\n" ++ "Location:" ++ NewUri ++ "\r\n" ++ "Content-Length:80\r\n\r\n" ++ "New place"; "/302.html" -> NewUri = ?URL_START ++ integer_to_list(?IP_PORT) ++ "/dummy.html", "HTTP/1.1 302 Found \r\n" ++ "Location:" ++ NewUri ++ "\r\n" ++ "Content-Length:80\r\n\r\n" ++ "New place"; "/307.html" -> NewUri = ?URL_START ++ integer_to_list(?IP_PORT) ++ "/dummy.html", "HTTP/1.1 307 Temporary Rediect \r\n" ++ "Location:" ++ NewUri ++ "\r\n" ++ "Content-Length:80\r\n\r\n" ++ "New place"; "/500.html" -> "HTTP/1.1 500 Internal Server Error\r\n" ++ "Content-Length:47\r\n\r\n" ++ "Internal Server Error"; "/503.html" -> case ets:lookup(unavailable, 503) of [{503, unavailable}] -> ets:insert(unavailable, {503, available}), "HTTP/1.1 503 Service Unavailable\r\n" ++ "Retry-After:5\r\n" ++ "Content-Length:47\r\n\r\n" ++ "Internal Server Error"; [{503, available}] -> DefaultResponse; [{503, long_unavailable}] -> "HTTP/1.1 503 Service Unavailable\r\n" ++ "Retry-After:120\r\n" ++ "Content-Length:47\r\n\r\n" ++ "Internal Server Error" end; "/redirectloop.html" -> %% Create a potential endless loop! {ok, Port} = inet:port(Socket), NewUri = ?URL_START ++ integer_to_list(Port) ++ "/redirectloop.html", "HTTP/1.1 300 Multiple Choices\r\n" ++ "Location:" ++ NewUri ++ "\r\n" ++ "Content-Length:0\r\n\r\n"; "/userinfo.html" -> Challange = "HTTP/1.1 401 Unauthorized \r\n" ++ "WWW-Authenticate:Basic" ++"\r\n" ++ "Content-Length:0\r\n\r\n", case auth_header(Headers) of {ok, Value} -> handle_auth(Value, Challange, DefaultResponse); _ -> Challange end; "/dummy_headers.html" -> %% The client will only care about the Transfer-Encoding %% header the rest of these headers are left to the %% user to evaluate. This is not a valid response %% it only tests that the header handling code works. Head = "HTTP/1.1 200 ok\r\n" ++ "Content-Length:32\r\n" ++ "Pragma:1#no-cache\r\n" ++ "Via:1.0 fred, 1.1 nowhere.com (Apache/1.1)\r\n" ++ "Warning:1#pseudonym foobar\r\n" ++ "Vary:*\r\n" ++ "Trailer:Other:inets_test\r\n" ++ "Upgrade:HTTP/2.0\r\n" ++ "Age:4711\r\n" ++ "Transfer-Encoding:chunked\r\n" ++ "Content-Encoding:foo\r\n" ++ "Content-Language:en\r\n" ++ "Content-Location:http://www.foobar.se\r\n" ++ "Content-MD5:104528739076276072743283077410617235478\r\n" ++ "Content-Range:Sat, 29 Oct 1994 19:43:31 GMT\r\n" ++ "Expires:Sat, 29 Oct 1994 19:43:31 GMT\r\n" ++ "Proxy-Authenticate:#1Basic" ++ "\r\n\r\n", Send(Socket, Head), Send(Socket, http_chunk:encode("fo")), Send(Socket, http_chunk:encode("obar")), http_chunk:encode_last(); "/capital_transfer_encoding.html" -> Head = "HTTP/1.1 200 ok\r\n" ++ "Transfer-Encoding:Chunked\r\n\r\n", Send(Socket, Head), Send(Socket, http_chunk:encode("fo")), Send(Socket, http_chunk:encode("obar")), http_chunk:encode_last(); "/cookie.html" -> "HTTP/1.1 200 ok\r\n" ++ "set-cookie:" ++ "test_cookie=true; path=/;" ++ "max-age=60000\r\n" ++ "Content-Length:32\r\n\r\n"++ "foobar"; "/missing_crlf.html" -> "HTTP/1.1 200 ok" ++ "Content-Length:32\r\n" ++ "foobar"; "/wrong_statusline.html" -> "ok 200 HTTP/1.1\r\n\r\n" ++ "Content-Length:32\r\n\r\n" ++ "foobar"; "/once_chunked.html" -> Head = "HTTP/1.1 200 ok\r\n" ++ "Transfer-Encoding:Chunked\r\n\r\n", Send(Socket, Head), Send(Socket, http_chunk:encode("fo")), Send(Socket, http_chunk:encode("obar")), http_chunk:encode_last(); "/once.html" -> Head = "HTTP/1.1 200 ok\r\n" ++ "Content-Length:32\r\n\r\n", Send(Socket, Head), Send(Socket, "fo"), test_server:sleep(1000), Send(Socket, "ob"), test_server:sleep(1000), Send(Socket, "ar"); "/invalid_http.html" -> "HTTP/1.1 301\r\nDate:Sun, 09 Dec 2007 13:04:18 GMT\r\n" ++ "Transfer-Encoding:chunked\r\n\r\n"; "/missing_reason_phrase.html" -> "HTTP/1.1 200\r\n" ++ "Content-Length: 32\r\n\r\n" "foobar"; "/missing_CR.html" -> "HTTP/1.1 200 ok\n" ++ "Content-Length:32\r\n\n" "foobar"; _ -> DefaultResponse end, tsp("handle_http_msg -> Msg: ~p", [Msg]), case Msg of ok -> %% Previously, this resulted in an {error, einval}. Now what? ok; close -> %% Nothing to send, just close Close(Socket); _ when is_list(Msg) orelse is_binary(Msg) -> Send(Socket, Msg) end, tsp("handle_http_msg -> done"), NextRequest. ensure_host_header_with_port([]) -> false; ensure_host_header_with_port(["host: " ++ Host| _]) -> case string:tokens(Host, [$:]) of [ActualHost, Port] -> tsp("ensure_host_header_with_port -> " "~n ActualHost: ~p" "~n Port: ~p", [ActualHost, Port]), true; _ -> false end; ensure_host_header_with_port([_|T]) -> ensure_host_header_with_port(T). auth_header([]) -> auth_header_not_found; auth_header(["authorization:" ++ Value | _]) -> {ok, string:strip(Value)}; auth_header([_ | Tail]) -> auth_header(Tail). handle_auth("Basic " ++ UserInfo, Challange, DefaultResponse) -> case string:tokens(base64:decode_to_string(UserInfo), ":") of ["alladin", "sesame"] = Auth -> test_server:format("Auth: ~p~n", [Auth]), DefaultResponse; Other -> test_server:format("UnAuth: ~p~n", [Other]), Challange end. check_cookie([]) -> tsf(no_cookie_header); check_cookie(["cookie:" ++ _Value | _]) -> ok; check_cookie([_Head | Tail]) -> check_cookie(Tail). content_length([]) -> 0; content_length(["content-length:" ++ Value | _]) -> list_to_integer(string:strip(Value)); content_length([_Head | Tail]) -> content_length(Tail). %% ------------------------------------------------------------------------- simple_request_and_verify(Config, Method, Request, HttpOpts, Opts, VerifyResult) when (is_list(Config) andalso is_atom(Method) andalso is_list(HttpOpts) andalso is_list(Opts) andalso is_function(VerifyResult, 1)) -> tsp("request_and_verify -> entry with" "~n Method: ~p" "~n Request: ~p" "~n HttpOpts: ~p" "~n Opts: ~p", [Method, Request, HttpOpts, Opts]), case ?config(local_server, Config) of ok -> tsp("request_and_verify -> local-server running"), Result = (catch httpc:request(Method, Request, HttpOpts, Opts)), VerifyResult(Result); _ -> tsp("request_and_verify -> local-server *not* running - skip"), hard_skip("Local http-server not running") end. not_implemented_yet() -> exit(not_implemented_yet). p(F) -> p(F, []). p(F, A) -> io:format("~p ~w:" ++ F ++ "~n", [self(), ?MODULE | A]). tsp(F) -> inets_test_lib:tsp("[~w]" ++ F, [?MODULE]). tsp(F, A) -> inets_test_lib:tsp("[~w]" ++ F, [?MODULE|A]). tsf(Reason) -> test_server:fail(Reason). dummy_ssl_server_hang(Caller, IpV, SslOpt) -> Pid = spawn(httpc_SUITE, dummy_ssl_server_hang_init, [Caller, IpV, SslOpt]), receive {port, Port} -> {Pid, Port} end. dummy_ssl_server_hang_init(Caller, IpV, SslOpt) -> {ok, ListenSocket} = case IpV of ipv4 -> ssl:listen(0, [binary, inet, {packet, 0}, {reuseaddr,true}, {active, false}] ++ SslOpt); ipv6 -> ssl:listen(0, [binary, inet6, {packet, 0}, {reuseaddr,true}, {active, false}] ++ SslOpt) end, {ok, {_,Port}} = ssl:sockname(ListenSocket), tsp("dummy_ssl_server_hang_init -> Port: ~p", [Port]), Caller ! {port, Port}, {ok, AcceptSocket} = ssl:transport_accept(ListenSocket), dummy_ssl_server_hang_loop(AcceptSocket). dummy_ssl_server_hang_loop(_) -> %% Do not do ssl:ssl_accept as we %% want to time out the underlying gen_tcp:connect receive stop -> ok end. hard_skip(Reason) -> throw(skip(Reason)). skip(Reason) -> {skip, Reason}.