PageRenderTime 68ms CodeModel.GetById 30ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/eldap/test/eldap_fsm.erl

https://github.com/babo/jungerl
Erlang | 960 lines | 574 code | 106 blank | 280 comment | 6 complexity | bcacc0adeb913e6e6af90b2db68b94db MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause, AGPL-1.0
  1. -module(eldap_fsm).
  2. %%% --------------------------------------------------------------------
  3. %%% Created: 12 Oct 2000 by Tobbe <tnt@home.se>
  4. %%% Function: Erlang client LDAP implementation according RFC 2251.
  5. %%% The interface is based on RFC 1823, and
  6. %%% draft-ietf-asid-ldap-c-api-00.txt
  7. %%%
  8. %%% Copyright (C) 2000 Torbjörn Törnkvist, tnt@home.se
  9. %%%
  10. %%% This program is free software; you can redistribute it and/or modify
  11. %%% it under the terms of the GNU General Public License as published by
  12. %%% the Free Software Foundation; either version 2 of the License, or
  13. %%% (at your option) any later version.
  14. %%%
  15. %%% This program is distributed in the hope that it will be useful,
  16. %%% but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. %%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. %%% GNU General Public License for more details.
  19. %%%
  20. %%% You should have received a copy of the GNU General Public License
  21. %%% along with this program; if not, write to the Free Software
  22. %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  23. %%% Modified by Sean Hinde <shinde@iee.org> 7th Dec 2000
  24. %%% Turned into gen_fsm, made non-blocking, added timers etc to support this.
  25. %%% Now has the concept of a name (string() or atom()) per instance which allows
  26. %%% multiple users to call by name if so desired.
  27. %%%
  28. %%% Can be configured with start_link parameters or use a config file to get
  29. %%% host to connect to, dn, password, log function etc.
  30. %%% --------------------------------------------------------------------
  31. -vc('$Id$ ').
  32. %%%----------------------------------------------------------------------
  33. %%% LDAP Client state machine.
  34. %%% Possible states are:
  35. %%% connecting - actually disconnected, but retrying periodically
  36. %%% wait_bind_response - connected and sent bind request
  37. %%% active - bound to LDAP Server and ready to handle commands
  38. %%%----------------------------------------------------------------------
  39. %%-compile(export_all).
  40. %%-export([Function/Arity, ...]).
  41. -behaviour(gen_fsm).
  42. %% External exports
  43. -export([start_link/1, start_link/5, start_link/6]).
  44. -export([baseObject/0,singleLevel/0,wholeSubtree/0,close/1,
  45. equalityMatch/2,greaterOrEqual/2,lessOrEqual/2,
  46. approxMatch/2,search/2,substrings/2,present/1,
  47. 'and'/1,'or'/1,'not'/1,modify/3, mod_add/2, mod_delete/2,
  48. mod_replace/2, add/3, delete/2, modify_dn/5]).
  49. -export([debug_level/2, get_status/1]).
  50. %% gen_fsm callbacks
  51. -export([init/1, connecting/2,
  52. connecting/3, wait_bind_response/3, active/3, handle_event/3,
  53. handle_sync_event/4, handle_info/3, terminate/3, code_change/4]).
  54. -import(lists,[concat/1]).
  55. -include("ELDAPv3.hrl").
  56. -include("eldap.hrl").
  57. -define(LDAP_VERSION, 3).
  58. -define(RETRY_TIMEOUT, 5000).
  59. -define(BIND_TIMEOUT, 10000).
  60. -define(CMD_TIMEOUT, 5000).
  61. -define(MAX_TRANSACTION_ID, 65535).
  62. -define(MIN_TRANSACTION_ID, 0).
  63. -record(eldap, {version = ?LDAP_VERSION,
  64. hosts, % Possible hosts running LDAP servers
  65. host = null, % Connected Host LDAP server
  66. port = 389 , % The LDAP server port
  67. fd = null, % Socket filedescriptor.
  68. rootdn = "", % Name of the entry to bind as
  69. passwd, % Password for (above) entry
  70. id = 0, % LDAP Request ID
  71. log, % User provided log function
  72. bind_timer, % Ref to bind timeout
  73. dict, % dict holding operation params and results
  74. debug_level % Integer debug/logging level
  75. }).
  76. %%%----------------------------------------------------------------------
  77. %%% API
  78. %%%----------------------------------------------------------------------
  79. start_link(Name) ->
  80. Reg_name = list_to_atom("eldap_" ++ Name),
  81. gen_fsm:start_link({local, Reg_name}, ?MODULE, [], []).
  82. start_link(Name, Hosts, Port, Rootdn, Passwd) ->
  83. Log = fun(N, Fmt, Args) -> io:format("---- " ++ Fmt, [Args]) end,
  84. Reg_name = list_to_atom("eldap_" ++ Name),
  85. gen_fsm:start_link({local, Reg_name}, ?MODULE, {Hosts, Port, Rootdn, Passwd, Log}, []).
  86. start_link(Name, Hosts, Port, Rootdn, Passwd, Log) ->
  87. Reg_name = list_to_atom("eldap_" ++ Name),
  88. gen_fsm:start_link({local, Reg_name}, ?MODULE, {Hosts, Port, Rootdn, Passwd, Log}, []).
  89. %%% --------------------------------------------------------------------
  90. %%% Set Debug Level. 0 - none, 1 - errors, 2 - ldap events
  91. %%% --------------------------------------------------------------------
  92. debug_level(Handle, N) when integer(N) ->
  93. Handle1 = get_handle(Handle),
  94. gen_fsm:sync_send_all_state_event(Handle1, {debug_level,N}).
  95. %%% --------------------------------------------------------------------
  96. %%% Get status of connection.
  97. %%% --------------------------------------------------------------------
  98. get_status(Handle) ->
  99. Handle1 = get_handle(Handle),
  100. gen_fsm:sync_send_all_state_event(Handle1, get_status).
  101. %%% --------------------------------------------------------------------
  102. %%% Shutdown connection (and process) asynchronous.
  103. %%% --------------------------------------------------------------------
  104. close(Handle) ->
  105. Handle1 = get_handle(Handle),
  106. gen_fsm:send_all_state_event(Handle1, close).
  107. %%% --------------------------------------------------------------------
  108. %%% Add an entry. The entry field MUST NOT exist for the AddRequest
  109. %%% to succeed. The parent of the entry MUST exist.
  110. %%% Example:
  111. %%%
  112. %%% add(Handle,
  113. %%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com",
  114. %%% [{"objectclass", ["person"]},
  115. %%% {"cn", ["Bill Valentine"]},
  116. %%% {"sn", ["Valentine"]},
  117. %%% {"telephoneNumber", ["545 555 00"]}]
  118. %%% )
  119. %%% --------------------------------------------------------------------
  120. add(Handle, Entry, Attributes) when list(Entry),list(Attributes) ->
  121. Handle1 = get_handle(Handle),
  122. gen_fsm:sync_send_event(Handle1, {add, Entry, add_attrs(Attributes)}).
  123. %%% Do sanity check !
  124. add_attrs(Attrs) ->
  125. F = fun({Type,Vals}) when list(Type),list(Vals) ->
  126. %% Confused ? Me too... :-/
  127. {'AddRequest_attributes',Type, Vals}
  128. end,
  129. case catch lists:map(F, Attrs) of
  130. {'EXIT', _} -> throw({error, attribute_values});
  131. Else -> Else
  132. end.
  133. %%% --------------------------------------------------------------------
  134. %%% Delete an entry. The entry consists of the DN of
  135. %%% the entry to be deleted.
  136. %%% Example:
  137. %%%
  138. %%% delete(Handle,
  139. %%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com"
  140. %%% )
  141. %%% --------------------------------------------------------------------
  142. delete(Handle, Entry) when list(Entry) ->
  143. Handle1 = get_handle(Handle),
  144. gen_fsm:sync_send_event(Handle1, {delete, Entry}).
  145. %%% --------------------------------------------------------------------
  146. %%% Modify an entry. Given an entry a number of modification
  147. %%% operations can be performed as one atomic operation.
  148. %%% Example:
  149. %%%
  150. %%% modify(Handle,
  151. %%% "cn=Torbjorn Tornkvist, ou=people, o=Bluetail AB, dc=bluetail, dc=com",
  152. %%% [replace("telephoneNumber", ["555 555 00"]),
  153. %%% add("description", ["LDAP hacker"])]
  154. %%% )
  155. %%% --------------------------------------------------------------------
  156. modify(Handle, Object, Mods) when list(Object), list(Mods) ->
  157. Handle1 = get_handle(Handle),
  158. gen_fsm:sync_send_event(Handle1, {modify, Object, Mods}).
  159. %%%
  160. %%% Modification operations.
  161. %%% Example:
  162. %%% replace("telephoneNumber", ["555 555 00"])
  163. %%%
  164. mod_add(Type, Values) when list(Type), list(Values) -> m(add, Type, Values).
  165. mod_delete(Type, Values) when list(Type), list(Values) -> m(delete, Type, Values).
  166. mod_replace(Type, Values) when list(Type), list(Values) -> m(replace, Type, Values).
  167. m(Operation, Type, Values) ->
  168. #'ModifyRequest_modification_SEQOF'{
  169. operation = Operation,
  170. modification = #'AttributeTypeAndValues'{
  171. type = Type,
  172. vals = Values}}.
  173. %%% --------------------------------------------------------------------
  174. %%% Modify an entry. Given an entry a number of modification
  175. %%% operations can be performed as one atomic operation.
  176. %%% Example:
  177. %%%
  178. %%% modify_dn(Handle,
  179. %%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com",
  180. %%% "cn=Ben Emerson",
  181. %%% true,
  182. %%% ""
  183. %%% )
  184. %%% --------------------------------------------------------------------
  185. modify_dn(Handle, Entry, NewRDN, DelOldRDN, NewSup)
  186. when list(Entry),list(NewRDN),atom(DelOldRDN),list(NewSup) ->
  187. Handle1 = get_handle(Handle),
  188. gen_fsm:sync_send_event(Handle1, {modify_dn, Entry, NewRDN, bool_p(DelOldRDN), optional(NewSup)}).
  189. %%% Sanity checks !
  190. bool_p(Bool) when Bool==true;Bool==false -> Bool.
  191. optional([]) -> asn1_NOVALUE;
  192. optional(Value) -> Value.
  193. %%% --------------------------------------------------------------------
  194. %%% Synchronous search of the Directory returning a
  195. %%% requested set of attributes.
  196. %%%
  197. %%% Example:
  198. %%%
  199. %%% Filter = eldap:substrings("sn", [{any,"o"}]),
  200. %%% eldap:search(S, [{base, "dc=bluetail, dc=com"},
  201. %%% {filter, Filter},
  202. %%% {attributes,["cn"]}])),
  203. %%%
  204. %%% Returned result: {ok, #eldap_search_result{}}
  205. %%%
  206. %%% Example:
  207. %%%
  208. %%% {ok,{eldap_search_result,
  209. %%% [{eldap_entry,
  210. %%% "cn=Magnus Froberg, dc=bluetail, dc=com",
  211. %%% [{"cn",["Magnus Froberg"]}]},
  212. %%% {eldap_entry,
  213. %%% "cn=Torbjorn Tornkvist, dc=bluetail, dc=com",
  214. %%% [{"cn",["Torbjorn Tornkvist"]}]}],
  215. %%% []}}
  216. %%%
  217. %%% --------------------------------------------------------------------
  218. search(Handle, A) when record(A, eldap_search) ->
  219. call_search(Handle, A);
  220. search(Handle, L) when list(Handle), list(L) ->
  221. case catch parse_search_args(L) of
  222. {error, Emsg} -> {error, Emsg};
  223. {'EXIT', Emsg} -> {error, Emsg};
  224. A when record(A, eldap_search) -> call_search(Handle, A)
  225. end.
  226. call_search(Handle, A) ->
  227. Handle1 = get_handle(Handle),
  228. gen_fsm:sync_send_event(Handle1, {search, A}).
  229. parse_search_args(Args) ->
  230. parse_search_args(Args, #eldap_search{scope = wholeSubtree}).
  231. parse_search_args([{base, Base}|T],A) ->
  232. parse_search_args(T,A#eldap_search{base = Base});
  233. parse_search_args([{filter, Filter}|T],A) ->
  234. parse_search_args(T,A#eldap_search{filter = Filter});
  235. parse_search_args([{scope, Scope}|T],A) ->
  236. parse_search_args(T,A#eldap_search{scope = Scope});
  237. parse_search_args([{attributes, Attrs}|T],A) ->
  238. parse_search_args(T,A#eldap_search{attributes = Attrs});
  239. parse_search_args([{types_only, TypesOnly}|T],A) ->
  240. parse_search_args(T,A#eldap_search{types_only = TypesOnly});
  241. parse_search_args([{timeout, Timeout}|T],A) when integer(Timeout) ->
  242. parse_search_args(T,A#eldap_search{timeout = Timeout});
  243. parse_search_args([H|T],A) ->
  244. throw({error,{unknown_arg, H}});
  245. parse_search_args([],A) ->
  246. A.
  247. %%%
  248. %%% The Scope parameter
  249. %%%
  250. baseObject() -> baseObject.
  251. singleLevel() -> singleLevel.
  252. wholeSubtree() -> wholeSubtree.
  253. %%%
  254. %%% Boolean filter operations
  255. %%%
  256. 'and'(ListOfFilters) when list(ListOfFilters) -> {'and',ListOfFilters}.
  257. 'or'(ListOfFilters) when list(ListOfFilters) -> {'or', ListOfFilters}.
  258. 'not'(Filter) when tuple(Filter) -> {'not',Filter}.
  259. %%%
  260. %%% The following Filter parameters consist of an attribute
  261. %%% and an attribute value. Example: F("uid","tobbe")
  262. %%%
  263. equalityMatch(Desc, Value) -> {equalityMatch, av_assert(Desc, Value)}.
  264. greaterOrEqual(Desc, Value) -> {greaterOrEqual, av_assert(Desc, Value)}.
  265. lessOrEqual(Desc, Value) -> {lessOrEqual, av_assert(Desc, Value)}.
  266. approxMatch(Desc, Value) -> {approxMatch, av_assert(Desc, Value)}.
  267. av_assert(Desc, Value) ->
  268. #'AttributeValueAssertion'{attributeDesc = Desc,
  269. assertionValue = Value}.
  270. %%%
  271. %%% Filter to check for the presence of an attribute
  272. %%%
  273. present(Attribute) when list(Attribute) ->
  274. {present, Attribute}.
  275. %%%
  276. %%% A substring filter seem to be based on a pattern:
  277. %%%
  278. %%% InitValue*AnyValue*FinalValue
  279. %%%
  280. %%% where all three parts seem to be optional (at least when
  281. %%% talking with an OpenLDAP server). Thus, the arguments
  282. %%% to substrings/2 looks like this:
  283. %%%
  284. %%% Type ::= string( <attribute> )
  285. %%% SubStr ::= listof( {initial,Value} | {any,Value}, {final,Value})
  286. %%%
  287. %%% Example: substrings("sn",[{initial,"To"},{any,"kv"},{final,"st"}])
  288. %%% will match entries containing: 'sn: Tornkvist'
  289. %%%
  290. substrings(Type, SubStr) when list(Type), list(SubStr) ->
  291. Ss = {'SubstringFilter_substrings',v_substr(SubStr)},
  292. {substrings,#'SubstringFilter'{type = Type,
  293. substrings = Ss}}.
  294. get_handle(Pid) when pid(Pid) -> Pid;
  295. get_handle(Atom) when atom(Atom) -> Atom;
  296. get_handle(Name) when list(Name) -> list_to_atom("eldap_" ++ Name).
  297. %%%----------------------------------------------------------------------
  298. %%% Callback functions from gen_fsm
  299. %%%----------------------------------------------------------------------
  300. %%----------------------------------------------------------------------
  301. %% Func: init/1
  302. %% Returns: {ok, StateName, StateData} |
  303. %% {ok, StateName, StateData, Timeout} |
  304. %% ignore |
  305. %% {stop, StopReason}
  306. %% I use the trick of setting a timeout of 0 to pass control into the
  307. %% process.
  308. %%----------------------------------------------------------------------
  309. init([]) ->
  310. case get_config() of
  311. {ok, Hosts, Rootdn, Passwd, Log} ->
  312. init({Hosts, Rootdn, Passwd, Log});
  313. {error, Reason} ->
  314. {stop, Reason}
  315. end;
  316. init({Hosts, Port, Rootdn, Passwd, Log}) ->
  317. {ok, connecting, #eldap{hosts = Hosts,
  318. port = Port,
  319. rootdn = Rootdn,
  320. passwd = Passwd,
  321. id = 0,
  322. log = Log,
  323. dict = dict:new(),
  324. debug_level = 0}, 0}.
  325. %%----------------------------------------------------------------------
  326. %% Func: StateName/2
  327. %% Called when gen_fsm:send_event/2,3 is invoked (async)
  328. %% Returns: {next_state, NextStateName, NextStateData} |
  329. %% {next_state, NextStateName, NextStateData, Timeout} |
  330. %% {stop, Reason, NewStateData}
  331. %%----------------------------------------------------------------------
  332. connecting(timeout, S) ->
  333. {ok, NextState, NewS} = connect_bind(S),
  334. {next_state, NextState, NewS}.
  335. %%----------------------------------------------------------------------
  336. %% Func: StateName/3
  337. %% Called when gen_fsm:sync_send_event/2,3 is invoked.
  338. %% Returns: {next_state, NextStateName, NextStateData} |
  339. %% {next_state, NextStateName, NextStateData, Timeout} |
  340. %% {reply, Reply, NextStateName, NextStateData} |
  341. %% {reply, Reply, NextStateName, NextStateData, Timeout} |
  342. %% {stop, Reason, NewStateData} |
  343. %% {stop, Reason, Reply, NewStateData}
  344. %%----------------------------------------------------------------------
  345. connecting(Event, From, S) ->
  346. Reply = {error, connecting},
  347. {reply, Reply, connecting, S}.
  348. wait_bind_response(Event, From, S) ->
  349. Reply = {error, wait_bind_response},
  350. {reply, Reply, wait_bind_response, S}.
  351. active(Event, From, S) ->
  352. case catch send_command(Event, From, S) of
  353. {ok, NewS} ->
  354. {next_state, active, NewS};
  355. {error, Reason} ->
  356. {reply, {error, Reason}, active, S};
  357. {'EXIT', Reason} ->
  358. {reply, {error, Reason}, active, S}
  359. end.
  360. %%----------------------------------------------------------------------
  361. %% Func: handle_event/3
  362. %% Called when gen_fsm:send_all_state_event/2 is invoked.
  363. %% Returns: {next_state, NextStateName, NextStateData} |
  364. %% {next_state, NextStateName, NextStateData, Timeout} |
  365. %% {stop, Reason, NewStateData}
  366. %%----------------------------------------------------------------------
  367. handle_event(close, StateName, S) ->
  368. gen_tcp:close(S#eldap.fd),
  369. {stop, closed, S};
  370. handle_event(Event, StateName, S) ->
  371. {next_state, StateName, S}.
  372. %%----------------------------------------------------------------------
  373. %% Func: handle_sync_event/4
  374. %% Called when gen_fsm:sync_send_all_state_event/2,3 is invoked
  375. %% Returns: {next_state, NextStateName, NextStateData} |
  376. %% {next_state, NextStateName, NextStateData, Timeout} |
  377. %% {reply, Reply, NextStateName, NextStateData} |
  378. %% {reply, Reply, NextStateName, NextStateData, Timeout} |
  379. %% {stop, Reason, NewStateData} |
  380. %% {stop, Reason, Reply, NewStateData}
  381. %%----------------------------------------------------------------------
  382. handle_sync_event({debug_level, N}, From, StateName, S) ->
  383. {reply, ok, StateName, S#eldap{debug_level = N}};
  384. handle_sync_event(Event, From, StateName, S) ->
  385. {reply, {StateName, S}, StateName, S};
  386. handle_sync_event(Event, From, StateName, S) ->
  387. Reply = ok,
  388. {reply, Reply, StateName, S}.
  389. %%----------------------------------------------------------------------
  390. %% Func: handle_info/3
  391. %% Returns: {next_state, NextStateName, NextStateData} |
  392. %% {next_state, NextStateName, NextStateData, Timeout} |
  393. %% {stop, Reason, NewStateData}
  394. %%----------------------------------------------------------------------
  395. %%
  396. %% Packets arriving in various states
  397. %%
  398. handle_info({tcp, Socket, Data}, connecting, S) ->
  399. log1("eldap. tcp packet received when disconnected!~n~p~n", [Data], S),
  400. {next_state, connecting, S};
  401. handle_info({tcp, Socket, Data}, wait_bind_response, S) ->
  402. cancel_timer(S#eldap.bind_timer),
  403. case catch recvd_wait_bind_response(Data, S) of
  404. bound -> {next_state, active, S};
  405. {fail_bind, Reason} -> close_and_retry(S),
  406. {next_state, connecting, S#eldap{fd = null}};
  407. {'EXIT', Reason} -> close_and_retry(S),
  408. {next_state, connecting, S#eldap{fd = null}};
  409. {error, Reason} -> close_and_retry(S),
  410. {next_state, connecting, S#eldap{fd = null}}
  411. end;
  412. handle_info({tcp, Socket, Data}, active, S) ->
  413. case catch recvd_packet(Data, S) of
  414. {reply, Reply, To, NewS} -> gen_fsm:reply(To, Reply),
  415. {next_state, active, NewS};
  416. {ok, NewS} -> {next_state, active, NewS};
  417. {'EXIT', Reason} -> {next_state, active, S};
  418. {error, Reason} -> {next_state, active, S}
  419. end;
  420. handle_info({tcp_closed, Socket}, All_fsm_states, S) ->
  421. F = fun(Id, [{Timer, From, Name}|Res]) ->
  422. gen_fsm:reply(From, {error, tcp_closed}),
  423. cancel_timer(Timer)
  424. end,
  425. dict:map(F, S#eldap.dict),
  426. retry_connect(),
  427. {next_state, connecting, S#eldap{fd = null,
  428. dict = dict:new()}};
  429. handle_info({tcp_error, Socket, Reason}, Fsm_state, S) ->
  430. log1("eldap received tcp_error: ~p~nIn State: ~p~n", [Reason, Fsm_state], S),
  431. {next_state, Fsm_state, S};
  432. %%
  433. %% Timers
  434. %%
  435. handle_info({timeout, Timer, {cmd_timeout, Id}}, active, S) ->
  436. case cmd_timeout(Timer, Id, S) of
  437. {reply, To, Reason, NewS} -> gen_fsm:reply(To, Reason),
  438. {next_state, active, NewS};
  439. {error, Reason} -> {next_state, active, S}
  440. end;
  441. handle_info({timeout, retry_connect}, connecting, S) ->
  442. {ok, NextState, NewS} = connect_bind(S),
  443. {next_state, NextState, NewS};
  444. handle_info({timeout, Timer, bind_timeout}, wait_bind_response, S) ->
  445. close_and_retry(S),
  446. {next_state, connecting, S#eldap{fd = null}};
  447. %%
  448. %% Make sure we don't fill the message queue with rubbish
  449. %%
  450. handle_info(Info, StateName, S) ->
  451. log1("eldap. Unexpected Info: ~p~nIn state: ~p~n when StateData is: ~p~n",
  452. [Info, StateName, S], S),
  453. {next_state, StateName, S}.
  454. %%----------------------------------------------------------------------
  455. %% Func: terminate/3
  456. %% Purpose: Shutdown the fsm
  457. %% Returns: any
  458. %%----------------------------------------------------------------------
  459. terminate(Reason, StateName, StatData) ->
  460. ok.
  461. %%----------------------------------------------------------------------
  462. %% Func: code_change/4
  463. %% Purpose: Convert process state when code is changed
  464. %% Returns: {ok, NewState, NewStateData}
  465. %%----------------------------------------------------------------------
  466. code_change(OldVsn, StateName, S, Extra) ->
  467. {ok, StateName, S}.
  468. %%%----------------------------------------------------------------------
  469. %%% Internal functions
  470. %%%----------------------------------------------------------------------
  471. send_command(Command, From, S) ->
  472. Id = bump_id(S),
  473. {Name, Request} = gen_req(Command),
  474. Message = #'LDAPMessage'{messageID = Id,
  475. protocolOp = {Name, Request}},
  476. log2("~p~n",[{Name, Request}], S),
  477. {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', Message),
  478. ok = gen_tcp:send(S#eldap.fd, Bytes),
  479. Timer = erlang:start_timer(?CMD_TIMEOUT, self(), {cmd_timeout, Id}),
  480. New_dict = dict:store(Id, [{Timer, From, Name}], S#eldap.dict),
  481. {ok, S#eldap{id = Id,
  482. dict = New_dict}}.
  483. gen_req({search, A}) ->
  484. {searchRequest,
  485. #'SearchRequest'{baseObject = A#eldap_search.base,
  486. scope = v_scope(A#eldap_search.scope),
  487. derefAliases = neverDerefAliases,
  488. sizeLimit = 0, % no size limit
  489. timeLimit = v_timeout(A#eldap_search.timeout),
  490. typesOnly = v_bool(A#eldap_search.types_only),
  491. filter = v_filter(A#eldap_search.filter),
  492. attributes = v_attributes(A#eldap_search.attributes)
  493. }};
  494. gen_req({add, Entry, Attrs}) ->
  495. {addRequest,
  496. #'AddRequest'{entry = Entry,
  497. attributes = Attrs}};
  498. gen_req({delete, Entry}) ->
  499. {delRequest, Entry};
  500. gen_req({modify, Obj, Mod}) ->
  501. v_modifications(Mod),
  502. {modifyRequest,
  503. #'ModifyRequest'{object = Obj,
  504. modification = Mod}};
  505. gen_req({modify_dn, Entry, NewRDN, DelOldRDN, NewSup}) ->
  506. {modDNRequest,
  507. #'ModifyDNRequest'{entry = Entry,
  508. newrdn = NewRDN,
  509. deleteoldrdn = DelOldRDN,
  510. newSuperior = NewSup}}.
  511. %%-----------------------------------------------------------------------
  512. %% recvd_packet
  513. %% Deals with incoming packets in the active state
  514. %% Will return one of:
  515. %% {ok, NewS} - Don't reply to client yet as this is part of a search
  516. %% result and we haven't got all the answers yet.
  517. %% {reply, Result, From, NewS} - Reply with result to client From
  518. %% {error, Reason}
  519. %% {'EXIT', Reason} - Broke
  520. %%-----------------------------------------------------------------------
  521. recvd_packet(Pkt, S) ->
  522. check_tag(Pkt),
  523. case asn1rt:decode('ELDAPv3', 'LDAPMessage', Pkt) of
  524. {ok,Msg} ->
  525. Op = Msg#'LDAPMessage'.protocolOp,
  526. log2("~p~n",[Op], S),
  527. Dict = S#eldap.dict,
  528. Id = Msg#'LDAPMessage'.messageID,
  529. {Timer, From, Name, Result_so_far} = get_op_rec(Id, Dict),
  530. case {Name, Op} of
  531. {searchRequest, {searchResEntry, R}} when
  532. record(R,'SearchResultEntry') ->
  533. New_dict = dict:append(Id, R, Dict),
  534. {ok, S#eldap{dict = New_dict}};
  535. {searchRequest, {searchResDone, Result}} ->
  536. case Result#'LDAPResult'.resultCode of
  537. success ->
  538. {Res, Ref} = polish(Result_so_far),
  539. New_dict = dict:erase(Id, Dict),
  540. cancel_timer(Timer),
  541. {reply, #eldap_search_result{entries = Res,
  542. referrals = Ref}, From,
  543. S#eldap{dict = New_dict}};
  544. Reason ->
  545. New_dict = dict:erase(Id, Dict),
  546. cancel_timer(Timer),
  547. {reply, {error, Reason}, From, S#eldap{dict = New_dict}}
  548. end;
  549. {searchRequest, {searchResRef, R}} ->
  550. New_dict = dict:append(Id, R, Dict),
  551. {ok, S#eldap{dict = New_dict}};
  552. {addRequest, {addResponse, Result}} ->
  553. New_dict = dict:erase(Id, Dict),
  554. cancel_timer(Timer),
  555. Reply = check_reply(Result, From),
  556. {reply, Reply, From, S#eldap{dict = New_dict}};
  557. {delRequest, {delResponse, Result}} ->
  558. New_dict = dict:erase(Id, Dict),
  559. cancel_timer(Timer),
  560. Reply = check_reply(Result, From),
  561. {reply, Reply, From, S#eldap{dict = New_dict}};
  562. {modifyRequest, {modifyResponse, Result}} ->
  563. New_dict = dict:erase(Id, Dict),
  564. cancel_timer(Timer),
  565. Reply = check_reply(Result, From),
  566. {reply, Reply, From, S#eldap{dict = New_dict}};
  567. {modDNRequest, {modDNResponse, Result}} ->
  568. New_dict = dict:erase(Id, Dict),
  569. cancel_timer(Timer),
  570. Reply = check_reply(Result, From),
  571. {reply, Reply, From, S#eldap{dict = New_dict}};
  572. {OtherName, OtherResult} ->
  573. New_dict = dict:erase(Id, Dict),
  574. cancel_timer(Timer),
  575. {reply, {error, {invalid_result, OtherName, OtherResult}},
  576. From, S#eldap{dict = New_dict}}
  577. end;
  578. Error -> Error
  579. end.
  580. check_reply(#'LDAPResult'{resultCode = success}, From) ->
  581. ok;
  582. check_reply(#'LDAPResult'{resultCode = Reason}, From) ->
  583. {error, Reason};
  584. check_reply(Other, From) ->
  585. {error, Other}.
  586. get_op_rec(Id, Dict) ->
  587. case dict:find(Id, Dict) of
  588. {ok, [{Timer, From, Name}|Res]} ->
  589. {Timer, From, Name, Res};
  590. error ->
  591. throw({error, unkown_id})
  592. end.
  593. %%-----------------------------------------------------------------------
  594. %% recvd_wait_bind_response packet
  595. %% Deals with incoming packets in the wait_bind_response state
  596. %% Will return one of:
  597. %% bound - Success - move to active state
  598. %% {fail_bind, Reason} - Failed
  599. %% {error, Reason}
  600. %% {'EXIT', Reason} - Broken packet
  601. %%-----------------------------------------------------------------------
  602. recvd_wait_bind_response(Pkt, S) ->
  603. check_tag(Pkt),
  604. case asn1rt:decode('ELDAPv3', 'LDAPMessage', Pkt) of
  605. {ok,Msg} ->
  606. log2("~p", [Msg], S),
  607. check_id(S#eldap.id, Msg#'LDAPMessage'.messageID),
  608. case Msg#'LDAPMessage'.protocolOp of
  609. {bindResponse, Result} ->
  610. case Result#'LDAPResult'.resultCode of
  611. success -> bound;
  612. Error -> {fail_bind, Error}
  613. end
  614. end;
  615. Else ->
  616. {fail_bind, Else}
  617. end.
  618. check_id(Id, Id) -> ok;
  619. check_id(_, _) -> throw({error, wrong_bind_id}).
  620. %%-----------------------------------------------------------------------
  621. %% General Helpers
  622. %%-----------------------------------------------------------------------
  623. cancel_timer(Timer) ->
  624. erlang:cancel_timer(Timer),
  625. receive
  626. {timeout, Timer, _} ->
  627. ok
  628. after 0 ->
  629. ok
  630. end.
  631. %%% Sanity check of received packet
  632. check_tag(Data) ->
  633. case asn1rt_ber:decode_tag(Data) of
  634. {Tag, Data1, Rb} ->
  635. case asn1rt_ber:decode_length(Data1) of
  636. {{Len,Data2}, Rb2} -> ok;
  637. _ -> throw({error,decoded_tag_length})
  638. end;
  639. _ -> throw({error,decoded_tag})
  640. end.
  641. close_and_retry(S) ->
  642. gen_tcp:close(S#eldap.fd),
  643. retry_connect().
  644. retry_connect() ->
  645. erlang:send_after(?RETRY_TIMEOUT, self(),
  646. {timeout, retry_connect}).
  647. %%-----------------------------------------------------------------------
  648. %% Sort out timed out commands
  649. %%-----------------------------------------------------------------------
  650. cmd_timeout(Timer, Id, S) ->
  651. Dict = S#eldap.dict,
  652. case dict:find(Id, Dict) of
  653. {ok, [{Id, Timer, From, Name}|Res]} ->
  654. case Name of
  655. searchRequest ->
  656. {Res1, Ref1} = polish(Res),
  657. New_dict = dict:erase(Id, Dict),
  658. {reply, From, {timeout,
  659. #eldap_search_result{entries = Res1,
  660. referrals = Ref1}},
  661. S#eldap{dict = New_dict}};
  662. Others ->
  663. New_dict = dict:erase(Id, Dict),
  664. {reply, From, {error, timeout}, S#eldap{dict = New_dict}}
  665. end;
  666. error ->
  667. {error, timed_out_cmd_not_in_dict}
  668. end.
  669. %%-----------------------------------------------------------------------
  670. %% Common stuff for results
  671. %%-----------------------------------------------------------------------
  672. %%%
  673. %%% Polish the returned search result
  674. %%%
  675. polish(Entries) ->
  676. polish(Entries, [], []).
  677. polish([H|T], Res, Ref) when record(H, 'SearchResultEntry') ->
  678. ObjectName = H#'SearchResultEntry'.objectName,
  679. F = fun({_,A,V}) -> {A,V} end,
  680. Attrs = lists:map(F, H#'SearchResultEntry'.attributes),
  681. polish(T, [#eldap_entry{object_name = ObjectName,
  682. attributes = Attrs}|Res], Ref);
  683. polish([H|T], Res, Ref) -> % No special treatment of referrals at the moment.
  684. polish(T, Res, [H|Ref]);
  685. polish([], Res, Ref) ->
  686. {Res, Ref}.
  687. %%-----------------------------------------------------------------------
  688. %% Connect to next server in list and attempt to bind to it.
  689. %%-----------------------------------------------------------------------
  690. connect_bind(S) ->
  691. Host = next_host(S#eldap.host, S#eldap.hosts),
  692. TcpOpts = [{packet, asn1}, {active, true}],
  693. case gen_tcp:connect(Host, S#eldap.port, TcpOpts) of
  694. {ok, Socket} ->
  695. case bind_request(Socket, S) of
  696. {ok, NewS} ->
  697. Timer = erlang:start_timer(?BIND_TIMEOUT, self(),
  698. {timeout, bind_timeout}),
  699. {ok, wait_bind_response, NewS#eldap{fd = Socket,
  700. host = Host,
  701. bind_timer = Timer}};
  702. {error, Reason} ->
  703. gen_tcp:close(Socket),
  704. erlang:send_after(?RETRY_TIMEOUT, self(),
  705. {timeout, retry_connect}),
  706. {ok, connecting, S#eldap{host = Host}}
  707. end;
  708. {error, Reason} ->
  709. erlang:send_after(?RETRY_TIMEOUT, self(),
  710. {timeout, retry_connect}),
  711. {ok, connecting, S#eldap{host = Host}}
  712. end.
  713. bind_request(Socket, S) ->
  714. Id = bump_id(S),
  715. Req = #'BindRequest'{version = S#eldap.version,
  716. name = S#eldap.rootdn,
  717. authentication = {simple, S#eldap.passwd}},
  718. Message = #'LDAPMessage'{messageID = Id,
  719. protocolOp = {bindRequest, Req}},
  720. log2("Message:~p~n",[Message], S),
  721. {ok, Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', Message),
  722. ok = gen_tcp:send(Socket, Bytes),
  723. {ok, S#eldap{id = Id}}.
  724. %% Given last tried Server, find next one to try
  725. next_host(null, [H|_]) -> H; % First time, take first
  726. next_host(Host, Hosts) -> % Find next in turn
  727. next_host(Host, Hosts, Hosts).
  728. next_host(Host, [Host], Hosts) -> hd(Hosts); % Wrap back to first
  729. next_host(Host, [Host|Tail], Hosts) -> hd(Tail); % Take next
  730. next_host(Host, [], Hosts) -> hd(Hosts); % Never connected before? (shouldn't happen)
  731. next_host(Host, [H|T], Hosts) -> next_host(Host, T, Hosts).
  732. %%% --------------------------------------------------------------------
  733. %%% Verify the input data
  734. %%% --------------------------------------------------------------------
  735. v_filter({'and',L}) -> {'and',L};
  736. v_filter({'or', L}) -> {'or',L};
  737. v_filter({'not',L}) -> {'not',L};
  738. v_filter({equalityMatch,AV}) -> {equalityMatch,AV};
  739. v_filter({greaterOrEqual,AV}) -> {greaterOrEqual,AV};
  740. v_filter({lessOrEqual,AV}) -> {lessOrEqual,AV};
  741. v_filter({approxMatch,AV}) -> {approxMatch,AV};
  742. v_filter({present,A}) -> {present,A};
  743. v_filter({substrings,S}) when record(S,'SubstringFilter') -> {substrings,S};
  744. v_filter(_Filter) -> throw({error,concat(["unknown filter: ",_Filter])}).
  745. v_modifications(Mods) ->
  746. F = fun({_,Op,_}) ->
  747. case lists:member(Op,[add,delete,replace]) of
  748. true -> true;
  749. _ -> throw({error,{mod_operation,Op}})
  750. end
  751. end,
  752. lists:foreach(F, Mods).
  753. v_substr([{Key,Str}|T]) when list(Str),Key==initial;Key==any;Key==final ->
  754. [{Key,Str}|v_substr(T)];
  755. v_substr([H|T]) ->
  756. throw({error,{substring_arg,H}});
  757. v_substr([]) ->
  758. [].
  759. v_scope(baseObject) -> baseObject;
  760. v_scope(singleLevel) -> singleLevel;
  761. v_scope(wholeSubtree) -> wholeSubtree;
  762. v_scope(_Scope) -> throw({error,concat(["unknown scope: ",_Scope])}).
  763. v_bool(true) -> true;
  764. v_bool(false) -> false;
  765. v_bool(_Bool) -> throw({error,concat(["not Boolean: ",_Bool])}).
  766. v_timeout(I) when integer(I), I>=0 -> I;
  767. v_timeout(_I) -> throw({error,concat(["timeout not positive integer: ",_I])}).
  768. v_attributes(Attrs) ->
  769. F = fun(A) when list(A) -> A;
  770. (A) -> throw({error,concat(["attribute not String: ",A])})
  771. end,
  772. lists:map(F,Attrs).
  773. %%% --------------------------------------------------------------------
  774. %%% Get and Validate the initial configuration
  775. %%% --------------------------------------------------------------------
  776. get_config() ->
  777. Priv_dir = code:priv_dir(eldap),
  778. File = filename:join(Priv_dir, "eldap.conf"),
  779. case file:consult(File) of
  780. {ok, Entries} ->
  781. case catch parse(Entries) of
  782. {ok, Hosts, Port, Rootdn, Passwd, Log} ->
  783. {ok, Hosts, Port, Rootdn, Passwd, Log};
  784. {error, Reason} ->
  785. {error, Reason};
  786. {'EXIT', Reason} ->
  787. {error, Reason}
  788. end;
  789. {error, Reason} ->
  790. {error, Reason}
  791. end.
  792. parse(Entries) ->
  793. {ok,
  794. get_hosts(host, Entries),
  795. get_integer(port, Entries),
  796. get_list(rootdn, Entries),
  797. get_list(passwd, Entries),
  798. get_log(log, Entries)}.
  799. get_integer(Key, List) ->
  800. case lists:keysearch(Key, 1, List) of
  801. {value, {Key, Value}} when integer(Value) ->
  802. Value;
  803. {value, {Key, Value}} ->
  804. throw({error, "Bad Value in Config for " ++ atom_to_list(Key)});
  805. false ->
  806. throw({error, "No Entry in Config for " ++ atom_to_list(Key)})
  807. end.
  808. get_list(Key, List) ->
  809. case lists:keysearch(Key, 1, List) of
  810. {value, {Key, Value}} when list(Value) ->
  811. Value;
  812. {value, {Key, Value}} ->
  813. throw({error, "Bad Value in Config for " ++ atom_to_list(Key)});
  814. false ->
  815. throw({error, "No Entry in Config for " ++ atom_to_list(Key)})
  816. end.
  817. get_log(Key, List) ->
  818. case lists:keysearch(Key, 1, List) of
  819. {value, {Key, Value}} when function(Value) ->
  820. Value;
  821. {value, {Key, Else}} ->
  822. false;
  823. false ->
  824. fun(Level, Format, Args) -> io:format("--- " ++ Format, Args) end
  825. end.
  826. get_hosts(Key, List) ->
  827. lists:map(fun({Key1, {A,B,C,D}}) when integer(A),
  828. integer(B),
  829. integer(C),
  830. integer(D),
  831. Key == Key1->
  832. {A,B,C,D};
  833. ({Key1, Value}) when list(Value),
  834. Key == Key1->
  835. Value;
  836. ({Else, Value}) ->
  837. throw({error, "Bad Hostname in config"})
  838. end, List).
  839. %%% --------------------------------------------------------------------
  840. %%% Other Stuff
  841. %%% --------------------------------------------------------------------
  842. bump_id(#eldap{id = Id}) when Id > ?MAX_TRANSACTION_ID ->
  843. ?MIN_TRANSACTION_ID;
  844. bump_id(#eldap{id = Id}) ->
  845. Id + 1.
  846. %%% --------------------------------------------------------------------
  847. %%% Log routines. Call a user provided log routine Fun.
  848. %%% --------------------------------------------------------------------
  849. log1(Str, Args, #eldap{log = Fun, debug_level = N}) -> log(Fun, Str, Args, 1, N).
  850. log2(Str, Args, #eldap{log = Fun, debug_level = N}) -> log(Fun, Str, Args, 2, N).
  851. log(Fun, Str, Args, This_level, Status) when function(Fun), This_level =< Status ->
  852. catch Fun(This_level, Str, Args);
  853. log(_, _, _, _, _) ->
  854. ok.