Browse Source

Add very initial RESUME cap and command

tags/v0.11.0-alpha
Daniel Oaks 6 years ago
parent
commit
d09f085b1a
8 changed files with 199 additions and 20 deletions
  1. 1
    1
      irc/capability.go
  2. 2
    0
      irc/caps/constants.go
  3. 125
    10
      irc/client.go
  4. 5
    0
      irc/commands.go
  5. 6
    0
      irc/help.go
  6. 21
    9
      irc/idletimer.go
  7. 3
    0
      irc/numerics.go
  8. 36
    0
      irc/server.go

+ 1
- 1
irc/capability.go View File

@@ -14,7 +14,7 @@ import (
14 14
 var (
15 15
 	// SupportedCapabilities are the caps we advertise.
16 16
 	// MaxLine, SASL and STS are set during server startup.
17
-	SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.ServerTime, caps.UserhostInNames)
17
+	SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.Resume, caps.ServerTime, caps.UserhostInNames)
18 18
 
19 19
 	// CapValues are the actual values we advertise to v3.2 clients.
20 20
 	// actual values are set during server startup.

+ 2
- 0
irc/caps/constants.go View File

@@ -37,6 +37,8 @@ const (
37 37
 	MultiPrefix Capability = "multi-prefix"
38 38
 	// Rename is this proposed capability: https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
39 39
 	Rename Capability = "draft/rename"
40
+	// Resume is this proposed capability: https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
41
+	Resume Capability = "draft/resume"
40 42
 	// SASL is this IRCv3 capability: http://ircv3.net/specs/extensions/sasl-3.2.html
41 43
 	SASL Capability = "sasl"
42 44
 	// ServerTime is this IRCv3 capability: http://ircv3.net/specs/extensions/server-time-3.2.html

+ 125
- 10
irc/client.go View File

@@ -69,6 +69,7 @@ type Client struct {
69 69
 	rawHostname        string
70 70
 	realname           string
71 71
 	registered         bool
72
+	resumeDetails      *ResumeDetails
72 73
 	saslInProgress     bool
73 74
 	saslMechanism      string
74 75
 	saslValue          string
@@ -294,11 +295,109 @@ func (client *Client) Register() {
294 295
 		return
295 296
 	}
296 297
 
298
+	// apply resume details if we're able to.
299
+	client.TryResume()
300
+
301
+	// finish registration
297 302
 	client.Touch()
298 303
 	client.updateNickMask("")
299 304
 	client.server.monitorManager.AlertAbout(client, true)
300 305
 }
301 306
 
307
+// TryResume tries to resume if the client asked us to.
308
+func (client *Client) TryResume() {
309
+	if client.resumeDetails == nil {
310
+		return
311
+	}
312
+
313
+	server := client.server
314
+
315
+	// just grab these mutexes for safety. later we can work out whether we can grab+release them earlier
316
+	server.clients.Lock()
317
+	defer server.clients.Unlock()
318
+	server.channels.Lock()
319
+	defer server.channels.Unlock()
320
+
321
+	oldnick := client.resumeDetails.OldNick
322
+	timestamp := client.resumeDetails.Timestamp
323
+	var timestampString string
324
+	if timestamp != nil {
325
+		timestampString := timestamp.UTC().Format("2006-01-02T15:04:05.999Z")
326
+	}
327
+
328
+	oldClient := server.clients.Get(oldnick)
329
+	if oldClient == nil {
330
+		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, old client not found")
331
+		return
332
+	}
333
+
334
+	oldAccountName := oldClient.AccountName()
335
+	newAccountName := client.AccountName()
336
+
337
+	if oldAccountName == "" || newAccountName == "" || oldAccountName != newAccountName {
338
+		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, old and new clients must be logged into the same account")
339
+		return
340
+	}
341
+
342
+	if !oldClient.HasMode(TLS) || !client.HasMode(TLS) {
343
+		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, old and new clients must have TLS")
344
+		return
345
+	}
346
+
347
+	// send RESUMED to the reconnecting client
348
+	if timestamp == nil {
349
+		client.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname())
350
+	} else {
351
+		client.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname(), timestampString)
352
+	}
353
+
354
+	// send QUIT/RESUMED to friends
355
+	for friend := range oldClient.Friends() {
356
+		if friend.capabilities.Has(caps.Resume) {
357
+			if timestamp == nil {
358
+				friend.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname())
359
+			} else {
360
+				friend.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname(), timestampString)
361
+			}
362
+		} else {
363
+			friend.Send(nil, oldClient.NickMaskString(), "QUIT", "Client reconnected")
364
+		}
365
+	}
366
+
367
+	// apply old client's details to new client
368
+	client.nick = oldClient.nick
369
+
370
+	for channel := range oldClient.channels {
371
+		channel.stateMutex.Lock()
372
+
373
+		oldModeSet := channel.members[oldClient]
374
+		channel.members.Remove(oldClient)
375
+		channel.members[client] = oldModeSet
376
+		channel.regenerateMembersCache()
377
+
378
+		// send join for old clients
379
+		for member := range channel.members {
380
+			if member.capabilities.Has(caps.Resume) {
381
+				continue
382
+			}
383
+
384
+			if member.capabilities.Has(caps.ExtendedJoin) {
385
+				member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
386
+			} else {
387
+				member.Send(nil, client.nickMaskString, "JOIN", channel.name)
388
+			}
389
+
390
+			//TODO(dan): send priv modes
391
+		}
392
+
393
+		channel.stateMutex.Unlock()
394
+	}
395
+
396
+	server.clients.byNick[oldnick] = client
397
+
398
+	oldClient.destroy()
399
+}
400
+
302 401
 // IdleTime returns how long this client's been idle.
303 402
 func (client *Client) IdleTime() time.Duration {
304 403
 	client.stateMutex.RLock()
@@ -494,7 +593,7 @@ func (client *Client) Quit(message string) {
494 593
 }
495 594
 
496 595
 // destroy gets rid of a client, removes them from server lists etc.
497
-func (client *Client) destroy() {
596
+func (client *Client) destroy(beingResumed bool) {
498 597
 	// allow destroy() to execute at most once
499 598
 	client.stateMutex.Lock()
500 599
 	isDestroyed := client.isDestroyed
@@ -504,14 +603,20 @@ func (client *Client) destroy() {
504 603
 		return
505 604
 	}
506 605
 
507
-	client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick))
606
+	if beingResumed {
607
+		client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", client.nick))
608
+	} else {
609
+		client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick))
610
+	}
508 611
 
509 612
 	// send quit/error message to client if they haven't been sent already
510 613
 	client.Quit("Connection closed")
511 614
 
512
-	client.server.whoWas.Append(client)
513 615
 	friends := client.Friends()
514 616
 	friends.Remove(client)
617
+	if !beingResumed {
618
+		client.server.whoWas.Append(client)
619
+	}
515 620
 
516 621
 	// remove from connection limits
517 622
 	ipaddr := client.IP()
@@ -527,14 +632,18 @@ func (client *Client) destroy() {
527 632
 
528 633
 	// clean up channels
529 634
 	for _, channel := range client.Channels() {
530
-		channel.Quit(client)
635
+		if !beingResumed {
636
+			channel.Quit(client)
637
+		}
531 638
 		for _, member := range channel.Members() {
532 639
 			friends.Add(member)
533 640
 		}
534 641
 	}
535 642
 
536 643
 	// clean up server
537
-	client.server.clients.Remove(client)
644
+	if !beingResumed {
645
+		client.server.clients.Remove(client)
646
+	}
538 647
 
539 648
 	// clean up self
540 649
 	if client.idletimer != nil {
@@ -544,14 +653,20 @@ func (client *Client) destroy() {
544 653
 	client.socket.Close()
545 654
 
546 655
 	// send quit messages to friends
547
-	for friend := range friends {
548
-		if client.quitMessage == "" {
549
-			client.quitMessage = "Exited"
656
+	if !beingResumed {
657
+		for friend := range friends {
658
+			if client.quitMessage == "" {
659
+				client.quitMessage = "Exited"
660
+			}
661
+			friend.Send(nil, client.nickMaskString, "QUIT", client.quitMessage)
550 662
 		}
551
-		friend.Send(nil, client.nickMaskString, "QUIT", client.quitMessage)
552 663
 	}
553 664
 	if !client.exitedSnomaskSent {
554
-		client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), client.nick))
665
+		if beingResumed {
666
+			client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick))
667
+		} else {
668
+			client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), client.nick))
669
+		}
555 670
 	}
556 671
 }
557 672
 

+ 5
- 0
irc/commands.go View File

@@ -223,6 +223,11 @@ var Commands = map[string]Command{
223 223
 		handler:   renameHandler,
224 224
 		minParams: 2,
225 225
 	},
226
+	"RESUME": {
227
+		handler:      resumeHandler,
228
+		usablePreReg: true,
229
+		minParams:    1,
230
+	},
226 231
 	"SANICK": {
227 232
 		handler:   sanickHandler,
228 233
 		minParams: 2,

+ 6
- 0
irc/help.go View File

@@ -423,6 +423,12 @@ Indicates that you're leaving the server, and shows everyone the given reason.`,
423 423
 		text: `REHASH
424 424
 
425 425
 Reloads the config file and updates TLS certificates on listeners`,
426
+	},
427
+	"resume": {
428
+		text: `RESUME <oldnick> [timestamp]
429
+
430
+Sent before registration has completed, this indicates that the client wants to
431
+resume their old connection <oldnick>.`,
426 432
 	},
427 433
 	"time": {
428 434
 		text: `TIME [server]

+ 21
- 9
irc/idletimer.go View File

@@ -7,6 +7,8 @@ import (
7 7
 	"fmt"
8 8
 	"sync"
9 9
 	"time"
10
+
11
+	"github.com/oragono/oragono/irc/caps"
10 12
 )
11 13
 
12 14
 const (
@@ -14,6 +16,8 @@ const (
14 16
 	RegisterTimeout = time.Minute
15 17
 	// IdleTimeout is how long without traffic before a registered client is considered idle.
16 18
 	IdleTimeout = time.Minute + time.Second*30
19
+	// IdleTimeoutWithResumeCap is how long without traffic before a registered client is considered idle, when they have the resume capability.
20
+	IdleTimeoutWithResumeCap = time.Minute*2 + time.Second*30
17 21
 	// QuitTimeout is how long without traffic before an idle client is disconnected
18 22
 	QuitTimeout = time.Minute
19 23
 )
@@ -33,10 +37,11 @@ type IdleTimer struct {
33 37
 	sync.Mutex // tier 1
34 38
 
35 39
 	// immutable after construction
36
-	registerTimeout time.Duration
37
-	idleTimeout     time.Duration
38
-	quitTimeout     time.Duration
39
-	client          *Client
40
+	registerTimeout       time.Duration
41
+	idleTimeout           time.Duration
42
+	idleTimeoutWithResume time.Duration
43
+	quitTimeout           time.Duration
44
+	client                *Client
40 45
 
41 46
 	// mutable
42 47
 	state TimerState
@@ -46,10 +51,11 @@ type IdleTimer struct {
46 51
 // NewIdleTimer sets up a new IdleTimer using constant timeouts.
47 52
 func NewIdleTimer(client *Client) *IdleTimer {
48 53
 	it := IdleTimer{
49
-		registerTimeout: RegisterTimeout,
50
-		idleTimeout:     IdleTimeout,
51
-		quitTimeout:     QuitTimeout,
52
-		client:          client,
54
+		registerTimeout:       RegisterTimeout,
55
+		idleTimeout:           IdleTimeout,
56
+		idleTimeoutWithResume: IdleTimeoutWithResumeCap,
57
+		quitTimeout:           QuitTimeout,
58
+		client:                client,
53 59
 	}
54 60
 	return &it
55 61
 }
@@ -119,7 +125,13 @@ func (it *IdleTimer) resetTimeout() {
119 125
 	case TimerUnregistered:
120 126
 		nextTimeout = it.registerTimeout
121 127
 	case TimerActive:
122
-		nextTimeout = it.idleTimeout
128
+		// if they have the resume cap, wait longer before pinging them out
129
+		// to give them a chance to resume their connection
130
+		if it.client.capabilities.Has(caps.Resume) {
131
+			nextTimeout = it.idleTimeoutWithResume
132
+		} else {
133
+			nextTimeout = it.idleTimeout
134
+		}
123 135
 	case TimerIdle:
124 136
 		nextTimeout = it.quitTimeout
125 137
 	case TimerDead:

+ 3
- 0
irc/numerics.go View File

@@ -192,4 +192,7 @@ const (
192 192
 	ERR_REG_INVALID_CALLBACK        = "929"
193 193
 	ERR_TOOMANYLANGUAGES            = "981"
194 194
 	ERR_NOLANGUAGE                  = "982"
195
+
196
+	// draft numerics
197
+	ERR_CANNOT_RESUME = "999"
195 198
 )

+ 36
- 0
irc/server.go View File

@@ -2071,6 +2071,42 @@ func lusersHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2071 2071
 	return false
2072 2072
 }
2073 2073
 
2074
+// ResumeDetails are the details that we use to resume connections.
2075
+type ResumeDetails struct {
2076
+	OldNick   string
2077
+	Timestamp *time.Time
2078
+}
2079
+
2080
+// RESUME <oldnick> [timestamp]
2081
+func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2082
+	oldnick := msg.Params[0]
2083
+
2084
+	if strings.Contains(oldnick, " ") {
2085
+		client.Send(nil, server.name, ERR_CANNOT_RESUME, "*", "Cannot resume connection, old nickname contains spaces")
2086
+		return false
2087
+	}
2088
+
2089
+	if client.Registered() {
2090
+		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, connection registration has already been completed")
2091
+		return false
2092
+	}
2093
+
2094
+	var timestamp *time.Time
2095
+	if 1 < len(msg.Params) {
2096
+		timestamp, err := time.Parse("2006-01-02T15:04:05.999Z", msg.Params[1])
2097
+		if err != nil {
2098
+			client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it")
2099
+		}
2100
+	}
2101
+
2102
+	client.resumeDetails = ResumeDetails{
2103
+		OldNick:   oldnick,
2104
+		Timestamp: timestamp,
2105
+	}
2106
+
2107
+	return true
2108
+}
2109
+
2074 2110
 // USERHOST <nickname> [<nickname> <nickname> ...]
2075 2111
 func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2076 2112
 	returnedNicks := make(map[string]bool)

Loading…
Cancel
Save