Skip to content

Commit c7248aa

Browse files
authored
Add function to escape distinguished names (#393)
* Add function to escape distinguished names * Test if escaping of trailing space works with multi-byte characters
1 parent cb504ae commit c7248aa

File tree

4 files changed

+130
-0
lines changed

4 files changed

+130
-0
lines changed

ldap.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io/ioutil"
66
"log"
77
"os"
8+
"strings"
89

910
ber "github.com/go-asn1-ber/asn1-ber"
1011
)
@@ -345,3 +346,43 @@ func EscapeFilter(filter string) string {
345346
}
346347
return string(buf)
347348
}
349+
350+
// EscapeDN escapes distinguished names as described in RFC4514. Characters in the
351+
// set `"+,;<>\` are escaped by prepending a backslash, which is also done for trailing
352+
// spaces or a leading `#`. Null bytes are replaced with `\00`.
353+
func EscapeDN(dn string) string {
354+
if dn == "" {
355+
return ""
356+
}
357+
358+
builder := strings.Builder{}
359+
360+
for i, r := range dn {
361+
// Escape leading and trailing spaces
362+
if (i == 0 || i == len(dn)-1) && r == ' ' {
363+
builder.WriteRune('\\')
364+
builder.WriteRune(r)
365+
continue
366+
}
367+
368+
// Escape leading '#'
369+
if i == 0 && r == '#' {
370+
builder.WriteRune('\\')
371+
builder.WriteRune(r)
372+
continue
373+
}
374+
375+
// Escape characters as defined in RFC4514
376+
switch r {
377+
case '"', '+', ',', ';', '<', '>', '\\':
378+
builder.WriteRune('\\')
379+
builder.WriteRune(r)
380+
case '\x00': // Null byte may not be escaped by a leading backslash
381+
builder.WriteString("\\00")
382+
default:
383+
builder.WriteRune(r)
384+
}
385+
}
386+
387+
return builder.String()
388+
}

ldap_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,27 @@ func Test_addControlDescriptions(t *testing.T) {
320320
})
321321
}
322322
}
323+
324+
func TestEscapeDN(t *testing.T) {
325+
tests := []struct {
326+
name string
327+
dn string
328+
want string
329+
}{
330+
{name: "emptyString", dn: "", want: ""},
331+
{name: "comma", dn: "test,user", want: "test\\,user"},
332+
{name: "numberSign", dn: "#test#user#", want: "\\#test#user#"},
333+
{name: "backslash", dn: "\\test\\user\\", want: "\\\\test\\\\user\\\\"},
334+
{name: "whitespaces", dn: " test user ", want: "\\ test user \\ "},
335+
{name: "nullByte", dn: "\u0000te\x00st\x00user" + string(rune(0)), want: "\\00te\\00st\\00user\\00"},
336+
{name: "variousCharacters", dn: "test\"+,;<>\\-_user", want: "test\\\"\\+\\,\\;\\<\\>\\\\-_user"},
337+
{name: "multiByteRunes", dn: "test\u0391user ", want: "test\u0391user\\ "},
338+
}
339+
for _, tt := range tests {
340+
t.Run(tt.name, func(t *testing.T) {
341+
if got := EscapeDN(tt.dn); got != tt.want {
342+
t.Errorf("EscapeDN(%s) = %s, expected %s", tt.dn, got, tt.want)
343+
}
344+
})
345+
}
346+
}

v3/ldap.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io/ioutil"
66
"log"
77
"os"
8+
"strings"
89

910
ber "github.com/go-asn1-ber/asn1-ber"
1011
)
@@ -345,3 +346,43 @@ func EscapeFilter(filter string) string {
345346
}
346347
return string(buf)
347348
}
349+
350+
// EscapeDN escapes distinguished names as described in RFC4514. Characters in the
351+
// set `"+,;<>\` are escaped by prepending a backslash, which is also done for trailing
352+
// spaces or a leading `#`. Null bytes are replaced with `\00`.
353+
func EscapeDN(dn string) string {
354+
if dn == "" {
355+
return ""
356+
}
357+
358+
builder := strings.Builder{}
359+
360+
for i, r := range dn {
361+
// Escape leading and trailing spaces
362+
if (i == 0 || i == len(dn)-1) && r == ' ' {
363+
builder.WriteRune('\\')
364+
builder.WriteRune(r)
365+
continue
366+
}
367+
368+
// Escape leading '#'
369+
if i == 0 && r == '#' {
370+
builder.WriteRune('\\')
371+
builder.WriteRune(r)
372+
continue
373+
}
374+
375+
// Escape characters as defined in RFC4514
376+
switch r {
377+
case '"', '+', ',', ';', '<', '>', '\\':
378+
builder.WriteRune('\\')
379+
builder.WriteRune(r)
380+
case '\x00': // Null byte may not be escaped by a leading backslash
381+
builder.WriteString("\\00")
382+
default:
383+
builder.WriteRune(r)
384+
}
385+
}
386+
387+
return builder.String()
388+
}

v3/ldap_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,27 @@ func Test_addControlDescriptions(t *testing.T) {
320320
})
321321
}
322322
}
323+
324+
func TestEscapeDN(t *testing.T) {
325+
tests := []struct {
326+
name string
327+
dn string
328+
want string
329+
}{
330+
{name: "emptyString", dn: "", want: ""},
331+
{name: "comma", dn: "test,user", want: "test\\,user"},
332+
{name: "numberSign", dn: "#test#user#", want: "\\#test#user#"},
333+
{name: "backslash", dn: "\\test\\user\\", want: "\\\\test\\\\user\\\\"},
334+
{name: "whitespaces", dn: " test user ", want: "\\ test user \\ "},
335+
{name: "nullByte", dn: "\u0000te\x00st\x00user" + string(rune(0)), want: "\\00te\\00st\\00user\\00"},
336+
{name: "variousCharacters", dn: "test\"+,;<>\\-_user", want: "test\\\"\\+\\,\\;\\<\\>\\\\-_user"},
337+
{name: "multiByteRunes", dn: "test\u0391user ", want: "test\u0391user\\ "},
338+
}
339+
for _, tt := range tests {
340+
t.Run(tt.name, func(t *testing.T) {
341+
if got := EscapeDN(tt.dn); got != tt.want {
342+
t.Errorf("EscapeDN(%s) = %s, expected %s", tt.dn, got, tt.want)
343+
}
344+
})
345+
}
346+
}

0 commit comments

Comments
 (0)