LDAP сервер на Erlang за 30 минут

Вот может кому интересно почему я начал писать SyncML сервер на эрланг. Если кратко, мне понравилось. Если длиннее: я определяю насколько просто писать на компьютерном языке по тому насколько просто прострелить себе ногу написать LDAP сервер. Задача такая: написать приложение которое отвечает Windows Mail/Apple Mail, etc. и возвращает по LDAP протоколу спискок адресатов для автоматической подставновки (inplace search) в адресных полях письма. Т.е. что бы из такого:

получить вот такое:

Прежде все я зашел на http://www.rfc-editor.org/rfc/rfc4511.txt и начал скачивать определение LDAP протокола в ASN.1 нотации. Простая копи паста примерно такого кода:

LDAP DEFINITIONS IMPLICIT TAGS ::= BEGIN LDAPMessage ::= SEQUENCE { messageID MessageID, protocolOp CHOICE { bindRequest BindRequest, bindResponse BindResponse, unbindRequest UnbindRequest, searchRequest SearchRequest, searchResEntry SearchResultEntry, searchResDone SearchResultDone, searchResRef SearchResultReference, modifyRequest ModifyRequest, modifyResponse ModifyResponse, addRequest AddRequest, addResponse AddResponse, delRequest DelRequest, delResponse DelResponse, modDNRequest ModifyDNRequest, modDNResponse ModifyDNResponse, compareRequest CompareRequest, compareResponse CompareResponse, abandonRequest AbandonRequest, extendedReq ExtendedRequest, extendedResp ExtendedResponse, ..., intermediateResponse IntermediateResponse }, controls [0] Controls OPTIONAL } MessageID ::= INTEGER (0 .. maxInt) maxInt INTEGER ::= 2147483647 -- (2^^31 - 1) -- LDAPString ::= OCTET STRING -- UTF-8 encoded, [ISO10646] characters

Дальше я с помощью Эрланг сгенерировал парсер этой хуеты.

> asn1ct:compile("LDAP.asn1"). ok

Потом я написал код который отвечает на LDAP запросы:

-module(eds). -author('Maxim Sokhatsky '). -export([start/0]). -compile(export_all). -include("LDAP.hrl"). -include("roster.hrl"). -define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
start() -> listen(389). listen(Port) -> {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS), accept(LSocket). accept(LSocket) -> {ok, Socket} = gen_tcp:accept(LSocket), spawn(fun() -> loop(Socket) end), accept(LSocket). loop(Socket) -> case gen_tcp:recv(Socket, 0) of {ok, Data} -> Decoded = asn1rt:decode('LDAP','LDAPMessage',Data), case Decoded of {ok,{'LDAPMessage',No,Message,Asn}} -> message(No,Message,Socket); _Else -> noLDAP end, loop(Socket); {error, closed} -> ok end.
message(No,Message,Socket) -> io:format("messageID: ~p~n",[No]), io:format("~p~n",[Message]), case Message of {bindRequest, {'BindRequest',Type,Uid,Auth}} -> bind(No,Uid,Auth,Socket); {abandonRequest,Type} -> abandon(No,Socket); {unbindRequest, Null} -> abandon(0,Socket); {searchRequest, {'SearchRequest',SearchDN,Scope,Deref,SizeLimit, TimeLimit,TypesOnly,Filter,Attributes}} -> search(No, SearchDN, Scope,Deref,SizeLimit, TimeLimit,TypesOnly, Filter,Attributes,Socket) end. bind(No,Uid,Auth,Socket) -> roster:init(Uid), Response = #'BindResponse'{resultCode = success, matchedDN = Uid, diagnosticMessage = "OK"}, answer(Response,No,bindResponse,Socket). answer(Response,No,ProtocolOp,Socket) -> Message = #'LDAPMessage'{messageID = No, protocolOp = {ProtocolOp, Response}}, {ok, Bytes} = asn1rt:encode('LDAP', 'LDAPMessage', Message), io:format("~p~n", [Message]), gen_tcp:send(Socket, list_to_binary(Bytes)). search(No,SearchDN,Scope,Deref,SizeLimit, TimeLimit,TypesOnly,Filter,Attributes,Socket) -> roster:traverse(fun({'ContactRecord',CommonName,GivenName,Mail}) -> CN = {'PartialAttribute', "cn", [CommonName]}, MAIL = {'PartialAttribute', "mail", [Mail]}, Response = {'SearchResultEntry', CommonName, [CN,MAIL]}, answer(Response,No,searchResEntry,Socket), continue end), Done = #'LDAPResult'{resultCode = success, matchedDN = SearchDN, diagnosticMessage = "OK"}, answer(Done,No,searchResDone,Socket). abandon(No,Socket) -> roster:done(), gen_tcp:close(Socket).

А также библиотеку для работы с контактной книгой

-module(roster). -include("roster.hrl"). -include_lib("stdlib/include/qlc.hrl"). -compile(export_all). -export([init/1,done/0,put/3,get/1,traverse/1]). init(FileName) -> case ets:info(ram) of undefined -> ets:new(ram, [named_table,{keypos,#'ContactRecord'.cn}, {write_concurrency,true},{read_concurrency,true}]); Else -> ok end, dets:open_file(disk, [{file, FileName},{keypos,#'ContactRecord'.cn}]), ets:from_dets(ram,disk). traverse(Fun) -> dets:traverse(disk,Fun). list() -> ets:foldl(fun(C,Acc) -> io:format("~p~n",[C]) end,none,ram). done() -> case ets:info(ram) of undefined -> ram_empty; ElseClearRam -> ets:delete(ram) end, case dets:info(disk) of {error,Reason} -> disk_closed; ElseCloseDisk -> Res = dets:close(disk), done end. put(CN,GivenName,EMail) -> Contact = #'ContactRecord'{cn = CN, givenName = GivenName, mail = EMail}, ets:insert(ram, Contact), dets:insert(disk, Contact), ok. get(CN) -> case ets:lookup(ram,CN) of [Contact] -> {ok, Contact}; [] -> {error,instance} end.

...В виде эрланг рекордов:

-record('ContactRecord', {cn, givenName, mail}).

Откомпилировал это:

erlc +debug_info -o ebin src\LDAP.erl src\eds.erl src\roster.erl

И все :)

P.S. https://github.com/synrc/eds