diff --git a/mod_irc/README.md b/mod_irc/README.md new file mode 100644 index 0000000..b960808 --- /dev/null +++ b/mod_irc/README.md @@ -0,0 +1,83 @@ +## mod_irc + +This module is an IRC transport that can be used to join channels on IRC +servers. + +End user information: + +- A XMPP client with ‘groupchat 1.0’ support or Multi-User Chat + support ([`XEP-0045`][72]) is + necessary to join IRC channels. + +- An IRC channel can be joined in nearly the same way as joining a + XMPP Multi-User Chat room. The difference is that the room name will + be ‘channel%`irc.example.org`’ in case `irc.example.org` is the IRC + server hosting ‘channel’. And of course the host should point to the + IRC transport instead of the Multi-User Chat service. + +- You can register your nickame by sending ‘IDENTIFY password’ to + `nickserver!irc.example.org@irc.jabberserver.org`. + +- Entering your password is possible by sending ‘LOGIN nick + password’ + to `nickserver!irc.example.org@irc.jabberserver.org`. + +- The IRC transport provides Ad-Hoc Commands + ([`XEP-0050`][73]) to join a + channel, and to set custom IRC username and encoding. + +- When using a popular XMPP server, it can occur that no connection + can be achieved with some IRC servers because they limit the number + of connections from one IP. + +Options: + +**`host: HostName`**: This option defines the Jabber ID of the service. If the `host` + option is not specified, the Jabber ID will be the hostname of the + virtual host with the prefix ‘`irc.`’. The keyword “@HOST@” is + replaced at start time with the real virtual host name. + +**`db_type: mnesia|sql|riak`**: Define the type of storage where the module will create the tables and store user information. The default is the storage defined by the global option `default_db`, or `mnesia` if omitted. If `sql` or `riak` value is defined, make sure you have defined the database, see [database](#database-and-ldap-configuration). + +**`access: AccessName`**: This option can be used to specify who may use the IRC transport + (default value: `all`). + +**`default_encoding: Encoding`**: Set the default IRC encoding. Default value: `iso8859-1` + +Examples: + +- In the first example, the IRC transport is available on (all) your + virtual host(s) with the prefix ‘`irc.`’. Furthermore, anyone is + able to use the transport. The default encoding is set to + “iso8859-15”. + + + modules: + ... + mod_irc: + access: all + default_encoding: "iso8859-15" + ... + +- In next example the IRC transport is available with JIDs with prefix + `irc-t.net`. Moreover, the transport is only accessible to two users + of `example.org`, and any user of `example.com`: + + + acl: + paying_customers: + user: + - "customer1": "example.org" + - "customer2": "example.org" + server: "example.com" + + access_rules: + irc_users: + - allow: paying_customers + + modules: + ... + mod_irc: + access: irc_users + host: "irc.example.net" + ... diff --git a/mod_irc/src/mod_irc.erl b/mod_irc/src/mod_irc.erl new file mode 100644 index 0000000..71712fa --- /dev/null +++ b/mod_irc/src/mod_irc.erl @@ -0,0 +1,1006 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_irc.erl +%%% Author : Alexey Shchepin +%%% Purpose : IRC transport +%%% Created : 15 Feb 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_irc). + +-author('alexey@process-one.net'). + +-behaviour(gen_server). + +-behaviour(gen_mod). + +%% API +-export([start/2, stop/1, reload/3, export/1, import/1, + import/3, closed_connection/3, get_connection_params/3, + data_to_binary/2, process_disco_info/1, process_disco_items/1, + process_register/1, process_vcard/1, process_command/1]). + +-export([init/1, handle_call/3, handle_cast/2, + handle_info/2, terminate/2, code_change/3, + mod_opt_type/1, mod_options/1, depends/2]). + +-include("logger.hrl"). +-include("xmpp.hrl"). +-include("mod_irc.hrl"). +-include("translate.hrl"). + +-define(DEFAULT_IRC_PORT, 6667). + +-define(POSSIBLE_ENCODINGS, + [<<"koi8-r">>, <<"iso8859-15">>, <<"iso8859-1">>, <<"iso8859-2">>, + <<"utf-8">>, <<"utf-8+latin-1">>]). + +-record(state, {hosts = [] :: [binary()], + server_host = <<"">> :: binary(), + access = all :: atom()}). + +-callback init(binary(), gen_mod:opts()) -> any(). +-callback import(binary(), #irc_custom{}) -> ok | pass. +-callback get_data(binary(), binary(), jid()) -> error | empty | irc_data(). +-callback set_data(binary(), binary(), jid(), irc_data()) -> {atomic, any()}. + +%%==================================================================== +%% gen_mod API +%%==================================================================== +start(Host, Opts) -> + start_supervisor(Host), + gen_mod:start_child(?MODULE, Host, Opts). + +stop(Host) -> + stop_supervisor(Host), + gen_mod:stop_child(?MODULE, Host). + +reload(Host, NewOpts, OldOpts) -> + Proc = gen_mod:get_module_proc(Host, ?MODULE), + gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + +depends(_Host, _Opts) -> + []. + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +init([Host, Opts]) -> + process_flag(trap_exit, true), + ejabberd:start_app(iconv), + MyHosts = gen_mod:get_opt_hosts(Host, Opts), + Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod:init(Host, Opts), + Access = gen_mod:get_opt(access, Opts), + catch ets:new(irc_connection, + [named_table, public, + {keypos, #irc_connection.jid_server_host}]), + lists:foreach( + fun(MyHost) -> + register_hooks(MyHost), + ejabberd_router:register_route(MyHost, Host) + end, MyHosts), + {ok, + #state{hosts = MyHosts, server_host = Host, + access = Access}}. + +%%-------------------------------------------------------------------- +%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +handle_cast({reload, ServerHost, NewOpts, OldOpts}, State) -> + NewHosts = gen_mod:get_opt_hosts(ServerHost, NewOpts), + OldHosts = gen_mod:get_opt_hosts(ServerHost, OldOpts), + NewMod = gen_mod:db_mod(ServerHost, NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(ServerHost, OldOpts, ?MODULE), + Access = gen_mod:get_opt(access, NewOpts), + if NewMod /= OldMod -> + NewMod:init(ServerHost, NewOpts); + true -> + ok + end, + lists:foreach( + fun(NewHost) -> + ejabberd_router:register_route(NewHost, ServerHost), + register_hooks(NewHost) + end, NewHosts -- OldHosts), + lists:foreach( + fun(OldHost) -> + ejabberd_router:unregister_route(OldHost), + unregister_hooks(OldHost) + end, OldHosts -- NewHosts), + Access = gen_mod:get_opt(access, NewOpts), + {noreply, State#state{hosts = NewHosts, access = Access}}; +handle_cast(Msg, State) -> + ?WARNING_MSG("unexpected cast: ~p", [Msg]), + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +handle_info({route, Packet}, + #state{server_host = ServerHost, access = Access} = + State) -> + To = xmpp:get_to(Packet), + Host = To#jid.lserver, + case catch do_route(Host, ServerHost, Access, Packet) of + {'EXIT', Reason} -> ?ERROR_MSG("~p", [Reason]); + _ -> ok + end, + {noreply, State}; +handle_info(_Info, State) -> {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +terminate(_Reason, #state{hosts = MyHosts}) -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:unregister_route(MyHost), + unregister_hooks(MyHost) + end, MyHosts). + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +code_change(_OldVsn, State, _Extra) -> {ok, State}. + +%%-------------------------------------------------------------------- +%%% Internal functions +%%-------------------------------------------------------------------- +register_hooks(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_REGISTER, + ?MODULE, process_register), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, + ?MODULE, process_vcard), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_COMMANDS, + ?MODULE, process_command). + +unregister_hooks(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_REGISTER), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_COMMANDS). + +start_supervisor(Host) -> + Proc = gen_mod:get_module_proc(Host, + ejabberd_mod_irc_sup), + ChildSpec = {Proc, + {ejabberd_tmp_sup, start_link, + [Proc, mod_irc_connection]}, + permanent, infinity, supervisor, [ejabberd_tmp_sup]}, + supervisor:start_child(ejabberd_gen_mod_sup, ChildSpec). + +stop_supervisor(Host) -> + Proc = gen_mod:get_module_proc(Host, + ejabberd_mod_irc_sup), + supervisor:terminate_child(ejabberd_gen_mod_sup, Proc), + supervisor:delete_child(ejabberd_gen_mod_sup, Proc). + +do_route(Host, ServerHost, Access, Packet) -> + #jid{luser = LUser, lresource = LResource} = xmpp:get_to(Packet), + From = xmpp:get_from(Packet), + case acl:match_rule(ServerHost, Access, From) of + allow -> + case Packet of + #iq{} when LUser == <<"">>, LResource == <<"">> -> + ejabberd_router:process_iq(Packet); + #iq{} when LUser == <<"">>, LResource /= <<"">> -> + Err = xmpp:err_service_unavailable(), + ejabberd_router:route_error(Packet, Err); + _ -> + sm_route(Host, ServerHost, Packet) + end; + deny -> + Lang = xmpp:get_lang(Packet), + Err = xmpp:err_forbidden(<<"Access denied by service policy">>, Lang), + ejabberd_router:route_error(Packet, Err) + end. + +process_disco_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_info(#iq{type = get, lang = Lang, to = To, + sub_els = [#disco_info{node = Node}]} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + Info = ejabberd_hooks:run_fold(disco_info, ServerHost, + [], [ServerHost, ?MODULE, <<"">>, <<"">>]), + case iq_disco(ServerHost, Node, Lang) of + undefined -> + xmpp:make_iq_result(IQ, #disco_info{}); + DiscoInfo -> + xmpp:make_iq_result(IQ, DiscoInfo#disco_info{xdata = Info}) + end. + +process_disco_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_items(#iq{type = get, lang = Lang, to = To, + sub_els = [#disco_items{node = Node}]} = IQ) -> + case Node of + <<"">> -> + xmpp:make_iq_result(IQ, #disco_items{}); + <<"join">> -> + xmpp:make_iq_result(IQ, #disco_items{}); + <<"register">> -> + xmpp:make_iq_result(IQ, #disco_items{}); + ?NS_COMMANDS -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + xmpp:make_iq_result( + IQ, #disco_items{node = Node, + items = command_items(ServerHost, Host, Lang)}); + _ -> + Txt = <<"Node not found">>, + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) + end. + +process_register(#iq{type = get, to = To, from = From, lang = Lang} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + case get_form(ServerHost, Host, From, Lang) of + {result, Res} -> + xmpp:make_iq_result(IQ, Res); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; +process_register(#iq{type = set, lang = Lang, to = To, from = From, + sub_els = [#register{xdata = #xdata{} = X}]} = IQ) -> + case X#xdata.type of + cancel -> + xmpp:make_iq_result(IQ, #register{}); + submit -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + case set_form(ServerHost, Host, From, Lang, X) of + {result, Res} -> + xmpp:make_iq_result(IQ, Res); + {error, Error} -> + xmpp:make_error(IQ, Error) + end; + _ -> + Txt = <<"Incorrect value of 'type' attribute">>, + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)) + end; +process_register(#iq{type = set, lang = Lang} = IQ) -> + Txt = <<"No data form found">>, + xmpp:make_error(IQ, xmpp:err_bad_request(Txt, Lang)). + +process_vcard(#iq{type = set, lang = Lang} = IQ) -> + Txt = <<"Value 'set' of 'type' attribute is not allowed">>, + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_vcard(#iq{type = get, lang = Lang} = IQ) -> + xmpp:make_iq_result(IQ, iq_get_vcard(Lang)). + +process_command(#iq{type = get, lang = Lang} = IQ) -> + Txt = <<"Value 'get' of 'type' attribute is not allowed">>, + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_command(#iq{type = set, lang = Lang, to = To, from = From, + sub_els = [#adhoc_command{node = Node} = Request]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + case lists:keyfind(Node, 1, commands(ServerHost)) of + {_, _, Function} -> + try Function(From, To, Request) of + ignore -> + ignore; + {error, Error} -> + xmpp:make_error(IQ, Error); + Command -> + xmpp:make_iq_result(IQ, Command) + catch E:R -> + ?ERROR_MSG("ad-hoc handler failed: ~p", + [{E, {R, erlang:get_stacktrace()}}]), + xmpp:make_error(IQ, xmpp:err_internal_server_error()) + end; + _ -> + Txt = <<"Node not found">>, + xmpp:make_error(IQ, xmpp:err_item_not_found(Txt, Lang)) + end. + +sm_route(Host, ServerHost, Packet) -> + From = xmpp:get_from(Packet), + #jid{user = ChanServ, resource = Resource} = xmpp:get_to(Packet), + case str:tokens(ChanServ, <<"%">>) of + [<<_, _/binary>> = Channel, <<_, _/binary>> = Server] -> + case ets:lookup(irc_connection, {From, Server, Host}) of + [] -> + ?DEBUG("open new connection~n", []), + {Username, Encoding, Port, Password} = + get_connection_params(Host, ServerHost, From, Server), + ConnectionUsername = case Packet of + %% If the user tries to join a + %% chatroom, the packet for sure + %% contains the desired username. + #presence{} -> Resource; + %% Otherwise, there is no firm + %% conclusion from the packet. + %% Better to use the configured + %% username (which defaults to the + %% username part of the JID). + _ -> Username + end, + Ident = extract_ident(Packet), + RemoteAddr = extract_ip_address(Packet), + RealName = get_realname(ServerHost), + WebircPassword = get_webirc_password(ServerHost), + {ok, Pid} = mod_irc_connection:start( + From, Host, ServerHost, Server, + ConnectionUsername, Encoding, Port, + Password, Ident, RemoteAddr, RealName, + WebircPassword, ?MODULE), + ets:insert(irc_connection, + #irc_connection{ + jid_server_host = {From, Server, Host}, + pid = Pid}), + mod_irc_connection:route_chan(Pid, Channel, Resource, Packet); + [R] -> + Pid = R#irc_connection.pid, + ?DEBUG("send to process ~p~n", [Pid]), + mod_irc_connection:route_chan(Pid, Channel, Resource, Packet) + end; + _ -> + Lang = xmpp:get_lang(Packet), + case str:tokens(ChanServ, <<"!">>) of + [<<_, _/binary>> = Nick, <<_, _/binary>> = Server] -> + case ets:lookup(irc_connection, {From, Server, Host}) of + [] -> + Txt = <<"IRC connection not found">>, + Err = xmpp:err_service_unavailable(Txt, Lang), + ejabberd_router:route_error(Packet, Err); + [R] -> + Pid = R#irc_connection.pid, + ?DEBUG("send to process ~p~n", [Pid]), + mod_irc_connection:route_nick(Pid, Nick, Packet) + end; + _ -> + Txt = <<"Failed to parse chanserv">>, + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Packet, Err) + end + end. + +closed_connection(Host, From, Server) -> + ets:delete(irc_connection, {From, Server, Host}). + +iq_disco(ServerHost, <<"">>, Lang) -> + Name = gen_mod:get_module_opt(ServerHost, ?MODULE, name), + #disco_info{ + identities = [#identity{category = <<"conference">>, + type = <<"irc">>, + name = translate:translate(Lang, Name)}], + features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, ?NS_MUC, + ?NS_REGISTER, ?NS_VCARD, ?NS_COMMANDS]}; +iq_disco(ServerHost, Node, Lang) -> + case lists:keyfind(Node, 1, commands(ServerHost)) of + {_, Name, _} -> + #disco_info{ + identities = [#identity{category = <<"automation">>, + type = <<"command-node">>, + name = translate:translate(Lang, Name)}], + features = [?NS_COMMANDS, ?NS_XDATA]}; + _ -> + undefined + end. + +iq_get_vcard(Lang) -> + #vcard_temp{fn = <<"ejabberd/mod_irc">>, + url = ejabberd_config:get_uri(), + desc = misc:get_descr(Lang, <<"ejabberd IRC module">>)}. + +command_items(ServerHost, Host, Lang) -> + lists:map(fun({Node, Name, _Function}) -> + #disco_item{jid = jid:make(Host), + node = Node, + name = translate:translate(Lang, Name)} + end, commands(ServerHost)). + +commands(ServerHost) -> + [{<<"join">>, <<"Join channel">>, fun adhoc_join/3}, + {<<"register">>, + <<"Configure username, encoding, port and " + "password">>, + fun (From, To, Request) -> + adhoc_register(ServerHost, From, To, Request) + end}]. + +get_data(ServerHost, Host, From) -> + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:get_data(LServer, Host, From). + +get_form(ServerHost, Host, From, Lang) -> + #jid{user = User, server = Server} = From, + DefaultEncoding = get_default_encoding(Host), + Customs = case get_data(ServerHost, Host, From) of + error -> + Txt1 = <<"Database failure">>, + {error, xmpp:err_internal_server_error(Txt1, Lang)}; + empty -> {User, []}; + Data -> get_username_and_connection_params(Data) + end, + case Customs of + {error, _Error} -> + Customs; + {Username, ConnectionsParams} -> + Fs = [#xdata_field{type = 'text-single', + label = translate:translate(Lang, <<"IRC username">>), + var = <<"username">>, + values = [Username]}, + #xdata_field{type = fixed, + values = [str:format( + translate:translate( + Lang, + <<"If you want to specify" + " different ports, " + "passwords, encodings " + "for IRC servers, " + "fill this list with " + "values in format " + "'{\"irc server\", " + "\"encoding\", port, " + "\"password\"}'. " + "By default this " + "service use \"~s\" " + "encoding, port ~p, " + "empty password.">>), + [DefaultEncoding, ?DEFAULT_IRC_PORT])]}, + #xdata_field{type = fixed, + values = [translate:translate( + Lang, + <<"Example: [{\"irc.lucky.net\", \"koi8-r\", " + "6667, \"secret\"}, {\"vendetta.fef.net\", " + "\"iso8859-1\", 7000}, {\"irc.sometestserver.n" + "et\", \"utf-8\"}].">>)]}, + #xdata_field{type = 'text-multi', + label = translate:translate( + Lang, <<"Connections parameters">>), + var = <<"connections_params">>, + values = str:tokens(str:format( + "~p.", + [conn_params_to_list( + ConnectionsParams)]), + <<"\n">>)}], + X = #xdata{type = form, + title = <<(translate:translate( + Lang, <<"Registration in mod_irc for ">>))/binary, + User/binary, "@", Server/binary>>, + instructions = + [translate:translate( + Lang, + <<"Enter username, encodings, ports and " + "passwords you wish to use for connecting " + "to IRC servers">>)], + fields = Fs}, + {result, + #register{instructions = + translate:translate(Lang, + <<"You need an x:data capable client to " + "configure mod_irc settings">>), + xdata = X}} + end. + +set_data(ServerHost, Host, From, Data) -> + LServer = jid:nameprep(ServerHost), + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:set_data(LServer, Host, From, data_to_binary(From, Data)). + +set_form(ServerHost, Host, From, Lang, XData) -> + case {xmpp_util:get_xdata_values(<<"username">>, XData), + xmpp_util:get_xdata_values(<<"connections_params">>, XData)} of + {[Username], [_|_] = Strings} -> + EncString = lists:foldl(fun (S, Res) -> + <> + end, <<"">>, Strings), + case erl_scan:string(binary_to_list(EncString)) of + {ok, Tokens, _} -> + case erl_parse:parse_term(Tokens) of + {ok, ConnectionsParams} -> + case set_data(ServerHost, Host, From, + [{username, Username}, + {connections_params, ConnectionsParams}]) of + {atomic, _} -> + {result, undefined}; + _ -> + Txt = <<"Database failure">>, + {error, xmpp:err_internal_server_error(Txt, Lang)} + end; + _ -> + Txt = <<"Parse error">>, + {error, xmpp:err_not_acceptable(Txt, Lang)} + end; + _ -> + {error, xmpp:err_not_acceptable(<<"Scan error">>, Lang)} + end; + _ -> + Txt = <<"Incorrect value in data form">>, + {error, xmpp:err_not_acceptable(Txt, Lang)} + end. + +get_connection_params(Host, From, IRCServer) -> + [_ | HostTail] = str:tokens(Host, <<".">>), + ServerHost = str:join(HostTail, <<".">>), + get_connection_params(Host, ServerHost, From, + IRCServer). + +get_default_encoding(ServerHost) -> + Result = gen_mod:get_module_opt(ServerHost, ?MODULE, default_encoding), + ?INFO_MSG("The default_encoding configured for " + "host ~p is: ~p~n", + [ServerHost, Result]), + Result. + +get_realname(ServerHost) -> + gen_mod:get_module_opt(ServerHost, ?MODULE, realname). + +get_webirc_password(ServerHost) -> + gen_mod:get_module_opt(ServerHost, ?MODULE, webirc_password). + +get_connection_params(Host, ServerHost, From, + IRCServer) -> + #jid{user = User, server = _Server} = From, + DefaultEncoding = get_default_encoding(ServerHost), + case get_data(ServerHost, Host, From) of + error -> + {User, DefaultEncoding, ?DEFAULT_IRC_PORT, <<"">>}; + empty -> + {User, DefaultEncoding, ?DEFAULT_IRC_PORT, <<"">>}; + Data -> + {Username, ConnParams} = get_username_and_connection_params(Data), + {NewUsername, NewEncoding, NewPort, NewPassword} = case + lists:keysearch(IRCServer, + 1, + ConnParams) + of + {value, + {_, Encoding, + Port, + Password}} -> + {Username, + Encoding, + Port, + Password}; + {value, + {_, Encoding, + Port}} -> + {Username, + Encoding, + Port, + <<"">>}; + {value, + {_, + Encoding}} -> + {Username, + Encoding, + ?DEFAULT_IRC_PORT, + <<"">>}; + _ -> + {Username, + DefaultEncoding, + ?DEFAULT_IRC_PORT, + <<"">>} + end, + {iolist_to_binary(NewUsername), + iolist_to_binary(NewEncoding), + if NewPort >= 0 andalso NewPort =< 65535 -> NewPort; + true -> ?DEFAULT_IRC_PORT + end, + iolist_to_binary(NewPassword)} + end. + +adhoc_join(_From, _To, #adhoc_command{action = cancel} = Request) -> + xmpp_util:make_adhoc_response(Request, #adhoc_command{status = canceled}); +adhoc_join(_From, _To, #adhoc_command{lang = Lang, xdata = undefined} = Request) -> + X = #xdata{type = form, + title = translate:translate(Lang, <<"Join IRC channel">>), + fields = [#xdata_field{var = <<"channel">>, + type = 'text-single', + label = translate:translate( + Lang, <<"IRC channel (don't put the first #)">>), + required = true}, + #xdata_field{var = <<"server">>, + type = 'text-single', + label = translate:translate(Lang, <<"IRC server">>), + required = true}]}, + xmpp_util:make_adhoc_response( + Request, #adhoc_command{status = executing, xdata = X}); +adhoc_join(From, To, #adhoc_command{lang = Lang, xdata = X} = Request) -> + Channel = case xmpp_util:get_xdata_values(<<"channel">>, X) of + [C] -> C; + _ -> false + end, + Server = case xmpp_util:get_xdata_values(<<"server">>, X) of + [S] -> S; + _ -> false + end, + if Channel /= false, Server /= false -> + RoomJID = jid:make(<>, + To#jid.server), + Reason = translate:translate(Lang, <<"Join the IRC channel here.">>), + BodyTxt = {<<"Join the IRC channel in this Jabber ID: ~s">>, + [jid:encode(RoomJID)]}, + Invite = #message{ + from = RoomJID, to = From, + body = xmpp:mk_text(BodyTxt, Lang), + sub_els = [#muc_user{ + invites = [#muc_invite{from = From, + reason = Reason}]}, + #x_conference{reason = Reason, + jid = RoomJID}]}, + ejabberd_router:route(Invite), + xmpp_util:make_adhoc_response( + Request, #adhoc_command{status = completed}); + true -> + Txt = <<"Missing 'channel' or 'server' in the data form">>, + {error, xmpp:err_bad_request(Txt, Lang)} + end. + +-spec adhoc_register(binary(), jid(), jid(), adhoc_command()) -> + adhoc_command() | {error, stanza_error()}. +adhoc_register(_ServerHost, _From, _To, + #adhoc_command{action = cancel} = Request) -> + xmpp_util:make_adhoc_response(Request, #adhoc_command{status = canceled}); +adhoc_register(ServerHost, From, To, + #adhoc_command{lang = Lang, xdata = X, + action = Action} = Request) -> + #jid{user = User} = From, + #jid{lserver = Host} = To, + {Username, ConnectionsParams} = + if X == undefined -> + case get_data(ServerHost, Host, From) of + error -> {User, []}; + empty -> {User, []}; + Data -> get_username_and_connection_params(Data) + end; + true -> + {case xmpp_util:get_xdata_values(<<"username">>, X) of + [U] -> U; + _ -> User + end, parse_connections_params(X)} + end, + if Action == complete -> + case set_data(ServerHost, Host, From, + [{username, Username}, + {connections_params, ConnectionsParams}]) of + {atomic, _} -> + xmpp_util:make_adhoc_response( + Request, #adhoc_command{status = completed}); + _ -> + Txt = <<"Database failure">>, + {error, xmpp:err_internal_server_error(Txt, Lang)} + end; + true -> + Form = generate_adhoc_register_form(Lang, Username, + ConnectionsParams), + xmpp_util:make_adhoc_response( + Request, #adhoc_command{ + status = executing, + xdata = Form, + actions = #adhoc_actions{next = true, + complete = true}}) + end. + +generate_adhoc_register_form(Lang, Username, + ConnectionsParams) -> + #xdata{type = form, + title = translate:translate(Lang, <<"IRC settings">>), + instructions = [translate:translate( + Lang, + <<"Enter username and encodings you wish " + "to use for connecting to IRC servers. " + " Press 'Next' to get more fields to " + "fill in. Press 'Complete' to save settings.">>)], + fields = [#xdata_field{ + var = <<"username">>, + type = 'text-single', + label = translate:translate(Lang, <<"IRC username">>), + required = true, + values = [Username]} + | generate_connection_params_fields( + Lang, ConnectionsParams, 1, [])]}. + +generate_connection_params_fields(Lang, [], Number, + Acc) -> + Field = generate_connection_params_field(Lang, <<"">>, + <<"">>, -1, <<"">>, Number), + lists:reverse(Field ++ Acc); +generate_connection_params_fields(Lang, + [ConnectionParams | ConnectionsParams], + Number, Acc) -> + case ConnectionParams of + {Server, Encoding, Port, Password} -> + Field = generate_connection_params_field(Lang, Server, + Encoding, Port, Password, + Number), + generate_connection_params_fields(Lang, + ConnectionsParams, Number + 1, + Field ++ Acc); + {Server, Encoding, Port} -> + Field = generate_connection_params_field(Lang, Server, + Encoding, Port, <<"">>, Number), + generate_connection_params_fields(Lang, + ConnectionsParams, Number + 1, + Field ++ Acc); + {Server, Encoding} -> + Field = generate_connection_params_field(Lang, Server, + Encoding, -1, <<"">>, Number), + generate_connection_params_fields(Lang, + ConnectionsParams, Number + 1, + Field ++ Acc); + _ -> [] + end. + +generate_connection_params_field(Lang, Server, Encoding, + Port, Password, Number) -> + EncodingUsed = case Encoding of + <<>> -> get_default_encoding(Server); + _ -> Encoding + end, + PortUsedInt = if Port >= 0 andalso Port =< 65535 -> + Port; + true -> ?DEFAULT_IRC_PORT + end, + PortUsed = integer_to_binary(PortUsedInt), + PasswordUsed = case Password of + <<>> -> <<>>; + _ -> Password + end, + NumberString = integer_to_binary(Number), + [#xdata_field{var = <<"password", NumberString/binary>>, + type = 'text-single', + label = str:format( + translate:translate(Lang, <<"Password ~b">>), + [Number]), + values = [PasswordUsed]}, + #xdata_field{var = <<"port", NumberString/binary>>, + type = 'text-single', + label = str:format( + translate:translate(Lang, <<"Port ~b">>), + [Number]), + values = [PortUsed]}, + #xdata_field{var = <<"encoding", NumberString/binary>>, + type = 'list-single', + label = str:format( + translate:translate(Lang, <<"Encoding for server ~b">>), + [Number]), + values = [EncodingUsed], + options = [#xdata_option{label = E, value = E} + || E <- ?POSSIBLE_ENCODINGS]}, + #xdata_field{var = <<"server", NumberString/binary>>, + type = 'text-single', + label = str:format( + translate:translate(Lang, <<"Server ~b">>), + [Number]), + values = [Server]}]. + +parse_connections_params(#xdata{fields = Fields}) -> + Servers = lists:flatmap( + fun(#xdata_field{var = <<"server", Var/binary>>, + values = Values}) -> + [{Var, Values}]; + (_) -> + [] + end, Fields), + Encodings = lists:flatmap( + fun(#xdata_field{var = <<"encoding", Var/binary>>, + values = Values}) -> + [{Var, Values}]; + (_) -> + [] + end, Fields), + Ports = lists:flatmap( + fun(#xdata_field{var = <<"port", Var/binary>>, + values = Values}) -> + [{Var, Values}]; + (_) -> + [] + end, Fields), + Passwords = lists:flatmap( + fun(#xdata_field{var = <<"password", Var/binary>>, + values = Values}) -> + [{Var, Values}]; + (_) -> + [] + end, Fields), + parse_connections_params(Servers, Encodings, Ports, Passwords). + +retrieve_connections_params(ConnectionParams, + ServerN) -> + case ConnectionParams of + [{ConnectionParamN, ConnectionParam} + | ConnectionParamsTail] -> + if ServerN == ConnectionParamN -> + {ConnectionParam, ConnectionParamsTail}; + ServerN < ConnectionParamN -> + {[], + [{ConnectionParamN, ConnectionParam} + | ConnectionParamsTail]}; + ServerN > ConnectionParamN -> {[], ConnectionParamsTail} + end; + _ -> {[], []} + end. + +parse_connections_params([], _, _, _) -> []; +parse_connections_params(_, [], [], []) -> []; +parse_connections_params([{ServerN, Server} | Servers], + Encodings, Ports, Passwords) -> + {NewEncoding, NewEncodings} = + retrieve_connections_params(Encodings, ServerN), + {NewPort, NewPorts} = retrieve_connections_params(Ports, + ServerN), + {NewPassword, NewPasswords} = + retrieve_connections_params(Passwords, ServerN), + [{Server, NewEncoding, NewPort, NewPassword} + | parse_connections_params(Servers, NewEncodings, + NewPorts, NewPasswords)]. + +get_username_and_connection_params(Data) -> + Username = case lists:keysearch(username, 1, Data) of + {value, {_, U}} when is_binary(U) -> + U; + _ -> + <<"">> + end, + ConnParams = case lists:keysearch(connections_params, 1, Data) of + {value, {_, L}} when is_list(L) -> + L; + _ -> + [] + end, + {Username, ConnParams}. + +data_to_binary(JID, Data) -> + lists:map( + fun({username, U}) -> + {username, iolist_to_binary(U)}; + ({connections_params, Params}) -> + {connections_params, + lists:flatmap( + fun(Param) -> + try + [conn_param_to_binary(Param)] + catch _:_ -> + if JID /= error -> + ?ERROR_MSG("failed to convert " + "parameter ~p for user ~s", + [Param, + jid:encode(JID)]); + true -> + ?ERROR_MSG("failed to convert " + "parameter ~p", + [Param]) + end, + [] + end + end, Params)}; + (Opt) -> + Opt + end, Data). + +conn_param_to_binary({S}) -> + {iolist_to_binary(S)}; +conn_param_to_binary({S, E}) -> + {iolist_to_binary(S), iolist_to_binary(E)}; +conn_param_to_binary({S, E, Port}) when is_integer(Port) -> + {iolist_to_binary(S), iolist_to_binary(E), Port}; +conn_param_to_binary({S, E, Port, P}) when is_integer(Port) -> + {iolist_to_binary(S), iolist_to_binary(E), Port, iolist_to_binary(P)}. + +conn_params_to_list(Params) -> + lists:map( + fun({S}) -> + {binary_to_list(S)}; + ({S, E}) -> + {binary_to_list(S), binary_to_list(E)}; + ({S, E, Port}) -> + {binary_to_list(S), binary_to_list(E), Port}; + ({S, E, Port, P}) -> + {binary_to_list(S), binary_to_list(E), + Port, binary_to_list(P)} + end, Params). + +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). + +import(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:import(LServer). + +import(LServer, DBType, Data) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:import(LServer, Data). + +mod_opt_type(access) -> + fun acl:access_rules_validator/1; +mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; +mod_opt_type(default_encoding) -> + fun iolist_to_binary/1; +mod_opt_type(name) -> + fun iolist_to_binary/1; +mod_opt_type(host) -> fun iolist_to_binary/1; +mod_opt_type(hosts) -> + fun (L) -> lists:map(fun iolist_to_binary/1, L) end; +mod_opt_type(realname) -> + fun iolist_to_binary/1; +mod_opt_type(webirc_password) -> + fun iolist_to_binary/1. + +mod_options(Host) -> + [{access, all}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {default_encoding, <<"iso8859-15">>}, + {host, <<"irc.@HOST@">>}, + {hosts, []}, + {realname, <<"WebIRC-User">>}, + {webirc_password, <<"">>}, + {name, ?T("IRC Transport")}]. + +-spec extract_ident(stanza()) -> binary(). +extract_ident(Packet) -> + Hdrs = extract_headers(Packet), + proplists:get_value(<<"X-Irc-Ident">>, Hdrs, <<"chatmovil">>). + +-spec extract_ip_address(stanza()) -> binary(). +extract_ip_address(Packet) -> + Hdrs = extract_headers(Packet), + proplists:get_value(<<"X-Forwarded-For">>, Hdrs, <<"127.0.0.1">>). + +-spec extract_headers(stanza()) -> [{binary(), binary()}]. +extract_headers(Packet) -> + case xmpp:get_subtag(Packet, #shim{}) of + #shim{headers = Hs} -> Hs; + false -> [] + end. diff --git a/mod_irc/src/mod_irc_connection.erl b/mod_irc/src/mod_irc_connection.erl new file mode 100644 index 0000000..a2837f5 --- /dev/null +++ b/mod_irc/src/mod_irc_connection.erl @@ -0,0 +1,1205 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_irc_connection.erl +%%% Author : Alexey Shchepin +%%% Purpose : +%%% Created : 15 Feb 2003 by Alexey Shchepin +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_irc_connection). + +-author('alexey@process-one.net'). + +-behaviour(p1_fsm). + +%% External exports +-export([start_link/12, start/13, route_chan/4, + route_nick/3]). + +%% gen_fsm callbacks +-export([init/1, open_socket/2, wait_for_registration/2, + stream_established/2, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, + code_change/4]). + +-include("logger.hrl"). +-include("xmpp.hrl"). + +-define(SETS, gb_sets). + +-record(state, + {socket :: inet:socket() | undefined, + encoding = <<"">> :: binary(), + port = 0 :: inet:port_number(), + password = <<"">> :: binary(), + user = #jid{} :: jid(), + host = <<"">> :: binary(), + server = <<"">> :: binary(), + remoteAddr = <<"">> :: binary(), + ident = <<"">> :: binary(), + realname = <<"">> :: binary(), + nick = <<"">> :: binary(), + channels = dict:new() :: dict:dict(), + nickchannel :: binary() | undefined, + webirc_password :: binary(), + mod = mod_irc :: atom(), + inbuf = <<"">> :: binary(), + outbuf = <<"">> :: binary()}). + +-type state() :: #state{}. + +%-define(DBGFSM, true). + +-ifdef(DBGFSM). + +-define(FSMOPTS, [{debug, [trace]}]). + +-else. + +-define(FSMOPTS, []). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +-endif. + +start(From, Host, ServerHost, Server, Username, + Encoding, Port, Password, Ident, RemoteAddr, RealName, WebircPassword, Mod) -> + Supervisor = gen_mod:get_module_proc(ServerHost, + ejabberd_mod_irc_sup), + supervisor:start_child(Supervisor, + [From, Host, Server, Username, Encoding, Port, + Password, Ident, RemoteAddr, RealName, WebircPassword, Mod]). + +start_link(From, Host, Server, Username, Encoding, Port, + Password, Ident, RemoteAddr, RealName, WebircPassword, Mod) -> + p1_fsm:start_link(?MODULE, + [From, Host, Server, Username, Encoding, Port, Password, + Ident, RemoteAddr, RealName, WebircPassword, Mod], + ?FSMOPTS). + +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_fsm +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, StateName, StateData} | +%% {ok, StateName, StateData, Timeout} | +%% ignore | +%% {stop, StopReason} +%%---------------------------------------------------------------------- +init([From, Host, Server, Username, Encoding, Port, + Password, Ident, RemoteAddr, RealName, WebircPassword, Mod]) -> + p1_fsm:send_event(self(), init), + {ok, open_socket, + #state{mod = Mod, + encoding = Encoding, port = Port, password = Password, + user = From, nick = Username, host = Host, + server = Server, ident = Ident, realname = RealName, + remoteAddr = RemoteAddr, webirc_password = WebircPassword }}. + +%%---------------------------------------------------------------------- +%% Func: StateName/2 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +open_socket(init, StateData) -> + Addr = StateData#state.server, + Port = StateData#state.port, + ?DEBUG("Connecting with IPv6 to ~s:~p", [Addr, Port]), + From = StateData#state.user, + #jid{user = JidUser, server = JidServer, resource = JidResource} = From, + UserIP = ejabberd_sm:get_user_ip(JidUser, JidServer, JidResource), + UserIPStr = inet_parse:ntoa(element(1, UserIP)), + Connect6 = gen_tcp:connect(binary_to_list(Addr), Port, + [inet6, binary, {packet, 0}]), + Connect = case Connect6 of + {error, _} -> + ?DEBUG("Connection with IPv6 to ~s:~p failed. " + "Now using IPv4.", + [Addr, Port]), + gen_tcp:connect(binary_to_list(Addr), Port, + [inet, binary, {packet, 0}]); + _ -> Connect6 + end, + case Connect of + {ok, Socket} -> + NewStateData = StateData#state{socket = Socket}, + send_text(NewStateData, + io_lib:format("WEBIRC ~s ~s ~s ~s\r\n", [StateData#state.webirc_password, JidResource, UserIPStr, UserIPStr])), + if StateData#state.password /= <<"">> -> + send_text(NewStateData, + io_lib:format("PASS ~s\r\n", + [StateData#state.password])); + true -> true + end, + send_text(NewStateData, + io_lib:format("NICK ~s\r\n", [StateData#state.nick])), + send_text(NewStateData, + io_lib:format("USER ~s ~s ~s :~s\r\n", + [StateData#state.ident, + StateData#state.nick, + StateData#state.host, + StateData#state.realname])), + {next_state, wait_for_registration, NewStateData}; + {error, Reason} -> + ?DEBUG("connect return ~p~n", [Reason]), + Text = case Reason of + timeout -> <<"Server Connect Timeout">>; + _ -> <<"Server Connect Failed">> + end, + bounce_messages(Text), + {stop, normal, StateData} + end. + +wait_for_registration(closed, StateData) -> + {stop, normal, StateData}. + +stream_established({xmlstreamend, _Name}, StateData) -> + {stop, normal, StateData}; +stream_established(timeout, StateData) -> + {stop, normal, StateData}; +stream_established(closed, StateData) -> + {stop, normal, StateData}. + +%%---------------------------------------------------------------------- +%% Func: StateName/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {reply, Reply, NextStateName, NextStateData} | +%% {reply, Reply, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} | +%% {stop, Reason, Reply, NewStateData} +%%---------------------------------------------------------------------- +%state_name(Event, From, StateData) -> +% Reply = ok, +% {reply, Reply, state_name, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_event/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_event(_Event, StateName, StateData) -> + {next_state, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: handle_sync_event/4 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {reply, Reply, NextStateName, NextStateData} | +%% {reply, Reply, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} | +%% {stop, Reason, Reply, NewStateData} +%%---------------------------------------------------------------------- +handle_sync_event(_Event, _From, StateName, + StateData) -> + Reply = ok, {reply, Reply, StateName, StateData}. + +code_change(_OldVsn, StateName, StateData, _Extra) -> + {ok, StateName, StateData}. + +-define(SEND(S), + if StateName == stream_established -> + send_text(StateData, S), StateData; + true -> + StateData#state{outbuf = <<(StateData#state.outbuf)/binary, + (iolist_to_binary(S))/binary>>} + end). + +-spec get_password_from_presence(presence()) -> {true, binary()} | false. +get_password_from_presence(#presence{} = Pres) -> + case xmpp:get_subtag(Pres, #muc{}) of + #muc{password = Password} -> + {true, Password}; + _ -> + false + end. + +%%---------------------------------------------------------------------- +%% Func: handle_info/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_info({route_chan, _, _, #presence{type = error}}, _, StateData) -> + {stop, normal, StateData}; +handle_info({route_chan, Channel, _, #presence{type = unavailable}}, + StateName, StateData) -> + send_stanza_unavailable(Channel, StateData), + S1 = (?SEND((io_lib:format("PART #~s\r\n", [Channel])))), + S2 = S1#state{channels = dict:erase(Channel, S1#state.channels)}, + {next_state, StateName, S2}; +handle_info({route_chan, Channel, Resource, + #presence{type = available} = Presence}, + StateName, StateData) -> + Nick = case Resource of + <<"">> -> StateData#state.nick; + _ -> Resource + end, + S1 = if Nick /= StateData#state.nick -> + S11 = (?SEND((io_lib:format("NICK ~s\r\n", [Nick])))), + S11#state{nickchannel = Channel}; + true -> StateData + end, + {next_state, StateName, + case dict:is_key(Channel, S1#state.channels) of + true -> S1; + _ -> + case get_password_from_presence(Presence) of + {true, Password} -> + S2 = ?SEND((io_lib:format("JOIN #~s ~s\r\n", + [Channel, Password]))); + _ -> + S2 = ?SEND((io_lib:format("JOIN #~s\r\n", [Channel]))) + end, + S2#state{channels = dict:store(Channel, ?SETS:new(), + S1#state.channels)} + end}; +handle_info({route_chan, Channel, _Resource, #message{type = groupchat} = Msg}, + StateName, StateData) -> + {next_state, StateName, + case xmpp:get_text(Msg#message.subject) of + <<"">> -> + ejabberd_router:route( + xmpp:set_from_to( + Msg, + jid:make( + iolist_to_binary([Channel, <<"%">>, StateData#state.server]), + StateData#state.host, + StateData#state.nick), + StateData#state.user)), + Body = xmpp:get_text(Msg#message.body), + case Body of + <<"/quote ", Rest/binary>> -> + ?SEND(<>); + <<"/msg ", Rest/binary>> -> + ?SEND(<<"PRIVMSG ", Rest/binary, "\r\n">>); + <<"/me ", Rest/binary>> -> + Strings = str:tokens(Rest, <<"\n">>), + Res = iolist_to_binary( + lists:map( + fun (S) -> + io_lib:format( + "PRIVMSG #~s :\001ACTION ~s\001\r\n", + [Channel, S]) + end, + Strings)), + ?SEND(Res); + <<"/ctcp ", Rest/binary>> -> + Words = str:tokens(Rest, <<" ">>), + case Words of + [CtcpDest | _] -> + CtcpCmd = str:to_upper( + str:substr( + Rest, str:str(Rest, <<" ">>) + 1)), + Res = io_lib:format("PRIVMSG ~s :\001~s\001\r\n", + [CtcpDest, CtcpCmd]), + ?SEND(Res); + _ -> ok + end; + _ -> + Strings = str:tokens(Body, <<"\n">>), + Res = iolist_to_binary( + lists:map( + fun (S) -> + io_lib:format("PRIVMSG #~s :~s\r\n", + [Channel, S]) + end, Strings)), + ?SEND(Res) + end; + Subject -> + Strings = str:tokens(Subject, <<"\n">>), + Res = iolist_to_binary( + lists:map( + fun (S) -> + io_lib:format("TOPIC #~s :~s\r\n", + [Channel, S]) + end, + Strings)), + ?SEND(Res) + end}; +handle_info({route_chan, _Channel, Resource, #message{type = Type} = Msg}, + StateName, StateData) when Type == chat; Type == normal -> + Body = xmpp:get_text(Msg#message.body), + {next_state, StateName, + case Body of + <<"/quote ", Rest/binary>> -> + ?SEND(<>); + <<"/msg ", Rest/binary>> -> + ?SEND(<<"PRIVMSG ", Rest/binary, "\r\n">>); + <<"/me ", Rest/binary>> -> + Strings = str:tokens(Rest, <<"\n">>), + Res = iolist_to_binary( + lists:map( + fun (S) -> + io_lib:format( + "PRIVMSG ~s :\001ACTION ~s\001\r\n", + [Resource, S]) + end, Strings)), + ?SEND(Res); + <<"/ctcp ", Rest/binary>> -> + Words = str:tokens(Rest, <<" ">>), + case Words of + [CtcpDest | _] -> + CtcpCmd = str:to_upper( + str:substr( + Rest, str:str(Rest, <<" ">>) + 1)), + Res = io_lib:format("PRIVMSG ~s :~s\r\n", + [CtcpDest, + <<"\001", CtcpCmd/binary, "\001">>]), + ?SEND(Res); + _ -> ok + end; + _ -> + Strings = str:tokens(Body, <<"\n">>), + Res = iolist_to_binary( + lists:map( + fun (S) -> + io_lib:format("PRIVMSG ~s :~s\r\n", + [Resource, S]) + end, Strings)), + ?SEND(Res) + end}; +handle_info({route_chan, _, _, #message{type = error}}, _, StateData) -> + {stop, normal, StateData}; +handle_info({route_chan, Channel, Resource, + #iq{type = T, sub_els = [_]} = Packet}, + StateName, StateData) when T == set; T == get -> + From = StateData#state.user, + To = jid:make(iolist_to_binary([Channel, <<"%">>, StateData#state.server]), + StateData#state.host, StateData#state.nick), + try xmpp:decode_els(Packet) of + #iq{sub_els = [SubEl]} = IQ -> + case xmpp:get_ns(SubEl) of + ?NS_MUC_ADMIN -> + iq_admin(StateData, Channel, From, To, IQ); + ?NS_VERSION -> + Res = io_lib:format("PRIVMSG ~s :\001VERSION\001\r\n", + [Resource]), + _ = (?SEND(Res)), + Err = xmpp:err_feature_not_implemented(), + ejabberd_router:route_error(Packet, Err); + ?NS_TIME -> + Res = io_lib:format("PRIVMSG ~s :\001TIME\001\r\n", + [Resource]), + _ = (?SEND(Res)), + Err = xmpp:err_feature_not_implemented(), + ejabberd_router:route_error(Packet, Err); + ?NS_VCARD -> + Res = io_lib:format("WHOIS ~s \r\n", [Resource]), + _ = (?SEND(Res)), + Err = xmpp:err_feature_not_implemented(), + ejabberd_router:route_error(Packet, Err); + _ -> + Err = xmpp:err_feature_not_implemented(), + ejabberd_router:route_error(Packet, Err) + end + catch _:{xmpp_codec, Why} -> + Err = xmpp:err_bad_request( + xmpp:io_format_error(Why), xmpp:get_lang(Packet)), + ejabberd_router:route_error(Packet, Err) + end, + {next_state, StateName, StateData}; +handle_info({route_chan, _Channel, _, #iq{} = IQ}, StateName, StateData) -> + Err = xmpp:err_feature_not_implemented(), + ejabberd_router:route_error(IQ, Err), + {next_state, StateName, StateData}; +handle_info({route_nick, Nick, #message{type = chat} = Msg}, + StateName, StateData) -> + Body = xmpp:get_text(Msg#message.body), + {next_state, StateName, + case Body of + <<"/quote ", Rest/binary>> -> + ?SEND(<>); + <<"/msg ", Rest/binary>> -> + ?SEND(<<"PRIVMSG ", Rest/binary, "\r\n">>); + <<"/me ", Rest/binary>> -> + Strings = str:tokens(Rest, <<"\n">>), + Res = iolist_to_binary( + lists:map( + fun (S) -> + io_lib:format( + "PRIVMSG ~s :\001ACTION ~s\001\r\n", + [Nick, S]) + end, + Strings)), + ?SEND(Res); + <<"/ctcp ", Rest/binary>> -> + Words = str:tokens(Rest, <<" ">>), + case Words of + [CtcpDest | _] -> + CtcpCmd = str:to_upper( + str:substr(Rest, + str:str(Rest, + <<" ">>) + + 1)), + Res = io_lib:format("PRIVMSG ~s :~s\r\n", + [CtcpDest, + <<"\001", + CtcpCmd/binary, + "\001">>]), + ?SEND(Res); + _ -> ok + end; + _ -> + Strings = str:tokens(Body, <<"\n">>), + Res = iolist_to_binary( + lists:map( + fun (S) -> + io_lib:format( + "PRIVMSG ~s :~s\r\n", + [Nick, S]) + end, + Strings)), + ?SEND(Res) + end}; +handle_info({route_nick, _, #message{type = error}}, _, StateData) -> + {stop, normal, StateData}; +handle_info({route_nick, _Nick, _Packet}, StateName, + StateData) -> + {next_state, StateName, StateData}; +handle_info({ircstring, + <<$P, $I, $N, $G, $\s, ID/binary>>}, + StateName, StateData) -> + send_text(StateData, <<"PONG ", ID/binary, "\r\n">>), + {next_state, StateName, StateData}; +handle_info({ircstring, <<$:, String/binary>>}, + wait_for_registration, StateData) -> + Words = str:tokens(String, <<" ">>), + {NewState, NewStateData} = case Words of + [_, <<"001">> | _] -> + send_text(StateData, + io_lib:format("CODEPAGE ~s\r\n", + [StateData#state.encoding])), + {stream_established, StateData}; + [_, <<"433">> | _] -> + {error, + {error, + error_nick_in_use(StateData, String), + StateData}}; + [_, <<$4, _, _>> | _] -> + {error, + {error, + error_unknown_num(StateData, String, + cancel), + StateData}}; + [_, <<$5, _, _>> | _] -> + {error, + {error, + error_unknown_num(StateData, String, + cancel), + StateData}}; + _ -> + ?DEBUG("unknown irc command '~s'~n", + [String]), + {wait_for_registration, StateData} + end, + if NewState == error -> {stop, normal, NewStateData}; + true -> {next_state, NewState, NewStateData} + end; +handle_info({ircstring, <<$:, String/binary>>}, + _StateName, StateData) -> + Words = str:tokens(String, <<" ">>), + NewStateData = case Words of + [_, <<"353">> | Items] -> + process_channel_list(StateData, Items); + [_, <<"332">>, _Nick, <<$#, Chan/binary>> | _] -> + process_channel_topic(StateData, Chan, String), + StateData; + [_, <<"333">>, _Nick, <<$#, Chan/binary>> | _] -> + process_channel_topic_who(StateData, Chan, String), + StateData; + [_, <<"318">>, _, Nick | _] -> + process_endofwhois(StateData, String, Nick), StateData; + [_, <<"311">>, _, Nick, Ident, Irchost | _] -> + process_whois311(StateData, String, Nick, Ident, + Irchost), + StateData; + [_, <<"312">>, _, Nick, Ircserver | _] -> + process_whois312(StateData, String, Nick, Ircserver), + StateData; + [_, <<"319">>, _, Nick | _] -> + process_whois319(StateData, String, Nick), StateData; + [_, <<"433">> | _] -> + process_nick_in_use(StateData, String); + % CODEPAGE isn't standard, so don't complain if it's not there. + [_, <<"421">>, _, <<"CODEPAGE">> | _] -> StateData; + [_, <<$4, _, _>> | _] -> + process_num_error(StateData, String); + [_, <<$5, _, _>> | _] -> + process_num_error(StateData, String); + [From, <<"PRIVMSG">>, <<$#, Chan/binary>> | _] -> + process_chanprivmsg(StateData, Chan, From, String), + StateData; + [From, <<"NOTICE">>, <<$#, Chan/binary>> | _] -> + process_channotice(StateData, Chan, From, String), + StateData; + [From, <<"PRIVMSG">>, Nick, <<":\001VERSION\001">> + | _] -> + process_version(StateData, Nick, From), StateData; + [From, <<"PRIVMSG">>, Nick, <<":\001USERINFO\001">> + | _] -> + process_userinfo(StateData, Nick, From), StateData; + [From, <<"PRIVMSG">>, Nick | _] -> + process_privmsg(StateData, Nick, From, String), + StateData; + [From, <<"NOTICE">>, Nick | _] -> + process_notice(StateData, Nick, From, String), + StateData; + [From, <<"TOPIC">>, <<$#, Chan/binary>> | _] -> + process_topic(StateData, Chan, From, String), + StateData; + [From, <<"PART">>, <<$#, Chan/binary>> | _] -> + process_part(StateData, Chan, From, String); + [From, <<"QUIT">> | _] -> + process_quit(StateData, From, String); + [From, <<"JOIN">>, Chan | _] -> + process_join(StateData, Chan, From, String); + [From, <<"MODE">>, <<$#, Chan/binary>>, <<"+o">>, Nick + | _] -> + process_mode_o(StateData, Chan, From, Nick, + admin, moderator), + StateData; + [From, <<"MODE">>, <<$#, Chan/binary>>, <<"-o">>, Nick + | _] -> + process_mode_o(StateData, Chan, From, Nick, + member, participant), + StateData; + [From, <<"KICK">>, <<$#, Chan/binary>>, Nick | _] -> + process_kick(StateData, Chan, From, Nick, String), + StateData; + [From, <<"NICK">>, Nick | _] -> + process_nick(StateData, From, Nick); + _ -> + ?DEBUG("unknown irc command '~s'~n", [String]), + StateData + end, + NewStateData1 = case StateData#state.outbuf of + <<"">> -> NewStateData; + Data -> + send_text(NewStateData, Data), + NewStateData#state{outbuf = <<"">>} + end, + {next_state, stream_established, NewStateData1}; +handle_info({ircstring, + <<$E, $R, $R, $O, $R, _/binary>> = String}, + StateName, StateData) -> + process_error(StateData, String), + {next_state, StateName, StateData}; +handle_info({ircstring, String}, StateName, + StateData) -> + ?DEBUG("unknown irc command '~s'~n", [String]), + {next_state, StateName, StateData}; +handle_info({send_text, Text}, StateName, StateData) -> + send_text(StateData, Text), + {next_state, StateName, StateData}; +handle_info({tcp, _Socket, Data}, StateName, + StateData) -> + Buf = <<(StateData#state.inbuf)/binary, Data/binary>>, + Strings = ejabberd_regexp:split(<< <> + || <> <= Buf, C /= $\r >>, + <<"\n">>), + ?DEBUG("strings=~p~n", [Strings]), + NewBuf = process_lines(StateData#state.encoding, + Strings), + {next_state, StateName, + StateData#state{inbuf = NewBuf}}; +handle_info({tcp_closed, _Socket}, StateName, + StateData) -> + p1_fsm:send_event(self(), closed), + {next_state, StateName, StateData}; +handle_info({tcp_error, _Socket, _Reason}, StateName, + StateData) -> + p1_fsm:send_event(self(), closed), + {next_state, StateName, StateData}. + +%%---------------------------------------------------------------------- +%% Func: terminate/3 +%% Purpose: Shutdown the fsm +%% Returns: any +%%---------------------------------------------------------------------- +terminate(_Reason, _StateName, FullStateData) -> + {Error, StateData} = case FullStateData of + {error, SError, SStateData} -> {SError, SStateData}; + _ -> + {xmpp:err_internal_server_error( + <<"Server Connect Failed">>, ejabberd_config:get_mylang()), + FullStateData} + end, + (StateData#state.mod):closed_connection(StateData#state.host, + StateData#state.user, + StateData#state.server), + bounce_messages(<<"Server Connect Failed">>), + lists:foreach(fun (Chan) -> + Stanza = xmpp:make_error(#presence{}, Error), + send_stanza(Chan, StateData, Stanza) + end, + dict:fetch_keys(StateData#state.channels)), + case StateData#state.socket of + undefined -> ok; + Socket -> gen_tcp:close(Socket) + end, + ok. + +-spec send_stanza(binary(), state(), stanza()) -> ok. +send_stanza(Chan, StateData, Stanza) -> + ejabberd_router:route( + xmpp:set_from_to( + Stanza, + jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, + StateData#state.nick), + StateData#state.user)). + +-spec send_stanza_unavailable(binary(), state()) -> ok. +send_stanza_unavailable(Chan, StateData) -> + Affiliation = member, + Role = none, + Stanza = #presence{ + type = unavailable, + sub_els = [#muc_user{ + items = [#muc_item{affiliation = Affiliation, + role = Role}], + status_codes = [110]}]}, + send_stanza(Chan, StateData, Stanza). + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- + +send_text(#state{socket = Socket, encoding = Encoding}, + Text) -> + CText = iconv:convert(<<"utf-8">>, Encoding, iolist_to_binary(Text)), + gen_tcp:send(Socket, CText). + +bounce_messages(Reason) -> + receive + {send_element, El} -> + Lang = xmpp:get_lang(El), + Err = xmpp:err_internal_server_error(Reason, Lang), + ejabberd_router:route_error(El, Err), + bounce_messages(Reason) + after 0 -> ok + end. + +route_chan(Pid, Channel, Resource, Packet) -> + Pid ! {route_chan, Channel, Resource, Packet}. + +route_nick(Pid, Nick, Packet) -> + Pid ! {route_nick, Nick, Packet}. + +process_lines(_Encoding, [S]) -> S; +process_lines(Encoding, [S | Ss]) -> + self() ! + {ircstring, iconv:convert(Encoding, <<"utf-8">>, S)}, + process_lines(Encoding, Ss). + +process_channel_list(StateData, Items) -> + process_channel_list_find_chan(StateData, Items). + +process_channel_list_find_chan(StateData, []) -> + StateData; +process_channel_list_find_chan(StateData, + [<<$#, Chan/binary>> | Items]) -> + process_channel_list_users(StateData, Chan, Items); +process_channel_list_find_chan(StateData, + [_ | Items]) -> + process_channel_list_find_chan(StateData, Items). + +process_channel_list_users(StateData, _Chan, []) -> + StateData; +process_channel_list_users(StateData, Chan, + [User | Items]) -> + NewStateData = process_channel_list_user(StateData, + Chan, User), + process_channel_list_users(NewStateData, Chan, Items). + +process_channel_list_user(StateData, Chan, User) -> + User1 = case User of + <<$:, U1/binary>> -> U1; + _ -> User + end, + {User2, Affiliation, Role} = case User1 of + <<$@, U2/binary>> -> + {U2, admin, moderator}; + <<$+, U2/binary>> -> + {U2, member, participant}; + <<$%, U2/binary>> -> + {U2, admin, moderator}; + <<$&, U2/binary>> -> + {U2, admin, moderator}; + <<$~, U2/binary>> -> + {U2, admin, moderator}; + _ -> {User1, member, participant} + end, + ejabberd_router:route( + #presence{ + from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, User2), + to = StateData#state.user, + sub_els = [#muc_user{items = [#muc_item{affiliation = Affiliation, + role = Role}]}]}), + case catch dict:update(Chan, + fun (Ps) -> (?SETS):add_element(User2, Ps) end, + StateData#state.channels) of + {'EXIT', _} -> StateData; + NS -> StateData#state{channels = NS} + end. + +process_channel_topic(StateData, Chan, String) -> + Msg = ejabberd_regexp:replace(String, <<".*332[^:]*:">>, + <<"">>), + Subject = filter_message(Msg), + Body = <<"Topic for #", Chan/binary, ": ", Subject/binary>>, + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host), + to = StateData#state.user, + type = groupchat, + subject = xmpp:mk_text(Subject), + body = xmpp:mk_text(Body)}). + +process_channel_topic_who(StateData, Chan, String) -> + Words = str:tokens(String, <<" ">>), + Msg1 = case Words of + [_, <<"333">>, _, _Chan, Whoset, Timeset] -> + {Unixtimeset, _Rest} = str:to_integer(Timeset), + <<"Topic for #", Chan/binary, " set by ", Whoset/binary, + " at ", (unixtime2string(Unixtimeset))/binary>>; + [_, <<"333">>, _, _Chan, Whoset | _] -> + <<"Topic for #", Chan/binary, " set by ", + Whoset/binary>>; + _ -> String + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, <<"">>), + to = StateData#state.user, + type = groupchat, body = xmpp:mk_text(Msg2)}). + +error_nick_in_use(_StateData, String) -> + Msg = ejabberd_regexp:replace(String, + <<".*433 +[^ ]* +">>, <<"">>), + Msg1 = filter_message(Msg), + xmpp:err_conflict(Msg1, ejabberd_config:get_mylang()). + +process_nick_in_use(StateData, String) -> + Error = error_nick_in_use(StateData, String), + case StateData#state.nickchannel of + undefined -> + % Shouldn't happen with a well behaved server + StateData; + Chan -> + ejabberd_router:route( + #presence{from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, + StateData#state.nick), + to = StateData#state.user, + type = error, sub_els = [Error]}), + StateData#state{nickchannel = undefined} + end. + +process_num_error(StateData, String) -> + Error = error_unknown_num(StateData, String, continue), + lists:foreach( + fun(Chan) -> + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Chan, $%, StateData#state.server]), + StateData#state.host, + StateData#state.nick), + to = StateData#state.user, + type = error, sub_els = [Error]}) + end, dict:fetch_keys(StateData#state.channels)), + StateData. + +process_endofwhois(StateData, _String, Nick) -> + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Nick, <<"!">>, StateData#state.server]), + StateData#state.host), + to = StateData#state.user, + type = chat, body = xmpp:mk_text(<<"End of WHOIS">>)}). + +process_whois311(StateData, String, Nick, Ident, + Irchost) -> + Fullname = ejabberd_regexp:replace(String, + <<".*311[^:]*:">>, <<"">>), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Nick, <<"!">>, StateData#state.server]), + StateData#state.host, <<"">>), + to = StateData#state.user, + type = chat, + body = xmpp:mk_text( + iolist_to_binary( + [<<"WHOIS: ">>, Nick, <<" is ">>, Ident, + <<"@">>, Irchost, <<" : ">>, Fullname]))}). + +process_whois312(StateData, String, Nick, Ircserver) -> + Ircserverdesc = ejabberd_regexp:replace(String, + <<".*312[^:]*:">>, <<"">>), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Nick, <<"!">>, StateData#state.server]), + StateData#state.host, <<"">>), + to = StateData#state.user, + type = chat, + body = xmpp:mk_text( + iolist_to_binary( + [<<"WHOIS: ">>, Nick, <<" use ">>, Ircserver, + <<" : ">>, Ircserverdesc]))}). + +process_whois319(StateData, String, Nick) -> + Chanlist = ejabberd_regexp:replace(String, + <<".*319[^:]*:">>, <<"">>), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Nick, <<"!">>, StateData#state.server]), + StateData#state.host, <<"">>), + to = StateData#state.user, + type = chat, + body = xmpp:mk_text( + iolist_to_binary( + [<<"WHOIS: ">>, Nick, <<" is on ">>, Chanlist]))}). + +process_chanprivmsg(StateData, Chan, From, String) -> + [FromUser | _] = str:tokens(From, <<"!">>), + Msg = ejabberd_regexp:replace(String, + <<".*PRIVMSG[^:]*:">>, <<"">>), + Msg1 = case Msg of + <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> + <<"/me ", Rest/binary>>; + _ -> Msg + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, FromUser), + to = StateData#state.user, + type = groupchat, body = xmpp:mk_text(Msg2)}). + +process_channotice(StateData, Chan, From, String) -> + [FromUser | _] = str:tokens(From, <<"!">>), + Msg = ejabberd_regexp:replace(String, + <<".*NOTICE[^:]*:">>, <<"">>), + Msg1 = case Msg of + <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> + <<"/me ", Rest/binary>>; + _ -> <<"/me NOTICE: ", Msg/binary>> + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, FromUser), + to = StateData#state.user, + type = groupchat, body = xmpp:mk_text(Msg2)}). + +process_privmsg(StateData, _Nick, From, String) -> + [FromUser | _] = str:tokens(From, <<"!">>), + Msg = ejabberd_regexp:replace(String, + <<".*PRIVMSG[^:]*:">>, <<"">>), + Msg1 = case Msg of + <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> + <<"/me ", Rest/binary>>; + _ -> Msg + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([FromUser, <<"!">>, StateData#state.server]), + StateData#state.host, <<"">>), + to = StateData#state.user, + type = chat, body = xmpp:mk_text(Msg2)}). + +process_notice(StateData, _Nick, From, String) -> + [FromUser | _] = str:tokens(From, <<"!">>), + Msg = ejabberd_regexp:replace(String, + <<".*NOTICE[^:]*:">>, <<"">>), + Msg1 = case Msg of + <<1, $A, $C, $T, $I, $O, $N, $\s, Rest/binary>> -> + <<"/me ", Rest/binary>>; + _ -> <<"/me NOTICE: ", Msg/binary>> + end, + Msg2 = filter_message(Msg1), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([FromUser, <<"!">>, StateData#state.server]), + StateData#state.host), + to = StateData#state.user, + type = chat, body = xmpp:mk_text(Msg2)}). + +process_version(StateData, _Nick, From) -> + [FromUser | _] = str:tokens(From, <<"!">>), + send_text(StateData, + io_lib:format("NOTICE ~s :\001VERSION ejabberd IRC " + "transport ~s (c) Alexey Shchepin\001\r\n", + [FromUser, ejabberd_config:get_version()]) + ++ + io_lib:format("NOTICE ~s :\001VERSION http://ejabberd.jabber" + "studio.org/\001\r\n", + [FromUser])). + +process_userinfo(StateData, _Nick, From) -> + [FromUser | _] = str:tokens(From, <<"!">>), + send_text(StateData, + io_lib:format("NOTICE ~s :\001USERINFO xmpp:~s\001\r\n", + [FromUser, + jid:encode(StateData#state.user)])). + +process_topic(StateData, Chan, From, String) -> + [FromUser | _] = str:tokens(From, <<"!">>), + Msg = ejabberd_regexp:replace(String, + <<".*TOPIC[^:]*:">>, <<"">>), + Msg1 = filter_message(Msg), + ejabberd_router:route( + #message{from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, FromUser), + to = StateData#state.user, + type = groupchat, + subject = xmpp:mk_text(Msg1), + body = xmpp:mk_text(<<"/me has changed the subject to: ", + Msg1/binary>>)}). + +process_part(StateData, Chan, From, String) -> + [FromUser | FromIdent] = str:tokens(From, <<"!">>), + Msg = ejabberd_regexp:replace(String, + <<".*PART[^:]*:">>, <<"">>), + Msg1 = filter_message(Msg), + ejabberd_router:route( + #presence{from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, FromUser), + to = StateData#state.user, + type = unavailable, + sub_els = [#muc_user{ + items = [#muc_item{affiliation = member, + role = none}]}], + status = xmpp:mk_text( + list_to_binary([Msg1, " (", FromIdent, ")"]))}), + case catch dict:update(Chan, + fun (Ps) -> remove_element(FromUser, Ps) end, + StateData#state.channels) + of + {'EXIT', _} -> StateData; + NS -> StateData#state{channels = NS} + end. + +process_quit(StateData, From, String) -> + [FromUser | FromIdent] = str:tokens(From, <<"!">>), + Msg = ejabberd_regexp:replace(String, + <<".*QUIT[^:]*:">>, <<"">>), + Msg1 = filter_message(Msg), + dict:map( + fun(Chan, Ps) -> + case (?SETS):is_member(FromUser, Ps) of + true -> + ejabberd_router:route( + #presence{from = jid:make(iolist_to_binary([Chan, $%, StateData#state.server]), + StateData#state.host, + FromUser), + to = StateData#state.user, + type = unavailable, + sub_els = [#muc_user{ + items = [#muc_item{ + affiliation = member, + role = none}]}], + status = xmpp:mk_text( + list_to_binary([Msg1, " (", FromIdent, ")"]))}), + remove_element(FromUser, Ps); + _ -> + Ps + end + end, StateData#state.channels), + StateData. + +process_join(StateData, Channel, From, _String) -> + [FromUser | FromIdent] = str:tokens(From, <<"!">>), + [Chan | _] = binary:split(Channel, <<":#">>), + ejabberd_router:route( + #presence{ + from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, FromUser), + to = StateData#state.user, + sub_els = [#muc_user{items = [#muc_item{affiliation = member, + role = participant}]}], + status = xmpp:mk_text(list_to_binary(FromIdent))}), + case catch dict:update(Chan, + fun (Ps) -> (?SETS):add_element(FromUser, Ps) end, + StateData#state.channels) + of + {'EXIT', _} -> StateData; + NS -> StateData#state{channels = NS} + end. + +process_mode_o(StateData, Chan, _From, Nick, + Affiliation, Role) -> + ejabberd_router:route( + #presence{ + from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, Nick), + to = StateData#state.user, + sub_els = [#muc_user{items = [#muc_item{affiliation = Affiliation, + role = Role}]}]}). + +process_kick(StateData, Chan, From, Nick, String) -> + Msg = lists:last(str:tokens(String, <<":">>)), + Msg2 = <>, + ejabberd_router:route( + #message{ + from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host), + to = StateData#state.user, + type = groupchat, body = xmpp:mk_text(Msg2)}), + ejabberd_router:route( + #presence{ + from = jid:make(iolist_to_binary([Chan, <<"%">>, StateData#state.server]), + StateData#state.host, Nick), + to = StateData#state.user, + type = unavailable, + sub_els = [#muc_user{items = [#muc_item{ + affiliation = none, + role = none}], + status_codes = [307]}]}). + +process_nick(StateData, From, NewNick) -> + [FromUser | _] = str:tokens(From, <<"!">>), + [Nick | _] = binary:split(NewNick, <<":">>), + NewChans = + dict:map( + fun(Chan, Ps) -> + case (?SETS):is_member(FromUser, Ps) of + true -> + ejabberd_router:route( + #presence{ + from = jid:make(iolist_to_binary([Chan, $%, StateData#state.server]), + StateData#state.host, + FromUser), + to = StateData#state.user, + type = unavailable, + sub_els = [#muc_user{ + items = [#muc_item{ + affiliation = member, + role = participant, + nick = Nick}], + status_codes = [303]}]}), + ejabberd_router:route( + #presence{ + from = jid:make(iolist_to_binary([Chan, $%, StateData#state.server]), + StateData#state.host, Nick), + to = StateData#state.user, + sub_els = [#muc_user{ + items = [#muc_item{ + affiliation = member, + role = participant}]}]}), + (?SETS):add_element(Nick, remove_element(FromUser, Ps)); + _ -> Ps + end + end, StateData#state.channels), + if FromUser == StateData#state.nick -> + StateData#state{nick = Nick, nickchannel = undefined, + channels = NewChans}; + true -> StateData#state{channels = NewChans} + end. + +process_error(StateData, String) -> + lists:foreach( + fun(Chan) -> + ejabberd_router:route( + #presence{ + from = jid:make(iolist_to_binary([Chan, $%, StateData#state.server]), + StateData#state.host, + StateData#state.nick), + to = StateData#state.user, + type = error, + sub_els = [xmpp:err_internal_server_error(String, ejabberd_config:get_mylang())]}) + end, dict:fetch_keys(StateData#state.channels)). + +error_unknown_num(_StateData, String, Type) -> + Msg = ejabberd_regexp:replace(String, + <<".*[45][0-9][0-9] +[^ ]* +">>, <<"">>), + Msg1 = filter_message(Msg), + xmpp:err_undefined_condition(Type, Msg1, ejabberd_config:get_mylang()). + +remove_element(E, Set) -> + case (?SETS):is_element(E, Set) of + true -> (?SETS):del_element(E, Set); + _ -> Set + end. + +iq_admin(StateData, Channel, From, _To, + #iq{type = Type, sub_els = [SubEl]} = IQ) -> + try process_iq_admin(StateData, Channel, Type, SubEl) of + {result, Result} -> + ejabberd_router:route(xmpp:make_iq_result(IQ, Result)); + {error, Error} -> + ejabberd_router:route_error(IQ, Error) + catch E:R -> + ?ERROR_MSG("failed to process admin query from ~s: ~p", + [jid:encode(From), {E, {R, erlang:get_stacktrace()}}]), + ejabberd_router:route_error( + IQ, xmpp:err_internal_server_error()) + end. + +process_iq_admin(_StateData, _Channel, set, #muc_admin{items = []}) -> + {error, xmpp:err_bad_request()}; +process_iq_admin(StateData, Channel, set, #muc_admin{items = [Item|_]}) -> + process_admin(StateData, Channel, Item); +process_iq_admin(_StateData, _Channel, _, _SubEl) -> + {error, xmpp:err_feature_not_implemented()}. + +process_admin(_StateData, _Channel, #muc_item{nick = <<"">>}) -> + {error, xmpp:err_feature_not_implemented()}; +process_admin(StateData, Channel, #muc_item{nick = Nick, + reason = Reason, + role = none}) -> + case Reason of + <<"">> -> + send_text(StateData, + io_lib:format("KICK #~s ~s\r\n", [Channel, Nick])); + _ -> + send_text(StateData, + io_lib:format("KICK #~s ~s :~s\r\n", + [Channel, Nick, Reason])) + end, + {result, undefined}; +process_admin(_StateData, _Channel, _Item) -> + {error, xmpp:err_feature_not_implemented()}. + +filter_message(Msg) -> + list_to_binary( + lists:filter(fun (C) -> + if (C < 32) and (C /= 9) and (C /= 10) and (C /= 13) -> + false; + true -> true + end + end, + binary_to_list(filter_mirc_colors(Msg)))). + +filter_mirc_colors(Msg) -> + ejabberd_regexp:greplace(Msg, + <<"(\\003[0-9]+)(,[0-9]+)?">>, <<"">>). + +unixtime2string(Unixtime) -> + Secs = Unixtime + + calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, + {0, 0, 0}}), + {{Year, Month, Day}, {Hour, Minute, Second}} = + calendar:universal_time_to_local_time(calendar:gregorian_seconds_to_datetime(Secs)), + (str:format("~4..0w-~2..0w-~2..0w ~2..0w:~2..0w:~2..0w", + [Year, Month, Day, Hour, Minute, Second])). diff --git a/mod_irc/src/mod_irc_mnesia.erl b/mod_irc/src/mod_irc_mnesia.erl new file mode 100644 index 0000000..f1cdf91 --- /dev/null +++ b/mod_irc/src/mod_irc_mnesia.erl @@ -0,0 +1,81 @@ +%%%------------------------------------------------------------------- +%%% File : mod_irc_mnesia.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_irc_mnesia). + +-behaviour(mod_irc). + +%% API +-export([init/2, get_data/3, set_data/4, import/2]). +-export([need_transform/1, transform/1]). + +-include("jid.hrl"). +-include("mod_irc.hrl"). +-include("logger.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ejabberd_mnesia:create(?MODULE, irc_custom, + [{disc_copies, [node()]}, + {attributes, record_info(fields, irc_custom)}]). + +get_data(_LServer, Host, From) -> + {U, S, _} = jid:tolower(From), + case catch mnesia:dirty_read({irc_custom, {{U, S}, Host}}) of + {'EXIT', _Reason} -> error; + [] -> empty; + [#irc_custom{data = Data}] -> Data + end. + +set_data(_LServer, Host, From, Data) -> + {U, S, _} = jid:tolower(From), + F = fun () -> + mnesia:write(#irc_custom{us_host = {{U, S}, Host}, + data = Data}) + end, + mnesia:transaction(F). + +import(_LServer, #irc_custom{} = R) -> + mnesia:dirty_write(R). + +need_transform(#irc_custom{us_host = {{U, S}, H}}) + when is_list(U) orelse is_list(S) orelse is_list(H) -> + ?INFO_MSG("Mnesia table 'irc_custom' will be converted to binary", []), + true; +need_transform(_) -> + false. + +transform(#irc_custom{us_host = {{U, S}, H}, + data = Data} = R) -> + JID = jid:make(U, S), + R#irc_custom{us_host = {{iolist_to_binary(U), + iolist_to_binary(S)}, + iolist_to_binary(H)}, + data = mod_irc:data_to_binary(JID, Data)}. + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== diff --git a/mod_irc/src/mod_irc_riak.erl b/mod_irc/src/mod_irc_riak.erl new file mode 100644 index 0000000..31d09ab --- /dev/null +++ b/mod_irc/src/mod_irc_riak.erl @@ -0,0 +1,65 @@ +%%%------------------------------------------------------------------- +%%% File : mod_irc_riak.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_irc_riak). + +-behaviour(mod_irc). + +%% API +-export([init/2, get_data/3, set_data/4, import/2]). + +-include("jid.hrl"). +-include("mod_irc.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_data(_LServer, Host, From) -> + {U, S, _} = jid:tolower(From), + case ejabberd_riak:get(irc_custom, irc_custom_schema(), {{U, S}, Host}) of + {ok, #irc_custom{data = Data}} -> + Data; + {error, notfound} -> + empty; + _Err -> + error + end. + +set_data(_LServer, Host, From, Data) -> + {U, S, _} = jid:tolower(From), + {atomic, ejabberd_riak:put(#irc_custom{us_host = {{U, S}, Host}, + data = Data}, + irc_custom_schema())}. + +import(_LServer, #irc_custom{} = R) -> + ejabberd_riak:put(R, irc_custom_schema()). + +%%%=================================================================== +%%% Internal functions +%%%=================================================================== +irc_custom_schema() -> + {record_info(fields, irc_custom), #irc_custom{}}. diff --git a/mod_irc/src/mod_irc_sql.erl b/mod_irc/src/mod_irc_sql.erl new file mode 100644 index 0000000..9dcdcea --- /dev/null +++ b/mod_irc/src/mod_irc_sql.erl @@ -0,0 +1,106 @@ +%%%------------------------------------------------------------------- +%%% File : mod_irc_sql.erl +%%% Author : Evgeny Khramtsov +%%% Created : 14 Apr 2016 by Evgeny Khramtsov +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- + +-module(mod_irc_sql). + +-compile([{parse_transform, ejabberd_sql_pt}]). + +-behaviour(mod_irc). + +%% API +-export([init/2, get_data/3, set_data/4, import/1, import/2, export/1]). + +-include("jid.hrl"). +-include("mod_irc.hrl"). +-include("ejabberd_sql_pt.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(_Host, _Opts) -> + ok. + +get_data(LServer, Host, From) -> + SJID = jid:encode(jid:tolower(jid:remove_resource(From))), + case catch ejabberd_sql:sql_query( + LServer, + ?SQL("select @(data)s from irc_custom" + " where jid=%(SJID)s and host=%(Host)s and %(LServer)H")) of + {selected, [{SData}]} -> + mod_irc:data_to_binary(From, ejabberd_sql:decode_term(SData)); + {'EXIT', _} -> error; + {selected, _} -> empty + end. + +set_data(LServer, Host, From, Data) -> + SJID = jid:encode(jid:tolower(jid:remove_resource(From))), + SData = misc:term_to_expr(Data), + F = fun () -> + ?SQL_UPSERT_T( + "irc_custom", + ["!jid=%(SJID)s", + "!host=%(Host)s", + "server_host=%(LServer)s", + "data=%(SData)s"]), + ok + end, + ejabberd_sql:sql_transaction(LServer, F). + +export(_Server) -> + [{irc_custom, + fun(Host, #irc_custom{us_host = {{U, S}, IRCHost}, + data = Data}) -> + case str:suffix(Host, IRCHost) of + true -> + SJID = jid:encode(jid:make(U, S)), + LServer = ejabberd_router:host_of_route(IRCHost), + SData = misc:term_to_expr(Data), + [?SQL("delete from irc_custom" + " where jid=%(SJID)s and host=%(IRCHost)s and %(LServer)H;"), + ?SQL_INSERT( + "irc_custom", + ["jid=%(SJID)s", + "host=%(Host)s", + "server_host=%(LServer)s", + "data=%(SData)s"])]; + false -> + [] + end + end}]. + +import(_LServer) -> + [{<<"select jid, host, data from irc_custom;">>, + fun([SJID, IRCHost, SData]) -> + #jid{luser = U, lserver = S} = jid:decode(SJID), + Data = ejabberd_sql:decode_term(SData), + #irc_custom{us_host = {{U, S}, IRCHost}, + data = Data} + end}]. + +import(_, _) -> + pass. + +%%%=================================================================== +%%% Internal functions +%%%===================================================================