sh3lly/auth/bans.go

300 lignes
6.8 KiB
Go

package auth
import (
"encoding/json"
"errors"
"net"
"strings"
"time"
"github.com/patrickmn/go-cache"
"golang.org/x/crypto/ssh"
)
// BanType is used to categorize what a particular ban targets.
type BanType uint32
const (
// Client is a ban targetting the users SSH client.
Client BanType = iota
// Name is a ban targetting nicknames.
Name
// Key is a ban targetting SSH public keys.
Key
// IP is a ban targetting IP addresses.
IP
)
var banCache *cache.Cache
// BanList represents our list of banned items.
type BanList struct {
Items []string
}
func init() {
banCache = cache.New(2*time.Hour, 25*time.Minute)
}
func parseBanQuery(s string) (string, BanType, error) {
query := strings.TrimSpace(s)
request := strings.Split(query, "=")
bantype, err := StringToBanType(request[0])
if err != nil {
log.Debug().Err(err).Str("caller", s).
Str("request[0]", request[0]).
Str("request[1]", request[1]).Msg("unknown key")
return "", 100, errors.New("unknown key")
}
return request[1], bantype, nil
}
// BanQuery is used to ingest strings that contain various types of bans seperated by an equals sign.
// e.g: /ban name=douchebag
func (users *UserDB) BanQuery(query string) (err error) {
target, bantype, err := parseBanQuery(query)
if err != nil {
return err
}
return users.BanOther(target, bantype, true)
}
// UnBanQuery is used to ingest strings that contain various types of bans seperated by an equals sign.
// e.g: /unban name=knucklehead
func (users *UserDB) UnBanQuery(query string) (err error) {
target, bantype, err := parseBanQuery(query)
if err != nil {
return err
}
return users.BanOther(target, bantype, false)
}
// Banned returns the current banned items list.
func (users *UserDB) Banned() ([]string, []string, []string, []string) {
users.mu.RLock()
defer users.mu.RUnlock()
names := users.banned(Name)
ips := users.banned(IP)
fprints := users.banned(Key)
clients := users.banned(Client)
return names, ips, fprints, clients
}
func (users *UserDB) banned(bantype BanType) []string {
var banned = new(BanList)
var bans = uint32ToBytes(uint32(bantype))
banbytes, err := users.DB.Get(bans)
if err != nil {
log.Debug().Err(err).Uint16("type", uint16(bantype)).Msg("failed to get bans")
return []string{}
}
err = json.Unmarshal(banbytes, &banned)
if err != nil {
log.Error().Caller().Err(err).Uint16("type", uint16(bantype)).Msg("failed to unmarshal")
}
return banned.Items
}
// CheckBans checks our ban list for the instance of any of the given elements.
func (users *UserDB) CheckBans(user string, addr net.Addr, key ssh.PublicKey, s string) error {
log.Debug().Caller().Msg("checkbans")
var (
userRet = errors.New("user is banned")
addrRet = errors.New("ip is banned")
keyRet = errors.New("key is banned")
clientRet = errors.New("client is banned")
)
present := func(target string) (ok bool) {
_, ok = banCache.Get(target)
return
}
clientAddr, _, err := net.SplitHostPort(addr.String())
if err != nil {
panic(err)
}
// check cache before we check the database
switch {
case present(user):
return userRet
case present(clientAddr):
return addrRet
case present(s):
return clientRet
default:
if key == nil {
break
}
if present(string(key.Marshal())) {
return keyRet
}
}
var toCheck = make(map[string]BanType)
toCheck[addr.String()] = IP
toCheck[s] = Client
toCheck[user] = Name
for item, bt := range toCheck {
if users.CheckBanOther(item, bt) {
if err := banCache.Add(addr.String(), true, cache.DefaultExpiration); err != nil {
log.Debug().Caller().Err(err).Msg("ban cache failure")
}
switch bt {
case IP:
return addrRet
case Client:
return clientRet
case Name:
return userRet
default:
log.Warn().Caller().Msg("unhandled ban")
}
}
}
if key == nil {
return nil
}
if users.CheckBanOther(string(key.Marshal()), Key) {
if err := banCache.Add(string(key.Marshal()), true, cache.DefaultExpiration); err != nil {
log.Debug().Caller().Err(err).Msg("ban cache failure")
}
return keyRet
}
return nil
}
// BanOther creates a ban on various types of client attributes, or unbans them if banunban is false.
func (users *UserDB) BanOther(target string, bantype BanType, banUnban bool) error {
bans := uint32ToBytes(uint32(bantype))
bad := &BanList{Items: []string{}}
var contains = false
if users.DB.Has(bans) {
banJSON, err := users.DB.Get(bans)
if err != nil {
return err
}
if err := json.Unmarshal(banJSON, &bad); err != nil {
return err
}
for _, b := range bad.Items {
if b == target {
contains = true
}
}
}
if contains && banUnban {
return errors.New("item is already banned: " + target)
}
if !contains && !banUnban {
return errors.New("ban does not exist")
}
var newbans []string
if banUnban {
bad.Items = append(bad.Items, target)
} else {
for _, item := range bad.Items {
if item != target {
newbans = append(bad.Items, item)
}
}
bad.Items = newbans
}
newitems, err := json.Marshal(bad)
if err != nil {
return err
}
return users.DB.Put(bans, newitems)
}
// StringToBanType takes a string and returns the corresponding BanType if found.
func StringToBanType(banstr string) (BanType, error) {
switch banstr {
case "name":
return Name, nil
case "ip":
return IP, nil
case "client":
return Client, nil
case "key":
return Key, nil
default:
return 100, errors.New("ban type not found")
}
}
// CheckBanOther checks a given target of bantype against our banned items.
func (users *UserDB) CheckBanOther(target string, bantype BanType) bool {
bans := uint32ToBytes(uint32(bantype))
if bantype == IP {
ip, _, err := net.SplitHostPort(target)
if err == nil {
target = ip
}
}
bad := &BanList{Items: []string{}}
if !users.DB.Has(bans) {
return false
}
badBytes, err := users.DB.Get(bans)
if err != nil {
log.Error().Err(err).Uint32("bantype", uint32(bantype)).Msg("failed to load bans!!")
// ban anyway for safety
return true
}
if err := json.Unmarshal(badBytes, bad); err != nil {
log.Error().Err(err).Uint32("bantype", uint32(bantype)).Msg("failed to load bans!!")
return true
}
for _, banned := range bad.Items {
if banned == target {
return true
}
}
return false
}
// Ban bans a user from accessing their account.
func (users *UserDB) Ban(username string) error {
user, err := users.GetUser(username)
if err != nil {
return err
}
if err := users.SetPrivLevel(user, LevelBanned); err != nil {
return err
}
return nil
}
// UnBan unbans a user from accessing their account and resets their privileges.
// If they were previously opped you will need to re-op them.
func (users *UserDB) UnBan(username string) error {
user, err := users.GetUser(username)
if err != nil {
return err
}
if err := users.SetPrivLevel(user, Default); err != nil {
return err
}
return nil
}