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{