From bd6c7472070a9dbaa0b3341ccf2cae7ec5c067f6 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Mon, 28 May 2018 14:04:38 +0900 Subject: [PATCH] foo --- zomp/etc/sys.conf | 10 +- zomp/lib/otpr/zx/0.1.0/src/zx.erl | 1119 +++----------------- zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl | 452 ++++++++ zomp/lib/otpr/zx/0.1.0/src/zx_conf_sys.erl | 152 ++- zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl | 16 +- zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl | 6 +- zomp/lib/otpr/zx/0.1.0/src/zx_key.erl | 280 +++++ zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl | 92 +- zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl | 101 ++ zomp/zx | 6 +- zx_dev | 14 +- 11 files changed, 1194 insertions(+), 1054 deletions(-) create mode 100644 zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/src/zx_key.erl create mode 100644 zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl diff --git a/zomp/etc/sys.conf b/zomp/etc/sys.conf index aeeb3d0..075c2dd 100644 --- a/zomp/etc/sys.conf +++ b/zomp/etc/sys.conf @@ -1,5 +1,5 @@ -{timeout, 5}. -{retry, 3}. -{maxconn, 5}. -{managed, []}. -{mirrors, []}. +{timeout,5}. +{retries,3}. +{maxconn,5}. +{managed,[]}. +{mirrors,[]}. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx.erl b/zomp/lib/otpr/zx/0.1.0/src/zx.erl index 0cc4b4d..977b688 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx.erl @@ -19,17 +19,17 @@ -license("GPL-3.0"). --export([do/1]). +-export([start/1]). -export([subscribe/1, unsubscribe/0]). --export([start/0, stop/0]). --export([start/2, stop/1]). +-export([start/2, stop/1, stop/0]). +-export([error_exit/2, error_exit/3]). -export_type([serial/0, package_id/0, package/0, realm/0, name/0, version/0, identifier/0, - option/0, host/0, key_id/0, key_name/0, key_data/0, - user_id/0, user_name/0, lower0_9/0, label/0, + user_id/0, user_name/0, contact_info/0, user_data/0, + lower0_9/0, label/0, package_meta/0]). -include("zx_logger.hrl"). @@ -46,7 +46,6 @@ -type version() :: {Major :: non_neg_integer() | z, Minor :: non_neg_integer() | z, Patch :: non_neg_integer() | z}. --type option() :: {string(), term()}. -type host() :: {string() | inet:ip_address(), inet:port_number()}. -type key_id() :: {realm(), key_name()}. -type key_name() :: lower0_9(). @@ -70,67 +69,52 @@ %%% Command Dispatch --spec do(Args) -> no_return() +-spec start(Args) -> no_return() when Args :: [string()]. %% Dispatch work functions based on the nature of the input arguments. -do(["help"]) -> +start(["help"]) -> usage_exit(0); -do(["run", PackageString | Args]) -> +start(["run", PackageString | Args]) -> + ok = start(), run(PackageString, Args); -do(["runlocal" | ArgV]) -> +start(["runlocal" | ArgV]) -> + ok = start(), run_local(ArgV); -do(["init", "app", PackageString]) -> +start(["init", "app", PackageString]) -> initialize(app, PackageString); -do(["init", "lib", PackageString]) -> +start(["init", "lib", PackageString]) -> initialize(lib, PackageString); -do(["install", PackageFile]) -> +start(["install", PackageFile]) -> + ok = start(), assimilate(PackageFile); -do(["set", "dep", PackageString]) -> +start(["set", "dep", PackageString]) -> set_dep(PackageString); -do(["set", "version", VersionString]) -> +start(["set", "version", VersionString]) -> set_version(VersionString); -do(["list", "realms"]) -> +start(["list", "realms"]) -> list_realms(); -do(["list", "packages", Realm]) -> +start(["list", "packages", Realm]) -> + ok = start(), list_packages(Realm); -do(["list", "versions", PackageName]) -> +start(["list", "versions", PackageName]) -> + ok = start(), list_versions(PackageName); -do(["list", "pending", PackageName]) -> - list_pending(PackageName); -do(["list", "resigns", Realm]) -> - list_resigns(Realm); -do(["add", "realm", RealmFile]) -> +start(["add", "realm", RealmFile]) -> add_realm(RealmFile); -do(["add", "package", PackageName]) -> - add_package(PackageName); -do(["add", "packager", Package, UserName]) -> - add_packager(Package, UserName); -do(["add", "maintainer", Package, UserName]) -> - add_maintainer(Package, UserName); -do(["review", PackageString]) -> - review(PackageString); -do(["approve", PackageString]) -> - PackageID = zx_lib:package_id(PackageString), - approve(PackageID); -do(["reject", PackageString]) -> - PackageID = zx_lib:package_id(PackageString), - reject(PackageID); -do(["resign", PackageString]) -> - resign(PackageString); -do(["drop", "dep", PackageString]) -> +start(["drop", "dep", PackageString]) -> PackageID = zx_lib:package_id(PackageString), drop_dep(PackageID); -do(["drop", "key", KeyID]) -> +start(["drop", "key", KeyID]) -> drop_key(KeyID); -do(["drop", "realm", Realm]) -> +start(["drop", "realm", Realm]) -> drop_realm(Realm); -do(["verup", Level]) -> +start(["verup", Level]) -> verup(Level); -do(["package"]) -> +start(["package"]) -> {ok, TargetDir} = file:get_cwd(), package(TargetDir); -do(["package", TargetDir]) -> +start(["package", TargetDir]) -> case filelib:is_dir(TargetDir) of true -> package(TargetDir); @@ -138,57 +122,47 @@ do(["package", TargetDir]) -> ok = log(error, "Target directory ~tp does not exist!", [TargetDir]), halt(22) end; -do(["submit", PackageFile]) -> - submit(PackageFile); -do(["dialyze"]) -> +start(["dialyze"]) -> dialyze(); -do(["create", "user", Realm, Name]) -> +start(["create", "user", Realm, Name]) -> create_user(Realm, Name); -do(["create", "keypair"]) -> - create_keypair(); -do(["create", "plt"]) -> +start(["create", "keypair"]) -> + zx_key:create_keypair(); +start(["create", "plt"]) -> create_plt(); -do(["create", "realm"]) -> +start(["create", "realm"]) -> create_realm(); -do(["create", "realmfile", Realm]) -> +start(["create", "realmfile", Realm]) -> create_realmfile(Realm); -do(["create", "sysop"]) -> - create_sysop(); -do(_) -> +start(["list", "pending", PackageName]) -> + zx_auth:list_pending(PackageName); +start(["list", "resigns", Realm]) -> + zx_auth:list_resigns(Realm); +start(["submit", PackageFile]) -> + zx_auth:submit(PackageFile); +start(["review", PackageString]) -> + zx_auth:review(PackageString); +start(["approve", PackageString]) -> + PackageID = zx_lib:package_id(PackageString), + zx_auth:approve(PackageID); +start(["reject", PackageString]) -> + PackageID = zx_lib:package_id(PackageString), + zx_auth:reject(PackageID); +start(["accept", PackageString]) -> + zx_auth:accept(PackageString); +start(["add", "packager", Package, UserName]) -> + zx_auth:add_packager(Package, UserName); +start(["add", "maintainer", Package, UserName]) -> + zx_auth:add_maintainer(Package, UserName); +start(["add", "sysop", Package, UserName]) -> + zx_auth:add_sysop(Package, UserName); +start(["add", "package", PackageName]) -> + zx_auth:add_package(PackageName); +start(_) -> usage_exit(22). -%%% Daemon Controls - --spec subscribe(package()) -> ok | {error, Reason :: term()}. -%% @doc -%% Initiates the zx_daemon and instructs it to subscribe to a package. -%% -%% Any events in the Zomp network that apply to the subscribed package will be -%% forwarded to the process that originally called subscribe/1. How the original -%% caller reacts to these notifications is up to the author -- not reply or "ack" -%% is expected. -%% -%% Package subscriptions can be used as the basis for user notification of updates, -%% automatic upgrade restarts, package catalog tracking, etc. - -subscribe(Package) -> - case application:start(?MODULE, normal) of - ok -> zx_daemon:subscribe(Package); - Error -> Error - end. - - --spec unsubscribe() -> ok | {error, Reason :: term()}. -%% @doc -%% Unsubscribes from package updates. - -unsubscribe() -> - zx_daemon:unsubscribe(). - - - %%% Application Start/Stop -spec start() -> ok | {error, Reason :: term()}. @@ -241,6 +215,36 @@ stop(_) -> +%%% Daemon Controls + +-spec subscribe(package()) -> ok | {error, Reason :: term()}. +%% @doc +%% Initiates the zx_daemon and instructs it to subscribe to a package. +%% +%% Any events in the Zomp network that apply to the subscribed package will be +%% forwarded to the process that originally called subscribe/1. How the original +%% caller reacts to these notifications is up to the author -- not reply or "ack" +%% is expected. +%% +%% Package subscriptions can be used as the basis for user notification of updates, +%% automatic upgrade restarts, package catalog tracking, etc. + +subscribe(Package) -> + case application:start(?MODULE, normal) of + ok -> zx_daemon:subscribe(Package); + Error -> Error + end. + + +-spec unsubscribe() -> ok | {error, Reason :: term()}. +%% @doc +%% Unsubscribes from package updates. + +unsubscribe() -> + zx_daemon:unsubscribe(). + + + %%% Execution of application -spec run(Identifier, RunArgs) -> no_return() @@ -264,7 +268,7 @@ stop(_) -> %% procedure the runtime will halt with an error message. run(Identifier, RunArgs) -> - ok = file:set_cwd(zx_lib:zomp_home()), + ok = file:set_cwd(zx_lib:zomp_dir()), ok = start(), FuzzyID = case zx_lib:package_id(Identifier) of @@ -299,7 +303,7 @@ run_local(RunArgs) -> PackageID = maps:get(package_id, Meta), ok = build(), {ok, Dir} = file:get_cwd(), - ok = file:set_cwd(zx_lib:zomp_home()), + ok = file:set_cwd(zx_lib:zomp_dir()), ok = start(), execute(PackageID, Meta, Dir, RunArgs). @@ -435,20 +439,20 @@ initialize(Type, PackageString) -> %% contents. assimilate(PackageFile) -> - Files = extract_zrp_or_die(PackageFile), + Files = zx_lib:extract_zsp_or_die(PackageFile), {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(zx_lib:zomp_home()), + ok = file:set_cwd(zx_lib:zomp_dir()), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), PackageID = maps:get(package_id, Meta), TgzFile = zx_lib:namify_tgz(PackageID), {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), {KeyID, Signature} = maps:get(sig, Meta), - {ok, PubKey} = loadkey(public, KeyID), + {ok, PubKey} = zx_key:load(public, KeyID), ok = case public_key:verify(TgzData, sha512, Signature, PubKey) of true -> - ZrpPath = filename:join("zrp", zx_lib:namify_zrp(PackageID)), + ZrpPath = filename:join("zsp", zx_lib:namify_zsp(PackageID)), file:copy(PackageFile, ZrpPath); false -> error_exit("Bad package signature: ~ts", [PackageFile], ?LINE) @@ -659,10 +663,7 @@ update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> %% stdout and the program will exit. list_realms() -> - Pattern = filename:join(zx_lib:zomp_home(), "*.realm"), - RealmFiles = filelib:wildcard(Pattern), - Realms = [filename:basename(RF, ".realm") || RF <- RealmFiles], - ok = lists:foreach(fun(R) -> io:format("~ts~n", [R]) end, Realms), + ok = lists:foreach(fun(R) -> io:format("~ts~n", [R]) end, zx_lib:list_realms()), halt(0). @@ -729,74 +730,6 @@ list_versions(PackageName) -> end. --spec list_pending(PackageName :: string()) -> no_return(). -%% @private -%% List the versions of a package that are pending review. The package name is input by -%% the user as a string of the form "otpr-zomp" and the output is a list of full -%% package IDs, printed one per line to stdout (like "otpr-zomp-3.2.2"). - -list_pending(PackageName) -> - Package = {Realm, Name} = - case zx_lib:package_id(PackageName) of - {ok, {R, N, {z, z, z}}} -> - {R, N}; - {error, invalid_package_string} -> - error_exit("~tp is not a valid package name.", [PackageName], ?LINE) - end, - ok = start(), - case zx_daemon:list_pending(Package) of - {ok, []} -> - Message = "Package ~ts has no versions pending.", - ok = log(info, Message, [PackageName]), - halt(0); - {ok, Versions} -> - Print = - fun(Version) -> - {ok, PackageString} = zx_lib:package_string({Realm, Name, Version}), - io:format("~ts~n", [PackageString]) - end, - ok = lists:foreach(Print, Versions), - halt(0); - {error, bad_realm} -> - error_exit("Bad realm name.", ?LINE); - {error, bad_package} -> - error_exit("Bad package name.", ?LINE); - {error, network} -> - Message = "Network issues are preventing connection to the realm.", - error_exit(Message, ?LINE) - end. - - --spec list_resigns(realm()) -> no_return(). -%% @private -%% List the package ids of all packages waiting in the resign queue for the given realm, -%% printed to stdout one per line. - -list_resigns(Realm) -> - ok = start(), - case zx_daemon:list_resigns(Realm) of - {ok, []} -> - Message = "No packages pending signature in ~tp.", - ok = log(info, Message, [Realm]), - halt(0); - {ok, PackageIDs} -> - Print = - fun(PackageID) -> - {ok, PackageString} = zx_lib:package_string(PackageID), - io:format("~ts~n", [PackageString]) - end, - ok = lists:foreach(Print, PackageIDs), - halt(0); - {error, bad_realm} -> - error_exit("Bad realm name.", ?LINE); - {error, no_realm} -> - error_exit("Realm \"~ts\" is not configured.", ?LINE); - {error, network} -> - Message = "Network issues are preventing connection to the realm.", - error_exit(Message, ?LINE) - end. - - %%% Add realm @@ -829,7 +762,7 @@ add_realm(Path) -> Data :: binary(). add_realm(Path, Data) -> - case erl_tar:extract({binary, Data}, [compressed, {cwd, zx_lib:zomp_home()}]) of + case erl_tar:extract({binary, Data}, [compressed, {cwd, zx_lib:zomp_dir()}]) of ok -> {Realm, _} = string:take(filename:basename(Path), ".", true), ok = log(info, "Realm ~ts is now visible to this system.", [Realm]), @@ -841,124 +774,6 @@ add_realm(Path, Data) -> end. --spec add_package(PackageName) -> no_return() - when PackageName :: package(). -%% @private -%% A sysop command that adds a package to a realm operated by the caller. - -add_package(PackageName) -> - ok = file:set_cwd(zx_lib:zomp_home()), - case zx_lib:package_id(PackageName) of - {ok, {Realm, Name, {z, z, z}}} -> - add_package(Realm, Name); - _ -> - error_exit("~tp is not a valid package name.", [PackageName], ?LINE) - end. - - --spec add_package(Realm, Name) -> no_return() - when Realm :: realm(), - Name :: name(). - -add_package(Realm, Name) -> - Socket = connect_auth_or_die(Realm), - ok = send(Socket, {add_package, {Realm, Name}}), - ok = recv_or_die(Socket), - ok = log(info, "\"~ts-~ts\" added successfully.", [Realm, Name]), - halt(0). - - -add_packager(Package, UserName) -> - ok = log(info, "Would add ~ts to packagers for ~160tp now.", [UserName, Package]), - halt(0). - - -add_maintainer(Package, UserName) -> - ok = log(info, "Would add ~ts to maintainer for ~160tp now.", [UserName, Package]), - halt(0). - - -review(PackageString) -> - PackageID = {Realm, _, _} = zx_lib:package_id(PackageString), - Socket = connect_auth_or_die(Realm), - ok = send(Socket, {review, PackageID}), - {ok, ZrpBin} = recv_or_die(Socket), - ok = send(Socket, ok), - ok = disconnect(Socket), - {ok, Files} = erl_tar:extract({binary, ZrpBin}, [memory]), - {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), - Meta = binary_to_term(MetaBin, [safe]), - PackageID = maps:get(package_id, Meta), - {KeyID, Signature} = maps:get(sig, Meta), - {ok, PubKey} = loadkey(public, KeyID), - TgzFile = PackageString ++ ".tgz", - {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), - ok = verify(TgzData, Signature, PubKey), - ok = - case file:make_dir(PackageString) of - ok -> - log(info, "Will unpack to directory ./~ts", [PackageString]); - {error, Error} -> - Message = "Creating dir ./~ts failed with ~ts. Aborting.", - ok = log(error, Message, [PackageString, Error]), - halt(1) - end, - ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageString}]), - ok = log(info, "Unpacked and awaiting inspection."), - halt(0). - - -approve(PackageID = {Realm, _, _}) -> - Socket = connect_auth_or_die(Realm), - ok = send(Socket, {approve, PackageID}), - ok = recv_or_die(Socket), - ok = log(info, "ok"), - halt(0). - - -reject(PackageID = {Realm, _, _}) -> - Socket = connect_auth_or_die(Realm), - ok = send(Socket, {reject, PackageID}), - ok = recv_or_die(Socket), - ok = log(info, "ok"), - halt(0). - - -resign(PackageString) -> - PackageID = {Realm, _, _} = zx_lib:package_id(PackageString), - RealmConf = load_realm_conf(Realm), - {package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf), - KeySelection = [{K, {R, K}} || {R, K} <- [element(1, K) || K <- PackageKeys]], - PackageKeyID = select(KeySelection), - {ok, PackageKey} = loadkey(private, PackageKeyID), - Socket = connect_auth_or_die(Realm), - ok = send(Socket, {resign, PackageID}), - {ok, ZrpBin} = recv_or_die(Socket), - {ok, Files} = erl_tar:extract({binary, ZrpBin}, [memory]), - {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), - Meta = binary_to_term(MetaBin, [safe]), - PackageID = maps:get(package_id, Meta), - {KeyID, Signature} = maps:get(sig, Meta), - {ok, PubKey} = loadkey(public, KeyID), - TgzFile = PackageString ++ ".tgz", - {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), - ok = verify(TgzData, Signature, PubKey), - ReSignature = public_key:sign(TgzData, sha512, PackageKey), - FinalMeta = maps:put(sig, {PackageKeyID, ReSignature}, Meta), - NewMetaBin = term_to_binary(FinalMeta), - NewFiles = lists:keystore("zomp.meta", 1, Files, {"zomp.meta", NewMetaBin}), - ResignedZrp = PackageString ++ ".zrp.resign", - ok = erl_tar:create(ResignedZrp, NewFiles), - {ok, ResignedBin} = file:read_file(ResignedZrp), - ok = gen_tcp:send(Socket, ResignedBin), - ok = recv_or_die(Socket), - ok = file:delete(ResignedZrp), - ok = recv_or_die(Socket), - ok = disconnect(Socket), - ok = log(info, "Resigned ~ts", [PackageString]), - halt(0). - - %%% Drop dependency @@ -994,9 +809,9 @@ drop_dep(PackageID) -> %% error exit value (this instruction is idempotent if used in shell scripts). drop_key({Realm, KeyName}) -> - ok = file:set_cwd(zx_lib:zomp_home()), + ok = file:set_cwd(zx_lib:zomp_dir()), KeyGlob = KeyName ++ ".{key,pub},der", - Pattern = filename:join([zx_lib:zomp_home(), "key", Realm, KeyGlob]), + Pattern = filename:join([zx_lib:zomp_dir(), "key", Realm, KeyGlob]), case filelib:wildcard(Pattern) of [] -> ok = log(warning, "Key ~ts/~ts not found", [Realm, KeyName]), @@ -1013,7 +828,7 @@ drop_key({Realm, KeyName}) -> -spec drop_realm(realm()) -> no_return(). drop_realm(Realm) -> - ok = file:set_cwd(zx_lib:zomp_home()), + ok = file:set_cwd(zx_lib:zomp_dir()), RealmConf = zx_lib:realm_conf(Realm), case filelib:is_regular(RealmConf) of true -> @@ -1022,7 +837,7 @@ drop_realm(Realm) -> " WARNING: Are you SURE you want to remove realm ~ts?~n" " (Only \"Y\" will confirm this action.)~n", ok = io:format(Message, [Realm]), - case get_input() of + case zx_tty:get_input() of "Y" -> ok = file:delete(RealmConf), ok = drop_prime(Realm), @@ -1058,7 +873,7 @@ drop_prime(Realm) -> -spec clear_keys(realm()) -> ok. clear_keys(Realm) -> - KeyDir = filename:join([zx_lib:zomp_home(), "key", Realm]), + KeyDir = filename:join([zx_lib:zomp_dir(), "key", Realm]), case filelib:is_dir(KeyDir) of true -> rm_rf(KeyDir); false -> log(warning, "Keydir ~ts not found", [KeyDir]) @@ -1133,21 +948,21 @@ package(TargetDir) -> ok = log(info, "Packaging ~ts", [TargetDir]), {ok, Meta} = zx_lib:read_project_meta(TargetDir), {Realm, _, _} = maps:get(package_id, Meta), - KeyDir = filename:join([zx_lib:zomp_home(), "key", Realm]), - ok = force_dir(KeyDir), + KeyDir = filename:join([zx_lib:zomp_dir(), "key", Realm]), + ok = zx_lib:force_dir(KeyDir), Pattern = KeyDir ++ "/*.key.der", case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of [] -> ok = log(info, "Need to generate key"), - KeyID = prompt_keygen(), - {ok, _, _} = generate_rsa(KeyID), + KeyID = zx_key:prompt_keygen(), + {ok, _, _} = zx_key:generate_rsa(KeyID), package(KeyID, TargetDir); [KeyName] -> KeyID = {Realm, KeyName}, ok = log(info, "Using key: ~ts/~ts", [Realm, KeyName]), package(KeyID, TargetDir); KeyNames -> - KeyName = select_string(KeyNames), + KeyName = zx_tty:select_string(KeyNames), package({Realm, KeyName}, TargetDir) end. @@ -1157,16 +972,16 @@ package(TargetDir) -> TargetDir :: file:filename(). %% @private %% Accept a KeyPrefix for signing and a TargetDir containing a project to package and -%% build a zrp package file ready to be submitted to a repository. +%% build a zsp package file ready to be submitted to a repository. package(KeyID, TargetDir) -> {ok, Meta} = zx_lib:read_project_meta(TargetDir), PackageID = maps:get(package_id, Meta), true = element(1, PackageID) == element(1, KeyID), {ok, PackageString} = zx_lib:package_string(PackageID), - ZrpFile = PackageString ++ ".zrp", + ZrpFile = PackageString ++ ".zsp", TgzFile = PackageString ++ ".tgz", - ok = halt_if_exists(ZrpFile), + ok = zx_lib:halt_if_exists(ZrpFile), ok = remove_binaries(TargetDir), {ok, Everything} = file:list_dir(TargetDir), DotFiles = filelib:wildcard(".*", TargetDir), @@ -1180,7 +995,7 @@ package(KeyID, TargetDir) -> ok = remove_binaries("."), ok = erl_tar:create(filename:join(CWD, TgzFile), Targets, [compressed]), ok = file:set_cwd(CWD), - {ok, Key} = loadkey(private, KeyID), + {ok, Key} = zx_key:load(private, KeyID), {ok, TgzBin} = file:read_file(TgzFile), Sig = public_key:sign(TgzBin, sha512, Key), Add = fun({K, V}, M) -> maps:put(K, V, M) end, @@ -1208,505 +1023,6 @@ remove_binaries(TargetDir) -> ok = log(info, "Removing: ~tp", [ToDelete]), lists:foreach(fun file:delete/1, ToDelete) end. - - - -%%% Package submission - --spec submit(PackageFile) -> no_return() - when PackageFile :: file:filename(). -%% @private -%% Submit a package to the appropriate "prime" server for the given realm. - -submit(PackageFile) -> - Files = extract_zrp_or_die(PackageFile), - {ok, PackageData} = file:read_file(PackageFile), - {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), - Meta = binary_to_term(MetaBin), - {Realm, Package, Version} = maps:get(package_id, Meta), - {ok, Socket} = connect_auth(Realm), - ok = send(Socket, {submit, {Realm, Package, Version}}), - ok = recv_or_die(Socket), - ok = gen_tcp:send(Socket, PackageData), - ok = log(info, "Done sending contents of ~tp", [PackageFile]), - Outcome = recv_or_die(Socket), - log(info, "Response: ~tp", [Outcome]), - ok = disconnect(Socket), - halt(0). - - - -%%% Authenticated communication with prime - --spec send(Socket, Message) -> ok - when Socket :: gen_tcp:socket(), - Message :: term(). -%% @private -%% Wrapper for the procedure necessary to send an internal message over the wire. - -send(Socket, Message) -> - Bin = term_to_binary(Message), - gen_tcp:send(Socket, Bin). - - --spec recv_or_die(Socket) -> Result | no_return() - when Socket :: gen_tcp:socket(), - Result :: ok | {ok, term()}. - -recv_or_die(Socket) -> - receive - {tcp, Socket, Bin} -> - case binary_to_term(Bin) of - ok -> - ok; - {ok, Response} -> - {ok, Response}; - {error, Reason} -> - error_exit("Action failed with: ~tp", [Reason], ?LINE); - Unexpected -> - error_exit("Unexpected message: ~tp", [Unexpected], ?LINE) - end; - {tcp_closed, Socket} -> - error_exit("Lost connection to node unexpectedly.", ?LINE) - after 5000 -> - error_exit("Connection timed out.", ?LINE) - end. - - --spec connect_auth_or_die(realm()) -> gen_tcp:socket() | no_return(). - -connect_auth_or_die(Realm) -> - case connect_auth(Realm) of - {ok, Socket} -> - Socket; - Error -> - M1 = "Connection failed to realm prime with ~160tp.", - ok = log(warning, M1, [Error]), - halt(1) - end. - - --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) -> - RealmConf = load_realm_conf(Realm), - {User, KeyID, Key} = prep_auth(Realm, RealmConf), - {prime, {Host, Port}} = lists:keyfind(prime, 1, RealmConf), - Options = [{packet, 4}, {mode, binary}, {active, true}], - case gen_tcp:connect(Host, Port, Options, 5000) of - {ok, Socket} -> - ok = log(info, "Connected to ~tp prime.", [Realm]), - connect_auth(Socket, Realm, User, KeyID, Key); - Error = {error, E} -> - ok = log(warning, "Connection problem: ~tp", [E]), - {error, Error} - end. - - --spec connect_auth(Socket, Realm, User, KeyID, Key) -> Result - when Socket :: gen_tcp:socket(), - Realm :: realm(), - User :: user_id(), - KeyID :: key_id(), - Key :: term(), - Result :: {ok, gen_tcp:socket()} - | {error, timeout}. -%% @private -%% 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. - -connect_auth(Socket, Realm, User, KeyID, Key) -> - ok = gen_tcp:send(Socket, <<"OTPR AUTH 1">>), - receive - {tcp, Socket, Bin} -> - ok = binary_to_term(Bin, [safe]), - confirm_auth(Socket, Realm, User, KeyID, Key); - {tcp_closed, Socket} -> - ok = log(warning, "Socket closed unexpectedly."), - halt(1) - after 5000 -> - 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} -> - ok = log(warning, "Socket closed unexpectedly."), - halt(1) - 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} -> - ok = log(warning, "Socket closed unexpectedly."), - halt(1) - after 5000 -> - {error, timeout} - end. - - --spec prep_auth(Realm, RealmConf) -> {User, KeyID, Key} | no_return() - when Realm :: realm(), - RealmConf :: [term()], - User :: user_id(), - 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) -> - UsersFile = filename:join(zx_lib:zomp_home(), "zomp.users"), - Users = - case file:consult(UsersFile) 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 disconnect(gen_tcp:socket()) -> ok. -%% @private -%% Gracefully shut down a socket, logging (but sidestepping) the case when the socket -%% has already been closed by the other side. - -disconnect(Socket) -> - case gen_tcp:shutdown(Socket, read_write) of - ok -> - log(info, "Disconnected from ~tp", [Socket]); - {error, Error} -> - Message = "Shutdown connection ~p failed with: ~p", - log(warning, Message, [Socket, Error]) - end. - - - -%%% Key utilities - --spec ensure_keypair(key_id()) -> true | no_return(). -%% @private -%% Check if both the public and private key based on KeyID exists. - -ensure_keypair(KeyID = {Realm, KeyName}) -> - case {have_public_key(KeyID), have_private_key(KeyID)} of - {true, true} -> - true; - {false, true} -> - Message = "Public key for ~tp/~tp cannot be found", - ok = log(error, Message, [Realm, KeyName]), - halt(1); - {true, false} -> - Message = "Private key for ~tp/~tp cannot be found", - ok = log(error, Message, [Realm, KeyName]), - halt(1); - {false, false} -> - Message = "Key pair for ~tp/~tp cannot be found", - ok = log(error, Message, [Realm, KeyName]), - halt(1) - end. - - --spec have_public_key(key_id()) -> boolean(). -%% @private -%% Determine whether the public key indicated by KeyID is in the keystore. - -have_public_key({Realm, KeyName}) -> - PublicKeyFile = KeyName ++ ".pub.der", - PublicKeyPath = filename:join([zx_lib:zomp_home(), "key", Realm, PublicKeyFile]), - filelib:is_regular(PublicKeyPath). - - --spec have_private_key(key_id()) -> boolean(). -%% @private -%% Determine whether the private key indicated by KeyID is in the keystore. - -have_private_key({Realm, KeyName}) -> - PrivateKeyFile = KeyName ++ ".key.der", - PrivateKeyPath = filename:join([zx_lib:zomp_home(), "key", Realm, PrivateKeyFile]), - filelib:is_regular(PrivateKeyPath). - - - -%%% Key generation - --spec prompt_keygen() -> key_id(). -%% @private -%% Prompt the user for a valid KeyPrefix to use for naming a new RSA keypair. - -prompt_keygen() -> - Message = - "~n Enter a name for your new keys.~n~n" - " Valid names must start with a lower-case letter, and can include~n" - " only lower-case letters, numbers, and periods, but no series of~n" - " consecutive periods. (That is: [a-z0-9\\.])~n~n" - " To designate the key as realm-specific, enter the realm name and~n" - " key name separated by a space.~n~n" - " Example: some.realm my.key~n", - ok = io:format(Message), - Input = get_input(), - {Realm, KeyName} = - case string:lexemes(Input, " ") of - [R, K] -> {R, K}; - [K] -> {"otpr", K} - end, - case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_label(KeyName)} of - {true, true} -> - {Realm, KeyName}; - {false, true} -> - ok = io:format("Bad realm name ~tp. Try again.~n", [Realm]), - prompt_keygen(); - {true, false} -> - ok = io:format("Bad key name ~tp. Try again.~n", [KeyName]), - prompt_keygen(); - {false, false} -> - ok = io:format("NUTS! Both key and realm names are illegal. Try again.~n"), - prompt_keygen() - end. - - --spec create_keypair() -> no_return(). -%% @private -%% Execute the key generation procedure for 16k RSA keys once and then terminate. - -create_keypair() -> - ok = file:set_cwd(zx_lib:zomp_home()), - KeyID = prompt_keygen(), - case generate_rsa(KeyID) of - {ok, _, _} -> halt(0); - Error -> error_exit("create_keypair/0 error: ~tp", [Error], ?LINE) - end. - - --spec generate_rsa(KeyID) -> Result - when KeyID :: key_id(), - Result :: {ok, KeyFile, PubFile} - | {error, keygen_fail}, - KeyFile :: file:filename(), - PubFile :: file:filename(). -%% @private -%% Generate an RSA keypair and write them in der format to the current directory, using -%% filenames derived from Prefix. -%% NOTE: The current version of this command is likely to only work on a unix system. - -generate_rsa({Realm, KeyName}) -> - KeyDir = filename:join([zx_lib:zomp_home(), "key", Realm]), - ok = force_dir(KeyDir), - PemFile = filename:join(KeyDir, KeyName ++ ".pub.pem"), - KeyFile = filename:join(KeyDir, KeyName ++ ".key.der"), - PubFile = filename:join(KeyDir, KeyName ++ ".pub.der"), - ok = lists:foreach(fun halt_if_exists/1, [PemFile, KeyFile, PubFile]), - ok = log(info, "Generating ~p and ~p. Please be patient...", [KeyFile, PubFile]), - ok = gen_p_key(KeyFile), - ok = der_to_pem(KeyFile, PemFile), - {ok, PemBin} = file:read_file(PemFile), - [PemData] = public_key:pem_decode(PemBin), - Pub = public_key:pem_entry_decode(PemData), - PubDer = public_key:der_encode('RSAPublicKey', Pub), - ok = file:write_file(PubFile, PubDer), - case check_key(KeyFile, PubFile) of - true -> - ok = file:delete(PemFile), - ok = log(info, "~ts and ~ts agree", [KeyFile, PubFile]), - ok = log(info, "Wrote private key to: ~ts.", [KeyFile]), - ok = log(info, "Wrote public key to: ~ts.", [PubFile]), - {ok, KeyFile, PubFile}; - false -> - ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]), - ok = log(error, "Something has gone wrong."), - {error, keygen_fail} - end. - - --spec halt_if_exists(file:filename()) -> ok | no_return(). -%% @private -%% A helper function to guard against overwriting an existing file. Halts execution if -%% the file is found to exist. - -halt_if_exists(Path) -> - case filelib:is_file(Path) of - true -> error_exit("~ts already exists! Halting.", [Path], ?LINE); - false -> ok - end. - - --spec gen_p_key(KeyFile) -> ok - when KeyFile :: file:filename(). -%% @private -%% Format an openssl shell command that will generate proper 16k RSA keys. - -gen_p_key(KeyFile) -> - Command = - io_lib:format("~ts genpkey" - " -algorithm rsa" - " -out ~ts" - " -outform DER" - " -pkeyopt rsa_keygen_bits:16384", - [openssl(), KeyFile]), - Out = os:cmd(Command), - io:format(Out). - - --spec der_to_pem(KeyFile, PemFile) -> ok - when KeyFile :: file:filename(), - PemFile :: file:filename(). -%% @private -%% Format an openssl shell command that will convert the given keyfile to a pemfile. -%% The reason for this conversion is to sidestep some formatting weirdness that OpenSSL -%% injects into its generated DER formatted key output (namely, a few empty headers) -%% which Erlang's ASN.1 defintion files do not take into account. A conversion to PEM -%% then a conversion back to DER (via Erlang's ASN.1 module) resolves this in a reliable -%% way. - -der_to_pem(KeyFile, PemFile) -> - Command = - io_lib:format("~ts rsa" - " -inform DER" - " -in ~ts" - " -outform PEM" - " -pubout" - " -out ~ts", - [openssl(), KeyFile, PemFile]), - Out = os:cmd(Command), - io:format(Out). - - --spec check_key(KeyFile, PubFile) -> Result - when KeyFile :: file:filename(), - PubFile :: file:filename(), - Result :: true | false. -%% @private -%% Compare two keys for pairedness. - -check_key(KeyFile, PubFile) -> - {ok, KeyBin} = file:read_file(KeyFile), - {ok, PubBin} = file:read_file(PubFile), - Key = public_key:der_decode('RSAPrivateKey', KeyBin), - Pub = public_key:der_decode('RSAPublicKey', PubBin), - TestMessage = <<"Some test data to sign.">>, - Signature = public_key:sign(TestMessage, sha512, Key), - public_key:verify(TestMessage, sha512, Signature, Pub). - - --spec openssl() -> Executable | no_return() - when Executable :: file:filename(). -%% @private -%% Attempt to locate the installed openssl executable for use in shell commands. -%% Halts execution with an error message if the executable cannot be found. - -openssl() -> - OpenSSL = - case os:type() of - {unix, _} -> "openssl"; - {win32, _} -> "openssl.exe" - end, - ok = - case os:find_executable(OpenSSL) of - false -> - ok = log(error, "OpenSSL could not be found in this system's PATH."), - ok = log(error, "Install OpenSSL and then retry."), - error_exit("Missing system dependenct: OpenSSL", ?LINE); - Path -> - log(info, "OpenSSL executable found at: ~ts", [Path]) - end, - OpenSSL. - - --spec loadkey(Type, KeyID) -> Result - when Type :: private | public, - KeyID :: key_id(), - Result :: {ok, DecodedKey :: term()} - | {error, Reason :: term()}. -%% @private -%% Hide the details behind reading and loading DER encoded RSA key files. - -loadkey(Type, {Realm, KeyName}) -> - {DerType, Path} = - case Type of - private -> - KeyDer = KeyName ++ ".key.der", - P = filename:join([zx_lib:zomp_home(), "key", Realm, KeyDer]), - {'RSAPrivateKey', P}; - public -> - PubDer = KeyName ++ ".pub.der", - P = filename:join([zx_lib:zomp_home(), "key", Realm, PubDer]), - {'RSAPublicKey', P} - end, - ok = log(info, "Loading key from file ~ts", [Path]), - case file:read_file(Path) of - {ok, Bin} -> {ok, public_key:der_decode(DerType, Bin)}; - Error -> Error - end. @@ -1749,7 +1065,7 @@ build_plt() -> -spec default_plt() -> file:filename(). default_plt() -> - filename:join(zx_lib:zomp_home(), "basic.plt"). + filename:join(zx_lib:zomp_dir(), "basic.plt"). @@ -1768,7 +1084,7 @@ dialyze() -> true -> log(info, "Using PLT: ~tp", [PLT]); false -> build_plt() end, - TmpDir = filename:join(zx_lib:zomp_home(), "tmp"), + TmpDir = filename:join(zx_lib:zomp_dir(), "tmp"), Me = escript:script_name(), EvilTwin = filename:join(TmpDir, filename:basename(Me ++ ".erl")), ok = log(info, "Temporarily reconstructing ~tp as ~tp", [Me, EvilTwin]), @@ -1815,10 +1131,10 @@ create_realm() -> " Names can contain only lower-case letters, numbers and the underscore.~n" " Names must begin with a lower-case letter.~n", ok = io:format(Instructions), - Realm = get_input(), + Realm = zx_tty:get_input(), case zx_lib:valid_lower0_9(Realm) of true -> - RealmFile = filename:join(zx_lib:zomp_home(), Realm ++ ".realm"), + RealmFile = filename:join(zx_lib:zomp_dir(), Realm ++ ".realm"), case filelib:is_regular(RealmFile) of false -> create_realm(Realm); @@ -1846,7 +1162,7 @@ create_realm(Realm) -> prompt_external_address() -> Message = external_address_prompt(), ok = io:format(Message), - case get_input() of + case zx_tty:get_input() of "" -> ok = io:format("You need to enter an address.~n"), prompt_external_address(); @@ -1914,7 +1230,7 @@ prompt_port_number(Current) -> " A valid port is any number from 1 to 65535." " [Press enter to accept the current setting: ~tw]~n", ok = io:format(Instructions, [Current]), - case get_input() of + case zx_tty:get_input() of "" -> Current; S -> @@ -1947,7 +1263,7 @@ create_realm(Realm, ExAddress, ExPort, InPort) -> " Names can contain only lower-case letters, numbers and the underscore.~n" " Names must begin with a lower-case letter.~n", ok = io:format(Instructions), - UserName = get_input(), + UserName = zx_tty:get_input(), case zx_lib:valid_lower0_9(UserName) of true -> create_realm(Realm, ExAddress, ExPort, InPort, UserName); @@ -1972,7 +1288,7 @@ create_realm(Realm, ExAddress, ExPort, InPort, UserName) -> "minimal. Check the address you enter carefully. The only people who will " "suffer from an invalid address are your users.~n", ok = io:format(Instructions), - Email = get_input(), + Email = zx_tty:get_input(), [User, Host] = string:lexemes(Email, "@"), case {zx_lib:valid_lower0_9(User), zx_lib:valid_label(Host)} of {true, true} -> @@ -2007,7 +1323,7 @@ create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email) -> " Enter the real name (or whatever name people recognize) for the sysop.~n" " There are no rules for this one. Any valid UTF-8 printables are legal.~n", ok = io:format(Instructions), - RealName = get_input(), + RealName = zx_tty:get_input(), create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName). @@ -2022,9 +1338,9 @@ create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email) -> create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName) -> ok = io:format("~nGenerating keys. This might take a while, so settle in...~n"), - {ok, RealmKey, RealmPub} = generate_rsa({Realm, Realm ++ ".1.realm"}), - {ok, PackageKey, PackagePub} = generate_rsa({Realm, Realm ++ ".1.package"}), - {ok, SysopKey, SysopPub} = generate_rsa({Realm, UserName ++ ".1"}), + {ok, RealmKey, RealmPub} = zx_key:generate_rsa({Realm, Realm ++ ".1.realm"}), + {ok, PackageKey, PackagePub} = zx_key:generate_rsa({Realm, Realm ++ ".1.package"}), + {ok, SysopKey, SysopPub} = zx_key:generate_rsa({Realm, UserName ++ ".1"}), ok = log(info, "Generated 16k RSA pair ~ts ~ts", [RealmKey, RealmPub]), ok = log(info, "Generated 16k RSA pair ~ts ~ts", [PackageKey, PackagePub]), ok = log(info, "Generated 16k RSA pair ~ts ~ts", [SysopKey, SysopPub]), @@ -2140,7 +1456,7 @@ create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName) -> -spec create_realmfile(realm()) -> no_return(). create_realmfile(Realm) -> - RealmConf = load_realm_conf(Realm), + RealmConf = zx_lib: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), @@ -2158,7 +1474,7 @@ create_realmfile(Realm) -> create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(zx_lib:zomp_home()), + ok = file:set_cwd(zx_lib:zomp_dir()), KeyPath = fun({R, K}) -> filename:join(["key", R, K ++ ".pub.der"]) end, RealmKeyPaths = lists:map(KeyPath, RealmKeyIDs), PackageKeyPaths = lists:map(KeyPath, PackageKeyIDs), @@ -2169,13 +1485,6 @@ create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> halt(0). --spec create_sysop() -> no_return(). - -create_sysop() -> - ok = log(info, "Fo' realz, yo! We be sysoppin up in hurr!"), - halt(0). - - %%% Package utilities @@ -2184,8 +1493,8 @@ create_sysop() -> %% @private %% Install a package from the cache into the local system. %% Before calling this function it must be known that: -%% - The zrp file is in the cache -%% - The zrp file is valid +%% - The zsp file is in the cache +%% - The zsp file is valid %% - This function will only be called on startup by the launch process %% - The package is not already installed %% - If this function crashes it will completely halt the system @@ -2193,38 +1502,22 @@ create_sysop() -> install(PackageID) -> {ok, PackageString} = zx_lib:package_string(PackageID), ok = log(info, "Installing ~ts", [PackageString]), - ZrpFile = filename:join("zrp", zx_lib:namify_zrp(PackageID)), - Files = extract_zrp_or_die(ZrpFile), + ZrpFile = filename:join("zsp", zx_lib:namify_zsp(PackageID)), + Files = zx_lib:extract_zsp_or_die(ZrpFile), TgzFile = zx_lib:namify_tgz(PackageID), {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), {KeyID, Signature} = maps:get(sig, Meta), - {ok, PubKey} = loadkey(public, KeyID), + {ok, PubKey} = zx_key:load(public, KeyID), ok = ensure_package_dirs(PackageID), PackageDir = filename:join("lib", PackageString), - ok = force_dir(PackageDir), - ok = verify(TgzData, Signature, PubKey), + ok = zx_lib:force_dir(PackageDir), + ok = zx_key:verify(TgzData, Signature, PubKey), ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageDir}]), log(info, "~ts installed", [PackageString]). --spec verify(Data, Signature, PubKey) -> ok | no_return() - when Data :: binary(), - Signature :: binary(), - PubKey :: public_key:rsa_public_key(). -%% @private -%% Verify the RSA Signature of some Data against the given PubKey or halt execution. -%% This function always assumes sha512 is the algorithm being used. -%% Should only ever be called by the initial launch process. - -verify(Data, Signature, PubKey) -> - case public_key:verify(Data, sha512, Signature, PubKey) of - true -> ok; - false -> error_exit("Bad package signature!", ?LINE) - end. - - -spec build(package_id()) -> ok. %% @private %% Given an AppID, build the project from source and add it to the current lib path. @@ -2258,90 +1551,6 @@ build() -> -%%% User menu interface (terminal) - --spec get_input() -> string(). -%% @private -%% Provide a standard input prompt and newline sanitized return value. - -get_input() -> - string:trim(io:get_line("(^C to quit): ")). - - --spec select(Options) -> Selected - when Options :: [option()], - Selected :: term(). -%% @private -%% Take a list of Options to present the user, then return the indicated option to the -%% caller once the user selects something. - -select(Options) -> - Max = show(Options), - case pick(string:to_integer(io:get_line("(or ^C to quit)~n ? ")), Max) of - error -> - ok = hurr(), - select(Options); - I -> - {_, Value} = lists:nth(I, Options), - Value - end. - - --spec select_string(Strings) -> Selected - when Strings :: [string()], - Selected :: string(). -%% @private -%% @equiv select([{S, S} || S <- Strings]) - -select_string(Strings) -> - select([{S, S} || S <- Strings]). - - --spec show(Options) -> Index - when Options :: [option()], - Index :: pos_integer(). -%% @private -%% @equiv show(Options, 0). - -show(Options) -> - show(Options, 0). - - --spec show(Options, Index) -> Count - when Options :: [option()], - Index :: non_neg_integer(), - Count :: pos_integer(). -%% @private -%% Display the list of options needed to the user, and return the option total count. - -show([], I) -> - I; -show([{Label, _} | Rest], I) -> - Z = I + 1, - ok = io:format(" ~2w - ~ts~n", [Z, Label]), - show(Rest, Z). - - --spec pick({Selection, term()}, Max) -> Result - when Selection :: error | integer(), - Max :: pos_integer(), - Result :: pos_integer() | error. -%% @private -%% Interpret a user's selection returning either a valid selection index or `error'. - -pick({error, _}, _) -> error; -pick({I, _}, Max) when 0 < I, I =< Max -> I; -pick(_, _) -> error. - - --spec hurr() -> ok. -%% @private -%% Present an appropriate response when the user derps on selection. - -hurr() -> io:format("That isn't an option.~n"). - - - %%% Directory & File Management -spec mktemp_dir(Prefix) -> Result @@ -2400,64 +1609,10 @@ ensure_package_dirs(PackageID = {Realm, Name, _}) -> PackageData = zx_lib:package_dir("var", Package), PackageConf = zx_lib:package_dir("etc", Package), Dirs = [PackageHome, PackageData, PackageConf], - ok = lists:foreach(fun force_dir/1, Dirs), + ok = lists:foreach(fun zx_lib:force_dir/1, Dirs), log(info, "Created dirs:~n\t~ts~n\t~ts~n\t~ts", Dirs). --spec force_dir(Path) -> Result - when Path :: file:filename(), - Result :: ok - | {error, file:posix()}. -%% @private -%% Guarantee a directory path is created if it is possible to create or if it already -%% exists. - -force_dir(Path) -> - case filelib:is_dir(Path) of - true -> ok; - false -> filelib:ensure_dir(filename:join(Path, "foo")) - end. - - --spec extract_zrp_or_die(FileName) -> Files | no_return() - when FileName :: file:filename(), - Files :: [{file:filename(), binary()}]. -%% @private -%% Extract a zrp archive, if possible. If not possible, halt execution with as accurate -%% an error message as can be managed. - -extract_zrp_or_die(FileName) -> - case erl_tar:extract(FileName, [memory]) of - {ok, Files} -> - Files; - {error, {FileName, enoent}} -> - Message = "Can't find file ~ts.", - error_exit(Message, [FileName], ?LINE); - {error, invalid_tar_checksum} -> - Message = "~ts is not a valid zrp archive.", - error_exit(Message, [FileName], ?LINE); - {error, Reason} -> - Message = "Extracting package file failed with: ~160tp.", - error_exit(Message, [Reason], ?LINE) - end. - - --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) -> - case zx_lib:load_realm_conf(Realm) of - {ok, C} -> - C; - {error, enoent} -> - ok = log(warning, "Realm ~tp is not configured.", [Realm]), - halt(1) - end. - - %%% Usage @@ -2503,7 +1658,7 @@ usage() -> " zx review PackageID~n" " zx approve PackageID~n" " zx reject PackageID~n" - " zx resign PackageID~n" + " zx accept PackageID~n" " zx drop dep PackageID~n" " zx drop key Realm KeyName~n" " zx drop realm Realm~n" @@ -2527,7 +1682,7 @@ usage() -> " Realm :: The name of a realm as a string [:a-z:]~n" " KeyName :: The prefix of a keypair to drop~n" " Level :: The version level, one of \"major\", \"minor\", or \"patch\"~n" - " Path :: Path to a valid project directory or .zrp file~n" + " Path :: Path to a valid project directory or .zsp file~n" "~n", io:format(T). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl new file mode 100644 index 0000000..e909189 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl @@ -0,0 +1,452 @@ +%%% @doc +%%% ZX Auth +%%% +%%% This module is where all the AUTH type command code lives. AUTH commands are special +%%% because they do not involve the zx_daemon at all, though they do perform network +%%% operations. +%%% +%%% All AUTH procedures terminate the runtime once complete. +%%% @end + +-module(zx_auth). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + +-export([list_pending/1, list_resigns/1, + submit/1, review/1, approve/1, reject/1, accept/1, + add_packager/2, add_maintainer/2, add_sysop/1, + add_package/1]). + +-include("zx_logger.hrl"). + + + +%%% Functions + + +-spec list_pending(PackageName :: string()) -> no_return(). +%% @private +%% List the versions of a package that are pending review. The package name is input by +%% the user as a string of the form "otpr-zomp" and the output is a list of full +%% package IDs, printed one per line to stdout (like "otpr-zomp-3.2.2"). + +list_pending(PackageName) -> + Package = {Realm, Name} = + case zx_lib:package_id(PackageName) of + {ok, {R, N, {z, z, z}}} -> + {R, N}; + {error, invalid_package_string} -> + zx:error_exit("~tp is not a valid package name.", [PackageName], ?LINE) + end, + case zx_daemon:list_pending(Package) of + {ok, []} -> + Message = "Package ~ts has no versions pending.", + ok = log(info, Message, [PackageName]), + halt(0); + {ok, Versions} -> + Print = + fun(Version) -> + {ok, PackageString} = zx_lib:package_string({Realm, Name, Version}), + io:format("~ts~n", [PackageString]) + end, + ok = lists:foreach(Print, Versions), + halt(0); + {error, bad_realm} -> + zx:error_exit("Bad realm name.", ?LINE); + {error, bad_package} -> + zx:error_exit("Bad package name.", ?LINE); + {error, network} -> + Message = "Network issues are preventing connection to the realm.", + zx:error_exit(Message, ?LINE) + end. + + +-spec list_resigns(zx:realm()) -> no_return(). +%% @private +%% List the package ids of all packages waiting in the resign queue for the given realm, +%% printed to stdout one per line. + +list_resigns(Realm) -> + case zx_daemon:list_resigns(Realm) of + {ok, []} -> + Message = "No packages pending signature in ~tp.", + ok = log(info, Message, [Realm]), + halt(0); + {ok, PackageIDs} -> + Print = + fun(PackageID) -> + {ok, PackageString} = zx_lib:package_string(PackageID), + io:format("~ts~n", [PackageString]) + end, + ok = lists:foreach(Print, PackageIDs), + halt(0); + {error, bad_realm} -> + zx:error_exit("Bad realm name.", ?LINE); + {error, no_realm} -> + zx:error_exit("Realm \"~ts\" is not configured.", ?LINE); + {error, network} -> + Message = "Network issues are preventing connection to the realm.", + zx:error_exit(Message, ?LINE) + end. + + +-spec submit(PackageFile) -> no_return() + when PackageFile :: file:filename(). +%% @private +%% Submit a package to the appropriate "prime" server for the given realm. + +submit(PackageFile) -> + Files = zx_lib:extract_zsp_or_die(PackageFile), + {ok, PackageData} = file:read_file(PackageFile), + {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), + Meta = binary_to_term(MetaBin), + {Realm, Package, Version} = maps:get(package_id, Meta), + {ok, Socket} = connect_auth(Realm), + ok = send(Socket, {submit, {Realm, Package, Version}}), + ok = recv_or_die(Socket), + ok = gen_tcp:send(Socket, PackageData), + ok = log(info, "Done sending contents of ~tp", [PackageFile]), + Outcome = recv_or_die(Socket), + log(info, "Response: ~tp", [Outcome]), + ok = disconnect(Socket), + halt(0). + + +review(PackageString) -> + PackageID = {Realm, _, _} = zx_lib:package_id(PackageString), + Socket = connect_auth_or_die(Realm), + ok = send(Socket, {review, PackageID}), + {ok, ZrpBin} = recv_or_die(Socket), + ok = send(Socket, ok), + ok = disconnect(Socket), + {ok, Files} = erl_tar:extract({binary, ZrpBin}, [memory]), + {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), + Meta = binary_to_term(MetaBin, [safe]), + PackageID = maps:get(package_id, Meta), + {KeyID, Signature} = maps:get(sig, Meta), + {ok, PubKey} = zx_key:load(public, KeyID), + TgzFile = PackageString ++ ".tgz", + {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), + ok = zx_key:verify(TgzData, Signature, PubKey), + ok = + case file:make_dir(PackageString) of + ok -> + log(info, "Will unpack to directory ./~ts", [PackageString]); + {error, Error} -> + Message = "Creating dir ./~ts failed with ~ts. Aborting.", + ok = log(error, Message, [PackageString, Error]), + halt(1) + end, + ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageString}]), + ok = log(info, "Unpacked and awaiting inspection."), + halt(0). + + +approve(PackageID = {Realm, _, _}) -> + Socket = connect_auth_or_die(Realm), + ok = send(Socket, {approve, PackageID}), + ok = recv_or_die(Socket), + ok = log(info, "ok"), + halt(0). + + +reject(PackageID = {Realm, _, _}) -> + Socket = connect_auth_or_die(Realm), + ok = send(Socket, {reject, PackageID}), + ok = recv_or_die(Socket), + ok = log(info, "ok"), + halt(0). + + +accept(PackageString) -> + PackageID = {Realm, _, _} = zx_lib:package_id(PackageString), + RealmConf = zx_lib:load_realm_conf(Realm), + {package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf), + KeySelection = [{K, {R, K}} || {R, K} <- [element(1, K) || K <- PackageKeys]], + PackageKeyID = zx_tty:select(KeySelection), + {ok, PackageKey} = zx_key:load(private, PackageKeyID), + Socket = connect_auth_or_die(Realm), + ok = send(Socket, {accept, PackageID}), + {ok, ZrpBin} = recv_or_die(Socket), + {ok, Files} = erl_tar:extract({binary, ZrpBin}, [memory]), + {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), + Meta = binary_to_term(MetaBin, [safe]), + PackageID = maps:get(package_id, Meta), + {KeyID, Signature} = maps:get(sig, Meta), + {ok, PubKey} = zx_key:load(public, KeyID), + TgzFile = PackageString ++ ".tgz", + {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), + ok = zx_key:verify(TgzData, Signature, PubKey), + ReSignature = public_key:sign(TgzData, sha512, PackageKey), + FinalMeta = maps:put(sig, {PackageKeyID, ReSignature}, Meta), + NewMetaBin = term_to_binary(FinalMeta), + NewFiles = lists:keystore("zomp.meta", 1, Files, {"zomp.meta", NewMetaBin}), + ResignedZrp = PackageString ++ ".zsp.resign", + ok = erl_tar:create(ResignedZrp, NewFiles), + {ok, ResignedBin} = file:read_file(ResignedZrp), + ok = gen_tcp:send(Socket, ResignedBin), + ok = recv_or_die(Socket), + ok = file:delete(ResignedZrp), + ok = recv_or_die(Socket), + ok = disconnect(Socket), + ok = log(info, "Resigned ~ts", [PackageString]), + halt(0). + + +add_packager(Package, UserFile) -> + ok = log(info, "Would add ~ts to packagers for ~160tp now.", [UserFile, Package]), + halt(0). + + +add_maintainer(Package, UserFile) -> + ok = log(info, "Would add ~ts to maintainer for ~160tp now.", [UserFile, Package]), + halt(0). + + +add_sysop(UserFile) -> + ok = log(info, "Would add ~ts to sysop list.", [UserFile]), + halt(0). + + +-spec add_package(PackageName) -> no_return() + when PackageName :: zx:package(). +%% @private +%% A sysop command that adds a package to a realm operated by the caller. + +add_package(PackageName) -> + ok = file:set_cwd(zx_lib:zomp_dir()), + case zx_lib:package_id(PackageName) of + {ok, {Realm, Name, {z, z, z}}} -> + add_package(Realm, Name); + _ -> + zx:error_exit("~tp is not a valid package name.", [PackageName], ?LINE) + end. + + +-spec add_package(Realm, Name) -> no_return() + when Realm :: zx:realm(), + Name :: zx:name(). + +add_package(Realm, Name) -> + Socket = connect_auth_or_die(Realm), + ok = send(Socket, {add_package, {Realm, Name}}), + ok = recv_or_die(Socket), + ok = log(info, "\"~ts-~ts\" added successfully.", [Realm, Name]), + halt(0). + + + +%%% Authenticated communication with prime + +-spec send(Socket, Message) -> ok + when Socket :: gen_tcp:socket(), + Message :: term(). +%% @private +%% Wrapper for the procedure necessary to send an internal message over the wire. + +send(Socket, Message) -> + Bin = term_to_binary(Message), + gen_tcp:send(Socket, Bin). + + +-spec recv_or_die(Socket) -> Result | no_return() + when Socket :: gen_tcp:socket(), + Result :: ok | {ok, term()}. + +recv_or_die(Socket) -> + receive + {tcp, Socket, Bin} -> + case binary_to_term(Bin) of + ok -> + ok; + {ok, Response} -> + {ok, Response}; + {error, Reason} -> + zx:error_exit("Action failed with: ~tp", [Reason], ?LINE); + Unexpected -> + zx:error_exit("Unexpected message: ~tp", [Unexpected], ?LINE) + end; + {tcp_closed, Socket} -> + zx:error_exit("Lost connection to node unexpectedly.", ?LINE) + after 5000 -> + zx:error_exit("Connection timed out.", ?LINE) + end. + + +-spec connect_auth_or_die(zx:realm()) -> gen_tcp:socket() | no_return(). + +connect_auth_or_die(Realm) -> + case connect_auth(Realm) of + {ok, Socket} -> + Socket; + Error -> + M1 = "Connection failed to realm prime with ~160tp.", + ok = log(warning, M1, [Error]), + halt(1) + end. + + +-spec connect_auth(Realm) -> Result + when Realm :: zx:realm(), + Result :: {ok, gen_tcp:socket()} + | {error, Reason :: term()}. +%% @private +%% Connect to one of the servers in the realm constellation. + +connect_auth(Realm) -> + RealmConf = zx_lib:load_realm_conf(Realm), + {User, KeyID, Key} = prep_auth(Realm, RealmConf), + {prime, {Host, Port}} = lists:keyfind(prime, 1, RealmConf), + Options = [{packet, 4}, {mode, binary}, {active, true}], + case gen_tcp:connect(Host, Port, Options, 5000) of + {ok, Socket} -> + ok = log(info, "Connected to ~tp prime.", [Realm]), + connect_auth(Socket, Realm, User, KeyID, Key); + Error = {error, E} -> + ok = log(warning, "Connection problem: ~tp", [E]), + {error, Error} + end. + + +-spec connect_auth(Socket, Realm, User, KeyID, Key) -> Result + when Socket :: gen_tcp:socket(), + Realm :: zx:realm(), + User :: zx:user_id(), + KeyID :: zx:key_id(), + Key :: term(), + Result :: {ok, gen_tcp:socket()} + | {error, timeout}. +%% @private +%% 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. + +connect_auth(Socket, Realm, User, KeyID, Key) -> + ok = gen_tcp:send(Socket, <<"OTPR AUTH 1">>), + receive + {tcp, Socket, Bin} -> + ok = binary_to_term(Bin, [safe]), + confirm_auth(Socket, Realm, User, KeyID, Key); + {tcp_closed, Socket} -> + ok = log(warning, "Socket closed unexpectedly."), + halt(1) + after 5000 -> + 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} -> + ok = log(warning, "Socket closed unexpectedly."), + halt(1) + 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} -> + ok = log(warning, "Socket closed unexpectedly."), + halt(1) + after 5000 -> + {error, timeout} + end. + + +-spec prep_auth(Realm, RealmConf) -> {User, KeyID, Key} | no_return() + when Realm :: zx:realm(), + RealmConf :: [term()], + User :: zx:user_id(), + KeyID :: zx: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) -> + UsersFile = filename:join(zx_lib:zomp_dir(), "zomp.users"), + Users = + case file:consult(UsersFile) 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 = zx_key:ensure_keypair(KeyID), + {ok, Key} = zx_key:load(private, KeyID), + {User, KeyID, Key}. + + +-spec disconnect(gen_tcp:socket()) -> ok. +%% @private +%% Gracefully shut down a socket, logging (but sidestepping) the case when the socket +%% has already been closed by the other side. + +disconnect(Socket) -> + case gen_tcp:shutdown(Socket, read_write) of + ok -> + log(info, "Disconnected from ~tp", [Socket]); + {error, Error} -> + Message = "Shutdown connection ~p failed with: ~p", + log(warning, Message, [Socket, Error]) + end. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_conf_sys.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_conf_sys.erl index d487626..146fec8 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_conf_sys.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_conf_sys.erl @@ -23,7 +23,7 @@ retries/1, retries/2, retry/1, retries_left/1, maxconn/1, maxconn/2, managed/1, managed/2, add_managed/2, rem_managed/2, - mirrors/1, mirrors/2, add_mirror/2, rem_mirror/2, next_mirror/1, + mirrors/1, mirrors/2, add_mirror/2, rem_mirror/2, reset/0]). -export_type([data/0]). @@ -35,11 +35,11 @@ %%% Type Definitions -record(d, - {timeout = 5 :: pos_integer(), - retries = {0, 3} :: non_neg_integer(), - maxconn = 5 :: pos_integer(), - managed = sets:new() :: sets:set(zx:realm()), - mirrors = queue:new() :: queue:queue(zx:host())}). + {timeout = 5 :: pos_integer(), + retries = {3, 3} :: non_neg_integer(), + maxconn = 5 :: pos_integer(), + managed = sets:new() :: sets:set(zx:realm()), + mirrors = [] :: [zx:host()]}). -opaque data() :: #d{}. @@ -55,11 +55,12 @@ %% be called by zx_daemon and utility code. load() -> - case file:consult(path()) of + Path = path(), + case file:consult(Path) of {ok, List} -> populate_data(List); {error, Reason} -> - ok = log(error, "Load etc/sys.conf failed with: ~tp", [Reason]), + ok = log(error, "Load ~tp failed with: ~tp", [Path, Reason]), Data = #d{}, ok = save(Data), Data @@ -69,28 +70,28 @@ load() -> populate_data(List) -> Timeout = case proplists:get_value(timeout, List, 5) of - V when is_integer(V) and V > 0 -> V; - _ -> 5 + TO when is_integer(TO) and TO > 0 -> TO; + _ -> 5 end, - Retry = - case proplists:get_value(retry, List, 3) of - V when is_integer(V) and V > 0 -> V; - _ -> 3 + Retries = + case proplists:get_value(retries, List, 3) of + RT when is_integer(RT) and RT > 0 -> {RT, RT}; + _ -> 3 end, MaxConn = case proplists:get_value(maxconn, List, 5) of - V when is_integer(V) and V > 0 -> V; - _ -> 5 + MC when is_integer(MC) and MC > 0 -> MC; + _ -> 5 end, Managed = case proplists:get_value(managed, List, []) of - V when is_list(V) -> sets:from_list(V); - _ -> sets:new() + MN when is_list(MN) -> sets:from_list(MN); + _ -> sets:new() end, Mirrors = case proplists:get_value(mirrors, List, []) of - V when is_list(V) -> queue:from_list(V); - _ -> queue:new() + MR when is_list(MR) -> MR; + _ -> [] end, #d{timeout = Timeout, retries = Retries, @@ -113,7 +114,7 @@ save(#d{timeout = Timeout, {retries, Retries}, {maxconn, MaxConn}, {managed, sets:to_list(Managed)}, - {mirrors, queue:to_list(Mirrors)}], + {mirrors, Mirrors}], ok = zx_lib:write_terms(path(), Terms), log(info, "Wrote etc/sys.conf"). @@ -142,7 +143,7 @@ timeout(Data, Value) %% @doc %% Return the retries value. -retries(#d{retries = Retries}) -> +retries(#d{retries = {_, Retries}}) -> Retries. @@ -153,7 +154,7 @@ retries(#d{retries = Retries}) -> %% @doc %% Set the retries attribute to a new value. -retries(Data = #d{retries = {Remaining, _}, Value) -> +retries(Data = #d{retries = {Remaining, _}}, Value) when is_integer(Value) and Value >= 0 -> Data#d{retries = {Remaining, Value}}. @@ -170,7 +171,7 @@ retries(Data = #d{retries = {Remaining, _}, Value) -> retry(#d{retries = {0, _}}) -> no_retries; retry(Data = #d{retries = {Remaining, Setting}}) -> - NewRemaining = Current - 1, + NewRemaining = Remaining - 1, NewData = Data#d{retries = {NewRemaining, Setting}}, {ok, NewData}. @@ -220,27 +221,108 @@ managed(#d{managed = Managed}) -> %% The realms must be configured on the current realm at a minimum. managed(Data, List) -> - Scrubbed = scrub_realms(List), - NewManaged = sets:from_list(Scrubbed), + Desired = sets:from_list(List), + Configured = sets:from_list(zx_lib:list_realms()), + NewManaged = sets:intersection(Desired, Configured), Data#d{managed = NewManaged}. -scrub_realms(List) -> - %... - []. - - --spec add_managed(Data, Realm) -> {ok, +-spec add_managed(Data, Realm) -> Result when Data :: data(), Realm :: zx:realm(), + Result :: {ok, NewData} + | {error, unconfigured}, NewData :: data(). %% @doc %% Add a new realm to the list of managed realms. The new realm must be configured on -%% the current node. +%% the current node. This node will then behave as the prime node for the realm (whether +%% it is or not). add_managed(Data = #d{managed = Managed}, Realm) -> - case zx_lib:valid_lower0_9(Realm) of - + case lists:member(Realm, zx_lib:list_realms()) of + true -> + NewData = Data#d{managed = sets:add_element(Realm, Managed)}, + ok = log(info, "Now managing realm: ~tp", [Realm]), + {ok, NewData}; + false -> + ok = log(warning, "Cannot manage unconfigured realm: ~tp", [Realm]), + {error, unconfigured} + end. + + +-spec rem_managed(Data, Realm) -> Result + when Data :: data(), + Realm :: zx:realm(), + Result :: {ok, NewData} + | {error, unmanaged}, + NewData :: data(). +%% @doc +%% Stop managing a realm. + +rem_managed(Data = #d{managed = Managed}, Realm) -> + case sets:is_element(Realm, Managed) of + true -> + NewData = Data#d{managed = sets:del_element(Realm, Managed)}, + ok = log(info, "No longer managing realm: ~tp", [Realm]), + {ok, NewData}; + false -> + ok = log(warning, "Cannot stop managing unmanaged realm: ~tp", [Realm]), + {error, unmanaged} + end. + + +-spec mirrors(data()) -> [zx:host()]. +%% @doc +%% Return the list of private mirrors. + +mirrors(#d{mirrors = Mirrors}) -> + Mirrors. + + +-spec mirrors(Data, Hosts) -> NewData + when Data :: data(), + Hosts :: [zx:host()], + NewData :: data(). +%% @private +%% Reset the mirror configuration. + +mirrors(Data, Hosts) -> + Data#d{mirrors = Hosts}. + + +-spec add_mirror(Data, Host) -> NewData + when Data :: data(), + Host :: zx:host(), + NewData :: data(). +%% @doc +%% Add a mirror to the permanent configuration. + +add_mirror(Data = #d{mirrors = Mirrors}, Host) -> + case lists:member(Host, Mirrors) of + false -> Data#d{mirrors = [Host | Mirrors]}; + true -> Data + end. + + +-spec rem_mirror(Data, Host) -> NewData + when Data :: data(), + Host :: zx:host(), + NewData :: data(). +%% @private +%% Remove a host from the list of permanent mirrors. + +rem_mirror(Data = #d{mirrors = Mirrors}, Host) -> + Data#d{mirrors = lists:delete(Host, Mirrors)}. + + +-spec reset() -> data(). +%% @private +%% Reset sys.conf. + +reset() -> + Data = #d{}, + save(Data), + Data. -spec path() -> file:filename(). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl index 1c84891..a302a85 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl @@ -352,19 +352,19 @@ interpret_response(Socket, {sub, Channel, Message}, Command) -> %% Download a package to the local cache. fetch(Socket, PackageID) -> - {ok, LatestID} = request_zrp(Socket, PackageID), - ok = receive_zrp(Socket, LatestID), + {ok, LatestID} = request_zsp(Socket, PackageID), + ok = receive_zsp(Socket, LatestID), Latest = zx_lib:package_string(LatestID), log(info, "Fetched ~ts", [Latest]). --spec request_zrp(Socket, PackageID) -> Result +-spec request_zsp(Socket, PackageID) -> Result when Socket :: gen_tcp:socket(), PackageID :: zx:package_id(), Result :: {ok, Latest :: zx:package_id()} | {error, Reason :: timeout | term()}. -request_zrp(Socket, PackageID) -> +request_zsp(Socket, PackageID) -> ok = zx_net:send(Socket, {fetch, PackageID}), receive {tcp, Socket, Bin} -> @@ -384,15 +384,15 @@ request_zrp(Socket, PackageID) -> end. --spec receive_zrp(Socket, PackageID) -> Result +-spec receive_zsp(Socket, PackageID) -> Result when Socket :: gen_tcp:socket(), PackageID :: zx:package_id(), Result :: ok | {error, timeout}. -receive_zrp(Socket, PackageID) -> +receive_zsp(Socket, PackageID) -> receive {tcp, Socket, Bin} -> - ZrpPath = filename:join("zrp", zx_lib:namify_zrp(PackageID)), + ZrpPath = filename:join("zsp", zx_lib:namify_zsp(PackageID)), ok = file:write_file(ZrpPath, Bin), ok = zx_net:send(Socket, ok), log(info, "Wrote ~ts", [ZrpPath]); @@ -516,7 +516,7 @@ terminate() -> %% sourced, but exit with an error if it cannot locate or acquire the package. % %ensure_dep(Socket, PackageID) -> -% ZrpFile = filename:join("zrp", namify_zrp(PackageID)), +% ZrpFile = filename:join("zsp", namify_zsp(PackageID)), % ok = % case filelib:is_regular(ZrpFile) of % true -> ok; diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl index 86296c1..a7dada8 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_daemon.erl @@ -1355,14 +1355,14 @@ cx_load() -> Reason :: no_realms | file:posix(). %% @private -%% This procedure, relying zx_lib:zomp_home() allows the system to load zomp data +%% This procedure, relying zx_lib:zomp_dir() allows the system to load zomp data %% from any arbitrary home for zomp. This has been included mostly to make testing of %% Zomp and ZX easier, but incidentally opens the possibility that an arbitrary Zomp %% home could be selected by an installer (especially on consumer systems like Windows %% where any number of wild things might be going on in the user's filesystem). cx_populate() -> - Home = zx_lib:zomp_home(), + Home = zx_lib:zomp_dir(), Pattern = filename:join(Home, "*.realm"), case filelib:wildcard(Pattern) of [] -> {error, no_realms}; @@ -1477,7 +1477,7 @@ cx_write_cache({Realm, -spec cx_cache_file(zx:realm()) -> file:filename(). cx_cache_file(Realm) -> - filename:join(zx_lib:zomp_home(), Realm ++ ".cache"). + filename:join(zx_lib:zomp_dir(), Realm ++ ".cache"). -spec cx_realms(conn_index()) -> [zx:realms()]. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl new file mode 100644 index 0000000..2fc785d --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl @@ -0,0 +1,280 @@ +%%% @doc +%%% ZX Key +%%% +%%% Abstraction module for dealing with keys. +%%% +%%% "Ewwwww! Keys!" +%%% -- Bertrand Russel +%%% @end + +-module(zx_key). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + +-export([ensure_keypair/1, have_public_key/1, have_private_key/1, + prompt_keygen/0, create_keypair/0, generate_rsa/1, + load/2, verify/3]). + +-include("zx_logger.hrl"). + + +%%% Functions + +-spec ensure_keypair(zx:key_id()) -> true | no_return(). +%% @private +%% Check if both the public and private key based on KeyID exists. + +ensure_keypair(KeyID = {Realm, KeyName}) -> + case {have_public_key(KeyID), have_private_key(KeyID)} of + {true, true} -> + true; + {false, true} -> + Message = "Public key for ~tp/~tp cannot be found", + ok = log(error, Message, [Realm, KeyName]), + halt(1); + {true, false} -> + Message = "Private key for ~tp/~tp cannot be found", + ok = log(error, Message, [Realm, KeyName]), + halt(1); + {false, false} -> + Message = "Key pair for ~tp/~tp cannot be found", + ok = log(error, Message, [Realm, KeyName]), + halt(1) + end. + + +-spec have_public_key(zx:key_id()) -> boolean(). +%% @private +%% Determine whether the public key indicated by KeyID is in the keystore. + +have_public_key({Realm, KeyName}) -> + PublicKeyFile = KeyName ++ ".pub.der", + PublicKeyPath = filename:join([zx_lib:zomp_dir(), "key", Realm, PublicKeyFile]), + filelib:is_regular(PublicKeyPath). + + +-spec have_private_key(zx:key_id()) -> boolean(). +%% @private +%% Determine whether the private key indicated by KeyID is in the keystore. + +have_private_key({Realm, KeyName}) -> + PrivateKeyFile = KeyName ++ ".key.der", + PrivateKeyPath = filename:join([zx_lib:zomp_dir(), "key", Realm, PrivateKeyFile]), + filelib:is_regular(PrivateKeyPath). + + + +%%% Key generation + +-spec prompt_keygen() -> zx:key_id(). +%% @private +%% Prompt the user for a valid KeyPrefix to use for naming a new RSA keypair. + +prompt_keygen() -> + Message = + "~n Enter a name for your new keys.~n~n" + " Valid names must start with a lower-case letter, and can include~n" + " only lower-case letters, numbers, and periods, but no series of~n" + " consecutive periods. (That is: [a-z0-9\\.])~n~n" + " To designate the key as realm-specific, enter the realm name and~n" + " key name separated by a space.~n~n" + " Example: some.realm my.key~n", + ok = io:format(Message), + Input = zx_tty:get_input(), + {Realm, KeyName} = + case string:lexemes(Input, " ") of + [R, K] -> {R, K}; + [K] -> {"otpr", K} + end, + case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_label(KeyName)} of + {true, true} -> + {Realm, KeyName}; + {false, true} -> + ok = io:format("Bad realm name ~tp. Try again.~n", [Realm]), + prompt_keygen(); + {true, false} -> + ok = io:format("Bad key name ~tp. Try again.~n", [KeyName]), + prompt_keygen(); + {false, false} -> + ok = io:format("NUTS! Both key and realm names are illegal. Try again.~n"), + prompt_keygen() + end. + + +-spec create_keypair() -> no_return(). +%% @private +%% Execute the key generation procedure for 16k RSA keys once and then terminate. + +create_keypair() -> + ok = file:set_cwd(zx_lib:zomp_dir()), + KeyID = prompt_keygen(), + case generate_rsa(KeyID) of + {ok, _, _} -> halt(0); + Error -> zx:error_exit("create_keypair/0 error: ~tp", [Error], ?LINE) + end. + + +-spec generate_rsa(KeyID) -> Result + when KeyID :: zx:key_id(), + Result :: {ok, KeyFile, PubFile} + | {error, keygen_fail}, + KeyFile :: file:filename(), + PubFile :: file:filename(). +%% @private +%% Generate an RSA keypair and write them in der format to the current directory, using +%% filenames derived from Prefix. +%% NOTE: The current version of this command is likely to only work on a unix system. + +generate_rsa({Realm, KeyName}) -> + KeyDir = filename:join([zx_lib:zomp_dir(), "key", Realm]), + ok = zx_lib:force_dir(KeyDir), + PemFile = filename:join(KeyDir, KeyName ++ ".pub.pem"), + KeyFile = filename:join(KeyDir, KeyName ++ ".key.der"), + PubFile = filename:join(KeyDir, KeyName ++ ".pub.der"), + ok = lists:foreach(fun zx_lib:halt_if_exists/1, [PemFile, KeyFile, PubFile]), + ok = log(info, "Generating ~p and ~p. Please be patient...", [KeyFile, PubFile]), + ok = gen_p_key(KeyFile), + ok = der_to_pem(KeyFile, PemFile), + {ok, PemBin} = file:read_file(PemFile), + [PemData] = public_key:pem_decode(PemBin), + Pub = public_key:pem_entry_decode(PemData), + PubDer = public_key:der_encode('RSAPublicKey', Pub), + ok = file:write_file(PubFile, PubDer), + case check_key(KeyFile, PubFile) of + true -> + ok = file:delete(PemFile), + ok = log(info, "~ts and ~ts agree", [KeyFile, PubFile]), + ok = log(info, "Wrote private key to: ~ts.", [KeyFile]), + ok = log(info, "Wrote public key to: ~ts.", [PubFile]), + {ok, KeyFile, PubFile}; + false -> + ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]), + ok = log(error, "Something has gone wrong."), + {error, keygen_fail} + end. + + +-spec gen_p_key(KeyFile) -> ok + when KeyFile :: file:filename(). +%% @private +%% Format an openssl shell command that will generate proper 16k RSA keys. + +gen_p_key(KeyFile) -> + Command = + io_lib:format("~ts genpkey" + " -algorithm rsa" + " -out ~ts" + " -outform DER" + " -pkeyopt rsa_keygen_bits:16384", + [openssl(), KeyFile]), + Out = os:cmd(Command), + io:format(Out). + + +-spec der_to_pem(KeyFile, PemFile) -> ok + when KeyFile :: file:filename(), + PemFile :: file:filename(). +%% @private +%% Format an openssl shell command that will convert the given keyfile to a pemfile. +%% The reason for this conversion is to sidestep some formatting weirdness that OpenSSL +%% injects into its generated DER formatted key output (namely, a few empty headers) +%% which Erlang's ASN.1 defintion files do not take into account. A conversion to PEM +%% then a conversion back to DER (via Erlang's ASN.1 module) resolves this in a reliable +%% way. + +der_to_pem(KeyFile, PemFile) -> + Command = + io_lib:format("~ts rsa" + " -inform DER" + " -in ~ts" + " -outform PEM" + " -pubout" + " -out ~ts", + [openssl(), KeyFile, PemFile]), + Out = os:cmd(Command), + io:format(Out). + + +-spec check_key(KeyFile, PubFile) -> Result + when KeyFile :: file:filename(), + PubFile :: file:filename(), + Result :: true | false. +%% @private +%% Compare two keys for pairedness. + +check_key(KeyFile, PubFile) -> + {ok, KeyBin} = file:read_file(KeyFile), + {ok, PubBin} = file:read_file(PubFile), + Key = public_key:der_decode('RSAPrivateKey', KeyBin), + Pub = public_key:der_decode('RSAPublicKey', PubBin), + TestMessage = <<"Some test data to sign.">>, + Signature = public_key:sign(TestMessage, sha512, Key), + public_key:verify(TestMessage, sha512, Signature, Pub). + + +-spec openssl() -> Executable | no_return() + when Executable :: file:filename(). +%% @private +%% Attempt to locate the installed openssl executable for use in shell commands. +%% Halts execution with an error message if the executable cannot be found. + +openssl() -> + OpenSSL = + case os:type() of + {unix, _} -> "openssl"; + {win32, _} -> "openssl.exe" + end, + ok = + case os:find_executable(OpenSSL) of + false -> + ok = log(error, "OpenSSL could not be found in this system's PATH."), + ok = log(error, "Install OpenSSL and then retry."), + zx:error_exit("Missing system dependenct: OpenSSL", ?LINE); + Path -> + log(info, "OpenSSL executable found at: ~ts", [Path]) + end, + OpenSSL. + + +-spec load(Type, KeyID) -> Result + when Type :: private | public, + KeyID :: zx:key_id(), + Result :: {ok, DecodedKey :: term()} + | {error, Reason :: term()}. +%% @private +%% Hide the details behind reading and loading DER encoded RSA key files. + +load(Type, {Realm, KeyName}) -> + {DerType, Path} = + case Type of + private -> + KeyDer = KeyName ++ ".key.der", + P = filename:join([zx_lib:zomp_dir(), "key", Realm, KeyDer]), + {'RSAPrivateKey', P}; + public -> + PubDer = KeyName ++ ".pub.der", + P = filename:join([zx_lib:zomp_dir(), "key", Realm, PubDer]), + {'RSAPublicKey', P} + end, + ok = log(info, "Loading key from file ~ts", [Path]), + case file:read_file(Path) of + {ok, Bin} -> {ok, public_key:der_decode(DerType, Bin)}; + Error -> Error + end. + + +-spec verify(Data, Signature, PubKey) -> ok | no_return() + when Data :: binary(), + Signature :: binary(), + PubKey :: public_key:rsa_public_key(). +%% @private +%% Verify the RSA Signature of some Data against the given PubKey or halt execution. +%% This function always assumes sha512 is the algorithm being used. +%% Should only ever be called by the initial launch process. + +verify(Data, Signature, PubKey) -> + case public_key:verify(Data, sha512, Signature, PubKey) of + true -> ok; + false -> zx:error_exit("Bad package signature!", ?LINE) + end. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl index afd1665..64ab93c 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl @@ -16,6 +16,8 @@ -export([zomp_dir/0, find_zomp_dir/0, path/1, path/2, path/3, + force_dir/1, + list_realms/0, hosts_cache_file/1, get_prime/1, realm_meta/1, read_project_meta/0, read_project_meta/1, read_package_meta/1, write_project_meta/1, write_project_meta/2, @@ -24,9 +26,10 @@ string_to_version/1, version_to_string/1, package_id/1, package_string/1, package_dir/1, package_dir/2, - namify_zrp/1, namify_tgz/1, + namify_zsp/1, namify_tgz/1, find_latest_compatible/2, installed/1, - realm_conf/1, load_realm_conf/1]). + realm_conf/1, load_realm_conf/1, + extract_zsp_or_die/1, halt_if_exists/1]). -include("zx_logger.hrl"). @@ -57,7 +60,7 @@ find_zomp_dir() -> case os:type() of {unix, _} -> Home = os:getenv("HOME"), - Dir = "zomp", + Dir = ".zomp", filename:join(Home, Dir); {win32, _} -> Home = os:getenv("LOCALAPPDATA"), @@ -73,7 +76,7 @@ find_zomp_dir() -> | log | lib, Result :: file:filename(). -%% @doc +%% @private %% Return the top-level path of the given type in the Zomp/ZX system. path(etc) -> filename:join(zomp_dir(), "etc"); @@ -91,7 +94,7 @@ path(lib) -> filename:join(zomp_dir(), "lib"). | lib, Realm :: zx:realm(), Result :: file:filename(). -%% @doc +%% @private %% Return the realm-level path of the given type in the Zomp/ZX system. path(Type, Realm) -> @@ -107,13 +110,28 @@ path(Type, Realm) -> Realm :: zx:realm(), Name :: zx:name(), Result :: file:filename(). -%% @doc +%% @private %% Return the package-level path of the given type in the Zomp/ZX system. path(Type, Realm, Name) -> filename:join([path(Type), Realm, Name]). +-spec force_dir(Path) -> Result + when Path :: file:filename(), + Result :: ok + | {error, file:posix()}. +%% @private +%% Guarantee a directory path is created if it is possible to create or if it already +%% exists. + +force_dir(Path) -> + case filelib:is_dir(Path) of + true -> ok; + false -> filelib:ensure_dir(filename:join(Path, "foo")) + end. + + -spec hosts_cache_file(zx:realm()) -> file:filename(). %% @private %% Given a Realm name, construct a realm's .hosts filename and return it. @@ -122,6 +140,15 @@ hosts_cache_file(Realm) -> filename:join(zomp_dir(), Realm ++ ".hosts"). +-spec list_realms() -> [zx:realm()]. +%% @private +%% Check the filesystem for etc/[Realm Name]/realm.conf files. + +list_realms() -> + Pattern = filename:join([path(etc), "*", "realm.conf"]), + [filename:basename(filename:dirname(C)) || C <- filelib:wildcard(Pattern)]. + + -spec get_prime(Realm) -> Result when Realm :: zx:realm(), Result :: {ok, zx:host()} @@ -151,7 +178,7 @@ get_prime(Realm) -> %% the file. realm_meta(Realm) -> - RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"), + RealmFile = filename:join(path(etc, Realm), "realm.conf"), file:consult(RealmFile). @@ -516,13 +543,13 @@ package_dir(Prefix, {Realm, Name}) -> filename:join([zomp_dir(), Prefix, PackageString]). --spec namify_zrp(PackageID) -> ZrpFileName +-spec namify_zsp(PackageID) -> ZrpFileName when PackageID :: zx:package_id(), ZrpFileName :: file:filename(). %% @private -%% Map an PackageID to its correct .zrp package file name. +%% Map an PackageID to its correct .zsp package file name. -namify_zrp(PackageID) -> namify(PackageID, "zrp"). +namify_zsp(PackageID) -> namify(PackageID, "zsp"). -spec namify_tgz(PackageID) -> TgzFileName @@ -620,5 +647,46 @@ realm_conf(Realm) -> %% Load the config for the given realm or halt with an error. load_realm_conf(Realm) -> - Path = filename:join(zomp_dir(), realm_conf(Realm)), - file:consult(Path). + Path = filename:join(path(etc, Realm), "realm.conf"), + case file:consult(Path) of + {ok, C} -> + C; + {error, enoent} -> + ok = log(warning, "Realm ~tp is not configured.", [Realm]), + halt(1) + end. + + +-spec extract_zsp_or_die(FileName) -> Files | no_return() + when FileName :: file:filename(), + Files :: [{file:filename(), binary()}]. +%% @private +%% Extract a zsp archive, if possible. If not possible, halt execution with as accurate +%% an error message as can be managed. + +extract_zsp_or_die(FileName) -> + case erl_tar:extract(FileName, [memory]) of + {ok, Files} -> + Files; + {error, {FileName, enoent}} -> + Message = "Can't find file ~ts.", + zx:error_exit(Message, [FileName], ?LINE); + {error, invalid_tar_checksum} -> + Message = "~ts is not a valid zsp archive.", + zx:error_exit(Message, [FileName], ?LINE); + {error, Reason} -> + Message = "Extracting package file failed with: ~160tp.", + zx:error_exit(Message, [Reason], ?LINE) + end. + + +-spec halt_if_exists(file:filename()) -> ok | no_return(). +%% @private +%% A helper function to guard against overwriting an existing file. Halts execution if +%% the file is found to exist. + +halt_if_exists(Path) -> + case filelib:is_file(Path) of + true -> zx:error_exit("~ts already exists! Halting.", [Path], ?LINE); + false -> ok + end. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl new file mode 100644 index 0000000..46e91e7 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl @@ -0,0 +1,101 @@ +%%% @doc +%%% ZX TTY +%%% +%%% This module lets other parts of ZX interact (very clumsily) with the user via a text +%%% interface. Hopefully this will never be called on Windows. +%%% @end + +-module(zx_tty). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + +-export([get_input/0, select/1, select_string/1]). + + +%%% Type Definitions + +-type option() :: {string(), term()}. + + +%%% User menu interface (terminal) + +-spec get_input() -> string(). +%% @private +%% Provide a standard input prompt and newline sanitized return value. + +get_input() -> + string:trim(io:get_line("(^C to quit): ")). + + +-spec select(Options) -> Selected + when Options :: [option()], + Selected :: term(). +%% @private +%% Take a list of Options to present the user, then return the indicated option to the +%% caller once the user selects something. + +select(Options) -> + Max = show(Options), + case pick(string:to_integer(io:get_line("(or ^C to quit)~n ? ")), Max) of + error -> + ok = hurr(), + select(Options); + I -> + {_, Value} = lists:nth(I, Options), + Value + end. + + +-spec select_string(Strings) -> Selected + when Strings :: [string()], + Selected :: string(). +%% @private +%% @equiv select([{S, S} || S <- Strings]) + +select_string(Strings) -> + select([{S, S} || S <- Strings]). + + +-spec show(Options) -> Index + when Options :: [option()], + Index :: pos_integer(). +%% @private +%% @equiv show(Options, 0). + +show(Options) -> + show(Options, 0). + + +-spec show(Options, Index) -> Count + when Options :: [option()], + Index :: non_neg_integer(), + Count :: pos_integer(). +%% @private +%% Display the list of options needed to the user, and return the option total count. + +show([], I) -> + I; +show([{Label, _} | Rest], I) -> + Z = I + 1, + ok = io:format(" ~2w - ~ts~n", [Z, Label]), + show(Rest, Z). + + +-spec pick({Selection, term()}, Max) -> Result + when Selection :: error | integer(), + Max :: pos_integer(), + Result :: pos_integer() | error. +%% @private +%% Interpret a user's selection returning either a valid selection index or `error'. + +pick({error, _}, _) -> error; +pick({I, _}, Max) when 0 < I, I =< Max -> I; +pick(_, _) -> error. + + +-spec hurr() -> ok. +%% @private +%% Present an appropriate response when the user derps on selection. + +hurr() -> io:format("That isn't an option.~n"). diff --git a/zomp/zx b/zomp/zx index 8f3cea1..96f8928 100755 --- a/zomp/zx +++ b/zomp/zx @@ -1,10 +1,10 @@ #!/bin/sh -ZOMP_DIR="$HOME/zomp" +ZOMP_DIR="$HOME/.zomp" VERSION=$(cat "$ZOMP_DIR/etc/version.txt") ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" -pushd "$ZX_DIR" +pushd "$ZX_DIR" > /dev/null ./make_zx -popd +popd > /dev/null erl -pa "$ZX_DIR/ebin" -run zx start $@ diff --git a/zx_dev b/zx_dev index 596b40b..2f4b9c2 100755 --- a/zx_dev +++ b/zx_dev @@ -1,11 +1,13 @@ #!/bin/bash -ZX_DEV_ROOT=$(dirname $BASH_SOURCE) -ZOMP_DIR="$ZX_DEV_ROOT/zomp" -VERSION=$(cat "$ZOMP_DIR/etc/version.txt") -ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" +pushd $(dirname $BASH_SOURCE) > /dev/null +export ZX_DEV_ROOT=$PWD +popd > /dev/null +export ZOMP_DIR="$ZX_DEV_ROOT/zomp" +export VERSION=$(cat "$ZOMP_DIR/etc/version.txt") +export ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" -pushd "$ZX_DIR" +pushd "$ZX_DIR" > /dev/null ./make_zx -popd +popd > /dev/null erl -pa "$ZX_DIR/ebin" -run zx start $@