Browse Source

deprecate message truncation

Implements #1577, but the issue should remain open until we clean up
the debugging loglines.
tags/v2.6.0-rc1
Shivaram Lingamneni 3 years ago
parent
commit
03185ea4a9

+ 7
- 0
default.yaml View File

@@ -235,6 +235,13 @@ server:
235 235
         # this works around that bug, allowing them to use SASL.
236 236
         send-unprefixed-sasl: true
237 237
 
238
+        # traditionally, IRC servers will truncate and send messages that are
239
+        # too long to be relayed intact. this behavior can be disabled by setting
240
+        # allow-truncation to false, in which case Oragono will reject the message
241
+        # and return an error to the client. (note that this option defaults to true
242
+        # when unset.)
243
+        allow-truncation: false
244
+
238 245
     # IP-based DoS protection
239 246
     ip-limits:
240 247
         # whether to limit the total number of concurrent connections per IP/CIDR

+ 2
- 2
go.mod View File

@@ -10,7 +10,7 @@ require (
10 10
 	github.com/go-sql-driver/mysql v1.5.0
11 11
 	github.com/go-test/deep v1.0.6 // indirect
12 12
 	github.com/gorilla/websocket v1.4.2
13
-	github.com/goshuirc/irc-go v0.0.0-20210301225436-2c4b83d64847
13
+	github.com/goshuirc/irc-go v0.0.0-20210304031553-cf78e9176f96
14 14
 	github.com/onsi/ginkgo v1.12.0 // indirect
15 15
 	github.com/onsi/gomega v1.9.0 // indirect
16 16
 	github.com/oragono/confusables v0.0.0-20201108231250-4ab98ab61fb1
@@ -24,4 +24,4 @@ require (
24 24
 	gopkg.in/yaml.v2 v2.3.0
25 25
 )
26 26
 
27
-replace github.com/gorilla/websocket  => github.com/oragono/websocket v1.4.2-oragono1
27
+replace github.com/gorilla/websocket => github.com/oragono/websocket v1.4.2-oragono1

+ 2
- 0
go.sum View File

@@ -40,6 +40,8 @@ github.com/goshuirc/irc-go v0.0.0-20210223005429-8d38e43fc6ed h1:cwwqHrmLafgEucS
40 40
 github.com/goshuirc/irc-go v0.0.0-20210223005429-8d38e43fc6ed/go.mod h1:q/JhvvKLmif3y9q8MDQM+gRCnjEKnu5ClF298TTXJug=
41 41
 github.com/goshuirc/irc-go v0.0.0-20210301225436-2c4b83d64847 h1:MmsZRpAsMxyw0P5/SFn2L6edhmIXRlolgXvOF+fgEiQ=
42 42
 github.com/goshuirc/irc-go v0.0.0-20210301225436-2c4b83d64847/go.mod h1:q/JhvvKLmif3y9q8MDQM+gRCnjEKnu5ClF298TTXJug=
43
+github.com/goshuirc/irc-go v0.0.0-20210304031553-cf78e9176f96 h1:sihI3HsrJWyS4MtBmxh5W4gDZD34SWodkWyUvJltswY=
44
+github.com/goshuirc/irc-go v0.0.0-20210304031553-cf78e9176f96/go.mod h1:q/JhvvKLmif3y9q8MDQM+gRCnjEKnu5ClF298TTXJug=
43 45
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
44 46
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
45 47
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

+ 9
- 0
irc/channel.go View File

@@ -1361,6 +1361,15 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
1361 1361
 	details := client.Details()
1362 1362
 	chname := channel.Name()
1363 1363
 
1364
+	if !client.server.Config().Server.Compatibility.allowTruncation {
1365
+		if !validateSplitMessageLen(histType, details.nickMask, chname, message) {
1366
+			rb.Add(nil, client.server.name, ERR_INPUTTOOLONG, details.nick, client.t("Line too long to be relayed without truncation"))
1367
+			// TODO(#1577) remove this logline:
1368
+			client.server.logger.Debug("internal", "rejected truncation-requiring DM from client", details.nick)
1369
+			return
1370
+		}
1371
+	}
1372
+
1364 1373
 	// STATUSMSG targets are prefixed with the supplied min-prefix, e.g., @#channel
1365 1374
 	if minPrefixMode != modes.Mode(0) {
1366 1375
 		chname = fmt.Sprintf("%s%s", modes.ChannelModePrefixes[minPrefixMode], chname)

+ 7
- 2
irc/client.go View File

@@ -740,8 +740,13 @@ func (client *Client) run(session *Session) {
740 740
 		msg, err := ircmsg.ParseLineStrict(line, true, MaxLineLen)
741 741
 		if err == ircmsg.ErrorLineIsEmpty {
742 742
 			continue
743
-		} else if err == ircmsg.ErrorLineTooLong {
743
+		} else if err == ircmsg.ErrorTagsTooLong {
744
+			session.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Input line contained excess tag data"))
745
+			continue
746
+		} else if err == ircmsg.ErrorBodyTooLong && !client.server.Config().Server.Compatibility.allowTruncation {
744 747
 			session.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Input line too long"))
748
+			// TODO(#1577) remove this logline:
749
+			client.server.logger.Debug("internal", "rejected MaxLineLen-exceeding line from client", client.Nick())
745 750
 			continue
746 751
 		} else if err != nil {
747 752
 			client.Quit(client.t("Received malformed line"), session)
@@ -1711,7 +1716,7 @@ func (session *Session) SendRawMessage(message ircmsg.IRCMessage, blocking bool)
1711 1716
 
1712 1717
 	// assemble message
1713 1718
 	line, err := message.LineBytesStrict(false, MaxLineLen)
1714
-	if err != nil {
1719
+	if !(err == nil || err == ircmsg.ErrorBodyTooLong) {
1715 1720
 		errorParams := []string{"Error assembling message for sending", err.Error(), message.Command}
1716 1721
 		errorParams = append(errorParams, message.Params...)
1717 1722
 		session.client.server.logger.Error("internal", errorParams...)

+ 4
- 1
irc/config.go View File

@@ -569,7 +569,9 @@ type Config struct {
569 569
 		Compatibility        struct {
570 570
 			ForceTrailing      *bool `yaml:"force-trailing"`
571 571
 			forceTrailing      bool
572
-			SendUnprefixedSasl bool `yaml:"send-unprefixed-sasl"`
572
+			SendUnprefixedSasl bool  `yaml:"send-unprefixed-sasl"`
573
+			AllowTruncation    *bool `yaml:"allow-truncation"`
574
+			allowTruncation    bool
573 575
 		}
574 576
 		isupport                 isupport.List
575 577
 		IPLimits                 connection_limits.LimiterConfig `yaml:"ip-limits"`
@@ -1378,6 +1380,7 @@ func LoadConfig(filename string) (config *Config, err error) {
1378 1380
 	}
1379 1381
 
1380 1382
 	config.Server.Compatibility.forceTrailing = utils.BoolDefaultTrue(config.Server.Compatibility.ForceTrailing)
1383
+	config.Server.Compatibility.allowTruncation = utils.BoolDefaultTrue(config.Server.Compatibility.AllowTruncation)
1381 1384
 
1382 1385
 	config.loadMOTD()
1383 1386
 

+ 46
- 2
irc/handlers.go View File

@@ -1981,6 +1981,43 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IRCMessage, rb *Resp
1981 1981
 	return false
1982 1982
 }
1983 1983
 
1984
+// check whether a PRIVMSG or NOTICE is too long to be relayed without truncation
1985
+func validateLineLen(msgType history.ItemType, source, target, payload string) (ok bool) {
1986
+	// :source PRIVMSG #target :payload\r\n
1987
+	// 1: initial colon on prefix
1988
+	// 1: space between prefix and command
1989
+	// 1: space between command and target (first parameter)
1990
+	// 1: space between target and payload (second parameter)
1991
+	// 1: colon to send the payload as a trailing (we force trailing for PRIVMSG and NOTICE)
1992
+	// 2: final \r\n
1993
+	limit := MaxLineLen - 7
1994
+	limit -= len(source)
1995
+	switch msgType {
1996
+	case history.Privmsg:
1997
+		limit -= 7
1998
+	case history.Notice:
1999
+		limit -= 6
2000
+	default:
2001
+		return true
2002
+	}
2003
+	limit -= len(payload)
2004
+	return limit >= 0
2005
+}
2006
+
2007
+// check validateLineLen for an entire SplitMessage (which may consist of multiple lines)
2008
+func validateSplitMessageLen(msgType history.ItemType, source, target string, message utils.SplitMessage) (ok bool) {
2009
+	if message.Is512() {
2010
+		return validateLineLen(msgType, source, target, message.Message)
2011
+	} else {
2012
+		for _, messagePair := range message.Split {
2013
+			if !validateLineLen(msgType, source, target, messagePair.Message) {
2014
+				return false
2015
+			}
2016
+		}
2017
+		return true
2018
+	}
2019
+}
2020
+
1984 2021
 // helper to store a batched PRIVMSG in the session object
1985 2022
 func absorbBatchedMessage(server *Server, client *Client, msg ircmsg.IRCMessage, batchTag string, histType history.ItemType, rb *ResponseBuffer) {
1986 2023
 	var errorCode, errorMessage string
@@ -2033,7 +2070,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IRCMessage, rb *R
2033 2070
 		return false
2034 2071
 	}
2035 2072
 
2036
-	cnick := client.Nick()
2037 2073
 	clientOnlyTags := msg.ClientOnlyTags()
2038 2074
 	if histType == history.Tagmsg && len(clientOnlyTags) == 0 {
2039 2075
 		// nothing to do
@@ -2046,7 +2082,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IRCMessage, rb *R
2046 2082
 		message = msg.Params[1]
2047 2083
 	}
2048 2084
 	if histType != history.Tagmsg && message == "" {
2049
-		rb.Add(nil, server.name, ERR_NOTEXTTOSEND, cnick, client.t("No text to send"))
2085
+		rb.Add(nil, server.name, ERR_NOTEXTTOSEND, client.Nick(), client.t("No text to send"))
2050 2086
 		return false
2051 2087
 	}
2052 2088
 
@@ -2147,6 +2183,14 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
2147 2183
 			rb.Add(nil, server.name, ERR_NEEDREGGEDNICK, client.Nick(), tnick, client.t("You must be registered to send a direct message to this user"))
2148 2184
 			return
2149 2185
 		}
2186
+		if !client.server.Config().Server.Compatibility.allowTruncation {
2187
+			if !validateSplitMessageLen(histType, client.NickMaskString(), tnick, message) {
2188
+				rb.Add(nil, server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Line too long to be relayed without truncation"))
2189
+				// TODO(#1577) remove this logline:
2190
+				client.server.logger.Debug("internal", "rejected truncation-requiring channel message from client", details.nick)
2191
+				return
2192
+			}
2193
+		}
2150 2194
 		nickMaskString := details.nickMask
2151 2195
 		accountName := details.accountName
2152 2196
 		var deliverySessions []*Session

+ 1
- 1
irc/message_cache.go View File

@@ -54,7 +54,7 @@ func addAllTags(msg *ircmsg.IRCMessage, tags map[string]string, serverTime time.
54 54
 }
55 55
 
56 56
 func (m *MessageCache) handleErr(server *Server, err error) bool {
57
-	if err != nil {
57
+	if !(err == nil || err == ircmsg.ErrorBodyTooLong) {
58 58
 		server.logger.Error("internal", "Error assembling message for sending", err.Error())
59 59
 		// blank these out so Send will be a no-op
60 60
 		m.fullTags = nil

+ 7
- 0
traditional.yaml View File

@@ -208,6 +208,13 @@ server:
208 208
         # this works around that bug, allowing them to use SASL.
209 209
         send-unprefixed-sasl: true
210 210
 
211
+        # traditionally, IRC servers will truncate and send messages that are
212
+        # too long to be relayed intact. this behavior can be disabled by setting
213
+        # allow-truncation to false, in which case Oragono will reject the message
214
+        # and return an error to the client. (note that this option defaults to true
215
+        # when unset.)
216
+        allow-truncation: true
217
+
211 218
     # IP-based DoS protection
212 219
     ip-limits:
213 220
         # whether to limit the total number of concurrent connections per IP/CIDR

+ 58
- 20
vendor/github.com/goshuirc/irc-go/ircmsg/message.go View File

@@ -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
 }

+ 4
- 0
vendor/github.com/goshuirc/irc-go/ircreader/ircreader.go View File

@@ -63,6 +63,10 @@ func (cc *IRCReader) ReadLine() ([]byte, error) {
63 63
 			line := cc.buf[cc.start : cc.searchFrom+nlidx]
64 64
 			cc.start = cc.searchFrom + nlidx + 1
65 65
 			cc.searchFrom = cc.start
66
+			// treat \r\n as the line terminator if it was present
67
+			if 0 < len(line) && line[len(line)-1] == '\r' {
68
+				line = line[:len(line)-1]
69
+			}
66 70
 			return line, nil
67 71
 		}
68 72
 

+ 1
- 1
vendor/modules.txt View File

@@ -21,7 +21,7 @@ github.com/go-sql-driver/mysql
21 21
 # github.com/gorilla/websocket v1.4.2 => github.com/oragono/websocket v1.4.2-oragono1
22 22
 ## explicit
23 23
 github.com/gorilla/websocket
24
-# github.com/goshuirc/irc-go v0.0.0-20210301225436-2c4b83d64847
24
+# github.com/goshuirc/irc-go v0.0.0-20210304031553-cf78e9176f96
25 25
 ## explicit
26 26
 github.com/goshuirc/irc-go/ircfmt
27 27
 github.com/goshuirc/irc-go/ircmsg

Loading…
Cancel
Save