Skip to content

Commit

Permalink
add DN parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
merlinz01 committed Apr 13, 2024
1 parent 3404ec4 commit 4c991b5
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 14 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ specifically focused on enabling the building of custom integrations.
## Usage

This package provides an interface similar to that of `net/http`.
See `test/main.go` for a working example implementation.
See `test/main.go` for a working demo implementation.

### Create a handler

Expand Down Expand Up @@ -165,12 +165,12 @@ Then define your Abandon method like this:

```go
func (t *TestHandler) Abandon(conn *ldapserver.Conn, msg *ldapserver.Message, messageID ldapserver.MessageID) {
t.abandonmentLock.Lock()
t.abandonmentLock.Lock()
// Set the flag only if the messageID is in the map
if _, exists := t.abandonment[messageID]; exists {
t.abandonment[messageID] = true
}
t.abandonmentLock.Unlock()
if _, exists := t.abandonment[messageID]; exists {
t.abandonment[messageID] = true
}
t.abandonmentLock.Unlock()
}
```

Expand All @@ -186,7 +186,7 @@ See `test/main.go` for an example.
- [x] TLS
- [x] Strict protocol validation
- [x] OID validation
- [ ] DN parsing support
- [x] DN parsing support
- [x] Full concurrency ability
- [x] Comprehensive message parsing tests
- [x] Abandon request
Expand Down
20 changes: 13 additions & 7 deletions ber.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,21 @@ func BerEncodeEnumerated(i int64) []byte {

// Return a BER-encoded element with the specified type code and data
func BerEncodeElement(etype BerType, data []byte) []byte {
res := make([]byte, len(data)+6)
res := make([]byte, 1, len(data)+6)
res[0] = byte(etype)
size := len(data)
res[1] = 0x84
res[2] = byte(size >> 24)
res[3] = byte(size >> 16)
res[4] = byte(size >> 8)
res[5] = byte(size)
copy(res[6:], data)
if size < 0x80 {
res = append(res, byte(size))
} else if size <= 0xffff {
res = append(res, 0x81, byte((size&0xff00)>>8), byte(size&0xff))
} else if size <= 0xffffff {
res = append(res, 0x82, byte((size&0xff0000)>>16), byte((size&0xff00)>>8), byte(size&0xff))
} else if size <= 0xffffffff {
res = append(res, 0x83, byte((size&0xff000000)>>24), byte((size&0xff0000)>>16), byte((size&0xff00)>>8), byte(size&0xff))
} else {
panic("size too large")
}
res = append(res, data...)
return res
}

Expand Down
235 changes: 235 additions & 0 deletions dn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package ldapserver

import (
"bytes"
"strconv"
"strings"
"unicode"
)

type DN []RDN

type RDN []RDNAttribute

type RDNAttribute struct {
Type string
Value string
}

func (d DN) String() string {
s := ""
for i, rdn := range d {
if i > 0 {
s += ","
}
s += rdn.String()
}
return s
}

func (d DN) Equal(other DN) bool {
if len(d) != len(other) {
return false
}
for i, rdn := range d {
if !rdn.Equal(other[i]) {
return false
}
}
return true
}

func (r RDN) String() string {
s := ""
for i, attr := range r {
if i > 0 {
s += "+"
}
s += attr.String()
}
return s
}

func (r RDN) Equal(other RDN) bool {
if len(r) != len(other) {
return false
}
for i, attr := range r {
if attr.Type != other[i].Type || attr.Value != other[i].Value {
return false
}
}
return true
}

func (a RDNAttribute) String() string {
if OID(a.Type).Validate() == nil {
buf := make([]byte, 1, len(a.Value)*2+1)
buf[0] = '#'
for _, b := range BerEncodeOctetString(a.Value) {
s := strconv.FormatUint(uint64(b), 16)
if len(s) == 1 {
s = "0" + s
}
buf = append(buf, strings.ToUpper(s)...)
}
return a.Type + "=" + string(buf)
} else {
buf := make([]byte, 0, len(a.Value))
for i, b := range []byte(a.Value) {
switch b {
case ' ':
if i == 0 || i == len(a.Value)-1 {
buf = append(buf, '\\', b)
} else {
buf = append(buf, b)
}
case '#':
if i == 0 {
buf = append(buf, '\\', b)
} else {
buf = append(buf, b)
}
case '"', '+', ',', ';', '<', '>', '\\', '=':
buf = append(buf, '\\', b)
case '\x00':
buf = append(buf, '\\', '0', '0')
default:
if unicode.IsPrint(rune(b)) {
buf = append(buf, b)
} else {
s := strconv.FormatUint(uint64(b), 16)
if len(s) == 1 {
s = "0" + s
}
s = "\\" + s
buf = append(buf, strings.ToUpper(s)...)
}
}
}
return a.Type + "=" + string(buf)
}
}

func ParseDN(s string) (DN, error) {
var dn DN
for _, rdn := range splitRDNs(s) {
var r RDN
for _, attr := range splitAttrs(rdn) {
parts := splitAttr(attr)
value, err := DecodeRDNAttributeValue(parts[1])
if err != nil {
return nil, err
}
r = append(r, RDNAttribute{Type: parts[0], Value: value})
}
dn = append(dn, r)
}
return dn, nil
}

func splitRDNs(s string) []string {
if s == "" {
return nil
}
a := make([]string, 0, 1)
start := 0
for i := 0; i < len(s); i++ {
if s[i] == ',' {
// Handle escaped comma
slash_index := i - 1
for slash_index >= 0 {
if s[slash_index] != '\\' {
break
}
slash_index--
}
if (i-slash_index)%2 == 1 {
a = append(a, s[start:i])
start = i + 1
}
}
}
a = append(a, s[start:])
return a
}

func splitAttrs(s string) []string {
if s == "" {
return nil
}
a := make([]string, 0, 1)
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '+' {
// Handle escaped plus
slash_index := i - 1
for slash_index >= 0 {
if s[slash_index] != '\\' {
break
}
slash_index--
}
if (i-slash_index)%2 == 1 {
a = append(a, s[start:i])
start = i + 1
}
}
}
a = append(a, s[start:])
return a
}

func splitAttr(s string) []string {
return strings.SplitN(s, "=", 2)
}

func DecodeRDNAttributeValue(s string) (string, error) {
if len(s) == 0 {
return s, nil
}
if s[0] == '#' {
buf := make([]byte, 0, len(s)/2)
for i := 1; i < len(s); i += 2 {
b := (s[i]-'0')<<4 + (s[i+1] - '0')
buf = append(buf, b)
}
s, err := BerReadElement(bytes.NewReader(buf))
if err != nil {
return "", err
}
if s.Type != BerTypeOctetString {
return "", ErrWrongElementType.WithInfo("RDNAttribute type", s.Type)
}
return BerGetOctetString(s.Data), nil
}
buf := make([]byte, 0, len(s))
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\\' {
if i+1 < len(s) {
b2 := s[i+1]
switch b2 {
case '"', '+', ',', ';', '<', '>', ' ', '\\', '=', '#':
buf = append(buf, b2)
i++
default:
if i+2 < len(s) {
bi, err := strconv.ParseUint(s[i+1:i+3], 16, 8)
if err == nil {
buf = append(buf, byte(bi))
i += 2
} else {
buf = append(buf, b)
}
} else {
buf = append(buf, b)
}
}
}
} else {
buf = append(buf, b)
}
}
return string(buf), nil
}
48 changes: 48 additions & 0 deletions dn_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ldapserver_test

import (
"testing"

"github.com/merlinz01/ldapserver"
)

func TestEncodeDN(t *testing.T) {
type dnTest struct {
dnStr string
dn ldapserver.DN
}
tests := []dnTest{
{"uid=jdoe,ou=users,dc=example,dc=com",
ldapserver.DN{{{"uid", "jdoe"}}, {{"ou", "users"}}, {{"dc", "example"}}, {{"dc", "com"}}}},
{"UID=jsmith,DC=example,DC=net",
ldapserver.DN{{{"UID", "jsmith"}}, {{"DC", "example"}}, {{"DC", "net"}}}},
{"OU=Sales+CN=J. Smith,DC=example,DC=net",
ldapserver.DN{{{"OU", "Sales"}, {"CN", "J. Smith"}}, {{"DC", "example"}}, {{"DC", "net"}}}},
{"CN=James \\\"Jim\\\" Smith,DC=example,DC=net",
ldapserver.DN{{{"CN", "James \"Jim\" Smith"}}, {{"DC", "example"}}, {{"DC", "net"}}}},
{"CN=Before\\0DAfter,DC=example,DC=net",
ldapserver.DN{{{"CN", "Before\rAfter"}}, {{"DC", "example"}}, {{"DC", "net"}}}},
{"1.3.6.1.4.1.1466.0=#04024869",
ldapserver.DN{{{"1.3.6.1.4.1.1466.0", "\x48\x69"}}}},
{"CN=Lu\xC4\\8Di\xC4\\87",
ldapserver.DN{{{"CN", "Lu\xC4\x8Di\xC4\x87"}}}},
{"uid=jdoe,ou=C\\+\\+ Developers,dc=example,dc=com",
ldapserver.DN{{{"uid", "jdoe"}}, {{"ou", "C++ Developers"}}, {{"dc", "example"}}, {{"dc", "com"}}}},
{"cn=John Doe\\, Jr.,ou=Developers,dc=example,dc=com",
ldapserver.DN{{{"cn", "John Doe, Jr."}}, {{"ou", "Developers"}}, {{"dc", "example"}}, {{"dc", "com"}}}},
{"cn=\\\"John A. Doe\\, Sr.\\, C\\\\C\\+\\+ Developer\\\"+givenName=John+sn=Doe,ou=Developers,dc=example,dc=com",
ldapserver.DN{{{"cn", `"John A. Doe, Sr., C\C++ Developer"`}, {"givenName", "John"}, {"sn", "Doe"}}, {{"ou", "Developers"}}, {{"dc", "example"}}, {{"dc", "com"}}}},
}
for _, dn := range tests {
pdn, err := ldapserver.ParseDN(dn.dnStr)
if err != nil {
t.Fatalf("Error parsing DN: %s", err)
} else if !pdn.Equal(dn.dn) {
t.Errorf("Expected %s", dn.dn)
t.Fatalf("Got %s", pdn)
} else if pdn.String() != dn.dnStr {
t.Errorf("Expected %s", dn.dnStr)
t.Fatalf("Got %s", pdn.String())
}
}
}

0 comments on commit 4c991b5

Please sign in to comment.