Explorar el Código

Add very initial RESUME cap and command

tags/v0.11.0-alpha
Daniel Oaks hace 6 años
padre
commit
d09f085b1a
Se han modificado 8 ficheros con 199 adiciones y 20 borrados
  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 Ver fichero

14
 var (
14
 var (
15
 	// SupportedCapabilities are the caps we advertise.
15
 	// SupportedCapabilities are the caps we advertise.
16
 	// MaxLine, SASL and STS are set during server startup.
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
 	// CapValues are the actual values we advertise to v3.2 clients.
19
 	// CapValues are the actual values we advertise to v3.2 clients.
20
 	// actual values are set during server startup.
20
 	// actual values are set during server startup.

+ 2
- 0
irc/caps/constants.go Ver fichero

37
 	MultiPrefix Capability = "multi-prefix"
37
 	MultiPrefix Capability = "multi-prefix"
38
 	// Rename is this proposed capability: https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
38
 	// Rename is this proposed capability: https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
39
 	Rename Capability = "draft/rename"
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
 	// SASL is this IRCv3 capability: http://ircv3.net/specs/extensions/sasl-3.2.html
42
 	// SASL is this IRCv3 capability: http://ircv3.net/specs/extensions/sasl-3.2.html
41
 	SASL Capability = "sasl"
43
 	SASL Capability = "sasl"
42
 	// ServerTime is this IRCv3 capability: http://ircv3.net/specs/extensions/server-time-3.2.html
44
 	// ServerTime is this IRCv3 capability: http://ircv3.net/specs/extensions/server-time-3.2.html

+ 125
- 10
irc/client.go Ver fichero

69
 	rawHostname        string
69
 	rawHostname        string
70
 	realname           string
70
 	realname           string
71
 	registered         bool
71
 	registered         bool
72
+	resumeDetails      *ResumeDetails
72
 	saslInProgress     bool
73
 	saslInProgress     bool
73
 	saslMechanism      string
74
 	saslMechanism      string
74
 	saslValue          string
75
 	saslValue          string
294
 		return
295
 		return
295
 	}
296
 	}
296
 
297
 
298
+	// apply resume details if we're able to.
299
+	client.TryResume()
300
+
301
+	// finish registration
297
 	client.Touch()
302
 	client.Touch()
298
 	client.updateNickMask("")
303
 	client.updateNickMask("")
299
 	client.server.monitorManager.AlertAbout(client, true)
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
 // IdleTime returns how long this client's been idle.
401
 // IdleTime returns how long this client's been idle.
303
 func (client *Client) IdleTime() time.Duration {
402
 func (client *Client) IdleTime() time.Duration {
304
 	client.stateMutex.RLock()
403
 	client.stateMutex.RLock()
494
 }
593
 }
495
 
594
 
496
 // destroy gets rid of a client, removes them from server lists etc.
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
 	// allow destroy() to execute at most once
597
 	// allow destroy() to execute at most once
499
 	client.stateMutex.Lock()
598
 	client.stateMutex.Lock()
500
 	isDestroyed := client.isDestroyed
599
 	isDestroyed := client.isDestroyed
504
 		return
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
 	// send quit/error message to client if they haven't been sent already
612
 	// send quit/error message to client if they haven't been sent already
510
 	client.Quit("Connection closed")
613
 	client.Quit("Connection closed")
511
 
614
 
512
-	client.server.whoWas.Append(client)
513
 	friends := client.Friends()
615
 	friends := client.Friends()
514
 	friends.Remove(client)
616
 	friends.Remove(client)
617
+	if !beingResumed {
618
+		client.server.whoWas.Append(client)
619
+	}
515
 
620
 
516
 	// remove from connection limits
621
 	// remove from connection limits
517
 	ipaddr := client.IP()
622
 	ipaddr := client.IP()
527
 
632
 
528
 	// clean up channels
633
 	// clean up channels
529
 	for _, channel := range client.Channels() {
634
 	for _, channel := range client.Channels() {
530
-		channel.Quit(client)
635
+		if !beingResumed {
636
+			channel.Quit(client)
637
+		}
531
 		for _, member := range channel.Members() {
638
 		for _, member := range channel.Members() {
532
 			friends.Add(member)
639
 			friends.Add(member)
533
 		}
640
 		}
534
 	}
641
 	}
535
 
642
 
536
 	// clean up server
643
 	// clean up server
537
-	client.server.clients.Remove(client)
644
+	if !beingResumed {
645
+		client.server.clients.Remove(client)
646
+	}
538
 
647
 
539
 	// clean up self
648
 	// clean up self
540
 	if client.idletimer != nil {
649
 	if client.idletimer != nil {
544
 	client.socket.Close()
653
 	client.socket.Close()
545
 
654
 
546
 	// send quit messages to friends
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
 	if !client.exitedSnomaskSent {
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 Ver fichero

223
 		handler:   renameHandler,
223
 		handler:   renameHandler,
224
 		minParams: 2,
224
 		minParams: 2,
225
 	},
225
 	},
226
+	"RESUME": {
227
+		handler:      resumeHandler,
228
+		usablePreReg: true,
229
+		minParams:    1,
230
+	},
226
 	"SANICK": {
231
 	"SANICK": {
227
 		handler:   sanickHandler,
232
 		handler:   sanickHandler,
228
 		minParams: 2,
233
 		minParams: 2,

+ 6
- 0
irc/help.go Ver fichero

423
 		text: `REHASH
423
 		text: `REHASH
424
 
424
 
425
 Reloads the config file and updates TLS certificates on listeners`,
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
 	"time": {
433
 	"time": {
428
 		text: `TIME [server]
434
 		text: `TIME [server]

+ 21
- 9
irc/idletimer.go Ver fichero

7
 	"fmt"
7
 	"fmt"
8
 	"sync"
8
 	"sync"
9
 	"time"
9
 	"time"
10
+
11
+	"github.com/oragono/oragono/irc/caps"
10
 )
12
 )
11
 
13
 
12
 const (
14
 const (
14
 	RegisterTimeout = time.Minute
16
 	RegisterTimeout = time.Minute
15
 	// IdleTimeout is how long without traffic before a registered client is considered idle.
17
 	// IdleTimeout is how long without traffic before a registered client is considered idle.
16
 	IdleTimeout = time.Minute + time.Second*30
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
 	// QuitTimeout is how long without traffic before an idle client is disconnected
21
 	// QuitTimeout is how long without traffic before an idle client is disconnected
18
 	QuitTimeout = time.Minute
22
 	QuitTimeout = time.Minute
19
 )
23
 )
33
 	sync.Mutex // tier 1
37
 	sync.Mutex // tier 1
34
 
38
 
35
 	// immutable after construction
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
 	// mutable
46
 	// mutable
42
 	state TimerState
47
 	state TimerState
46
 // NewIdleTimer sets up a new IdleTimer using constant timeouts.
51
 // NewIdleTimer sets up a new IdleTimer using constant timeouts.
47
 func NewIdleTimer(client *Client) *IdleTimer {
52
 func NewIdleTimer(client *Client) *IdleTimer {
48
 	it := IdleTimer{
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
 	return &it
60
 	return &it
55
 }
61
 }
119
 	case TimerUnregistered:
125
 	case TimerUnregistered:
120
 		nextTimeout = it.registerTimeout
126
 		nextTimeout = it.registerTimeout
121
 	case TimerActive:
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
 	case TimerIdle:
135
 	case TimerIdle:
124
 		nextTimeout = it.quitTimeout
136
 		nextTimeout = it.quitTimeout
125
 	case TimerDead:
137
 	case TimerDead:

+ 3
- 0
irc/numerics.go Ver fichero

192
 	ERR_REG_INVALID_CALLBACK        = "929"
192
 	ERR_REG_INVALID_CALLBACK        = "929"
193
 	ERR_TOOMANYLANGUAGES            = "981"
193
 	ERR_TOOMANYLANGUAGES            = "981"
194
 	ERR_NOLANGUAGE                  = "982"
194
 	ERR_NOLANGUAGE                  = "982"
195
+
196
+	// draft numerics
197
+	ERR_CANNOT_RESUME = "999"
195
 )
198
 )

+ 36
- 0
irc/server.go Ver fichero

2071
 	return false
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
 // USERHOST <nickname> [<nickname> <nickname> ...]
2110
 // USERHOST <nickname> [<nickname> <nickname> ...]
2075
 func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2111
 func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
2076
 	returnedNicks := make(map[string]bool)
2112
 	returnedNicks := make(map[string]bool)

Loading…
Cancelar
Guardar