Skip to content

Commit 4635f0c

Browse files
fabriziosalmiclaude
andcommitted
perf: broad performance improvements for v3.3.0
Backend: - Fix SQLite WAL mode activation (modernc.org/sqlite _pragma= syntax) - Fix WebSocket/CORS origin check for IP-based access - Combine DashboardSummary into single aggregate query (8 queries → 1) - Replace bubble sort with sort.Slice in ShadowIT handler - Pre-compile CIDR networks at init instead of per-request parsing - Use buffered scanner in log tailer instead of io.ReadAll - Cache GDPR setting with 30s TTL instead of per-row DB query - Pool HTTP clients for WAF/proxy calls (reuse vs per-request alloc) - Replace readAll with bytes.Buffer for efficient memory usage - Pre-format import timestamp outside loop in blacklist import Frontend: - Increase React Query staleTime from 10s to 60s - Increase Dashboard refetch interval from 10s to 30s - Increase ThreatIntel refetch intervals (15s→30s, 30s→60s) - Add visibility check to sidebar health poll, increase to 30s Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8cd42f8 commit 4635f0c

15 files changed

Lines changed: 130 additions & 68 deletions

File tree

backend-go/cmd/server/main.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,19 @@ func run() error {
150150
if origin == "" {
151151
return true // non-browser clients (curl, etc.)
152152
}
153-
_, ok := wsAllowed[origin]
154-
return ok
153+
if _, ok := wsAllowed[origin]; ok {
154+
return true
155+
}
156+
// Allow WebSocket from the same host the request arrived on
157+
// (covers IP-based access where CORS_ALLOWED_ORIGINS lists only localhost).
158+
if r.Host != "" {
159+
for _, scheme := range []string{"https://", "http://"} {
160+
if origin == scheme+r.Host {
161+
return true
162+
}
163+
}
164+
}
165+
return false
155166
},
156167
ReadBufferSize: 1024,
157168
WriteBufferSize: 1024,
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package config
22

33
// AppVersion is the semantic version of this backend build.
4-
const AppVersion = "3.2.2"
4+
const AppVersion = "3.3.0"

backend-go/internal/database/db.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ func Open(path string) (*sql.DB, error) {
1818
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
1919
return nil, fmt.Errorf("mkdir for db: %w", err)
2020
}
21-
dsn := path + "?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on&_synchronous=NORMAL"
21+
// modernc.org/sqlite uses _pragma= syntax (not _journal_mode= like mattn/go-sqlite3).
22+
dsn := path + "?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(ON)&_pragma=synchronous(NORMAL)"
2223
db, err := sql.Open("sqlite", dsn)
2324
if err != nil {
2425
return nil, fmt.Errorf("sql.Open: %w", err)
@@ -31,6 +32,12 @@ func Open(path string) (*sql.DB, error) {
3132
return nil, fmt.Errorf("db ping: %w", err)
3233
}
3334

35+
// Verify WAL mode is active.
36+
var journalMode string
37+
if err := db.QueryRow("PRAGMA journal_mode").Scan(&journalMode); err == nil {
38+
log.Info().Str("journal_mode", journalMode).Msg("SQLite journal mode")
39+
}
40+
3441
// Performance PRAGMAs — applied per-connection.
3542
for _, pragma := range []string{
3643
"PRAGMA cache_size = -50000", // 50 MB page cache (vs default 2 MB)

backend-go/internal/handlers/analytics.go

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"net/http"
99
"regexp"
10+
"sort"
1011
"strings"
1112
"time"
1213

@@ -20,6 +21,12 @@ import (
2021
// wafBreaker protects against cascading failures when the WAF service is down.
2122
var wafBreaker = appMW.NewCircuitBreaker(3, 30*time.Second)
2223

24+
// Shared HTTP clients — reused across requests to avoid per-request allocation.
25+
var (
26+
wafClient = &http.Client{Timeout: 3 * time.Second}
27+
probeClient = &http.Client{Timeout: 1 * time.Second}
28+
)
29+
2330
type AnalyticsHandlers struct {
2431
db *sql.DB
2532
cfg *config.Config
@@ -50,8 +57,7 @@ func (h *AnalyticsHandlers) Register(r chi.Router, authMW func(http.Handler) htt
5057

5158
func (h *AnalyticsHandlers) Status(w http.ResponseWriter, r *http.Request) {
5259
proxyStatus := "error"
53-
client := &http.Client{Timeout: 1 * time.Second}
54-
resp, err := client.Get(h.cfg.ProxyURL)
60+
resp, err := probeClient.Get(h.cfg.ProxyURL)
5561
if err == nil {
5662
resp.Body.Close()
5763
if resp.StatusCode == 400 || resp.StatusCode == 200 {
@@ -284,8 +290,7 @@ func (h *AnalyticsHandlers) WAFStats(w http.ResponseWriter, r *http.Request) {
284290
writeError(w, http.StatusServiceUnavailable, "WAF service circuit open — retrying soon")
285291
return
286292
}
287-
client := &http.Client{Timeout: 3 * time.Second}
288-
resp, err := client.Get(h.cfg.WAFURL + "/stats")
293+
resp, err := wafClient.Get(h.cfg.WAFURL + "/stats")
289294
if err != nil {
290295
wafBreaker.RecordFailure()
291296
writeError(w, http.StatusBadGateway, "WAF service unreachable")
@@ -308,9 +313,8 @@ func (h *AnalyticsHandlers) ResetCounters(w http.ResponseWriter, r *http.Request
308313
deleted, _ = res.RowsAffected()
309314
}
310315

311-
client := &http.Client{Timeout: 3 * time.Second}
312316
wafReset := false
313-
if resp, err := client.Post(h.cfg.WAFURL+"/reset", "application/json", nil); err == nil {
317+
if resp, err := wafClient.Post(h.cfg.WAFURL+"/reset", "application/json", nil); err == nil {
314318
resp.Body.Close()
315319
wafReset = resp.StatusCode == 200
316320
}
@@ -320,13 +324,16 @@ func (h *AnalyticsHandlers) ResetCounters(w http.ResponseWriter, r *http.Request
320324
func (h *AnalyticsHandlers) DashboardSummary(w http.ResponseWriter, r *http.Request) {
321325
result := map[string]any{}
322326

323-
var totalReqs, blockedReqs, todayReqs, todayBlocked, ipBLCount, domainBLCount int
324-
h.db.QueryRow("SELECT COUNT(*) FROM proxy_logs").Scan(&totalReqs) //nolint:errcheck
325-
h.db.QueryRow("SELECT COUNT(*) FROM proxy_logs WHERE status LIKE '%DENIED%' OR status LIKE '%403%' OR status LIKE '%BLOCKED%'").Scan(&blockedReqs) //nolint:errcheck
326-
327327
today := time.Now().Format("2006-01-02")
328-
h.db.QueryRow("SELECT COUNT(*) FROM proxy_logs WHERE timestamp >= ?", today).Scan(&todayReqs) //nolint:errcheck
329-
h.db.QueryRow("SELECT COUNT(*) FROM proxy_logs WHERE (status LIKE '%DENIED%' OR status LIKE '%403%') AND timestamp >= ?", today).Scan(&todayBlocked) //nolint:errcheck
328+
var totalReqs, blockedReqs, todayReqs, todayBlocked int
329+
h.db.QueryRow(`SELECT
330+
COUNT(*),
331+
SUM(CASE WHEN status LIKE '%DENIED%' OR status LIKE '%403%' OR status LIKE '%BLOCKED%' THEN 1 ELSE 0 END),
332+
SUM(CASE WHEN timestamp >= ? THEN 1 ELSE 0 END),
333+
SUM(CASE WHEN (status LIKE '%DENIED%' OR status LIKE '%403%') AND timestamp >= ? THEN 1 ELSE 0 END)
334+
FROM proxy_logs`, today, today).Scan(&totalReqs, &blockedReqs, &todayReqs, &todayBlocked) //nolint:errcheck
335+
336+
var ipBLCount, domainBLCount int
330337

331338
result["total_requests"] = totalReqs
332339
result["blocked_requests"] = blockedReqs
@@ -351,6 +358,7 @@ func (h *AnalyticsHandlers) DashboardSummary(w http.ResponseWriter, r *http.Requ
351358

352359
h.db.QueryRow("SELECT COUNT(*) FROM ip_blacklist").Scan(&ipBLCount) //nolint:errcheck
353360
h.db.QueryRow("SELECT COUNT(*) FROM domain_blacklist").Scan(&domainBLCount) //nolint:errcheck
361+
// Note: these two are on different tables so cannot be combined into the proxy_logs query above.
354362
result["ip_blacklist_count"] = ipBLCount
355363
result["domain_blacklist_count"] = domainBLCount
356364

@@ -408,7 +416,6 @@ func (h *AnalyticsHandlers) DashboardSummary(w http.ResponseWriter, r *http.Requ
408416
result["recent_blocks"] = recentBlocks
409417

410418
// WAF stats
411-
wafClient := &http.Client{Timeout: 2 * time.Second}
412419
if resp, err := wafClient.Get(h.cfg.WAFURL + "/stats"); err == nil {
413420
var wafData any
414421
json.NewDecoder(resp.Body).Decode(&wafData) //nolint:errcheck
@@ -502,14 +509,9 @@ func (h *AnalyticsHandlers) ShadowIT(w http.ResponseWriter, r *http.Request) {
502509
for _, e := range services {
503510
result = append(result, e)
504511
}
505-
// Simple sort by requests descending.
506-
for i := 0; i < len(result); i++ {
507-
for j := i + 1; j < len(result); j++ {
508-
if result[j].Requests > result[i].Requests {
509-
result[i], result[j] = result[j], result[i]
510-
}
511-
}
512-
}
512+
sort.Slice(result, func(i, j int) bool {
513+
return result[i].Requests > result[j].Requests
514+
})
513515
if result == nil {
514516
result = []*entry{}
515517
}
@@ -640,8 +642,7 @@ func (h *AnalyticsHandlers) WAFCategories(w http.ResponseWriter, r *http.Request
640642
writeError(w, http.StatusServiceUnavailable, "WAF circuit open")
641643
return
642644
}
643-
client := &http.Client{Timeout: 3 * time.Second}
644-
resp, err := client.Get(h.cfg.WAFURL + "/categories")
645+
resp, err := wafClient.Get(h.cfg.WAFURL + "/categories")
645646
if err != nil {
646647
wafBreaker.RecordFailure()
647648
writeError(w, http.StatusBadGateway, "WAF unreachable")
@@ -660,8 +661,7 @@ func (h *AnalyticsHandlers) WAFCategoryToggle(w http.ResponseWriter, r *http.Req
660661
writeError(w, http.StatusServiceUnavailable, "WAF circuit open")
661662
return
662663
}
663-
client := &http.Client{Timeout: 3 * time.Second}
664-
resp, err := client.Post(h.cfg.WAFURL+"/categories/toggle", "application/json", r.Body)
664+
resp, err := wafClient.Post(h.cfg.WAFURL+"/categories/toggle", "application/json", r.Body)
665665
if err != nil {
666666
wafBreaker.RecordFailure()
667667
writeError(w, http.StatusBadGateway, "WAF unreachable")

backend-go/internal/handlers/blacklists.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,7 @@ func (h *BlacklistHandlers) Import(w http.ResponseWriter, r *http.Request) {
351351
}
352352

353353
var toInsert [][2]string
354+
importDesc := "Imported on " + time.Now().Format("2006-01-02")
354355
added, skipped := 0, 0
355356
for _, line := range strings.Split(content, "\n") {
356357
line = strings.TrimSpace(line)
@@ -387,7 +388,7 @@ func (h *BlacklistHandlers) Import(w http.ResponseWriter, r *http.Request) {
387388
continue
388389
}
389390
existing[entry] = struct{}{}
390-
toInsert = append(toInsert, [2]string{entry, "Imported on " + time.Now().Format("2006-01-02")})
391+
toInsert = append(toInsert, [2]string{entry, importDesc})
391392
added++
392393
}
393394

backend-go/internal/handlers/helpers.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package handlers
22

33
import (
4+
"bytes"
45
"context"
56
"encoding/json"
67
"errors"
@@ -167,22 +168,18 @@ func (lr *limitedReader) Read(p []byte) (int, error) {
167168
}
168169

169170
func readAll(r interface{ Read([]byte) (int, error) }) ([]byte, error) {
170-
var out []byte
171-
buf := make([]byte, 1<<20) // 1 MB chunks
172-
for {
173-
n, err := r.Read(buf)
174-
if n > 0 {
175-
out = append(out, buf[:n]...)
176-
}
177-
if err != nil {
178-
if errors.Is(err, io.EOF) {
179-
return out, nil
180-
}
181-
return out, err
182-
}
171+
var buf bytes.Buffer
172+
if _, err := buf.ReadFrom(ioReader{r}); err != nil && !errors.Is(err, io.EOF) {
173+
return buf.Bytes(), err
183174
}
175+
return buf.Bytes(), nil
184176
}
185177

178+
// ioReader wraps a minimal Read interface into io.Reader for bytes.Buffer.ReadFrom.
179+
type ioReader struct{ r interface{ Read([]byte) (int, error) } }
180+
181+
func (w ioReader) Read(p []byte) (int, error) { return w.r.Read(p) }
182+
186183
// extractDomain parses a destination like "http://example.com:443/path" → "example.com".
187184
func extractDomain(dest string) string {
188185
d := dest

backend-go/internal/handlers/logs.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,19 @@ import (
44
"database/sql"
55
"net/http"
66
"strconv"
7+
"sync"
8+
"time"
79

810
"github.com/go-chi/chi/v5"
911
"github.com/fabriziosalmi/secure-proxy-manager/backend-go/internal/middleware"
1012
)
1113

12-
type LogHandlers struct{ db *sql.DB }
14+
type LogHandlers struct {
15+
db *sql.DB
16+
gdprMu sync.RWMutex
17+
gdprVal bool
18+
gdprTime time.Time
19+
}
1320

1421
func NewLogHandlers(db *sql.DB) *LogHandlers { return &LogHandlers{db: db} }
1522

@@ -22,11 +29,25 @@ func (h *LogHandlers) Register(r chi.Router, authMW func(http.Handler) http.Hand
2229
}
2330

2431
func (h *LogHandlers) gdprEnabled() bool {
25-
var val string
26-
if err := h.db.QueryRow("SELECT setting_value FROM settings WHERE setting_name = 'gdpr_mode'").Scan(&val); err != nil {
27-
return false
32+
const ttl = 30 * time.Second
33+
h.gdprMu.RLock()
34+
if time.Since(h.gdprTime) < ttl {
35+
v := h.gdprVal
36+
h.gdprMu.RUnlock()
37+
return v
2838
}
29-
return val == "true"
39+
h.gdprMu.RUnlock()
40+
41+
var val string
42+
enabled := false
43+
if err := h.db.QueryRow("SELECT setting_value FROM settings WHERE setting_name = 'gdpr_mode'").Scan(&val); err == nil {
44+
enabled = val == "true"
45+
}
46+
h.gdprMu.Lock()
47+
h.gdprVal = enabled
48+
h.gdprTime = time.Now()
49+
h.gdprMu.Unlock()
50+
return enabled
3051
}
3152

3253
func (h *LogHandlers) GetLogs(w http.ResponseWriter, r *http.Request) {

backend-go/internal/middleware/middleware.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,17 @@ func CORS(cfg *config.Config) func(http.Handler) http.Handler {
4343
return func(next http.Handler) http.Handler {
4444
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4545
origin := r.Header.Get("Origin")
46-
if _, ok := allowed[origin]; ok {
46+
_, ok := allowed[origin]
47+
if !ok && origin != "" && r.Host != "" {
48+
// Allow same-host origin (covers IP-based access).
49+
for _, scheme := range []string{"https://", "http://"} {
50+
if origin == scheme+r.Host {
51+
ok = true
52+
break
53+
}
54+
}
55+
}
56+
if ok {
4757
w.Header().Set("Access-Control-Allow-Origin", origin)
4858
w.Header().Set("Vary", "Origin")
4959
}

backend-go/internal/middleware/ratelimit.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,21 @@ func extractIP(r *http.Request) string {
104104
return host
105105
}
106106

107-
func isPrivate(ip net.IP) bool {
108-
private := []string{"127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
109-
for _, cidr := range private {
107+
var privateNets = func() []*net.IPNet {
108+
cidrs := []string{"127.0.0.0/8", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
109+
nets := make([]*net.IPNet, 0, len(cidrs))
110+
for _, cidr := range cidrs {
110111
_, network, _ := net.ParseCIDR(cidr)
111-
if network != nil && network.Contains(ip) {
112+
if network != nil {
113+
nets = append(nets, network)
114+
}
115+
}
116+
return nets
117+
}()
118+
119+
func isPrivate(ip net.IP) bool {
120+
for _, network := range privateNets {
121+
if network.Contains(ip) {
112122
return true
113123
}
114124
}

backend-go/internal/workers/log_tailer.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
package workers
33

44
import (
5+
"bufio"
56
"context"
67
"database/sql"
78
"encoding/json"
@@ -52,15 +53,10 @@ func StartLogTailer(ctx context.Context, db *sql.DB, logPath string, hub *websoc
5253
f.Close()
5354
continue
5455
}
55-
buf, err := io.ReadAll(f)
56-
f.Close()
57-
if err != nil {
58-
continue
59-
}
60-
offset += int64(len(buf))
61-
lines := strings.Split(string(buf), "\n")
62-
for _, line := range lines {
63-
line = strings.TrimSpace(line)
56+
scanner := bufio.NewScanner(f)
57+
scanner.Buffer(make([]byte, 64*1024), 256*1024) // 64KB default, 256KB max line
58+
for scanner.Scan() {
59+
line := strings.TrimSpace(scanner.Text())
6460
if line == "" {
6561
continue
6662
}
@@ -76,6 +72,14 @@ func StartLogTailer(ctx context.Context, db *sql.DB, logPath string, hub *websoc
7672
}
7773
}
7874
}
75+
// Update offset to current file position.
76+
newOffset, err := f.Seek(0, io.SeekCurrent)
77+
f.Close()
78+
if err == nil {
79+
offset = newOffset
80+
} else {
81+
offset = fi.Size()
82+
}
7983
}
8084
}()
8185
log.Info().Str("path", logPath).Msg("log tailer started")

0 commit comments

Comments
 (0)