aboutsummaryrefslogtreecommitdiffstats
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/gun_test.erl4
-rw-r--r--test/handlers/cookie_echo_h.erl11
-rw-r--r--test/handlers/cookie_parser_h.erl23
-rw-r--r--test/handlers/cookie_parser_result_h.erl25
-rw-r--r--test/handlers/cookie_set_h.erl36
-rw-r--r--test/handlers/ws_cookie_h.erl24
-rw-r--r--test/rfc6265bis_SUITE.erl499
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.