%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2012-2016. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %% %% %% ts:run(inets, httpc_proxy_SUITE, [batch]). %% ct:run("../inets_test", httpc_proxy_SUITE). %% -module(httpc_proxy_SUITE). -include_lib("common_test/include/ct.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). -define(LOCAL_PROXY_SCRIPT, "server_proxy.sh"). -define(p(F, A), % Debug printout begin io:format( "~w ~w: " ++ begin F end, [self(),?MODULE] ++ begin A end) end). %%-------------------------------------------------------------------- %% Common Test interface functions ----------------------------------- %%-------------------------------------------------------------------- suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [{group,local_proxy}, {group,local_proxy_https}]. groups() -> [{local_proxy,[], [http_emulate_lower_versions |local_proxy_cases()]}, {local_proxy_https,[], local_proxy_cases() ++ local_proxy_https_cases()}]. %% internal functions local_proxy_cases() -> [http_head, http_get, http_options, http_trace, http_post, http_put, http_delete, http_delete_body, http_headers, http_proxy_auth, http_doesnotexist, http_stream, http_not_modified_otp_6821]. local_proxy_https_cases() -> [https_connect_error]. %%-------------------------------------------------------------------- init_per_suite(Config0) -> case init_apps(suite_apps(), Config0) of Config when is_list(Config) -> make_cert_files(dsa, "server-", Config), Config; Other -> Other end. end_per_suite(_Config) -> [app_stop(App) || App <- r(suite_apps())], ok. %% internal functions suite_apps() -> [asn1,crypto,public_key]. %%-------------------------------------------------------------------- init_per_group(local_proxy, Config) -> init_local_proxy([{protocol,http}|Config]); init_per_group(local_proxy_https, Config) -> init_local_proxy([{protocol,https}|Config]). end_per_group(Group, Config) when Group =:= local_proxy; Group =:= local_proxy_https -> rcmd_local_proxy(["stop"], Config), Config; end_per_group(_, Config) -> Config. %%-------------------------------------------------------------------- init_per_testcase(Case, Config0) -> ct:timetrap({seconds,30}), Apps = apps(Case, Config0), case init_apps(Apps, Config0) of Config when is_list(Config) -> case app_start(inets, Config) of ok -> Config; Error -> [app_stop(N) || N <- [inets|r(Apps)]], ct:fail({could_not_init_inets,Error}) end; E3 -> E3 end. end_per_testcase(_Case, Config) -> app_stop(inets), Config. %% internal functions apps(_Case, Config) -> case proplists:get_value(protocol, Config) of https -> [ssl]; _ -> [] end. %%-------------------------------------------------------------------- %% Test Cases -------------------------------------------------------- %%-------------------------------------------------------------------- http_head(doc) -> ["Test http/https HEAD request."]; http_head(Config) when is_list(Config) -> Method = head, URL = url("/index.html", Config), Request = {URL,[]}, HttpOpts = [], Opts = [], {ok,{{_,200,_},[_|_],[]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_get(doc) -> ["Test http/https GET request."]; http_get(Config) when is_list(Config) -> Method = get, URL = url("/index.html", Config), Request = {URL,[]}, Timeout = timer:seconds(1), ConnTimeout = Timeout + timer:seconds(1), HttpOpts1 = [{timeout,Timeout},{connect_timeout,ConnTimeout}], Opts1 = [], {ok,{{_,200,_},[_|_],[_|_]=B1}} = httpc:request(Method, Request, HttpOpts1, Opts1), inets_test_lib:check_body(B1), HttpOpts2 = [], Opts2 = [{body_format,binary}], {ok,{{_,200,_},[_|_],B2}} = httpc:request(Method, Request, HttpOpts2, Opts2), inets_test_lib:check_body(binary_to_list(B2)). %%-------------------------------------------------------------------- http_options(doc) -> ["Perform an OPTIONS request."]; http_options(Config) when is_list(Config) -> Method = options, URL = url("/index.html", Config), Request = {URL,[]}, HttpOpts = [], Opts = [], {ok,{{_,200,_},Headers,_}} = httpc:request(Method, Request, HttpOpts, Opts), {value,_} = lists:keysearch("allow", 1, Headers), ok. %%-------------------------------------------------------------------- http_trace(doc) -> ["Perform a TRACE request."]; http_trace(Config) when is_list(Config) -> Method = trace, URL = url("/index.html", Config), Request = {URL,[]}, HttpOpts = [], Opts = [], {ok,{{_,200,_},[_|_],"TRACE "++_}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_post(doc) -> ["Perform a POST request that goes through a proxy. When the " "request goes to an ordinary file it seems the POST data " "is ignored."]; http_post(Config) when is_list(Config) -> Method = post, URL = url("/index.html", Config), Request = {URL,[],"text/plain","foobar"}, HttpOpts = [], Opts = [], {ok,{{_,200,_},[_|_],[_|_]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_put(doc) -> ["Perform a PUT request. The server will not allow it " "but we only test sending the request."]; http_put(Config) when is_list(Config) -> Method = put, URL = url("/put.html", Config), Content = "

foo

bar

", Request = {URL,[],"html",Content}, HttpOpts = [], Opts = [], {ok,{{_,405,_},[_|_],[_|_]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_delete(doc) -> ["Perform a DELETE request that goes through a proxy. Note the server " "will reject the request with a 405 Method Not Allowed," "but this is just a test of sending the request."]; http_delete(Config) when is_list(Config) -> Method = delete, URL = url("/delete.html", Config), Request = {URL,[]}, HttpOpts = [], Opts = [], {ok,{{_,405,_},[_|_],[_|_]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_delete_body(doc) -> ["Perform a DELETE request with a content body. The server will not allow it " "but we only test sending the request."]; http_delete_body(Config) when is_list(Config) -> Method = delete, URL = url("/delete.html", Config), Content = "foo=bar", Request = {URL,[],"application/x-www-form-urlencoded",Content}, HttpOpts = [], Opts = [], {ok,{{_,405,_},[_|_],[_|_]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_headers(doc) -> ["Use as many request headers as possible"]; http_headers(Config) when is_list(Config) -> Method = get, URL = url("/index.html", Config), Headers = [{"Accept", "text/*, text/html, text/html;level=1, */*"}, {"Accept-Charset", "iso-8859-5, unicode-1-1;q=0.8"}, {"Accept-Encoding", "*"}, {"Accept-Language", "sv, en-gb;q=0.8, en;q=0.7"}, {"User-Agent", "inets"}, {"Max-Forwards","5"}, {"Referer", "http://otp.ericsson.se:8000/product/internal"}], Request = {URL,Headers}, HttpOpts = [], Opts = [], {ok,{{_,200,_},[_|_],[_|_]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_proxy_auth(doc) -> ["Test the code for sending of proxy authorization."]; http_proxy_auth(Config) when is_list(Config) -> %% Our proxy seems to ignore the header, however our proxy %% does not requirer an auth header, but we want to know %% atleast the code for sending the header does not crash! Method = get, URL = url("/index.html", Config), Request = {URL,[]}, HttpOpts = [{proxy_auth,{"foo","bar"}}], Opts = [], {ok,{{_,200,_},[_|_],[_|_]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_doesnotexist(doc) -> ["Test that we get a 404 when the page is not found."]; http_doesnotexist(Config) when is_list(Config) -> Method = get, URL = url("/doesnotexist.html", Config), Request = {URL,[]}, HttpOpts = [{proxy_auth,{"foo","bar"}}], Opts = [], {ok,{{_,404,_},[_|_],[_|_]}} = httpc:request(Method, Request, HttpOpts, Opts), ok. %%-------------------------------------------------------------------- http_stream(doc) -> ["Test the option stream for asynchronous requests"]; http_stream(Config) when is_list(Config) -> Method = get, URL = url("/index.html", Config), Request = {URL,[]}, HttpOpts = [], Opts1 = [{body_format,binary}], {ok,{{_,200,_},[_|_],Body}} = httpc:request(Method, Request, HttpOpts, Opts1), Opts2 = [{sync,false},{stream,self}], {ok,RequestId} = httpc:request(Method, Request, HttpOpts, Opts2), receive {http,{RequestId,stream_start,[_|_]}} -> ok end, case http_stream(RequestId, <<>>) of Body -> ok end. %% StreamedBody = http_stream(RequestId, <<>>), %% Body =:= StreamedBody, %% ok. http_stream(RequestId, Body) -> receive {http,{RequestId,stream,Bin}} -> http_stream(RequestId, <>); {http,{RequestId,stream_end,_Headers}} -> Body end. %%-------------------------------------------------------------------- http_emulate_lower_versions(doc) -> ["Perform requests as 0.9 and 1.0 clients."]; http_emulate_lower_versions(Config) when is_list(Config) -> Method = get, URL = url("/index.html", Config), Request = {URL,[]}, Opts = [], HttpOpts1 = [{version,"HTTP/0.9"}], {ok,[_|_]=B1} = httpc:request(Method, Request, HttpOpts1, Opts), inets_test_lib:check_body(B1), HttpOpts2 = [{version,"HTTP/1.0"}], {ok,{{_,200,_},[_|_],[_|_]=B2}} = httpc:request(Method, Request, HttpOpts2, Opts), inets_test_lib:check_body(B2), HttpOpts3 = [{version,"HTTP/1.1"}], {ok,{{_,200,_},[_|_],[_|_]=B3}} = httpc:request(Method, Request, HttpOpts3, Opts), inets_test_lib:check_body(B3), ok. %%-------------------------------------------------------------------- http_not_modified_otp_6821(doc) -> ["If unmodified no body should be returned"]; http_not_modified_otp_6821(Config) when is_list(Config) -> Method = get, URL = url("/index.html", Config), Opts = [], Request1 = {URL,[]}, HttpOpts1 = [], {ok,{{_,200,_},ReplyHeaders,[_|_]}} = httpc:request(Method, Request1, HttpOpts1, Opts), ETag = header_value("etag", ReplyHeaders), LastModified = header_value("last-modified", ReplyHeaders), Request2 = {URL, [{"If-None-Match",ETag}, {"If-Modified-Since",LastModified}]}, HttpOpts2 = [{timeout,15000}], % Limit wait for bug result {ok,{{_,304,_},_,[]}} = % Page Unchanged httpc:request(Method, Request2, HttpOpts2, Opts), ok. header_value(Name, [{HeaderName,HeaderValue}|Headers]) -> case string:to_lower(HeaderName) of Name -> HeaderValue; _ -> header_value(Name, Headers) end. %%-------------------------------------------------------------------- https_connect_error(doc) -> ["Error from CONNECT tunnel should be returned"]; https_connect_error(Config) when is_list(Config) -> {HttpServer,HttpPort} = proplists:get_value(http, Config), Method = get, %% using HTTPS scheme with HTTP port to trigger connection error URL = "https://" ++ HttpServer ++ ":" ++ integer_to_list(HttpPort) ++ "/index.html", Opts = [], HttpOpts = [], Request = {URL,[]}, {error,{failed_connect,[_,{tls,_,_}]}} = httpc:request(Method, Request, HttpOpts, Opts). %%-------------------------------------------------------------------- %% Internal Functions ------------------------------------------------ %%-------------------------------------------------------------------- init_apps([], Config) -> Config; init_apps([App|Apps], Config) -> case app_start(App, Config) of ok -> init_apps(Apps, Config); Error -> Msg = lists:flatten( io_lib:format( "Could not start ~p due to ~p.~n", [App, Error])), {skip,Msg} end. app_start(App, Config) -> try case App of crypto -> crypto:stop(), ok = crypto:start(); inets -> application:stop(App), ok = application:start(App), case proplists:get_value(proxy, Config) of undefined -> ok; {_,ProxySpec} -> ok = httpc:set_options([{proxy,ProxySpec}]) end; _ -> application:stop(App), ok = application:start(App) end catch Class:Reason -> {exception,Class,Reason} end. app_stop(App) -> application:stop(App). make_cert_files(Alg, Prefix, Config) -> PrivDir = proplists:get_value(priv_dir, Config), CaInfo = {CaCert,_} = erl_make_certs:make_cert([{key,Alg}]), {Cert,CertKey} = erl_make_certs:make_cert([{key,Alg},{issuer,CaInfo}]), CaCertFile = filename:join(PrivDir, Prefix++"cacerts.pem"), CertFile = filename:join(PrivDir, Prefix++"cert.pem"), KeyFile = filename:join(PrivDir, Prefix++"key.pem"), der_to_pem(CaCertFile, [{'Certificate', CaCert, not_encrypted}]), der_to_pem(CertFile, [{'Certificate', Cert, not_encrypted}]), der_to_pem(KeyFile, [CertKey]), ok. der_to_pem(File, Entries) -> PemBin = public_key:pem_encode(Entries), file:write_file(File, PemBin). url(AbsPath, Config) -> Protocol = proplists:get_value(protocol, Config), {ServerName,ServerPort} = proplists:get_value(Protocol, Config), atom_to_list(Protocol) ++ "://" ++ ServerName ++ ":" ++ integer_to_list(ServerPort) ++ AbsPath. %%-------------------------------------------------------------------- init_local_proxy(Config) -> case os:type() of {unix,_} -> case rcmd_local_proxy(["start"], Config) of {0,[":STARTED:"++String]} -> init_local_proxy_string(String, Config); {_,[":SKIP:"++_|_]}=Reason -> {skip,Reason}; Error -> rcmd_local_proxy(["stop"], Config), ct:fail({local_proxy_start_failed,Error}) end; _ -> {skip,"Platform can not run local proxy start script"} end. init_local_proxy_string(String, Config) -> {Proxy,Server} = split($|, String), {ProxyName,ProxyPort} = split($:, Proxy), {ServerName,ServerPorts} = split($:, Server), {ServerHttpPort,ServerHttpsPort} = split($:, ServerPorts), [{proxy,{local,{{ProxyName,list_to_integer(ProxyPort)},[]}}}, {http,{ServerName,list_to_integer(ServerHttpPort)}}, {https,{ServerName,list_to_integer(ServerHttpsPort)}} |Config]. rcmd_local_proxy(Args, Config) -> DataDir = proplists:get_value(data_dir, Config), PrivDir = proplists:get_value(priv_dir, Config), Script = filename:join(DataDir, ?LOCAL_PROXY_SCRIPT), rcmd(Script, Args, [{cd,PrivDir}]). rcmd(Cmd, Args, Opts) -> Port = erlang:open_port( {spawn_executable,Cmd}, [{args,Args},{line,80},exit_status,eof,hide|Opts]), rcmd_loop(Port, [], [], undefined, false). rcmd_loop(Port, Lines, Buf, Exit, EOF) -> receive {Port,{data,{Flag,Line}}} -> case Flag of noeol -> rcmd_loop(Port, Lines, r(Line, Buf), Exit, EOF); eol -> rcmd_loop(Port, [r(Buf, Line)|Lines], [], Exit, EOF) end; {Port,{exit_status,Status}} when Exit =:= undefined -> case EOF of true -> rcmd_close(Port, Lines, Buf, Status); false -> rcmd_loop(Port, Lines, Buf, Status, EOF) end; {Port,eof} when EOF =:= false -> case Exit of undefined -> rcmd_loop(Port, Lines, Buf, Exit, true); Status -> rcmd_close(Port, Lines, Buf, Status) end; {Port,_}=Unexpected -> ct:fail({unexpected_from_port,Unexpected}) end. rcmd_close(Port, Lines, Buf, Status) -> catch port_close(Port), case Buf of [] -> {Status,Lines}; _ -> {Status,[r(Buf)|Lines]} end. %%-------------------------------------------------------------------- %% Split on first match of X in Ys, do not include X in neither part split(X, Ys) -> split(X, Ys, []). %% split(X, [X|Ys], Rs) -> {r(Rs),Ys}; split(X, [Y|Ys], Rs) -> split(X, Ys, [Y|Rs]). r(L) -> lists:reverse(L). r(L, R) -> lists:reverse(L, R).