This commit is contained in:
Craig Everett 2017-12-03 16:03:08 +09:00
parent b64ddffe8b
commit 996e2e29b2

368
zx
View File

@ -46,6 +46,8 @@
% DER :: binary()}.
-type key_id() :: {realm(), key_name()}.
-type key_name() :: label().
-type user() :: {realm(), username()}.
-type username() :: label().
-type lower0_9() :: [$a..$z | $0..$9 | $_].
-type label() :: [$a..$z | $0..$9 | $_ | $- | $.].
-type package_meta() :: map().
@ -87,6 +89,8 @@ start(["set", "version", VersionString]) ->
set_version(VersionString);
start(["add", "realm", RealmFile]) ->
add_realm(RealmFile);
start(["add", "package", PackageName]) ->
add_package(PackageName);
start(["drop", "dep", PackageString]) ->
PackageID = package_id(PackageString),
drop_dep(PackageID);
@ -584,6 +588,67 @@ add_realm(Path, Data) ->
end.
-spec add_package(PackageName) -> no_return()
when PackageName :: package().
add_package(PackageName) ->
ok = file:set_cwd(zomp_dir()),
case string:lexemes(PackageName, "-") of
[Realm, Name] ->
case {valid_lower0_9(Realm), valid_lower0_9(Name)} of
{true, true} ->
add_package(Realm, Name);
{false, true} ->
ok = log(warning, "Invalid realm name: ~tp", [Realm]),
halt(1);
{true, false} ->
ok = log(warning, "Invalid package name: ~tp", [Name]),
halt(1);
{false, false} ->
ok = log(warning, "Invalid realm and package names."),
halt(1)
end;
_ ->
ok = log(warning, "Name ~tp is not a valid package name.", [PackageName]),
halt(1)
end.
-spec add_package(Realm, Name) -> no_return()
when Realm :: realm(),
Name :: name().
%% @private
%% This sysop-only command can add a package to a realm operated by the caller.
add_package(Realm, Name) ->
Socket =
case connect_auth(Realm) of
{ok, S} ->
S;
Error ->
M1 = "Connection failed to realm prime with ~160tp.",
ok = log(warning, M1, [Error]),
halt(1)
end,
ok = send(Socket, {add_package, {Realm, Name}}),
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin, [safe]) of
ok ->
ok = log(info, "\"~ts-~ts\" added successfully.", [Realm, Name]),
halt(0);
{error, Reason} ->
M2 = "Operation failed. Server sends reason: ~160tp",
ok = log(error, M2, [Reason]),
halt(1)
end;
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
ok = log(warning, "Operation timed After submission to server."),
halt(1)
end.
%%% Drop dependency
@ -649,7 +714,10 @@ drop_realm(Realm) ->
case get_input() of
"Y" ->
ok = file:delete(RealmConf),
clear_keys(Realm);
ok = drop_prime(Realm),
ok = clear_keys(Realm),
ok = log(info, "All traces of realm ~ts have been removed."),
halt(0);
_ ->
ok = log(info, "Aborting."),
halt(0)
@ -659,8 +727,23 @@ drop_realm(Realm) ->
clear_keys(Realm)
end.
-spec drop_prime(realm()) -> ok.
-spec clear_keys(realm()) -> no_return().
drop_prime(Realm) ->
Path = "zomp.conf",
case file:consult(Path) of
{ok, Conf} ->
{managed, Primes} = lists:keyfind(managed, 1, Conf),
NewPrimes = lists:delete(Realm, Primes),
NewConf = lists:keystore(managed, 1, Primes, {managed, NewPrimes}),
ok = write_terms(Path, NewConf),
log(info, "Ensuring ~ts is not a prime in ~ts", [Realm, Path]);
{error, enoent} ->
ok
end.
-spec clear_keys(realm()) -> ok.
clear_keys(Realm) ->
KeyDir = filename:join([zomp_dir(), "key", Realm]),
@ -671,11 +754,9 @@ clear_keys(Realm) ->
Delete = fun(K) -> file:delete(K) end,
ok = lists:foreach(Delete, Keys),
ok = file:del_dir(KeyDir),
ok = log(info, "Done!"),
halt(0);
log(info, "Done!");
false ->
ok = log(warning, "Keydir ~ts not found", [KeyDir]),
halt(1)
log(warning, "Keydir ~ts not found", [KeyDir])
end.
@ -726,13 +807,22 @@ run_local(Args) ->
execute(State, Args).
-spec execute(State, Args) -> no_return()
when State :: state(),
Args :: [string()].
%% @private
%% Gets all the target application's ducks in a row and launches them, then enters
%% the exec_wait/1 loop to wait for any queries from the application.
execute(State = #s{type = app, realm = Realm, name = Name, version = Version}, Args) ->
true = register(zx, self()),
ok = inets:start(),
ok = log(info, "Starting ~ts", [package_string({Realm, Name, Version})]),
{ok, Apps} = application:ensure_all_started(list_to_atom(Name)),
AppMod = list_to_atom(Name),
{ok, Apps} = application:ensure_all_started(AppMod),
ok = log(info, "Started, ~tp", [Apps]),
exec_wait(State#s{});
ok = pass_argv(AppMod, Args),
exec_wait(State);
execute(#s{type = lib, realm = Realm, name = Name, version = Version}, _) ->
Message = "Lib ~ts is available on the system, but is not a standalone app.",
PackageString = package_string({Realm, Name, Version}),
@ -740,6 +830,20 @@ execute(#s{type = lib, realm = Realm, name = Name, version = Version}, _) ->
halt(0).
-spec pass_argv(AppMod, Args) -> ok
when AppMod :: module(),
Args :: [string()].
%% @private
%% Check whether the AppMod:accept_argv/1 is implemented. If so, pass in the
%% command line arguments provided.
pass_argv(AppMod, Args) ->
case lists:member({accept_argv, 1}, AppMod:module_info(exports)) of
true -> AppMod:accept_argv(Args);
false -> ok
end.
%%% Package generation
@ -848,6 +952,15 @@ remove_binaries(TargetDir) ->
%% the registered zompc process convert itself to a gen_server via zompc_lib to
%% provide more advanced functionality?)
exec_wait(State = #s{pid = none, mon = none}) ->
receive
{monitor, Pid} ->
Mon = monitor(process, Pid),
exec_wait(State#s{pid = Pid, mon = Mon});
Unexpected ->
ok = log(warning, "Unexpected message: ~tp", [Unexpected]),
exec_wait(State)
end;
exec_wait(State = #s{pid = Pid, mon = Mon}) ->
receive
{check_update, Requester, Ref} ->
@ -899,9 +1012,7 @@ submit(PackageFile) ->
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
{package_id, {Realm, Package, Version}} = lists:keyfind(package_id, 1, Meta),
{sig, {KeyID = {Realm, KeyName}, _}} = lists:keyfind(sig, 1, Meta),
true = ensure_keypair(KeyID),
{ok, Socket} = connect_auth(Realm, KeyName),
{ok, Socket} = connect_auth(Realm),
ok = send(Socket, {submit, {Realm, Package, Version}}),
ok =
receive
@ -912,7 +1023,9 @@ submit(PackageFile) ->
{error, Reason} ->
ok = log(info, "Server refused with ~tp", [Reason]),
halt(0)
end
end;
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
ok = log(warning, "Server timed out!"),
halt(0)
@ -922,7 +1035,9 @@ submit(PackageFile) ->
ok =
receive
{tcp, Socket, Response2} ->
log(info, "Response: ~tp", [Response2])
log(info, "Response: ~tp", [Response2]);
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
log(warning, "Server timed out!")
end,
@ -941,6 +1056,13 @@ send(Socket, Message) ->
gen_tcp:send(Socket, Bin).
-spec halt_on_unexpected_close() -> no_return().
halt_on_unexpected_close() ->
ok = log(warning, "Socket closed unexpectedly."),
halt(1).
-spec connect_user(realm()) -> gen_tcp:socket() | no_return().
%% @private
%% Connect to a given realm, whatever method is required.
@ -961,7 +1083,12 @@ connect_user(Realm) ->
connect_user(Realm, []) ->
{Host, Port} = get_prime(Realm),
ok = log(info, "Trying prime at ~ts:~tp", [inet:ntoa(Host), Port]),
HostString =
case io_lib:printable_unicode_list(Host) of
true -> Host;
false -> inet:ntoa(Host)
end,
ok = log(info, "Trying prime at ~ts:~160tp", [HostString, Port]),
case gen_tcp:connect(Host, Port, connect_options(), 5000) of
{ok, Socket} ->
confirm_user(Realm, Socket, []);
@ -1002,7 +1129,9 @@ confirm_user(Realm, Socket, Hosts) ->
ok = log(info, "Redirected..."),
ok = disconnect(Socket),
connect_user(Realm, Next ++ Hosts)
end
end;
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
ok = log(warning, "Host ~ts:~p timed out.", [Host, Port]),
ok = disconnect(Socket),
@ -1053,7 +1182,9 @@ confirm_serial(Realm, Socket, Hosts) ->
ok = log(info, "Node is no longer serving realm. Trying another."),
ok = disconnect(Socket),
connect_user(Realm, Hosts)
end
end;
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
ok = log(info, "Host timed out on confirm_serial. Trying another."),
ok = disconnect(Socket),
@ -1061,30 +1192,32 @@ confirm_serial(Realm, Socket, Hosts) ->
end.
-spec connect_auth(Realm, KeyName) -> Result
when Realm :: realm(),
KeyName :: key_name(),
Result :: {ok, gen_tcp:socket()}
| {error, Reason :: term()}.
-spec connect_auth(Realm) -> Result
when Realm :: realm(),
Result :: {ok, gen_tcp:socket()}
| {error, Reason :: term()}.
%% @private
%% Connect to one of the servers in the realm constellation.
connect_auth(Realm, KeyName) ->
{ok, Key} = loadkey(private, {Realm, KeyName}),
{Host, Port} = get_prime(Realm),
connect_auth(Realm) ->
RealmConf = load_realm_conf(Realm),
{User, KeyID, Key} = prep_auth(Realm, RealmConf),
{prime, {Host, Port}} = lists:keyfind(prime, 1, RealmConf),
case gen_tcp:connect(Host, Port, connect_options(), 5000) of
{ok, Socket} ->
ok = log(info, "Connected to ~tp prime.", [Realm]),
confirm_auth(Socket, Key);
connect_auth(Socket, Realm, User, KeyID, Key);
Error = {error, E} ->
ok = log(warning, "Connection problem: ~tp", [E]),
{error, Error}
end.
-spec confirm_auth(Socket, Key) -> Result
-spec connect_auth(Socket, Realm, User, KeyID, Key) -> Result
when Socket :: gen_tcp:socket(),
Realm :: realm(),
User :: user(),
KeyID :: key_id(),
Key :: term(),
Result :: {ok, gen_tcp:socket()}
| {error, timeout}.
@ -1092,19 +1225,118 @@ connect_auth(Realm, KeyName) ->
%% Send a protocol ID string to notify the server what we're up to, disconnect
%% if it does not return an "OK" response within 5 seconds.
confirm_auth(Socket, Key) ->
ok = log(info, "Would be using key ~tp now", [Key]),
{ok, {Host, Port}} = inet:peername(Socket),
connect_auth(Socket, Realm, User, KeyID, Key) ->
ok = gen_tcp:send(Socket, <<"OTPR AUTH 1">>),
receive
{tcp, Socket, <<"OK">>} ->
{ok, Socket}
{tcp, Socket, Bin} ->
ok = binary_to_term(Bin, [safe]),
confirm_auth(Socket, Realm, User, KeyID, Key);
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
ok = log(warning, "Host ~s:~p timed out.", [Host, Port]),
ok = log(warning, "Host realm ~160tp prime timed out.", [Realm]),
{error, auth_timeout}
end.
confirm_auth(Socket, Realm, User, KeyID, Key) ->
ok = send(Socket, {User, KeyID}),
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin, [safe]) of
{sign, Blob} ->
Sig = public_key:sign(Blob, sha512, Key),
ok = send(Socket, {signed, Sig}),
confirm_auth(Socket);
{error, not_prime} ->
M1 = "Connected node is not prime for realm ~160tp",
ok = log(warning, M1, [Realm]),
ok = disconnect(Socket),
{error, not_prime};
{error, bad_user} ->
M2 = "Bad user record ~160tp",
ok = log(warning, M2, [User]),
ok = disconnect(Socket),
{error, bad_user};
{error, unauthorized_key} ->
M3 = "Unauthorized user key ~160tp",
ok = log(warning, M3, [KeyID]),
ok = disconnect(Socket),
{error, unauthorized_key};
{error, Reason} ->
Message = "Could not begin auth exchange. Failed with ~160tp",
ok = log(warning, Message, [Reason]),
ok = disconnect(Socket),
{error, Reason}
end;
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
ok = log(warning, "Host realm ~tp prime timed out.", [Realm]),
{error, auth_timeout}
end.
confirm_auth(Socket) ->
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin, [safe]) of
ok -> {ok, Socket};
Other -> {error, Other}
end;
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 5000 ->
{error, timeout}
end.
-spec prep_auth(Realm, RealmConf) -> {User, KeyID, Key} | no_return()
when Realm :: realm(),
RealmConf :: [term()],
User :: user(),
KeyID :: key_id(),
Key :: term().
%% @private
%% Loads the appropriate User, KeyID and reads in a registered key for use in
%% connect_auth/4.
prep_auth(Realm, RealmConf) ->
Users =
case file:consult("zomp.users") of
{ok, U} ->
U;
{error, enoent} ->
ok = log(warning, "You do not have any users configured."),
halt(1)
end,
{User, KeyIDs} =
case lists:keyfind(Realm, 1, Users) of
{Realm, UserName, []} ->
W = "User ~tp does not have any keys registered for realm ~tp.",
ok = log(warning, W, [UserName, Realm]),
ok = log(info, "Contact the following sysop(s) to register a key:"),
{sysops, Sysops} = lists:keyfind(sysops, 1, RealmConf),
PrintContact =
fun({_, _, Email, Name, _, _}) ->
log(info, "Sysop: ~ts Email: ~ts", [Name, Email])
end,
ok = lists:foreach(PrintContact, Sysops),
halt(1);
{Realm, UserName, KeyNames} ->
KIDs = [{Realm, KeyName} || KeyName <- KeyNames],
{{Realm, UserName}, KIDs};
false ->
Message = "You are not a user of the given realm: ~160tp.",
ok = log(warning, Message, [Realm]),
halt(1)
end,
KeyID = hd(KeyIDs),
true = ensure_keypair(KeyID),
{ok, Key} = loadkey(private, KeyID),
{User, KeyID, Key}.
-spec connect_options() -> [gen_tcp:connect_option()].
%% @private
%% Hide away the default options used for TCP connections.
@ -1411,10 +1643,10 @@ loadkey(Type, {Realm, KeyName}) ->
{DerType, Path} =
case Type of
private ->
P = filename:join([zomp_dir(), "key", Realm, KeyName ++ "key.der"]),
P = filename:join([zomp_dir(), "key", Realm, KeyName ++ ".key.der"]),
{'RSAPrivateKey', P};
public ->
P = filename:join([zomp_dir(), "key", Realm, KeyName ++ "pub.der"]),
P = filename:join([zomp_dir(), "key", Realm, KeyName ++ ".pub.der"]),
{'RSAPublicKey', P}
end,
ok = log(info, "Loading key from file ~ts", [Path]),
@ -1642,8 +1874,8 @@ create_realm(ZompConf, Realm, ExAddress, ExPort) ->
Message =
"~n"
" Enter the local (internal/LAN) port number at which this service should be "
"available. (This might be different from the public port visible from the internet"
"if you are port forwarding or have a complex network layout.)~n",
"available. (This might be different from the public port visible from the "
"internet if you are port forwarding or have a complex network layout.)~n",
ok = io:format(Message),
InPort = prompt_port_number(Current),
create_realm(ZompConf, Realm, ExAddress, ExPort, InPort).
@ -1667,7 +1899,7 @@ prompt_port_number(Current) ->
Port when 16#ffff >= Port, Port > 0 ->
Port;
Illegal ->
Whoops = "Whoops! ~tw is out of bounds (1~65535). Try again...~n",
Whoops = "Whoops! ~tw is out of bounds (1~65535). Try again.~n",
ok = io:format(Whoops, [Illegal]),
prompt_port_number(Current)
end
@ -1811,12 +2043,10 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email) ->
" directory on your personal or dev machine.~n",
ok = io:format(Message, [Realm]),
UserRecord =
{{UserName, Realm},
{{Realm, UserName},
[filename:basename(SysopPub, ".pub.der")],
Email,
RealName,
1,
Timestamp},
RealName},
RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"),
RealmMeta =
[{realm, Realm},
@ -1849,35 +2079,29 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email) ->
-spec create_realmfile(realm()) -> no_return().
create_realmfile(Realm) ->
ConfPath = filename:join(zomp_dir(), realm_conf(Realm)),
case file:consult(ConfPath) of
{ok, RealmConf} ->
ok = log(info, "Realm found, creating realm file..."),
{revision, Revision} = lists:keyfind(revision, 1, RealmConf),
{realm_keys, RealmKeys} = lists:keyfind(realm_keys, 1, RealmConf),
{package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf),
RealmKeyIDs = [element(1, K) || K <- RealmKeys],
PackageKeyIDs = [element(1, K) || K <- PackageKeys],
create_realmfile(Realm, ConfPath, Revision, RealmKeyIDs, PackageKeyIDs);
{error, enoent} ->
ok = log(warning, "There is no configured realm called ~ts.", [Realm]),
halt(1)
end.
RealmConf = load_realm_conf(Realm),
ok = log(info, "Realm found, creating realm file..."),
{revision, Revision} = lists:keyfind(revision, 1, RealmConf),
{realm_keys, RealmKeys} = lists:keyfind(realm_keys, 1, RealmConf),
{package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf),
RealmKeyIDs = [element(1, K) || K <- RealmKeys],
PackageKeyIDs = [element(1, K) || K <- PackageKeys],
create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs).
-spec create_realmfile(Realm, ConfPath, Revision, RealmKeyIDs, PackageKeyIDs) -> ok
-spec create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> ok
when Realm :: realm(),
ConfPath :: file:filename(),
Revision :: non_neg_integer(),
RealmKeyIDs :: [key_id()],
PackageKeyIDs :: [key_id()].
create_realmfile(Realm, ConfPath, Revision, RealmKeyIDs, PackageKeyIDs) ->
create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) ->
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zomp_dir()),
KeyPath = fun({R, K}) -> filename:join(["key", R, K ++ ".pub.der"]) end,
RealmKeyPaths = lists:map(KeyPath, RealmKeyIDs),
PackageKeyPaths = lists:map(KeyPath, PackageKeyIDs),
Targets = [filename:basename(ConfPath) | RealmKeyPaths ++ PackageKeyPaths],
Targets = [realm_conf(Realm) | RealmKeyPaths ++ PackageKeyPaths],
OutFile = filename:join(CWD, Realm ++ "." ++ integer_to_list(Revision) ++ ".zrf"),
ok = erl_tar:create(OutFile, Targets, [compressed]),
ok = log(info, "Realm conf file written to ~ts", [OutFile]),
@ -1987,7 +2211,9 @@ request_zrp(Socket, PackageID) ->
Message = "Error receiving package ~ts: ~tp",
ok = log(info, Message, [PackageString, Reason]),
Error
end
end;
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 60000 ->
{error, timeout}
end.
@ -2004,7 +2230,9 @@ receive_zrp(Socket, PackageID) ->
ZrpPath = filename:join("zrp", namify_zrp(PackageID)),
ok = file:write_file(ZrpPath, Bin),
ok = send(Socket, ok),
log(info, "Wrote ~ts", [ZrpPath])
log(info, "Wrote ~ts", [ZrpPath]);
{tcp_closed, Socket} ->
halt_on_unexpected_close()
after 60000 ->
ok = log(error, "Timeout in socket receive for ~tp", [PackageID]),
{error, timeout}
@ -2525,6 +2753,22 @@ realm_conf(Realm) ->
Realm ++ ".realm".
-spec load_realm_conf(Realm) -> RealmConf | no_return()
when Realm :: realm(),
RealmConf :: list().
%% @private
%% Load the config for the given realm or halt with an error.
load_realm_conf(Realm) ->
Path = filename:join(zomp_dir(), realm_conf(Realm)),
case file:consult(Path) of
{ok, C} ->
C;
{error, enoent} ->
ok = log(warning, "Realm ~tp is not configured.", [Realm]),
halt(1)
end.
%%% Usage