From 31aa201ee8ce0c016403916fee06684b15ff2829 Mon Sep 17 00:00:00 2001 From: Badlop Date: Mon, 25 May 2009 17:15:48 +0000 Subject: [PATCH] Support LDAPS with TLS (EJAB-109)(thanks to Thomas Baden, Andy Harb, Sergei Golovan, Anton Podavalov) SVN Revision: 2098 --- doc/guide.html | 17 ++++-- doc/guide.tex | 17 ++++-- src/ejabberd.cfg.example | 19 +++++-- src/ejabberd_auth_ldap.erl | 15 ++++-- src/eldap/eldap.erl | 107 ++++++++++++++++++++++++++++--------- src/eldap/eldap.hrl | 3 ++ src/eldap/eldap_pool.erl | 6 +-- src/mod_vcard_ldap.erl | 24 +++++++-- 8 files changed, 159 insertions(+), 49 deletions(-) diff --git a/doc/guide.html b/doc/guide.html index 5bbd6ea72..467a4dfe5 100644 --- a/doc/guide.html +++ b/doc/guide.html @@ -1568,12 +1568,15 @@ create accounts, change password or edit vCard that is stored in LDAP.

ldap_servers
List of IP addresses or DNS names of your LDAP servers. This option is required. +
ldap_encrypt
Type of connection encryption to the LDAP server. +Allowed values are: none, tls. +Note that STARTTLS is not supported. +The default value is: none.
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.
ldap_rootdn
Bind DN. The default value is "" which means ‘anonymous connection’.
ldap_password
Bind password. The default @@ -1628,14 +1631,18 @@ Example values:
Common example

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:

%% 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 d8717f186..e496e6b94 100644
--- a/doc/guide.tex
+++ b/doc/guide.tex
@@ -2105,12 +2105,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
@@ -2185,8 +2188,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
@@ -2196,6 +2201,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 26e3f51f6..e53f187ba 100644
--- a/src/ejabberd.cfg.example
+++ b/src/ejabberd.cfg.example
@@ -235,17 +235,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 ea3de540a..2b97bd793 100644
--- a/src/ejabberd_auth_ldap.erl
+++ b/src/ejabberd_auth_ldap.erl
@@ -66,6 +66,7 @@
 		servers,
 		backups,
 		port,
+		encrypt,
 		dn,
 		password,
 		base,
@@ -122,13 +123,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}.
 
 plain_password_required() ->
@@ -354,8 +357,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
@@ -390,6 +398,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 7de172d1c..68faa97da 100644
--- a/src/mod_vcard_ldap.erl
+++ b/src/mod_vcard_ldap.erl
@@ -62,6 +62,7 @@
 		servers,
 		backups,
 		port,
+		encrypt,
 		dn,
 		base,
 		password,
@@ -179,7 +180,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);
@@ -673,11 +675,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,
@@ -747,6 +760,7 @@ parse_options(Host, Opts) ->
 	   servers = LDAPServers,
 	   backups = LDAPBackups,
 	   port = LDAPPort,
+	   encrypt = LDAPEncrypt,
 	   dn = RootDN,
 	   base = LDAPBase,
 	   password = Password,