This commit is contained in:
Craig Everett 2018-05-28 14:04:38 +09:00
parent 7e79c4393f
commit bd6c747207
11 changed files with 1194 additions and 1054 deletions

View File

@ -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

View 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.

View File

@ -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]).
@ -36,10 +36,10 @@
-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,28 +221,109 @@ 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().
%% @private %% @private

View File

@ -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;

View File

@ -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()].

View 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.

View File

@ -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.

View 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").

View File

@ -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
View File

@ -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 $@