gmhive_client/src/gmhc_eureka.erl
2025-10-15 12:41:00 +02:00

280 lines
9.2 KiB
Erlang

-module(gmhc_eureka).
-vsn("0.8.3").
-export([get_pool_address/0]).
-export([cache_good_address/1,
invalidate_cache/0,
cached_address/0,
cache_filename/0,
cache_dir/0]).
-include_lib("kernel/include/logger.hrl").
-include("gmhc_events.hrl").
get_pool_address() ->
case cached_address() of
{ok, _} = Ok -> Ok;
{error, _} ->
get_pool_address_()
end.
cached_address() ->
I0 = cache_info(),
CacheF = cache_filename(I0),
?LOG_DEBUG("Eureka cache filename: ~p", [CacheF]),
case file:read_file(CacheF) of
{ok, Bin} ->
NowTS = erlang:system_time(seconds),
OldestTS = NowTS - 24*60*60,
try binary_to_term(Bin) of
#{ ts := TS
, network := N
, pubkey := PK
, host := _
, port := _
, pool_id := _} = Map when N == map_get(network, I0),
PK == map_get(pubkey, I0) ->
if TS >= OldestTS ->
Result = maps:remove(ts, Map),
?LOG_DEBUG("Cached eureka info: ~p", [Result]),
{ok, Result};
true ->
{error, outdated}
end;
Other ->
{error, {invalid_cache_term, Other}}
catch
error:E ->
{error, {invalid_cache_data, E}}
end;
{error, _} = Err ->
Err
end.
cache_good_address(#{host := _,
port := _,
pool_id := _} = I0) ->
CacheInfo = cache_info(I0),
CacheF = cache_filename(CacheInfo),
ToCache = CacheInfo#{ts => erlang:system_time(seconds)},
case filelib:ensure_dir(CacheF) of
ok ->
case file:write_file(CacheF, term_to_binary(ToCache)) of
ok ->
?LOG_DEBUG("Cached eureka info in: ~p", [CacheF]),
{ok, ToCache};
{error, _} = Err ->
?LOG_DEBUG("Couldn't cache eureka in ~p: ~p", [CacheF, Err]),
Err
end;
{error, _} = Err ->
?LOG_ERROR("Cannot save cached info to ~s", [CacheF]),
Err
end.
invalidate_cache() ->
CacheF = cache_filename(),
case file:delete(CacheF) of
ok ->
?LOG_DEBUG("Eureka cache file removed (~p)", [CacheF]),
ok;
{error, _} = Err ->
?LOG_DEBUG("Couldn't remove Eureka cache (~p): ~p", [CacheF, Err]),
Err
end.
cache_info(#{ host := Addr
, port := Port
, pool_id := PoolId }) ->
I0 = cache_info(),
I0#{ host => unicode:characters_to_binary(Addr)
, port => Port
, pool_id => unicode:characters_to_binary(PoolId)}.
cache_info() ->
Pubkey = gmhc_config:get_config([<<"pubkey">>]),
Network = gmhc_config:get_config([<<"network">>]),
#{ pubkey => Pubkey
, network => Network }.
cache_filename() ->
cache_filename(cache_info()).
cache_filename(#{network := Network, pubkey := Pubkey}) ->
Path = filename:join(cache_dir(), Network),
<<"ak_", PKShort:8/binary, _/binary>> = Pubkey,
filename:join(Path, "gmhc_eureka." ++ binary_to_list(PKShort) ++ ".cache").
cache_dir() ->
case gmconfig:find_config([<<"cache_dir">>]) of
{ok, D} ->
D;
undefined ->
case setup_zomp:is_zomp_context() of
true ->
cache_dir_zomp();
false ->
filename:join(setup:data_dir(), "gmhive.cache")
end
end.
cache_dir_zomp() ->
#{package_id := {Realm, App, _}} = zx_daemon:meta(),
filename:join(zx_lib:ppath(var, {Realm, App}), "gmhive.cache").
get_pool_address_() ->
case gmconfig:find_config([<<"pool_admin">>, <<"url">>], [user_config]) of
{ok, URL0} ->
case expand_url(URL0) of
<<"local">> ->
{ok, #{host => <<"127.0.0.1">>,
port => gmconfig:get_config(
[<<"pool">>, <<"port">>], [schema_default]),
pool_id => gmhc_config:get_config([<<"pool">>, <<"id">>]) }};
URL ->
?LOG_INFO("Trying to connect to ~p", [URL]),
connect1(URL)
end;
undefined ->
Network = gmconfig:get_config([<<"network">>]),
URL0 = gmconfig:get_config([ <<"pool_admin">>
, <<"default_per_network">>
, Network ],
[schema_default]),
URL = expand_url(URL0),
?LOG_INFO("Using default for ~p: ~p", [Network, URL]),
connect1(URL)
end.
connect1(URL0) ->
URL = binary_to_list(URL0),
Res = request(get, URL),
?LOG_DEBUG("Res = ~p", [Res]),
case Res of
{ok, Body} ->
try get_host_port(json:decode(iolist_to_binary(Body)))
catch
error:_ ->
gmhc_events:publish(error, ?ERR_EVT(#{error => invalid_json,
data => Body})),
{error, invalid_json}
end;
{error, _} = Error ->
gmhc_events:publish(error, ?ERR_EVT(#{error => connect_failure,
data => Error})),
Error
end.
get_host_port(#{ <<"address">> := Addr
, <<"port">> := Port
, <<"pool_id">> := PoolId } = Data) ->
?LOG_DEBUG("Data = ~p", [Data]),
{ok, #{ host => Addr
, port => Port
, pool_id => PoolId }}.
request(get, URL) ->
case request(get, URL, []) of
{ok, #{body := Body}} ->
{ok, Body};
Other ->
%% TODO: perhaps return a more informative reason?
gmhc_events:publish(error, ?ERR_EVT(#{error => get_failed,
url => URL,
data => Other })),
{error, failed}
end.
request(get, URL, []) ->
Headers = [],
HttpOpts = [{timeout, 15000}],
Opts = [],
Profile = default,
request_result(httpc:request(get, {URL, Headers}, HttpOpts, Opts, Profile)).
%% request(post, URL, Body) ->
%% post_request(URL, Body).
expand_url(URL) ->
case re:run(URL, <<"{[^}]+}">>, []) of
{match, _} ->
expand_vars(URL);
nomatch ->
URL
end.
expand_vars(S) ->
expand_vars(S, <<>>).
expand_vars(<<"{", Rest/binary>>, Acc) ->
{Var, Rest1} = get_var_name(Rest),
expand_vars(Rest1, <<Acc/binary, (expand_var(Var))/binary>>);
expand_vars(<<H, T/binary>>, Acc) ->
expand_vars(T, <<Acc/binary, H>>);
expand_vars(<<>>, Acc) ->
Acc.
expand_var(<<"CLIENT_ID">>) ->
gmhc_config:get_config([<<"pubkey">>]).
get_var_name(S) ->
get_var_name(S, <<>>).
get_var_name(<<"}", Rest/binary>>, Acc) ->
{Acc, Rest};
get_var_name(<<H, T/binary>>, Acc) ->
get_var_name(T, <<Acc/binary, H>>).
%% From hz.erl ==========================================================
% This is Bitcoin's variable-length unsigned integer encoding
% See: https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer
%% vencode(N) when N =< 0 ->
%% {error, {non_pos_N, N}};
%% vencode(N) when N < 16#FD ->
%% {ok, <<N>>};
%% vencode(N) when N =< 16#FFFF ->
%% NBytes = eu(N, 2),
%% {ok, <<16#FD, NBytes/binary>>};
%% vencode(N) when N =< 16#FFFF_FFFF ->
%% NBytes = eu(N, 4),
%% {ok, <<16#FE, NBytes/binary>>};
%% vencode(N) when N < (2 bsl 64) ->
%% NBytes = eu(N, 8),
%% {ok, <<16#FF, NBytes/binary>>}.
% eu = encode unsigned (little endian with a given byte width)
% means add zero bytes to the end as needed
%% eu(N, Size) ->
%% Bytes = binary:encode_unsigned(N, little),
%% NExtraZeros = Size - byte_size(Bytes),
%% ExtraZeros = << <<0>> || _ <- lists:seq(1, NExtraZeros) >>,
%% <<Bytes/binary, ExtraZeros/binary>>.
%% ======================================================================
%% From gmplugin_web_demo_handler.erl ===================================
%% post_request(URL, Map) ->
%% ?LOG_DEBUG("Map = ~p", [Map]),
%% Body = json:encode(Map),
%% ?LOG_DEBUG("Body = ~s", [Body]),
%% PostRes = httpc:request(post, {URL, [], "application/json", Body}, [], []),
%% request_result(PostRes).
%% ======================================================================
request_result(Result) ->
?LOG_DEBUG("Request result: ~p", [Result]),
request_result_(Result).
request_result_({ok, {{_, C200, Ok}, _Hdrs, Body}}) when C200 >= 200, C200 < 300 ->
{ok, #{code => C200, msg => Ok, body => Body}};
request_result_({ok, {{_, C200, Ok}, Body}}) when C200 >= 200, C200 < 300 ->
{ok, #{code => C200, msg => Ok, body => Body}};
request_result_({ok, {{_, Code, Error}, _Hdrs, Body}}) ->
{error, #{code => Code, msg => Error, body => Body}};
request_result_(_) ->
{error, #{code => 500, msg => <<"Internal error">>, body => <<>>}}.