├── tests ├── config.nims ├── test_asn1.nim ├── test_sync.nim └── test_async.nim ├── examples ├── config.nims ├── dump_sync.nim ├── dump_async.nim └── listen_async.nim ├── config.nims ├── src ├── nimldap.nim └── nimldap │ ├── tinyasn1.nim │ ├── sync.nim │ ├── async.nim │ ├── shared.nim │ └── bindings.nim ├── .gitignore ├── README.md ├── nimldap.nimble └── .github └── workflows └── test.yaml /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | -------------------------------------------------------------------------------- /examples/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") 2 | -------------------------------------------------------------------------------- /config.nims: -------------------------------------------------------------------------------- 1 | switch("gc", "orc") 2 | switch("define", "useMalloc") 3 | -------------------------------------------------------------------------------- /src/nimldap.nim: -------------------------------------------------------------------------------- 1 | import nimldap/sync 2 | import nimldap/async 3 | 4 | export sync 5 | export async 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tests/test_asn1 2 | tests/test_async 3 | tests/test_sync 4 | examples/dump_sync 5 | examples/dump_async 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Tests](https://github.com/inv2004/nimldap/actions/workflows/test.yaml/badge.svg)](https://github.com/inv2004/nimldap/actions/workflows/test.yaml) 2 | 3 | # nimldap 4 | 5 | - [x] sync 6 | - [x] async 7 | - [x] custom controls 8 | - [x] LDAP_PAGED_RESULT_OID_STRING control support. via ```pageSize``` parameter 9 | - [x] LDAP_SERVER_NOTIFICATION_OID in async 10 | 11 | ## Examples 12 | https://github.com/inv2004/nimldap/tree/main/examples 13 | -------------------------------------------------------------------------------- /nimldap.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.5.3" 4 | author = "inv2004" 5 | description = "LDAP client bindings" 6 | license = "MIT" 7 | srcDir = "src" 8 | 9 | 10 | # Dependencies 11 | 12 | requires "nim >= 1.6.8" 13 | requires "stew" 14 | 15 | task fulltest, "valgrind memleak": 16 | for f in listFiles("tests"): 17 | if f.endsWith ".nim": 18 | exec "nim c --forceBuild --gc:orc -d:useMalloc " & f 19 | exec "valgrind --error-exitcode=1 --leak-check=yes --errors-for-leak-kinds=definite " & f[0..^5] 20 | -------------------------------------------------------------------------------- /examples/dump_sync.nim: -------------------------------------------------------------------------------- 1 | import nimldap 2 | import strutils 3 | import strformat 4 | 5 | const host = "ldap://ldap.forumsys.com:389" 6 | const login = "cn=read-only-admin,dc=example,dc=com" 7 | const pass = "password" 8 | 9 | let ld = newLdap host 10 | ld.saslBind login, pass 11 | echo ld.whoAmI() 12 | 13 | for entry in ld.search("(objectclass=*)", ["+", "*"], LdapScope.Base, ""): 14 | echo entry.dn() 15 | for attr, vals in entry: 16 | echo fmt""" {attr} ({vals.len}): {(vals.values().join", ")}""" 17 | 18 | for entry in ld.search "(objectclass=*)": 19 | echo entry.dn() 20 | for attr, vals in entry: 21 | echo fmt""" {attr} ({vals.len}): {(vals.values().join", ")}""" 22 | -------------------------------------------------------------------------------- /examples/dump_async.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch 2 | import strutils 3 | import strformat 4 | 5 | import nimldap 6 | 7 | const host = "ldap://ldap.forumsys.com:389" 8 | const login = "cn=read-only-admin,dc=example,dc=com" 9 | const pass = "password" 10 | 11 | proc main() {.async.} = 12 | let ld = newLdapAsync host 13 | await ld.saslBind(login, pass) 14 | echo waitFor ld.whoAmI() 15 | let s = ld.search "(objectclass=*)" 16 | while true: 17 | let entry = await s.next() 18 | if entry.done: 19 | break 20 | echo entry.dn() 21 | for attr in entry: 22 | let vals = entry{attr} 23 | echo fmt""" {attr} ({vals.len}): {(vals.join", ")}""" 24 | 25 | waitFor main() 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: "Tests" 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | timeout-minutes: 10 8 | strategy: 9 | matrix: 10 | nim: [ 'stable' ] 11 | os: [ 'ubuntu-20.04' ] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: install ldap2 valgrind 15 | run: sudo apt install libldap2-dev valgrind -y 16 | - uses: actions/checkout@v2 17 | # - uses: actions/cache@v2 18 | # with: 19 | # path: ~/.nimble 20 | # key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }} 21 | - uses: jiro4989/setup-nim-action@v1 22 | with: 23 | nim-version: ${{ matrix.nim }} 24 | - run: nim --version && nimble install -d -y && nimble fulltest -y 25 | -------------------------------------------------------------------------------- /tests/test_asn1.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | import strutils 3 | 4 | import nimldap/tinyasn1 5 | 6 | test "encode": 7 | check newAsnInt(33).toHex == "020121" 8 | check newAsnInt(333).toHex == "0202014D" 9 | check newAsnString("r".repeat 33).toHex == "0421" & "72".repeat 33 10 | check newAsnString("r".repeat 333).toHex == "0482014D" & "72".repeat 333 11 | check newAsnString("r".repeat 3333).toHex == "04820D05" & "72".repeat 3333 12 | check newPagingValue(7, "").toHex == "30050201070400" 13 | check newPagingValue(18, "").toHex == "30050201120400" 14 | check newPagingValue(5000, "abc").toHex == "3009020213880403616263" 15 | check newPagingValue(5000, "r".repeat 5000).toHex == "308213900202138804821388" & "72".repeat 5000 16 | 17 | test "decode": 18 | check newPagingValue(7, "").readPagingValues == (7, "") 19 | check newPagingValue(18, "").readPagingValues == (18, "") 20 | check newPagingValue(5000, "abc").readPagingValues == (5000, "abc") 21 | check newPagingValue(5000, "r".repeat 5000).readPagingValues == (5000, "r".repeat 5000) 22 | -------------------------------------------------------------------------------- /examples/listen_async.nim: -------------------------------------------------------------------------------- 1 | import asyncdispatch 2 | import strutils 3 | import xmlparser 4 | import xmltree 5 | import sequtils 6 | 7 | import nimldap 8 | 9 | # The server does not support LDAP_SERVER_NOTIFICATION_OID 10 | const host = "ldap://ldap.forumsys.com:389" 11 | const login = "read-only-admin@example.com" 12 | const pass = "password" 13 | 14 | let ctrls = @[newCtrl Extension.LDAP_SERVER_NOTIFICATION_OID] 15 | 16 | proc findChange(metas: seq[string]) = 17 | let idx = metas 18 | .mapIt(parseXml(it)) 19 | .mapIt(it.findAll("usnLocalChange")[0].innerText.parseInt()) 20 | .maxIndex() 21 | 22 | echo metas[idx] 23 | 24 | proc main() {.async.} = 25 | let ld = newLdapAsync host 26 | await ld.saslBind(login, pass) 27 | echo waitFor ld.whoAmI() 28 | let s = ld.search("(objectclass=*)", attrs = @["+", "*", 29 | "msDS-ReplAttributeMetaData"], ctrls = ctrls) 30 | while true: 31 | let entry = await s.next() 32 | if entry.done: 33 | break 34 | echo entry.dn() 35 | findChange entry{"msDS-ReplAttributeMetaData"} 36 | 37 | waitFor main() 38 | 39 | -------------------------------------------------------------------------------- /src/nimldap/tinyasn1.nim: -------------------------------------------------------------------------------- 1 | import stew/endians2 2 | import bitops 3 | import streams 4 | import bindings 5 | 6 | proc beLen[T: static[int]](be: array[T, byte]): int = 7 | var len0 = 0 8 | for x in be: 9 | if x == 0: 10 | inc len0 11 | else: 12 | break 13 | be.len - len0 14 | 15 | proc genLen(len: int): string = 16 | if len < 0x80: 17 | result.add len.char 18 | else: 19 | let bb = len.uint64.toBytesBE 20 | let lenlen = bb.beLen 21 | result.add char(0x80 or lenlen.byte) 22 | for x in bb[^lenlen..^1]: result.add x.char 23 | 24 | proc newAsnInt*(size: uint32): string = 25 | result.add '\x02' 26 | let be = size.toBytesBE 27 | let len = be.beLen 28 | result.add genLen(len) 29 | for x in be[^len..^1]: result.add x.char 30 | 31 | proc newAsnString*(s: string): string = 32 | result.add '\x04' 33 | result.add genLen(s.len) 34 | result.add s 35 | 36 | proc newPagingValue*(size: int, cookie: string): string = 37 | result.add "\x30" 38 | let i = newAsnInt(size.uint32) 39 | let s = newAsnString(cookie) 40 | result.add genLen((i.len + s.len)) 41 | result.add i 42 | result.add s 43 | 44 | proc getLen*(be: openArray[byte]): (int, int) = 45 | if not be[1].testBit(7): 46 | return (be[1].int, 1) 47 | else: 48 | let lenlen = int(be[1] and 0x7F) 49 | for i in 2..<2+lenlen: 50 | result[0] = 256*result[0] + be[i].int 51 | result[1] = 1+lenlen 52 | 53 | proc readLen(s: StringStream): int = 54 | let len0 = s.readUint8 55 | if len0.testBit(7): 56 | let lenlen = int(len0 and 0x7F) 57 | for _ in 0.. 0: 114 | cookie = cookieFromMsg(ld, pageSize, msg) 115 | if cookie == "": 116 | break 117 | -------------------------------------------------------------------------------- /src/nimldap/shared.nim: -------------------------------------------------------------------------------- 1 | import bindings 2 | import tinyasn1 3 | 4 | import strutils 5 | import endians 6 | import system/ansi_c 7 | 8 | type 9 | Ldap* = object 10 | r*: ptr LdapInt 11 | base*: string 12 | pageSize*: int 13 | LdapRef* = ref Ldap 14 | 15 | LdapAsync* = object 16 | r*: ptr LdapInt 17 | base*: string 18 | pageSize*: int 19 | LdapAsyncRef* = ref LdapAsync 20 | 21 | Entry* = ref object 22 | entry*: EntryInt 23 | msg*: LdapMessageRef 24 | ld*: LdapRef 25 | 26 | EntryAsync* = ref object 27 | entry*: EntryInt 28 | msg*: LdapMessageRef 29 | ld*: LdapAsyncRef 30 | done*: bool 31 | 32 | Ctrl* = object 33 | oid*: string 34 | val*: string 35 | isCritical*: bool 36 | 37 | AnyEntry = Entry | EntryAsync 38 | 39 | LdapException* = object of ValueError 40 | errCode*: int 41 | 42 | const defaultNetworkTimeout* = 2 43 | const rootDC* = "/" 44 | 45 | type 46 | Extension* = enum 47 | LDAP_SERVER_NOTIFICATION_OID = "1.2.840.113556.1.4.528" 48 | LDAP_PAGED_RESULT_OID_STRING = "1.2.840.113556.1.4.319" 49 | 50 | proc `=destroy`(x: var Ldap) = 51 | if x.r != nil: 52 | discard ldap_unbind_ext_s(x.r, nil, nil) 53 | `=destroy`(x.base) 54 | 55 | proc `=destroy`(x: var LdapAsync) = 56 | if x.r != nil: 57 | discard ldap_unbind_ext_s(x.r, nil, nil) 58 | `=destroy`(x.base) 59 | 60 | proc newLdapException*(err: int, msg = ""): ref LdapException = 61 | new(result) 62 | result.errCode = err 63 | result.msg = msg & " with error code " & $err 64 | 65 | template checkErr*(body: untyped): untyped = 66 | let err = body 67 | if err != 0: 68 | let str = $ldap_err2string(err) 69 | raise newLdapException(err, str) 70 | 71 | proc setOption*(ld: LdapRef|LdapAsyncRef, opt: LdapOption, value: int) = 72 | var val = value.int 73 | checkErr ldap_set_option(ld.r, opt, val.addr) 74 | 75 | proc setOption*(ld: LdapRef|LdapAsyncRef, opt: LdapOption, value: LdapVersion) = 76 | ld.setOption(opt, value.int) 77 | 78 | proc extractDC*(login: string): string = 79 | let lcLogin = login.toLowerAscii 80 | if lcLogin.find"dc=" >= 0: 81 | for p in lcLogin.split ",": 82 | if p.startsWith "dc=": 83 | if result.len > 0: result.add "," 84 | result.add p 85 | else: 86 | let emailParts = lcLogin.split '@' 87 | let domain = emailParts[^1] 88 | for p in domain.split ".": 89 | if result.len > 0: result.add "," 90 | result.add "dc="&p 91 | 92 | proc unbind*(ld: LdapRef|LdapAsyncRef) = 93 | checkErr ldap_unbind_ext_s(ld.r, nil, nil) 94 | ld.r = nil 95 | 96 | proc dn*(e: AnyEntry): string = 97 | $ldap_get_dn(e.ld.r, e.entry) 98 | 99 | proc `[]`*(e: AnyEntry, attr: string): string = 100 | let vals = ldap_get_values_len(e.ld.r, e.entry, attr.cstring) 101 | if vals.r == nil: 102 | raise newException(KeyError, "key not found: " & attr) 103 | var val = vals[result.len] 104 | if val != nil: 105 | result = $val[] 106 | 107 | proc `{}`*(e: AnyEntry, attr: string): seq[string] = 108 | let vals = ldap_get_values_len(e.ld.r, e.entry, attr.cstring) 109 | if vals.r == nil: 110 | raise newException(KeyError, "key not found: " & attr) 111 | var val = vals[result.len] 112 | while val != nil: 113 | result.add $val[] 114 | val = vals[result.len] 115 | 116 | proc pretty*(e: AnyEntry): string = 117 | result.add e.dn() & "\n" 118 | for k, v in e: 119 | var res = v.values() 120 | for i, s in res: 121 | var visible = true 122 | for c in s: 123 | if c notin {' '..'~'}: 124 | visible = false 125 | break 126 | if not visible: 127 | res[i] = "0x" & s.toHex() 128 | result.add " " & $k & ": " & res.join(" | ") & "\n" 129 | 130 | iterator items*(e: AnyEntry): string = 131 | var berElem: BerElement 132 | var attr = ldap_first_attribute(e.ld.r, e.entry, berElem) 133 | while attr.r != nil: 134 | yield $attr 135 | attr = ldap_next_attribute(e.ld.r, e.entry, berElem) 136 | 137 | iterator pairs*(e: AnyEntry): (string, BerArrRef) = 138 | var berElem: BerElement 139 | var attr = ldap_first_attribute(e.ld.r, e.entry, berElem) 140 | while attr.r != nil: 141 | let vals = BerArrRef() 142 | vals[] = ldap_get_values_len(e.ld.r, e.entry, attr.r) 143 | yield ($attr, vals) 144 | attr = ldap_next_attribute(e.ld.r, e.entry, berElem) 145 | 146 | iterator items*(vals: BerArrRef): string = 147 | var idx = 0 148 | var val = vals[idx] 149 | while val != nil: 150 | yield $val[] 151 | inc idx 152 | val = vals[idx] 153 | 154 | proc len*(vals: BerArrRef): int = 155 | ldapCountValuesLen(nil, vals) 156 | 157 | proc attrs*(e: AnyEntry): seq[string] = 158 | var ber: BerElement 159 | var attr = ldap_first_attribute(e.ld.r, e.entry, ber) 160 | while attr.r != nil: 161 | result.add $attr 162 | attr = ldap_next_attribute(e.ld.r, e.entry, ber) 163 | 164 | proc values*(vals: BerArrRef): seq[string] = 165 | var val = vals[result.len] 166 | while val != nil: 167 | result.add $val[] 168 | val = vals[result.len] 169 | 170 | proc `$`*(vals: BerArrRef): string = 171 | $values(vals) 172 | 173 | proc contains*(e: AnyEntry, attr: string): bool = 174 | attr in e.attrs() 175 | 176 | proc getOrEmpty*(e: AnyEntry, attr: string): string = 177 | if attr in e: 178 | return e[attr] 179 | 180 | proc newCtrl*(oid: string|Extension, val = "", isCritical = true): Ctrl = 181 | Ctrl(oid: $oid, val: val, isCritical: isCritical) 182 | 183 | proc newControls*(ctrls: openArray[Ctrl]): CtrlArr = 184 | if ctrls.len == 0: 185 | return 186 | 187 | result.r = cast[ptr UncheckedArray[ptr CtrlInt]](c_calloc(ctrls.len.csize_t+1, 188 | sizeof(ptr CtrlInt).csize_t)) 189 | 190 | for i, ctrl in ctrls: 191 | let val = newBer(ctrl.val) 192 | checkErr ldap_control_create(ctrl.oid.cstring, ctrl.isCritical.int, val, 1, 193 | result.r[i]) 194 | 195 | proc newPagingCtrl*(size: int, cookie: string): Ctrl = 196 | newCtrl(LDAP_PAGED_RESULT_OID_STRING, newPagingValue(size, cookie)) 197 | 198 | proc newCtrlsWithPage*(ctrls: openArray[Ctrl], pageSize: int, 199 | cookie: string): CtrlArr = 200 | if pageSize > 0: 201 | newControls(@ctrls & newPagingCtrl(pageSize, cookie)) 202 | else: 203 | newControls(ctrls) 204 | 205 | proc cookieFromMsg*(ld: LdapRef|LdapAsyncRef, pageSize: int, 206 | msg: LdapMessageRef): string = 207 | if pageSize == 0: 208 | return 209 | var errcode = 0 210 | var ctrls = CtrlArr() 211 | checkErr ldap_parse_result(ld.r, msg.r, errcode, nil, nil, nil, ctrls.r, 0) 212 | var i = 0 213 | while ctrls.r[i] != nil: 214 | if $LDAP_PAGED_RESULT_OID_STRING == $ctrls.r[i].oid: 215 | return readCook(ctrls.r[0]) 216 | inc i 217 | 218 | proc sid*(s: string): string = 219 | if s.len < 2: 220 | raise newException(ValueError, "cannot parse sid: too short") 221 | let cnt = s[1].int 222 | if s.len != 8 + 4*cnt: 223 | raise newException(ValueError, "cannot parse sid: wrong len") 224 | var ia: uint64 225 | bigEndian64(ia.addr, s[0].unsafeAddr) 226 | ia = ia and 0x0000FFFFFFFFFFFF'u64 227 | result.add "S-" & $s[0].byte 228 | result.add "-" & $ia 229 | for x in 0..