~samwhited/xmpp

ref: 5c686e599f8b024a0189eaa1865b5bb1fb8f5fa9 xmpp/jid.go -rw-r--r-- 8.5 KiB
5c686e59Sam Whited Rename the license file (use markdown) 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
// 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

// TODO: Use a proper stringprep library like "code.google.com/p/go-idn/idna"
import (
	"code.google.com/p/go.net/idna"
	"code.google.com/p/go.text/unicode/norm"

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

// Define some reusable error messages.
const (
	ERROR_INVALID_STRING = "String is not valid UTF-8"
	ERROR_EMPTY_PART     = "JID parts must be greater than 0 bytes"
	ERROR_LONG_PART      = "JID parts must be less than 1023 bytes"
	ERROR_INVALID_JID    = "String is not a valid JID"
	ERROR_ILLEGAL_RUNE   = "String contains an illegal chartacter"
	ERROR_ILLEGAL_SPACE  = "String contains illegal whitespace"
)

// 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

// A struct representing a JID. You 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
}

// Create a new JID from the given string. Returns a struct of type `jid` with
// three fields (all strings): `localpart`, `domainpart`, and `resourcepart`.
func NewJID(s string) (JID, error) {
	j := JID{}
	err := j.FromString(s)
	return j, err
}

// Tests for JID equality by testing the individual parts.
func (jid *JID) Equals(jid2 JID) bool {
	domainpart, err := jid.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 (jid.LocalPart() == jid2.LocalPart() && domainpart == domainpart2 && jid.ResourcePart() == jid2.ResourcePart())
}

// Get the local part of a JID
func (address *JID) LocalPart() string {
	return address.localpart
}

// Get the domainpart of a JID
func (address *JID) DomainPart() (string, error) {
	return idna.ToUnicode(address.domainpart)
}

// Get the resourcepart of a JID
func (address *JID) ResourcePart() string {
	return address.resourcepart
}

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

// Set the localpart 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 (address *JID) SetLocalPart(localpart string) error {
	normalized, err := NormalizeJIDPart(localpart)
	if err != nil {
		return err
	}
	(*address).localpart = normalized
	return nil
}

// Set 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 (address *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 address
	normalized = strings.TrimPrefix(normalized, "[")
	normalized = strings.TrimSuffix(normalized, "]")
	// If the domain is a valid IPv6 address 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 + "]"
	}
	address.domainpart = normalized
	return nil
}

// Set the resourcepart 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 (address *JID) SetResourcePart(resourcepart string) error {
	normalized, err := NormalizeJIDPart(resourcepart)
	if err != nil {
		return err
	}
	address.resourcepart = normalized
	return nil
}

// Return the full JID as a string
func (address *JID) String() (string, error) {
	out, err := address.DomainPart()
	if lp := address.LocalPart(); lp != "" {
		out = address.LocalPart() + "@" + out
	}
	if rp := address.ResourcePart(); rp != "" {
		out = out + "/" + rp
	}
	return out, err
}

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

// Set the existing JID from a string.
func (address *JID) FromString(s string) error {

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

	// 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 errors.New(ERROR_ILLEGAL_SPACE)
	}

	atCount := strings.Count(s, "@")
	slashCount := strings.Count(s, "/")

	switch {
	case atCount == 0 && slashCount == 0: // domainpart only
		err := address.SetDomainPart(s)
		if err != nil {
			return err
		}

	case atCount == 1 && slashCount == 0: // Bare JID
		atLoc := strings.IndexRune(s, '@')
		if atLoc == 0 || atLoc == len(s)-1 {
			return errors.New(ERROR_EMPTY_PART)
		}
		err := address.SetLocalPart(s[0:atLoc])
		if err != nil {
			return err
		}
		err = address.SetDomainPart(s[atLoc+1:])
		if err != nil {
			return err
		}

	case atCount == 0 && slashCount == 1: // domainpart + resourcepart
		slashLoc := strings.IndexRune(s, '/')
		if slashLoc == 0 || slashLoc == len(s)-1 {
			return errors.New(ERROR_EMPTY_PART)
		}
		err := address.SetDomainPart(s[0:slashLoc])
		if err != nil {
			return err
		}
		err = address.SetResourcePart(s[slashLoc+1:])
		if err != nil {
			return err
		}

	case atCount == 1 && slashCount == 1: // Full JID
		atLoc := strings.IndexRune(s, '@')
		slashLoc := strings.IndexRune(s, '/')
		if slashLoc < atLoc {
			return errors.New(ERROR_INVALID_JID)
		}
		last := len(s) - 1
		if atLoc == 0 || slashLoc == 0 || atLoc == last || slashLoc == last || slashLoc == atLoc+1 {
			return errors.New(ERROR_EMPTY_PART)
		}
		err := address.SetLocalPart(s[0:atLoc])
		if err != nil {
			return err
		}
		err = address.SetDomainPart(s[atLoc+1 : slashLoc])
		if err != nil {
			return err
		}
		err = address.SetResourcePart(s[slashLoc+1:])
		if err != nil {
			return err
		}

	default: // Too many '@' or '/' symbols
		return errors.New(ERROR_ILLEGAL_RUNE)
	}

	return nil
}