sdlan/src/ssl/sdlan_ssl_transport.erl
2026-05-26 13:59:59 +08:00

206 lines
8.1 KiB
Erlang

%%%-------------------------------------------------------------------
%%% @author anlicheng
%%% @copyright (C) 2026, <COMPANY>
%%% @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.