-module(gmhc_eureka). -vsn("0.6.1"). -export([get_pool_address/0]). -export([cache_good_address/1, invalidate_cache/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() -> CacheF = cache_filename(), ?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 , host := _ , port := _ , pool_id := _} = Map -> 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 := Addr, port := Port, pool_id := PoolId}) -> CacheF = cache_filename(), ToCache = #{ host => unicode:characters_to_binary(Addr) , port => Port , pool_id => unicode:characters_to_binary(PoolId) , ts => erlang:system_time(seconds)}, 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. 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_filename() -> <<"ak_", PKShort:8/binary, _/binary>> = gmhc_config:get_config([<<"pubkey">>]), filename:join(setup:data_dir(), "gmhc_eureka." ++ binary_to_list(PKShort) ++ ".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, <>); expand_vars(<>, Acc) -> expand_vars(T, <>); 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(<>, Acc) -> get_var_name(T, <>). %% 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, <>}; %% 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) >>, %% <>. %% ====================================================================== %% 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 => <<>>}}.