Skip to content

Commit 5022be2

Browse files
committed
Trust Cloudflare ranges for real client IP
1 parent 68e3915 commit 5022be2

3 files changed

Lines changed: 72 additions & 20 deletions

File tree

internal/server/domainlog.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import (
1515
)
1616

1717
const (
18-
defaultMaxLogSize = 50 * 1024 * 1024 // 50MB per log file
19-
defaultMaxBackups = 5
20-
defaultMaxAge = 30 * 24 * time.Hour // 30 days
21-
cleanupInterval = 1 * time.Hour
22-
rotatedTimeFormat = "20060102-150405"
18+
defaultMaxLogSize = 50 * 1024 * 1024 // 50MB per log file
19+
defaultMaxBackups = 5
20+
defaultMaxAge = 30 * 24 * time.Hour // 30 days
21+
cleanupInterval = 1 * time.Hour
22+
rotatedTimeFormat = "20060102-150405.000000000"
2323
)
2424

2525
// domainLogManager manages per-domain access log files with rotation.
@@ -33,6 +33,7 @@ type domainLogManager struct {
3333
mu sync.RWMutex
3434
files map[string]*domainLogFile
3535
stop chan struct{}
36+
bg sync.WaitGroup
3637
}
3738

3839
type domainLogFile struct {
@@ -129,15 +130,23 @@ func (m *domainLogManager) rotateLocked(host string, dlf *domainLogFile) {
129130
ts := time.Now().Format(rotatedTimeFormat)
130131
rotatedName := fmt.Sprintf("%s.%s", dlf.path, ts)
131132
if err := os.Rename(dlf.path, rotatedName); err == nil {
132-
go compressFile(rotatedName) // compress in background
133+
m.bg.Add(1)
134+
go func() {
135+
defer m.bg.Done()
136+
compressFile(rotatedName)
137+
}()
133138
}
134139

135140
// Enforce max backups
136141
maxBackups := dlf.rotate.MaxBackups
137142
if maxBackups <= 0 {
138143
maxBackups = defaultMaxBackups
139144
}
140-
go pruneBackups(dlf.path, maxBackups)
145+
m.bg.Add(1)
146+
go func() {
147+
defer m.bg.Done()
148+
pruneBackups(dlf.path, maxBackups)
149+
}()
141150

142151
// Open fresh log file
143152
f, err := os.OpenFile(dlf.path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
@@ -204,13 +213,14 @@ func (m *domainLogManager) cleanupOld() {
204213
func (m *domainLogManager) Close() {
205214
close(m.stop)
206215
m.mu.Lock()
207-
defer m.mu.Unlock()
208216
for _, dlf := range m.files {
209217
dlf.mu.Lock()
210218
dlf.f.Close()
211219
dlf.mu.Unlock()
212220
}
213221
m.files = make(map[string]*domainLogFile)
222+
m.mu.Unlock()
223+
m.bg.Wait()
214224
}
215225

216226
// compressFile gzips a file in-place (src → src.gz, then removes src).

internal/server/server.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ func (s *Server) buildMiddlewareChain() http.Handler {
687687
mws := []middleware.Middleware{
688688
middleware.Recovery(s.logger),
689689
middleware.RequestID(),
690-
middleware.RealIP(s.config.Global.TrustedProxies),
690+
middleware.RealIP(s.realIPTrustedProxies()),
691691
middleware.SecurityHeaders(),
692692
middleware.Gzip(1024), // compress responses > 1KB
693693
}
@@ -715,6 +715,15 @@ func (s *Server) buildMiddlewareChain() http.Handler {
715715
return chain(http.HandlerFunc(s.handleRequest))
716716
}
717717

718+
func (s *Server) realIPTrustedProxies() []string {
719+
if s == nil || s.config == nil {
720+
return nil
721+
}
722+
trusted := append([]string(nil), s.config.Global.TrustedProxies...)
723+
trusted = append(trusted, s.config.Global.Cloudflare.IPRanges...)
724+
return trusted
725+
}
726+
718727
// Start starts all listeners and blocks until shutdown.
719728
func (s *Server) Start() error {
720729
workers := runtime.NumCPU()
@@ -1091,15 +1100,7 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
10911100
isMonitor := r.UserAgent() == "UWAS-Monitor/1.0"
10921101
if s.admin != nil && !isMonitor && r.Host != "localhost:80" && r.Host != "localhost" {
10931102
elapsed := time.Since(start)
1094-
// Use real client IP from X-Forwarded-For or X-Real-IP
1095-
remoteIP := r.RemoteAddr
1096-
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
1097-
if parts := strings.SplitN(xff, ",", 2); len(parts) > 0 {
1098-
remoteIP = strings.TrimSpace(parts[0])
1099-
}
1100-
} else if xri := r.Header.Get("X-Real-IP"); xri != "" {
1101-
remoteIP = xri
1102-
}
1103+
remoteIP := normalizedRemoteIP(r)
11031104
s.admin.RecordLog(admin.LogEntry{
11041105
Time: start,
11051106
Host: r.Host,
@@ -1658,6 +1659,16 @@ func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
16581659
}
16591660
}
16601661

1662+
func normalizedRemoteIP(r *http.Request) string {
1663+
if r == nil {
1664+
return ""
1665+
}
1666+
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
1667+
return host
1668+
}
1669+
return strings.TrimSpace(r.RemoteAddr)
1670+
}
1671+
16611672
func (s *Server) handleFileRequest(ctx *router.RequestContext, domain *config.Domain) {
16621673
// Save original URI before any rewriting (PHP needs this for SCRIPT_NAME)
16631674
if ctx.OriginalURI == "" {

internal/server/server_test.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/uwaserver/uwas/internal/cache"
1616
"github.com/uwaserver/uwas/internal/config"
1717
"github.com/uwaserver/uwas/internal/logger"
18+
"github.com/uwaserver/uwas/internal/middleware"
1819
)
1920

2021
func testConfig(root string) *config.Config {
@@ -997,7 +998,7 @@ RewriteRule . /index.php [L]
997998
Global: config.GlobalConfig{WorkerCount: "1", LogLevel: "error", LogFormat: "text"},
998999
Domains: []config.Domain{{
9991000
Host: "wp.test", Root: dir, Type: "php",
1000-
SSL: config.SSLConfig{Mode: "off"},
1001+
SSL: config.SSLConfig{Mode: "off"},
10011002
Htaccess: config.HtaccessConfig{Mode: "import"},
10021003
}},
10031004
}
@@ -1259,7 +1260,7 @@ func TestHandleHTTPNonSSLServesContent(t *testing.T) {
12591260
func TestHandleHTTPUnknownHostNonSSL(t *testing.T) {
12601261
// No domains configured — no fallback, unknown host is rejected.
12611262
cfg := &config.Config{
1262-
Global: config.GlobalConfig{LogLevel: "error", LogFormat: "text"},
1263+
Global: config.GlobalConfig{LogLevel: "error", LogFormat: "text"},
12631264
Domains: []config.Domain{},
12641265
}
12651266
log := logger.New("error", "text")
@@ -1314,6 +1315,36 @@ func TestBuildMiddlewareChainWithRateLimit(t *testing.T) {
13141315
}
13151316
}
13161317

1318+
func TestRealIPTrustedProxiesIncludesCloudflareRanges(t *testing.T) {
1319+
cfg := &config.Config{
1320+
Global: config.GlobalConfig{
1321+
LogLevel: "error",
1322+
LogFormat: "text",
1323+
TrustedProxies: []string{"10.0.0.0/8"},
1324+
Cloudflare: config.CloudflareConfig{
1325+
IPRanges: []string{"203.0.113.0/24"},
1326+
},
1327+
},
1328+
}
1329+
log := logger.New("error", "text")
1330+
s := New(cfg, log)
1331+
1332+
captured := ""
1333+
h := middleware.RealIP(s.realIPTrustedProxies())(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1334+
captured = r.RemoteAddr
1335+
w.WriteHeader(http.StatusNoContent)
1336+
}))
1337+
1338+
req := httptest.NewRequest("GET", "/", nil)
1339+
req.RemoteAddr = "203.0.113.44:443"
1340+
req.Header.Set("CF-Connecting-IP", "198.51.100.77")
1341+
h.ServeHTTP(httptest.NewRecorder(), req)
1342+
1343+
if captured != "198.51.100.77:0" {
1344+
t.Fatalf("captured RemoteAddr = %q, want real Cloudflare client IP", captured)
1345+
}
1346+
}
1347+
13171348
func TestBuildMiddlewareChainWithSecurityGuard(t *testing.T) {
13181349
cfg := &config.Config{
13191350
Global: config.GlobalConfig{LogLevel: "error", LogFormat: "text"},

0 commit comments

Comments
 (0)