diff --git a/zx b/zx index 26b26f2..6180a47 100755 --- a/zx +++ b/zx @@ -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