Implement ban guarding. Some ip fixes
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user