123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- // Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
- // released under the MIT license
-
- package irc
-
- import (
- "encoding/json"
- "fmt"
- "sort"
- "strings"
- "sync"
- "time"
-
- "github.com/goshuirc/irc-go/ircfmt"
- "github.com/goshuirc/irc-go/ircmatch"
- "github.com/goshuirc/irc-go/ircmsg"
- "github.com/oragono/oragono/irc/custime"
- "github.com/oragono/oragono/irc/sno"
- "github.com/tidwall/buntdb"
- )
-
- const (
- keyKlineEntry = "bans.kline %s"
- )
-
- // KLineInfo contains the address itself and expiration time for a given network.
- type KLineInfo struct {
- // Mask that is blocked.
- Mask string
- // Matcher, to facilitate fast matching.
- Matcher ircmatch.Matcher
- // Info contains information on the ban.
- Info IPBanInfo
- }
-
- // KLineManager manages and klines.
- type KLineManager struct {
- sync.RWMutex
- // kline'd entries
- entries map[string]*KLineInfo
- }
-
- // NewKLineManager returns a new KLineManager.
- func NewKLineManager() *KLineManager {
- var km KLineManager
- km.entries = make(map[string]*KLineInfo)
- return &km
- }
-
- // AllBans returns all bans (for use with APIs, etc).
- func (km *KLineManager) AllBans() map[string]IPBanInfo {
- allb := make(map[string]IPBanInfo)
-
- km.RLock()
- defer km.RUnlock()
- for name, info := range km.entries {
- allb[name] = info.Info
- }
-
- return allb
- }
-
- // AddMask adds to the blocked list.
- func (km *KLineManager) AddMask(mask string, length *IPRestrictTime, reason string, operReason string) {
- kln := KLineInfo{
- Mask: mask,
- Matcher: ircmatch.MakeMatch(mask),
- Info: IPBanInfo{
- Time: length,
- Reason: reason,
- OperReason: operReason,
- },
- }
- km.Lock()
- km.entries[mask] = &kln
- km.Unlock()
- }
-
- // RemoveMask removes a mask from the blocked list.
- func (km *KLineManager) RemoveMask(mask string) {
- km.Lock()
- delete(km.entries, mask)
- km.Unlock()
- }
-
- // CheckMasks returns whether or not the hostmask(s) are banned, and how long they are banned for.
- func (km *KLineManager) CheckMasks(masks ...string) (isBanned bool, info *IPBanInfo) {
- doCleanup := false
- defer func() {
- // asynchronously remove expired bans
- if doCleanup {
- go func() {
- km.Lock()
- defer km.Unlock()
- for key, entry := range km.entries {
- if entry.Info.Time.IsExpired() {
- delete(km.entries, key)
- }
- }
- }()
- }
- }()
-
- km.RLock()
- defer km.RUnlock()
-
- for _, entryInfo := range km.entries {
- if entryInfo.Info.Time != nil && entryInfo.Info.Time.IsExpired() {
- doCleanup = true
- continue
- }
-
- matches := false
- for _, mask := range masks {
- if entryInfo.Matcher.Match(mask) {
- matches = true
- break
- }
- }
- if matches {
- return true, &entryInfo.Info
- }
- }
-
- // no matches!
- return false, nil
- }
-
- // KLINE [ANDKILL] [MYSELF] [duration] <mask> [ON <server>] [reason [| oper reason]]
- // KLINE LIST
- func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
- // check oper permissions
- if !client.class.Capabilities["oper:local_ban"] {
- client.Send(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, "Insufficient oper privs")
- return false
- }
-
- currentArg := 0
-
- // if they say LIST, we just list the current klines
- if len(msg.Params) == currentArg+1 && strings.ToLower(msg.Params[currentArg]) == "list" {
- bans := server.klines.AllBans()
-
- if len(bans) == 0 {
- client.Notice("No KLINEs have been set!")
- }
-
- for key, info := range bans {
- client.Notice(fmt.Sprintf("Ban - %s - %s", key, info.BanMessage("%s")))
- }
-
- return false
- }
-
- // when setting a ban, if they say "ANDKILL" we should also kill all users who match it
- var andKill bool
- if len(msg.Params) > currentArg+1 && strings.ToLower(msg.Params[currentArg]) == "andkill" {
- andKill = true
- currentArg++
- }
-
- // when setting a ban that covers the oper's current connection, we require them to say
- // "KLINE MYSELF" so that we're sure they really mean it.
- var klineMyself bool
- if len(msg.Params) > currentArg+1 && strings.ToLower(msg.Params[currentArg]) == "myself" {
- klineMyself = true
- currentArg++
- }
-
- // duration
- duration, err := custime.ParseDuration(msg.Params[currentArg])
- durationIsUsed := err == nil
- if durationIsUsed {
- currentArg++
- }
-
- // get mask
- if len(msg.Params) < currentArg+1 {
- client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, "Not enough parameters")
- return false
- }
- mask := strings.ToLower(msg.Params[currentArg])
- currentArg++
-
- // check mask
- if !strings.Contains(mask, "!") && !strings.Contains(mask, "@") {
- mask = mask + "!*@*"
- } else if !strings.Contains(mask, "@") {
- mask = mask + "@*"
- }
-
- matcher := ircmatch.MakeMatch(mask)
-
- for _, clientMask := range client.AllNickmasks() {
- if !klineMyself && matcher.Match(clientMask) {
- 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>")
- return false
- }
- }
-
- // check remote
- if len(msg.Params) > currentArg && msg.Params[currentArg] == "ON" {
- client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, "Remote servers not yet supported")
- return false
- }
-
- // get comment(s)
- reason := "No reason given"
- operReason := "No reason given"
- if len(msg.Params) > currentArg {
- tempReason := strings.TrimSpace(msg.Params[currentArg])
- if len(tempReason) > 0 && tempReason != "|" {
- tempReasons := strings.SplitN(tempReason, "|", 2)
- if tempReasons[0] != "" {
- reason = tempReasons[0]
- }
- if len(tempReasons) > 1 && tempReasons[1] != "" {
- operReason = tempReasons[1]
- } else {
- operReason = reason
- }
- }
- }
-
- // assemble ban info
- var banTime *IPRestrictTime
- if durationIsUsed {
- banTime = &IPRestrictTime{
- Duration: duration,
- Expires: time.Now().Add(duration),
- }
- }
-
- info := IPBanInfo{
- Reason: reason,
- OperReason: operReason,
- Time: banTime,
- }
-
- // save in datastore
- err = server.store.Update(func(tx *buntdb.Tx) error {
- klineKey := fmt.Sprintf(keyKlineEntry, mask)
-
- // assemble json from ban info
- b, err := json.Marshal(info)
- if err != nil {
- return err
- }
-
- tx.Set(klineKey, string(b), nil)
-
- return nil
- })
-
- if err != nil {
- client.Notice(fmt.Sprintf("Could not successfully save new K-LINE: %s", err.Error()))
- return false
- }
-
- server.klines.AddMask(mask, banTime, reason, operReason)
-
- var snoDescription string
- if durationIsUsed {
- client.Notice(fmt.Sprintf("Added temporary (%s) K-Line for %s", duration.String(), mask))
- snoDescription = fmt.Sprintf(ircfmt.Unescape("%s$r added temporary (%s) K-Line for %s"), client.nick, duration.String(), mask)
- } else {
- client.Notice(fmt.Sprintf("Added K-Line for %s", mask))
- snoDescription = fmt.Sprintf(ircfmt.Unescape("%s$r added K-Line for %s"), client.nick, mask)
- }
- server.snomasks.Send(sno.LocalXline, snoDescription)
-
- var killClient bool
- if andKill {
- var clientsToKill []*Client
- var killedClientNicks []string
-
- server.clients.ByNickMutex.RLock()
- for _, mcl := range server.clients.ByNick {
- for _, clientMask := range mcl.AllNickmasks() {
- if matcher.Match(clientMask) {
- clientsToKill = append(clientsToKill, mcl)
- killedClientNicks = append(killedClientNicks, mcl.nick)
- }
- }
- }
- server.clients.ByNickMutex.RUnlock()
-
- for _, mcl := range clientsToKill {
- mcl.exitedSnomaskSent = true
- mcl.Quit(fmt.Sprintf("You have been banned from this server (%s)", reason))
- if mcl == client {
- killClient = true
- } else {
- // if mcl == client, we kill them below
- mcl.destroy()
- }
- }
-
- // send snomask
- sort.Strings(killedClientNicks)
- server.snomasks.Send(sno.LocalKills, fmt.Sprintf(ircfmt.Unescape("%s killed %d clients with a KLINE $c[grey][$r%s$c[grey]]"), client.nick, len(killedClientNicks), strings.Join(killedClientNicks, ", ")))
- }
-
- return killClient
- }
-
- func unKLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
- // check oper permissions
- if !client.class.Capabilities["oper:local_unban"] {
- client.Send(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, "Insufficient oper privs")
- return false
- }
-
- // get host
- mask := msg.Params[0]
-
- if !strings.Contains(mask, "!") && !strings.Contains(mask, "@") {
- mask = mask + "!*@*"
- } else if !strings.Contains(mask, "@") {
- mask = mask + "@*"
- }
-
- // save in datastore
- err := server.store.Update(func(tx *buntdb.Tx) error {
- klineKey := fmt.Sprintf(keyKlineEntry, mask)
-
- // check if it exists or not
- val, err := tx.Get(klineKey)
- if val == "" {
- return errNoExistingBan
- } else if err != nil {
- return err
- }
-
- tx.Delete(klineKey)
- return nil
- })
-
- if err != nil {
- client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, fmt.Sprintf("Could not remove ban [%s]", err.Error()))
- return false
- }
-
- server.klines.RemoveMask(mask)
-
- client.Notice(fmt.Sprintf("Removed K-Line for %s", mask))
- server.snomasks.Send(sno.LocalXline, fmt.Sprintf(ircfmt.Unescape("%s$r removed K-Line for %s"), client.nick, mask))
- return false
- }
-
- func (s *Server) loadKLines() {
- s.klines = NewKLineManager()
-
- // load from datastore
- s.store.View(func(tx *buntdb.Tx) error {
- //TODO(dan): We could make this safer
- tx.AscendKeys("bans.kline *", func(key, value string) bool {
- // get address name
- key = key[len("bans.kline "):]
- mask := key
-
- // load ban info
- var info IPBanInfo
- json.Unmarshal([]byte(value), &info)
-
- // add to the server
- s.klines.AddMask(mask, info.Time, info.Reason, info.OperReason)
-
- return true // true to continue I guess?
- })
- return nil
- })
- }
|