Support LDAPS with TLS (EJAB-109)(thanks to Thomas Baden, Andy Harb, Sergei Golovan, Anton Podavalov)

SVN Revision: 2098
This commit is contained in:
Badlop 2009-05-25 17:15:48 +00:00
parent 3ec3e78baa
commit 31aa201ee8
8 changed files with 159 additions and 49 deletions

View File

@ -1568,12 +1568,15 @@ create accounts, change password or edit vCard that is stored in LDAP.</P><P> <A
</P><DL CLASS="description"><DT CLASS="dt-description">
<B><TT>ldap_servers</TT></B></DT><DD CLASS="dd-description"> List of IP addresses or DNS names of your
LDAP servers. This option is required.
</DD><DT CLASS="dt-description"><B><TT>ldap_encrypt</TT></B></DT><DD CLASS="dd-description"> Type of connection encryption to the LDAP server.
Allowed values are: <TT>none</TT>, <TT>tls</TT>.
Note that STARTTLS is not supported.
The default value is: <TT>none</TT>.
</DD><DT CLASS="dt-description"><B><TT>ldap_port</TT></B></DT><DD CLASS="dd-description"> Port to connect to your LDAP server.
The initial default value is&#XA0;389, so it is used when nothing is set into the
configuration file.
The default port is&#XA0;389 if encryption is disabled; and 636 if encryption is enabled.
If you configure a value, it is stored in <TT>ejabberd</TT>&#X2019;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.
</DD><DT CLASS="dt-description"><B><TT>ldap_rootdn</TT></B></DT><DD CLASS="dd-description"> Bind DN. The default value
is&#XA0;<TT>""</TT> which means &#X2018;anonymous connection&#X2019;.
</DD><DT CLASS="dt-description"><B><TT>ldap_password</TT></B></DT><DD CLASS="dd-description"> Bind password. The default
@ -1628,14 +1631,18 @@ Example values:
<H5 CLASS="paragraph"><!--SEC ANCHOR --><A HREF="#ldapcommonexample">Common example</A></H5><!--SEC END --><P> <A NAME="ldapcommonexample"></A> </P><P>Let&#X2019;s say <TT>ldap.example.org</TT> is the name of our LDAP server. We have
users with their passwords in <TT>"ou=Users,dc=example,dc=org"</TT> directory.
Also we have addressbook, which contains users emails and their additional
infos in <TT>"ou=AddressBook,dc=example,dc=org"</TT> directory. Corresponding
authentication section should looks like this:</P><PRE CLASS="verbatim">%% Authentication method
infos in <TT>"ou=AddressBook,dc=example,dc=org"</TT> directory.
The connection to the LDAP server is encrypted using TLS,
and using the custom port 6123.
Corresponding authentication section should looks like this:</P><PRE CLASS="verbatim">%% 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

View File

@ -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

View File

@ -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:

View File

@ -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,

View File

@ -42,6 +42,12 @@
%%% Modified by Mickael Remond <mremond@process-one.net>
%%% Now use ejabberd log mechanism
%%% Modified by:
%%% Thomas Baden <roo@ham9.net> 2008 April 6th
%%% Andy Harb <Ahmad.N.Abou-Harb@jpl.nasa.gov> 2008 April 28th
%%% Anton Podavalov <a.podavalov@gmail.com> 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),

View File

@ -19,6 +19,9 @@
%%%
%%%----------------------------------------------------------------------
-define(LDAP_PORT, 389).
-define(LDAPS_PORT, 636).
-record(eldap_search, {scope = wholeSubtree,
base = [],
filter,

View File

@ -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);
_ ->

View File

@ -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,