diff --git a/dn.go b/dn.go index f89e73a9..e80ce6cd 100644 --- a/dn.go +++ b/dn.go @@ -46,6 +46,7 @@ import ( enchex "encoding/hex" "errors" "fmt" + "regexp" "strings" "gopkg.in/asn1-ber.v1" @@ -212,6 +213,89 @@ func (d *DN) AncestorOf(other *DN) bool { return true } +// String returns string representation of the RDN Object with required +// escaping of spaces and formatting applied +func (r RelativeDN) String() string { + var buffer bytes.Buffer + for j := range r.Attributes { + if j > 0 { + buffer.WriteString("+") + } + buffer.WriteString(r.Attributes[j].Type) + buffer.WriteString("=") + //Escape the value before building DN string + val := EscapeAttrValue(r.Attributes[j].Value) + buffer.WriteString(val) + } + return buffer.String() +} + +// String returns string representation of the DN Object with required +// escaping of spaces and formatting applied +func (d DN) String() string { + var buffer bytes.Buffer + + for i := range d.RDNs { + if i > 0 { + buffer.WriteString(",") + } + buffer.WriteString(d.RDNs[i].String()) + } + return buffer.String() +} + +//EscapeAttrValue function escapes the LDAP special characters from the value part +//of the RDN +/** +distinguishedName = [ relativeDistinguishedName + *( COMMA relativeDistinguishedName ) ] +relativeDistinguishedName = attributeTypeAndValue + *( PLUS attributeTypeAndValue ) +attributeTypeAndValue = attributeType EQUALS attributeValue +attributeType = descr / numericoid +attributeValue = string / hexstring + +; The following characters are to be escaped when they appear +; in the value to be encoded: ESC, one of , leading +; SHARP or SPACE, trailing SPACE, and NULL. +string = [ ( leadchar / pair ) [ *( stringchar / pair ) + ( trailchar / pair ) ] ] + +leadchar = LUTF1 / UTFMB +LUTF1 = %x01-1F / %x21 / %x24-2A / %x2D-3A / + %x3D / %x3F-5B / %x5D-7F + +trailchar = TUTF1 / UTFMB +TUTF1 = %x01-1F / %x21 / %x23-2A / %x2D-3A / +%x3D / %x3F-5B / %x5D-7F + +stringchar = SUTF1 / UTFMB +SUTF1 = %x01-21 / %x23-2A / %x2D-3A / +%x3D / %x3F-5B / %x5D-7F + +pair = ESC ( ESC / special / hexpair ) +special = escaped / SPACE / SHARP / EQUALS +escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE +hexstring = SHARP 1*hexpair +hexpair = HEX HEX + +where the productions , , , , +, , , , , , , , +, , and are defined in [RFC4512]. +*/ +func EscapeAttrValue(in string) string { + + escape := regexp.MustCompile("[,\\#+<>;\"=]") + out := escape.ReplaceAllString(in, "\\$0") + if strings.HasPrefix(out, " ") { + out = "\\" + out + } + if strings.HasSuffix(out, " ") { + out = out[:len(out)-1] + "\\ " + } + return out +} + // Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch). // Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues // and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type. diff --git a/dn_test.go b/dn_test.go index 511c8343..2c90eb88 100644 --- a/dn_test.go +++ b/dn_test.go @@ -171,6 +171,90 @@ func TestDNEqual(t *testing.T) { } } +func TestEscapeAttrValue(t *testing.T) { + testcases := []struct { + A string + B string + Equal bool + }{ + // Exact match + {"", "", true}, + {"o=A", "o\\=A", true}, + {"o,A", "o\\,A", true}, + {"o#A", "o\\#A", true}, + {"oA", "o\\>A", true}, + {"o;A", "o\\;A", true}, + {"o\"A", "o\\\"A", true}, + {"o+A", "o\\+A", true}, + {" o=A", "\\ o\\=A", true}, + {"o=A ", "o\\=A\\ ", true}, + {" o=A ", "\\ o\\=A\\ ", true}, + } + for i, tc := range testcases { + a := EscapeAttrValue(tc.A) + if tc.B != a { + t.Errorf("%d: when escaping string '%s', got '%s' expected and '%s'", i, tc.A, a, tc.B) + continue + } + } +} + +func TestDNString(t *testing.T) { + testcases := []struct { + A string + B string + Equal bool + }{ + // Exact match + {"", "", true}, + {"o=A", "o=A", true}, + {"o=A", "o=B", false}, + + {"o= A,o=B", "o=A,o=B", true}, + {"o=A,o=B", "o=A,o=C", false}, + + {"o=A+o=B", "o=A+o=B", true}, + {"o=A + o=B", "o=A+o=B", true}, + {"o= A +o= B", "o=A+o=B", true}, + + // Number of RDN attributes is significant + {"o=A+o=B", "O=B+o=A+O=B", false}, + + // Missing values are significant + {"o=A+o=B", "O=B+o=A+O=C", false}, // missing values matter + {"o=A+o=B+o=C", "O=B+o=A", false}, // missing values matter + + // Real examples + // Difference in leading/trailing chars is ignored + { + "cn=John Doe, ou=People, dc=sun.com", + "cn=John Doe,ou=People,dc=sun.com", + true, + }, + // Difference in values is significant + { + "cn=John Doe, ou=People, dc=sun.com", + "cn=John Doe, ou=People, dc=sun.com", + false, + }, + } + + for i, tc := range testcases { + a, err := ParseDN(tc.A) + if err != nil { + t.Errorf("%d: %v", i, err) + continue + } + dnstr := a.String() + + if tc.Equal == (tc.B != dnstr) { + t.Errorf("%d: when converting to string '%s', got '%s' expected and '%s'", i, tc.A, dnstr, tc.B) + continue + } + } +} + func TestDNAncestor(t *testing.T) { testcases := []struct { A string