瀏覽代碼

draft/resume-0.2 implementation, message history support

tags/v1.0.0-rc1
Shivaram Lingamneni 5 年之前
父節點
當前提交
a0bf548fc5

+ 1
- 0
Makefile 查看文件

20
 	python3 ./gencapdefs.py | diff - ${capdef_file}
20
 	python3 ./gencapdefs.py | diff - ${capdef_file}
21
 	cd irc && go test . && go vet .
21
 	cd irc && go test . && go vet .
22
 	cd irc/caps && go test . && go vet .
22
 	cd irc/caps && go test . && go vet .
23
+	cd irc/history && go test . && go vet .
23
 	cd irc/isupport && go test . && go vet .
24
 	cd irc/isupport && go test . && go vet .
24
 	cd irc/modes && go test . && go vet .
25
 	cd irc/modes && go test . && go vet .
25
 	cd irc/passwd && go test . && go vet .
26
 	cd irc/passwd && go test . && go vet .

+ 1
- 1
gencapdefs.py 查看文件

107
     ),
107
     ),
108
     CapDef(
108
     CapDef(
109
         identifier="Resume",
109
         identifier="Resume",
110
-        name="draft/resume",
110
+        name="draft/resume-0.2",
111
         url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
111
         url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
112
         standard="proposed IRCv3",
112
         standard="proposed IRCv3",
113
     ),
113
     ),

+ 3
- 7
irc/accounts.go 查看文件

4
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
-	"crypto/rand"
8
-	"crypto/subtle"
9
-	"encoding/hex"
10
 	"encoding/json"
7
 	"encoding/json"
11
 	"errors"
8
 	"errors"
12
 	"fmt"
9
 	"fmt"
20
 
17
 
21
 	"github.com/oragono/oragono/irc/caps"
18
 	"github.com/oragono/oragono/irc/caps"
22
 	"github.com/oragono/oragono/irc/passwd"
19
 	"github.com/oragono/oragono/irc/passwd"
20
+	"github.com/oragono/oragono/irc/utils"
23
 	"github.com/tidwall/buntdb"
21
 	"github.com/tidwall/buntdb"
24
 )
22
 )
25
 
23
 
336
 
334
 
337
 func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
335
 func (am *AccountManager) dispatchMailtoCallback(client *Client, casefoldedAccount string, callbackValue string) (code string, err error) {
338
 	config := am.server.AccountConfig().Registration.Callbacks.Mailto
336
 	config := am.server.AccountConfig().Registration.Callbacks.Mailto
339
-	buf := make([]byte, 16)
340
-	rand.Read(buf)
341
-	code = hex.EncodeToString(buf)
337
+	code = utils.GenerateSecretToken()
342
 
338
 
343
 	subject := config.VerifyMessageSubject
339
 	subject := config.VerifyMessageSubject
344
 	if subject == "" {
340
 	if subject == "" {
412
 			storedCode, err := tx.Get(verificationCodeKey)
408
 			storedCode, err := tx.Get(verificationCodeKey)
413
 			if err == nil {
409
 			if err == nil {
414
 				// this is probably unnecessary
410
 				// this is probably unnecessary
415
-				if storedCode == "" || subtle.ConstantTimeCompare([]byte(code), []byte(storedCode)) == 1 {
411
+				if storedCode == "" || utils.SecretTokensMatch(storedCode, code) {
416
 					success = true
412
 					success = true
417
 				}
413
 				}
418
 			}
414
 			}

+ 2
- 2
irc/caps/defs.go 查看文件

73
 	// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
73
 	// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
74
 	Rename Capability = iota
74
 	Rename Capability = iota
75
 
75
 
76
-	// Resume is the proposed IRCv3 capability named "draft/resume":
76
+	// Resume is the proposed IRCv3 capability named "draft/resume-0.2":
77
 	// https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
77
 	// https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
78
 	Resume Capability = iota
78
 	Resume Capability = iota
79
 
79
 
112
 		"draft/message-tags-0.2",
112
 		"draft/message-tags-0.2",
113
 		"multi-prefix",
113
 		"multi-prefix",
114
 		"draft/rename",
114
 		"draft/rename",
115
-		"draft/resume",
115
+		"draft/resume-0.2",
116
 		"sasl",
116
 		"sasl",
117
 		"server-time",
117
 		"server-time",
118
 		"sts",
118
 		"sts",

+ 159
- 15
irc/channel.go 查看文件

7
 
7
 
8
 import (
8
 import (
9
 	"bytes"
9
 	"bytes"
10
-	"crypto/subtle"
11
 	"fmt"
10
 	"fmt"
12
 	"strconv"
11
 	"strconv"
13
 	"time"
12
 	"time"
16
 
15
 
17
 	"github.com/goshuirc/irc-go/ircmsg"
16
 	"github.com/goshuirc/irc-go/ircmsg"
18
 	"github.com/oragono/oragono/irc/caps"
17
 	"github.com/oragono/oragono/irc/caps"
18
+	"github.com/oragono/oragono/irc/history"
19
 	"github.com/oragono/oragono/irc/modes"
19
 	"github.com/oragono/oragono/irc/modes"
20
+	"github.com/oragono/oragono/irc/utils"
20
 )
21
 )
21
 
22
 
22
 // Channel represents a channel that clients can join.
23
 // Channel represents a channel that clients can join.
39
 	topicSetTime      time.Time
40
 	topicSetTime      time.Time
40
 	userLimit         uint64
41
 	userLimit         uint64
41
 	accountToUMode    map[string]modes.Mode
42
 	accountToUMode    map[string]modes.Mode
43
+	history           history.Buffer
42
 }
44
 }
43
 
45
 
44
 // NewChannel creates a new channel from a `Server` and a `name`
46
 // NewChannel creates a new channel from a `Server` and a `name`
65
 		accountToUMode: make(map[string]modes.Mode),
67
 		accountToUMode: make(map[string]modes.Mode),
66
 	}
68
 	}
67
 
69
 
70
+	config := s.Config()
71
+
68
 	if regInfo != nil {
72
 	if regInfo != nil {
69
 		channel.applyRegInfo(regInfo)
73
 		channel.applyRegInfo(regInfo)
70
 	} else {
74
 	} else {
71
-		for _, mode := range s.DefaultChannelModes() {
75
+		for _, mode := range config.Channels.defaultModes {
72
 			channel.flags.SetMode(mode, true)
76
 			channel.flags.SetMode(mode, true)
73
 		}
77
 		}
74
 	}
78
 	}
75
 
79
 
80
+	channel.history.Initialize(config.History.ChannelLength)
81
+
76
 	return channel
82
 	return channel
77
 }
83
 }
78
 
84
 
214
 		prefix := modes.Prefixes(isMultiPrefix)
220
 		prefix := modes.Prefixes(isMultiPrefix)
215
 		if buffer.Len()+len(nick)+len(prefix)+1 > maxNamLen {
221
 		if buffer.Len()+len(nick)+len(prefix)+1 > maxNamLen {
216
 			namesLines = append(namesLines, buffer.String())
222
 			namesLines = append(namesLines, buffer.String())
217
-			// memset(&buffer, 0, sizeof(bytes.Buffer));
218
-			var newBuffer bytes.Buffer
219
-			buffer = newBuffer
223
+			buffer.Reset()
220
 		}
224
 		}
221
 		if buffer.Len() > 0 {
225
 		if buffer.Len() > 0 {
222
 			buffer.WriteString(" ")
226
 			buffer.WriteString(" ")
344
 // CheckKey returns true if the key is not set or matches the given key.
348
 // CheckKey returns true if the key is not set or matches the given key.
345
 func (channel *Channel) CheckKey(key string) bool {
349
 func (channel *Channel) CheckKey(key string) bool {
346
 	chkey := channel.Key()
350
 	chkey := channel.Key()
347
-	if chkey == "" {
348
-		return true
349
-	}
350
-
351
-	return subtle.ConstantTimeCompare([]byte(key), []byte(chkey)) == 1
351
+	return chkey == "" || utils.SecretTokensMatch(chkey, key)
352
 }
352
 }
353
 
353
 
354
 func (channel *Channel) IsEmpty() bool {
354
 func (channel *Channel) IsEmpty() bool {
462
 	if givenMode != 0 {
462
 	if givenMode != 0 {
463
 		rb.Add(nil, client.server.name, "MODE", chname, modestr, nick)
463
 		rb.Add(nil, client.server.name, "MODE", chname, modestr, nick)
464
 	}
464
 	}
465
+
466
+	channel.history.Add(history.Item{
467
+		Type:        history.Join,
468
+		Nick:        nickmask,
469
+		AccountName: accountName,
470
+	})
465
 }
471
 }
466
 
472
 
467
 // Part parts the given client from this channel, with the given message.
473
 // Part parts the given client from this channel, with the given message.
480
 	}
486
 	}
481
 	rb.Add(nil, nickmask, "PART", chname, message)
487
 	rb.Add(nil, nickmask, "PART", chname, message)
482
 
488
 
489
+	channel.history.Add(history.Item{
490
+		Type:        history.Part,
491
+		Nick:        nickmask,
492
+		AccountName: client.AccountName(),
493
+		Message:     utils.MakeSplitMessage(message, true),
494
+	})
495
+
483
 	client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", client.nick, chname))
496
 	client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", client.nick, chname))
484
 }
497
 }
485
 
498
 
499
+// Resume is called after a successful global resume to:
500
+// 1. Replace the old client with the new in the channel's data structures
501
+// 2. Send JOIN and MODE lines to channel participants (including the new client)
502
+// 3. Replay missed message history to the client
503
+func (channel *Channel) Resume(newClient, oldClient *Client, timestamp time.Time) {
504
+	now := time.Now()
505
+	channel.resumeAndAnnounce(newClient, oldClient)
506
+	if !timestamp.IsZero() {
507
+		channel.replayHistory(newClient, timestamp, now)
508
+	}
509
+}
510
+
511
+func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
512
+	var oldModeSet *modes.ModeSet
513
+
514
+	func() {
515
+		channel.joinPartMutex.Lock()
516
+		defer channel.joinPartMutex.Unlock()
517
+
518
+		defer channel.regenerateMembersCache()
519
+
520
+		channel.stateMutex.Lock()
521
+		defer channel.stateMutex.Unlock()
522
+
523
+		newClient.channels[channel] = true
524
+		oldModeSet = channel.members[oldClient]
525
+		if oldModeSet == nil {
526
+			oldModeSet = modes.NewModeSet()
527
+		}
528
+		channel.members.Remove(oldClient)
529
+		channel.members[newClient] = oldModeSet
530
+	}()
531
+
532
+	// construct fake modestring if necessary
533
+	oldModes := oldModeSet.String()
534
+	if 0 < len(oldModes) {
535
+		oldModes = "+" + oldModes
536
+	}
537
+
538
+	// send join for old clients
539
+	nick := newClient.Nick()
540
+	nickMask := newClient.NickMaskString()
541
+	accountName := newClient.AccountName()
542
+	realName := newClient.Realname()
543
+	for _, member := range channel.Members() {
544
+		if member.capabilities.Has(caps.Resume) {
545
+			continue
546
+		}
547
+
548
+		if member.capabilities.Has(caps.ExtendedJoin) {
549
+			member.Send(nil, nickMask, "JOIN", channel.name, accountName, realName)
550
+		} else {
551
+			member.Send(nil, nickMask, "JOIN", channel.name)
552
+		}
553
+
554
+		if 0 < len(oldModes) {
555
+			member.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick)
556
+		}
557
+	}
558
+
559
+	rb := NewResponseBuffer(newClient)
560
+	// use blocking i/o to synchronize with the later history replay
561
+	rb.SetBlocking(true)
562
+	if newClient.capabilities.Has(caps.ExtendedJoin) {
563
+		rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
564
+	} else {
565
+		rb.Add(nil, nickMask, "JOIN", channel.name)
566
+	}
567
+	channel.SendTopic(newClient, rb)
568
+	channel.Names(newClient, rb)
569
+	if 0 < len(oldModes) {
570
+		rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick)
571
+	}
572
+	rb.Send()
573
+}
574
+
575
+func (channel *Channel) replayHistory(newClient *Client, after time.Time, before time.Time) {
576
+	chname := channel.Name()
577
+	extendedJoin := newClient.capabilities.Has(caps.ExtendedJoin)
578
+
579
+	items, complete := channel.history.Between(after, before)
580
+	for _, item := range items {
581
+		switch item.Type {
582
+		case history.Privmsg:
583
+			newClient.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, "PRIVMSG", chname, item.Message)
584
+		case history.Notice:
585
+			newClient.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, "NOTICE", chname, item.Message)
586
+		case history.Join:
587
+			if extendedJoin {
588
+				newClient.sendInternal(true, item.Time, nil, item.Nick, "JOIN", chname, item.AccountName, "")
589
+			} else {
590
+				newClient.sendInternal(true, item.Time, nil, item.Nick, "JOIN", chname)
591
+			}
592
+		case history.Quit:
593
+			// XXX: send QUIT as PART to avoid having to correctly deduplicate and synchronize
594
+			// QUIT messages across channels
595
+			fallthrough
596
+		case history.Part:
597
+			newClient.sendInternal(true, item.Time, nil, item.Nick, "PART", chname, item.Message.Original)
598
+		case history.Kick:
599
+			newClient.sendInternal(true, item.Time, nil, item.Nick, "KICK", chname, item.Msgid, item.Message.Original)
600
+		}
601
+	}
602
+
603
+	if !complete && !newClient.resumeDetails.HistoryIncomplete {
604
+		// warn here if we didn't warn already
605
+		newClient.sendInternal(true, time.Time{}, nil, "HistServ", "NOTICE", chname, newClient.t("Some additional message history may have been lost"))
606
+	}
607
+}
608
+
486
 // SendTopic sends the channel topic to the given client.
609
 // SendTopic sends the channel topic to the given client.
487
 func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer) {
610
 func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer) {
488
 	if !channel.hasClient(client) {
611
 	if !channel.hasClient(client) {
622
 }
745
 }
623
 
746
 
624
 // SplitPrivMsg sends a private message to everyone in this channel.
747
 // SplitPrivMsg sends a private message to everyone in this channel.
625
-func (channel *Channel) SplitPrivMsg(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message SplitMessage, rb *ResponseBuffer) {
626
-	channel.sendSplitMessage(msgid, "PRIVMSG", minPrefix, clientOnlyTags, client, &message, rb)
748
+func (channel *Channel) SplitPrivMsg(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message utils.SplitMessage, rb *ResponseBuffer) {
749
+	channel.sendSplitMessage(msgid, "PRIVMSG", history.Privmsg, minPrefix, clientOnlyTags, client, &message, rb)
627
 }
750
 }
628
 
751
 
629
 // SplitNotice sends a private message to everyone in this channel.
752
 // SplitNotice sends a private message to everyone in this channel.
630
-func (channel *Channel) SplitNotice(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message SplitMessage, rb *ResponseBuffer) {
631
-	channel.sendSplitMessage(msgid, "NOTICE", minPrefix, clientOnlyTags, client, &message, rb)
753
+func (channel *Channel) SplitNotice(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message utils.SplitMessage, rb *ResponseBuffer) {
754
+	channel.sendSplitMessage(msgid, "NOTICE", history.Notice, minPrefix, clientOnlyTags, client, &message, rb)
632
 }
755
 }
633
 
756
 
634
-func (channel *Channel) sendSplitMessage(msgid, cmd string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *SplitMessage, rb *ResponseBuffer) {
757
+func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.ItemType, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *utils.SplitMessage, rb *ResponseBuffer) {
635
 	if !channel.CanSpeak(client) {
758
 	if !channel.CanSpeak(client) {
636
 		rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel"))
759
 		rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel"))
637
 		return
760
 		return
654
 			rb.AddSplitMessageFromClient(msgid, client, tagsToUse, cmd, channel.name, *message)
777
 			rb.AddSplitMessageFromClient(msgid, client, tagsToUse, cmd, channel.name, *message)
655
 		}
778
 		}
656
 	}
779
 	}
780
+
781
+	nickmask := client.NickMaskString()
782
+	account := client.AccountName()
783
+
657
 	for _, member := range channel.Members() {
784
 	for _, member := range channel.Members() {
658
 		if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
785
 		if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
659
 			// STATUSMSG
786
 			// STATUSMSG
668
 			tagsToUse = clientOnlyTags
795
 			tagsToUse = clientOnlyTags
669
 		}
796
 		}
670
 
797
 
798
+		// TODO(slingamn) evaluate an optimization where we reuse `nickmask` and `account`
671
 		if message == nil {
799
 		if message == nil {
672
 			member.SendFromClient(msgid, client, tagsToUse, cmd, channel.name)
800
 			member.SendFromClient(msgid, client, tagsToUse, cmd, channel.name)
673
 		} else {
801
 		} else {
674
 			member.SendSplitMsgFromClient(msgid, client, tagsToUse, cmd, channel.name, *message)
802
 			member.SendSplitMsgFromClient(msgid, client, tagsToUse, cmd, channel.name, *message)
675
 		}
803
 		}
676
 	}
804
 	}
805
+
806
+	channel.history.Add(history.Item{
807
+		Type:        histType,
808
+		Msgid:       msgid,
809
+		Message:     *message,
810
+		Nick:        nickmask,
811
+		AccountName: account,
812
+	})
677
 }
813
 }
678
 
814
 
679
 func (channel *Channel) applyModeToMember(client *Client, mode modes.Mode, op modes.ModeOp, nick string, rb *ResponseBuffer) (result *modes.ModeChange) {
815
 func (channel *Channel) applyModeToMember(client *Client, mode modes.Mode, op modes.ModeOp, nick string, rb *ResponseBuffer) (result *modes.ModeChange) {
806
 		member.Send(nil, clientMask, "KICK", channel.name, targetNick, comment)
942
 		member.Send(nil, clientMask, "KICK", channel.name, targetNick, comment)
807
 	}
943
 	}
808
 
944
 
945
+	channel.history.Add(history.Item{
946
+		Type:        history.Kick,
947
+		Nick:        clientMask,
948
+		Message:     utils.MakeSplitMessage(comment, true),
949
+		AccountName: target.AccountName(),
950
+		Msgid:       targetNick, // XXX abuse this field
951
+	})
952
+
809
 	channel.Quit(target)
953
 	channel.Quit(target)
810
 }
954
 }
811
 
955
 

+ 275
- 123
irc/client.go 查看文件

19
 	"github.com/goshuirc/irc-go/ircmsg"
19
 	"github.com/goshuirc/irc-go/ircmsg"
20
 	ident "github.com/oragono/go-ident"
20
 	ident "github.com/oragono/go-ident"
21
 	"github.com/oragono/oragono/irc/caps"
21
 	"github.com/oragono/oragono/irc/caps"
22
+	"github.com/oragono/oragono/irc/history"
22
 	"github.com/oragono/oragono/irc/modes"
23
 	"github.com/oragono/oragono/irc/modes"
23
 	"github.com/oragono/oragono/irc/sno"
24
 	"github.com/oragono/oragono/irc/sno"
24
 	"github.com/oragono/oragono/irc/utils"
25
 	"github.com/oragono/oragono/irc/utils"
26
 
27
 
27
 const (
28
 const (
28
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
29
 	// IdentTimeoutSeconds is how many seconds before our ident (username) check times out.
29
-	IdentTimeoutSeconds = 1.5
30
+	IdentTimeoutSeconds  = 1.5
31
+	IRCv3TimestampFormat = "2006-01-02T15:04:05.999Z"
30
 )
32
 )
31
 
33
 
32
 var (
34
 var (
33
 	LoopbackIP = net.ParseIP("127.0.0.1")
35
 	LoopbackIP = net.ParseIP("127.0.0.1")
34
 )
36
 )
35
 
37
 
38
+// ResumeDetails is a place to stash data at various stages of
39
+// the resume process: when handling the RESUME command itself,
40
+// when completing the registration, and when rejoining channels.
41
+type ResumeDetails struct {
42
+	OldClient         *Client
43
+	OldNick           string
44
+	OldNickMask       string
45
+	PresentedToken    string
46
+	Timestamp         time.Time
47
+	ResumedAt         time.Time
48
+	Channels          []string
49
+	HistoryIncomplete bool
50
+}
51
+
36
 // Client is an IRC client.
52
 // Client is an IRC client.
37
 type Client struct {
53
 type Client struct {
38
 	account            string
54
 	account            string
71
 	realname           string
87
 	realname           string
72
 	registered         bool
88
 	registered         bool
73
 	resumeDetails      *ResumeDetails
89
 	resumeDetails      *ResumeDetails
90
+	resumeToken        string
74
 	saslInProgress     bool
91
 	saslInProgress     bool
75
 	saslMechanism      string
92
 	saslMechanism      string
76
 	saslValue          string
93
 	saslValue          string
79
 	stateMutex         sync.RWMutex // tier 1
96
 	stateMutex         sync.RWMutex // tier 1
80
 	username           string
97
 	username           string
81
 	vhost              string
98
 	vhost              string
99
+	history            *history.Buffer
82
 }
100
 }
83
 
101
 
84
 // NewClient sets up a new client and starts its goroutine.
102
 // NewClient sets up a new client and starts its goroutine.
101
 		nick:           "*", // * is used until actual nick is given
119
 		nick:           "*", // * is used until actual nick is given
102
 		nickCasefolded: "*",
120
 		nickCasefolded: "*",
103
 		nickMaskString: "*", // * is used until actual nick is given
121
 		nickMaskString: "*", // * is used until actual nick is given
122
+		history:        history.NewHistoryBuffer(config.History.ClientLength),
104
 	}
123
 	}
105
 	client.languages = server.languages.Default()
124
 	client.languages = server.languages.Default()
106
 
125
 
350
 	}
369
 	}
351
 
370
 
352
 	server := client.server
371
 	server := client.server
353
-
354
-	// just grab these mutexes for safety. later we can work out whether we can grab+release them earlier
355
-	server.clients.Lock()
356
-	defer server.clients.Unlock()
357
-	server.channels.Lock()
358
-	defer server.channels.Unlock()
372
+	config := server.Config()
359
 
373
 
360
 	oldnick := client.resumeDetails.OldNick
374
 	oldnick := client.resumeDetails.OldNick
361
 	timestamp := client.resumeDetails.Timestamp
375
 	timestamp := client.resumeDetails.Timestamp
362
 	var timestampString string
376
 	var timestampString string
363
-	if timestamp != nil {
364
-		timestampString = timestamp.UTC().Format("2006-01-02T15:04:05.999Z")
377
+	if !timestamp.IsZero() {
378
+		timestampString = timestamp.UTC().Format(IRCv3TimestampFormat)
365
 	}
379
 	}
366
 
380
 
367
-	// can't use server.clients.Get since we hold server.clients' tier 1 mutex
368
-	casefoldedName, err := CasefoldName(oldnick)
369
-	if err != nil {
370
-		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old client not found"))
381
+	oldClient := server.clients.Get(oldnick)
382
+	if oldClient == nil {
383
+		client.Send(nil, server.name, "RESUME", "ERR", oldnick, client.t("Cannot resume connection, old client not found"))
384
+		client.resumeDetails = nil
371
 		return
385
 		return
372
 	}
386
 	}
387
+	oldNick := oldClient.Nick()
388
+	oldNickmask := oldClient.NickMaskString()
373
 
389
 
374
-	oldClient := server.clients.byNick[casefoldedName]
375
-	if oldClient == nil {
376
-		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old client not found"))
390
+	resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS))
391
+	if !resumeAllowed {
392
+		client.Send(nil, server.name, "RESUME", "ERR", oldnick, client.t("Cannot resume connection, old and new clients must have TLS"))
393
+		client.resumeDetails = nil
377
 		return
394
 		return
378
 	}
395
 	}
379
 
396
 
380
-	oldAccountName := oldClient.Account()
381
-	newAccountName := client.Account()
382
-
383
-	if oldAccountName == "" || newAccountName == "" || oldAccountName != newAccountName {
384
-		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old and new clients must be logged into the same account"))
397
+	oldResumeToken := oldClient.ResumeToken()
398
+	if oldResumeToken == "" || !utils.SecretTokensMatch(oldResumeToken, client.resumeDetails.PresentedToken) {
399
+		client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, invalid resume token"))
400
+		client.resumeDetails = nil
385
 		return
401
 		return
386
 	}
402
 	}
387
 
403
 
388
-	if !oldClient.HasMode(modes.TLS) || !client.HasMode(modes.TLS) {
389
-		client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, old and new clients must have TLS"))
404
+	err := server.clients.Resume(client, oldClient)
405
+	if err != nil {
406
+		client.resumeDetails = nil
407
+		client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection"))
390
 		return
408
 		return
391
 	}
409
 	}
392
 
410
 
393
-	// unmark the new client's nick as being occupied
394
-	server.clients.removeInternal(client)
411
+	// this is a bit racey
412
+	client.resumeDetails.ResumedAt = time.Now()
395
 
413
 
396
-	// send RESUMED to the reconnecting client
397
-	if timestamp == nil {
398
-		client.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname())
399
-	} else {
400
-		client.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname(), timestampString)
414
+	client.nickTimer.Touch()
415
+
416
+	// resume successful, proceed to copy client state (nickname, flags, etc.)
417
+	// after this, the server thinks that `newClient` owns the nickname
418
+
419
+	client.resumeDetails.OldClient = oldClient
420
+
421
+	// transfer monitor stuff
422
+	server.monitorManager.Resume(client, oldClient)
423
+
424
+	// record the names, not the pointers, of the channels,
425
+	// to avoid dumb annoying race conditions
426
+	channels := oldClient.Channels()
427
+	client.resumeDetails.Channels = make([]string, len(channels))
428
+	for i, channel := range channels {
429
+		client.resumeDetails.Channels[i] = channel.Name()
401
 	}
430
 	}
402
 
431
 
403
-	// send QUIT/RESUMED to friends
404
-	for friend := range oldClient.Friends() {
432
+	username := client.Username()
433
+	hostname := client.Hostname()
434
+
435
+	friends := make(ClientSet)
436
+	oldestLostMessage := time.Now()
437
+
438
+	// work out how much time, if any, is not covered by history buffers
439
+	for _, channel := range channels {
440
+		for _, member := range channel.Members() {
441
+			friends.Add(member)
442
+			lastDiscarded := channel.history.LastDiscarded()
443
+			if lastDiscarded.Before(oldestLostMessage) {
444
+				oldestLostMessage = lastDiscarded
445
+			}
446
+		}
447
+	}
448
+	personalHistory := oldClient.history.All()
449
+	lastDiscarded := oldClient.history.LastDiscarded()
450
+	if lastDiscarded.Before(oldestLostMessage) {
451
+		oldestLostMessage = lastDiscarded
452
+	}
453
+	for _, item := range personalHistory {
454
+		if item.Type == history.Privmsg || item.Type == history.Notice {
455
+			sender := server.clients.Get(item.Nick)
456
+			if sender != nil {
457
+				friends.Add(sender)
458
+			}
459
+		}
460
+	}
461
+
462
+	gap := lastDiscarded.Sub(timestamp)
463
+	client.resumeDetails.HistoryIncomplete = gap > 0
464
+	gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion
465
+
466
+	// send quit/resume messages to friends
467
+	for friend := range friends {
405
 		if friend.capabilities.Has(caps.Resume) {
468
 		if friend.capabilities.Has(caps.Resume) {
406
-			if timestamp == nil {
407
-				friend.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname())
469
+			if timestamp.IsZero() {
470
+				friend.Send(nil, oldNickmask, "RESUMED", username, hostname)
408
 			} else {
471
 			} else {
409
-				friend.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname(), timestampString)
472
+				friend.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
410
 			}
473
 			}
411
 		} else {
474
 		} else {
412
-			friend.Send(nil, oldClient.NickMaskString(), "QUIT", friend.t("Client reconnected"))
475
+			if client.resumeDetails.HistoryIncomplete {
476
+				friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
477
+			} else {
478
+				friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
479
+			}
413
 		}
480
 		}
414
 	}
481
 	}
415
 
482
 
416
-	// apply old client's details to new client
417
-	client.nick = oldClient.nick
418
-	client.updateNickMaskNoMutex()
419
-
420
-	rejoinChannel := func(channel *Channel) {
421
-		channel.joinPartMutex.Lock()
422
-		defer channel.joinPartMutex.Unlock()
483
+	if client.resumeDetails.HistoryIncomplete {
484
+		client.Send(nil, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
485
+	}
423
 
486
 
424
-		channel.stateMutex.Lock()
425
-		client.channels[channel] = true
426
-		client.resumeDetails.SendFakeJoinsFor = append(client.resumeDetails.SendFakeJoinsFor, channel.name)
487
+	client.Send(nil, "RESUME", "SUCCESS", oldNick)
427
 
488
 
428
-		oldModeSet := channel.members[oldClient]
429
-		channel.members.Remove(oldClient)
430
-		channel.members[client] = oldModeSet
431
-		channel.stateMutex.Unlock()
489
+	// after we send the rest of the registration burst, we'll try rejoining channels
490
+}
432
 
491
 
433
-		channel.regenerateMembersCache()
492
+func (client *Client) tryResumeChannels() {
493
+	details := client.resumeDetails
494
+	if details == nil {
495
+		return
496
+	}
434
 
497
 
435
-		// construct fake modestring if necessary
436
-		oldModes := oldModeSet.String()
437
-		var params []string
438
-		if 0 < len(oldModes) {
439
-			params = []string{channel.name, "+" + oldModes}
440
-			for range oldModes {
441
-				params = append(params, client.nick)
442
-			}
498
+	channels := make([]*Channel, len(details.Channels))
499
+	for _, name := range details.Channels {
500
+		channel := client.server.channels.Get(name)
501
+		if channel == nil {
502
+			continue
443
 		}
503
 		}
504
+		channel.Resume(client, details.OldClient, details.Timestamp)
505
+		channels = append(channels, channel)
506
+	}
444
 
507
 
445
-		// send join for old clients
446
-		for member := range channel.members {
447
-			if member.capabilities.Has(caps.Resume) {
508
+	// replay direct PRIVSMG history
509
+	if !details.Timestamp.IsZero() {
510
+		now := time.Now()
511
+		nick := client.Nick()
512
+		items, complete := client.history.Between(details.Timestamp, now)
513
+		for _, item := range items {
514
+			var command string
515
+			switch item.Type {
516
+			case history.Privmsg:
517
+				command = "PRIVMSG"
518
+			case history.Notice:
519
+				command = "NOTICE"
520
+			default:
448
 				continue
521
 				continue
449
 			}
522
 			}
450
-
451
-			if member.capabilities.Has(caps.ExtendedJoin) {
452
-				member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.AccountName(), client.realname)
453
-			} else {
454
-				member.Send(nil, client.nickMaskString, "JOIN", channel.name)
455
-			}
456
-
457
-			// send fake modestring if necessary
458
-			if 0 < len(oldModes) {
459
-				member.Send(nil, server.name, "MODE", params...)
460
-			}
523
+			client.sendSplitMsgFromClientInternal(true, item.Time, item.Msgid, item.Nick, item.AccountName, nil, command, nick, item.Message)
524
+		}
525
+		if !complete {
526
+			client.Send(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost"))
461
 		}
527
 		}
462
 	}
528
 	}
463
 
529
 
464
-	for channel := range oldClient.channels {
465
-		rejoinChannel(channel)
466
-	}
530
+	details.OldClient.destroy(true)
531
+}
532
+
533
+// copy applicable state from oldClient to client as part of a resume
534
+func (client *Client) copyResumeData(oldClient *Client) {
535
+	oldClient.stateMutex.RLock()
536
+	flags := oldClient.flags
537
+	history := oldClient.history
538
+	nick := oldClient.nick
539
+	nickCasefolded := oldClient.nickCasefolded
540
+	vhost := oldClient.vhost
541
+	account := oldClient.account
542
+	accountName := oldClient.accountName
543
+	oldClient.stateMutex.RUnlock()
544
+
545
+	// copy all flags, *except* TLS (in the case that the admins enabled
546
+	// resume over plaintext)
547
+	hasTLS := client.flags.HasMode(modes.TLS)
548
+	temp := modes.NewModeSet()
549
+	temp.Copy(flags)
550
+	temp.SetMode(modes.TLS, hasTLS)
551
+	client.flags.Copy(temp)
467
 
552
 
468
-	server.clients.byNick[oldnick] = client
553
+	client.stateMutex.Lock()
554
+	defer client.stateMutex.Unlock()
469
 
555
 
470
-	oldClient.destroy(true)
556
+	// reuse the old client's history buffer
557
+	client.history = history
558
+	// copy other data
559
+	client.nick = nick
560
+	client.nickCasefolded = nickCasefolded
561
+	client.vhost = vhost
562
+	client.account = account
563
+	client.accountName = accountName
564
+	client.updateNickMaskNoMutex()
471
 }
565
 }
472
 
566
 
473
 // IdleTime returns how long this client's been idle.
567
 // IdleTime returns how long this client's been idle.
501
 	return client.username != "" && client.username != "*"
595
 	return client.username != "" && client.username != "*"
502
 }
596
 }
503
 
597
 
598
+func (client *Client) SetNames(username, realname string) error {
599
+	_, err := CasefoldName(username)
600
+	if err != nil {
601
+		return errInvalidUsername
602
+	}
603
+
604
+	client.stateMutex.Lock()
605
+	defer client.stateMutex.Unlock()
606
+
607
+	if client.username == "" {
608
+		client.username = "~" + username
609
+	}
610
+
611
+	if client.realname == "" {
612
+		client.realname = realname
613
+	}
614
+
615
+	return nil
616
+}
617
+
504
 // HasRoleCapabs returns true if client has the given (role) capabilities.
618
 // HasRoleCapabs returns true if client has the given (role) capabilities.
505
 func (client *Client) HasRoleCapabs(capabs ...string) bool {
619
 func (client *Client) HasRoleCapabs(capabs ...string) bool {
506
 	oper := client.Oper()
620
 	oper := client.Oper()
561
 func (client *Client) sendChghost(oldNickMask string, vhost string) {
675
 func (client *Client) sendChghost(oldNickMask string, vhost string) {
562
 	username := client.Username()
676
 	username := client.Username()
563
 	for fClient := range client.Friends(caps.ChgHost) {
677
 	for fClient := range client.Friends(caps.ChgHost) {
564
-		fClient.sendFromClientInternal("", client, oldNickMask, nil, "CHGHOST", username, vhost)
678
+		fClient.sendFromClientInternal(false, time.Time{}, "", oldNickMask, client.AccountName(), nil, "CHGHOST", username, vhost)
565
 	}
679
 	}
566
 }
680
 }
567
 
681
 
711
 // destroy gets rid of a client, removes them from server lists etc.
825
 // destroy gets rid of a client, removes them from server lists etc.
712
 func (client *Client) destroy(beingResumed bool) {
826
 func (client *Client) destroy(beingResumed bool) {
713
 	// allow destroy() to execute at most once
827
 	// allow destroy() to execute at most once
714
-	if !beingResumed {
715
-		client.stateMutex.Lock()
716
-	}
828
+	client.stateMutex.Lock()
717
 	isDestroyed := client.isDestroyed
829
 	isDestroyed := client.isDestroyed
718
 	client.isDestroyed = true
830
 	client.isDestroyed = true
719
-	if !beingResumed {
720
-		client.stateMutex.Unlock()
721
-	}
831
+	quitMessage := client.quitMessage
832
+	nickMaskString := client.nickMaskString
833
+	accountName := client.accountName
834
+	client.stateMutex.Unlock()
835
+
722
 	if isDestroyed {
836
 	if isDestroyed {
723
 		return
837
 		return
724
 	}
838
 	}
758
 	for _, channel := range client.Channels() {
872
 	for _, channel := range client.Channels() {
759
 		if !beingResumed {
873
 		if !beingResumed {
760
 			channel.Quit(client)
874
 			channel.Quit(client)
875
+			channel.history.Add(history.Item{
876
+				Type:        history.Quit,
877
+				Nick:        nickMaskString,
878
+				AccountName: accountName,
879
+				Message:     utils.MakeSplitMessage(quitMessage, true),
880
+			})
761
 		}
881
 		}
762
 		for _, member := range channel.Members() {
882
 		for _, member := range channel.Members() {
763
 			friends.Add(member)
883
 			friends.Add(member)
791
 		}
911
 		}
792
 
912
 
793
 		for friend := range friends {
913
 		for friend := range friends {
794
-			if client.quitMessage == "" {
795
-				client.quitMessage = "Exited"
914
+			if quitMessage == "" {
915
+				quitMessage = "Exited"
796
 			}
916
 			}
797
-			friend.Send(nil, client.nickMaskString, "QUIT", client.quitMessage)
917
+			friend.Send(nil, client.nickMaskString, "QUIT", quitMessage)
798
 		}
918
 		}
799
 	}
919
 	}
800
 	if !client.exitedSnomaskSent {
920
 	if !client.exitedSnomaskSent {
808
 
928
 
809
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
929
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
810
 // Adds account-tag to the line as well.
930
 // Adds account-tag to the line as well.
811
-func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command, target string, message SplitMessage) {
812
-	if client.capabilities.Has(caps.MaxLine) {
813
-		client.SendFromClient(msgid, from, tags, command, target, message.ForMaxLine)
931
+func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags Tags, command, target string, message utils.SplitMessage) {
932
+	client.sendSplitMsgFromClientInternal(false, time.Time{}, msgid, from.NickMaskString(), from.AccountName(), tags, command, target, message)
933
+}
934
+
935
+func (client *Client) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags Tags, command, target string, message utils.SplitMessage) {
936
+	if client.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
937
+		client.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, target, message.Original)
814
 	} else {
938
 	} else {
815
-		for _, str := range message.For512 {
816
-			client.SendFromClient(msgid, from, tags, command, target, str)
939
+		for _, str := range message.Wrapped {
940
+			client.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, target, str)
817
 		}
941
 		}
818
 	}
942
 	}
819
 }
943
 }
820
 
944
 
821
 // SendFromClient sends an IRC line coming from a specific client.
945
 // SendFromClient sends an IRC line coming from a specific client.
822
 // Adds account-tag to the line as well.
946
 // Adds account-tag to the line as well.
823
-func (client *Client) SendFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
824
-	return client.sendFromClientInternal(msgid, from, from.NickMaskString(), tags, command, params...)
947
+func (client *Client) SendFromClient(msgid string, from *Client, tags Tags, command string, params ...string) error {
948
+	return client.sendFromClientInternal(false, time.Time{}, msgid, from.NickMaskString(), from.AccountName(), tags, command, params...)
949
+}
950
+
951
+// helper to add a tag to `tags` (or create a new tag set if the current one is nil)
952
+func ensureTag(tags Tags, tagName, tagValue string) (result Tags) {
953
+	if tags == nil {
954
+		result = ircmsg.MakeTags(tagName, tagValue)
955
+	} else {
956
+		result = tags
957
+		(*tags)[tagName] = ircmsg.MakeTagValue(tagValue)
958
+	}
959
+	return
825
 }
960
 }
826
 
961
 
827
 // XXX this is a hack where we allow overriding the client's nickmask
962
 // XXX this is a hack where we allow overriding the client's nickmask
828
 // this is to support CHGHOST, which requires that we send the *original* nickmask with the response
963
 // this is to support CHGHOST, which requires that we send the *original* nickmask with the response
829
-func (client *Client) sendFromClientInternal(msgid string, from *Client, nickmask string, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
964
+func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags Tags, command string, params ...string) error {
830
 	// attach account-tag
965
 	// attach account-tag
831
-	if client.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
832
-		if tags == nil {
833
-			tags = ircmsg.MakeTags("account", from.AccountName())
834
-		} else {
835
-			(*tags)["account"] = ircmsg.MakeTagValue(from.AccountName())
836
-		}
966
+	if client.capabilities.Has(caps.AccountTag) && accountName != "*" {
967
+		tags = ensureTag(tags, "account", accountName)
837
 	}
968
 	}
838
 	// attach message-id
969
 	// attach message-id
839
 	if len(msgid) > 0 && client.capabilities.Has(caps.MessageTags) {
970
 	if len(msgid) > 0 && client.capabilities.Has(caps.MessageTags) {
840
-		if tags == nil {
841
-			tags = ircmsg.MakeTags("draft/msgid", msgid)
842
-		} else {
843
-			(*tags)["draft/msgid"] = ircmsg.MakeTagValue(msgid)
844
-		}
971
+		tags = ensureTag(tags, "draft/msgid", msgid)
845
 	}
972
 	}
846
 
973
 
847
-	return client.Send(tags, nickmask, command, params...)
974
+	return client.sendInternal(blocking, serverTime, tags, nickmask, command, params...)
848
 }
975
 }
849
 
976
 
850
 var (
977
 var (
861
 )
988
 )
862
 
989
 
863
 // SendRawMessage sends a raw message to the client.
990
 // SendRawMessage sends a raw message to the client.
864
-func (client *Client) SendRawMessage(message ircmsg.IrcMessage) error {
991
+func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error {
865
 	// use dumb hack to force the last param to be a trailing param if required
992
 	// use dumb hack to force the last param to be a trailing param if required
866
 	var usedTrailingHack bool
993
 	var usedTrailingHack bool
867
 	if commandsThatMustUseTrailing[strings.ToUpper(message.Command)] && len(message.Params) > 0 {
994
 	if commandsThatMustUseTrailing[strings.ToUpper(message.Command)] && len(message.Params) > 0 {
883
 		message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
1010
 		message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
884
 		line, _ := message.LineBytes()
1011
 		line, _ := message.LineBytes()
885
 
1012
 
886
-		client.socket.Write(line)
1013
+		if blocking {
1014
+			client.socket.BlockingWrite(line)
1015
+		} else {
1016
+			client.socket.Write(line)
1017
+		}
887
 		return err
1018
 		return err
888
 	}
1019
 	}
889
 
1020
 
898
 		client.server.logger.Debug("useroutput", client.nick, " ->", logline)
1029
 		client.server.logger.Debug("useroutput", client.nick, " ->", logline)
899
 	}
1030
 	}
900
 
1031
 
901
-	client.socket.Write(line)
902
-
903
-	return nil
1032
+	if blocking {
1033
+		return client.socket.BlockingWrite(line)
1034
+	} else {
1035
+		return client.socket.Write(line)
1036
+	}
904
 }
1037
 }
905
 
1038
 
906
-// Send sends an IRC line to the client.
907
-func (client *Client) Send(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) error {
908
-	// attach server-time
1039
+func (client *Client) sendInternal(blocking bool, serverTime time.Time, tags Tags, prefix string, command string, params ...string) error {
1040
+	// attach server time
909
 	if client.capabilities.Has(caps.ServerTime) {
1041
 	if client.capabilities.Has(caps.ServerTime) {
910
-		t := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
911
-		if tags == nil {
912
-			tags = ircmsg.MakeTags("time", t)
913
-		} else {
914
-			(*tags)["time"] = ircmsg.MakeTagValue(t)
1042
+		if serverTime.IsZero() {
1043
+			serverTime = time.Now()
915
 		}
1044
 		}
1045
+		tags = ensureTag(tags, "time", serverTime.UTC().Format(IRCv3TimestampFormat))
916
 	}
1046
 	}
917
 
1047
 
918
 	// send out the message
1048
 	// send out the message
919
 	message := ircmsg.MakeMessage(tags, prefix, command, params...)
1049
 	message := ircmsg.MakeMessage(tags, prefix, command, params...)
920
-	client.SendRawMessage(message)
1050
+	client.SendRawMessage(message, blocking)
921
 	return nil
1051
 	return nil
922
 }
1052
 }
923
 
1053
 
1054
+// Send sends an IRC line to the client.
1055
+func (client *Client) Send(tags Tags, prefix string, command string, params ...string) error {
1056
+	return client.sendInternal(false, time.Time{}, tags, prefix, command, params...)
1057
+}
1058
+
924
 // Notice sends the client a notice from the server.
1059
 // Notice sends the client a notice from the server.
925
 func (client *Client) Notice(text string) {
1060
 func (client *Client) Notice(text string) {
926
 	limit := 400
1061
 	limit := 400
927
 	if client.capabilities.Has(caps.MaxLine) {
1062
 	if client.capabilities.Has(caps.MaxLine) {
928
 		limit = client.server.Limits().LineLen.Rest - 110
1063
 		limit = client.server.Limits().LineLen.Rest - 110
929
 	}
1064
 	}
930
-	lines := wordWrap(text, limit)
1065
+	lines := utils.WordWrap(text, limit)
931
 
1066
 
932
 	// force blank lines to be sent if we receive them
1067
 	// force blank lines to be sent if we receive them
933
 	if len(lines) == 0 {
1068
 	if len(lines) == 0 {
950
 	delete(client.channels, channel)
1085
 	delete(client.channels, channel)
951
 	client.stateMutex.Unlock()
1086
 	client.stateMutex.Unlock()
952
 }
1087
 }
1088
+
1089
+// Ensures the client has a cryptographically secure resume token, and returns
1090
+// its value. An error is returned if a token was previously assigned.
1091
+func (client *Client) generateResumeToken() (token string, err error) {
1092
+	newToken := utils.GenerateSecretToken()
1093
+
1094
+	client.stateMutex.Lock()
1095
+	defer client.stateMutex.Unlock()
1096
+
1097
+	if client.resumeToken == "" {
1098
+		client.resumeToken = newToken
1099
+	} else {
1100
+		err = errResumeTokenAlreadySet
1101
+	}
1102
+
1103
+	return client.resumeToken, err
1104
+}

+ 24
- 3
irc/client_lookup_set.go 查看文件

63
 	return nil
63
 	return nil
64
 }
64
 }
65
 
65
 
66
-func (clients *ClientManager) removeInternal(client *Client) (removed bool) {
66
+func (clients *ClientManager) removeInternal(client *Client) (err error) {
67
 	// requires holding the writable Lock()
67
 	// requires holding the writable Lock()
68
 	oldcfnick := client.NickCasefolded()
68
 	oldcfnick := client.NickCasefolded()
69
 	currentEntry, present := clients.byNick[oldcfnick]
69
 	currentEntry, present := clients.byNick[oldcfnick]
70
 	if present {
70
 	if present {
71
 		if currentEntry == client {
71
 		if currentEntry == client {
72
 			delete(clients.byNick, oldcfnick)
72
 			delete(clients.byNick, oldcfnick)
73
-			removed = true
74
 		} else {
73
 		} else {
75
 			// this shouldn't happen, but we can ignore it
74
 			// this shouldn't happen, but we can ignore it
76
 			client.server.logger.Warning("internal", fmt.Sprintf("clients for nick %s out of sync", oldcfnick))
75
 			client.server.logger.Warning("internal", fmt.Sprintf("clients for nick %s out of sync", oldcfnick))
76
+			err = errNickMissing
77
 		}
77
 		}
78
 	}
78
 	}
79
 	return
79
 	return
87
 	if !client.HasNick() {
87
 	if !client.HasNick() {
88
 		return errNickMissing
88
 		return errNickMissing
89
 	}
89
 	}
90
-	clients.removeInternal(client)
90
+	return clients.removeInternal(client)
91
+}
92
+
93
+// Resume atomically replaces `oldClient` with `newClient`, updating
94
+// newClient's data to match. It is the caller's responsibility first
95
+// to verify that the resume is allowed, and then later to call oldClient.destroy().
96
+func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) {
97
+	clients.Lock()
98
+	defer clients.Unlock()
99
+
100
+	// atomically grant the new client the old nick
101
+	err = clients.removeInternal(oldClient)
102
+	if err != nil {
103
+		// oldClient no longer owns its nick, fail out
104
+		return err
105
+	}
106
+	// nick has been reclaimed, grant it to the new client
107
+	clients.removeInternal(newClient)
108
+	clients.byNick[oldClient.NickCasefolded()] = newClient
109
+
110
+	newClient.copyResumeData(oldClient)
111
+
91
 	return nil
112
 	return nil
92
 }
113
 }
93
 
114
 

+ 1
- 1
irc/commands.go 查看文件

222
 		"RESUME": {
222
 		"RESUME": {
223
 			handler:      resumeHandler,
223
 			handler:      resumeHandler,
224
 			usablePreReg: true,
224
 			usablePreReg: true,
225
-			minParams:    1,
225
+			minParams:    2,
226
 		},
226
 		},
227
 		"SAJOIN": {
227
 		"SAJOIN": {
228
 			handler:   sajoinHandler,
228
 			handler:   sajoinHandler,

+ 32
- 17
irc/config.go 查看文件

208
 	}
208
 	}
209
 
209
 
210
 	Server struct {
210
 	Server struct {
211
-		Password            string
212
-		passwordBytes       []byte
213
-		Name                string
214
-		nameCasefolded      string
215
-		Listen              []string
216
-		UnixBindMode        os.FileMode                 `yaml:"unix-bind-mode"`
217
-		TLSListeners        map[string]*TLSListenConfig `yaml:"tls-listeners"`
218
-		STS                 STSConfig
219
-		CheckIdent          bool `yaml:"check-ident"`
220
-		MOTD                string
221
-		MOTDFormatting      bool           `yaml:"motd-formatting"`
222
-		ProxyAllowedFrom    []string       `yaml:"proxy-allowed-from"`
223
-		WebIRC              []webircConfig `yaml:"webirc"`
224
-		MaxSendQString      string         `yaml:"max-sendq"`
225
-		MaxSendQBytes       int
226
-		ConnectionLimiter   connection_limits.LimiterConfig   `yaml:"connection-limits"`
227
-		ConnectionThrottler connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
211
+		Password             string
212
+		passwordBytes        []byte
213
+		Name                 string
214
+		nameCasefolded       string
215
+		Listen               []string
216
+		UnixBindMode         os.FileMode                 `yaml:"unix-bind-mode"`
217
+		TLSListeners         map[string]*TLSListenConfig `yaml:"tls-listeners"`
218
+		STS                  STSConfig
219
+		CheckIdent           bool `yaml:"check-ident"`
220
+		MOTD                 string
221
+		MOTDFormatting       bool           `yaml:"motd-formatting"`
222
+		ProxyAllowedFrom     []string       `yaml:"proxy-allowed-from"`
223
+		WebIRC               []webircConfig `yaml:"webirc"`
224
+		MaxSendQString       string         `yaml:"max-sendq"`
225
+		MaxSendQBytes        int
226
+		AllowPlaintextResume bool                              `yaml:"allow-plaintext-resume"`
227
+		ConnectionLimiter    connection_limits.LimiterConfig   `yaml:"connection-limits"`
228
+		ConnectionThrottler  connection_limits.ThrottlerConfig `yaml:"connection-throttling"`
228
 	}
229
 	}
229
 
230
 
230
 	Languages struct {
231
 	Languages struct {
266
 
267
 
267
 	Fakelag FakelagConfig
268
 	Fakelag FakelagConfig
268
 
269
 
270
+	History struct {
271
+		Enabled       bool
272
+		ChannelLength int `yaml:"channel-length"`
273
+		ClientLength  int `yaml:"client-length"`
274
+	}
275
+
269
 	Filename string
276
 	Filename string
270
 }
277
 }
271
 
278
 
712
 		config.Accounts.Registration.BcryptCost = passwd.DefaultCost
719
 		config.Accounts.Registration.BcryptCost = passwd.DefaultCost
713
 	}
720
 	}
714
 
721
 
722
+	// in the current implementation, we disable history by creating a history buffer
723
+	// with zero capacity. but the `enabled` config option MUST be respected regardless
724
+	// of this detail
725
+	if !config.History.Enabled {
726
+		config.History.ChannelLength = 0
727
+		config.History.ClientLength = 0
728
+	}
729
+
715
 	return config, nil
730
 	return config, nil
716
 }
731
 }

+ 2
- 0
irc/errors.go 查看文件

38
 	errRenamePrivsNeeded              = errors.New("Only chanops can rename channels")
38
 	errRenamePrivsNeeded              = errors.New("Only chanops can rename channels")
39
 	errInsufficientPrivs              = errors.New("Insufficient privileges")
39
 	errInsufficientPrivs              = errors.New("Insufficient privileges")
40
 	errSaslFail                       = errors.New("SASL failed")
40
 	errSaslFail                       = errors.New("SASL failed")
41
+	errResumeTokenAlreadySet          = errors.New("Client was already assigned a resume token")
42
+	errInvalidUsername                = errors.New("Invalid username")
41
 )
43
 )
42
 
44
 
43
 // Socket Errors
45
 // Socket Errors

+ 6
- 0
irc/getters.go 查看文件

102
 	return client.realname
102
 	return client.realname
103
 }
103
 }
104
 
104
 
105
+func (client *Client) ResumeToken() string {
106
+	client.stateMutex.RLock()
107
+	defer client.stateMutex.RUnlock()
108
+	return client.resumeToken
109
+}
110
+
105
 func (client *Client) Oper() *Oper {
111
 func (client *Client) Oper() *Oper {
106
 	client.stateMutex.RLock()
112
 	client.stateMutex.RLock()
107
 	defer client.stateMutex.RUnlock()
113
 	defer client.stateMutex.RUnlock()

+ 39
- 30
irc/handlers.go 查看文件

26
 	"github.com/goshuirc/irc-go/ircmsg"
26
 	"github.com/goshuirc/irc-go/ircmsg"
27
 	"github.com/oragono/oragono/irc/caps"
27
 	"github.com/oragono/oragono/irc/caps"
28
 	"github.com/oragono/oragono/irc/custime"
28
 	"github.com/oragono/oragono/irc/custime"
29
+	"github.com/oragono/oragono/irc/history"
29
 	"github.com/oragono/oragono/irc/modes"
30
 	"github.com/oragono/oragono/irc/modes"
30
 	"github.com/oragono/oragono/irc/sno"
31
 	"github.com/oragono/oragono/irc/sno"
31
 	"github.com/oragono/oragono/irc/utils"
32
 	"github.com/oragono/oragono/irc/utils"
483
 		client.capabilities.Union(capabilities)
484
 		client.capabilities.Union(capabilities)
484
 		rb.Add(nil, server.name, "CAP", client.nick, "ACK", capString)
485
 		rb.Add(nil, server.name, "CAP", client.nick, "ACK", capString)
485
 
486
 
487
+		// if this is the first time the client is requesting a resume token,
488
+		// send it to them
489
+		if capabilities.Has(caps.Resume) {
490
+			token, err := client.generateResumeToken()
491
+			if err == nil {
492
+				rb.Add(nil, server.name, "RESUME", "TOKEN", token)
493
+			}
494
+		}
495
+
486
 	case "END":
496
 	case "END":
487
 		if !client.Registered() {
497
 		if !client.Registered() {
488
 			client.capState = caps.NegotiatedState
498
 			client.capState = caps.NegotiatedState
1648
 	message := msg.Params[1]
1658
 	message := msg.Params[1]
1649
 
1659
 
1650
 	// split privmsg
1660
 	// split privmsg
1651
-	splitMsg := server.splitMessage(message, !client.capabilities.Has(caps.MaxLine))
1661
+	splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine))
1652
 
1662
 
1653
 	for i, targetString := range targets {
1663
 	for i, targetString := range targets {
1654
 		// max of four targets per privmsg
1664
 		// max of four targets per privmsg
1699
 			if client.capabilities.Has(caps.EchoMessage) {
1709
 			if client.capabilities.Has(caps.EchoMessage) {
1700
 				rb.AddSplitMessageFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
1710
 				rb.AddSplitMessageFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
1701
 			}
1711
 			}
1712
+
1713
+			user.history.Add(history.Item{
1714
+				Type:        history.Notice,
1715
+				Msgid:       msgid,
1716
+				Message:     splitMsg,
1717
+				Nick:        client.NickMaskString(),
1718
+				AccountName: client.AccountName(),
1719
+			})
1702
 		}
1720
 		}
1703
 	}
1721
 	}
1704
 	return false
1722
 	return false
1848
 	message := msg.Params[1]
1866
 	message := msg.Params[1]
1849
 
1867
 
1850
 	// split privmsg
1868
 	// split privmsg
1851
-	splitMsg := server.splitMessage(message, !client.capabilities.Has(caps.MaxLine))
1869
+	splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine))
1852
 
1870
 
1853
 	for i, targetString := range targets {
1871
 	for i, targetString := range targets {
1854
 		// max of four targets per privmsg
1872
 		// max of four targets per privmsg
1905
 				//TODO(dan): possibly implement cooldown of away notifications to users
1923
 				//TODO(dan): possibly implement cooldown of away notifications to users
1906
 				rb.Add(nil, server.name, RPL_AWAY, user.nick, user.awayMessage)
1924
 				rb.Add(nil, server.name, RPL_AWAY, user.nick, user.awayMessage)
1907
 			}
1925
 			}
1926
+
1927
+			user.history.Add(history.Item{
1928
+				Type:        history.Privmsg,
1929
+				Msgid:       msgid,
1930
+				Message:     splitMsg,
1931
+				Nick:        client.NickMaskString(),
1932
+				AccountName: client.AccountName(),
1933
+			})
1908
 		}
1934
 		}
1909
 	}
1935
 	}
1910
 	return false
1936
 	return false
2018
 	return false
2044
 	return false
2019
 }
2045
 }
2020
 
2046
 
2021
-// RESUME <oldnick> [timestamp]
2047
+// RESUME <oldnick> <token> [timestamp]
2022
 func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2048
 func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2023
 	oldnick := msg.Params[0]
2049
 	oldnick := msg.Params[0]
2024
-
2025
-	if strings.Contains(oldnick, " ") {
2026
-		rb.Add(nil, server.name, ERR_CANNOT_RESUME, "*", client.t("Cannot resume connection, old nickname contains spaces"))
2027
-		return false
2028
-	}
2050
+	token := msg.Params[1]
2029
 
2051
 
2030
 	if client.Registered() {
2052
 	if client.Registered() {
2031
 		rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, connection registration has already been completed"))
2053
 		rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, connection registration has already been completed"))
2032
 		return false
2054
 		return false
2033
 	}
2055
 	}
2034
 
2056
 
2035
-	var timestamp *time.Time
2036
-	if 1 < len(msg.Params) {
2037
-		ts, err := time.Parse("2006-01-02T15:04:05.999Z", msg.Params[1])
2057
+	var timestamp time.Time
2058
+	if 2 < len(msg.Params) {
2059
+		ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[2])
2038
 		if err == nil {
2060
 		if err == nil {
2039
-			timestamp = &ts
2061
+			timestamp = ts
2040
 		} else {
2062
 		} else {
2041
 			rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it"))
2063
 			rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it"))
2042
 		}
2064
 		}
2043
 	}
2065
 	}
2044
 
2066
 
2045
 	client.resumeDetails = &ResumeDetails{
2067
 	client.resumeDetails = &ResumeDetails{
2046
-		OldNick:   oldnick,
2047
-		Timestamp: timestamp,
2068
+		OldNick:        oldnick,
2069
+		Timestamp:      timestamp,
2070
+		PresentedToken: token,
2048
 	}
2071
 	}
2049
 
2072
 
2050
 	return false
2073
 	return false
2280
 		return false
2303
 		return false
2281
 	}
2304
 	}
2282
 
2305
 
2283
-	if client.username != "" && client.realname != "" {
2284
-		return false
2285
-	}
2286
-
2287
-	// confirm that username is valid
2288
-	//
2289
-	_, err := CasefoldName(msg.Params[0])
2290
-	if err != nil {
2306
+	err := client.SetNames(msg.Params[0], msg.Params[3])
2307
+	if err == errInvalidUsername {
2291
 		rb.Add(nil, "", "ERROR", client.t("Malformed username"))
2308
 		rb.Add(nil, "", "ERROR", client.t("Malformed username"))
2292
 		return true
2309
 		return true
2293
 	}
2310
 	}
2294
 
2311
 
2295
-	if !client.HasUsername() {
2296
-		client.username = "~" + msg.Params[0]
2297
-		// don't bother updating nickmask here, it's not valid anyway
2298
-	}
2299
-	if client.realname == "" {
2300
-		client.realname = msg.Params[3]
2301
-	}
2302
-
2303
 	return false
2312
 	return false
2304
 }
2313
 }
2305
 
2314
 

+ 249
- 0
irc/history/history.go 查看文件

1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package history
5
+
6
+import (
7
+	"github.com/oragono/oragono/irc/utils"
8
+	"sync"
9
+	"sync/atomic"
10
+	"time"
11
+)
12
+
13
+type ItemType uint
14
+
15
+const (
16
+	uninitializedItem ItemType = iota
17
+	Privmsg
18
+	Notice
19
+	Join
20
+	Part
21
+	Kick
22
+	Quit
23
+	Mode
24
+)
25
+
26
+// Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data
27
+type Item struct {
28
+	Type ItemType
29
+	Time time.Time
30
+
31
+	Nick string
32
+	// this is the uncasefolded account name, if there's no account it should be set to "*"
33
+	AccountName string
34
+	Message     utils.SplitMessage
35
+	Msgid       string
36
+}
37
+
38
+// Buffer is a ring buffer holding message/event history for a channel or user
39
+type Buffer struct {
40
+	sync.RWMutex
41
+
42
+	// ring buffer, see irc/whowas.go for conventions
43
+	buffer []Item
44
+	start  int
45
+	end    int
46
+
47
+	lastDiscarded time.Time
48
+
49
+	enabled uint32
50
+}
51
+
52
+func NewHistoryBuffer(size int) (result *Buffer) {
53
+	result = new(Buffer)
54
+	result.Initialize(size)
55
+	return
56
+}
57
+
58
+func (hist *Buffer) Initialize(size int) {
59
+	hist.buffer = make([]Item, size)
60
+	hist.start = -1
61
+	hist.end = -1
62
+
63
+	hist.setEnabled(size)
64
+}
65
+
66
+func (hist *Buffer) setEnabled(size int) {
67
+	var enabled uint32
68
+	if size != 0 {
69
+		enabled = 1
70
+	}
71
+	atomic.StoreUint32(&hist.enabled, enabled)
72
+}
73
+
74
+// Enabled returns whether the buffer is currently storing messages
75
+// (a disabled buffer blackholes everything it sees)
76
+func (list *Buffer) Enabled() bool {
77
+	return atomic.LoadUint32(&list.enabled) != 0
78
+}
79
+
80
+// Add adds a history item to the buffer
81
+func (list *Buffer) Add(item Item) {
82
+	// fast path without a lock acquisition for when we are not storing history
83
+	if !list.Enabled() {
84
+		return
85
+	}
86
+
87
+	if item.Time.IsZero() {
88
+		item.Time = time.Now()
89
+	}
90
+
91
+	list.Lock()
92
+	defer list.Unlock()
93
+
94
+	var pos int
95
+	if list.start == -1 { // empty
96
+		pos = 0
97
+		list.start = 0
98
+		list.end = 1 % len(list.buffer)
99
+	} else if list.start != list.end { // partially full
100
+		pos = list.end
101
+		list.end = (list.end + 1) % len(list.buffer)
102
+	} else if list.start == list.end { // full
103
+		pos = list.end
104
+		list.end = (list.end + 1) % len(list.buffer)
105
+		list.start = list.end // advance start as well, overwriting first entry
106
+		// record the timestamp of the overwritten item
107
+		if list.lastDiscarded.Before(list.buffer[pos].Time) {
108
+			list.lastDiscarded = list.buffer[pos].Time
109
+		}
110
+	}
111
+
112
+	list.buffer[pos] = item
113
+}
114
+
115
+// Between returns all history items with a time `after` <= time <= `before`,
116
+// with an indication of whether the results are complete or are missing items
117
+// because some of that period was discarded. A zero value of `before` is considered
118
+// higher than all other times.
119
+func (list *Buffer) Between(after, before time.Time) (results []Item, complete bool) {
120
+	if !list.Enabled() {
121
+		return
122
+	}
123
+
124
+	list.RLock()
125
+	defer list.RUnlock()
126
+
127
+	complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded)
128
+
129
+	if list.start == -1 {
130
+		return
131
+	}
132
+
133
+	satisfies := func(itime time.Time) bool {
134
+		return (after.IsZero() || itime.After(after)) && (before.IsZero() || itime.Before(before))
135
+	}
136
+
137
+	// TODO: if we can guarantee that the insertion order is also the monotonic clock order,
138
+	// then this can do a single allocation and use binary search and 1-2 copy calls
139
+
140
+	pos := list.prev(list.end)
141
+	for {
142
+		if satisfies(list.buffer[pos].Time) {
143
+			results = append(results, list.buffer[pos])
144
+		}
145
+		if pos == list.start {
146
+			break
147
+		}
148
+		pos = list.prev(pos)
149
+	}
150
+
151
+	// reverse the results
152
+	for i, j := 0, len(results)-1; i < j; i, j = i+1, j-1 {
153
+		results[i], results[j] = results[j], results[i]
154
+	}
155
+	return
156
+}
157
+
158
+// All returns all available history items as a slice
159
+func (list *Buffer) All() (results []Item) {
160
+	list.RLock()
161
+	defer list.RUnlock()
162
+
163
+	if list.start == -1 {
164
+		return
165
+	}
166
+	results = make([]Item, list.length())
167
+	if list.start < list.end {
168
+		copy(results, list.buffer[list.start:list.end])
169
+	} else {
170
+		initialSegment := copy(results, list.buffer[list.start:])
171
+		copy(results[initialSegment:], list.buffer[:list.end])
172
+	}
173
+	return
174
+}
175
+
176
+// LastDiscarded returns the latest time of any entry that was evicted
177
+// from the ring buffer.
178
+func (list *Buffer) LastDiscarded() time.Time {
179
+	list.RLock()
180
+	defer list.RUnlock()
181
+
182
+	return list.lastDiscarded
183
+}
184
+
185
+func (list *Buffer) prev(index int) int {
186
+	switch index {
187
+	case 0:
188
+		return len(list.buffer) - 1
189
+	default:
190
+		return index - 1
191
+	}
192
+}
193
+
194
+// Resize shrinks or expands the buffer
195
+func (list *Buffer) Resize(size int) {
196
+	newbuffer := make([]Item, size)
197
+	list.Lock()
198
+	defer list.Unlock()
199
+
200
+	list.setEnabled(size)
201
+
202
+	if list.start == -1 {
203
+		// indices are already correct and nothing needs to be copied
204
+	} else if size == 0 {
205
+		// this is now the empty list
206
+		list.start = -1
207
+		list.end = -1
208
+	} else {
209
+		currentLength := list.length()
210
+		start := list.start
211
+		end := list.end
212
+		// if we're truncating, keep the latest entries, not the earliest
213
+		if size < currentLength {
214
+			start = list.end - size
215
+			if start < 0 {
216
+				start += len(list.buffer)
217
+			}
218
+			// update lastDiscarded for discarded entries
219
+			for i := list.start; i != start; i = (i + 1) % len(list.buffer) {
220
+				if list.lastDiscarded.Before(list.buffer[i].Time) {
221
+					list.lastDiscarded = list.buffer[i].Time
222
+				}
223
+			}
224
+		}
225
+		if start < end {
226
+			copied := copy(newbuffer, list.buffer[start:end])
227
+			list.start = 0
228
+			list.end = copied % size
229
+		} else {
230
+			lenInitial := len(list.buffer) - start
231
+			copied := copy(newbuffer, list.buffer[start:])
232
+			copied += copy(newbuffer[lenInitial:], list.buffer[:end])
233
+			list.start = 0
234
+			list.end = copied % size
235
+		}
236
+	}
237
+
238
+	list.buffer = newbuffer
239
+}
240
+
241
+func (hist *Buffer) length() int {
242
+	if hist.start == -1 {
243
+		return 0
244
+	} else if hist.start < hist.end {
245
+		return hist.end - hist.start
246
+	} else {
247
+		return len(hist.buffer) - (hist.start - hist.end)
248
+	}
249
+}

+ 156
- 0
irc/history/history_test.go 查看文件

1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package history
5
+
6
+import (
7
+	"reflect"
8
+	"testing"
9
+	"time"
10
+)
11
+
12
+const (
13
+	timeFormat = "2006-01-02 15:04:05Z"
14
+)
15
+
16
+func TestEmptyBuffer(t *testing.T) {
17
+	pastTime := easyParse(timeFormat)
18
+
19
+	buf := NewHistoryBuffer(0)
20
+	if buf.Enabled() {
21
+		t.Error("the buffer of size 0 must be considered disabled")
22
+	}
23
+
24
+	buf.Add(Item{
25
+		Nick: "testnick",
26
+	})
27
+
28
+	since, complete := buf.Between(pastTime, time.Now())
29
+	if len(since) != 0 {
30
+		t.Error("shouldn't be able to add to disabled buf")
31
+	}
32
+	if complete {
33
+		t.Error("the empty/disabled buffer should report results as incomplete")
34
+	}
35
+
36
+	buf.Resize(1)
37
+	if !buf.Enabled() {
38
+		t.Error("the buffer of size 1 must be considered enabled")
39
+	}
40
+	since, complete = buf.Between(pastTime, time.Now())
41
+	assertEqual(complete, true, t)
42
+	assertEqual(len(since), 0, t)
43
+	buf.Add(Item{
44
+		Nick: "testnick",
45
+	})
46
+	since, complete = buf.Between(pastTime, time.Now())
47
+	if len(since) != 1 {
48
+		t.Error("should be able to store items in a nonempty buffer")
49
+	}
50
+	if !complete {
51
+		t.Error("results should be complete")
52
+	}
53
+	if since[0].Nick != "testnick" {
54
+		t.Error("retrived junk data")
55
+	}
56
+
57
+	buf.Add(Item{
58
+		Nick: "testnick2",
59
+	})
60
+	since, complete = buf.Between(pastTime, time.Now())
61
+	if len(since) != 1 {
62
+		t.Error("expect exactly 1 item")
63
+	}
64
+	if complete {
65
+		t.Error("results must be marked incomplete")
66
+	}
67
+	if since[0].Nick != "testnick2" {
68
+		t.Error("retrieved junk data")
69
+	}
70
+	assertEqual(toNicks(buf.All()), []string{"testnick2"}, t)
71
+}
72
+
73
+func toNicks(items []Item) (result []string) {
74
+	result = make([]string, len(items))
75
+	for i, item := range items {
76
+		result[i] = item.Nick
77
+	}
78
+	return
79
+}
80
+
81
+func easyParse(timestamp string) time.Time {
82
+	result, err := time.Parse(timeFormat, timestamp)
83
+	if err != nil {
84
+		panic(err)
85
+	}
86
+	return result
87
+}
88
+
89
+func assertEqual(supplied, expected interface{}, t *testing.T) {
90
+	if !reflect.DeepEqual(supplied, expected) {
91
+		t.Errorf("expected %v but got %v", expected, supplied)
92
+	}
93
+}
94
+
95
+func TestBuffer(t *testing.T) {
96
+	start := easyParse("2006-01-01 00:00:00Z")
97
+
98
+	buf := NewHistoryBuffer(3)
99
+	buf.Add(Item{
100
+		Nick: "testnick0",
101
+		Time: easyParse("2006-01-01 15:04:05Z"),
102
+	})
103
+
104
+	buf.Add(Item{
105
+		Nick: "testnick1",
106
+		Time: easyParse("2006-01-02 15:04:05Z"),
107
+	})
108
+
109
+	buf.Add(Item{
110
+		Nick: "testnick2",
111
+		Time: easyParse("2006-01-03 15:04:05Z"),
112
+	})
113
+
114
+	since, complete := buf.Between(start, time.Now())
115
+	assertEqual(complete, true, t)
116
+	assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t)
117
+
118
+	// add another item, evicting the first
119
+	buf.Add(Item{
120
+		Nick: "testnick3",
121
+		Time: easyParse("2006-01-04 15:04:05Z"),
122
+	})
123
+	since, complete = buf.Between(start, time.Now())
124
+	assertEqual(complete, false, t)
125
+	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
126
+	// now exclude the time of the discarded entry; results should be complete again
127
+	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now())
128
+	assertEqual(complete, true, t)
129
+	assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t)
130
+	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), easyParse("2006-01-03 00:00:00Z"))
131
+	assertEqual(complete, true, t)
132
+	assertEqual(toNicks(since), []string{"testnick1"}, t)
133
+
134
+	// shrink the buffer, cutting off testnick1
135
+	buf.Resize(2)
136
+	since, complete = buf.Between(easyParse("2006-01-02 00:00:00Z"), time.Now())
137
+	assertEqual(complete, false, t)
138
+	assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t)
139
+
140
+	buf.Resize(5)
141
+	buf.Add(Item{
142
+		Nick: "testnick4",
143
+		Time: easyParse("2006-01-05 15:04:05Z"),
144
+	})
145
+	buf.Add(Item{
146
+		Nick: "testnick5",
147
+		Time: easyParse("2006-01-06 15:04:05Z"),
148
+	})
149
+	buf.Add(Item{
150
+		Nick: "testnick6",
151
+		Time: easyParse("2006-01-07 15:04:05Z"),
152
+	})
153
+	since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now())
154
+	assertEqual(complete, true, t)
155
+	assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t)
156
+}

+ 5
- 0
irc/modes/modes.go 查看文件

353
 	return utils.BitsetSet(set[:], uint(mode)-minMode, on)
353
 	return utils.BitsetSet(set[:], uint(mode)-minMode, on)
354
 }
354
 }
355
 
355
 
356
+// copy the contents of another modeset on top of this one
357
+func (set *ModeSet) Copy(other *ModeSet) {
358
+	utils.BitsetCopy(set[:], other[:])
359
+}
360
+
356
 // return the modes in the set as a slice
361
 // return the modes in the set as a slice
357
 func (set *ModeSet) AllModes() (result []Mode) {
362
 func (set *ModeSet) AllModes() (result []Mode) {
358
 	if set == nil {
363
 	if set == nil {

+ 18
- 0
irc/monitor.go 查看文件

81
 	return nil
81
 	return nil
82
 }
82
 }
83
 
83
 
84
+func (manager *MonitorManager) Resume(newClient, oldClient *Client) error {
85
+	manager.Lock()
86
+	defer manager.Unlock()
87
+
88
+	// newClient is now watching everyone oldClient was watching
89
+	oldTargets := manager.watching[oldClient]
90
+	delete(manager.watching, oldClient)
91
+	manager.watching[newClient] = oldTargets
92
+
93
+	// update watchedby as well
94
+	for watchedNick := range oldTargets {
95
+		delete(manager.watchedby[watchedNick], oldClient)
96
+		manager.watchedby[watchedNick][newClient] = true
97
+	}
98
+
99
+	return nil
100
+}
101
+
84
 // RemoveAll unregisters `client` from receiving notifications about *all* nicks.
102
 // RemoveAll unregisters `client` from receiving notifications about *all* nicks.
85
 func (manager *MonitorManager) RemoveAll(client *Client) {
103
 func (manager *MonitorManager) RemoveAll(client *Client) {
86
 	manager.Lock()
104
 	manager.Lock()

+ 2
- 1
irc/nickname.go 查看文件

16
 
16
 
17
 var (
17
 var (
18
 	restrictedNicknames = map[string]bool{
18
 	restrictedNicknames = map[string]bool{
19
-		"=scene=": true, // used for rp commands
19
+		"=scene=":  true, // used for rp commands
20
+		"HistServ": true, // TODO(slingamn) this should become a real service
20
 	}
21
 	}
21
 )
22
 )
22
 
23
 

+ 13
- 7
irc/responsebuffer.go 查看文件

8
 
8
 
9
 	"github.com/goshuirc/irc-go/ircmsg"
9
 	"github.com/goshuirc/irc-go/ircmsg"
10
 	"github.com/oragono/oragono/irc/caps"
10
 	"github.com/oragono/oragono/irc/caps"
11
+	"github.com/oragono/oragono/irc/utils"
11
 )
12
 )
12
 
13
 
13
 // ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
14
 // ResponseBuffer - put simply - buffers messages and then outputs them to a given client.
19
 	Label    string
20
 	Label    string
20
 	target   *Client
21
 	target   *Client
21
 	messages []ircmsg.IrcMessage
22
 	messages []ircmsg.IrcMessage
23
+	blocking bool
22
 }
24
 }
23
 
25
 
24
 // GetLabel returns the label from the given message.
26
 // GetLabel returns the label from the given message.
33
 	}
35
 	}
34
 }
36
 }
35
 
37
 
38
+func (rb *ResponseBuffer) SetBlocking(blocking bool) {
39
+	rb.blocking = blocking
40
+}
41
+
36
 // Add adds a standard new message to our queue.
42
 // Add adds a standard new message to our queue.
37
 func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
43
 func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) {
38
 	message := ircmsg.MakeMessage(tags, prefix, command, params...)
44
 	message := ircmsg.MakeMessage(tags, prefix, command, params...)
63
 }
69
 }
64
 
70
 
65
 // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
71
 // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
66
-func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, target string, message SplitMessage) {
67
-	if rb.target.capabilities.Has(caps.MaxLine) {
68
-		rb.AddFromClient(msgid, from, tags, command, target, message.ForMaxLine)
72
+func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, target string, message utils.SplitMessage) {
73
+	if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
74
+		rb.AddFromClient(msgid, from, tags, command, target, message.Original)
69
 	} else {
75
 	} else {
70
-		for _, str := range message.For512 {
76
+		for _, str := range message.Wrapped {
71
 			rb.AddFromClient(msgid, from, tags, command, target, str)
77
 			rb.AddFromClient(msgid, from, tags, command, target, str)
72
 		}
78
 		}
73
 	}
79
 	}
103
 	for _, message := range rb.messages {
109
 	for _, message := range rb.messages {
104
 		// attach server-time if needed
110
 		// attach server-time if needed
105
 		if rb.target.capabilities.Has(caps.ServerTime) {
111
 		if rb.target.capabilities.Has(caps.ServerTime) {
106
-			t := time.Now().UTC().Format("2006-01-02T15:04:05.999Z")
112
+			t := time.Now().UTC().Format(IRCv3TimestampFormat)
107
 			message.Tags["time"] = ircmsg.MakeTagValue(t)
113
 			message.Tags["time"] = ircmsg.MakeTagValue(t)
108
 		}
114
 		}
109
 
115
 
113
 		}
119
 		}
114
 
120
 
115
 		// send message out
121
 		// send message out
116
-		rb.target.SendRawMessage(message)
122
+		rb.target.SendRawMessage(message, rb.blocking)
117
 	}
123
 	}
118
 
124
 
119
 	// end batch if required
125
 	// end batch if required
122
 	}
128
 	}
123
 
129
 
124
 	// clear out any existing messages
130
 	// clear out any existing messages
125
-	rb.messages = []ircmsg.IrcMessage{}
131
+	rb.messages = rb.messages[:0]
126
 
132
 
127
 	return nil
133
 	return nil
128
 }
134
 }

+ 17
- 105
irc/server.go 查看文件

430
 	// continue registration
430
 	// continue registration
431
 	server.logger.Debug("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname))
431
 	server.logger.Debug("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname))
432
 	server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", c.nick, c.username, c.rawHostname, c.IPString(), c.realname))
432
 	server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", c.nick, c.username, c.rawHostname, c.IPString(), c.realname))
433
+
434
+	// "register"; this includes the initial phase of session resumption
433
 	c.Register()
435
 	c.Register()
434
 
436
 
435
 	// send welcome text
437
 	// send welcome text
455
 	}
457
 	}
456
 
458
 
457
 	// if resumed, send fake channel joins
459
 	// if resumed, send fake channel joins
458
-	if c.resumeDetails != nil {
459
-		for _, name := range c.resumeDetails.SendFakeJoinsFor {
460
-			channel := server.channels.Get(name)
461
-			if channel == nil {
462
-				continue
463
-			}
464
-
465
-			if c.capabilities.Has(caps.ExtendedJoin) {
466
-				c.Send(nil, c.nickMaskString, "JOIN", channel.name, c.AccountName(), c.realname)
467
-			} else {
468
-				c.Send(nil, c.nickMaskString, "JOIN", channel.name)
469
-			}
470
-			// reuse the last rb
471
-			channel.SendTopic(c, rb)
472
-			channel.Names(c, rb)
473
-			rb.Send()
474
-
475
-			// construct and send fake modestring if necessary
476
-			c.stateMutex.RLock()
477
-			myModes := channel.members[c]
478
-			c.stateMutex.RUnlock()
479
-			if myModes == nil {
480
-				continue
481
-			}
482
-			oldModes := myModes.String()
483
-			if 0 < len(oldModes) {
484
-				params := []string{channel.name, "+" + oldModes}
485
-				for range oldModes {
486
-					params = append(params, c.nick)
487
-				}
488
-
489
-				c.Send(nil, server.name, "MODE", params...)
490
-			}
491
-		}
492
-	}
460
+	c.tryResumeChannels()
493
 }
461
 }
494
 
462
 
495
 // t returns the translated version of the given string, based on the languages configured by the client.
463
 // t returns the translated version of the given string, based on the languages configured by the client.
519
 	rb.Add(nil, server.name, RPL_ENDOFMOTD, client.nick, client.t("End of MOTD command"))
487
 	rb.Add(nil, server.name, RPL_ENDOFMOTD, client.nick, client.t("End of MOTD command"))
520
 }
488
 }
521
 
489
 
522
-// wordWrap wraps the given text into a series of lines that don't exceed lineWidth characters.
523
-func wordWrap(text string, lineWidth int) []string {
524
-	var lines []string
525
-	var cacheLine, cacheWord string
526
-
527
-	for _, char := range text {
528
-		if char == '\r' {
529
-			continue
530
-		} else if char == '\n' {
531
-			cacheLine += cacheWord
532
-			lines = append(lines, cacheLine)
533
-			cacheWord = ""
534
-			cacheLine = ""
535
-		} else if (char == ' ' || char == '-') && len(cacheLine)+len(cacheWord)+1 < lineWidth {
536
-			// natural word boundary
537
-			cacheLine += cacheWord + string(char)
538
-			cacheWord = ""
539
-		} else if lineWidth <= len(cacheLine)+len(cacheWord)+1 {
540
-			// time to wrap to next line
541
-			if len(cacheLine) < (lineWidth / 2) {
542
-				// this word takes up more than half a line... just split in the middle of the word
543
-				cacheLine += cacheWord + string(char)
544
-				cacheWord = ""
545
-			} else {
546
-				cacheWord += string(char)
547
-			}
548
-			lines = append(lines, cacheLine)
549
-			cacheLine = ""
550
-		} else {
551
-			// normal character
552
-			cacheWord += string(char)
553
-		}
554
-	}
555
-	if 0 < len(cacheWord) {
556
-		cacheLine += cacheWord
557
-	}
558
-	if 0 < len(cacheLine) {
559
-		lines = append(lines, cacheLine)
560
-	}
561
-
562
-	return lines
563
-}
564
-
565
-// SplitMessage represents a message that's been split for sending.
566
-type SplitMessage struct {
567
-	For512     []string
568
-	ForMaxLine string
569
-}
570
-
571
-func (server *Server) splitMessage(original string, origIs512 bool) SplitMessage {
572
-	var newSplit SplitMessage
573
-
574
-	newSplit.ForMaxLine = original
575
-
576
-	if !origIs512 {
577
-		newSplit.For512 = wordWrap(original, 400)
578
-	} else {
579
-		newSplit.For512 = []string{original}
580
-	}
581
-
582
-	return newSplit
583
-}
584
-
585
 // WhoisChannelsNames returns the common channel names between two users.
490
 // WhoisChannelsNames returns the common channel names between two users.
586
 func (client *Client) WhoisChannelsNames(target *Client) []string {
491
 func (client *Client) WhoisChannelsNames(target *Client) []string {
587
 	isMultiPrefix := client.capabilities.Has(caps.MultiPrefix)
492
 	isMultiPrefix := client.capabilities.Has(caps.MultiPrefix)
817
 		updatedCaps.Add(caps.STS)
722
 		updatedCaps.Add(caps.STS)
818
 	}
723
 	}
819
 
724
 
725
+	// resize history buffers as needed
726
+	if oldConfig != nil {
727
+		if oldConfig.History.ChannelLength != config.History.ChannelLength {
728
+			for _, channel := range server.channels.Channels() {
729
+				channel.history.Resize(config.History.ChannelLength)
730
+			}
731
+		}
732
+		if oldConfig.History.ClientLength != config.History.ClientLength {
733
+			for _, client := range server.clients.AllClients() {
734
+				client.history.Resize(config.History.ClientLength)
735
+			}
736
+		}
737
+	}
738
+
820
 	// burst new and removed caps
739
 	// burst new and removed caps
821
 	var capBurstClients ClientSet
740
 	var capBurstClients ClientSet
822
 	added := make(map[caps.Version]string)
741
 	added := make(map[caps.Version]string)
1107
 	rb.Add(nil, target.server.name, RPL_LIST, target.nick, channel.name, strconv.Itoa(memberCount), channel.topic)
1026
 	rb.Add(nil, target.server.name, RPL_LIST, target.nick, channel.name, strconv.Itoa(memberCount), channel.topic)
1108
 }
1027
 }
1109
 
1028
 
1110
-// ResumeDetails are the details that we use to resume connections.
1111
-type ResumeDetails struct {
1112
-	OldNick          string
1113
-	Timestamp        *time.Time
1114
-	SendFakeJoinsFor []string
1115
-}
1116
-
1117
 var (
1029
 var (
1118
 	infoString1 = strings.Split(`      ▄▄▄   ▄▄▄·  ▄▄ •        ▐ ▄
1030
 	infoString1 = strings.Split(`      ▄▄▄   ▄▄▄·  ▄▄ •        ▐ ▄
1119
 ▪     ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪     •█▌▐█▪     
1031
 ▪     ▀▄ █·▐█ ▀█ ▐█ ▀ ▪▪     •█▌▐█▪     

+ 40
- 3
irc/socket.go 查看文件

146
 	return
146
 	return
147
 }
147
 }
148
 
148
 
149
+// BlockingWrite sends the given string out of Socket. Requirements:
150
+// 1. MUST block until the message is sent
151
+// 2. MUST bypass sendq (calls to BlockingWrite cannot, on their own, cause a sendq overflow)
152
+// 3. MUST provide mutual exclusion for socket.conn.Write
153
+// 4. MUST respect the same ordering guarantees as Write (i.e., if a call to Write that sends
154
+//    message m1 happens-before a call to BlockingWrite that sends message m2,
155
+//    m1 must be sent on the wire before m2
156
+// Callers MUST be writing to the client's socket from the client's own goroutine;
157
+// other callers must use the nonblocking Write call instead. Otherwise, a client
158
+// with a slow/unreliable connection risks stalling the progress of the system as a whole.
159
+func (socket *Socket) BlockingWrite(data []byte) (err error) {
160
+	if len(data) == 0 {
161
+		return
162
+	}
163
+
164
+	// blocking acquire of the trylock
165
+	socket.writerSemaphore.Acquire()
166
+	defer socket.writerSemaphore.Release()
167
+
168
+	// first, flush any buffered data, to preserve the ordering guarantees
169
+	closed := socket.performWrite()
170
+	if closed {
171
+		return io.EOF
172
+	}
173
+
174
+	_, err = socket.conn.Write(data)
175
+	if err != nil {
176
+		socket.finalize()
177
+	}
178
+	return
179
+}
180
+
149
 // wakeWriter starts the goroutine that actually performs the write, without blocking
181
 // wakeWriter starts the goroutine that actually performs the write, without blocking
150
 func (socket *Socket) wakeWriter() {
182
 func (socket *Socket) wakeWriter() {
151
 	if socket.writerSemaphore.TryAcquire() {
183
 	if socket.writerSemaphore.TryAcquire() {
199
 }
231
 }
200
 
232
 
201
 // write the contents of the buffer, then see if we need to close
233
 // write the contents of the buffer, then see if we need to close
202
-func (socket *Socket) performWrite() {
234
+// returns whether we closed
235
+func (socket *Socket) performWrite() (closed bool) {
203
 	// retrieve the buffered data, clear the buffer
236
 	// retrieve the buffered data, clear the buffer
204
 	socket.Lock()
237
 	socket.Lock()
205
 	buffers := socket.buffers
238
 	buffers := socket.buffers
214
 	shouldClose := (err != nil) || socket.closed || socket.sendQExceeded
247
 	shouldClose := (err != nil) || socket.closed || socket.sendQExceeded
215
 	socket.Unlock()
248
 	socket.Unlock()
216
 
249
 
217
-	if !shouldClose {
218
-		return
250
+	if shouldClose {
251
+		socket.finalize()
219
 	}
252
 	}
253
+	return shouldClose
254
+}
220
 
255
 
256
+// mark closed and send final data. you must be holding the semaphore to call this:
257
+func (socket *Socket) finalize() {
221
 	// mark the socket closed (if someone hasn't already), then write error lines
258
 	// mark the socket closed (if someone hasn't already), then write error lines
222
 	socket.Lock()
259
 	socket.Lock()
223
 	socket.closed = true
260
 	socket.closed = true

+ 3
- 0
irc/types.go 查看文件

6
 package irc
6
 package irc
7
 
7
 
8
 import "github.com/oragono/oragono/irc/modes"
8
 import "github.com/oragono/oragono/irc/modes"
9
+import "github.com/goshuirc/irc-go/ircmsg"
9
 
10
 
10
 // ClientSet is a set of clients.
11
 // ClientSet is a set of clients.
11
 type ClientSet map[*Client]bool
12
 type ClientSet map[*Client]bool
56
 
57
 
57
 // ChannelSet is a set of channels.
58
 // ChannelSet is a set of channels.
58
 type ChannelSet map[*Channel]bool
59
 type ChannelSet map[*Channel]bool
60
+
61
+type Tags *map[string]ircmsg.TagValue

+ 9
- 0
irc/utils/bitset.go 查看文件

80
 		}
80
 		}
81
 	}
81
 	}
82
 }
82
 }
83
+
84
+// BitsetCopy copies the contents of `other` over `set`.
85
+// Similar caveats about race conditions as with `BitsetUnion` apply.
86
+func BitsetCopy(set []uint64, other []uint64) {
87
+	for i := 0; i < len(set); i++ {
88
+		data := atomic.LoadUint64(&other[i])
89
+		atomic.StoreUint64(&set[i], data)
90
+	}
91
+}

+ 15
- 0
irc/utils/bitset_test.go 查看文件

62
 			t.Error("all bits should be set except 72")
62
 			t.Error("all bits should be set except 72")
63
 		}
63
 		}
64
 	}
64
 	}
65
+
66
+	var t3 testBitset
67
+	t3s := t3[:]
68
+	BitsetSet(t3s, 72, true)
69
+	if !BitsetGet(t3s, 72) {
70
+		t.Error("bit 72 should be set")
71
+	}
72
+	// copy t1 on top of t2
73
+	BitsetCopy(t3s, t1s)
74
+	for i = 0; i < 128; i++ {
75
+		expected := (i != 72)
76
+		if BitsetGet(t1s, i) != expected {
77
+			t.Error("all bits should be set except 72")
78
+		}
79
+	}
65
 }
80
 }

+ 30
- 0
irc/utils/crypto.go 查看文件

1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"crypto/rand"
8
+	"crypto/subtle"
9
+	"encoding/hex"
10
+)
11
+
12
+// generate a secret token that cannot be brute-forced via online attacks
13
+func GenerateSecretToken() string {
14
+	// 128 bits of entropy are enough to resist any online attack:
15
+	var buf [16]byte
16
+	rand.Read(buf[:])
17
+	// 32 ASCII characters, should be fine for most purposes
18
+	return hex.EncodeToString(buf[:])
19
+}
20
+
21
+// securely check if a supplied token matches a stored token
22
+func SecretTokensMatch(storedToken string, suppliedToken string) bool {
23
+	// XXX fix a potential gotcha: if the stored token is uninitialized,
24
+	// then nothing should match it, not even supplying an empty token.
25
+	if len(storedToken) == 0 {
26
+		return false
27
+	}
28
+
29
+	return subtle.ConstantTimeCompare([]byte(storedToken), []byte(suppliedToken)) == 1
30
+}

+ 48
- 0
irc/utils/crypto_test.go 查看文件

1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"testing"
8
+)
9
+
10
+const (
11
+	storedToken = "1e82d113a59a874cccf82063ec603221"
12
+	badToken    = "1e82d113a59a874cccf82063ec603222"
13
+	shortToken  = "1e82d113a59a874cccf82063ec60322"
14
+	longToken   = "1e82d113a59a874cccf82063ec6032211"
15
+)
16
+
17
+func TestGenerateSecretToken(t *testing.T) {
18
+	token := GenerateSecretToken()
19
+	if len(token) != 32 {
20
+		t.Errorf("bad token: %v", token)
21
+	}
22
+}
23
+
24
+func TestTokenCompare(t *testing.T) {
25
+	if !SecretTokensMatch(storedToken, storedToken) {
26
+		t.Error("matching tokens must match")
27
+	}
28
+
29
+	if SecretTokensMatch(storedToken, badToken) {
30
+		t.Error("non-matching tokens must not match")
31
+	}
32
+
33
+	if SecretTokensMatch(storedToken, shortToken) {
34
+		t.Error("non-matching tokens must not match")
35
+	}
36
+
37
+	if SecretTokensMatch(storedToken, longToken) {
38
+		t.Error("non-matching tokens must not match")
39
+	}
40
+
41
+	if SecretTokensMatch("", "") {
42
+		t.Error("the empty token should not match anything")
43
+	}
44
+
45
+	if SecretTokensMatch("", storedToken) {
46
+		t.Error("the empty token should not match anything")
47
+	}
48
+}

+ 67
- 0
irc/utils/text.go 查看文件

1
+// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import "bytes"
7
+
8
+// WordWrap wraps the given text into a series of lines that don't exceed lineWidth characters.
9
+func WordWrap(text string, lineWidth int) []string {
10
+	var lines []string
11
+	var cacheLine, cacheWord bytes.Buffer
12
+
13
+	for _, char := range text {
14
+		if char == '\r' {
15
+			continue
16
+		} else if char == '\n' {
17
+			cacheLine.Write(cacheWord.Bytes())
18
+			lines = append(lines, cacheLine.String())
19
+			cacheWord.Reset()
20
+			cacheLine.Reset()
21
+		} else if (char == ' ' || char == '-') && cacheLine.Len()+cacheWord.Len()+1 < lineWidth {
22
+			// natural word boundary
23
+			cacheLine.Write(cacheWord.Bytes())
24
+			cacheLine.WriteRune(char)
25
+			cacheWord.Reset()
26
+		} else if lineWidth <= cacheLine.Len()+cacheWord.Len()+1 {
27
+			// time to wrap to next line
28
+			if cacheLine.Len() < (lineWidth / 2) {
29
+				// this word takes up more than half a line... just split in the middle of the word
30
+				cacheLine.Write(cacheWord.Bytes())
31
+				cacheLine.WriteRune(char)
32
+				cacheWord.Reset()
33
+			} else {
34
+				cacheWord.WriteRune(char)
35
+			}
36
+			lines = append(lines, cacheLine.String())
37
+			cacheLine.Reset()
38
+		} else {
39
+			// normal character
40
+			cacheWord.WriteRune(char)
41
+		}
42
+	}
43
+	if 0 < cacheWord.Len() {
44
+		cacheLine.Write(cacheWord.Bytes())
45
+	}
46
+	if 0 < cacheLine.Len() {
47
+		lines = append(lines, cacheLine.String())
48
+	}
49
+
50
+	return lines
51
+}
52
+
53
+// SplitMessage represents a message that's been split for sending.
54
+type SplitMessage struct {
55
+	Original string
56
+	Wrapped  []string // if this is nil, Original didn't need wrapping and can be sent to anyone
57
+}
58
+
59
+func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) {
60
+	result.Original = original
61
+
62
+	if !origIs512 {
63
+		result.Wrapped = WordWrap(original, 400)
64
+	}
65
+
66
+	return
67
+}

+ 60
- 0
irc/utils/text_test.go 查看文件

1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package utils
5
+
6
+import (
7
+	"reflect"
8
+	"strings"
9
+	"testing"
10
+)
11
+
12
+const (
13
+	threeMusketeers = "In the meantime D’Artagnan, who had plunged into a bypath, continued his route and reached St. Cloud; but instead of following the main street he turned behind the château, reached a sort of retired lane, and found himself soon in front of the pavilion named. It was situated in a very private spot. A high wall, at the angle of which was the pavilion, ran along one side of this lane, and on the other was a little garden connected with a poor cottage which was protected by a hedge from passers-by."
14
+
15
+	monteCristo = `Both the count and Baptistin had told the truth when they announced to Morcerf the proposed visit of the major, which had served Monte Cristo as a pretext for declining Albert's invitation. Seven o'clock had just struck, and M. Bertuccio, according to the command which had been given him, had two hours before left for Auteuil, when a cab stopped at the door, and after depositing its occupant at the gate, immediately hurried away, as if ashamed of its employment. The visitor was about fifty-two years of age, dressed in one of the green surtouts, ornamented with black frogs, which have so long maintained their popularity all over Europe. He wore trousers of blue cloth, boots tolerably clean, but not of the brightest polish, and a little too thick in the soles, buckskin gloves, a hat somewhat resembling in shape those usually worn by the gendarmes, and a black cravat striped with white, which, if the proprietor had not worn it of his own free will, might have passed for a halter, so much did it resemble one. Such was the picturesque costume of the person who rang at the gate, and demanded if it was not at No. 30 in the Avenue des Champs-Elysees that the Count of Monte Cristo lived, and who, being answered by the porter in the affirmative, entered, closed the gate after him, and began to ascend the steps.`
16
+)
17
+
18
+func assertWrapCorrect(text string, lineWidth int, allowSplitWords bool, t *testing.T) {
19
+	lines := WordWrap(text, lineWidth)
20
+
21
+	reconstructed := strings.Join(lines, "")
22
+	if text != reconstructed {
23
+		t.Errorf("text %v does not match original %v", text, reconstructed)
24
+	}
25
+
26
+	for _, line := range lines {
27
+		if len(line) > lineWidth {
28
+			t.Errorf("line too long: %d, %v", len(line), line)
29
+		}
30
+	}
31
+
32
+	if !allowSplitWords {
33
+		origWords := strings.Fields(text)
34
+		var newWords []string
35
+		for _, line := range lines {
36
+			newWords = append(newWords, strings.Fields(line)...)
37
+		}
38
+
39
+		if !reflect.DeepEqual(origWords, newWords) {
40
+			t.Errorf("words %v do not match wrapped words %v", origWords, newWords)
41
+		}
42
+	}
43
+
44
+}
45
+
46
+func TestWordWrap(t *testing.T) {
47
+	assertWrapCorrect("jackdaws love my big sphinx of quartz", 12, false, t)
48
+	// long word that will necessarily be split:
49
+	assertWrapCorrect("jackdawslovemybigsphinxofquartz", 12, true, t)
50
+
51
+	assertWrapCorrect(threeMusketeers, 40, true, t)
52
+	assertWrapCorrect(monteCristo, 20, false, t)
53
+}
54
+
55
+func BenchmarkWordWrap(b *testing.B) {
56
+	for i := 0; i < b.N; i++ {
57
+		WordWrap(threeMusketeers, 40)
58
+		WordWrap(monteCristo, 60)
59
+	}
60
+}

+ 15
- 0
oragono.yaml 查看文件

92
                 # - "127.0.0.1/8"
92
                 # - "127.0.0.1/8"
93
                 # - "0::1"
93
                 # - "0::1"
94
 
94
 
95
+    # allow use of the RESUME extension over plaintext connections:
96
+    # do not enable this unless the ircd is only accessible over internal networks
97
+    allow-plaintext-resume: false
98
+
95
     # maximum length of clients' sendQ in bytes
99
     # maximum length of clients' sendQ in bytes
96
     # this should be big enough to hold /LIST and HELP replies
100
     # this should be big enough to hold /LIST and HELP replies
97
     max-sendq: 16k
101
     max-sendq: 16k
439
     # client status resets to the default state if they go this long without
443
     # client status resets to the default state if they go this long without
440
     # sending any commands:
444
     # sending any commands:
441
     cooldown: 2s
445
     cooldown: 2s
446
+
447
+# message history tracking, for the RESUME extension and possibly other uses in future
448
+history:
449
+    # should we store messages for later playback?
450
+    enabled: true
451
+
452
+    # how many channel-specific events (messages, joins, parts) should be tracked per channel?
453
+    channel-length: 256
454
+
455
+    # how many direct messages and notices should be tracked per user?
456
+    client-length: 64

Loading…
取消
儲存