Basic implementation of KLINEs

This commit is contained in:
Daniel Oaks 2017-01-11 22:38:16 +10:00
parent 1bc08f18b7
commit 4168eaafbb
5 changed files with 379 additions and 1 deletions

View File

@ -353,6 +353,32 @@ func (client *Client) updateNickMask() {
client.nickMaskCasefolded = nickMaskCasefolded
}
// AllNickmasks returns all the possible nickmasks for the client.
func (client *Client) AllNickmasks() []string {
var masks []string
var mask string
var err error
if len(client.vhost) > 0 {
mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.vhost))
if err == nil {
masks = append(masks, mask)
}
}
mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.rawHostname))
if err == nil {
masks = append(masks, mask)
}
mask2, err := Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, IPString(client.socket.conn.RemoteAddr())))
if err == nil && mask2 != mask {
masks = append(masks, mask2)
}
return masks
}
// SetNickname sets the very first nickname for the client.
func (client *Client) SetNickname(nickname string) error {
if client.HasNick() {

View File

@ -107,6 +107,11 @@ var Commands = map[string]Command{
oper: true,
capabs: []string{"oper:local_kill"}, //TODO(dan): when we have S2S, this will be checked in the command handler itself
},
"KLINE": {
handler: klineHandler,
minParams: 1,
oper: true,
},
"LIST": {
handler: listHandler,
minParams: 0,
@ -210,6 +215,11 @@ var Commands = map[string]Command{
minParams: 1,
oper: true,
},
"UNKLINE": {
handler: unKLineHandler,
minParams: 1,
oper: true,
},
"USER": {
handler: userHandler,
usablePreReg: true,

View File

@ -157,6 +157,30 @@ channel privs.`,
Removes the given user from the network, showing them the reason if it is
supplied.`,
},
"kline": {
oper: true,
text: `KLINE [MYSELF] [duration] <mask> [ON <server>] [reason [| oper reason]]
Bans a mask from connecting to the server. If the duration is given then only for that
long. The reason is shown to the user themselves, but everyone else will see a standard
message. The oper reason is shown to operators getting info about the KLINEs that exist.
Bans are saved across subsequent launches of the server.
"MYSELF" is required when the KLINE matches the address the person applying it is connected
from. If "MYSELF" is not given, trying to KLINE yourself will result in an error.
[duration] can be of the following forms:
10h 8m 13s
<mask> is specified in typical IRC format. For example:
dan
dan!5*@127.*
ON <server> specifies that the ban is to be set on that specific server.
[reason] and [oper reason], if they exist, are separated by a vertical bar (|).`,
},
"list": {
text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}]
@ -309,6 +333,16 @@ Removes an existing ban on an IP address or a network.
127.0.0.1/8
8.8.8.8/24`,
},
"unkline": {
oper: true,
text: `UNKLINE <mask>
Removes an existing ban on a mask.
For example:
dan
dan!5*@127.*`,
},
"user": {
text: `USER <username> 0 * <realname>

291
irc/kline.go Normal file
View File

@ -0,0 +1,291 @@
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/DanielOaks/girc-go/ircmatch"
"github.com/DanielOaks/girc-go/ircmsg"
"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 {
// 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)
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.entries[mask] = &kln
}
// RemoveMask removes a mask from the blocked list.
func (km *KLineManager) RemoveMask(mask string) {
delete(km.entries, mask)
}
// 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) {
// check networks
var masksToRemove []string
for _, entryInfo := range km.entries {
var matches bool
for _, mask := range masks {
if entryInfo.Matcher.Match(mask) {
matches = true
break
}
}
if !matches {
continue
}
if entryInfo.Info.Time != nil {
if entryInfo.Info.Time.IsExpired() {
// ban on network has expired, remove it from our blocked list
masksToRemove = append(masksToRemove, entryInfo.Mask)
} else {
return true, &entryInfo.Info
}
} else {
return true, &entryInfo.Info
}
}
// remove expired networks
for _, expiredMask := range masksToRemove {
km.RemoveMask(expiredMask)
}
// no matches!
return false, nil
}
// KLINE [MYSELF] [duration] <mask> [ON <server>] [reason [| oper reason]]
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
// 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 := time.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
})
server.klines.AddMask(mask, banTime, reason, operReason)
if durationIsUsed {
client.Notice(fmt.Sprintf("Added temporary (%s) K-Line for %s", duration.String(), mask))
} else {
client.Notice(fmt.Sprintf("Added K-Line for %s", mask))
}
return false
}
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))
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
})
}

View File

@ -87,6 +87,7 @@ type Server struct {
dlines *DLineManager
idle chan *Client
isupport *ISupportList
klines *KLineManager
limits Limits
listenerEventActMutex sync.Mutex
listeners map[string]ListenerInterface
@ -214,8 +215,9 @@ func NewServer(configFilename string, config *Config) *Server {
return nil
}
// load dlines
// load *lines
server.loadDLines()
server.loadKLines()
// load password manager
err = server.store.View(func(tx *buntdb.Tx) error {
@ -569,6 +571,21 @@ func (server *Server) tryRegister(c *Client) {
(c.capState == CapNegotiating) {
return
}
// check KLINEs
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
if isBanned {
reason := info.Reason
if info.Time != nil {
reason += fmt.Sprintf(" [%s]", info.Time.Duration.String())
}
c.Send(nil, "", "ERROR", fmt.Sprintf("You are banned from this server (%s)", reason))
c.quitMessageSent = true
c.destroy()
return
}
// continue registration
c.Register()
// send welcome text