Implement ban guarding. Some ip fixes

This commit is contained in:
Dominic Ferrando
2025-04-24 01:14:33 -04:00
parent 73d66560b6
commit 99f8c7f97d
6 changed files with 97 additions and 26 deletions
+40 -2
View File
@@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"errors" "errors"
"io/fs" "io/fs"
"log"
"os" "os"
"path" "path"
"time" "time"
@@ -349,9 +350,46 @@ func GetAdmin(db *sql.DB, username string) (Admin, error) {
return result, nil return result, nil
} }
func BanIp(db *sql.DB, ip string, reason string, expiration time.Time) error { func BanIp(db *sql.DB, ipHash string, reason string, expiration time.Time) error {
log.Printf("IP: %s, reason: %s, expiration: %v", ipHash, reason, expiration)
// if the ban already exists for ip, only update if its greater than existing
_, err := db.Exec(` _, err := db.Exec(`
INSERT INTO bans (ip_hash, reason, expiration) INSERT INTO bans (ip_hash, reason, expiration)
VALUES (?, ?, ?)`, ip, reason, expiration) VALUES (?, ?, ?)
ON CONFLICT(ip_hash) DO UPDATE SET
reason = excluded.reason,
expiration = excluded.expiration
WHERE excluded.expiration > bans.expiration
`, ipHash, reason, expiration)
return err return err
} }
var ErrBanNotFound = errors.New("ban not found")
func GetBan(db *sql.DB, ip string) (Ban, error) {
row := db.QueryRow(`
SELECT ip_hash, reason, expiration
FROM bans
where ip_hash = ?`, ip)
var r Ban
err := row.Scan(&r.IpHash, &r.Reason, &r.Expiration)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Ban{}, ErrBanNotFound
}
return Ban{}, err
}
if time.Now().After(r.Expiration) {
_, err := db.Exec(`
DELETE FROM bans
WHERE ip_hash = ?`, ip)
if err != nil {
return Ban{}, err
}
return Ban{}, ErrBanNotFound
}
return r, nil
}
+6
View File
@@ -34,3 +34,9 @@ type Admin struct {
Username string Username string
Password string Password string
} }
type Ban struct {
IpHash string
Reason string
Expiration time.Time
}
+1 -1
View File
@@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS admins (
CREATE TABLE IF NOT EXISTS bans ( CREATE TABLE IF NOT EXISTS bans (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_hash TEXT NOT NULL, ip_hash TEXT NOT NULL UNIQUE,
reason TEXT NOT NULL, reason TEXT NOT NULL,
expiration DATETIME NOT NULL expiration DATETIME NOT NULL
); );
+5 -5
View File
@@ -16,9 +16,9 @@ const POST_COOLDOWN = 15 * time.Second
const THREAD_COOLDOWN = 2 * time.Minute const THREAD_COOLDOWN = 2 * time.Minute
func GetRemainingCooldown(ip string, m map[string]time.Time, duration time.Duration) time.Duration { func GetRemainingCooldown(ipHash string, m map[string]time.Time, duration time.Duration) time.Duration {
CooldownMutex.RLock() CooldownMutex.RLock()
last, exists := m[ip] last, exists := m[ipHash]
CooldownMutex.RUnlock() CooldownMutex.RUnlock()
if !exists { if !exists {
return 0 return 0
@@ -26,12 +26,12 @@ func GetRemainingCooldown(ip string, m map[string]time.Time, duration time.Durat
return duration - time.Since(last) return duration - time.Since(last)
} }
func BeginCooldown(ip string, m map[string]time.Time, duration time.Duration) { func BeginCooldown(ipHash string, m map[string]time.Time, duration time.Duration) {
CooldownMutex.Lock() CooldownMutex.Lock()
defer CooldownMutex.Unlock() defer CooldownMutex.Unlock()
last, exists := m[ip] last, exists := m[ipHash]
if !exists || time.Since(last) >= duration { if !exists || time.Since(last) >= duration {
m[ip] = time.Now() m[ipHash] = time.Now()
} }
} }
+44 -17
View File
@@ -49,14 +49,6 @@ func disableCacheInDevMode(next http.Handler) http.Handler {
}) })
} }
func SumUniquePostIps(posts []database.Post) int {
uniqueIpHashes := map[string]bool{}
for _, post := range posts {
uniqueIpHashes[post.IpHash] = true
}
return len(uniqueIpHashes)
}
func isAdmin(r *http.Request) bool { func isAdmin(r *http.Request) bool {
if util.DevMode { if util.DevMode {
return true return true
@@ -159,10 +151,25 @@ func main() {
// CREATE THREAD // CREATE THREAD
r.Post("/{slug}/threads", func(w http.ResponseWriter, r *http.Request) { r.Post("/{slug}/threads", func(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug") slug := chi.URLParam(r, "slug")
ip := util.GetIP(r) ipHash := util.HashIp(util.GetIP(r))
// guard banned ips
ban, err := database.GetBan(db, ipHash)
if err != nil {
if !errors.Is(err, database.ErrBanNotFound) {
http.Error(w, "Failed to get ban", http.StatusInternalServerError)
return
}
} else {
msg := fmt.Sprintf("You are banned until: %s. Reason: %s",
ban.Expiration.Format("2006-01-02 15:04"),
ban.Reason)
http.Error(w, msg, http.StatusForbidden)
return
}
// check cooldown // check cooldown
timeRemaining := util.GetRemainingCooldown(ip, util.ThreadCooldowns, util.THREAD_COOLDOWN) timeRemaining := util.GetRemainingCooldown(ipHash, util.ThreadCooldowns, util.THREAD_COOLDOWN)
if timeRemaining > 0 && !isAdmin(r) { if timeRemaining > 0 && !isAdmin(r) {
response := fmt.Sprintf("Please wait %.0f seconds", timeRemaining.Seconds()) response := fmt.Sprintf("Please wait %.0f seconds", timeRemaining.Seconds())
io.Copy(io.Discard, r.Body) io.Copy(io.Discard, r.Body)
@@ -240,14 +247,14 @@ func main() {
return return
} }
threadId, err := database.PutThread(db, slug, subject, body, savedMediaPath, savedThumbPath, util.HashIp(ip)) threadId, err := database.PutThread(db, slug, subject, body, savedMediaPath, savedThumbPath, ipHash)
if err != nil { if err != nil {
http.Error(w, "Failed to create thread", http.StatusInternalServerError) http.Error(w, "Failed to create thread", http.StatusInternalServerError)
log.Printf("PutThread: %v", err) log.Printf("PutThread: %v", err)
return return
} }
util.BeginCooldown(ip, util.ThreadCooldowns, util.THREAD_COOLDOWN) util.BeginCooldown(ipHash, util.ThreadCooldowns, util.THREAD_COOLDOWN)
// Check if it's an HTMX request // Check if it's an HTMX request
redirectUrl := fmt.Sprintf("/%s/threads/%d", slug, threadId) redirectUrl := fmt.Sprintf("/%s/threads/%d", slug, threadId)
@@ -262,10 +269,25 @@ func main() {
// CREATE POST // CREATE POST
r.Post("/{slug}/threads/{threadId}", func(w http.ResponseWriter, r *http.Request) { r.Post("/{slug}/threads/{threadId}", func(w http.ResponseWriter, r *http.Request) {
slug := chi.URLParam(r, "slug") slug := chi.URLParam(r, "slug")
ip := util.GetIP(r) ipHash := util.HashIp(util.GetIP(r))
// guard banned ips
ban, err := database.GetBan(db, ipHash)
if err != nil {
if !errors.Is(err, database.ErrBanNotFound) {
http.Error(w, "Failed to get ban", http.StatusInternalServerError)
return
}
} else {
msg := fmt.Sprintf("You are banned until: %s. Reason: %s",
ban.Expiration.Format("2006-01-02 15:04"),
ban.Reason)
http.Error(w, msg, http.StatusForbidden)
return
}
// check cooldown // check cooldown
timeRemaining := util.GetRemainingCooldown(ip, util.PostCooldowns, util.POST_COOLDOWN) timeRemaining := util.GetRemainingCooldown(ipHash, util.PostCooldowns, util.POST_COOLDOWN)
if timeRemaining > 0 && !isAdmin(r) { if timeRemaining > 0 && !isAdmin(r) {
response := fmt.Sprintf("Please wait %.0f seconds", timeRemaining.Seconds()) response := fmt.Sprintf("Please wait %.0f seconds", timeRemaining.Seconds())
io.Copy(io.Discard, r.Body) io.Copy(io.Discard, r.Body)
@@ -352,13 +374,13 @@ func main() {
return return
} }
if err := database.PutPost(db, slug, threadId, body, mediaPath, thumbPath, util.HashIp(ip)); err != nil { if err := database.PutPost(db, slug, threadId, body, mediaPath, thumbPath, ipHash); err != nil {
http.Error(w, "Failed to create post", http.StatusInternalServerError) http.Error(w, "Failed to create post", http.StatusInternalServerError)
log.Printf("PutPost: %v", err) log.Printf("PutPost: %v", err)
return return
} }
util.BeginCooldown(ip, util.PostCooldowns, util.POST_COOLDOWN) util.BeginCooldown(ipHash, util.PostCooldowns, util.POST_COOLDOWN)
}) })
// ----------------- // -----------------
@@ -393,6 +415,11 @@ func main() {
return return
} }
uniqueIpHashes := map[string]bool{}
for _, post := range posts {
uniqueIpHashes[post.IpHash] = true
}
previews = append(previews, views.CatalogThreadPreview{ previews = append(previews, views.CatalogThreadPreview{
Subject: thread.Subject, Subject: thread.Subject,
Body: op.Body, Body: op.Body,
@@ -400,7 +427,7 @@ func main() {
ThreadURL: fmt.Sprintf("/%s/threads/%d", slug, thread.Id), ThreadURL: fmt.Sprintf("/%s/threads/%d", slug, thread.Id),
ThumbPath: op.ThumbPath, ThumbPath: op.ThumbPath,
ReplyCount: len(posts), ReplyCount: len(posts),
IpCount: SumUniquePostIps(posts), IpCount: len(uniqueIpHashes),
}) })
} }
+1 -1
View File
@@ -23,7 +23,7 @@ function smoothScrollTo(loc) {
} }
function isHttpWarningStatus(code) { function isHttpWarningStatus(code) {
const warningStatuses = [429, 400, 413]; const warningStatuses = [429, 400, 413, 403];
return warningStatuses.includes(code); return warningStatuses.includes(code);
} }