diff --git a/doc/guide.html b/doc/guide.html index 3bd1d7a2b..09176d608 100644 --- a/doc/guide.html +++ b/doc/guide.html @@ -1532,12 +1532,15 @@ create accounts, change password or edit vCard that is stored in LDAP.
Let’s say ldap.example.org is the name of our LDAP server. We have
users with their passwords in "ou=Users,dc=example,dc=org" directory.
Also we have addressbook, which contains users emails and their additional
-infos in "ou=AddressBook,dc=example,dc=org" directory. Corresponding
-authentication section should looks like this:Common example
%% Authentication method
+infos in "ou=AddressBook,dc=example,dc=org" directory.
+The connection to the LDAP server is encrypted using TLS,
+and using the custom port 6123.
+Corresponding authentication section should looks like this:
%% Authentication method {auth_method, ldap}. %% DNS name of our LDAP server {ldap_servers, ["ldap.example.org"]}. %% Bind to LDAP server as "cn=Manager,dc=example,dc=org" with password "secret" {ldap_rootdn, "cn=Manager,dc=example,dc=org"}. {ldap_password, "secret"}. +{ldap_encrypt, tls}. +{ldap_port, 6123}. %% Define the user's base {ldap_base, "ou=Users,dc=example,dc=org"}. %% We want to authorize users from 'shadowAccount' object class only diff --git a/doc/guide.tex b/doc/guide.tex index 404abcd32..195fb875e 100644 --- a/doc/guide.tex +++ b/doc/guide.tex @@ -2057,12 +2057,15 @@ Parameters: \begin{description} \titem{ldap\_servers} \ind{options!ldap\_server}List of IP addresses or DNS names of your LDAP servers. This option is required. +\titem{ldap\_encrypt} \ind{options!ldap\_encrypt}Type of connection encryption to the LDAP server. +Allowed values are: \term{none}, \term{tls}. +Note that STARTTLS is not supported. +The default value is: \term{none}. \titem{ldap\_port} \ind{options!ldap\_port}Port to connect to your LDAP server. - The initial default value is~389, so it is used when nothing is set into the -configuration file. +The default port is~389 if encryption is disabled; and 636 if encryption is enabled. If you configure a value, it is stored in \ejabberd{}'s database. Then, if you remove that value from the configuration file, -the value previously stored in the database will be used instead of the default 389. +the value previously stored in the database will be used instead of the default port. \titem{ldap\_rootdn} \ind{options!ldap\_rootdn}Bind DN. The default value is~\term{""} which means `anonymous connection'. \titem{ldap\_password} \ind{options!ldap\_password}Bind password. The default @@ -2137,8 +2140,10 @@ You can authenticate users against an LDAP directory. Available options are: Let's say \term{ldap.example.org} is the name of our LDAP server. We have users with their passwords in \term{"ou=Users,dc=example,dc=org"} directory. Also we have addressbook, which contains users emails and their additional -infos in \term{"ou=AddressBook,dc=example,dc=org"} directory. Corresponding -authentication section should looks like this: +infos in \term{"ou=AddressBook,dc=example,dc=org"} directory. +The connection to the LDAP server is encrypted using TLS, +and using the custom port 6123. +Corresponding authentication section should looks like this: \begin{verbatim} %% Authentication method @@ -2148,6 +2153,8 @@ authentication section should looks like this: %% Bind to LDAP server as "cn=Manager,dc=example,dc=org" with password "secret" {ldap_rootdn, "cn=Manager,dc=example,dc=org"}. {ldap_password, "secret"}. +{ldap_encrypt, tls}. +{ldap_port, 6123}. %% Define the user's base {ldap_base, "ou=Users,dc=example,dc=org"}. %% We want to authorize users from 'shadowAccount' object class only diff --git a/src/ejabberd.cfg.example b/src/ejabberd.cfg.example index 9f8c34b24..7161e5e34 100644 --- a/src/ejabberd.cfg.example +++ b/src/ejabberd.cfg.example @@ -234,17 +234,28 @@ %% List of LDAP servers: %%{ldap_servers, ["localhost"]}. %% -%% LDAP attribute that holds user ID: -%%{ldap_uids, [{"mail", "%u@mail.example.org"}]}. +%% Encryption of connection to LDAP servers: +%%{ldap_encrypt, none}. +%%{ldap_encrypt, tls}. %% -%% Search base of LDAP directory: -%%{ldap_base, "dc=example,dc=com"}. +%% Port connect to LDAP servers: +%%{ldap_port, 389}. +%%{ldap_port, 636}. %% %% LDAP manager: %%{ldap_rootdn, "dc=example,dc=com"}. %% %% Password to LDAP manager: %%{ldap_password, "******"}. +%% +%% Search base of LDAP directory: +%%{ldap_base, "dc=example,dc=com"}. +%% +%% LDAP attribute that holds user ID: +%%{ldap_uids, [{"mail", "%u@mail.example.org"}]}. +%% +%% LDAP filter: +%%{ldap_filter, "(objectClass=shadowAccount)"}. %% %% Anonymous login support: diff --git a/src/ejabberd_auth_ldap.erl b/src/ejabberd_auth_ldap.erl index 7a31d6bae..a373a0678 100644 --- a/src/ejabberd_auth_ldap.erl +++ b/src/ejabberd_auth_ldap.erl @@ -66,6 +66,7 @@ servers, backups, port, + encrypt, dn, password, base, @@ -137,13 +138,15 @@ init(Host) -> State#state.backups, State#state.port, State#state.dn, - State#state.password), + State#state.password, + State#state.encrypt), eldap_pool:start_link(State#state.bind_eldap_id, State#state.servers, State#state.backups, State#state.port, State#state.dn, - State#state.password), + State#state.password, + State#state.encrypt), {ok, State}. %% @spec () -> true @@ -443,8 +446,13 @@ parse_options(Host) -> undefined -> []; Backups -> Backups end, + LDAPEncrypt = ejabberd_config:get_local_option({ldap_encrypt, Host}), LDAPPort = case ejabberd_config:get_local_option({ldap_port, Host}) of - undefined -> 389; + undefined -> case LDAPEncrypt of + tls -> ?LDAPS_PORT; + starttls -> ?LDAP_PORT; + _ -> ?LDAP_PORT + end; P -> P end, RootDN = case ejabberd_config:get_local_option({ldap_rootdn, Host}) of @@ -479,6 +487,7 @@ parse_options(Host) -> servers = LDAPServers, backups = LDAPBackups, port = LDAPPort, + encrypt = LDAPEncrypt, dn = RootDN, password = Password, base = LDAPBase, diff --git a/src/eldap/eldap.erl b/src/eldap/eldap.erl index 6a16b9b54..3c3e736ae 100644 --- a/src/eldap/eldap.erl +++ b/src/eldap/eldap.erl @@ -42,6 +42,12 @@ %%% Modified by Mickael Remond%%% Now use ejabberd log mechanism +%%% Modified by: +%%% Thomas Baden 2008 April 6th +%%% Andy Harb 2008 April 28th +%%% Anton Podavalov 2009 February 22th +%%% Added LDAPS support, modeled off jungerl eldap.erl version. +%%% NOTICE: STARTTLS is not supported. %%% -------------------------------------------------------------------- -vc('$Id$ '). @@ -61,7 +67,7 @@ -include("ejabberd.hrl"). %% External exports --export([start_link/1, start_link/5]). +-export([start_link/1, start_link/6]). -export([baseObject/0,singleLevel/0,wholeSubtree/0,close/1, equalityMatch/2,greaterOrEqual/2,lessOrEqual/2, @@ -94,10 +100,17 @@ %% Grace period after "soft" LDAP bind errors: -define(GRACEFUL_RETRY_TIMEOUT, 5000). +-define(SUPPORTEDEXTENSION, "1.3.6.1.4.1.1466.101.120.7"). +-define(SUPPORTEDEXTENSIONSYNTAX, "1.3.6.1.4.1.1466.115.121.1.38"). +-define(STARTTLS, "1.3.6.1.4.1.1466.20037"). + -record(eldap, {version = ?LDAP_VERSION, hosts, % Possible hosts running LDAP servers host = null, % Connected Host LDAP server port = 389, % The LDAP server port + sockmod, % SockMod (gen_tcp|tls) + tls = none, % LDAP/LDAPS (none|starttls|tls) + tls_options = [], fd = null, % Socket filedescriptor. rootdn = "", % Name of the entry to bind as passwd, % Password for (above) entry @@ -114,9 +127,9 @@ 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) -> +start_link(Name, Hosts, Port, Rootdn, Passwd, Encrypt) -> Reg_name = list_to_atom("eldap_" ++ Name), - gen_fsm:start_link({local, Reg_name}, ?MODULE, {Hosts, Port, Rootdn, Passwd}, []). + gen_fsm:start_link({local, Reg_name}, ?MODULE, {Hosts, Port, Rootdn, Passwd, Encrypt}, []). %%% -------------------------------------------------------------------- %%% Get status of connection. @@ -380,16 +393,34 @@ get_handle(Name) when is_list(Name) -> list_to_atom("eldap_" ++ Name). %%---------------------------------------------------------------------- init([]) -> case get_config() of - {ok, Hosts, Rootdn, Passwd} -> - init({Hosts, Rootdn, Passwd}); + {ok, Hosts, Rootdn, Passwd, Encrypt} -> + init({Hosts, Rootdn, Passwd, Encrypt}); {error, Reason} -> {stop, Reason} end; -init({Hosts, Port, Rootdn, Passwd}) -> +init({Hosts, Port, Rootdn, Passwd, Encrypt}) -> + catch ssl:start(), + {X1,X2,X3} = erlang:now(), + ssl:seed(integer_to_list(X1) ++ integer_to_list(X2) ++ integer_to_list(X3)), + PortTemp = case Port of + undefined -> + case Encrypt of + tls -> + ?LDAPS_PORT; + starttls -> + ?LDAP_PORT; + _ -> + ?LDAP_PORT + end; + PT -> PT + end, + TLSOpts = [verify_none], {ok, connecting, #eldap{hosts = Hosts, - port = Port, + port = PortTemp, rootdn = Rootdn, passwd = Passwd, + tls = Encrypt, + tls_options = TLSOpts, id = 0, dict = dict:new(), req_q = queue:new()}, 0}. @@ -438,7 +469,7 @@ active(Event, From, S) -> %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- handle_event(close, _StateName, S) -> - catch gen_tcp:close(S#eldap.fd), + catch (S#eldap.sockmod):close(S#eldap.fd), {stop, normal, S}; handle_event(_Event, StateName, S) -> @@ -467,11 +498,13 @@ handle_sync_event(_Event, _From, StateName, S) -> %% %% Packets arriving in various states %% -handle_info({tcp, _Socket, Data}, connecting, S) -> +handle_info({Tag, _Socket, Data}, connecting, S) + when Tag == tcp; Tag == ssl -> ?DEBUG("tcp packet received when disconnected!~n~p", [Data]), {next_state, connecting, S}; -handle_info({tcp, _Socket, Data}, wait_bind_response, S) -> +handle_info({Tag, _Socket, Data}, wait_bind_response, S) + when Tag == tcp; Tag == ssl -> cancel_timer(S#eldap.bind_timer), case catch recvd_wait_bind_response(Data, S) of bound -> @@ -487,8 +520,9 @@ handle_info({tcp, _Socket, Data}, wait_bind_response, S) -> {next_state, connecting, close_and_retry(S)} end; -handle_info({tcp, _Socket, Data}, StateName, S) - when StateName == active orelse StateName == active_bind -> +handle_info({Tag, _Socket, Data}, StateName, S) + when (StateName == active orelse StateName == active_bind) andalso + (Tag == tcp orelse Tag == ssl) -> case catch recvd_packet(Data, S) of {response, Response, RequestType} -> NewS = case Response of @@ -509,12 +543,14 @@ handle_info({tcp, _Socket, Data}, StateName, S) {next_state, StateName, S} end; -handle_info({tcp_closed, _Socket}, Fsm_state, S) -> +handle_info({Tag, _Socket}, Fsm_state, S) + when Tag == tcp_closed; Tag == ssl_closed -> ?WARNING_MSG("LDAP server closed the connection: ~s:~p~nIn State: ~p", [S#eldap.host, S#eldap.port ,Fsm_state]), {next_state, connecting, close_and_retry(S)}; -handle_info({tcp_error, _Socket, Reason}, Fsm_state, S) -> +handle_info({Tag, _Socket, Reason}, Fsm_state, S) + when Tag == tcp_error; Tag == ssl_error -> ?DEBUG("eldap received tcp_error: ~p~nIn State: ~p", [Reason, Fsm_state]), {next_state, connecting, close_and_retry(S)}; @@ -597,7 +633,7 @@ send_command(Command, From, S) -> protocolOp = {Name, Request}}, ?DEBUG("~p~n",[{Name, Request}]), {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', Message), - case gen_tcp:send(S#eldap.fd, Bytes) of + case (S#eldap.sockmod):send(S#eldap.fd, Bytes) of ok -> Timer = erlang:start_timer(?CMD_TIMEOUT, self(), {cmd_timeout, Id}), New_dict = dict:store(Id, [{Timer, Command, From, Name}], S#eldap.dict), @@ -796,7 +832,7 @@ check_tag(Data) -> end. close_and_retry(S, Timeout) -> - catch gen_tcp:close(S#eldap.fd), + catch (S#eldap.sockmod):close(S#eldap.fd), Queue = dict:fold( fun(_Id, [{Timer, Command, From, _Name}|_], Q) -> cancel_timer(Timer), @@ -863,16 +899,28 @@ polish([], Res, Ref) -> %%----------------------------------------------------------------------- connect_bind(S) -> Host = next_host(S#eldap.host, S#eldap.hosts), - TcpOpts = [{packet, asn1}, {active, true}, {keepalive, true}, - {send_timeout, ?SEND_TIMEOUT}, binary], ?INFO_MSG("LDAP connection on ~s:~p", [Host, S#eldap.port]), - case gen_tcp:connect(Host, S#eldap.port, TcpOpts) of + SocketData = case S#eldap.tls of + tls -> + SockMod = ssl, + SslOpts = [{packet, asn1}, {active, true}, {keepalive, true}, + binary], + ssl:connect(Host, S#eldap.port, SslOpts); + %% starttls -> %% TODO: Implement STARTTLS; + _ -> + SockMod = gen_tcp, + TcpOpts = [{packet, asn1}, {active, true}, {keepalive, true}, + {send_timeout, ?SEND_TIMEOUT}, binary], + gen_tcp:connect(Host, S#eldap.port, TcpOpts) + end, + case SocketData of {ok, Socket} -> - case bind_request(Socket, S) of + case bind_request(Socket, S#eldap{sockmod = SockMod}) of {ok, NewS} -> Timer = erlang:start_timer(?BIND_TIMEOUT, self(), {timeout, bind_timeout}), {ok, wait_bind_response, NewS#eldap{fd = Socket, + sockmod = SockMod, host = Host, bind_timer = Timer}}; {error, Reason} -> @@ -896,7 +944,7 @@ bind_request(Socket, S) -> protocolOp = {bindRequest, Req}}, ?DEBUG("Bind Request Message:~p~n",[Message]), {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', Message), - case gen_tcp:send(Socket, Bytes) of + case (S#eldap.sockmod):send(Socket, Bytes) of ok -> {ok, S#eldap{id = Id}}; Error -> Error end. @@ -970,8 +1018,8 @@ get_config() -> case file:consult(File) of {ok, Entries} -> case catch parse(Entries) of - {ok, Hosts, Port, Rootdn, Passwd} -> - {ok, Hosts, Port, Rootdn, Passwd}; + {ok, Hosts, Port, Rootdn, Passwd, Encrypt} -> + {ok, Hosts, Port, Rootdn, Passwd, Encrypt}; {error, Reason} -> {error, Reason}; {'EXIT', Reason} -> @@ -986,7 +1034,8 @@ parse(Entries) -> get_hosts(host, Entries), get_integer(port, Entries), get_list(rootdn, Entries), - get_list(passwd, Entries)}. + get_list(passwd, Entries), + get_atom(encrypt, Entries)}. get_integer(Key, List) -> case lists:keysearch(Key, 1, List) of @@ -1008,6 +1057,16 @@ get_list(Key, List) -> throw({error, "No Entry in Config for " ++ atom_to_list(Key)}) end. +get_atom(Key, List) -> + case lists:keysearch(Key, 1, List) of + {value, {Key, Value}} when atom(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_hosts(Key, List) -> lists:map(fun({Key1, {A,B,C,D}}) when is_integer(A), is_integer(B), diff --git a/src/eldap/eldap.hrl b/src/eldap/eldap.hrl index 5ffc464c9..95aa6f9c5 100644 --- a/src/eldap/eldap.hrl +++ b/src/eldap/eldap.hrl @@ -19,6 +19,9 @@ %%% %%%---------------------------------------------------------------------- +-define(LDAP_PORT, 389). +-define(LDAPS_PORT, 636). + -record(eldap_search, {scope = wholeSubtree, base = [], filter, diff --git a/src/eldap/eldap_pool.erl b/src/eldap/eldap_pool.erl index f714129b5..d7f3acfab 100644 --- a/src/eldap/eldap_pool.erl +++ b/src/eldap/eldap_pool.erl @@ -29,7 +29,7 @@ %% API -export([ - start_link/6, + start_link/7, bind/3, search/2 ]). @@ -45,12 +45,12 @@ bind(PoolName, DN, Passwd) -> search(PoolName, Opts) -> do_request(PoolName, {search, [Opts]}). -start_link(Name, Hosts, Backups, Port, Rootdn, Passwd) -> +start_link(Name, Hosts, Backups, Port, Rootdn, Passwd, Encrypt) -> PoolName = make_id(Name), pg2:create(PoolName), lists:foreach(fun(Host) -> ID = erlang:ref_to_list(make_ref()), - case catch eldap:start_link(ID, [Host|Backups], Port, Rootdn, Passwd) of + case catch eldap:start_link(ID, [Host|Backups], Port, Rootdn, Passwd, Encrypt) of {ok, Pid} -> pg2:join(PoolName, Pid); _ -> diff --git a/src/mod_vcard_ldap.erl b/src/mod_vcard_ldap.erl index 4f907be80..148d6b0af 100644 --- a/src/mod_vcard_ldap.erl +++ b/src/mod_vcard_ldap.erl @@ -63,6 +63,7 @@ servers, backups, port, + encrypt, dn, base, password, @@ -183,7 +184,8 @@ init([Host, Opts]) -> State#state.backups, State#state.port, State#state.dn, - State#state.password), + State#state.password, + State#state.encrypt), case State#state.search of true -> ejabberd_router:register_route(State#state.myhost); @@ -653,11 +655,22 @@ parse_options(Host, Opts) -> ejabberd_config:get_local_option({ldap_servers, Host}); Backups -> Backups end, - LDAPPort = case gen_mod:get_opt(ldap_port, Opts, undefined) of + LDAPEncrypt = case gen_mod:get_opt(ldap_encrypt, Opts, undefined) of + undefined -> + ejabberd_config:get_local_option({ldap_encrypt, Host}); + E -> E + end, + LDAPPortTemp = case gen_mod:get_opt(ldap_port, Opts, undefined) of + undefined -> + ejabberd_config:get_local_option({ldap_port, Host}); + PT -> PT + end, + LDAPPort = case LDAPPortTemp of undefined -> - case ejabberd_config:get_local_option({ldap_port, Host}) of - undefined -> 389; - P -> P + case LDAPEncrypt of + tls -> ?LDAPS_PORT; + starttls -> ?LDAP_PORT; + _ -> ?LDAP_PORT end; P -> P end, @@ -727,6 +740,7 @@ parse_options(Host, Opts) -> servers = LDAPServers, backups = LDAPBackups, port = LDAPPort, + encrypt = LDAPEncrypt, dn = RootDN, base = LDAPBase, password = Password,