expose message truncation in the API

ErrorInputTooLong has been split into ErrorTagsTooLong (fatal)
and ErrorBodyTooLong (non-fatal, returns a truncated message).
This commit is contained in:
Shivaram Lingamneni 2021-03-03 01:02:36 -05:00
parent 61e3317dd1
commit 617723503e
2 changed files with 81 additions and 49 deletions

@ -35,16 +35,29 @@ const (
var ( var (
// ErrorLineIsEmpty indicates that the given IRC line was empty. // ErrorLineIsEmpty indicates that the given IRC line was empty.
ErrorLineIsEmpty = errors.New("Line is empty") ErrorLineIsEmpty = errors.New("Line is empty")
// ErrorLineContainsBadChar indicates that the line contained invalid characters // ErrorLineContainsBadChar indicates that the line contained invalid characters
ErrorLineContainsBadChar = errors.New("Line contains invalid characters") ErrorLineContainsBadChar = errors.New("Line contains invalid characters")
// ErrorLineTooLong indicates that the message exceeded the maximum tag length
// (the name references 417 ERR_INPUTTOOLONG; we reserve the right to return it // ErrorBodyTooLong indicates that the message body exceeded the specified
// for messages that exceed the non-tag length limit) // length limit (typically 512 bytes). This error is non-fatal; if encountered
ErrorLineTooLong = errors.New("Line could not be parsed because a specified length limit was exceeded") // when parsing a message, the message is parsed up to the length limit, and
// if encountered when serializing a message, the message is truncated to the limit.
ErrorBodyTooLong = errors.New("Line could not be processed because its body exceeded the length limit")
// ErrorTagsTooLong indicates that the message exceeded the maximum tag length
// (the specified response on the server side is 417 ERR_INPUTTOOLONG).
ErrorTagsTooLong = errors.New("Line could not be processed because its tag data exceeded the length limit")
// ErrorInvalidTagContent indicates that a tag name or value was invalid // ErrorInvalidTagContent indicates that a tag name or value was invalid
ErrorInvalidTagContent = errors.New("Line could not be processed because it contained an invalid tag name or value") ErrorInvalidTagContent = errors.New("Line could not be processed because it contained an invalid tag name or value")
// ErrorCommandMissing indicates that an IRC message was invalid because it lacked a command.
ErrorCommandMissing = errors.New("IRC messages MUST have a command") ErrorCommandMissing = errors.New("IRC messages MUST have a command")
// ErrorBadParam indicates that an IRC message could not be serialized because
// its parameters violated the syntactic constraints on IRC parameters:
// non-final parameters cannot be empty, contain a space, or start with `:`.
ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter") ErrorBadParam = errors.New("Cannot have an empty param, a param with spaces, or a param that starts with ':' before the last parameter")
) )
@ -171,6 +184,14 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
// remove either \n or \r\n from the end of the line: // remove either \n or \r\n from the end of the line:
line = strings.TrimSuffix(line, "\n") line = strings.TrimSuffix(line, "\n")
line = strings.TrimSuffix(line, "\r") line = strings.TrimSuffix(line, "\r")
// whether we removed them ourselves, or whether they were removed previously,
// they count against the line limit:
if truncateLen != 0 {
if truncateLen <= 2 {
return ircmsg, ErrorLineIsEmpty
}
truncateLen -= 2
}
// now validate for the 3 forbidden bytes: // now validate for the 3 forbidden bytes:
if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 { if strings.IndexByte(line, '\x00') != -1 || strings.IndexByte(line, '\n') != -1 || strings.IndexByte(line, '\r') != -1 {
return ircmsg, ErrorLineContainsBadChar return ircmsg, ErrorLineContainsBadChar
@ -188,7 +209,7 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
} }
tags := line[1:tagEnd] tags := line[1:tagEnd]
if 0 < maxTagDataLength && maxTagDataLength < len(tags) { if 0 < maxTagDataLength && maxTagDataLength < len(tags) {
return ircmsg, ErrorLineTooLong return ircmsg, ErrorTagsTooLong
} }
err = ircmsg.parseTags(tags) err = ircmsg.parseTags(tags)
if err != nil { if err != nil {
@ -200,6 +221,7 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
// truncate if desired // truncate if desired
if 0 < truncateLen && truncateLen < len(line) { if 0 < truncateLen && truncateLen < len(line) {
err = ErrorBodyTooLong
line = line[:truncateLen] line = line[:truncateLen]
} }
@ -253,7 +275,7 @@ func parseLine(line string, maxTagDataLength int, truncateLen int) (ircmsg IRCMe
line = line[paramEnd+1:] line = line[paramEnd+1:]
} }
return ircmsg, nil return ircmsg, err
} }
// helper to parse tags // helper to parse tags
@ -338,8 +360,8 @@ func paramRequiresTrailing(param string) bool {
} }
// line returns a sendable line created from an IRCMessage. // line returns a sendable line created from an IRCMessage.
func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) ([]byte, error) { func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagDataLimit, truncateLen int) (result []byte, err error) {
if len(ircmsg.Command) < 1 { if len(ircmsg.Command) == 0 {
return nil, ErrorCommandMissing return nil, ErrorCommandMissing
} }
@ -383,10 +405,10 @@ func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagD
lenTags = buf.Len() lenTags = buf.Len()
if 0 < tagLimit && tagLimit < buf.Len() { if 0 < tagLimit && tagLimit < buf.Len() {
return nil, ErrorLineTooLong return nil, ErrorTagsTooLong
} }
if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) { if (0 < clientOnlyTagDataLimit && clientOnlyTagDataLimit < lenClientOnlyTags) || (0 < serverAddedTagDataLimit && serverAddedTagDataLimit < lenRegularTags) {
return nil, ErrorLineTooLong return nil, ErrorTagsTooLong
} }
if len(ircmsg.Prefix) > 0 { if len(ircmsg.Prefix) > 0 {
@ -411,6 +433,7 @@ func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagD
// truncate if desired; leave 2 bytes over for \r\n: // truncate if desired; leave 2 bytes over for \r\n:
if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) { if truncateLen != 0 && (truncateLen-2) < (buf.Len()-lenTags) {
err = ErrorBodyTooLong
newBufLen := lenTags + (truncateLen - 2) newBufLen := lenTags + (truncateLen - 2)
buf.Truncate(newBufLen) buf.Truncate(newBufLen)
// XXX: we may have truncated in the middle of a UTF8-encoded codepoint; // XXX: we may have truncated in the middle of a UTF8-encoded codepoint;
@ -431,10 +454,10 @@ func (ircmsg *IRCMessage) line(tagLimit, clientOnlyTagDataLimit, serverAddedTagD
} }
buf.WriteString("\r\n") buf.WriteString("\r\n")
result := buf.Bytes() result = buf.Bytes()
toValidate := result[:len(result)-2] toValidate := result[:len(result)-2]
if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 { if bytes.IndexByte(toValidate, '\x00') != -1 || bytes.IndexByte(toValidate, '\r') != -1 || bytes.IndexByte(toValidate, '\n') != -1 {
return nil, ErrorLineContainsBadChar return nil, ErrorLineContainsBadChar
} }
return result, nil return result, err
} }

@ -17,27 +17,28 @@ type testcodewithlen struct {
raw string raw string
length int length int
message IRCMessage message IRCMessage
truncateExpected bool
} }
var decodelentests = []testcodewithlen{ var decodelentests = []testcodewithlen{
{":dan-!d@localhost PRIVMSG dan #test :What a cool message\r\n", 20, {":dan-!d@localhost PRIVMSG dan #test :What a cool message\r\n", 22,
MakeMessage(nil, "dan-!d@localhost", "PR")}, MakeMessage(nil, "dan-!d@localhost", "PR"), true},
{"@time=12732;re TEST *\r\n", 512, {"@time=12732;re TEST *\r\n", 512,
MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*")}, MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*"), false},
{"@time=12732;re TEST *\r\n", 512, {"@time=12732;re TEST *\r\n", 512,
MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*")}, MakeMessage(map[string]string{"time": "12732", "re": ""}, "", "TEST", "*"), false},
{":dan- TESTMSG\r\n", 2048, {":dan- TESTMSG\r\n", 2048,
MakeMessage(nil, "dan-", "TESTMSG")}, MakeMessage(nil, "dan-", "TESTMSG"), false},
{":dan- TESTMSG dan \r\n", 12, {":dan- TESTMSG dan \r\n", 14,
MakeMessage(nil, "dan-", "TESTMS")}, MakeMessage(nil, "dan-", "TESTMS"), true},
{"TESTMSG\r\n", 6, {"TESTMSG\r\n", 6,
MakeMessage(nil, "", "TESTMS")}, MakeMessage(nil, "", "TEST"), true},
{"TESTMSG\r\n", 7, {"TESTMSG\r\n", 7,
MakeMessage(nil, "", "TESTMSG")}, MakeMessage(nil, "", "TESTM"), true},
{"TESTMSG\r\n", 8, {"TESTMSG\r\n", 8,
MakeMessage(nil, "", "TESTMSG")}, MakeMessage(nil, "", "TESTMS"), true},
{"TESTMSG\r\n", 9, {"TESTMSG\r\n", 9,
MakeMessage(nil, "", "TESTMSG")}, MakeMessage(nil, "", "TESTMSG"), false},
} }
// map[string]string{"time": "12732", "re": ""} // map[string]string{"time": "12732", "re": ""}
@ -103,15 +104,22 @@ var decodetesterrors = []testparseerror{
{"privmsg #channel :command injection attempt \r:Nickserv PRIVMSG user :Please re-enter your password", ErrorLineContainsBadChar}, {"privmsg #channel :command injection attempt \r:Nickserv PRIVMSG user :Please re-enter your password", ErrorLineContainsBadChar},
} }
func validateTruncateError(pair testcodewithlen, err error, t *testing.T) {
if pair.truncateExpected {
if err != ErrorBodyTooLong {
t.Error("For", pair.raw, "expected truncation, but got error", err)
}
} else {
if err != nil {
t.Error("For", pair.raw, "expected no error, but got", err)
}
}
}
func TestDecode(t *testing.T) { func TestDecode(t *testing.T) {
for _, pair := range decodelentests { for _, pair := range decodelentests {
ircmsg, err := ParseLineStrict(pair.raw, true, pair.length) ircmsg, err := ParseLineStrict(pair.raw, true, pair.length)
if err != nil { validateTruncateError(pair, err, t)
t.Error(
"For", pair.raw,
"Failed to parse line:", err,
)
}
if !reflect.DeepEqual(ircmsg, pair.message) { if !reflect.DeepEqual(ircmsg, pair.message) {
t.Error( t.Error(
@ -162,11 +170,11 @@ var encodetests = []testcode{
} }
var encodelentests = []testcodewithlen{ var encodelentests = []testcodewithlen{
{":dan-!d@lo\r\n", 12, {":dan-!d@lo\r\n", 12,
MakeMessage(nil, "dan-!d@localhost", "PRIVMSG", "dan", "#test", "What a cool message")}, MakeMessage(nil, "dan-!d@localhost", "PRIVMSG", "dan", "#test", "What a cool message"), true},
{"@time=12732 TEST *\r\n", 52, {"@time=12732 TEST *\r\n", 52,
MakeMessage(map[string]string{"time": "12732"}, "", "TEST", "*")}, MakeMessage(map[string]string{"time": "12732"}, "", "TEST", "*"), false},
{"@riohwihowihirgowihre TEST *\r\n", 8, {"@riohwihowihirgowihre TEST *\r\n", 8,
MakeMessage(map[string]string{"riohwihowihirgowihre": ""}, "", "TEST", "*", "*")}, MakeMessage(map[string]string{"riohwihowihirgowihre": ""}, "", "TEST", "*", "*"), true},
} }
func TestEncode(t *testing.T) { func TestEncode(t *testing.T) {
@ -206,12 +214,7 @@ func TestEncode(t *testing.T) {
} }
for _, pair := range encodelentests { for _, pair := range encodelentests {
line, err := pair.message.LineBytesStrict(true, pair.length) line, err := pair.message.LineBytesStrict(true, pair.length)
if err != nil { validateTruncateError(pair, err, t)
t.Error(
"For", pair.raw,
"Failed to parse line:", err,
)
}
if string(line) != pair.raw { if string(line) != pair.raw {
t.Error( t.Error(
@ -376,7 +379,7 @@ func TestErrorLineTooLongGeneration(t *testing.T) {
message.SetTag(fmt.Sprintf("+client-tag-%d", i), "ok") message.SetTag(fmt.Sprintf("+client-tag-%d", i), "ok")
} }
line, err = message.LineBytesStrict(true, 0) line, err = message.LineBytesStrict(true, 0)
if err != ErrorLineTooLong { if err != ErrorTagsTooLong {
t.Error(err) t.Error(err)
} }
@ -385,7 +388,7 @@ func TestErrorLineTooLongGeneration(t *testing.T) {
message.SetTag(fmt.Sprintf("server-tag-%d", i), "ok") message.SetTag(fmt.Sprintf("server-tag-%d", i), "ok")
} }
line, err = message.LineBytesStrict(true, 0) line, err = message.LineBytesStrict(true, 0)
if err != ErrorLineTooLong { if err != ErrorTagsTooLong {
t.Error(err) t.Error(err)
} }
@ -397,7 +400,7 @@ func TestErrorLineTooLongGeneration(t *testing.T) {
} }
// client cannot send this much tag data: // client cannot send this much tag data:
line, err = message.LineBytesStrict(true, 0) line, err = message.LineBytesStrict(true, 0)
if err != ErrorLineTooLong { if err != ErrorTagsTooLong {
t.Error(err) t.Error(err)
} }
// but a server can, since the tags are split between client and server budgets: // but a server can, since the tags are split between client and server budgets:
@ -459,13 +462,19 @@ func TestTruncate(t *testing.T) {
param := buildPingParam(initialLen, initialLen+i, s) param := buildPingParam(initialLen, initialLen+i, s)
msg := MakeMessage(nil, "", "PING", param) msg := MakeMessage(nil, "", "PING", param)
msgBytes, err := msg.LineBytesStrict(false, 512) msgBytes, err := msg.LineBytesStrict(false, 512)
msgBytesNonTrunc, _ := msg.LineBytes()
if len(msgBytes) == len(msgBytesNonTrunc) {
if err != nil { if err != nil {
panic(err) t.Error("message was not truncated, but got error", err)
}
} else {
if err != ErrorBodyTooLong {
t.Error("message was truncated, but got error", err)
}
} }
if len(msgBytes) > 512 { if len(msgBytes) > 512 {
t.Errorf("invalid serialized length %d", len(msgBytes)) t.Errorf("invalid serialized length %d", len(msgBytes))
} }
msgBytesNonTrunc, _ := msg.LineBytes()
if len(msgBytes) < min(512-3, len(msgBytesNonTrunc)) { if len(msgBytes) < min(512-3, len(msgBytesNonTrunc)) {
t.Errorf("invalid serialized length %d", len(msgBytes)) t.Errorf("invalid serialized length %d", len(msgBytes))
} }
@ -491,7 +500,7 @@ func TestTruncateNonUTF8(t *testing.T) {
param := buf.String() param := buf.String()
msg := MakeMessage(nil, "", "PING", param) msg := MakeMessage(nil, "", "PING", param)
msgBytes, err := msg.LineBytesStrict(false, 512) msgBytes, err := msg.LineBytesStrict(false, 512)
if err != nil { if !(err == nil || err == ErrorBodyTooLong) {
panic(err) panic(err)
} }
if len(msgBytes) > 512 { if len(msgBytes) > 512 {