Browse Source

Basic implementation of KLINEs

tags/v0.6.0
Daniel Oaks 7 years ago
parent
commit
4168eaafbb
5 changed files with 379 additions and 1 deletions
  1. 26
    0
      irc/client.go
  2. 10
    0
      irc/commands.go
  3. 34
    0
      irc/help.go
  4. 291
    0
      irc/kline.go
  5. 18
    1
      irc/server.go

+ 26
- 0
irc/client.go View File

@@ -353,6 +353,32 @@ func (client *Client) updateNickMask() {
353 353
 	client.nickMaskCasefolded = nickMaskCasefolded
354 354
 }
355 355
 
356
+// AllNickmasks returns all the possible nickmasks for the client.
357
+func (client *Client) AllNickmasks() []string {
358
+	var masks []string
359
+	var mask string
360
+	var err error
361
+
362
+	if len(client.vhost) > 0 {
363
+		mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.vhost))
364
+		if err == nil {
365
+			masks = append(masks, mask)
366
+		}
367
+	}
368
+
369
+	mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.rawHostname))
370
+	if err == nil {
371
+		masks = append(masks, mask)
372
+	}
373
+
374
+	mask2, err := Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, IPString(client.socket.conn.RemoteAddr())))
375
+	if err == nil && mask2 != mask {
376
+		masks = append(masks, mask2)
377
+	}
378
+
379
+	return masks
380
+}
381
+
356 382
 // SetNickname sets the very first nickname for the client.
357 383
 func (client *Client) SetNickname(nickname string) error {
358 384
 	if client.HasNick() {

+ 10
- 0
irc/commands.go View File

@@ -107,6 +107,11 @@ var Commands = map[string]Command{
107 107
 		oper:      true,
108 108
 		capabs:    []string{"oper:local_kill"}, //TODO(dan): when we have S2S, this will be checked in the command handler itself
109 109
 	},
110
+	"KLINE": {
111
+		handler:   klineHandler,
112
+		minParams: 1,
113
+		oper:      true,
114
+	},
110 115
 	"LIST": {
111 116
 		handler:   listHandler,
112 117
 		minParams: 0,
@@ -210,6 +215,11 @@ var Commands = map[string]Command{
210 215
 		minParams: 1,
211 216
 		oper:      true,
212 217
 	},
218
+	"UNKLINE": {
219
+		handler:   unKLineHandler,
220
+		minParams: 1,
221
+		oper:      true,
222
+	},
213 223
 	"USER": {
214 224
 		handler:      userHandler,
215 225
 		usablePreReg: true,

+ 34
- 0
irc/help.go View File

@@ -157,6 +157,30 @@ channel privs.`,
157 157
 
158 158
 Removes the given user from the network, showing them the reason if it is
159 159
 supplied.`,
160
+	},
161
+	"kline": {
162
+		oper: true,
163
+		text: `KLINE [MYSELF] [duration] <mask> [ON <server>] [reason [| oper reason]]
164
+
165
+Bans a mask from connecting to the server. If the duration is given then only for that
166
+long. The reason is shown to the user themselves, but everyone else will see a standard
167
+message. The oper reason is shown to operators getting info about the KLINEs that exist.
168
+
169
+Bans are saved across subsequent launches of the server.
170
+
171
+"MYSELF" is required when the KLINE matches the address the person applying it is connected
172
+from. If "MYSELF" is not given, trying to KLINE yourself will result in an error.
173
+
174
+[duration] can be of the following forms:
175
+	10h 8m 13s
176
+
177
+<mask> is specified in typical IRC format. For example:
178
+	dan
179
+	dan!5*@127.*
180
+
181
+ON <server> specifies that the ban is to be set on that specific server.
182
+
183
+[reason] and [oper reason], if they exist, are separated by a vertical bar (|).`,
160 184
 	},
161 185
 	"list": {
162 186
 		text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]
@@ -309,6 +333,16 @@ Removes an existing ban on an IP address or a network.
309 333
 	127.0.0.1/8
310 334
 	8.8.8.8/24`,
311 335
 	},
336
+	"unkline": {
337
+		oper: true,
338
+		text: `UNKLINE <mask>
339
+
340
+Removes an existing ban on a mask.
341
+
342
+For example:
343
+	dan
344
+	dan!5*@127.*`,
345
+	},
312 346
 	"user": {
313 347
 		text: `USER <username> 0 * <realname>
314 348
 

+ 291
- 0
irc/kline.go View File

@@ -0,0 +1,291 @@
1
+// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"encoding/json"
8
+	"fmt"
9
+	"strings"
10
+	"time"
11
+
12
+	"github.com/DanielOaks/girc-go/ircmatch"
13
+	"github.com/DanielOaks/girc-go/ircmsg"
14
+	"github.com/tidwall/buntdb"
15
+)
16
+
17
+const (
18
+	keyKlineEntry = "bans.kline %s"
19
+)
20
+
21
+// KLineInfo contains the address itself and expiration time for a given network.
22
+type KLineInfo struct {
23
+	// Mask that is blocked.
24
+	Mask string
25
+	// Matcher, to facilitate fast matching.
26
+	Matcher ircmatch.Matcher
27
+	// Info contains information on the ban.
28
+	Info IPBanInfo
29
+}
30
+
31
+// KLineManager manages and klines.
32
+type KLineManager struct {
33
+	// kline'd entries
34
+	entries map[string]*KLineInfo
35
+}
36
+
37
+// NewKLineManager returns a new KLineManager.
38
+func NewKLineManager() *KLineManager {
39
+	var km KLineManager
40
+	km.entries = make(map[string]*KLineInfo)
41
+	return &km
42
+}
43
+
44
+// AllBans returns all bans (for use with APIs, etc).
45
+func (km *KLineManager) AllBans() map[string]IPBanInfo {
46
+	allb := make(map[string]IPBanInfo)
47
+
48
+	for name, info := range km.entries {
49
+		allb[name] = info.Info
50
+	}
51
+
52
+	return allb
53
+}
54
+
55
+// AddMask adds to the blocked list.
56
+func (km *KLineManager) AddMask(mask string, length *IPRestrictTime, reason string, operReason string) {
57
+	kln := KLineInfo{
58
+		Mask:    mask,
59
+		Matcher: ircmatch.MakeMatch(mask),
60
+		Info: IPBanInfo{
61
+			Time:       length,
62
+			Reason:     reason,
63
+			OperReason: operReason,
64
+		},
65
+	}
66
+	km.entries[mask] = &kln
67
+}
68
+
69
+// RemoveMask removes a mask from the blocked list.
70
+func (km *KLineManager) RemoveMask(mask string) {
71
+	delete(km.entries, mask)
72
+}
73
+
74
+// CheckMasks returns whether or not the hostmask(s) are banned, and how long they are banned for.
75
+func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info *IPBanInfo) {
76
+	// check networks
77
+	var masksToRemove []string
78
+
79
+	for _, entryInfo := range km.entries {
80
+		var matches bool
81
+		for _, mask := range masks {
82
+			if entryInfo.Matcher.Match(mask) {
83
+				matches = true
84
+				break
85
+			}
86
+		}
87
+		if !matches {
88
+			continue
89
+		}
90
+
91
+		if entryInfo.Info.Time != nil {
92
+			if entryInfo.Info.Time.IsExpired() {
93
+				// ban on network has expired, remove it from our blocked list
94
+				masksToRemove = append(masksToRemove, entryInfo.Mask)
95
+			} else {
96
+				return true, &entryInfo.Info
97
+			}
98
+		} else {
99
+			return true, &entryInfo.Info
100
+		}
101
+	}
102
+
103
+	// remove expired networks
104
+	for _, expiredMask := range masksToRemove {
105
+		km.RemoveMask(expiredMask)
106
+	}
107
+
108
+	// no matches!
109
+	return false, nil
110
+}
111
+
112
+// KLINE [MYSELF] [duration] <mask> [ON <server>] [reason [| oper reason]]
113
+func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
114
+	// check oper permissions
115
+	if !client.class.Capabilities["oper:local_ban"] {
116
+		client.Send(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, "Insufficient oper privs")
117
+		return false
118
+	}
119
+
120
+	currentArg := 0
121
+
122
+	// when setting a ban that covers the oper's current connection, we require them to say
123
+	// "KLINE MYSELF" so that we're sure they really mean it.
124
+	var klineMyself bool
125
+	if len(msg.Params) > currentArg+1 && strings.ToLower(msg.Params[currentArg]) == "myself" {
126
+		klineMyself = true
127
+		currentArg++
128
+	}
129
+
130
+	// duration
131
+	duration, err := time.ParseDuration(msg.Params[currentArg])
132
+	durationIsUsed := err == nil
133
+	if durationIsUsed {
134
+		currentArg++
135
+	}
136
+
137
+	// get mask
138
+	if len(msg.Params) < currentArg+1 {
139
+		client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, "Not enough parameters")
140
+		return false
141
+	}
142
+	mask := strings.ToLower(msg.Params[currentArg])
143
+	currentArg++
144
+
145
+	// check mask
146
+	if !strings.Contains(mask, "!") && !strings.Contains(mask, "@") {
147
+		mask = mask + "!*@*"
148
+	} else if !strings.Contains(mask, "@") {
149
+		mask = mask + "@*"
150
+	}
151
+
152
+	matcher := ircmatch.MakeMatch(mask)
153
+
154
+	for _, clientMask := range client.AllNickmasks() {
155
+		if !klineMyself && matcher.Match(clientMask) {
156
+			client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "This ban matches you. To KLINE yourself, you must use the command:  /KLINE MYSELF <arguments>")
157
+			return false
158
+		}
159
+	}
160
+
161
+	// check remote
162
+	if len(msg.Params) > currentArg && msg.Params[currentArg] == "ON" {
163
+		client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "Remote servers not yet supported")
164
+		return false
165
+	}
166
+
167
+	// get comment(s)
168
+	reason := "No reason given"
169
+	operReason := "No reason given"
170
+	if len(msg.Params) > currentArg {
171
+		tempReason := strings.TrimSpace(msg.Params[currentArg])
172
+		if len(tempReason) > 0 && tempReason != "|" {
173
+			tempReasons := strings.SplitN(tempReason, "|", 2)
174
+			if tempReasons[0] != "" {
175
+				reason = tempReasons[0]
176
+			}
177
+			if len(tempReasons) > 1 && tempReasons[1] != "" {
178
+				operReason = tempReasons[1]
179
+			} else {
180
+				operReason = reason
181
+			}
182
+		}
183
+	}
184
+
185
+	// assemble ban info
186
+	var banTime *IPRestrictTime
187
+	if durationIsUsed {
188
+		banTime = &IPRestrictTime{
189
+			Duration: duration,
190
+			Expires:  time.Now().Add(duration),
191
+		}
192
+	}
193
+
194
+	info := IPBanInfo{
195
+		Reason:     reason,
196
+		OperReason: operReason,
197
+		Time:       banTime,
198
+	}
199
+
200
+	// save in datastore
201
+	err = server.store.Update(func(tx *buntdb.Tx) error {
202
+		klineKey := fmt.Sprintf(keyKlineEntry, mask)
203
+
204
+		// assemble json from ban info
205
+		b, err := json.Marshal(info)
206
+		if err != nil {
207
+			return err
208
+		}
209
+
210
+		tx.Set(klineKey, string(b), nil)
211
+
212
+		return nil
213
+	})
214
+
215
+	server.klines.AddMask(mask, banTime, reason, operReason)
216
+
217
+	if durationIsUsed {
218
+		client.Notice(fmt.Sprintf("Added temporary (%s) K-Line for %s", duration.String(), mask))
219
+	} else {
220
+		client.Notice(fmt.Sprintf("Added K-Line for %s", mask))
221
+	}
222
+
223
+	return false
224
+}
225
+
226
+func unKLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
227
+	// check oper permissions
228
+	if !client.class.Capabilities["oper:local_unban"] {
229
+		client.Send(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, "Insufficient oper privs")
230
+		return false
231
+	}
232
+
233
+	// get host
234
+	mask := msg.Params[0]
235
+
236
+	if !strings.Contains(mask, "!") && !strings.Contains(mask, "@") {
237
+		mask = mask + "!*@*"
238
+	} else if !strings.Contains(mask, "@") {
239
+		mask = mask + "@*"
240
+	}
241
+
242
+	// save in datastore
243
+	err := server.store.Update(func(tx *buntdb.Tx) error {
244
+		klineKey := fmt.Sprintf(keyKlineEntry, mask)
245
+
246
+		// check if it exists or not
247
+		val, err := tx.Get(klineKey)
248
+		if val == "" {
249
+			return errNoExistingBan
250
+		} else if err != nil {
251
+			return err
252
+		}
253
+
254
+		tx.Delete(klineKey)
255
+		return nil
256
+	})
257
+
258
+	if err != nil {
259
+		client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, fmt.Sprintf("Could not remove ban [%s]", err.Error()))
260
+		return false
261
+	}
262
+
263
+	server.klines.RemoveMask(mask)
264
+
265
+	client.Notice(fmt.Sprintf("Removed K-Line for %s", mask))
266
+	return false
267
+}
268
+
269
+func (s *Server) loadKLines() {
270
+	s.klines = NewKLineManager()
271
+
272
+	// load from datastore
273
+	s.store.View(func(tx *buntdb.Tx) error {
274
+		//TODO(dan): We could make this safer
275
+		tx.AscendKeys("bans.kline *", func(key, value string) bool {
276
+			// get address name
277
+			key = key[len("bans.kline "):]
278
+			mask := key
279
+
280
+			// load ban info
281
+			var info IPBanInfo
282
+			json.Unmarshal([]byte(value), &info)
283
+
284
+			// add to the server
285
+			s.klines.AddMask(mask, info.Time, info.Reason, info.OperReason)
286
+
287
+			return true // true to continue I guess?
288
+		})
289
+		return nil
290
+	})
291
+}

+ 18
- 1
irc/server.go View File

@@ -87,6 +87,7 @@ type Server struct {
87 87
 	dlines                *DLineManager
88 88
 	idle                  chan *Client
89 89
 	isupport              *ISupportList
90
+	klines                *KLineManager
90 91
 	limits                Limits
91 92
 	listenerEventActMutex sync.Mutex
92 93
 	listeners             map[string]ListenerInterface
@@ -214,8 +215,9 @@ func NewServer(configFilename string, config *Config) *Server {
214 215
 		return nil
215 216
 	}
216 217
 
217
-	// load dlines
218
+	// load *lines
218 219
 	server.loadDLines()
220
+	server.loadKLines()
219 221
 
220 222
 	// load password manager
221 223
 	err = server.store.View(func(tx *buntdb.Tx) error {
@@ -569,6 +571,21 @@ func (server *Server) tryRegister(c *Client) {
569 571
 		(c.capState == CapNegotiating) {
570 572
 		return
571 573
 	}
574
+
575
+	// check KLINEs
576
+	isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
577
+	if isBanned {
578
+		reason := info.Reason
579
+		if info.Time != nil {
580
+			reason += fmt.Sprintf(" [%s]", info.Time.Duration.String())
581
+		}
582
+		c.Send(nil, "", "ERROR", fmt.Sprintf("You are banned from this server (%s)", reason))
583
+		c.quitMessageSent = true
584
+		c.destroy()
585
+		return
586
+	}
587
+
588
+	// continue registration
572 589
 	c.Register()
573 590
 
574 591
 	// send welcome text

Loading…
Cancel
Save