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, 00:30 UTC
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
Mohammed Al Sahaf committed
Aug 5, 2023 at 23:30 UTC
65e33fc1ee4798bb3450f6e291bfc88404982636
7 files changed
+365
-40
caddytest/integration/caddyfile_adapt/replaceable_upstream.txt
new
+100
@@ -0,0 +1,100 @@
1
+*.sandbox.localhost {
2
+ @sandboxPort {
3
+ header_regexp first_label Host ^([0-9]{3})\.sandbox\.
4
+ }
5
+ handle @sandboxPort {
6
+ reverse_proxy {re.first_label.1}
7
+ }
8
+ handle {
9
+ redir {scheme}://application.localhost
10
+ }
11
+}
12
+
13
+----------
14
+{
15
+ "apps": {
16
+ "http": {
17
+ "servers": {
18
+ "srv0": {
19
+ "listen": [
20
+ ":443"
21
+ ],
22
+ "routes": [
23
+ {
24
+ "match": [
25
+ {
26
+ "host": [
27
+ "*.sandbox.localhost"
28
+ ]
29
+ }
30
+ ],
31
+ "handle": [
32
+ {
33
+ "handler": "subroute",
34
+ "routes": [
35
+ {
36
+ "group": "group2",
37
+ "handle": [
38
+ {
39
+ "handler": "subroute",
40
+ "routes": [
41
+ {
42
+ "handle": [
43
+ {
44
+ "handler": "reverse_proxy",
45
+ "upstreams": [
46
+ {
47
+ "dial": "{http.regexp.first_label.1}"
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+ ],
56
+ "match": [
57
+ {
58
+ "header_regexp": {
59
+ "Host": {
60
+ "name": "first_label",
61
+ "pattern": "^([0-9]{3})\\.sandbox\\."
62
+ }
63
+ }
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "group": "group2",
69
+ "handle": [
70
+ {
71
+ "handler": "subroute",
72
+ "routes": [
73
+ {
74
+ "handle": [
75
+ {
76
+ "handler": "static_response",
77
+ "headers": {
78
+ "Location": [
79
+ "{http.request.scheme}://application.localhost"
80
+ ]
81
+ },
82
+ "status_code": 302
83
+ }
84
+ ]
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ }
92
+ ],
93
+ "terminal": true
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ }
99
+ }
100
+}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/replaceable_upstream_partial_port.txt
new
+100
@@ -0,0 +1,100 @@
1
+*.sandbox.localhost {
2
+ @sandboxPort {
3
+ header_regexp port Host ^([0-9]{3})\.sandbox\.
4
+ }
5
+ handle @sandboxPort {
6
+ reverse_proxy app:6{re.port.1}
7
+ }
8
+ handle {
9
+ redir {scheme}://application.localhost
10
+ }
11
+}
12
+
13
+----------
14
+{
15
+ "apps": {
16
+ "http": {
17
+ "servers": {
18
+ "srv0": {
19
+ "listen": [
20
+ ":443"
21
+ ],
22
+ "routes": [
23
+ {
24
+ "match": [
25
+ {
26
+ "host": [
27
+ "*.sandbox.localhost"
28
+ ]
29
+ }
30
+ ],
31
+ "handle": [
32
+ {
33
+ "handler": "subroute",
34
+ "routes": [
35
+ {
36
+ "group": "group2",
37
+ "handle": [
38
+ {
39
+ "handler": "subroute",
40
+ "routes": [
41
+ {
42
+ "handle": [
43
+ {
44
+ "handler": "reverse_proxy",
45
+ "upstreams": [
46
+ {
47
+ "dial": "app:6{http.regexp.port.1}"
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+ ],
56
+ "match": [
57
+ {
58
+ "header_regexp": {
59
+ "Host": {
60
+ "name": "port",
61
+ "pattern": "^([0-9]{3})\\.sandbox\\."
62
+ }
63
+ }
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "group": "group2",
69
+ "handle": [
70
+ {
71
+ "handler": "subroute",
72
+ "routes": [
73
+ {
74
+ "handle": [
75
+ {
76
+ "handler": "static_response",
77
+ "headers": {
78
+ "Location": [
79
+ "{http.request.scheme}://application.localhost"
80
+ ]
81
+ },
82
+ "status_code": 302
83
+ }
84
+ ]
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ }
92
+ ],
93
+ "terminal": true
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ }
99
+ }
100
+}
\ No newline at end of file
caddytest/integration/caddyfile_adapt/replaceable_upstream_port.txt
new
+100
@@ -0,0 +1,100 @@
1
+*.sandbox.localhost {
2
+ @sandboxPort {
3
+ header_regexp port Host ^([0-9]{3})\.sandbox\.
4
+ }
5
+ handle @sandboxPort {
6
+ reverse_proxy app:{re.port.1}
7
+ }
8
+ handle {
9
+ redir {scheme}://application.localhost
10
+ }
11
+}
12
+
13
+----------
14
+{
15
+ "apps": {
16
+ "http": {
17
+ "servers": {
18
+ "srv0": {
19
+ "listen": [
20
+ ":443"
21
+ ],
22
+ "routes": [
23
+ {
24
+ "match": [
25
+ {
26
+ "host": [
27
+ "*.sandbox.localhost"
28
+ ]
29
+ }
30
+ ],
31
+ "handle": [
32
+ {
33
+ "handler": "subroute",
34
+ "routes": [
35
+ {
36
+ "group": "group2",
37
+ "handle": [
38
+ {
39
+ "handler": "subroute",
40
+ "routes": [
41
+ {
42
+ "handle": [
43
+ {
44
+ "handler": "reverse_proxy",
45
+ "upstreams": [
46
+ {
47
+ "dial": "app:{http.regexp.port.1}"
48
+ }
49
+ ]
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+ ],
56
+ "match": [
57
+ {
58
+ "header_regexp": {
59
+ "Host": {
60
+ "name": "port",
61
+ "pattern": "^([0-9]{3})\\.sandbox\\."
62
+ }
63
+ }
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "group": "group2",
69
+ "handle": [
70
+ {
71
+ "handler": "subroute",
72
+ "routes": [
73
+ {
74
+ "handle": [
75
+ {
76
+ "handler": "static_response",
77
+ "headers": {
78
+ "Location": [
79
+ "{http.request.scheme}://application.localhost"
80
+ ]
81
+ },
82
+ "status_code": 302
83
+ }
84
+ ]
85
+ }
86
+ ]
87
+ }
88
+ ]
89
+ }
90
+ ]
91
+ }
92
+ ],
93
+ "terminal": true
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ }
99
+ }
100
+}
\ No newline at end of file
modules/caddyhttp/reverseproxy/addresses.go
+43
-24
@@ -23,11 +23,43 @@ import (
23
"github.com/caddyserver/caddy/v2"
24
)
25
26
+type parsedAddr struct {
27
+ network, scheme, host, port string
28
+ valid bool
29
+}
30
+
31
+func (p parsedAddr) dialAddr() string {
32
+ if !p.valid {
33
+ return ""
34
+ }
35
+ // for simplest possible config, we only need to include
36
+ // the network portion if the user specified one
37
+ if p.network != "" {
38
+ return caddy.JoinNetworkAddress(p.network, p.host, p.port)
39
+ }
40
+
41
+ // if the host is a placeholder, then we don't want to join with an empty port,
42
+ // because that would just append an extra ':' at the end of the address.
43
+ if p.port == "" && strings.Contains(p.host, "{") {
44
+ return p.host
45
+ }
46
+ return net.JoinHostPort(p.host, p.port)
47
+}
48
+func (p parsedAddr) rangedPort() bool {
49
+ return strings.Contains(p.port, "-")
50
+}
51
+func (p parsedAddr) replaceablePort() bool {
52
+ return strings.Contains(p.port, "{") && strings.Contains(p.port, "}")
53
+}
54
+func (p parsedAddr) isUnix() bool {
55
+ return caddy.IsUnixNetwork(p.network)
56
+}
57
+
58
// parseUpstreamDialAddress parses configuration inputs for
59
// the dial address, including support for a scheme in front
60
// as a shortcut for the port number, and a network type,
61
// for example 'unix' to dial a unix socket.
30
-func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
62
+func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
63
var network, scheme, host, port string
64
65
if strings.Contains(upstreamAddr, "://") {
@@ -35,7 +67,7 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
67
// so we return a more user-friendly error message instead
68
// to explain what to do instead
69
if strings.Contains(upstreamAddr, "{") {
38
- return "", "", fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
70
+ return parsedAddr{}, fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
71
}
72
73
toURL, err := url.Parse(upstreamAddr)
@@ -46,19 +78,19 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
78
if strings.Contains(err.Error(), "invalid port") && strings.Contains(err.Error(), "-") {
79
index := strings.LastIndex(upstreamAddr, ":")
80
if index == -1 {
49
- return "", "", fmt.Errorf("parsing upstream URL: %v", err)
81
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
82
}
83
portRange := upstreamAddr[index+1:]
84
if strings.Count(portRange, "-") != 1 {
53
- return "", "", fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
85
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: parse \"%v\": port range invalid: %v", upstreamAddr, portRange)
86
}
87
toURL, err = url.Parse(strings.ReplaceAll(upstreamAddr, portRange, "0"))
88
if err != nil {
57
- return "", "", fmt.Errorf("parsing upstream URL: %v", err)
89
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
90
}
91
port = portRange
92
} else {
61
- return "", "", fmt.Errorf("parsing upstream URL: %v", err)
93
+ return parsedAddr{}, fmt.Errorf("parsing upstream URL: %v", err)
94
}
95
}
96
if port == "" {
@@ -69,18 +101,18 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
101
// a backend and proxying to it, so we cannot allow extra components
102
// in backend URLs
103
if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {
72
- return "", "", fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
104
+ return parsedAddr{}, fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
105
}
106
107
// ensure the port and scheme aren't in conflict
108
if toURL.Scheme == "http" && port == "443" {
77
- return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
109
+ return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
110
}
111
if toURL.Scheme == "https" && port == "80" {
80
- return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
112
+ return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
113
}
114
if toURL.Scheme == "h2c" && port == "443" {
83
- return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
115
+ return parsedAddr{}, fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
116
}
117
118
// if port is missing, attempt to infer from scheme
@@ -112,18 +144,5 @@ func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
144
network = "unix"
145
scheme = "h2c"
146
}
115
-
116
- // for simplest possible config, we only need to include
117
- // the network portion if the user specified one
118
- if network != "" {
119
- return caddy.JoinNetworkAddress(network, host, port), scheme, nil
120
- }
121
-
122
- // if the host is a placeholder, then we don't want to join with an empty port,
123
- // because that would just append an extra ':' at the end of the address.
124
- if port == "" && strings.Contains(host, "{") {
125
- return host, scheme, nil
126
- }
127
-
128
- return net.JoinHostPort(host, port), scheme, nil
147
+ return parsedAddr{network, scheme, host, port, true}, nil
148
}
modules/caddyhttp/reverseproxy/addresses_test.go
+5
-5
@@ -265,18 +265,18 @@ func TestParseUpstreamDialAddress(t *testing.T) {
265
expectScheme: "h2c",
266
},
267
} {
268
- actualHostPort, actualScheme, err := parseUpstreamDialAddress(tc.input)
268
+ actualAddr, err := parseUpstreamDialAddress(tc.input)
269
if tc.expectErr && err == nil {
270
t.Errorf("Test %d: Expected error but got %v", i, err)
271
}
272
if !tc.expectErr && err != nil {
273
t.Errorf("Test %d: Expected no error but got %v", i, err)
274
}
275
- if actualHostPort != tc.expectHostPort {
276
- t.Errorf("Test %d: Expected host and port '%s' but got '%s'", i, tc.expectHostPort, actualHostPort)
275
+ if actualAddr.dialAddr() != tc.expectHostPort {
276
+ t.Errorf("Test %d: input %s: Expected host and port '%s' but got '%s'", i, tc.input, tc.expectHostPort, actualAddr.dialAddr())
277
}
278
- if actualScheme != tc.expectScheme {
279
- t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualScheme)
278
+ if actualAddr.scheme != tc.expectScheme {
279
+ t.Errorf("Test %d: Expected scheme '%s' but got '%s'", i, tc.expectScheme, actualAddr.scheme)
280
}
281
}
282
}
modules/caddyhttp/reverseproxy/caddyfile.go
+13
-7
@@ -146,7 +146,7 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
146
// appendUpstream creates an upstream for address and adds
147
// it to the list.
148
appendUpstream := func(address string) error {
149
- dialAddr, scheme, err := parseUpstreamDialAddress(address)
149
+ pa, err := parseUpstreamDialAddress(address)
150
if err != nil {
151
return d.WrapErr(err)
152
}
@@ -154,21 +154,27 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
154
// the underlying JSON does not yet support different
155
// transports (protocols or schemes) to each backend,
156
// so we remember the last one we see and compare them
157
- if commonScheme != "" && scheme != commonScheme {
157
+ if commonScheme != "" && pa.scheme != commonScheme {
158
return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
159
- commonScheme, scheme)
159
+ commonScheme, pa.scheme)
160
}
161
- commonScheme = scheme
161
+ commonScheme = pa.scheme
162
163
- parsedAddr, err := caddy.ParseNetworkAddress(dialAddr)
163
+ // if the port of upstream address contains a placeholder, only wrap it with the `Upstream` struct,
164
+ // delaying actual resolution of the address until request time.
165
+ if pa.replaceablePort() {
166
+ h.Upstreams = append(h.Upstreams, &Upstream{Dial: pa.dialAddr()})
167
+ return nil
168
+ }
169
+ parsedAddr, err := caddy.ParseNetworkAddress(pa.dialAddr())
170
if err != nil {
171
return d.WrapErr(err)
172
}
173
168
- if parsedAddr.StartPort == 0 && parsedAddr.EndPort == 0 {
174
+ if pa.isUnix() || !pa.rangedPort() {
175
// unix networks don't have ports
176
h.Upstreams = append(h.Upstreams, &Upstream{
171
- Dial: dialAddr,
177
+ Dial: pa.dialAddr(),
178
})
179
} else {
180
// expand a port range into multiple upstreams
modules/caddyhttp/reverseproxy/command.go
+4
-4
@@ -132,14 +132,14 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
132
toAddresses := make([]string, len(to))
133
var toScheme string
134
for i, toLoc := range to {
135
- addr, scheme, err := parseUpstreamDialAddress(toLoc)
135
+ addr, err := parseUpstreamDialAddress(toLoc)
136
if err != nil {
137
return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", toLoc, err)
138
}
139
- if scheme != "" && toScheme == "" {
140
- toScheme = scheme
139
+ if addr.scheme != "" && toScheme == "" {
140
+ toScheme = addr.scheme
141
}
142
- toAddresses[i] = addr
142
+ toAddresses[i] = addr.dialAddr()
143
}
144
145
// proceed to build the handler and server