677 lines
23 KiB
Erlang
677 lines
23 KiB
Erlang
%%%=============================================================================
|
|
%%% @copyright (C) 1999-2013, Erlang Solutions Ltd
|
|
%%% @author Diana Corbacho <diana.corbacho@erlang-solutions.com>
|
|
%%% @doc
|
|
%%% @end
|
|
%%%=============================================================================
|
|
-module(fusco_tests_eqc).
|
|
-copyright("2013, Erlang Solutions Ltd.").
|
|
|
|
-include_lib("eqc/include/eqc.hrl").
|
|
-include("fusco.hrl").
|
|
|
|
-define(TWO_OK, <<"HTTP/1.1 200 OK\r\n\r\n">>).
|
|
-define(FOUR_BAD_REQUEST, <<"HTTP/1.1 400 Bad Request\r\n">>).
|
|
-define(TWO_OK(V), case V of
|
|
undefined ->
|
|
?TWO_OK;
|
|
_ ->
|
|
<<"HTTP/1.1 200 OK\r\nConnection: ",
|
|
V/binary,"\r\n\r\n">>
|
|
end).
|
|
|
|
-export([prop_http_request_per_family/3,
|
|
prop_persistent_connection_per_family/3,
|
|
prop_reconnect_per_family/3,
|
|
prop_client_close_connection_per_family/3,
|
|
prop_connection_refused_per_family/3,
|
|
prop_http_request_cookie_path/3,
|
|
prop_http_request_supersede_cookie/3,
|
|
prop_http_request_max_age/3,
|
|
prop_http_request_expires/3]).
|
|
|
|
%%==============================================================================
|
|
%% Quickcheck generators
|
|
%%==============================================================================
|
|
valid_http_request() ->
|
|
?LET({RequestLine, Headers},
|
|
{http_eqc_gen:request_line(), http_eqc_gen:request_headers()},
|
|
?LET(Body, http_eqc_encoding:body(any),
|
|
{RequestLine, http_eqc_encoding:add_content_length(Headers, Body),
|
|
Body})).
|
|
|
|
valid_http_response() ->
|
|
?LET({StatusLine, Headers},
|
|
{status_line(), http_eqc_gen:headers()},
|
|
?LET(Body, http_eqc_encoding:body(StatusLine),
|
|
{StatusLine, Headers, Body}
|
|
)
|
|
).
|
|
|
|
status_line() ->
|
|
%% Discard CONTINUE for cookie testing, client waits for next messages
|
|
?SUCHTHAT({_, S, _}, http_eqc_gen:status_line(),
|
|
not lists:member(S, [<<"100">>, <<"101">>])).
|
|
|
|
token() ->
|
|
non_empty(list(choose($A, $z))).
|
|
|
|
path() ->
|
|
?LET({Path, Slash}, {non_empty(list(token())), slash()},
|
|
Path ++ Slash).
|
|
|
|
domain() ->
|
|
?LET(Domain, path(), list_to_binary(string:join(Domain, "."))).
|
|
|
|
slash() ->
|
|
oneof([["/"], []]).
|
|
|
|
subpath(Path, true) ->
|
|
?LET({Length, Slash}, {choose(1, length(Path)), slash()},
|
|
begin
|
|
{H, _} = lists:split(Length, Path),
|
|
case lists:last(H) of
|
|
"/" ->
|
|
H;
|
|
_ ->
|
|
H ++ Slash
|
|
end
|
|
end);
|
|
subpath(Path, false) ->
|
|
?SUCHTHAT(SubPath, path(), hd(SubPath) =/= hd(Path)).
|
|
|
|
max_age() ->
|
|
%% Make cookie expire on nat/0 and not expire on largeint/0
|
|
%% Otherwise, in black box testing we lose the control to make cookie
|
|
%% expire on values near the current time. It needs unit testing
|
|
%% to verify the expiration is precise.
|
|
?LET({Expires, MaxAge},
|
|
oneof([{true, nat()}, {false, largeint()}]),
|
|
case MaxAge of
|
|
0 ->
|
|
{true, MaxAge};
|
|
_ ->
|
|
{Expires, abs(MaxAge)}
|
|
end).
|
|
|
|
past() ->
|
|
?SUCHTHAT(Date, http_eqc_gen:sane_cookie_date(), is_past(Date)).
|
|
|
|
future() ->
|
|
?SUCHTHAT(Date, http_eqc_gen:sane_cookie_date(), is_future(Date)).
|
|
|
|
expires() ->
|
|
oneof([{true, past()}, {false, future()}]).
|
|
|
|
set_cookie(Path) ->
|
|
{<<"Set-Cookie">>, {http_eqc_gen:cookie_pair(),
|
|
[{<<"Path">>, encode_path(Path)}]}}.
|
|
|
|
set_cookie(Path, MaxAge) when is_integer(MaxAge) ->
|
|
{<<"Set-Cookie">>, {http_eqc_gen:cookie_pair(),
|
|
[{<<"Path">>, encode_path(Path)},
|
|
{<<"Max-Age">>, integer_to_binary(MaxAge)}]}};
|
|
set_cookie(Path, Expires) ->
|
|
{<<"Set-Cookie">>,
|
|
{http_eqc_gen:cookie_pair(),
|
|
[{<<"Path">>, encode_path(Path)},
|
|
{<<"Expires">>, Expires}]}}.
|
|
|
|
set_cookie(Path, Domain, Name, Value) ->
|
|
{<<"Set-Cookie">>, {{Name, Value}, [{<<"Path">>, encode_path(Path)},
|
|
{<<"Domain">>, Domain}]}}.
|
|
|
|
cookie_path() ->
|
|
%% Cookie rejected if the value for the Path attribute
|
|
%% is not a prefix of the requested-URI
|
|
?LET({Path, IsSubPath}, {path(), bool()},
|
|
?LET(SubPath, subpath(Path, IsSubPath),
|
|
?LET(Cookie, set_cookie(SubPath),
|
|
{Cookie, encode_path(Path), IsSubPath}
|
|
)
|
|
)
|
|
).
|
|
|
|
cookie_max_age() ->
|
|
%% Path is needed in the test setup to ensure cookie is not rejected
|
|
?LET({Path, {Expires, MaxAge}}, {path(), max_age()},
|
|
?LET(Cookie, set_cookie(Path, MaxAge),
|
|
{Cookie, encode_path(Path), Expires, MaxAge}
|
|
)
|
|
).
|
|
|
|
cookie_expires() ->
|
|
%% Path is needed in the test setup to ensure cookie is not rejected
|
|
?LET({Path, {Expires, Date}}, {path(), expires()},
|
|
?LET(Cookie, set_cookie(Path, Date),
|
|
{Cookie, encode_path(Path), Expires}
|
|
)
|
|
).
|
|
|
|
maybe_different_cookie_data(true, Path, Domain, Name) ->
|
|
{Path, Domain, Name};
|
|
maybe_different_cookie_data(false, Path, Domain, Name) ->
|
|
%% Generates a combination of 1 or more mutations on Path, Domain and Name
|
|
?LET([ChangePath, ChangeDomain, ChangeName],
|
|
?SUCHTHAT(V, vector(3, bool()),
|
|
lists:any(fun(X) -> X == true end, V)),
|
|
{change_path(ChangePath, Path),
|
|
change_domain(ChangeDomain, Domain),
|
|
change_name(ChangeName, Name)}
|
|
).
|
|
|
|
change_path(false, Path) ->
|
|
Path;
|
|
change_path(true, Path) ->
|
|
?SUCHTHAT(P, path(), hd(P) =/= hd(Path)).
|
|
|
|
change_domain(false, Domain) ->
|
|
Domain;
|
|
change_domain(true, Domain) ->
|
|
?SUCHTHAT(D, domain(), D =/= Domain).
|
|
|
|
change_name(false, Name) ->
|
|
Name;
|
|
change_name(true, Name) ->
|
|
?SUCHTHAT(N, http_eqc_gen:small_valid_bin(), N =/= Name).
|
|
|
|
%%==============================================================================
|
|
%% Quickcheck properties
|
|
%%==============================================================================
|
|
prop_http_request_per_family(Host, Family, Ssl) ->
|
|
eqc:numtests(
|
|
500,
|
|
?FORALL({{Method, Uri, _Version}, Headers, Body} = Msg,
|
|
valid_http_request(),
|
|
begin
|
|
Module = select_module(Ssl),
|
|
{ok, Listener, LS, Port} =
|
|
webserver:start(Module, [validate_msg(Msg)], Family),
|
|
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
|
|
{ok, {Status, _, _, _, _}}
|
|
= fusco:request(Client, Uri, Method, Headers, Body, 10000),
|
|
ok = fusco:disconnect(Client),
|
|
webserver:stop(Module, Listener, LS),
|
|
Expected = {<<"200">>, <<"OK">>},
|
|
?WHENFAIL(io:format("Status: ~p~nExpected: ~p~n",
|
|
[Status, Expected]),
|
|
case Status of
|
|
Expected ->
|
|
true;
|
|
_ ->
|
|
false
|
|
end)
|
|
end)).
|
|
|
|
prop_persistent_connection_per_family(Host, Family, Ssl) ->
|
|
%% Fusco must keep the connection alive and be able to reconnect
|
|
%% Individual properties defined for reconnect and keep-alive
|
|
?FORALL(
|
|
Msgs,
|
|
non_empty(list({valid_http_request(), http_eqc_gen:connection_header()})),
|
|
begin
|
|
Module = select_module(Ssl),
|
|
{ok, Listener, LS, Port} =
|
|
webserver:start(Module,
|
|
[reply_msg(?TWO_OK(ConHeader)) || {_, ConHeader} <- Msgs],
|
|
Family),
|
|
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
|
|
Replies = lists:map(fun({{{Method, Uri, _Version}, Headers, Body}, _}) ->
|
|
fusco:request(Client, Uri, Method, Headers, Body, 10000)
|
|
end, Msgs),
|
|
ok = fusco:disconnect(Client),
|
|
webserver:stop(Module, Listener, LS),
|
|
?WHENFAIL(io:format("Replies: ~p~nExpected: 200 OK~n", [Replies]),
|
|
lists:all(fun({ok, {{<<"200">>, <<"OK">>}, _, _, _, _}}) ->
|
|
true;
|
|
(_) ->
|
|
false
|
|
end, Replies))
|
|
end).
|
|
|
|
prop_reconnect_per_family(Host, Family, Ssl) ->
|
|
%% Connection is always closed in the server and fusco must reconnect
|
|
eqc:numtests(
|
|
50,
|
|
?FORALL(
|
|
Msgs,
|
|
non_empty(list(valid_http_request())),
|
|
begin
|
|
Module = select_module(Ssl),
|
|
{ok, Listener, LS, Port} =
|
|
webserver:start(Module,
|
|
[reply_and_close_msg(?TWO_OK) || _ <- Msgs],
|
|
Family),
|
|
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
|
|
Replies = lists:map(fun({{Method, Uri, _Version}, Headers, Body}) ->
|
|
Hdrs = lists:keydelete(<<"Connection">>, 1, Headers),
|
|
fusco:request(Client, Uri, Method, Hdrs, Body, 10000)
|
|
end, Msgs),
|
|
ok = fusco:disconnect(Client),
|
|
webserver:stop(Module, Listener, LS),
|
|
?WHENFAIL(io:format("Replies: ~p~nExpected: 200 OK~n", [Replies]),
|
|
lists:all(fun({ok, {{<<"200">>, <<"OK">>}, _, _, _, _}}) ->
|
|
true;
|
|
(_) ->
|
|
false
|
|
end, Replies))
|
|
end)).
|
|
|
|
prop_client_close_connection_per_family(Host, Family, Ssl) ->
|
|
%% Fusco must close the connection if requested by the server
|
|
eqc:numtests(
|
|
25,
|
|
?FORALL({{{Method, Uri, _Version}, Headers, Body}, Connection},
|
|
{valid_http_request(), http_eqc_gen:connection_header()},
|
|
begin
|
|
Id = erlang:now(),
|
|
Module = select_module(Ssl),
|
|
{ok, Listener, LS, Port} =
|
|
webserver:start(Module,
|
|
[reply_msg_and_check(Id, ?TWO_OK(Connection))],
|
|
Family),
|
|
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
|
|
{ok, {Status, _, _, _, _}}
|
|
= fusco:request(Client, Uri, Method, Headers, Body, 10000),
|
|
Closed = receive
|
|
{Id, closed} ->
|
|
true
|
|
after 1000 ->
|
|
false
|
|
end,
|
|
ok = fusco:disconnect(Client),
|
|
webserver:stop(Module, Listener, LS),
|
|
Expected = {<<"200">>, <<"OK">>},
|
|
MustClose = must_close(Headers, Connection),
|
|
?WHENFAIL(io:format("Connection: ~p~nStatus: ~p~nExpected:"
|
|
" ~p~nMust close: ~p~nClosed: ~p~n",
|
|
[Connection, Status, Expected, MustClose, Closed]),
|
|
case Status of
|
|
Expected ->
|
|
MustClose == Closed;
|
|
_ ->
|
|
false
|
|
end)
|
|
end)).
|
|
|
|
prop_connection_refused_per_family(Host, Family, Ssl) ->
|
|
eqc:numtests(1,
|
|
begin
|
|
Module = select_module(Ssl),
|
|
{ok, Listener, LS, Port} =
|
|
webserver:start(Module, [reply_msg(<<>>)], Family),
|
|
webserver:stop(Module, Listener, LS),
|
|
{ok, Client} = fusco:start({Host, Port, Ssl}, []),
|
|
Reply = fusco:connect(Client),
|
|
Expected = {error, econnrefused},
|
|
?WHENFAIL(io:format("Reply: ~p~nExpected: ~p~n",
|
|
[Reply, Expected]),
|
|
case Reply of
|
|
Expected ->
|
|
true;
|
|
_ ->
|
|
false
|
|
end)
|
|
end).
|
|
|
|
%% Cookie rejected if the value for the Path attribute
|
|
%% is not a prefix of the requested-URI
|
|
prop_http_request_cookie_path(Host, Family, Ssl) ->
|
|
?FORALL(
|
|
{Request, {Cookie, Path, IsSubPath},
|
|
{{_, Status, Reason}, _, _} = Response},
|
|
{valid_http_request(), cookie_path(), valid_http_response()},
|
|
begin
|
|
ResponseBin = build_response(Response, [Cookie]),
|
|
ValidationFun = validate_cookie(ResponseBin, fun check_cookie_deleted/3,
|
|
[not IsSubPath, Cookie]),
|
|
Responses =
|
|
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path, Request,
|
|
[<<"first">>, <<"second">>], 0),
|
|
check_responses(Status, Reason, Responses)
|
|
end
|
|
).
|
|
|
|
%% Supersed old cookie if Name is the same as existing cookie,
|
|
%% and Domain and Path exactly match pre-existing ones
|
|
prop_http_request_supersede_cookie(Host, Family, Ssl) ->
|
|
?FORALL(
|
|
{Request, Path, Domain, {Name, Value}, Supersede,
|
|
{{_, Status, Reason}, _, _} = Response},
|
|
{valid_http_request(), path(), domain(), http_eqc_gen:cookie_pair(),
|
|
bool(), valid_http_response()},
|
|
%% Generate a second cookie that could supersed or not the previous one
|
|
%% Uses 'Supersede' as generation parameter
|
|
?LET(
|
|
{{SPath, SDomain, SName}, SValue},
|
|
{maybe_different_cookie_data(Supersede, Path, Domain, Name),
|
|
?SUCHTHAT(V, http_eqc_gen:small_valid_bin(), V =/= Value)},
|
|
begin
|
|
FirstCookie = set_cookie(Path, Domain, Name, Value),
|
|
SecondCookie = set_cookie(SPath, SDomain, SName, SValue),
|
|
FirstServerResponse = build_response(Response, [FirstCookie]),
|
|
SecondServerResponse = build_response(Response, [SecondCookie]),
|
|
ValidationFun = validate_cookie_supersede(
|
|
FirstServerResponse, SecondServerResponse,
|
|
Supersede, {Name, Value, encode_path(Path), Domain},
|
|
{SName, SValue, encode_path(SPath), SDomain}),
|
|
%% Three requests to supersede the value
|
|
%% First get cookie
|
|
%% Second get second cookie
|
|
%% Third checks received cookies
|
|
Responses =
|
|
send_cookie_requests(Host, Ssl, Family, ValidationFun,
|
|
encode_path(Path), Request,
|
|
[<<"first">>, <<"second">>, <<"third">>], 0),
|
|
check_responses(Status, Reason, Responses)
|
|
end
|
|
)
|
|
).
|
|
|
|
prop_http_request_max_age(Host, Family, Ssl) ->
|
|
eqc:numtests(
|
|
25,
|
|
?FORALL(
|
|
{Request, {Cookie, Path, Expires, MaxAge},
|
|
{{_, Status, Reason}, _, _} = Response},
|
|
{valid_http_request(), cookie_max_age(), valid_http_response()},
|
|
begin
|
|
ResponseBin = build_response(Response, [Cookie]),
|
|
ValidationFun = validate_cookie(ResponseBin, fun check_cookie_deleted/3,
|
|
[Expires, Cookie]),
|
|
WaitTime = expiration_time(Expires, MaxAge),
|
|
Responses =
|
|
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path,
|
|
Request, [<<"first">>, <<"second">>],
|
|
WaitTime),
|
|
check_responses(Status, Reason, Responses)
|
|
end
|
|
)
|
|
).
|
|
|
|
prop_http_request_expires(Host, Family, Ssl) ->
|
|
?FORALL(
|
|
{Request, {Cookie, Path, Expires},
|
|
{{_, Status, Reason}, _, _} = Response},
|
|
{valid_http_request(), cookie_expires(), valid_http_response()},
|
|
begin
|
|
ResponseBin = build_response(Response, [Cookie]),
|
|
ValidationFun = validate_cookie(ResponseBin, fun check_cookie_deleted/3,
|
|
[Expires, Cookie]),
|
|
Responses =
|
|
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path,
|
|
Request, [<<"first">>, <<"second">>], 0),
|
|
check_responses(Status, Reason, Responses)
|
|
end
|
|
).
|
|
%%==============================================================================
|
|
%% Internal functions
|
|
%%==============================================================================
|
|
validate_msg({{_Method, _Uri, _Version}, SentHeaders, SentBody}) ->
|
|
fun(Module, Socket, _Request, GotHeaders, GotBody) when SentBody == GotBody ->
|
|
case validate_headers(SentBody, GotHeaders, SentHeaders) of
|
|
true ->
|
|
Module:send(Socket, ?TWO_OK);
|
|
false ->
|
|
Module:send(Socket, ?FOUR_BAD_REQUEST)
|
|
end;
|
|
(Module, Socket, _Request, _GotHeaders, _) ->
|
|
Module:send(Socket, ?FOUR_BAD_REQUEST)
|
|
end.
|
|
|
|
validate_cookie(Response, Fun, Params) ->
|
|
fun(Module, Socket, _Request, Headers, _Body) ->
|
|
case proplists:get_value("Test", Headers) of
|
|
"first" ->
|
|
Module:send(Socket, Response);
|
|
"second" ->
|
|
case erlang:apply(Fun, [Headers | Params]) of
|
|
true ->
|
|
Module:send(Socket, ?TWO_OK);
|
|
false ->
|
|
Module:send(Socket, ?FOUR_BAD_REQUEST)
|
|
end
|
|
end
|
|
end.
|
|
|
|
validate_cookie_supersede(FirstResponse, SecondResponse, Supersede, FirstPair,
|
|
SecondPair) ->
|
|
fun(Module, Socket, _Request, Headers, _Body) ->
|
|
case proplists:get_value("Test", Headers) of
|
|
"first" ->
|
|
Module:send(Socket, FirstResponse);
|
|
"second" ->
|
|
Module:send(Socket, SecondResponse);
|
|
"third" ->
|
|
case check_cookie_supersede(Headers, Supersede, FirstPair, SecondPair) of
|
|
true ->
|
|
Module:send(Socket, ?TWO_OK);
|
|
false ->
|
|
Module:send(Socket, ?FOUR_BAD_REQUEST)
|
|
end
|
|
end
|
|
end.
|
|
|
|
build_cookie(N, V) ->
|
|
binary_to_list(<<N/binary,"=",V/binary>>).
|
|
|
|
check_cookie_deleted(Headers, true, _) ->
|
|
undefined == proplists:get_value("Cookie", Headers);
|
|
check_cookie_deleted(Headers, false, {_, {{N, V}, _}}) ->
|
|
build_cookie(N, V) == proplists:get_value("Cookie", Headers).
|
|
|
|
%% http://tools.ietf.org/search/rfc6265#section-4.1.2
|
|
check_cookie_supersede(Headers, true, {Name, _, _, _}, {_, NewValue, _, _}) ->
|
|
build_cookie(Name, NewValue) == proplists:get_value("Cookie", Headers);
|
|
check_cookie_supersede(Headers, false, {Name, Value, Path, _},
|
|
{NewName, NewValue, Path, _}) ->
|
|
lists:sort([build_cookie(Name, Value), build_cookie(NewName, NewValue)])
|
|
== lists:sort(string:tokens(proplists:get_value("Cookie", Headers), "; "));
|
|
check_cookie_supersede(Headers, false, {Name, Value, _, _}, _) ->
|
|
build_cookie(Name, Value)
|
|
== proplists:get_value("Cookie", Headers).
|
|
|
|
verify_host(GotHeaders, SentHeaders) ->
|
|
%% Host must be added by the client if it is not part of the headers list
|
|
%% http://tools.ietf.org/html/rfc2616#section-14.23
|
|
Key = "host",
|
|
case lists:keytake(Key, 1, GotHeaders) of
|
|
{value, {_, Value}, NewGotHeaders} ->
|
|
case lists:keytake(Key, 1, SentHeaders) of
|
|
%% The user sent the 'Host' header, value must match
|
|
{value, {_, Value}, NewSentHeaders} ->
|
|
{NewGotHeaders, NewSentHeaders};
|
|
false ->
|
|
{NewGotHeaders, SentHeaders};
|
|
_ ->
|
|
false
|
|
end;
|
|
false ->
|
|
false
|
|
end.
|
|
|
|
verify_content_length(Body, GotHeaders, SentHeaders) ->
|
|
%% Must be updated when the code supports transfer-encoding
|
|
%% http://tools.ietf.org/html/rfc2616#section-14.13
|
|
Key = "content-length",
|
|
ContentLength = iolist_size(Body),
|
|
{NewGotHeaders, GotContentLength}
|
|
= case lists:keytake(Key, 1, GotHeaders) of
|
|
{value, {_, Value}, H} ->
|
|
{H, list_to_integer(Value)};
|
|
false ->
|
|
{GotHeaders, 0}
|
|
end,
|
|
case ContentLength == GotContentLength of
|
|
true ->
|
|
{NewGotHeaders, lists:keydelete(Key, 1, SentHeaders)};
|
|
false ->
|
|
false
|
|
end.
|
|
|
|
validate_headers(Body, GotHeaders, SentHeaders) ->
|
|
CleanGotHeaders = lists:keysort(1, [{string:to_lower(K), V}
|
|
|| {K, V} <- GotHeaders]),
|
|
CleanSentHeaders = lists:keysort(1, [{string:to_lower(binary_to_list(K)),
|
|
binary_to_list(V)}
|
|
|| {K, V} <- SentHeaders]),
|
|
case verify_host(CleanGotHeaders, CleanSentHeaders) of
|
|
false ->
|
|
false;
|
|
{GotHeaders1, Headers1} ->
|
|
case verify_content_length(Body, GotHeaders1, Headers1) of
|
|
false ->
|
|
false;
|
|
{GotHeaders2, Headers2} ->
|
|
GotHeaders2 == Headers2
|
|
end
|
|
end.
|
|
|
|
reply_msg(Msg) ->
|
|
fun(Module, Socket, _Request, _Headers, _Body) ->
|
|
Module:send(Socket, Msg)
|
|
end.
|
|
|
|
reply_msg_and_check(Id, Msg) ->
|
|
Parent = self(),
|
|
fun(Module, Socket, _Request, _Headers, _Body) ->
|
|
Module:send(Socket, Msg),
|
|
case Module:recv(Socket, 0) of
|
|
{error, closed} ->
|
|
Parent ! {Id, closed};
|
|
_ ->
|
|
ok
|
|
end
|
|
end.
|
|
|
|
reply_and_close_msg(Msg) ->
|
|
fun(Module, Socket, _Request, _Headers, _Body) ->
|
|
Module:send(Socket, Msg),
|
|
Module:close(Socket)
|
|
end.
|
|
|
|
must_close(Headers, Connection) ->
|
|
case proplists:get_value(<<"Connection">>, Headers) of
|
|
<<"close">> ->
|
|
true;
|
|
_ ->
|
|
case Connection of
|
|
<<"close">> ->
|
|
true;
|
|
_ ->
|
|
false
|
|
end
|
|
end.
|
|
|
|
select_module(Ssl) ->
|
|
case Ssl of
|
|
true ->
|
|
ssl;
|
|
false ->
|
|
gen_tcp
|
|
end.
|
|
|
|
encode_path(Path) ->
|
|
case lists:split(length(Path) - 1, Path) of
|
|
{P, ["/"]} ->
|
|
list_to_binary(["/", string:join(P, "/"), "/"]);
|
|
_ ->
|
|
list_to_binary(["/", string:join(Path, "/")])
|
|
end.
|
|
|
|
build_response({StatusLine, Headers, Body}, Cookies) ->
|
|
http_eqc_encoding:build_valid_response(
|
|
StatusLine,
|
|
http_eqc_encoding:add_content_length(Headers, Body),
|
|
Cookies, Body).
|
|
|
|
send_cookie_requests(Host, Ssl, Family, ValidationFun, Path,
|
|
{{Method, _Uri, _Version}, Headers, Body},
|
|
RequestTags, WaitTime) ->
|
|
Module = select_module(Ssl),
|
|
{ok, Listener, LS, Port} =
|
|
webserver:start(Module, [ValidationFun], Family),
|
|
{ok, Client} = fusco:start({Host, Port, Ssl}, [{use_cookies, true}]),
|
|
%% Use header "test" to distinguish requests in the server side
|
|
Responses = lists:map(
|
|
fun(Header) ->
|
|
{ok, {Response, _, _, _, _}}
|
|
= fusco:request(Client, Path, Method,
|
|
[{<<"test">>, Header} | Headers],
|
|
Body, 10000),
|
|
timer:sleep(WaitTime),
|
|
Response
|
|
end, RequestTags),
|
|
ok = fusco:disconnect(Client),
|
|
webserver:stop(Module, Listener, LS),
|
|
Responses.
|
|
|
|
expiration_time(Expires, MaxAge) ->
|
|
case Expires of
|
|
true ->
|
|
MaxAge*1000;
|
|
false ->
|
|
0
|
|
end.
|
|
|
|
check_responses(Status, Reason, [First, Second]) ->
|
|
Expected = {Status, Reason},
|
|
?WHENFAIL(io:format("First: ~p~nExpected: ~p~nSecond: ~p~n",
|
|
[First, Expected, Second]),
|
|
{First, Second} == {Expected, {<<"200">>, <<"OK">>}}
|
|
);
|
|
check_responses(Status, Reason, [First, Second, Third]) ->
|
|
Expected = {Status, Reason},
|
|
?WHENFAIL(io:format("First: ~p~nSecond: ~p~nExpected: ~p~nThird ~p~n",
|
|
[First, Second, Expected, Third]),
|
|
{First, Second, Third}
|
|
== {Expected, Expected, {<<"200">>, <<"OK">>}}
|
|
).
|
|
|
|
diff(A, A) ->
|
|
eq;
|
|
diff(A, B) when A < B ->
|
|
lt;
|
|
diff(_, _) ->
|
|
gt.
|
|
|
|
is_past(DateTime) ->
|
|
{Date, {H, M, _}} = http_eqc_encoding:expires_datetime(DateTime),
|
|
case diff(Date, date()) of
|
|
lt ->
|
|
true;
|
|
gt ->
|
|
false;
|
|
eq ->
|
|
{HH, MM, _} = time(),
|
|
%% Set 30 min margin, safe side
|
|
case diff((HH*60+MM) - (H*60+M), 30) of
|
|
gt ->
|
|
true;
|
|
eq ->
|
|
true;
|
|
lt ->
|
|
false
|
|
end
|
|
end.
|
|
|
|
is_future(DateTime) ->
|
|
{Date, {H, M, _}} = http_eqc_encoding:expires_datetime(DateTime),
|
|
case diff(Date, date()) of
|
|
gt ->
|
|
true;
|
|
lt ->
|
|
false;
|
|
eq ->
|
|
{HH, MM, _} = time(),
|
|
%% Set 30 min margin, safe side
|
|
case diff((H*60+M) - (HH*60+MM), 30) of
|
|
gt ->
|
|
true;
|
|
eq ->
|
|
true;
|
|
lt ->
|
|
false
|
|
end
|
|
end.
|