mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 05:19:25 +01:00
initial UBAN implementation
This commit is contained in:
parent
64bc363cf1
commit
bb5276553d
@ -175,6 +175,17 @@ SET modifies a channel's settings. The following settings are available:`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 3,
|
||||
},
|
||||
"howtoban": {
|
||||
handler: csHowToBanHandler,
|
||||
helpShort: `$bHOWTOBAN$b suggests the best available way of banning a user`,
|
||||
help: `Syntax: $bHOWTOBAN #channel <nick>
|
||||
|
||||
The best way to ban a user from a channel will depend on how they are
|
||||
connected to the server. $bHOWTOBAN$b suggests a ban command that will
|
||||
(ideally) prevent the user from returning to the channel.`,
|
||||
enabled: chanregEnabled,
|
||||
minParams: 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@ -809,3 +820,83 @@ func csSetHandler(service *ircService, server *Server, client *Client, command s
|
||||
service.Notice(rb, client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
|
||||
func csHowToBanHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
success := false
|
||||
defer func() {
|
||||
if success {
|
||||
service.Notice(rb, client.t("Note that if the user is currently in the channel, you must /KICK them after you ban them"))
|
||||
}
|
||||
}()
|
||||
|
||||
chname, nick := params[0], params[1]
|
||||
channel := server.channels.Get(chname)
|
||||
if channel == nil {
|
||||
service.Notice(rb, client.t("No such channel"))
|
||||
return
|
||||
}
|
||||
|
||||
if !channel.ClientIsAtLeast(client, modes.Operator) || client.HasRoleCapabs("samode") {
|
||||
service.Notice(rb, client.t("Insufficient privileges"))
|
||||
return
|
||||
}
|
||||
|
||||
var details WhoWas
|
||||
target := server.clients.Get(nick)
|
||||
if target == nil {
|
||||
whowasList := server.whoWas.Find(nick, 1)
|
||||
if len(whowasList) == 0 {
|
||||
service.Notice(rb, client.t("No such nick"))
|
||||
return
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: %s is not currently connected to the server. Using WHOWAS data, which may be inaccurate:"), nick))
|
||||
details = whowasList[0]
|
||||
} else {
|
||||
details = target.Details().WhoWas
|
||||
}
|
||||
|
||||
if details.account != "" {
|
||||
if channel.getAmode(details.account) != modes.Mode(0) {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s currently has a persistent channel privilege granted with CS AMODE. If this mode is not removed, bans will not be respected"), details.accountName))
|
||||
return
|
||||
} else if details.account == channel.Founder() {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: account %s is the channel founder and cannot be banned"), details.accountName))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
config := server.Config()
|
||||
if !config.Server.Cloaks.EnabledForAlwaysOn {
|
||||
service.Notice(rb, client.t("Warning: server.ip-cloaking.enabled-for-always-on is disabled. This reduces the precision of channel bans."))
|
||||
}
|
||||
|
||||
if details.account != "" {
|
||||
if config.Accounts.NickReservation.ForceNickEqualsAccount || target.AlwaysOn() {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s is authenticated and can be banned by nickname: /MODE %[2]s +b %[3]s!*@*"), details.nick, channel.Name(), details.nick))
|
||||
success = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ban := fmt.Sprintf("*!*@%s", strings.ToLower(details.hostname))
|
||||
banRe, err := utils.CompileGlob(ban, false)
|
||||
if err != nil {
|
||||
server.logger.Error("internal", "couldn't compile ban regex", ban, err.Error())
|
||||
service.Notice(rb, "An error occurred")
|
||||
return
|
||||
}
|
||||
var collateralDamage []string
|
||||
for _, mcl := range channel.Members() {
|
||||
if mcl != target && banRe.MatchString(mcl.NickMaskCasefolded()) {
|
||||
collateralDamage = append(collateralDamage, mcl.Nick())
|
||||
}
|
||||
}
|
||||
service.Notice(rb, fmt.Sprintf(client.t("User %[1]s can be banned by hostname: /MODE %[2]s +b %[3]s"), details.nick, channel.Name(), ban))
|
||||
success = true
|
||||
if len(collateralDamage) != 0 {
|
||||
service.Notice(rb, fmt.Sprintf(client.t("Warning: this ban will affect %d other users:"), len(collateralDamage)))
|
||||
for _, line := range utils.BuildTokenLines(400, collateralDamage, " ") {
|
||||
service.Notice(rb, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -292,6 +292,9 @@ type WhoWas struct {
|
||||
username string
|
||||
hostname string
|
||||
realname string
|
||||
// technically not required for WHOWAS:
|
||||
account string
|
||||
accountName string
|
||||
}
|
||||
|
||||
// ClientDetails is a standard set of details about a client
|
||||
@ -300,8 +303,6 @@ type ClientDetails struct {
|
||||
|
||||
nickMask string
|
||||
nickMaskCasefolded string
|
||||
account string
|
||||
accountName string
|
||||
}
|
||||
|
||||
// RunClient sets up a new client and runs its goroutine.
|
||||
|
@ -319,6 +319,11 @@ func init() {
|
||||
handler: topicHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"UBAN": {
|
||||
handler: ubanHandler,
|
||||
minParams: 1,
|
||||
capabs: []string{"ban"},
|
||||
},
|
||||
"UNDLINE": {
|
||||
handler: unDLineHandler,
|
||||
minParams: 1,
|
||||
|
@ -209,6 +209,38 @@ func (cl *Limiter) RemoveClient(addr flatip.IP) {
|
||||
cl.limiter[addrString] = count
|
||||
}
|
||||
|
||||
type LimiterStatus struct {
|
||||
Exempt bool
|
||||
|
||||
Count int
|
||||
MaxCount int
|
||||
|
||||
Throttle int
|
||||
MaxPerWindow int
|
||||
ThrottleDuration time.Duration
|
||||
}
|
||||
|
||||
func (cl *Limiter) Status(addr flatip.IP) (status LimiterStatus) {
|
||||
cl.Lock()
|
||||
defer cl.Unlock()
|
||||
|
||||
if flatip.IPInNets(addr, cl.config.exemptedNets) {
|
||||
status.Exempt = true
|
||||
return
|
||||
}
|
||||
|
||||
status.ThrottleDuration = cl.config.Window
|
||||
|
||||
addrString, maxConcurrent, maxPerWindow := cl.addrToKey(addr)
|
||||
status.MaxCount = maxConcurrent
|
||||
status.MaxPerWindow = maxPerWindow
|
||||
|
||||
status.Count = cl.limiter[addrString]
|
||||
status.Throttle = cl.throttler[addrString].Count
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ResetThrottle resets the throttle count for an IP
|
||||
func (cl *Limiter) ResetThrottle(addr flatip.IP) {
|
||||
cl.Lock()
|
||||
|
35
irc/dline.go
35
irc/dline.go
@ -6,13 +6,11 @@ package irc
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/flatip"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
@ -48,7 +46,11 @@ func (info IPBanInfo) TimeLeft() string {
|
||||
|
||||
// BanMessage returns the ban message.
|
||||
func (info IPBanInfo) BanMessage(message string) string {
|
||||
message = fmt.Sprintf(message, info.Reason)
|
||||
reason := info.Reason
|
||||
if reason == "" {
|
||||
reason = "No reason given"
|
||||
}
|
||||
message = fmt.Sprintf(message, reason)
|
||||
if info.Duration != 0 {
|
||||
message += fmt.Sprintf(" [%s]", info.TimeLeft())
|
||||
}
|
||||
@ -86,14 +88,14 @@ func (dm *DLineManager) AllBans() map[string]IPBanInfo {
|
||||
defer dm.RUnlock()
|
||||
|
||||
for key, info := range dm.networks {
|
||||
allb[key.String()] = info
|
||||
allb[key.HumanReadableString()] = info
|
||||
}
|
||||
|
||||
return allb
|
||||
}
|
||||
|
||||
// AddNetwork adds a network to the blocked list.
|
||||
func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, reason, operReason, operName string) error {
|
||||
func (dm *DLineManager) AddNetwork(network flatip.IPNet, duration time.Duration, reason, operReason, operName string) error {
|
||||
dm.persistenceMutex.Lock()
|
||||
defer dm.persistenceMutex.Unlock()
|
||||
|
||||
@ -110,8 +112,7 @@ func (dm *DLineManager) AddNetwork(network net.IPNet, duration time.Duration, re
|
||||
return dm.persistDline(id, info)
|
||||
}
|
||||
|
||||
func (dm *DLineManager) addNetworkInternal(network net.IPNet, info IPBanInfo) (id flatip.IPNet) {
|
||||
flatnet := flatip.FromNetIPNet(network)
|
||||
func (dm *DLineManager) addNetworkInternal(flatnet flatip.IPNet, info IPBanInfo) (id flatip.IPNet) {
|
||||
id = flatnet
|
||||
|
||||
var timeLeft time.Duration
|
||||
@ -193,11 +194,11 @@ func (dm *DLineManager) unpersistDline(id flatip.IPNet) error {
|
||||
}
|
||||
|
||||
// RemoveNetwork removes a network from the blocked list.
|
||||
func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
|
||||
func (dm *DLineManager) RemoveNetwork(network flatip.IPNet) error {
|
||||
dm.persistenceMutex.Lock()
|
||||
defer dm.persistenceMutex.Unlock()
|
||||
|
||||
id := flatip.FromNetIPNet(network)
|
||||
id := network
|
||||
|
||||
present := func() bool {
|
||||
dm.Lock()
|
||||
@ -215,22 +216,8 @@ func (dm *DLineManager) RemoveNetwork(network net.IPNet) error {
|
||||
return dm.unpersistDline(id)
|
||||
}
|
||||
|
||||
// AddIP adds an IP address to the blocked list.
|
||||
func (dm *DLineManager) AddIP(addr net.IP, duration time.Duration, reason, operReason, operName string) error {
|
||||
return dm.AddNetwork(utils.NormalizeIPToNet(addr), duration, reason, operReason, operName)
|
||||
}
|
||||
|
||||
// RemoveIP removes an IP address from the blocked list.
|
||||
func (dm *DLineManager) RemoveIP(addr net.IP) error {
|
||||
return dm.RemoveNetwork(utils.NormalizeIPToNet(addr))
|
||||
}
|
||||
|
||||
// CheckIP returns whether or not an IP address was banned, and how long it is banned for.
|
||||
func (dm *DLineManager) CheckIP(addr flatip.IP) (isBanned bool, info IPBanInfo) {
|
||||
if addr.IsLoopback() {
|
||||
return // #671
|
||||
}
|
||||
|
||||
dm.RLock()
|
||||
defer dm.RUnlock()
|
||||
|
||||
@ -257,7 +244,7 @@ func (dm *DLineManager) loadFromDatastore() {
|
||||
key = strings.TrimPrefix(key, dlinePrefix)
|
||||
|
||||
// load addr/net
|
||||
hostNet, err := utils.NormalizedNetFromString(key)
|
||||
hostNet, err := flatip.ParseToNormalizedNet(key)
|
||||
if err != nil {
|
||||
dm.server.logger.Error("internal", "bad dline cidr", err.Error())
|
||||
return true
|
||||
|
@ -183,6 +183,16 @@ func (cidr IPNet) String() string {
|
||||
return ipnet.String()
|
||||
}
|
||||
|
||||
// HumanReadableString returns a string representation of an IPNet;
|
||||
// if the network contains only a single IP address, it returns
|
||||
// a representation of that address.
|
||||
func (cidr IPNet) HumanReadableString() string {
|
||||
if cidr.PrefixLen == 128 {
|
||||
return cidr.IP.String()
|
||||
}
|
||||
return cidr.String()
|
||||
}
|
||||
|
||||
// IsZero tests whether ipnet is the zero value of an IPNet, 0::0/0.
|
||||
// Although this is a valid subnet, it can still be used as a sentinel
|
||||
// value in some contexts.
|
||||
|
@ -314,6 +314,12 @@ func (client *Client) setCloakedHostname(cloak string) {
|
||||
client.updateNickMaskNoMutex()
|
||||
}
|
||||
|
||||
func (client *Client) CloakedHostname() string {
|
||||
client.stateMutex.Lock()
|
||||
defer client.stateMutex.Unlock()
|
||||
return client.cloakedHostname
|
||||
}
|
||||
|
||||
func (client *Client) historyCutoff() (cutoff time.Time) {
|
||||
client.stateMutex.Lock()
|
||||
if client.account != "" {
|
||||
@ -553,3 +559,9 @@ func (channel *Channel) Ctime() (ctime time.Time) {
|
||||
channel.stateMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (channel *Channel) getAmode(cfaccount string) (result modes.Mode) {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
return channel.accountToUMode[cfaccount]
|
||||
}
|
||||
|
@ -906,7 +906,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
operName = server.name
|
||||
}
|
||||
|
||||
err = server.dlines.AddNetwork(hostNet, duration, reason, operReason, operName)
|
||||
err = server.dlines.AddNetwork(flatip.FromNetIPNet(hostNet), duration, reason, operReason, operName)
|
||||
|
||||
if err != nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("Could not successfully save new D-LINE: %s"), err.Error()))
|
||||
@ -2833,13 +2833,8 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
// get host
|
||||
hostString := msg.Params[0]
|
||||
|
||||
// TODO(#1447) consolidate this into the "unban" command
|
||||
if flatip, ipErr := flatip.ParseIP(hostString); ipErr == nil {
|
||||
server.connectionLimiter.ResetThrottle(flatip)
|
||||
}
|
||||
|
||||
// check host
|
||||
hostNet, err := utils.NormalizedNetFromString(hostString)
|
||||
hostNet, err := flatip.ParseToNormalizedNet(hostString)
|
||||
|
||||
if err != nil {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, client.nick, msg.Command, client.t("Could not parse IP address or CIDR network"))
|
||||
@ -2853,7 +2848,7 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
return false
|
||||
}
|
||||
|
||||
hostString = utils.NetToNormalizedString(hostNet)
|
||||
hostString = hostNet.String()
|
||||
rb.Notice(fmt.Sprintf(client.t("Removed D-Line for %s"), hostString))
|
||||
server.snomasks.Send(sno.LocalXline, fmt.Sprintf(ircfmt.Unescape("%s$r removed D-Line for %s"), client.nick, hostString))
|
||||
return false
|
||||
|
13
irc/help.go
13
irc/help.go
@ -520,6 +520,19 @@ Shows the time of the current, or the given, server.`,
|
||||
|
||||
If [topic] is given, sets the topic in the channel to that. If [topic] is not
|
||||
given, views the current topic on the channel.`,
|
||||
},
|
||||
"uban": {
|
||||
text: `UBAN <subcommand> [arguments]
|
||||
|
||||
Oragono's "unified ban" system. Accepts the following subcommands:
|
||||
|
||||
1. UBAN ADD <target> [DURATION <duration>] [REASON...]
|
||||
2. UBAN DEL <target>
|
||||
3. UBAN LIST
|
||||
4. UBAN INFO <target>
|
||||
|
||||
<target> may be an IP, a CIDR, a nickmask with wildcards, or the name of an
|
||||
account to suspend.`,
|
||||
},
|
||||
"undline": {
|
||||
oper: true,
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright (c) 2020 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
|
11
irc/kline.go
11
irc/kline.go
@ -189,6 +189,17 @@ func (km *KLineManager) RemoveMask(mask string) error {
|
||||
return km.unpersistKLine(mask)
|
||||
}
|
||||
|
||||
func (km *KLineManager) ContainsMask(mask string) (isBanned bool, info IPBanInfo) {
|
||||
km.RLock()
|
||||
defer km.RUnlock()
|
||||
|
||||
klineInfo, isBanned := km.entries[mask]
|
||||
if isBanned {
|
||||
info = klineInfo.Info
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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) {
|
||||
km.RLock()
|
||||
|
@ -1361,11 +1361,16 @@ func (a ByCreationTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) }
|
||||
|
||||
func nsSuspendListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
|
||||
suspensions := server.accounts.ListSuspended()
|
||||
listAccountSuspensions(client, rb, service.prefix)
|
||||
}
|
||||
|
||||
func listAccountSuspensions(client *Client, rb *ResponseBuffer, source string) {
|
||||
suspensions := client.server.accounts.ListSuspended()
|
||||
sort.Sort(ByCreationTime(suspensions))
|
||||
service.Notice(rb, fmt.Sprintf(client.t("There are %d active suspensions."), len(suspensions)))
|
||||
nick := client.Nick()
|
||||
rb.Add(nil, source, "NOTICE", nick, fmt.Sprintf(client.t("There are %d active account suspensions."), len(suspensions)))
|
||||
for _, suspension := range suspensions {
|
||||
service.Notice(rb, suspensionToString(client, suspension))
|
||||
rb.Add(nil, source, "NOTICE", nick, suspensionToString(client, suspension))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,8 +162,14 @@ func (server *Server) Run() {
|
||||
}
|
||||
|
||||
func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool) (banned bool, requireSASL bool, message string) {
|
||||
// #671: do not enforce bans against loopback, as a failsafe
|
||||
// note that this function is not used for Tor connections (checkTorLimits is used instead)
|
||||
if ipaddr.IsLoopback() {
|
||||
return
|
||||
}
|
||||
|
||||
if server.Defcon() == 1 {
|
||||
if !(ipaddr.IsLoopback() || utils.IPInNets(ipaddr, server.Config().Server.secureNets)) {
|
||||
if !utils.IPInNets(ipaddr, server.Config().Server.secureNets) {
|
||||
return true, false, "New connections to this server are temporarily restricted"
|
||||
}
|
||||
}
|
||||
@ -198,7 +204,7 @@ func (server *Server) checkBans(config *Config, ipaddr net.IP, checkScripts bool
|
||||
}
|
||||
// TODO: currently no way to cache results other than IPBanned
|
||||
if output.Result == IPBanned && output.CacheSeconds != 0 {
|
||||
network, err := utils.NormalizedNetFromString(output.CacheNet)
|
||||
network, err := flatip.ParseToNormalizedNet(output.CacheNet)
|
||||
if err != nil {
|
||||
server.logger.Error("internal", "invalid dline net from IP ban script", ipaddr.String(), output.CacheNet)
|
||||
} else {
|
||||
@ -339,12 +345,14 @@ func (server *Server) tryRegister(c *Client, session *Session) (exiting bool) {
|
||||
// count new user in statistics (before checking KLINEs, see #1303)
|
||||
server.stats.Register(c.HasMode(modes.Invisible))
|
||||
|
||||
// check KLINEs
|
||||
// check KLINEs (#671: ignore KLINEs for loopback connections)
|
||||
if !session.IP().IsLoopback() || session.isTor {
|
||||
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
|
||||
if isBanned {
|
||||
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
server.playRegistrationBurst(session)
|
||||
return false
|
||||
|
381
irc/uban.go
Normal file
381
irc/uban.go
Normal file
@ -0,0 +1,381 @@
|
||||
// Copyright (c) 2021 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/goshuirc/irc-go/ircmsg"
|
||||
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/flatip"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
)
|
||||
|
||||
func consumeDuration(params []string, rb *ResponseBuffer) (duration time.Duration, remainingParams []string, err error) {
|
||||
remainingParams = params
|
||||
if 2 <= len(remainingParams) && strings.ToLower(remainingParams[0]) == "duration" {
|
||||
duration, err = custime.ParseDuration(remainingParams[1])
|
||||
if err != nil {
|
||||
rb.Notice(rb.session.client.t("Invalid time duration for NS SUSPEND"))
|
||||
return
|
||||
}
|
||||
remainingParams = remainingParams[2:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// a UBAN target is one of these syntactically unambiguous entities:
|
||||
// an IP, a CIDR, a NUH mask, or an account name
|
||||
type ubanType uint
|
||||
|
||||
const (
|
||||
ubanCIDR ubanType = iota
|
||||
ubanNickmask
|
||||
ubanNick
|
||||
)
|
||||
|
||||
// tagged union, i guess
|
||||
type ubanTarget struct {
|
||||
banType ubanType
|
||||
|
||||
cidr flatip.IPNet
|
||||
matcher *regexp.Regexp
|
||||
nickOrMask string
|
||||
}
|
||||
|
||||
func parseUbanTarget(param string) (target ubanTarget, err error) {
|
||||
if utils.SafeErrorParam(param) == "*" {
|
||||
err = errInvalidParams
|
||||
return
|
||||
}
|
||||
|
||||
ipnet, ipErr := flatip.ParseToNormalizedNet(param)
|
||||
if ipErr == nil {
|
||||
target.banType = ubanCIDR
|
||||
target.cidr = ipnet
|
||||
return
|
||||
}
|
||||
|
||||
if strings.IndexByte(param, '!') != -1 || strings.IndexByte(param, '@') != -1 {
|
||||
canonicalized, cErr := CanonicalizeMaskWildcard(param)
|
||||
if cErr != nil {
|
||||
err = errInvalidParams
|
||||
return
|
||||
}
|
||||
re, reErr := utils.CompileGlob(canonicalized, false)
|
||||
if reErr != nil {
|
||||
err = errInvalidParams
|
||||
return
|
||||
}
|
||||
target.banType = ubanNickmask
|
||||
target.nickOrMask = canonicalized
|
||||
target.matcher = re
|
||||
return
|
||||
}
|
||||
|
||||
if _, cErr := CasefoldName(param); cErr == nil {
|
||||
target.banType = ubanNick
|
||||
target.nickOrMask = param
|
||||
return
|
||||
}
|
||||
|
||||
err = errInvalidParams
|
||||
return
|
||||
}
|
||||
|
||||
// UBAN <subcommand> [target] [DURATION <duration>] [reason...]
|
||||
func ubanHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
subcommand := strings.ToLower(msg.Params[0])
|
||||
params := msg.Params[1:]
|
||||
var target ubanTarget
|
||||
if subcommand != "list" {
|
||||
if len(msg.Params) == 1 {
|
||||
rb.Add(nil, client.server.name, "FAIL", "UBAN", "INVALID_PARAMS", client.t("Not enough parameters"))
|
||||
return false
|
||||
}
|
||||
var parseErr error
|
||||
target, parseErr = parseUbanTarget(params[0])
|
||||
if parseErr != nil {
|
||||
rb.Add(nil, client.server.name, "FAIL", "UBAN", "INVALID_PARAMS", client.t("Couldn't parse ban target"))
|
||||
return false
|
||||
}
|
||||
params = params[1:]
|
||||
}
|
||||
|
||||
switch subcommand {
|
||||
case "add":
|
||||
return ubanAddHandler(client, target, params, rb)
|
||||
case "del", "remove", "rm":
|
||||
return ubanDelHandler(client, target, params, rb)
|
||||
case "list":
|
||||
return ubanListHandler(client, params, rb)
|
||||
case "info":
|
||||
return ubanInfoHandler(client, target, params, rb)
|
||||
default:
|
||||
rb.Add(nil, server.name, "FAIL", "UBAN", "UNKNOWN_COMMAND", client.t("Unknown command"))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func sessionsForCIDR(server *Server, cidr flatip.IPNet, exclude *Session) (sessions []*Session, nicks []string) {
|
||||
for _, client := range server.clients.AllClients() {
|
||||
for _, session := range client.Sessions() {
|
||||
seen := false
|
||||
if session != exclude && cidr.Contains(flatip.FromNetIP(session.IP())) {
|
||||
sessions = append(sessions, session)
|
||||
if !seen {
|
||||
seen = true
|
||||
nicks = append(nicks, session.client.Nick())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func ubanAddHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
|
||||
duration, params, err := consumeDuration(params, rb)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
operReason := strings.Join(params, " ")
|
||||
|
||||
switch target.banType {
|
||||
case ubanCIDR:
|
||||
ubanAddCIDR(client, target, duration, operReason, rb)
|
||||
case ubanNickmask:
|
||||
ubanAddNickmask(client, target, duration, operReason, rb)
|
||||
case ubanNick:
|
||||
account := target.nickOrMask
|
||||
err := client.server.accounts.Suspend(account, duration, client.Oper().Name, "UBAN")
|
||||
switch err {
|
||||
case nil:
|
||||
rb.Notice(fmt.Sprintf(client.t("Successfully suspended account %s"), account))
|
||||
case errAccountDoesNotExist:
|
||||
rb.Notice(client.t("No such account"))
|
||||
default:
|
||||
rb.Notice(client.t("An error occurred"))
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ubanAddCIDR(client *Client, target ubanTarget, duration time.Duration, operReason string, rb *ResponseBuffer) {
|
||||
err := client.server.dlines.AddNetwork(target.cidr, duration, "", operReason, client.Oper().Name)
|
||||
if err == nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("Successfully added UBAN for %s"), target.cidr.HumanReadableString()))
|
||||
} else {
|
||||
client.server.logger.Error("internal", "ubanAddCIDR failed", err.Error())
|
||||
rb.Notice(client.t("An error occurred"))
|
||||
return
|
||||
}
|
||||
|
||||
sessions, nicks := sessionsForCIDR(client.server, target.cidr, rb.session)
|
||||
for _, session := range sessions {
|
||||
session.client.Quit("You have been banned from this server", session)
|
||||
session.client.destroy(session)
|
||||
}
|
||||
|
||||
if len(sessions) != 0 {
|
||||
rb.Notice(fmt.Sprintf(client.t("Killed %[1]d active client(s) from %[2]s, associated with %[3]d nickname(s):"), len(sessions), target.cidr.String(), len(nicks)))
|
||||
for _, line := range utils.BuildTokenLines(400, nicks, " ") {
|
||||
rb.Notice(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ubanAddNickmask(client *Client, target ubanTarget, duration time.Duration, operReason string, rb *ResponseBuffer) {
|
||||
err := client.server.klines.AddMask(target.nickOrMask, duration, "", operReason, client.Oper().Name)
|
||||
if err == nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("Successfully added UBAN for %s"), target.nickOrMask))
|
||||
} else {
|
||||
client.server.logger.Error("internal", "ubanAddNickmask failed", err.Error())
|
||||
rb.Notice(client.t("An error occurred"))
|
||||
return
|
||||
}
|
||||
|
||||
var killed []string
|
||||
var alwaysOn []string
|
||||
for _, mcl := range client.server.clients.AllClients() {
|
||||
if mcl != client && target.matcher.MatchString(client.NickMaskCasefolded()) {
|
||||
if !mcl.AlwaysOn() {
|
||||
killed = append(killed, mcl.Nick())
|
||||
mcl.destroy(nil)
|
||||
} else {
|
||||
alwaysOn = append(alwaysOn, mcl.Nick())
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(killed) != 0 {
|
||||
rb.Notice(fmt.Sprintf(client.t("Killed %d clients:"), len(killed)))
|
||||
for _, line := range utils.BuildTokenLines(400, killed, " ") {
|
||||
rb.Notice(line)
|
||||
}
|
||||
}
|
||||
if len(alwaysOn) != 0 {
|
||||
rb.Notice(fmt.Sprintf(client.t("Warning: %d clients matched this rule, but were not killed due to being always-on:"), len(alwaysOn)))
|
||||
for _, line := range utils.BuildTokenLines(400, alwaysOn, " ") {
|
||||
rb.Notice(line)
|
||||
}
|
||||
rb.Notice(client.t("You can suspend their accounts instead; try /UBAN ADD <nickname>"))
|
||||
}
|
||||
}
|
||||
|
||||
func ubanDelHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
|
||||
var err error
|
||||
var targetString string
|
||||
switch target.banType {
|
||||
case ubanCIDR:
|
||||
if target.cidr.PrefixLen == 128 {
|
||||
client.server.connectionLimiter.ResetThrottle(target.cidr.IP)
|
||||
rb.Notice(fmt.Sprintf(client.t("Reset throttle for IP: %s"), target.cidr.IP.String()))
|
||||
}
|
||||
targetString = target.cidr.HumanReadableString()
|
||||
err = client.server.dlines.RemoveNetwork(target.cidr)
|
||||
case ubanNickmask:
|
||||
targetString = target.nickOrMask
|
||||
err = client.server.klines.RemoveMask(target.nickOrMask)
|
||||
case ubanNick:
|
||||
targetString = target.nickOrMask
|
||||
err = client.server.accounts.Unsuspend(target.nickOrMask)
|
||||
}
|
||||
if err == nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("Successfully removed ban on %s"), targetString))
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("Could not remove ban: %v"), err))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ubanListHandler(client *Client, params []string, rb *ResponseBuffer) bool {
|
||||
allDlines := client.server.dlines.AllBans()
|
||||
rb.Notice(fmt.Sprintf(client.t("There are %d active IP/network ban(s) (DLINEs)"), len(allDlines)))
|
||||
for key, info := range allDlines {
|
||||
rb.Notice(formatBanForListing(client, key, info))
|
||||
}
|
||||
rb.Notice(client.t("Some IPs may also be prevented from connecting by the connection limiter and/or throttler"))
|
||||
|
||||
allKlines := client.server.klines.AllBans()
|
||||
rb.Notice(fmt.Sprintf(client.t("There are %d active ban(s) on nick-user-host masks (KLINEs)"), len(allKlines)))
|
||||
for key, info := range allKlines {
|
||||
rb.Notice(formatBanForListing(client, key, info))
|
||||
}
|
||||
|
||||
listAccountSuspensions(client, rb, client.server.name)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ubanInfoHandler(client *Client, target ubanTarget, params []string, rb *ResponseBuffer) bool {
|
||||
switch target.banType {
|
||||
case ubanCIDR:
|
||||
ubanInfoCIDR(client, target, rb)
|
||||
case ubanNickmask:
|
||||
ubanInfoNickmask(client, target, rb)
|
||||
case ubanNick:
|
||||
ubanInfoNick(client, target, rb)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ubanInfoCIDR(client *Client, target ubanTarget, rb *ResponseBuffer) {
|
||||
if target.cidr.PrefixLen == 128 {
|
||||
status := client.server.connectionLimiter.Status(target.cidr.IP)
|
||||
str := target.cidr.IP.String()
|
||||
if status.Exempt {
|
||||
rb.Notice(fmt.Sprintf(client.t("IP %s is exempt from connection limits"), str))
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("IP %[1]s has %[2]d active connections out of a maximum of %[3]d"), str, status.Count, status.MaxCount))
|
||||
rb.Notice(fmt.Sprintf(client.t("IP %[1]s has had %[2]d connection attempts in the past %[3]v, out of a maximum of %[4]d"), str, status.Throttle, status.ThrottleDuration, status.MaxPerWindow))
|
||||
}
|
||||
}
|
||||
|
||||
str := target.cidr.HumanReadableString()
|
||||
isBanned, banInfo := client.server.dlines.CheckIP(target.cidr.IP)
|
||||
if isBanned {
|
||||
rb.Notice(formatBanForListing(client, str, banInfo))
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("There is no active IP ban against %s"), str))
|
||||
}
|
||||
|
||||
sessions, nicks := sessionsForCIDR(client.server, target.cidr, nil)
|
||||
if len(sessions) != 0 {
|
||||
rb.Notice(fmt.Sprintf(client.t("There are %[1]d active client(s) from %[2]s, associated with %[3]d nickname(s):"), len(sessions), target.cidr.String(), len(nicks)))
|
||||
for _, line := range utils.BuildTokenLines(400, nicks, " ") {
|
||||
rb.Notice(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ubanInfoNickmask(client *Client, target ubanTarget, rb *ResponseBuffer) {
|
||||
isBanned, info := client.server.klines.ContainsMask(target.nickOrMask)
|
||||
if isBanned {
|
||||
rb.Notice(formatBanForListing(client, target.nickOrMask, info))
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("No ban exists for %[1]s"), target.nickOrMask))
|
||||
}
|
||||
|
||||
affectedCount := 0
|
||||
alwaysOnCount := 0
|
||||
for _, mcl := range client.server.clients.AllClients() {
|
||||
matches := false
|
||||
for _, mask := range mcl.AllNickmasks() {
|
||||
if target.matcher.MatchString(mask) {
|
||||
matches = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matches {
|
||||
if mcl.AlwaysOn() {
|
||||
alwaysOnCount++
|
||||
} else {
|
||||
affectedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rb.Notice(fmt.Sprintf(client.t("Adding this mask would affect %[1]d clients (an additional %[2]d clients are exempt due to always-on)"), affectedCount, alwaysOnCount))
|
||||
}
|
||||
|
||||
func ubanInfoNick(client *Client, target ubanTarget, rb *ResponseBuffer) {
|
||||
mcl := client.server.clients.Get(target.nickOrMask)
|
||||
if mcl != nil {
|
||||
details := mcl.Details()
|
||||
if details.account == "" {
|
||||
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is unauthenticated and connected from %[2]s"), details.nick, client.IP().String()))
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is logged into account %[2]s and has %[3]d active clients (see /NICKSERV CLIENTS LIST %[4]s for more info"), details.nick, details.accountName, len(mcl.Sessions()), details.nick))
|
||||
ip := client.IP()
|
||||
if !ip.IsLoopback() {
|
||||
rb.Notice(fmt.Sprintf(client.t("Client %[1]s is associated with IP %[2]s; you can ban this IP with /UBAN ADD"), details.nick, ip.String()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("No client is currently using that nickname")))
|
||||
}
|
||||
|
||||
account, err := client.server.accounts.LoadAccount(target.nickOrMask)
|
||||
if err != nil {
|
||||
if err == errAccountDoesNotExist {
|
||||
rb.Notice(fmt.Sprintf(client.t("There is no account registered for %s"), target.nickOrMask))
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("Couldn't load account: %v"), err.Error()))
|
||||
}
|
||||
return
|
||||
}
|
||||
if account.Verified {
|
||||
if account.Suspended == nil {
|
||||
rb.Notice(fmt.Sprintf(client.t("Account %[1]s is in good standing; see /NICKSERV INFO %[2]s for more details"), target.nickOrMask, target.nickOrMask))
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("Account %[1]s has been suspended: %[2]s"), target.nickOrMask, suspensionToString(client, *account.Suspended)))
|
||||
}
|
||||
} else {
|
||||
rb.Notice(fmt.Sprintf(client.t("Account %[1]s was created, but has not been verified"), target.nickOrMask))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user