From 77ff0ca084d6eadbf8c5d181d0bf8dc7a157b411 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Thu, 21 May 2026 14:32:38 +0900 Subject: [PATCH] WIP --- include/gd_sock.hrl | 9 ++ src/gd_gui.erl | 4 +- src/gd_n_recvr.erl | 229 ++++++++++++++++++++++++++++++------------- src/gd_v_express.erl | 95 +++++++++++++++--- 4 files changed, 252 insertions(+), 85 deletions(-) create mode 100644 include/gd_sock.hrl diff --git a/include/gd_sock.hrl b/include/gd_sock.hrl new file mode 100644 index 0000000..f9f04f0 --- /dev/null +++ b/include/gd_sock.hrl @@ -0,0 +1,9 @@ +send(Socket, Binary) -> + case gen_tcp:send(Socket, Binary) of + ok -> + ok; + Error -> + ok = tell(info, "Failure on ~w:send/2: ~p", [?MODULE, Error]), + ok = zx_net:disconnect(Socket), + exit(normal) + end. diff --git a/src/gd_gui.erl b/src/gd_gui.erl index b0e5c29..713bd22 100644 --- a/src/gd_gui.erl +++ b/src/gd_gui.erl @@ -206,8 +206,8 @@ init(Prefs) -> ok = gd_v:safe_size(Frame, Prefs), - HK_Express = wxAcceleratorEntry:new([{flags, ?wxACCEL_CTRL}, {keyCode, $E}, {cmd, ?openEXPRESS}]), - HK_FWeaver = wxAcceleratorEntry:new([{flags, ?wxACCEL_CTRL}, {keyCode, $F}, {cmd, ?openFWEAVER}]), + HK_Express = wxAcceleratorEntry:new([{flags, ?wxACCEL_NORMAL}, {keyCode, $E}, {cmd, ?openEXPRESS}]), + HK_FWeaver = wxAcceleratorEntry:new([{flags, ?wxACCEL_NORMAL}, {keyCode, $F}, {cmd, ?openFWEAVER}]), Entries = [HK_Express, HK_FWeaver], Hotkeys = wxAcceleratorTable:new(length(Entries), Entries), ok = wxFrame:setAcceleratorTable(Frame, Hotkeys), diff --git a/src/gd_n_recvr.erl b/src/gd_n_recvr.erl index 3195a1c..1e16c92 100644 --- a/src/gd_n_recvr.erl +++ b/src/gd_n_recvr.erl @@ -11,24 +11,62 @@ -copyright("QPQ AG "). -license("GPL-3.0-or-later"). --export([fetch/2]). +-export([check/1, response/2, fetch/2]). -export([init/2, stop/1]). -include("$zx_include/zx_logger.hrl"). -record(s, {id = <<>> :: binary(), - host = none :: none | {Addr :: term(), Port :: term()}, % FIXME, obvsly + host = none :: none | host(), socket = none :: none | gen_tcp:socket()}). +-type host() :: {Addr :: inet:ip_address() | inet:hostname(), + Port :: inet:port_number()}. + %%% Service interface --spec fetch(Rider, ParcelID) -> ok - when Rider :: pid(), +-spec check(Recvr) -> Result + when Recvr :: pid(), + Result :: {ok, Challenge} | {error, Reason}, + Challenge :: binary(), + Reason :: term(). + +check(Recvr) -> + call(Recvr, check). + + +-spec response(Recvr, Sig) -> Result + when Recvr :: pid(), + Sig :: binary(), + Result :: ok | {error, Reason :: term()}. + +response(Recvr, Sig) -> + call(Recvr, {response, Sig}). + + +-spec fetch(Recvr, ParcelID) -> ok + when Recvr :: pid(), ParcelID :: binary(). -fetch(Rider, Parcel) -> - Rider ! {fetch, Parcel}, +fetch(Recvr, Parcel) -> + Recvr ! {fetch, Parcel}, + ok. + + +call(Recvr, Message) -> + Ref = make_ref(), + Recvr ! {Ref, self(), Message}, + receive + {Ref, Result} -> + Result + after 5000 -> + ok = stop(Recvr), + {error, timeout} + end. + +stop(Recvr) -> + Recvr ! retire, ok. @@ -36,86 +74,124 @@ fetch(Rider, Parcel) -> init(ID, Host = {Addr, Port}) -> ok = tell(info, "Addr: ~p, Port: ~p", [Addr, Port]), - Options = [{mode, binary}, {active, once}, {packet, 4}, {keepalive, true}], State = #s{id = ID, host = Host}, + disconnected(State). + + +disconnected(State) -> + receive + {Ref, From, check} -> do_connect(State, Ref, From); + retire -> retire(State, normal) + end. + + +do_connect(State = #s{host = Host = {Addr, Port}, id = ID}, Ref, From) -> + Options = [{mode, binary}, {active, once}, {packet, 4}, {keepalive, true}], case gen_tcp:connect(Addr, Port, Options, 5000) of {ok, Socket} -> ok = tell(info, "Socket: ~p", [Socket]), NextState = State#s{socket = Socket}, - ok = gen_tcp:send(Socket, <<"GajuExpress 1 RECVR">>), - authenticate(NextState); + ok = send(Socket, <<"GajuExpress 1 RECVR:", ID/binary>>), + handshake(NextState, Ref, From); Error -> ok = tell(warning, "Failed to connect to ~p with ~p", [Host, Error]), - retire(State, normal) + retire(State, normal, "Connect failed") end. -stop(Rider) -> - Rider ! retire, - ok. +handshake(State = #s{socket = Socket}, Ref, From) -> + ok = active_once(State), + receive + {tcp, Socket, <<"GajuExpress 1 RECVR:", Challenge/binary>>} -> + case is_sus(Challenge) of + false -> + From ! {Ref, {ok, Challenge}}, + authenticate(State); + true -> + From ! {Ref, {error, "Challenge was sus."}}, + retire(State, normal) + end; + {tcp_closed, Socket} -> + From ! {Ref, {error, tcp_closed}}, + retire(State, normal, "Handshake died") + after 5000 -> + From ! {Ref, {error, timeout}}, + retire(State, normal, "Handshake timed out") + end. +is_sus(Challenge) -> + case string:split(Challenge, "_", all) of + [<<"GajuExpress-Challenge">>, <<"TS-", TS/binary>>, Rand] -> is_sus2(TS, Rand); + _ -> true + end. + +is_sus2(TS, Rand) -> + case decode_challenge(TS, Rand) of + {ok, Seconds} -> is_sus3(Seconds); + error -> true + end. + +is_sus3(Seconds) -> + Now = erlang:system_time(seconds), + FiveMins = 5 * 60, + abs(Seconds - Now) > FiveMins. + +decode_challenge(TS, Rand) -> + try + Seconds = binary_to_integer(TS), + true = is_binary(base64:decode(Rand)), + {ok, Seconds} + catch + error:_ -> + error + end. + authenticate(State = #s{socket = Socket}) -> - ok = inet:setopts(Socket, [{active, once}]), receive + {Ref, From, {response, Sig}} -> + ok = send(Socket, Sig), + await_auth(State, Ref, From); + nope -> + retire(State, normal); + retire -> + retire(State, normal); + Other -> + ok = tell(info, "Got weird message in authenticate/1: ~p", [Other]), + authenticate(State) + end. + +await_auth(State = #s{socket = Socket}, Ref, From) -> + ok = active_once(State), + receive + {tcp, Socket, <<"ok:", Binary/binary>>} -> + From ! {Ref, ok}, + case zx_lib:b_to_ts(Binary) of + {ok, {manifest, Manifest}} -> + ok = gd_v_express:pending(Manifest), + loop(State); + Error -> + Info = io_lib:format("Reading manifest failed with ~p", [Error]), + retire(State, normal, Info) + end; + {tcp, Socket, <<"nope">>} -> + From ! {Ref, {error, bad_auth}}, + retire(State, normal, "Authentication failed"); {tcp, Socket, Binary} -> - read_challenge(State, Binary); + From ! {Ref, {error, "Unknown response"}}, + Info = io_lib:format("GajuExpress sent this trash: ~p", [Binary]), + retire(State, normal, Info); {tcp_closed, Socket} -> - retire(State, normal) + From ! {Ref, {error, tcp_closed}}, + retire(State, normal, "Socket closed before manifest arrived") after 5000 -> - ok = tell(info, "GajuExpress timed out."), - retire(State, normal) - end. - -read_challenge(State, Binary) -> - case zx_lib:b_to_ts(Binary) of - {ok, {challenge, Message = <<"GajuExpress-Challenge", _/binary>>}} -> - accept_challenge(State, Message); - error -> - ok = tell(info, "handle_challenge: bad_term"), - retire(State, normal); - Garbage -> - ok = tell(info, "GajuExpress sent garbage: ~p", [Garbage]), - retire(State, normal) - end. - -accept_challenge(State = #s{id = ID, socket = Socket}, Message) -> - case gd_con:sign_binary(ID, Message) of - {ok, Sig} -> - Credential = term_to_binary({cred, ID, Sig}), - ok = gen_tcp:send(Socket, Credential), - get_list(State); - {error, bad_key} -> - ok = tell(info, "Bad ID: ~p", [ID]), - retire(State, normal) - end. - -get_list(State = #s{socket = Socket}) -> - ok = inet:setopts(Socket, [{active, once}]), - receive - {tcp, Socket, Binary} -> - read_manifest(State, Binary) - after 5000 -> - ok = tell(info, "Timed out on get_list/1"), - retire(State, normal) - end. - -read_manifest(State, Binary) -> - case zx_lib:b_to_ts(Binary) of - {ok, {pending, Manifest}} -> - ok = gd_v_express:pending(Manifest), - loop(State); - error -> - ok = tell(info, "GajuExpress sent a bad binary manifest"), - retire(State, normal); - Garbage -> - ok = tell(info, "Decoded garbage binary manifest: ~p", [Garbage]), - retire(State, normal) + From ! {Ref, {error, timeout}}, + retire(State, normal, "GajuExpress timeout") end. loop(State = #s{socket = Socket}) -> - ok = inet:setopts(Socket, [{active, once}]), + ok = active_once(State), receive {tcp, Socket, Message} -> ok = tell(info, "Got: ~tp", [Message]), @@ -126,7 +202,7 @@ loop(State = #s{socket = Socket}) -> {tcp_closed, Socket} -> retire(State, normal); retire -> - ok = gen_tcp:send(<<"bye">>), + ok = send(Socket, <<"bye">>), retire(State, normal) end. @@ -136,10 +212,23 @@ do_fetch(State, ParcelID) -> ok. -retire(#s{socket = none}, Reason) -> - gd_v_express ! {retiring, self(), Reason}, +active_once(State = #s{socket = Socket}) -> + case inet:setopts(Socket, [{active, once}]) of + ok -> ok; + Error -> retire(State, normal, Error) + end. + + +retire(State, Reason) -> + retire(State, Reason, Reason). + +retire(#s{socket = none}, Reason, Info) -> + ok = gd_v_express:retire(self(), Info), exit(Reason); -retire(#s{socket = Socket}, Reason) -> +retire(#s{socket = Socket}, Reason, Info) -> ok = zx_net:disconnect(Socket), - gd_v_express ! {retiring, self(), Reason}, + ok = gd_v_express:retire(self(), Info), exit(Reason). + + +-include("gd_sock.hrl"). diff --git a/src/gd_v_express.erl b/src/gd_v_express.erl index 24b28a9..55f9e25 100644 --- a/src/gd_v_express.erl +++ b/src/gd_v_express.erl @@ -44,7 +44,7 @@ %-behavior(gd_v). -include_lib("wx/include/wx.hrl"). -export([to_front/0, to_front/1, trouble/1]). --export([pending/1, accounts/1]). +-export([pending/1, accounts/1, retire/2]). -export([start_link/1]). -export([init/1, terminate/2, code_change/3, handle_call/3, handle_cast/2, handle_info/2, handle_event/2]). @@ -62,6 +62,7 @@ keys = #w{} :: #w{}, accs = [] :: [#wr{}], rider = none :: none | pid(), + recvr = none :: none | pid(), check = #w{} :: #w{}, list = #w{} :: #w{}, dl = #w{} :: #w{}, @@ -115,6 +116,14 @@ accounts(Manifest) -> wx_object:cast(?MODULE, {accounts, Manifest}). +-spec retire(PID, Info) -> ok + when PID :: pid(), + Info :: term(). + +retire(PID, Info) -> + gen_server:cast(?MODULE, {retire, PID, Info}). + + %%% Startup @@ -210,8 +219,10 @@ init({Prefs, {Selected, Keys}}) -> ok = gd_v:safe_size(Frame, NewPrefs), ok = wxFrame:connect(Frame, command_button_clicked), + ok = wxFrame:connect(Frame, command_choice_selected), ok = wxFrame:connect(Frame, close_window), ok = wxListBox:connect(DownloadP, command_listbox_doubleclicked), + true = wxFrame:show(Frame), State = #s{wx = Wx, frame = Frame, lang = Lang, j = J, prefs = Prefs, keys = KP, accs = Keys, check = CheckB, list = DL_L, @@ -239,6 +250,9 @@ handle_cast({pending, Manifest}, State) -> handle_cast({accounts, Manifest}, State) -> NewState = do_accounts(Manifest, State), {noreply, NewState}; +handle_cast({retire, PID, Info}, State) -> + NewState = do_retire(PID, Info, State), + {noreply, NewState}; handle_cast(to_front, State = #s{frame = Frame}) -> ok = ensure_shown(Frame), ok = wxFrame:raise(Frame), @@ -251,9 +265,6 @@ handle_cast(Unexpected, State) -> {noreply, State}. -handle_info({retiring, PID, Reason}, State = #s{rider = PID}) -> - ok = tell(info, "Rider retired with: ~p", [Reason]), - {noreply, State#s{rider = none}}; handle_info(Unexpected, State) -> ok = log(warning, "Unexpected info: ~tp~n", [Unexpected]), {noreply, State}. @@ -271,8 +282,10 @@ handle_event(#wx{event = #wxCommand{type = command_button_clicked}, id = ID}, State = #s{ul = #w{id = ID}}) -> NewState = do_ul(State), {noreply, NewState}; -handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked}}, - State) -> +handle_event(#wx{event = #wxCommand{type = command_choice_selected}}, State) -> + ok = kill_recvr(State), + {noreply, State}; +handle_event(#wx{event = #wxCommand{type = command_listbox_doubleclicked}}, State) -> NewState = do_dl(State), {noreply, NewState}; handle_event(#wx{event = #wxClose{}}, State) -> @@ -310,14 +323,55 @@ do_accounts(State, Manifest) -> State. -do_check(State = #s{rider = none}) -> - PID = spawn_link(gd_n_rider, init, [{"localhost", 7777}]), - do_check(State#s{rider = PID}); -do_check(State = #s{rider = PID, keys = #w{wx = KeyP}}) -> +do_check(State = #s{recvr = none, keys = #w{wx = KeyP}}) -> + case wxChoice:getStringSelection(KeyP) of + "" -> + State; + KeyID -> + PubKey = list_to_binary(KeyID), + PID = spawn_link(gd_n_recvr, init, [PubKey, {"localhost", 7777}]), + do_check2(State#s{recvr = PID}) + end; +do_check(State = #s{recvr = PID}) -> + case gd_n_recvr:check(PID) of + ok -> State; + {ok, Challenge} -> challenge(State, Challenge) + end. + + +do_check2(State = #s{recvr = PID}) -> + case gd_n_recvr:check(PID) of + {ok, Challenge} -> + challenge(State, Challenge); + {error, Reason} -> + ok = tell(info, "GajuExpress connection failed with: ~p", [Reason]), + State#s{recvr = none} + end. + +challenge(State = #s{recvr = PID, keys = KeyP}, Challenge) -> + case wxChoice:getStringSelection(KeyP) of + "" -> + ok = gd_n_recvr:stop(PID), + State; + KeyID -> + PubKey = list_to_binary(KeyID), + handle_challenge(State, PubKey, Challenge) + end. + +handle_challenge(State = #s{recvr = PID}, PubKey, Challenge) -> + case gd_con:sign_binary(PubKey, Challenge) of + {ok, Sig} -> + respond(State, Sig); + {error, bad_key} -> + ok = gd_n_recvr:stop(PID), + State + end. + +respond(State = #s{recvr = PID}, Sig) -> ok = - case wxChoice:getStringSelection(KeyP) of - "" -> ok; - KeyID -> gd_n_rider:check(PID, KeyID) + case gd_n_recvr:response(PID, Sig) of + ok -> ok; + {error, Reason} -> ok = tell(info, "~p: ~p", [PID, Reason]) end, State. @@ -354,6 +408,21 @@ do_close(#s{frame = Frame, prefs = Prefs}) -> % unicode:characters_to_list(Name ++ ".gaju"). +kill_recvr(#s{recvr = none}) -> ok; +kill_recvr(#s{recvr = PID}) -> gd_n_recvr:stop(PID). + + +do_retire(PID, Info, State = #s{rider = PID}) -> + ok = tell(info, "Rider retired with: ~p", [Info]), + State#s{rider = none}; +do_retire(PID, Info, State = #s{recvr = PID}) -> + ok = tell(info, "Rider retired with: ~p", [Info]), + State#s{recvr = none}; +do_retire(PID, Info, State) -> + ok = tell(info, "~p retired with: ~p", [PID, Info]), + State. + + ensure_shown(Frame) -> case wxWindow:isShown(Frame) of true ->