Skip to content

Commit

Permalink
feat(hb_http_signature): implemented extracting field from msg and en…
Browse files Browse the repository at this point in the history
…code within signature base #13
  • Loading branch information
TillaTheHun0 committed Nov 27, 2024
1 parent 7964a5e commit 9c95286
Showing 1 changed file with 147 additions and 35 deletions.
182 changes: 147 additions & 35 deletions src/hb_http_signature.erl
Original file line number Diff line number Diff line change
Expand Up @@ -88,46 +88,115 @@ signature_params_line(ComponentIdentifiers, SigParams) when is_list(SigParams) -
bin(Res).

identifier_to_component(Identifier = <<"@", _R/bits>>, Req, Res) -> derive_component(Identifier, Req, Res);
identifier_to_component(Identifier, Req, Res) -> extract_header(Identifier, Req, Res).
identifier_to_component(Identifier, Req, Res) -> extract_field(Identifier, Req, Res).

extract_header(Identifier, Req, Res = #{}) ->
extract_header(Identifier, Req, Res, req);
extract_header(Identifier, Req, Res) ->
extract_header(Identifier, Req, Res, res).
extract_header(Identifier, Req, Res, _Subject) ->
extract_field(Identifier, Req, Res = #{}) ->
extract_field(Identifier, Req, Res, req);
extract_field(Identifier, Req, Res) ->
extract_field(Identifier, Req, Res, res).
extract_field(Identifier, Req, Res, _Subject) ->
% The Identifier may have params and so we need to parse it
% See https://datatracker.ietf.org/doc/html/rfc9421#section-2.2-6
{item, {string, IParsed}, IParams} = sf_item(Identifier),
IsStrictFormat = get_sf_item_param(<<"sf">>, IParams, false) andalso true,
IsByteSequenceEncoded = get_sf_item_param(<<"bs">>, IParams, false) andalso true,
DictKey = get_sf_item_param(<<"key">>, IParams, false),
{IParsed, IParams} = sf_item(Identifier),
[IsStrictFormat, IsByteSequenceEncoded, DictKey] = [
find_sf_strict_format_param(IParams),
find_sf_byte_sequence_param(IParams),
find_sf_key_param(IParams)
],
case (IsStrictFormat orelse DictKey) andalso IsByteSequenceEncoded of
true ->
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.5-7.2.2.5.2.2
{conflicting_params_error, <<"Component Identifier MUST not contain both 'sf' and 'bs' parameters">>};
{conflicting_params_error, <<"Component Identifier parameter 'bs' MUST not be used with 'sf' or 'key'">>};
_ ->
Lowered = lower_bin(IParsed),
IsRequestIdentifier = get_sf_item_param(<<"req">>, IParams, false),
Msg = IsRequestIdentifier andalso Req orelse Res,
% Fields are case-insensitive, so we perform a search across the Msg fields
MaybeField = lists:keyfind(Lowered, 1, [
{lower_bin(Key), Value}
|| {Key, Value} <- maps:to_list(maps:get(fields, Msg, #{}))
]),
case MaybeField of
false ->
NormalizedItem = hb_http_structured_fields:item({item, {string, Lowered}, IParams}),
[IsRequestIdentifier, IsTrailerField] = [find_sf_request_param(IParams), find_sf_trailer_param(IParams)],
% There may be multiple fields that match the identifier on the Msg,
% so we filter, instead of find
MaybeRawFields = lists:filter(
fun({Key, _Value}) -> Key =:= Lowered end,
% Fields are case-insensitive, so we perform a case-insensitive search across the Msg fields
[
{lower_bin(Key), Value}
|| {Key, Value} <- maps:to_list(
maps:get(
% The field will almost certainly be a header, but could also be a trailer
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.1-18.10.1
IsTrailerField andalso trailers orelse headers,
% The header may exist on any message in the context of the signature
% which could be the Request or Response Message
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.1-18.8.1
IsRequestIdentifier andalso Req orelse Res,
#{}
)
)
]
),
case MaybeRawFields of
[] ->
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.5-7.2.2.5.2.6
{field_not_found_error, <<"Component Identifier for a field MUST be present on the message">>};
_ ->
FieldPairs ->
% The Field was found, but we still need to potentially parse it
% (it could be a Structured Field) and potentially extract
% subsequent values ie. specific dictionary key and its parameters
% TODO: implement
ok
% subsequent values ie. specific dictionary key and its parameters, or further encode it
Extracted = extract_field_value(
[bin(Value) || {_Key, Value} <- FieldPairs],
[DictKey, IsStrictFormat, IsByteSequenceEncoded]
),
{ok, {NormalizedItem, bin(Extracted)}}
end,
ok
end.

extract_field_value(RawFields, [Key, IsStrictFormat, IsByteSequenceEncoded]) ->
% TODO: (maybe this already works?) empty string for empty header
case not (Key orelse IsStrictFormat orelse IsByteSequenceEncoded) of
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.1-5
true ->
Normalized = [trim_and_normalize(Field) || Field <- RawFields],
bin(lists:join(<<", ">>, Normalized));
_ ->
case IsByteSequenceEncoded of
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.1.3-2
true ->
SfList = [
{item, {binary, trim_and_normalize(Field)}, {}}
|| Field <- RawFields
],
hb_http_structured_fields:list(SfList);
_ ->
Combined = bin(lists:join(<<", ">>, RawFields)),
case sf_parse(Combined) of
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.1.1-3
{error, _} ->
{sf_parsing_error, <<"Component Identifier value could not be parsed as a structured field">>};
{ok, SF} ->
case Key of
% Not accessing a key, so just re-serialize, which should
% properly format the data in Strict-Formatting style
false -> sf_serialize(SF);
_ -> extract_dictionary_field_value(SF, Key)
end
end
end
end.

extract_dictionary_field_value(StructuredField = [Elem | _Rest], Key) ->
case Elem of
{Name, _} when is_binary(Name) ->
case lists:keyfind(Key, 1, StructuredField) of
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.1.2-5
false ->
{sf_key_not_found_error, <<"Component Identifier references key not found in dictionary structured field">>};
{_, Value} ->
sf_serialize(Value)
end,
ok;
_ ->
{sf_not_dictionary_error, <<"Component Identifier cannot reference key on a non-dictionary structured field">>}
end.

derive_component(Identifier, Req, Res = #{}) ->
derive_component(Identifier, Req, Res, req);
derive_component(Identifier, Req, Res) ->
Expand All @@ -139,8 +208,8 @@ derive_component(Identifier, Req, Res, Subject) when is_atom(Identifier) ->
derive_component(Identifier, Req, Res, Subject) when is_binary(Identifier) ->
% The Identifier may have params and so we need to parse it
% See https://datatracker.ietf.org/doc/html/rfc9421#section-2.2-6
{item, {string, IParsed}, IParams} = sf_item(Identifier),
case get_sf_item_param(<<"req">>, IParams, false) andalso Subject =:= req of
{IParsed, IParams} = sf_item(Identifier),
case find_sf_request_param(IParams) andalso Subject =:= req of
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.5-7.2.2.5.2.3
true ->
{req_identifier_error,
Expand Down Expand Up @@ -190,12 +259,14 @@ derive_component(Identifier, Req, Res, Subject) when is_binary(Identifier) ->
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.2-4.14.1
<<"@query">> ->
URI = uri_string:parse(maps:get(url, Req)),
% No query params results in a "?" value
% See https://datatracker.ietf.org/doc/html/rfc9421#section-2.2.7-14
Query = maps:get(query, URI, ?EMPTY_QUERY_PARAMS),
{ok, {NormalizedItem, bin(Query)}};
% https://datatracker.ietf.org/doc/html/rfc9421#section-2.2-4.16.1
<<"@query-param">> ->
URI = uri_string:parse(maps:get(url, Req)),
case get_sf_item_param(<<"name">>, IParams, false) of
case find_sf_name_param(IParams) of
% The name parameter MUST be provided when specifiying a @query-param
% Derived Component. See https://datatracker.ietf.org/doc/html/rfc9421#section-2.2.8-1
false ->
Expand Down Expand Up @@ -227,26 +298,67 @@ derive_component(Identifier, Req, Res, Subject) when is_binary(Identifier) ->
end.

%%%
%%% Data Utilities
%%% Strucutured Field Utilities
%%%

sf_item(ComponentIdentifier = {item, _Item, _Params}) ->
ComponentIdentifier;
sf_parse(Raw) when is_list(Raw) -> sf_parse(list_to_binary(Raw));
sf_parse(Raw) when is_binary(Raw) ->
Parsers = [],
sf_parse(Parsers, Raw).

sf_parse([], _Raw) ->
{error, undefined};
sf_parse([Parser | Rest], Raw) ->
case catch Parser(Raw) of
% skip parsers that fail
{'EXIT', _} -> sf_parse(Rest, Raw);
Parsed -> {ok, Parsed}
end.

sf_serialize(StructuredField = {item, _, _}) ->
hb_http_structured_fields:item(StructuredField);
sf_serialize(StructuredField = [Elem | _Rest]) ->
case Elem of
{Name, _} when is_binary(Name) -> hb_http_structured_fields:dictionary(StructuredField);
_ -> hb_http_structured_fields:list(StructuredField)
end.

sf_item({item, {_Kind, Parsed}, Params}) ->
{Parsed, Params};
% TODO: should we check whether the string is already quoted?
sf_item(ComponentIdentifier) when is_list(ComponentIdentifier) ->
sf_item(<<$", (lower_bin(ComponentIdentifier))/binary, $">>);
sf_item(ComponentIdentifier) when is_binary(ComponentIdentifier) ->
hb_http_structured_fields:parse_item(ComponentIdentifier).
sf_item(hb_http_structured_fields:parse_item(ComponentIdentifier)).

get_sf_item_param(Name, SfItemParams, Default) when is_list(Name) ->
get_sf_item_param(list_to_binary(Name), SfItemParams, Default);
get_sf_item_param(Name, SfItemParams, Default) ->
find_sf_param(Name, Params, Default) when is_list(Name) ->
find_sf_param(list_to_binary(Name), Params, Default);
find_sf_param(Name, Params, Default) ->
% [{<<"name">>,{string,<<"baz">>}}]
case lists:keyfind(Name, 1, SfItemParams) of
case lists:keyfind(Name, 1, Params) of
{_, {_, Value}} -> Value;
_ -> Default
end.

%%%
%%% https://datatracker.ietf.org/doc/html/rfc9421#section-6.5.2-1
%%% using functions allows encapsulating default values
%%%
find_sf_strict_format_param(Params) -> find_sf_param(<<"sf">>, Params, false).
find_sf_key_param(Params) -> find_sf_param(<<"key">>, Params, false).
find_sf_byte_sequence_param(Params) -> find_sf_param(<<"bs">>, Params, false).
find_sf_trailer_param(Params) -> find_sf_param(<<"tr">>, Params, false).
find_sf_request_param(Params) -> find_sf_param(<<"req">>, Params, false).
find_sf_name_param(Params) -> find_sf_param(<<"name">>, Params, false).

%%%
%%% Data Utilities
%%%

% https://datatracker.ietf.org/doc/html/rfc9421#section-2.1-5
trim_and_normalize(Bin) ->
binary:replace(binary:trim(Bin), <<$\n>>, <<" ">>, [global]).

upper_bin(Item) when is_binary(Item) -> upper_bin(binary_to_list(Item));
upper_bin(Item) when is_list(Item) -> bin(string:uppercase(Item)).

Expand Down

0 comments on commit 9c95286

Please sign in to comment.