Parcourir la 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 il y a 2 ans
Parent
révision
c5579a6a34
10 fichiers modifiés avec 250 ajouts et 35 suppressions
  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 Voir le fichier

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 Voir le fichier

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 Voir le fichier

1318
 
1318
 
1319
 	// clean up server
1319
 	// clean up server
1320
 	client.server.clients.Remove(client)
1320
 	client.server.clients.Remove(client)
1321
-
1322
-	// clean up self
1321
+	client.server.accepts.Remove(client)
1323
 	client.server.accounts.Logout(client)
1322
 	client.server.accounts.Logout(client)
1324
 
1323
 
1325
 	if quitMessage == "" {
1324
 	if quitMessage == "" {

+ 4
- 0
irc/commands.go Voir le fichier

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

+ 10
- 10
irc/fakelag_test.go Voir le fichier

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

+ 40
- 1
irc/handlers.go Voir le fichier

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))
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
 // AUTHENTICATE [<mechanism>|<data>|*]
183
 // AUTHENTICATE [<mechanism>|<data>|*]
149
 func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
184
 func authenticateHandler(server *Server, client *Client, msg ircmsg.Message, rb *ResponseBuffer) bool {
150
 	session := rb.session
185
 	session := rb.session
2284
 			return
2319
 			return
2285
 		}
2320
 		}
2286
 		// restrict messages appropriately when +R is set
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
 			rb.Add(nil, server.name, ERR_NEEDREGGEDNICK, client.Nick(), tnick, client.t("You must be registered to send a direct message to this user"))
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
 			return
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
 		if !client.server.Config().Server.Compatibility.allowTruncation {
2330
 		if !client.server.Config().Server.Compatibility.allowTruncation {
2292
 			if !validateSplitMessageLen(histType, client.NickMaskString(), tnick, message) {
2331
 			if !validateSplitMessageLen(histType, client.NickMaskString(), tnick, message) {
2293
 				rb.Add(nil, server.name, ERR_INPUTTOOLONG, client.Nick(), client.t("Line too long to be relayed without truncation"))
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 Voir le fichier

110
 // Help contains the help strings distributed with the IRCd.
110
 // Help contains the help strings distributed with the IRCd.
111
 var Help = map[string]HelpEntry{
111
 var Help = map[string]HelpEntry{
112
 	// Commands
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
 	"ambiance": {
120
 	"ambiance": {
114
 		text: `AMBIANCE <target> <text to be sent>
121
 		text: `AMBIANCE <target> <text to be sent>
115
 
122
 

+ 10
- 10
irc/misc_test.go Voir le fichier

9
 )
9
 )
10
 
10
 
11
 func TestZncTimestampParser(t *testing.T) {
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 Voir le fichier

4
 package irc
4
 package irc
5
 
5
 
6
 import (
6
 import (
7
+	"fmt"
7
 	"reflect"
8
 	"reflect"
8
 	"testing"
9
 	"testing"
9
 
10
 
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
 func TestChannelUserModeHasPrivsOver(t *testing.T) {
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 Voir le fichier

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

Chargement…
Annuler
Enregistrer