--- ejabberd-1.1.1/src/ejabberd.cfg.example 2006-04-22 15:50:30.000000000 +0200 +++ ejabberd-1.1.1/src/ejabberd.cfg.example 2006-06-23 09:38:17.000000000 +0200 @@ -125,6 +125,7 @@ {max_stanza_size, 131072} ]}, {5280, ejabberd_http, [http_poll, web_admin]}, + %{7777, proxy65_listener, [{shaper, c2s_shaper}]}, {8888, ejabberd_service, [{access, all}, {hosts, ["icq.localhost", "sms.localhost"], [{password, "secret"}]}]} @@ -171,6 +172,11 @@ {mod_pubsub, []}, {mod_time, []}, {mod_last, []}, + %% Simple configuration (hostname:7777) + %%{mod_proxy65, []}, + %% Several possible hostnames + %%{mod_proxy65, [{access, all}, + %% {streamhosts, [{"example.com", 7777}, {"192.168.0.42", 7777}]}]}, {mod_version, []} ]}. --- ejabberd-1.1.1/src/jlib.hrl 2006-01-20 17:21:39.000000000 +0100 +++ ejabberd-1.1.1/src/jlib.hrl 2006-06-23 09:38:17.000000000 +0200 @@ -34,6 +34,7 @@ -define(NS_PUBSUB_OWNER, "http://jabber.org/protocol/pubsub#owner"). -define(NS_PUBSUB_NMI, "http://jabber.org/protocol/pubsub#node-meta-info"). -define(NS_COMMANDS, "http://jabber.org/protocol/commands"). +-define(NS_BYTESTREAMS, "http://jabber.org/protocol/bytestreams"). -define(NS_EJABBERD_CONFIG, "ejabberd:config"). --- ejabberd-1.1.1/src/mod_proxy65.erl 1970-01-01 01:00:00.000000000 +0100 +++ ejabberd-1.1.1/src/mod_proxy65.erl 2006-06-23 09:38:17.000000000 +0200 @@ -0,0 +1,189 @@ +%%%---------------------------------------------------------------------- +%%% File : mod_proxy65.erl +%%% Author : Magnus Henoch +%%% Purpose : Handle Jabber communications for JEP-0065 proxy +%%% Created : 27 Dec 2005 by Magnus Henoch +%%% Id : $Id: ejabberd_c2s.erl 440 2005-11-22 18:00:56Z alexey $ +%%%---------------------------------------------------------------------- + +-module(mod_proxy65). +-author('henoch@dtek.chalmers.se'). +-vsn('$Revision$ '). + +-behaviour(gen_mod). + +-export([start/2, + init/1, + stop/1]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(proxy65_connection, {cookie, firstpid = none, secondpid = none}). + +-record(proxy65_options, {host, access, streamhosts}). + +-define(PROCNAME, ejabberd_mod_proxy65). + +start(Host, Opts) -> + mnesia:create_table(proxy65_connection, + [{ram_copies, [node()]}, + {attributes, record_info(fields, proxy65_connection)}]), + MyHost = gen_mod:get_opt(host, Opts, "proxy." ++ Host), + Access = gen_mod:get_opt(access, Opts, all), + Streamhosts = gen_mod:get_opt(streamhosts, Opts, [{Host, 7777}]), + + register(gen_mod:get_module_proc(Host, ?PROCNAME), + spawn(?MODULE, init, [#proxy65_options{host = MyHost, access = Access, + streamhosts = Streamhosts}])). + + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + Proc ! stop, + {wait, Proc}. + +init(#proxy65_options{host = Host} = Opts) -> + ejabberd_router:register_route(Host), + loop(Opts). + +loop(#proxy65_options{host = Host} = Opts) -> + receive + {route, From, To, Packet} -> + case catch do_route(Opts, From, To, Packet) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p", [Reason]); + _ -> + ok + end, + loop(Opts); + stop -> + ejabberd_router:unregister_route(Host), + ok; + _ -> + loop(Opts) + end. + +do_route(#proxy65_options{host = Host, access = Access} = Opts, + From, To, Packet) -> + case acl:match_rule(Host, Access, From) of + allow -> + do_route1(Opts, From, To, Packet); + _ -> + {xmlelement, _Name, Attrs, _Els} = Packet, + Lang = xml:get_attr_s("xml:lang", Attrs), + ErrText = "Access denied by service policy", + Err = jlib:make_error_reply(Packet, + ?ERRT_FORBIDDEN(Lang, ErrText)), + ejabberd_router:route(To, From, Err) + end. + +do_route1(#proxy65_options{host = Host, streamhosts = Streamhosts}, From, To, Packet) -> + {xmlelement, Name, _Attrs, _Els} = Packet, + case Name of + "iq" -> + case jlib:iq_query_info(Packet) of + #iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS, + lang = Lang, sub_el = SubEl} = IQ -> + Node = xml:get_tag_attr_s("node", SubEl), + if Node == [] -> + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + [{xmlelement, "identity", + [{"category", "proxy"}, + {"type", "bytestreams"}, + {"name", translate:translate(Lang, "SOCKS5 bytestreams proxy")}], + []}, + {xmlelement, "feature", + [{"var", ?NS_BYTESTREAMS}], []}]}]}; + true -> + Res = jlib:make_error_reply(Packet, ?ERR_ITEM_NOT_FOUND) + end; + #iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS} = IQ -> + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], []}]}; + #iq{type = get, xmlns = ?NS_VERSION} = IQ -> + OSType = case os:type() of + {Osfamily, Osname} -> + atom_to_list(Osfamily) ++ "/" ++ + atom_to_list(Osname); + Osfamily -> + atom_to_list(Osfamily) + end, + OSVersion = case os:version() of + {Major, Minor, Release} -> + lists:flatten( + io_lib:format("~w.~w.~w", + [Major, Minor, Release])); + VersionString -> + VersionString + end, + OS = OSType ++ " " ++ OSVersion, + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", ?NS_VERSION}], + [{xmlelement, "name", [], + [{xmlcdata, "ejabberd mod_proxy65 (unofficial)"}]}, + {xmlelement, "version", [], + [{xmlcdata, "0.1"}]}, + {xmlelement, "os", [], + [{xmlcdata, OS}]} + ]}]}; + #iq{type = get, xmlns = ?NS_BYTESTREAMS = XMLNS} = IQ -> + Res = IQ#iq{type = result, + sub_el = [{xmlelement, "query", + [{"xmlns", XMLNS}], + return_streamhosts(Host, Streamhosts)}]}; + #iq{type = set, xmlns = ?NS_BYTESTREAMS} = IQ -> + Res = activate(Packet, From, To, IQ); + _ -> + Res = jlib:make_error_reply(Packet, ?ERR_FEATURE_NOT_IMPLEMENTED) + end, + case Res of + #iq{} -> + ejabberd_router:route(To, From, jlib:iq_to_xml(Res)); + _ -> + ejabberd_router:route(To, From, Res) + end; + _ -> + ejabberd_router:route(To, From, jlib:make_error_reply(Packet, ?ERR_FEATURE_NOT_IMPLEMENTED)) + end. + +return_streamhosts(_JID, []) -> + []; +return_streamhosts(JID, [{Host, Port} | Streamhosts]) -> + %% This is not tail-recursive, but it doesn't matter. + [{xmlelement, "streamhost", + [{"jid", JID}, + {"host", Host}, + {"port", integer_to_list(Port)}], + []} | return_streamhosts(JID, Streamhosts)]. + +activate(Packet, From, _To, #iq{sub_el = SubEl} = IQ) -> + case SubEl of + {xmlelement, "query", Attrs, _SubEls} -> + Sid = xml:get_attr_s("sid", Attrs), + ActivateTag = xml:get_subtag(SubEl, "activate"), + if ActivateTag /= false -> + TargetJID = jlib:string_to_jid(xml:get_tag_cdata(ActivateTag)); + true -> + TargetJID = false + end, + + if Sid /= [], TargetJID /= false, TargetJID /= error -> + case proxy65_listener:activate(From, TargetJID, Sid) of + ok -> + ?INFO_MSG("Activated connection between ~s and ~s", + [jlib:jid_to_string(From), TargetJID]), + IQ#iq{type = result, sub_el = []}; + _ -> + jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR) + end; + true -> + jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST) + end; + _ -> + jlib:make_error_reply(Packet, ?ERR_BAD_REQUEST) + end. --- ejabberd-1.1.1/src/proxy65_listener.erl 1970-01-01 01:00:00.000000000 +0100 +++ ejabberd-1.1.1/src/proxy65_listener.erl 2006-06-23 09:38:17.000000000 +0200 @@ -0,0 +1,192 @@ +%%%---------------------------------------------------------------------- +%%% File : proxy65_listener.erl +%%% Author : Magnus Henoch +%%% Purpose : Handle SOCKS5 connections for JEP-0065 proxy +%%% Created : 27 Dec 2005 by Magnus Henoch +%%% Id : $Id: ejabberd_c2s.erl 440 2005-11-22 18:00:56Z alexey $ +%%%---------------------------------------------------------------------- + +-module(proxy65_listener). +-author('henoch@dtek.chalmers.se'). +-vsn('$Revision$ '). + +-export([start/2, handle_connection/2, activate/3]). + +-include("ejabberd.hrl"). +-include("jlib.hrl"). + +-record(proxy65_connection, {cookie, firstpid = none, secondpid = none}). + +start({SockMod, Socket}, Opts) -> + {ok, proc_lib:spawn(?MODULE, handle_connection, [{SockMod, Socket}, Opts])}. + +read_bytes(_SockOpts, 0, Data) -> + lists:flatten(Data); +read_bytes({SockMod, Socket} = SockOpts, N, Data) -> + Timeout = 1000, + case SockMod:recv(Socket, N, Timeout) of + {error, closed} -> + %% On closed connection, return everything we have, + %% but not if we have nothing. + if Data == [] -> + erlang:error(closed); + true -> + lists:flatten(Data) + end; + {ok, MoreData} -> + ?DEBUG("read ~p", [MoreData]), + DataList = binary_to_list(MoreData), + read_bytes(SockOpts, N - length(DataList), [Data, DataList]) + end. + +handle_connection({SockMod, Socket}, Opts) -> + ?DEBUG("in handle_connection", []), + case catch handle_auth({SockMod, Socket}, Opts) of + {'EXIT', Reason} -> + ?ERROR_MSG("~p abnormal termination:~n\t~p~n", + [?MODULE, Reason]), + SockMod:close(Socket); + _ -> + ok + end. + +handle_auth({SockMod, Socket} = SockOpts, Opts) -> + ?DEBUG("in handle_auth", []), + %% SOCKS protocol stuff... + [5, NAuthMethods] = read_bytes(SockOpts, 2, []), + AuthMethods = read_bytes(SockOpts, NAuthMethods, []), + SupportsNoAuth = lists:member(0, AuthMethods), + + %% Must support no authentication, otherwise crash + true = SupportsNoAuth, + + SockMod:send(Socket, [5, 0]), + + %% And done. + handle_connect(SockOpts, Opts). + +handle_connect({SockMod, Socket} = SockOpts, Opts) -> + ?DEBUG("in handle_connect", []), + %% Expect a CONNECT command and nothing else + [5, 1, _, 3, AddressLength] = read_bytes(SockOpts, 5, []), + Cookie = read_bytes(SockOpts, AddressLength, []), + [0, 0] = read_bytes(SockOpts, 2, []), + + %% Make sure no more than two connections claim the same cookie. + F = fun() -> + case mnesia:read({proxy65_connection, Cookie}) of + [] -> + mnesia:write(#proxy65_connection{cookie = Cookie, + firstpid = self()}), + ok; + [#proxy65_connection{secondpid = none} = C] -> + mnesia:write(C#proxy65_connection{secondpid = self()}), + ok + end + end, + + case mnesia:transaction(F) of + {atomic, ok} -> + SockMod:send(Socket, [5, 0, 0, 3, AddressLength, Cookie, 0, 0]), + wait_for_activation(SockOpts, Opts); + Error -> + %% conflict. send "general SOCKS server failure". + SockMod:send(Socket, [5, 1, 0, 3, AddressLength, Cookie, 0, 0]), + erlang:error({badconnect, Error}) + end. + +wait_for_activation(SockOpts, Opts) -> + ?DEBUG("in wait_for_activation", []), + receive + {get_socket, ReplyTo} -> + ReplyTo ! SockOpts, + wait_for_activation(SockOpts, Opts); + {activate, TargetSocket, Initiator, Target} -> + ?DEBUG("activated", []), + + %% We have no way of knowing which connection belongs to + %% which participant, so give both the maximum traffic + %% allowed to either. + Shapers = case lists:keysearch(shaper, 1, Opts) of + {value, {_, S}} -> S; + _ -> none + end, + ?DEBUG("we have shapers: ~p", [Shapers]), + Shaper1 = acl:match_rule(global, Shapers, jlib:string_to_jid(Initiator)), + Shaper2 = acl:match_rule(global, Shapers, jlib:string_to_jid(Target)), + if Shaper1 == none; Shaper2 == none -> + MaxShaper = none; + true -> + ShaperValue1 = ejabberd_config:get_global_option({shaper, Shaper1}), + ShaperValue2 = ejabberd_config:get_global_option({shaper, Shaper2}), + + if ShaperValue1 > ShaperValue2 -> + MaxShaper = Shaper1; + true -> + MaxShaper = Shaper2 + end, + ?DEBUG("shapers have values ~p and ~p~nusing ~p", [ShaperValue1, ShaperValue2, MaxShaper]), + ok + end, + + transfer_data(SockOpts, TargetSocket, shaper:new(MaxShaper)) + end. + +transfer_data({SockMod, Socket} = SockOpts, {TargetSockMod, TargetSocket} = TargetSockOpts, + Shaper) -> + case SockMod:recv(Socket, 0, infinity) of + {ok, Data} -> + if Data /= <<>> -> + NewShaper = case Shaper of + none -> none; + _ -> + shaper:update(Shaper, size(Data)) + end, + ok = TargetSockMod:send(TargetSocket, Data); + true -> + NewShaper = Shaper + end, + transfer_data(SockOpts, TargetSockOpts, NewShaper); + {error, _} -> + TargetSockMod:shutdown(TargetSocket, read_write) + end. + +get_socket(PID) -> + PID ! {get_socket, self()}, + receive + {_SockMod, _Socket} = SockOpts -> + SockOpts + end. + +%% If any argument is a jid record, convert it to normalized string form... +activate(#jid{} = Initiator, Target, SessionID) -> + NormalizedInitiator = jlib:jid_to_string(jlib:make_jid(jlib:jid_tolower(Initiator))), + activate(NormalizedInitiator, Target, SessionID); +activate(Initiator, #jid{} = Target, SessionID) -> + NormalizedTarget = jlib:jid_to_string(jlib:make_jid(jlib:jid_tolower(Target))), + activate(Initiator, NormalizedTarget, SessionID); +%% ...and get on with the activation. +activate(Initiator, Target, SessionID) -> + Cookie = sha:sha(SessionID ++ Initiator ++ Target), + F = fun() -> + case mnesia:read({proxy65_connection, Cookie}) of + [#proxy65_connection{firstpid = FirstPID, + secondpid = SecondPID}] + when is_pid(FirstPID), is_pid(SecondPID) -> + mnesia:delete({proxy65_connection, Cookie}), + {FirstPID, SecondPID}; + _ -> + error + end + end, + case mnesia:transaction(F) of + {atomic, {FirstPID, SecondPID}} -> + FirstSocket = get_socket(FirstPID), + SecondSocket = get_socket(SecondPID), + FirstPID ! {activate, SecondSocket, Initiator, Target}, + SecondPID ! {activate, FirstSocket, Initiator, Target}, + ok; + Error -> + ?ERROR_MSG("Proxy activation failed: ~p", [Error]), + error + end.