foo
This commit is contained in:
parent
7e79c4393f
commit
bd6c747207
@ -1,5 +1,5 @@
|
|||||||
{timeout, 5}.
|
{timeout,5}.
|
||||||
{retry, 3}.
|
{retries,3}.
|
||||||
{maxconn, 5}.
|
{maxconn,5}.
|
||||||
{managed, []}.
|
{managed,[]}.
|
||||||
{mirrors, []}.
|
{mirrors,[]}.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
452
zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl
Normal file
452
zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl
Normal file
@ -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 <zxq9@zxq9.com>").
|
||||||
|
-copyright("Craig Everett <zxq9@zxq9.com>").
|
||||||
|
-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.
|
||||||
@ -23,7 +23,7 @@
|
|||||||
retries/1, retries/2, retry/1, retries_left/1,
|
retries/1, retries/2, retry/1, retries_left/1,
|
||||||
maxconn/1, maxconn/2,
|
maxconn/1, maxconn/2,
|
||||||
managed/1, managed/2, add_managed/2, rem_managed/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]).
|
reset/0]).
|
||||||
|
|
||||||
-export_type([data/0]).
|
-export_type([data/0]).
|
||||||
@ -35,11 +35,11 @@
|
|||||||
%%% Type Definitions
|
%%% Type Definitions
|
||||||
|
|
||||||
-record(d,
|
-record(d,
|
||||||
{timeout = 5 :: pos_integer(),
|
{timeout = 5 :: pos_integer(),
|
||||||
retries = {0, 3} :: non_neg_integer(),
|
retries = {3, 3} :: non_neg_integer(),
|
||||||
maxconn = 5 :: pos_integer(),
|
maxconn = 5 :: pos_integer(),
|
||||||
managed = sets:new() :: sets:set(zx:realm()),
|
managed = sets:new() :: sets:set(zx:realm()),
|
||||||
mirrors = queue:new() :: queue:queue(zx:host())}).
|
mirrors = [] :: [zx:host()]}).
|
||||||
|
|
||||||
-opaque data() :: #d{}.
|
-opaque data() :: #d{}.
|
||||||
|
|
||||||
@ -55,11 +55,12 @@
|
|||||||
%% be called by zx_daemon and utility code.
|
%% be called by zx_daemon and utility code.
|
||||||
|
|
||||||
load() ->
|
load() ->
|
||||||
case file:consult(path()) of
|
Path = path(),
|
||||||
|
case file:consult(Path) of
|
||||||
{ok, List} ->
|
{ok, List} ->
|
||||||
populate_data(List);
|
populate_data(List);
|
||||||
{error, Reason} ->
|
{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{},
|
Data = #d{},
|
||||||
ok = save(Data),
|
ok = save(Data),
|
||||||
Data
|
Data
|
||||||
@ -69,28 +70,28 @@ load() ->
|
|||||||
populate_data(List) ->
|
populate_data(List) ->
|
||||||
Timeout =
|
Timeout =
|
||||||
case proplists:get_value(timeout, List, 5) of
|
case proplists:get_value(timeout, List, 5) of
|
||||||
V when is_integer(V) and V > 0 -> V;
|
TO when is_integer(TO) and TO > 0 -> TO;
|
||||||
_ -> 5
|
_ -> 5
|
||||||
end,
|
end,
|
||||||
Retry =
|
Retries =
|
||||||
case proplists:get_value(retry, List, 3) of
|
case proplists:get_value(retries, List, 3) of
|
||||||
V when is_integer(V) and V > 0 -> V;
|
RT when is_integer(RT) and RT > 0 -> {RT, RT};
|
||||||
_ -> 3
|
_ -> 3
|
||||||
end,
|
end,
|
||||||
MaxConn =
|
MaxConn =
|
||||||
case proplists:get_value(maxconn, List, 5) of
|
case proplists:get_value(maxconn, List, 5) of
|
||||||
V when is_integer(V) and V > 0 -> V;
|
MC when is_integer(MC) and MC > 0 -> MC;
|
||||||
_ -> 5
|
_ -> 5
|
||||||
end,
|
end,
|
||||||
Managed =
|
Managed =
|
||||||
case proplists:get_value(managed, List, []) of
|
case proplists:get_value(managed, List, []) of
|
||||||
V when is_list(V) -> sets:from_list(V);
|
MN when is_list(MN) -> sets:from_list(MN);
|
||||||
_ -> sets:new()
|
_ -> sets:new()
|
||||||
end,
|
end,
|
||||||
Mirrors =
|
Mirrors =
|
||||||
case proplists:get_value(mirrors, List, []) of
|
case proplists:get_value(mirrors, List, []) of
|
||||||
V when is_list(V) -> queue:from_list(V);
|
MR when is_list(MR) -> MR;
|
||||||
_ -> queue:new()
|
_ -> []
|
||||||
end,
|
end,
|
||||||
#d{timeout = Timeout,
|
#d{timeout = Timeout,
|
||||||
retries = Retries,
|
retries = Retries,
|
||||||
@ -113,7 +114,7 @@ save(#d{timeout = Timeout,
|
|||||||
{retries, Retries},
|
{retries, Retries},
|
||||||
{maxconn, MaxConn},
|
{maxconn, MaxConn},
|
||||||
{managed, sets:to_list(Managed)},
|
{managed, sets:to_list(Managed)},
|
||||||
{mirrors, queue:to_list(Mirrors)}],
|
{mirrors, Mirrors}],
|
||||||
ok = zx_lib:write_terms(path(), Terms),
|
ok = zx_lib:write_terms(path(), Terms),
|
||||||
log(info, "Wrote etc/sys.conf").
|
log(info, "Wrote etc/sys.conf").
|
||||||
|
|
||||||
@ -142,7 +143,7 @@ timeout(Data, Value)
|
|||||||
%% @doc
|
%% @doc
|
||||||
%% Return the retries value.
|
%% Return the retries value.
|
||||||
|
|
||||||
retries(#d{retries = Retries}) ->
|
retries(#d{retries = {_, Retries}}) ->
|
||||||
Retries.
|
Retries.
|
||||||
|
|
||||||
|
|
||||||
@ -153,7 +154,7 @@ retries(#d{retries = Retries}) ->
|
|||||||
%% @doc
|
%% @doc
|
||||||
%% Set the retries attribute to a new value.
|
%% 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 ->
|
when is_integer(Value) and Value >= 0 ->
|
||||||
Data#d{retries = {Remaining, Value}}.
|
Data#d{retries = {Remaining, Value}}.
|
||||||
|
|
||||||
@ -170,7 +171,7 @@ retries(Data = #d{retries = {Remaining, _}, Value) ->
|
|||||||
retry(#d{retries = {0, _}}) ->
|
retry(#d{retries = {0, _}}) ->
|
||||||
no_retries;
|
no_retries;
|
||||||
retry(Data = #d{retries = {Remaining, Setting}}) ->
|
retry(Data = #d{retries = {Remaining, Setting}}) ->
|
||||||
NewRemaining = Current - 1,
|
NewRemaining = Remaining - 1,
|
||||||
NewData = Data#d{retries = {NewRemaining, Setting}},
|
NewData = Data#d{retries = {NewRemaining, Setting}},
|
||||||
{ok, NewData}.
|
{ok, NewData}.
|
||||||
|
|
||||||
@ -220,27 +221,108 @@ managed(#d{managed = Managed}) ->
|
|||||||
%% The realms must be configured on the current realm at a minimum.
|
%% The realms must be configured on the current realm at a minimum.
|
||||||
|
|
||||||
managed(Data, List) ->
|
managed(Data, List) ->
|
||||||
Scrubbed = scrub_realms(List),
|
Desired = sets:from_list(List),
|
||||||
NewManaged = sets:from_list(Scrubbed),
|
Configured = sets:from_list(zx_lib:list_realms()),
|
||||||
|
NewManaged = sets:intersection(Desired, Configured),
|
||||||
Data#d{managed = NewManaged}.
|
Data#d{managed = NewManaged}.
|
||||||
|
|
||||||
|
|
||||||
scrub_realms(List) ->
|
-spec add_managed(Data, Realm) -> Result
|
||||||
%...
|
|
||||||
[].
|
|
||||||
|
|
||||||
|
|
||||||
-spec add_managed(Data, Realm) -> {ok,
|
|
||||||
when Data :: data(),
|
when Data :: data(),
|
||||||
Realm :: zx:realm(),
|
Realm :: zx:realm(),
|
||||||
|
Result :: {ok, NewData}
|
||||||
|
| {error, unconfigured},
|
||||||
NewData :: data().
|
NewData :: data().
|
||||||
%% @doc
|
%% @doc
|
||||||
%% Add a new realm to the list of managed realms. The new realm must be configured on
|
%% 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) ->
|
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().
|
-spec path() -> file:filename().
|
||||||
|
|||||||
@ -352,19 +352,19 @@ interpret_response(Socket, {sub, Channel, Message}, Command) ->
|
|||||||
%% Download a package to the local cache.
|
%% Download a package to the local cache.
|
||||||
|
|
||||||
fetch(Socket, PackageID) ->
|
fetch(Socket, PackageID) ->
|
||||||
{ok, LatestID} = request_zrp(Socket, PackageID),
|
{ok, LatestID} = request_zsp(Socket, PackageID),
|
||||||
ok = receive_zrp(Socket, LatestID),
|
ok = receive_zsp(Socket, LatestID),
|
||||||
Latest = zx_lib:package_string(LatestID),
|
Latest = zx_lib:package_string(LatestID),
|
||||||
log(info, "Fetched ~ts", [Latest]).
|
log(info, "Fetched ~ts", [Latest]).
|
||||||
|
|
||||||
|
|
||||||
-spec request_zrp(Socket, PackageID) -> Result
|
-spec request_zsp(Socket, PackageID) -> Result
|
||||||
when Socket :: gen_tcp:socket(),
|
when Socket :: gen_tcp:socket(),
|
||||||
PackageID :: zx:package_id(),
|
PackageID :: zx:package_id(),
|
||||||
Result :: {ok, Latest :: zx:package_id()}
|
Result :: {ok, Latest :: zx:package_id()}
|
||||||
| {error, Reason :: timeout | term()}.
|
| {error, Reason :: timeout | term()}.
|
||||||
|
|
||||||
request_zrp(Socket, PackageID) ->
|
request_zsp(Socket, PackageID) ->
|
||||||
ok = zx_net:send(Socket, {fetch, PackageID}),
|
ok = zx_net:send(Socket, {fetch, PackageID}),
|
||||||
receive
|
receive
|
||||||
{tcp, Socket, Bin} ->
|
{tcp, Socket, Bin} ->
|
||||||
@ -384,15 +384,15 @@ request_zrp(Socket, PackageID) ->
|
|||||||
end.
|
end.
|
||||||
|
|
||||||
|
|
||||||
-spec receive_zrp(Socket, PackageID) -> Result
|
-spec receive_zsp(Socket, PackageID) -> Result
|
||||||
when Socket :: gen_tcp:socket(),
|
when Socket :: gen_tcp:socket(),
|
||||||
PackageID :: zx:package_id(),
|
PackageID :: zx:package_id(),
|
||||||
Result :: ok | {error, timeout}.
|
Result :: ok | {error, timeout}.
|
||||||
|
|
||||||
receive_zrp(Socket, PackageID) ->
|
receive_zsp(Socket, PackageID) ->
|
||||||
receive
|
receive
|
||||||
{tcp, Socket, Bin} ->
|
{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 = file:write_file(ZrpPath, Bin),
|
||||||
ok = zx_net:send(Socket, ok),
|
ok = zx_net:send(Socket, ok),
|
||||||
log(info, "Wrote ~ts", [ZrpPath]);
|
log(info, "Wrote ~ts", [ZrpPath]);
|
||||||
@ -516,7 +516,7 @@ terminate() ->
|
|||||||
%% sourced, but exit with an error if it cannot locate or acquire the package.
|
%% sourced, but exit with an error if it cannot locate or acquire the package.
|
||||||
%
|
%
|
||||||
%ensure_dep(Socket, PackageID) ->
|
%ensure_dep(Socket, PackageID) ->
|
||||||
% ZrpFile = filename:join("zrp", namify_zrp(PackageID)),
|
% ZrpFile = filename:join("zsp", namify_zsp(PackageID)),
|
||||||
% ok =
|
% ok =
|
||||||
% case filelib:is_regular(ZrpFile) of
|
% case filelib:is_regular(ZrpFile) of
|
||||||
% true -> ok;
|
% true -> ok;
|
||||||
|
|||||||
@ -1355,14 +1355,14 @@ cx_load() ->
|
|||||||
Reason :: no_realms
|
Reason :: no_realms
|
||||||
| file:posix().
|
| file:posix().
|
||||||
%% @private
|
%% @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
|
%% 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
|
%% 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
|
%% 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).
|
%% where any number of wild things might be going on in the user's filesystem).
|
||||||
|
|
||||||
cx_populate() ->
|
cx_populate() ->
|
||||||
Home = zx_lib:zomp_home(),
|
Home = zx_lib:zomp_dir(),
|
||||||
Pattern = filename:join(Home, "*.realm"),
|
Pattern = filename:join(Home, "*.realm"),
|
||||||
case filelib:wildcard(Pattern) of
|
case filelib:wildcard(Pattern) of
|
||||||
[] -> {error, no_realms};
|
[] -> {error, no_realms};
|
||||||
@ -1477,7 +1477,7 @@ cx_write_cache({Realm,
|
|||||||
-spec cx_cache_file(zx:realm()) -> file:filename().
|
-spec cx_cache_file(zx:realm()) -> file:filename().
|
||||||
|
|
||||||
cx_cache_file(Realm) ->
|
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()].
|
-spec cx_realms(conn_index()) -> [zx:realms()].
|
||||||
|
|||||||
280
zomp/lib/otpr/zx/0.1.0/src/zx_key.erl
Normal file
280
zomp/lib/otpr/zx/0.1.0/src/zx_key.erl
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
%%% @doc
|
||||||
|
%%% ZX Key
|
||||||
|
%%%
|
||||||
|
%%% Abstraction module for dealing with keys.
|
||||||
|
%%%
|
||||||
|
%%% "Ewwwww! Keys!"
|
||||||
|
%%% -- Bertrand Russel
|
||||||
|
%%% @end
|
||||||
|
|
||||||
|
-module(zx_key).
|
||||||
|
-author("Craig Everett <zxq9@zxq9.com>").
|
||||||
|
-copyright("Craig Everett <zxq9@zxq9.com>").
|
||||||
|
-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.
|
||||||
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
-export([zomp_dir/0, find_zomp_dir/0,
|
-export([zomp_dir/0, find_zomp_dir/0,
|
||||||
path/1, path/2, path/3,
|
path/1, path/2, path/3,
|
||||||
|
force_dir/1,
|
||||||
|
list_realms/0,
|
||||||
hosts_cache_file/1, get_prime/1, realm_meta/1,
|
hosts_cache_file/1, get_prime/1, realm_meta/1,
|
||||||
read_project_meta/0, read_project_meta/1, read_package_meta/1,
|
read_project_meta/0, read_project_meta/1, read_package_meta/1,
|
||||||
write_project_meta/1, write_project_meta/2,
|
write_project_meta/1, write_project_meta/2,
|
||||||
@ -24,9 +26,10 @@
|
|||||||
string_to_version/1, version_to_string/1,
|
string_to_version/1, version_to_string/1,
|
||||||
package_id/1, package_string/1,
|
package_id/1, package_string/1,
|
||||||
package_dir/1, package_dir/2,
|
package_dir/1, package_dir/2,
|
||||||
namify_zrp/1, namify_tgz/1,
|
namify_zsp/1, namify_tgz/1,
|
||||||
find_latest_compatible/2, installed/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").
|
-include("zx_logger.hrl").
|
||||||
|
|
||||||
@ -57,7 +60,7 @@ find_zomp_dir() ->
|
|||||||
case os:type() of
|
case os:type() of
|
||||||
{unix, _} ->
|
{unix, _} ->
|
||||||
Home = os:getenv("HOME"),
|
Home = os:getenv("HOME"),
|
||||||
Dir = "zomp",
|
Dir = ".zomp",
|
||||||
filename:join(Home, Dir);
|
filename:join(Home, Dir);
|
||||||
{win32, _} ->
|
{win32, _} ->
|
||||||
Home = os:getenv("LOCALAPPDATA"),
|
Home = os:getenv("LOCALAPPDATA"),
|
||||||
@ -73,7 +76,7 @@ find_zomp_dir() ->
|
|||||||
| log
|
| log
|
||||||
| lib,
|
| lib,
|
||||||
Result :: file:filename().
|
Result :: file:filename().
|
||||||
%% @doc
|
%% @private
|
||||||
%% Return the top-level path of the given type in the Zomp/ZX system.
|
%% Return the top-level path of the given type in the Zomp/ZX system.
|
||||||
|
|
||||||
path(etc) -> filename:join(zomp_dir(), "etc");
|
path(etc) -> filename:join(zomp_dir(), "etc");
|
||||||
@ -91,7 +94,7 @@ path(lib) -> filename:join(zomp_dir(), "lib").
|
|||||||
| lib,
|
| lib,
|
||||||
Realm :: zx:realm(),
|
Realm :: zx:realm(),
|
||||||
Result :: file:filename().
|
Result :: file:filename().
|
||||||
%% @doc
|
%% @private
|
||||||
%% Return the realm-level path of the given type in the Zomp/ZX system.
|
%% Return the realm-level path of the given type in the Zomp/ZX system.
|
||||||
|
|
||||||
path(Type, Realm) ->
|
path(Type, Realm) ->
|
||||||
@ -107,13 +110,28 @@ path(Type, Realm) ->
|
|||||||
Realm :: zx:realm(),
|
Realm :: zx:realm(),
|
||||||
Name :: zx:name(),
|
Name :: zx:name(),
|
||||||
Result :: file:filename().
|
Result :: file:filename().
|
||||||
%% @doc
|
%% @private
|
||||||
%% Return the package-level path of the given type in the Zomp/ZX system.
|
%% Return the package-level path of the given type in the Zomp/ZX system.
|
||||||
|
|
||||||
path(Type, Realm, Name) ->
|
path(Type, Realm, Name) ->
|
||||||
filename:join([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().
|
-spec hosts_cache_file(zx:realm()) -> file:filename().
|
||||||
%% @private
|
%% @private
|
||||||
%% Given a Realm name, construct a realm's .hosts filename and return it.
|
%% 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").
|
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
|
-spec get_prime(Realm) -> Result
|
||||||
when Realm :: zx:realm(),
|
when Realm :: zx:realm(),
|
||||||
Result :: {ok, zx:host()}
|
Result :: {ok, zx:host()}
|
||||||
@ -151,7 +178,7 @@ get_prime(Realm) ->
|
|||||||
%% the file.
|
%% the file.
|
||||||
|
|
||||||
realm_meta(Realm) ->
|
realm_meta(Realm) ->
|
||||||
RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"),
|
RealmFile = filename:join(path(etc, Realm), "realm.conf"),
|
||||||
file:consult(RealmFile).
|
file:consult(RealmFile).
|
||||||
|
|
||||||
|
|
||||||
@ -516,13 +543,13 @@ package_dir(Prefix, {Realm, Name}) ->
|
|||||||
filename:join([zomp_dir(), Prefix, PackageString]).
|
filename:join([zomp_dir(), Prefix, PackageString]).
|
||||||
|
|
||||||
|
|
||||||
-spec namify_zrp(PackageID) -> ZrpFileName
|
-spec namify_zsp(PackageID) -> ZrpFileName
|
||||||
when PackageID :: zx:package_id(),
|
when PackageID :: zx:package_id(),
|
||||||
ZrpFileName :: file:filename().
|
ZrpFileName :: file:filename().
|
||||||
%% @private
|
%% @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
|
-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 the config for the given realm or halt with an error.
|
||||||
|
|
||||||
load_realm_conf(Realm) ->
|
load_realm_conf(Realm) ->
|
||||||
Path = filename:join(zomp_dir(), realm_conf(Realm)),
|
Path = filename:join(path(etc, Realm), "realm.conf"),
|
||||||
file:consult(Path).
|
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.
|
||||||
|
|||||||
101
zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl
Normal file
101
zomp/lib/otpr/zx/0.1.0/src/zx_tty.erl
Normal file
@ -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 <zxq9@zxq9.com>").
|
||||||
|
-copyright("Craig Everett <zxq9@zxq9.com>").
|
||||||
|
-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").
|
||||||
6
zomp/zx
6
zomp/zx
@ -1,10 +1,10 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
ZOMP_DIR="$HOME/zomp"
|
ZOMP_DIR="$HOME/.zomp"
|
||||||
VERSION=$(cat "$ZOMP_DIR/etc/version.txt")
|
VERSION=$(cat "$ZOMP_DIR/etc/version.txt")
|
||||||
ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION"
|
ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION"
|
||||||
|
|
||||||
pushd "$ZX_DIR"
|
pushd "$ZX_DIR" > /dev/null
|
||||||
./make_zx
|
./make_zx
|
||||||
popd
|
popd > /dev/null
|
||||||
erl -pa "$ZX_DIR/ebin" -run zx start $@
|
erl -pa "$ZX_DIR/ebin" -run zx start $@
|
||||||
|
|||||||
14
zx_dev
14
zx_dev
@ -1,11 +1,13 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
ZX_DEV_ROOT=$(dirname $BASH_SOURCE)
|
pushd $(dirname $BASH_SOURCE) > /dev/null
|
||||||
ZOMP_DIR="$ZX_DEV_ROOT/zomp"
|
export ZX_DEV_ROOT=$PWD
|
||||||
VERSION=$(cat "$ZOMP_DIR/etc/version.txt")
|
popd > /dev/null
|
||||||
ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION"
|
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
|
./make_zx
|
||||||
popd
|
popd > /dev/null
|
||||||
erl -pa "$ZX_DIR/ebin" -run zx start $@
|
erl -pa "$ZX_DIR/ebin" -run zx start $@
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user