summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMohammed Al Sahaf <[email protected]>2023-08-05 23:30:02 +0200
committerGitHub <[email protected]>2023-08-05 23:30:02 +0200
commit65e33fc1ee4798bb3450f6e291bfc88404982636 (patch)
tree6df2a4fc05295246c3cb9ab412fbe6d2828be8fd
parent9f34383c02f1691e54280285a6499893fcbbb4c7 (diff)
downloadcaddy-65e33fc1ee4798bb3450f6e291bfc88404982636.tar.gz
caddy-65e33fc1ee4798bb3450f6e291bfc88404982636.zip
reverseproxy: do not parse upstream address too early if it contains replaceble parts (#5695)
* reverseproxy: do not parse upstream address too early if it contains replaceble parts * remove unused method * cleanup * accommodate partially replaceable port
-rw-r--r--caddytest/integration/caddyfile_adapt/replaceable_upstream.txt100
-rw-r--r--caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.txt100
-rw-r--r--caddytest/integration/caddyfile_adapt/replaceable_upstream_port.txt100
-rw-r--r--modules/caddyhttp/reverseproxy/addresses.go67
-rw-r--r--modules/caddyhttp/reverseproxy/addresses_test.go10
-rw-r--r--modules/caddyhttp/reverseproxy/caddyfile.go20
-rw-r--r--modules/caddyhttp/reverseproxy/command.go8
7 files changed, 365 insertions, 40 deletions
diff --git a/caddytest/integration/caddyfile_adapt/replaceable_upstream.txt b/caddytest/integration/caddyfile_adapt/replaceable_upstream.txt
new file mode 100644
index 000000000..202e33043
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/replaceable_upstream.txt
@@ -0,0 +1,100 @@
+*.sandbox.localhost {
+ @sandboxPort {
+ header_regexp first_label Host ^([0-9]{3})\.sandbox\.
+ }
+ handle @sandboxPort {
+ reverse_proxy {re.first_label.1}
+ }
+ handle {
+ redir {scheme}://application.localhost
+ }
+}
+
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "*.sandbox.localhost"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "{http.regexp.first_label.1}"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "header_regexp": {
+ "Host": {
+ "name": "first_label",
+ "pattern": "^([0-9]{3})\\.sandbox\\."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "static_response",
+ "headers": {
+ "Location": [
+ "{http.request.scheme}://application.localhost"
+ ]
+ },
+ "status_code": 302
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.txt b/caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.txt
new file mode 100644
index 000000000..7fbcb5c78
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.txt
@@ -0,0 +1,100 @@
+*.sandbox.localhost {
+ @sandboxPort {
+ header_regexp port Host ^([0-9]{3})\.sandbox\.
+ }
+ handle @sandboxPort {
+ reverse_proxy app:6{re.port.1}
+ }
+ handle {
+ redir {scheme}://application.localhost
+ }
+}
+
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "*.sandbox.localhost"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "app:6{http.regexp.port.1}"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "header_regexp": {
+ "Host": {
+ "name": "port",
+ "pattern": "^([0-9]{3})\\.sandbox\\."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "static_response",
+ "headers": {
+ "Location": [
+ "{http.request.scheme}://application.localhost"
+ ]
+ },
+ "status_code": 302
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/caddytest/integration/caddyfile_adapt/replaceable_upstream_port.txt b/caddytest/integration/caddyfile_adapt/replaceable_upstream_port.txt
new file mode 100644
index 000000000..8f75c5bd0
--- /dev/null
+++ b/caddytest/integration/caddyfile_adapt/replaceable_upstream_port.txt
@@ -0,0 +1,100 @@
+*.sandbox.localhost {
+ @sandboxPort {
+ header_regexp port Host ^([0-9]{3})\.sandbox\.
+ }
+ handle @sandboxPort {
+ reverse_proxy app:{re.port.1}
+ }
+ handle {
+ redir {scheme}://application.localhost
+ }
+}
+
+----------
+{
+ "apps": {
+ "http": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":443"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "host": [
+ "*.sandbox.localhost"
+ ]
+ }
+ ],
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "reverse_proxy",
+ "upstreams": [
+ {
+ "dial": "app:{http.regexp.port.1}"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "match": [
+ {
+ "header_regexp": {
+ "Host": {
+ "name": "port",
+ "pattern": "^([0-9]{3})\\.sandbox\\."
+ }
+ }
+ }
+ ]
+ },
+ {
+ "group": "group2",
+ "handle": [
+ {
+ "handler": "subroute",
+ "routes": [
+ {
+ "handle": [
+ {
+ "handler": "static_response",
+ "headers": {
+ "Location": [
+ "{http.request.scheme}://application.localhost"
+ ]
+ },
+ "status_code": 302
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "terminal": true
+ }
+ ]
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/caddyhttp/reverseproxy/addresses.go b/modules/caddyhttp/reverseproxy/addresses.go
index 6078f1194..093949739 100644
--- a/modules/caddyhttp/reverseproxy/addresses.go
+++ b/modules/caddyhttp/reverseproxy/addresses.go
@@ -23,11 +23,43 @@ import (
"github.com/caddyserver/caddy/v2"
)
+type parsedAddr struct {
+ network, scheme, host, port string
+ valid bool
+}
+
+func (p parsedAddr) dialAddr() string {
+ if !p.valid {
+ return ""
+ }
+ // for simplest possible config, we only need to include
+ // the network portion if the user specified one
+ if p.network != "" {
+ return caddy.JoinNetworkAddress(p.network, p.host, p.port)
+ }
+
+ // if the host is a placeholder, then we don't want to join with an empty port,
+ // because that would just append an extra ':' at the end of the address.
+ if p.port == "" && strings.Contains(p.host, "{") {
+ return p.host
+ }
+ return net.JoinHostPort(p.host, p.port)
+}
+func (p parsedAddr) rangedPort() bool {
+ return strings.Contains(p.port, "-")
+}
+func (p parsedAddr) replaceablePort() bool {
+ return strings.Contains(p.port, "{") && strings.Contains(p.port, "}")
+}
+func (p parsedAddr) isUnix() bool {
+ return caddy.IsUnixNetwork(p.network)
+}
+
// parseUpstreamDialAddress parses configuration inputs for
// the dial address, including support for a scheme in front
// as a shortcut for the port number, and a network type,
// for example 'unix' to dial a unix socket.
-func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
+func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
var network, scheme, host, port string
if strings.Contains(upstreamAddr, "://") {
@@ -35,7 +67,7 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
// so we return a more user-friendly error message instead
// to explain what to do instead
if strings.Contains(upstreamAddr, "{") {
- return "", "", fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
+ return parsedAddr{}, fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
}
toURL, err := url.Parse(upstreamAddr)
@@ -46,19 +78,19 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
if strings.Contains(err.Error(), "invalid port") && strings.Contains(err.Error(), "-") {
index := strings.LastIndex(upstreamAddr, ":")
if index == -1 {
- return "", "", fmt.Errorf("parsing upstream URL: %v", err)
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
}
portRange := upstreamAddr[index+1:]
if strings.Count(portRange, "-") != 1 {
- return "", "", fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
}
toURL, err = url.Parse(strings.ReplaceAll(upstreamAddr, portRange, "0"))
if err != nil {
- return "", "", fmt.Errorf("parsing upstream URL: %v", err)
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
}
port = portRange
} else {
- return "", "", fmt.Errorf("parsing upstream URL: %v", err)
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
}
}
if port == "" {
@@ -69,18 +101,18 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
// a backend and proxying to it, so we cannot allow extra components
// in backend URLs
if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {
- return "", "", fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
+ return parsedAddr{}, fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
}
// ensure the port and scheme aren't in conflict
if toURL.Scheme == "http" && port == "443" {
- return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
+ return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
}
if toURL.Scheme == "https" && port == "80" {
- return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
+ return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
}
if toURL.Scheme == "h2c" && port == "443" {
- return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
+ return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
}
// if port is missing, attempt to infer from scheme
@@ -112,18 +144,5 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
network = "unix"
scheme = "h2c"
}
-
- // for simplest possible config, we only need to include
- // the network portion if the user specified one
- if network != "" {
- return caddy.JoinNetworkAddress(network, host, port), scheme, nil
- }
-
- // if the host is a placeholder, then we don't want to join with an empty port,
- // because that would just append an extra ':' at the end of the address.
- if port == "" && strings.Contains(host, "{") {
- return host, scheme, nil
- }
-
- return net.JoinHostPort(host, port), scheme, nil
+ return parsedAddr{network, scheme, host, port, true}, nil
}
diff --git a/modules/caddyhttp/reverseproxy/addresses_test.go b/modules/caddyhttp/reverseproxy/addresses_test.go
index 0c7ad7b31..0c5141942 100644
--- a/modules/caddyhttp/reverseproxy/addresses_test.go
+++ b/modules/caddyhttp/reverseproxy/addresses_test.go
@@ -265,18 +265,18 @@ func TestParseUpstreamDialAddress(t *testing.T) {
expectScheme: "h2c",
},
} {
- actualHostPort, actualScheme, err := parseUpstreamDialAddress(tc.input)
+ actualAddr, err := parseUpstreamDialAddress(tc.input)
if tc.expectErr && err == nil {
t.Errorf("Test %d: Expected error but got %v", i, err)
}
if !tc.expectErr && err != nil {
t.Errorf("Test %d: Expected no error but got %v", i, err)
}
- if actualHostPort != tc.expectHostPort {
- t.Errorf("Test %d: Expected host and port '%s' but got '%s'", i, tc.expectHostPort, actualHostPort)
+ if actualAddr.dialAddr() != tc.expectHostPort {
+ t.Errorf("Test %d: input %s: Expected host and port '%s' but got '%s'", i, tc.input, tc.expectHostPort, actualAddr.dialAddr())
}
- if actualScheme != tc.expectScheme {
- t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualScheme)
+ if actualAddr.scheme != tc.expectScheme {
+ t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualAddr.scheme)
}
}
}
diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go
index 1d86bebd7..f5eb7a5d0 100644
--- a/modules/caddyhttp/reverseproxy/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/caddyfile.go
@@ -146,7 +146,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// appendUpstream creates an upstream for address and adds
// it to the list.
appendUpstream := func(address string) error {
- dialAddr, scheme, err := parseUpstreamDialAddress(address)
+ pa, err := parseUpstreamDialAddress(address)
if err != nil {
return d.WrapErr(err)
}
@@ -154,21 +154,27 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// the underlying JSON does not yet support different
// transports (protocols or schemes) to each backend,
// so we remember the last one we see and compare them
- if commonScheme != "" && scheme != commonScheme {
+ if commonScheme != "" && pa.scheme != commonScheme {
return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
- commonScheme, scheme)
+ commonScheme, pa.scheme)
}
- commonScheme = scheme
+ commonScheme = pa.scheme
- parsedAddr, err := caddy.ParseNetworkAddress(dialAddr)
+ // if the port of upstream address contains a placeholder, only wrap it with the `Upstream` struct,
+ // delaying actual resolution of the address until request time.
+ if pa.replaceablePort() {
+ h.Upstreams = append(h.Upstreams, &Upstream{Dial: pa.dialAddr()})
+ return nil
+ }
+ parsedAddr, err := caddy.ParseNetworkAddress(pa.dialAddr())
if err != nil {
return d.WrapErr(err)
}
- if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 {
+ if pa.isUnix() || !pa.rangedPort() {
// unix networks don't have ports
h.Upstreams = append(h.Upstreams, &Upstream{
- Dial: dialAddr,
+ Dial: pa.dialAddr(),
})
} else {
// expand a port range into multiple upstreams
diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go
index 48fabd56d..8438b7278 100644
--- a/modules/caddyhttp/reverseproxy/command.go
+++ b/modules/caddyhttp/reverseproxy/command.go
@@ -132,14 +132,14 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
toAddresses := make([]string, len(to))
var toScheme string
for i, toLoc := range to {
- addr, scheme, err := parseUpstreamDialAddress(toLoc)
+ addr, err := parseUpstreamDialAddress(toLoc)
if err != nil {
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
}
- if scheme != "" && toScheme == "" {
- toScheme = scheme
+ if addr.scheme != "" && toScheme == "" {
+ toScheme = addr.scheme
}
- toAddresses[i] = addr
+ toAddresses[i] = addr.dialAddr()
}
// proceed to build the handler and server