diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/gun_test.erl | 4 | ||||
-rw-r--r-- | test/handlers/cookie_echo_h.erl | 11 | ||||
-rw-r--r-- | test/handlers/cookie_parser_h.erl | 23 | ||||
-rw-r--r-- | test/handlers/cookie_parser_result_h.erl | 25 | ||||
-rw-r--r-- | test/handlers/cookie_set_h.erl | 36 | ||||
-rw-r--r-- | test/handlers/ws_cookie_h.erl | 24 | ||||
-rw-r--r-- | test/rfc6265bis_SUITE.erl | 499 |
7 files changed, 622 insertions, 0 deletions
diff --git a/test/gun_test.erl b/test/gun_test.erl index a263335..bb162f4 100644 --- a/test/gun_test.erl +++ b/test/gun_test.erl @@ -22,6 +22,10 @@ %% Cowboy listeners. +init_cowboy_tcp(Ref, ProtoOpts, Config) -> + {ok, _} = cowboy:start_clear(Ref, [{port, 0}], ProtoOpts), + [{ref, Ref}, {port, ranch:get_port(Ref)}|Config]. + init_cowboy_tls(Ref, ProtoOpts, Config) -> Opts = ct_helper:get_certs_from_ets(), {ok, _} = cowboy:start_tls(Ref, Opts ++ [{port, 0}], ProtoOpts), diff --git a/test/handlers/cookie_echo_h.erl b/test/handlers/cookie_echo_h.erl new file mode 100644 index 0000000..28c5dde --- /dev/null +++ b/test/handlers/cookie_echo_h.erl @@ -0,0 +1,11 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_echo_h). + +-export([init/2]). + +init(Req, State) -> + {ok, cowboy_req:reply(200, + #{<<"content-type">> => <<"text/plain">>}, + cowboy_req:header(<<"cookie">>, Req, <<"UNDEF">>), + Req), State}. diff --git a/test/handlers/cookie_parser_h.erl b/test/handlers/cookie_parser_h.erl new file mode 100644 index 0000000..cff5901 --- /dev/null +++ b/test/handlers/cookie_parser_h.erl @@ -0,0 +1,23 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_parser_h). + +-export([init/2]). + +init(Req0=#{qs := Qs}, State) -> + %% Hardcoded path, but I doubt it's going to break anytime soon. + TestFile = iolist_to_binary(["../../test/wpt/cookies/", Qs, "-test"]), + {ok, Test} = file:read_file(TestFile), + %% We don't want the final empty line. + Lines = lists:reverse(tl(lists:reverse(string:split(Test, <<"\n">>, all)))), + Req = lists:foldl(fun + (<<"Set-Cookie: ",SetCookie/bits>>, Req1) -> + %% We do not use set_resp_cookie because we want to preserve ordering. + SetCookieList = cowboy_req:resp_header(<<"set-cookie">>, Req1, []), + cowboy_req:set_resp_header(<<"set-cookie">>, SetCookieList ++ [SetCookie], Req1); + (<<"Set-Cookie:">>, Req1) -> + Req1; + (<<"Location: ",Location/bits>>, Req1) -> + cowboy_req:set_resp_header(<<"location">>, Location, Req1) + end, Req0, Lines), + {ok, cowboy_req:reply(204, Req), State}. diff --git a/test/handlers/cookie_parser_result_h.erl b/test/handlers/cookie_parser_result_h.erl new file mode 100644 index 0000000..a1fa899 --- /dev/null +++ b/test/handlers/cookie_parser_result_h.erl @@ -0,0 +1,25 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_parser_result_h). + +-export([init/2]). + +init(Req=#{qs := Qs}, State) -> + %% Hardcoded path, but I doubt it's going to break anytime soon. + ExpectedFile = iolist_to_binary(["../../test/wpt/cookies/", Qs, "-expected"]), + CookieHd = cowboy_req:header(<<"cookie">>, Req), + case file:read_file(ExpectedFile) of + {ok, Expected} when Expected =:= <<>>; Expected =:= <<"\n">> -> + undefined = CookieHd, + ok; + {ok, <<"Cookie: ",CookiesBin0/bits>>} -> + %% We only care about the first line. + [CookiesBin, <<>>|_] = string:split(CookiesBin0, <<"\n">>, all), + CookiesBin = CookieHd, + ok + end, + %% We echo back the cookie header in order to log it. + {ok, cowboy_req:reply(204, case CookieHd of + undefined -> #{<<"x-no-cookie-received">> => <<"Cookie header missing.">>}; + _ -> #{<<"x-cookie-received">> => CookieHd} + end, Req), State}. diff --git a/test/handlers/cookie_set_h.erl b/test/handlers/cookie_set_h.erl new file mode 100644 index 0000000..29ff351 --- /dev/null +++ b/test/handlers/cookie_set_h.erl @@ -0,0 +1,36 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(cookie_set_h). + +-export([init/2]). + +init(Req0, State) -> + SetCookieList = set_cookie_list(Req0), + Req = cowboy_req:set_resp_header(<<"set-cookie">>, SetCookieList, Req0), + {ok, cowboy_req:reply(204, Req), State}. + +-define(HOST, "web-platform.test"). + +set_cookie_list(#{qs := <<"domain_with_and_without_leading_period">>}) -> + [ + <<"a=b; Path=/; Domain=." ?HOST>>, + <<"a=c; Path=/; Domain=" ?HOST>> + ]; +set_cookie_list(#{qs := <<"domain_with_leading_period">>}) -> + [<<"a=b; Path=/; Domain=." ?HOST>>]; +set_cookie_list(#{qs := <<"domain_matches_host">>}) -> + [<<"a=b; Path=/; Domain=" ?HOST>>]; +set_cookie_list(#{qs := <<"domain_missing">>}) -> + [<<"a=b; Path=/;">>]; +set_cookie_list(#{qs := <<"path_default">>}) -> + [<<"cookie-path-default=1">>]; +set_cookie_list(#{qs := <<"path_default_expire">>}) -> + [<<"cookie-path-default=1; Max-Age=0">>]; +set_cookie_list(#{qs := <<"path=",Path/bits>>}) -> + [[<<"a=b; Path=">>, Path]]; +set_cookie_list(Req=#{qs := <<"prefix">>}) -> + [cowboy_req:header(<<"please-set-cookie">>, Req)]; +set_cookie_list(#{qs := <<"secure_http">>}) -> + [<<"secure_from_nonsecure_http=1; Secure; Path=/">>]; +set_cookie_list(#{qs := <<"secure_https">>}) -> + [<<"secure_from_secure_http=1; Secure; Path=/">>]. diff --git a/test/handlers/ws_cookie_h.erl b/test/handlers/ws_cookie_h.erl new file mode 100644 index 0000000..39889b3 --- /dev/null +++ b/test/handlers/ws_cookie_h.erl @@ -0,0 +1,24 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_cookie_h). + +-export([init/2]). +-export([websocket_handle/2]). +-export([websocket_info/2]). + +init(Req0, _) -> + Req = cowboy_req:set_resp_header(<<"set-cookie">>, + [<<"ws_cookie=1; Secure; path=/">>], Req0), + {cowboy_websocket, Req, undefined, #{ + compress => true + }}. + +websocket_handle({text, Data}, State) -> + {[{text, Data}], State}; +websocket_handle({binary, Data}, State) -> + {[{binary, Data}], State}; +websocket_handle(_Frame, State) -> + {[], State}. + +websocket_info(_Info, State) -> + {[], State}. diff --git a/test/rfc6265bis_SUITE.erl b/test/rfc6265bis_SUITE.erl new file mode 100644 index 0000000..ce24c40 --- /dev/null +++ b/test/rfc6265bis_SUITE.erl @@ -0,0 +1,499 @@ +%% Copyright (c) 2020, Loïc Hoguin <[email protected]> +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(rfc6265bis_SUITE). +-compile(export_all). +-compile(nowarn_export_all). + +-import(ct_helper, [config/2]). +-import(ct_helper, [doc/1]). + +%% ct. + +all() -> + [ + {group, http}, + {group, https}, + {group, h2c}, + {group, h2} + ]. + +groups() -> + CommonTests = ct_helper:all(?MODULE) -- [wpt_http_state], + NumFiles = length(get_test_files()), + NumDisabledTlsFiles = length(get_disabled_tls_test_files()), + [ + {http, [parallel], CommonTests + ++ [{testcase, wpt_http_state, [{repeat, NumFiles}]}]}, + {https, [parallel], CommonTests + ++ [{testcase, wpt_http_state, [{repeat, NumFiles - NumDisabledTlsFiles}]}]}, + %% Websocket over HTTP/2 is currently not supported. + {h2c, [parallel], (CommonTests -- [wpt_secure_ws]) + ++ [{testcase, wpt_http_state, [{repeat, NumFiles}]}]}, + {h2, [parallel], (CommonTests -- [wpt_secure_ws]) + ++ [{testcase, wpt_http_state, [{repeat, NumFiles - NumDisabledTlsFiles}]}]} + ]. + +init_per_group(Ref, Config0) when Ref =:= http; Ref =:= h2c -> + Protocol = case Ref of + http -> http; + h2c -> http2 + end, + Config = gun_test:init_cowboy_tcp(Ref, #{ + env => #{dispatch => cowboy_router:compile(init_routes())} + }, Config0), + init_per_group_common([{transport, tcp}, {protocol, Protocol}|Config]); +init_per_group(Ref, Config0) when Ref =:= https; Ref =:= h2 -> + Protocol = case Ref of + https -> http; + h2 -> http2 + end, + Config = gun_test:init_cowboy_tls(Ref, #{ + env => #{dispatch => cowboy_router:compile(init_routes())} + }, Config0), + init_per_group_common([{transport, tls}, {protocol, Protocol}|Config]). + +init_per_group_common(Config = [{transport, Transport}|_]) -> + GiverPid = spawn(fun() -> do_test_giver_init(Transport) end), + [{test_giver_pid, GiverPid}|Config]. + +end_per_group(Ref, _) -> + cowboy:stop_listener(Ref). + +init_routes() -> [ + {'_', [ + {"/cookie-echo/[...]", cookie_echo_h, []}, + {"/cookie-parser/[...]", cookie_parser_h, []}, + {"/cookie-parser-result/[...]", cookie_parser_result_h, []}, + {"/cookie-set/[...]", cookie_set_h, []}, + {"/cookies/resources/echo-cookie.html", cookie_echo_h, []}, + {"/cookies/resources/set-cookie.html", cookie_set_h, []}, + {<<"/cookies/resources/echo.py">>, cookie_echo_h, []}, + {<<"/cookies/resources/set.py">>, cookie_set_h, []}, + {<<"/ws">>, ws_cookie_h, []} + ]} +]. + +%% Test files. + +get_test_files() -> + %% Hardcoded path, but I doubt it's going to break anytime soon. + gun_cookies:wpt_http_state_test_files("../../test/"). + +get_disabled_tls_test_files() -> + %% These tests include the Secure attribute and are written for + %% clear text. They must therefore be disabled over TLS. + [ + "../../test/wpt/cookies/0010-test", + "../../test/wpt/cookies/attribute0001-test", + "../../test/wpt/cookies/attribute0002-test", + "../../test/wpt/cookies/attribute0004-test", + "../../test/wpt/cookies/attribute0005-test", + "../../test/wpt/cookies/attribute0007-test", + "../../test/wpt/cookies/attribute0008-test", + "../../test/wpt/cookies/attribute0009-test", + "../../test/wpt/cookies/attribute0010-test", + "../../test/wpt/cookies/attribute0011-test", + "../../test/wpt/cookies/attribute0012-test", + "../../test/wpt/cookies/attribute0013-test", + "../../test/wpt/cookies/attribute0025-test", + "../../test/wpt/cookies/attribute0026-test" + ]. + +do_test_giver_init(Transport) -> + TestFiles0 = get_test_files(), + TestFiles = case Transport of + tcp -> TestFiles0; + tls -> TestFiles0 -- get_disabled_tls_test_files() + end, + do_test_giver_loop(TestFiles). + +do_test_giver_loop([]) -> + ok; +do_test_giver_loop([TestFile|Tail]) -> + receive + {request_test_file, FromPid, FromRef} -> + FromPid ! {FromRef, TestFile}, + do_test_giver_loop(Tail) + after 1000 -> + error(timeout) + end. + +do_request_test_file(Config) -> + Ref = make_ref(), + GiverPid = config(test_giver_pid, Config), + GiverPid ! {request_test_file, self(), Ref}, + receive + {Ref, TestFile} -> + TestFile + after 1000 -> + error(timeout) + end. + +%% Tests. + +-define(HOST, "web-platform.test"). + +%% WPT: domain/domain-attribute-host-with-and-without-leading-period +wpt_domain_with_and_without_leading_period(Config) -> + doc("Domain with and without leading period."), + #{ + same_origin := [{<<"a">>, <<"c">>}], + subdomain := [{<<"a">>, <<"c">>}] + } = do_domain_test(Config, "domain_with_and_without_leading_period"), + ok. + +%% WPT: domain/domain-attribute-host-with-leading-period +wpt_domain_with_leading_period(Config) -> + doc("Domain with leading period."), + #{ + same_origin := [{<<"a">>, <<"b">>}], + subdomain := [{<<"a">>, <<"b">>}] + } = do_domain_test(Config, "domain_with_leading_period"), + ok. + +%% WPT: domain/domain-attribute-matches-host +wpt_domain_matches_host(Config) -> + doc("Domain matches host header."), + #{ + same_origin := [{<<"a">>, <<"b">>}], + subdomain := [{<<"a">>, <<"b">>}] + } = do_domain_test(Config, "domain_matches_host"), + ok. + +%% WPT: domain/domain-attribute-missing +wpt_domain_missing(Config) -> + doc("Domain attribute missing."), + #{ + same_origin := [{<<"a">>, <<"b">>}], + subdomain := undefined + } = do_domain_test(Config, "domain_missing"), + ok. + +do_domain_test(Config, TestCase) -> + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, ["/cookie-set?", TestCase], #{<<"host">> => ?HOST}), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookie-echo", #{<<"host">> => ?HOST}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + StreamRef3 = gun:get(ConnPid, "/cookie-echo", #{<<"host">> => "sub." ?HOST}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), + {ok, Body3} = gun:await_body(ConnPid, StreamRef3), + ct:log("Body3:~n~p", [Body3]), + gun:close(ConnPid), + #{ + same_origin => case Body2 of <<"UNDEF">> -> undefined; _ -> cow_cookie:parse_cookie(Body2) end, + subdomain => case Body3 of <<"UNDEF">> -> undefined; _ -> cow_cookie:parse_cookie(Body3) end + }. + +%% WPT: http-state/*-tests +wpt_http_state(Config) -> + TestFile = do_request_test_file(Config), + Test = string:replace(filename:basename(TestFile), "-test", ""), + doc("http-state: " ++ Test), + ct:log("Test file:~n~s", [element(2, file:read_file(TestFile))]), + ct:log("Expected file:~n~s", [element(2, file:read_file(string:replace(TestFile, "-test", "-expected")))]), + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, "/cookie-parser?" ++ Test, #{<<"host">> => "home.example.org"}), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + {Host, Path} = case lists:keyfind(<<"location">>, 1, Headers1) of + false -> + {"home.example.org", "/cookie-parser-result?" ++ Test}; + {_, Location} -> + case uri_string:parse(Location) of + #{host := Host0, path := Path0, query := Qs0} -> + {Host0, [Path0, $?, Qs0]}; + #{path := Path0, query := Qs0} -> + {"home.example.org", [Path0, $?, Qs0]} + end + end, + StreamRef2 = gun:get(ConnPid, Path, #{<<"host">> => Host}), + %% The validation is done in the handler. An error results in a 4xx or 5xx. + {response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef2), + ct:log("Headers2:~n~p", [Headers2]), + gun:close(ConnPid). + +%% WPT: path/default +wpt_path_default(Config) -> + doc("Cookie set on the default path can be retrieved."), + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, "/cookie-set?path_default"), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + [{<<"cookie-path-default">>, <<"1">>}] = cow_cookie:parse_cookie(Body2), + %% Expire the cookie. + StreamRef3 = gun:get(ConnPid, "/cookie-set?path_default_expire"), + {response, fin, 204, Headers3} = gun:await(ConnPid, StreamRef3), + ct:log("Headers3:~n~p", [Headers3]), + StreamRef4 = gun:get(ConnPid, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef4), + {ok, Body4} = gun:await_body(ConnPid, StreamRef4), + ct:log("Body4:~n~p", [Body4]), + <<"UNDEF">> = Body4, + gun:close(ConnPid). + +%% WPT: path/match +wpt_path_match(Config) -> + doc("Cookie path match."), + MatchTests = [ + <<"/">>, + <<"match.html">>, + <<"cookies">>, + <<"/cookies">>, + <<"/cookies/">>, + <<"/cookies/resources/echo-cookie.html">> + ], + NegTests = [ + <<"/cook">>, + <<"/w/">> + ], + Protocol = config(protocol, Config), + _ = [begin + ct:log("Positive test: ~s", [P]), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, ["/cookies/resources/set-cookie.html?path=", P]), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookies/resources/echo-cookie.html"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + [{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body2), + gun:close(ConnPid) + end || P <- MatchTests], + _ = [begin + ct:log("Negative test: ~s", [P]), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, ["/cookies/resources/set-cookie.html?path=", P]), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookies/resources/echo-cookie.html"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + <<"UNDEF">> = Body2, + gun:close(ConnPid) + end || P <- NegTests], + ok. + +%% WPT: prefix/__host.header +wpt_prefix_host(Config) -> + doc("__Host- prefix."), + Tests = case config(transport, Config) of + tcp -> [ + {<<"__Host-foo=bar; Path=/;">>, false}, + {<<"__Host-foo=bar; Path=/;domain=" ?HOST>>, false}, + {<<"__Host-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Host-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;domain=" ?HOST>>, false}, + {<<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; ">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; domain=" ?HOST>>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false} + ]; + tls -> [ + {<<"__Host-foo=bar; Path=/;">>, false}, + {<<"__Host-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Host-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/;">>, true}, + {<<"__Host-foo=bar; Secure; Path=/;Max-Age=10">>, true}, + {<<"__Host-foo=bar; Secure; Path=/;HttpOnly">>, true}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; ">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?HOST "; HttpOnly">>, false}, + {<<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false} + ] + end, + _ = [do_wpt_prefix_common(Config, TestCase, Expected, <<"__Host-foo">>) + || {TestCase, Expected} <- Tests], + ok. + +%% WPT: prefix/__secure.header +wpt_prefix_secure(Config) -> + doc("__Secure- prefix."), + Tests = case config(transport, Config) of + tcp -> [ + {<<"__Secure-foo=bar; Path=/;">>, false}, + {<<"__Secure-foo=bar; Path=/;domain=" ?HOST>>, false}, + {<<"__Secure-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Secure-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;domain=" ?HOST>>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, false} + ]; + tls -> [ + {<<"__Secure-foo=bar; Path=/;">>, false}, + {<<"__Secure-foo=bar; Path=/;Max-Age=10">>, false}, + {<<"__Secure-foo=bar; Path=/;HttpOnly">>, false}, + {<<"__Secure-foo=bar; Secure; Path=/;">>, true}, + {<<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, true}, + {<<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, true} + %% Missing two SameSite cases from prefix/__secure.header.https. (Not implemented.) + ] + end, + _ = [do_wpt_prefix_common(Config, TestCase, Expected, <<"__Secure-foo">>) + || {TestCase, Expected} <- Tests], + ok. + +do_wpt_prefix_common(Config, TestCase, Expected, Name) -> + Protocol = config(protocol, Config), + ct:log("Test case: ~s~nCookie must be set? ~s", [TestCase, Expected]), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + %% Set and retrieve the cookie. + StreamRef1 = gun:get(ConnPid, "/cookies/resources/set.py?prefix", #{ + <<"host">> => ?HOST, + <<"please-set-cookie">> => TestCase + }), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookies/resources/echo.py", #{ + <<"host">> => ?HOST + }), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + case Expected of + true -> + [{Name, _}] = cow_cookie:parse_cookie(Body2), + ok; + false -> + <<"UNDEF">> = Body2, + ok + end, + gun:close(ConnPid). + +%% WPT: samesite-none-secure/ (Not implemented.) +%% WPT: samesite/ (Not implemented.) + +wpt_secure(Config) -> + doc("Secure attribute."), + case config(transport, Config) of + tcp -> + undefined = do_wpt_secure_common(Config, <<"secure_http">>), + ok; + tls -> + [{<<"secure_from_secure_http">>, <<"1">>}] = do_wpt_secure_common(Config, <<"secure_https">>), + ok + end. + +do_wpt_secure_common(Config, TestCase) -> + Protocol = config(protocol, Config), + {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid), + StreamRef1 = gun:get(ConnPid, ["/cookie-set?", TestCase]), + {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + StreamRef2 = gun:get(ConnPid, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + gun:close(ConnPid), + case Body2 of + <<"UNDEF">> -> undefined; + _ -> cow_cookie:parse_cookie(Body2) + end. + +%% WPT: secure/set-from-ws* +wpt_secure_ws(Config) -> + doc("Secure attribute in Websocket upgrade response."), + case config(transport, Config) of + tcp -> + undefined = do_wpt_secure_ws_common(Config), + ok; + tls -> + [{<<"ws_cookie">>, <<"1">>}] = do_wpt_secure_ws_common(Config), + ok + end. + +do_wpt_secure_ws_common(Config) -> + Protocol = config(protocol, Config), + {ok, ConnPid1} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => gun_cookies_list:init() + }), + {ok, Protocol} = gun:await_up(ConnPid1), + StreamRef1 = gun:ws_upgrade(ConnPid1, "/ws"), + {upgrade, [<<"websocket">>], Headers1} = gun:await(ConnPid1, StreamRef1), + ct:log("Headers1:~n~p", [Headers1]), + %% We must extract the cookie store because it is tied to the connection. + #{cookie_store := CookieStore} = gun:info(ConnPid1), + gun:close(ConnPid1), + {ok, ConnPid2} = gun:open("localhost", config(port, Config), #{ + transport => config(transport, Config), + protocols => [Protocol], + cookie_store => CookieStore + }), + StreamRef2 = gun:get(ConnPid2, "/cookie-echo"), + {response, nofin, 200, _} = gun:await(ConnPid2, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid2, StreamRef2), + ct:log("Body2:~n~p", [Body2]), + gun:close(ConnPid2), + case Body2 of + <<"UNDEF">> -> undefined; + _ -> cow_cookie:parse_cookie(Body2) + end. |