From ddb1d2c2b11b860f1e91b43d830d283d1e1363b2 Mon Sep 17 00:00:00 2001 From: Emily Date: Wed, 27 Mar 2024 22:36:53 +0100 Subject: [PATCH] caddyhttp: add http.request.local{,.host,.port} placeholder (#6182) * caddyhttp: add `http.request.local{,.host,.port}` placeholder This is the counterpart of `http.request.remote{,.host,.port}`. `http.request.remote` operates on the remote client's address, while `http.request.local` operates on the address the connection arrived on. Take the following example: - Caddy serving on `203.0.113.1:80` - Client on `203.0.113.2` `http.request.remote.host` would return `203.0.113.2` (client IP) `http.request.local.host` would return `203.0.113.1` (server IP) `http.request.local.port` would return `80` (server port) I find this helpful for debugging setups with multiple servers and/or multiple network paths (multiple IPs, AnyIP, Anycast). Co-authored-by: networkException * caddyhttp: add unit test for `http.request.local{,.host,.port}` * caddyhttp: add integration test for `http.request.local.port` * caddyhttp: fix `http.request.local.host` placeholder handling with unix sockets The implementation matches the one of `http.request.remote.host` now and returns the unix socket path (just like `http.request.local` already did) instead of an empty string. --------- Co-authored-by: networkException --- caddytest/integration/caddyfile_test.go | 19 +++++++++++++++++ modules/caddyhttp/app.go | 3 +++ modules/caddyhttp/replacer.go | 27 +++++++++++++++++++++++++ modules/caddyhttp/replacer_test.go | 15 ++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/caddytest/integration/caddyfile_test.go b/caddytest/integration/caddyfile_test.go index 628363a5..11ffc08a 100644 --- a/caddytest/integration/caddyfile_test.go +++ b/caddytest/integration/caddyfile_test.go @@ -517,6 +517,25 @@ func TestUriOps(t *testing.T) { tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test") } +// Tests the `http.request.local.port` placeholder. +// We don't test the very similar `http.request.local.host` placeholder, +// because depending on the host the test is running on, localhost might +// refer to 127.0.0.1 or ::1. +// TODO: Test each http version separately (especially http/3) +func TestHttpRequestLocalPortPlaceholder(t *testing.T) { + tester := caddytest.NewTester(t) + + tester.InitServer(` + { + admin localhost:2999 + http_port 9080 + } + :9080 + respond "{http.request.local.port}"`, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/", 200, "9080") +} + func TestSetThenAddQueryParams(t *testing.T) { tester := caddytest.NewTester(t) diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go index cdb368c4..f46c891f 100644 --- a/modules/caddyhttp/app.go +++ b/modules/caddyhttp/app.go @@ -68,6 +68,9 @@ func init() { // `{http.request.orig_uri.query}` | The request's original query string (without `?`) // `{http.request.port}` | The port part of the request's Host header // `{http.request.proto}` | The protocol of the request +// `{http.request.local.host}` | The host (IP) part of the local address the connection arrived on +// `{http.request.local.port}` | The port part of the local address the connection arrived on +// `{http.request.local}` | The local address the connection arrived on // `{http.request.remote.host}` | The host (IP) part of the remote client's address // `{http.request.remote.port}` | The port part of the remote client's address // `{http.request.remote}` | The address of the remote client diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go index 10258da0..1cf3ec47 100644 --- a/modules/caddyhttp/replacer.go +++ b/modules/caddyhttp/replacer.go @@ -119,11 +119,38 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo return port, true case "http.request.hostport": return req.Host, true + case "http.request.local": + localAddr, _ := req.Context().Value(http.LocalAddrContextKey).(net.Addr) + return localAddr.String(), true + case "http.request.local.host": + localAddr, _ := req.Context().Value(http.LocalAddrContextKey).(net.Addr) + host, _, err := net.SplitHostPort(localAddr.String()) + if err != nil { + // localAddr is host:port for tcp and udp sockets and /unix/socket.path + // for unix sockets. net.SplitHostPort only operates on tcp and udp sockets, + // not unix sockets and will fail with the latter. + // We assume when net.SplitHostPort fails, localAddr is a unix socket and thus + // already "split" and save to return. + return localAddr, true + } + return host, true + case "http.request.local.port": + localAddr, _ := req.Context().Value(http.LocalAddrContextKey).(net.Addr) + _, port, _ := net.SplitHostPort(localAddr.String()) + if portNum, err := strconv.Atoi(port); err == nil { + return portNum, true + } + return port, true case "http.request.remote": return req.RemoteAddr, true case "http.request.remote.host": host, _, err := net.SplitHostPort(req.RemoteAddr) if err != nil { + // req.RemoteAddr is host:port for tcp and udp sockets and /unix/socket.path + // for unix sockets. net.SplitHostPort only operates on tcp and udp sockets, + // not unix sockets and will fail with the latter. + // We assume when net.SplitHostPort fails, req.RemoteAddr is a unix socket + // and thus already "split" and save to return. return req.RemoteAddr, true } return host, true diff --git a/modules/caddyhttp/replacer_test.go b/modules/caddyhttp/replacer_test.go index 44c6f2a8..50a2e8c6 100644 --- a/modules/caddyhttp/replacer_test.go +++ b/modules/caddyhttp/replacer_test.go @@ -19,6 +19,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/pem" + "net" "net/http" "net/http/httptest" "testing" @@ -29,7 +30,9 @@ import ( func TestHTTPVarReplacement(t *testing.T) { req, _ := http.NewRequest(http.MethodGet, "/foo/bar.tar.gz", nil) repl := caddy.NewReplacer() + localAddr, _ := net.ResolveTCPAddr("tcp", "192.168.159.1:80") ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + ctx = context.WithValue(ctx, http.LocalAddrContextKey, localAddr) req = req.WithContext(ctx) req.Host = "example.com:80" req.RemoteAddr = "192.168.159.32:1234" @@ -95,6 +98,18 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV get: "http.request.hostport", expect: "example.com:80", }, + { + get: "http.request.local.host", + expect: "192.168.159.1", + }, + { + get: "http.request.local.port", + expect: "80", + }, + { + get: "http.request.local", + expect: "192.168.159.1:80", + }, { get: "http.request.remote.host", expect: "192.168.159.32",