diff options
Diffstat (limited to 'test/rfc6265bis_SUITE.erl')
-rw-r--r-- | test/rfc6265bis_SUITE.erl | 499 |
1 files changed, 499 insertions, 0 deletions
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. |