Files
software-station/internal/security/bot_blocker.go
Sudo-Ivan ab3c188e91 Add RSS feed generation and improve security features
- Implemented structured RSS feed generation using XML encoding.
- Enhanced URL registration by incorporating a random salt for hash generation.
- Introduced a bot blocker to the security middleware for improved bot detection.
- Updated security middleware to utilize the new bot blocker and added more entropy to request fingerprinting.
2025-12-27 03:15:42 -06:00

152 lines
3.1 KiB
Go

package security
import (
"bufio"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type BotBlocker struct {
mu sync.RWMutex
badUAs map[string]struct{}
blocklistURLs []string
cacheFile string
}
func NewBotBlocker(blocklistPath string) *BotBlocker {
bb := &BotBlocker{
badUAs: make(map[string]struct{}),
cacheFile: ".cache/bad-user-agents.txt",
}
if blocklistPath != "" {
// #nosec G304
if file, err := os.Open(filepath.Clean(blocklistPath)); err == nil {
scanner := bufio.NewScanner(file)
for scanner.Scan() {
url := strings.TrimSpace(scanner.Text())
if url != "" && !strings.HasPrefix(url, "#") {
bb.blocklistURLs = append(bb.blocklistURLs, url)
}
}
_ = file.Close()
}
}
// Load existing cache if available
bb.loadFromCache()
// If we have URLs, start background updater
if len(bb.blocklistURLs) > 0 {
go bb.startUpdater()
}
return bb
}
func (bb *BotBlocker) loadFromCache() {
if _, err := os.Stat(bb.cacheFile); err == nil {
if file, err := os.Open(bb.cacheFile); err == nil {
bb.mu.Lock()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
ua := strings.TrimSpace(scanner.Text())
if ua != "" {
bb.badUAs[strings.ToLower(ua)] = struct{}{}
}
}
bb.mu.Unlock()
_ = file.Close()
}
}
}
func (bb *BotBlocker) startUpdater() {
// Immediate fetch on start
bb.fetchAndRefresh()
ticker := time.NewTicker(24 * time.Hour)
for range ticker.C {
bb.fetchAndRefresh()
}
}
func (bb *BotBlocker) fetchAndRefresh() {
newUAs := make(map[string]struct{})
client := &http.Client{Timeout: 30 * time.Second}
for _, url := range bb.blocklistURLs {
resp, err := client.Get(url)
if err != nil {
log.Printf("Error fetching bot blocklist from %s: %v", url, err)
continue
}
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
ua := strings.TrimSpace(scanner.Text())
if ua != "" && !strings.HasPrefix(ua, "#") {
newUAs[strings.ToLower(ua)] = struct{}{}
}
}
_ = resp.Body.Close()
}
if len(newUAs) > 0 {
bb.mu.Lock()
bb.badUAs = newUAs
bb.mu.Unlock()
// Save to cache
_ = os.MkdirAll(".cache", 0750)
if file, err := os.Create(bb.cacheFile); err == nil {
writer := bufio.NewWriter(file)
for ua := range newUAs {
_, _ = writer.WriteString(ua + "\n")
}
_ = writer.Flush()
_ = file.Close()
}
log.Printf("Bot blocklist updated with %d entries", len(newUAs))
}
}
func (bb *BotBlocker) IsBot(ua string) bool {
if ua == "" {
return false
}
uaLower := strings.ToLower(ua)
// Check static list first (fast)
for _, bot := range BotUserAgents {
if strings.Contains(uaLower, bot) {
return true
}
}
// Check dynamic list
bb.mu.RLock()
defer bb.mu.RUnlock()
// Some lists contain partial strings, some contain exact matches.
// We'll do a partial match check for each entry in our dynamic list.
// This might be slow if the list is huge.
// Optimization: check exact match first, then partial if needed.
if _, ok := bb.badUAs[uaLower]; ok {
return true
}
for badUA := range bb.badUAs {
if strings.Contains(uaLower, badUA) {
return true
}
}
return false
}