From 180ce23aba863d94c17ee0667cad1ed916f56da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Fri, 6 May 2022 14:23:33 +0200 Subject: Update cookie tests against latest WPT The http-state tests were removed and replaced with tests in HTML pages. I have devised a way to semi- automatically import them and test them. Additional fixes were made following changes in the rfc6265bis draft. --- test/rfc6265bis_SUITE.erl | 602 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 475 insertions(+), 127 deletions(-) (limited to 'test/rfc6265bis_SUITE.erl') diff --git a/test/rfc6265bis_SUITE.erl b/test/rfc6265bis_SUITE.erl index 4301fc3..7adc82d 100644 --- a/test/rfc6265bis_SUITE.erl +++ b/test/rfc6265bis_SUITE.erl @@ -30,19 +30,13 @@ all() -> ]. groups() -> - CommonTests = ct_helper:all(?MODULE) -- [wpt_http_state], - NumFiles = length(get_test_files()), - NumDisabledTlsFiles = length(get_disabled_tls_test_files()), + CommonTests = ct_helper:all(?MODULE), [ - {http, [parallel], CommonTests - ++ [{testcase, wpt_http_state, [{repeat, NumFiles}]}]}, - {https, [parallel], CommonTests - ++ [{testcase, wpt_http_state, [{repeat, NumFiles - NumDisabledTlsFiles}]}]}, + {http, [parallel], CommonTests}, + {https, [parallel], CommonTests}, %% 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}]}]} + {h2c, [parallel], (CommonTests -- [wpt_secure_ws])}, + {h2, [parallel], (CommonTests -- [wpt_secure_ws])} ]. init_per_group(Ref, Config0) when Ref =:= http; Ref =:= h2c -> @@ -53,7 +47,7 @@ init_per_group(Ref, Config0) when Ref =:= http; Ref =:= h2c -> Config = gun_test:init_cowboy_tcp(Ref, #{ env => #{dispatch => cowboy_router:compile(init_routes())} }, Config0), - init_per_group_common([{transport, tcp}, {protocol, Protocol}|Config]); + [{transport, tcp}, {protocol, Protocol}|Config]; init_per_group(Ref, Config0) when Ref =:= https; Ref =:= h2 -> Protocol = case Ref of https -> http; @@ -62,11 +56,7 @@ init_per_group(Ref, Config0) when Ref =:= https; Ref =:= h2 -> 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]. + [{transport, tls}, {protocol, Protocol}|Config]. end_per_group(Ref, _) -> cowboy:stop_listener(Ref). @@ -86,60 +76,6 @@ init_routes() -> [ ]} ]. -%% 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) - 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. dont_ignore_informational_set_cookie(Config) -> @@ -239,45 +175,273 @@ set_cookie_connect_tls(Config) -> [{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body3), gun:close(ConnPid). --define(HOST, "web-platform.test"). +%% Web Platform Tests converted to Erlang. +%% +%% Tests are not automatically updated, the process is manual. +%% Some test data is exported in JSON files in the "test/wpt" directory. +%% https://github.com/web-platform-tests/wpt/tree/master/cookies + +-define(WPT_HOST, "web-platform.test"). + +%% WPT: browser-only tests +%% +%% cookie-enabled-noncookie-frame.html +%% meta-blocked.html +%% navigated-away.html +%% prefix/document-cookie.non-secure.html +%% prefix/__host.document-cookie.html +%% prefix/__host.document-cookie.https.html +%% prefix/__secure.document-cookie.html +%% prefix/__secure.document-cookie.https.html +%% secure/set-from-dom.https.sub.html +%% secure/set-from-dom.sub.html + +%% WPT: attributes/attributes-ctl +%% +%% attributes/attributes-ctl.sub.html +%% +%% The original tests use the DOM. We can't do that so +%% we use a simple HTTP test instead. The original test +%% also includes a string representation of the CTL in +%% the cookie name. We don't bother. +%% +%% The expected value is only used for the \t CTL. +%% The original test retains the \t in the value because +%% it uses the DOM. The Set-Cookie algorithm requires +%% us to drop it. +wpt_attributes_ctl_domain(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Domain attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testdomain">>, + <<"testdomain=t; Domain=test", CTL, ".co; Domain=", ?WPT_HOST>>, + <<"testdomain=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_domain2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Domain attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testdomain2">>, + <<"testdomain2=t; Domain=", ?WPT_HOST, CTL>>, + <<"testdomain2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_path(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Path attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testpath">>, + <<"testpath=t; Path=/te", CTL, "st; Path=/cookies/attributes">>, + <<"testpath=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_path2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Path attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testpath2">>, + <<"testpath2=t; Path=/cookies/attributes", CTL>>, + <<"testpath2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_max_age(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Max-Age attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testmaxage">>, + <<"testmaxage=t; Max-Age=10", CTL, "00; Max-Age=1000">>, + <<"testmaxage=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_max_age2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Max-Age attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testmaxage2">>, + <<"testmaxage2=t; Max-Age=1000", CTL>>, + <<"testmaxage2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_expires(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Expires attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testexpires">>, + <<"testexpires=t" + "; Expires=Fri, 01 Jan 20", CTL, "38 00:00:00 GMT" + "; Expires=Fri, 01 Jan 2038 00:00:00 GMT">>, + <<"testexpires=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_expires2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Expires attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testexpires2">>, + <<"testexpires2=t; Expires=Fri, 01 Jan 2038 00:00:00 GMT", CTL>>, + <<"testexpires2=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_secure(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in Secure attribute."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsecure">>, + <<"testsecure=t; Sec", CTL, "ure">>, + <<"testsecure=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_secure2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after Secure attribute."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsecure2">>, + <<"testsecure2=t; Secure", CTL>>, + case config(transport, Config) of + tcp -> <<>>; %% Secure causes the cookie to be rejected over TCP. + tls -> <<"testsecure2=t">> + end + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_httponly(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in HttpOnly attribute."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testhttponly">>, + <<"testhttponly=t; Http", CTL, "Only">>, + <<"testhttponly=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_samesite(Config) -> + doc("Test cookie attribute parsing with control characters: " + "in SameSite attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsamesite">>, + <<"testsamesite=t; SameSite=No", CTL, "ne; SameSite=None">>, + <<"testsamesite=t">> + } end, "/cookies/attributes", Config). + +wpt_attributes_ctl_samesite2(Config) -> + doc("Test cookie attribute parsing with control characters: " + "after SameSite attribute value."), + do_wpt_ctl_test(fun(CTL) -> { + <<"testsamesite2">>, + <<"testsamesite2=t; SameSite=None", CTL>>, + <<"testsamesite2=t">> + } end, "/cookies/attributes", Config). + +%% @todo Redirect cookie test. +%% attributes/domain.sub.html +%% attributes/resources/domain-child.sub.html + +%% WPT: attributes/expires +%% +%% attributes/expires.html +wpt_attributes_expires(Config) -> + doc("Test expires attribute parsing."), + do_wpt_json_test("attributes_expires", "/cookies/attributes", Config). + +%% WPT: attributes/invalid +%% +%% attributes/invalid.html +wpt_attributes_invalid(Config) -> + doc("Test invalid attribute parsing."), + do_wpt_json_test("attributes_invalid", "/cookies/attributes", Config). + +%% WPT: attributes/max_age +%% +%% attributes/max-age.html +wpt_attributes_max_age(Config) -> + doc("Test max-age attribute parsing."), + do_wpt_json_test("attributes_max_age", "/cookies/attributes", Config). + +%% WPT: attributes/path +%% +%% attributes/path.html +wpt_attributes_path(Config) -> + doc("Test cookie path attribute parsing."), + do_wpt_json_test("attributes_path", "/cookies/attributes", Config). + +%% @todo Redirect cookie test. +%% attributes/path-redirect.html +%% attributes/resources/pathfakeout.html +%% attributes/resources/path-redirect-shared.js +%% attributes/resources/path.html +%% attributes/resources/path.html.headers +%% attributes/resources/path/one.html +%% attributes/resources/path/three.html +%% attributes/resources/path/two.html +%% attributes/resources/pathfakeout/one.html + +%% WPT: attributes/secure +%% +%% attributes/secure.https.html +%% attributes/secure-non-secure.html +%% attributes/resources/secure-non-secure-child.html +wpt_attributes_secure(Config) -> + doc("Test cookie secure attribute parsing."), + TestFile = case config(transport, Config) of + tcp -> "attributes_secure_non_secure"; + tls -> "attributes_secure" + end, + do_wpt_json_test(TestFile, "/cookies/attributes", Config). %% WPT: domain/domain-attribute-host-with-and-without-leading-period +%% +%% domain/domain-attribute-host-with-and-without-leading-period.sub.https.html +%% domain/domain-attribute-host-with-and-without-leading-period.sub.https.html.sub.headers 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"), + } = do_wpt_domain_test(Config, "domain_with_and_without_leading_period"), ok. %% WPT: domain/domain-attribute-host-with-leading-period +%% +%% domain/domain-attribute-host-with-leading-period.sub.https.html +%% domain/domain-attribute-host-with-leading-period.sub.https.html.sub.headers 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"), + } = do_wpt_domain_test(Config, "domain_with_leading_period"), ok. +%% @todo WPT: domain/domain-attribute-idn-host +%% +%% domain/domain-attribute-idn-host.sub.https.html +%% domain/support/idn-child.sub.https.html +%% domain/support/idn.py + %% WPT: domain/domain-attribute-matches-host +%% +%% domain/domain-attribute-matches-host.sub.https.html +%% domain/domain-attribute-matches-host.sub.https.html.sub.headers wpt_domain_matches_host(Config) -> doc("Domain matches host header."), #{ same_origin := [{<<"a">>, <<"b">>}], subdomain := [{<<"a">>, <<"b">>}] - } = do_domain_test(Config, "domain_matches_host"), + } = do_wpt_domain_test(Config, "domain_matches_host"), ok. %% WPT: domain/domain-attribute-missing +%% +%% domain/domain-attribute-missing.sub.html +%% domain/domain-attribute-missing.sub.html.headers wpt_domain_missing(Config) -> doc("Domain attribute missing."), #{ same_origin := [{<<"a">>, <<"b">>}], subdomain := undefined - } = do_domain_test(Config, "domain_missing"), + } = do_wpt_domain_test(Config, "domain_missing"), ok. -do_domain_test(Config, TestCase) -> +do_wpt_domain_test(Config, TestCase) -> Protocol = config(protocol, Config), {ok, ConnPid} = gun:open("localhost", config(port, Config), #{ transport => config(transport, Config), @@ -285,14 +449,14 @@ do_domain_test(Config, TestCase) -> cookie_store => gun_cookies_list:init() }), {ok, Protocol} = gun:await_up(ConnPid), - StreamRef1 = gun:get(ConnPid, ["/cookie-set?", TestCase], #{<<"host">> => ?HOST}), + StreamRef1 = gun:get(ConnPid, ["/cookie-set?", TestCase], #{<<"host">> => ?WPT_HOST}), {response, fin, 204, Headers1} = gun:await(ConnPid, StreamRef1), ct:log("Headers1:~n~p", [Headers1]), - StreamRef2 = gun:get(ConnPid, "/cookie-echo", #{<<"host">> => ?HOST}), + StreamRef2 = gun:get(ConnPid, "/cookie-echo", #{<<"host">> => ?WPT_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}), + StreamRef3 = gun:get(ConnPid, "/cookie-echo", #{<<"host">> => "sub." ?WPT_HOST}), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef3), {ok, Body3} = gun:await_body(ConnPid, StreamRef3), ct:log("Body3:~n~p", [Body3]), @@ -302,41 +466,50 @@ do_domain_test(Config, TestCase) -> 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: encoding/charset +%% +%% encoding/charset.html +wpt_encoding(Config) -> + doc("Test UTF-8 and ASCII cookie parsing."), + do_wpt_json_test("encoding_charset", "/cookies/encoding", Config). + +%% WPT: name/name +%% +%% name/name.html +wpt_name(Config) -> + doc("Test cookie name parsing."), + do_wpt_json_test("name", "/cookies/name", Config). + +%% WPT: name/name-ctl +%% +%% name/name-ctl.html +%% +%% The original tests use the DOM. We can't do that so +%% we use a simple HTTP test instead. The original test +%% also includes a string representation of the CTL in +%% the cookie name. We don't bother. +%% +%% The expected value is only used for the \t CTL. +%% The original test retains the \t in the value because +%% it uses the DOM. The Set-Cookie algorithm requires +%% us to drop it. +wpt_name_ctl(Config) -> + doc("Test cookie name parsing with control characters."), + do_wpt_ctl_test(fun(CTL) -> { + <<"test", CTL, "name">>, + <<"test", CTL, "name=", CTL>>, + <<"test", CTL, "name=">> + } end, "/cookies/name", Config). + +%% @todo Redirect cookie test. +%% ordering/ordering.sub.html +%% ordering/resources/ordering-child.sub.html + +%% WPT: partitioned-cookies (Not implemented; proposal.) %% WPT: path/default +%% +%% path/default.html wpt_path_default(Config) -> doc("Cookie set on the default path can be retrieved."), Protocol = config(protocol, Config), @@ -367,6 +540,8 @@ wpt_path_default(Config) -> gun:close(ConnPid). %% WPT: path/match +%% +%% path/match.html wpt_path_match(Config) -> doc("Cookie path match."), MatchTests = [ @@ -423,22 +598,25 @@ wpt_path_match(Config) -> ok. %% WPT: prefix/__host.header +%% +%% prefix/__host.header.html +%% prefix/__host.header.https.html 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=/;domain=" ?WPT_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=/;domain=" ?WPT_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=/; Domain=" ?WPT_HOST "; ">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; domain=" ?WPT_HOST>>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; HttpOnly">>, false}, {<<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false} ]; tls -> [ @@ -448,9 +626,9 @@ wpt_prefix_host(Config) -> {<<"__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=/; Domain=" ?WPT_HOST "; ">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; Max-Age=10">>, false}, + {<<"__Host-foo=bar; Secure; Path=/; Domain=" ?WPT_HOST "; HttpOnly">>, false}, {<<"__Host-foo=bar; Secure; Path=/cookies/resources/list.py">>, false} ] end, @@ -459,16 +637,19 @@ wpt_prefix_host(Config) -> ok. %% WPT: prefix/__secure.header +%% +%% prefix/__secure.header.html +%% prefix/__secure.header.https.html 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=/;domain=" ?WPT_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=/;domain=" ?WPT_HOST>>, false}, {<<"__Secure-foo=bar; Secure; Path=/;Max-Age=10">>, false}, {<<"__Secure-foo=bar; Secure; Path=/;HttpOnly">>, false} ]; @@ -497,13 +678,13 @@ do_wpt_prefix_common(Config, TestCase, Expected, Name) -> {ok, Protocol} = gun:await_up(ConnPid), %% Set and retrieve the cookie. StreamRef1 = gun:get(ConnPid, "/cookies/resources/set.py?prefix", #{ - <<"host">> => ?HOST, + <<"host">> => ?WPT_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 + <<"host">> => ?WPT_HOST }), {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), {ok, Body2} = gun:await_body(ConnPid, StreamRef2), @@ -518,9 +699,16 @@ do_wpt_prefix_common(Config, TestCase, Expected, Name) -> end, gun:close(ConnPid). -%% WPT: samesite-none-secure/ (Not implemented.) %% WPT: samesite/ (Not implemented.) +%% WPT: samesite-none-secure/ (Not implemented.) +%% WPT: schemeful-same-site/ (Not implemented.) +%% WPT: secure/set-from-http.* +%% +%% secure/set-from-http.sub.html +%% secure/set-from-http.sub.html.headers +%% secure/set-from-http.https.sub.html +%% secure/set-from-http.https.sub.html.headers wpt_secure(Config) -> doc("Secure attribute."), case config(transport, Config) of @@ -554,6 +742,9 @@ do_wpt_secure_common(Config, TestCase) -> end. %% WPT: secure/set-from-ws* +%% +%% secure/set-from-ws.sub.html +%% secure/set-from-wss.https.sub.html wpt_secure_ws(Config) -> doc("Secure attribute in Websocket upgrade response."), case config(transport, Config) of @@ -593,3 +784,160 @@ do_wpt_secure_ws_common(Config) -> <<"UNDEF">> -> undefined; _ -> cow_cookie:parse_cookie(Body2) end. + +%% WPT: size/attributes +%% +%% size/attributes.www.sub.html +wpt_size_attributes(Config) -> + doc("Test cookie attribute size restrictions."), + do_wpt_json_test("size_attributes", "/cookies/size", Config). + +%% WPT: size/name-and-value +%% +%% size/name-and-value.html +wpt_size_name_and_value(Config) -> + doc("Test cookie name/value size restrictions."), + do_wpt_json_test("size_name_and_value", "/cookies/size", Config). + +%% WPT: value/value +%% +%% value/value.html +wpt_value(Config) -> + doc("Test cookie value parsing."), + Tests = do_load_json("value"), + _ = [begin + #{ + <<"name">> := Name, + <<"cookie">> := Cookie, + <<"expected">> := Expected + } = Test, + false = maps:is_key(<<"defaultPath">>, Test), + do_wpt_set_test(<<"/cookies/value">>, + Name, Cookie, Expected, Config) + end || Test <- Tests, + %% The original test uses the DOM, we use HTTP, and are + %% required to drop the cookie entirely if it contains + %% a \n (RFC6265bis 5.4) so we skip this test. + maps:get(<<"expected">>, Test) =/= <<"test=13">>], + ok. + +%% WPT: value/value-ctl +%% +%% value/value-ctl.html +%% +%% The original tests use the DOM. We can't do that so +%% we use a simple HTTP test instead. The original test +%% also includes a string representation of the CTL in +%% the cookie value. We don't bother. +%% +%% The expected value is only used for the \t CTL. +%% The original test retains the \t in the value because +%% it uses the DOM. The Set-Cookie algorithm requires +%% us to drop it. +wpt_value_ctl(Config) -> + doc("Test cookie value parsing with control characters."), + do_wpt_ctl_test(fun(CTL) -> { + <<"test">>, + <<"test=", CTL, "value">>, + <<"test=value">> + } end, "/cookies/value", Config). + +%% JSON files are created by taking the Javascript Object +%% from the HTML files in the WPT suite, using the browser +%% Developer console to convert into JSON: +%% Obj = +%% JSON.stringify(Obj) +%% Then copying the result into the JSON file; removing +%% the quoting (first and last character) and if needed +%% fixing the escaping in Vim using: +%% :%s/\\\\/\\/g +%% The host may also need to be replaced to match WPT_HOST. +do_load_json(File0) -> + File = "../../test/wpt/cookies/" ++ File0 ++ ".json", + {ok, Bin} = file:read_file(File), + jsx:decode(Bin, [{return_maps, true}]). + +do_wpt_json_test(TestFile, TestPath, Config) -> + Tests = do_load_json(TestFile), + _ = [begin + #{ + <<"name">> := Name, + <<"cookie">> := Cookie, + <<"expected">> := Expected + } = Test, + DefaultPath = maps:get(<<"defaultPath">>, Test, true), + do_wpt_set_test(TestPath, Name, Cookie, Expected, DefaultPath, Config) + end || Test <- Tests], + ok. + +do_wpt_ctl_test(Fun, TestPath, Config) -> + %% Control characters are defined by RFC5234 to be %x00-1F / %x7F. + %% We exclude \r for HTTP/1.1 because this causes errors + %% at the header parsing level. + CTLs0 = lists:seq(0, 16#1F) ++ [16#7F], + CTLs = case config(protocol, Config) of + http -> CTLs0 -- "\r"; + http2 -> CTLs0 + end, + %% All CTLs except \t should cause the cookie to be rejected. + _ = [begin + {Name, Cookie, Expected} = Fun(CTL), + case CTL of + $\t -> + do_wpt_set_test(TestPath, Name, Cookie, Expected, false, Config); + _ -> + do_wpt_set_test(TestPath, Name, Cookie, <<>>, false, Config) + end + end || CTL <- CTLs], + ok. + +%% Equivalent to httpCookieTest. +do_wpt_set_test(TestPath, Name, Cookie, Expected, Config) -> + do_wpt_set_test(TestPath, Name, Cookie, Expected, true, Config). + +do_wpt_set_test(TestPath, Name, Cookie, Expected, DefaultPath, Config) -> + ct:log("Name: ~s", [Name]), + 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?ttb=", cow_qs:urlencode(term_to_binary(Cookie))], + #{<<"host">> => ?WPT_HOST}), + {response, fin, 204, Headers} = gun:await(ConnPid, StreamRef1), + ct:log("Headers:~n~p", [Headers]), + #{cookie_store := Store} = gun:info(ConnPid), + ct:log("Store:~n~p", [Store]), + Result1 = case DefaultPath of + true -> + %% We do another request to get the cookie. + StreamRef2 = gun:get(ConnPid, "/cookie-echo", + #{<<"host">> => ?WPT_HOST}), + {response, nofin, 200, _} = gun:await(ConnPid, StreamRef2), + {ok, Body2} = gun:await_body(ConnPid, StreamRef2), + case Body2 of + <<"UNDEF">> -> <<>>; + _ -> Body2 + end; + false -> + %% We call this function to get a request header representation + %% of a cookie, similar to what document.cookie returns. + case gun_cookies:add_cookie_header( + case config(transport, Config) of + tcp -> <<"http">>; + tls -> <<"https">> + end, + <>, TestPath, [], Store) of + {[{<<"cookie">>, Result0}], _} -> + Result0; + {[], _} -> + <<>> + end + end, + Result = unicode:characters_to_binary(Result1), + ct:log("Expected:~n~p~nResult:~n~p", [Expected, Result]), + {Name, Cookie, Expected} = {Name, Cookie, Result}, + gun:close(ConnPid). -- cgit v1.2.3