Skip to content

Commit

Permalink
feat(msg-to-http): wip converting hb msg to http msg #13
Browse files Browse the repository at this point in the history
  • Loading branch information
TillaTheHun0 committed Dec 16, 2024
1 parent 9a360d4 commit 908b3bb
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 53 deletions.
22 changes: 0 additions & 22 deletions src/hb_http_message.erl

This file was deleted.

91 changes: 66 additions & 25 deletions src/hb_http_signature.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@

-module(hb_http_signature).

% Signing/Verifying
-export([authority/3, sign/2, sign/3, verify/2, verify/3]).

% Mapping
-export([sf_signature/1, sf_signature_params/2, sf_signature_param/1]).

% https://datatracker.ietf.org/doc/html/rfc9421#section-2.2.7-14
-define(EMPTY_QUERY_PARAMS, $?).
% https://datatracker.ietf.org/doc/html/rfc9421#name-signature-parameters
-define(SIGNATURE_PARAMS, [created, expired, nonce, alg, keyid, tag]).

-include("include/hb.hrl").

Expand Down Expand Up @@ -221,34 +227,11 @@ signature_components_line(ComponentIdentifiers, Req, Res) ->
ComponentsLine = lists:join(<<"\n">>, ComponentsLines),
bin(ComponentsLine).

%%%
%%% @doc construct the "signature-params-line" part of the signature base.
%%%
%%% See https://datatracker.ietf.org/doc/html/rfc9421#section-2.5-7.3.2.4
signature_params_line(ComponentIdentifiers, SigParams) when is_map(SigParams) ->
AsList = maps:to_list(SigParams),
Sorted = lists:sort(fun({Key1, _}, {Key2, _}) -> Key1 < Key2 end, AsList),
signature_params_line(ComponentIdentifiers, Sorted);
signature_params_line(ComponentIdentifiers, SigParams) when is_list(SigParams) ->
SfList = [
{
list,
lists:map(
fun(ComponentIdentifier) ->
{item, {_Kind, Value}, Params} = sf_item(ComponentIdentifier),
{item, {string, lower_bin(Value)}, Params}
end,
ComponentIdentifiers
),
lists:map(
fun
({K, V}) when is_integer(V) -> {bin(K), V};
({K, V}) -> {bin(K), {string, bin(V)}}
end,
SigParams
)
}
],
signature_params_line(ComponentIdentifiers, SigParams) ->
SfList = sf_signature_params(ComponentIdentifiers, SigParams),
Res = hb_http_structured_fields:list(SfList),
bin(Res).

Expand Down Expand Up @@ -505,6 +488,64 @@ derive_component({item, {_Kind, IParsed}, IParams}, Req, Res, Subject) ->
%%% Strucutured Field Utilities
%%%

%%% @doc construct the structured field Parameter for the signature parameter,
%%% checking whether the parameter name is valid according RFC-9421
%%%
%%% See https://datatracker.ietf.org/doc/html/rfc9421#section-2.3-3
sf_signature_param({Name, Param}) ->
NormalizedName = bin(Name),
NormalizedNames = lists:map(fun bin/1, ?SIGNATURE_PARAMS),
case lists:member(NormalizedName, NormalizedNames) of
false -> {unknown_signature_param, NormalizedName};
% all signature params are either integer or string values
true -> case Param of
I when is_integer(I) -> {ok, {NormalizedName, Param}};
P when is_atom(P) orelse is_list(P) orelse is_binary(P) -> {ok, {NormalizedName, {string, bin(P)}}};
P -> {invalid_signature_param_value, P}
end
end.

%%% @doc construct the structured field List for the
%%% "signature-params-line" part of the signature base.
%%%
%%% Can be parsed into a binary by simply passing to hb_structured_fields:list/1
%%%
%%% See https://datatracker.ietf.org/doc/html/rfc9421#section-2.5-7.3.2.4
sf_signature_params(ComponentIdentifiers, SigParams) when is_map(SigParams) ->
AsList = maps:to_list(SigParams),
Sorted = lists:sort(fun({Key1, _}, {Key2, _}) -> Key1 < Key2 end, AsList),
sf_signature_params(ComponentIdentifiers, Sorted);
sf_signature_params(ComponentIdentifiers, SigParams) when is_list(SigParams) ->
[
{
list,
lists:map(
fun(ComponentIdentifier) ->
{item, {_Kind, Value}, Params} = sf_item(ComponentIdentifier),
{item, {string, lower_bin(Value)}, Params}
end,
ComponentIdentifiers
),
lists:foldl(
fun (RawParam, Params) ->
case sf_signature_param(RawParam) of
{ok, Param} -> Params ++ [Param];
% Ignore unknown signature parameters
{unknown_signature_param, _} -> Params
% TODO: what to do about invalid_signature_param_value?
% For now will cause badmatch
end
end,
[],
SigParams
)
}
].

% TODO: should this also handle the Signature already being encoded?
sf_signature(Signature) ->
{item, {binary, Signature}, []}.

%%% @doc Attempt to parse the binary into a data structure that represents
%%% an HTTP Structured Field.
%%%
Expand Down
88 changes: 82 additions & 6 deletions src/hb_http_structured_fields.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,9 @@

-module(hb_http_structured_fields).

-export([parse_dictionary/1]).
-export([parse_item/1]).
-export([parse_list/1]).
-export([dictionary/1]).
-export([item/1]).
-export([list/1]).
-export([parse_dictionary/1, parse_item/1, parse_list/1]).
-export([dictionary/1, item/1, list/1]).
-export([to_dictionary/1, to_list/1, to_item/1, to_item/2]).

-include("include/hb_http.hrl").

Expand All @@ -46,6 +43,85 @@
(C =:= $z)
).

%% Mapping

to_dictionary(Map) when is_map(Map) ->
to_dictionary(maps:to_list(Map));
to_dictionary(Pairs) when is_list(Pairs) ->
to_dictionary([], Pairs).

to_dictionary(Dict, []) ->
{ok, Dict};
to_dictionary(_Dict, [{ Name, Value } | _Rest]) when is_map(Value) ->
{too_deep, Name};
to_dictionary(Dict, [{Name, Value} | Rest]) when is_list(Value) ->
to_dictionary(
[{key_to_binary(Name), to_inner_list(Value)} | Dict],
Rest
);
to_dictionary(Dict, [{Name, Value} | Rest]) ->
to_dictionary(
[{key_to_binary(Name), to_item(Value)} | Dict],
Rest
).

to_item(Item) ->
to_item(Item, []).
to_item(Item, Params) ->
{ok, {item, to_bare_item(Item), [to_param(Param) || Param <- Params]}}.

to_list(List) when is_list(List) ->
to_list([], List).
to_list(Acc, []) ->
{ok, Acc};
to_list(Acc, [Item | Rest]) when is_map(Item) ->
{too_deep, Item};
to_list(Acc, [Item | Rest]) ->
% Item
% foo -> Item no params
% [foo, [...]] Item + params
% [[...]] inner list no params
% [ [...], [...] ] inner list plus params
{not_implemented}.

to_inner_list(List) ->
to_inner_list(List, []).
to_inner_list(List, Params) when is_list(List) ->
to_inner_list([], List, Params).

to_inner_list(Inner, [], Params) ->
{ok, {list, Inner, [to_param(Param) || Param <- Params]}};
to_inner_list(_Inner, [Item | _Rest], _Params) when is_map(Item) ->
{too_deep, Item};

% TODO: implement for list + params

to_inner_list(Inner, [Item | Rest], Params) ->
to_inner_list(
[to_item(Item) | Inner],
Rest,
Params
).

to_param({Name, Value}) when is_atom(Name)->
to_param({atom_to_binary(Name), Value});
to_param({Name, Value}) ->
NormalizedName = iolist_to_binary(Name),
{NormalizedName, to_bare_item(Value)}.

to_bare_item(BareItem) ->
case BareItem of
% Serialize -> Parse numbers in order to ensure their lengths adhere to structured fields
I when is_integer(I) -> parse_bare_item(bare_item(I));
F when is_float(F) -> parse_bare_item(bare_item({decimal, {F, 0}}));
A when is_atom(A) -> {token, atom_to_binary(A)};
B when is_boolean(B) -> B;
S when is_binary(S) or is_list(S) -> {string, iolist_to_binary(S)}
end.

key_to_binary(Key) when is_atom(Key) -> atom_to_binary(Key);
key_to_binary(Key) -> iolist_to_binary(Key).

%% Parsing.

-spec parse_dictionary(binary()) -> sh_dictionary().
Expand Down
119 changes: 119 additions & 0 deletions src/hb_message.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
-export([load/2, sign/2, verify/1]).
-export([serialize/1, serialize/2, deserialize/1, deserialize/2, signers/1]).
-export([message_to_tx/1, tx_to_message/1]).
-export([message_to_http/1]).
%%% Debugging tools:
-export([print/1, format/1, format/2]).
-include("include/hb.hrl").
Expand Down Expand Up @@ -330,6 +331,124 @@ message_to_tx(Other) ->
?event({unexpected_message_form, {explicit, Other}}),
throw(invalid_tx).

%%% @doc Maps the native HyperBEAM Message
%%% to an "HTTP" message. An HTTP Message has the following shape:
%%%
%%% #{
%%% headers => [
%%% {<<"Example-Header">>, <<"Value">>}
%%% ],
%%% body: <<"Some body">>
%%% }
%%%
%%%
%%% For each HyperBEAM Message Key:
%%%
%%% The Key will be ignored if:
%%% - The field is private (according to hb_private:is_private/1)
%%% - The field is one of ?REGEN_KEYS
%%%
%%% The Key will be mapped according to the following rules:
%%% signatures -> {SignatureInput, Signature} header Tuples, each encoded as a Structured Field Dictionary
%%% body:
%%% - If a map, then every value is assumed another Msg to recursively transform, then combine in
%%% a multipart response sent as the body
%%% - Otherwise, make this the body of the Http Message
%%% _ -> {Name/binary, Value/binary} header Tuple
%%% - If the header is a VALID list of dictionary, then attempt to encode as a structured field headers
%%% - header is considered valid if:
%%% - Header size is <2KB
%%% - Only a single depth, as only a single depth is supported by structured fields
%%%
message_to_http(Msg) ->
PublicMsg = hb_private:reset(Msg),
MinimizedMsg = minimize(PublicMsg),
NormalizedMsg = normalize_keys(MinimizedMsg),
Http = lists:foldl(
fun
({<<"signatures">>, Signatures}, Http) -> signatures_to_http(Http, Signatures);
({<<"body">>, Body}, Http) -> body_to_http(Http, Body);
({Name, Value}, Http) -> field_to_http(Http, {Name, Value})
end,
#{ headers => [], body => <<>> },
maps:to_list(NormalizedMsg)
),
Http.

signatures_to_http(Http, Signatures) when is_map(Signatures) ->
signatures_to_http(Http, maps:to_list(Signatures));
signatures_to_http(Http, Signatures) when is_list(Signatures) ->
{SfSigInputs, SfSigs} = lists:foldl(
fun ({SigName, SignatureMap = #{ inputs := Inputs, signature := Signature }}, {SfSigInputs, SfSigs}) ->
NextSigInput = hb_http_signature:sf_signature_params(Inputs, SignatureMap),
NextSig = hb_http_signature:sf_signature(Signature),
NextName = hb_converge:key_to_binary(SigName),
{
[{NextName, NextSigInput} | SfSigInputs],
[{NextName, NextSig} | SfSigs]
}
end,
% Start with empty Structured Field Dictionaries
{[], []},
Signatures
),
Headers = maps:get(headers, Http),
% Upsert these headers to ensure they are not duplicated
H1 = lists:keystore(<<"Signature">>, 1, Headers, {<<"Signature">>, hb_structured_fields:dictionary(SfSigs)}),
NewHeaders = lists:keystore(<<"Signature-Input">>, 1, H1, {<<"Signature-Input">>, hb_http_structured_fields:dictionary(SfSigInputs)}),
maps:put(headers, NewHeaders, Http).

field_to_http(Http, {Name, Map}) when is_map(Map) ->
{not_implemented, Map};
field_to_http(Http, {Name, List}) when is_list(List) ->
{not_implemented, List};
field_to_http(Http, {Name, Value}) ->
NormalizedName = hb_converge:key_to_binary(Name),
NormalizedValue = hb_converge:key_to_binary(Value),
Headers = maps:get(headers, Http),
NewHeaders = lists:append(Headers, [{NormalizedName, hb_structured_fields:dictionary(NormalizedValue)}]),
maps:put(headers, NewHeaders, Http).

body_to_http(Http, Body) when is_map(Body)->
% recursively call message_to_http for each Msg and
% and then programattically join in the body using the Boundary,
% according to multipart/form-data semantics
?no_prod("What should the Boundary be?"),
Boundary = base64:encode(crypto:strong_rand_bytes(32)),
Parts = maps:map(
fun (Key, Msg) ->
?no_prod("What should the name be?"),
NormalizedKey = hb_converge:key_to_binary(Key),
#{ headers := SubHeaders, body := SubBody } = message_to_http(Msg),
% Serialize the headers, to be included in the part of the multipart response
SerializedHeaders = lists:foldl(
fun ({Name, Value}, Acc) ->
<<Acc/binary, "\n", Name/binary, ": ", Value/binary>>
end,
<<"Content-Disposition: form-data; name=", NormalizedKey/binary>>,
SubHeaders
),
% Content-Disposition: form-data; name="fgserbvrebserfe"
% Content-Type: image/png
%
% <body>
<<SerializedHeaders/binary, <<"\n\n">>, SubBody/binary>>
end,
Body
),
JoinedBody = lists:join(<<<<"--">>, Boundary/binary>>, Parts),
Headers = maps:get(headers, Http),
% Upsert a Content-Type header to make the Msg multipart
NewHeaders = lists:keystore(<<"Content-Type">>, 1, Headers,
{<<"Content-Type">>, <<"multipart/form-data; boundary=", Boundary/binary>>}
),
body_to_http(
maps:put(headers, NewHeaders, Http),
iolist_to_binary(JoinedBody)
);
body_to_http(Http, Body) when is_binary(Body) ->
maps:merge(Http, #{ body => Body }).

%% @doc Convert non-binary values to binary for serialization.
decode_value(decimal, Value) ->
{item, Number, _} = hb_http_structured_fields:parse_item(Value),
Expand Down

0 comments on commit 908b3bb

Please sign in to comment.