~samwhited/xmpp

ref: f51a9463afb31948cddad60bcc571f7a102e7f1b xmpp/jid.go -rw-r--r-- 10.0 KiB
f51a9463Sam Whited Get deps from golang.org instead of Google Code 7 years ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// Copyright 2014 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package jid

import (
	"golang.org/x/net/idna"
	"golang.org/x/text/unicode/norm"

	"encoding/xml"
	"errors"
	"net"
	"strings"
	"unicode/utf8"
)

// Define some reusable error messages.
var (
	ErrorInvalidString   = errors.New("String is not valid UTF-8")
	ErrorEmptyPart       = errors.New("JID parts must be > 0 bytes")
	ErrorLongPart        = errors.New("JID parts must be < 1023 bytes")
	ErrorLongDomainLabel = errors.New("Domain names must be ≤ 63 chars per label")
	ErrorLongDomainName  = errors.New("Domain names must be ≤ 253 chars")
	ErrorLongDomainBytes = errors.New("Domain names must be ≤ 255 octets")
	ErrorInvalidJid      = errors.New("String is not a valid JID")
	ErrorIllegalRune     = errors.New("String contains an illegal chartacter")
	ErrorIllegalSpace    = errors.New("String contains illegal whitespace")
)

// NF is the Unicode normalization form to use. According to RFC 6122:
//
//      This profile specifies the use of Unicode Normalization Form KC, as
//      described in [STRINGPREP].
//
const NF norm.Form = norm.NFKC

// JID structs should not create one of these directly; instead, use the
// `NewJID()` function or the `jid.FromString(string)` method.
type JID struct {
	localpart    string
	domainpart   string
	resourcepart string
}

// NewJID creates a new JID from the given string.
func NewJID(s string) (JID, error) {
	j := JID{}
	err := j.FromString(s)
	return j, err
}

// Equals tests for JID equality by testing the three individual parts of a JID
// (localpart, domainpart, and resourcepart).
func (j *JID) Equals(jid2 JID) bool {
	domainpart, err := j.DomainPart()
	// Supressing an error, but if the domainpart errors it should never be equal.
	if err != nil {
		return false
	}
	domainpart2, err := jid2.DomainPart()
	if err != nil {
		return false
	}
	return (j.LocalPart() == jid2.LocalPart() && domainpart == domainpart2 && j.ResourcePart() == jid2.ResourcePart())
}

// LocalPart gets the localpart of a JID (eg "username").
func (j *JID) LocalPart() string {
	return j.localpart
}

// DomainPart gets the domainpart of a JID (eg. "example.net").
func (j *JID) DomainPart() (string, error) {
	return idna.ToUnicode(j.domainpart)
}

// ResourcePart gets the resourcepart of a JID (eg. "mobile").
func (j *JID) ResourcePart() string {
	return j.resourcepart
}

// NormalizeJIDPart verifies that the JID part is valid and returns a normalized
// string. You do NOT need to do this before passing parts to `NewJID()` or any
// of the `SetPart` methods; they handle validation and normalization for you.
// Eventually, this should be replaced with a proper stringprep implementation.
func NormalizeJIDPart(part string) (string, error) {
	switch normalized := NF.String(part); {
	case len(normalized) == 0:
		// The normalized length should be > 0 bytes
		return "", ErrorEmptyPart
	case len(normalized) > 1023:
		// The normalized length should be ≤ 1023 bytes
		return "", ErrorLongPart
	case !utf8.ValidString(part):
		// The original string should be valid UTF-8
		return "", ErrorInvalidString
	case strings.ContainsAny(part, "\"&'/:<>@"):
		// The original string should not contain any illegal characters. After
		// normalization some of these characters maybe present.
		return "", ErrorIllegalRune
	// TODO: Is there no function or method to just do this?
	case len(strings.Fields("'"+normalized+"'")) != 1:
		// There should be no whitespace in the normalized part.
		return "", ErrorIllegalSpace
		// TODO: Use a proper stringprep library to make sure this is all correct.
	default:
		return normalized, nil
	}
}

// NormalizeResourcePart verifies that the JID resource part is valid and
// returns a normalized string. You probably do NOT need to call this manually,
// as creating a JID handles this for you. Eventually, this should be replaced
// with a proper stringprep implementation.
func NormalizeResourcePart(part string) (string, error) {
	switch normalized := NF.String(part); {
	case len(normalized) == 0:
		// The normalized length should be > 0 bytes
		return "", ErrorEmptyPart
	case len(normalized) > 1023:
		// The normalized length should be ≤ 1023 bytes
		return "", ErrorLongPart
	case !utf8.ValidString(part):
		// The original string should be valid UTF-8
		return "", ErrorInvalidString
	// TODO: Is there no function or method to just do this?
	case len(strings.Fields("'"+normalized+"'")) != 1:
		// There should be no whitespace in the normalized part.
		return "", ErrorIllegalSpace
		// TODO: Use a proper stringprep library to make sure this is all correct.
	default:
		return normalized, nil
	}
}

// SetLocalPart sets the localpart of a JID and verifies that it is a
// valid/normalized UTF-8 string which is greater than 0 bytes and less than
// 1023 bytes.
func (j *JID) SetLocalPart(localpart string) error {
	normalized, err := NormalizeJIDPart(localpart)
	if err != nil {
		return err
	}
	(*j).localpart = normalized
	return nil
}

// SetDomainPart sets the domainpart of a JID and verify that it is a
// valid/normalized UTF-8 string which is greater than 0 bytes and less than
// 1023 bytes.
func (j *JID) SetDomainPart(domainpart string) error {

	// From RFC 6122 §2.2 Domainpart:
	//
	//     If the domainpart includes a final character considered to be a label
	//     separator (dot) by [IDNA2003] or [DNS], this character MUST be stripped
	//     from the domainpart before the JID of which it is a part is used for
	//     the purpose of routing an XML stanza, comparing against another JID, or
	//     constructing an [XMPP‑URI]. In particular, the character MUST be
	//     stripped before any other canonicalization steps are taken, such as
	//     application of the [NAMEPREP] profile of [STRINGPREP] or completion of
	//     the ToASCII operation as described in [IDNA2003].
	//
	domainpart = strings.TrimRight(domainpart, ".")

	normalized, err := idna.ToASCII(domainpart)
	if err != nil {
		return err
	}
	// Remove brackets if they already exist so that we can validate IPv6
	// TODO: Check if brackets exist and don't allow them if this isn't a v6 j
	normalized = strings.TrimPrefix(normalized, "[")
	normalized = strings.TrimSuffix(normalized, "]")
	// If the domain is a valid IPv6 j without brackets (it's a valid IP and
	// does not fit in 4 bytes), wrap it in brackets.
	// TODO: This is not very future proof.
	if ip := net.ParseIP(normalized); ip != nil && ip.To4() == nil {
		normalized = "[" + normalized + "]"
	}
	j.domainpart = normalized
	return nil
}

// SetResourcePart sets the resourcepart of a JID and verifies that it is a
// valid/normalized UTF-8 string which is greater than 0 bytes and less than
// 1023 bytes.
func (j *JID) SetResourcePart(resourcepart string) error {
	normalized, err := NormalizeResourcePart(resourcepart)
	if err != nil {
		return err
	}
	j.resourcepart = normalized
	return nil
}

// String converts the full JID to a string.
func (j *JID) String() string {
	out, _ := j.DomainPart()
	if lp := j.LocalPart(); lp != "" {
		out = j.LocalPart() + "@" + out
	}
	if rp := j.ResourcePart(); rp != "" {
		out = out + "/" + rp
	}
	return out
}

// Bare returns the bare JID (no resourcepart) as a string.
func (j *JID) Bare() (string, error) {
	out, err := j.DomainPart()
	if lp := j.LocalPart(); lp != "" {
		out = lp + "@" + out
	}
	return out, err
}

// FromString sets the fields in an existing JID from a string.
func (j *JID) FromString(s string) error {

	// Make sure the string is valid UTF-8
	if !utf8.ValidString(s) {
		return ErrorInvalidString
	}

	// According to RFC 6122:
	//
	//     Implementation Note: When dividing a JID into its component parts, an
	//     implementation needs to match the separator characters '@' and '/'
	//     before applying any transformation algorithms, which might decompose
	//     certain Unicode code points to the separator characters (e.g., U+FE6B
	//     SMALL COMMERCIAL AT might decompose into U+0040 COMMERCIAL AT).
	//
	// So don't normalize until after we've checked the various parts.

	// Trim any whitespace before we begin.
	s = strings.TrimSpace(s)

	// Do not allow whitespace elsewhere in the string…
	if len(strings.Fields(s)) != 1 {
		return ErrorIllegalSpace
	}

	atCount := strings.Count(s, "@")
	slashCount := strings.Count(s, "/")
	atLoc := strings.IndexRune(s, '@')
	slashLoc := strings.IndexRune(s, '/')

	switch {
	case atCount == 0 && slashCount == 0:
		// domainpart only (eg. "example.net" or "example")
		err := j.SetDomainPart(s)
		if err != nil {
			return err
		}

	case atCount == 1 && slashCount == 0:
		// Bare JID ("test@example.net" or "test@example")
		if atLoc == 0 || atLoc == len(s)-1 {
			return ErrorEmptyPart
		}
		err := j.SetLocalPart(s[0:atLoc])
		if err != nil {
			return err
		}
		err = j.SetDomainPart(s[atLoc+1:])
		if err != nil {
			return err
		}

	case slashCount > 0 && (atCount == 0 || atLoc > slashLoc):
		// domainpart + resourcepart (eg. "example/rp" or "example/@/")
		if slashLoc == 0 || slashLoc == len(s)-1 {
			// Error if JID is of the form "/jid" or "jid/" ("jid//" is okay)
			return ErrorEmptyPart
		}
		err := j.SetDomainPart(s[0:slashLoc])
		if err != nil {
			return err
		}
		err = j.SetResourcePart(s[slashLoc+1:])
		if err != nil {
			return err
		}

	case slashCount > 0 && atCount > 0 && atLoc < slashLoc:
		// Full JID (eg. "test@example.net/resourcepart" or "test@example.net/@/")
		last := len(s) - 1
		if atLoc == 0 || slashLoc == 0 || atLoc == last || slashLoc == last || slashLoc == atLoc+1 {
			return ErrorEmptyPart
		}
		err := j.SetLocalPart(s[0:atLoc])
		if err != nil {
			return err
		}
		err = j.SetDomainPart(s[atLoc+1 : slashLoc])
		if err != nil {
			return err
		}
		err = j.SetResourcePart(s[slashLoc+1:])
		if err != nil {
			return err
		}

	default: // Too many '@' or '/' symbols
		return ErrorIllegalRune
	}

	return nil
}

// MarshalXMLAttr marshals the JID as an XML attriute for use with the
// `encoding/xml' package.
func (j *JID) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
	return xml.Attr{Name: name, Value: j.String()}, nil
}