%% Copyright (c) 2020 Guilherme Andrade %% %% Permission is hereby granted, free of charge, to any person obtaining a %% copy of this software and associated documentation files (the "Software"), %% to deal in the Software without restriction, including without limitation %% the rights to use, copy, modify, merge, publish, distribute, sublicense, %% and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %% DEALINGS IN THE SOFTWARE. %% %% locus is an independent project and has not been authorized, sponsored, %% or otherwise approved by MaxMind. -module(locus_maxmind_download). -behaviour(gen_server). %% ------------------------------------------------------------------ %% API Function Exports %% ------------------------------------------------------------------ -export( [validate_opts/1, start_link/3 ]). -ignore_xref( [start_link/3 ]). %% ------------------------------------------------------------------ %% proc_lib Function Exports %% ------------------------------------------------------------------ -export( [init_/1 ]). -ignore_xref( [init_/1 ]). %% ------------------------------------------------------------------ %% gen_server Function Exports %% ------------------------------------------------------------------ -export( [init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). %% ------------------------------------------------------------------ %% Record and Type Definitions %% ------------------------------------------------------------------ -type opt() :: {license_key, binary() | string()} | % TODO support reading it from file or environment {date, calendar:date()} | locus_http_download:opt(). -export_type([opt/0]). -type msg() :: {event, event()} | locus_http_download:msg(). -export_type([msg/0]). -type event() :: {finished, {error, no_license_key_defined}} | locus_http_download:event(). -export_type([event/0]). -type success() :: locus_http_download:success(). -export_type([success/0]). -record(state, { owner_pid :: pid(), edition :: atom(), opts :: [opt()], http_download_pid :: pid() }). -type state() :: #state{}. %% ------------------------------------------------------------------ %% API Function Definitions %% ------------------------------------------------------------------ -spec validate_opts(proplists:proplist()) -> {ok, {[opt()], proplists:proplist()}} | {error, BadOpt :: term()}. %% @private validate_opts(MixedOpts) -> try lists:partition( fun ({license_key, Value} = Opt) -> validate_license_key_opt(Value) orelse error({badopt,Opt}); ({date, Value} = Opt) -> validate_date_opt(Value) orelse error({badopt,Opt}); (_) -> false end, MixedOpts) of {MyOpts, OtherOpts} -> case locus_http_download:validate_opts(OtherOpts) of {ok, {HttpDownloadOpts, RemainingOpts}} -> {ok, {MyOpts ++ HttpDownloadOpts, RemainingOpts}}; {error, BadOpt} -> {error, BadOpt} end catch error:{badopt,BadOpt} -> {error, BadOpt} end. -spec start_link(atom(), locus_http_download:headers(), [opt()]) -> {ok, pid()}. %% @private start_link(Edition, RequestHeaders, Opts) -> proc_lib:start_link(?MODULE, init_, [[self(), Edition, RequestHeaders, Opts]]). %% ------------------------------------------------------------------ %% proc_lib Function Definitions %% ------------------------------------------------------------------ -spec init_([InitArg, ...]) -> no_return() when InitArg :: OwnerPid | Edition | RequestHeaders | Opts, OwnerPid :: pid(), Edition :: atom(), RequestHeaders :: locus_http_download:headers(), Opts :: [opt()]. %% @private init_([OwnerPid, Edition, RequestHeaders, Opts]) -> _ = process_flag(trap_exit, true), proc_lib:init_ack(OwnerPid, {ok, self()}), {MyOpts, HttpDownloadOpts} = lists:partition( fun ({Opt, _}) -> lists:member(Opt, [license_key, date]); (_) -> false end, Opts), case get_license_key(Opts) of {ok, LicenseKey} -> URL = build_download_url(Edition, LicenseKey, Opts), {ok, HttpDownloadPid} = locus_http_download:start_link(URL, RequestHeaders, HttpDownloadOpts), State = #state{ owner_pid = OwnerPid, edition = Edition, opts = MyOpts, http_download_pid = HttpDownloadPid }, gen_server:enter_loop(?MODULE, [], State); {error, Reason} -> notify_owner_process(OwnerPid, {finished, {error, Reason}}), exit(normal) end. %% ------------------------------------------------------------------ %% gen_server Function Definitions %% ------------------------------------------------------------------ -spec init(_) -> no_return(). %% @private init(_) -> exit(not_called). -spec handle_call(term(), {pid(),reference()}, state()) -> {stop, unexpected_call, state()}. %% @private handle_call(_Call, _From, State) -> {stop, unexpected_call, State}. -spec handle_cast(term(), state()) -> {stop, unexpected_cast, state()}. %% @private handle_cast(_Cast, State) -> {stop, unexpected_cast, State}. -spec handle_info(term(), state()) -> {noreply, state()} | {stop, normal, state()} | {stop, unexpected_info, state()}. %% @private handle_info({HttpDownloadPid, Msg}, State) when HttpDownloadPid =:= State#state.http_download_pid -> handle_http_download_msg(Msg, State); handle_info({'EXIT', Pid, Reason}, State) -> handle_linked_process_death(Pid, Reason, State); handle_info(_Info, State) -> {stop, unexpected_info, State}. -spec terminate(term(), state()) -> ok. %% @private terminate(_Reason, _State) -> ok. -spec code_change(term(), state(), term()) -> {ok, state()}. %% @private code_change(_OldVsn, #state{} = State, _Extra) -> {ok, State}. %% ------------------------------------------------------------------ %% Internal Function Definitions %% ------------------------------------------------------------------ -spec validate_license_key_opt(term()) -> boolean(). validate_license_key_opt(Value) -> locus_util:is_utf8_binary(Value) orelse locus_util:is_unicode_string(Value). -spec validate_date_opt(term()) -> boolean(). validate_date_opt(Date) -> locus_util:is_date(Date). -spec get_license_key([opt()]) -> {ok, binary()} | {error, no_license_key_defined}. get_license_key(Opts) -> OptValue = proplists:get_value(license_key, Opts), AppConfigValue = application:get_env(locus, license_key, undefined), if is_binary(OptValue) -> {ok, OptValue}; is_binary(AppConfigValue), AppConfigValue =/= <<"YOUR_LICENSE_KEY">> -> {ok, AppConfigValue}; length(AppConfigValue) >= 0, AppConfigValue =/= "YOUR_LICENSE_KEY" -> <> = unicode:characters_to_binary(AppConfigValue), {ok, Value}; true -> {error, no_license_key_defined} end. -spec build_download_url(atom(), binary(), [opt()]) -> string(). build_download_url(Edition, LicenseKey, Opts) -> BinEdition = atom_to_binary(Edition, utf8), BaseQueryIoPairs = [["edition_id=", locus_util:url_query_encode(BinEdition)], ["license_key=", locus_util:url_query_encode(LicenseKey)], ["suffix=tar.gz"]], QueryIoPairs = lists:foldl( fun ({date, Date}, Acc) -> {DateYear, DateMonth, DateDay} = Date, IoDate = io_lib:format("~4..0B~2..0B~2..0B", [DateYear, DateMonth, DateDay]), [["date=", IoDate] | Acc]; (_, Acc) -> Acc end, BaseQueryIoPairs, Opts), QueryIoString = lists:join($&, QueryIoPairs), Binary = iolist_to_binary(["https://download.maxmind.com/app/geoip_download?", QueryIoString]), binary_to_list(Binary). -spec handle_http_download_msg(locus_http_download:msg(), state()) -> {noreply, state()} | {stop, normal, state()}. handle_http_download_msg({finished,_} = Msg, State) -> locus_util:expect_linked_process_termination(State#state.http_download_pid), notify_owner(Msg, State), {stop, normal, State}; handle_http_download_msg(Msg, State) -> notify_owner(Msg, State), {noreply, State}. -spec notify_owner(msg(), state()) -> ok. notify_owner(Msg, State) -> #state{owner_pid = OwnerPid} = State, notify_owner_process(OwnerPid, Msg). -spec notify_owner_process(pid(), msg()) -> ok. notify_owner_process(OwnerPid, Msg) -> _ = erlang:send(OwnerPid, {self(),Msg}, [noconnect]), ok. -spec handle_linked_process_death(pid(), term(), state()) -> {stop, normal, state()}. handle_linked_process_death(Pid, _, State) when Pid =:= State#state.owner_pid -> {stop, normal, State}; handle_linked_process_death(Pid, Reason, State) when Pid =:= State#state.http_download_pid -> {stop, {http_download_stopped, Pid, Reason}, State}.