Browse Source

fix #843

tags/v2.2.0-rc1
Shivaram Lingamneni 4 years ago
parent
commit
218bea5a3e
6 changed files with 146 additions and 48 deletions
  1. 15
    11
      irc/accounts.go
  2. 61
    10
      irc/client.go
  3. 9
    0
      irc/errors.go
  4. 13
    5
      irc/getters.go
  5. 43
    14
      irc/handlers.go
  6. 5
    8
      irc/nickserv.go

+ 15
- 11
irc/accounts.go View File

@@ -617,11 +617,12 @@ func (am *AccountManager) loadModes(account string) (uModes modes.Modes) {
617 617
 	return
618 618
 }
619 619
 
620
-func (am *AccountManager) saveLastSeen(account string, lastSeen time.Time) {
620
+func (am *AccountManager) saveLastSeen(account string, lastSeen map[string]time.Time) {
621 621
 	key := fmt.Sprintf(keyAccountLastSeen, account)
622 622
 	var val string
623
-	if !lastSeen.IsZero() {
624
-		val = strconv.FormatInt(lastSeen.UnixNano(), 10)
623
+	if len(lastSeen) != 0 {
624
+		text, _ := json.Marshal(lastSeen)
625
+		val = string(text)
625 626
 	}
626 627
 	am.server.store.Update(func(tx *buntdb.Tx) error {
627 628
 		if val != "" {
@@ -633,20 +634,19 @@ func (am *AccountManager) saveLastSeen(account string, lastSeen time.Time) {
633 634
 	})
634 635
 }
635 636
 
636
-func (am *AccountManager) loadLastSeen(account string) (lastSeen time.Time) {
637
+func (am *AccountManager) loadLastSeen(account string) (lastSeen map[string]time.Time) {
637 638
 	key := fmt.Sprintf(keyAccountLastSeen, account)
638 639
 	var lsText string
639 640
 	am.server.store.Update(func(tx *buntdb.Tx) error {
640 641
 		lsText, _ = tx.Get(key)
641
-		// XXX clear this on startup, because it's not clear when it's
642
-		// going to be overwritten, and restarting the server twice in a row
643
-		// could result in a large amount of duplicated history replay
644
-		tx.Delete(key)
645 642
 		return nil
646 643
 	})
647
-	lsNum, err := strconv.ParseInt(lsText, 10, 64)
648
-	if err == nil {
649
-		return time.Unix(0, lsNum).UTC()
644
+	if lsText == "" {
645
+		return nil
646
+	}
647
+	err := json.Unmarshal([]byte(lsText), &lastSeen)
648
+	if err != nil {
649
+		return nil
650 650
 	}
651 651
 	return
652 652
 }
@@ -1052,6 +1052,10 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
1052 1052
 		}
1053 1053
 	}
1054 1054
 
1055
+	if throttled, remainingTime := client.checkLoginThrottle(); throttled {
1056
+		return &ThrottleError{remainingTime}
1057
+	}
1058
+
1055 1059
 	var account ClientAccount
1056 1060
 
1057 1061
 	defer func() {

+ 61
- 10
irc/client.go View File

@@ -30,6 +30,11 @@ const (
30 30
 	// IdentTimeout is how long before our ident (username) check times out.
31 31
 	IdentTimeout         = time.Second + 500*time.Millisecond
32 32
 	IRCv3TimestampFormat = utils.IRCv3TimestampFormat
33
+	// limit the number of device IDs a client can use, as a DoS mitigation
34
+	maxDeviceIDsPerClient = 64
35
+	// controls how often often we write an autoreplay-missed client's
36
+	// deviceid->lastseentime mapping to the database
37
+	lastSeenWriteInterval = time.Minute * 10
33 38
 )
34 39
 
35 40
 // ResumeDetails is a place to stash data at various stages of
@@ -60,8 +65,9 @@ type Client struct {
60 65
 	invitedTo          map[string]bool
61 66
 	isSTSOnly          bool
62 67
 	languages          []string
63
-	lastActive         time.Time // last time they sent a command that wasn't PONG or similar
64
-	lastSeen           time.Time // last time they sent any kind of command
68
+	lastActive         time.Time            // last time they sent a command that wasn't PONG or similar
69
+	lastSeen           map[string]time.Time // maps device ID (including "") to time of last received command
70
+	lastSeenLastWrite  time.Time            // last time `lastSeen` was written to the datastore
65 71
 	loginThrottle      connection_limits.GenericThrottle
66 72
 	nick               string
67 73
 	nickCasefolded     string
@@ -112,6 +118,8 @@ const (
112 118
 type Session struct {
113 119
 	client *Client
114 120
 
121
+	deviceID string
122
+
115 123
 	ctime      time.Time
116 124
 	lastActive time.Time
117 125
 
@@ -299,7 +307,7 @@ func (server *Server) RunClient(conn IRCConn) {
299 307
 	// give them 1k of grace over the limit:
300 308
 	socket := NewSocket(conn, config.Server.MaxSendQBytes)
301 309
 	client := &Client{
302
-		lastSeen:   now,
310
+		lastSeen:   make(map[string]time.Time),
303 311
 		lastActive: now,
304 312
 		channels:   make(ChannelSet),
305 313
 		ctime:      now,
@@ -358,11 +366,14 @@ func (server *Server) RunClient(conn IRCConn) {
358 366
 	client.run(session)
359 367
 }
360 368
 
361
-func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen time.Time, uModes modes.Modes) {
369
+func (server *Server) AddAlwaysOnClient(account ClientAccount, chnames []string, lastSeen map[string]time.Time, uModes modes.Modes) {
362 370
 	now := time.Now().UTC()
363 371
 	config := server.Config()
364
-	if lastSeen.IsZero() {
365
-		lastSeen = now
372
+	if lastSeen == nil {
373
+		lastSeen = make(map[string]time.Time)
374
+		if account.Settings.AutoreplayMissed {
375
+			lastSeen[""] = now
376
+		}
366 377
 	}
367 378
 
368 379
 	client := &Client{
@@ -714,14 +725,39 @@ func (client *Client) playReattachMessages(session *Session) {
714 725
 // at this time, modulo network latency and fakelag). `active` means not a PING or suchlike
715 726
 // (i.e. the user should be sitting in front of their client).
716 727
 func (client *Client) Touch(active bool, session *Session) {
728
+	var markDirty bool
717 729
 	now := time.Now().UTC()
718 730
 	client.stateMutex.Lock()
719
-	defer client.stateMutex.Unlock()
720
-	client.lastSeen = now
721 731
 	if active {
722 732
 		client.lastActive = now
723 733
 		session.lastActive = now
724 734
 	}
735
+	if client.accountSettings.AutoreplayMissed {
736
+		client.setLastSeen(now, session.deviceID)
737
+		if now.Sub(client.lastSeenLastWrite) > lastSeenWriteInterval {
738
+			markDirty = true
739
+			client.lastSeenLastWrite = now
740
+		}
741
+	}
742
+	client.stateMutex.Unlock()
743
+	if markDirty {
744
+		client.markDirty(IncludeLastSeen)
745
+	}
746
+}
747
+
748
+func (client *Client) setLastSeen(now time.Time, deviceID string) {
749
+	client.lastSeen[deviceID] = now
750
+	// evict the least-recently-used entry if necessary
751
+	if maxDeviceIDsPerClient < len(client.lastSeen) {
752
+		var minLastSeen time.Time
753
+		var minClientId string
754
+		for deviceID, lastSeen := range client.lastSeen {
755
+			if minLastSeen.IsZero() || lastSeen.Before(minLastSeen) {
756
+				minClientId, minLastSeen = deviceID, lastSeen
757
+			}
758
+		}
759
+		delete(client.lastSeen, minClientId)
760
+	}
725 761
 }
726 762
 
727 763
 // Ping sends the client a PING message.
@@ -1604,6 +1640,12 @@ func (client *Client) attemptAutoOper(session *Session) {
1604 1640
 	}
1605 1641
 }
1606 1642
 
1643
+func (client *Client) checkLoginThrottle() (throttled bool, remainingTime time.Duration) {
1644
+	client.stateMutex.Lock()
1645
+	defer client.stateMutex.Unlock()
1646
+	return client.loginThrottle.Touch()
1647
+}
1648
+
1607 1649
 func (client *Client) historyStatus(config *Config) (status HistoryStatus, target string) {
1608 1650
 	if !config.History.Enabled {
1609 1651
 		return HistoryDisabled, ""
@@ -1624,6 +1666,16 @@ func (client *Client) historyStatus(config *Config) (status HistoryStatus, targe
1624 1666
 	return
1625 1667
 }
1626 1668
 
1669
+func (client *Client) copyLastSeen() (result map[string]time.Time) {
1670
+	client.stateMutex.RLock()
1671
+	defer client.stateMutex.RUnlock()
1672
+	result = make(map[string]time.Time, len(client.lastSeen))
1673
+	for id, lastSeen := range client.lastSeen {
1674
+		result[id] = lastSeen
1675
+	}
1676
+	return
1677
+}
1678
+
1627 1679
 // these are bit flags indicating what part of the client status is "dirty"
1628 1680
 // and needs to be read from memory and written to the db
1629 1681
 const (
@@ -1669,7 +1721,6 @@ func (client *Client) performWrite() {
1669 1721
 	dirtyBits := client.dirtyBits
1670 1722
 	client.dirtyBits = 0
1671 1723
 	account := client.account
1672
-	lastSeen := client.lastSeen
1673 1724
 	client.stateMutex.Unlock()
1674 1725
 
1675 1726
 	if account == "" {
@@ -1686,7 +1737,7 @@ func (client *Client) performWrite() {
1686 1737
 		client.server.accounts.saveChannels(account, channelNames)
1687 1738
 	}
1688 1739
 	if (dirtyBits & IncludeLastSeen) != 0 {
1689
-		client.server.accounts.saveLastSeen(account, lastSeen)
1740
+		client.server.accounts.saveLastSeen(account, client.copyLastSeen())
1690 1741
 	}
1691 1742
 	if (dirtyBits & IncludeUserModes) != 0 {
1692 1743
 		uModes := make(modes.Modes, 0, len(modes.SupportedUserModes))

+ 9
- 0
irc/errors.go View File

@@ -8,6 +8,7 @@ package irc
8 8
 import (
9 9
 	"errors"
10 10
 	"fmt"
11
+	"time"
11 12
 
12 13
 	"github.com/oragono/oragono/irc/utils"
13 14
 )
@@ -89,6 +90,14 @@ func (ck *CertKeyError) Error() string {
89 90
 	return fmt.Sprintf("Invalid TLS cert/key pair: %v", ck.Err)
90 91
 }
91 92
 
93
+type ThrottleError struct {
94
+	time.Duration
95
+}
96
+
97
+func (te *ThrottleError) Error() string {
98
+	return fmt.Sprintf(`Please wait at least %v and try again`, te.Duration)
99
+}
100
+
92 101
 // Config Errors
93 102
 var (
94 103
 	ErrDatastorePathMissing    = errors.New("Datastore path missing")

+ 13
- 5
irc/getters.go View File

@@ -62,6 +62,7 @@ type SessionData struct {
62 62
 	ip       net.IP
63 63
 	hostname string
64 64
 	certfp   string
65
+	deviceID string
65 66
 }
66 67
 
67 68
 func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) {
@@ -79,6 +80,7 @@ func (client *Client) AllSessionData(currentSession *Session) (data []SessionDat
79 80
 			ctime:    session.ctime,
80 81
 			hostname: session.rawHostname,
81 82
 			certfp:   session.certfp,
83
+			deviceID: session.deviceID,
82 84
 		}
83 85
 		if session.proxiedIP != nil {
84 86
 			data[i].ip = session.proxiedIP
@@ -103,7 +105,7 @@ func (client *Client) AddSession(session *Session) (success bool, numSessions in
103 105
 	copy(newSessions, client.sessions)
104 106
 	newSessions[len(newSessions)-1] = session
105 107
 	if client.accountSettings.AutoreplayMissed {
106
-		lastSeen = client.lastSeen
108
+		lastSeen = client.lastSeen[session.deviceID]
107 109
 	}
108 110
 	client.sessions = newSessions
109 111
 	if client.autoAway {
@@ -324,17 +326,23 @@ func (client *Client) AccountSettings() (result AccountSettings) {
324 326
 
325 327
 func (client *Client) SetAccountSettings(settings AccountSettings) {
326 328
 	// we mark dirty if the client is transitioning to always-on
327
-	markDirty := false
329
+	var becameAlwaysOn, autoreplayMissedDisabled bool
328 330
 	alwaysOn := persistenceEnabled(client.server.Config().Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
329 331
 	client.stateMutex.Lock()
330
-	client.accountSettings = settings
331 332
 	if client.registered {
332
-		markDirty = !client.alwaysOn && alwaysOn
333
+		autoreplayMissedDisabled = (client.accountSettings.AutoreplayMissed && !settings.AutoreplayMissed)
334
+		becameAlwaysOn = (!client.alwaysOn && alwaysOn)
333 335
 		client.alwaysOn = alwaysOn
336
+		if autoreplayMissedDisabled {
337
+			client.lastSeen = make(map[string]time.Time)
338
+		}
334 339
 	}
340
+	client.accountSettings = settings
335 341
 	client.stateMutex.Unlock()
336
-	if markDirty {
342
+	if becameAlwaysOn {
337 343
 		client.markDirty(IncludeAllAttrs)
344
+	} else if autoreplayMissedDisabled {
345
+		client.markDirty(IncludeLastSeen)
338 346
 	}
339 347
 }
340 348
 

+ 43
- 14
irc/handlers.go View File

@@ -236,6 +236,11 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
236 236
 		return false
237 237
 	}
238 238
 
239
+	// see #843: strip the device ID for the benefit of clients that don't
240
+	// distinguish user/ident from account name
241
+	if strudelIndex := strings.IndexByte(authcid, '@'); strudelIndex != -1 {
242
+		authcid = authcid[:strudelIndex]
243
+	}
239 244
 	password := string(splitValue[2])
240 245
 	err := server.accounts.AuthenticateByPassphrase(client, authcid, password)
241 246
 	if err != nil {
@@ -251,6 +256,10 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
251 256
 }
252 257
 
253 258
 func authErrorToMessage(server *Server, err error) (msg string) {
259
+	if throttled, ok := err.(*ThrottleError); ok {
260
+		return throttled.Error()
261
+	}
262
+
254 263
 	switch err {
255 264
 	case errAccountDoesNotExist, errAccountUnverified, errAccountInvalidCredentials, errAuthzidAuthcidMismatch, errNickAccountMismatch:
256 265
 		return err.Error()
@@ -280,6 +289,11 @@ func authExternalHandler(server *Server, client *Client, mechanism string, value
280 289
 	}
281 290
 
282 291
 	if err == nil {
292
+		// see #843: strip the device ID for the benefit of clients that don't
293
+		// distinguish user/ident from account name
294
+		if strudelIndex := strings.IndexByte(authzid, '@'); strudelIndex != -1 {
295
+			authzid = authzid[:strudelIndex]
296
+		}
283 297
 		err = server.accounts.AuthenticateByCertFP(client, rb.session.certfp, authzid)
284 298
 	}
285 299
 
@@ -2180,8 +2194,8 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2180 2194
 		rb.Add(nil, server.name, ERR_ALREADYREGISTRED, client.nick, client.t("You may not reregister"))
2181 2195
 		return false
2182 2196
 	}
2183
-	// only give them one try to run the PASS command (all code paths end with this
2184
-	// variable being set):
2197
+	// only give them one try to run the PASS command (if a server password is set,
2198
+	// then all code paths end with this variable being set):
2185 2199
 	if rb.session.passStatus != serverPassUnsent {
2186 2200
 		return false
2187 2201
 	}
@@ -2192,18 +2206,17 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2192 2206
 	if config.Accounts.LoginViaPassCommand {
2193 2207
 		colonIndex := strings.IndexByte(password, ':')
2194 2208
 		if colonIndex != -1 && client.Account() == "" {
2195
-			// TODO consolidate all login throttle checks into AccountManager
2196
-			throttled, _ := client.loginThrottle.Touch()
2197
-			if !throttled {
2198
-				account, accountPass := password[:colonIndex], password[colonIndex+1:]
2199
-				err := server.accounts.AuthenticateByPassphrase(client, account, accountPass)
2200
-				if err == nil {
2201
-					sendSuccessfulAccountAuth(client, rb, false, true)
2202
-					// login-via-pass-command entails that we do not need to check
2203
-					// an actual server password (either no password or skip-server-password)
2204
-					rb.session.passStatus = serverPassSuccessful
2205
-					return false
2206
-				}
2209
+			account, accountPass := password[:colonIndex], password[colonIndex+1:]
2210
+			if strudelIndex := strings.IndexByte(account, '@'); strudelIndex != -1 {
2211
+				account, rb.session.deviceID = account[:strudelIndex], account[strudelIndex+1:]
2212
+			}
2213
+			err := server.accounts.AuthenticateByPassphrase(client, account, accountPass)
2214
+			if err == nil {
2215
+				sendSuccessfulAccountAuth(client, rb, false, true)
2216
+				// login-via-pass-command entails that we do not need to check
2217
+				// an actual server password (either no password or skip-server-password)
2218
+				rb.session.passStatus = serverPassSuccessful
2219
+				return false
2207 2220
 			}
2208 2221
 		}
2209 2222
 	}
@@ -2521,6 +2534,22 @@ func userHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2521 2534
 		return false
2522 2535
 	}
2523 2536
 
2537
+	// #843: we accept either: `USER user:pass@clientid` or `USER user@clientid`
2538
+	if strudelIndex := strings.IndexByte(username, '@'); strudelIndex != -1 {
2539
+		username, rb.session.deviceID = username[:strudelIndex], username[strudelIndex+1:]
2540
+		if colonIndex := strings.IndexByte(username, ':'); colonIndex != -1 {
2541
+			var password string
2542
+			username, password = username[:colonIndex], username[colonIndex+1:]
2543
+			err := server.accounts.AuthenticateByPassphrase(client, username, password)
2544
+			if err == nil {
2545
+				sendSuccessfulAccountAuth(client, rb, false, true)
2546
+			} else {
2547
+				// this is wrong, but send something for debugging that will show up in a raw transcript
2548
+				rb.Add(nil, server.name, ERR_SASLFAIL, client.Nick(), client.t("SASL authentication failed"))
2549
+			}
2550
+		}
2551
+	}
2552
+
2524 2553
 	err := client.SetNames(username, realname, false)
2525 2554
 	if err == errInvalidUsername {
2526 2555
 		// if client's using a unicode nick or something weird, let's just set 'em up with a stock username instead.

+ 5
- 8
irc/nickserv.go View File

@@ -649,14 +649,11 @@ func nsGroupHandler(server *Server, client *Client, command string, params []str
649 649
 }
650 650
 
651 651
 func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) {
652
-	client.stateMutex.Lock()
653
-	throttled, remainingTime := client.loginThrottle.Touch()
654
-	client.stateMutex.Unlock()
652
+	throttled, remainingTime := client.checkLoginThrottle()
655 653
 	if throttled {
656 654
 		nsNotice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime))
657
-		return false
658 655
 	}
659
-	return true
656
+	return !throttled
660 657
 }
661 658
 
662 659
 func nsIdentifyHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
@@ -685,9 +682,6 @@ func nsIdentifyHandler(server *Server, client *Client, command string, params []
685 682
 
686 683
 	// try passphrase
687 684
 	if passphrase != "" {
688
-		if !nsLoginThrottleCheck(client, rb) {
689
-			return
690
-		}
691 685
 		err = server.accounts.AuthenticateByPassphrase(client, username, passphrase)
692 686
 		loginSuccessful = (err == nil)
693 687
 	}
@@ -1070,6 +1064,9 @@ func nsSessionsHandler(server *Server, client *Client, command string, params []
1070 1064
 		} else {
1071 1065
 			nsNotice(rb, fmt.Sprintf(client.t("Session %d:"), i+1))
1072 1066
 		}
1067
+		if session.deviceID != "" {
1068
+			nsNotice(rb, fmt.Sprintf(client.t("Device ID:   %s"), session.deviceID))
1069
+		}
1073 1070
 		nsNotice(rb, fmt.Sprintf(client.t("IP address:  %s"), session.ip.String()))
1074 1071
 		nsNotice(rb, fmt.Sprintf(client.t("Hostname:    %s"), session.hostname))
1075 1072
 		nsNotice(rb, fmt.Sprintf(client.t("Created at:  %s"), session.ctime.Format(time.RFC1123)))

Loading…
Cancel
Save