@marcel91200 / caddy Mirror · read-only / commits / 546752e2
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 07:15 UTC

feat(fastcgi): populate SERVER_ADDR by default (#7912)

* feat(fastcgi): populate SERVER_ADDR by default Populate the SERVER_ADDR FastCGI environment variable using the local socket address of the incoming connection from the request context. This improves out-of-the-box compatibility for PHP applications that rely on $_SERVER['SERVER_ADDR'] (such as legacy frameworks and custom telemetry packages) when migrating from Nginx or Apache. Co-developed-by: Gemini AI <renich+gemini@woralelandia.com> Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com> * fix(fastcgi): validate SERVER_ADDR IP format and add test coverage Validate that SERVER_ADDR is only populated when the local address parses as a valid IP address, preventing Unix socket paths from being assigned if SplitHostPort fails. Add test cases covering IPv4, IPv6, missing context, Unix sockets, and explicit overrides. Co-developed-by: Gemini AI <renich+gemini@woralelandia.com> Signed-off-by: Rénich Bon Ćirić <renich@woralelandia.com>

Renich Bon Ciric committed Aug 9, 2026 at 23:24 UTC 546752e2141f36ef292ef533a5e42cadaa559758
2 files changed +88
modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+12
@@ -365,6 +365,18 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
365 "SCRIPT_NAME": scriptName,
366 }
367
368 + if localAddr, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
369 + var ipStr string
370 + if host, _, err := net.SplitHostPort(localAddr.String()); err == nil {
371 + ipStr = host
372 + } else {
373 + ipStr = localAddr.String()
374 + }
375 + if ip := net.ParseIP(ipStr); ip != nil {
376 + env["SERVER_ADDR"] = ipStr
377 + }
378 + }
379 +
380 // compliance with the CGI specification requires that
381 // PATH_TRANSLATED should only exist if PATH_INFO is defined.
382 // Info: https://www.ietf.org/rfc/rfc3875 Page 14
modules/caddyhttp/reverseproxy/fastcgi/fastcgi_test.go
+76
@@ -1,6 +1,9 @@
1 package fastcgi
2
3 import (
4 + "context"
5 + "net"
6 + "net/http"
7 "strings"
8 "testing"
9
@@ -8,6 +11,7 @@ import (
11 "github.com/stretchr/testify/require"
12
13 "github.com/caddyserver/caddy/v2"
14 + "github.com/caddyserver/caddy/v2/modules/caddyhttp"
15 )
16
17 func TestProvisionSplitPath(t *testing.T) {
@@ -355,3 +359,75 @@ func TestSplitPosSecurityRegressionUnicodeBypass(t *testing.T) {
359 assert.Equalf(t, -1, tr.splitPos(p), "payload %q must not be detected as .php", p)
360 }
361 }
362 +
363 +func TestBuildEnvServerAddr(t *testing.T) {
364 + testCases := []struct {
365 + name string
366 + localAddr net.Addr
367 + envVars map[string]string
368 + expectedAddr string
369 + expectExists bool
370 + }{
371 + {
372 + name: "IPv4 TCP listener",
373 + localAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.12"), Port: 80},
374 + expectedAddr: "10.0.0.12",
375 + expectExists: true,
376 + },
377 + {
378 + name: "IPv6 TCP listener",
379 + localAddr: &net.TCPAddr{IP: net.ParseIP("2001:db8::1"), Port: 443},
380 + expectedAddr: "2001:db8::1",
381 + expectExists: true,
382 + },
383 + {
384 + name: "Missing local address context",
385 + localAddr: nil,
386 + expectExists: false,
387 + },
388 + {
389 + name: "Unix listener",
390 + localAddr: &net.UnixAddr{Name: "/var/run/caddy.sock", Net: "unix"},
391 + expectExists: false,
392 + },
393 + {
394 + name: "Explicitly configured SERVER_ADDR override",
395 + localAddr: &net.TCPAddr{IP: net.ParseIP("10.0.0.12"), Port: 80},
396 + envVars: map[string]string{"SERVER_ADDR": "192.168.1.100"},
397 + expectedAddr: "192.168.1.100",
398 + expectExists: true,
399 + },
400 + }
401 +
402 + for _, tc := range testCases {
403 + t.Run(tc.name, func(t *testing.T) {
404 + tr := Transport{
405 + Root: "/var/www",
406 + EnvVars: tc.envVars,
407 + }
408 + err := tr.Provision(caddy.Context{})
409 + require.NoError(t, err)
410 +
411 + req, err := http.NewRequest(http.MethodGet, "http://localhost/index.php", nil)
412 + require.NoError(t, err)
413 +
414 + req = req.WithContext(context.WithValue(req.Context(), caddyhttp.OriginalRequestCtxKey, *req))
415 + repl := caddy.NewReplacer()
416 + req = req.WithContext(context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl))
417 +
418 + if tc.localAddr != nil {
419 + req = req.WithContext(context.WithValue(req.Context(), http.LocalAddrContextKey, tc.localAddr))
420 + }
421 +
422 + env, err := tr.buildEnv(req)
423 + require.NoError(t, err)
424 +
425 + val, exists := env["SERVER_ADDR"]
426 + assert.Equal(t, tc.expectExists, exists)
427 + if tc.expectExists {
428 + assert.Equal(t, tc.expectedAddr, val)
429 + }
430 + })
431 + }
432 +}
433 +