Parsing ejabberd packet with erlang - erlang

I am using ejabberd18.09 for IM application. the application has few features that needed to add extensions to the ejabberd(xmpp) messages.
I created a custom module on the offline_message_hook to capture the offline messages and send them to my own url for further processing .
the messages that are being sent to ejabberd have different cases depending on the type of message as following
if I am sending a photo the message will be as following
<message xmlns="jabber:client" xml:lang="en" to="someuserjid2" from="{someuserjid}" type="chat" id="mP8tO-8">
<mtype xmlns="urn:xmpp:mtype" value="media" />
<url xmlns="urn:xmpp:url" id="myId" mediaType="photo" link="myphotourl.com" />
<body>thumbnail string</body>
</message>
when sending a text
<message xmlns="jabber:client" xml:lang="en" to="someuserjid2" from="{someuserjid}" type="chat" id="mP8tO-8">
<mtype xmlns="urn:xmpp:mtype" value="text" />
<body>Hi John</body>
</message>
when sending a location
<message xmlns="jabber:client" xml:lang="en" to="someuserjid2" from="{someuserjid}" type="chat" id="mP8tO-8">
<mtype xmlns="urn:xmpp:mtype" value="location" />
<location xmlns="urn:xmpp:geo" lat="1.2" lng="2.2 " />
<body>location thumbnailstring</body>
</message>
I used a .erl code to read the body and the message ID as following
create_message(_From, _To, Packet) when (Packet#message.type == chat) and (Packet#message.body /= []) ->
Body = fxml:get_path_s(Packet, [{elem, list_to_binary("body")}, cdata]),
MessageId = fxml:get_tag_attr_s(list_to_binary("id"), Packet),
post_offline_message(_From, _To, Body, MessageId),
ok.
what I want is how (in erlang) can I read the value attribute of the mtype tag the create a switch statement on ( media, location , test ) values so that I can process each message separately ?

You can pass attr in the list of arguments to fxml:get_path_s to pick out the value of an attribute of a certain element:
case fxml:get_path_s(Packet, [{elem, <<"mtype">>}, {attr, <<"value">>}]) of
<<"media">> ->
%% handle media...
<<"location">> ->
%% handle location...
<<"">> ->
%% no mtype element, or missing value attribute!
%% let's handle that case as well
end
Another thought: do you actually need the <mtype> element? It looks like you could just check for the presence of a <location> or <url> element. You could do it like this:
case fxml:get_subtag(Packet, <<"location">>) of
false ->
%% No location. Try url
case fxml:get_subtag(Packet, <<"url">>) of
false ->
%% Neither location nor url
%% Handle this case somehow...
Url ->
%% Do something with Url...
end
Location ->
%% Do something with Location...
end
The code gets a bit messy, since we need to nest the case expressions, unlike the previous version where we just checked the value of a single expression. One thing you could do is writing a helper function that tries each element in turn:
find_one_of(Packet, []) ->
not_found;
find_one_of(Packet, [ElementName | Rest]) ->
case fxml:get_subtag(Packet, ElementName) of
false ->
find_one_of(Packet, Rest);
Element ->
{ElementName, Element}
end.
And then call it like:
case find_one_of(Packet, [<<"location">>, <<"url">>]) of
{<<"location">>, Location} ->
%% Do something with Location...
{<<"url">>, Url} ->
%% Do something with Url...
not_found ->
%% Neither location nor url
end

Related

How to retrieve archived messages in the reverse order with XMPPFramework?

I'm implementing retrieving XMPP chat history with XEP-0313 Message Archive Management using XMPPFramework for iOS. I receive messages page by page using XMPPFramework's class XMPPMessageArchiveManagement. I'm able to receive the pages in the reverse order providing the message before which the next page should be.
let resultSet = XMPPResultSet(max: 50, before: theIdOfTheOldestReceivedMessage ?? "")
self.xmppMAM?.retrieveMessageArchive(at: to,
withFields: [field],
with: resultSet)
But I also would like to receive the messages on each page in the reverse order from the last one to the first one. According to the documentation I should provide the element in the query:
A client wishing for a reversed page should include the <flip-page/> element in its query, like so:
<iq type='set' id='q29309'>
<query xmlns='urn:xmpp:mam:2'>
<x xmlns='jabber:x:data' type='submit'>
<field var='FORM_TYPE' type='hidden'><value>urn:xmpp:mam:2</value></field>
<field var='start'>\<value>2010-08-07T00:00:00Z\</value>\</field>
</x>
<set xmlns='http://jabber.org/protocol/rsm'>
<max>10</max>
<after>09af3-cc343-b409f\</after>
</set>
<flip-page/>
</query>
</iq>
The source: https://xmpp.org/extensions/xep-0313.html#query-paging-flip.
The problem is that I don't populate the query myself: XMPPFramework is responsible for that. The framework generates the final XML and I'm only able to provide fields and a result set. The provided fields will be inside the x element, the provided result set will be a separate element of the queue. But how to provide the <flip-page/> element to the queue with XMPPMessageArchiveManagement by XMPPFramework?

How to beautifully make some API functions available only to authorized users in Erlang?

I have functions in my API code, some of which should only return requested results only to those requests that have a correct token. If the request is not authorized, then return a generic 401 response.
I have created a helper function is_authorised() which returns true/false.
Now, in a function that I want to make accessible only to authorized users, I check the result of the is_authorised() function and return respective response.
Here is an example:
get_players(SessionID, _Env, _Input) ->
case is_authorized(_Env) of
true ->
Response = [json_header(), players:select_all()];
false ->
Response = get_unauthorized_response()
end,
mod_esi:deliver(SessionID, Response).
I wonder if it's possible to make this checking look more elegant, like Python decorators used in Flask.
#app.route('/user')
#required_roles('admin', 'user')
def user_page(self):
return "You've got permission to access this page."
I know I can't use custom functions in guards, so I think it's not possible to make it as a guard.
Please advise.
Not as elegant as Python decorators, but you can use higher order functions to make this much cleaner. Create a function that accepts SessionID, Env, Input, and a callback function that should be executed when the user is authorized, and call that from all the functions that need to do the authorization check.
(Untested) code:
authorized(SessionID, Env, Input, Fun) ->
Response = case is_authorized(Env) of
true ->
Fun();
false ->
get_unauthorized_response()
end,
mod_esi:deliver(SessionID, Response).
get_players(SessionID, Env, Input) ->
authorized(SessionID, Env, Input, fun() ->
[json_header(), players:select_all()]
end).
You can pass more arguments to authorized if you want to do more extensive checks. With a role_of/1 function that accepts Env and returns the role as an atom, you can allow access to certain users with something like this:
authorized(SessionID, Env, Input, Roles, Fun) ->
Response = case lists:member(role_of(Env), Roles) of
true ->
Fun();
false ->
get_unauthorized_response()
end,
mod_esi:deliver(SessionID, Response).
get_players(SessionID, Env, Input) ->
authorized(SessionID, Env, Input, [admin, user], fun() ->
[json_header(), players:select_all()]
end).
You might want to look into cowboy or webmachine as they'd give you a framework over a state machine to handle REST requests. Implement the 'forbidden/2' callback to indicate unauthorized access.
On a side note: a 401 status code indicates a failure of authentication at the http layer when doing one of the well known http auth mechanisms like basic. See also https://en.wikipedia.org/wiki/Basic_access_authentication
What you are dealing with is a authorization failure and the correct associated http status code for that case would be 403 (as the forbidden callback from above would return).

Ejabberd Custom IQ handler: disconnecting user after receiving custom IQ stanza. Ejabberd 15.07

I'm a beginner on Erlang-Ejabberd development and i want to create a module on top of Ejabberd 15.07. So i got some code as example in many posts and some tutorials to start developing my own module. Now i came out with this code to get started.
-module(mod_test)
-behaviour(gen_mod).
-ifndef(LAGER).
-define(LAGER, 1).
-endif.
-define(NS_SEEN, <<"jabber:iq:seen">>).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("jlib.hrl").
-export([start/2, stop/1, process_sm_iq/3]).
start(Host, Opt) ->
?INFO_MSG(" mod_test starting ", []),
IQDisc = gen_mod:get_opt(iqdisc, Opt, fun gen_iq_handler:check_type/1, one_queue),
gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_SEEN, ?MODULE, process_sm_iq, IQDisc),
ok.
stop(Host) ->
?INFO_MSG(" mod_test2 stopping ", []),
gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_SEEN),
ok.
process_sm_iq(_From, _To, #iq{type = get, xmlns = ?NS_SEEN} = IQ) ->
?INFO_MSG(" Processing IQ Get query:~n ~p ", [IQ]),
IQ#iq{type = result, sub_el = [{xmlelement, <<"value">>, [], [{xmlcdata, <<"Hello World of Testing. ">>}]}]}.
So here, using Strophe.js i send a custom IQ stanza with this function.
var iq = $iq( {type: 'get', id : 'id123', from : 'renato#localhost'}).c('query', {xmlns : 'jabber:iq:seen'});
connection.sendIQ(iq, onResult);
And it comes out with a Stanza like this.
<iq type="get" id="id123" from="renato#localhost" xmlns="jabber:client">
<query xmlns="jabber:iq:seen" >
</iq>
Then i get disconnected and get no result from the server as expected. This is what i got in the log file.
2016-05-11 17:47:28.965 [info]<0.486.0>#ejabberd_listener:accept:299 (#Port<0.3939>) Accepted connection 127.0.0.1:49120 -> 127.0.0.1:5280
2016-05-11 17:47:28.966 [info]<0.507.0>#ejabberd_http:init:157 started: {gen_tcp,#Port<0.3939>}
2016-05-11 17:47:29.511 [info]<0.509.0>#ejabberd_c2s:wait_for_sasl_response:932 ({socket_state,ejabberd_http_bind,{http_bind,<0.508.0>, {{127,0,0,1},49120}},ejabberd_http_bind}) Accepted authentication for renato by undefined from 127.0.0.1
2016-05-11 17:47:30.150 [info]<0.509.0>#ejabberd_c2s:wait_for_session:1120 ({socket_state,ejabberd_http_bind,{http_bind,<0.508.0>, {{127,0,0,1},49120}},ejabberd_http_bind}) Opened session for renato#localhost/32685205291462981649937971
2016-05-11 17:47:31.451 [info]<0.486.0>#ejabberd_listener:accept:299 (#Port<0.3948>) Accepted connection 127.0.0.1:49122 -> 127.0.0.1:5280
2016-05-11 17:47:31.452 [info]<0.511.0>#ejabberd_http:init:157 started: {gen_tcp,#Port<0.3948>}
2016-05-11 17:47:31.453 [info]<0.390.0>#mod_test2:process_sm_iq:45 Processing IQ Get query:
{iq,<<"id123">>,get,<<"jabber:iq:seen">>,<<>>,{xmlel,<<"query">>, [{<<"xmlns">>,<<"jabber:iq:seen">>},{<<"querytype">>,<<"seen">>}],[]}}
2016-05-11 17:49:01.556 [info]<0.508.0>#ejabberd_http_bind:handle_info:522 Session timeout. Closing the HTTP bind session: <<"1199a026fb06de7e7c728425587b09a0b9c81433">>
2016-05-11 17:49:01.556 [info]<0.509.0>#ejabberd_c2s:terminate:1842 ({socket_state,ejabberd_http_bind,{http_bind,<0.508.0>, {{127,0,0,1},49120}},ejabberd_http_bind}) Close session for renato#localhost/32685205291462981649937971
I'm running ejabberd 15.07. I installed the run package and compiled the module using these commands.
erlc -DNO_EXT_LIB -I lib/ejabberd-15.07/include -pz lib/ejabberd-15.07/lib mod_test.erl
After that, i copied the file to ebin/ directory then i started the ejabberd server and everything else worked fine. Someone can help me out and explain me what i have to, and show me if i have to change the code, configuration file or maybe use a newer version. I'm using this version for months. Thanks...

Ejabberd Single Request Sign On

I have a working Ejabberd server (version 2.1.9) and my client application running just fine, but I wish to modify the way the application's XMPP client connects to Ejabberd in order to reduce the number of requests/responses between them, because its for a mobile environment and I wish to reduce the initial connection time.
I've looked up the XMPP protocol specification (RFC 6120) and some protocol extensions (XEPs), namely XEP-0305 Quickstart, but the protocol itself doesn't specify single request sign in and the Quickstart extension although aims to reduce the number of requests isn't enough for the time reduction I'm looking for.
After searching and not finding any solution I've started to modify both client and server and wish to accomplish the following for now as a proof of concept:
//Client Request
<?xml version='1.0'?>
<stream:stream ... user='user' pass='pass'>
//Server Response
<?xml version='1.0'?>
<stream:stream ... success='1'>
I've managed to modify my client accordingly and the Ejabberd server, and it seems they connect successfully, but any request the client makes after establishing the session doesn't get a response by the server. I've used Wireshark to check the TCP connection client and server side: client side its open and the request is sent, and on the server side is also open and the request is received, but when I try to send the response it is not sent.
I've modified ONLY the file ejabberd_c2s.erl and the changes are the following:
//init function
...
%% changed the first state of the fsm to point to quickstart
%%          {ok, wait_for_stream, #state{socket = Socket1,
{ok, wait_for_quickstart, #state{socket = Socket1,
...
//wait_for_quickstart function
...
case resource_conflict_action(U, StateData#state.server, R) of
closenew ->
send_header(StateData, Server, "1.0", DefaultLang, "0"),
send_trailer(StateData),
{stop, normal, StateData};
{accept_resource, R2} ->
JID = jlib:make_jid(U, StateData#state.server, R2),
allow = acl:match_rule(Server,c2s,JID),  
case ejabberd_auth:check_password(U, Server, P) of
true ->
send_header(StateData, Server, "1.0", DefaultLang, "1"),
change_shaper(StateData, JID),
{Fs, Ts} = ejabberd_hooks:run_fold(
roster_get_subscription_lists,
StateData#state.server,
{[], []},
[U, StateData#state.server]),
LJID = jlib:jid_tolower(jlib:jid_remove_resource(JID)),
Fs1 = [LJID | Fs],
Ts1 = [LJID | Ts],
PrivList =
ejabberd_hooks:run_fold(
privacy_get_user_list,
StateData#state.server,
#userlist{},
[U, StateData#state.server]),
SID = {now(), self()},
Conn = get_conn_type(StateData),
Info = [{ip, StateData#state.ip},
{conn, Conn},
{auth_module, StateData#state.auth_module}],
ejabberd_sm:open_session(SID, U, StateData#state.server, R, Info),
NewStateData =
StateData#state{
user = U,
resource = R2,
jid = JID,
sid = SID,
conn = Conn,
auth_module = ejabberd_auth_internal,
authenticated = true,
pres_f = ?SETS:from_list(Fs1),
pres_t = ?SETS:from_list(Ts1),
privacy_list = PrivList},
fsm_next_state_pack(session_established,
NewStateData);
_ ->
%%auth fail
end
end.
Just to clarify: the initial client authentication request and server response are being transmitted just fine, subsequent requests are also being transmitted but there is no response to them.
I'm I overlooking something?
Thanks in advance
#Nuno-Freitas Indeed that was what was failing, thanks for your helpful insight.
I added the code:
R1 = xml:get_attr_s("r",Attrs),
R = case jlib:resourceprep(R1) of
error -> error;
"" ->
lists:concat([randoms:get_string() | tuple_to_list(now())]);
Resource -> Resource
end,
That made the server respond to my requests, but there was other thing amiss: the <presence/> tag was breaking on server because the #state.lang was not defined, so I had to define it in the wait_for_quickstart function and now I have a single sign in XMPP client server working proof of concept.

webmachine and redirect unauthenticated users

In my new project I want to use webmachine and mochiweb. First thing i want to do is authentication.
I edit "dispatch.conf" and make some resources, like:
{["auth"], my_res_auth, []}.
{["protected"], my_res_protected, []}.
{['*'], my_res_default, []}.
When some one access "protected" resource i want to redirect him to "auth" resource if he not logged in. "auth" resource contains web form with user name and password, it do all auth work.
I put such code inside of my_res_protected.erl:
is_authorized(ReqData, State) ->
case my_auth:is_authorized(ReqData) of
true -> {true, ReqData, State};
false ->
% here should be something to redirect user to "auth" resource
% currently i put such thing, which is incorrect:
{true, wrq:do_redirect(true, wrq:set_resp_header("location", "/auth", ReqData)), State}
% dont know what should i put instead of "true"
end.
i googled some example of how to do it, but dont like that i should have to put this functions in all resources, which requires auth.
Is there any way to do it?
i think i found right way, put this code into auth.hrl file and include it in my resources
is_authorized(ReqData, State) ->
case my_auth:is_authorized(ReqData) of
true -> {true, ReqData, State};
false ->
% there i got address, to which should i redirect
% this address is defined in dispatch.conf
% and walk trough my_res_protected:init/1 into State
case proplists:get_value(do_redirect, State, false) of
false ->
{{halt, 401}, wrq:set_resp_header(
"Content-type", "text/plain",
wrq:set_resp_body("NOT AUTHORIZED", ReqData)
), State};
Location ->
{{halt, 302}, wrq:set_resp_header("Location", Location, ReqData), State}
end
end.
In the case where you're not authorized and do_redirect is false, why not just return { false, ReqData, State } like webmachine expects for is_authorized(), instead of constructing the response yourself?

Resources