@marcel91200 / caddy Mirror · read-only / commits / 75b690d2
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 13, 01:45 UTC

reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile (#5494)

* reverseproxy: Expand port ranges to multiple upstreams in CLI + Caddyfile * Add clarifying comment

Francis Lavoie committed May 15, 2023 at 14:14 UTC 75b690d248c7681dd974f6179c98a363af417a25
5 files changed +173 -15
caddytest/integration/caddyfile_adapt/reverse_proxy_port_range.txt new
+67
@@ -0,0 +1,67 @@
1 +:8884 {
2 + # Port range
3 + reverse_proxy localhost:8001-8002
4 +
5 + # Port range with placeholder
6 + reverse_proxy {host}:8001-8002
7 +
8 + # Port range with scheme
9 + reverse_proxy https://localhost:8001-8002
10 +}
11 +----------
12 +{
13 + "apps": {
14 + "http": {
15 + "servers": {
16 + "srv0": {
17 + "listen": [
18 + ":8884"
19 + ],
20 + "routes": [
21 + {
22 + "handle": [
23 + {
24 + "handler": "reverse_proxy",
25 + "upstreams": [
26 + {
27 + "dial": "localhost:8001"
28 + },
29 + {
30 + "dial": "localhost:8002"
31 + }
32 + ]
33 + },
34 + {
35 + "handler": "reverse_proxy",
36 + "upstreams": [
37 + {
38 + "dial": "{http.request.host}:8001"
39 + },
40 + {
41 + "dial": "{http.request.host}:8002"
42 + }
43 + ]
44 + },
45 + {
46 + "handler": "reverse_proxy",
47 + "transport": {
48 + "protocol": "http",
49 + "tls": {}
50 + },
51 + "upstreams": [
52 + {
53 + "dial": "localhost:8001"
54 + },
55 + {
56 + "dial": "localhost:8002"
57 + }
58 + ]
59 + }
60 + ]
61 + }
62 + ]
63 + }
64 + }
65 + }
66 + }
67 +}
\ No newline at end of file
modules/caddyhttp/reverseproxy/addresses.go
+30 -11
@@ -40,7 +40,29 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
40
41 toURL, err := url.Parse(upstreamAddr)
42 if err != nil {
43 - return "", "", fmt.Errorf("parsing upstream URL: %v", err)
43 + // if the error seems to be due to a port range,
44 + // try to replace the port range with a dummy
45 + // single port so that url.Parse() will succeed
46 + if strings.Contains(err.Error(), "invalid port") && strings.Contains(err.Error(), "-") {
47 + index := strings.LastIndex(upstreamAddr, ":")
48 + if index == -1 {
49 + return "", "", fmt.Errorf("parsing upstream URL: %v", err)
50 + }
51 + portRange := upstreamAddr[index+1:]
52 + if strings.Count(portRange, "-") != 1 {
53 + return "", "", fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
54 + }
55 + toURL, err = url.Parse(strings.ReplaceAll(upstreamAddr, portRange, "0"))
56 + if err != nil {
57 + return "", "", fmt.Errorf("parsing upstream URL: %v", err)
58 + }
59 + port = portRange
60 + } else {
61 + return "", "", fmt.Errorf("parsing upstream URL: %v", err)
62 + }
63 + }
64 + if port == "" {
65 + port = toURL.Port()
66 }
67
68 // there is currently no way to perform a URL rewrite between choosing
@@ -51,30 +73,27 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
73 }
74
75 // ensure the port and scheme aren't in conflict
54 - urlPort := toURL.Port()
55 - if toURL.Scheme == "http" && urlPort == "443" {
76 + if toURL.Scheme == "http" && port == "443" {
77 return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
78 }
58 - if toURL.Scheme == "https" && urlPort == "80" {
79 + if toURL.Scheme == "https" && port == "80" {
80 return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
81 }
61 - if toURL.Scheme == "h2c" && urlPort == "443" {
82 + if toURL.Scheme == "h2c" && port == "443" {
83 return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
84 }
85
86 // if port is missing, attempt to infer from scheme
66 - if toURL.Port() == "" {
67 - var toPort string
87 + if port == "" {
88 switch toURL.Scheme {
89 case "", "http", "h2c":
70 - toPort = "80"
90 + port = "80"
91 case "https":
72 - toPort = "443"
92 + port = "443"
93 }
74 - toURL.Host = net.JoinHostPort(toURL.Hostname(), toPort)
94 }
95
77 - scheme, host, port = toURL.Scheme, toURL.Hostname(), toURL.Port()
96 + scheme, host = toURL.Scheme, toURL.Hostname()
97 } else {
98 var err error
99 network, host, port, err = caddy.SplitNetworkAddress(upstreamAddr)
modules/caddyhttp/reverseproxy/addresses_test.go
+38
@@ -149,6 +149,24 @@ func TestParseUpstreamDialAddress(t *testing.T) {
149 expectHostPort: "[::1]:1234",
150 expectScheme: "h2c",
151 },
152 + {
153 + input: "localhost:1001-1009",
154 + expectHostPort: "localhost:1001-1009",
155 + },
156 + {
157 + input: "{host}:1001-1009",
158 + expectHostPort: "{host}:1001-1009",
159 + },
160 + {
161 + input: "http://localhost:1001-1009",
162 + expectHostPort: "localhost:1001-1009",
163 + expectScheme: "http",
164 + },
165 + {
166 + input: "https://localhost:1001-1009",
167 + expectHostPort: "localhost:1001-1009",
168 + expectScheme: "https",
169 + },
170 {
171 input: "unix//var/php.sock",
172 expectHostPort: "unix//var/php.sock",
@@ -196,6 +214,26 @@ func TestParseUpstreamDialAddress(t *testing.T) {
214 input: "http://localhost#fragment",
215 expectErr: true,
216 },
217 + {
218 + input: "http://localhost:8001-8002-8003",
219 + expectErr: true,
220 + },
221 + {
222 + input: "http://localhost:8001-8002/foo:bar",
223 + expectErr: true,
224 + },
225 + {
226 + input: "http://localhost:8001-8002/foo:1",
227 + expectErr: true,
228 + },
229 + {
230 + input: "http://localhost:8001-8002/foo:1-2",
231 + expectErr: true,
232 + },
233 + {
234 + input: "http://localhost:8001-8002#foo:1",
235 + expectErr: true,
236 + },
237 {
238 input: "http://foo:443",
239 expectErr: true,
modules/caddyhttp/reverseproxy/caddyfile.go
+20 -1
@@ -15,6 +15,7 @@
15 package reverseproxy
16
17 import (
18 + "fmt"
19 "net/http"
20 "reflect"
21 "strconv"
@@ -157,7 +158,25 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
158 }
159 commonScheme = scheme
160
160 - h.Upstreams = append(h.Upstreams, &Upstream{Dial: dialAddr})
161 + parsedAddr, err := caddy.ParseNetworkAddress(dialAddr)
162 + if err != nil {
163 + return d.WrapErr(err)
164 + }
165 +
166 + if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 {
167 + // unix networks don't have ports
168 + h.Upstreams = append(h.Upstreams, &Upstream{
169 + Dial: dialAddr,
170 + })
171 + } else {
172 + // expand a port range into multiple upstreams
173 + for i := parsedAddr.StartPort; i <= parsedAddr.EndPort; i++ {
174 + h.Upstreams = append(h.Upstreams, &Upstream{
175 + Dial: caddy.JoinNetworkAddress("", parsedAddr.Host, fmt.Sprint(i)),
176 + })
177 + }
178 + }
179 +
180 return nil
181 }
182
modules/caddyhttp/reverseproxy/command.go
+18 -3
@@ -153,9 +153,24 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
153
154 upstreamPool := UpstreamPool{}
155 for _, toAddr := range toAddresses {
156 - upstreamPool = append(upstreamPool, &Upstream{
157 - Dial: toAddr,
158 - })
156 + parsedAddr, err := caddy.ParseNetworkAddress(toAddr)
157 + if err != nil {
158 + return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toAddr, err)
159 + }
160 +
161 + if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 {
162 + // unix networks don't have ports
163 + upstreamPool = append(upstreamPool, &Upstream{
164 + Dial: toAddr,
165 + })
166 + } else {
167 + // expand a port range into multiple upstreams
168 + for i := parsedAddr.StartPort; i <= parsedAddr.EndPort; i++ {
169 + upstreamPool = append(upstreamPool, &Upstream{
170 + Dial: caddy.JoinNetworkAddress("", parsedAddr.Host, fmt.Sprint(i)),
171 + })
172 + }
173 + }
174 }
175
176 handler := Handler{