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
 	client.nickMaskCasefolded = nickMaskCasefolded
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
 // SetNickname sets the very first nickname for the client.
382
 // SetNickname sets the very first nickname for the client.
357
 func (client *Client) SetNickname(nickname string) error {
383
 func (client *Client) SetNickname(nickname string) error {
358
 	if client.HasNick() {
384
 	if client.HasNick() {

+ 10
- 0
irc/commands.go View File

107
 		oper:      true,
107
 		oper:      true,
108
 		capabs:    []string{"oper:local_kill"}, //TODO(dan): when we have S2S, this will be checked in the command handler itself
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
 	"LIST": {
115
 	"LIST": {
111
 		handler:   listHandler,
116
 		handler:   listHandler,
112
 		minParams: 0,
117
 		minParams: 0,
210
 		minParams: 1,
215
 		minParams: 1,
211
 		oper:      true,
216
 		oper:      true,
212
 	},
217
 	},
218
+	"UNKLINE": {
219
+		handler:   unKLineHandler,
220
+		minParams: 1,
221
+		oper:      true,
222
+	},
213
 	"USER": {
223
 	"USER": {
214
 		handler:      userHandler,
224
 		handler:      userHandler,
215
 		usablePreReg: true,
225
 		usablePreReg: true,

+ 34
- 0
irc/help.go View File

157
 
157
 
158
 Removes the given user from the network, showing them the reason if it is
158
 Removes the given user from the network, showing them the reason if it is
159
 supplied.`,
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
 	"list": {
185
 	"list": {
162
 		text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]
186
 		text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]
309
 	127.0.0.1/8
333
 	127.0.0.1/8
310
 	8.8.8.8/24`,
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
 	"user": {
346
 	"user": {
313
 		text: `USER <username> 0 * <realname>
347
 		text: `USER <username> 0 * <realname>
314
 
348
 

+ 291
- 0
irc/kline.go View File

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
 	dlines                *DLineManager
87
 	dlines                *DLineManager
88
 	idle                  chan *Client
88
 	idle                  chan *Client
89
 	isupport              *ISupportList
89
 	isupport              *ISupportList
90
+	klines                *KLineManager
90
 	limits                Limits
91
 	limits                Limits
91
 	listenerEventActMutex sync.Mutex
92
 	listenerEventActMutex sync.Mutex
92
 	listeners             map[string]ListenerInterface
93
 	listeners             map[string]ListenerInterface
214
 		return nil
215
 		return nil
215
 	}
216
 	}
216
 
217
 
217
-	// load dlines
218
+	// load *lines
218
 	server.loadDLines()
219
 	server.loadDLines()
220
+	server.loadKLines()
219
 
221
 
220
 	// load password manager
222
 	// load password manager
221
 	err = server.store.View(func(tx *buntdb.Tx) error {
223
 	err = server.store.View(func(tx *buntdb.Tx) error {
569
 		(c.capState == CapNegotiating) {
571
 		(c.capState == CapNegotiating) {
570
 		return
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
 	c.Register()
589
 	c.Register()
573
 
590
 
574
 	// send welcome text
591
 	// send welcome text

Loading…
Cancel
Save