2
%%% --------------------------------------------------------------------
3
%%% Created: 12 Oct 2000 by Tobbe <tnt@home.se>
4
%%% Function: Erlang client LDAP implementation according RFC 2251,2253
5
%%% and 2255. The interface is based on RFC 1823, and
6
%%% draft-ietf-asid-ldap-c-api-00.txt
7
%%% --------------------------------------------------------------------
8
-vc('$Id: eldap.erl,v 1.5 2006/11/24 09:38:11 etnt Exp $ ').
9
-export([open/1,open/2,simple_bind/3,controlling_process/2,
10
baseObject/0,singleLevel/0,wholeSubtree/0,close/1,
11
equalityMatch/2,greaterOrEqual/2,lessOrEqual/2,
12
approxMatch/2,search/2,substrings/2,present/1,
13
'and'/1,'or'/1,'not'/1,modify/3, mod_add/2, mod_delete/2,
14
mod_replace/2, add/3, delete/2, modify_dn/5,parse_dn/1,
17
-import(lists,[concat/1]).
19
-include("ELDAPv3.hrl").
20
-include("eldap.hrl").
22
-define(LDAP_VERSION, 3).
23
-define(LDAP_PORT, 389).
24
-define(LDAPS_PORT, 636).
26
-record(eldap, {version = ?LDAP_VERSION,
27
host, % Host running LDAP server
28
port = ?LDAP_PORT, % The LDAP server port
29
fd, % Socket filedescriptor.
30
binddn = "", % Name of the entry to bind as
31
passwd, % Password for (above) entry
32
id = 0, % LDAP Request ID
33
log, % User provided log function
34
timeout = infinity, % Request timeout
35
anon_auth = false, % Allow anonymous authentication
36
use_tls = false % LDAP/LDAPS
39
%%% For debug purposes
40
%%-define(PRINT(S, A), io:fwrite("~w(~w): " ++ S, [?MODULE,?LINE|A])).
41
-define(PRINT(S, A), true).
43
-define(elog(S, A), error_logger:info_msg("~w(~w): "++S,[?MODULE,?LINE|A])).
45
%%% ====================================================================
46
%%% Exported interface
47
%%% ====================================================================
49
%%% --------------------------------------------------------------------
50
%%% open(Hosts [,Opts] )
51
%%% --------------------
52
%%% Setup a connection to on of the Hosts in the argument
53
%%% list. Stop at the first successful connection attempt.
54
%%% Valid Opts are: Where:
56
%%% {port, Port} - Port is the port number
57
%%% {log, F} - F(LogLevel, FormatString, ListOfArgs)
58
%%% {timeout, milliSec} - request timeout
60
%%% --------------------------------------------------------------------
64
open(Hosts, Opts) when list(Hosts), list(Opts) ->
66
Pid = spawn_link(fun() -> init(Hosts, Opts, Self) end),
69
%%% --------------------------------------------------------------------
70
%%% Shutdown connection (and process) asynchronous.
71
%%% --------------------------------------------------------------------
73
close(Handle) when pid(Handle) ->
76
%%% --------------------------------------------------------------------
77
%%% Set who we should link ourselves to
78
%%% --------------------------------------------------------------------
80
controlling_process(Handle, Pid) when pid(Handle),pid(Pid) ->
82
send(Handle, {cnt_proc, Pid}),
85
%%% --------------------------------------------------------------------
86
%%% Authenticate ourselves to the Directory
87
%%% using simple authentication.
89
%%% Dn - The name of the entry to bind as
90
%%% Passwd - The password to be used
92
%%% Returns: ok | {error, Error}
93
%%% --------------------------------------------------------------------
94
simple_bind(Handle, Dn, Passwd) when pid(Handle) ->
95
send(Handle, {simple_bind, Dn, Passwd}),
98
%%% --------------------------------------------------------------------
99
%%% Add an entry. The entry field MUST NOT exist for the AddRequest
100
%%% to succeed. The parent of the entry MUST exist.
104
%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com",
105
%%% [{"objectclass", ["person"]},
106
%%% {"cn", ["Bill Valentine"]},
107
%%% {"sn", ["Valentine"]},
108
%%% {"telephoneNumber", ["545 555 00"]}]
110
%%% --------------------------------------------------------------------
111
add(Handle, Entry, Attributes) when pid(Handle),list(Entry),list(Attributes) ->
112
send(Handle, {add, Entry, add_attrs(Attributes)}),
115
%%% Do sanity check !
117
F = fun({Type,Vals}) when list(Type),list(Vals) ->
118
%% Confused ? Me too... :-/
119
{'AddRequest_attributes',Type, Vals}
121
case catch lists:map(F, Attrs) of
122
{'EXIT', _} -> throw({error, attribute_values});
126
%%% --------------------------------------------------------------------
127
%%% Delete an entry. The entry consists of the DN of
128
%%% the entry to be deleted.
132
%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com"
134
%%% --------------------------------------------------------------------
135
delete(Handle, Entry) when pid(Handle), list(Entry) ->
136
send(Handle, {delete, Entry}),
139
%%% --------------------------------------------------------------------
140
%%% Modify an entry. Given an entry a number of modification
141
%%% operations can be performed as one atomic operation.
145
%%% "cn=Torbjorn Tornkvist, ou=people, o=Bluetail AB, dc=bluetail, dc=com",
146
%%% [replace("telephoneNumber", ["555 555 00"]),
147
%%% add("description", ["LDAP hacker"])]
149
%%% --------------------------------------------------------------------
150
modify(Handle, Object, Mods) when pid(Handle), list(Object), list(Mods) ->
151
send(Handle, {modify, Object, Mods}),
155
%%% Modification operations.
157
%%% replace("telephoneNumber", ["555 555 00"])
159
mod_add(Type, Values) when list(Type), list(Values) -> m(add, Type, Values).
160
mod_delete(Type, Values) when list(Type), list(Values) -> m(delete, Type, Values).
161
mod_replace(Type, Values) when list(Type), list(Values) -> m(replace, Type, Values).
163
m(Operation, Type, Values) ->
164
#'ModifyRequest_modification_SEQOF'{
165
operation = Operation,
166
modification = #'AttributeTypeAndValues'{
170
%%% --------------------------------------------------------------------
171
%%% Modify an entry. Given an entry a number of modification
172
%%% operations can be performed as one atomic operation.
175
%%% modify_dn(Handle,
176
%%% "cn=Bill Valentine, ou=people, o=Bluetail AB, dc=bluetail, dc=com",
177
%%% "cn=Ben Emerson",
181
%%% --------------------------------------------------------------------
182
modify_dn(Handle, Entry, NewRDN, DelOldRDN, NewSup)
183
when pid(Handle),list(Entry),list(NewRDN),atom(DelOldRDN),list(NewSup) ->
184
send(Handle, {modify_dn, Entry, NewRDN,
185
bool_p(DelOldRDN), optional(NewSup)}),
190
bool_p(Bool) when Bool==true;Bool==false -> Bool.
192
optional([]) -> asn1_NOVALUE;
193
optional(Value) -> Value.
195
%%% --------------------------------------------------------------------
196
%%% Synchronous search of the Directory returning a
197
%%% requested set of attributes.
201
%%% Filter = eldap:substrings("sn", [{any,"o"}]),
202
%%% eldap:search(S, [{base, "dc=bluetail, dc=com"},
203
%%% {filter, Filter},
204
%%% {attributes,["cn"]}])),
206
%%% Returned result: {ok, #eldap_search_result{}}
210
%%% {ok,{eldap_search_result,
212
%%% "cn=Magnus Froberg, dc=bluetail, dc=com",
213
%%% [{"cn",["Magnus Froberg"]}]},
215
%%% "cn=Torbjorn Tornkvist, dc=bluetail, dc=com",
216
%%% [{"cn",["Torbjorn Tornkvist"]}]}],
219
%%% --------------------------------------------------------------------
220
search(Handle, A) when pid(Handle), record(A, eldap_search) ->
221
call_search(Handle, A);
222
search(Handle, L) when pid(Handle), list(L) ->
223
case catch parse_search_args(L) of
224
{error, Emsg} -> {error, Emsg};
225
A when record(A, eldap_search) -> call_search(Handle, A)
228
call_search(Handle, A) ->
229
send(Handle, {search, A}),
232
parse_search_args(Args) ->
233
parse_search_args(Args, #eldap_search{scope = wholeSubtree}).
235
parse_search_args([{base, Base}|T],A) ->
236
parse_search_args(T,A#eldap_search{base = Base});
237
parse_search_args([{filter, Filter}|T],A) ->
238
parse_search_args(T,A#eldap_search{filter = Filter});
239
parse_search_args([{scope, Scope}|T],A) ->
240
parse_search_args(T,A#eldap_search{scope = Scope});
241
parse_search_args([{attributes, Attrs}|T],A) ->
242
parse_search_args(T,A#eldap_search{attributes = Attrs});
243
parse_search_args([{types_only, TypesOnly}|T],A) ->
244
parse_search_args(T,A#eldap_search{types_only = TypesOnly});
245
parse_search_args([{timeout, Timeout}|T],A) when integer(Timeout) ->
246
parse_search_args(T,A#eldap_search{timeout = Timeout});
247
parse_search_args([H|_],_) ->
248
throw({error,{unknown_arg, H}});
249
parse_search_args([],A) ->
253
%%% The Scope parameter
255
baseObject() -> baseObject.
256
singleLevel() -> singleLevel.
257
wholeSubtree() -> wholeSubtree.
260
%%% Boolean filter operations
262
'and'(ListOfFilters) when list(ListOfFilters) -> {'and',ListOfFilters}.
263
'or'(ListOfFilters) when list(ListOfFilters) -> {'or', ListOfFilters}.
264
'not'(Filter) when tuple(Filter) -> {'not',Filter}.
267
%%% The following Filter parameters consist of an attribute
268
%%% and an attribute value. Example: F("uid","tobbe")
270
equalityMatch(Desc, Value) -> {equalityMatch, av_assert(Desc, Value)}.
271
greaterOrEqual(Desc, Value) -> {greaterOrEqual, av_assert(Desc, Value)}.
272
lessOrEqual(Desc, Value) -> {lessOrEqual, av_assert(Desc, Value)}.
273
approxMatch(Desc, Value) -> {approxMatch, av_assert(Desc, Value)}.
275
av_assert(Desc, Value) ->
276
#'AttributeValueAssertion'{attributeDesc = Desc,
277
assertionValue = Value}.
280
%%% Filter to check for the presence of an attribute
282
present(Attribute) when list(Attribute) ->
283
{present, Attribute}.
287
%%% A substring filter seem to be based on a pattern:
289
%%% InitValue*AnyValue*FinalValue
291
%%% where all three parts seem to be optional (at least when
292
%%% talking with an OpenLDAP server). Thus, the arguments
293
%%% to substrings/2 looks like this:
295
%%% Type ::= string( <attribute> )
296
%%% SubStr ::= listof( {initial,Value} | {any,Value}, {final,Value})
298
%%% Example: substrings("sn",[{initial,"To"},{any,"kv"},{final,"st"}])
299
%%% will match entries containing: 'sn: Tornkvist'
301
substrings(Type, SubStr) when list(Type), list(SubStr) ->
302
Ss = {'SubstringFilter_substrings',v_substr(SubStr)},
303
{substrings,#'SubstringFilter'{type = Type,
306
%%% --------------------------------------------------------------------
307
%%% Worker process. We keep track of a controlling process to
308
%%% be able to terminate together with it.
309
%%% --------------------------------------------------------------------
311
init(Hosts, Opts, Cpid) ->
312
Data = parse_args(Opts, Cpid, #eldap{}),
313
case try_connect(Hosts, Data) of
315
send(Cpid, {ok,self()}),
316
put(req_timeout, Data#eldap.timeout), % kludge...
324
parse_args([{port, Port}|T], Cpid, Data) when integer(Port) ->
325
parse_args(T, Cpid, Data#eldap{port = Port});
326
parse_args([{timeout, Timeout}|T], Cpid, Data) when integer(Timeout),Timeout>0 ->
327
parse_args(T, Cpid, Data#eldap{timeout = Timeout});
328
parse_args([{anon_auth, true}|T], Cpid, Data) ->
329
parse_args(T, Cpid, Data#eldap{anon_auth = false});
330
parse_args([{anon_auth, _}|T], Cpid, Data) ->
331
parse_args(T, Cpid, Data);
332
parse_args([{ssl, true}|T], Cpid, Data) ->
333
parse_args(T, Cpid, Data#eldap{use_tls = true});
334
parse_args([{ssl, _}|T], Cpid, Data) ->
335
parse_args(T, Cpid, Data);
336
parse_args([{log, F}|T], Cpid, Data) when function(F) ->
337
parse_args(T, Cpid, Data#eldap{log = F});
338
parse_args([{log, _}|T], Cpid, Data) ->
339
parse_args(T, Cpid, Data);
340
parse_args([H|_], Cpid, _) ->
341
send(Cpid, {error,{wrong_option,H}}),
343
parse_args([], _, Data) ->
346
%%% Try to connect to the hosts in the listed order,
347
%%% and stop with the first one to which a successful
348
%%% connection is made.
350
try_connect([Host|Hosts], Data) ->
351
TcpOpts = [{packet, asn1}, {active,false}],
352
case do_connect(Host, Data, TcpOpts) of
353
{ok,Fd} -> {ok,Data#eldap{host = Host, fd = Fd}};
354
_ -> try_connect(Hosts, Data)
357
{error,"connect failed"}.
359
do_connect(Host, Data, Opts) when Data#eldap.use_tls == false ->
360
gen_tcp:connect(Host, Data#eldap.port, Opts, Data#eldap.timeout);
361
do_connect(Host, Data, Opts) when Data#eldap.use_tls == true ->
362
Vsn = erlang:system_info(version),
364
%% In R9C, but not in R9B
365
{_,_,X} = erlang:now(),
366
ssl:seed("bkrlnateqqo" ++ integer_to_list(X));
369
ssl:connect(Host, Data#eldap.port, [{verify,0}|Opts]).
375
{From, {search, A}} ->
376
{Res,NewData} = do_search(Data, A),
380
{From, {modify, Obj, Mod}} ->
381
{Res,NewData} = do_modify(Data, Obj, Mod),
385
{From, {modify_dn, Obj, NewRDN, DelOldRDN, NewSup}} ->
386
{Res,NewData} = do_modify_dn(Data, Obj, NewRDN, DelOldRDN, NewSup),
390
{From, {add, Entry, Attrs}} ->
391
{Res,NewData} = do_add(Data, Entry, Attrs),
395
{From, {delete, Entry}} ->
396
{Res,NewData} = do_delete(Data, Entry),
400
{From, {simple_bind, Dn, Passwd}} ->
401
{Res,NewData} = do_simple_bind(Data, Dn, Passwd),
405
{From, {cnt_proc, NewCpid}} ->
408
?PRINT("New Cpid is: ~p~n",[NewCpid]),
415
{Cpid, 'EXIT', Reason} ->
416
?PRINT("Got EXIT from Cpid, reason=~p~n",[Reason]),
420
?PRINT("loop got: ~p~n",[_XX]),
425
%%% --------------------------------------------------------------------
427
%%% --------------------------------------------------------------------
429
%%% Authenticate ourselves to the directory using
430
%%% simple authentication.
432
do_simple_bind(Data, anon, anon) -> %% For testing
433
do_the_simple_bind(Data, "", "");
434
do_simple_bind(Data, Dn, _Passwd) when Dn=="",Data#eldap.anon_auth==false ->
435
{{error,anonymous_auth},Data};
436
do_simple_bind(Data, _Dn, Passwd) when Passwd=="",Data#eldap.anon_auth==false ->
437
{{error,anonymous_auth},Data};
438
do_simple_bind(Data, Dn, Passwd) ->
439
do_the_simple_bind(Data, Dn, Passwd).
441
do_the_simple_bind(Data, Dn, Passwd) ->
442
case catch exec_simple_bind(Data#eldap{binddn = Dn,
444
id = bump_id(Data)}) of
445
{ok,NewData} -> {ok,NewData};
446
{error,Emsg} -> {{error,Emsg},Data};
447
Else -> {{error,Else},Data}
450
exec_simple_bind(Data) ->
451
Req = #'BindRequest'{version = Data#eldap.version,
452
name = Data#eldap.binddn,
453
authentication = {simple, Data#eldap.passwd}},
454
log2(Data, "bind request = ~p~n", [Req]),
455
Reply = request(Data#eldap.fd, Data, Data#eldap.id, {bindRequest, Req}),
456
log2(Data, "bind reply = ~p~n", [Reply]),
457
exec_simple_bind_reply(Data, Reply).
459
exec_simple_bind_reply(Data, {ok,Msg}) when
460
Msg#'LDAPMessage'.messageID == Data#eldap.id ->
461
case Msg#'LDAPMessage'.protocolOp of
462
{bindResponse, Result} ->
463
case Result#'BindResponse'.resultCode of
464
success -> {ok,Data};
465
Error -> {error, Error}
467
Other -> {error, Other}
469
exec_simple_bind_reply(_, Error) ->
473
%%% --------------------------------------------------------------------
475
%%% --------------------------------------------------------------------
477
do_search(Data, A) ->
478
case catch do_search_0(Data, A) of
479
{error,Emsg} -> {ldap_closed_p(Data, Emsg),Data};
480
{'EXIT',Error} -> {ldap_closed_p(Data, Error),Data};
481
{ok,Res,Ref,NewData} -> {{ok,polish(Res, Ref)},NewData};
482
Else -> {ldap_closed_p(Data, Else),Data}
486
%%% Polish the returned search result
490
R = polish_result(Res),
491
%%% No special treatment of referrals at the moment.
492
#eldap_search_result{entries = R,
495
polish_result([H|T]) when record(H, 'SearchResultEntry') ->
496
ObjectName = H#'SearchResultEntry'.objectName,
497
F = fun({_,A,V}) -> {A,V} end,
498
Attrs = lists:map(F, H#'SearchResultEntry'.attributes),
499
[#eldap_entry{object_name = ObjectName,
505
do_search_0(Data, A) ->
506
Req = #'SearchRequest'{baseObject = A#eldap_search.base,
507
scope = v_scope(A#eldap_search.scope),
508
derefAliases = neverDerefAliases,
509
sizeLimit = 0, % no size limit
510
timeLimit = v_timeout(A#eldap_search.timeout),
511
typesOnly = v_bool(A#eldap_search.types_only),
512
filter = v_filter(A#eldap_search.filter),
513
attributes = v_attributes(A#eldap_search.attributes)
516
collect_search_responses(Data#eldap{id=Id}, Req, Id).
518
%%% The returned answers cames in one packet per entry
519
%%% mixed with possible referals
521
collect_search_responses(Data, Req, ID) ->
523
log2(Data, "search request = ~p~n", [Req]),
524
send_request(S, Data, ID, {searchRequest, Req}),
525
Resp = recv_response(S, Data),
526
log2(Data, "search reply = ~p~n", [Resp]),
527
collect_search_responses(Data, S, ID, Resp, [], []).
529
collect_search_responses(Data, S, ID, {ok,Msg}, Acc, Ref)
530
when record(Msg,'LDAPMessage') ->
531
case Msg#'LDAPMessage'.protocolOp of
532
{'searchResDone',R} when R#'LDAPResult'.resultCode == success ->
533
log2(Data, "search reply = searchResDone ~n", []),
535
{'searchResEntry',R} when record(R,'SearchResultEntry') ->
536
Resp = recv_response(S, Data),
537
log2(Data, "search reply = ~p~n", [Resp]),
538
collect_search_responses(Data, S, ID, Resp, [R|Acc], Ref);
539
{'searchResRef',R} ->
540
%% At the moment we don't do anyting sensible here since
541
%% I haven't been able to trigger the server to generate
542
%% a response like this.
543
Resp = recv_response(S, Data),
544
log2(Data, "search reply = ~p~n", [Resp]),
545
collect_search_responses(Data, S, ID, Resp, Acc, [R|Ref]);
549
collect_search_responses(_, _, _, Else, _, _) ->
552
%%% --------------------------------------------------------------------
554
%%% --------------------------------------------------------------------
556
do_add(Data, Entry, Attrs) ->
557
case catch do_add_0(Data, Entry, Attrs) of
558
{error,Emsg} -> {ldap_closed_p(Data, Emsg),Data};
559
{'EXIT',Error} -> {ldap_closed_p(Data, Error),Data};
560
{ok,NewData} -> {ok,NewData};
561
Else -> {ldap_closed_p(Data, Else),Data}
564
do_add_0(Data, Entry, Attrs) ->
565
Req = #'AddRequest'{entry = Entry,
569
log2(Data, "add request = ~p~n", [Req]),
570
Resp = request(S, Data, Id, {addRequest, Req}),
571
log2(Data, "add reply = ~p~n", [Resp]),
572
check_reply(Data#eldap{id = Id}, Resp, addResponse).
575
%%% --------------------------------------------------------------------
577
%%% --------------------------------------------------------------------
579
do_delete(Data, Entry) ->
580
case catch do_delete_0(Data, Entry) of
581
{error,Emsg} -> {ldap_closed_p(Data, Emsg),Data};
582
{'EXIT',Error} -> {ldap_closed_p(Data, Error),Data};
583
{ok,NewData} -> {ok,NewData};
584
Else -> {ldap_closed_p(Data, Else),Data}
587
do_delete_0(Data, Entry) ->
590
log2(Data, "del request = ~p~n", [Entry]),
591
Resp = request(S, Data, Id, {delRequest, Entry}),
592
log2(Data, "del reply = ~p~n", [Resp]),
593
check_reply(Data#eldap{id = Id}, Resp, delResponse).
596
%%% --------------------------------------------------------------------
598
%%% --------------------------------------------------------------------
600
do_modify(Data, Obj, Mod) ->
601
case catch do_modify_0(Data, Obj, Mod) of
602
{error,Emsg} -> {ldap_closed_p(Data, Emsg),Data};
603
{'EXIT',Error} -> {ldap_closed_p(Data, Error),Data};
604
{ok,NewData} -> {ok,NewData};
605
Else -> {ldap_closed_p(Data, Else),Data}
608
do_modify_0(Data, Obj, Mod) ->
609
v_modifications(Mod),
610
Req = #'ModifyRequest'{object = Obj,
614
log2(Data, "modify request = ~p~n", [Req]),
615
Resp = request(S, Data, Id, {modifyRequest, Req}),
616
log2(Data, "modify reply = ~p~n", [Resp]),
617
check_reply(Data#eldap{id = Id}, Resp, modifyResponse).
619
%%% --------------------------------------------------------------------
621
%%% --------------------------------------------------------------------
623
do_modify_dn(Data, Entry, NewRDN, DelOldRDN, NewSup) ->
624
case catch do_modify_dn_0(Data, Entry, NewRDN, DelOldRDN, NewSup) of
625
{error,Emsg} -> {ldap_closed_p(Data, Emsg),Data};
626
{'EXIT',Error} -> {ldap_closed_p(Data, Error),Data};
627
{ok,NewData} -> {ok,NewData};
628
Else -> {ldap_closed_p(Data, Else),Data}
631
do_modify_dn_0(Data, Entry, NewRDN, DelOldRDN, NewSup) ->
632
Req = #'ModifyDNRequest'{entry = Entry,
634
deleteoldrdn = DelOldRDN,
635
newSuperior = NewSup},
638
log2(Data, "modify DN request = ~p~n", [Req]),
639
Resp = request(S, Data, Id, {modDNRequest, Req}),
640
log2(Data, "modify DN reply = ~p~n", [Resp]),
641
check_reply(Data#eldap{id = Id}, Resp, modDNResponse).
643
%%% --------------------------------------------------------------------
644
%%% Send an LDAP request and receive the answer
645
%%% --------------------------------------------------------------------
647
request(S, Data, ID, Request) ->
648
send_request(S, Data, ID, Request),
649
recv_response(S, Data).
651
send_request(S, Data, ID, Request) ->
652
Message = #'LDAPMessage'{messageID = ID,
653
protocolOp = Request},
654
{ok,Bytes} = asn1rt:encode('ELDAPv3', 'LDAPMessage', Message),
655
case do_send(S, Data, Bytes) of
656
{error,Reason} -> throw({gen_tcp_error,Reason});
660
do_send(S, Data, Bytes) when Data#eldap.use_tls == false ->
661
gen_tcp:send(S, Bytes);
662
do_send(S, Data, Bytes) when Data#eldap.use_tls == true ->
665
do_recv(S, Data, Len, Timeout) when Data#eldap.use_tls == false ->
666
gen_tcp:recv(S, Len, Timeout);
667
do_recv(S, Data, Len, Timeout) when Data#eldap.use_tls == true ->
668
ssl:recv(S, Len, Timeout).
670
recv_response(S, Data) ->
671
Timeout = get(req_timeout), % kludge...
672
case do_recv(S, Data, 0, Timeout) of
675
case asn1rt:decode('ELDAPv3', 'LDAPMessage', Packet) of
676
{ok,Resp} -> {ok,Resp};
677
Error -> throw(Error)
680
throw({gen_tcp_error, Reason});
685
%%% Sanity check of received packet
687
case asn1rt_ber_bin:decode_tag(b2l(Data)) of
688
{_Tag, Data1, _Rb} ->
689
case asn1rt_ber_bin:decode_length(b2l(Data1)) of
690
{{_Len, _Data2}, _Rb2} -> ok;
691
_ -> throw({error,decoded_tag_length})
693
_ -> throw({error,decoded_tag})
696
%%% Check for expected kind of reply
697
check_reply(Data, {ok,Msg}, Op) when
698
Msg#'LDAPMessage'.messageID == Data#eldap.id ->
699
case Msg#'LDAPMessage'.protocolOp of
701
case Result#'LDAPResult'.resultCode of
702
success -> {ok,Data};
703
Error -> {error, Error}
705
Other -> {error, Other}
707
check_reply(_, Error, _) ->
711
%%% --------------------------------------------------------------------
712
%%% Verify the input data
713
%%% --------------------------------------------------------------------
715
v_filter({'and',L}) -> {'and',L};
716
v_filter({'or', L}) -> {'or',L};
717
v_filter({'not',L}) -> {'not',L};
718
v_filter({equalityMatch,AV}) -> {equalityMatch,AV};
719
v_filter({greaterOrEqual,AV}) -> {greaterOrEqual,AV};
720
v_filter({lessOrEqual,AV}) -> {lessOrEqual,AV};
721
v_filter({approxMatch,AV}) -> {approxMatch,AV};
722
v_filter({present,A}) -> {present,A};
723
v_filter({substrings,S}) when record(S,'SubstringFilter') -> {substrings,S};
724
v_filter(_Filter) -> throw({error,concat(["unknown filter: ",_Filter])}).
726
v_modifications(Mods) ->
728
case lists:member(Op,[add,delete,replace]) of
730
_ -> throw({error,{mod_operation,Op}})
733
lists:foreach(F, Mods).
735
v_substr([{Key,Str}|T]) when list(Str),Key==initial;Key==any;Key==final ->
736
[{Key,Str}|v_substr(T)];
738
throw({error,{substring_arg,H}});
741
v_scope(baseObject) -> baseObject;
742
v_scope(singleLevel) -> singleLevel;
743
v_scope(wholeSubtree) -> wholeSubtree;
744
v_scope(_Scope) -> throw({error,concat(["unknown scope: ",_Scope])}).
746
v_bool(true) -> true;
747
v_bool(false) -> false;
748
v_bool(_Bool) -> throw({error,concat(["not Boolean: ",_Bool])}).
750
v_timeout(I) when integer(I), I>=0 -> I;
751
v_timeout(_I) -> throw({error,concat(["timeout not positive integer: ",_I])}).
753
v_attributes(Attrs) ->
754
F = fun(A) when list(A) -> A;
755
(A) -> throw({error,concat(["attribute not String: ",A])})
760
%%% --------------------------------------------------------------------
761
%%% Log routines. Call a user provided log routine F.
762
%%% --------------------------------------------------------------------
764
log1(Data, Str, Args) -> log(Data, Str, Args, 1).
765
log2(Data, Str, Args) -> log(Data, Str, Args, 2).
767
log(Data, Str, Args, Level) when function(Data#eldap.log) ->
768
catch (Data#eldap.log)(Level, Str, Args);
773
%%% --------------------------------------------------------------------
775
%%% --------------------------------------------------------------------
777
send(To,Msg) -> To ! {self(),Msg}.
778
recv(From) -> receive {From,Msg} -> Msg end.
780
ldap_closed_p(Data, Emsg) when Data#eldap.use_tls == true ->
781
%% Check if the SSL socket seems to be alive or not
782
case catch ssl:sockname(Data#eldap.fd) of
784
ssl:close(Data#eldap.fd),
785
{error, ldap_closed};
789
%% sockname crashes if the socket pid is not alive
792
ldap_closed_p(Data, Emsg) ->
794
case inet:port(Data#eldap.fd) of
795
{error,_} -> {error, ldap_closed};
799
bump_id(Data) -> Data#eldap.id + 1.
802
%%% --------------------------------------------------------------------
803
%%% parse_dn/1 - Implementation of RFC 2253:
805
%%% "UTF-8 String Representation of Distinguished Names"
809
%%% The simplest case:
811
%%% 1> eldap:parse_dn("CN=Steve Kille,O=Isode Limited,C=GB").
812
%%% {ok,[[{attribute_type_and_value,"CN","Steve Kille"}],
813
%%% [{attribute_type_and_value,"O","Isode Limited"}],
814
%%% [{attribute_type_and_value,"C","GB"}]]}
816
%%% The first RDN is multi-valued:
818
%%% 2> eldap:parse_dn("OU=Sales+CN=J. Smith,O=Widget Inc.,C=US").
819
%%% {ok,[[{attribute_type_and_value,"OU","Sales"},
820
%%% {attribute_type_and_value,"CN","J. Smith"}],
821
%%% [{attribute_type_and_value,"O","Widget Inc."}],
822
%%% [{attribute_type_and_value,"C","US"}]]}
826
%%% 3> eldap:parse_dn("CN=L. Eagle,O=Sue\\, Grabbit and Runn,C=GB").
827
%%% {ok,[[{attribute_type_and_value,"CN","L. Eagle"}],
828
%%% [{attribute_type_and_value,"O","Sue\\, Grabbit and Runn"}],
829
%%% [{attribute_type_and_value,"C","GB"}]]}
831
%%% A value contains a carriage return:
833
%%% 4> eldap:parse_dn("CN=Before
834
%%% 4> After,O=Test,C=GB").
835
%%% {ok,[[{attribute_type_and_value,"CN","Before\nAfter"}],
836
%%% [{attribute_type_and_value,"O","Test"}],
837
%%% [{attribute_type_and_value,"C","GB"}]]}
839
%%% 5> eldap:parse_dn("CN=Before\\0DAfter,O=Test,C=GB").
840
%%% {ok,[[{attribute_type_and_value,"CN","Before\\0DAfter"}],
841
%%% [{attribute_type_and_value,"O","Test"}],
842
%%% [{attribute_type_and_value,"C","GB"}]]}
844
%%% An RDN in OID form:
846
%%% 6> eldap:parse_dn("1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB").
847
%%% {ok,[[{attribute_type_and_value,"1.3.6.1.4.1.1466.0","#04024869"}],
848
%%% [{attribute_type_and_value,"O","Test"}],
849
%%% [{attribute_type_and_value,"C","GB"}]]}
852
%%% --------------------------------------------------------------------
854
parse_dn("") -> % empty DN string
856
parse_dn([H|_] = Str) when H=/=$, -> % 1:st name-component !
857
case catch parse_name(Str,[]) of
858
{'EXIT',Reason} -> {parse_error,internal_error,Reason};
862
parse_name("",Acc) ->
863
{ok,lists:reverse(Acc)};
864
parse_name([$,|T],Acc) -> % N:th name-component !
866
parse_name(Str,Acc) ->
867
{Rest,NameComponent} = parse_name_component(Str),
868
parse_name(Rest,[NameComponent|Acc]).
870
parse_name_component(Str) ->
871
parse_name_component(Str,[]).
873
parse_name_component(Str,Acc) ->
874
case parse_attribute_type_and_value(Str) of
876
parse_name_component(Rest,[ATV|Acc]);
878
{Rest,lists:reverse([ATV|Acc])}
881
parse_attribute_type_and_value(Str) ->
882
case parse_attribute_type(Str) of
884
error(expecting_attribute_type,Str);
886
Rest2 = parse_equal_sign(Rest),
887
{Rest3,Value} = parse_attribute_value(Rest2),
888
{Rest3,{attribute_type_and_value,Type,Value}}
891
-define(IS_ALPHA(X) , X>=$a,X=<$z;X>=$A,X=<$Z ).
892
-define(IS_DIGIT(X) , X>=$0,X=<$9 ).
893
-define(IS_SPECIAL(X) , X==$,;X==$=;X==$+;X==$<;X==$>;X==$#;X==$; ).
894
-define(IS_QUOTECHAR(X) , X=/=$\\,X=/=$" ).
895
-define(IS_STRINGCHAR(X) ,
896
X=/=$,,X=/=$=,X=/=$+,X=/=$<,X=/=$>,X=/=$#,X=/=$;,?IS_QUOTECHAR(X) ).
897
-define(IS_HEXCHAR(X) , ?IS_DIGIT(X);X>=$a,X=<$f;X>=$A,X=<$F ).
899
parse_attribute_type([H|T]) when ?IS_ALPHA(H) ->
900
%% NB: It must be an error in the RFC in the definition
901
%% of 'attributeType', should be: (ALPHA *keychar)
902
{Rest,KeyChars} = parse_keychars(T),
904
parse_attribute_type([H|_] = Str) when ?IS_DIGIT(H) ->
906
parse_attribute_type(Str) ->
907
error(invalid_attribute_type,Str).
912
parse_attribute_value([$#,X,Y|T]) when ?IS_HEXCHAR(X),?IS_HEXCHAR(Y) ->
913
{Rest,HexString} = parse_hexstring(T),
914
{Rest,[$#,X,Y|HexString]};
915
%%% Is a "quotation-sequence" !
916
parse_attribute_value([$"|T]) ->
917
{Rest,Quotation} = parse_quotation(T),
918
{Rest,[$"|Quotation]};
919
%%% Is a stringchar , pair or Empty !
920
parse_attribute_value(Str) ->
923
parse_hexstring(Str) ->
924
parse_hexstring(Str,[]).
926
parse_hexstring([X,Y|T],Acc) when ?IS_HEXCHAR(X),?IS_HEXCHAR(Y) ->
927
parse_hexstring(T,[Y,X|Acc]);
928
parse_hexstring(T,Acc) ->
929
{T,lists:reverse(Acc)}.
931
parse_quotation([$"|T]) -> % an empty: "" is ok !
933
parse_quotation(Str) ->
934
parse_quotation(Str,[]).
936
%%% Parse to end of quotation
937
parse_quotation([$"|T],Acc) ->
938
{T,lists:reverse([$"|Acc])};
939
parse_quotation([X|T],Acc) when ?IS_QUOTECHAR(X) ->
940
parse_quotation(T,[X|Acc]);
941
parse_quotation([$\\,X|T],Acc) when ?IS_SPECIAL(X) ->
942
parse_quotation(T,[X,$\\|Acc]);
943
parse_quotation([$\\,$\\|T],Acc) ->
944
parse_quotation(T,[$\\,$\\|Acc]);
945
parse_quotation([$\\,$"|T],Acc) ->
946
parse_quotation(T,[$",$\\|Acc]);
947
parse_quotation([$\\,X,Y|T],Acc) when ?IS_HEXCHAR(X),?IS_HEXCHAR(Y) ->
948
parse_quotation(T,[Y,X,$\\|Acc]);
949
parse_quotation(T,_) ->
950
error(expecting_double_quote_mark,T).
953
parse_string(Str,[]).
955
parse_string("",Acc) ->
956
{"",lists:reverse(Acc)};
957
parse_string([H|T],Acc) when ?IS_STRINGCHAR(H) ->
958
parse_string(T,[H|Acc]);
959
parse_string([$\\,X|T],Acc) when ?IS_SPECIAL(X) -> % is a pair !
960
parse_string(T,[X,$\\|Acc]);
961
parse_string([$\\,$\\|T],Acc) -> % is a pair !
962
parse_string(T,[$\\,$\\|Acc]);
963
parse_string([$\\,$" |T],Acc) -> % is a pair !
964
parse_string(T,[$" ,$\\|Acc]);
965
parse_string([$\\,X,Y|T],Acc) when ?IS_HEXCHAR(X),?IS_HEXCHAR(Y) -> % is a pair!
966
parse_string(T,[Y,X,$\\|Acc]);
967
parse_string(T,Acc) ->
968
{T,lists:reverse(Acc)}.
970
parse_equal_sign([$=|T]) -> T;
971
parse_equal_sign(T) -> error(expecting_equal_sign,T).
973
parse_keychars(Str) -> parse_keychars(Str,[]).
975
parse_keychars([H|T],Acc) when ?IS_ALPHA(H) -> parse_keychars(T,[H|Acc]);
976
parse_keychars([H|T],Acc) when ?IS_DIGIT(H) -> parse_keychars(T,[H|Acc]);
977
parse_keychars([$-|T],Acc) -> parse_keychars(T,[$-|Acc]);
978
parse_keychars(T,Acc) -> {T,lists:reverse(Acc)}.
980
parse_oid(Str) -> parse_oid(Str,[]).
982
parse_oid([H,$.|T], Acc) when ?IS_DIGIT(H) ->
983
parse_oid(T,[$.,H|Acc]);
984
parse_oid([H|T], Acc) when ?IS_DIGIT(H) ->
985
parse_oid(T,[H|Acc]);
987
{T,lists:reverse(Acc)}.
990
throw({parse_error,Emsg,Rest}).
993
%%% --------------------------------------------------------------------
994
%%% Parse LDAP url according to RFC 2255
998
%%% 2> eldap:parse_ldap_url("ldap://10.42.126.33:389/cn=Administrative%20CA,o=Post%20Danmark,c=DK?certificateRevokationList;binary").
999
%%% {ok,{{10,42,126,33},389},
1000
%%% [[{attribute_type_and_value,"cn","Administrative%20CA"}],
1001
%%% [{attribute_type_and_value,"o","Post%20Danmark"}],
1002
%%% [{attribute_type_and_value,"c","DK"}]],
1003
%%% {attributes,["certificateRevokationList;binary"]}}
1005
%%% --------------------------------------------------------------------
1007
parse_ldap_url("ldap://" ++ Rest1 = Str) ->
1008
{Rest2,HostPort} = parse_hostport(Rest1),
1009
%% Split the string into DN and Attributes+etc
1010
{Sdn,Rest3} = split_string(rm_leading_slash(Rest2),$?),
1011
case parse_dn(Sdn) of
1012
{parse_error,internal_error,_Reason} ->
1013
{parse_error,internal_error,{Str,[]}};
1014
{parse_error,Emsg,Tail} ->
1015
Head = get_head(Str,Tail),
1016
{parse_error,Emsg,{Head,Tail}};
1018
%% We stop parsing here for now and leave
1019
%% 'scope', 'filter' and 'extensions' to
1020
%% be implemented later if needed.
1021
{_Rest4,Attributes} = parse_attributes(Rest3),
1022
{ok,HostPort,DN,Attributes}
1025
rm_leading_slash([$/|Tail]) -> Tail;
1026
rm_leading_slash(Tail) -> Tail.
1028
parse_attributes([$?|Tail]) ->
1029
case split_string(Tail,$?) of
1031
{[],{attributes,string:tokens(Attributes,",")}};
1032
{Attributes,Rest} ->
1033
{Rest,{attributes,string:tokens(Attributes,",")}}
1036
parse_hostport(Str) ->
1037
{HostPort,Rest} = split_string(Str,$/),
1038
case split_string(HostPort,$:) of
1040
{Rest,{parse_host(Rest,Shost),?LDAP_PORT}};
1041
{Shost,[$:|Sport]} ->
1042
{Rest,{parse_host(Rest,Shost),
1043
parse_port(Rest,Sport)}}
1046
parse_port(Rest,Sport) ->
1047
case list_to_integer(Sport) of
1048
Port when integer(Port) -> Port;
1049
_ -> error(parsing_port,Rest)
1052
parse_host(Rest,Shost) ->
1053
case catch validate_host(Shost) of
1054
{parse_error,Emsg,_} -> error(Emsg,Rest);
1058
validate_host(Shost) ->
1059
case inet_parse:address(Shost) of
1062
case inet_parse:domain(Shost) of
1064
_ -> error(parsing_host,Shost)
1069
split_string(Str,Key) ->
1070
Pred = fun(X) when X==Key -> false; (_) -> true end,
1071
lists:splitwith(Pred, Str).
1073
get_head(Str,Tail) ->
1074
get_head(Str,Tail,[]).
1076
%%% Should always succeed !
1077
get_head([H|Tail],Tail,Rhead) -> lists:reverse([H|Rhead]);
1078
get_head([H|Rest],Tail,Rhead) -> get_head(Rest,Tail,[H|Rhead]).
1080
b2l(B) when binary(B) -> B;
1081
b2l(L) when list(L) -> list_to_binary(L).