|
@@ -56,6 +56,8 @@ const (
|
56
|
56
|
// This is how long a client gets without sending any message, including the PONG to our
|
57
|
57
|
// PING, before we disconnect them:
|
58
|
58
|
DefaultTotalTimeout = 2*time.Minute + 30*time.Second
|
|
59
|
+ // Resumeable clients (clients who have negotiated caps.Resume) get longer:
|
|
60
|
+ ResumeableTotalTimeout = 3*time.Minute + 30*time.Second
|
59
|
61
|
|
60
|
62
|
// round off the ping interval by this much, see below:
|
61
|
63
|
PingCoalesceThreshold = time.Second
|
|
@@ -65,6 +67,15 @@ var (
|
65
|
67
|
MaxLineLen = DefaultMaxLineLen
|
66
|
68
|
)
|
67
|
69
|
|
|
70
|
+// ResumeDetails is a place to stash data at various stages of
|
|
71
|
+// the resume process: when handling the RESUME command itself,
|
|
72
|
+// when completing the registration, and when rejoining channels.
|
|
73
|
+type ResumeDetails struct {
|
|
74
|
+ PresentedToken string
|
|
75
|
+ Timestamp time.Time
|
|
76
|
+ HistoryIncomplete bool
|
|
77
|
+}
|
|
78
|
+
|
68
|
79
|
// Client is an IRC client.
|
69
|
80
|
type Client struct {
|
70
|
81
|
account string
|
|
@@ -72,6 +83,7 @@ type Client struct {
|
72
|
83
|
accountRegDate time.Time
|
73
|
84
|
accountSettings AccountSettings
|
74
|
85
|
awayMessage string
|
|
86
|
+ brbTimer BrbTimer
|
75
|
87
|
channels ChannelSet
|
76
|
88
|
ctime time.Time
|
77
|
89
|
destroyed bool
|
|
@@ -101,6 +113,7 @@ type Client struct {
|
101
|
113
|
registered bool
|
102
|
114
|
registerCmdSent bool // already sent the draft/register command, can't send it again
|
103
|
115
|
registrationTimer *time.Timer
|
|
116
|
+ resumeID string
|
104
|
117
|
server *Server
|
105
|
118
|
skeleton string
|
106
|
119
|
sessions []*Session
|
|
@@ -155,6 +168,7 @@ type Session struct {
|
155
|
168
|
|
156
|
169
|
fakelag Fakelag
|
157
|
170
|
deferredFakelagCount int
|
|
171
|
+ destroyed uint32
|
158
|
172
|
|
159
|
173
|
certfp string
|
160
|
174
|
peerCerts []*x509.Certificate
|
|
@@ -174,6 +188,8 @@ type Session struct {
|
174
|
188
|
|
175
|
189
|
registrationMessages int
|
176
|
190
|
|
|
191
|
+ resumeID string
|
|
192
|
+ resumeDetails *ResumeDetails
|
177
|
193
|
zncPlaybackTimes *zncPlaybackTimes
|
178
|
194
|
autoreplayMissedSince time.Time
|
179
|
195
|
|
|
@@ -247,6 +263,20 @@ func (s *Session) IP() net.IP {
|
247
|
263
|
return s.realIP
|
248
|
264
|
}
|
249
|
265
|
|
|
266
|
+// returns whether the session was actively destroyed (for example, by ping
|
|
267
|
+// timeout or NS GHOST).
|
|
268
|
+// avoids a race condition between asynchronous idle-timing-out of sessions,
|
|
269
|
+// and a condition that allows implicit BRB on connection errors (since
|
|
270
|
+// destroy()'s socket.Close() appears to socket.Read() as a connection error)
|
|
271
|
+func (session *Session) Destroyed() bool {
|
|
272
|
+ return atomic.LoadUint32(&session.destroyed) == 1
|
|
273
|
+}
|
|
274
|
+
|
|
275
|
+// sets the timed-out flag
|
|
276
|
+func (session *Session) SetDestroyed() {
|
|
277
|
+ atomic.StoreUint32(&session.destroyed, 1)
|
|
278
|
+}
|
|
279
|
+
|
250
|
280
|
// returns whether the client supports a smart history replay cap,
|
251
|
281
|
// and therefore autoreplay-on-join and similar should be suppressed
|
252
|
282
|
func (session *Session) HasHistoryCaps() bool {
|
|
@@ -345,6 +375,7 @@ func (server *Server) RunClient(conn IRCConn) {
|
345
|
375
|
client.requireSASLMessage = banMsg
|
346
|
376
|
}
|
347
|
377
|
client.history.Initialize(config.History.ClientLength, time.Duration(config.History.AutoresizeWindow))
|
|
378
|
+ client.brbTimer.Initialize(client)
|
348
|
379
|
session := &Session{
|
349
|
380
|
client: client,
|
350
|
381
|
socket: socket,
|
|
@@ -432,6 +463,7 @@ func (server *Server) AddAlwaysOnClient(account ClientAccount, channelToStatus m
|
432
|
463
|
client.SetMode(m, true)
|
433
|
464
|
}
|
434
|
465
|
client.history.Initialize(0, 0)
|
|
466
|
+ client.brbTimer.Initialize(client)
|
435
|
467
|
|
436
|
468
|
server.accounts.Login(client, account)
|
437
|
469
|
|
|
@@ -525,7 +557,7 @@ func (client *Client) lookupHostname(session *Session, overwrite bool) {
|
525
|
557
|
cloakedHostname := config.Server.Cloaks.ComputeCloak(ip)
|
526
|
558
|
client.stateMutex.Lock()
|
527
|
559
|
defer client.stateMutex.Unlock()
|
528
|
|
- // update the hostname if this is a new connection, but not if it's a reattach
|
|
560
|
+ // update the hostname if this is a new connection or a resume, but not if it's a reattach
|
529
|
561
|
if overwrite || client.rawHostname == "" {
|
530
|
562
|
client.rawHostname = hostname
|
531
|
563
|
client.cloakedHostname = cloakedHostname
|
|
@@ -643,7 +675,14 @@ func (client *Client) run(session *Session) {
|
643
|
675
|
isReattach := client.Registered()
|
644
|
676
|
if isReattach {
|
645
|
677
|
client.Touch(session)
|
646
|
|
- client.playReattachMessages(session)
|
|
678
|
+ if session.resumeDetails != nil {
|
|
679
|
+ session.playResume()
|
|
680
|
+ session.resumeDetails = nil
|
|
681
|
+ client.brbTimer.Disable()
|
|
682
|
+ session.SetAway("") // clear BRB message if any
|
|
683
|
+ } else {
|
|
684
|
+ client.playReattachMessages(session)
|
|
685
|
+ }
|
647
|
686
|
}
|
648
|
687
|
|
649
|
688
|
firstLine := !isReattach
|
|
@@ -662,6 +701,11 @@ func (client *Client) run(session *Session) {
|
662
|
701
|
quitMessage = "connection closed"
|
663
|
702
|
}
|
664
|
703
|
client.Quit(quitMessage, session)
|
|
704
|
+ // since the client did not actually send us a QUIT,
|
|
705
|
+ // give them a chance to resume if applicable:
|
|
706
|
+ if !session.Destroyed() {
|
|
707
|
+ client.brbTimer.Enable()
|
|
708
|
+ }
|
665
|
709
|
break
|
666
|
710
|
}
|
667
|
711
|
|
|
@@ -812,6 +856,9 @@ func (client *Client) updateIdleTimer(session *Session, now time.Time) {
|
812
|
856
|
|
813
|
857
|
func (session *Session) handleIdleTimeout() {
|
814
|
858
|
totalTimeout := DefaultTotalTimeout
|
|
859
|
+ if session.capabilities.Has(caps.Resume) {
|
|
860
|
+ totalTimeout = ResumeableTotalTimeout
|
|
861
|
+ }
|
815
|
862
|
pingTimeout := DefaultIdleTimeout
|
816
|
863
|
if session.isTor {
|
817
|
864
|
pingTimeout = TorIdleTimeout
|
|
@@ -868,6 +915,151 @@ func (session *Session) Ping() {
|
868
|
915
|
session.Send(nil, "", "PING", session.client.Nick())
|
869
|
916
|
}
|
870
|
917
|
|
|
918
|
+// tryResume tries to resume if the client asked us to.
|
|
919
|
+func (session *Session) tryResume() (success bool) {
|
|
920
|
+ var oldResumeID string
|
|
921
|
+
|
|
922
|
+ defer func() {
|
|
923
|
+ if success {
|
|
924
|
+ // "On a successful request, the server [...] terminates the old client's connection"
|
|
925
|
+ oldSession := session.client.GetSessionByResumeID(oldResumeID)
|
|
926
|
+ if oldSession != nil {
|
|
927
|
+ session.client.destroy(oldSession)
|
|
928
|
+ }
|
|
929
|
+ } else {
|
|
930
|
+ session.resumeDetails = nil
|
|
931
|
+ }
|
|
932
|
+ }()
|
|
933
|
+
|
|
934
|
+ client := session.client
|
|
935
|
+ server := client.server
|
|
936
|
+ config := server.Config()
|
|
937
|
+
|
|
938
|
+ oldClient, oldResumeID := server.resumeManager.VerifyToken(client, session.resumeDetails.PresentedToken)
|
|
939
|
+ if oldClient == nil {
|
|
940
|
+ session.Send(nil, server.name, "FAIL", "RESUME", "INVALID_TOKEN", client.t("Cannot resume connection, token is not valid"))
|
|
941
|
+ return
|
|
942
|
+ }
|
|
943
|
+
|
|
944
|
+ resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS))
|
|
945
|
+ if !resumeAllowed {
|
|
946
|
+ session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection, old and new clients must have TLS"))
|
|
947
|
+ return
|
|
948
|
+ }
|
|
949
|
+
|
|
950
|
+ err := server.clients.Resume(oldClient, session)
|
|
951
|
+ if err != nil {
|
|
952
|
+ session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection"))
|
|
953
|
+ return
|
|
954
|
+ }
|
|
955
|
+
|
|
956
|
+ success = true
|
|
957
|
+ client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", oldClient.Nick()))
|
|
958
|
+
|
|
959
|
+ return
|
|
960
|
+}
|
|
961
|
+
|
|
962
|
+// playResume is called from the session's fresh goroutine after a resume;
|
|
963
|
+// it sends notifications to friends, then plays the registration burst and replays
|
|
964
|
+// stored history to the session
|
|
965
|
+func (session *Session) playResume() {
|
|
966
|
+ client := session.client
|
|
967
|
+ server := client.server
|
|
968
|
+ config := server.Config()
|
|
969
|
+
|
|
970
|
+ friends := make(ClientSet)
|
|
971
|
+ var oldestLostMessage time.Time
|
|
972
|
+
|
|
973
|
+ // work out how much time, if any, is not covered by history buffers
|
|
974
|
+ // assume that a persistent buffer covers the whole resume period
|
|
975
|
+ for _, channel := range client.Channels() {
|
|
976
|
+ for _, member := range channel.auditoriumFriends(client) {
|
|
977
|
+ friends.Add(member)
|
|
978
|
+ }
|
|
979
|
+ status, _, _ := channel.historyStatus(config)
|
|
980
|
+ if status == HistoryEphemeral {
|
|
981
|
+ lastDiscarded := channel.history.LastDiscarded()
|
|
982
|
+ if oldestLostMessage.Before(lastDiscarded) {
|
|
983
|
+ oldestLostMessage = lastDiscarded
|
|
984
|
+ }
|
|
985
|
+ }
|
|
986
|
+ }
|
|
987
|
+ cHistoryStatus, _ := client.historyStatus(config)
|
|
988
|
+ if cHistoryStatus == HistoryEphemeral {
|
|
989
|
+ lastDiscarded := client.history.LastDiscarded()
|
|
990
|
+ if oldestLostMessage.Before(lastDiscarded) {
|
|
991
|
+ oldestLostMessage = lastDiscarded
|
|
992
|
+ }
|
|
993
|
+ }
|
|
994
|
+
|
|
995
|
+ timestamp := session.resumeDetails.Timestamp
|
|
996
|
+ gap := oldestLostMessage.Sub(timestamp)
|
|
997
|
+ session.resumeDetails.HistoryIncomplete = gap > 0 || timestamp.IsZero()
|
|
998
|
+ gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
|
|
999
|
+
|
|
1000
|
+ details := client.Details()
|
|
1001
|
+ oldNickmask := details.nickMask
|
|
1002
|
+ client.lookupHostname(session, true)
|
|
1003
|
+ hostname := client.Hostname() // may be a vhost
|
|
1004
|
+ timestampString := timestamp.Format(IRCv3TimestampFormat)
|
|
1005
|
+
|
|
1006
|
+ // send quit/resume messages to friends
|
|
1007
|
+ for friend := range friends {
|
|
1008
|
+ if friend == client {
|
|
1009
|
+ continue
|
|
1010
|
+ }
|
|
1011
|
+ for _, fSession := range friend.Sessions() {
|
|
1012
|
+ if fSession.capabilities.Has(caps.Resume) {
|
|
1013
|
+ if !session.resumeDetails.HistoryIncomplete {
|
|
1014
|
+ fSession.Send(nil, oldNickmask, "RESUMED", hostname, "ok")
|
|
1015
|
+ } else if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
|
|
1016
|
+ fSession.Send(nil, oldNickmask, "RESUMED", hostname, timestampString)
|
|
1017
|
+ } else {
|
|
1018
|
+ fSession.Send(nil, oldNickmask, "RESUMED", hostname)
|
|
1019
|
+ }
|
|
1020
|
+ } else {
|
|
1021
|
+ if !session.resumeDetails.HistoryIncomplete {
|
|
1022
|
+ fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected"))
|
|
1023
|
+ } else if session.resumeDetails.HistoryIncomplete && !timestamp.IsZero() {
|
|
1024
|
+ fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of message history lost)"), gapSeconds))
|
|
1025
|
+ } else {
|
|
1026
|
+ fSession.Send(nil, oldNickmask, "QUIT", friend.t("Client reconnected (message history may have been lost)"))
|
|
1027
|
+ }
|
|
1028
|
+ }
|
|
1029
|
+ }
|
|
1030
|
+ }
|
|
1031
|
+
|
|
1032
|
+ if session.resumeDetails.HistoryIncomplete {
|
|
1033
|
+ if !timestamp.IsZero() {
|
|
1034
|
+ session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
|
|
1035
|
+ } else {
|
|
1036
|
+ session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Resume may have lost some message history"))
|
|
1037
|
+ }
|
|
1038
|
+ }
|
|
1039
|
+
|
|
1040
|
+ session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick)
|
|
1041
|
+
|
|
1042
|
+ server.playRegistrationBurst(session)
|
|
1043
|
+
|
|
1044
|
+ for _, channel := range client.Channels() {
|
|
1045
|
+ channel.Resume(session, timestamp)
|
|
1046
|
+ }
|
|
1047
|
+
|
|
1048
|
+ // replay direct PRIVSMG history
|
|
1049
|
+ _, privmsgSeq, err := server.GetHistorySequence(nil, client, "")
|
|
1050
|
+ if !timestamp.IsZero() && err == nil && privmsgSeq != nil {
|
|
1051
|
+ after := history.Selector{Time: timestamp}
|
|
1052
|
+ items, _ := privmsgSeq.Between(after, history.Selector{}, config.History.ZNCMax)
|
|
1053
|
+ if len(items) != 0 {
|
|
1054
|
+ rb := NewResponseBuffer(session)
|
|
1055
|
+ client.replayPrivmsgHistory(rb, items, "")
|
|
1056
|
+ rb.Send(true)
|
|
1057
|
+ }
|
|
1058
|
+ }
|
|
1059
|
+
|
|
1060
|
+ session.resumeDetails = nil
|
|
1061
|
+}
|
|
1062
|
+
|
871
|
1063
|
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, target string) {
|
872
|
1064
|
var batchID string
|
873
|
1065
|
details := client.Details()
|
|
@@ -1200,6 +1392,8 @@ func (client *Client) destroy(session *Session) {
|
1200
|
1392
|
client.stateMutex.Lock()
|
1201
|
1393
|
|
1202
|
1394
|
details := client.detailsNoMutex()
|
|
1395
|
+ brbState := client.brbTimer.state
|
|
1396
|
+ brbAt := client.brbTimer.brbAt
|
1203
|
1397
|
wasReattach := session != nil && session.client != client
|
1204
|
1398
|
sessionRemoved := false
|
1205
|
1399
|
registered := client.registered
|
|
@@ -1241,7 +1435,9 @@ func (client *Client) destroy(session *Session) {
|
1241
|
1435
|
}
|
1242
|
1436
|
|
1243
|
1437
|
// should we destroy the whole client this time?
|
1244
|
|
- shouldDestroy := !client.destroyed && remainingSessions == 0 && !alwaysOn
|
|
1438
|
+ // BRB is not respected if this is a destroy of the whole client (i.e., session == nil)
|
|
1439
|
+ brbEligible := session != nil && brbState == BrbEnabled
|
|
1440
|
+ shouldDestroy := !client.destroyed && remainingSessions == 0 && !brbEligible && !alwaysOn
|
1245
|
1441
|
// decrement stats on a true destroy, or for the removal of the last connected session
|
1246
|
1442
|
// of an always-on client
|
1247
|
1443
|
shouldDecrement := shouldDestroy || (alwaysOn && len(sessionsToDestroy) != 0 && len(client.sessions) == 0)
|
|
@@ -1287,6 +1483,7 @@ func (client *Client) destroy(session *Session) {
|
1287
|
1483
|
// send quit/error message to client if they haven't been sent already
|
1288
|
1484
|
client.Quit("", session)
|
1289
|
1485
|
quitMessage = session.quitMessage // doesn't need synch, we already detached
|
|
1486
|
+ session.SetDestroyed()
|
1290
|
1487
|
session.socket.Close()
|
1291
|
1488
|
|
1292
|
1489
|
// clean up monitor state
|
|
@@ -1345,6 +1542,8 @@ func (client *Client) destroy(session *Session) {
|
1345
|
1542
|
client.server.whoWas.Append(client.WhoWas())
|
1346
|
1543
|
}
|
1347
|
1544
|
|
|
1545
|
+ client.server.resumeManager.Delete(client)
|
|
1546
|
+
|
1348
|
1547
|
// alert monitors
|
1349
|
1548
|
if registered {
|
1350
|
1549
|
client.server.monitorManager.AlertAbout(details.nick, details.nickCasefolded, false)
|
|
@@ -1366,8 +1565,20 @@ func (client *Client) destroy(session *Session) {
|
1366
|
1565
|
client.server.clients.Remove(client)
|
1367
|
1566
|
|
1368
|
1567
|
// clean up self
|
|
1568
|
+ client.brbTimer.Disable()
|
|
1569
|
+
|
1369
|
1570
|
client.server.accounts.Logout(client)
|
1370
|
1571
|
|
|
1572
|
+ // this happens under failure to return from BRB
|
|
1573
|
+ if quitMessage == "" {
|
|
1574
|
+ if brbState == BrbDead && !brbAt.IsZero() {
|
|
1575
|
+ awayMessage := client.AwayMessage()
|
|
1576
|
+ if awayMessage == "" {
|
|
1577
|
+ awayMessage = "Disconnected" // auto-BRB
|
|
1578
|
+ }
|
|
1579
|
+ quitMessage = fmt.Sprintf("%s [%s ago]", awayMessage, time.Since(brbAt).Truncate(time.Second).String())
|
|
1580
|
+ }
|
|
1581
|
+ }
|
1371
|
1582
|
if quitMessage == "" {
|
1372
|
1583
|
quitMessage = "Exited"
|
1373
|
1584
|
}
|