-module(claws_xmpp). -behaviour(gen_statem). -behaviour(claws). -include_lib("fast_xml/include/fxml.hrl"). -include("snatch.hrl"). -record(data, { user :: binary(), domain :: binary(), password :: binary(), resource :: binary(), host :: inet:socket_address(), port :: inet:port_number(), socket :: gen_tcp:socket(), stream }). -export([start_link/1, connect/0, disconnect/0]). -export([init/1, callback_mode/0, handle_event/4, code_change/4, terminate/3]). -export([disconnected/3, retrying/3, connected/3, stream_init/3, authenticate/3, bind/3, binding/3, binded/3]). -export([send/2, send/3]). -define(INIT(D), <<"" "">>). -define(AUTH(U, P, R), <<"" "" "", U/binary, "" "", P/binary, "" "", R/binary, "" "">>). -define(AUTH_SASL(B64), << "", B64/binary, "">>). -define(BIND(R), <<"" "" "", R/binary, "">>). -define(SESSION, <<"" "">>). -define(PRESENCE, <<"">>). -define(SERVER, ?MODULE). start_link(Params) -> gen_statem:start_link({local, ?MODULE}, ?MODULE, Params, []). init(#{host := Host, port := Port, user := User, domain := Domain, password := Password, resource := Resource} = Cfg) -> Events = case maps:get(auto_connect, Cfg, false) of true -> [{next_event, cast, connect}]; false -> [] end, {ok, disconnected, #data{host = Host, port = Port, user = User, domain = Domain, password = Password, resource = Resource}, Events}. callback_mode() -> handle_event_function. %% API connect() -> gen_statem:cast(?SERVER, connect). disconnect() -> gen_statem:cast(?SERVER, disconnect). %% States disconnected(Type, connect, #data{host = Host, port = Port} = Data) when Type =:= cast orelse Type =:= state_timeout -> case gen_tcp:connect(Host, Port, [binary, {active, true}]) of {ok, NewSocket} -> {next_state, connected, Data#data{socket = NewSocket}, [{next_event, cast, init_stream}]}; Error -> error_logger:error_msg("Connecting Error [~p:~p]: ~p~n", [Host, Port, Error]), {next_state, retrying, Data, [{next_event, cast, connect}]} end; disconnected(cast, disconnect, _Data) -> {keep_state_and_data, []}. retrying(cast, connect, Data) -> {next_state, disconnected, Data, [{state_timeout, 3000, connect}]}. connected(cast, init_stream, #data{} = Data) -> Stream = fxml_stream:new(whereis(?SERVER)), {next_state, stream_init, Data#data{stream = Stream}, [{next_event, cast, init}]}. stream_init(cast, init, #data{domain = Domain, socket = Socket} = Data) -> gen_tcp:send(Socket, ?INIT(Domain)), {keep_state, Data, []}; stream_init(cast, {received, _Packet}, Data) -> {next_state, authenticate, Data, [{next_event, cast, auth_sasl}]}. authenticate(cast, auth, #data{user = User, password = Password, resource = Resource, socket = Socket} = Data) -> gen_tcp:send(Socket, ?AUTH(User, Password, Resource)), {keep_state, Data, []}; authenticate(cast, auth_sasl, #data{user = User, password = Password, socket = Socket} = Data) -> B64 = base64:encode(<<0, User/binary, 0, Password/binary>>), gen_tcp:send(Socket, ?AUTH_SASL(B64)), {keep_state, Data, []}; authenticate(cast, {received, _Packet}, Data) -> {next_state, bind, Data, [{next_event, cast, bind}]}. bind(cast, bind, #data{resource = Resource, socket = Socket, domain = Domain, stream = Stream} = Data) -> close_stream(Stream), NewStream = fxml_stream:new(whereis(?SERVER)), gen_tcp:send(Socket, ?INIT(Domain)), gen_tcp:send(Socket, ?BIND(Resource)), {keep_state, Data#data{stream = NewStream}, []}; bind(cast, {received, _Packet}, #data{socket = Socket} = Data) -> gen_tcp:send(Socket, ?SESSION), gen_tcp:send(Socket, ?PRESENCE), {next_state, binding, Data, []}. binding(cast, {received, _Packet}, Data) -> snatch:connected(?MODULE), {next_state, binded, Data, []}. binded(cast, {send, Packet}, #data{socket = Socket}) -> gen_tcp:send(Socket, Packet), {keep_state_and_data, []}; binded(cast, {received, #xmlel{} = Packet}, _Data) -> From = snatch_xml:get_attr(<<"from">>, Packet), To = snatch_xml:get_attr(<<"to">>, Packet), Via = #via{jid = From, exchange = To, claws = ?MODULE}, snatch:received(Packet, Via), {keep_state_and_data, []}; binded(cast, {received, Packet}, _Data) -> snatch:received(Packet), {keep_state_and_data, []}; binded(cast, _Unknown, _Data) -> {keep_state_and_data, []}. handle_event(info, {tcp, _Socket, Packet}, _State, #data{stream = Stream} = Data) -> NewStream = fxml_stream:parse(Stream, Packet), {keep_state, Data#data{stream = NewStream}, []}; handle_event(info, {tcp_closed, _Socket}, _State, #data{stream = Stream} = Data) -> snatch:disconnected(?MODULE), close_stream(Stream), {next_state, retrying, Data, [{next_event, cast, connect}]}; handle_event(info, {tcp_error, _Socket, Reason}, _State, #data{stream = Stream} = Data) -> snatch:disconnected(?MODULE), close_stream(Stream), error_logger:error_msg("tcp closed error: ~p~n", [Reason]), {next_state, retrying, Data, [{next_event, cast, connect}]}; handle_event(info, {'$gen_event', {xmlstreamstart, _Name, _Attribs}}, _State, _Data) -> {keep_state_and_data, []}; handle_event(info, {'$gen_event', {xmlstreamend, _Name}}, _State, #data{stream = Stream} = Data) -> snatch:disconnected(?MODULE), close_stream(Stream), {next_state, retrying, Data, [{next_event, cast, connect}]}; handle_event(info, {'$gen_event', {xmlstreamerror, Error}}, _State, #data{stream = Stream} = Data) -> error_logger:error_msg("Stream Error: ~p ~n", [Error]), snatch:disconnected(?MODULE), close_stream(Stream), {next_state, retrying, Data, [{next_event, cast, connect}]}; handle_event(info, {'$gen_event', {xmlstreamelement, Packet}}, _State, Data) -> {keep_state, Data,[{next_event, cast, {received, Packet}}]}; handle_event(Type, Content, State, Data) -> case erlang:function_exported(?MODULE, State, 3) of true -> ?MODULE:State(Type, Content, Data); _ -> error_logger:error_msg("Unknown Function: ~p~n", [State]) end. terminate(_Reason, _StateName, _StateData) -> ok. code_change(_OldVsn, State, Data, _Extra) -> {ok, State, Data}. send(Data, _JID) -> gen_statem:cast(?MODULE, {send, Data}). send(Data, _JID, _ID) -> gen_statem:cast(?MODULE, {send, Data}). close_stream(<<>>) -> ok; close_stream(Stream) -> fxml_stream:close(Stream).