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	"strings"
10	"sync"
11	"time"
12
13	"github.com/ergochat/ergo/irc/flatip"
14	"github.com/tidwall/buntdb"
15)
16
17const (
18	keyDlineEntry = "bans.dlinev2 %s"
19)
20
21// IPBanInfo holds info about an IP/net ban.
22type IPBanInfo struct {
23	// RequireSASL indicates a "soft" ban; connections are allowed but they must SASL
24	RequireSASL bool
25	// Reason is the ban reason.
26	Reason string `json:"reason"`
27	// OperReason is an oper ban reason.
28	OperReason string `json:"oper_reason"`
29	// OperName is the oper who set the ban.
30	OperName string `json:"oper_name"`
31	// time of ban creation
32	TimeCreated time.Time
33	// duration of the ban; 0 means "permanent"
34	Duration time.Duration
35}
36
37func (info IPBanInfo) timeLeft() time.Duration {
38	return time.Until(info.TimeCreated.Add(info.Duration))
39}
40
41func (info IPBanInfo) TimeLeft() string {
42	if info.Duration == 0 {
43		return "indefinite"
44	} else {
45		return info.timeLeft().Truncate(time.Second).String()
46	}
47}
48
49// BanMessage returns the ban message.
50func (info IPBanInfo) BanMessage(message string) string {
51	reason := info.Reason
52	if reason == "" {
53		reason = "No reason given"
54	}
55	message = fmt.Sprintf(message, reason)
56	if info.Duration != 0 {
57		message += fmt.Sprintf(" [%s]", info.TimeLeft())
58	}
59	return message
60}
61
62// DLineManager manages and dlines.
63type DLineManager struct {
64	sync.RWMutex                // tier 1
65	persistenceMutex sync.Mutex // tier 2
66	// networks that are dlined:
67	networks map[flatip.IPNet]IPBanInfo
68	// this keeps track of expiration timers for temporary bans
69	expirationTimers map[flatip.IPNet]*time.Timer
70	server           *Server
71}
72
73// NewDLineManager returns a new DLineManager.
74func NewDLineManager(server *Server) *DLineManager {
75	var dm DLineManager
76	dm.networks = make(map[flatip.IPNet]IPBanInfo)
77	dm.expirationTimers = make(map[flatip.IPNet]*time.Timer)
78	dm.server = server
79
80	dm.loadFromDatastore()
81
82	return &dm
83}
84
85// AllBans returns all bans (for use with APIs, etc).
86func (dm *DLineManager) AllBans() map[string]IPBanInfo {
87	allb := make(map[string]IPBanInfo)
88
89	dm.RLock()
90	defer dm.RUnlock()
91
92	for key, info := range dm.networks {
93		allb[key.HumanReadableString()] = info
94	}
95
96	return allb
97}
98
99// AddNetwork adds a network to the blocked list.
100func (dm *DLineManager) AddNetwork(network flatip.IPNet, duration time.Duration, requireSASL bool, reason, operReason, operName string) error {
101	dm.persistenceMutex.Lock()
102	defer dm.persistenceMutex.Unlock()
103
104	// assemble ban info
105	info := IPBanInfo{
106		RequireSASL: requireSASL,
107		Reason:      reason,
108		OperReason:  operReason,
109		OperName:    operName,
110		TimeCreated: time.Now().UTC(),
111		Duration:    duration,
112	}
113
114	id := dm.addNetworkInternal(network, info)
115	return dm.persistDline(id, info)
116}
117
118func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) {
119	id = flatnet
120
121	var timeLeft time.Duration
122	if info.Duration != 0 {
123		timeLeft = info.timeLeft()
124		if timeLeft <= 0 {
125			return
126		}
127	}
128
129	dm.Lock()
130	defer dm.Unlock()
131
132	dm.networks[flatnet] = info
133
134	dm.cancelTimer(flatnet)
135
136	if info.Duration == 0 {
137		return
138	}
139
140	// set up new expiration timer
141	timeCreated := info.TimeCreated
142	processExpiration := func() {
143		dm.Lock()
144		defer dm.Unlock()
145
146		banInfo, ok := dm.networks[flatnet]
147		if ok && banInfo.TimeCreated.Equal(timeCreated) {
148			delete(dm.networks, flatnet)
149			// TODO(slingamn) here's where we'd remove it from the radix tree
150			delete(dm.expirationTimers, flatnet)
151		}
152	}
153	dm.expirationTimers[flatnet] = time.AfterFunc(timeLeft, processExpiration)
154
155	return
156}
157
158func (dm *DLineManager) cancelTimer(flatnet flatip.IPNet) {
159	oldTimer := dm.expirationTimers[flatnet]
160	if oldTimer != nil {
161		oldTimer.Stop()
162		delete(dm.expirationTimers, flatnet)
163	}
164}
165
166func (dm *DLineManager) persistDline(id flatip.IPNet, info IPBanInfo) error {
167	// save in datastore
168	dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
169	// assemble json from ban info
170	b, err := json.Marshal(info)
171	if err != nil {
172		dm.server.logger.Error("internal", "couldn't marshal d-line", err.Error())
173		return err
174	}
175	bstr := string(b)
176	var setOptions *buntdb.SetOptions
177	if info.Duration != 0 {
178		setOptions = &buntdb.SetOptions{Expires: true, TTL: info.Duration}
179	}
180
181	err = dm.server.store.Update(func(tx *buntdb.Tx) error {
182		_, _, err := tx.Set(dlineKey, bstr, setOptions)
183		return err
184	})
185	if err != nil {
186		dm.server.logger.Error("internal", "couldn't store d-line", err.Error())
187	}
188	return err
189}
190
191func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
192	dlineKey := fmt.Sprintf(keyDlineEntry, id.String())
193	return dm.server.store.Update(func(tx *buntdb.Tx) error {
194		_, err := tx.Delete(dlineKey)
195		return err
196	})
197}
198
199// RemoveNetwork removes a network from the blocked list.
200func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
201	dm.persistenceMutex.Lock()
202	defer dm.persistenceMutex.Unlock()
203
204	id := network
205
206	present := func() bool {
207		dm.Lock()
208		defer dm.Unlock()
209		_, ok := dm.networks[id]
210		delete(dm.networks, id)
211		dm.cancelTimer(id)
212		return ok
213	}()
214
215	if !present {
216		return errNoExistingBan
217	}
218
219	return dm.unpersistDline(id)
220}
221
222// CheckIP returns whether or not an IP address was banned, and how long it is banned for.
223func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) {
224	dm.RLock()
225	defer dm.RUnlock()
226
227	// check networks
228	// TODO(slingamn) use a radix tree as the data plane for this
229	for flatnet, info := range dm.networks {
230		if flatnet.Contains(addr) {
231			return true, info
232		}
233	}
234	// no matches!
235	return
236}
237
238func (dm *DLineManager) loadFromDatastore() {
239	dlinePrefix := fmt.Sprintf(keyDlineEntry, "")
240	dm.server.store.View(func(tx *buntdb.Tx) error {
241		tx.AscendGreaterOrEqual("", dlinePrefix, func(key, value string) bool {
242			if !strings.HasPrefix(key, dlinePrefix) {
243				return false
244			}
245
246			// get address name
247			key = strings.TrimPrefix(key, dlinePrefix)
248
249			// load addr/net
250			hostNet, err := flatip.ParseToNormalizedNet(key)
251			if err != nil {
252				dm.server.logger.Error("internal", "bad dline cidr", err.Error())
253				return true
254			}
255
256			// load ban info
257			var info IPBanInfo
258			err = json.Unmarshal([]byte(value), &info)
259			if err != nil {
260				dm.server.logger.Error("internal", "bad dline data", err.Error())
261				return true
262			}
263
264			// set opername if it isn't already set
265			if info.OperName == "" {
266				info.OperName = dm.server.name
267			}
268
269			// add to the server
270			dm.addNetworkInternal(hostNet, info)
271
272			return true
273		})
274		return nil
275	})
276}
277
278func (s *Server) loadDLines() {
279	s.dlines = NewDLineManager(s)
280}
281