Packages
oidcc
3.2.4
3.7.2
3.7.1
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
retired
3.4.0
3.3.0
3.2.6
3.2.5
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
3.2.0-beta.3
3.2.0-beta.2
retired
3.2.0-beta.1
retired
3.1.2
3.1.2-beta.1
retired
3.1.1
retired
3.1.0
retired
3.1.0-beta.2
retired
3.1.0-beta.1
retired
3.0.2
3.0.1
retired
3.0.0
retired
3.0.0-rc.6
retired
3.0.0-rc.5
retired
3.0.0-rc.4
retired
3.0.0-rc.3
retired
3.0.0-rc.2
retired
3.0.0-rc.1
retired
3.0.0-alpha.5
retired
3.0.0-alpha.4
retired
3.0.0-alpha.3
retired
3.0.0-alpha.2
retired
3.0.0-alpha.1
retired
2.0.0-alpha.2
retired
2.0.0-alpha.1
retired
1.8.1
retired
1.8.0
retired
1.7.0
retired
1.6.0
retired
1.5.1
retired
1.5.0
retired
1.4.0
retired
1.3.0
retired
1.2.1
retired
1.2.0
retired
1.1.0
retired
1.0.1
retired
1.0.0
retired
OpenID Connect client library for the BEAM.
Current section
Files
Jump to
Current section
Files
src/oidcc_http_util.erl
-module(oidcc_http_util).
-feature(maybe_expr, enable).
-include("internal/doc.hrl").
?MODULEDOC("HTTP Client Utilities").
-export([basic_auth_header/2]).
-export([bearer_auth_header/1]).
-export([headers_to_cache_deadline/2]).
-export([request/4]).
-export_type([
http_header/0, error/0, httpc_error/0, query_params/0, telemetry_opts/0, request_opts/0
]).
?DOC("See `uri_string:compose_query/1`.").
?DOC(#{since => <<"3.0.0">>}).
-type query_params() :: [{unicode:chardata(), unicode:chardata() | true}].
?DOC("See `httpc:request/5`.").
?DOC(#{since => <<"3.0.0">>}).
-type http_header() :: {Field :: [byte()] | binary(), Value :: iodata()}.
?DOC(#{since => <<"3.0.0">>}).
-type error() ::
{http_error, StatusCode :: pos_integer(), HttpBodyResult :: binary() | map()}
| {use_dpop_nonce, Nonce :: binary(), HttpBodyResult :: binary() | map()}
| invalid_content_type
| httpc_error().
?DOC("See `httpc:request/5` for additional errors.").
?DOC(#{since => <<"3.0.0">>}).
-type httpc_error() :: term().
?DOC("""
See `httpc:request/5`.
## Parameters
* `timeout` - timeout for request
* `ssl` - TLS config
""").
?DOC(#{since => <<"3.0.0">>}).
-type request_opts() :: #{
timeout => timeout(),
ssl => [ssl:tls_option()],
httpc_profile => atom() | pid()
}.
?DOC(#{since => <<"3.0.0">>}).
-type telemetry_opts() :: #{
topic := [atom()],
extra_meta => map()
}.
?DOC(false).
-spec basic_auth_header(User, Secret) -> http_header() when
User :: binary(),
Secret :: binary().
basic_auth_header(User, Secret) ->
UserEnc = uri_string:compose_query([{User, true}]),
SecretEnc = uri_string:compose_query([{Secret, true}]),
RawAuth = <<UserEnc/binary, <<":">>/binary, SecretEnc/binary>>,
AuthData = base64:encode(RawAuth),
{"authorization", [<<"Basic ">>, AuthData]}.
?DOC(false).
-spec bearer_auth_header(Token) -> http_header() when Token :: binary().
bearer_auth_header(Token) ->
{"authorization", [<<"Bearer ">>, Token]}.
?DOC(false).
-spec request(Method, Request, TelemetryOpts, RequestOpts) ->
{ok, {{json, term()} | {jwt, binary()}, [http_header()]}}
| {error, error()}
when
Method :: head | get | put | patch | post | trace | options | delete,
Request ::
{uri_string:uri_string(), [http_header()]}
| {
uri_string:uri_string(),
[http_header()],
ContentType :: uri_string:uri_string(),
HttpBody
},
HttpBody ::
iolist()
| binary()
| {
fun((Accumulator :: term()) -> eof | {ok, iolist(), Accumulator :: term()}),
Accumulator :: term()
}
| {chunkify, fun((Accumulator :: term()) -> eof | {ok, iolist(), Accumulator :: term()}),
Accumulator :: term()},
TelemetryOpts :: telemetry_opts(),
RequestOpts :: request_opts().
request(Method, Request, TelemetryOpts, RequestOpts) ->
TelemetryTopic = maps:get(topic, TelemetryOpts),
TelemetryExtraMeta = maps:get(extra_meta, TelemetryOpts, #{}),
Timeout = maps:get(timeout, RequestOpts, timer:minutes(1)),
SslOpts = maps:get(ssl, RequestOpts, undefined),
HttpProfile = maps:get(httpc_profile, RequestOpts, default),
HttpOpts0 = [{timeout, Timeout}],
HttpOpts =
case SslOpts of
undefined -> HttpOpts0;
_Opts -> [{ssl, SslOpts} | HttpOpts0]
end,
telemetry:span(
TelemetryTopic,
TelemetryExtraMeta,
fun() ->
maybe
{ok, {_StatusLine, Headers, _Result} = Response} ?=
httpc:request(
Method,
Request,
HttpOpts,
[{body_format, binary}],
HttpProfile
),
{ok, BodyAndFormat} ?= extract_successful_response(Response),
{{ok, {BodyAndFormat, Headers}}, TelemetryExtraMeta}
else
{error, Reason} ->
{{error, Reason}, maps:put(error, Reason, TelemetryExtraMeta)}
end
end
).
-spec extract_successful_response({StatusLine, [HttpHeader], HttpBodyResult}) ->
{ok, {json, term()} | {jwt, binary()}} | {error, error()}
when
StatusLine :: {HttpVersion, StatusCode, string()},
HttpVersion :: uri_string:uri_string(),
StatusCode :: pos_integer(),
HttpHeader :: http_header(),
HttpBodyResult :: binary().
extract_successful_response({{_HttpVersion, Status, _HttpStatusName}, Headers, HttpBodyResult}) when
Status == 200 orelse Status == 201
->
case fetch_content_type(Headers) of
json ->
{ok, {json, jose:decode(HttpBodyResult)}};
jwt ->
{ok, {jwt, HttpBodyResult}};
unknown ->
{error, invalid_content_type}
end;
extract_successful_response({{_HttpVersion, StatusCode, _HttpStatusName}, Headers, HttpBodyResult}) ->
Body =
case fetch_content_type(Headers) of
json ->
jose:decode(HttpBodyResult);
jwt ->
HttpBodyResult;
unknown ->
HttpBodyResult
end,
case proplists:lookup("dpop-nonce", Headers) of
{"dpop-nonce", DpopNonce} ->
{error, {use_dpop_nonce, iolist_to_binary(DpopNonce), Body}};
_ ->
{error, {http_error, StatusCode, Body}}
end.
-spec fetch_content_type(Headers) -> json | jwt | unknown when Headers :: [http_header()].
fetch_content_type(Headers) ->
case proplists:lookup("content-type", Headers) of
{"content-type", "application/jwk-set+json" ++ _Rest} ->
json;
{"content-type", "application/json" ++ _Rest} ->
json;
{"content-type", "application/jwt" ++ _Rest} ->
jwt;
_Other ->
unknown
end.
-spec headers_to_cache_deadline(Headers, DefaultExpiry) -> pos_integer() when
Headers :: [{Header :: binary(), Value :: binary()}], DefaultExpiry :: non_neg_integer().
headers_to_cache_deadline(Headers, DefaultExpiry) ->
case proplists:lookup("cache-control", Headers) of
{"cache-control", Cache} ->
try
cache_deadline(Cache, DefaultExpiry)
catch
_:_ ->
DefaultExpiry
end;
none ->
DefaultExpiry
end.
-spec cache_deadline(Cache :: iodata(), Fallback :: pos_integer()) -> pos_integer().
cache_deadline(Cache, Fallback) ->
Entries =
binary:split(iolist_to_binary(Cache), [<<",">>, <<"=">>, <<" ">>], [global, trim_all]),
MaxAge =
fun
(<<"0">>, true) ->
Fallback;
(Entry, true) ->
erlang:convert_time_unit(binary_to_integer(Entry), second, millisecond);
(<<"max-age">>, _) ->
true;
(_, Res) ->
Res
end,
lists:foldl(MaxAge, Fallback, Entries).