diff options
author | Alexey Shchepin <alexey@process-one.net> | 2003-11-23 20:11:21 +0000 |
---|---|---|
committer | Alexey Shchepin <alexey@process-one.net> | 2003-11-23 20:11:21 +0000 |
commit | 0822a55f05bb327f0d362e0a3de205f5f1ce604a (patch) | |
tree | 288319f357281e47946b284b65c1ad70a70e5810 | |
parent | * examples/mtr/ejabberd: Updated (thanks to Marshall T. Rose) (diff) |
* src/cyrsasl_digest.erl: Bugfix (thanks to Sergei Golovan)
* src/ejabberd.cfg.example: Updated
* src/ejabberd_auth.erl: Support for LDAP authentification
* src/cyrsasl_digest.erl: Likewise
* src/mod_register.erl: Likewise
* src/ejabberd_c2s.erl: Likewise
* src/eldap/: Imported "eldap" package
* src/ejabberd_sm.erl: Bugfix
* src/mod_muc/mod_muc_room.erl: Bugfixes
SVN Revision: 176
-rw-r--r-- | ChangeLog | 21 | ||||
-rw-r--r-- | doc/guide.html | 10 | ||||
-rw-r--r-- | doc/guide.tex | 11 | ||||
-rw-r--r-- | src/Makefile.in | 2 | ||||
-rwxr-xr-x | src/configure | 28 | ||||
-rw-r--r-- | src/configure.ac | 2 | ||||
-rw-r--r-- | src/cyrsasl_digest.erl | 11 | ||||
-rw-r--r-- | src/ejabberd.cfg.example | 11 | ||||
-rw-r--r-- | src/ejabberd.hrl | 2 | ||||
-rw-r--r-- | src/ejabberd_auth.erl | 122 | ||||
-rw-r--r-- | src/ejabberd_c2s.erl | 23 | ||||
-rw-r--r-- | src/ejabberd_sm.erl | 24 | ||||
-rw-r--r-- | src/eldap/ELDAPv3.asn | 291 | ||||
-rw-r--r-- | src/eldap/Makefile.in | 36 | ||||
-rw-r--r-- | src/eldap/eldap.erl | 995 | ||||
-rw-r--r-- | src/eldap/eldap.hrl | 13 | ||||
-rw-r--r-- | src/mod_muc/mod_muc_room.erl | 33 | ||||
-rw-r--r-- | src/mod_register.erl | 2 |
18 files changed, 1600 insertions, 37 deletions
@@ -1,3 +1,24 @@ +2003-11-23 Alexey Shchepin <alexey@sevcom.net> + + * src/cyrsasl_digest.erl: Bugfix (thanks to Sergei Golovan) + + * src/ejabberd.cfg.example: Updated + + * src/ejabberd_auth.erl: Support for LDAP authentification + * src/cyrsasl_digest.erl: Likewise + * src/mod_register.erl: Likewise + * src/ejabberd_c2s.erl: Likewise + + * src/eldap/: Imported "eldap" package + + * src/ejabberd_sm.erl: Bugfix + +2003-11-16 Alexey Shchepin <alexey@sevcom.net> + + * src/mod_muc/mod_muc_room.erl: Bugfixes + + * (all): Version 0.5 released + 2003-11-13 Alexey Shchepin <alexey@sevcom.net> * examples/mtr/ejabberd: Updated (thanks to Marshall T. Rose) diff --git a/doc/guide.html b/doc/guide.html index 8430cbd5f..861864c44 100644 --- a/doc/guide.html +++ b/doc/guide.html @@ -129,6 +129,10 @@ Works on most of popular platforms: *nix (tested on Linux, FreeBSD and <A HREF="http://www.jabber.org/jeps/jep-0060.html">Publish-Subscribe</A> service <LI>Built-in Jabber Users Directory service based on users vCards +<LI>SSL support +<LI>Ability to interface with external components (JIT, MSN-t, Yahoo-t, etc) +<LI>Migration from jabberd14 is possible +<LI>Mostly XMPP-compliant <LI>Support for <A HREF="http://www.jabber.org/jeps/jep-0030.html">JEP-0030</A> (Service Discovery). @@ -137,6 +141,12 @@ Works on most of popular platforms: *nix (tested on Linux, FreeBSD and (Statistics Gathering). <LI>Support for <TT>xml:lang</TT> attribute in many XML elements </UL> +The misfeatures of <TT>ejabberd</TT> is: +<UL><LI> +No support for external authentification +<LI>No support for virtual domains +<LI>No support for STARTTLS +</UL> <!--TOC section Installation--> <H2><A NAME="htoc2">2</A> Installation</H2><!--SEC END --> diff --git a/doc/guide.tex b/doc/guide.tex index ed4072b59..d20912c81 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -78,6 +78,10 @@ The main features of \ejabberd{} is: \footahref{http://www.jabber.org/jeps/jep-0060.html}{Publish-Subscribe} service \item Built-in Jabber Users Directory service based on users vCards +\item SSL support +\item Ability to interface with external components (JIT, MSN-t, Yahoo-t, etc) +\item Migration from jabberd14 is possible +\item Mostly XMPP-compliant \item Support for \footahref{http://www.jabber.org/jeps/jep-0030.html}{JEP-0030} (Service Discovery). @@ -87,7 +91,12 @@ The main features of \ejabberd{} is: \item Support for \ns{xml:lang} attribute in many XML elements \end{itemize} - +The misfeatures of \ejabberd{} is: +\begin{itemize} +\item No support for external authentification +\item No support for virtual domains +\item No support for STARTTLS +\end{itemize} \section{Installation} diff --git a/src/Makefile.in b/src/Makefile.in index 6378dda1f..a73cc8471 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -12,7 +12,7 @@ INCLUDES = @ERLANG_CFLAGS@ @EXPAT_CFLAGS@ LIBDIRS = @ERLANG_LIBS@ @EXPAT_LIBS@ -SUBDIRS = @mod_irc@ @mod_pubsub@ @mod_muc@ stringprep +SUBDIRS = @mod_irc@ @mod_pubsub@ @mod_muc@ @eldap@ stringprep ERLSHLIBS = expat_erl.so diff --git a/src/configure b/src/configure index d4328b9a8..c80056810 100755 --- a/src/configure +++ b/src/configure @@ -828,6 +828,7 @@ Optional Features: --enable-mod_pubsub enable mod_pubsub (default: yes) --enable-mod_irc enable mod_irc (default: yes) --enable-mod_muc enable mod_muc (default: yes) + --enable-eldap enable eldap (default: yes) Optional Packages: --with-PACKAGE[=ARG] use PACKAGE [ARG=yes] @@ -3675,8 +3676,30 @@ echo "${ECHO_T}$mr_enable_mod_muc" >&6 +eldap= +make_eldap= +echo "$as_me:$LINENO: checking whether build eldap" >&5 +echo $ECHO_N "checking whether build eldap... $ECHO_C" >&6 +# Check whether --enable-eldap or --disable-eldap was given. +if test "${enable_eldap+set}" = set; then + enableval="$enable_eldap" + mr_enable_eldap="$enableval" +else + mr_enable_eldap=yes +fi; +if test "$mr_enable_eldap" = yes; then +eldap=eldap +make_eldap=eldap/Makefile +fi +echo "$as_me:$LINENO: result: $mr_enable_eldap" >&5 +echo "${ECHO_T}$mr_enable_eldap" >&6 + + + + + -ac_config_files="$ac_config_files Makefile $make_mod_irc $make_mod_muc $make_mod_pubsub stringprep/Makefile" +ac_config_files="$ac_config_files Makefile $make_mod_irc $make_mod_muc $make_mod_pubsub $make_eldap stringprep/Makefile" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -4178,6 +4201,7 @@ do "$make_mod_irc" ) CONFIG_FILES="$CONFIG_FILES $make_mod_irc" ;; "$make_mod_muc" ) CONFIG_FILES="$CONFIG_FILES $make_mod_muc" ;; "$make_mod_pubsub" ) CONFIG_FILES="$CONFIG_FILES $make_mod_pubsub" ;; + "$make_eldap" ) CONFIG_FILES="$CONFIG_FILES $make_eldap" ;; "stringprep/Makefile" ) CONFIG_FILES="$CONFIG_FILES stringprep/Makefile" ;; *) { { echo "$as_me:$LINENO: error: invalid argument: $ac_config_target" >&5 echo "$as_me: error: invalid argument: $ac_config_target" >&2;} @@ -4283,6 +4307,8 @@ s,@mod_irc@,$mod_irc,;t t s,@make_mod_irc@,$make_mod_irc,;t t s,@mod_muc@,$mod_muc,;t t s,@make_mod_muc@,$make_mod_muc,;t t +s,@eldap@,$eldap,;t t +s,@make_eldap@,$make_eldap,;t t CEOF _ACEOF diff --git a/src/configure.ac b/src/configure.ac index 643fe2626..e594927c7 100644 --- a/src/configure.ac +++ b/src/configure.ac @@ -29,11 +29,13 @@ AC_HEADER_STDC AC_MOD_ENABLE(mod_pubsub, yes) AC_MOD_ENABLE(mod_irc, yes) AC_MOD_ENABLE(mod_muc, yes) +AC_MOD_ENABLE(eldap, yes) AC_CONFIG_FILES([Makefile $make_mod_irc $make_mod_muc $make_mod_pubsub + $make_eldap stringprep/Makefile]) AC_OUTPUT diff --git a/src/cyrsasl_digest.erl b/src/cyrsasl_digest.erl index 5d768b926..ae94c181f 100644 --- a/src/cyrsasl_digest.erl +++ b/src/cyrsasl_digest.erl @@ -21,8 +21,13 @@ -record(state, {step, nonce, username, authzid}). start(Opts) -> - cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE), - ok. + case ejabberd_auth:plain_password_required() of + true -> + ok; + false -> + cyrsasl:register_mechanism("DIGEST-MD5", ?MODULE), + ok + end. stop() -> ok. @@ -31,7 +36,7 @@ mech_new() -> {ok, #state{step = 1, nonce = randoms:get_string()}}. -mech_step(#state{step = 1, nonce = Nonce} = State, "") -> +mech_step(#state{step = 1, nonce = Nonce} = State, _) -> {continue, "nonce=\"" ++ Nonce ++ "\",qop=\"auth\",charset=utf-8,algorithm=md5-sess", diff --git a/src/ejabberd.cfg.example b/src/ejabberd.cfg.example index 36b47fee5..baf46eae1 100644 --- a/src/ejabberd.cfg.example +++ b/src/ejabberd.cfg.example @@ -61,6 +61,17 @@ % Admins of this server are also admins of MUC service: {access, muc_admin, [{allow, admin}]}. + +% Authentification method. If you want to use internal user base, then use +% this line: +{auth_method, internal}. + +% For LDAP uthentification use these lines instead of above one: +%{auth_method, ldap}. +%{ldap_servers, ["localhost"]}. % List of LDAP servers +%{ldap_base, "dc=example,dc=com"}. % Base of LDAP directory + + % Host name: {host, "localhost"}. diff --git a/src/ejabberd.hrl b/src/ejabberd.hrl index 29d2fcb24..042e5f125 100644 --- a/src/ejabberd.hrl +++ b/src/ejabberd.hrl @@ -6,7 +6,7 @@ %%% Id : $Id$ %%%---------------------------------------------------------------------- --define(VERSION, "0.1-alpha"). +-define(VERSION, "0.6-alpha"). %-define(ejabberd_debug, true). %-define(DBGFSM, true). diff --git a/src/ejabberd_auth.erl b/src/ejabberd_auth.erl index 1503f7193..be7da007b 100644 --- a/src/ejabberd_auth.erl +++ b/src/ejabberd_auth.erl @@ -23,7 +23,11 @@ get_password_s/1, is_user_exists/1, remove_user/1, - remove_user/2]). + remove_user/2, + plain_password_required/0, + check_password_ldap/2, % TODO: remove + is_user_exists_ldap/1 % TODO: remove + ]). %% gen_server callbacks -export([init/1, @@ -33,6 +37,8 @@ handle_info/2, terminate/2]). +-include("eldap/eldap.hrl"). + -record(state, {}). -record(passwd, {user, password}). @@ -59,6 +65,13 @@ start_link() -> init([]) -> mnesia:create_table(passwd,[{disc_copies, [node()]}, {attributes, record_info(fields, passwd)}]), + case auth_method() of + internal -> + ok; + ldap -> + LDAPServers = ejabberd_config:get_local_option(ldap_servers), + eldap:start_link("ejabberd", LDAPServers, 389, "", "") + end, {ok, #state{}}. %%---------------------------------------------------------------------- @@ -108,7 +121,32 @@ terminate(_Reason, _State) -> %%% Internal functions %%%---------------------------------------------------------------------- +auth_method() -> + case ejabberd_config:get_local_option(auth_method) of + ldap -> + ldap; + _ -> + internal + end. + +plain_password_required() -> + case auth_method() of + internal -> + false; + ldap -> + true + end. + + check_password(User, Password) -> + case auth_method() of + internal -> + check_password_internal(User, Password); + ldap -> + check_password_ldap(User, Password) + end. + +check_password_internal(User, Password) -> LUser = jlib:nodeprep(User), case catch mnesia:dirty_read({passwd, LUser}) of [#passwd{password = Password}] -> @@ -118,6 +156,14 @@ check_password(User, Password) -> end. check_password(User, Password, StreamID, Digest) -> + case auth_method() of + internal -> + check_password_internal(User, Password, StreamID, Digest); + ldap -> + check_password_ldap(User, Password, StreamID, Digest) + end. + +check_password_internal(User, Password, StreamID, Digest) -> LUser = jlib:nodeprep(User), case catch mnesia:dirty_read({passwd, LUser}) of [#passwd{password = Passwd}] -> @@ -148,7 +194,16 @@ set_password(User, Password) -> mnesia:transaction(F) end. + try_register(User, Password) -> + case auth_method() of + internal -> + try_register_internal(User, Password); + ldap -> + {error, not_allowed} + end. + +try_register_internal(User, Password) -> case jlib:nodeprep(User) of error -> {error, invalid_jid}; LUser -> @@ -187,6 +242,14 @@ get_password_s(User) -> end. is_user_exists(User) -> + case auth_method() of + internal -> + is_user_exists_internal(User); + ldap -> + is_user_exists_ldap(User) + end. + +is_user_exists_internal(User) -> LUser = jlib:nodeprep(User), case catch mnesia:dirty_read({passwd, LUser}) of [] -> @@ -198,6 +261,14 @@ is_user_exists(User) -> end. remove_user(User) -> + case auth_method() of + internal -> + remove_user_internal(User); + ldap -> + {error, not_allowed} + end. + +remove_user_internal(User) -> LUser = jlib:nodeprep(User), F = fun() -> mnesia:delete({passwd, LUser}) @@ -210,6 +281,14 @@ remove_user(User) -> catch mod_private:remove_user(User). remove_user(User, Password) -> + case auth_method() of + internal -> + remove_user_internal(User, Password); + ldap -> + not_allowed + end. + +remove_user_internal(User, Password) -> LUser = jlib:nodeprep(User), F = fun() -> case mnesia:read({passwd, LUser}) of @@ -236,3 +315,44 @@ remove_user(User, Password) -> bad_request end. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +check_password_ldap(User, Password, StreamID, Digest) -> + check_password_ldap(User, Password). + +check_password_ldap(User, Password) -> + case find_user_dn(User) of + false -> + false; + DN -> + case eldap:bind("ejabberd", DN, Password) of + ok -> + true; + _ -> + false + end + end. + +is_user_exists_ldap(User) -> + case find_user_dn(User) of + false -> + false; + _DN -> + true + end. + +find_user_dn(User) -> + Filter = eldap:equalityMatch("uid", User), + Base = ejabberd_config:get_local_option(ldap_base), + case eldap:search("ejabberd", [{base, Base}, + {filter, Filter}, + {attributes, []}]) of + #eldap_search_result{entries = [E | _]} -> + E#eldap_entry.object_name; + _ -> + false + end. + + + diff --git a/src/ejabberd_c2s.erl b/src/ejabberd_c2s.erl index 222436da6..fa7d8f726 100644 --- a/src/ejabberd_c2s.erl +++ b/src/ejabberd_c2s.erl @@ -201,13 +201,22 @@ wait_for_auth({xmlstreamelement, El}, StateData) -> {next_state, wait_for_auth, StateData}; {auth, ID, get, {U, _, _, _}} -> {xmlelement, Name, Attrs, Els} = jlib:make_result_iq_reply(El), - Res = {xmlelement, Name, Attrs, - [{xmlelement, "query", [{"xmlns", ?NS_AUTH}], - [{xmlelement, "username", [], [{xmlcdata, U}]}, - {xmlelement, "password", [], []}, - {xmlelement, "digest", [], []}, - {xmlelement, "resource", [], []} - ]}]}, + Res = case ejabberd_auth:plain_password_required() of + false -> + {xmlelement, Name, Attrs, + [{xmlelement, "query", [{"xmlns", ?NS_AUTH}], + [{xmlelement, "username", [], [{xmlcdata, U}]}, + {xmlelement, "password", [], []}, + {xmlelement, "digest", [], []}, + {xmlelement, "resource", [], []} + ]}]}; + true -> + {xmlelement, Name, Attrs, + [{xmlelement, "query", [{"xmlns", ?NS_AUTH}], + [{xmlelement, "username", [], [{xmlcdata, U}]}, + {xmlelement, "password", [], []} + ]}]} + end, send_element(StateData, Res), {next_state, wait_for_auth, StateData}; {auth, ID, set, {U, P, D, ""}} -> diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index e58562e85..2b3cec75f 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -298,12 +298,12 @@ route_message(From, To, Packet) -> #jid{luser = LUser} = To, case catch lists:max(get_user_present_resources(LUser)) of {'EXIT', _} -> - case ejabberd_auth:is_user_exists(LUser) of - true -> - case xml:get_tag_attr_s("type", Packet) of - "error" -> - ok; - _ -> + case xml:get_tag_attr_s("type", Packet) of + "error" -> + ok; + _ -> + case ejabberd_auth:is_user_exists(LUser) of + true -> case catch mod_offline:store_packet( From, To, Packet) of {'EXIT', _} -> @@ -312,12 +312,12 @@ route_message(From, To, Packet) -> ejabberd_router:route(To, From, Err); _ -> ok - end - end; - _ -> - Err = jlib:make_error_reply( - Packet, ?ERR_ITEM_NOT_FOUND), - ejabberd_router:route(To, From, Err) + end; + _ -> + Err = jlib:make_error_reply( + Packet, ?ERR_ITEM_NOT_FOUND), + ejabberd_router:route(To, From, Err) + end end; {_, R} -> ejabberd_sm ! {route, diff --git a/src/eldap/ELDAPv3.asn b/src/eldap/ELDAPv3.asn new file mode 100644 index 000000000..0cfac48c3 --- /dev/null +++ b/src/eldap/ELDAPv3.asn @@ -0,0 +1,291 @@ +-- LDAPv3 ASN.1 specification, taken from RFC 2251 + +-- Lightweight-Directory-Access-Protocol-V3 DEFINITIONS +ELDAPv3 DEFINITIONS +IMPLICIT TAGS ::= + +BEGIN + +LDAPMessage ::= SEQUENCE { + messageID MessageID, + protocolOp CHOICE { + bindRequest BindRequest, + bindResponse BindResponse, + unbindRequest UnbindRequest, + searchRequest SearchRequest, + searchResEntry SearchResultEntry, + searchResDone SearchResultDone, + searchResRef SearchResultReference, + modifyRequest ModifyRequest, + modifyResponse ModifyResponse, + addRequest AddRequest, + addResponse AddResponse, + delRequest DelRequest, + delResponse DelResponse, + modDNRequest ModifyDNRequest, + modDNResponse ModifyDNResponse, + compareRequest CompareRequest, + compareResponse CompareResponse, + abandonRequest AbandonRequest, + extendedReq ExtendedRequest, + extendedResp ExtendedResponse }, + controls [0] Controls OPTIONAL } + +MessageID ::= INTEGER (0 .. maxInt) + +maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) -- + +LDAPString ::= OCTET STRING + +LDAPOID ::= OCTET STRING + +LDAPDN ::= LDAPString + +RelativeLDAPDN ::= LDAPString + +AttributeType ::= LDAPString + +AttributeDescription ::= LDAPString + + + + +-- Wahl, et. al. Standards Track [Page 44] +-- +-- RFC 2251 LDAPv3 December 1997 + + +AttributeDescriptionList ::= SEQUENCE OF + AttributeDescription + +AttributeValue ::= OCTET STRING + +AttributeValueAssertion ::= SEQUENCE { + attributeDesc AttributeDescription, + assertionValue AssertionValue } + +AssertionValue ::= OCTET STRING + +Attribute ::= SEQUENCE { + type AttributeDescription, + vals SET OF AttributeValue } + +MatchingRuleId ::= LDAPString + +LDAPResult ::= SEQUENCE { + resultCode ENUMERATED { + success (0), + operationsError (1), + protocolError (2), + timeLimitExceeded (3), + sizeLimitExceeded (4), + compareFalse (5), + compareTrue (6), + authMethodNotSupported (7), + strongAuthRequired (8), + -- 9 reserved -- + referral (10), -- new + adminLimitExceeded (11), -- new + unavailableCriticalExtension (12), -- new + confidentialityRequired (13), -- new + saslBindInProgress (14), -- new + noSuchAttribute (16), + undefinedAttributeType (17), + inappropriateMatching (18), + constraintViolation (19), + attributeOrValueExists (20), + invalidAttributeSyntax (21), + -- 22-31 unused -- + noSuchObject (32), + aliasProblem (33), + invalidDNSyntax (34), + -- 35 reserved for undefined isLeaf -- + aliasDereferencingProblem (36), + -- 37-47 unused -- + inappropriateAuthentication (48), + +-- Wahl, et. al. Standards Track [Page 45] +-- +-- RFC 2251 LDAPv3 December 1997 + + + invalidCredentials (49), + insufficientAccessRights (50), + busy (51), + unavailable (52), + unwillingToPerform (53), + loopDetect (54), + -- 55-63 unused -- + namingViolation (64), + objectClassViolation (65), + notAllowedOnNonLeaf (66), + notAllowedOnRDN (67), + entryAlreadyExists (68), + objectClassModsProhibited (69), + -- 70 reserved for CLDAP -- + affectsMultipleDSAs (71), -- new + -- 72-79 unused -- + other (80) }, + -- 81-90 reserved for APIs -- + matchedDN LDAPDN, + errorMessage LDAPString, + referral [3] Referral OPTIONAL } + +Referral ::= SEQUENCE OF LDAPURL + +LDAPURL ::= LDAPString -- limited to characters permitted in URLs + +Controls ::= SEQUENCE OF Control + +Control ::= SEQUENCE { + controlType LDAPOID, + criticality BOOLEAN DEFAULT FALSE, + controlValue OCTET STRING OPTIONAL } + +BindRequest ::= [APPLICATION 0] SEQUENCE { + version INTEGER (1 .. 127), + name LDAPDN, + authentication AuthenticationChoice } + +AuthenticationChoice ::= CHOICE { + simple [0] OCTET STRING, + -- 1 and 2 reserved + sasl [3] SaslCredentials } + +SaslCredentials ::= SEQUENCE { + mechanism LDAPString, + credentials OCTET STRING OPTIONAL } + +BindResponse ::= [APPLICATION 1] SEQUENCE { + +-- Wahl, et. al. Standards Track [Page 46] +-- +-- RFC 2251 LDAPv3 December 1997 + + + COMPONENTS OF LDAPResult, + serverSaslCreds [7] OCTET STRING OPTIONAL } + +UnbindRequest ::= [APPLICATION 2] NULL + +SearchRequest ::= [APPLICATION 3] SEQUENCE { + baseObject LDAPDN, + scope ENUMERATED { + baseObject (0), + singleLevel (1), + wholeSubtree (2) }, + derefAliases ENUMERATED { + neverDerefAliases (0), + derefInSearching (1), + derefFindingBaseObj (2), + derefAlways (3) }, + sizeLimit INTEGER (0 .. maxInt), + timeLimit INTEGER (0 .. maxInt), + typesOnly BOOLEAN, + filter Filter, + attributes AttributeDescriptionList } + +Filter ::= CHOICE { + and [0] SET OF Filter, + or [1] SET OF Filter, + not [2] Filter, + equalityMatch [3] AttributeValueAssertion, + substrings [4] SubstringFilter, + greaterOrEqual [5] AttributeValueAssertion, + lessOrEqual [6] AttributeValueAssertion, + present [7] AttributeDescription, + approxMatch [8] AttributeValueAssertion, + extensibleMatch [9] MatchingRuleAssertion } + +SubstringFilter ::= SEQUENCE { + type AttributeDescription, + -- at least one must be present + substrings SEQUENCE OF CHOICE { + initial [0] LDAPString, + any [1] LDAPString, + final [2] LDAPString } } + +MatchingRuleAssertion ::= SEQUENCE { + matchingRule [1] MatchingRuleId OPTIONAL, + type [2] AttributeDescription OPTIONAL, + matchValue [3] AssertionValue, + dnAttributes [4] BOOLEAN DEFAULT FALSE } + +-- Wahl, et. al. Standards Track [Page 47] +-- +-- RFC 2251 LDAPv3 December 1997 + +SearchResultEntry ::= [APPLICATION 4] SEQUENCE { + objectName LDAPDN, + attributes PartialAttributeList } + +PartialAttributeList ::= SEQUENCE OF SEQUENCE { + type AttributeDescription, + vals SET OF AttributeValue } + +SearchResultReference ::= [APPLICATION 19] SEQUENCE OF LDAPURL + +SearchResultDone ::= [APPLICATION 5] LDAPResult + +ModifyRequest ::= [APPLICATION 6] SEQUENCE { + object LDAPDN, + modification SEQUENCE OF SEQUENCE { + operation ENUMERATED { + add (0), + delete (1), + replace (2) }, + modification AttributeTypeAndValues } } + +AttributeTypeAndValues ::= SEQUENCE { + type AttributeDescription, + vals SET OF AttributeValue } + +ModifyResponse ::= [APPLICATION 7] LDAPResult + +AddRequest ::= [APPLICATION 8] SEQUENCE { + entry LDAPDN, + attributes AttributeList } + +AttributeList ::= SEQUENCE OF SEQUENCE { + type AttributeDescription, + vals SET OF AttributeValue } + +AddResponse ::= [APPLICATION 9] LDAPResult + +DelRequest ::= [APPLICATION 10] LDAPDN + +DelResponse ::= [APPLICATION 11] LDAPResult + +ModifyDNRequest ::= [APPLICATION 12] SEQUENCE { + entry LDAPDN, + newrdn RelativeLDAPDN, + deleteoldrdn BOOLEAN, + newSuperior [0] LDAPDN OPTIONAL } + +ModifyDNResponse ::= [APPLICATION 13] LDAPResult + +-- Wahl, et. al. Standards Track [Page 48] +-- +-- RFC 2251 LDAPv3 December 1997 + + +CompareRequest ::= [APPLICATION 14] SEQUENCE { + entry LDAPDN, + ava AttributeValueAssertion } + +CompareResponse ::= [APPLICATION 15] LDAPResult + +AbandonRequest ::= [APPLICATION 16] MessageID + +ExtendedRequest ::= [APPLICATION 23] SEQUENCE { + requestName [0] LDAPOID, + requestValue [1] OCTET STRING OPTIONAL } + +ExtendedResponse ::= [APPLICATION 24] SEQUENCE { + COMPONENTS OF LDAPResult, + responseName [10] LDAPOID OPTIONAL, + response [11] OCTET STRING OPTIONAL } + +END + + diff --git a/src/eldap/Makefile.in b/src/eldap/Makefile.in new file mode 100644 index 000000000..b0abb25eb --- /dev/null +++ b/src/eldap/Makefile.in @@ -0,0 +1,36 @@ +# $Id$ + +CC = @CC@ +CFLAGS = @CFLAGS@ +CPPFLAGS = @CPPFLAGS@ +LDFLAGS = @LDFLAGS@ +LIBS = @LIBS@ + +INCLUDES = @ERLANG_CFLAGS@ + +LIBDIRS = @ERLANG_LIBS@ + +SUBDIRS = + + +OUTDIR = .. +EFLAGS = -I .. -pz .. +OBJS = \ + $(OUTDIR)/eldap.beam \ + $(OUTDIR)/ELDAPv3.beam + +all: $(OBJS) + +ELDAPv3.erl: ELDAPv3.asn + erlc -bber_bin -W $(EFLAGS) $< + +$(OUTDIR)/%.beam: %.erl ELDAPv3.erl + erlc -W $(EFLAGS) -o $(OUTDIR) $< + + +clean: + rm -f $(OBJS) + +TAGS: + etags *.erl + diff --git a/src/eldap/eldap.erl b/src/eldap/eldap.erl new file mode 100644 index 000000000..939a45e7e --- /dev/null +++ b/src/eldap/eldap.erl @@ -0,0 +1,995 @@ +-module(eldap). +%%% -------------------------------------------------------------------- +%%% Created: 12 Oct 2000 by Tobbe <tnt@home.se> +%%% Function: Erlang client LDAP implementation according RFC 2251. +%%% The interface is based on RFC 1823, and +%%% draft-ietf-asid-ldap-c-api-00.txt +%%% +%%% Copyright (C) 2000 Torbjörn Törnkvist, tnt@home.se +%%% +%%% 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +%%% Modified by Sean Hinde <shinde@iee.org> 7th Dec 2000 +%%% Turned into gen_fsm, made non-blocking, added timers etc to support this. +%%% Now has the concept of a name (string() or atom()) per instance which allows +%%% multiple users to call by name if so desired. +%%% +%%% Can be configured with start_link parameters or use a config file to get +%%% host to connect to, dn, password, log function etc. + + +%%% Modified by Alexey Shchepin <alexey@sevcom.net> +%%% -------------------------------------------------------------------- +-vc('$Id$ '). + + +%%%---------------------------------------------------------------------- +%%% LDAP Client state machine. +%%% Possible states are: +%%% connecting - actually disconnected, but retrying periodically +%%% wait_bind_response - connected and sent bind request +%%% active - bound to LDAP Server and ready to handle commands +%%%---------------------------------------------------------------------- + +%%-compile(export_all). +%%-export([Function/Arity, ...]). + +-behaviour(gen_fsm). + +%% External exports +-export([start_link/1, start_link/5, start_link/6]). + +-export([baseObject/0,singleLevel/0,wholeSubtree/0,close/1, + equalityMatch/2,greaterOrEqual/2,lessOrEqual/2, + approxMatch/2,search/2,substrings/2,present/1, + 'and'/1,'or'/1,'not'/1,modify/3, mod_add/2, mod_delete/2, + mod_replace/2, add/3, delete/2, modify_dn/5, bind/3]). +-export([debug_level/2, get_status/1]). + +%% gen_fsm callbacks +-export([init/1, connecting/2, + connecting/3, wait_bind_response/3, active/3, handle_event/3, + handle_sync_event/4, handle_info/3, terminate/3, code_change/4]). + + +-import(lists,[concat/1]). + +-include("ELDAPv3.hrl"). +-include("eldap.hrl"). + +-define(LDAP_VERSION, 3). +-define(RETRY_TIMEOUT, 5000). +-define(BIND_TIMEOUT, 10000). +-define(CMD_TIMEOUT, 5000). +-define(MAX_TRANSACTION_ID, 65535). +-define(MIN_TRANSACTION_ID, 0). + +-record(eldap, {version = ?LDAP_VERSION, + hosts, % Possible hosts running LDAP servers + host = null, % Connected Host LDAP server + port = 389 , % The LDAP server port + fd = null, % Socket filedescriptor. + rootdn = "", % Name of the entry to bind as + passwd, % Password for (above) entry + id = 0, % LDAP Request ID + log, % User provided log function + bind_timer, % Ref to bind timeout + dict, % dict holding operation params and results + debug_level % Integer debug/logging level + }). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start_link(Name) -> + Reg_name = list_to_atom("eldap_" ++ Name), + gen_fsm:start_link({local, Reg_name}, ?MODULE, [], []). + +start_link(Name, Hosts, Port, Rootdn, Passwd) -> + Log = fun(N, Fmt, Args) -> io:format("---- " ++ Fmt, [Args]) end, + Reg_name = list_to_atom("eldap_" ++ Name), + gen_fsm:start_link({local, Reg_name}, ?MODULE, {Hosts, Port, Rootdn, Passwd, Log}, []). + +start_link(Name, Hosts, Port, Rootdn, Passwd, Log) -> + Reg_name = list_to_atom("eldap_" ++ Name), + gen_fsm:start_link({local, Reg_name}, ?MODULE, {Hosts, Port, Rootdn, Passwd, Log}, []). + +%%% -------------------------------------------------------------------- +%%% Set Debug Level. 0 - none, 1 - errors, 2 - ldap events +%%% -------------------------------------------------------------------- +debug_level(Handle, N) when integer(N) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_all_state_event(Handle1, {debug_level,N}). + +%%% -------------------------------------------------------------------- +%%% Get status of connection. +%%% -------------------------------------------------------------------- +get_status(Handle) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_all_state_event(Handle1, get_status). + +%%% -------------------------------------------------------------------- +%%% Shutdown connection (and process) asynchronous. +%%% -------------------------------------------------------------------- +close(Handle) -> + Handle1 = get_handle(Handle), + gen_fsm:send_all_state_event(Handle1, close). + +%%% -------------------------------------------------------------------- +%%% Add an entry. The entry field MUST NOT exist for the AddRequest +%%% to succeed. The parent of the entry MUST exist. +%%% Example: +%%% +%%% add(Handle, +%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com", +%%% [{"objectclass", ["person"]}, +%%% {"cn", ["Bill Valentine"]}, +%%% {"sn", ["Valentine"]}, +%%% {"telephoneNumber", ["545 555 00"]}] +%%% ) +%%% -------------------------------------------------------------------- +add(Handle, Entry, Attributes) when list(Entry),list(Attributes) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {add, Entry, add_attrs(Attributes)}). + +%%% Do sanity check ! +add_attrs(Attrs) -> + F = fun({Type,Vals}) when list(Type),list(Vals) -> + %% Confused ? Me too... :-/ + {'AddRequest_attributes',Type, Vals} + end, + case catch lists:map(F, Attrs) of + {'EXIT', _} -> throw({error, attribute_values}); + Else -> Else + end. + + +%%% -------------------------------------------------------------------- +%%% Delete an entry. The entry consists of the DN of +%%% the entry to be deleted. +%%% Example: +%%% +%%% delete(Handle, +%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com" +%%% ) +%%% -------------------------------------------------------------------- +delete(Handle, Entry) when list(Entry) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {delete, Entry}). + +%%% -------------------------------------------------------------------- +%%% Modify an entry. Given an entry a number of modification +%%% operations can be performed as one atomic operation. +%%% Example: +%%% +%%% modify(Handle, +%%% "cn=Torbjorn Tornkvist, ou=people, o=Bluetail AB, dc=bluetail, dc=com", +%%% [replace("telephoneNumber", ["555 555 00"]), +%%% add("description", ["LDAP hacker"])] +%%% ) +%%% -------------------------------------------------------------------- +modify(Handle, Object, Mods) when list(Object), list(Mods) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {modify, Object, Mods}). + +%%% +%%% Modification operations. +%%% Example: +%%% replace("telephoneNumber", ["555 555 00"]) +%%% +mod_add(Type, Values) when list(Type), list(Values) -> m(add, Type, Values). +mod_delete(Type, Values) when list(Type), list(Values) -> m(delete, Type, Values). +mod_replace(Type, Values) when list(Type), list(Values) -> m(replace, Type, Values). + +m(Operation, Type, Values) -> + #'ModifyRequest_modification_SEQOF'{ + operation = Operation, + modification = #'AttributeTypeAndValues'{ + type = Type, + vals = Values}}. + +%%% -------------------------------------------------------------------- +%%% Modify an entry. Given an entry a number of modification +%%% operations can be performed as one atomic operation. +%%% Example: +%%% +%%% modify_dn(Handle, +%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com", +%%% "cn=Ben Emerson", +%%% true, +%%% "" +%%% ) +%%% -------------------------------------------------------------------- +modify_dn(Handle, Entry, NewRDN, DelOldRDN, NewSup) + when list(Entry),list(NewRDN),atom(DelOldRDN),list(NewSup) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {modify_dn, Entry, NewRDN, bool_p(DelOldRDN), optional(NewSup)}). + + +%%% -------------------------------------------------------------------- +%%% Bind. +%%% Example: +%%% +%%% bind(Handle, +%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com", +%%% "secret") +%%% -------------------------------------------------------------------- +bind(Handle, RootDN, Passwd) + when list(RootDN),list(Passwd) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {bind, RootDN, Passwd}). + +%%% Sanity checks ! + +bool_p(Bool) when Bool==true;Bool==false -> Bool. + +optional([]) -> asn1_NOVALUE; +optional(Value) -> Value. + +%%% -------------------------------------------------------------------- +%%% Synchronous search of the Directory returning a +%%% requested set of attributes. +%%% +%%% Example: +%%% +%%% Filter = eldap:substrings("sn", [{any,"o"}]), +%%% eldap:search(S, [{base, "dc=bluetail, dc=com"}, +%%% {filter, Filter}, +%%% {attributes,["cn"]}])), +%%% +%%% Returned result: {ok, #eldap_search_result{}} +%%% +%%% Example: +%%% +%%% {ok,{eldap_search_result, +%%% [{eldap_entry, +%%% "cn=Magnus Froberg, dc=bluetail, dc=com", +%%% [{"cn",["Magnus Froberg"]}]}, +%%% {eldap_entry, +%%% "cn=Torbjorn Tornkvist, dc=bluetail, dc=com", +%%% [{"cn",["Torbjorn Tornkvist"]}]}], +%%% []}} +%%% +%%% -------------------------------------------------------------------- +search(Handle, A) when record(A, eldap_search) -> + call_search(Handle, A); +search(Handle, L) when list(Handle), list(L) -> + case catch parse_search_args(L) of + {error, Emsg} -> {error, Emsg}; + {'EXIT', Emsg} -> {error, Emsg}; + A when record(A, eldap_search) -> call_search(Handle, A) + end. + +call_search(Handle, A) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {search, A}). + +parse_search_args(Args) -> + parse_search_args(Args, #eldap_search{scope = wholeSubtree}). + +parse_search_args([{base, Base}|T],A) -> + parse_search_args(T,A#eldap_search{base = Base}); +parse_search_args([{filter, Filter}|T],A) -> + parse_search_args(T,A#eldap_search{filter = Filter}); +parse_search_args([{scope, Scope}|T],A) -> + parse_search_args(T,A#eldap_search{scope = Scope}); +parse_search_args([{attributes, Attrs}|T],A) -> + parse_search_args(T,A#eldap_search{attributes = Attrs}); +parse_search_args([{types_only, TypesOnly}|T],A) -> + parse_search_args(T,A#eldap_search{types_only = TypesOnly}); +parse_search_args([{timeout, Timeout}|T],A) when integer(Timeout) -> + parse_search_args(T,A#eldap_search{timeout = Timeout}); +parse_search_args([H|T],A) -> + throw({error,{unknown_arg, H}}); +parse_search_args([],A) -> + A. + +%%% +%%% The Scope parameter +%%% +baseObject() -> baseObject. +singleLevel() -> singleLevel. +wholeSubtree() -> wholeSubtree. + +%%% +%%% Boolean filter operations +%%% +'and'(ListOfFilters) when list(ListOfFilters) -> {'and',ListOfFilters}. +'or'(ListOfFilters) when list(ListOfFilters) -> {'or', ListOfFilters}. +'not'(Filter) when tuple(Filter) -> {'not',Filter}. + +%%% +%%% The following Filter parameters consist of an attribute +%%% and an attribute value. Example: F("uid","tobbe") +%%% +equalityMatch(Desc, Value) -> {equalityMatch, av_assert(Desc, Value)}. +greaterOrEqual(Desc, Value) -> {greaterOrEqual, av_assert(Desc, Value)}. +lessOrEqual(Desc, Value) -> {lessOrEqual, av_assert(Desc, Value)}. +approxMatch(Desc, Value) -> {approxMatch, av_assert(Desc, Value)}. + +av_assert(Desc, Value) -> + #'AttributeValueAssertion'{attributeDesc = Desc, + assertionValue = Value}. + +%%% +%%% Filter to check for the presence of an attribute +%%% +present(Attribute) when list(Attribute) -> + {present, Attribute}. + + +%%% +%%% A substring filter seem to be based on a pattern: +%%% +%%% InitValue*AnyValue*FinalValue +%%% +%%% where all three parts seem to be optional (at least when +%%% talking with an OpenLDAP server). Thus, the arguments +%%% to substrings/2 looks like this: +%%% +%%% Type ::= string( <attribute> ) +%%% SubStr ::= listof( {initial,Value} | {any,Value}, {final,Value}) +%%% +%%% Example: substrings("sn",[{initial,"To"},{any,"kv"},{final,"st"}]) +%%% will match entries containing: 'sn: Tornkvist' +%%% +substrings(Type, SubStr) when list(Type), list(SubStr) -> + Ss = {'SubstringFilter_substrings',v_substr(SubStr)}, + {substrings,#'SubstringFilter'{type = Type, + substrings = Ss}}. + + +get_handle(Pid) when pid(Pid) -> Pid; +get_handle(Atom) when atom(Atom) -> Atom; +get_handle(Name) when list(Name) -> list_to_atom("eldap_" ++ Name). +%%%---------------------------------------------------------------------- +%%% Callback functions from gen_fsm +%%%---------------------------------------------------------------------- + +%%---------------------------------------------------------------------- +%% Func: init/1 +%% Returns: {ok, StateName, StateData} | +%% {ok, StateName, StateData, Timeout} | +%% ignore | +%% {stop, StopReason} +%% I use the trick of setting a timeout of 0 to pass control into the +%% process. +%%---------------------------------------------------------------------- +init([]) -> + case get_config() of + {ok, Hosts, Rootdn, Passwd, Log} -> + init({Hosts, Rootdn, Passwd, Log}); + {error, Reason} -> + {stop, Reason} + end; +init({Hosts, Port, Rootdn, Passwd, Log}) -> + {ok, connecting, #eldap{hosts = Hosts, + port = Port, + rootdn = Rootdn, + passwd = Passwd, + id = 0, + log = Log, + dict = dict:new(), + debug_level = 0}, 0}. + +%%---------------------------------------------------------------------- +%% Func: StateName/2 +%% Called when gen_fsm:send_event/2,3 is invoked (async) +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +connecting(timeout, S) -> + {ok, NextState, NewS} = connect_bind(S), + {next_state, NextState, NewS}. + +%%---------------------------------------------------------------------- +%% Func: StateName/3 +%% Called when gen_fsm:sync_send_event/2,3 is invoked. +%% 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} +%%---------------------------------------------------------------------- +connecting(Event, From, S) -> + Reply = {error, connecting}, + {reply, Reply, connecting, S}. + +wait_bind_response(Event, From, S) -> + Reply = {error, wait_bind_response}, + {reply, Reply, wait_bind_response, S}. + +active(Event, From, S) -> + case catch send_command(Event, From, S) of + {ok, NewS} -> + {next_state, active, NewS}; + {error, Reason} -> + {reply, {error, Reason}, active, S}; + {'EXIT', Reason} -> + {reply, {error, Reason}, active, S} + end. + +%%---------------------------------------------------------------------- +%% Func: handle_event/3 +%% Called when gen_fsm:send_all_state_event/2 is invoked. +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- +handle_event(close, StateName, S) -> + gen_tcp:close(S#eldap.fd), + {stop, closed, S}; + +handle_event(Event, StateName, S) -> + {next_state, StateName, S}. + +%%---------------------------------------------------------------------- +%% Func: handle_sync_event/4 +%% Called when gen_fsm:sync_send_all_state_event/2,3 is invoked +%% 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({debug_level, N}, From, StateName, S) -> + {reply, ok, StateName, S#eldap{debug_level = N}}; + +handle_sync_event(Event, From, StateName, S) -> + {reply, {StateName, S}, StateName, S}; + +handle_sync_event(Event, From, StateName, S) -> + Reply = ok, + {reply, Reply, StateName, S}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- + +%% +%% Packets arriving in various states +%% +handle_info({tcp, Socket, Data}, connecting, S) -> + log1("eldap. tcp packet received when disconnected!~n~p~n", [Data], S), + {next_state, connecting, S}; + +handle_info({tcp, Socket, Data}, wait_bind_response, S) -> + cancel_timer(S#eldap.bind_timer), + case catch recvd_wait_bind_response(Data, S) of + bound -> {next_state, active, S}; + {fail_bind, Reason} -> close_and_retry(S), + {next_state, connecting, S#eldap{fd = null}}; + {'EXIT', Reason} -> close_and_retry(S), + {next_state, connecting, S#eldap{fd = null}}; + {error, Reason} -> close_and_retry(S), + {next_state, connecting, S#eldap{fd = null}} + end; + +handle_info({tcp, Socket, Data}, active, S) -> + case catch recvd_packet(Data, S) of + {reply, Reply, To, NewS} -> gen_fsm:reply(To, Reply), + {next_state, active, NewS}; + {ok, NewS} -> {next_state, active, NewS}; + {'EXIT', Reason} -> {next_state, active, S}; + {error, Reason} -> {next_state, active, S} + end; + +handle_info({tcp_closed, Socket}, All_fsm_states, S) -> + F = fun(Id, [{Timer, From, Name}|Res]) -> + gen_fsm:reply(From, {error, tcp_closed}), + cancel_timer(Timer) + end, + dict:map(F, S#eldap.dict), + retry_connect(), + {next_state, connecting, S#eldap{fd = null, + dict = dict:new()}}; + +handle_info({tcp_error, Socket, Reason}, Fsm_state, S) -> + log1("eldap received tcp_error: ~p~nIn State: ~p~n", [Reason, Fsm_state], S), + {next_state, Fsm_state, S}; +%% +%% Timers +%% +handle_info({timeout, Timer, {cmd_timeout, Id}}, active, S) -> + case cmd_timeout(Timer, Id, S) of + {reply, To, Reason, NewS} -> gen_fsm:reply(To, Reason), + {next_state, active, NewS}; + {error, Reason} -> {next_state, active, S} + end; + +handle_info({timeout, retry_connect}, connecting, S) -> + {ok, NextState, NewS} = connect_bind(S), + {next_state, NextState, NewS}; + +handle_info({timeout, Timer, bind_timeout}, wait_bind_response, S) -> + close_and_retry(S), + {next_state, connecting, S#eldap{fd = null}}; + +%% +%% Make sure we don't fill the message queue with rubbish +%% +handle_info(Info, StateName, S) -> + log1("eldap. Unexpected Info: ~p~nIn state: ~p~n when StateData is: ~p~n", + [Info, StateName, S], S), + {next_state, StateName, S}. + +%%---------------------------------------------------------------------- +%% Func: terminate/3 +%% Purpose: Shutdown the fsm +%% Returns: any +%%---------------------------------------------------------------------- +terminate(Reason, StateName, StatData) -> + ok. + +%%---------------------------------------------------------------------- +%% Func: code_change/4 +%% Purpose: Convert process state when code is changed +%% Returns: {ok, NewState, NewStateData} +%%---------------------------------------------------------------------- +code_change(OldVsn, StateName, S, Extra) -> + {ok, StateName, S}. + +%%%---------------------------------------------------------------------- +%%% Internal functions +%%%---------------------------------------------------------------------- +send_command(Command, From, S) -> + Id = bump_id(S), + {Name, Request} = gen_req(Command), + Message = #'LDAPMessage'{messageID = Id, + protocolOp = {Name, Request}}, + log2("~p~n",[{Name, Request}], S), + {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', Message), + ok = gen_tcp:send(S#eldap.fd, Bytes), + Timer = erlang:start_timer(?CMD_TIMEOUT, self(), {cmd_timeout, Id}), + New_dict = dict:store(Id, [{Timer, From, Name}], S#eldap.dict), + {ok, S#eldap{id = Id, + dict = New_dict}}. + +gen_req({search, A}) -> + {searchRequest, + #'SearchRequest'{baseObject = A#eldap_search.base, + scope = v_scope(A#eldap_search.scope), + derefAliases = neverDerefAliases, + sizeLimit = 0, % no size limit + timeLimit = v_timeout(A#eldap_search.timeout), + typesOnly = v_bool(A#eldap_search.types_only), + filter = v_filter(A#eldap_search.filter), + attributes = v_attributes(A#eldap_search.attributes) + }}; +gen_req({add, Entry, Attrs}) -> + {addRequest, + #'AddRequest'{entry = Entry, + attributes = Attrs}}; +gen_req({delete, Entry}) -> + {delRequest, Entry}; +gen_req({modify, Obj, Mod}) -> + v_modifications(Mod), + {modifyRequest, + #'ModifyRequest'{object = Obj, + modification = Mod}}; +gen_req({modify_dn, Entry, NewRDN, DelOldRDN, NewSup}) -> + {modDNRequest, + #'ModifyDNRequest'{entry = Entry, + newrdn = NewRDN, + deleteoldrdn = DelOldRDN, + newSuperior = NewSup}}; + +gen_req({bind, RootDN, Passwd}) -> + {bindRequest, + #'BindRequest'{version = ?LDAP_VERSION, + name = RootDN, + authentication = {simple, Passwd}}}. + +%%----------------------------------------------------------------------- +%% recvd_packet +%% Deals with incoming packets in the active state +%% Will return one of: +%% {ok, NewS} - Don't reply to client yet as this is part of a search +%% result and we haven't got all the answers yet. +%% {reply, Result, From, NewS} - Reply with result to client From +%% {error, Reason} +%% {'EXIT', Reason} - Broke +%%----------------------------------------------------------------------- +recvd_packet(Pkt, S) -> + check_tag(Pkt), + case asn1rt:decode('ELDAPv3', 'LDAPMessage', Pkt) of + {ok,Msg} -> + Op = Msg#'LDAPMessage'.protocolOp, + log2("~p~n",[Op], S), + Dict = S#eldap.dict, + Id = Msg#'LDAPMessage'.messageID, + {Timer, From, Name, Result_so_far} = get_op_rec(Id, Dict), + case {Name, Op} of + {searchRequest, {searchResEntry, R}} when + record(R,'SearchResultEntry') -> + New_dict = dict:append(Id, R, Dict), + {ok, S#eldap{dict = New_dict}}; + {searchRequest, {searchResDone, Result}} -> + case Result#'LDAPResult'.resultCode of + success -> + {Res, Ref} = polish(Result_so_far), + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + {reply, #eldap_search_result{entries = Res, + referrals = Ref}, From, + S#eldap{dict = New_dict}}; + Reason -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + {reply, {error, Reason}, From, S#eldap{dict = New_dict}} + end; + {searchRequest, {searchResRef, R}} -> + New_dict = dict:append(Id, R, Dict), + {ok, S#eldap{dict = New_dict}}; + {addRequest, {addResponse, Result}} -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + Reply = check_reply(Result, From), + {reply, Reply, From, S#eldap{dict = New_dict}}; + {delRequest, {delResponse, Result}} -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + Reply = check_reply(Result, From), + {reply, Reply, From, S#eldap{dict = New_dict}}; + {modifyRequest, {modifyResponse, Result}} -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + Reply = check_reply(Result, From), + {reply, Reply, From, S#eldap{dict = New_dict}}; + {modDNRequest, {modDNResponse, Result}} -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + Reply = check_reply(Result, From), + {reply, Reply, From, S#eldap{dict = New_dict}}; + {bindRequest, {bindResponse, Result}} -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + Reply = check_bind_reply(Result, From), + {reply, Reply, From, S#eldap{dict = New_dict}}; + {OtherName, OtherResult} -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + {reply, {error, {invalid_result, OtherName, OtherResult}}, + From, S#eldap{dict = New_dict}} + end; + Error -> Error + end. + +check_reply(#'LDAPResult'{resultCode = success}, From) -> + ok; +check_reply(#'LDAPResult'{resultCode = Reason}, From) -> + {error, Reason}; +check_reply(Other, From) -> + {error, Other}. + +check_bind_reply(#'BindResponse'{resultCode = success}, From) -> + ok; +check_bind_reply(#'BindResponse'{resultCode = Reason}, From) -> + {error, Reason}; +check_bind_reply(Other, From) -> + {error, Other}. + +get_op_rec(Id, Dict) -> + case dict:find(Id, Dict) of + {ok, [{Timer, From, Name}|Res]} -> + {Timer, From, Name, Res}; + error -> + throw({error, unkown_id}) + end. + +%%----------------------------------------------------------------------- +%% recvd_wait_bind_response packet +%% Deals with incoming packets in the wait_bind_response state +%% Will return one of: +%% bound - Success - move to active state +%% {fail_bind, Reason} - Failed +%% {error, Reason} +%% {'EXIT', Reason} - Broken packet +%%----------------------------------------------------------------------- +recvd_wait_bind_response(Pkt, S) -> + check_tag(Pkt), + case asn1rt:decode('ELDAPv3', 'LDAPMessage', Pkt) of + {ok,Msg} -> + log2("~p", [Msg], S), + check_id(S#eldap.id, Msg#'LDAPMessage'.messageID), + case Msg#'LDAPMessage'.protocolOp of + {bindResponse, Result} -> + case Result#'LDAPResult'.resultCode of + success -> bound; + Error -> {fail_bind, Error} + end + end; + Else -> + {fail_bind, Else} + end. + +check_id(Id, Id) -> ok; +check_id(_, _) -> throw({error, wrong_bind_id}). + +%%----------------------------------------------------------------------- +%% General Helpers +%%----------------------------------------------------------------------- + +cancel_timer(Timer) -> + erlang:cancel_timer(Timer), + receive + {timeout, Timer, _} -> + ok + after 0 -> + ok + end. + + +%%% Sanity check of received packet +check_tag(Data) -> + case asn1rt_ber_bin:decode_tag(Data) of + {Tag, Data1, Rb} -> + case asn1rt_ber_bin:decode_length(Data1) of + {{Len,Data2}, Rb2} -> ok; + _ -> throw({error,decoded_tag_length}) + end; + _ -> throw({error,decoded_tag}) + end. + +close_and_retry(S) -> + gen_tcp:close(S#eldap.fd), + retry_connect(). + +retry_connect() -> + erlang:send_after(?RETRY_TIMEOUT, self(), + {timeout, retry_connect}). + + +%%----------------------------------------------------------------------- +%% Sort out timed out commands +%%----------------------------------------------------------------------- +cmd_timeout(Timer, Id, S) -> + Dict = S#eldap.dict, + case dict:find(Id, Dict) of + {ok, [{Id, Timer, From, Name}|Res]} -> + case Name of + searchRequest -> + {Res1, Ref1} = polish(Res), + New_dict = dict:erase(Id, Dict), + {reply, From, {timeout, + #eldap_search_result{entries = Res1, + referrals = Ref1}}, + S#eldap{dict = New_dict}}; + Others -> + New_dict = dict:erase(Id, Dict), + {reply, From, {error, timeout}, S#eldap{dict = New_dict}} + end; + error -> + {error, timed_out_cmd_not_in_dict} + end. + +%%----------------------------------------------------------------------- +%% Common stuff for results +%%----------------------------------------------------------------------- +%%% +%%% Polish the returned search result +%%% + +polish(Entries) -> + polish(Entries, [], []). + +polish([H|T], Res, Ref) when record(H, 'SearchResultEntry') -> + ObjectName = H#'SearchResultEntry'.objectName, + F = fun({_,A,V}) -> {A,V} end, + Attrs = lists:map(F, H#'SearchResultEntry'.attributes), + polish(T, [#eldap_entry{object_name = ObjectName, + attributes = Attrs}|Res], Ref); +polish([H|T], Res, Ref) -> % No special treatment of referrals at the moment. + polish(T, Res, [H|Ref]); +polish([], Res, Ref) -> + {Res, Ref}. + +%%----------------------------------------------------------------------- +%% Connect to next server in list and attempt to bind to it. +%%----------------------------------------------------------------------- +connect_bind(S) -> + Host = next_host(S#eldap.host, S#eldap.hosts), + TcpOpts = [{packet, asn1}, {active, true}, binary], + case gen_tcp:connect(Host, S#eldap.port, TcpOpts) of + {ok, Socket} -> + case bind_request(Socket, S) of + {ok, NewS} -> + Timer = erlang:start_timer(?BIND_TIMEOUT, self(), + {timeout, bind_timeout}), + {ok, wait_bind_response, NewS#eldap{fd = Socket, + host = Host, + bind_timer = Timer}}; + {error, Reason} -> + gen_tcp:close(Socket), + erlang:send_after(?RETRY_TIMEOUT, self(), + {timeout, retry_connect}), + {ok, connecting, S#eldap{host = Host}} + end; + {error, Reason} -> + erlang:send_after(?RETRY_TIMEOUT, self(), + {timeout, retry_connect}), + {ok, connecting, S#eldap{host = Host}} + end. + +bind_request(Socket, S) -> + Id = bump_id(S), + Req = #'BindRequest'{version = S#eldap.version, + name = S#eldap.rootdn, + authentication = {simple, S#eldap.passwd}}, + Message = #'LDAPMessage'{messageID = Id, + protocolOp = {bindRequest, Req}}, + log2("Message:~p~n",[Message], S), + {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', Message), + ok = gen_tcp:send(Socket, Bytes), + {ok, S#eldap{id = Id}}. + +%% Given last tried Server, find next one to try +next_host(null, [H|_]) -> H; % First time, take first +next_host(Host, Hosts) -> % Find next in turn + next_host(Host, Hosts, Hosts). + +next_host(Host, [Host], Hosts) -> hd(Hosts); % Wrap back to first +next_host(Host, [Host|Tail], Hosts) -> hd(Tail); % Take next +next_host(Host, [], Hosts) -> hd(Hosts); % Never connected before? (shouldn't happen) +next_host(Host, [H|T], Hosts) -> next_host(Host, T, Hosts). + + +%%% -------------------------------------------------------------------- +%%% Verify the input data +%%% -------------------------------------------------------------------- + +v_filter({'and',L}) -> {'and',L}; +v_filter({'or', L}) -> {'or',L}; +v_filter({'not',L}) -> {'not',L}; +v_filter({equalityMatch,AV}) -> {equalityMatch,AV}; +v_filter({greaterOrEqual,AV}) -> {greaterOrEqual,AV}; +v_filter({lessOrEqual,AV}) -> {lessOrEqual,AV}; +v_filter({approxMatch,AV}) -> {approxMatch,AV}; +v_filter({present,A}) -> {present,A}; +v_filter({substrings,S}) when record(S,'SubstringFilter') -> {substrings,S}; +v_filter(_Filter) -> throw({error,concat(["unknown filter: ",_Filter])}). + +v_modifications(Mods) -> + F = fun({_,Op,_}) -> + case lists:member(Op,[add,delete,replace]) of + true -> true; + _ -> throw({error,{mod_operation,Op}}) + end + end, + lists:foreach(F, Mods). + +v_substr([{Key,Str}|T]) when list(Str),Key==initial;Key==any;Key==final -> + [{Key,Str}|v_substr(T)]; +v_substr([H|T]) -> + throw({error,{substring_arg,H}}); +v_substr([]) -> + []. +v_scope(baseObject) -> baseObject; +v_scope(singleLevel) -> singleLevel; +v_scope(wholeSubtree) -> wholeSubtree; +v_scope(_Scope) -> throw({error,concat(["unknown scope: ",_Scope])}). + +v_bool(true) -> true; +v_bool(false) -> false; +v_bool(_Bool) -> throw({error,concat(["not Boolean: ",_Bool])}). + +v_timeout(I) when integer(I), I>=0 -> I; +v_timeout(_I) -> throw({error,concat(["timeout not positive integer: ",_I])}). + +v_attributes(Attrs) -> + F = fun(A) when list(A) -> A; + (A) -> throw({error,concat(["attribute not String: ",A])}) + end, + lists:map(F,Attrs). + + +%%% -------------------------------------------------------------------- +%%% Get and Validate the initial configuration +%%% -------------------------------------------------------------------- +get_config() -> + Priv_dir = code:priv_dir(eldap), + File = filename:join(Priv_dir, "eldap.conf"), + case file:consult(File) of + {ok, Entries} -> + case catch parse(Entries) of + {ok, Hosts, Port, Rootdn, Passwd, Log} -> + {ok, Hosts, Port, Rootdn, Passwd, Log}; + {error, Reason} -> + {error, Reason}; + {'EXIT', Reason} -> + {error, Reason} + end; + {error, Reason} -> + {error, Reason} + end. + +parse(Entries) -> + {ok, + get_hosts(host, Entries), + get_integer(port, Entries), + get_list(rootdn, Entries), + get_list(passwd, Entries), + get_log(log, Entries)}. + +get_integer(Key, List) -> + case lists:keysearch(Key, 1, List) of + {value, {Key, Value}} when integer(Value) -> + Value; + {value, {Key, Value}} -> + throw({error, "Bad Value in Config for " ++ atom_to_list(Key)}); + false -> + throw({error, "No Entry in Config for " ++ atom_to_list(Key)}) + end. + +get_list(Key, List) -> + case lists:keysearch(Key, 1, List) of + {value, {Key, Value}} when list(Value) -> + Value; + {value, {Key, Value}} -> + throw({error, "Bad Value in Config for " ++ atom_to_list(Key)}); + false -> + throw({error, "No Entry in Config for " ++ atom_to_list(Key)}) + end. + +get_log(Key, List) -> + case lists:keysearch(Key, 1, List) of + {value, {Key, Value}} when function(Value) -> + Value; + {value, {Key, Else}} -> + false; + false -> + fun(Level, Format, Args) -> io:format("--- " ++ Format, Args) end + end. + +get_hosts(Key, List) -> + lists:map(fun({Key1, {A,B,C,D}}) when integer(A), + integer(B), + integer(C), + integer(D), + Key == Key1-> + {A,B,C,D}; + ({Key1, Value}) when list(Value), + Key == Key1-> + Value; + ({Else, Value}) -> + throw({error, "Bad Hostname in config"}) + end, List). + +%%% -------------------------------------------------------------------- +%%% Other Stuff +%%% -------------------------------------------------------------------- +bump_id(#eldap{id = Id}) when Id > ?MAX_TRANSACTION_ID -> + ?MIN_TRANSACTION_ID; +bump_id(#eldap{id = Id}) -> + Id + 1. + +%%% -------------------------------------------------------------------- +%%% Log routines. Call a user provided log routine Fun. +%%% -------------------------------------------------------------------- + +log1(Str, Args, #eldap{log = Fun, debug_level = N}) -> log(Fun, Str, Args, 1, N). +log2(Str, Args, #eldap{log = Fun, debug_level = N}) -> log(Fun, Str, Args, 2, N). + +log(Fun, Str, Args, This_level, Status) when function(Fun), This_level =< Status -> + catch Fun(This_level, Str, Args); +log(_, _, _, _, _) -> + ok. diff --git a/src/eldap/eldap.hrl b/src/eldap/eldap.hrl new file mode 100644 index 000000000..e31a34991 --- /dev/null +++ b/src/eldap/eldap.hrl @@ -0,0 +1,13 @@ +-record(eldap_search, {scope = wholeSubtree, + base = [], + filter, + attributes = [], + types_only = false, + timeout = 0}). + + +-record(eldap_search_result, {entries, + referrals}). + +-record(eldap_entry, {object_name, + attributes}). diff --git a/src/mod_muc/mod_muc_room.erl b/src/mod_muc/mod_muc_room.erl index 71c1a4417..f9affed9d 100644 --- a/src/mod_muc/mod_muc_room.erl +++ b/src/mod_muc/mod_muc_room.erl @@ -308,6 +308,8 @@ normal_state({route, From, "", _ -> {next_state, normal_state, NewStateData} end; + reply -> + {next_state, normal_state, StateData}; _ -> Err = jlib:make_error_reply( Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), @@ -448,11 +450,17 @@ normal_state({route, From, ToNick, true -> case find_jid_by_nick(ToNick, StateData) of false -> - Err = jlib:make_error_reply( - Packet, ?ERR_ITEM_NOT_FOUND), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, ToNick), - From, Err); + case jlib:iq_query_info(Packet) of + reply -> + ok; + _ -> + Err = jlib:make_error_reply( + Packet, ?ERR_ITEM_NOT_FOUND), + ejabberd_router:route( + jlib:jid_replace_resource( + StateData#state.jid, ToNick), + From, Err) + end; ToJID -> {ok, #user{nick = FromNick}} = ?DICT:find(jlib:jid_tolower(From), @@ -462,11 +470,16 @@ normal_state({route, From, ToNick, ToJID, Packet) end; _ -> - Err = jlib:make_error_reply( - Packet, ?ERR_NOT_ALLOWED), - ejabberd_router:route( - jlib:jid_replace_resource(StateData#state.jid, ToNick), From, - Err) + case jlib:iq_query_info(Packet) of + reply -> + ok; + _ -> + Err = jlib:make_error_reply( + Packet, ?ERR_NOT_ALLOWED), + ejabberd_router:route( + jlib:jid_replace_resource(StateData#state.jid, ToNick), + From, Err) + end end, {next_state, normal_state, StateData}; diff --git a/src/mod_register.erl b/src/mod_register.erl index 62a348d61..52ebcb00f 100644 --- a/src/mod_register.erl +++ b/src/mod_register.erl @@ -130,6 +130,8 @@ try_register(User, Password) -> {error, ?ERR_CONFLICT}; {error, invalid_jid} -> {error, ?ERR_JID_MALFORMED}; + {error, not_allowed} -> + {error, ?ERR_NOT_ALLOWED}; {error, _Reason} -> {error, ?ERR_INTERNAL_SERVER_ERROR} end |