2017-03-27 14:15:02 +02:00
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2016-11-04 03:42:58 +01:00
// released under the MIT license
package irc
2016-11-04 12:14:52 +01:00
import (
"errors"
"fmt"
"net"
2017-06-11 18:01:39 +02:00
"sort"
2017-09-29 08:07:09 +02:00
"sync"
2016-11-04 12:14:52 +01:00
"time"
"strings"
"encoding/json"
2017-06-15 18:14:19 +02:00
"github.com/goshuirc/irc-go/ircfmt"
"github.com/goshuirc/irc-go/ircmsg"
2017-06-14 20:00:53 +02:00
"github.com/oragono/oragono/irc/custime"
"github.com/oragono/oragono/irc/sno"
2016-11-04 12:14:52 +01:00
"github.com/tidwall/buntdb"
)
const (
keyDlineEntry = "bans.dline %s"
)
var (
errNoExistingBan = errors . New ( "Ban does not exist" )
)
2016-11-04 03:42:58 +01:00
// IPRestrictTime contains the expiration info about the given IP.
type IPRestrictTime struct {
2016-11-04 12:14:52 +01:00
// Duration is how long this block lasts for.
2017-03-07 10:55:14 +01:00
Duration time . Duration ` json:"duration" `
2016-11-04 03:42:58 +01:00
// Expires is when this block expires.
2017-03-07 10:55:14 +01:00
Expires time . Time ` json:"expires" `
2016-11-04 03:42:58 +01:00
}
// IsExpired returns true if the time has expired.
func ( iptime * IPRestrictTime ) IsExpired ( ) bool {
return iptime . Expires . Before ( time . Now ( ) )
}
// IPBanInfo holds info about an IP/net ban.
type IPBanInfo struct {
// Reason is the ban reason.
2017-03-07 10:55:14 +01:00
Reason string ` json:"reason" `
2016-11-04 03:42:58 +01:00
// OperReason is an oper ban reason.
2016-11-04 12:14:52 +01:00
OperReason string ` json:"oper_reason" `
2017-11-19 01:27:40 +01:00
// OperName is the oper who set the ban.
OperName string ` json:"oper_name" `
2016-11-04 03:42:58 +01:00
// Time holds details about the duration, if it exists.
2017-03-07 10:55:14 +01:00
Time * IPRestrictTime ` json:"time" `
2016-11-04 03:42:58 +01:00
}
2017-09-11 09:13:53 +02:00
// BanMessage returns the ban message.
func ( info IPBanInfo ) BanMessage ( message string ) string {
message = fmt . Sprintf ( message , info . Reason )
if info . Time != nil {
message += fmt . Sprintf ( " [%s]" , info . Time . Duration . String ( ) )
}
return message
}
2016-11-04 03:42:58 +01:00
// dLineAddr contains the address itself and expiration time for a given network.
type dLineAddr struct {
// Address is the address that is blocked.
Address net . IP
// Info contains information on the ban.
Info IPBanInfo
}
// dLineNet contains the net itself and expiration time for a given network.
type dLineNet struct {
// Network is the network that is blocked.
Network net . IPNet
// Info contains information on the ban.
Info IPBanInfo
}
// DLineManager manages and dlines.
type DLineManager struct {
2017-11-22 10:41:11 +01:00
sync . RWMutex // tier 1
2016-11-04 03:42:58 +01:00
// addresses that are dlined
addresses map [ string ] * dLineAddr
// networks that are dlined
networks map [ string ] * dLineNet
}
// NewDLineManager returns a new DLineManager.
func NewDLineManager ( ) * DLineManager {
2016-11-04 12:14:52 +01:00
var dm DLineManager
dm . addresses = make ( map [ string ] * dLineAddr )
dm . networks = make ( map [ string ] * dLineNet )
2016-11-04 03:42:58 +01:00
return & dm
}
2016-11-06 02:05:29 +01:00
// AllBans returns all bans (for use with APIs, etc).
func ( dm * DLineManager ) AllBans ( ) map [ string ] IPBanInfo {
allb := make ( map [ string ] IPBanInfo )
2017-09-29 08:07:09 +02:00
dm . RLock ( )
defer dm . RUnlock ( )
2016-11-06 02:05:29 +01:00
for name , info := range dm . addresses {
allb [ name ] = info . Info
}
for name , info := range dm . networks {
allb [ name ] = info . Info
}
return allb
}
2016-11-04 03:42:58 +01:00
// AddNetwork adds a network to the blocked list.
2017-11-19 01:27:40 +01:00
func ( dm * DLineManager ) AddNetwork ( network net . IPNet , length * IPRestrictTime , reason , operReason , operName string ) {
2016-11-04 03:42:58 +01:00
netString := network . String ( )
dln := dLineNet {
Network : network ,
Info : IPBanInfo {
Time : length ,
2016-11-04 12:14:52 +01:00
Reason : reason ,
OperReason : operReason ,
2017-11-19 01:27:40 +01:00
OperName : operName ,
2016-11-04 03:42:58 +01:00
} ,
}
2017-09-29 08:07:09 +02:00
dm . Lock ( )
2016-11-04 03:42:58 +01:00
dm . networks [ netString ] = & dln
2017-09-29 08:07:09 +02:00
dm . Unlock ( )
2016-11-04 03:42:58 +01:00
}
// RemoveNetwork removes a network from the blocked list.
func ( dm * DLineManager ) RemoveNetwork ( network net . IPNet ) {
netString := network . String ( )
2017-09-29 08:07:09 +02:00
dm . Lock ( )
2016-11-04 03:42:58 +01:00
delete ( dm . networks , netString )
2017-09-29 08:07:09 +02:00
dm . Unlock ( )
2016-11-04 03:42:58 +01:00
}
// AddIP adds an IP address to the blocked list.
2017-11-19 01:27:40 +01:00
func ( dm * DLineManager ) AddIP ( addr net . IP , length * IPRestrictTime , reason , operReason , operName string ) {
2016-11-04 03:42:58 +01:00
addrString := addr . String ( )
dla := dLineAddr {
Address : addr ,
Info : IPBanInfo {
Time : length ,
2016-11-04 12:14:52 +01:00
Reason : reason ,
OperReason : operReason ,
2017-11-19 01:27:40 +01:00
OperName : operName ,
2016-11-04 03:42:58 +01:00
} ,
}
2017-09-29 08:07:09 +02:00
dm . Lock ( )
2016-11-04 03:42:58 +01:00
dm . addresses [ addrString ] = & dla
2017-09-29 08:07:09 +02:00
dm . Unlock ( )
2016-11-04 03:42:58 +01:00
}
// RemoveIP removes an IP from the blocked list.
func ( dm * DLineManager ) RemoveIP ( addr net . IP ) {
addrString := addr . String ( )
2017-09-29 08:07:09 +02:00
dm . Lock ( )
2016-11-04 03:42:58 +01:00
delete ( dm . addresses , addrString )
2017-09-29 08:07:09 +02:00
dm . Unlock ( )
2016-11-04 03:42:58 +01:00
}
// CheckIP returns whether or not an IP address was banned, and how long it is banned for.
func ( dm * DLineManager ) CheckIP ( addr net . IP ) ( isBanned bool , info * IPBanInfo ) {
// check IP addr
addrString := addr . String ( )
2017-09-29 08:07:09 +02:00
dm . RLock ( )
2016-11-04 03:42:58 +01:00
addrInfo := dm . addresses [ addrString ]
2017-09-29 08:07:09 +02:00
dm . RUnlock ( )
2016-11-04 03:42:58 +01:00
if addrInfo != nil {
if addrInfo . Info . Time != nil {
if addrInfo . Info . Time . IsExpired ( ) {
// ban on IP has expired, remove it from our blocked list
dm . RemoveIP ( addr )
} else {
return true , & addrInfo . Info
}
} else {
2016-11-04 13:15:14 +01:00
return true , & addrInfo . Info
2016-11-04 03:42:58 +01:00
}
}
// check networks
2017-09-29 08:07:09 +02:00
doCleanup := false
defer func ( ) {
if doCleanup {
go func ( ) {
dm . Lock ( )
defer dm . Unlock ( )
for key , netInfo := range dm . networks {
if netInfo . Info . Time . IsExpired ( ) {
delete ( dm . networks , key )
}
}
} ( )
2016-11-04 03:42:58 +01:00
}
2017-09-29 08:07:09 +02:00
} ( )
2016-11-04 03:42:58 +01:00
2017-09-29 08:07:09 +02:00
dm . RLock ( )
defer dm . RUnlock ( )
2016-11-04 03:42:58 +01:00
2017-09-29 08:07:09 +02:00
for _ , netInfo := range dm . networks {
if netInfo . Info . Time != nil && netInfo . Info . Time . IsExpired ( ) {
// expired ban, ignore and clean up later
doCleanup = true
} else if netInfo . Network . Contains ( addr ) {
return true , & netInfo . Info
}
2016-11-04 03:42:58 +01:00
}
// no matches!
return false , nil
}
2016-11-04 12:14:52 +01:00
2017-05-24 08:58:36 +02:00
// DLINE [ANDKILL] [MYSELF] [duration] <ip>/<net> [ON <server>] [reason [| oper reason]]
2017-10-14 23:53:13 +02:00
// DLINE LIST
2016-11-04 12:14:52 +01:00
func dlineHandler ( 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
2017-10-14 23:53:13 +02:00
// if they say LIST, we just list the current dlines
if len ( msg . Params ) == currentArg + 1 && strings . ToLower ( msg . Params [ currentArg ] ) == "list" {
bans := server . dlines . AllBans ( )
if len ( bans ) == 0 {
client . Notice ( "No DLINEs have been set!" )
}
for key , info := range bans {
2017-11-19 01:27:40 +01:00
client . Notice ( fmt . Sprintf ( "Ban - %s - added by %s - %s" , key , info . OperName , info . BanMessage ( "%s" ) ) )
2017-10-14 23:53:13 +02:00
}
return false
}
2017-05-24 08:58:36 +02:00
// 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 ++
}
2016-11-05 12:44:49 +01:00
// when setting a ban that covers the oper's current connection, we require them to say
// "DLINE MYSELF" so that we're sure they really mean it.
var dlineMyself bool
if len ( msg . Params ) > currentArg + 1 && strings . ToLower ( msg . Params [ currentArg ] ) == "myself" {
dlineMyself = true
currentArg ++
}
2016-11-04 12:14:52 +01:00
// duration
2017-03-07 10:56:21 +01:00
duration , err := custime . ParseDuration ( msg . Params [ currentArg ] )
2016-11-04 12:14:52 +01:00
durationIsUsed := err == nil
if durationIsUsed {
currentArg ++
}
// get host
if len ( msg . Params ) < currentArg + 1 {
client . Send ( nil , server . name , ERR_NEEDMOREPARAMS , client . nick , msg . Command , "Not enough parameters" )
return false
}
hostString := msg . Params [ currentArg ]
currentArg ++
// check host
var hostAddr net . IP
var hostNet * net . IPNet
_ , hostNet , err = net . ParseCIDR ( hostString )
if err != nil {
hostAddr = net . ParseIP ( hostString )
}
if hostAddr == nil && hostNet == nil {
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , msg . Command , "Could not parse IP address or CIDR network" )
return false
}
if hostNet == nil {
hostString = hostAddr . String ( )
2017-05-24 08:58:36 +02:00
if ! dlineMyself && hostAddr . Equal ( client . IP ( ) ) {
2016-11-06 03:46:11 +01:00
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , msg . Command , "This ban matches you. To DLINE yourself, you must use the command: /DLINE MYSELF <arguments>" )
2016-11-05 12:44:49 +01:00
return false
}
2016-11-04 12:14:52 +01:00
} else {
hostString = hostNet . String ( )
2017-05-24 08:58:36 +02:00
if ! dlineMyself && hostNet . Contains ( client . IP ( ) ) {
2016-11-06 03:46:11 +01:00
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , msg . Command , "This ban matches you. To DLINE yourself, you must use the command: /DLINE MYSELF <arguments>" )
2016-11-05 12:44:49 +01:00
return false
}
2016-11-04 12:14:52 +01:00
}
// 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
}
}
}
2017-11-19 01:27:40 +01:00
operName := client . operName
if operName == "" {
operName = server . name
}
2016-11-04 12:14:52 +01:00
// assemble ban info
var banTime * IPRestrictTime
if durationIsUsed {
banTime = & IPRestrictTime {
Duration : duration ,
Expires : time . Now ( ) . Add ( duration ) ,
}
}
info := IPBanInfo {
Reason : reason ,
OperReason : operReason ,
2017-11-19 01:27:40 +01:00
OperName : operName ,
2016-11-04 12:14:52 +01:00
Time : banTime ,
}
// save in datastore
err = server . store . Update ( func ( tx * buntdb . Tx ) error {
dlineKey := fmt . Sprintf ( keyDlineEntry , hostString )
// assemble json from ban info
b , err := json . Marshal ( info )
if err != nil {
return err
}
tx . Set ( dlineKey , string ( b ) , nil )
return nil
} )
2017-04-16 03:35:44 +02:00
if err != nil {
client . Notice ( fmt . Sprintf ( "Could not successfully save new D-LINE: %s" , err . Error ( ) ) )
return false
}
2016-11-04 12:14:52 +01:00
if hostNet == nil {
2017-11-19 01:27:40 +01:00
server . dlines . AddIP ( hostAddr , banTime , reason , operReason , operName )
2016-11-04 12:14:52 +01:00
} else {
2017-11-19 01:27:40 +01:00
server . dlines . AddNetwork ( * hostNet , banTime , reason , operReason , operName )
2016-11-04 12:14:52 +01:00
}
2017-06-11 18:17:55 +02:00
var snoDescription string
2016-11-04 12:14:52 +01:00
if durationIsUsed {
client . Notice ( fmt . Sprintf ( "Added temporary (%s) D-Line for %s" , duration . String ( ) , hostString ) )
2017-11-19 01:27:40 +01:00
snoDescription = fmt . Sprintf ( ircfmt . Unescape ( "%s [%s]$r added temporary (%s) D-Line for %s" ) , client . nick , operName , duration . String ( ) , hostString )
2016-11-04 12:14:52 +01:00
} else {
client . Notice ( fmt . Sprintf ( "Added D-Line for %s" , hostString ) )
2017-11-19 01:27:40 +01:00
snoDescription = fmt . Sprintf ( ircfmt . Unescape ( "%s [%s]$r added D-Line for %s" ) , client . nick , operName , hostString )
2016-11-04 12:14:52 +01:00
}
2017-06-11 18:17:55 +02:00
server . snomasks . Send ( sno . LocalXline , snoDescription )
2016-11-04 12:14:52 +01:00
2017-05-24 08:58:36 +02:00
var killClient bool
if andKill {
var clientsToKill [ ] * Client
2017-06-11 18:01:39 +02:00
var killedClientNicks [ ] string
2017-05-24 08:58:36 +02:00
var toKill bool
2017-11-22 10:41:11 +01:00
for _ , mcl := range server . clients . AllClients ( ) {
2017-05-24 08:58:36 +02:00
if hostNet == nil {
toKill = hostAddr . Equal ( mcl . IP ( ) )
} else {
toKill = hostNet . Contains ( mcl . IP ( ) )
}
if toKill {
clientsToKill = append ( clientsToKill , mcl )
2017-06-11 18:01:39 +02:00
killedClientNicks = append ( killedClientNicks , mcl . nick )
2017-05-24 08:58:36 +02:00
}
}
for _ , mcl := range clientsToKill {
2017-06-11 18:01:39 +02:00
mcl . exitedSnomaskSent = true
2017-05-24 08:58:36 +02:00
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 ( )
}
}
2017-06-11 18:01:39 +02:00
// send snomask
sort . Strings ( killedClientNicks )
2017-11-19 01:27:40 +01:00
server . snomasks . Send ( sno . LocalKills , fmt . Sprintf ( ircfmt . Unescape ( "%s [%s] killed %d clients with a DLINE $c[grey][$r%s$c[grey]]" ) , client . nick , operName , len ( killedClientNicks ) , strings . Join ( killedClientNicks , ", " ) ) )
2017-05-24 08:58:36 +02:00
}
return killClient
2016-11-04 12:14:52 +01:00
}
func unDLineHandler ( 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
hostString := msg . Params [ 0 ]
// check host
var hostAddr net . IP
var hostNet * net . IPNet
_ , hostNet , err := net . ParseCIDR ( hostString )
if err != nil {
hostAddr = net . ParseIP ( hostString )
}
if hostAddr == nil && hostNet == nil {
client . Send ( nil , server . name , ERR_UNKNOWNERROR , client . nick , msg . Command , "Could not parse IP address or CIDR network" )
return false
}
if hostNet == nil {
hostString = hostAddr . String ( )
} else {
hostString = hostNet . String ( )
}
// save in datastore
err = server . store . Update ( func ( tx * buntdb . Tx ) error {
dlineKey := fmt . Sprintf ( keyDlineEntry , hostString )
// check if it exists or not
val , err := tx . Get ( dlineKey )
if val == "" {
return errNoExistingBan
} else if err != nil {
return err
}
2017-01-11 13:22:26 +01:00
tx . Delete ( dlineKey )
2016-11-04 12:14:52 +01:00
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 ( ) ) )
2017-01-11 13:37:58 +01:00
return false
2016-11-04 12:14:52 +01:00
}
if hostNet == nil {
server . dlines . RemoveIP ( hostAddr )
} else {
server . dlines . RemoveNetwork ( * hostNet )
}
client . Notice ( fmt . Sprintf ( "Removed D-Line for %s" , hostString ) )
2017-06-11 18:17:55 +02:00
server . snomasks . Send ( sno . LocalXline , fmt . Sprintf ( ircfmt . Unescape ( "%s$r removed D-Line for %s" ) , client . nick , hostString ) )
2016-11-04 12:14:52 +01:00
return false
}
func ( s * Server ) loadDLines ( ) {
s . dlines = NewDLineManager ( )
// load from datastore
s . store . View ( func ( tx * buntdb . Tx ) error {
//TODO(dan): We could make this safer
tx . AscendKeys ( "bans.dline *" , func ( key , value string ) bool {
2016-11-04 13:15:14 +01:00
// get address name
key = key [ len ( "bans.dline " ) : ]
2016-11-04 12:14:52 +01:00
// load addr/net
var hostAddr net . IP
var hostNet * net . IPNet
_ , hostNet , err := net . ParseCIDR ( key )
if err != nil {
hostAddr = net . ParseIP ( key )
}
// load ban info
var info IPBanInfo
json . Unmarshal ( [ ] byte ( value ) , & info )
2017-11-19 01:27:40 +01:00
// set opername if it isn't already set
if info . OperName == "" {
info . OperName = s . name
}
2016-11-04 12:14:52 +01:00
// add to the server
if hostNet == nil {
2017-11-19 01:27:40 +01:00
s . dlines . AddIP ( hostAddr , info . Time , info . Reason , info . OperReason , info . OperName )
2016-11-04 12:14:52 +01:00
} else {
2017-11-19 01:27:40 +01:00
s . dlines . AddNetwork ( * hostNet , info . Time , info . Reason , info . OperReason , info . OperName )
2016-11-04 12:14:52 +01:00
}
return true // true to continue I guess?
} )
return nil
} )
}