1// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2// released under the MIT license
3
4package irc
5
6import (
7	"encoding/json"
8	"fmt"
9	"regexp"
10	"strings"
11	"sync"
12	"time"
13
14	"github.com/tidwall/buntdb"
15
16	"github.com/ergochat/ergo/irc/utils"
17)
18
19const (
20	keyKlineEntry = "bans.klinev2 %s"
21)
22
23// KLineInfo contains the address itself and expiration time for a given network.
24type KLineInfo struct {
25	// Mask that is blocked.
26	Mask string
27	// Matcher, to facilitate fast matching.
28	Matcher *regexp.Regexp
29	// Info contains information on the ban.
30	Info IPBanInfo
31}
32
33// KLineManager manages and klines.
34type KLineManager struct {
35	sync.RWMutex                // tier 1
36	persistenceMutex sync.Mutex // tier 2
37	// kline'd entries
38	entries          map[string]KLineInfo
39	expirationTimers map[string]*time.Timer
40	server           *Server
41}
42
43// NewKLineManager returns a new KLineManager.
44func NewKLineManager(s *Server) *KLineManager {
45	var km KLineManager
46	km.entries = make(map[string]KLineInfo)
47	km.expirationTimers = make(map[string]*time.Timer)
48	km.server = s
49
50	km.loadFromDatastore()
51
52	return &km
53}
54
55// AllBans returns all bans (for use with APIs, etc).
56func (km *KLineManager) AllBans() map[string]IPBanInfo {
57	allb := make(map[string]IPBanInfo)
58
59	km.RLock()
60	defer km.RUnlock()
61	for name, info := range km.entries {
62		allb[name] = info.Info
63	}
64
65	return allb
66}
67
68// AddMask adds to the blocked list.
69func (km *KLineManager) AddMask(mask string, duration time.Duration, reason, operReason, operName string) error {
70	km.persistenceMutex.Lock()
71	defer km.persistenceMutex.Unlock()
72
73	info := IPBanInfo{
74		Reason:      reason,
75		OperReason:  operReason,
76		OperName:    operName,
77		TimeCreated: time.Now().UTC(),
78		Duration:    duration,
79	}
80	km.addMaskInternal(mask, info)
81	return km.persistKLine(mask, info)
82}
83
84func (km *KLineManager) addMaskInternal(mask string, info IPBanInfo) {
85	re, err := utils.CompileGlob(mask, false)
86	// this is validated externally and shouldn't fail regardless
87	if err != nil {
88		return
89	}
90	kln := KLineInfo{
91		Mask:    mask,
92		Matcher: re,
93		Info:    info,
94	}
95
96	var timeLeft time.Duration
97	if info.Duration > 0 {
98		timeLeft = info.timeLeft()
99		if timeLeft <= 0 {
100			return
101		}
102	}
103
104	km.Lock()
105	defer km.Unlock()
106
107	km.entries[mask] = kln
108	km.cancelTimer(mask)
109
110	if info.Duration == 0 {
111		return
112	}
113
114	// set up new expiration timer
115	timeCreated := info.TimeCreated
116	processExpiration := func() {
117		km.Lock()
118		defer km.Unlock()
119
120		maskBan, ok := km.entries[mask]
121		if ok && maskBan.Info.TimeCreated.Equal(timeCreated) {
122			delete(km.entries, mask)
123			delete(km.expirationTimers, mask)
124		}
125	}
126	km.expirationTimers[mask] = time.AfterFunc(timeLeft, processExpiration)
127}
128
129func (km *KLineManager) cancelTimer(id string) {
130	oldTimer := km.expirationTimers[id]
131	if oldTimer != nil {
132		oldTimer.Stop()
133		delete(km.expirationTimers, id)
134	}
135}
136
137func (km *KLineManager) persistKLine(mask string, info IPBanInfo) error {
138	// save in datastore
139	klineKey := fmt.Sprintf(keyKlineEntry, mask)
140	// assemble json from ban info
141	b, err := json.Marshal(info)
142	if err != nil {
143		return err
144	}
145	bstr := string(b)
146	var setOptions *buntdb.SetOptions
147	if info.Duration != 0 {
148		setOptions = &buntdb.SetOptions{Expires: true, TTL: info.Duration}
149	}
150
151	err = km.server.store.Update(func(tx *buntdb.Tx) error {
152		_, _, err := tx.Set(klineKey, bstr, setOptions)
153		return err
154	})
155
156	return err
157
158}
159
160func (km *KLineManager) unpersistKLine(mask string) error {
161	// save in datastore
162	klineKey := fmt.Sprintf(keyKlineEntry, mask)
163	return km.server.store.Update(func(tx *buntdb.Tx) error {
164		_, err := tx.Delete(klineKey)
165		return err
166	})
167}
168
169// RemoveMask removes a mask from the blocked list.
170func (km *KLineManager) RemoveMask(mask string) error {
171	km.persistenceMutex.Lock()
172	defer km.persistenceMutex.Unlock()
173
174	present := func() bool {
175		km.Lock()
176		defer km.Unlock()
177		_, ok := km.entries[mask]
178		if ok {
179			delete(km.entries, mask)
180		}
181		km.cancelTimer(mask)
182		return ok
183	}()
184
185	if !present {
186		return errNoExistingBan
187	}
188
189	return km.unpersistKLine(mask)
190}
191
192func (km *KLineManager) ContainsMask(mask string) (isBanned bool, info IPBanInfo) {
193	km.RLock()
194	defer km.RUnlock()
195
196	klineInfo, isBanned := km.entries[mask]
197	if isBanned {
198		info = klineInfo.Info
199	}
200	return
201}
202
203// CheckMasks returns whether or not the hostmask(s) are banned, and how long they are banned for.
204func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info IPBanInfo) {
205	km.RLock()
206	defer km.RUnlock()
207
208	for _, entryInfo := range km.entries {
209		for _, mask := range masks {
210			if entryInfo.Matcher.MatchString(mask) {
211				return true, entryInfo.Info
212			}
213		}
214	}
215
216	// no matches!
217	isBanned = false
218	return
219}
220
221func (km *KLineManager) loadFromDatastore() {
222	// load from datastore
223	klinePrefix := fmt.Sprintf(keyKlineEntry, "")
224	km.server.store.View(func(tx *buntdb.Tx) error {
225		tx.AscendGreaterOrEqual("", klinePrefix, func(key, value string) bool {
226			if !strings.HasPrefix(key, klinePrefix) {
227				return false
228			}
229
230			// get address name
231			mask := strings.TrimPrefix(key, klinePrefix)
232
233			// load ban info
234			var info IPBanInfo
235			err := json.Unmarshal([]byte(value), &info)
236			if err != nil {
237				km.server.logger.Error("internal", "couldn't unmarshal kline", err.Error())
238				return true
239			}
240
241			// add oper name if it doesn't exist already
242			if info.OperName == "" {
243				info.OperName = km.server.name
244			}
245
246			// add to the server
247			km.addMaskInternal(mask, info)
248
249			return true
250		})
251		return nil
252	})
253
254}
255
256func (s *Server) loadKLines() {
257	s.klines = NewKLineManager(s)
258}
259