Browse Source

fix #1688

* Add ACCEPT-tracking functionality (authorizing users to send DMs
  despite +R or other applicable restrictions)
* Sending a DM automatically accepts the recipient
* Add explicit ACCEPT command
tags/v2.10.0-rc1
Shivaram Lingamneni 2 years ago
parent
commit
c5579a6a34
10 changed files with 250 additions and 35 deletions
  1. 76
    0
      irc/accept.go
  2. 87
    0
      irc/accept_test.go
  3. 1
    2
      irc/client.go
  4. 4
    0
      irc/commands.go
  5. 10
    10
      irc/fakelag_test.go
  6. 40
    1
      irc/handlers.go
  7. 7
    0
      irc/help.go
  8. 10
    10
      irc/misc_test.go
  9. 13
    12
      irc/modes_test.go
  10. 2
    0
      irc/server.go

+ 76
- 0
irc/accept.go View File

@@ -0,0 +1,76 @@
1
+package irc
2
+
3
+import (
4
+	"sync"
5
+
6
+	"github.com/ergochat/ergo/irc/utils"
7
+)
8
+
9
+// tracks ACCEPT relationships, i.e., `accepter` is willing to receive DMs from
10
+// `accepted` despite some restriction (currently the only relevant restriction
11
+// is that `accepter` is +R and `accepted` is not logged in)
12
+
13
+type AcceptManager struct {
14
+	sync.RWMutex
15
+
16
+	// maps recipient -> whitelist of permitted senders:
17
+	// this is what we actually check
18
+	clientToAccepted map[*Client]utils.HashSet[*Client]
19
+	// this is the reverse mapping, it's needed so we can
20
+	// clean up the forward mapping during (*Client).destroy():
21
+	clientToAccepters map[*Client]utils.HashSet[*Client]
22
+}
23
+
24
+func (am *AcceptManager) Initialize() {
25
+	am.clientToAccepted = make(map[*Client]utils.HashSet[*Client])
26
+	am.clientToAccepters = make(map[*Client]utils.HashSet[*Client])
27
+}
28
+
29
+func (am *AcceptManager) MaySendTo(sender, recipient *Client) (result bool) {
30
+	am.RLock()
31
+	defer am.RUnlock()
32
+	return am.clientToAccepted[recipient].Has(sender)
33
+}
34
+
35
+func (am *AcceptManager) Accept(accepter, accepted *Client) {
36
+	am.Lock()
37
+	defer am.Unlock()
38
+
39
+	var m utils.HashSet[*Client]
40
+
41
+	m = am.clientToAccepted[accepter]
42
+	if m == nil {
43
+		m = make(utils.HashSet[*Client])
44
+		am.clientToAccepted[accepter] = m
45
+	}
46
+	m.Add(accepted)
47
+
48
+	m = am.clientToAccepters[accepted]
49
+	if m == nil {
50
+		m = make(utils.HashSet[*Client])
51
+		am.clientToAccepters[accepted] = m
52
+	}
53
+	m.Add(accepter)
54
+}
55
+
56
+func (am *AcceptManager) Unaccept(accepter, accepted *Client) {
57
+	am.Lock()
58
+	defer am.Unlock()
59
+
60
+	delete(am.clientToAccepted[accepter], accepted)
61
+	delete(am.clientToAccepters[accepted], accepter)
62
+}
63
+
64
+func (am *AcceptManager) Remove(client *Client) {
65
+	am.Lock()
66
+	defer am.Unlock()
67
+
68
+	for accepter := range am.clientToAccepters[client] {
69
+		delete(am.clientToAccepted[accepter], client)
70
+	}
71
+	for accepted := range am.clientToAccepted[client] {
72
+		delete(am.clientToAccepters[accepted], client)
73
+	}
74
+	delete(am.clientToAccepters, client)
75
+	delete(am.clientToAccepted, client)
76
+}

+ 87
- 0
irc/accept_test.go View File

@@ -0,0 +1,87 @@
1
+package irc
2
+
3
+import (
4
+	"testing"
5
+)
6
+
7
+func TestAccept(t *testing.T) {
8
+	var am AcceptManager
9
+	am.Initialize()
10
+
11
+	alice := new(Client)
12
+	bob := new(Client)
13
+	eve := new(Client)
14
+
15
+	assertEqual(am.MaySendTo(alice, bob), false)
16
+	assertEqual(am.MaySendTo(bob, alice), false)
17
+	assertEqual(am.MaySendTo(alice, eve), false)
18
+	assertEqual(am.MaySendTo(eve, alice), false)
19
+	assertEqual(am.MaySendTo(bob, eve), false)
20
+	assertEqual(am.MaySendTo(eve, bob), false)
21
+
22
+	am.Accept(alice, bob)
23
+
24
+	assertEqual(am.MaySendTo(alice, bob), false)
25
+	assertEqual(am.MaySendTo(bob, alice), true)
26
+	assertEqual(am.MaySendTo(alice, eve), false)
27
+	assertEqual(am.MaySendTo(eve, alice), false)
28
+	assertEqual(am.MaySendTo(bob, eve), false)
29
+	assertEqual(am.MaySendTo(eve, bob), false)
30
+
31
+	am.Accept(bob, alice)
32
+
33
+	assertEqual(am.MaySendTo(alice, bob), true)
34
+	assertEqual(am.MaySendTo(bob, alice), true)
35
+	assertEqual(am.MaySendTo(alice, eve), false)
36
+	assertEqual(am.MaySendTo(eve, alice), false)
37
+	assertEqual(am.MaySendTo(bob, eve), false)
38
+	assertEqual(am.MaySendTo(eve, bob), false)
39
+
40
+	am.Accept(bob, eve)
41
+
42
+	assertEqual(am.MaySendTo(alice, bob), true)
43
+	assertEqual(am.MaySendTo(bob, alice), true)
44
+	assertEqual(am.MaySendTo(alice, eve), false)
45
+	assertEqual(am.MaySendTo(eve, alice), false)
46
+	assertEqual(am.MaySendTo(bob, eve), false)
47
+	assertEqual(am.MaySendTo(eve, bob), true)
48
+
49
+	am.Remove(alice)
50
+
51
+	assertEqual(am.MaySendTo(alice, bob), false)
52
+	assertEqual(am.MaySendTo(bob, alice), false)
53
+	assertEqual(am.MaySendTo(alice, eve), false)
54
+	assertEqual(am.MaySendTo(eve, alice), false)
55
+	assertEqual(am.MaySendTo(bob, eve), false)
56
+	assertEqual(am.MaySendTo(eve, bob), true)
57
+
58
+	am.Remove(bob)
59
+
60
+	assertEqual(am.MaySendTo(alice, bob), false)
61
+	assertEqual(am.MaySendTo(bob, alice), false)
62
+	assertEqual(am.MaySendTo(alice, eve), false)
63
+	assertEqual(am.MaySendTo(eve, alice), false)
64
+	assertEqual(am.MaySendTo(bob, eve), false)
65
+	assertEqual(am.MaySendTo(eve, bob), false)
66
+}
67
+
68
+func TestAcceptInternal(t *testing.T) {
69
+	var am AcceptManager
70
+	am.Initialize()
71
+
72
+	alice := new(Client)
73
+	bob := new(Client)
74
+	eve := new(Client)
75
+
76
+	am.Accept(alice, bob)
77
+	am.Accept(bob, alice)
78
+	am.Accept(bob, eve)
79
+	am.Remove(alice)
80
+	am.Remove(bob)
81
+
82
+	// assert that there is no memory leak
83
+	for _, client := range []*Client{alice, bob, eve} {
84
+		assertEqual(len(am.clientToAccepted[client]), 0)
85
+		assertEqual(len(am.clientToAccepters[client]), 0)
86
+	}
87
+}

+ 1
- 2
irc/client.go View File

@@ -1318,8 +1318,7 @@ func (client *Client) destroy(session *Session) {
1318 1318
 
1319 1319
 	// clean up server
1320 1320
 	client.server.clients.Remove(client)
1321
-
1322
-	// clean up self
1321
+	client.server.accepts.Remove(client)
1323 1322
 	client.server.accounts.Logout(client)
1324 1323
 
1325 1324
 	if quitMessage == "" {

+ 4
- 0
irc/commands.go View File

@@ -75,6 +75,10 @@ var Commands map[string]Command
75 75
 
76 76
 func init() {
77 77
 	Commands = map[string]Command{
78
+		"ACCEPT": {
79
+			handler:   acceptHandler,
80
+			minParams: 1,
81
+		},
78 82
 		"AMBIANCE": {
79 83
 			handler:   sceneHandler,
80 84
 			minParams: 2,

+ 10
- 10
irc/fakelag_test.go View File

@@ -125,31 +125,31 @@ func TestFakelag(t *testing.T) {
125 125
 func TestSuspend(t *testing.T) {
126 126
 	window, _ := time.ParseDuration("1s")
127 127
 	fl, _ := newFakelagForTesting(window, 3, 2, window)
128
-	assertEqual(fl.config.Enabled, true, t)
128
+	assertEqual(fl.config.Enabled, true)
129 129
 
130 130
 	// suspend idempotently disables
131 131
 	fl.Suspend()
132
-	assertEqual(fl.config.Enabled, false, t)
132
+	assertEqual(fl.config.Enabled, false)
133 133
 	fl.Suspend()
134
-	assertEqual(fl.config.Enabled, false, t)
134
+	assertEqual(fl.config.Enabled, false)
135 135
 	// unsuspend idempotently enables
136 136
 	fl.Unsuspend()
137
-	assertEqual(fl.config.Enabled, true, t)
137
+	assertEqual(fl.config.Enabled, true)
138 138
 	fl.Unsuspend()
139
-	assertEqual(fl.config.Enabled, true, t)
139
+	assertEqual(fl.config.Enabled, true)
140 140
 	fl.Suspend()
141
-	assertEqual(fl.config.Enabled, false, t)
141
+	assertEqual(fl.config.Enabled, false)
142 142
 
143 143
 	fl2, _ := newFakelagForTesting(window, 3, 2, window)
144 144
 	fl2.config.Enabled = false
145 145
 
146 146
 	// if we were never enabled, suspend and unsuspend are both no-ops
147 147
 	fl2.Suspend()
148
-	assertEqual(fl2.config.Enabled, false, t)
148
+	assertEqual(fl2.config.Enabled, false)
149 149
 	fl2.Suspend()
150
-	assertEqual(fl2.config.Enabled, false, t)
150
+	assertEqual(fl2.config.Enabled, false)
151 151
 	fl2.Unsuspend()
152
-	assertEqual(fl2.config.Enabled, false, t)
152
+	assertEqual(fl2.config.Enabled, false)
153 153
 	fl2.Unsuspend()
154
-	assertEqual(fl2.config.Enabled, false, t)
154
+	assertEqual(fl2.config.Enabled, false)
155 155
 }

+ 40
- 1
irc/handlers.go View File

@@ -145,6 +145,41 @@ func (server *Server) sendLoginSnomask(nickMask, accountName string) {
145 145
 	server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] logged into account $c[grey][$r%s$c[grey]]"), nickMask, accountName))
146 146
 }
147 147
 
148
+// ACCEPT <nicklist>
149
+// nicklist is a comma-delimited list of nicknames; each may be prefixed with -
150
+// to indicate that it should be removed from the list
151
+func acceptHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
152
+	for _, tNick := range strings.Split(msg.Params[0], ",") {
153
+		add := true
154
+		if strings.HasPrefix(tNick, "-") {
155
+			add = false
156
+			tNick = strings.TrimPrefix(tNick, "-")
157
+		}
158
+
159
+		target := server.clients.Get(tNick)
160
+		if target == nil {
161
+			rb.Add(nil, server.name, "FAIL", "ACCEPT", "INVALID_USER", utils.SafeErrorParam(tNick), client.t("No such user"))
162
+			continue
163
+		}
164
+
165
+		if add {
166
+			server.accepts.Accept(client, target)
167
+		} else {
168
+			server.accepts.Unaccept(client, target)
169
+		}
170
+
171
+		// https://github.com/solanum-ircd/solanum/blob/main/doc/features/modeg.txt
172
+		// Charybdis/Solanum define various error numerics that could be sent here,
173
+		// but this doesn't seem important to me. One thing to note is that we are not
174
+		// imposing an upper bound on the size of the accept list, since in our
175
+		// implementation you can only ACCEPT clients who are actually present,
176
+		// and an attacker attempting to DoS has much easier resource exhaustion
177
+		// strategies available (for example, channel history buffers).
178
+	}
179
+
180
+	return false
181
+}
182
+
148 183
 // AUTHENTICATE [<mechanism>|<data>|*]
149 184
 func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
150 185
 	session := rb.session
@@ -2284,10 +2319,14 @@ func dispatchMessageToTarget(client *Client, tags map[string]string, histType hi
2284 2319
 			return
2285 2320
 		}
2286 2321
 		// restrict messages appropriately when +R is set
2287
-		if details.account == "" && user.HasMode(modes.RegisteredOnly) {
2322
+		if details.account == "" && user.HasMode(modes.RegisteredOnly) && !server.accepts.MaySendTo(client, user) {
2288 2323
 			rb.Add(nil, server.name, ERR_NEEDREGGEDNICK, client.Nick(), tnick, client.t("You must be registered to send a direct message to this user"))
2289 2324
 			return
2290 2325
 		}
2326
+		if client.HasMode(modes.RegisteredOnly) && tDetails.account == "" {
2327
+			// #1688: auto-ACCEPT on DM
2328
+			server.accepts.Accept(client, user)
2329
+		}
2291 2330
 		if !client.server.Config().Server.Compatibility.allowTruncation {
2292 2331
 			if !validateSplitMessageLen(histType, client.NickMaskString(), tnick, message) {
2293 2332
 				rb.Add(nil, server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Line too long to be relayed without truncation"))

+ 7
- 0
irc/help.go View File

@@ -110,6 +110,13 @@ For instance, this would set the kill, oper, account and xline snomasks on dan:
110 110
 // Help contains the help strings distributed with the IRCd.
111 111
 var Help = map[string]HelpEntry{
112 112
 	// Commands
113
+	"accept": {
114
+		text: `ACCEPT <target>
115
+
116
+ACCEPT allows the target user to send you direct messages, overriding any
117
+restrictions that might otherwise prevent this. Currently, the only
118
+applicable restriction is the +R registered-only mode.`,
119
+	},
113 120
 	"ambiance": {
114 121
 		text: `AMBIANCE <target> <text to be sent>
115 122
 

+ 10
- 10
irc/misc_test.go View File

@@ -9,14 +9,14 @@ import (
9 9
 )
10 10
 
11 11
 func TestZncTimestampParser(t *testing.T) {
12
-	assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000).UTC(), t)
13
-	assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000).UTC(), t)
14
-	assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0).UTC(), t)
15
-	assertEqual(zncWireTimeToTime("1558338348.99999999999999999999999999999"), time.Unix(1558338348, 999999999).UTC(), t)
16
-	assertEqual(zncWireTimeToTime("1558338348.999999999111111111"), time.Unix(1558338348, 999999999).UTC(), t)
17
-	assertEqual(zncWireTimeToTime("1558338348.999999991111111111"), time.Unix(1558338348, 999999991).UTC(), t)
18
-	assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000).UTC(), t)
19
-	assertEqual(zncWireTimeToTime("0"), time.Unix(0, 0).UTC(), t)
20
-	assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0).UTC(), t)
21
-	assertEqual(zncWireTimeToTime(""), time.Unix(0, 0).UTC(), t)
12
+	assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000).UTC())
13
+	assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000).UTC())
14
+	assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0).UTC())
15
+	assertEqual(zncWireTimeToTime("1558338348.99999999999999999999999999999"), time.Unix(1558338348, 999999999).UTC())
16
+	assertEqual(zncWireTimeToTime("1558338348.999999999111111111"), time.Unix(1558338348, 999999999).UTC())
17
+	assertEqual(zncWireTimeToTime("1558338348.999999991111111111"), time.Unix(1558338348, 999999991).UTC())
18
+	assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000).UTC())
19
+	assertEqual(zncWireTimeToTime("0"), time.Unix(0, 0).UTC())
20
+	assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0).UTC())
21
+	assertEqual(zncWireTimeToTime(""), time.Unix(0, 0).UTC())
22 22
 }

+ 13
- 12
irc/modes_test.go View File

@@ -4,6 +4,7 @@
4 4
 package irc
5 5
 
6 6
 import (
7
+	"fmt"
7 8
 	"reflect"
8 9
 	"testing"
9 10
 
@@ -74,21 +75,21 @@ func TestUmodeGreaterThan(t *testing.T) {
74 75
 	}
75 76
 }
76 77
 
77
-func assertEqual(supplied, expected interface{}, t *testing.T) {
78
-	if !reflect.DeepEqual(supplied, expected) {
79
-		t.Errorf("expected %v but got %v", expected, supplied)
78
+func assertEqual(found, expected interface{}) {
79
+	if !reflect.DeepEqual(found, expected) {
80
+		panic(fmt.Sprintf("found %#v, expected %#v", found, expected))
80 81
 	}
81 82
 }
82 83
 
83 84
 func TestChannelUserModeHasPrivsOver(t *testing.T) {
84
-	assertEqual(channelUserModeHasPrivsOver(modes.Voice, modes.Halfop), false, t)
85
-	assertEqual(channelUserModeHasPrivsOver(modes.Mode(0), modes.Halfop), false, t)
86
-	assertEqual(channelUserModeHasPrivsOver(modes.Voice, modes.Mode(0)), false, t)
87
-	assertEqual(channelUserModeHasPrivsOver(modes.ChannelAdmin, modes.ChannelAdmin), false, t)
88
-	assertEqual(channelUserModeHasPrivsOver(modes.Halfop, modes.Halfop), false, t)
89
-	assertEqual(channelUserModeHasPrivsOver(modes.Voice, modes.Voice), false, t)
85
+	assertEqual(channelUserModeHasPrivsOver(modes.Voice, modes.Halfop), false)
86
+	assertEqual(channelUserModeHasPrivsOver(modes.Mode(0), modes.Halfop), false)
87
+	assertEqual(channelUserModeHasPrivsOver(modes.Voice, modes.Mode(0)), false)
88
+	assertEqual(channelUserModeHasPrivsOver(modes.ChannelAdmin, modes.ChannelAdmin), false)
89
+	assertEqual(channelUserModeHasPrivsOver(modes.Halfop, modes.Halfop), false)
90
+	assertEqual(channelUserModeHasPrivsOver(modes.Voice, modes.Voice), false)
90 91
 
91
-	assertEqual(channelUserModeHasPrivsOver(modes.Halfop, modes.Voice), true, t)
92
-	assertEqual(channelUserModeHasPrivsOver(modes.ChannelFounder, modes.ChannelAdmin), true, t)
93
-	assertEqual(channelUserModeHasPrivsOver(modes.ChannelOperator, modes.ChannelOperator), true, t)
92
+	assertEqual(channelUserModeHasPrivsOver(modes.Halfop, modes.Voice), true)
93
+	assertEqual(channelUserModeHasPrivsOver(modes.ChannelFounder, modes.ChannelAdmin), true)
94
+	assertEqual(channelUserModeHasPrivsOver(modes.ChannelOperator, modes.ChannelOperator), true)
94 95
 }

+ 2
- 0
irc/server.go View File

@@ -61,6 +61,7 @@ var (
61 61
 
62 62
 // Server is the main Oragono server.
63 63
 type Server struct {
64
+	accepts           AcceptManager
64 65
 	accounts          AccountManager
65 66
 	channels          ChannelManager
66 67
 	channelRegistry   ChannelRegistry
@@ -104,6 +105,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
104 105
 		defcon:       5,
105 106
 	}
106 107
 
108
+	server.accepts.Initialize()
107 109
 	server.clients.Initialize()
108 110
 	server.semaphores.Initialize()
109 111
 	server.whoWas.Initialize(config.Limits.WhowasEntries)

Loading…
Cancel
Save