@marcel91200 / caddy Mirror · read-only / commits / 65e33fc1
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, 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