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