diff options
author | Alexey Shchepin <alexey@process-one.net> | 2006-09-14 02:54:21 +0000 |
---|---|---|
committer | Alexey Shchepin <alexey@process-one.net> | 2006-09-14 02:54:21 +0000 |
commit | 7d2a1af9d972e0b37eddafb86e5920956fd5334d (patch) | |
tree | 99206bb934f30d34d239ab68aa61448915a8f6cf /src | |
parent | * src/odbc/mssql.sql: Removed unused fields. (diff) |
* doc/guide.tex: Updated (thanks to Evgeniy Khramtsov)
* src/ejabberd_auth_ldap.erl: Better LDAP support (thanks to
Evgeniy Khramtsov)
* src/mod_vcard_ldap.erl: Likewise
* src/eldap/eldap_filter.erl: Likewise
SVN Revision: 606
Diffstat (limited to 'src')
-rw-r--r-- | src/ejabberd_auth_ldap.erl | 353 | ||||
-rw-r--r-- | src/eldap/Makefile.in | 3 | ||||
-rw-r--r-- | src/eldap/Makefile.win32 | 3 | ||||
-rw-r--r-- | src/eldap/eldap_filter.erl | 269 | ||||
-rw-r--r-- | src/mod_vcard_ldap.erl | 877 |
5 files changed, 1111 insertions, 394 deletions
diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 6e454cb80..a1ce74609 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -10,8 +10,21 @@ -author('alexey@sevcom.net'). -vsn('$Revision$ '). +-behaviour(gen_server). + +%% gen_server callbacks +-export([init/1, + handle_info/2, + handle_call/3, + handle_cast/2, + terminate/2, + code_change/3 + ]). + %% External exports -export([start/1, + stop/1, + start_link/1, set_password/3, check_password/3, check_password/5, @@ -29,39 +42,84 @@ -include("ejabberd.hrl"). -include("eldap/eldap.hrl"). +-record(state, {host, + eldap_id, + servers, + port, + dn, + password, + base, + uidattr, + uidattr_format, + ufilter, + sfilter, + dn_filter, + dn_filter_attrs + }). + +%% Unused callbacks. +handle_cast(_Request, State) -> + {noreply, State}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. +handle_info(_Info, State) -> + {noreply, State}. +%% ----- + %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- + start(Host) -> - LDAPServers = ejabberd_config:get_local_option({ldap_servers, Host}), - RootDN = ejabberd_config:get_local_option({ldap_rootdn, Host}), - Password = ejabberd_config:get_local_option({ldap_password, Host}), - eldap:start_link(get_eldap_id(Host, ejabberd), - LDAPServers, 389, RootDN, Password), - eldap:start_link(get_eldap_id(Host, ejabberd_bind), - LDAPServers, 389, RootDN, Password), + Proc = gen_mod:get_module_proc(Host, ?MODULE), + ChildSpec = { + Proc, {?MODULE, start_link, [Host]}, + permanent, 1000, worker, [?MODULE] + }, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?MODULE), + gen_server:call(Proc, stop), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + +start_link(Host) -> + Proc = gen_mod:get_module_proc(Host, ?MODULE), + gen_server:start_link({local, Proc}, ?MODULE, Host, []). + +terminate(_Reason, State) -> + ejabberd_ctl:unregister_commands( + State#state.host, + [{"registered-users", "list all registered users"}], + ejabberd_auth, ctl_process_get_registered). + +init(Host) -> + State = parse_options(Host), + eldap:start_link(State#state.eldap_id, + State#state.servers, + State#state.port, + State#state.dn, + State#state.password), ejabberd_ctl:register_commands( Host, [{"registered-users", "list all registered users"}], ejabberd_auth, ctl_process_get_registered), - ok. + {ok, State}. + +-define(REPLY_TIMEOUT, 10000). plain_password_required() -> true. check_password(User, Server, Password) -> - case find_user_dn(User, Server) of - false -> + Proc = gen_mod:get_module_proc(Server, ?MODULE), + case catch gen_server:call(Proc, + {check_pass, User, Password}, ?REPLY_TIMEOUT) of + {'EXIT', _} -> false; - DN -> - LServer = jlib:nameprep(Server), - case eldap:bind(get_eldap_id(LServer, ejabberd_bind), - DN, Password) of - ok -> - true; - _ -> - false - end + Result -> + Result end. check_password(User, Server, Password, _StreamID, _Digest) -> @@ -77,31 +135,13 @@ dirty_get_registered_users() -> get_vh_registered_users(?MYNAME). get_vh_registered_users(Server) -> - LServer = jlib:nameprep(Server), - Attr = ejabberd_config:get_local_option({ldap_uidattr, LServer}), - Filter = eldap:present(Attr), - Base = ejabberd_config:get_local_option({ldap_base, LServer}), - case eldap:search(get_eldap_id(LServer, ejabberd), - [{base, Base}, - {filter, Filter}, - {attributes, [Attr]}]) of - #eldap_search_result{entries = Es} -> - lists:flatmap( - fun(E) -> - case lists:keysearch(Attr, 1, E#eldap_entry.attributes) of - {value, {_, [U]}} -> - case jlib:nodeprep(U) of - error -> - []; - LU -> - [{LU, LServer}] - end; - _ -> - [] - end - end, Es); - _ -> - [] + Proc = gen_mod:get_module_proc(Server, ?MODULE), + case catch gen_server:call(Proc, + get_vh_registered_users, ?REPLY_TIMEOUT) of + {'EXIT', _} -> + []; + Result -> + Result end. get_password(_User, _Server) -> @@ -111,11 +151,13 @@ get_password_s(_User, _Server) -> "". is_user_exists(User, Server) -> - case find_user_dn(User, Server) of - false -> + Proc = gen_mod:get_module_proc(Server, ?MODULE), + case catch gen_server:call(Proc, + {is_user_exists, User}, ?REPLY_TIMEOUT) of + {'EXIT', _} -> false; - _DN -> - true + Result -> + Result end. remove_user(_User, _Server) -> @@ -124,25 +166,212 @@ remove_user(_User, _Server) -> remove_user(_User, _Server, _Password) -> not_allowed. - %%%---------------------------------------------------------------------- %%% Internal functions %%%---------------------------------------------------------------------- +handle_call({check_pass, User, Password}, _From, State) -> + Reply = case find_user_dn(User, State) of + false -> + false; + DN -> + case eldap:bind(State#state.eldap_id, DN, Password) of + ok -> true; + _ -> false + end + end, + {reply, Reply, State}; + +handle_call(get_vh_registered_users, _From, State) -> + UA = State#state.uidattr, + UAF = State#state.uidattr_format, + Eldap_ID = State#state.eldap_id, + Server = State#state.host, + SortedDNAttrs = usort_attrs(State#state.dn_filter_attrs), + Reply = case eldap_filter:parse(State#state.sfilter) of + {ok, EldapFilter} -> + case eldap:search(Eldap_ID, [{base, State#state.base}, + {filter, EldapFilter}, + {attributes, SortedDNAttrs}]) of + #eldap_search_result{entries = Entries} -> + lists:flatmap( + fun(#eldap_entry{attributes = Attrs, + object_name = DN}) -> + case is_valid_dn(DN, Attrs, State) of + false -> []; + _ -> + case get_ldap_attr(UA, Attrs) of + "" -> []; + User -> + case get_user_part(User, UAF) of + {ok, U} -> + case jlib:nodeprep(U) of + error -> []; + LU -> [{LU, jlib:nameprep(Server)}] + end; + _ -> [] + end + end + end + end, Entries); + _ -> + [] + end; + _ -> + [] + end, + {reply, Reply, State}; + +handle_call({is_user_exists, User}, _From, State) -> + Reply = case find_user_dn(User, State) of + false -> false; + _DN -> true + end, + {reply, Reply, State}; + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; -find_user_dn(User, Server) -> - LServer = jlib:nameprep(Server), - Attr = ejabberd_config:get_local_option({ldap_uidattr, LServer}), - Filter = eldap:equalityMatch(Attr, User), - Base = ejabberd_config:get_local_option({ldap_base, LServer}), - case eldap:search(get_eldap_id(LServer, ejabberd), - [{base, Base}, - {filter, Filter}, - {attributes, []}]) of - #eldap_search_result{entries = [E | _]} -> - E#eldap_entry.object_name; +handle_call(_Request, _From, State) -> + {reply, bad_request, State}. + +find_user_dn(User, State) -> + DNAttrs = usort_attrs(State#state.dn_filter_attrs), + case eldap_filter:parse(State#state.ufilter, [{"%u", User}]) of + {ok, Filter} -> + case eldap:search(State#state.eldap_id, [{base, State#state.base}, + {filter, Filter}, + {attributes, DNAttrs}]) of + #eldap_search_result{entries = [#eldap_entry{attributes = Attrs, + object_name = DN} | _]} -> + is_valid_dn(DN, Attrs, State); + _ -> + false + end; _ -> false end. -get_eldap_id(Host, Name) -> - atom_to_list(gen_mod:get_module_proc(Host, Name)). +is_valid_dn(DN, _, #state{dn_filter = undefined}) -> + DN; + +is_valid_dn(DN, Attrs, State) -> + DNAttrs = State#state.dn_filter_attrs, + UA = State#state.uidattr, + UAF = State#state.uidattr_format, + Values = [{"%s", get_ldap_attr(Attr, Attrs), 1} || Attr <- DNAttrs], + SubstValues = case get_ldap_attr(UA, Attrs) of + "" -> Values; + S -> + case get_user_part(S, UAF) of + {ok, U} -> [{"%u", U} | Values]; + _ -> Values + end + end ++ [{"%d", State#state.host}, {"%D", DN}], + case eldap_filter:parse(State#state.dn_filter, SubstValues) of + {ok, EldapFilter} -> + case eldap:search(State#state.eldap_id, [ + {base, State#state.base}, + {filter, EldapFilter}, + {attributes, ["dn"]}]) of + #eldap_search_result{entries = [_|_]} -> + DN; + _ -> + false + end; + _ -> + false + end. + +%%%---------------------------------------------------------------------- +%%% Auxiliary functions +%%%---------------------------------------------------------------------- +get_user_part(String, Pattern) -> + F = fun(S, P) -> + First = string:str(P, "%u"), + TailLength = length(P) - (First+1), + string:sub_string(S, First, length(S) - TailLength) + end, + case catch F(String, Pattern) of + {'EXIT', _} -> + {error, badmatch}; + Result -> + case regexp:sub(Pattern, "%u", Result) of + {ok, String, _} -> {ok, Result}; + _ -> {error, badmatch} + end + end. + +case_insensitive_match(X, Y) -> + X1 = stringprep:tolower(X), + Y1 = stringprep:tolower(Y), + if + X1 == Y1 -> true; + true -> false + end. + +get_ldap_attr(LDAPAttr, Attributes) -> + Res = lists:filter( + fun({Name, _}) -> + case_insensitive_match(Name, LDAPAttr) + end, Attributes), + case Res of + [{_, [Value|_]}] -> Value; + _ -> "" + end. + +usort_attrs(Attrs) when is_list(Attrs) -> + lists:usort(Attrs); + +usort_attrs(_) -> + []. + +parse_options(Host) -> + Eldap_ID = atom_to_list(gen_mod:get_module_proc(Host, ?MODULE)), + LDAPServers = ejabberd_config:get_local_option({ldap_servers, Host}), + LDAPPort = case ejabberd_config:get_local_option({ldap_port, Host}) of + undefined -> 389; + P -> P + end, + RootDN = case ejabberd_config:get_local_option({ldap_rootdn, Host}) of + undefined -> ""; + RDN -> RDN + end, + Password = case ejabberd_config:get_local_option({ldap_password, Host}) of + undefined -> ""; + Pass -> Pass + end, + UIDAttr = case ejabberd_config:get_local_option({ldap_uidattr, Host}) of + undefined -> "uid"; + UA -> UA + end, + UIDAttrFormat = case ejabberd_config:get_local_option({ldap_uidattr_format, Host}) of + undefined -> "%u"; + UAF -> UAF + end, + SubFilter = "(" ++ UIDAttr ++ "=" ++ UIDAttrFormat ++ ")", + UserFilter = case ejabberd_config:get_local_option({ldap_filter, Host}) of + undefined -> SubFilter; + "" -> SubFilter; + F -> "(&" ++ SubFilter ++ F ++ ")" + end, + SearchFilter = eldap_filter:do_sub(UserFilter, [{"%u", "*"}]), + LDAPBase = ejabberd_config:get_local_option({ldap_base, Host}), + {DNFilter, DNFilterAttrs} = + case ejabberd_config:get_local_option({ldap_dn_filter, Host}) of + undefined -> {undefined, undefined}; + {DNF, DNFA} -> {DNF, DNFA} + end, + #state{host = Host, + eldap_id = Eldap_ID, + servers = LDAPServers, + port = LDAPPort, + dn = RootDN, + password = Password, + base = LDAPBase, + uidattr = UIDAttr, + uidattr_format = UIDAttrFormat, + ufilter = UserFilter, + sfilter = SearchFilter, + dn_filter = DNFilter, + dn_filter_attrs = DNFilterAttrs + }. diff --git a/src/eldap/Makefile.in b/src/eldap/Makefile.in index 91384b328..72e7b4632 100644 --- a/src/eldap/Makefile.in +++ b/src/eldap/Makefile.in @@ -12,7 +12,8 @@ OUTDIR = .. EFLAGS = -I .. -pz .. OBJS = \ $(OUTDIR)/eldap.beam \ - $(OUTDIR)/ELDAPv3.beam + $(OUTDIR)/ELDAPv3.beam \ + $(OUTDIR)/eldap_filter.beam all: $(OBJS) diff --git a/src/eldap/Makefile.win32 b/src/eldap/Makefile.win32 index 6feda635c..5d737d05a 100644 --- a/src/eldap/Makefile.win32 +++ b/src/eldap/Makefile.win32 @@ -6,7 +6,8 @@ EFLAGS = -I .. -pz .. OBJS = \ $(OUTDIR)\eldap.beam \ - $(OUTDIR)\ELDAPv3.beam + $(OUTDIR)\ELDAPv3.beam \ + $(OUTDIR)\eldap_filter.beam ALL : $(OBJS) diff --git a/src/eldap/eldap_filter.erl b/src/eldap/eldap_filter.erl new file mode 100644 index 000000000..76fb7aede --- /dev/null +++ b/src/eldap/eldap_filter.erl @@ -0,0 +1,269 @@ +%%%==================================================== +%%% File: eldap_filter.erl +%%% Purpose: Converts String Representation of +%%% LDAP Search Filter (RFC 2254) +%%% to eldap's representation of filter +%%% Author: Evgeniy Khramtsov <xramtsov@gmail.com> +%%% License: GPL +%%%==================================================== + +-module(eldap_filter). +-author('xram@jabber.ru'). + +%%%====================== +%%% Export functions +%%%====================== + +-export([parse/1, + parse/2, + do_sub/2 + ]). + +%%%------------------------------------------------------------------------- +%%% Arity: parse/1 +%%% Function: parse(RFC2254_Filter) -> {ok, EldapFilter} | +%%% {error, bad_filter} +%%% +%%% RFC2254_Filter = string(). +%%% +%%% Description: Converts String Representation of LDAP Search Filter (RFC 2254) +%%% to eldap's representation of filter. +%%% +%%% Example: +%%% > eldap_filter:parse("(&(!(uid<=100))(mail=*))"). +%%% +%%% {ok,{'and',[{'not',{lessOrEqual,{'AttributeValueAssertion',"uid","100"}}}, +%%% {present,"mail"}]}} +%%%------------------------------------------------------------------------- +parse(RFC2254_Filter) -> + parse(RFC2254_Filter, []). + +%%%------------------------------------------------------------------------- +%%% Arity: parse/2 +%%% Function: parse(RFC2254_Filter, [SubstValue |...]) -> +%%% {ok, EldapFilter} | +%%% {error, bad_filter} | +%%% {error, bad_regexp} | +%%% {error, max_substitute_recursion} +%%% +%%% SubstValue = {RegExp, Value} | {RegExp, Value, N}, +%%% RFC2254_Filter = RegExp = Value = string(), +%%% N = integer(). +%%% +%%% Description: The same as parse/1, but substitutes N or all occurences +%%% of RegExp with Value *after* parsing. +%%% +%%% Example: +%%% > eldap_filter:parse( +%%% "(|(mail=%u@%d)(jid=%u@%d))", +%%% [{"%u", "xramtsov"},{"%d","gmail.com"}]). +%%% +%%% {ok,{'or',[{equalityMatch,{'AttributeValueAssertion', +%%% "mail", +%%% "xramtsov@gmail.com"}}, +%%% {equalityMatch,{'AttributeValueAssertion', +%%% "jid", +%%% "xramtsov@gmail.com"}}]}} +%%%-------------------------------------------------------------------------- +parse(RFC2254_Filter, ListOfSubValues) -> + case catch convert_filter(parse_filter(RFC2254_Filter), ListOfSubValues) of + [EldapFilter] when is_tuple(EldapFilter) -> + {ok, EldapFilter}; + {regexp, Error} -> + {error, Error}; + _ -> + {error, bad_filter} + end. + +%%%========================== +%%% Internal functions +%%%========================== + +%%%---------------------- +%%% split/1,4 +%%%---------------------- +split(Filter) -> + split(Filter, 0, [], []). + +split([], _, _, Result) -> + Result; + +split([H|T], Num, Rest, Result) -> + NewNum = case H of + $( -> Num + 1; + $) -> Num - 1; + _ -> Num + end, + if + NewNum == 0 -> + X = Rest++[H], + LenX = length(X), + if + LenX > 2 -> + split(T, 0, [], Result ++ [lists:sublist(X, 2, LenX-2)]); + true -> + split(T, 0, Rest, Result) + end; + true -> + split(T, NewNum, Rest++[H], Result) + end. + +%%%----------------------- +%%% parse_filter/1 +%%%----------------------- +parse_filter(Filter) -> + case Filter of + [$! | T] -> + {'not', parse_filter(T)}; + [$| | T] -> + {'or', parse_filter(T)}; + [$& | T] -> + {'and', parse_filter(T)}; + [$( | _] -> + parse_filter(split(Filter)); + [List | _] when is_list(List) -> + [parse_filter(X) || X <- Filter]; + _ -> + Filter + end. + +%%%-------------------- +%%% convert_filter/2 +%%%-------------------- +convert_filter({'not', [Val | _]}, Replace) -> + eldap:'not'(convert_filter(Val, Replace)); + +convert_filter({'or', Vals}, Replace) -> + eldap:'or'([convert_filter(X, Replace) || X <- Vals]); + +convert_filter({'and', Vals}, Replace) -> + eldap:'and'([convert_filter(X, Replace) || X <- Vals]); + +convert_filter([H|_] = Filter, Replace) when is_integer(H) -> + parse_attr(Filter, Replace); + +convert_filter(Filter, Replace) when is_list(Filter) -> + [convert_filter(X, Replace) || X <- Filter]. + +%%%----------------- +%%% parse_attr/2,3 +%%%----------------- +parse_attr(Attr, ListOfSubValues) -> + {Action, [_|_] = Name, [_|_] = Value} = split_attribute(Attr), + parse_attr(Action, {Name, Value}, ListOfSubValues). + +parse_attr(approx, {Name, Value}, ListOfSubValues) -> + NewValue = do_sub(Value, ListOfSubValues), + eldap:approxMatch(Name, NewValue); + +parse_attr(greater, {Name, Value}, ListOfSubValues) -> + NewValue = do_sub(Value, ListOfSubValues), + eldap:greaterOrEqual(Name, NewValue); + +parse_attr(less, {Name, Value}, ListOfSubValues) -> + NewValue = do_sub(Value, ListOfSubValues), + eldap:lessOrEqual(Name, NewValue); + +parse_attr(equal, {Name, Value}, ListOfSubValues) -> + {ok, RegSList} = regexp:split(remove_extra_asterisks(Value), "[*]"), + Pattern = case [do_sub(X, ListOfSubValues) || X <- RegSList] of + [Head | Tail] when Tail /= [] -> + {Head, lists:sublist(Tail, length(Tail)-1), lists:last(Tail)}; + R -> + R + end, + case Pattern of + [V] -> + eldap:equalityMatch(Name, V); + {[], [], []} -> + eldap:present(Name); + {"", Any, ""} -> + eldap:substrings(Name, [{any, X} || X<-Any]); + {H, Any, ""} -> + eldap:substrings(Name, [{initial, H}]++[{any, X} || X<-Any]); + {"", Any, T} -> + eldap:substrings(Name, [{any, X} || X<-Any]++[{final, T}]); + {H, Any, T} -> + eldap:substrings(Name, [{initial, H}]++[{any, X} || X<-Any]++[{final, T}]) + end; + +parse_attr(_, _, _) -> + false. + +%%%-------------------- +%%% do_sub/2,3 +%%%-------------------- + +-define(MAX_RECURSION, 100). + +do_sub(S, []) -> + S; + +do_sub([], _) -> + []; + +do_sub(S, [H | T]) -> + Result = do_sub(S, H, 1), + do_sub(Result, T). + +do_sub(S, {RegExp, New}, Iter) -> + case regexp:sub(S, RegExp, New) of + {ok, NewS, 0} -> + NewS; + {ok, NewS, _} when Iter =< ?MAX_RECURSION -> + do_sub(NewS, {RegExp, New}, Iter+1); + {ok, _, _} when Iter > ?MAX_RECURSION -> + throw({regexp, max_substitute_recursion}); + _ -> + throw({regexp, bad_regexp}) + end; + +do_sub(S, {_, _, N}, _) when N<1 -> + S; + +do_sub(S, {RegExp, New, Times}, Iter) -> + case regexp:sub(S, RegExp, New) of + {ok, NewS, 0} -> + NewS; + {ok, NewS, _} when Iter < Times -> + do_sub(NewS, {RegExp, New, Times}, Iter+1); + {ok, NewS, _} -> + NewS; + _ -> + throw({regexp, bad_regexp}) + end. + +remove_extra_asterisks(String) -> + {Res, _} = lists:foldl( + fun(X, {Acc, Last}) -> + case X of + $* when Last==$* -> + {Acc, X}; + _ -> + {Acc ++ [X], X} + end + end, + {"", ""}, String), + Res. + +split_attribute(String) -> + split_attribute(String, "", $0). + +split_attribute([], _, _) -> + {error, "", ""}; + +split_attribute([H|Tail], Acc, Last) -> + case H of + $= when Last==$> -> + {greater, lists:sublist(Acc, 1, length(Acc)-1), Tail}; + $= when Last==$< -> + {less, lists:sublist(Acc, 1, length(Acc)-1), Tail}; + $= when Last==$~ -> + {approx, lists:sublist(Acc, 1, length(Acc)-1), Tail}; + $= when Last==$: -> + {equal, lists:sublist(Acc, 1, length(Acc)-1), Tail}; + $= -> + {equal, Acc, Tail}; + _ -> + split_attribute(Tail, Acc++[H], H) + end. diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 68e1fc8a2..4389e4bce 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -1,8 +1,7 @@ - %%%---------------------------------------------------------------------- %%% File : mod_vcard_ldap.erl %%% Author : Alexey Shchepin <alexey@sevcom.net> -%%% Purpose : +%%% Purpose : Support for VCards from LDAP storage. %%% Created : 2 Jan 2003 by Alexey Shchepin <alexey@sevcom.net> %%% Id : $Id$ %%%---------------------------------------------------------------------- @@ -11,13 +10,26 @@ -author('alexey@sevcom.net'). -vsn('$Revision$ '). +-behaviour(gen_server). -behaviour(gen_mod). --export([start/2, init/3, stop/1, - get_sm_features/5, +%% gen_server callbacks. +-export([init/1, + handle_info/2, + handle_call/3, + handle_cast/2, + terminate/2, + code_change/3 + ]). + +-export([start/2, + start_link/2, + stop/1, + get_sm_features/5, process_local_iq/3, process_sm_iq/3, - remove_user/1]). + remove_user/1 + ]). -include("ejabberd.hrl"). -include("eldap/eldap.hrl"). @@ -25,69 +37,164 @@ -define(PROCNAME, ejabberd_mod_vcard_ldap). +-record(state, {serverhost, + myhost, + eldap_id, + search, + servers, + port, + dn, + base, + password, + uid, + uid_format, + vcard_map, + vcard_map_attrs, + user_filter, + search_filter, + search_fields, + search_reported, + search_reported_attrs + }). + +-define(VCARD_MAP, + [{"NICKNAME", "%u", []}, + {"FN", "%s", ["displayName"]}, + {"FAMILY", "%s", ["sn"]}, + {"GIVEN", "%s", ["givenName"]}, + {"MIDDLE", "%s", ["initials"]}, + {"ORGNAME", "%s", ["o"]}, + {"ORGUNIT", "%s", ["ou"]}, + {"CTRY", "%s", ["c"]}, + {"LOCALITY", "%s", ["l"]}, + {"STREET", "%s", ["street"]}, + {"REGION", "%s", ["st"]}, + {"PCODE", "%s", ["postalCode"]}, + {"TITLE", "%s", ["title"]}, + {"URL", "%s", ["labeleduri"]}, + {"DESC", "%s", ["description"]}, + {"TEL", "%s", ["telephoneNumber"]}, + {"EMAIL", "%s", ["mail"]}, + {"BDAY", "%s", ["birthDay"]}, + {"ROLE", "%s", ["employeeType"]}, + {"PHOTO", "%s", ["jpegPhoto"]} + ]). + +-define(SEARCH_FIELDS, + [{"User", "%u"}, + {"Full Name", "displayName"}, + {"Given Name", "givenName"}, + {"Middle Name", "initials"}, + {"Family Name", "sn"}, + {"Nickname", "%u"}, + {"Birthday", "birthDay"}, + {"Country", "c"}, + {"City", "l"}, + {"Email", "mail"}, + {"Organization Name", "o"}, + {"Organization Unit", "ou"} + ]). + +-define(SEARCH_REPORTED, + [{"Full Name", "FN"}, + {"Given Name", "GIVEN"}, + {"Middle Name", "MIDDLE"}, + {"Family Name", "FAMILY"}, + {"Nickname", "NICKNAME"}, + {"Birthday", "BDAY"}, + {"Country", "CTRY"}, + {"City", "LOCALITY"}, + {"Email", "EMAIL"}, + {"Organization Name", "ORGNAME"}, + {"Organization Unit", "ORGUNIT"} + ]). + +%% Unused callbacks. +handle_cast(_Request, State) -> + {noreply, State}. +code_change(_OldVsn, State, _Extra) -> + {ok, State}. +%% ----- + + start(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = { + Proc, {?MODULE, start_link, [Host, Opts]}, + permanent, 1000, worker, [?MODULE] + }, + supervisor:start_child(ejabberd_sup, ChildSpec). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:terminate_child(ejabberd_sup, Proc), + supervisor:delete_child(ejabberd_sup, Proc). + +terminate(_Reason, State) -> + Host = State#state.serverhost, + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD), + ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), + case State#state.search of + true -> + ejabberd_router:unregister_route(State#state.myhost); + _ -> + ok + end. + +start_link(Host, Opts) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). + +init([Host, Opts]) -> + State = parse_options(Host, Opts), IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue), gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_VCARD, ?MODULE, process_local_iq, IQDisc), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_VCARD, ?MODULE, process_sm_iq, IQDisc), ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, get_sm_features, 50), - LDAPServers = ejabberd_config:get_local_option({ldap_servers, Host}), - RootDN = ejabberd_config:get_local_option({ldap_rootdn, Host}), - Password = ejabberd_config:get_local_option({ldap_password, Host}), - eldap:start_link("mod_vcard_ldap", LDAPServers, 389, RootDN, Password), - MyHost = gen_mod:get_opt(host, Opts, "vjud." ++ Host), - Search = gen_mod:get_opt(search, Opts, true), - register(gen_mod:get_module_proc(Host, ?PROCNAME), - spawn(?MODULE, init, [MyHost, Host, Search])). - -init(Host, ServerHost, Search) -> - case Search of - false -> - loop(Host, ServerHost); + eldap:start_link(State#state.eldap_id, + State#state.servers, + State#state.port, + State#state.dn, + State#state.password), + case State#state.search of + true -> + ejabberd_router:register_route(State#state.myhost); _ -> - ejabberd_router:register_route(Host), - loop(Host, ServerHost) - end. - -loop(Host, ServerHost) -> - receive - {route, From, To, Packet} -> - case catch do_route(ServerHost, From, To, Packet) of - {'EXIT', Reason} -> - ?ERROR_MSG("~p", [Reason]); - _ -> - ok - end, - loop(Host, ServerHost); - stop -> - ejabberd_router:unregister_route(Host), - ok; + ok + end, + {ok, State}. + +handle_info({route, From, To, Packet}, State) -> + case catch do_route(State, From, To, Packet) of + {'EXIT', Reason} -> + Err = jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR), + ejabberd_router:route(To, From, Err), + %% Fail-Stop. Let the supervisor restarts us + {stop, Reason, State}; _ -> - loop(Host, ServerHost) - end. + {noreply, State} + end; -stop(Host) -> - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_VCARD), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_VCARD), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, get_sm_features, 50), - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - Proc ! stop, - {wait, Proc}. +handle_info(_Info, State) -> + {noreply, State}. get_sm_features({error, _Error} = Acc, _From, _To, _Node, _Lang) -> Acc; get_sm_features(Acc, _From, _To, Node, _Lang) -> case Node of - [] -> - case Acc of - {result, Features} -> - {result, [?NS_VCARD | Features]}; - empty -> - {result, [?NS_VCARD]} - end; - _ -> - Acc + [] -> + case Acc of + {result, Features} -> + {result, [?NS_VCARD | Features]}; + empty -> + {result, [?NS_VCARD]} + end; + _ -> + Acc end. process_local_iq(_From, _To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> @@ -102,7 +209,7 @@ process_local_iq(_From, _To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) [{xmlcdata, "ejabberd"}]}, {xmlelement, "URL", [], [{xmlcdata, - "http://ejabberd.jabberstudio.org/"}]}, + "http://ejabberd.jabber.ru/"}]}, {xmlelement, "DESC", [], [{xmlcdata, translate:translate( @@ -114,113 +221,165 @@ process_local_iq(_From, _To, #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) ]}]} end. -find_ldap_user(Host, User) -> - Attr = ejabberd_config:get_local_option({ldap_uidattr, Host}), - Filter = eldap:equalityMatch(Attr, User), - Base = ejabberd_config:get_local_option({ldap_base, Host}), - case eldap:search("mod_vcard_ldap", [{base, Base}, - {filter, Filter}, - {attributes, []}]) of - #eldap_search_result{entries = [E | _]} -> - E; - _ -> - false +-define(SM_IQ_TIMEOUT, 20000). + +process_sm_iq(From, #jid{lserver=LServer} = To, #iq{sub_el = SubEl} = IQ) -> + Proc = gen_mod:get_module_proc(LServer, ?PROCNAME), + case catch gen_server:call(Proc, + {process_sm_iq, From, To, IQ}, ?SM_IQ_TIMEOUT) of + {'EXIT', Reason} -> + case Reason of + {timeout, _} -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_REMOTE_SERVER_TIMEOUT]}; + _ -> + IQ#iq{type = error, + sub_el = [SubEl, ?ERR_INTERNAL_SERVER_ERROR]} + end; + Other -> + Other end. -is_attribute_read_allowed(Name,From,To) -> - true. - -ldap_attribute_to_vcard(Prefix,{Name,Values},From,To) -> - case is_attribute_read_allowed(Name,From,To) of - true -> - ldap_lca_to_vcard(Prefix,stringprep:tolower(Name),Values); +handle_call({process_sm_iq, _From, To, IQ}, _FromPid, State) -> + #iq{type = Type, sub_el = SubEl} = IQ, + Reply = case Type of + set -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; + get -> + #jid{luser = LUser} = To, + LServer = State#state.serverhost, + case ejabberd_auth:is_user_exists(LUser, LServer) of + true -> + VCardMap = State#state.vcard_map, + case find_ldap_user(LUser, State) of + #eldap_entry{attributes = Attributes} -> + Vcard = ldap_attributes_to_vcard(Attributes, VCardMap, {LUser, LServer}), + IQ#iq{type = result, sub_el = Vcard}; + _ -> + IQ#iq{type = result, sub_el = []} + end; + _ -> + IQ#iq{type = result, sub_el = []} + end + end, + {reply, Reply, State}; + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; + +handle_call(_Request, _From, State) -> + {reply, bad_request, State}. + +find_ldap_user(User, State) -> + Base = State#state.base, + RFC2254_Filter = State#state.user_filter, + Eldap_ID = State#state.eldap_id, + VCardAttrs = State#state.vcard_map_attrs, + case eldap_filter:parse(RFC2254_Filter, [{"%u", User}]) of + {ok, EldapFilter} -> + case eldap:search(Eldap_ID, [{base, Base}, + {filter, EldapFilter}, + {attributes, VCardAttrs}]) of + #eldap_search_result{entries = [E | _]} -> + E; + _ -> + false + end; _ -> - none + false end. -ldap_lca_to_vcard(vCard,"displayname",[Value|_]) -> +ldap_attributes_to_vcard(Attributes, VCardMap, UD) -> + Attrs = lists:map( + fun({VCardName, _, _}) -> + {stringprep:tolower(VCardName), + map_vcard_attr(VCardName, Attributes, VCardMap, UD)} + end, VCardMap), + Elts = [ldap_attribute_to_vcard(vCard, Attr) || Attr <- Attrs], + NElts = [ldap_attribute_to_vcard(vCardN, Attr) || Attr <- Attrs], + OElts = [ldap_attribute_to_vcard(vCardO, Attr) || Attr <- Attrs], + AElts = [ldap_attribute_to_vcard(vCardA, Attr) || Attr <- Attrs], + [{xmlelement, "vCard", [{"xmlns", ?NS_VCARD}], + lists:append([X || X <- Elts, X /= none], + [{xmlelement,"N",[], [X || X <- NElts, X /= none]}, + {xmlelement,"ORG",[], [X || X <- OElts, X /= none]}, + {xmlelement,"ADR",[], [X || X <- AElts, X /= none]}]) + }]. + +ldap_attribute_to_vcard(vCard, {"fn", Value}) -> {xmlelement,"FN",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCard,"uid",[Value|_]) -> +ldap_attribute_to_vcard(vCard, {"nickname", Value}) -> {xmlelement,"NICKNAME",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCard,"title",[Value|_]) -> +ldap_attribute_to_vcard(vCard, {"title", Value}) -> {xmlelement,"TITLE",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCard,"labeleduri",[Value|_]) -> +ldap_attribute_to_vcard(vCard, {"bday", Value}) -> + {xmlelement,"BDAY",[],[{xmlcdata,Value}]}; + +ldap_attribute_to_vcard(vCard, {"url", Value}) -> {xmlelement,"URL",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCard,"description",[Value|_]) -> +ldap_attribute_to_vcard(vCard, {"desc", Value}) -> {xmlelement,"DESC",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCard,"telephonenumber",[Value|_]) -> +ldap_attribute_to_vcard(vCard, {"role", Value}) -> + {xmlelement,"ROLE",[],[{xmlcdata,Value}]}; + +ldap_attribute_to_vcard(vCard, {"tel", Value}) -> {xmlelement,"TEL",[],[{xmlelement,"VOICE",[],[]}, {xmlelement,"WORK",[],[]}, {xmlelement,"NUMBER",[],[{xmlcdata,Value}]}]}; -ldap_lca_to_vcard(vCard,"mail",[Value|_]) -> +ldap_attribute_to_vcard(vCard, {"email", Value}) -> {xmlelement,"EMAIL",[],[{xmlelement,"INTERNET",[],[]}, {xmlelement,"PREF",[],[]}, {xmlelement,"USERID",[],[{xmlcdata,Value}]}]}; -ldap_lca_to_vcard(vCardN,"sn",[Value|_]) -> +ldap_attribute_to_vcard(vCard, {"photo", Value}) -> + {xmlelement,"PHOTO",[],[ + {xmlelement,"BINVAL",[],[{xmlcdata, jlib:encode_base64(Value)}]}]}; + +ldap_attribute_to_vcard(vCardN, {"family", Value}) -> {xmlelement,"FAMILY",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCardN,"givenname",[Value|_]) -> +ldap_attribute_to_vcard(vCardN, {"given", Value}) -> {xmlelement,"GIVEN",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCardN,"initials",[Value|_]) -> +ldap_attribute_to_vcard(vCardN, {"middle", Value}) -> {xmlelement,"MIDDLE",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCardO,"o",[Value|_]) -> +ldap_attribute_to_vcard(vCardO, {"orgname", Value}) -> {xmlelement,"ORGNAME",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(vCardO,"ou",[Value|_]) -> +ldap_attribute_to_vcard(vCardO, {"orgunit", Value}) -> {xmlelement,"ORGUNIT",[],[{xmlcdata,Value}]}; -ldap_lca_to_vcard(_,_,_) -> none. - -ldap_attributes_to_vcard(Attributes,From,To) -> - Elts = lists:map(fun(Attr) -> - ldap_attribute_to_vcard(vCard,Attr,From,To) - end,Attributes), - FElts = [ X || X <- Elts, X /= none ], - NElts = lists:map(fun(Attr) -> - ldap_attribute_to_vcard(vCardN,Attr,From,To) - end,Attributes), - FNElts = [ X || X <- NElts, X /= none ], - OElts = lists:map(fun(Attr) -> - ldap_attribute_to_vcard(vCardO,Attr,From,To) - end,Attributes), - FOElts = [ X || X <- OElts, X /= none ], - [{xmlelement, "vCard", [{"xmlns", ?NS_VCARD}], - lists:append(FElts, - [{xmlelement,"N",[],FNElts}, - {xmlelement,"ORG",[],FOElts}]) - }]. +ldap_attribute_to_vcard(vCardA, {"locality", Value}) -> + {xmlelement,"LOCALITY",[],[{xmlcdata,Value}]}; -process_sm_iq(From, To, #iq{type = Type, sub_el = SubEl} = IQ) -> - case Type of - set -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_NOT_ALLOWED]}; - get -> - #jid{luser = LUser, lserver = LServer} = To, - case find_ldap_user(LServer, LUser) of - #eldap_entry{attributes = Attributes} -> - Vcard = ldap_attributes_to_vcard(Attributes,From,To), - IQ#iq{type = result, sub_el = Vcard}; - _ -> - IQ#iq{type = result, sub_el = []} - end - end. +ldap_attribute_to_vcard(vCardA, {"street", Value}) -> + {xmlelement,"STREET",[],[{xmlcdata,Value}]}; + +ldap_attribute_to_vcard(vCardA, {"ctry", Value}) -> + {xmlelement,"CTRY",[],[{xmlcdata,Value}]}; + +ldap_attribute_to_vcard(vCardA, {"region", Value}) -> + {xmlelement,"REGION",[],[{xmlcdata,Value}]}; + +ldap_attribute_to_vcard(vCardA, {"pcode", Value}) -> + {xmlelement,"PCODE",[],[{xmlcdata,Value}]}; + +ldap_attribute_to_vcard(_, _) -> + none. -define(TLFIELD(Type, Label, Var), {xmlelement, "field", [{"type", Type}, {"label", translate:translate(Lang, Label)}, {"var", Var}], []}). - --define(FORM(JID), +-define(FORM(JID, SearchFields), [{xmlelement, "instructions", [], [{xmlcdata, translate:translate(Lang, "You need an x:data capable client to search")}]}, {xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}], @@ -229,30 +388,15 @@ process_sm_iq(From, To, #iq{type = Type, sub_el = SubEl} = IQ) -> jlib:jid_to_string(JID)}]}, {xmlelement, "instructions", [], [{xmlcdata, translate:translate(Lang, "Fill in fields to search " - "for any matching Jabber User")}]}, - ?TLFIELD("text-single", "User", "user"), - ?TLFIELD("text-single", "Full Name", "fn"), - ?TLFIELD("text-single", "Given Name", "given"), - ?TLFIELD("text-single", "Middle Name", "middle"), - ?TLFIELD("text-single", "Family Name", "family"), - ?TLFIELD("text-single", "Nickname", "nickname"), - ?TLFIELD("text-single", "Birthday", "bday"), - ?TLFIELD("text-single", "Country", "ctry"), - ?TLFIELD("text-single", "City", "locality"), - ?TLFIELD("text-single", "Email", "email"), - ?TLFIELD("text-single", "Organization Name", "orgname"), - ?TLFIELD("text-single", "Organization Unit", "orgunit") - ]}]). - + "for any matching Jabber User")}]} + ] ++ lists:map(fun({X,Y}) -> ?TLFIELD("text-single", X, Y) end, SearchFields)}]). - - -do_route(ServerHost, From, To, Packet) -> +do_route(State, From, To, Packet) -> #jid{user = User, resource = Resource} = To, if (User /= "") or (Resource /= "") -> Err = jlib:make_error_reply(Packet, ?ERR_SERVICE_UNAVAILABLE), - ejabberd_router ! {route, To, From, Err}; + ejabberd_router:route(To, From, Err); true -> IQ = jlib:iq_query_info(Packet), case IQ of @@ -285,24 +429,25 @@ do_route(ServerHost, From, To, Packet) -> [{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "result"}], - search_result(Lang, To, ServerHost, XData) + search_result(Lang, To, State, XData) }]}]}, ejabberd_router:route( To, From, jlib:iq_to_xml(ResIQ)) end end; get -> + SearchFields = State#state.search_fields, ResIQ = IQ#iq{type = result, sub_el = [{xmlelement, "query", [{"xmlns", ?NS_SEARCH}], - ?FORM(To) + ?FORM(To, SearchFields) }]}, ejabberd_router:route(To, From, jlib:iq_to_xml(ResIQ)) end; - #iq{type = Type, xmlns = ?NS_DISCO_INFO, sub_el = SubEl} -> + #iq{type = Type, xmlns = ?NS_DISCO_INFO} -> case Type of set -> Err = jlib:make_error_reply( @@ -330,7 +475,7 @@ do_route(ServerHost, From, To, Packet) -> From, jlib:iq_to_xml(ResIQ)) end; - #iq{type = Type, xmlns = ?NS_DISCO_ITEMS, sub_el = SubEl} -> + #iq{type = Type, xmlns = ?NS_DISCO_ITEMS} -> case Type of set -> Err = jlib:make_error_reply( @@ -369,223 +514,295 @@ iq_get_vcard(Lang) -> [{xmlcdata, "ejabberd/mod_vcard"}]}, {xmlelement, "URL", [], [{xmlcdata, - "http://ejabberd.jabberstudio.org/"}]}, + "http://ejabberd.jabber.ru/"}]}, {xmlelement, "DESC", [], [{xmlcdata, translate:translate( Lang, "ejabberd vCard module\n" "Copyright (c) 2003-2006 Alexey Shchepin")}]}]. -find_xdata_el({xmlelement, _Name, _Attrs, SubEls}) -> - find_xdata_el1(SubEls). - -find_xdata_el1([]) -> - false; -find_xdata_el1([{xmlelement, Name, Attrs, SubEls} | Els]) -> - case xml:get_attr_s("xmlns", Attrs) of - ?NS_XDATA -> - {xmlelement, Name, Attrs, SubEls}; - _ -> - find_xdata_el1(Els) - end; -find_xdata_el1([_ | Els]) -> - find_xdata_el1(Els). - -define(LFIELD(Label, Var), {xmlelement, "field", [{"label", translate:translate(Lang, Label)}, {"var", Var}], []}). -search_result(Lang, JID, ServerHost, Data) -> - [{xmlelement, "title", [], - [{xmlcdata, translate:translate(Lang, "Search Results for ") ++ - jlib:jid_to_string(JID)}]}, - {xmlelement, "reported", [], - [?LFIELD("Jabber ID", "jid"), - ?LFIELD("Full Name", "fn"), - ?LFIELD("Given Name", "given"), - ?LFIELD("Middle Name", "middle"), - ?LFIELD("Family Name", "family"), - ?LFIELD("Nickname", "nickname"), - ?LFIELD("Birthday", "bday"), - ?LFIELD("Country", "ctry"), - ?LFIELD("City", "locality"), - ?LFIELD("Email", "email"), - ?LFIELD("Organization Name", "orgname"), - ?LFIELD("Organization Unit", "orgunit") - ]}] ++ lists:map(fun(E) -> - record_to_item(E#eldap_entry.attributes) - end, search(ServerHost, Data)). +search_result(Lang, JID, State, Data) -> + SearchReported = State#state.search_reported, + Header = [{xmlelement, "title", [], + [{xmlcdata, translate:translate(Lang, "Search Results for ") ++ + jlib:jid_to_string(JID)}]}, + {xmlelement, "reported", [], + [?LFIELD("Jabber ID", "jid")] ++ + lists:map( + fun({Name, Value}) -> ?LFIELD(Name, Value) end, + SearchReported) + }], + case search(State, Data) of + error -> + Header; + Result -> + Header ++ Result + end. -define(FIELD(Var, Val), {xmlelement, "field", [{"var", Var}], [{xmlelement, "value", [], [{xmlcdata, Val}]}]}). -case_exact_compare(none,_) -> - false; -case_exact_compare(_,none) -> - false; -case_exact_compare(X,Y) -> - X > Y. - -ldap_sort_entries(L) -> - lists:sort(fun(E1,E2) -> - case_exact_compare(ldap_get_value(E1,"cn"),ldap_get_value(E2,"cn")) - end,L). - -ldap_get_value(E,Attribute) -> - #eldap_entry{attributes = Attributes} = E, - case lists:filter(fun({A,_}) -> - string:equal(A,Attribute) - end,Attributes) of - [{Attr,[Value|_]}] -> - Value; - _ -> - none - end. - - -ldap_attribute_to_item("uid",Value) -> - [ - ?FIELD("jid",Value ++ "@" ++ ?MYNAME), - ?FIELD("uid",Value), - ?FIELD("nickname",Value) - ]; - -ldap_attribute_to_item("displayname",Value) -> - [ - ?FIELD("fn",Value) - ]; - -ldap_attribute_to_item("sn",Value) -> - [ - ?FIELD("family",Value) - ]; - -ldap_attribute_to_item("displayname",Value) -> - [ - ?FIELD("fn",Value) - ]; - -ldap_attribute_to_item("givenname",Value) -> - [ - ?FIELD("given",Value) - ]; - -ldap_attribute_to_item("initials",Value) -> - [ - ?FIELD("middle",Value) - ]; - -ldap_attribute_to_item("mail",Value) -> - [ - ?FIELD("email",Value) - ]; - -ldap_attribute_to_item("o",Value) -> - [ - ?FIELD("orgname",Value) - ]; - -ldap_attribute_to_item("ou",Value) -> - [ - ?FIELD("orgunit",Value) - ]; - -ldap_attribute_to_item(_,_) -> - [none]. - -record_to_item(Attributes) -> - List = lists:append(lists:map(fun({Attr,[Value|_]}) -> - ldap_attribute_to_item(stringprep:tolower(Attr),Value) - end,Attributes)), - FList = [X || X <- List, X /= none], - {xmlelement, "item", [],FList}. - -search(LServer, Data) -> - Filter = make_filter(Data), - Base = ejabberd_config:get_local_option({ldap_base, LServer}), - UIDAttr = ejabberd_config:get_local_option({ldap_uidattr, LServer}), - case eldap:search("mod_vcard_ldap",[{base, Base}, - {filter, Filter}, - {attributes, []}]) of +search(State, Data) -> + Base = State#state.base, + SearchFilter = State#state.search_filter, + Eldap_ID = State#state.eldap_id, + UA = State#state.uid, + UAF = State#state.uid_format, + ReportedAttrs = State#state.search_reported_attrs, + Filter = eldap:'and'([SearchFilter, make_filter(Data, UA, UAF)]), + case eldap:search(Eldap_ID, [{base, Base}, + {filter, Filter}, + {attributes, ReportedAttrs}]) of #eldap_search_result{entries = E} -> - [X || X <- E, - ejabberd_auth:is_user_exists( - ldap_get_value(X, UIDAttr), LServer)]; - Err -> - ?ERROR_MSG("Bad search: ~p", [[LServer, {base, Base}, - {filter, Filter}, - {attributes, []}]]) + search_items(E, State); + _ -> + error end. - -make_filter(Data) -> - Filter = [X || X <- lists:map(fun(R) -> - make_assertion(R) - end, Data), - X /= none ], +search_items(Entries, State) -> + LServer = State#state.serverhost, + SearchReported = State#state.search_reported, + VCardMap = State#state.vcard_map, + UIDAttr = State#state.uid, + UIDAttrFormat = State#state.uid_format, + Attributes = lists:map( + fun(E) -> + #eldap_entry{attributes = Attrs} = E, + Attrs + end, Entries), + lists:flatmap( + fun(Attrs) -> + U = get_ldap_attr(UIDAttr, Attrs), + case get_user_part(U, UIDAttrFormat) of + {ok, Username} -> + case ejabberd_auth:is_user_exists(Username, LServer) of + true -> + RFields = lists:map( + fun({_, VCardName}) -> + {VCardName, + map_vcard_attr( + VCardName, + Attrs, + VCardMap, + {Username, ?MYNAME})} + end, SearchReported), + Result = [?FIELD("jid", Username ++ "@" ++ LServer)] ++ + [?FIELD(Name, Value) || {Name, Value} <- RFields], + [{xmlelement, "item", [], Result}]; + _ -> + [] + end; + _ -> + [] + end + end, Attributes). + +make_filter(Data, UAttr, UAttrFormat) -> + Filter = lists:flatmap( + fun({Name, [Value | _]}) -> + case Name of + "%u" when Value /= "" -> + {ok, UAF, _} = regexp:sub(UAttrFormat, "%u", "*%u*"), + case eldap_filter:parse( + "("++UAttr++"="++UAF++")", [{"%u", Value}]) of + {ok, F} -> [F]; + _ -> [] + end; + _ when Value /= "" -> + [eldap:substrings(Name, [{any, Value}])]; + _ -> + [] + end + end, Data), case Filter of - [F] -> + [F] -> F; _ -> eldap:'and'(Filter) end. +remove_user(_User) -> + true. -make_assertion("givenName",Value) -> - eldap:substrings("givenName",[{any,Value}]); - -make_assertion("cn",Value) -> - eldap:substrings("cn",[{any,Value}]); - -make_assertion("sn",Value) -> - eldap:substrings("sn",[{any,Value}]); - -make_assertion(Attr, Value) -> - eldap:equalityMatch(Attr,Value). - -make_assertion({SVar, [Val]}) -> - LAttr = ldap_attribute(SVar), - case LAttr of - none -> - none; - _ -> - if - is_list(Val) and (Val /= "") -> - make_assertion(LAttr,Val); - true -> - none +%%%----------------------- +%%% Auxiliary functions. +%%%----------------------- + +get_user_part(String, Pattern) -> + F = fun(S, P) -> + First = string:str(P, "%u"), + TailLength = length(P) - (First+1), + string:sub_string(S, First, length(S) - TailLength) + end, + case catch F(String, Pattern) of + {'EXIT', _} -> + {error, badmatch}; + Result -> + case regexp:sub(Pattern, "%u", Result) of + {ok, String, _} -> {ok, Result}; + _ -> {error, badmatch} end end. -ldap_attribute("user") -> - "uid"; - -ldap_attribute("fn") -> - "cn"; - -ldap_attribute("family") -> - "sn"; - -ldap_attribute("given") -> - "givenName"; - -ldap_attribute("middle") -> - "initials"; - -ldap_attribute("email") -> - "mail"; +case_insensitive_match(X, Y) -> + X1 = stringprep:tolower(X), + Y1 = stringprep:tolower(Y), + if + X1 == Y1 -> true; + true -> false + end. -ldap_attribute("orgname") -> - "o"; +map_vcard_attr(VCardName, Attributes, Pattern, UD) -> + Res = lists:filter( + fun({Name, _, _}) -> + case_insensitive_match(Name, VCardName) + end, Pattern), + case Res of + [{_, Str, Attrs}] -> + process_pattern(Str, UD, + [get_ldap_attr(X, Attributes) || X<-Attrs]); + _ -> "" + end. -ldap_attribute("orgunit") -> - "ou"; +process_pattern(Str, {User, Domain}, Attrs) -> + Res = lists:foldl( + fun(X, Acc) -> + {ok, NewStr, _} = regexp:sub(Acc, "%s", X), + NewStr + end, + Str, Attrs), + eldap_filter:do_sub(Res, [{"%u", User},{"%d", Domain}]). + +get_ldap_attr(LDAPAttr, Attributes) -> + Res = lists:filter( + fun({Name, _}) -> + case_insensitive_match(Name, LDAPAttr) + end, Attributes), + case Res of + [{_, [Value|_]}] -> Value; + _ -> "" + end. -ldap_attribute(_) -> - none. +find_xdata_el({xmlelement, _Name, _Attrs, SubEls}) -> + find_xdata_el1(SubEls). -remove_user(User) -> - true. +find_xdata_el1([]) -> + false; +find_xdata_el1([{xmlelement, Name, Attrs, SubEls} | Els]) -> + case xml:get_attr_s("xmlns", Attrs) of + ?NS_XDATA -> + {xmlelement, Name, Attrs, SubEls}; + _ -> + find_xdata_el1(Els) + end; +find_xdata_el1([_ | Els]) -> + find_xdata_el1(Els). +parse_options(Host, Opts) -> + MyHost = gen_mod:get_opt(host, Opts, "vjud." ++ Host), + Search = gen_mod:get_opt(search, Opts, true), + Eldap_ID = atom_to_list(gen_mod:get_module_proc(Host, ?PROCNAME)), + LDAPServers = case gen_mod:get_opt(ldap_servers, Opts, undefined) of + undefined -> + ejabberd_config:get_local_option({ldap_servers, Host}); + S -> S + end, + LDAPPort = case gen_mod:get_opt(ldap_port, Opts, undefined) of + undefined -> + case ejabberd_config:get_local_option({ldap_port, Host}) of + undefined -> 389; + P -> P + end; + P -> P + end, + LDAPBase = case gen_mod:get_opt(ldap_base, Opts, undefined) of + undefined -> + ejabberd_config:get_local_option({ldap_base, Host}); + B -> B + end, + UIDAttr = case gen_mod:get_opt(ldap_uidattr, Opts, undefined) of + undefined -> + case ejabberd_config:get_local_option({ldap_uidattr, Host}) of + undefined -> "uid"; + UA -> UA + end; + UA -> UA + end, + UIDAttrFormat = case gen_mod:get_opt(ldap_uidattr_format, Opts, undefined) of + undefined -> + case ejabberd_config:get_local_option({ldap_uidattr_format, Host}) of + undefined -> "%u"; + UAF -> UAF + end; + UAF -> UAF + end, + RootDN = case gen_mod:get_opt(ldap_rootdn, Opts, undefined) of + undefined -> + case ejabberd_config:get_local_option({ldap_rootdn, Host}) of + undefined -> ""; + RDN -> RDN + end; + RDN -> RDN + end, + Password = case gen_mod:get_opt(ldap_password, Opts, undefined) of + undefined -> + case ejabberd_config:get_local_option({ldap_password, Host}) of + undefined -> ""; + Pass -> Pass + end; + Pass -> Pass + end, + SubFilter = "("++UIDAttr++"="++UIDAttrFormat++")", + UserFilter = case gen_mod:get_opt(ldap_filter, Opts, undefined) of + undefined -> + case ejabberd_config:get_local_option({ldap_filter, Host}) of + undefined -> SubFilter; + "" -> SubFilter; + F -> "(&" ++ SubFilter ++ F ++ ")" + end; + "" -> SubFilter; + F -> "(&" ++ SubFilter ++ F ++ ")" + end, + {ok, SearchFilter} = eldap_filter:parse( + eldap_filter:do_sub(UserFilter, [{"%u","*"}])), + VCardMap = gen_mod:get_opt(ldap_vcard_map, Opts, ?VCARD_MAP), + SearchFields = gen_mod:get_opt(ldap_search_fields, Opts, ?SEARCH_FIELDS), + SearchReported = gen_mod:get_opt(ldap_search_reported, Opts, ?SEARCH_REPORTED), + %% In search requests we need to fetch only attributes defined + %% in vcard-map and search-reported. In some cases, + %% this will essentially reduce network traffic from an LDAP server. + VCardMapAttrs = lists:usort( + lists:append([A || {_, _, A} <- VCardMap]) ++ [UIDAttr]), + SearchReportedAttrs = + lists:usort(lists:flatmap( + fun({_, N}) -> + case lists:keysearch(N, 1, VCardMap) of + {value, {_, _, L}} -> L; + _ -> [] + end + end, SearchReported) ++ [UIDAttr]), + #state{serverhost = Host, + myhost = MyHost, + eldap_id = Eldap_ID, + search = Search, + servers = LDAPServers, + port = LDAPPort, + dn = RootDN, + base = LDAPBase, + password = Password, + uid = UIDAttr, + uid_format = UIDAttrFormat, + vcard_map = VCardMap, + vcard_map_attrs = VCardMapAttrs, + user_filter = UserFilter, + search_filter = SearchFilter, + search_fields = SearchFields, + search_reported = SearchReported, + search_reported_attrs = SearchReportedAttrs + }. |