|
@@ -9,6 +9,7 @@ import (
|
9
|
9
|
"bytes"
|
10
|
10
|
"errors"
|
11
|
11
|
"strings"
|
|
12
|
+ "unicode/utf8"
|
12
|
13
|
)
|
13
|
14
|
|
14
|
15
|
const (
|
|
@@ -34,17 +35,30 @@ const (
|
34
|
35
|
var (
|
35
|
36
|
// ErrorLineIsEmpty indicates that the given IRC line was empty.
|
36
|
37
|
ErrorLineIsEmpty = errors.New("Line is empty")
|
|
38
|
+
|
37
|
39
|
// ErrorLineContainsBadChar indicates that the line contained invalid characters
|
38
|
40
|
ErrorLineContainsBadChar = errors.New("Line contains invalid characters")
|
39
|
|
- // ErrorLineTooLong indicates that the message exceeded the maximum tag length
|
40
|
|
- // (the name references 417 ERR_INPUTTOOLONG; we reserve the right to return it
|
41
|
|
- // for messages that exceed the non-tag length limit)
|
42
|
|
- ErrorLineTooLong = errors.New("Line could not be parsed because a specified length limit was exceeded")
|
|
41
|
+
|
|
42
|
+ // ErrorBodyTooLong indicates that the message body exceeded the specified
|
|
43
|
+ // length limit (typically 512 bytes). This error is non-fatal; if encountered
|
|
44
|
+ // when parsing a message, the message is parsed up to the length limit, and
|
|
45
|
+ // if encountered when serializing a message, the message is truncated to the limit.
|
|
46
|
+ ErrorBodyTooLong = errors.New("Line body exceeded the specified length limit; outgoing messages will be truncated")
|
|
47
|
+
|
|
48
|
+ // ErrorTagsTooLong indicates that the message exceeded the maximum tag length
|
|
49
|
+ // (the specified response on the server side is 417 ERR_INPUTTOOLONG).
|
|
50
|
+ ErrorTagsTooLong = errors.New("Line could not be processed because its tag data exceeded the length limit")
|
|
51
|
+
|
43
|
52
|
// ErrorInvalidTagContent indicates that a tag name or value was invalid
|
44
|
53
|
ErrorInvalidTagContent = errors.New("Line could not be processed because it contained an invalid tag name or value")
|
45
|
54
|
|
|
55
|
+ // ErrorCommandMissing indicates that an IRC message was invalid because it lacked a command.
|
46
|
56
|
ErrorCommandMissing = errors.New("IRC messages MUST have a command")
|
47
|
|
- ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
|
|
57
|
+
|
|
58
|
+ // ErrorBadParam indicates that an IRC message could not be serialized because
|
|
59
|
+ // its parameters violated the syntactic constraints on IRC parameters:
|
|
60
|
+ // non-final parameters cannot be empty, contain a space, or start with `:`.
|
|
61
|
+ ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
|
48
|
62
|
)
|
49
|
63
|
|
50
|
64
|
// IRCMessage represents an IRC message, as defined by the RFCs and as
|
|
@@ -161,7 +175,7 @@ func ParseLineStrict(line string, fromClient bool, truncateLen int) (ircmsg IRCM
|
161
|
175
|
// slice off any amount of ' ' from the front of the string
|
162
|
176
|
func trimInitialSpaces(str string) string {
|
163
|
177
|
var i int
|
164
|
|
- for i = 0; i < len(str) && str[i] == ' '; i += 1 {
|
|
178
|
+ for i = 0; i < len(str) && str[i] == ' '; i++ {
|
165
|
179
|
}
|
166
|
180
|
return str[i:]
|
167
|
181
|
}
|
|
@@ -170,6 +184,14 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
|
170
|
184
|
// remove either \n or \r\n from the end of the line:
|
171
|
185
|
line = strings.TrimSuffix(line, "\n")
|
172
|
186
|
line = strings.TrimSuffix(line, "\r")
|
|
187
|
+ // whether we removed them ourselves, or whether they were removed previously,
|
|
188
|
+ // they count against the line limit:
|
|
189
|
+ if truncateLen != 0 {
|
|
190
|
+ if truncateLen <= 2 {
|
|
191
|
+ return ircmsg, ErrorLineIsEmpty
|
|
192
|
+ }
|
|
193
|
+ truncateLen -= 2
|
|
194
|
+ }
|
173
|
195
|
// now validate for the 3 forbidden bytes:
|
174
|
196
|
if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 {
|
175
|
197
|
return ircmsg, ErrorLineContainsBadChar
|
|
@@ -187,7 +209,7 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
|
187
|
209
|
}
|
188
|
210
|
tags := line[1:tagEnd]
|
189
|
211
|
if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
|
190
|
|
- return ircmsg, ErrorLineTooLong
|
|
212
|
+ return ircmsg, ErrorTagsTooLong
|
191
|
213
|
}
|
192
|
214
|
err = ircmsg.parseTags(tags)
|
193
|
215
|
if err != nil {
|
|
@@ -198,7 +220,8 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
|
198
|
220
|
}
|
199
|
221
|
|
200
|
222
|
// truncate if desired
|
201
|
|
- if 0 < truncateLen && truncateLen < len(line) {
|
|
223
|
+ if truncateLen != 0 && truncateLen < len(line) {
|
|
224
|
+ err = ErrorBodyTooLong
|
202
|
225
|
line = line[:truncateLen]
|
203
|
226
|
}
|
204
|
227
|
|
|
@@ -252,7 +275,7 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
|
252
|
275
|
line = line[paramEnd+1:]
|
253
|
276
|
}
|
254
|
277
|
|
255
|
|
- return ircmsg, nil
|
|
278
|
+ return ircmsg, err
|
256
|
279
|
}
|
257
|
280
|
|
258
|
281
|
// helper to parse tags
|
|
@@ -337,8 +360,8 @@ func paramRequiresTrailing(param string) bool {
|
337
|
360
|
}
|
338
|
361
|
|
339
|
362
|
// line returns a sendable line created from an IRCMessage.
|
340
|
|
-func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) ([]byte, error) {
|
341
|
|
- if len(ircmsg.Command) < 1 {
|
|
363
|
+func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) (result []byte, err error) {
|
|
364
|
+ if len(ircmsg.Command) == 0 {
|
342
|
365
|
return nil, ErrorCommandMissing
|
343
|
366
|
}
|
344
|
367
|
|
|
@@ -382,10 +405,10 @@ func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagD
|
382
|
405
|
lenTags = buf.Len()
|
383
|
406
|
|
384
|
407
|
if 0 < tagLimit && tagLimit < buf.Len() {
|
385
|
|
- return nil, ErrorLineTooLong
|
|
408
|
+ return nil, ErrorTagsTooLong
|
386
|
409
|
}
|
387
|
410
|
if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
|
388
|
|
- return nil, ErrorLineTooLong
|
|
411
|
+ return nil, ErrorTagsTooLong
|
389
|
412
|
}
|
390
|
413
|
|
391
|
414
|
if len(ircmsg.Prefix) > 0 {
|
|
@@ -408,18 +431,33 @@ func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagD
|
408
|
431
|
buf.WriteString(param)
|
409
|
432
|
}
|
410
|
433
|
|
411
|
|
- // truncate if desired
|
412
|
|
- // -2 for \r\n
|
413
|
|
- restLen := buf.Len() - lenTags
|
414
|
|
- if 0 < truncateLen && (truncateLen-2) < restLen {
|
415
|
|
- buf.Truncate(lenTags + (truncateLen - 2))
|
|
434
|
+ // truncate if desired; leave 2 bytes over for \r\n:
|
|
435
|
+ if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) {
|
|
436
|
+ err = ErrorBodyTooLong
|
|
437
|
+ newBufLen := lenTags + (truncateLen - 2)
|
|
438
|
+ buf.Truncate(newBufLen)
|
|
439
|
+ // XXX: we may have truncated in the middle of a UTF8-encoded codepoint;
|
|
440
|
+ // if so, remove additional bytes, stopping when the sequence either
|
|
441
|
+ // ends in a valid codepoint, or we have removed 3 bytes (the maximum
|
|
442
|
+ // length of the remnant of a once-valid, truncated codepoint; we don't
|
|
443
|
+ // want to truncate the entire message if it wasn't UTF8 in the first
|
|
444
|
+ // place).
|
|
445
|
+ for i := 0; i < (utf8.UTFMax - 1); i++ {
|
|
446
|
+ r, n := utf8.DecodeLastRune(buf.Bytes())
|
|
447
|
+ if r == utf8.RuneError && n <= 1 {
|
|
448
|
+ newBufLen--
|
|
449
|
+ buf.Truncate(newBufLen)
|
|
450
|
+ } else {
|
|
451
|
+ break
|
|
452
|
+ }
|
|
453
|
+ }
|
416
|
454
|
}
|
417
|
455
|
buf.WriteString("\r\n")
|
418
|
456
|
|
419
|
|
- result := buf.Bytes()
|
|
457
|
+ result = buf.Bytes()
|
420
|
458
|
toValidate := result[:len(result)-2]
|
421
|
459
|
if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 {
|
422
|
460
|
return nil, ErrorLineContainsBadChar
|
423
|
461
|
}
|
424
|
|
- return result, nil
|
|
462
|
+ return result, err
|
425
|
463
|
}
|