From 75b690d248c7681dd974f6179c98a363af417a25 Mon Sep 17 00:00:00 2001 From: Francis Lavoie Date: Mon, 15 May 2023 14:14:50 -0400 Subject: reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile (#5494) * reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile * Add clarifying comment --- modules/caddyhttp/reverseproxy/addresses.go | 41 +++++++++++++++++------- modules/caddyhttp/reverseproxy/addresses_test.go | 38 ++++++++++++++++++++++ modules/caddyhttp/reverseproxy/caddyfile.go | 21 +++++++++++- modules/caddyhttp/reverseproxy/command.go | 21 ++++++++++-- 4 files changed, 106 insertions(+), 15 deletions(-) (limited to 'modules/caddyhttp') diff --git a/modules/caddyhttp/reverseproxy/addresses.go b/modules/caddyhttp/reverseproxy/addresses.go index 8152108..6078f11 100644 --- a/modules/caddyhttp/reverseproxy/addresses.go +++ b/modules/caddyhttp/reverseproxy/addresses.go @@ -40,7 +40,29 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) { toURL, err := url.Parse(upstreamAddr) if err != nil { - return "", "", fmt.Errorf("parsing upstream URL: %v", err) + // if the error seems to be due to a port range, + // try to replace the port range with a dummy + // single port so that url.Parse() will succeed + 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) + } + portRange := upstreamAddr[index+1:] + if strings.Count(portRange, "-") != 1 { + return "", "", 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) + } + port = portRange + } else { + return "", "", fmt.Errorf("parsing upstream URL: %v", err) + } + } + if port == "" { + port = toURL.Port() } // there is currently no way to perform a URL rewrite between choosing @@ -51,30 +73,27 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) { } // ensure the port and scheme aren't in conflict - urlPort := toURL.Port() - if toURL.Scheme == "http" && urlPort == "443" { + if toURL.Scheme == "http" && port == "443" { return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)") } - if toURL.Scheme == "https" && urlPort == "80" { + if toURL.Scheme == "https" && port == "80" { return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)") } - if toURL.Scheme == "h2c" && urlPort == "443" { + if toURL.Scheme == "h2c" && port == "443" { return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)") } // if port is missing, attempt to infer from scheme - if toURL.Port() == "" { - var toPort string + if port == "" { switch toURL.Scheme { case "", "http", "h2c": - toPort = "80" + port = "80" case "https": - toPort = "443" + port = "443" } - toURL.Host = net.JoinHostPort(toURL.Hostname(), toPort) } - scheme, host, port = toURL.Scheme, toURL.Hostname(), toURL.Port() + scheme, host = toURL.Scheme, toURL.Hostname() } else { var err error network, host, port, err = caddy.SplitNetworkAddress(upstreamAddr) diff --git a/modules/caddyhttp/reverseproxy/addresses_test.go b/modules/caddyhttp/reverseproxy/addresses_test.go index 6355c75..0c7ad7b 100644 --- a/modules/caddyhttp/reverseproxy/addresses_test.go +++ b/modules/caddyhttp/reverseproxy/addresses_test.go @@ -149,6 +149,24 @@ func TestParseUpstreamDialAddress(t *testing.T) { expectHostPort: "[::1]:1234", expectScheme: "h2c", }, + { + input: "localhost:1001-1009", + expectHostPort: "localhost:1001-1009", + }, + { + input: "{host}:1001-1009", + expectHostPort: "{host}:1001-1009", + }, + { + input: "http://localhost:1001-1009", + expectHostPort: "localhost:1001-1009", + expectScheme: "http", + }, + { + input: "https://localhost:1001-1009", + expectHostPort: "localhost:1001-1009", + expectScheme: "https", + }, { input: "unix//var/php.sock", expectHostPort: "unix//var/php.sock", @@ -196,6 +214,26 @@ func TestParseUpstreamDialAddress(t *testing.T) { input: "http://localhost#fragment", expectErr: true, }, + { + input: "http://localhost:8001-8002-8003", + expectErr: true, + }, + { + input: "http://localhost:8001-8002/foo:bar", + expectErr: true, + }, + { + input: "http://localhost:8001-8002/foo:1", + expectErr: true, + }, + { + input: "http://localhost:8001-8002/foo:1-2", + expectErr: true, + }, + { + input: "http://localhost:8001-8002#foo:1", + expectErr: true, + }, { input: "http://foo:443", expectErr: true, diff --git a/modules/caddyhttp/reverseproxy/caddyfile.go b/modules/caddyhttp/reverseproxy/caddyfile.go index 728bc2f..a79bd09 100644 --- a/modules/caddyhttp/reverseproxy/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/caddyfile.go @@ -15,6 +15,7 @@ package reverseproxy import ( + "fmt" "net/http" "reflect" "strconv" @@ -157,7 +158,25 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } commonScheme = scheme - h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr}) + parsedAddr, err := caddy.ParseNetworkAddress(dialAddr) + if err != nil { + return d.WrapErr(err) + } + + if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 { + // unix networks don't have ports + h.Upstreams = append(h.Upstreams, &Upstream{ + Dial: dialAddr, + }) + } else { + // expand a port range into multiple upstreams + for i := parsedAddr.StartPort; i <= parsedAddr.EndPort; i++ { + h.Upstreams = append(h.Upstreams, &Upstream{ + Dial: caddy.JoinNetworkAddress("", parsedAddr.Host, fmt.Sprint(i)), + }) + } + } + return nil } diff --git a/modules/caddyhttp/reverseproxy/command.go b/modules/caddyhttp/reverseproxy/command.go index bd3efcd..48fabd5 100644 --- a/modules/caddyhttp/reverseproxy/command.go +++ b/modules/caddyhttp/reverseproxy/command.go @@ -153,9 +153,24 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) { upstreamPool := UpstreamPool{} for _, toAddr := range toAddresses { - upstreamPool = append(upstreamPool, &Upstream{ - Dial: toAddr, - }) + parsedAddr, err := caddy.ParseNetworkAddress(toAddr) + if err != nil { + return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toAddr, err) + } + + if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 { + // unix networks don't have ports + upstreamPool = append(upstreamPool, &Upstream{ + Dial: toAddr, + }) + } else { + // expand a port range into multiple upstreams + for i := parsedAddr.StartPort; i <= parsedAddr.EndPort; i++ { + upstreamPool = append(upstreamPool, &Upstream{ + Dial: caddy.JoinNetworkAddress("", parsedAddr.Host, fmt.Sprint(i)), + }) + } + } } handler := Handler{ -- cgit v1.2.3