1// File contains DN parsing functionality
2//
3// https://tools.ietf.org/html/rfc4514
4//
5//   distinguishedName = [ relativeDistinguishedName
6//         *( COMMA relativeDistinguishedName ) ]
7//     relativeDistinguishedName = attributeTypeAndValue
8//         *( PLUS attributeTypeAndValue )
9//     attributeTypeAndValue = attributeType EQUALS attributeValue
10//     attributeType = descr / numericoid
11//     attributeValue = string / hexstring
12//
13//     ; The following characters are to be escaped when they appear
14//     ; in the value to be encoded: ESC, one of <escaped>, leading
15//     ; SHARP or SPACE, trailing SPACE, and NULL.
16//     string =   [ ( leadchar / pair ) [ *( stringchar / pair )
17//        ( trailchar / pair ) ] ]
18//
19//     leadchar = LUTF1 / UTFMB
20//     LUTF1 = %x01-1F / %x21 / %x24-2A / %x2D-3A /
21//        %x3D / %x3F-5B / %x5D-7F
22//
23//     trailchar  = TUTF1 / UTFMB
24//     TUTF1 = %x01-1F / %x21 / %x23-2A / %x2D-3A /
25//        %x3D / %x3F-5B / %x5D-7F
26//
27//     stringchar = SUTF1 / UTFMB
28//     SUTF1 = %x01-21 / %x23-2A / %x2D-3A /
29//        %x3D / %x3F-5B / %x5D-7F
30//
31//     pair = ESC ( ESC / special / hexpair )
32//     special = escaped / SPACE / SHARP / EQUALS
33//     escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE
34//     hexstring = SHARP 1*hexpair
35//     hexpair = HEX HEX
36//
37//  where the productions <descr>, <numericoid>, <COMMA>, <DQUOTE>,
38//  <EQUALS>, <ESC>, <HEX>, <LANGLE>, <NULL>, <PLUS>, <RANGLE>, <SEMI>,
39//  <SPACE>, <SHARP>, and <UTFMB> are defined in [RFC4512].
40//
41
42package ldap
43
44import (
45	"bytes"
46	enchex "encoding/hex"
47	"errors"
48	"fmt"
49	"strings"
50
51	"github.com/go-asn1-ber/asn1-ber"
52)
53
54// AttributeTypeAndValue represents an attributeTypeAndValue from https://tools.ietf.org/html/rfc4514
55type AttributeTypeAndValue struct {
56	// Type is the attribute type
57	Type string
58	// Value is the attribute value
59	Value string
60}
61
62// RelativeDN represents a relativeDistinguishedName from https://tools.ietf.org/html/rfc4514
63type RelativeDN struct {
64	Attributes []*AttributeTypeAndValue
65}
66
67// DN represents a distinguishedName from https://tools.ietf.org/html/rfc4514
68type DN struct {
69	RDNs []*RelativeDN
70}
71
72// ParseDN returns a distinguishedName or an error
73func ParseDN(str string) (*DN, error) {
74	dn := new(DN)
75	dn.RDNs = make([]*RelativeDN, 0)
76	rdn := new(RelativeDN)
77	rdn.Attributes = make([]*AttributeTypeAndValue, 0)
78	buffer := bytes.Buffer{}
79	attribute := new(AttributeTypeAndValue)
80	escaping := false
81
82	unescapedTrailingSpaces := 0
83	stringFromBuffer := func() string {
84		s := buffer.String()
85		s = s[0 : len(s)-unescapedTrailingSpaces]
86		buffer.Reset()
87		unescapedTrailingSpaces = 0
88		return s
89	}
90
91	for i := 0; i < len(str); i++ {
92		char := str[i]
93		switch {
94		case escaping:
95			unescapedTrailingSpaces = 0
96			escaping = false
97			switch char {
98			case ' ', '"', '#', '+', ',', ';', '<', '=', '>', '\\':
99				buffer.WriteByte(char)
100				continue
101			}
102			// Not a special character, assume hex encoded octet
103			if len(str) == i+1 {
104				return nil, errors.New("got corrupted escaped character")
105			}
106
107			dst := []byte{0}
108			n, err := enchex.Decode([]byte(dst), []byte(str[i:i+2]))
109			if err != nil {
110				return nil, fmt.Errorf("failed to decode escaped character: %s", err)
111			} else if n != 1 {
112				return nil, fmt.Errorf("expected 1 byte when un-escaping, got %d", n)
113			}
114			buffer.WriteByte(dst[0])
115			i++
116		case char == '\\':
117			unescapedTrailingSpaces = 0
118			escaping = true
119		case char == '=':
120			attribute.Type = stringFromBuffer()
121			// Special case: If the first character in the value is # the
122			// following data is BER encoded so we can just fast forward
123			// and decode.
124			if len(str) > i+1 && str[i+1] == '#' {
125				i += 2
126				index := strings.IndexAny(str[i:], ",+")
127				data := str
128				if index > 0 {
129					data = str[i : i+index]
130				} else {
131					data = str[i:]
132				}
133				rawBER, err := enchex.DecodeString(data)
134				if err != nil {
135					return nil, fmt.Errorf("failed to decode BER encoding: %s", err)
136				}
137				packet, err := ber.DecodePacketErr(rawBER)
138				if err != nil {
139					return nil, fmt.Errorf("failed to decode BER packet: %s", err)
140				}
141				buffer.WriteString(packet.Data.String())
142				i += len(data) - 1
143			}
144		case char == ',' || char == '+':
145			// We're done with this RDN or value, push it
146			if len(attribute.Type) == 0 {
147				return nil, errors.New("incomplete type, value pair")
148			}
149			attribute.Value = stringFromBuffer()
150			rdn.Attributes = append(rdn.Attributes, attribute)
151			attribute = new(AttributeTypeAndValue)
152			if char == ',' {
153				dn.RDNs = append(dn.RDNs, rdn)
154				rdn = new(RelativeDN)
155				rdn.Attributes = make([]*AttributeTypeAndValue, 0)
156			}
157		case char == ' ' && buffer.Len() == 0:
158			// ignore unescaped leading spaces
159			continue
160		default:
161			if char == ' ' {
162				// Track unescaped spaces in case they are trailing and we need to remove them
163				unescapedTrailingSpaces++
164			} else {
165				// Reset if we see a non-space char
166				unescapedTrailingSpaces = 0
167			}
168			buffer.WriteByte(char)
169		}
170	}
171	if buffer.Len() > 0 {
172		if len(attribute.Type) == 0 {
173			return nil, errors.New("DN ended with incomplete type, value pair")
174		}
175		attribute.Value = stringFromBuffer()
176		rdn.Attributes = append(rdn.Attributes, attribute)
177		dn.RDNs = append(dn.RDNs, rdn)
178	}
179	return dn, nil
180}
181
182// Equal returns true if the DNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
183// Returns true if they have the same number of relative distinguished names
184// and corresponding relative distinguished names (by position) are the same.
185func (d *DN) Equal(other *DN) bool {
186	if len(d.RDNs) != len(other.RDNs) {
187		return false
188	}
189	for i := range d.RDNs {
190		if !d.RDNs[i].Equal(other.RDNs[i]) {
191			return false
192		}
193	}
194	return true
195}
196
197// AncestorOf returns true if the other DN consists of at least one RDN followed by all the RDNs of the current DN.
198// "ou=widgets,o=acme.com" is an ancestor of "ou=sprockets,ou=widgets,o=acme.com"
199// "ou=widgets,o=acme.com" is not an ancestor of "ou=sprockets,ou=widgets,o=foo.com"
200// "ou=widgets,o=acme.com" is not an ancestor of "ou=widgets,o=acme.com"
201func (d *DN) AncestorOf(other *DN) bool {
202	if len(d.RDNs) >= len(other.RDNs) {
203		return false
204	}
205	// Take the last `len(d.RDNs)` RDNs from the other DN to compare against
206	otherRDNs := other.RDNs[len(other.RDNs)-len(d.RDNs):]
207	for i := range d.RDNs {
208		if !d.RDNs[i].Equal(otherRDNs[i]) {
209			return false
210		}
211	}
212	return true
213}
214
215// Equal returns true if the RelativeDNs are equal as defined by rfc4517 4.2.15 (distinguishedNameMatch).
216// Relative distinguished names are the same if and only if they have the same number of AttributeTypeAndValues
217// and each attribute of the first RDN is the same as the attribute of the second RDN with the same attribute type.
218// The order of attributes is not significant.
219// Case of attribute types is not significant.
220func (r *RelativeDN) Equal(other *RelativeDN) bool {
221	if len(r.Attributes) != len(other.Attributes) {
222		return false
223	}
224	return r.hasAllAttributes(other.Attributes) && other.hasAllAttributes(r.Attributes)
225}
226
227func (r *RelativeDN) hasAllAttributes(attrs []*AttributeTypeAndValue) bool {
228	for _, attr := range attrs {
229		found := false
230		for _, myattr := range r.Attributes {
231			if myattr.Equal(attr) {
232				found = true
233				break
234			}
235		}
236		if !found {
237			return false
238		}
239	}
240	return true
241}
242
243// Equal returns true if the AttributeTypeAndValue is equivalent to the specified AttributeTypeAndValue
244// Case of the attribute type is not significant
245func (a *AttributeTypeAndValue) Equal(other *AttributeTypeAndValue) bool {
246	return strings.EqualFold(a.Type, other.Type) && a.Value == other.Value
247}
248