From e37af7ac0caffc661def1593c55b212cc2f05d3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sun, 15 Mar 2020 18:41:48 +0100 Subject: Document the cookie store option and related modules Also contains a few small changes and Dialyzer fixes. --- doc/src/manual/gun.asciidoc | 13 ++ doc/src/manual/gun.info.asciidoc | 7 +- doc/src/manual/gun_app.asciidoc | 2 + doc/src/manual/gun_cookies.asciidoc | 179 +++++++++++++++++++++++ doc/src/manual/gun_cookies.domain_match.asciidoc | 52 +++++++ doc/src/manual/gun_cookies.path_match.asciidoc | 52 +++++++ doc/src/manual/gun_cookies_list.asciidoc | 55 +++++++ src/gun.erl | 2 +- src/gun_cookies.erl | 19 +-- src/gun_cookies_list.erl | 10 +- 10 files changed, 373 insertions(+), 18 deletions(-) create mode 100644 doc/src/manual/gun_cookies.asciidoc create mode 100644 doc/src/manual/gun_cookies.domain_match.asciidoc create mode 100644 doc/src/manual/gun_cookies.path_match.asciidoc create mode 100644 doc/src/manual/gun_cookies_list.asciidoc diff --git a/doc/src/manual/gun.asciidoc b/doc/src/manual/gun.asciidoc index c22f1ef..2067d96 100644 --- a/doc/src/manual/gun.asciidoc +++ b/doc/src/manual/gun.asciidoc @@ -251,6 +251,7 @@ Time between pings in milliseconds. ---- opts() :: #{ connect_timeout => timeout(), + cookie_store => gun_cookies:store(), domain_lookup_timeout => timeout(), http_opts => http_opts(), http2_opts => http2_opts(), @@ -276,6 +277,15 @@ connect_timeout (infinity):: Connection timeout. +cookie_store - see below:: + +The cookie store that Gun will use for this connection. +When configured, Gun will query the store for cookies +and include them in the request headers; and add cookies +found in response headers to the store. ++ +By default no cookie store will be used. + domain_lookup_timeout (infinity):: Domain lookup timeout. @@ -525,6 +535,9 @@ when receiving a ping. == Changelog +* *2.0*: The option `cookie_store` was added. It can be used + to configure a cookie store that Gun will use + automatically. * *2.0*: The types `protocols()` and `socks_opts()` have been added. Support for the Socks protocol has been added in every places where protocol selection is available. diff --git a/doc/src/manual/gun.info.asciidoc b/doc/src/manual/gun.info.asciidoc index 8f98942..095492e 100644 --- a/doc/src/manual/gun.info.asciidoc +++ b/doc/src/manual/gun.info.asciidoc @@ -6,8 +6,6 @@ gun:info - Obtain information about the connection == Description -// @todo Document the cookie_store key when documenting cookies. - [source,erlang] ---- info(ConnPid) -> Info @@ -22,7 +20,8 @@ Info :: #{ sock_port => inet:port_number(), origin_host => inet:hostname() | inet:ip_address(), origin_port => inet:port_number(), - intermediaries => [Intermediary] + intermediaries => [Intermediary], + cookie_store => gun_cookies:cookie_store() } Intermediary :: #{ type => connect | socks5, @@ -48,7 +47,7 @@ the connection. == Changelog -* *2.0*: The value `owner` was added. +* *2.0*: The values `owner` and `cookie_store` were added. * *1.3*: The values `socket`, `transport`, `protocol`, `origin_host`, `origin_port` and `intermediaries` were added. * *1.0*: Function introduced. diff --git a/doc/src/manual/gun_app.asciidoc b/doc/src/manual/gun_app.asciidoc index c369095..6ebbfe8 100644 --- a/doc/src/manual/gun_app.asciidoc +++ b/doc/src/manual/gun_app.asciidoc @@ -16,6 +16,8 @@ to the server and reconnects automatically when necessary. == Modules * link:man:gun(3)[gun(3)] - Asynchronous HTTP client +* link:man:gun_cookies(3)[gun_cookies(3)] - Cookie store engine +* link:man:gun_cookies_list(3)[gun_cookies_list(3)] - Cookie store backend: in-memory, per connection == Dependencies diff --git a/doc/src/manual/gun_cookies.asciidoc b/doc/src/manual/gun_cookies.asciidoc new file mode 100644 index 0000000..06f1daf --- /dev/null +++ b/doc/src/manual/gun_cookies.asciidoc @@ -0,0 +1,179 @@ += gun_cookies(3) + +== Name + +gun_cookies - Cookie store engine + +== Description + +The `gun_cookies` module implements a cookie store engine. +It will be used by Gun when a cookie store is configured. +It also defines the interface and provides functions used +to implement cookie store backends. + +== Callbacks + +Cookie store backends implement the following interface. +Functions are organized by theme: initialization, querying, +storing and garbage collecting: + +=== init + +[source,erlang] +---- +init(Opts :: any()) -> gun_cookies:store() +---- + +Initialize the cookie store. + +=== query + +[source,erlang] +---- +query(State, URI) -> {ok, [Cookie], State} + +URI :: uri_string:uri_map() +Cookie :: gun_cookies:cookie() +State :: any() +---- + +Query the store for the cookies for the given URI. + +=== set_cookie_secure_match + +[source,erlang] +---- +set_cookie_secure_match(State, Match) -> match | nomatch + +State :: any() +Match :: #{ + name := binary(), +% secure_only := true, + domain := binary(), + path := binary() +} +---- + +Perform a secure match against cookies already in the store. +This is part of the heuristics that the cookie store engine +applies to decide whether the cookie must be stored. + +The `secure_only` attribute is implied, it is not actually +passed in the argument. + +=== set_cookie_get_exact_match + +[source,erlang] +---- +set_cookie_get_exact_match(State, Match) + -> {ok, gun_cookies:cookie(), State} | error + +State :: any() +Match :: #{ + name := binary(), + domain := binary(), + host_only := boolean(), + path := binary() +} +---- + +Perform an exact match against cookies already in the store. +This is part of the heuristics that the cookie store engine +applies to decide whether the cookie must be stored. + +When a cookie is found, it must be returned so that it gets +updated. When nothing is found a new cookie will be stored. + +=== store + +[source,erlang] +---- +store(State, gun_cookies:cookie()) + -> {ok, State} | {error, any()} + +State :: any() +---- + +Unconditionally store the cookie into the cookie store. + +=== gc + +[source,erlang] +---- +gc(State) -> {ok, State} + +State :: any() +---- + +Remove all cookies from the cookie store that are expired. + +Other cookies may be removed as well, at the discretion +of the cookie store. For example excess cookies may be +removed to reduce the memory footprint. + +=== session_gc + +[source,erlang] +---- +session_gc(State) -> {ok, State} + +State :: any() +---- + +Remove all cookies from the cookie store that have the +`persistent` flag set to `false`. + +== Exports + +* link:man:gun_cookies:domain_match(3)[gun_cookies:domain_match(3)] - Cookie domain match +* link:man:gun_cookies:path_match(3)[gun_cookies:path_match(3)] - Cookie path match + +== Types + +=== cookie() + +[source,erlang] +---- +cookie() :: #{ + name := binary(), + value := binary(), + domain := binary(), + path := binary(), + creation_time := calendar:datetime(), + last_access_time := calendar:datetime(), + expiry_time := calendar:datetime() | infinity, + persistent := boolean(), + host_only := boolean(), + secure_only := boolean(), + http_only := boolean(), + same_site := strict | lax | none +} +---- + +A cookie. + +This contains the cookie name, value, attributes and flags. +This is the representation that the cookie store engine +and Gun expects. Cookies do not have to be kept in this +format in the cookie store backend. + +=== store() + +[source,erlang] +---- +store() :: {module(), StoreState :: any()} +---- + +The cookie store. + +This is a tuple containing the cookie store backend module +and its current state. + +== Changelog + +* *2.0*: Module introduced. + +== See also + +link:man:gun(7)[gun(7)], +link:man:gun_cookies_list(3)[gun_cookies_list(3)] diff --git a/doc/src/manual/gun_cookies.domain_match.asciidoc b/doc/src/manual/gun_cookies.domain_match.asciidoc new file mode 100644 index 0000000..34f52ec --- /dev/null +++ b/doc/src/manual/gun_cookies.domain_match.asciidoc @@ -0,0 +1,52 @@ += gun_cookies:domain_match(3) + +== Name + +gun_cookies:domain_match - Cookie domain match + +== Description + +[source,erlang] +---- +domain_match(String, DomainString) -> boolean() + +String :: binary() +DomainString :: binary() +---- + +Cookie domain match. + +This function can be used when implementing the +`set_cookie_secure_match` callback of a cookie store. + +== Arguments + +String:: + +The string to match. + +DomainString:: + +The domain string that will be matched against. + +== Return value + +Returns `true` when `String` domain-matches `DomainString`, +and `false` otherwise. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Perform a domain match +[source,erlang] +---- +Match = gun_cookies:domain_match(Domain, CookieDomain). +---- + +== See also + +link:man:gun_cookies(3)[gun_cookies(3)], +link:man:gun_cookies:path_match(3)[gun_cookies:path_match(3)] diff --git a/doc/src/manual/gun_cookies.path_match.asciidoc b/doc/src/manual/gun_cookies.path_match.asciidoc new file mode 100644 index 0000000..2e1771a --- /dev/null +++ b/doc/src/manual/gun_cookies.path_match.asciidoc @@ -0,0 +1,52 @@ += gun_cookies:path_match(3) + +== Name + +gun_cookies:path_match - Cookie path match + +== Description + +[source,erlang] +---- +path_match(ReqPath, CookiePath) -> boolean() + +ReqPath :: binary() +CookiePath :: binary() +---- + +Cookie path match. + +This function can be used when implementing the +`set_cookie_secure_match` callback of a cookie store. + +== Arguments + +ReqPath:: + +The request path to match. + +CookiePath:: + +The cookie path that will be matched against. + +== Return value + +Returns `true` when `ReqPath` path-matches `CookiePath`, +and `false` otherwise. + +== Changelog + +* *2.0*: Function introduced. + +== Examples + +.Perform a path match +[source,erlang] +---- +Match = gun_cookies:path_match(ReqPath, CookiePath). +---- + +== See also + +link:man:gun_cookies(3)[gun_cookies(3)], +link:man:gun_cookies:domain_match(3)[gun_cookies:domain_match(3)] diff --git a/doc/src/manual/gun_cookies_list.asciidoc b/doc/src/manual/gun_cookies_list.asciidoc new file mode 100644 index 0000000..2daef8e --- /dev/null +++ b/doc/src/manual/gun_cookies_list.asciidoc @@ -0,0 +1,55 @@ += gun_cookies_list(3) + +== Name + +gun_cookies_list - Cookie store backend: in-memory, per connection + +== Description + +The `gun_cookies_list` module implements a cookie store +backend that keeps all the cookie data in-memory and tied +to a specific connection. + +It is possible to implement a custom backend on top of +`gun_cookies_list` in order to add persistence or sharing +properties. + +== Exports + +This module implements the callbacks defined in +link:man:gun_cookies(3)[gun_cookies(3)]. + +== Types + +=== opts() + +[source,erlang] +---- +opts() :: #{ +} +---- + +Cookie store backend options. + +There are currently no options available for this backend. + +// The default value is given next to the option name: + +== Changelog + +* *2.0*: Module introduced. + +== Examples + +.Open a connection with a cookie store configured +[source,erlang] +---- +{ok, ConnPid} = gun:open(Host, Port, #{ + cookie_store => gun_cookies_list:init(#{}) +}) +---- + +== See also + +link:man:gun(7)[gun(7)], +link:man:gun_cookies(3)[gun_cookies(3)] diff --git a/src/gun.erl b/src/gun.erl index 7e468f3..b8030cf 100644 --- a/src/gun.erl +++ b/src/gun.erl @@ -1585,7 +1585,7 @@ owner_down(Reason, State) -> {stop, {shutdown, {owner_down, Reason}}, State}. terminate(Reason, StateName, #state{event_handler=EvHandler, event_handler_state=EvHandlerState, cookie_store=Store}) -> - case Store of + _ = case Store of undefined -> ok; %% Optimization: gun_cookies_list isn't a persistent cookie store. {gun_cookies_list, _} -> ok; diff --git a/src/gun_cookies.erl b/src/gun_cookies.erl index d4c5423..b8fa4da 100644 --- a/src/gun_cookies.erl +++ b/src/gun_cookies.erl @@ -39,7 +39,7 @@ last_access_time := calendar:datetime(), expiry_time := calendar:datetime() | infinity, persistent := boolean(), - host_only => boolean(), + host_only := boolean(), secure_only := boolean(), http_only := boolean(), same_site := strict | lax | none @@ -49,7 +49,7 @@ -callback init(any()) -> store(). -callback query(State, uri_string:uri_map()) - -> {ok, [{binary(), binary()}], State} + -> {ok, [gun_cookies:cookie()], State} when State::store_state(). -callback set_cookie_secure_match(store_state(), #{ @@ -59,12 +59,13 @@ path := binary() }) -> match | nomatch. --callback set_cookie_exact_match(store_state(), #{ +-callback set_cookie_get_exact_match(State, #{ name := binary(), domain := binary(), host_only := boolean(), path := binary() -}) -> {match, cookie()} | nomatch. +}) -> {ok, cookie(), State} | error + when State::store_state(). -callback store(State, cookie()) -> {ok, State} | {error, any()} @@ -137,7 +138,7 @@ path_match_test_() -> %% @todo The given URI must be normalized. -spec query(Store, uri_string:uri_map()) - -> {ok, [{binary(), binary()}], Store} + -> {ok, [cookie()], Store} when Store::store(). query({Mod, State0}, URI) -> {ok, Cookies0, State} = Mod:query(State0, URI), @@ -288,7 +289,7 @@ set_cookie3(Store, Attrs, Cookie=#{name := Name, set_cookie_store(Store0, Cookie) -> Match = maps:with([name, domain, host_only, path], Cookie), - case set_cookie_take_exact_match(Store0, Match) of + case set_cookie_get_exact_match(Store0, Match) of {ok, #{creation_time := CreationTime}, Store} -> %% This is where we would reject a new non-HTTP cookie %% if the OldCookie has http_only set to true. @@ -297,8 +298,8 @@ set_cookie_store(Store0, Cookie) -> store(Store0, Cookie) end. -set_cookie_take_exact_match({Mod, State0}, Match) -> - case Mod:set_cookie_take_exact_match(State0, Match) of +set_cookie_get_exact_match({Mod, State0}, Match) -> + case Mod:set_cookie_get_exact_match(State0, Match) of {ok, Cookie, State} -> {ok, Cookie, {Mod, State}}; Error -> @@ -624,5 +625,5 @@ wpt_secure_http_test() -> {error, secure_scheme_only} = set_cookie(gun_cookies_list:init(), URIMap, N, V, A), ok. -%% @todo WPT: secure/set-from-ws* - Anything special required? +%% WPT: secure/set-from-ws* (Anything special required?) -endif. diff --git a/src/gun_cookies_list.erl b/src/gun_cookies_list.erl index e8cf17a..77cc236 100644 --- a/src/gun_cookies_list.erl +++ b/src/gun_cookies_list.erl @@ -15,12 +15,13 @@ %% A reference cookie store implemented as a list of cookies. %% This cookie store cannot be shared between connections. -module(gun_cookies_list). +-behavior(gun_cookies). -export([init/0]). -export([init/1]). -export([query/2]). -export([set_cookie_secure_match/2]). --export([set_cookie_take_exact_match/2]). +-export([set_cookie_get_exact_match/2]). -export([store/2]). -export([gc/1]). -export([session_gc/1]). @@ -33,6 +34,7 @@ -type opts() :: #{ }. +-export_type([opts/0]). -spec init() -> {?MODULE, state()}. init() -> @@ -43,7 +45,7 @@ init(_Opts) -> {?MODULE, #{cookies => []}}. -spec query(State, uri_string:uri_map()) - -> {ok, [{binary(), binary()}], State} + -> {ok, [gun_cookies:cookie()], State} when State::state(). query(State=#{cookies := Cookies}, URI) -> CurrentTime = erlang:universaltime(), @@ -100,13 +102,13 @@ set_cookie_secure_match(#{cookies := Cookies}, _ -> match end. --spec set_cookie_take_exact_match(State, #{ +-spec set_cookie_get_exact_match(State, #{ name := binary(), domain := binary(), host_only := boolean(), path := binary() }) -> {ok, gun_cookies:cookie(), State} | error when State::state(). -set_cookie_take_exact_match(State=#{cookies := Cookies0}, Match) -> +set_cookie_get_exact_match(State=#{cookies := Cookies0}, Match) -> Result = [Cookie || Cookie <- Cookies0, Match =:= maps:with([name, domain, host_only, path], Cookie)], Cookies = [Cookie || Cookie <- Cookies0, -- cgit v1.2.3