%% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2001-2011. 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% %% %% -module(httpd_test_lib). -include("inets_test_lib.hrl"). %% Poll functions -export([verify_request/6, verify_request/7, is_expect/1]). -record(state, {request, % string() socket, % socket() status_line, % {Version, StatusCode, ReasonPharse} headers, % #http_response_h{} body, % binary() mfa = {httpc_response, parse, [nolimit, false]}, canceled = [], % [RequestId] max_header_size = nolimit, % nolimit | integer() max_body_size = nolimit, % nolimit | integer() print = false }). %%% Part of http.hrl - Temporary solution %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Response headers -record(http_response_h,{ %%% --- Standard "General" headers 'cache-control', connection, date, pragma, trailer, 'transfer-encoding', upgrade, via, warning, %%% --- Standard "Response" headers 'accept-ranges', age, etag, location, 'proxy-authenticate', 'retry-after', server, vary, 'www-authenticate', %%% --- Standard "Entity" headers allow, 'content-encoding', 'content-language', 'content-length' = "0", 'content-location', 'content-md5', 'content-range', 'content-type', expires, 'last-modified', other=[] % list() - Key/Value list with other headers }). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%-------------------------------------------------------------------- %% API %%------------------------------------------------------------------ verify_request(SocketType, Host, Port, Node, RequestStr, Options) -> verify_request(SocketType, Host, Port, Node, RequestStr, Options, 30000). verify_request(SocketType, Host, Port, Node, RequestStr, Options, TimeOut) -> tsp("verify_request -> connect to [~w] ~p:~w", [SocketType, Host, Port]), {ok, Socket} = inets_test_lib:connect_bin(SocketType, Host, Port), inets_test_lib:send(SocketType, Socket, RequestStr), State = case inets_regexp:match(RequestStr, "printenv") of nomatch -> #state{}; _ -> #state{print = true} end, case request(State#state{request = RequestStr, socket = Socket}, TimeOut) of {error, Reson} -> {error, Reson}; NewState -> ValidateResult = validate(RequestStr, NewState, Options, Node, Port), inets_test_lib:close(SocketType, Socket), ValidateResult end. request(#state{mfa = {Module, Function, Args}, request = RequestStr, socket = Socket} = State, TimeOut) -> io:format("~p ~w[~w]request -> entry with" "~n Module: ~p" "~n Function: ~p" "~n Args: ~p" "~n", [self(), ?MODULE, ?LINE, Module, Function, Args]), HeadRequest = lists:sublist(RequestStr, 1, 4), receive {tcp, Socket, Data} -> io:format("~p ~w[~w]request -> received (tcp) data" "~n Data: ~p" "~n", [self(), ?MODULE, ?LINE, Data]), print(tcp, Data, State), case Module:Function([Data | Args]) of {ok, Parsed} -> handle_http_msg(Parsed, State); {_, whole_body, _} when HeadRequest == "HEAD" -> State#state{body = <<>>}; NewMFA -> request(State#state{mfa = NewMFA}, TimeOut) end; {tcp_closed, Socket} when Function == whole_body -> io:format("~p ~w[~w]request -> " "received (tcp) closed when whole_body" "~n", [self(), ?MODULE, ?LINE]), print(tcp, "closed", State), State#state{body = hd(Args)}; {tcp_closed, Socket} -> io:format("~p ~w[~w]request -> received (tcp) closed" "~n", [self(), ?MODULE, ?LINE]), test_server:fail(connection_closed); {tcp_error, Socket, Reason} -> io:format("~p ~w[~w]request -> received (tcp) error" "~n Reason: ~p" "~n", [self(), ?MODULE, ?LINE, Reason]), test_server:fail({tcp_error, Reason}); {ssl, Socket, Data} -> print(ssl, Data, State), case Module:Function([Data | Args]) of {ok, Parsed} -> handle_http_msg(Parsed, State); {_, whole_body, _} when HeadRequest == "HEAD" -> State#state{body = <<>>}; NewMFA -> request(State#state{mfa = NewMFA}, TimeOut) end; {ssl_closed, Socket} when Function == whole_body -> print(ssl, "closed", State), State#state{body = hd(Args)}; {ssl_closed, Socket} -> test_server:fail(connection_closed); {ssl_error, Socket, Reason} -> test_server:fail({ssl_error, Reason}) after TimeOut -> io:format("~p ~w[~w]request -> timeout" "~n", [self(), ?MODULE, ?LINE]), test_server:fail(connection_timed_out) end. handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, State = #state{request = RequestStr}) -> io:format("~p ~w[~w]handle_http_msg -> entry with" "~n Version: ~p" "~n StatusCode: ~p" "~n ReasonPharse: ~p" "~n Headers: ~p" "~n Body: ~p" "~n", [self(), ?MODULE, ?LINE, Version, StatusCode, ReasonPharse, Headers, Body]), case is_expect(RequestStr) of true -> State#state{status_line = {Version, StatusCode, ReasonPharse}, headers = Headers}; false -> handle_http_body(Body, State#state{status_line = {Version, StatusCode, ReasonPharse}, headers = Headers}) end; handle_http_msg({ChunkedHeaders, Body}, State = #state{headers = Headers}) -> NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), State#state{headers = NewHeaders, body = Body}; handle_http_msg(Body, State) -> State#state{body = Body}. handle_http_body(<<>>, State = #state{request = "HEAD" ++ _}) -> State#state{body = <<>>}; handle_http_body(Body, State = #state{headers = Headers, max_body_size = MaxBodySize}) -> case Headers#http_response_h.'transfer-encoding' of "chunked" -> case http_chunk:decode(Body, State#state.max_body_size, State#state.max_header_size) of {Module, Function, Args} -> request(State#state{mfa = {Module, Function, Args}}, 30000); {ok, {ChunkedHeaders, NewBody}} -> NewHeaders = http_chunk:handle_headers(Headers, ChunkedHeaders), State#state{headers = NewHeaders, body = NewBody} end; _ -> Length = list_to_integer(Headers#http_response_h.'content-length'), case ((Length =< MaxBodySize) or (MaxBodySize == nolimit)) of true -> case httpc_response:whole_body(Body, Length) of {ok, NewBody} -> State#state{body = NewBody}; MFA -> request(State#state{mfa = MFA}, 5000) end; false -> test_server:fail(body_too_big) end end. validate(RequestStr, #state{status_line = {Version, StatusCode, _}, headers = Headers, body = Body}, Options, N, P) -> check_version(Version, Options), case lists:keysearch(statuscode, 1, Options) of {value, _} -> check_status_code(StatusCode, Options, Options); _ -> ok end, do_validate(http_response:header_list(Headers), Options, N, P), check_body(RequestStr, StatusCode, Headers#http_response_h.'content-type', list_to_integer(Headers#http_response_h.'content-length'), Body). %%-------------------------------------------------------------------- %% Internal functions %%------------------------------------------------------------------ check_version(Version, Options) -> case lists:keysearch(version, 1, Options) of {value, {version, Version}} -> ok; {value, {version, Ver}} -> tsf({wrong_version, [{got, Version}, {expected, Ver}]}); _ -> case Version of "HTTP/1.1" -> ok; _ -> tsf({wrong_version, [{got, Version}, {expected, "HTTP/1.1"}]}) end end. check_status_code(StatusCode, [], Options) -> tsf({wrong_status_code, [{got, StatusCode}, {expected, Options}]}); check_status_code(StatusCode, Current = [_ | Rest], Options) -> case lists:keysearch(statuscode, 1, Current) of {value, {statuscode, StatusCode}} -> ok; {value, {statuscode, _OtherStatus}} -> check_status_code(StatusCode, Rest, Options); false -> tsf({wrong_status_code, [{got, StatusCode}, {expected, Options}]}) end. do_validate(_, [], _, _) -> ok; do_validate(Header, [{statuscode, _Code} | Rest], N, P) -> do_validate(Header, Rest, N, P); do_validate(Header, [{header, HeaderField}|Rest], N, P) -> LowerHeaderField = http_util:to_lower(HeaderField), case lists:keysearch(LowerHeaderField, 1, Header) of {value, {LowerHeaderField, _Value}} -> ok; false -> test_server:fail({missing_header_field, LowerHeaderField, Header}); _ -> test_server:fail({missing_header_field, LowerHeaderField, Header}) end, do_validate(Header, Rest, N, P); do_validate(Header, [{header, HeaderField, Value}|Rest],N,P) -> LowerHeaderField = http_util:to_lower(HeaderField), case lists:keysearch(LowerHeaderField, 1, Header) of {value, {LowerHeaderField, Value}} -> ok; false -> test_server:fail({wrong_header_field_value, LowerHeaderField, Header}); _ -> test_server:fail({wrong_header_field_value, LowerHeaderField, Header}) end, do_validate(Header, Rest, N, P); do_validate(Header,[{no_last_modified, HeaderField}|Rest],N,P) -> case lists:keysearch(HeaderField,1,Header) of {value,_} -> test_server:fail({wrong_header_field_value, HeaderField, Header}); _ -> ok end, do_validate(Header, Rest, N, P); do_validate(Header, [_Unknown | Rest], N, P) -> do_validate(Header, Rest, N, P). is_expect(RequestStr) -> case inets_regexp:match(RequestStr, "xpect:100-continue") of {match, _, _}-> true; _ -> false end. %% OTP-5775, content-length check_body("GET /cgi-bin/erl/httpd_example:get_bin HTTP/1.0\r\n\r\n", 200, "text/html", Length, _Body) when (Length =/= 274) -> tsf(content_length_error); check_body("GET /cgi-bin/cgi_echo HTTP/1.0\r\n\r\n", 200, "text/plain", _, Body) -> case size(Body) of 100 -> ok; _ -> tsf(content_length_error) end; check_body(RequestStr, 200, "text/html", _, Body) -> HeadRequest = lists:sublist(RequestStr, 1, 3), case HeadRequest of "GET" -> inets_test_lib:check_body(binary_to_list(Body)); _ -> ok end; check_body(_, _, _, _,_) -> ok. print(Proto, Data, #state{print = true}) -> test_server:format("Received ~p: ~p~n", [Proto, Data]); print(_, _, #state{print = false}) -> ok. tsf(Reason) -> test_server:fail(Reason). %% tsp(F) -> %% tsp(F, []). tsp(F, A) -> Timestamp = formated_timestamp(), test_server:format("** ~s ** ~p ~p:" ++ F ++ "~n", [Timestamp, self(), ?MODULE | A]). formated_timestamp() -> format_timestamp( os:timestamp() ). format_timestamp({_N1, _N2, N3} = Now) -> {Date, Time} = calendar:now_to_datetime(Now), {YYYY,MM,DD} = Date, {Hour,Min,Sec} = Time, FormatDate = io_lib:format("~.4w:~.2.0w:~.2.0w ~.2.0w:~.2.0w:~.2.0w 4~w", [YYYY,MM,DD,Hour,Min,Sec,round(N3/1000)]), lists:flatten(FormatDate).