%%%------------------------------------------------------------------- %%% @author anlicheng %%% @copyright (C) 2026, %%% @doc %%% SSL transport for sdlan sessions. %%% @end %%%------------------------------------------------------------------- -module(sdlan_ssl_transport). -author("anlicheng"). -include("sdlan.hrl"). -include("sdlan_pb.hrl"). -behaviour(gen_statem). -behaviour(ranch_protocol). %% Ranch protocol callback -export([start_link/4]). %% API -export([send_event/2, command/4, stop/2, debug_info/1]). %% gen_statem callbacks -export([init/1, handle_event/4, terminate/3, code_change/4, callback_mode/0]). -record(state, { ref :: ranch:ref(), socket :: undefined | ssl:sslsocket(), transport :: module(), max_packet_size = 16384, heartbeat_sec = 15, session :: sdlan_session:session(), close_reason = undefined }). %%%=================================================================== %%% Ranch protocol callback %%%=================================================================== -spec send_event(Pid :: pid(), Event :: binary()) -> ok. send_event(Pid, ProtobufEvent) when is_pid(Pid), is_binary(ProtobufEvent) -> gen_statem:cast(Pid, {send_event, ProtobufEvent}). -spec command(Pid :: pid(), Ref :: reference(), ReceiverPid :: pid(), {Tag :: atom(), SubCommand :: any()}) -> ok. command(Pid, Ref, ReceiverPid, SubCommand) when is_pid(Pid), is_pid(ReceiverPid) -> gen_statem:cast(Pid, {command, Ref, ReceiverPid, SubCommand}). -spec stop(Pid :: pid(), Reason :: term()) -> ok. stop(Pid, Reason) when is_pid(Pid) -> gen_statem:stop(Pid, Reason, 2000). -spec debug_info(Pid :: pid()) -> map(). debug_info(Pid) when is_pid(Pid) -> gen_statem:call(Pid, debug_info). -spec start_link(Ref :: ranch:ref(), Socket :: inet:socket(), Transport :: module(), Limits :: proplists:proplist()) -> gen_statem:start_ret(). start_link(Ref, Socket, Transport, Limits) -> gen_statem:start_link(?MODULE, [Ref, Socket, Transport, Limits], []). %%%=================================================================== %%% gen_statem callbacks %%%=================================================================== -spec init(Args :: term()) -> gen_statem:init_result(atom(), #state{}). init([Ref, Socket, Transport, Limits]) -> MaxPacketSize = proplists:get_value(max_packet_size, Limits, 16384), HeartbeatSec = proplists:get_value(heartbeat_sec, Limits, 15), Session = sdlan_session:new(HeartbeatSec, ?MODULE), {ok, handshaking, #state{ ref = Ref, socket = Socket, transport = Transport, max_packet_size = MaxPacketSize, heartbeat_sec = HeartbeatSec, session = Session }}. -spec callback_mode() -> gen_statem:callback_mode_result(). callback_mode() -> handle_event_function. -spec handle_event(EventType :: gen_statem:event_type(), EventContent :: term(), StateName :: atom(), State :: #state{}) -> gen_statem:event_handler_result(atom(), #state{}). handle_event(info, {handshake, Ref, Transport, Socket, Timeout}, handshaking, State = #state{ref = Ref, transport = Transport, max_packet_size = MaxPacketSize, heartbeat_sec = HeartbeatSec}) -> case Transport:handshake(Socket, [], Timeout) of {ok, SslSocket} -> ok = Transport:setopts(SslSocket, [{mode, binary}, {packet, 2}, {active, true}]), ssl_send(Transport, SslSocket, sdlan_session:welcome_packet(MaxPacketSize, HeartbeatSec)), logger:debug("[sdlan_ssl_transport] ssl handshake ok, send welcome"), {next_state, initialized, State#state{socket = SslSocket}}; {error, Reason} -> {stop, {ssl_handshake_failed, Reason}, State} end; handle_event(info, {ssl, Socket, Frame}, _StateName, State = #state{socket = Socket, transport = Transport, session = Session}) when is_binary(Frame) -> case sdlan_session:handle_frame(Frame, Session) of {ok, NStateName, NSession, Packets} -> ssl_send(Transport, Socket, Packets), {next_state, NStateName, State#state{session = NSession}}; {stop, Reason, _NStateName, NSession, Packets} -> ssl_send(Transport, Socket, Packets), {stop, Reason, State#state{session = NSession}} end; handle_event(info, {ssl_closed, Socket}, _StateName, State = #state{socket = Socket}) -> expected_stop(socket_closed, State); handle_event(info, {ssl_error, Socket, Reason}, _StateName, State = #state{socket = Socket}) -> expected_stop({socket_error, Reason}, State); handle_event(info, {timeout, TimerRef, ping_ticker}, _StateName, State = #state{session = Session}) -> case sdlan_session:handle_timeout(TimerRef, Session) of {ok, NStateName, NSession} -> {next_state, NStateName, State#state{session = NSession}}; {stop, Reason, _NStateName, NSession} -> expected_stop(Reason, State#state{session = NSession}) end; %% 发送事件信息 handle_event(cast, {send_event, Event}, _StateName, State = #state{socket = Socket, transport = Transport, session = Session}) -> case sdlan_session:send_event(Event, Session) of {ok, NStateName, NSession, Packets} -> ssl_send(Transport, Socket, Packets), {next_state, NStateName, State#state{session = NSession}}; {error, not_registered} -> keep_state_and_data end; %% 发送命令信息 handle_event(cast, {command, Ref, ReceiverPid, SubCommand}, _StateName, State = #state{socket = Socket, transport = Transport, session = Session}) -> case sdlan_session:command(Ref, ReceiverPid, SubCommand, Session) of {ok, NStateName, NSession, Packets} -> ssl_send(Transport, Socket, Packets), {next_state, NStateName, State#state{session = NSession}}; {error, not_registered} -> keep_state_and_data end; handle_event({call, From}, debug_info, StateName, State) -> {keep_state, State, [{reply, From, debug_info(StateName, State)}]}; handle_event(EventType, Info, StateName, State) -> logger:notice("[sdlan_ssl_transport] state: ~p, state_name: ~p, event_type: ~p, info: ~p", [State, StateName, EventType, Info]), keep_state_and_data. -spec terminate(Reason :: term(), StateName :: atom(), State :: #state{}) -> ok. terminate(Reason, _StateName, #state{socket = Socket, transport = Transport, session = Session, close_reason = CloseReason}) -> Socket =/= undefined andalso catch Transport:close(Socket), logger:notice("[sdlan_ssl_transport] terminate closed with reason: ~p, close_reason: ~p", [Reason, CloseReason]), sdlan_session:close(Session), ok. -spec code_change(OldVsn :: term(), StateName :: atom(), State :: #state{}, Extra :: term()) -> {ok, atom(), #state{}}. code_change(_OldVsn, StateName, State = #state{}, _Extra) -> {ok, StateName, State}. %%%=================================================================== %%% Internal functions %%%=================================================================== ssl_send(_Transport, _Socket, []) -> ok; ssl_send(Transport, Socket, Data) -> Len = iolist_size(Data), true = Len =< 65535, case Transport:send(Socket, Data) of ok -> incr_counter(ssl_frames_sent, 1), incr_counter(ssl_bytes_sent, Len + 2), ok; {error, Reason} -> exit({ssl_send_failed, Reason}) end. expected_stop(Reason, State) -> logger:notice("[sdlan_ssl_transport] expected close: ~p", [Reason]), {stop, normal, State#state{close_reason = Reason}}. debug_info(StateName, #state{session = Session, heartbeat_sec = HeartbeatSec}) -> ProcInfo = maps:from_list(process_info(self(), [message_queue_len, memory, reductions])), SessionInfo = sdlan_session:debug_info(Session), maps:merge(SessionInfo, ProcInfo#{ state => StateName, session => SessionInfo, frames_sent => get_counter(ssl_frames_sent), bytes_sent => get_counter(ssl_bytes_sent), heartbeat_sec => HeartbeatSec }). incr_counter(Key, Inc) -> erlang:put(Key, get_counter(Key) + Inc). get_counter(Key) -> case erlang:get(Key) of undefined -> 0; Value when is_integer(Value) -> Value end.