Packages

macula

14.2.0
14.5.0 14.4.0 14.3.0 14.2.1 14.2.0 14.1.0 14.0.0 13.6.0 13.5.0 13.4.0 13.3.0 13.2.2 13.2.1 13.2.0 13.1.0 13.0.1 13.0.0 12.12.0 12.11.1 12.11.0 12.10.0 12.9.1 12.9.0 12.8.0 12.7.0 12.6.0 12.5.1 12.5.0 12.4.0 12.3.0 12.2.1 12.2.0 12.1.0 12.0.0 11.5.0 11.4.0 11.3.1 11.3.0 11.2.0 11.1.0 11.0.0 10.25.0 10.24.0 10.23.0 10.22.0 10.21.0 10.20.3 10.20.2 10.20.0 10.19.2 10.19.1 10.19.0 10.18.0 10.17.0 10.16.0 10.15.0 10.14.5 10.14.4 10.14.2 10.14.1 10.14.0 10.13.2 10.13.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 10.9.0 10.8.0 10.7.0 10.5.8 10.5.7 10.5.6 10.5.5 10.5.4 10.5.3 10.5.2 10.5.1 10.5.0 10.4.0 10.2.0 10.1.1 10.1.0 10.0.2 10.0.1 10.0.0 9.13.8 9.8.2 9.8.1 9.8.0 9.5.0 9.4.0 9.3.1 9.3.0 9.2.0 9.1.1 9.1.0 9.0.0 8.7.0 8.6.0 8.5.0 8.4.1 8.4.0 8.3.0 8.2.0 8.1.0 8.0.2 8.0.1 8.0.0 7.1.0 7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_app_resolve.erl
Raw

src/macula_app_resolve.erl

%%%-------------------------------------------------------------------
%%% @doc Where an app runs, from its MRI (macula#75): the app record the org
%%% signed for it, then each of its services' procedures through the same
%%% provider lookup a call uses.
%%%
%%% The app record is looked up under `macula_record:app_key/3' of the MRI's
%%% realm, org and app, and is trusted only when `macula_record:verify_app/3'
%%% passes against the realm key the pool pinned for that realm: the record
%%% carries the realm-signed org directory, so the check needs no second
%%% lookup. Of several records that pass, the newest is used. A procedure no
%%% provider serves right now is listed with no providers and the reason, so
%%% an app that serves nothing at the moment still resolves.
%%%
%%% The DHT lookup, the pinned realm key and the provider lookup come from
%%% the `resolve_io/0' seam (each entry defaults to the real function), so a
%%% test stubs them here rather than replacing a module other processes call.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_app_resolve).
-export([resolve/3, resolve/4]).
-define(TYPE_APP_RECORD, 16#17).
-type resolved() :: #{app := #{realm_id := <<_:256>>, org_name := binary(), app_name := binary(),
version := binary()},
services := [#{name := binary(),
procedures := [#{procedure := binary(),
providers := [#{provider := <<_:256>>,
station := <<_:256>>}],
unresolved => term()}]}]}.
-type resolve_io() :: #{find_records => fun((pid(), <<_:256>>, pos_integer()) ->
{ok, [macula_record:m_record()]} | {error, term()}),
realm_key => fun((pid(), <<_:256>>) -> {ok, binary()} | none),
providers => fun((pid(), <<_:256>>, binary(), pos_integer()) ->
{ok, [map()]} | {error, term()})}.
-export_type([resolved/0, resolve_io/0]).
%% @doc Resolve the app `AppMri' (`mri:app:<realm>/<org>/<app>') to its
%% services and the providers serving each of their procedures, within
%% `TimeoutMs' in all. `{error, {invalid_app_mri, Reason}}' for anything but a
%% valid app MRI; `{error, {unresolved, app_not_registered}}' when no record
%% is stored under it; `{error, {unresolved, {no_trusted_app_record,
%% Refusals}}}' when none passes `verify_app/3', naming each refusal.
-spec resolve(pid(), binary(), pos_integer()) -> {ok, resolved()} | {error, term()}.
resolve(Pool, AppMri, TimeoutMs) ->
resolve(Pool, AppMri, TimeoutMs, #{}).
%% @doc As `resolve/3', with the lookups taken from `Io' (see `resolve_io/0').
-spec resolve(pid(), binary(), pos_integer(), resolve_io()) -> {ok, resolved()} | {error, term()}.
resolve(Pool, AppMri, TimeoutMs, Io) when is_binary(AppMri), is_integer(TimeoutMs), TimeoutMs > 0 ->
Deadline = erlang:monotonic_time(millisecond) + TimeoutMs,
app_parts(macula_mri:validate(AppMri), macula_mri:parse(AppMri), {Pool, resolve_io(Io)}, Deadline).
resolve_io(Io) ->
#{find_records => maps:get(find_records, Io, fun macula:find_records/3),
realm_key => maps:get(realm_key, Io, fun macula_client:realm_key/2),
providers => maps:get(providers, Io, fun macula_direct_dial:providers/4)}.
app_parts(ok, {ok, #{type := app, realm := RealmName, path := [OrgName, AppName]}},
{Pool, #{find_records := FindRecords}} = Lookup, Deadline) ->
RealmId = macula_realm:id(RealmName),
Records = FindRecords(Pool, macula_record:app_key(RealmId, OrgName, AppName), remaining(Deadline)),
app_record(Records, {RealmId, OrgName, AppName}, trust(Lookup, RealmId), Lookup, Deadline);
app_parts(ok, {ok, _NotAnApp}, _Lookup, _Deadline) ->
{error, {invalid_app_mri, not_an_app}};
app_parts({error, Reason}, _Parsed, _Lookup, _Deadline) ->
{error, {invalid_app_mri, Reason}}.
app_record({ok, Records}, Named, Trust, Lookup, Deadline) ->
Apps = [R || R <- Records, macula_record:type(R) =:= ?TYPE_APP_RECORD, names(R, Named)],
Checked = [{R, macula_record:verify_app(R, Trust, erlang:system_time(millisecond))} || R <- Apps],
trusted([R || {R, ok} <- Checked], [Why || {_R, {error, Why}} <- Checked], Lookup, Deadline);
app_record({error, not_found}, _Named, _Trust, _Lookup, _Deadline) ->
{error, {unresolved, app_not_registered}};
app_record({error, Reason}, _Named, _Trust, _Lookup, _Deadline) ->
{error, {unresolved, Reason}}.
%% The slot is a multiset: a record stored there must still name this app.
names(Record, {RealmId, OrgName, AppName}) ->
#{realm_id := R, org_name := O, app_name := A} = macula_record:read_app_record(Record),
{R, O, A} =:= {RealmId, OrgName, AppName}.
trusted([], [], _Lookup, _Deadline) ->
{error, {unresolved, app_not_registered}};
trusted([], Refusals, _Lookup, _Deadline) ->
{error, {unresolved, {no_trusted_app_record, lists:usort(Refusals)}}};
trusted(Trusted, _Refusals, Lookup, Deadline) ->
Newest = lists:last(lists:sort(fun(A, B) -> macula_record:created_at(A) =< macula_record:created_at(B) end,
Trusted)),
#{realm_id := RealmId, org_name := Org, app_name := App, version := Version, services := Services} =
macula_record:read_app_record(Newest),
{ok, #{app => #{realm_id => RealmId, org_name => Org, app_name => App, version => Version},
services => [#{name => Name, procedures => [served(Lookup, RealmId, P, Deadline) || P <- Procedures]}
|| #{name := Name, procedures := Procedures} <- Services]}}.
served({Pool, #{providers := Providers}}, RealmId, Procedure, Deadline) ->
with_providers(Procedure, Providers(Pool, RealmId, Procedure, remaining(Deadline))).
with_providers(Procedure, {ok, Providers}) ->
#{procedure => Procedure, providers => Providers};
with_providers(Procedure, {error, {unresolved, Reason}}) ->
#{procedure => Procedure, providers => [], unresolved => Reason};
with_providers(Procedure, {error, Reason}) ->
#{procedure => Procedure, providers => [], unresolved => Reason}.
%% What an app record is checked against: the node's crypto profile and the
%% realm key the pool pinned for the realm, as for a call's advertisements.
trust({Pool, #{realm_key := RealmKey}}, RealmId) ->
{ok, Profile} = macula_crypto_profile:configured(),
with_realm_key(RealmKey(Pool, RealmId), #{profile => Profile}).
with_realm_key({ok, RealmKey}, Trust) -> Trust#{realm_key => RealmKey};
with_realm_key(none, Trust) -> Trust.
%% What is left of the deadline, at least 1 ms so a late lookup still answers.
remaining(Deadline) ->
max(1, Deadline - erlang:monotonic_time(millisecond)).