dudeswave2/gun/test/rfc6265bis_SUITE.erl

957 lines
33 KiB
Erlang

%% Copyright (c) 2020-2023, Loïc Hoguin <essen@ninenines.eu>
%%
%% 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),
[
{http, [parallel], CommonTests},
{https, [parallel], CommonTests},
%% Websocket over HTTP/2 is currently not supported.
{h2c, [parallel], (CommonTests -- [wpt_secure_ws])},
{h2, [parallel], (CommonTests -- [wpt_secure_ws])}
].
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),
[{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),
[{transport, tls}, {protocol, Protocol}|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, []},
{"/informational", cookie_informational_h, []},
{"/ws", ws_cookie_h, []}
]}
].
%% Tests.
dont_ignore_informational_set_cookie(Config) ->
doc("User agents may accept set-cookie headers "
"sent in informational responses. (RFC6265bis 3)"),
[{<<"informational">>, <<"1">>}, {<<"final">>, <<"1">>}]
= do_informational_set_cookie(Config, false).
ignore_informational_set_cookie(Config) ->
doc("User agents may ignore set-cookie headers "
"sent in informational responses. (RFC6265bis 3)"),
[{<<"final">>, <<"1">>}]
= do_informational_set_cookie(Config, true).
do_informational_set_cookie(Config, Boolean) ->
Protocol = config(protocol, Config),
{ok, ConnPid} = gun:open("localhost", config(port, Config), #{
transport => config(transport, Config),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
protocols => [{Protocol, #{cookie_ignore_informational => Boolean}}],
cookie_store => gun_cookies_list:init()
}),
{ok, Protocol} = gun:await_up(ConnPid),
StreamRef1 = gun:get(ConnPid, "/informational"),
{inform, 103, Headers1} = gun:await(ConnPid, StreamRef1),
ct:log("Headers1:~n~p", [Headers1]),
{response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef1),
ct:log("Headers2:~n~p", [Headers2]),
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]),
Res = cow_cookie:parse_cookie(Body2),
gun:close(ConnPid),
Res.
set_cookie_connect_tcp(Config) ->
doc("Cookies may also be set in responses going through CONNECT tunnels."),
Transport = config(transport, Config),
Protocol = config(protocol, Config),
{ok, ProxyPid, ProxyPort} = event_SUITE:do_proxy_start(Protocol, tcp),
{ok, ConnPid} = gun:open("localhost", ProxyPort, #{
transport => tcp,
protocols => [Protocol],
cookie_store => gun_cookies_list:init()
}),
{ok, Protocol} = gun:await_up(ConnPid),
tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid),
StreamRef1 = gun:connect(ConnPid, #{
host => "localhost",
port => config(port, Config),
transport => Transport,
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
protocols => [Protocol]
}),
{response, fin, 200, _} = gun:await(ConnPid, StreamRef1),
{up, Protocol} = gun:await(ConnPid, StreamRef1),
StreamRef2 = gun:get(ConnPid, "/cookie-set?prefix", #{
<<"please-set-cookie">> => <<"a=b">>
}, #{tunnel => StreamRef1}),
{response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef2),
ct:log("Headers2:~n~p", [Headers2]),
StreamRef3 = gun:get(ConnPid, "/cookie-echo", [], #{tunnel => StreamRef1}),
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef3),
{ok, Body3} = gun:await_body(ConnPid, StreamRef3),
ct:log("Body3:~n~p", [Body3]),
[{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body3),
gun:close(ConnPid).
set_cookie_connect_tls(Config) ->
doc("Cookies may also be set in responses going through CONNECT tunnels."),
Transport = config(transport, Config),
Protocol = config(protocol, Config),
{ok, ProxyPid, ProxyPort} = event_SUITE:do_proxy_start(Protocol, tls),
{ok, ConnPid} = gun:open("localhost", ProxyPort, #{
transport => tls,
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
protocols => [Protocol],
cookie_store => gun_cookies_list:init()
}),
{ok, Protocol} = gun:await_up(ConnPid),
tunnel_SUITE:do_handshake_completed(Protocol, ProxyPid),
StreamRef1 = gun:connect(ConnPid, #{
host => "localhost",
port => config(port, Config),
transport => Transport,
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
protocols => [Protocol]
}),
{response, fin, 200, _} = gun:await(ConnPid, StreamRef1),
{up, Protocol} = gun:await(ConnPid, StreamRef1),
StreamRef2 = gun:get(ConnPid, "/cookie-set?prefix", #{
<<"please-set-cookie">> => <<"a=b">>
}, #{tunnel => StreamRef1}),
{response, fin, 204, Headers2} = gun:await(ConnPid, StreamRef2),
ct:log("Headers2:~n~p", [Headers2]),
StreamRef3 = gun:get(ConnPid, "/cookie-echo", [], #{tunnel => StreamRef1}),
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef3),
{ok, Body3} = gun:await_body(ConnPid, StreamRef3),
ct:log("Body3:~n~p", [Body3]),
[{<<"a">>, <<"b">>}] = cow_cookie:parse_cookie(Body3),
gun:close(ConnPid).
%% 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_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_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_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_wpt_domain_test(Config, "domain_missing"),
ok.
do_wpt_domain_test(Config, TestCase) ->
Protocol = config(protocol, Config),
{ok, ConnPid} = gun:open("localhost", config(port, Config), #{
transport => config(transport, Config),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
protocols => [Protocol],
cookie_store => gun_cookies_list:init()
}),
{ok, Protocol} = gun:await_up(ConnPid),
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">> => ?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." ?WPT_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: 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),
{ok, ConnPid} = gun:open("localhost", config(port, Config), #{
transport => config(transport, Config),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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
%%
%% path/match.html
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),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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
%%
%% 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=" ?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=" ?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=" ?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 -> [
{<<"__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=" ?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,
_ = [do_wpt_prefix_common(Config, TestCase, Expected, <<"__Host-foo">>)
|| {TestCase, Expected} <- Tests],
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=" ?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=" ?WPT_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),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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">> => ?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">> => ?WPT_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/ (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
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),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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*
%%
%% 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
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),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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.
%% 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 = <Paste>
%% 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),
tls_opts => [{verify, verify_none}, {versions, ['tlsv1.2']}],
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,
<<?WPT_HOST>>, 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).