diff options
Diffstat (limited to 'src/eldap.erl')
-rw-r--r-- | src/eldap.erl | 1199 |
1 files changed, 1199 insertions, 0 deletions
diff --git a/src/eldap.erl b/src/eldap.erl new file mode 100644 index 000000000..0dae3cbe8 --- /dev/null +++ b/src/eldap.erl @@ -0,0 +1,1199 @@ +-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 Torbjorn Tornkvist, 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> + +%%% Modified by Evgeniy Khramtsov <ekhramtsov@process-one.net> +%%% Implemented queue for bind() requests to prevent pending binds. +%%% Implemented extensibleMatch/2 function. +%%% Implemented LDAP Extended Operations (currently only Password Modify +%%% is supported - RFC 3062). + +%%% Modified by Christophe Romain <christophe.romain@process-one.net> +%%% Improve error case handling + +%%% 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$ '). + +%%%---------------------------------------------------------------------- +%%% 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 +%%% active_bind - sent bind() request and waiting for response +%%%---------------------------------------------------------------------- + +-behaviour(gen_fsm). + +-include("ejabberd.hrl"). +-include("logger.hrl"). + +%% External exports +-export([start_link/1, 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, extensibleMatch/2, 'and'/1, 'or'/1, 'not'/1, + modify/3, mod_add/2, mod_delete/2, mod_replace/2, add/3, + delete/2, modify_dn/5, modify_passwd/3, bind/3]). + +-export([get_status/1]). + +%% gen_fsm callbacks +-export([init/1, connecting/2, connecting/3, + wait_bind_response/3, active/3, active_bind/3, + handle_event/3, handle_sync_event/4, handle_info/3, + terminate/3, code_change/4]). + +-export_type([filter/0]). + +-include("ELDAPv3.hrl"). + +-include("eldap.hrl"). + +-define(LDAP_VERSION, 3). + +-define(RETRY_TIMEOUT, 500). + +-define(BIND_TIMEOUT, 10000). + +-define(CMD_TIMEOUT, 100000). +%% Used in gen_fsm sync calls. +%% Used as a timeout for gen_tcp:send/2 + +-define(CALL_TIMEOUT, + (?CMD_TIMEOUT) + (?BIND_TIMEOUT) + (?RETRY_TIMEOUT)). + +-define(SEND_TIMEOUT, 30000). + +-define(MAX_TRANSACTION_ID, 65535). + +-define(MIN_TRANSACTION_ID, 0). +%% 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">>). + +-type handle() :: pid() | atom() | binary(). + +-record(eldap, + {version = ?LDAP_VERSION :: non_neg_integer(), + hosts = [] :: [binary()], + host :: binary(), + port = 389 :: inet:port_number(), + sockmod = gen_tcp :: ssl | gen_tcp, + tls = none :: none | tls, + tls_options = [] :: [{cacertfile, string()} | + {depth, non_neg_integer()} | + {verify, non_neg_integer()}], + fd, + rootdn = <<"">> :: binary(), + passwd = <<"">> :: binary(), + id = 0 :: non_neg_integer(), + bind_timer = make_ref() :: reference(), + dict = dict:new() :: dict(), + req_q = queue:new() :: queue()}). + +%%%---------------------------------------------------------------------- +%%% API +%%%---------------------------------------------------------------------- +start_link(Name) -> + Reg_name = jlib:binary_to_atom(<<"eldap_", + Name/binary>>), + gen_fsm:start_link({local, Reg_name}, ?MODULE, [], []). + +-spec start_link(binary(), [binary()], inet:port_number(), binary(), + binary(), tlsopts()) -> any(). + +start_link(Name, Hosts, Port, Rootdn, Passwd, Opts) -> + Reg_name = jlib:binary_to_atom(<<"eldap_", + Name/binary>>), + gen_fsm:start_link({local, Reg_name}, ?MODULE, + [Hosts, Port, Rootdn, Passwd, Opts], []). + +-spec get_status(handle()) -> any(). + +%%% -------------------------------------------------------------------- +%%% 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. +%%% -------------------------------------------------------------------- +-spec close(handle()) -> any(). + +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) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, + {add, Entry, add_attrs(Attributes)}, ?CALL_TIMEOUT). + +%%% Do sanity check ! +add_attrs(Attrs) -> + F = fun ({Type, Vals}) -> + {'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) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {delete, Entry}, + ?CALL_TIMEOUT). + +%%% -------------------------------------------------------------------- +%%% 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"])] +%%% ) +%%% -------------------------------------------------------------------- +-spec modify(handle(), any(), [add | delete | replace]) -> any(). + +modify(Handle, Object, Mods) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {modify, Object, Mods}, + ?CALL_TIMEOUT). + +%%% +%%% Modification operations. +%%% Example: +%%% replace("telephoneNumber", ["555 555 00"]) +%%% +mod_add(Type, Values) -> + m(add, Type, Values). + +mod_delete(Type, Values) -> + m(delete, Type, 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, +%%% "" +%%% ) +%%% -------------------------------------------------------------------- +mod_replace(Type, Values) -> + m(replace, Type, Values). + +m(Operation, Type, Values) -> + #'ModifyRequest_modification_SEQOF'{operation = + Operation, + modification = + #'AttributeTypeAndValues'{type = + Type, + vals = + Values}}. + +modify_dn(Handle, Entry, NewRDN, DelOldRDN, NewSup) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, + {modify_dn, Entry, NewRDN, bool_p(DelOldRDN), + optional(NewSup)}, + ?CALL_TIMEOUT). + +-spec modify_passwd(handle(), binary(), binary()) -> any(). + +modify_passwd(Handle, DN, Passwd) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, + {modify_passwd, DN, Passwd}, ?CALL_TIMEOUT). + +%%% -------------------------------------------------------------------- +%%% Bind. +%%% Example: +%%% +%%% bind(Handle, +%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com", +%%% "secret") +%%% -------------------------------------------------------------------- +-spec bind(handle(), binary(), binary()) -> any(). + +bind(Handle, RootDN, Passwd) -> + Handle1 = get_handle(Handle), + gen_fsm:sync_send_event(Handle1, {bind, RootDN, Passwd}, + ?CALL_TIMEOUT). + +%%% 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"]}]}], +%%% []}} +%%% +%%% -------------------------------------------------------------------- +-type search_args() :: [{base, binary()} | + {filter, filter()} | + {scope, scope()} | + {attributes, [binary()]} | + {types_only, boolean()} | + {timeout, non_neg_integer()} | + {limit, non_neg_integer()} | + {deref_aliases, never | searching | finding | always}]. + +-spec search(handle(), eldap_search() | search_args()) -> any(). + +search(Handle, A) when is_record(A, eldap_search) -> + call_search(Handle, A); +search(Handle, L) when is_list(L) -> + case catch parse_search_args(L) of + {error, Emsg} -> {error, Emsg}; + {'EXIT', Emsg} -> {error, Emsg}; + A when is_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}, + ?CALL_TIMEOUT). + +-spec parse_search_args(search_args()) -> eldap_search(). + +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 is_integer(Timeout) -> + parse_search_args(T, A#eldap_search{timeout = Timeout}); +parse_search_args([{limit, Limit} | T], A) + when is_integer(Limit) -> + parse_search_args(T, A#eldap_search{limit = Limit}); +parse_search_args([{deref_aliases, never} | T], A) -> + parse_search_args(T, + A#eldap_search{deref_aliases = neverDerefAliases}); +parse_search_args([{deref_aliases, searching} | T], + A) -> + parse_search_args(T, + A#eldap_search{deref_aliases = derefInSearching}); +parse_search_args([{deref_aliases, finding} | T], A) -> + parse_search_args(T, + A#eldap_search{deref_aliases = derefFindingBaseObj}); +parse_search_args([{deref_aliases, always} | T], A) -> + parse_search_args(T, + A#eldap_search{deref_aliases = derefAlways}); +parse_search_args([H | _], _) -> + throw({error, {unknown_arg, H}}); +parse_search_args([], A) -> A. + +baseObject() -> baseObject. + +singleLevel() -> singleLevel. + +%%% +%%% The Scope parameter +%%% +wholeSubtree() -> wholeSubtree. + +%%% +%%% Boolean filter operations +%%% +-type filter() :: 'and'() | 'or'() | 'not'() | equalityMatch() | + greaterOrEqual() | lessOrEqual() | approxMatch() | + present() | substrings() | extensibleMatch(). + +%%% +%%% The following Filter parameters consist of an attribute +%%% and an attribute value. Example: F("uid","tobbe") +%%% +-type 'and'() :: {'and', [filter()]}. +-spec 'and'([filter()]) -> 'and'(). + +'and'(ListOfFilters) when is_list(ListOfFilters) -> + {'and', ListOfFilters}. + +-type 'or'() :: {'or', [filter()]}. +-spec 'or'([filter()]) -> 'or'(). + +'or'(ListOfFilters) when is_list(ListOfFilters) -> + {'or', ListOfFilters}. + +-type 'not'() :: {'not', filter()}. +-spec 'not'(filter()) -> 'not'(). + +'not'(Filter) when is_tuple(Filter) -> {'not', Filter}. + +-type equalityMatch() :: {equalityMatch, 'AttributeValueAssertion'()}. +-spec equalityMatch(binary(), binary()) -> equalityMatch(). + +equalityMatch(Desc, Value) -> + {equalityMatch, av_assert(Desc, Value)}. + +-type greaterOrEqual() :: {greaterOrEqual, 'AttributeValueAssertion'()}. +-spec greaterOrEqual(binary(), binary()) -> greaterOrEqual(). + +greaterOrEqual(Desc, Value) -> + {greaterOrEqual, av_assert(Desc, Value)}. + +-type lessOrEqual() :: {lessOrEqual, 'AttributeValueAssertion'()}. +-spec lessOrEqual(binary(), binary()) -> lessOrEqual(). + +lessOrEqual(Desc, Value) -> + {lessOrEqual, av_assert(Desc, Value)}. + +-type approxMatch() :: {approxMatch, 'AttributeValueAssertion'()}. +-spec approxMatch(binary(), binary()) -> approxMatch(). + +approxMatch(Desc, Value) -> + {approxMatch, av_assert(Desc, Value)}. + +-type 'AttributeValueAssertion'() :: + #'AttributeValueAssertion'{attributeDesc :: binary(), + assertionValue :: binary()}. + +-spec av_assert(binary(), binary()) -> 'AttributeValueAssertion'(). + +av_assert(Desc, Value) -> + #'AttributeValueAssertion'{attributeDesc = Desc, + assertionValue = Value}. + +%%% +%%% Filter to check for the presence of an attribute +%%% +-type present() :: {present, binary()}. +-spec present(binary()) -> present(). + +%%% +%%% 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' +%%% +present(Attribute) -> + {present, Attribute}. + +%%% +%%% extensibleMatch filter. +%%% FIXME: Describe the purpose of this filter. +%%% +%%% Value ::= string( <attribute> ) +%%% Opts ::= listof( {matchingRule, Str} | {type, Str} | {dnAttributes, true} ) +%%% +%%% Example: extensibleMatch("Fred", [{matchingRule, "1.2.3.4.5"}, {type, "cn"}]). +%%% +-type substr() :: [{initial | any | final, binary()}]. +-type 'SubstringFilter'() :: + #'SubstringFilter'{type :: binary(), + substrings :: {'SubstringFilter_substrings', + substr()}}. + +-type substrings() :: {substrings, 'SubstringFilter'()}. +-spec substrings(binary(), substr()) -> substrings(). + +substrings(Type, SubStr) -> + Ss = {'SubstringFilter_substrings', SubStr}, + {substrings, + #'SubstringFilter'{type = Type, substrings = Ss}}. + +-type match_opts() :: [{matchingRule | type, binary()} | + {dnAttributes, boolean()}]. + +-type 'MatchingRuleAssertion'() :: + #'MatchingRuleAssertion'{matchValue :: binary(), + type :: asn1_NOVALUE | binary(), + matchingRule :: asn1_NOVALUE | binary(), + dnAttributes :: asn1_DEFAULT | true}. + +-type extensibleMatch() :: {extensibleMatch, 'MatchingRuleAssertion'()}. +-spec extensibleMatch(binary(), match_opts()) -> extensibleMatch(). + +extensibleMatch(Value, Opts) -> + MRA = #'MatchingRuleAssertion'{matchValue = Value}, + {extensibleMatch, extensibleMatch_opts(Opts, MRA)}. + +extensibleMatch_opts([{matchingRule, Rule} | Opts], MRA) -> + extensibleMatch_opts(Opts, + MRA#'MatchingRuleAssertion'{matchingRule = Rule}); +extensibleMatch_opts([{type, Desc} | Opts], MRA) -> + extensibleMatch_opts(Opts, + MRA#'MatchingRuleAssertion'{type = Desc}); +extensibleMatch_opts([{dnAttributes, true} | Opts], + MRA) -> + extensibleMatch_opts(Opts, + MRA#'MatchingRuleAssertion'{dnAttributes = true}); +extensibleMatch_opts([_ | Opts], MRA) -> + extensibleMatch_opts(Opts, MRA); +extensibleMatch_opts([], MRA) -> MRA. + +get_handle(Pid) when is_pid(Pid) -> Pid; +get_handle(Atom) when is_atom(Atom) -> Atom; +get_handle(Name) when is_binary(Name) -> + jlib:binary_to_atom(<<"eldap_", + Name/binary>>). + +%%%---------------------------------------------------------------------- +%%% 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([Hosts, Port, Rootdn, Passwd, Opts]) -> + Encrypt = case gen_mod:get_opt(encrypt, Opts, + fun(tls) -> tls; + (starttls) -> starttls; + (none) -> none + end) of + tls -> tls; + _ -> none + end, + PortTemp = case Port of + undefined -> + case Encrypt of + tls -> ?LDAPS_PORT; + _ -> ?LDAP_PORT + end; + PT -> PT + end, + CacertOpts = case gen_mod:get_opt( + tls_cacertfile, Opts, + fun(S) when is_binary(S) -> + binary_to_list(S); + (undefined) -> + undefined + end) of + undefined -> + []; + Path -> + [{cacertfile, Path}] + end, + DepthOpts = case gen_mod:get_opt( + tls_depth, Opts, + fun(I) when is_integer(I), I>=0 -> + I; + (undefined) -> + undefined + end) of + undefined -> + []; + Depth -> + [{depth, Depth}] + end, + Verify = gen_mod:get_opt(tls_verify, Opts, + fun(hard) -> hard; + (soft) -> soft; + (false) -> false + end, false), + TLSOpts = if (Verify == hard orelse Verify == soft) + andalso CacertOpts == [] -> + ?WARNING_MSG("TLS verification is enabled but no CA " + "certfiles configured, so verification " + "is disabled.", + []), + []; + Verify == soft -> + [{verify, 1}] ++ CacertOpts ++ DepthOpts; + Verify == hard -> + [{verify, 2}] ++ CacertOpts ++ DepthOpts; + true -> [] + end, + {ok, connecting, + #eldap{hosts = Hosts, port = PortTemp, rootdn = Rootdn, + passwd = Passwd, tls = Encrypt, tls_options = TLSOpts, + id = 0, dict = dict:new(), req_q = queue:new()}, + 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) -> + Q = queue:in({Event, From}, S#eldap.req_q), + {next_state, connecting, S#eldap{req_q = Q}}. + +wait_bind_response(Event, From, S) -> + Q = queue:in({Event, From}, S#eldap.req_q), + {next_state, wait_bind_response, S#eldap{req_q = Q}}. + +active_bind(Event, From, S) -> + Q = queue:in({Event, From}, S#eldap.req_q), + {next_state, active_bind, S#eldap{req_q = Q}}. + +active(Event, From, S) -> + process_command(S, Event, From). + +%%---------------------------------------------------------------------- +%% 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) -> + catch (S#eldap.sockmod):close(S#eldap.fd), + {stop, normal, 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(_Event, _From, StateName, S) -> + {reply, {StateName, S}, StateName, S}. + +%%---------------------------------------------------------------------- +%% Func: handle_info/3 +%% Returns: {next_state, NextStateName, NextStateData} | +%% {next_state, NextStateName, NextStateData, Timeout} | +%% {stop, Reason, NewStateData} +%% {stop, Reason, NewStateData} +%%---------------------------------------------------------------------- + +%% +%% Packets arriving in various states +%% +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({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 -> dequeue_commands(S); + {fail_bind, Reason} -> + report_bind_failure(S#eldap.host, S#eldap.port, Reason), + {next_state, connecting, + close_and_retry(S, ?GRACEFUL_RETRY_TIMEOUT)}; + {'EXIT', Reason} -> + report_bind_failure(S#eldap.host, S#eldap.port, Reason), + {next_state, connecting, close_and_retry(S)}; + {error, Reason} -> + report_bind_failure(S#eldap.host, S#eldap.port, Reason), + {next_state, connecting, close_and_retry(S)} + end; +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 + {reply, Reply, To, S1} -> gen_fsm:reply(To, Reply), S1; + {ok, S1} -> S1 + end, + if StateName == active_bind andalso + RequestType == bindRequest + orelse StateName == active -> + dequeue_commands(NewS); + true -> {next_state, StateName, NewS} + end; + _ -> {next_state, StateName, S} + end; +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({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)}; +%% +%% Timers +%% +handle_info({timeout, Timer, {cmd_timeout, Id}}, + StateName, S) -> + case cmd_timeout(Timer, Id, S) of + {reply, To, Reason, NewS} -> + gen_fsm:reply(To, Reason), + {next_state, StateName, NewS}; + {error, _Reason} -> {next_state, StateName, 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) -> + {next_state, connecting, close_and_retry(S)}; +%% +%% Make sure we don't fill the message queue with rubbish +%% +handle_info(Info, StateName, S) -> + ?DEBUG("eldap. Unexpected Info: ~p~nIn state: " + "~p~n when StateData is: ~p", + [Info, StateName, 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 +%%%---------------------------------------------------------------------- +dequeue_commands(S) -> + case queue:out(S#eldap.req_q) of + {{value, {Event, From}}, Q} -> + case process_command(S#eldap{req_q = Q}, Event, From) of + {_, active, NewS} -> dequeue_commands(NewS); + Res -> Res + end; + {empty, _} -> {next_state, active, S} + end. + +process_command(S, Event, From) -> + case send_command(Event, From, S) of + {ok, NewS} -> + case Event of + {bind, _, _} -> {next_state, active_bind, NewS}; + _ -> {next_state, active, NewS} + end; + {error, _Reason} -> + Q = queue:in_r({Event, From}, S#eldap.req_q), + NewS = close_and_retry(S#eldap{req_q = Q}), + {next_state, connecting, NewS} + end. + +send_command(Command, From, S) -> + Id = bump_id(S), + {Name, Request} = gen_req(Command), + Message = #'LDAPMessage'{messageID = Id, + protocolOp = {Name, Request}}, + ?DEBUG("~p~n", [{Name, Request}]), + {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', + Message), + 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), + {ok, S#eldap{id = Id, dict = New_dict}}; + Error -> Error + end. + +gen_req({search, A}) -> + {searchRequest, + #'SearchRequest'{baseObject = A#eldap_search.base, + scope = A#eldap_search.scope, + derefAliases = A#eldap_search.deref_aliases, + sizeLimit = A#eldap_search.limit, + timeLimit = A#eldap_search.timeout, + typesOnly = A#eldap_search.types_only, + filter = A#eldap_search.filter, + 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}) -> + {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({modify_passwd, DN, Passwd}) -> + {ok, ReqVal} = asn1rt:encode('ELDAPv3', + 'PasswdModifyRequestValue', + #'PasswdModifyRequestValue'{userIdentity = DN, + newPasswd = + Passwd}), + {extendedReq, + #'ExtendedRequest'{requestName = ?passwdModifyOID, + requestValue = iolist_to_binary(ReqVal)}}; +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) -> + case asn1rt:decode('ELDAPv3', 'LDAPMessage', Pkt) of + {ok, Msg} -> + Op = Msg#'LDAPMessage'.protocolOp, + ?DEBUG("~p", [Op]), + Dict = S#eldap.dict, + Id = Msg#'LDAPMessage'.messageID, + {Timer, From, Name, Result_so_far} = get_op_rec(Id, + Dict), + Answer = case {Name, Op} of + {searchRequest, {searchResEntry, R}} + when is_record(R, 'SearchResultEntry') -> + New_dict = dict:append(Id, R, Dict), + {ok, S#eldap{dict = New_dict}}; + {searchRequest, {searchResDone, Result}} -> + Reason = Result#'LDAPResult'.resultCode, + if Reason == success; Reason == sizeLimitExceeded -> + {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}}; + true -> + 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}}; + {extendedReq, {extendedResp, Result}} -> + New_dict = dict:erase(Id, Dict), + cancel_timer(Timer), + Reply = check_extended_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, + {response, Answer, Name}; + 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}. + +%% TODO: process reply depending on requestName: +%% this requires BER-decoding of #'ExtendedResponse'.response +check_extended_reply(#'ExtendedResponse'{resultCode = + success}, + _From) -> + ok; +check_extended_reply(#'ExtendedResponse'{resultCode = + Reason}, + _From) -> + {error, Reason}; +check_extended_reply(Other, _From) -> {error, Other}. + +get_op_rec(Id, Dict) -> + case dict:find(Id, Dict) of + {ok, [{Timer, _Command, 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) -> + case asn1rt:decode('ELDAPv3', 'LDAPMessage', Pkt) of + {ok, Msg} -> + ?DEBUG("~p", [Msg]), + check_id(S#eldap.id, Msg#'LDAPMessage'.messageID), + case Msg#'LDAPMessage'.protocolOp of + {bindResponse, Result} -> + case Result#'BindResponse'.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. + + +close_and_retry(S, Timeout) -> + catch (S#eldap.sockmod):close(S#eldap.fd), + Queue = dict:fold(fun (_Id, + [{Timer, Command, From, _Name} | _], Q) -> + cancel_timer(Timer), + queue:in_r({Command, From}, Q); + (_, _, Q) -> Q + end, + S#eldap.req_q, S#eldap.dict), + erlang:send_after(Timeout, self(), + {timeout, retry_connect}), + S#eldap{fd = undefined, req_q = Queue, dict = dict:new()}. + +close_and_retry(S) -> + close_and_retry(S, ?RETRY_TIMEOUT). + +report_bind_failure(Host, Port, Reason) -> + ?WARNING_MSG("LDAP bind failed on ~s:~p~nReason: ~p", + [Host, Port, Reason]). + +%%----------------------------------------------------------------------- +%% Sort out timed out commands +%%----------------------------------------------------------------------- +cmd_timeout(Timer, Id, S) -> + Dict = S#eldap.dict, + case dict:find(Id, Dict) of + {ok, [{Timer, _Command, 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}}; + _ -> + 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 is_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), + ?INFO_MSG("LDAP connection on ~s:~p", + [Host, S#eldap.port]), + Opts = if S#eldap.tls == tls -> + [{packet, asn1}, {active, true}, {keepalive, true}, + binary + | S#eldap.tls_options]; + true -> + [{packet, asn1}, {active, true}, {keepalive, true}, + {send_timeout, ?SEND_TIMEOUT}, binary] + end, + HostS = binary_to_list(Host), + SocketData = case S#eldap.tls of + tls -> + SockMod = ssl, ssl:connect(HostS, S#eldap.port, Opts); + %% starttls -> %% TODO: Implement STARTTLS; + _ -> + SockMod = gen_tcp, + gen_tcp:connect(HostS, S#eldap.port, Opts) + end, + case SocketData of + {ok, Socket} -> + 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} -> + report_bind_failure(Host, S#eldap.port, Reason), + NewS = close_and_retry(S), + {ok, connecting, NewS#eldap{host = Host}} + end; + {error, Reason} -> + ?ERROR_MSG("LDAP connection failed:~n** Server: " + "~s:~p~n** Reason: ~p~n** Socket options: ~p", + [Host, S#eldap.port, Reason, Opts]), + NewS = close_and_retry(S), + {ok, connecting, NewS#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}}, + ?DEBUG("Bind Request Message:~p~n", [Message]), + {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', + Message), + case (S#eldap.sockmod):send(Socket, Bytes) of + ok -> {ok, S#eldap{id = Id}}; + Error -> Error + end. + +%% Given last tried Server, find next one to try +next_host(undefined, [H | _]) -> + H; % First time, take first +next_host(Host, + Hosts) -> % Find next in turn + next_host(Host, Hosts, Hosts). + +%%% -------------------------------------------------------------------- +%%% Verify the input data +%%% -------------------------------------------------------------------- +%%% -------------------------------------------------------------------- +%%% Get and Validate the initial configuration +%%% -------------------------------------------------------------------- +%% get_atom(Key, List) -> +%% case lists:keysearch(Key, 1, List) of +%% {value, {Key, Value}} when is_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. +%%% -------------------------------------------------------------------- +%%% Other Stuff +%%% -------------------------------------------------------------------- +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, [_ | T], Hosts) -> + next_host(Host, T, Hosts). + +bump_id(#eldap{id = Id}) + when Id > (?MAX_TRANSACTION_ID) -> + ?MIN_TRANSACTION_ID; +bump_id(#eldap{id = Id}) -> Id + 1. |