Add ban and unban commands

This commit is contained in:
Trevor Slocum 2024-09-30 21:39:20 -07:00
parent 0a4b5e9b09
commit 253dd9b2ae
10 changed files with 271 additions and 12 deletions

View file

@ -150,6 +150,14 @@ must write some data to the server at least once every 40 seconds.
4. Warning message is broadcast to all users.
5. Normal operation.
- `ban <username> [reason]`
- Ban a user by IP addresss and account (if logged in).
- This command is only available to server administrators and moderators.
- `unban <IP>/<username> <reason>`
- Unban a user by IP address or account.
- This command is only available to server administrators and moderators.
- `shutdown <minutes> <reason>`
- Prevent the creation of new matches and periodically warn players about the server shutting down.
- This command is only available to server administrators.

View file

@ -34,6 +34,8 @@ const (
CommandMOTD = "motd" // Read (or write) the message of the day.
CommandBroadcast = "broadcast" // Send a message to all players.
CommandDefcon = "defcon" // Apply restrictions to guests to prevent abuse.
CommandBan = "ban" // Ban an IP address or account.
CommandUnban = "unban" // Unban an IP address or account.
CommandShutdown = "shutdown" // Prevent the creation of new matches.
)
@ -91,5 +93,7 @@ var HelpText = map[string]string{
CommandMOTD: "[message] - View (or set) message of the day. Specifying a new message of the day is only available to server administrators.",
CommandBroadcast: "<message> - Send a message to all players. This command is only available to server administrators.",
CommandDefcon: "[level] - Apply restrictions to guests to prevent abuse. Levels:\n1. Disallow new accounts from being registered.\n2. Only registered users may create and join matches.\n3. Only registered users may chat and set custom match titles.\n4. Warning message is broadcast to all users.\n5. Normal operation.",
CommandBan: "<username> - Ban a user by IP addresss and account (if logged in).",
CommandUnban: "<IP>/<username> - Unban a user by IP address or account.",
CommandShutdown: "<minutes> <reason> - Prevent the creation of new matches and periodically warn players about the server shutting down. This command is only available to server administrators.",
}

View file

@ -26,7 +26,7 @@ type socketClient struct {
func newSocketClient(conn net.Conn, commands chan<- []byte, events chan []byte, verbose bool) *socketClient {
return &socketClient{
conn: conn,
address: conn.RemoteAddr().String(),
address: hashIP(conn.RemoteAddr().String()),
events: events,
commands: commands,
verbose: verbose,

View file

@ -38,7 +38,7 @@ func newWebSocketClient(r *http.Request, w http.ResponseWriter, commands chan<-
return &webSocketClient{
conn: conn,
address: r.RemoteAddr,
address: hashIP(r.RemoteAddr),
events: events,
commands: commands,
verbose: verbose,

View file

@ -91,6 +91,14 @@ CREATE TABLE follow (
FOREIGN KEY(target)
REFERENCES account(id)
);
CREATE TABLE ban (
ip text NOT NULL,
account integer NOT NULL,
created integer NOT NULL,
staff integer NOT NULL,
reason text NOT NULL,
UNIQUE (ip, account)
);
`
var (
@ -739,6 +747,108 @@ func replayByID(id int) ([]byte, error) {
return replay, nil
}
func addBan(ipHash string, account int, staff int, reason string) error {
dbLock.Lock()
defer dbLock.Unlock()
if db == nil || (ipHash == "" && account == 0) {
return nil
}
tx, err := begin()
if err != nil {
return err
}
defer tx.Commit(context.Background())
timestamp := time.Now().Unix()
if ipHash != "" {
var result int
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM ban WHERE ip = $1", ipHash).Scan(&result)
if err != nil {
log.Fatal(err)
} else if result == 0 {
_, err = tx.Exec(context.Background(), "INSERT INTO ban (ip, account, created, staff, reason) VALUES ($1, $2, $3, $4, $5)", ipHash, 0, timestamp, staff, reason)
if err != nil {
log.Fatal(err)
}
}
}
if account != 0 {
var result int
err = tx.QueryRow(context.Background(), "SELECT COUNT(*) FROM ban WHERE account = $1", account).Scan(&result)
if err != nil {
log.Fatal(err)
} else if result == 0 {
_, err = tx.Exec(context.Background(), "INSERT INTO ban (ip, account, created, staff, reason) VALUES ($1, $2, $3, $4, $5)", "", account, timestamp, staff, reason)
if err != nil {
log.Fatal(err)
}
}
}
return nil
}
func checkBan(ipHash string, account int) (bool, string) {
dbLock.Lock()
defer dbLock.Unlock()
if db == nil || (ipHash == "" && account == 0) {
return false, ""
}
tx, err := begin()
if err != nil {
return false, ""
}
defer tx.Commit(context.Background())
var row pgx.Row
if account == 0 {
row = tx.QueryRow(context.Background(), "SELECT reason FROM ban WHERE ip = $1 LIMIT 1", ipHash)
} else {
row = tx.QueryRow(context.Background(), "SELECT reason FROM ban WHERE ip = $1 OR account = $2 LIMIT 1", ipHash, account)
}
var reason string
err = row.Scan(&reason)
if err != nil {
return false, ""
}
return true, reason
}
func deleteBan(ipHash string, account int) error {
dbLock.Lock()
defer dbLock.Unlock()
if db == nil || (ipHash == "" && account == 0) {
return nil
}
tx, err := begin()
if err != nil {
return err
}
defer tx.Commit(context.Background())
if ipHash != "" {
_, err = tx.Exec(context.Background(), "DELETE FROM ban WHERE ip = $1", ipHash)
if err != nil {
log.Fatal(err)
}
}
if account != 0 {
_, err = tx.Exec(context.Background(), "DELETE FROM ban WHERE account = $1", account)
if err != nil {
log.Fatal(err)
}
}
return nil
}
func matchHistory(username string) ([]*bgammon.HistoryMatch, error) {
dbLock.Lock()
defer dbLock.Unlock()

View file

@ -63,6 +63,18 @@ func replayByID(id int) ([]byte, error) {
return nil, nil
}
func addBan(ipHash string, account int, staff int, reason string) error {
return nil
}
func checkBan(ipHash string, account int) (bool, string) {
return false, ""
}
func deleteBan(ipHash string, account int) error {
return nil
}
func recordGameResult(g *serverGame, winType int8, replay [][]byte) error {
return nil
}

View file

@ -41,6 +41,8 @@ var (
var allowDebugCommands bool
var ipSalt string
//go:embed locales
var assetFS embed.FS
@ -88,7 +90,6 @@ type server struct {
mailServer string
passwordSalt string
resetSalt string
ipSalt string
tz *time.Location
languageTags []language.Tag
@ -106,7 +107,7 @@ type server struct {
shutdownReason string
}
func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, ipSalt string, certDomain string, certFolder string, certEmail string, certAddress string, relayChat bool, verbose bool, allowDebug bool) *server {
func NewServer(tz string, dataSource string, mailServer string, passwordSalt string, resetSalt string, ipAddressSalt string, certDomain string, certFolder string, certEmail string, certAddress string, relayChat bool, verbose bool, allowDebug bool) *server {
const bufferSize = 10
s := &server{
newGameIDs: make(chan int),
@ -117,7 +118,6 @@ func NewServer(tz string, dataSource string, mailServer string, passwordSalt str
mailServer: mailServer,
passwordSalt: passwordSalt,
resetSalt: resetSalt,
ipSalt: ipSalt,
certDomain: certDomain,
certFolder: certFolder,
certEmail: certEmail,
@ -142,6 +142,8 @@ func NewServer(tz string, dataSource string, mailServer string, passwordSalt str
s.tz = time.UTC
}
ipSalt = ipAddressSalt
if dataSource != "" {
err := connectDB(dataSource)
if err != nil {

View file

@ -81,8 +81,7 @@ func (s *server) handleFirstCommand(cmd serverCommand, keyword string, params []
username: username,
password: password,
}
ipHash := s.hashIP(cmd.client.Address())
err := registerAccount(s.passwordSalt, a, ipHash)
err := registerAccount(s.passwordSalt, a, cmd.client.Address())
if err != nil {
cmd.client.Terminate(fmt.Sprintf("Failed to register: %s", err))
return
@ -178,6 +177,16 @@ func (s *server) handleFirstCommand(cmd serverCommand, keyword string, params []
cmd.client.name = username
}
banned, banReason := checkBan(cmd.client.Address(), cmd.client.accountID)
if banned {
msg := "You are banned"
if banReason != "" {
msg += ": " + banReason
}
cmd.client.Terminate(msg)
return
}
cmd.client.sendEvent(&bgammon.EventWelcome{
PlayerName: string(cmd.client.name),
Clients: len(s.clients),
@ -340,7 +349,7 @@ COMMANDS:
clientGame := s.gameByClient(cmd.client)
if clientGame != nil && clientGame.client1 != cmd.client && clientGame.client2 != cmd.client {
switch keyword {
case bgammon.CommandHelp, "h", bgammon.CommandJSON, bgammon.CommandList, "ls", bgammon.CommandBoard, "b", bgammon.CommandLeave, "l", bgammon.CommandReplay, bgammon.CommandSet, bgammon.CommandPassword, bgammon.CommandFollow, bgammon.CommandUnfollow, bgammon.CommandPong, bgammon.CommandDisconnect, bgammon.CommandMOTD, bgammon.CommandBroadcast, bgammon.CommandShutdown:
case bgammon.CommandHelp, "h", bgammon.CommandJSON, bgammon.CommandList, "ls", bgammon.CommandBoard, "b", bgammon.CommandLeave, "l", bgammon.CommandReplay, bgammon.CommandSet, bgammon.CommandPassword, bgammon.CommandFollow, bgammon.CommandUnfollow, bgammon.CommandPong, bgammon.CommandDisconnect, bgammon.CommandMOTD, bgammon.CommandBroadcast, bgammon.CommandDefcon, bgammon.CommandBan, bgammon.CommandUnban, bgammon.CommandShutdown:
// These commands are allowed to be used by spectators.
default:
cmd.client.sendNotice(gotext.GetD(cmd.client.language, "Command ignored: You are spectating this match."))
@ -1473,6 +1482,120 @@ COMMANDS:
}
s.clientsLock.Unlock()
}
case bgammon.CommandBan:
if len(params) == 0 {
cmd.client.sendNotice("Please specify an IP address or username.")
continue
} else if !cmd.client.Admin() && !cmd.client.Mod() {
cmd.client.sendNotice("Access denied.")
continue
}
var reason string
if len(params) > 1 {
reason = string(bytes.Join(params[1:], []byte(" ")))
}
msg := "You are banned"
if reason != "" {
msg += ": " + reason
}
isIP := bytes.ContainsRune(params[0], '.') || bytes.ContainsRune(params[0], ':')
if isIP {
ip := hashIP(string(params[0]))
err := addBan(ip, 0, cmd.client.accountID, reason)
if err != nil {
cmd.client.sendNotice("Failed to add ban: " + err.Error())
}
s.clientsLock.Lock()
for _, sc := range s.clients {
if sc.Address() == ip {
sc.Client.Terminate(msg)
}
}
s.clientsLock.Unlock()
cmd.client.sendNotice(fmt.Sprintf("Banned %s.", params[0]))
} else {
account, err := accountByUsername(string(params[0]))
if err != nil {
cmd.client.sendNotice("Failed to add ban: " + err.Error())
continue
} else if account == nil || account.id == 0 {
var found bool
nameLower := bytes.ToLower(params[0])
s.clientsLock.Lock()
for _, sc := range s.clients {
if bytes.Equal(bytes.ToLower(sc.name), nameLower) {
found = true
err := addBan(sc.Address(), 0, cmd.client.accountID, reason)
if err != nil {
cmd.client.sendNotice("Failed to add ban: " + err.Error())
}
sc.Client.Terminate(msg)
break
}
}
s.clientsLock.Unlock()
if !found {
cmd.client.sendNotice("No account was found with that username.")
} else {
cmd.client.sendNotice(fmt.Sprintf("Banned %s.", params[0]))
}
continue
}
err = addBan("", account.id, cmd.client.accountID, reason)
if err != nil {
cmd.client.sendNotice("Failed to add ban: " + err.Error())
}
s.clientsLock.Lock()
for _, sc := range s.clients {
if sc.accountID == account.id {
sc.Client.Terminate(msg)
break
}
}
s.clientsLock.Unlock()
cmd.client.sendNotice(fmt.Sprintf("Banned %s.", params[0]))
}
case bgammon.CommandUnban:
if len(params) == 0 {
cmd.client.sendNotice("Please specify an IP address or username.")
continue
} else if !cmd.client.Admin() && !cmd.client.Mod() {
cmd.client.sendNotice("Access denied.")
continue
}
isIP := bytes.ContainsRune(params[0], '.') || bytes.ContainsRune(params[0], ':')
if isIP {
err := deleteBan(hashIP(string(params[0])), 0)
if err != nil {
cmd.client.sendNotice("Failed to remove ban: " + err.Error())
continue
}
} else {
account, err := accountByUsername(string(params[0]))
if err != nil {
cmd.client.sendNotice("Failed to remove ban: " + err.Error())
continue
} else if account == nil || account.id == 0 {
cmd.client.sendNotice("No account was found with that username.")
continue
}
err = deleteBan("", account.id)
if err != nil {
cmd.client.sendNotice("Failed to remove ban: " + err.Error())
continue
}
}
cmd.client.sendNotice(fmt.Sprintf("Unbanned %s.", params[0]))
case bgammon.CommandShutdown:
if !cmd.client.Admin() {
cmd.client.sendNotice("Access denied.")

View file

@ -10,6 +10,6 @@ func (s *server) Listen(network string, address string) {
log.Fatal("bgammon-server was built without the 'full' tag. Only local connections are possible.")
}
func (s *server) hashIP(address string) string {
func hashIP(address string) string {
return address
}

View file

@ -20,7 +20,7 @@ import (
)
func (s *server) Listen(network string, address string) {
if s.passwordSalt == "" || s.resetSalt == "" || s.ipSalt == "" {
if s.passwordSalt == "" || s.resetSalt == "" || ipSalt == "" {
log.Fatal("error: password, reset and ip salts must be configured")
}
@ -381,7 +381,7 @@ func (s *server) handlePrintWildBGStats(w http.ResponseWriter, r *http.Request)
w.Write(s.cachedStats(4))
}
func (s *server) hashIP(address string) string {
func hashIP(address string) string {
leftBracket, rightBracket := strings.IndexByte(address, '['), strings.IndexByte(address, ']')
if leftBracket != -1 && rightBracket != -1 && rightBracket > leftBracket {
address = address[1:rightBracket]
@ -392,7 +392,7 @@ func (s *server) hashIP(address string) string {
}
}
buf := []byte(address + s.ipSalt)
buf := []byte(address + ipSalt)
h := make([]byte, 64)
sha3.ShakeSum256(h, buf)
return fmt.Sprintf("%x\n", h)