Skip to content

Commit 8d48b19

Browse files
committed
Add reverse proxy IP header support for web server logging
Added: - getClientIP helper function to extract real client IPs from proxy headers - X-Real-IP header parsing (higher priority) - X-Forwarded-For header parsing with comma-separated IP support - Security check to only trust headers from localhost connections Changed: - Web request logging to use getClientIP instead of r.RemoteAddr - Log output now shows real client IPs when behind trusted reverse proxy
1 parent 318b781 commit 8d48b19

File tree

1 file changed

+43
-2
lines changed

1 file changed

+43
-2
lines changed

internal/web/web.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,47 @@ type WebNav struct {
4242
Target string
4343
}
4444

45+
// getClientIP extracts the real client IP address from the request.
46+
// It checks for X-Real-IP and X-Forwarded-For headers when the direct
47+
// connection is from localhost (trusted proxy), otherwise returns the
48+
// direct connection IP.
49+
func getClientIP(r *http.Request) string {
50+
// Get the direct connection IP
51+
remoteAddr := r.RemoteAddr
52+
53+
// Extract just the IP part (remove port)
54+
host, _, err := net.SplitHostPort(remoteAddr)
55+
if err != nil {
56+
// If splitting fails, use the whole remoteAddr
57+
host = remoteAddr
58+
}
59+
60+
// Only trust proxy headers if the connection is from localhost
61+
if host == "127.0.0.1" || host == "::1" || host == "localhost" {
62+
// Check X-Real-IP first (higher priority)
63+
if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
64+
return realIP
65+
}
66+
67+
// Check X-Forwarded-For (may contain multiple IPs)
68+
if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
69+
// X-Forwarded-For can contain comma-separated IPs
70+
// The first one is the original client
71+
ips := strings.Split(forwardedFor, ",")
72+
if len(ips) > 0 {
73+
// Trim any whitespace from the IP
74+
clientIP := strings.TrimSpace(ips[0])
75+
if clientIP != "" {
76+
return clientIP
77+
}
78+
}
79+
}
80+
}
81+
82+
// Return the direct connection IP if no proxy headers or not from localhost
83+
return host
84+
}
85+
4586
type WebPlugin interface {
4687
NavLinks() map[string]string // Name=>Path pairs
4788
WebRequest(r *http.Request) (html string, templateData map[string]any, ok bool) // Get the first handler of a given request
@@ -106,7 +147,7 @@ func serveTemplate(w http.ResponseWriter, r *http.Request) {
106147
}
107148

108149
if !pageFound || len(fileBase) > 0 && fileBase[0] == '_' {
109-
mudlog.Info("Web", "ip", r.RemoteAddr, "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "error", "Not found")
150+
mudlog.Info("Web", "ip", getClientIP(r), "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "error", "Not found")
110151

111152
fullPath = filepath.Join(httpRoot, `404.html`)
112153
fInfo, err = os.Stat(fullPath)
@@ -122,7 +163,7 @@ func serveTemplate(w http.ResponseWriter, r *http.Request) {
122163
}
123164

124165
// Log the request
125-
mudlog.Info("Web", "ip", r.RemoteAddr, "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "file source", source, "size", fmt.Sprintf(`%.2fk`, float64(fSize)/1024))
166+
mudlog.Info("Web", "ip", getClientIP(r), "ref", r.Header.Get("Referer"), "file path", fullPath, "file extension", fileExt, "file source", source, "size", fmt.Sprintf(`%.2fk`, float64(fSize)/1024))
126167

127168
// For non-HTML files, serve them statically.
128169
if fileExt != ".html" {

0 commit comments

Comments
 (0)