Packages
smppex
0.0.2
3.3.0
3.2.4
3.2.3
3.2.2
3.2.1
3.2.0
retired
3.1.0
retired
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.4.0
2.3.3
2.3.2
2.3.1
2.3.0
2.2.9
2.2.8
2.2.7
2.2.6
2.2.5
2.2.4
2.2.3
2.2.2
2.2.1
2.2.0
2.1.0
2.0.1
2.0.0
1.0.1
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
SMPP 3.4 protocol and framework implemented in Elixir
Current section
Files
Jump to
Current section
Files
lib/smppex/esme.ex
defmodule SMPPEX.ESME do
alias SMPPEX.ClientPool
alias SMPPEX.Protocol.CommandNames
alias SMPPEX.ESME
alias SMPPEX.Pdu
alias SMPPEX.PduStorage
alias SMPPEX.Session
alias SMPPEX.SMPPTimers
use GenServer
require Logger
defstruct [
:client_pool,
:smpp_session,
:module,
:module_state,
:timers,
:pdus,
:response_limit,
:bound,
:sequence_number,
:time,
:timer_resolution,
:tick_timer_ref
]
@default_timeout 5000
@default_enquire_link_limit 30000
@default_enquire_link_resp_limit 30000
@default_inactivity_limit :infinity
@default_response_limit 60000
@default_timer_resolution 100
@default_call_timeout 5000
@type state :: term
@type request :: term
@callback init(args :: term) :: {:ok, state} | {:close, reason :: term}
@callback handle_pdu(Pdu.t, state) :: state
@callback handle_resp(Pdu.t, Pdu.t, state) :: state
@callback handle_resp_timeout(Pdu.t, state) :: state
@callback handle_send_pdu_result(Pdu.t, SMPPEX.SMPPHandler.send_pdu_result, state) :: state
@callback handle_stop(state) :: any
@callback handle_call(request, GenServer.from, state) :: {:reply, reply :: term, state} | {:noreply, state}
@callback handle_cast(request, state) :: state
@callback handle_info(request, state) :: state
defmacro __using__(_) do
quote location: :keep do
@behaviour SMPPEX.ESME
def init(args) do
{:ok, args}
end
def handle_pdu(_pdu, state), do: state
def handle_resp(_pdu, _original_pdu, state), do: state
def handle_resp_timeout(_pdu, state), do: state
def handle_send_pdu_result(_pdu, _result, state), do: state
def handle_stop(_state), do: nil
def handle_call(_request, _from, state), do: {:reply, :ok, state}
def handle_cast(_request, state), do: state
def handle_info(_request, state), do: state
defoverridable [
init: 1,
handle_pdu: 2,
handle_resp: 3,
handle_resp_timeout: 2,
handle_send_pdu_result: 3,
handle_stop: 1,
handle_call: 3,
handle_cast: 2,
handle_info: 2
]
end
end
# Public interface
def start_link(host, port, {module, args}, opts \\ []) do
transport = Keyword.get(opts, :transport, :tcp)
gen_server_opts = Keyword.get(opts, :gen_server_opts, [])
timeout = Keyword.get(opts, :timeout, @default_timeout)
esme_opts = Keyword.get(opts, :esme_opts, [])
GenServer.start_link(
__MODULE__,
[convert_host(host), port, {module, args}, ranch_transport(transport), timeout, esme_opts],
gen_server_opts
)
end
def send_pdu(esme, pdu) do
GenServer.cast(esme, {:send_pdu, pdu})
end
def reply(esme, pdu, reply_pdu) do
GenServer.cast(esme, {:reply, pdu, reply_pdu})
end
def stop(esme) do
GenServer.cast(esme, :stop)
end
def handle_pdu(esme, pdu) do
GenServer.call(esme, {:handle_pdu, pdu})
end
def handle_stop(esme) do
GenServer.call(esme, :handle_stop)
end
def handle_send_pdu_result(esme, pdu, send_pdu_result) do
GenServer.call(esme, {:handle_send_pdu_result, pdu, send_pdu_result})
end
def call(esme, request, timeout \\ @default_call_timeout) do
GenServer.call(esme, {:call, request}, timeout)
end
def cast(esme, request) do
GenServer.cast(esme, {:cast, request})
end
# GenServer callbacks
def init([host, port, mod_with_args, transport, timeout, esme_opts]) do
esme = self
handler = fn(ref, _socket, _transport, session) ->
Kernel.send esme, {ref, session}
{:ok, SMPPEX.ESME.Session.new(esme)}
end
case start_session(handler, host, port, transport, timeout) do
{:ok, pool, session} ->
init_esme(mod_with_args, pool, session, esme_opts)
{:error, reason} -> {:stop, reason}
end
end
def handle_call({:handle_pdu, pdu}, _from, st) do
case resp?(pdu) do
true -> do_handle_resp(pdu, st)
false -> do_handle_pdu(pdu, st)
end
end
def handle_call(:handle_stop, _from, st) do
do_handle_stop(st)
end
def handle_call({:handle_send_pdu_result, pdu, send_pdu_result}, _from, st) do
do_handle_send_pdu_result(pdu, send_pdu_result, st)
end
def handle_call({:call, request}, from, st) do
case st.module.handle_call(request, from, st.module_state) do
{:reply, reply, new_module_state} ->
new_st = %ESME{ st | module_state: new_module_state }
{:reply, reply, new_st}
{:noreply, new_module_state} ->
new_st = %ESME{ st | module_state: new_module_state }
{:noreply, new_st}
end
end
def handle_cast({:send_pdu, pdu}, st) do
new_st = do_send_pdu(pdu, st)
{:noreply, new_st}
end
def handle_cast({:reply, pdu, reply_pdu}, st) do
new_st = do_reply(pdu, reply_pdu, st)
{:noreply, new_st}
end
def handle_cast(:stop, st) do
Session.stop(st.smpp_session)
{:noreply, st}
end
def handle_cast({:cast, request}, st) do
new_module_state = st.module.handle_cast(request, st.module_state)
new_st = %ESME{ st | module_state: new_module_state }
{:noreply, new_st}
end
def handle_info({:timeout, _timer_ref, :emit_tick}, st) do
new_tick_timer_ref = :erlang.start_timer(st.timer_resolution, self, :emit_tick)
:erlang.cancel_timer(st.tick_timer_ref)
Kernel.send self, {:tick, :erlang.system_time(:milli_seconds)}
{:noreply, %ESME{ st | tick_timer_ref: new_tick_timer_ref }}
end
def handle_info({:tick, time}, st) do
do_handle_tick(time, st)
end
def handle_info(request, st) do
new_module_state = st.module.handle_info(request, st.module_state)
new_st = %ESME{ st | module_state: new_module_state }
{:noreply, new_st}
end
# Private functions
defp start_session(handler, host, port, transport, timeout) do
case transport.connect(host, port, [:binary, {:packet, 0}, {:active, :once}], timeout) do
{:ok, socket} ->
pool = ClientPool.start(handler, 2, transport, timeout)
ClientPool.start_session(pool, socket)
ref = ClientPool.ref(pool)
receive do
{^ref, session} ->
{:ok, pool, session}
after timeout ->
{:error, :session_init_timeout}
end
{:error, _} = err -> err
end
end
defp init_esme({module, args}, pool, session, esme_opts) do
case module.init(args) do
{:ok, state} ->
timer_resolution = Keyword.get(esme_opts, :timer_resolution, @default_timer_resolution)
timer_ref = :erlang.start_timer(timer_resolution, self, :emit_tick)
enquire_link_limit = Keyword.get(esme_opts, :enquire_link_limit, @default_enquire_link_limit)
enquire_link_resp_limit = Keyword.get(esme_opts, :enquire_link_resp_limit, @default_enquire_link_resp_limit)
inactivity_limit = Keyword.get(esme_opts, :inactivity_limit, @default_inactivity_limit)
time = :erlang.system_time(:milli_seconds)
timers = SMPPTimers.new(
time,
:infinity,
enquire_link_limit,
enquire_link_resp_limit,
inactivity_limit
)
{:ok, pdu_storage} = PduStorage.start_link
response_limit = Keyword.get(esme_opts, :response_limit, @default_response_limit)
{:ok, %ESME{
client_pool: pool,
smpp_session: session,
module: module,
module_state: state,
timers: timers,
pdus: pdu_storage,
response_limit: response_limit,
bound: false,
sequence_number: 0,
time: time,
timer_resolution: timer_resolution,
tick_timer_ref: timer_ref
}}
{:stop, _} = stop ->
ClientPool.stop(pool)
stop
end
end
defp ranch_transport(:tcp), do: :ranch_tcp
defp ranch_transport(:ssl), do: :ranch_ssl
defp resp?(pdu), do: Pdu.command_id(pdu) > 0x80000000
defp do_handle_pdu(pdu, st) do
new_module_state = st.module.handle_pdu(pdu, st.module_state)
new_timers = SMPPTimers.handle_peer_transaction(st.timers, st.time)
{:reply, :ok, %ESME{ st | module_state: new_module_state, timers: new_timers }}
end
defp do_handle_resp(pdu, st) do
sequence_number = Pdu.sequence_number(pdu)
new_timers = SMPPTimers.handle_peer_action(st.timers, st.time)
new_st = %ESME{ st | timers: new_timers }
case PduStorage.fetch(st.pdus, sequence_number) do
[] ->
Logger.info("esme #{inspect self}, resp for unknown pdu(sequence_number: #{sequence_number}), dropping")
{:reply, :ok, new_st}
[original_pdu] ->
do_handle_resp_for_pdu(pdu, original_pdu, new_st)
end
end
defp do_handle_resp_for_pdu(pdu, original_pdu, st) do
new_module_state = st.module.handle_resp(pdu, original_pdu, st.module_state)
new_st = %ESME{ st | module_state: new_module_state }
case bind_resp?(pdu) do
true -> do_handle_bind_resp(pdu, new_st)
false -> {:reply, :ok, new_st}
end
end
defp do_handle_bind_resp(pdu, st) do
case success_resp?(pdu) do
true ->
new_timers = SMPPTimers.handle_bind(st.timers, st.time)
new_st = %ESME{ st | timers: new_timers, bound: true }
{:reply, :ok, new_st}
false ->
Logger.info("esme #{inspect self}, bind failed with status #{Pdu.command_status(pdu)}, stopping")
Session.stop(st.smpp_session)
{:reply, :ok, st}
end
end
defp success_resp?(pdu) do
Pdu.command_status(pdu) == 0
end
defp bind_resp?(pdu) do
command_id = Pdu.command_id(pdu)
{:ok, command_name} = CommandNames.name_by_id(command_id)
command_name == :bind_receiver_resp or command_name == :bind_transmitter_resp or command_name == :bind_transceiver_resp
end
defp do_handle_stop(st) do
_ = st.module.handle_stop(st.module_state)
ClientPool.stop(st.client_pool)
{:stop, :normal, :ok, st}
end
defp do_handle_send_pdu_result(pdu, send_pdu_result, st) do
new_module_state = st.module.handle_send_pdu_result(pdu, send_pdu_result, st.module_state)
new_st = %ESME{ st | module_state: new_module_state }
{:reply, :ok, new_st}
end
defp do_handle_tick(time, st) do
expired_pdus = PduStorage.fetch_expired(st.pdus, time)
new_st = do_handle_expired_pdus(expired_pdus, st)
do_handle_timers(time, new_st)
end
defp do_handle_expired_pdus([], st), do: st
defp do_handle_expired_pdus([pdu | pdus], st) do
new_module_state = st.module.handle_resp_timeout(pdu, st.module_state)
new_st = %ESME{ st | module_state: new_module_state }
do_handle_expired_pdus(pdus, new_st)
end
defp do_handle_timers(time, st) do
case SMPPTimers.handle_tick(st.timers, time) do
{:ok, new_timers} ->
new_st = %ESME{ st | timers: new_timers, time: time }
{:noreply, new_st}
{:stop, reason} ->
Logger.info("esme #{inspect self}, being stopped by timers(#{reason})")
Session.stop(st.smpp_session)
{:noreply, st}
{:enquire_link, new_timers} ->
new_st = %ESME{ st | timers: new_timers, time: time }
do_send_enquire_link(new_st)
end
end
defp do_send_enquire_link(st) do
enquire_link = SMPPEX.Pdu.Factory.enquire_link
new_st = do_send_pdu(enquire_link, st)
{:noreply, new_st}
end
defp do_send_pdu(pdu, st) do
sequence_number = st.sequence_number + 1
new_pdu = %Pdu{ pdu | sequence_number: sequence_number}
true = PduStorage.store(st.pdus, new_pdu, st.time + st.response_limit)
Session.send_pdu(st.smpp_session, new_pdu)
new_st = %ESME{ st | sequence_number: sequence_number}
new_st
end
defp do_reply(pdu, reply_pdu, st) do
new_reply_pdu = %Pdu{ reply_pdu | sequence_number: pdu.sequence_number }
Session.send_pdu(st.smpp_session, new_reply_pdu)
st
end
defp convert_host(host) when is_binary(host), do: to_char_list(host)
defp convert_host(host), do: host
end