Sfoglia il codice sorgente

initial implementation of bouncer functionality

tags/v1.1.0-rc1
Shivaram Lingamneni 5 anni fa
parent
commit
c2faeed4b5
19 ha cambiato i file con 733 aggiunte e 441 eliminazioni
  1. 12
    0
      gencapdefs.py
  2. 11
    4
      irc/accounts.go
  3. 11
    1
      irc/caps/defs.go
  4. 26
    0
      irc/caps/set.go
  5. 129
    66
      irc/channel.go
  6. 244
    174
      irc/client.go
  7. 23
    17
      irc/client_lookup_set.go
  8. 5
    5
      irc/commands.go
  9. 5
    1
      irc/config.go
  10. 6
    6
      irc/gateways.go
  11. 37
    6
      irc/getters.go
  12. 109
    76
      irc/handlers.go
  13. 14
    9
      irc/idletimer.go
  14. 15
    9
      irc/nickname.go
  15. 2
    2
      irc/nickserv.go
  16. 13
    11
      irc/responsebuffer.go
  17. 9
    8
      irc/roleplay.go
  18. 49
    46
      irc/server.go
  19. 13
    0
      oragono.yaml

+ 12
- 0
gencapdefs.py Vedi File

@@ -147,6 +147,18 @@ CAPDEFS = [
147 147
         url="https://ircv3.net/specs/extensions/userhost-in-names-3.2.html",
148 148
         standard="IRCv3",
149 149
     ),
150
+    CapDef(
151
+        identifier="Bouncer",
152
+        name="oragono.io/bnc",
153
+        url="https://oragono.io/bnc",
154
+        standard="Oragono-specific",
155
+    ),
156
+    CapDef(
157
+        identifier="ZNCSelfMessage",
158
+        name="znc.in/self-message",
159
+        url="https://wiki.znc.in/Query_buffers",
160
+        standard="ZNC vendor",
161
+    ),
150 162
 ]
151 163
 
152 164
 def validate_defs():

+ 11
- 4
irc/accounts.go Vedi File

@@ -221,8 +221,6 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
221 221
 		nickMethod := finalEnforcementMethod(nickAccount)
222 222
 		skelMethod := finalEnforcementMethod(skelAccount)
223 223
 		switch {
224
-		case nickMethod == NickReservationNone && skelMethod == NickReservationNone:
225
-			return nickAccount, NickReservationNone
226 224
 		case skelMethod == NickReservationNone:
227 225
 			return nickAccount, nickMethod
228 226
 		case nickMethod == NickReservationNone:
@@ -234,6 +232,15 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
234 232
 	}
235 233
 }
236 234
 
235
+func (am *AccountManager) BouncerAllowed(account string, session *Session) bool {
236
+	// TODO stub
237
+	config := am.server.Config()
238
+	if !config.Accounts.Bouncer.Enabled {
239
+		return false
240
+	}
241
+	return config.Accounts.Bouncer.AllowedByDefault || session.capabilities.Has(caps.Bouncer)
242
+}
243
+
237 244
 // Looks up the enforcement method stored in the database for an account
238 245
 // (typically you want EnforcementStatus instead, which respects the config)
239 246
 func (am *AccountManager) getStoredEnforcementStatus(account string) string {
@@ -928,9 +935,9 @@ func (am *AccountManager) Unregister(account string) error {
928 935
 	}
929 936
 	for _, client := range clients {
930 937
 		if config.Accounts.RequireSasl.Enabled {
931
-			client.Quit(client.t("You are no longer authorized to be on this server"))
938
+			client.Quit(client.t("You are no longer authorized to be on this server"), nil)
932 939
 			// destroy acquires a semaphore so we can't call it while holding a lock
933
-			go client.destroy(false)
940
+			go client.destroy(false, nil)
934 941
 		} else {
935 942
 			am.logoutOfAccount(client)
936 943
 		}

+ 11
- 1
irc/caps/defs.go Vedi File

@@ -7,7 +7,7 @@ package caps
7 7
 
8 8
 const (
9 9
 	// number of recognized capabilities:
10
-	numCapabs = 22
10
+	numCapabs = 24
11 11
 	// length of the uint64 array that represents the bitset:
12 12
 	bitsetLen = 1
13 13
 )
@@ -100,6 +100,14 @@ const (
100 100
 	// UserhostInNames is the IRCv3 capability named "userhost-in-names":
101 101
 	// https://ircv3.net/specs/extensions/userhost-in-names-3.2.html
102 102
 	UserhostInNames Capability = iota
103
+
104
+	// Bouncer is the Oragono-specific capability named "oragono.io/bnc":
105
+	// https://oragono.io/bnc
106
+	Bouncer Capability = iota
107
+
108
+	// ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message":
109
+	// https://wiki.znc.in/Query_buffers
110
+	ZNCSelfMessage Capability = iota
103 111
 )
104 112
 
105 113
 // `capabilityNames[capab]` is the string name of the capability `capab`
@@ -127,5 +135,7 @@ var (
127 135
 		"draft/setname",
128 136
 		"sts",
129 137
 		"userhost-in-names",
138
+		"oragono.io/bnc",
139
+		"znc.in/self-message",
130 140
 	}
131 141
 )

+ 26
- 0
irc/caps/set.go Vedi File

@@ -20,6 +20,16 @@ func NewSet(capabs ...Capability) *Set {
20 20
 	return &newSet
21 21
 }
22 22
 
23
+// NewCompleteSet returns a new Set, with all defined capabilities enabled.
24
+func NewCompleteSet() *Set {
25
+	var newSet Set
26
+	asSlice := newSet[:]
27
+	for i := 0; i < numCapabs; i += 1 {
28
+		utils.BitsetSet(asSlice, uint(i), true)
29
+	}
30
+	return &newSet
31
+}
32
+
23 33
 // Enable enables the given capabilities.
24 34
 func (s *Set) Enable(capabs ...Capability) {
25 35
 	asSlice := s[:]
@@ -53,6 +63,16 @@ func (s *Set) Has(capab Capability) bool {
53 63
 	return utils.BitsetGet(s[:], uint(capab))
54 64
 }
55 65
 
66
+// HasAll returns true if the set has all the given capabilities.
67
+func (s *Set) HasAll(capabs ...Capability) bool {
68
+	for _, capab := range capabs {
69
+		if !s.Has(capab) {
70
+			return false
71
+		}
72
+	}
73
+	return true
74
+}
75
+
56 76
 // Union adds all the capabilities of another set to this set.
57 77
 func (s *Set) Union(other *Set) {
58 78
 	utils.BitsetUnion(s[:], other[:])
@@ -94,3 +114,9 @@ func (s *Set) String(version Version, values *Values) string {
94 114
 
95 115
 	return strings.Join(strs, " ")
96 116
 }
117
+
118
+// returns whether we should send `znc.in/self-message`-style echo messages
119
+// to sessions other than that which originated the message
120
+func (capabs *Set) SelfMessagesEnabled() bool {
121
+	return capabs.Has(EchoMessage) || capabs.Has(ZNCSelfMessage)
122
+}

+ 129
- 66
irc/channel.go Vedi File

@@ -335,8 +335,8 @@ func (channel *Channel) regenerateMembersCache() {
335 335
 
336 336
 // Names sends the list of users joined to the channel to the given client.
337 337
 func (channel *Channel) Names(client *Client, rb *ResponseBuffer) {
338
-	isMultiPrefix := client.capabilities.Has(caps.MultiPrefix)
339
-	isUserhostInNames := client.capabilities.Has(caps.UserhostInNames)
338
+	isMultiPrefix := rb.session.capabilities.Has(caps.MultiPrefix)
339
+	isUserhostInNames := rb.session.capabilities.Has(caps.UserhostInNames)
340 340
 
341 341
 	maxNamLen := 480 - len(client.server.name) - len(client.Nick())
342 342
 	var namesLines []string
@@ -578,28 +578,35 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
578 578
 	}
579 579
 
580 580
 	for _, member := range channel.Members() {
581
-		if member == client {
582
-			continue
583
-		}
584
-		if member.capabilities.Has(caps.ExtendedJoin) {
585
-			member.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
586
-		} else {
587
-			member.Send(nil, details.nickMask, "JOIN", chname)
588
-		}
589
-		if givenMode != 0 {
590
-			member.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
581
+		for _, session := range member.Sessions() {
582
+			if session == rb.session {
583
+				continue
584
+			} else if client == session.client {
585
+				channel.playJoinForSession(session)
586
+				continue
587
+			}
588
+			if session.capabilities.Has(caps.ExtendedJoin) {
589
+				session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
590
+			} else {
591
+				session.Send(nil, details.nickMask, "JOIN", chname)
592
+			}
593
+			if givenMode != 0 {
594
+				session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick)
595
+			}
591 596
 		}
592 597
 	}
593 598
 
594
-	if client.capabilities.Has(caps.ExtendedJoin) {
599
+	if rb.session.capabilities.Has(caps.ExtendedJoin) {
595 600
 		rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname)
596 601
 	} else {
597 602
 		rb.Add(nil, details.nickMask, "JOIN", chname)
598 603
 	}
599 604
 
600
-	channel.SendTopic(client, rb, false)
601
-
602
-	channel.Names(client, rb)
605
+	if rb.session.client == client {
606
+		// don't send topic and names for a SAJOIN of a different client
607
+		channel.SendTopic(client, rb, false)
608
+		channel.Names(client, rb)
609
+	}
603 610
 
604 611
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
605 612
 	rb.Flush(true)
@@ -612,6 +619,23 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
612 619
 	}
613 620
 }
614 621
 
622
+// plays channel join messages (the JOIN line, topic, and names) to a session.
623
+// this is used when attaching a new session to an existing client that already has
624
+// channels, and also when one session of a client initiates a JOIN and the other
625
+// sessions need to receive the state change
626
+func (channel *Channel) playJoinForSession(session *Session) {
627
+	client := session.client
628
+	sessionRb := NewResponseBuffer(session)
629
+	if session.capabilities.Has(caps.ExtendedJoin) {
630
+		sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name(), client.AccountName(), client.Realname())
631
+	} else {
632
+		sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name())
633
+	}
634
+	channel.SendTopic(client, sessionRb, false)
635
+	channel.Names(client, sessionRb)
636
+	sessionRb.Send(false)
637
+}
638
+
615 639
 // Part parts the given client from this channel, with the given message.
616 640
 func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) {
617 641
 	chname := channel.Name()
@@ -627,6 +651,11 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
627 651
 		member.Send(nil, details.nickMask, "PART", chname, message)
628 652
 	}
629 653
 	rb.Add(nil, details.nickMask, "PART", chname, message)
654
+	for _, session := range client.Sessions() {
655
+		if session != rb.session {
656
+			session.Send(nil, details.nickMask, "PART", chname, message)
657
+		}
658
+	}
630 659
 
631 660
 	channel.history.Add(history.Item{
632 661
 		Type:        history.Part,
@@ -683,24 +712,26 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
683 712
 	accountName := newClient.AccountName()
684 713
 	realName := newClient.Realname()
685 714
 	for _, member := range channel.Members() {
686
-		if member.capabilities.Has(caps.Resume) {
687
-			continue
688
-		}
715
+		for _, session := range member.Sessions() {
716
+			if session.capabilities.Has(caps.Resume) {
717
+				continue
718
+			}
689 719
 
690
-		if member.capabilities.Has(caps.ExtendedJoin) {
691
-			member.Send(nil, nickMask, "JOIN", channel.name, accountName, realName)
692
-		} else {
693
-			member.Send(nil, nickMask, "JOIN", channel.name)
694
-		}
720
+			if session.capabilities.Has(caps.ExtendedJoin) {
721
+				session.Send(nil, nickMask, "JOIN", channel.name, accountName, realName)
722
+			} else {
723
+				session.Send(nil, nickMask, "JOIN", channel.name)
724
+			}
695 725
 
696
-		if 0 < len(oldModes) {
697
-			member.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick)
726
+			if 0 < len(oldModes) {
727
+				session.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick)
728
+			}
698 729
 		}
699 730
 	}
700 731
 
701
-	rb := NewResponseBuffer(newClient)
732
+	rb := NewResponseBuffer(newClient.Sessions()[0])
702 733
 	// use blocking i/o to synchronize with the later history replay
703
-	if newClient.capabilities.Has(caps.ExtendedJoin) {
734
+	if rb.session.capabilities.Has(caps.ExtendedJoin) {
704 735
 		rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName)
705 736
 	} else {
706 737
 		rb.Add(nil, nickMask, "JOIN", channel.name)
@@ -715,7 +746,7 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
715 746
 
716 747
 func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) {
717 748
 	items, complete := channel.history.Between(after, before, false, 0)
718
-	rb := NewResponseBuffer(newClient)
749
+	rb := NewResponseBuffer(newClient.Sessions()[0])
719 750
 	channel.replayHistoryItems(rb, items)
720 751
 	if !complete && !newClient.resumeDetails.HistoryIncomplete {
721 752
 		// warn here if we didn't warn already
@@ -735,7 +766,7 @@ func stripMaskFromNick(nickMask string) (nick string) {
735 766
 func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) {
736 767
 	chname := channel.Name()
737 768
 	client := rb.target
738
-	serverTime := client.capabilities.Has(caps.ServerTime)
769
+	serverTime := rb.session.capabilities.Has(caps.ServerTime)
739 770
 
740 771
 	for _, item := range items {
741 772
 		var tags map[string]string
@@ -778,18 +809,19 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
778 809
 // SendTopic sends the channel topic to the given client.
779 810
 // `sendNoTopic` controls whether RPL_NOTOPIC is sent when the topic is unset
780 811
 func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer, sendNoTopic bool) {
781
-	if !channel.hasClient(client) {
782
-		rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.name, client.t("You're not on that channel"))
783
-		return
784
-	}
785
-
786 812
 	channel.stateMutex.RLock()
787 813
 	name := channel.name
788 814
 	topic := channel.topic
789 815
 	topicSetBy := channel.topicSetBy
790 816
 	topicSetTime := channel.topicSetTime
817
+	_, hasClient := channel.members[client]
791 818
 	channel.stateMutex.RUnlock()
792 819
 
820
+	if !hasClient {
821
+		rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, client.Nick(), channel.name, client.t("You're not on that channel"))
822
+		return
823
+	}
824
+
793 825
 	if topic == "" {
794 826
 		if sendNoTopic {
795 827
 			rb.Add(nil, client.server.name, RPL_NOTOPIC, client.nick, name, client.t("No topic is set"))
@@ -824,11 +856,14 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
824 856
 	channel.topicSetTime = time.Now()
825 857
 	channel.stateMutex.Unlock()
826 858
 
859
+	prefix := client.NickMaskString()
827 860
 	for _, member := range channel.Members() {
828
-		if member == client {
829
-			rb.Add(nil, client.nickMaskString, "TOPIC", channel.name, topic)
830
-		} else {
831
-			member.Send(nil, client.nickMaskString, "TOPIC", channel.name, topic)
861
+		for _, session := range member.Sessions() {
862
+			if session == rb.session {
863
+				rb.Add(nil, prefix, "TOPIC", channel.name, topic)
864
+			} else {
865
+				session.Send(nil, prefix, "TOPIC", channel.name, topic)
866
+			}
832 867
 		}
833 868
 	}
834 869
 
@@ -880,51 +915,68 @@ func (channel *Channel) SendSplitMessage(command string, minPrefix *modes.Mode,
880 915
 		return
881 916
 	}
882 917
 
918
+	nickmask := client.NickMaskString()
919
+	account := client.AccountName()
920
+	chname := channel.Name()
921
+	now := time.Now().UTC()
922
+
883 923
 	// for STATUSMSG
884 924
 	var minPrefixMode modes.Mode
885 925
 	if minPrefix != nil {
886 926
 		minPrefixMode = *minPrefix
887 927
 	}
888 928
 	// send echo-message
889
-	if client.capabilities.Has(caps.EchoMessage) {
929
+	// TODO this should use `now` as the time for consistency
930
+	if rb.session.capabilities.Has(caps.EchoMessage) {
890 931
 		var tagsToUse map[string]string
891
-		if client.capabilities.Has(caps.MessageTags) {
932
+		if rb.session.capabilities.Has(caps.MessageTags) {
892 933
 			tagsToUse = clientOnlyTags
893 934
 		}
894
-		nickMaskString := client.NickMaskString()
895
-		accountName := client.AccountName()
896
-		if histType == history.Tagmsg && client.capabilities.Has(caps.MessageTags) {
897
-			rb.AddFromClient(message.Msgid, nickMaskString, accountName, tagsToUse, command, channel.name)
935
+		if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
936
+			rb.AddFromClient(message.Msgid, nickmask, account, tagsToUse, command, chname)
898 937
 		} else {
899
-			rb.AddSplitMessageFromClient(nickMaskString, accountName, tagsToUse, command, channel.name, message)
938
+			rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message)
900 939
 		}
901 940
 	}
902
-
903
-	nickmask := client.NickMaskString()
904
-	account := client.AccountName()
905
-
906
-	now := time.Now().UTC()
907
-
908
-	for _, member := range channel.Members() {
909
-		if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
910
-			// STATUSMSG
941
+	// send echo-message to other connected sessions
942
+	for _, session := range client.Sessions() {
943
+		if session == rb.session || !session.capabilities.SelfMessagesEnabled() {
911 944
 			continue
912 945
 		}
946
+		var tagsToUse map[string]string
947
+		if session.capabilities.Has(caps.MessageTags) {
948
+			tagsToUse = clientOnlyTags
949
+		}
950
+		if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) {
951
+			session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname)
952
+		} else {
953
+			session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message)
954
+		}
955
+	}
956
+
957
+	for _, member := range channel.Members() {
913 958
 		// echo-message is handled above, so skip sending the msg to the user themselves as well
914 959
 		if member == client {
915 960
 			continue
916 961
 		}
917
-		var tagsToUse map[string]string
918
-		if member.capabilities.Has(caps.MessageTags) {
919
-			tagsToUse = clientOnlyTags
920
-		} else if histType == history.Tagmsg {
962
+		if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) {
963
+			// STATUSMSG
921 964
 			continue
922 965
 		}
923 966
 
924
-		if histType == history.Tagmsg {
925
-			member.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, channel.name)
926
-		} else {
927
-			member.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, channel.name, message)
967
+		for _, session := range member.Sessions() {
968
+			var tagsToUse map[string]string
969
+			if session.capabilities.Has(caps.MessageTags) {
970
+				tagsToUse = clientOnlyTags
971
+			} else if histType == history.Tagmsg {
972
+				continue
973
+			}
974
+
975
+			if histType == history.Tagmsg {
976
+				session.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, chname)
977
+			} else {
978
+				session.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, chname, message)
979
+			}
928 980
 		}
929 981
 	}
930 982
 
@@ -1059,9 +1111,15 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
1059 1111
 
1060 1112
 	clientMask := client.NickMaskString()
1061 1113
 	targetNick := target.Nick()
1114
+	chname := channel.Name()
1062 1115
 	for _, member := range channel.Members() {
1063
-		member.Send(nil, clientMask, "KICK", channel.name, targetNick, comment)
1116
+		for _, session := range member.Sessions() {
1117
+			if session != rb.session {
1118
+				session.Send(nil, clientMask, "KICK", chname, targetNick, comment)
1119
+			}
1120
+		}
1064 1121
 	}
1122
+	rb.Add(nil, clientMask, "KICK", chname, targetNick, comment)
1065 1123
 
1066 1124
 	message := utils.SplitMessage{}
1067 1125
 	message.Message = comment
@@ -1094,8 +1152,13 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
1094 1152
 	}
1095 1153
 
1096 1154
 	for _, member := range channel.Members() {
1097
-		if member.capabilities.Has(caps.InviteNotify) && member != inviter && member != invitee && channel.ClientIsAtLeast(member, modes.Halfop) {
1098
-			member.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname)
1155
+		if member == inviter || member == invitee || !channel.ClientIsAtLeast(member, modes.Halfop) {
1156
+			continue
1157
+		}
1158
+		for _, session := range member.Sessions() {
1159
+			if session.capabilities.Has(caps.InviteNotify) {
1160
+				session.Send(nil, inviter.NickMaskString(), "INVITE", invitee.Nick(), chname)
1161
+			}
1099 1162
 		}
1100 1163
 	}
1101 1164
 

+ 244
- 174
irc/client.go Vedi File

@@ -50,26 +50,16 @@ type Client struct {
50 50
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
51 51
 	atime              time.Time
52 52
 	awayMessage        string
53
-	capabilities       caps.Set
54
-	capState           caps.State
55
-	capVersion         caps.Version
56 53
 	certfp             string
57 54
 	channels           ChannelSet
58 55
 	ctime              time.Time
59 56
 	exitedSnomaskSent  bool
60
-	fakelag            Fakelag
61 57
 	flags              modes.ModeSet
62
-	hasQuit            bool
63
-	hops               int
64 58
 	hostname           string
65
-	idletimer          IdleTimer
66 59
 	invitedTo          map[string]bool
67
-	isDestroyed        bool
68 60
 	isTor              bool
69
-	isQuitting         bool
70 61
 	languages          []string
71 62
 	loginThrottle      connection_limits.GenericThrottle
72
-	maxlenRest         uint32
73 63
 	nick               string
74 64
 	nickCasefolded     string
75 65
 	nickMaskCasefolded string
@@ -78,7 +68,6 @@ type Client struct {
78 68
 	oper               *Oper
79 69
 	preregNick         string
80 70
 	proxiedIP          net.IP // actual remote IP if using the PROXY protocol
81
-	quitMessage        string
82 71
 	rawHostname        string
83 72
 	realname           string
84 73
 	realIP             net.IP
@@ -91,13 +80,64 @@ type Client struct {
91 80
 	sentPassCommand    bool
92 81
 	server             *Server
93 82
 	skeleton           string
94
-	socket             *Socket
83
+	sessions           []*Session
95 84
 	stateMutex         sync.RWMutex // tier 1
96 85
 	username           string
97 86
 	vhost              string
98 87
 	history            *history.Buffer
99 88
 }
100 89
 
90
+// Session is an individual client connection to the server (TCP connection
91
+// and associated per-connection data, such as capabilities). There is a
92
+// many-one relationship between sessions and clients.
93
+type Session struct {
94
+	client *Client
95
+
96
+	socket    *Socket
97
+	idletimer IdleTimer
98
+	fakelag   Fakelag
99
+
100
+	quitMessage string
101
+
102
+	capabilities caps.Set
103
+	maxlenRest   uint32
104
+	capState     caps.State
105
+	capVersion   caps.Version
106
+
107
+	// TODO track per-connection real IP, proxied IP, and hostname here,
108
+	// so we can list attached sessions and their details
109
+}
110
+
111
+// sets the session quit message, if there isn't one already
112
+func (sd *Session) SetQuitMessage(message string) (set bool) {
113
+	if message == "" {
114
+		if sd.quitMessage == "" {
115
+			sd.quitMessage = "Connection closed"
116
+			return true
117
+		} else {
118
+			return false
119
+		}
120
+	} else {
121
+		sd.quitMessage = message
122
+		return true
123
+	}
124
+}
125
+
126
+// set the negotiated message length based on session capabilities
127
+func (session *Session) SetMaxlenRest() {
128
+	maxlenRest := 512
129
+	if session.capabilities.Has(caps.MaxLine) {
130
+		maxlenRest = session.client.server.Config().Limits.LineLen.Rest
131
+	}
132
+	atomic.StoreUint32(&session.maxlenRest, uint32(maxlenRest))
133
+}
134
+
135
+// allow the negotiated message length limit to be read without locks; this is a convenience
136
+// so that Session.SendRawMessage doesn't have to acquire any Client locks
137
+func (session *Session) MaxlenRest() int {
138
+	return int(atomic.LoadUint32(&session.maxlenRest))
139
+}
140
+
101 141
 // WhoWas is the subset of client details needed to answer a WHOWAS query
102 142
 type WhoWas struct {
103 143
 	nick           string
@@ -125,32 +165,35 @@ func RunNewClient(server *Server, conn clientConn) {
125 165
 	// give them 1k of grace over the limit:
126 166
 	socket := NewSocket(conn.Conn, fullLineLenLimit+1024, config.Server.MaxSendQBytes)
127 167
 	client := &Client{
128
-		atime:      now,
129
-		capState:   caps.NoneState,
130
-		capVersion: caps.Cap301,
131
-		channels:   make(ChannelSet),
132
-		ctime:      now,
133
-		isTor:      conn.IsTor,
134
-		languages:  server.Languages().Default(),
168
+		atime:     now,
169
+		channels:  make(ChannelSet),
170
+		ctime:     now,
171
+		isTor:     conn.IsTor,
172
+		languages: server.Languages().Default(),
135 173
 		loginThrottle: connection_limits.GenericThrottle{
136 174
 			Duration: config.Accounts.LoginThrottling.Duration,
137 175
 			Limit:    config.Accounts.LoginThrottling.MaxAttempts,
138 176
 		},
139 177
 		server:         server,
140
-		socket:         socket,
141 178
 		accountName:    "*",
142 179
 		nick:           "*", // * is used until actual nick is given
143 180
 		nickCasefolded: "*",
144 181
 		nickMaskString: "*", // * is used until actual nick is given
145 182
 		history:        history.NewHistoryBuffer(config.History.ClientLength),
146 183
 	}
147
-
148
-	client.recomputeMaxlens()
184
+	session := &Session{
185
+		client:     client,
186
+		socket:     socket,
187
+		capVersion: caps.Cap301,
188
+		capState:   caps.NoneState,
189
+	}
190
+	session.SetMaxlenRest()
191
+	client.sessions = []*Session{session}
149 192
 
150 193
 	if conn.IsTLS {
151 194
 		client.SetMode(modes.TLS, true)
152 195
 		// error is not useful to us here anyways so we can ignore it
153
-		client.certfp, _ = client.socket.CertFP()
196
+		client.certfp, _ = socket.CertFP()
154 197
 	}
155 198
 
156 199
 	if conn.IsTor {
@@ -168,7 +211,7 @@ func RunNewClient(server *Server, conn clientConn) {
168 211
 		}
169 212
 	}
170 213
 
171
-	client.run()
214
+	client.run(session)
172 215
 }
173 216
 
174 217
 func (client *Client) doIdentLookup(conn net.Conn) {
@@ -214,10 +257,10 @@ func (client *Client) isAuthorized(config *Config) bool {
214 257
 	return !config.Accounts.RequireSasl.Enabled || saslSent || utils.IPInNets(client.IP(), config.Accounts.RequireSasl.exemptedNets)
215 258
 }
216 259
 
217
-func (client *Client) resetFakelag() {
218
-	var flc FakelagConfig = client.server.Config().Fakelag
219
-	flc.Enabled = flc.Enabled && !client.HasRoleCapabs("nofakelag")
220
-	client.fakelag.Initialize(flc)
260
+func (session *Session) resetFakelag() {
261
+	var flc FakelagConfig = session.client.server.Config().Fakelag
262
+	flc.Enabled = flc.Enabled && !session.client.HasRoleCapabs("nofakelag")
263
+	session.fakelag.Initialize(flc)
221 264
 }
222 265
 
223 266
 // IP returns the IP address of this client.
@@ -244,28 +287,7 @@ func (client *Client) IPString() string {
244 287
 // command goroutine
245 288
 //
246 289
 
247
-func (client *Client) recomputeMaxlens() int {
248
-	maxlenRest := 512
249
-	if client.capabilities.Has(caps.MaxLine) {
250
-		maxlenRest = client.server.Limits().LineLen.Rest
251
-	}
252
-
253
-	atomic.StoreUint32(&client.maxlenRest, uint32(maxlenRest))
254
-
255
-	return maxlenRest
256
-}
257
-
258
-// allow these negotiated length limits to be read without locks; this is a convenience
259
-// so that Client.Send doesn't have to acquire any Client locks
260
-func (client *Client) MaxlenRest() int {
261
-	return int(atomic.LoadUint32(&client.maxlenRest))
262
-}
263
-
264
-func (client *Client) run() {
265
-	var err error
266
-	var isExiting bool
267
-	var line string
268
-	var msg ircmsg.IrcMessage
290
+func (client *Client) run(session *Session) {
269 291
 
270 292
 	defer func() {
271 293
 		if r := recover(); r != nil {
@@ -278,27 +300,30 @@ func (client *Client) run() {
278 300
 			}
279 301
 		}
280 302
 		// ensure client connection gets closed
281
-		client.destroy(false)
303
+		client.destroy(false, session)
282 304
 	}()
283 305
 
284
-	client.idletimer.Initialize(client)
306
+	session.idletimer.Initialize(session)
307
+	session.resetFakelag()
285 308
 
286
-	client.nickTimer.Initialize(client)
287
-
288
-	client.resetFakelag()
309
+	isReattach := client.Registered()
310
+	// don't reset the nick timer during a reattach
311
+	if !isReattach {
312
+		client.nickTimer.Initialize(client)
313
+	}
289 314
 
290 315
 	firstLine := true
291 316
 
292 317
 	for {
293
-		maxlenRest := client.recomputeMaxlens()
318
+		maxlenRest := session.MaxlenRest()
294 319
 
295
-		line, err = client.socket.Read()
320
+		line, err := session.socket.Read()
296 321
 		if err != nil {
297 322
 			quitMessage := "connection closed"
298 323
 			if err == errReadQ {
299 324
 				quitMessage = "readQ exceeded"
300 325
 			}
301
-			client.Quit(quitMessage)
326
+			client.Quit(quitMessage, session)
302 327
 			break
303 328
 		}
304 329
 
@@ -307,10 +332,10 @@ func (client *Client) run() {
307 332
 		}
308 333
 
309 334
 		// special-cased handling of PROXY protocol, see `handleProxyCommand` for details:
310
-		if firstLine {
335
+		if !isReattach && firstLine {
311 336
 			firstLine = false
312 337
 			if strings.HasPrefix(line, "PROXY") {
313
-				err = handleProxyCommand(client.server, client, line)
338
+				err = handleProxyCommand(client.server, client, session, line)
314 339
 				if err != nil {
315 340
 					break
316 341
 				} else {
@@ -319,14 +344,14 @@ func (client *Client) run() {
319 344
 			}
320 345
 		}
321 346
 
322
-		msg, err = ircmsg.ParseLineStrict(line, true, maxlenRest)
347
+		msg, err := ircmsg.ParseLineStrict(line, true, maxlenRest)
323 348
 		if err == ircmsg.ErrorLineIsEmpty {
324 349
 			continue
325 350
 		} else if err == ircmsg.ErrorLineTooLong {
326 351
 			client.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Input line too long"))
327 352
 			continue
328 353
 		} else if err != nil {
329
-			client.Quit(client.t("Received malformed line"))
354
+			client.Quit(client.t("Received malformed line"), session)
330 355
 			break
331 356
 		}
332 357
 
@@ -340,13 +365,24 @@ func (client *Client) run() {
340 365
 			continue
341 366
 		}
342 367
 
343
-		isExiting = cmd.Run(client.server, client, msg)
344
-		if isExiting || client.isQuitting {
368
+		isExiting := cmd.Run(client.server, client, session, msg)
369
+		if isExiting {
370
+			break
371
+		} else if session.client != client {
372
+			// bouncer reattach
373
+			session.playReattachMessages()
374
+			go session.client.run(session)
345 375
 			break
346 376
 		}
347 377
 	}
348 378
 }
349 379
 
380
+func (session *Session) playReattachMessages() {
381
+	for _, channel := range session.client.Channels() {
382
+		channel.playJoinForSession(session)
383
+	}
384
+}
385
+
350 386
 //
351 387
 // idle, quit, timers and timeouts
352 388
 //
@@ -359,9 +395,8 @@ func (client *Client) Active() {
359 395
 }
360 396
 
361 397
 // Ping sends the client a PING message.
362
-func (client *Client) Ping() {
363
-	client.Send(nil, "", "PING", client.nick)
364
-
398
+func (session *Session) Ping() {
399
+	session.Send(nil, "", "PING", session.client.Nick())
365 400
 }
366 401
 
367 402
 // tryResume tries to resume if the client asked us to.
@@ -400,6 +435,11 @@ func (client *Client) tryResume() (success bool) {
400 435
 		return
401 436
 	}
402 437
 
438
+	if 1 < len(oldClient.Sessions()) {
439
+		client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume a client with multiple attached sessions"))
440
+		return
441
+	}
442
+
403 443
 	err := server.clients.Resume(client, oldClient)
404 444
 	if err != nil {
405 445
 		client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection"))
@@ -467,17 +507,19 @@ func (client *Client) tryResume() (success bool) {
467 507
 
468 508
 	// send quit/resume messages to friends
469 509
 	for friend := range friends {
470
-		if friend.capabilities.Has(caps.Resume) {
471
-			if timestamp.IsZero() {
472
-				friend.Send(nil, oldNickmask, "RESUMED", username, hostname)
473
-			} else {
474
-				friend.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
475
-			}
476
-		} else {
477
-			if client.resumeDetails.HistoryIncomplete {
478
-				friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
510
+		for _, session := range friend.Sessions() {
511
+			if session.capabilities.Has(caps.Resume) {
512
+				if timestamp.IsZero() {
513
+					session.Send(nil, oldNickmask, "RESUMED", username, hostname)
514
+				} else {
515
+					session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString)
516
+				}
479 517
 			} else {
480
-				friend.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
518
+				if client.resumeDetails.HistoryIncomplete {
519
+					session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
520
+				} else {
521
+					session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
522
+				}
481 523
 			}
482 524
 		}
483 525
 	}
@@ -509,17 +551,17 @@ func (client *Client) tryResumeChannels() {
509 551
 	if !details.Timestamp.IsZero() {
510 552
 		now := time.Now()
511 553
 		items, complete := client.history.Between(details.Timestamp, now, false, 0)
512
-		rb := NewResponseBuffer(client)
554
+		rb := NewResponseBuffer(client.Sessions()[0])
513 555
 		client.replayPrivmsgHistory(rb, items, complete)
514 556
 		rb.Send(true)
515 557
 	}
516 558
 
517
-	details.OldClient.destroy(true)
559
+	details.OldClient.destroy(true, nil)
518 560
 }
519 561
 
520 562
 func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
521 563
 	nick := client.Nick()
522
-	serverTime := client.capabilities.Has(caps.ServerTime)
564
+	serverTime := rb.session.capabilities.Has(caps.ServerTime)
523 565
 	for _, item := range items {
524 566
 		var command string
525 567
 		switch item.Type {
@@ -661,37 +703,27 @@ func (client *Client) ModeString() (str string) {
661 703
 }
662 704
 
663 705
 // Friends refers to clients that share a channel with this client.
664
-func (client *Client) Friends(capabs ...caps.Capability) ClientSet {
665
-	friends := make(ClientSet)
706
+func (client *Client) Friends(capabs ...caps.Capability) (result map[*Session]bool) {
707
+	result = make(map[*Session]bool)
666 708
 
667
-	// make sure that I have the right caps
668
-	hasCaps := true
669
-	for _, capab := range capabs {
670
-		if !client.capabilities.Has(capab) {
671
-			hasCaps = false
672
-			break
709
+	// look at the client's own sessions
710
+	for _, session := range client.Sessions() {
711
+		if session.capabilities.HasAll(capabs...) {
712
+			result[session] = true
673 713
 		}
674 714
 	}
675
-	if hasCaps {
676
-		friends.Add(client)
677
-	}
678 715
 
679 716
 	for _, channel := range client.Channels() {
680 717
 		for _, member := range channel.Members() {
681
-			// make sure they have all the required caps
682
-			hasCaps = true
683
-			for _, capab := range capabs {
684
-				if !member.capabilities.Has(capab) {
685
-					hasCaps = false
686
-					break
718
+			for _, session := range member.Sessions() {
719
+				if session.capabilities.HasAll(capabs...) {
720
+					result[session] = true
687 721
 				}
688 722
 			}
689
-			if hasCaps {
690
-				friends.Add(member)
691
-			}
692 723
 		}
693 724
 	}
694
-	return friends
725
+
726
+	return
695 727
 }
696 728
 
697 729
 func (client *Client) SetOper(oper *Oper) {
@@ -816,47 +848,88 @@ func (client *Client) RplISupport(rb *ResponseBuffer) {
816 848
 // Quit sets the given quit message for the client.
817 849
 // (You must ensure separately that destroy() is called, e.g., by returning `true` from
818 850
 // the command handler or calling it yourself.)
819
-func (client *Client) Quit(message string) {
820
-	client.stateMutex.Lock()
821
-	alreadyQuit := client.isQuitting
822
-	if !alreadyQuit {
823
-		client.isQuitting = true
824
-		client.quitMessage = message
825
-	}
826
-	registered := client.registered
827
-	prefix := client.nickMaskString
828
-	client.stateMutex.Unlock()
851
+func (client *Client) Quit(message string, session *Session) {
852
+	setFinalData := func(sess *Session) {
853
+		message := sess.quitMessage
854
+		var finalData []byte
855
+		// #364: don't send QUIT lines to unregistered clients
856
+		if client.registered {
857
+			quitMsg := ircmsg.MakeMessage(nil, client.nickMaskString, "QUIT", message)
858
+			finalData, _ = quitMsg.LineBytesStrict(false, 512)
859
+		}
829 860
 
830
-	if alreadyQuit {
831
-		return
832
-	}
861
+		errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message)
862
+		errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512)
863
+		finalData = append(finalData, errorMsgBytes...)
833 864
 
834
-	var finalData []byte
835
-	// #364: don't send QUIT lines to unregistered clients
836
-	if registered {
837
-		quitMsg := ircmsg.MakeMessage(nil, prefix, "QUIT", message)
838
-		finalData, _ = quitMsg.LineBytesStrict(false, 512)
865
+		sess.socket.SetFinalData(finalData)
839 866
 	}
840 867
 
841
-	errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message)
842
-	errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512)
843
-	finalData = append(finalData, errorMsgBytes...)
868
+	client.stateMutex.Lock()
869
+	defer client.stateMutex.Unlock()
870
+
871
+	var sessions []*Session
872
+	if session != nil {
873
+		sessions = []*Session{session}
874
+	} else {
875
+		sessions = client.sessions
876
+	}
844 877
 
845
-	client.socket.SetFinalData(finalData)
878
+	for _, session := range sessions {
879
+		if session.SetQuitMessage(message) {
880
+			setFinalData(session)
881
+		}
882
+	}
846 883
 }
847 884
 
848 885
 // destroy gets rid of a client, removes them from server lists etc.
849
-func (client *Client) destroy(beingResumed bool) {
886
+// if `session` is nil, destroys the client unconditionally, removing all sessions;
887
+// otherwise, destroys one specific session, only destroying the client if it
888
+// has no more sessions.
889
+func (client *Client) destroy(beingResumed bool, session *Session) {
890
+	var sessionsToDestroy []*Session
891
+
850 892
 	// allow destroy() to execute at most once
851 893
 	client.stateMutex.Lock()
852
-	isDestroyed := client.isDestroyed
853
-	client.isDestroyed = true
854
-	quitMessage := client.quitMessage
855 894
 	nickMaskString := client.nickMaskString
856 895
 	accountName := client.accountName
896
+
897
+	alreadyDestroyed := len(client.sessions) == 0
898
+	sessionRemoved := false
899
+	var remainingSessions int
900
+	if session == nil {
901
+		sessionRemoved = !alreadyDestroyed
902
+		sessionsToDestroy = client.sessions
903
+		client.sessions = nil
904
+		remainingSessions = 0
905
+	} else {
906
+		sessionRemoved, remainingSessions = client.removeSession(session)
907
+		if sessionRemoved {
908
+			sessionsToDestroy = []*Session{session}
909
+		}
910
+	}
911
+	var quitMessage string
912
+	if 0 < len(sessionsToDestroy) {
913
+		quitMessage = sessionsToDestroy[0].quitMessage
914
+	}
857 915
 	client.stateMutex.Unlock()
858 916
 
859
-	if isDestroyed {
917
+	if alreadyDestroyed || !sessionRemoved {
918
+		return
919
+	}
920
+
921
+	for _, session := range sessionsToDestroy {
922
+		if session.client != client {
923
+			// session has been attached to a new client; do not destroy it
924
+			continue
925
+		}
926
+		session.idletimer.Stop()
927
+		session.socket.Close()
928
+		// send quit/error message to client if they haven't been sent already
929
+		client.Quit("", session)
930
+	}
931
+
932
+	if remainingSessions != 0 {
860 933
 		return
861 934
 	}
862 935
 
@@ -871,9 +944,6 @@ func (client *Client) destroy(beingResumed bool) {
871 944
 		client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick))
872 945
 	}
873 946
 
874
-	// send quit/error message to client if they haven't been sent already
875
-	client.Quit("Connection closed")
876
-
877 947
 	if !beingResumed {
878 948
 		client.server.whoWas.Append(client.WhoWas())
879 949
 	}
@@ -916,13 +986,10 @@ func (client *Client) destroy(beingResumed bool) {
916 986
 	}
917 987
 
918 988
 	// clean up self
919
-	client.idletimer.Stop()
920 989
 	client.nickTimer.Stop()
921 990
 
922 991
 	client.server.accounts.Logout(client)
923 992
 
924
-	client.socket.Close()
925
-
926 993
 	// send quit messages to friends
927 994
 	if !beingResumed {
928 995
 		if client.Registered() {
@@ -953,16 +1020,12 @@ func (client *Client) destroy(beingResumed bool) {
953 1020
 
954 1021
 // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
955 1022
 // Adds account-tag to the line as well.
956
-func (client *Client) SendSplitMsgFromClient(from *Client, tags map[string]string, command, target string, message utils.SplitMessage) {
957
-	client.sendSplitMsgFromClientInternal(false, time.Time{}, from.NickMaskString(), from.AccountName(), tags, command, target, message)
958
-}
959
-
960
-func (client *Client) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
961
-	if client.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
962
-		client.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
1023
+func (session *Session) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) {
1024
+	if session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
1025
+		session.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.Message)
963 1026
 	} else {
964 1027
 		for _, messagePair := range message.Wrapped {
965
-			client.sendFromClientInternal(blocking, serverTime, messagePair.Msgid, nickmask, accountName, tags, command, target, messagePair.Message)
1028
+			session.sendFromClientInternal(blocking, serverTime, messagePair.Msgid, nickmask, accountName, tags, command, target, messagePair.Message)
966 1029
 		}
967 1030
 	}
968 1031
 }
@@ -976,22 +1039,32 @@ func (client *Client) SendFromClient(msgid string, from *Client, tags map[string
976 1039
 // this is SendFromClient, but directly exposing nickmask and accountName,
977 1040
 // for things like history replay and CHGHOST where they no longer (necessarily)
978 1041
 // correspond to the current state of a client
979
-func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) error {
1042
+func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
1043
+	for _, session := range client.Sessions() {
1044
+		err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...)
1045
+		if err_ != nil {
1046
+			err = err_
1047
+		}
1048
+	}
1049
+	return
1050
+}
1051
+
1052
+func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) {
980 1053
 	msg := ircmsg.MakeMessage(tags, nickmask, command, params...)
981 1054
 	// attach account-tag
982
-	if client.capabilities.Has(caps.AccountTag) && accountName != "*" {
1055
+	if session.capabilities.Has(caps.AccountTag) && accountName != "*" {
983 1056
 		msg.SetTag("account", accountName)
984 1057
 	}
985 1058
 	// attach message-id
986
-	if msgid != "" && client.capabilities.Has(caps.MessageTags) {
1059
+	if msgid != "" && session.capabilities.Has(caps.MessageTags) {
987 1060
 		msg.SetTag("draft/msgid", msgid)
988 1061
 	}
989 1062
 	// attach server-time
990
-	if client.capabilities.Has(caps.ServerTime) {
1063
+	if session.capabilities.Has(caps.ServerTime) {
991 1064
 		msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
992 1065
 	}
993 1066
 
994
-	return client.SendRawMessage(msg, blocking)
1067
+	return session.SendRawMessage(msg, blocking)
995 1068
 }
996 1069
 
997 1070
 var (
@@ -1008,7 +1081,7 @@ var (
1008 1081
 )
1009 1082
 
1010 1083
 // SendRawMessage sends a raw message to the client.
1011
-func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error {
1084
+func (session *Session) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error {
1012 1085
 	// use dumb hack to force the last param to be a trailing param if required
1013 1086
 	var usedTrailingHack bool
1014 1087
 	if commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 {
@@ -1021,19 +1094,19 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e
1021 1094
 	}
1022 1095
 
1023 1096
 	// assemble message
1024
-	maxlenRest := client.MaxlenRest()
1097
+	maxlenRest := session.MaxlenRest()
1025 1098
 	line, err := message.LineBytesStrict(false, maxlenRest)
1026 1099
 	if err != nil {
1027 1100
 		logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack())
1028
-		client.server.logger.Error("internal", logline)
1101
+		session.client.server.logger.Error("internal", logline)
1029 1102
 
1030
-		message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
1103
+		message = ircmsg.MakeMessage(nil, session.client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending")
1031 1104
 		line, _ := message.LineBytesStrict(false, 0)
1032 1105
 
1033 1106
 		if blocking {
1034
-			client.socket.BlockingWrite(line)
1107
+			session.socket.BlockingWrite(line)
1035 1108
 		} else {
1036
-			client.socket.Write(line)
1109
+			session.socket.Write(line)
1037 1110
 		}
1038 1111
 		return err
1039 1112
 	}
@@ -1044,43 +1117,40 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e
1044 1117
 		line = line[:len(line)-1]
1045 1118
 	}
1046 1119
 
1047
-	if client.server.logger.IsLoggingRawIO() {
1120
+	if session.client.server.logger.IsLoggingRawIO() {
1048 1121
 		logline := string(line[:len(line)-2]) // strip "\r\n"
1049
-		client.server.logger.Debug("useroutput", client.nick, " ->", logline)
1122
+		session.client.server.logger.Debug("useroutput", session.client.Nick(), " ->", logline)
1050 1123
 	}
1051 1124
 
1052 1125
 	if blocking {
1053
-		return client.socket.BlockingWrite(line)
1126
+		return session.socket.BlockingWrite(line)
1054 1127
 	} else {
1055
-		return client.socket.Write(line)
1128
+		return session.socket.Write(line)
1056 1129
 	}
1057 1130
 }
1058 1131
 
1059 1132
 // Send sends an IRC line to the client.
1060
-func (client *Client) Send(tags map[string]string, prefix string, command string, params ...string) error {
1133
+func (client *Client) Send(tags map[string]string, prefix string, command string, params ...string) (err error) {
1134
+	for _, session := range client.Sessions() {
1135
+		err_ := session.Send(tags, prefix, command, params...)
1136
+		if err_ != nil {
1137
+			err = err_
1138
+		}
1139
+	}
1140
+	return
1141
+}
1142
+
1143
+func (session *Session) Send(tags map[string]string, prefix string, command string, params ...string) (err error) {
1061 1144
 	msg := ircmsg.MakeMessage(tags, prefix, command, params...)
1062
-	if client.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") {
1145
+	if session.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") {
1063 1146
 		msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
1064 1147
 	}
1065
-	return client.SendRawMessage(msg, false)
1148
+	return session.SendRawMessage(msg, false)
1066 1149
 }
1067 1150
 
1068 1151
 // Notice sends the client a notice from the server.
1069 1152
 func (client *Client) Notice(text string) {
1070
-	limit := 400
1071
-	if client.capabilities.Has(caps.MaxLine) {
1072
-		limit = client.server.Limits().LineLen.Rest - 110
1073
-	}
1074
-	lines := utils.WordWrap(text, limit)
1075
-
1076
-	// force blank lines to be sent if we receive them
1077
-	if len(lines) == 0 {
1078
-		lines = []string{""}
1079
-	}
1080
-
1081
-	for _, line := range lines {
1082
-		client.Send(nil, client.server.name, "NOTICE", client.nick, line)
1083
-	}
1153
+	client.Send(nil, client.server.name, "NOTICE", client.Nick(), text)
1084 1154
 }
1085 1155
 
1086 1156
 func (client *Client) addChannel(channel *Channel) {

+ 23
- 17
irc/client_lookup_set.go Vedi File

@@ -11,7 +11,9 @@ import (
11 11
 	"strings"
12 12
 
13 13
 	"github.com/goshuirc/irc-go/ircmatch"
14
+
14 15
 	"github.com/oragono/oragono/irc/caps"
16
+	"github.com/oragono/oragono/irc/modes"
15 17
 
16 18
 	"sync"
17 19
 )
@@ -131,7 +133,7 @@ func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) {
131 133
 }
132 134
 
133 135
 // SetNick sets a client's nickname, validating it against nicknames in use
134
-func (clients *ClientManager) SetNick(client *Client, newNick string) error {
136
+func (clients *ClientManager) SetNick(client *Client, session *Session, newNick string) error {
135 137
 	newcfnick, err := CasefoldName(newNick)
136 138
 	if err != nil {
137 139
 		return err
@@ -142,21 +144,31 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error {
142 144
 	}
143 145
 
144 146
 	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
147
+	account := client.Account()
148
+	bouncerAllowed := client.server.accounts.BouncerAllowed(account, session)
145 149
 
146 150
 	clients.Lock()
147 151
 	defer clients.Unlock()
148 152
 
149
-	currentNewEntry := clients.byNick[newcfnick]
153
+	currentClient := clients.byNick[newcfnick]
150 154
 	// the client may just be changing case
151
-	if currentNewEntry != nil && currentNewEntry != client {
152
-		return errNicknameInUse
155
+	if currentClient != nil && currentClient != client {
156
+		// these conditions forbid reattaching to an existing session:
157
+		if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.isTor != currentClient.isTor || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) {
158
+			return errNicknameInUse
159
+		}
160
+		if !currentClient.AddSession(session) {
161
+			return errNicknameInUse
162
+		}
163
+		// successful reattach:
164
+		return nil
153 165
 	}
154 166
 	// analogous checks for skeletons
155 167
 	skeletonHolder := clients.bySkeleton[newSkeleton]
156 168
 	if skeletonHolder != nil && skeletonHolder != client {
157 169
 		return errNicknameInUse
158 170
 	}
159
-	if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() {
171
+	if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account {
160 172
 		return errNicknameReserved
161 173
 	}
162 174
 	clients.removeInternal(client)
@@ -179,24 +191,18 @@ func (clients *ClientManager) AllClients() (result []*Client) {
179 191
 }
180 192
 
181 193
 // AllWithCaps returns all clients with the given capabilities.
182
-func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (set ClientSet) {
183
-	set = make(ClientSet)
184
-
194
+func (clients *ClientManager) AllWithCaps(capabs ...caps.Capability) (sessions []*Session) {
185 195
 	clients.RLock()
186 196
 	defer clients.RUnlock()
187
-	var client *Client
188
-	for _, client = range clients.byNick {
189
-		// make sure they have all the required caps
190
-		for _, capab := range capabs {
191
-			if !client.capabilities.Has(capab) {
192
-				continue
197
+	for _, client := range clients.byNick {
198
+		for _, session := range client.Sessions() {
199
+			if session.capabilities.HasAll(capabs...) {
200
+				sessions = append(sessions, session)
193 201
 			}
194 202
 		}
195
-
196
-		set.Add(client)
197 203
 	}
198 204
 
199
-	return set
205
+	return
200 206
 }
201 207
 
202 208
 // FindAll returns all clients that match the given userhost mask.

+ 5
- 5
irc/commands.go Vedi File

@@ -21,7 +21,7 @@ type Command struct {
21 21
 }
22 22
 
23 23
 // Run runs this command with the given client/message.
24
-func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
24
+func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ircmsg.IrcMessage) bool {
25 25
 	if !client.registered && !cmd.usablePreReg {
26 26
 		client.Send(nil, server.name, ERR_NOTREGISTERED, "*", client.t("You need to register before you can use that command"))
27 27
 		return false
@@ -40,22 +40,22 @@ func (cmd *Command) Run(server *Server, client *Client, msg ircmsg.IrcMessage) b
40 40
 	}
41 41
 
42 42
 	if client.registered {
43
-		client.fakelag.Touch()
43
+		session.fakelag.Touch()
44 44
 	}
45 45
 
46
-	rb := NewResponseBuffer(client)
46
+	rb := NewResponseBuffer(session)
47 47
 	rb.Label = GetLabel(msg)
48 48
 	exiting := cmd.handler(server, client, msg, rb)
49 49
 	rb.Send(true)
50 50
 
51 51
 	// after each command, see if we can send registration to the client
52 52
 	if !client.registered {
53
-		server.tryRegister(client)
53
+		server.tryRegister(client, session)
54 54
 	}
55 55
 
56 56
 	// most servers do this only for PING/PONG, but we'll do it for any command:
57 57
 	if client.registered {
58
-		client.idletimer.Touch()
58
+		session.idletimer.Touch()
59 59
 	}
60 60
 
61 61
 	if !cmd.leaveClientIdle {

+ 5
- 1
irc/config.go Vedi File

@@ -66,7 +66,11 @@ type AccountConfig struct {
66 66
 	} `yaml:"login-throttling"`
67 67
 	SkipServerPassword bool                  `yaml:"skip-server-password"`
68 68
 	NickReservation    NickReservationConfig `yaml:"nick-reservation"`
69
-	VHosts             VHostConfig
69
+	Bouncer            struct {
70
+		Enabled          bool
71
+		AllowedByDefault bool `yaml:"allowed-by-default"`
72
+	}
73
+	VHosts VHostConfig
70 74
 }
71 75
 
72 76
 // AccountRegistrationConfig controls account registration.

+ 6
- 6
irc/gateways.go Vedi File

@@ -46,7 +46,7 @@ func (wc *webircConfig) Populate() (err error) {
46 46
 }
47 47
 
48 48
 // ApplyProxiedIP applies the given IP to the client.
49
-func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool) {
49
+func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls bool) (success bool) {
50 50
 	// PROXY and WEBIRC are never accepted from a Tor listener, even if the address itself
51 51
 	// is whitelisted:
52 52
 	if client.isTor {
@@ -56,13 +56,13 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool)
56 56
 	// ensure IP is sane
57 57
 	parsedProxiedIP := net.ParseIP(proxiedIP).To16()
58 58
 	if parsedProxiedIP == nil {
59
-		client.Quit(fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP))
59
+		client.Quit(fmt.Sprintf(client.t("Proxied IP address is not valid: [%s]"), proxiedIP), session)
60 60
 		return false
61 61
 	}
62 62
 
63 63
 	isBanned, banMsg := client.server.checkBans(parsedProxiedIP)
64 64
 	if isBanned {
65
-		client.Quit(banMsg)
65
+		client.Quit(banMsg, session)
66 66
 		return false
67 67
 	}
68 68
 
@@ -88,10 +88,10 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (success bool)
88 88
 // PROXY TCP[46] SOURCEIP DESTIP SOURCEPORT DESTPORT\r\n
89 89
 // unfortunately, an ipv6 SOURCEIP can start with a double colon; in this case,
90 90
 // the message is invalid IRC and can't be parsed normally, hence the special handling.
91
-func handleProxyCommand(server *Server, client *Client, line string) (err error) {
91
+func handleProxyCommand(server *Server, client *Client, session *Session, line string) (err error) {
92 92
 	defer func() {
93 93
 		if err != nil {
94
-			client.Quit(client.t("Bad or unauthorized PROXY command"))
94
+			client.Quit(client.t("Bad or unauthorized PROXY command"), session)
95 95
 		}
96 96
 	}()
97 97
 
@@ -102,7 +102,7 @@ func handleProxyCommand(server *Server, client *Client, line string) (err error)
102 102
 
103 103
 	if utils.IPInNets(client.realIP, server.Config().Server.proxyAllowedFromNets) {
104 104
 		// assume PROXY connections are always secure
105
-		if client.ApplyProxiedIP(params[2], true) {
105
+		if client.ApplyProxiedIP(session, params[2], true) {
106 106
 			return nil
107 107
 		} else {
108 108
 			return errBadProxyLine

+ 37
- 6
irc/getters.go Vedi File

@@ -62,6 +62,43 @@ func (server *Server) Languages() (lm *languages.Manager) {
62 62
 	return server.Config().languageManager
63 63
 }
64 64
 
65
+func (client *Client) Sessions() (sessions []*Session) {
66
+	client.stateMutex.RLock()
67
+	sessions = make([]*Session, len(client.sessions))
68
+	copy(sessions, client.sessions)
69
+	client.stateMutex.RUnlock()
70
+	return
71
+}
72
+
73
+func (client *Client) AddSession(session *Session) (success bool) {
74
+	client.stateMutex.Lock()
75
+	defer client.stateMutex.Unlock()
76
+
77
+	if len(client.sessions) == 0 {
78
+		return false
79
+	}
80
+	session.client = client
81
+	client.sessions = append(client.sessions, session)
82
+	return true
83
+}
84
+
85
+func (client *Client) removeSession(session *Session) (success bool, length int) {
86
+	if len(client.sessions) == 0 {
87
+		return
88
+	}
89
+	sessions := make([]*Session, 0, len(client.sessions)-1)
90
+	for _, currentSession := range client.sessions {
91
+		if session == currentSession {
92
+			success = true
93
+		} else {
94
+			sessions = append(sessions, currentSession)
95
+		}
96
+	}
97
+	client.sessions = sessions
98
+	length = len(sessions)
99
+	return
100
+}
101
+
65 102
 func (client *Client) Nick() string {
66 103
 	client.stateMutex.RLock()
67 104
 	defer client.stateMutex.RUnlock()
@@ -167,12 +204,6 @@ func (client *Client) SetAwayMessage(message string) {
167 204
 	client.stateMutex.Unlock()
168 205
 }
169 206
 
170
-func (client *Client) Destroyed() bool {
171
-	client.stateMutex.RLock()
172
-	defer client.stateMutex.RUnlock()
173
-	return client.isDestroyed
174
-}
175
-
176 207
 func (client *Client) Account() string {
177 208
 	client.stateMutex.RLock()
178 209
 	defer client.stateMutex.RUnlock()

+ 109
- 76
irc/handlers.go Vedi File

@@ -482,14 +482,20 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
482 482
 		Mode: modes.Away,
483 483
 		Op:   op,
484 484
 	}}
485
-	rb.Add(nil, server.name, "MODE", client.nick, modech.String())
485
+
486
+	details := client.Details()
487
+	modeString := modech.String()
488
+	rb.Add(nil, server.name, "MODE", details.nick, modeString)
486 489
 
487 490
 	// dispatch away-notify
488
-	for friend := range client.Friends(caps.AwayNotify) {
491
+	for session := range client.Friends(caps.AwayNotify) {
492
+		if session != rb.session && rb.session.client == client {
493
+			session.Send(nil, server.name, "MODE", details.nick, modeString)
494
+		}
489 495
 		if isAway {
490
-			friend.SendFromClient("", client, nil, "AWAY", awayMessage)
496
+			session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY", awayMessage)
491 497
 		} else {
492
-			friend.SendFromClient("", client, nil, "AWAY")
498
+			session.sendFromClientInternal(false, time.Time{}, "", details.nickMask, details.account, nil, "AWAY")
493 499
 		}
494 500
 	}
495 501
 
@@ -527,23 +533,23 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
527 533
 	switch subCommand {
528 534
 	case "LS":
529 535
 		if !client.registered {
530
-			client.capState = caps.NegotiatingState
536
+			rb.session.capState = caps.NegotiatingState
531 537
 		}
532 538
 		if len(msg.Params) > 1 && msg.Params[1] == "302" {
533
-			client.capVersion = 302
539
+			rb.session.capVersion = 302
534 540
 		}
535 541
 		// weechat 1.4 has a bug here where it won't accept the CAP reply unless it contains
536 542
 		// the server.name source... otherwise it doesn't respond to the CAP message with
537 543
 		// anything and just hangs on connection.
538 544
 		//TODO(dan): limit number of caps and send it multiline in 3.2 style as appropriate.
539
-		rb.Add(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(client.capVersion, CapValues))
545
+		rb.Add(nil, server.name, "CAP", client.nick, subCommand, SupportedCapabilities.String(rb.session.capVersion, CapValues))
540 546
 
541 547
 	case "LIST":
542
-		rb.Add(nil, server.name, "CAP", client.nick, subCommand, client.capabilities.String(caps.Cap301, CapValues)) // values not sent on LIST so force 3.1
548
+		rb.Add(nil, server.name, "CAP", client.nick, subCommand, rb.session.capabilities.String(caps.Cap301, CapValues)) // values not sent on LIST so force 3.1
543 549
 
544 550
 	case "REQ":
545 551
 		if !client.registered {
546
-			client.capState = caps.NegotiatingState
552
+			rb.session.capState = caps.NegotiatingState
547 553
 		}
548 554
 
549 555
 		// make sure all capabilities actually exist
@@ -551,8 +557,8 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
551 557
 			rb.Add(nil, server.name, "CAP", client.nick, "NAK", capString)
552 558
 			return false
553 559
 		}
554
-		client.capabilities.Union(toAdd)
555
-		client.capabilities.Subtract(toRemove)
560
+		rb.session.capabilities.Union(toAdd)
561
+		rb.session.capabilities.Subtract(toRemove)
556 562
 		rb.Add(nil, server.name, "CAP", client.nick, "ACK", capString)
557 563
 
558 564
 		// if this is the first time the client is requesting a resume token,
@@ -564,9 +570,12 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
564 570
 			}
565 571
 		}
566 572
 
573
+		// update maxlenrest, just in case they altered the maxline cap
574
+		rb.session.SetMaxlenRest()
575
+
567 576
 	case "END":
568 577
 		if !client.registered {
569
-			client.capState = caps.NegotiatedState
578
+			rb.session.capState = caps.NegotiatedState
570 579
 		}
571 580
 
572 581
 	default:
@@ -600,7 +609,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
600 609
 		if success && len(items) > 0 {
601 610
 			return
602 611
 		}
603
-		newRb := NewResponseBuffer(client)
612
+		newRb := NewResponseBuffer(rb.session)
604 613
 		newRb.Label = rb.Label // same label, new batch
605 614
 		// TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate
606 615
 		if hist == nil {
@@ -1006,12 +1015,12 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
1006 1015
 
1007 1016
 		for _, mcl := range clientsToKill {
1008 1017
 			mcl.exitedSnomaskSent = true
1009
-			mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason))
1018
+			mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
1010 1019
 			if mcl == client {
1011 1020
 				killClient = true
1012 1021
 			} else {
1013 1022
 				// if mcl == client, we kill them below
1014
-				mcl.destroy(false)
1023
+				mcl.destroy(false, nil)
1015 1024
 			}
1016 1025
 		}
1017 1026
 
@@ -1240,7 +1249,6 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
1240 1249
 				return false
1241 1250
 			}
1242 1251
 			channelString = msg.Params[1]
1243
-			rb = NewResponseBuffer(target)
1244 1252
 		}
1245 1253
 	}
1246 1254
 
@@ -1248,9 +1256,6 @@ func sajoinHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
1248 1256
 	for _, chname := range channels {
1249 1257
 		server.channels.Join(target, chname, "", true, rb)
1250 1258
 	}
1251
-	if client != target {
1252
-		rb.Send(false)
1253
-	}
1254 1259
 	return false
1255 1260
 }
1256 1261
 
@@ -1321,8 +1326,8 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1321 1326
 	server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s$r was killed by %s $c[grey][$r%s$c[grey]]"), target.nick, client.nick, comment))
1322 1327
 	target.exitedSnomaskSent = true
1323 1328
 
1324
-	target.Quit(quitMsg)
1325
-	target.destroy(false)
1329
+	target.Quit(quitMsg, nil)
1330
+	target.destroy(false, nil)
1326 1331
 	return false
1327 1332
 }
1328 1333
 
@@ -1447,12 +1452,12 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
1447 1452
 
1448 1453
 		for _, mcl := range clientsToKill {
1449 1454
 			mcl.exitedSnomaskSent = true
1450
-			mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason))
1455
+			mcl.Quit(fmt.Sprintf(mcl.t("You have been banned from this server (%s)"), reason), nil)
1451 1456
 			if mcl == client {
1452 1457
 				killClient = true
1453 1458
 			} else {
1454 1459
 				// if mcl == client, we kill them below
1455
-				mcl.destroy(false)
1460
+				mcl.destroy(false, nil)
1456 1461
 			}
1457 1462
 		}
1458 1463
 
@@ -1660,19 +1665,25 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
1660 1665
 	}
1661 1666
 
1662 1667
 	// send out changes
1668
+	prefix := client.NickMaskString()
1663 1669
 	if len(applied) > 0 {
1664 1670
 		//TODO(dan): we should change the name of String and make it return a slice here
1665 1671
 		args := append([]string{channel.name}, strings.Split(applied.String(), " ")...)
1666 1672
 		for _, member := range channel.Members() {
1667 1673
 			if member == client {
1668
-				rb.Add(nil, client.nickMaskString, "MODE", args...)
1674
+				rb.Add(nil, prefix, "MODE", args...)
1675
+				for _, session := range client.Sessions() {
1676
+					if session != rb.session {
1677
+						session.Send(nil, prefix, "MODE", args...)
1678
+					}
1679
+				}
1669 1680
 			} else {
1670
-				member.Send(nil, client.nickMaskString, "MODE", args...)
1681
+				member.Send(nil, prefix, "MODE", args...)
1671 1682
 			}
1672 1683
 		}
1673 1684
 	} else {
1674 1685
 		args := append([]string{client.nick, channel.name}, channel.modeStrings(client)...)
1675
-		rb.Add(nil, client.nickMaskString, RPL_CHANNELMODEIS, args...)
1686
+		rb.Add(nil, prefix, RPL_CHANNELMODEIS, args...)
1676 1687
 		rb.Add(nil, client.nickMaskString, RPL_CHANNELCREATED, client.nick, channel.name, strconv.FormatInt(channel.createdTime.Unix(), 10))
1677 1688
 	}
1678 1689
 	return false
@@ -1913,7 +1924,7 @@ func namesHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
1913 1924
 // NICK <nickname>
1914 1925
 func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1915 1926
 	if client.registered {
1916
-		performNickChange(server, client, client, msg.Params[0], rb)
1927
+		performNickChange(server, client, client, nil, msg.Params[0], rb)
1917 1928
 	} else {
1918 1929
 		client.preregNick = msg.Params[0]
1919 1930
 	}
@@ -1953,7 +1964,7 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1953 1964
 
1954 1965
 	for i, targetString := range targets {
1955 1966
 		// each target gets distinct msgids
1956
-		splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine))
1967
+		splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine))
1957 1968
 		now := time.Now().UTC()
1958 1969
 
1959 1970
 		// max of four targets per privmsg
@@ -1992,10 +2003,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1992 2003
 			}
1993 2004
 			tnick := user.Nick()
1994 2005
 
1995
-			if histType == history.Tagmsg && !user.capabilities.Has(caps.MessageTags) {
1996
-				continue // nothing to do
1997
-			}
1998
-
1999 2006
 			nickMaskString := client.NickMaskString()
2000 2007
 			accountName := client.AccountName()
2001 2008
 			// restrict messages appropriately when +R is set
@@ -2003,19 +2010,36 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
2003 2010
 			allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount()
2004 2011
 			allowedTor := !user.isTor || !isRestrictedCTCPMessage(message)
2005 2012
 			if allowedPlusR && allowedTor {
2006
-				if histType == history.Tagmsg {
2007
-					user.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
2008
-				} else {
2009
-					user.SendSplitMsgFromClient(client, clientOnlyTags, msg.Command, tnick, splitMsg)
2013
+				for _, session := range user.Sessions() {
2014
+					if histType == history.Tagmsg {
2015
+						// don't send TAGMSG at all if they don't have the tags cap
2016
+						if session.capabilities.Has(caps.MessageTags) {
2017
+							session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
2018
+						}
2019
+					} else {
2020
+						session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
2021
+					}
2010 2022
 				}
2011 2023
 			}
2012
-			if client.capabilities.Has(caps.EchoMessage) {
2013
-				if histType == history.Tagmsg && client.capabilities.Has(caps.MessageTags) {
2024
+			// an echo-message may need to be included in the response:
2025
+			if rb.session.capabilities.Has(caps.EchoMessage) {
2026
+				if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
2014 2027
 					rb.AddFromClient(splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
2015 2028
 				} else {
2016 2029
 					rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
2017 2030
 				}
2018 2031
 			}
2032
+			// an echo-message may need to go out to other client sessions:
2033
+			for _, session := range client.Sessions() {
2034
+				if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() {
2035
+					continue
2036
+				}
2037
+				if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
2038
+					session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
2039
+				} else {
2040
+					session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
2041
+				}
2042
+			}
2019 2043
 			if histType != history.Notice && user.HasMode(modes.Away) {
2020 2044
 				//TODO(dan): possibly implement cooldown of away notifications to users
2021 2045
 				rb.Add(nil, server.name, RPL_AWAY, cnick, tnick, user.AwayMessage())
@@ -2084,7 +2108,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2084 2108
 	}
2085 2109
 	if !authorized {
2086 2110
 		rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.Nick(), client.t("Password incorrect"))
2087
-		client.Quit(client.t("Password incorrect"))
2111
+		client.Quit(client.t("Password incorrect"), rb.session)
2088 2112
 		return true
2089 2113
 	}
2090 2114
 
@@ -2109,7 +2133,9 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2109 2133
 	server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name))
2110 2134
 
2111 2135
 	// client may now be unthrottled by the fakelag system
2112
-	client.resetFakelag()
2136
+	for _, session := range client.Sessions() {
2137
+		session.resetFakelag()
2138
+	}
2113 2139
 
2114 2140
 	return false
2115 2141
 }
@@ -2148,7 +2174,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2148 2174
 	password := []byte(msg.Params[0])
2149 2175
 	if bcrypt.CompareHashAndPassword(serverPassword, password) != nil {
2150 2176
 		rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
2151
-		client.Quit(client.t("Password incorrect"))
2177
+		client.Quit(client.t("Password incorrect"), rb.session)
2152 2178
 		return true
2153 2179
 	}
2154 2180
 
@@ -2180,7 +2206,7 @@ func quitHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
2180 2206
 	if len(msg.Params) > 0 {
2181 2207
 		reason += ": " + msg.Params[0]
2182 2208
 	}
2183
-	client.Quit(reason)
2209
+	client.Quit(reason, rb.session)
2184 2210
 	return true
2185 2211
 }
2186 2212
 
@@ -2242,34 +2268,36 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2242 2268
 	// send RENAME messages
2243 2269
 	clientPrefix := client.NickMaskString()
2244 2270
 	for _, mcl := range channel.Members() {
2245
-		targetRb := rb
2246
-		targetPrefix := clientPrefix
2247
-		if mcl != client {
2248
-			targetRb = NewResponseBuffer(mcl)
2249
-			targetPrefix = mcl.NickMaskString()
2250
-		}
2251
-		if mcl.capabilities.Has(caps.Rename) {
2252
-			if reason != "" {
2253
-				targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason)
2254
-			} else {
2255
-				targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName)
2271
+		for _, mSession := range mcl.Sessions() {
2272
+			targetRb := rb
2273
+			targetPrefix := clientPrefix
2274
+			if mSession != rb.session {
2275
+				targetRb = NewResponseBuffer(mSession)
2276
+				targetPrefix = mcl.NickMaskString()
2256 2277
 			}
2257
-		} else {
2258
-			if reason != "" {
2259
-				targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
2278
+			if mSession.capabilities.Has(caps.Rename) {
2279
+				if reason != "" {
2280
+					targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName, reason)
2281
+				} else {
2282
+					targetRb.Add(nil, clientPrefix, "RENAME", oldName, newName)
2283
+				}
2260 2284
 			} else {
2261
-				targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
2285
+				if reason != "" {
2286
+					targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed: %s"), reason))
2287
+				} else {
2288
+					targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
2289
+				}
2290
+				if mSession.capabilities.Has(caps.ExtendedJoin) {
2291
+					targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname())
2292
+				} else {
2293
+					targetRb.Add(nil, targetPrefix, "JOIN", newName)
2294
+				}
2295
+				channel.SendTopic(mcl, targetRb, false)
2296
+				channel.Names(mcl, targetRb)
2262 2297
 			}
2263
-			if mcl.capabilities.Has(caps.ExtendedJoin) {
2264
-				targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname())
2265
-			} else {
2266
-				targetRb.Add(nil, targetPrefix, "JOIN", newName)
2298
+			if mcl != client {
2299
+				targetRb.Send(false)
2267 2300
 			}
2268
-			channel.SendTopic(mcl, targetRb, false)
2269
-			channel.Names(mcl, targetRb)
2270
-		}
2271
-		if mcl != client {
2272
-			targetRb.Send(false)
2273 2301
 		}
2274 2302
 	}
2275 2303
 
@@ -2311,7 +2339,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2311 2339
 		rb.Add(nil, server.name, ERR_NOSUCHNICK, client.nick, msg.Params[0], client.t("No such nick"))
2312 2340
 		return false
2313 2341
 	}
2314
-	performNickChange(server, client, target, msg.Params[1], rb)
2342
+	performNickChange(server, client, target, nil, msg.Params[1], rb)
2315 2343
 	return false
2316 2344
 }
2317 2345
 
@@ -2334,9 +2362,12 @@ func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
2334 2362
 	client.realname = realname
2335 2363
 	client.stateMutex.Unlock()
2336 2364
 
2365
+	details := client.Details()
2366
+
2337 2367
 	// alert friends
2338
-	for friend := range client.Friends(caps.SetName) {
2339
-		friend.SendFromClient("", client, nil, "SETNAME", realname)
2368
+	now := time.Now().UTC()
2369
+	for session := range client.Friends(caps.SetName) {
2370
+		session.sendFromClientInternal(false, now, "", details.nickMask, details.account, nil, "SETNAME", details.realname)
2340 2371
 	}
2341 2372
 
2342 2373
 	return false
@@ -2519,7 +2550,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2519 2550
 			lkey := strings.ToLower(key)
2520 2551
 			if lkey == "tls" || lkey == "secure" {
2521 2552
 				// only accept "tls" flag if the gateway's connection to us is secure as well
2522
-				if client.HasMode(modes.TLS) || utils.AddrIsLocal(client.socket.conn.RemoteAddr()) {
2553
+				if client.HasMode(modes.TLS) || client.realIP.IsLoopback() {
2523 2554
 					secure = true
2524 2555
 				}
2525 2556
 			}
@@ -2543,11 +2574,11 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
2543 2574
 			if strings.HasPrefix(proxiedIP, "[") && strings.HasSuffix(proxiedIP, "]") {
2544 2575
 				proxiedIP = proxiedIP[1 : len(proxiedIP)-1]
2545 2576
 			}
2546
-			return !client.ApplyProxiedIP(proxiedIP, secure)
2577
+			return !client.ApplyProxiedIP(rb.session, proxiedIP, secure)
2547 2578
 		}
2548 2579
 	}
2549 2580
 
2550
-	client.Quit(client.t("WEBIRC command is not usable from your address or incorrect password given"))
2581
+	client.Quit(client.t("WEBIRC command is not usable from your address or incorrect password given"), rb.session)
2551 2582
 	return true
2552 2583
 }
2553 2584
 
@@ -2568,8 +2599,6 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
2568 2599
 		mask = casefoldedMask
2569 2600
 	}
2570 2601
 
2571
-	friends := client.Friends()
2572
-
2573 2602
 	//TODO(dan): is this used and would I put this param in the Modern doc?
2574 2603
 	// if not, can we remove it?
2575 2604
 	//var operatorOnly bool
@@ -2581,8 +2610,12 @@ func whoHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
2581 2610
 		// TODO implement wildcard matching
2582 2611
 		//TODO(dan): ^ only for opers
2583 2612
 		channel := server.channels.Get(mask)
2584
-		if channel != nil {
2585
-			whoChannel(client, channel, friends, rb)
2613
+		if channel != nil && channel.hasClient(client) {
2614
+			for _, member := range channel.Members() {
2615
+				if !member.HasMode(modes.Invisible) {
2616
+					client.rplWhoReply(channel, member, rb)
2617
+				}
2618
+			}
2586 2619
 		}
2587 2620
 	} else {
2588 2621
 		for mclient := range server.clients.FindAll(mask) {

+ 14
- 9
irc/idletimer.go Vedi File

@@ -45,7 +45,7 @@ type IdleTimer struct {
45 45
 
46 46
 	// immutable after construction
47 47
 	registerTimeout time.Duration
48
-	client          *Client
48
+	session         *Session
49 49
 
50 50
 	// mutable
51 51
 	idleTimeout time.Duration
@@ -56,14 +56,19 @@ type IdleTimer struct {
56 56
 
57 57
 // Initialize sets up an IdleTimer and starts counting idle time;
58 58
 // if there is no activity from the client, it will eventually be stopped.
59
-func (it *IdleTimer) Initialize(client *Client) {
60
-	it.client = client
59
+func (it *IdleTimer) Initialize(session *Session) {
60
+	it.session = session
61 61
 	it.registerTimeout = RegisterTimeout
62 62
 	it.idleTimeout, it.quitTimeout = it.recomputeDurations()
63
+	registered := session.client.Registered()
63 64
 
64 65
 	it.Lock()
65 66
 	defer it.Unlock()
66
-	it.state = TimerUnregistered
67
+	if registered {
68
+		it.state = TimerActive
69
+	} else {
70
+		it.state = TimerUnregistered
71
+	}
67 72
 	it.resetTimeout()
68 73
 }
69 74
 
@@ -72,12 +77,12 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio
72 77
 	totalTimeout := DefaultTotalTimeout
73 78
 	// if they have the resume cap, wait longer before pinging them out
74 79
 	// to give them a chance to resume their connection
75
-	if it.client.capabilities.Has(caps.Resume) {
80
+	if it.session.capabilities.Has(caps.Resume) {
76 81
 		totalTimeout = ResumeableTotalTimeout
77 82
 	}
78 83
 
79 84
 	idleTimeout = DefaultIdleTimeout
80
-	if it.client.isTor {
85
+	if it.session.client.isTor {
81 86
 		idleTimeout = TorIdleTimeout
82 87
 	}
83 88
 
@@ -118,10 +123,10 @@ func (it *IdleTimer) processTimeout() {
118 123
 	}()
119 124
 
120 125
 	if previousState == TimerActive {
121
-		it.client.Ping()
126
+		it.session.Ping()
122 127
 	} else {
123
-		it.client.Quit(it.quitMessage(previousState))
124
-		it.client.destroy(false)
128
+		it.session.client.Quit(it.quitMessage(previousState), it.session)
129
+		it.session.client.destroy(false, it.session)
125 130
 	}
126 131
 }
127 132
 

+ 15
- 9
irc/nickname.go Vedi File

@@ -23,7 +23,7 @@ var (
23 23
 )
24 24
 
25 25
 // returns whether the change succeeded or failed
26
-func performNickChange(server *Server, client *Client, target *Client, newnick string, rb *ResponseBuffer) bool {
26
+func performNickChange(server *Server, client *Client, target *Client, session *Session, newnick string, rb *ResponseBuffer) bool {
27 27
 	nickname := strings.TrimSpace(newnick)
28 28
 	cfnick, err := CasefoldName(nickname)
29 29
 	currentNick := client.Nick()
@@ -44,8 +44,8 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
44 44
 
45 45
 	hadNick := target.HasNick()
46 46
 	origNickMask := target.NickMaskString()
47
-	whowas := client.WhoWas()
48
-	err = client.server.clients.SetNick(target, nickname)
47
+	whowas := target.WhoWas()
48
+	err = client.server.clients.SetNick(target, session, nickname)
49 49
 	if err == errNicknameInUse {
50 50
 		rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use"))
51 51
 		return false
@@ -57,16 +57,16 @@ func performNickChange(server *Server, client *Client, target *Client, newnick s
57 57
 		return false
58 58
 	}
59 59
 
60
-	client.nickTimer.Touch()
60
+	target.nickTimer.Touch()
61 61
 
62 62
 	client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick))
63 63
 	if hadNick {
64 64
 		target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname))
65 65
 		target.server.whoWas.Append(whowas)
66 66
 		rb.Add(nil, origNickMask, "NICK", nickname)
67
-		for friend := range target.Friends() {
68
-			if friend != client {
69
-				friend.Send(nil, origNickMask, "NICK", nickname)
67
+		for session := range target.Friends() {
68
+			if session != rb.session {
69
+				session.Send(nil, origNickMask, "NICK", nickname)
70 70
 			}
71 71
 		}
72 72
 	}
@@ -86,8 +86,14 @@ func (server *Server) RandomlyRename(client *Client) {
86 86
 	buf := make([]byte, 8)
87 87
 	rand.Read(buf)
88 88
 	nick := fmt.Sprintf("%s%s", prefix, hex.EncodeToString(buf))
89
-	rb := NewResponseBuffer(client)
90
-	performNickChange(server, client, client, nick, rb)
89
+	sessions := client.Sessions()
90
+	if len(sessions) == 0 {
91
+		return
92
+	}
93
+	// XXX arbitrarily pick the first session to receive error messages;
94
+	// all other sessions receive a `NICK` line same as a friend would
95
+	rb := NewResponseBuffer(sessions[0])
96
+	performNickChange(server, client, client, nil, nick, rb)
91 97
 	rb.Send(false)
92 98
 	// technically performNickChange can fail to change the nick,
93 99
 	// but if they're still delinquent, the timer will get them later

+ 2
- 2
irc/nickserv.go Vedi File

@@ -229,8 +229,8 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str
229 229
 		return
230 230
 	}
231 231
 
232
-	ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()))
233
-	ghost.destroy(false)
232
+	ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil)
233
+	ghost.destroy(false, nil)
234 234
 }
235 235
 
236 236
 func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {

+ 13
- 11
irc/responsebuffer.go Vedi File

@@ -25,9 +25,10 @@ const (
25 25
 type ResponseBuffer struct {
26 26
 	Label     string
27 27
 	batchID   string
28
-	target    *Client
29 28
 	messages  []ircmsg.IrcMessage
30 29
 	finalized bool
30
+	target    *Client
31
+	session   *Session
31 32
 }
32 33
 
33 34
 // GetLabel returns the label from the given message.
@@ -37,9 +38,10 @@ func GetLabel(msg ircmsg.IrcMessage) string {
37 38
 }
38 39
 
39 40
 // NewResponseBuffer returns a new ResponseBuffer.
40
-func NewResponseBuffer(target *Client) *ResponseBuffer {
41
+func NewResponseBuffer(session *Session) *ResponseBuffer {
41 42
 	return &ResponseBuffer{
42
-		target: target,
43
+		session: session,
44
+		target:  session.client,
43 45
 	}
44 46
 }
45 47
 
@@ -66,11 +68,11 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
66 68
 	msg.UpdateTags(tags)
67 69
 
68 70
 	// attach account-tag
69
-	if rb.target.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
71
+	if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" {
70 72
 		msg.SetTag("account", fromAccount)
71 73
 	}
72 74
 	// attach message-id
73
-	if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) {
75
+	if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) {
74 76
 		msg.SetTag("draft/msgid", msgid)
75 77
 	}
76 78
 
@@ -79,7 +81,7 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA
79 81
 
80 82
 // AddSplitMessageFromClient adds a new split message from a specific client to our queue.
81 83
 func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) {
82
-	if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
84
+	if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil {
83 85
 		rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message)
84 86
 	} else {
85 87
 		for _, messagePair := range message.Wrapped {
@@ -110,7 +112,7 @@ func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) {
110 112
 	if rb.Label != "" {
111 113
 		message.SetTag(caps.LabelTagName, rb.Label)
112 114
 	}
113
-	rb.target.SendRawMessage(message, blocking)
115
+	rb.session.SendRawMessage(message, blocking)
114 116
 }
115 117
 
116 118
 func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
@@ -120,7 +122,7 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) {
120 122
 	}
121 123
 
122 124
 	message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+rb.batchID)
123
-	rb.target.SendRawMessage(message, blocking)
125
+	rb.session.SendRawMessage(message, blocking)
124 126
 }
125 127
 
126 128
 // Send sends all messages in the buffer to the client.
@@ -146,7 +148,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
146 148
 		return nil
147 149
 	}
148 150
 
149
-	useLabel := rb.target.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
151
+	useLabel := rb.session.capabilities.Has(caps.LabeledResponse) && rb.Label != ""
150 152
 	// use a batch if we have a label, and we either currently have 0 or 2+ messages,
151 153
 	// or we are doing a Flush() and we have to assume that there will be more messages
152 154
 	// in the future.
@@ -162,7 +164,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
162 164
 	// send each message out
163 165
 	for _, message := range rb.messages {
164 166
 		// attach server-time if needed
165
-		if rb.target.capabilities.Has(caps.ServerTime) && !message.HasTag("time") {
167
+		if rb.session.capabilities.Has(caps.ServerTime) && !message.HasTag("time") {
166 168
 			message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat))
167 169
 		}
168 170
 
@@ -172,7 +174,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error {
172 174
 		}
173 175
 
174 176
 		// send message out
175
-		rb.target.SendRawMessage(message, blocking)
177
+		rb.session.SendRawMessage(message, blocking)
176 178
 	}
177 179
 
178 180
 	// end batch if required

+ 9
- 8
irc/roleplay.go Vedi File

@@ -46,13 +46,14 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
46 46
 		}
47 47
 
48 48
 		for _, member := range channel.Members() {
49
-			if member == client && !client.capabilities.Has(caps.EchoMessage) {
50
-				continue
51
-			}
52
-			if member == client {
53
-				rb.Add(nil, source, "PRIVMSG", channel.name, message)
54
-			} else {
55
-				member.Send(nil, source, "PRIVMSG", channel.name, message)
49
+			for _, session := range member.Sessions() {
50
+				if member == client && !session.capabilities.Has(caps.EchoMessage) {
51
+					continue
52
+				} else if rb.session == session {
53
+					rb.Add(nil, source, "PRIVMSG", channel.name, message)
54
+				} else if member == client || session.capabilities.Has(caps.EchoMessage) {
55
+					session.Send(nil, source, "PRIVMSG", channel.name, message)
56
+				}
56 57
 			}
57 58
 		}
58 59
 	} else {
@@ -71,7 +72,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
71 72
 		cnick := client.Nick()
72 73
 		tnick := user.Nick()
73 74
 		user.Send(nil, source, "PRIVMSG", tnick, message)
74
-		if client.capabilities.Has(caps.EchoMessage) {
75
+		if rb.session.capabilities.Has(caps.EchoMessage) {
75 76
 			rb.Add(nil, source, "PRIVMSG", tnick, message)
76 77
 		}
77 78
 		if user.HasMode(modes.Away) {

+ 49
- 46
irc/server.go Vedi File

@@ -41,8 +41,8 @@ var (
41 41
 	supportedChannelModesString = modes.SupportedChannelModes.String()
42 42
 
43 43
 	// SupportedCapabilities are the caps we advertise.
44
-	// MaxLine, SASL and STS are set during server startup.
45
-	SupportedCapabilities = caps.NewSet(caps.Acc, caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.Batch, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.LabeledResponse, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.Resume, caps.ServerTime, caps.SetName, caps.UserhostInNames)
44
+	// MaxLine, SASL and STS may be unset during server startup / rehash.
45
+	SupportedCapabilities = caps.NewCompleteSet()
46 46
 
47 47
 	// CapValues are the actual values we advertise to v3.2 clients.
48 48
 	// actual values are set during server startup.
@@ -374,7 +374,7 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b
374 374
 // server functionality
375 375
 //
376 376
 
377
-func (server *Server) tryRegister(c *Client) {
377
+func (server *Server) tryRegister(c *Client, session *Session) {
378 378
 	resumed := false
379 379
 	// try to complete registration, either via RESUME token or normally
380 380
 	if c.resumeDetails != nil {
@@ -383,7 +383,7 @@ func (server *Server) tryRegister(c *Client) {
383 383
 		}
384 384
 		resumed = true
385 385
 	} else {
386
-		if c.preregNick == "" || !c.HasUsername() || c.capState == caps.NegotiatingState {
386
+		if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
387 387
 			return
388 388
 		}
389 389
 
@@ -391,13 +391,13 @@ func (server *Server) tryRegister(c *Client) {
391 391
 		// before completing the other registration commands
392 392
 		config := server.Config()
393 393
 		if !c.isAuthorized(config) {
394
-			c.Quit(c.t("Bad password"))
395
-			c.destroy(false)
394
+			c.Quit(c.t("Bad password"), nil)
395
+			c.destroy(false, nil)
396 396
 			return
397 397
 		}
398 398
 
399
-		rb := NewResponseBuffer(c)
400
-		nickAssigned := performNickChange(server, c, c, c.preregNick, rb)
399
+		rb := NewResponseBuffer(session)
400
+		nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb)
401 401
 		rb.Send(true)
402 402
 		if !nickAssigned {
403 403
 			c.preregNick = ""
@@ -407,20 +407,24 @@ func (server *Server) tryRegister(c *Client) {
407 407
 		// check KLINEs
408 408
 		isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
409 409
 		if isBanned {
410
-			c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")))
411
-			c.destroy(false)
410
+			c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
411
+			c.destroy(false, nil)
412 412
 			return
413 413
 		}
414 414
 	}
415 415
 
416
-	// registration has succeeded:
417
-	c.SetRegistered()
416
+	reattached := session.client != c
418 417
 
419
-	// count new user in statistics
420
-	server.stats.ChangeTotal(1)
418
+	if !reattached {
419
+		// registration has succeeded:
420
+		c.SetRegistered()
421 421
 
422
-	if !resumed {
423
-		server.monitorManager.AlertAbout(c, true)
422
+		// count new user in statistics
423
+		server.stats.ChangeTotal(1)
424
+
425
+		if !resumed {
426
+			server.monitorManager.AlertAbout(c, true)
427
+		}
424 428
 	}
425 429
 
426 430
 	// continue registration
@@ -436,7 +440,7 @@ func (server *Server) tryRegister(c *Client) {
436 440
 	//TODO(dan): Look at adding last optional [<channel modes with a parameter>] parameter
437 441
 	c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString)
438 442
 
439
-	rb := NewResponseBuffer(c)
443
+	rb := NewResponseBuffer(session)
440 444
 	c.RplISupport(rb)
441 445
 	server.MOTD(c, rb)
442 446
 	rb.Send(true)
@@ -480,8 +484,7 @@ func (server *Server) MOTD(client *Client, rb *ResponseBuffer) {
480 484
 }
481 485
 
482 486
 // WhoisChannelsNames returns the common channel names between two users.
483
-func (client *Client) WhoisChannelsNames(target *Client) []string {
484
-	isMultiPrefix := client.capabilities.Has(caps.MultiPrefix)
487
+func (client *Client) WhoisChannelsNames(target *Client, multiPrefix bool) []string {
485 488
 	var chstrs []string
486 489
 	for _, channel := range target.Channels() {
487 490
 		// channel is secret and the target can't see it
@@ -490,7 +493,7 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
490 493
 				continue
491 494
 			}
492 495
 		}
493
-		chstrs = append(chstrs, channel.ClientPrefixes(target, isMultiPrefix)+channel.name)
496
+		chstrs = append(chstrs, channel.ClientPrefixes(target, multiPrefix)+channel.name)
494 497
 	}
495 498
 	return chstrs
496 499
 }
@@ -501,7 +504,7 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
501 504
 	rb.Add(nil, client.server.name, RPL_WHOISUSER, cnick, targetInfo.nick, targetInfo.username, targetInfo.hostname, "*", targetInfo.realname)
502 505
 	tnick := targetInfo.nick
503 506
 
504
-	whoischannels := client.WhoisChannelsNames(target)
507
+	whoischannels := client.WhoisChannelsNames(target, rb.session.capabilities.Has(caps.MultiPrefix))
505 508
 	if whoischannels != nil {
506 509
 		rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, cnick, tnick, strings.Join(whoischannels, " "))
507 510
 	}
@@ -555,18 +558,12 @@ func (target *Client) rplWhoReply(channel *Channel, client *Client, rb *Response
555 558
 	}
556 559
 
557 560
 	if channel != nil {
558
-		flags += channel.ClientPrefixes(client, target.capabilities.Has(caps.MultiPrefix))
561
+		// TODO is this right?
562
+		flags += channel.ClientPrefixes(client, rb.session.capabilities.Has(caps.MultiPrefix))
559 563
 		channelName = channel.name
560 564
 	}
561
-	rb.Add(nil, target.server.name, RPL_WHOREPLY, target.nick, channelName, client.Username(), client.Hostname(), client.server.name, client.Nick(), flags, strconv.Itoa(client.hops)+" "+client.Realname())
562
-}
563
-
564
-func whoChannel(client *Client, channel *Channel, friends ClientSet, rb *ResponseBuffer) {
565
-	for _, member := range channel.Members() {
566
-		if !client.HasMode(modes.Invisible) || friends[client] {
567
-			client.rplWhoReply(channel, member, rb)
568
-		}
569
-	}
565
+	// hardcode a hopcount of 0 for now
566
+	rb.Add(nil, target.server.name, RPL_WHOREPLY, target.nick, channelName, client.Username(), client.Hostname(), client.server.name, client.Nick(), flags, "0 "+client.Realname())
570 567
 }
571 568
 
572 569
 // rehash reloads the config and applies the changes from the config file.
@@ -691,6 +688,8 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
691 688
 		SupportedCapabilities.Enable(caps.MaxLine)
692 689
 		value := fmt.Sprintf("%d", config.Limits.LineLen.Rest)
693 690
 		CapValues.Set(caps.MaxLine, value)
691
+	} else {
692
+		SupportedCapabilities.Disable(caps.MaxLine)
694 693
 	}
695 694
 
696 695
 	// STS
@@ -699,20 +698,24 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
699 698
 	stsDisabledByRehash := false
700 699
 	stsCurrentCapValue, _ := CapValues.Get(caps.STS)
701 700
 	server.logger.Debug("server", "STS Vals", stsCurrentCapValue, stsValue, fmt.Sprintf("server[%v] config[%v]", stsPreviouslyEnabled, config.Server.STS.Enabled))
702
-	if config.Server.STS.Enabled && !stsPreviouslyEnabled {
701
+	if config.Server.STS.Enabled {
703 702
 		// enabling STS
704 703
 		SupportedCapabilities.Enable(caps.STS)
705
-		addedCaps.Add(caps.STS)
706
-		CapValues.Set(caps.STS, stsValue)
707
-	} else if !config.Server.STS.Enabled && stsPreviouslyEnabled {
704
+		if !stsPreviouslyEnabled {
705
+			addedCaps.Add(caps.STS)
706
+			CapValues.Set(caps.STS, stsValue)
707
+		} else if stsValue != stsCurrentCapValue {
708
+			// STS policy updated
709
+			CapValues.Set(caps.STS, stsValue)
710
+			updatedCaps.Add(caps.STS)
711
+		}
712
+	} else {
708 713
 		// disabling STS
709 714
 		SupportedCapabilities.Disable(caps.STS)
710
-		removedCaps.Add(caps.STS)
711
-		stsDisabledByRehash = true
712
-	} else if config.Server.STS.Enabled && stsPreviouslyEnabled && stsValue != stsCurrentCapValue {
713
-		// STS policy updated
714
-		CapValues.Set(caps.STS, stsValue)
715
-		updatedCaps.Add(caps.STS)
715
+		if stsPreviouslyEnabled {
716
+			removedCaps.Add(caps.STS)
717
+			stsDisabledByRehash = true
718
+		}
716 719
 	}
717 720
 
718 721
 	// resize history buffers as needed
@@ -730,7 +733,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
730 733
 	}
731 734
 
732 735
 	// burst new and removed caps
733
-	var capBurstClients ClientSet
736
+	var capBurstSessions []*Session
734 737
 	added := make(map[caps.Version]string)
735 738
 	var removed string
736 739
 
@@ -741,7 +744,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
741 744
 	removedCaps.Union(updatedCaps)
742 745
 
743 746
 	if !addedCaps.Empty() || !removedCaps.Empty() {
744
-		capBurstClients = server.clients.AllWithCaps(caps.CapNotify)
747
+		capBurstSessions = server.clients.AllWithCaps(caps.CapNotify)
745 748
 
746 749
 		added[caps.Cap301] = addedCaps.String(caps.Cap301, CapValues)
747 750
 		added[caps.Cap302] = addedCaps.String(caps.Cap302, CapValues)
@@ -749,7 +752,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
749 752
 		removed = removedCaps.String(caps.Cap301, CapValues)
750 753
 	}
751 754
 
752
-	for sClient := range capBurstClients {
755
+	for _, sSession := range capBurstSessions {
753 756
 		if stsDisabledByRehash {
754 757
 			// remove STS policy
755 758
 			//TODO(dan): this is an ugly hack. we can write this better.
@@ -763,10 +766,10 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) {
763 766
 		}
764 767
 		// DEL caps and then send NEW ones so that updated caps get removed/added correctly
765 768
 		if !removedCaps.Empty() {
766
-			sClient.Send(nil, server.name, "CAP", sClient.nick, "DEL", removed)
769
+			sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "DEL", removed)
767 770
 		}
768 771
 		if !addedCaps.Empty() {
769
-			sClient.Send(nil, server.name, "CAP", sClient.nick, "NEW", added[sClient.capVersion])
772
+			sSession.Send(nil, server.name, "CAP", sSession.client.Nick(), "NEW", added[sSession.capVersion])
770 773
 		}
771 774
 	}
772 775
 

+ 13
- 0
oragono.yaml Vedi File

@@ -262,6 +262,19 @@ accounts:
262 262
         # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
263 263
         rename-prefix: Guest-
264 264
 
265
+    # bouncer controls whether oragono can act as a bouncer, i.e., allowing
266
+    # multiple connections to attach to the same client/nickname identity
267
+    bouncer:
268
+        # when disabled, each connection must use a separate nickname (as is the
269
+        # typical behavior of IRC servers). when enabled, a new connection that
270
+        # has authenticated with SASL can associate itself with an existing
271
+        # client
272
+        enabled: true
273
+
274
+        # clients can opt in to bouncer functionality using the cap system, or
275
+        # via nickserv. if this is enabled, then they have to opt out instead
276
+        allowed-by-default: false
277
+
265 278
     # vhosts controls the assignment of vhosts (strings displayed in place of the user's
266 279
     # hostname/IP) by the HostServ service
267 280
     vhosts:

Loading…
Annulla
Salva