aboutsummaryrefslogblamecommitdiffstats
path: root/lib/inets/test/httpd_test_lib.erl
blob: b6525037b2b73cd6988a0dbf59386804beff935e (plain) (tree)
1
2
3
4
5
6
7
8
9


                   
                                                        
   


                                                                   
  






                                                                           
  







                               

                                                                           
















































                                                                              

 




                                                                           
 
                                                                    

                                                                             

                                                                              

                                                                                         

                                                                           

                                                                                   
                                                                                         
                                                                         
                       
                                                                     
                                                                             

                                     
                                





                                                                   

                                    
                                     
                                                                            
                                                             
                                   
                
 
                       

                                                           

              




                                                                            

        







































                                                                                                

                                                                          
            


                                                  


                                                    
                                                                 



                                                               
                                                            

                                          
                                                   
                                      
                                             
                              


                                                    
                                                                 



                                                               
                                                             

                                         
                                                   
                                      













                                                                  
                    
                                                
                                                           
                                             

        








                                                                                  




















































                                                                           
                                          



                                                                   

                                                                  
    






                                                            










                                                                                 
 
                                                                     






                                                                    

                                                        
            


                             
                   


                                                                      


                                             
                                                                           






                                                               
                                                                                  











                                                          
                                                                      
            
                                                                     







                                                               
                                                                                 
            
                                                                                

                                    
                                                          

                                                 
                                                                     







                                               

                                                                       
                 
                  



                           
                                                                                                                               
                                  





                                                                        
                                         













                                                            
%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2001-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%
%%
%%
-module(httpd_test_lib).

-include("inets_test_lib.hrl").

%% Poll functions
-export([verify_request/6, verify_request/7, verify_request/8, is_expect/1,
	 verify_request_N/9]).

-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, TranspOpts, Node, RequestStr, Options) 
  when is_list(TranspOpts) ->
    verify_request(SocketType, Host, Port, TranspOpts, Node, RequestStr, Options, 30000);

verify_request(SocketType, Host, Port, Node, RequestStr, Options, TimeOut) 
  when (is_integer(TimeOut) orelse (TimeOut =:= infinity)) ->
    verify_request(SocketType, Host, Port, [], Node, RequestStr, Options, TimeOut).

verify_request(SocketType, Host, Port, TranspOpts, Node, RequestStr, Options, TimeOut) ->
    try inets_test_lib:connect_bin(SocketType, Host, Port, TranspOpts) of
	{ok, Socket} ->
	    ok = inets_test_lib:send(SocketType, Socket, RequestStr),
	    State = case re:run(RequestStr, "printenv", [{capture, none}]) of
			nomatch ->
			    #state{};
			match ->
			    #state{print = true}
		    end,
	    
	    case request(State#state{request = RequestStr, 
				     socket  = Socket}, TimeOut) of
		{error, Reason} ->
		    {error, Reason};
		NewState ->
		    ValidateResult = 
		   	validate(RequestStr, NewState, Options, Node, Port),
		    inets_test_lib:close(SocketType, Socket),
		    ValidateResult 
	    end;

	ConnectError ->
	    ct:fail({connect_error, ConnectError, 
		     [SocketType, Host, Port, TranspOpts]})
    catch
	T:E ->
	    ct:fail({connect_failure, 
		     [{type,       T}, 
		      {error,      E}, 
		      {stacktrace, erlang:get_stacktrace()}, 
		      {args,       [SocketType, Host, Port, TranspOpts]}]}) 
    end.

verify_request_N(SocketType, Host, Port, TranspOpts, Node, RequestStr, Options, TimeOut, N) ->
    State = #state{},
     try inets_test_lib:connect_bin(SocketType, Host, Port, TranspOpts) of
	{ok, Socket} ->
	     request_N(SocketType, Socket, RequestStr, Options, TimeOut, Node, Port, State, N); 
	ConnectError ->
	    ct:fail({connect_error, ConnectError, 
		     [SocketType, Host, Port, TranspOpts]})
    catch
	T:E ->
	    ct:fail({connect_failure, 
		     [{type,       T}, 
		      {error,      E}, 
		      {stacktrace, erlang:get_stacktrace()}, 
		      {args,       [SocketType, Host, Port, TranspOpts]}]}) 
    end.

request_N(SocketType, Socket, RequestStr, Options, TimeOut, Node, Port, State, 0) ->
    ok = inets_test_lib:send(SocketType, Socket, RequestStr),
    case request(State#state{request = RequestStr, 
			     socket  = Socket}, TimeOut) of
	{error, Reason} ->
	    {error, Reason};	
	NewState ->
	    ValidateResult = 
		validate(RequestStr, NewState, Options, Node, Port),
	    inets_test_lib:close(SocketType, Socket),
	    ValidateResult
    end;
request_N(SocketType, Socket, RequestStr, Options, TimeOut, Node, Port, State, N) ->
    ok = inets_test_lib:send(SocketType, Socket, RequestStr),
    case request(State#state{request = RequestStr, 
			     socket  = Socket}, TimeOut) of
	{error, Reason} ->
	    {error, Reason};
	_NewState ->
	    request_N(SocketType, Socket, RequestStr, Options, TimeOut, Node, Port, 
		      #state{}, N-1)
    end.

request(#state{mfa = {Module, Function, Args}, 
	       request = RequestStr, socket = Socket} = State, TimeOut) ->
            
    HeadRequest = lists:sublist(RequestStr, 1, 4),
    receive 
	{tcp, Socket, Data} ->
	    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 ->
	    State#state{body = hd(Args)}; 
	{tcp_closed, Socket} ->
	    exit({test_failed, connection_closed});
	{tcp_error, Socket, Reason} ->
	    ct:fail({tcp_error, Reason});    
	{ssl, Socket, Data} ->
	    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 ->
	    State#state{body = hd(Args)};
	{ssl_closed, Socket} ->
	    exit({test_failed, connection_closed});
	{ssl_error, Socket, Reason} ->
	    ct:fail({ssl_error, Reason});
	{Socket, {data, Data}} when is_port(Socket) ->
	    case Module:Function([list_to_binary(Data) | Args]) of
		{ok, Parsed} ->
		    port_handle_http_msg(Parsed, State); 
		{_, whole_body, _} when HeadRequest =:= "HEAD" ->
		    State#state{body = <<>>}; 
		NewMFA ->
		    request(State#state{mfa = NewMFA}, TimeOut)
	    end;
	{Socket, closed}  when Function =:= whole_body -> 
	    State#state{body = hd(Args)};
	{Socket, closed} ->
	    exit({test_failed, connection_closed})
    after TimeOut ->
	    ct:pal("~p ~w[~w]request -> timeout"
		   "~p~n", [self(), ?MODULE, ?LINE, Args]),
	    ct:fail(connection_timed_out)    
    end.


port_handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, State) ->
    State#state{status_line = {Version, 
			       StatusCode,
			       ReasonPharse},
		headers = Headers,
		body = Body}.


handle_http_msg({Version, StatusCode, ReasonPharse, Headers, Body}, 
		State = #state{request = RequestStr}) ->
    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 ->
		     ct: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,
    HList = http_response:header_list(Headers),
    do_validate(HList, Options, N, P),
    case lists:keysearch("warning", 1, HList) of
	{value, _} ->
	    ok;
	_ ->
	    check_body(RequestStr, StatusCode, 
		       Headers#http_response_h.'content-type',
		       list_to_integer(Headers#http_response_h.'content-length'),
		       Body)
    end.

%--------------------------------------------------------------------
%% Internal functions
%%------------------------------------------------------------------
check_version(Version, Options) ->
    case lists:keysearch(version, 1, Options) of
	{value, {version, Version}} ->
	    	   ok;
	{value, {version, Ver}} ->
	    ct:fail({wrong_version, [{got, Version},
				     {expected, Ver}]});
	_ ->
	    case Version of
		"HTTP/1.1" ->
		    ok;
	       _ ->
		    ct:fail({wrong_version, [{got,      Version}, 
					     {expected, "HTTP/1.1"}]})
	    end
    end.

check_status_code(StatusCode, [], Options) ->
    ct:fail({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 ->
	    ct:fail({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 ->
	    ct:fail({missing_header_field, LowerHeaderField, Header});
	_ ->
	    ct: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 ->
	    ct:fail({wrong_header_field_value, LowerHeaderField, Header, Value});
	_ ->
	    ct:fail({wrong_header_field_value, LowerHeaderField, Header, Value})
    end,
    do_validate(Header, Rest, N, P);
do_validate(Header,[{no_header, HeaderField}|Rest],N,P) ->
    case lists:keysearch(HeaderField,1,Header) of
	{value,_} ->
	    ct: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 re:run(RequestStr, "xpect:100-continue", [{capture, none}]) of
	match->
	    true;
	nomatch ->
	    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) ->
    ct:fail(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;
	_ ->
	    ct:fail(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.