Packages

macula

11.0.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_stations.erl
Raw

src/macula_stations.erl

%%%-------------------------------------------------------------------
%%% @doc Parse the `MACULA_STATIONS' seed list into the pinned seeds a
%%% pool takes: comma-separated `<node id>@<host>:<port>' entries, the
%%% node id as 64 lowercase hexadecimal characters, the host a DNS
%%% name, an IPv4 address or an IPv6 address in brackets, the port 1
%%% to 65535. A station becomes the seed the pool takes, pinned to its
%%% node id, with its host as given. A refusal names the entry's
%%% position and what is wrong with it, and carries neither the value,
%%% nor a node id, nor a host.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_stations).
-export([parse/1]).
-export_type([seed/0]).
-type seed() :: #{host := binary(), port := inet:port_number(),
expected_node_id := <<_:256>>}.
-define(MAX_STATIONS, 64).
%% @doc Parse a `MACULA_STATIONS' value. Up to 64 entries, each
%% `<node id>@<host>:<port>'. Refusals name the entry's position:
%% `{entry, N, form | node_id | port | host_dns | host_ipv4 | host_ipv6}',
%% or a duplicate across entries `{repeated_node_id, N, FirstN}' /
%% `{repeated_host_and_port, N, FirstN}', or `{too_many, 64}' past the
%% cap, or `empty'. No refusal carries a node id, a host, or the value.
-spec parse(binary()) -> {ok, [seed()]} | {error, term()}.
parse(<<>>) ->
{error, empty};
parse(Value) when is_binary(Value) ->
Entries = binary:split(Value, <<",">>, [global]),
case length(Entries) =< ?MAX_STATIONS of
false ->
{error, {too_many, ?MAX_STATIONS}};
true ->
seeds(parse_entries(Entries, 1, []))
end.
%% Every entry parses, or the first refusal is the answer.
parse_entries([], _N, Acc) ->
{ok, lists:reverse(Acc)};
parse_entries([Entry | Rest], N, Acc) ->
case parse_entry(Entry, N) of
{ok, Seed} -> parse_entries(Rest, N + 1, [Seed | Acc]);
{error, _} = Refusal -> Refusal
end.
%% Once every entry parsed, the duplicates across entries are judged in
%% position order: a repeated node id first, then a repeated host+port.
seeds({error, _} = Refusal) ->
Refusal;
seeds({ok, Seeds}) ->
seeds_no_dup(Seeds, 1, #{}, #{}).
seeds_no_dup([], _N, _Ids, _Hosts) ->
{ok, []};
seeds_no_dup([#{expected_node_id := Id} = Seed | Rest], N, Ids, Hosts) ->
HostPort = {maps:get(host, Seed), maps:get(port, Seed)},
case maps:find(Id, Ids) of
{ok, FirstN} ->
{error, {repeated_node_id, N, FirstN}};
error ->
seeds_no_host_dup(Seed, Rest, N, Id, HostPort, Ids, Hosts)
end.
seeds_no_host_dup(Seed, Rest, N, Id, HostPort, Ids, Hosts) ->
case maps:find(HostPort, Hosts) of
{ok, FirstN} ->
{error, {repeated_host_and_port, N, FirstN}};
error ->
seeds_no_dup_result(Seed, Rest, N, Id, HostPort, Ids, Hosts)
end.
seeds_no_dup_result(Seed, Rest, N, Id, HostPort, Ids, Hosts) ->
case seeds_no_dup(Rest, N + 1, Ids#{Id => N}, Hosts#{HostPort => N}) of
{ok, Seeds} -> {ok, [Seed | Seeds]};
{error, _} = Dup -> Dup
end.
%% One entry: `<64 lowercase hex node id>@<host>:<port>'.
parse_entry(Entry, N) ->
case binary:split(Entry, <<"@">>) of
[NodeIdHex, HostAndPort] ->
node_id_checked(NodeIdHex, HostAndPort, N);
_ ->
{error, {entry, N, form}}
end.
node_id_checked(NodeIdHex, HostAndPort, N) ->
case byte_size(NodeIdHex) =:= 64 andalso is_lower_hex(NodeIdHex) of
true ->
host_and_port_checked(binary:decode_hex(NodeIdHex), HostAndPort, N);
false ->
{error, {entry, N, node_id}}
end.
is_lower_hex(Bin) ->
lists:all(fun is_lower_hex_byte/1, binary_to_list(Bin)).
is_lower_hex_byte(C) when C >= $0, C =< $9 -> true;
is_lower_hex_byte(C) when C >= $a, C =< $f -> true;
is_lower_hex_byte(_) -> false.
%% The host:port pair: a bracketed IPv6 with a port after the bracket,
%% or a single host and a port.
host_and_port_checked(NodeId, <<"[", Rest/binary>>, N) ->
bracketed_ipv6_checked(NodeId, Rest, N);
host_and_port_checked(NodeId, HostAndPort, N) ->
case binary:split(HostAndPort, <<":">>, [global]) of
[Host, Port] -> host_and_port_finish(NodeId, Host, Port, N);
_ -> {error, {entry, N, form}}
end.
bracketed_ipv6_checked(NodeId, Rest, N) ->
case binary:split(Rest, <<"]">>) of
[Inner, <<":", Port/binary>>] ->
ipv6_checked(NodeId, Inner, Port, N);
_ ->
{error, {entry, N, form}}
end.
ipv6_checked(NodeId, Host, Port, N) ->
case port_number(Port) of
{ok, PortNumber} ->
ipv6_host_checked(NodeId, Host, PortNumber, N);
error ->
{error, {entry, N, port}}
end.
%% A zone id (`%eth0') is refused: inet parses it, but no seed carries
%% one, and a zone id names an interface, not a station.
ipv6_host_checked(NodeId, Host, Port, N) ->
case binary:match(Host, <<"%">>) of
nomatch ->
ipv6_host_valid(valid_ipv6(Host), NodeId, Host, Port, N);
_ ->
{error, {entry, N, host_ipv6}}
end.
ipv6_host_valid(true, NodeId, Host, Port, _N) ->
{ok, #{host => Host, port => Port, expected_node_id => NodeId}};
ipv6_host_valid(false, _NodeId, _Host, _Port, N) ->
{error, {entry, N, host_ipv6}}.
%% inet:parse_ipv6_address/1 returns {error, einval} or raises on a
%% malformed address, so the parse is guarded.
valid_ipv6(Host) ->
try inet:parse_ipv6_address(binary_to_list(Host)) of
{ok, _} -> true;
{error, _} -> false
catch
_:_ -> false
end.
host_and_port_finish(NodeId, Host, Port, N) ->
case port_number(Port) of
{ok, PortNumber} -> host_checked(NodeId, Host, PortNumber, N);
error -> {error, {entry, N, port}}
end.
%% No leading zeros, 1 to 65535, decimal digits only.
port_number(Port) when byte_size(Port) > 0 ->
case (Port =:= <<"0">> orelse binary:first(Port) =/= $0) andalso digits(Port) of
true ->
port_in_range(binary_to_integer(Port));
false ->
error
end;
port_number(_) ->
error.
port_in_range(P) when P >= 1, P =< 65535 -> {ok, P};
port_in_range(_OutOfRange) -> error.
digits(<<>>) -> true;
digits(<<C, _/binary>>) when C < $0; C > $9 -> false;
digits(<<_, Rest/binary>>) -> digits(Rest).
%% An IPv4-shaped host (only digits and dots) is judged as an IPv4
%% address; anything else as a DNS name.
host_checked(NodeId, Host, Port, N) ->
case ipv4_shaped(Host) of
true -> ipv4_checked(NodeId, Host, Port, N);
false -> dns_checked(NodeId, Host, Port, N)
end.
ipv4_shaped(Host) ->
Host =/= <<>> andalso lists:all(fun is_ipv4_byte/1, binary_to_list(Host)).
is_ipv4_byte(C) when C >= $0, C =< $9 -> true;
is_ipv4_byte($.) -> true;
is_ipv4_byte(_) -> false.
ipv4_checked(NodeId, Host, Port, N) ->
case canonical_ipv4(Host) of
true -> {ok, #{host => Host, port => Port, expected_node_id => NodeId}};
false -> {error, {entry, N, host_ipv4}}
end.
%% Four octets, each without leading zeros, 0 to 255.
canonical_ipv4(Host) ->
Parts = binary:split(Host, <<".">>, [global]),
length(Parts) =:= 4 andalso lists:all(fun octet/1, Parts).
octet(Part) when byte_size(Part) >= 1, byte_size(Part) =< 3 ->
(Part =:= <<"0">> orelse binary:first(Part) =/= $0)
andalso digits(Part) andalso binary_to_integer(Part) =< 255;
octet(_) ->
false.
dns_checked(NodeId, Host, Port, N) ->
case valid_dns(Host) of
true -> {ok, #{host => Host, port => Port, expected_node_id => NodeId}};
false -> {error, {entry, N, host_dns}}
end.
%% Labels of letters, digits and hyphens, no leading or trailing
%% hyphen, 1 to 63 characters, at most 253 in total.
valid_dns(Host) ->
byte_size(Host) =< 253 andalso
lists:all(fun label/1, binary:split(Host, <<".">>, [global])).
label(Label) when byte_size(Label) >= 1, byte_size(Label) =< 63 ->
binary:first(Label) =/= $- andalso binary:last(Label) =/= $-
andalso lists:all(fun dns_byte/1, binary_to_list(Label));
label(_) ->
false.
dns_byte(C) when C >= $a, C =< $z -> true;
dns_byte(C) when C >= $A, C =< $Z -> true;
dns_byte(C) when C >= $0, C =< $9 -> true;
dns_byte($-) -> true;
dns_byte(_) -> false.