This commit is contained in:
Craig Everett 2018-06-02 23:31:58 +09:00
parent 1e6132f7e5
commit f20f205c4f
6 changed files with 210 additions and 212 deletions

View File

@ -64,7 +64,7 @@
type := app | lib}.
-type outcome() :: ok
| {error, Reason :: atom()}
| {error, Reason :: term()}
| {error, Code :: non_neg_integer()}
| {error, Info :: string(), Code :: non_neg_integer()}.
@ -100,8 +100,10 @@ do(["list", "deps"]) ->
done(zx_local:list_deps());
do(["list", "deps", PackageString]) ->
done(zx_local:list_deps(PackageString));
do(["install", PackageFile]) ->
done(zx_local:assimilate(PackageFile));
do(["import", "zrp", PackageFile]) ->
done(zx_daemon:import_zrp(PackageFile));
do(["install", PackageString]) ->
done(zx_daemon:install(PackageString));
do(["set", "dep", PackageString]) ->
done(zx_local:set_dep(PackageString));
do(["set", "version", VersionString]) ->
@ -331,7 +333,7 @@ run(Identifier, RunArgs) ->
end,
{ok, PackageID} = ensure_installed(FuzzyID),
ok = build(PackageID),
Dir = zx_lib:path(lib, PackageID),
Dir = zx_lib:ppath(lib, PackageID),
{ok, Meta} = zx_lib:read_project_meta(Dir),
prepare(PackageID, Meta, Dir, RunArgs).
@ -527,46 +529,13 @@ tuplize(String, Acc) ->
end.
%%% Package utilities
-spec install(package_id()) -> ok.
%% @private
%% Install a package from the cache into the local system.
%% Before calling this function it must be known that:
%% - The zsp file is in the cache
%% - The zsp file is valid
%% - This function will only be called on startup by the launch process
%% - The package is not already installed
%% - If this function crashes it will completely halt the system
install(PackageID = {Realm, Name, _}) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "Installing ~ts", [PackageString]),
ZrpFile = filename:join(zx_lib:path(zsp, Realm, Name), zx_lib:namify_zsp(PackageID)),
Files = zx_lib:extract_zsp_or_die(ZrpFile),
TgzFile = zx_lib:namify_tgz(PackageID),
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
ok = ensure_package_dirs(PackageID),
PackageDir = zx_lib:path(lib, PackageID),
ok = zx_lib:force_dir(PackageDir),
ok = zx_key:verify(TgzData, Signature, PubKey),
ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageDir}]),
log(info, "~ts installed", [PackageString]).
-spec build(package_id()) -> ok.
%% @private
%% Given an AppID, build the project from source and add it to the current lib path.
build(PackageID) ->
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zx_lib:path(lib, PackageID)),
ok = file:set_cwd(zx_lib:ppath(lib, PackageID)),
ok = zx_lib:build(),
file:set_cwd(CWD).
@ -577,7 +546,7 @@ build(PackageID) ->
%% run have been created or halt execution.
ensure_package_dirs(PackageID) ->
Dirs = [zx_lib:path(D, PackageID) || D <- [etc, var, tmp, log, lib]],
Dirs = [zx_lib:ppath(D, PackageID) || D <- [etc, var, tmp, log, lib]],
lists:foreach(fun zx_lib:force_dir/1, Dirs).
@ -642,10 +611,9 @@ usage() ->
" zx reject PackageID~n"
" zx add key Realm KeyName~n"
" zx get key Realm KeyName~n"
" zx rem key Realm KeyName~n"
" zx create user~n"
" zx create userfiles Realm UserName~n"
" zx create keypair Realm~n"
" zx create keypair~n"
" zx export user UserID~n"
" zx import user ZdufFile~n"
"~n"
@ -658,7 +626,6 @@ usage() ->
" zx accept PackageID~n"
" zx create realm~n"
" zx create realmfile Realm~n"
" zx create sysop~n"
"~n"
"Where~n"
" PackageID :: A string of the form Realm-Name[-Version]~n"

View File

@ -91,26 +91,24 @@ list_resigns(Realm) ->
end.
-spec submit(PackageFile) -> no_return()
when PackageFile :: file:filename().
-spec submit(ZspPath :: file:filename()) -> zx:outcome().
%% @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}}),
submit(ZspPath) ->
{ok, ZspBin} = file:read_file(ZspPath),
<<SigSize:24, Sig:SigSize/binary, Signed/binary>> = ZspBin,
<<MetaSize:16, MetaBin:MetaSize/binary, TarGz/binary>> = Signed,
{ok, {PackageID, SigKeyName}} = zx_lib:b_to_ts(MetaBin),
{ok, PubKey} = zx_key:load(public, {element(1, PackageID), SigKeyName}),
true = zx_key:verify(Signed, Sig, PubKey),
ok = send(Socket, {submit, PackageID}),
ok = recv_or_die(Socket),
ok = gen_tcp:send(Socket, PackageData),
ok = gen_tcp:send(Socket, ZspBin),
ok = log(info, "Done sending contents of ~tp", [PackageFile]),
Outcome = recv_or_die(Socket),
log(info, "Response: ~tp", [Outcome]),
ok = disconnect(Socket),
halt(0).
disconnect(Socket).
review(PackageString) ->

View File

@ -148,6 +148,7 @@
subscribe/1, unsubscribe/1,
list/0, list/1, list/2, list/3, latest/1,
fetch/1, verify_key/1,
install/1, import_zsp/1,
pending/1, packagers/1, maintainers/1, sysops/1]).
-export([report/1, result/2, notify/2]).
-export([start_link/0, stop/0]).
@ -485,6 +486,27 @@ verify_key({Realm, KeyName}) ->
request({verify_key, Realm, KeyName}).
-spec import_zsp(Path :: file:filename()) -> zx:outcome().
%% @doc
%% Install a package from a local file.
import_zsp(Path) ->
gen_server:call(?MODULE, {import_zsp, Path}).
-spec install(PackageString :: string()) -> zx:outcome().
%% @doc
%% Install the specified package.
install(PackageString) ->
case zx_lib:package_id(PackageString) of
{ok, PackageID} ->
gen_server:call(?MODULE, {install, PackageID});
{error, invalid_package_string} ->
{error, "Invalid package string. Try again.", 22}
end.
-spec pending(Package) -> {ok, RequestID}
when Package :: zx:package(),
RequestID :: id().
@ -557,7 +579,7 @@ sysops(Realm) ->
%% Private function to wrap the necessary bits up.
request(Action) ->
gen_server:call(?MODULE, {request, self(), Action}).
gen_server:call(?MODULE, {request, Action}).
@ -689,12 +711,19 @@ stop() ->
handle_call({request, list}, _, State = #s{cx = CX}) ->
Realms = cx_realms(CX),
{reply, {ok, Realms}, State};
handle_call({request, Requestor, Action}, From, State = #s{id = ID}) ->
handle_call({request, Action}, From, State = #s{id = ID}) ->
NewID = ID + 1,
_ = gen_server:reply(From, {ok, NewID}),
Requestor = element(1, From),
NextState = do_request(Requestor, Action, State#s{id = NewID}),
NewState = eval_queue(NextState),
{noreply, NewState};
handle_call({install, PackageID}, _, State) ->
{Result, NewState} = do_install(PackageID, State),
{reply, Result, NewState};
handle_call({import_zsp, Path}, _, State) ->
Result = do_import_zsp(Path),
{reply, Result, NewState};
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call ~tp: ~tp", [From, Unexpected]),
{noreply, State}.
@ -1208,6 +1237,86 @@ drop_requests(ReqIDs, Dropped, Requests) ->
lists:fold(Partition, {Dropped, Requests}, ReqIDs).
-spec install(PackageID, State) -> {Result, NewState}
when PackageID :: zx:package_id(),
State :: state(),
Result :: zx:outcome(),
NewState :: state().
%% @private
%% FIXME: This is about as useful as psuedocode at the moment. Meh.
install(PackageID, State) ->
Path = zx_lib:zxp_path(PackageID),
case file:is_regular(Path) of
true ->
do_install(PackageID, Path);
false ->
ok = do_fetch(PackageID),
do_install(PackageID, Path)
end.
-spec do_import_zsp(file:filename()) -> zx:outcome().
%% @private
%% Dealing with data from the (probably local) filesystem can fail in a bajillion ways
%% and spring memory leaks if one tries to get too clever. So I'm sidestepping all the
%% madness with a "try++" here: spawning a suicidal helper.
do_import_zsp(Path) ->
{Pid, Mon} = spawn_monitor(fun() -> actually_import(Path) end),
receive
{Pid, Outcome} ->
true = demonitor(Mon, [flush]),
Outcome;
{'DOWN', Pid, process, Mon, Info} ->
{error, Info};
after 5000 ->
{error, timeout}
end.
-spec actually_import(ZspPath) -> no_return()
when ZspPath :: file:filename().
%% @private
%% The happy path of .zsp installation.
%% Must NEVER be executed by the zx_daemon directly.
%% More generally, there are a few phases:
%% 1- Loading the binary to extract the PackageID
%% 2- Checking the signature
%% 3- Moving the file to the cache
%% 4- Wiping the destination directory
%% 5- Extracting the TarGz to the destination
%% Some combination of functions should make these steps happen in a way that isn't
%% totally ridiculous, OR the bullet should just be bitten an allow for the
%% redundant lines here and there in different package management functions.
%%
%% Use cases are:
%% - Install a missing package from upstream
%% - Install a missing package from the local cache
%% - Reinstall a package from the local cache
%% - Import a package to the cache from the local filesystem and install it
%%
%% The Correct Approach as determine by The Royal Me is that I'm going to accept the
%% redundant code in the short-term because the data format is already decided.
%% If a place to get more fancy with the phases becomes really obvious after writing
%% identicalish segements of functions a few places then I'll break things apart.
actually_import(ZspPath) ->
{ok, Bin = <<Size:24, Sig:Size/binary, Signed/binary>>} = file:read_file(ZspPath),
<<MetaSize:16, MetaBin:MetaSize/binary, TarGz/binary>> = Signed,
{ok, {PackageID, SigKeyName}} = zx_lib:b_to_ts(MetaBin),
{ok, PubKey} = zx_key:load(public, {element(1, PackageID), SigKeyName}),
true = zx_key:verify(Signed, Sig, PubKey),
ok = file:write_file(zx_lib:zsp_path(PackageID), Bin),
Destination = zx_lib:ppath(lib, PackageID),
ok = filelib:ensure_dir(Destination),
ok = zx_lib:rm_rf(Destination),
ok = file:make_dir(Destination),
ok = erl_tar:extract(TarGZ, [{cwd, Destination}]),
zx_daemon ! {self(), ok}.
%%% Monitor Index ADT Interface Functions

View File

@ -21,7 +21,7 @@
%%% Functions
-spec ensure_keypair(zx:key_id()) -> true | no_return().
-spec ensure_keypair(zx:key_id()) -> zx:outcome().
%% @private
%% Check if both the public and private key based on KeyID exists.
@ -30,17 +30,17 @@ ensure_keypair(KeyID = {Realm, KeyName}) ->
{true, true} ->
true;
{false, true} ->
Message = "Public key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1);
Format = "Public key ~tp/~tp cannot be found",
Message = io_lib:format(Message, [Realm, KeyName]),
{error, Message, 2};
{true, false} ->
Message = "Private key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1);
Format = "Private key ~tp/~tp cannot be found",
Message = io_lib:format(Message, [Realm, KeyName]),
{error, Message, 2};
{false, false} ->
Message = "Key pair ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1)
Format = "Key pair ~tp/~tp cannot be found",
Message = io_lib:format(Message, [Realm, KeyName]),
{error, Message, 2}
end.
@ -120,39 +120,51 @@ generate_rsa(KeyID = {Realm, KeyName}) ->
PubFile = path(public, KeyID),
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),
log(info, "~ts and ~ts agree", [KeyFile, PubFile]);
false ->
ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]),
ok = log(error, "Something has gone wrong."),
case gen_p_key(KeyFile) of
ok ->
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),
log(info, "~ts and ~ts agree", [KeyFile, PubFile]);
false ->
ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]),
ok = log(error, "Something has gone wrong."),
{error, keygen_fail}
end;
{error, no_ssl} ->
ok = log(error, "OpenSSL not found."),
{error, keygen_fail}
end.
-spec gen_p_key(KeyFile) -> ok
when KeyFile :: file:filename().
-spec gen_p_key(KeyFile) -> Result
when KeyFile :: file:filename()
Result :: ok
| {error, no_ssl}.
%% @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).
case openssl() of
{ok, OpenSSL} ->
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);
Error ->
Error
end.
-spec der_to_pem(KeyFile, PemFile) -> ok
@ -196,11 +208,14 @@ check_key(KeyFile, PubFile) ->
public_key:verify(TestMessage, sha512, Signature, Pub).
-spec openssl() -> Executable | no_return()
when Executable :: file:filename().
-spec openssl() -> Result
when Result :: {ok, Executable}
| {error, no_ssl},
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.
%% TODO: Determine whether it is even worth it to perform this check VS restricting
%% os:cmd/1 directed zx_key functions by platform.
openssl() ->
OpenSSL =
@ -208,16 +223,15 @@ openssl() ->
{unix, _} -> "openssl";
{win32, _} -> "openssl.exe"
end,
ok =
case os:find_executable(OpenSSL) of
false ->
ok = log(error, "OpenSSL could not be found in this system's PATH."),
ok = log(error, "Install OpenSSL and then retry."),
error_exit("Missing system dependenct: OpenSSL", ?LINE);
Path ->
log(info, "OpenSSL executable found at: ~ts", [Path])
end,
OpenSSL.
case os:find_executable(OpenSSL) of
false ->
ok = log(error, "OpenSSL could not be found in this system's PATH."),
ok = log(error, "Install OpenSSL and then retry."),
{error, no_ssl};
Path ->
log(info, "OpenSSL executable found at: ~ts", [Path]),
OpenSSL
end.
-spec load(Type, KeyID) -> Result
@ -242,20 +256,16 @@ load(Type, KeyID) ->
end.
-spec verify(Data, Signature, PubKey) -> ok | no_return()
-spec verify(Data, Signature, PubKey) -> boolean()
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.
%% Curry out the choice of algorithm. This will probably disappear in a few more
%% versions as the details of sha512 and RSA gradually give way to the Brave New World.
verify(Data, Signature, PubKey) ->
case public_key:verify(Data, sha512, Signature, PubKey) of
true -> ok;
false -> error_exit("Bad package signature!", ?LINE)
end.
public_key:verify(Data, sha512, Signature, PubKey).

View File

@ -25,11 +25,9 @@
valid_lower0_9/1, valid_label/1, valid_version/1,
string_to_version/1, version_to_string/1,
package_id/1, package_string/1,
namify_zsp/1, namify_tgz/1,
zsp_path/1,
namify_zsp/1, zsp_path/1,
find_latest_compatible/2, installed/1,
realm_conf/1, load_realm_conf/1,
extract_zsp_or_die/1, halt_if_exists/1,
build/0,
rm_rf/1, rm/1,
b_to_t/1, b_to_ts/1]).
@ -582,37 +580,17 @@ package_string(_) ->
when PackageID :: zx:package_id(),
ZrpFileName :: file:filename().
%% @private
%% Map an PackageID to its correct .zsp package file name.
%% Map a PackageID to its correct .zsp package file name.
namify_zsp(PackageID) -> namify(PackageID, "zsp").
-spec namify_tgz(PackageID) -> TgzFileName
when PackageID :: zx:package_id(),
TgzFileName :: file:filename().
%% @private
%% Map an PackageID to its correct gzipped tarball source bundle filename.
namify_tgz(PackageID) -> namify(PackageID, "tgz").
-spec namify(PackageID, Suffix) -> FileName
when PackageID :: zx:package_id(),
Suffix :: string(),
FileName :: file:filename().
%% @private
%% Converts an PackageID to a canonical string, then appends the provided
%% filename Suffix.
namify(PackageID, Suffix) ->
namify_zsp(PackageID) ->
{ok, PackageString} = package_string(PackageID),
PackageString ++ "." ++ Suffix.
PackageString ++ ".zsp".
-spec zsp_path(zx:package_id()) -> file:filename().
zsp_path(PackageID) ->
filename:join(path(zsp, element(1, PackageID)), namify_zsp(PackageID)).
zsp_path({Realm, _, _}) ->
filename:join(path(zsp, Realm), namify_zsp(PackageID)).
-spec find_latest_compatible(Version, Versions) -> Result
@ -683,7 +661,7 @@ realm_conf(Realm) ->
| file:posix()
| {Line :: integer(), Mod :: module(), Cause :: term()}.
%% @private
%% Load the config for the given realm or halt with an error.
%% Load the config for the given realm.
load_realm_conf(Realm) ->
Path = realm_conf(Realm),
@ -696,41 +674,6 @@ load_realm_conf(Realm) ->
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.",
error_exit(Message, [FileName], ?LINE);
{error, invalid_tar_checksum} ->
Message = "~ts is not a valid zsp archive.",
error_exit(Message, [FileName], ?LINE);
{error, Reason} ->
Message = "Extracting package file failed with: ~160tp.",
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 -> error_exit("~ts already exists! Halting.", [Path], ?LINE);
false -> ok
end.
-spec build() -> ok.
%% @private
%% Run any local `zxmake' script needed by the project for non-Erlang code (if present),
@ -755,6 +698,7 @@ build() ->
-spec rm_rf(file:filename()) -> ok | {error, file:posix()}.
%% @private
%% Recursively remove files and directories. Equivalent to `rm -rf'.
%% Does not return an error on a nonexistant path.
rm_rf(Path) ->
case filelib:is_dir(Path) of
@ -764,7 +708,10 @@ rm_rf(Path) ->
ok = lists:foreach(fun rm/1, Contents),
file:del_dir(Path);
false ->
file:delete(Path)
case filelib:is_regular(Path) of
true -> file:delete(Path);
false -> ok
end
end.

View File

@ -10,7 +10,7 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([initialize/2, assimilate/1, set_version/1,
-export([initialize/2, set_version/1,
list_realms/0, list_packages/1, list_versions/1,
set_dep/1, list_deps/0, list_deps/1, drop_dep/1, verup/1, package/1,
add_realm/1, drop_realm/1,
@ -268,39 +268,6 @@ initialize_app_file({_, Name, Version}, AppStart) ->
zx_lib:write_terms(AppFile, [AppProfile]).
-spec assimilate(PackageFile) -> zx:outcome()
when PackageFile :: file:filename().
%% @private
%% Receives a path to a file containing package data, examines it, and copies it to a
%% canonical location under a canonical name.
assimilate(PackageFile) ->
Files = zx_lib:extract_zsp_or_die(PackageFile),
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zx_lib:zomp_dir()),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
PackageID = maps:get(package_id, Meta),
TgzFile = zx_lib:namify_tgz(PackageID),
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
case public_key:verify(TgzData, sha512, Signature, PubKey) of
true ->
ok = file:copy(PackageFile, zx_lib:zsp_path(PackageID)),
assimilate2(CWD, PackageID);
false ->
{error, "Bad package signature.", 1}
end.
assimilate2(CWD, PackageID) ->
ok = file:set_cwd(CWD),
Message = "~ts is now locally available.",
{ok, PackageString} = zx_lib:package_string(PackageID),
log(info, Message, [PackageString]).
-spec set_version(VersionString) -> zx:outcome()
when VersionString :: string().
%% @private