Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@

{project_plugins, [{covertool, "2.0.6"}, {rebar3_ex_doc, "0.2.30"}, {rebar3_hank, "1.4.0"}]}.

{deps, [{hex_core, "0.12.2"}, {verl, "1.1.1"}]}.
%% TODO: Use released hex_core version
{deps, [{hex_core, {git, "https://github.com/maennchen/hex_core.git", {branch, "jm/hex_auth"}}}, {verl, "1.1.1"}]}.

{profiles, [
{test, [
Expand Down
7 changes: 4 additions & 3 deletions rebar.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{"1.2.0",
[{<<"hex_core">>,{pkg,<<"hex_core">>,<<"0.12.2">>},0},
[{<<"hex_core">>,
{git,"https://github.com/maennchen/hex_core.git",
{ref,"b8cea90a9e2e497e9d8553c81b06939d78cf076d"}},
0},
{<<"verl">>,{pkg,<<"verl">>,<<"1.1.1">>},0}]}.
[
{pkg_hash,[
{<<"hex_core">>, <<"116F75685E707624BA79B6F6B06BE4F5E14C5FCCECCF31D4EA3ED4FED6134F8C">>},
{<<"verl">>, <<"98F3EC48B943AA4AE8E29742DE86A7CD752513687911FE07D2E00ECDF3107E45">>}]},
{pkg_hash_ext,[
{<<"hex_core">>, <<"00E402C302BF4B8CEDC2D3425E22B55F8C70A2542D69C87A2E5BA7AF2745F6C6">>},
{<<"verl">>, <<"0925E51CD92A0A8BE271765B02430B2E2CFF8AC30EF24D123BD0D58511E8FB18">>}]}
].
39 changes: 31 additions & 8 deletions src/rebar3_hex.erl
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,37 @@
-export_type([task/0]).

init(State) ->
lists:foldl(fun provider_init/2, {ok, State}, [rebar3_hex_user,
rebar3_hex_build,
rebar3_hex_cut,
rebar3_hex_owner,
rebar3_hex_organization,
rebar3_hex_search,
rebar3_hex_retire,
rebar3_hex_publish]).
State1 = override_http_adapters(State),
lists:foldl(fun provider_init/2, {ok, State1}, [rebar3_hex_user,
rebar3_hex_build,
rebar3_hex_cut,
rebar3_hex_owner,
rebar3_hex_organization,
rebar3_hex_repo,
rebar3_hex_search,
rebar3_hex_retire,
rebar3_hex_publish]).

%% Override http_adapter for all repos to use rebar3_hex_httpc_adapter
override_http_adapters(State) ->
Resources = rebar_state:resources(State),
Resources1 = lists:map(fun(Resource) ->
%% Resource is a #resource{} record: {resource, Type, Module, State, Impl}
case element(2, Resource) of
pkg ->
PkgState = element(4, Resource),
case maps:find(repos, PkgState) of
{ok, Repos} ->
Repos1 = [rebar3_hex_config:set_http_adapter(R) || R <- Repos],
setelement(4, Resource, PkgState#{repos := Repos1});
error ->
Resource
end;
_ ->
Resource
end
end, Resources),
rebar_state:set_resources(State, Resources1).

provider_init(Module, {ok, State}) ->
Module:init(State).
Expand Down
175 changes: 20 additions & 155 deletions src/rebar3_hex_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
-export([ api_key_name/1
, api_key_name/2
, all_repos/1
, encrypt_write_key/3
, decrypt_write_key/3
, repos_key_name/0
, org_key_name/2
, parent_repos/1
, get_hex_config/3
, default_repo/1
, repo/1
, repo/2
, set_http_adapter/1
, update_auth_config/2
, auth_config/1
, update_repo_auth_config/3
, remove_from_auth_config/2
]).

-include("rebar3_hex.hrl").
Expand All @@ -31,46 +32,6 @@ api_key_name(Key, Suffix) ->
Prefix = key_name_prefix(Key),
key_name(Prefix, <<"-api-">>, Suffix).

-ifdef(POST_OTP_22).
-spec encrypt_write_key(binary(), binary(), binary()) -> {binary(), {binary(), binary()}}.
encrypt_write_key(Username, LocalPassword, WriteKey) ->
AAD = Username,
IV = crypto:strong_rand_bytes(16),
Key = pad(LocalPassword),
{IV, crypto:crypto_one_time_aead(cipher(Key), Key, IV, WriteKey, AAD, true)}.
-else.
-spec encrypt_write_key(binary(), binary(), binary()) -> {binary(), {binary(), binary()}}.
encrypt_write_key(Username, LocalPassword, WriteKey) ->
AAD = Username,
IV = crypto:strong_rand_bytes(16),
{IV, crypto:block_encrypt(aes_gcm, pad(LocalPassword), IV, {AAD, WriteKey})}.
-endif.

-ifdef(POST_OTP_22).
decrypt_write_key(Username, LocalPassword, {IV, {CipherText, CipherTag}}) ->
Key = pad(LocalPassword),
crypto:crypto_one_time_aead(cipher(Key), Key, IV, CipherText, Username, CipherTag, false).
-else.
decrypt_write_key(Username, LocalPassword, {IV, {CipherText, CipherTag}}) ->
crypto:block_decrypt(aes_gcm, pad(LocalPassword), IV, {Username, CipherText, CipherTag}).
-endif.

-ifdef(POST_OTP_22).
cipher(Key) when byte_size(Key) == 16 -> aes_128_gcm;
cipher(Key) when byte_size(Key) == 24 -> aes_192_gcm;
cipher(Key) when byte_size(Key) == 32 -> aes_256_gcm.
-endif.

pad(Binary) ->
case byte_size(Binary) of
Size when Size =< 16 ->
<<Binary/binary, 0:((16 - Size) * 8)>>;
Size when Size =< 24 ->
<<Binary/binary, 0:((24 - Size) * 8)>>;
Size when Size =< 32 ->
<<Binary/binary, 0:((32 - Size) * 8)>>
end.

-spec repos_key_name() -> binary().
repos_key_name() ->
key_name(hostname(), <<"-repositories">>).
Expand All @@ -97,6 +58,15 @@ key_name_prefix(Key) -> Key.
update_auth_config(Config, State) ->
rebar_hex_repos:update_auth_config(Config, State).

auth_config(State) ->
rebar_hex_repos:auth_config(State).

update_repo_auth_config(RepoConfig, RepoName, State) ->
rebar_hex_repos:update_repo_auth_config(RepoConfig, RepoName, State).

remove_from_auth_config(RepoName, State) ->
rebar_hex_repos:remove_from_auth_config(RepoName, State).

all_repos(State) ->
Resources = rebar_state:resources(State),
#{repos := Repos} = rebar_resource_v2:find_resource_state(pkg, Resources),
Expand All @@ -111,12 +81,7 @@ repo(State) ->
Res = [R || R <- Repos, maps:get(name, R) =/= ?DEFAULT_HEX_REPO],
case Res of
[] ->
case rebar_hex_repos:get_repo_config(?DEFAULT_HEX_REPO, Repos) of
{ok, Repo} ->
{ok, set_http_adapter(Repo)};
_ ->
{error, no_repo_in_state}
end;
repo(State, ?DEFAULT_HEX_REPO);
[_Repo|_Rest] ->
{error, {required, repo}}
end;
Expand All @@ -126,129 +91,29 @@ repo(State) ->

repo(State, RepoName) ->
BinName = rebar_utils:to_binary(RepoName),
Repos = all_repos(State),
MaybeFound1 = get_repo(BinName, all_repos(State)),
MaybeParentRepo = <<"hexpm:">>,
MaybeFound2 = get_repo(<<MaybeParentRepo/binary, BinName/binary>>, Repos),
case {MaybeFound1, MaybeFound2} of
{{ok, Repo1}, undefined} ->
Repo2 = set_http_adapter(merge_with_env(Repo1)),
Repo3 = maybe_set_api_organization(Repo2),
{ok, maybe_set_api_repository(Repo3)};
{undefined, {ok, Repo2}} ->
Repo3 = set_http_adapter(merge_with_env(Repo2)),
Repo4 = maybe_set_api_organization(Repo3),
{ok, maybe_set_api_repository(Repo4)};
{undefined, undefined} ->
try rebar_hex_repos:get_repo_config(BinName, State) of
{ok, Repo} ->
{ok, set_http_adapter(Repo)}
catch
throw:{error, {rebar_hex_repos, {repo_not_found, _}}} ->
{error, {not_valid_repo, RepoName}}
end.


-define( ENV_VARS
, [ {"HEX_API_KEY", {api_key, {string, undefined}}}
, {"HEX_API_URL", {api_url, {string, undefined}}}
, {"HEX_UNSAFE_REGISTRY", {repo_verify, {boolean, false}}}
, {"HEX_NO_VERIFY_REPO_ORIGIN", {repo_verify_origin, {boolean, true}}}
]
).

merge_with_env(Repo) ->
lists:foldl(fun({EnvName, {Key, _} = Default}, Acc) ->
Val = maybe_env_val(EnvName, Default),
maybe_put_key(Key, Val, Acc)
end, Repo, ?ENV_VARS).

maybe_put_key(_Key, undefined, Repo) ->
Repo;
maybe_put_key(Key, Val, Repo) ->
case maps:get(Key, Repo, undefined) of
Val ->
Repo;
_ ->
Repo#{Key => Val}
end.

maybe_env_val(K, {_, {Type, Default}}) ->
case {os:getenv(K), {Type, Default}} of
{false, {_, Default}} ->
Default;
{"", {_, Default}} ->
Default;
{Val, {boolean, _}} ->
to_bool(string:to_lower(Val));
{Val, {string, _}} ->
rebar_utils:to_binary(Val)
end.

set_http_adapter(Repo) ->
Repo#{http_adapter => {rebar3_hex_httpc_adapter, #{profile => rebar}}}.

to_bool("0") -> false;
to_bool("false") -> false;
to_bool(_) -> true.

parent_repos(State) ->
Fun = fun(#{name := Name} = Repo, Acc) ->
[Parent|_] = rebar3_hex_io:str_split(Name, <<":">>),
case maps:is_key(Parent, Acc) of
true ->
Acc;
false ->
maps:put(name, Repo, Acc)
maps:put(name, set_http_adapter(Repo), Acc)
end
end,
Map = lists:foldl(Fun, #{}, all_repos(State)),
maps:values(Map).

default_repo(State) ->
rebar_hex_repos:get_repo_config(?DEFAULT_HEX_REPO, all_repos(State)).

get_repo(BinaryName, Repos) ->
try rebar_hex_repos:get_repo_config(BinaryName, Repos) of
Name ->
Name
catch
{error,{rebar_hex_repos,{repo_not_found,BinaryName}}} -> undefined
end.

-spec get_hex_config(module(), map(), read | write) -> map().
get_hex_config(Module, Repo, Mode) ->
case hex_config(Repo, Mode) of
{ok, HexConfig} ->
HexConfig;
{error, Reason} ->
erlang:error({error, {Module, {get_hex_config, Reason}}})
end.

hex_config(Repo, read) ->
hex_config_read(Repo);
hex_config(Repo, write) ->
hex_config_write(Repo).

hex_config_write(#{api_key := Key} = HexConfig) when is_binary(Key) ->
{ok, set_http_adapter(HexConfig)};
hex_config_write(#{write_key := undefined}) ->
{error, no_write_key};
hex_config_write(#{write_key := WriteKey, username := Username} = HexConfig) ->
DecryptedWriteKey = rebar3_hex_user:decrypt_write_key(Username, WriteKey),
{ok, set_http_adapter(HexConfig#{api_key => DecryptedWriteKey})};
hex_config_write(_) ->
{error, no_write_key}.

hex_config_read(#{read_key := ReadKey} = HexConfig) ->
{ok, set_http_adapter(HexConfig#{api_key => ReadKey})};
hex_config_read(_Config) ->
{error, no_read_key}.

maybe_set_api_organization(#{name := Name} = Repo) ->
case binary:split(Name, <<":">>) of
[_] ->
Repo#{api_organization => undefined};
[_,Org] ->
Repo#{api_organization => Org}
end.

maybe_set_api_repository(#{api_repository := _} = Repo) ->
Repo;
maybe_set_api_repository(#{} = Repo) ->
Repo#{api_repository => undefined}.
10 changes: 4 additions & 6 deletions src/rebar3_hex_error.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ format_error({not_valid_repo, RepoName}) ->
io_lib:format("Could not find ~ts in repo configuration. Be sure to authenticate first with rebar3 hex user auth.",
[RepoName]);

format_error({get_hex_config, no_read_key}) ->
"No read key found for user. Be sure to authenticate first with:"
" rebar3 hex user auth";
format_error({auth_error, no_credentials}) ->
"No Hex credentials found. Authenticate with: rebar3 hex user auth";

format_error({get_hex_config, no_write_key}) ->
"No write key found for user. Be sure to authenticate first with:"
" rebar3 hex user auth";
format_error({auth_error, Reason}) ->
io_lib:format("Authentication error: ~p", [Reason]);

format_error({Cmd, unsupported_params}) ->
io_lib:format("Either some or all of the parameters supplied for the ~ts command are ", [Cmd])
Expand Down
46 changes: 45 additions & 1 deletion src/rebar3_hex_httpc_adapter.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

-module(rebar3_hex_httpc_adapter).
-behaviour(hex_http).
-export([request/5]).
-export([request/5, request_to_file/6]).

%%====================================================================
%% API functions
Expand All @@ -20,10 +20,54 @@ request(Method, URI, ReqHeaders, Body, AdapterConfig) ->
{error, Reason} -> {error, Reason}
end.

request_to_file(Method, URI, ReqHeaders, Body, Filename, AdapterConfig) ->
Profile = maps:get(profile, AdapterConfig, default),
Request = build_request(URI, ReqHeaders, Body),
SSLOpts = [{ssl, rebar_utils:ssl_opts(URI)}],
case httpc:request(Method, Request, SSLOpts, [{sync, false}, {stream, self}], Profile) of
{ok, RequestId} ->
stream_to_file(RequestId, Filename);
{error, Reason} ->
{error, Reason}
end.

%%====================================================================
%% Internal functions
%%====================================================================

%% @private
%% httpc streams 200/206 responses as messages and returns non-2xx as
%% a normal response tuple. stream_start includes the response headers.
stream_to_file(RequestId, Filename) ->
receive
{http, {RequestId, stream_start, Headers}} ->
{ok, File} = file:open(Filename, [write, binary]),
case stream_body(RequestId, File) of
ok ->
ok = file:close(File),
{ok, {200, load_headers(Headers)}};
{error, Reason} ->
ok = file:close(File),
{error, Reason}
end;
{http, {RequestId, {{_, StatusCode, _}, RespHeaders, _RespBody}}} ->
{ok, {StatusCode, load_headers(RespHeaders)}};
{http, {RequestId, {error, Reason}}} ->
{error, Reason}
end.

%% @private
stream_body(RequestId, File) ->
receive
{http, {RequestId, stream, BinBodyPart}} ->
ok = file:write(File, BinBodyPart),
stream_body(RequestId, File);
{http, {RequestId, stream_end, _Headers}} ->
ok;
{http, {RequestId, {error, Reason}}} ->
{error, Reason}
end.

build_request(URI, ReqHeaders, Body) ->
build_request2(binary_to_list(URI), dump_headers(ReqHeaders), Body).

Expand Down
Loading
Loading