@marcel91200 / caddy Mirror · read-only / commits / d058dee1
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, 21:45 UTC

reverseproxy: Refactor dial address parsing, augment command parsing (#4616)

Francis Lavoie committed Mar 5, 2022 at 18:34 UTC d058dee11d7cfcf0b711f0378d10c9e5cabc8982
4 files changed +134 -119
caddyconfig/caddyfile/dispenser.go
+5 -1
@@ -350,7 +350,11 @@ func (d *Dispenser) Err(msg string) error {
350
351 // Errf is like Err, but for formatted error messages
352 func (d *Dispenser) Errf(format string, args ...interface{}) error {
353 - err := fmt.Errorf(format, args...)
353 + return d.WrapErr(fmt.Errorf(format, args...))
354 +}
355 +
356 +// WrapErr takes an existing error and adds the Caddyfile file and line number.
357 +func (d *Dispenser) WrapErr(err error) error {
358 return fmt.Errorf("%s:%d - Error during parsing: %w", d.File(), d.Line(), err)
359 }
360
modules/caddyhttp/reverseproxy/addresses.go new
+112
@@ -0,0 +1,112 @@
1 +// Copyright 2015 Matthew Holt and The Caddy Authors
2 +//
3 +// Licensed under the Apache License, Version 2.0 (the "License");
4 +// you may not use this file except in compliance with the License.
5 +// You may obtain a copy of the License at
6 +//
7 +// http://www.apache.org/licenses/LICENSE-2.0
8 +//
9 +// Unless required by applicable law or agreed to in writing, software
10 +// distributed under the License is distributed on an "AS IS" BASIS,
11 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 +// See the License for the specific language governing permissions and
13 +// limitations under the License.
14 +
15 +package reverseproxy
16 +
17 +import (
18 + "fmt"
19 + "net"
20 + "net/url"
21 + "strings"
22 +
23 + "github.com/caddyserver/caddy/v2"
24 +)
25 +
26 +// parseUpstreamDialAddress parses configuration inputs for
27 +// the dial address, including support for a scheme in front
28 +// as a shortcut for the port number, and a network type,
29 +// for example 'unix' to dial a unix socket.
30 +//
31 +// TODO: the logic in this function is kind of sensitive, we
32 +// need to write tests before making any more changes to it
33 +func parseUpstreamDialAddress(upstreamAddr string) (string, string, error) {
34 + var network, scheme, host, port string
35 +
36 + if strings.Contains(upstreamAddr, "://") {
37 + // we get a parsing error if a placeholder is specified
38 + // so we return a more user-friendly error message instead
39 + // to explain what to do instead
40 + if strings.Contains(upstreamAddr, "{") {
41 + return "", "", fmt.Errorf("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
42 + }
43 +
44 + toURL, err := url.Parse(upstreamAddr)
45 + if err != nil {
46 + return "", "", fmt.Errorf("parsing upstream URL: %v", err)
47 + }
48 +
49 + // there is currently no way to perform a URL rewrite between choosing
50 + // a backend and proxying to it, so we cannot allow extra components
51 + // in backend URLs
52 + if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {
53 + return "", "", fmt.Errorf("for now, URLs for proxy upstreams only support scheme, host, and port components")
54 + }
55 +
56 + // ensure the port and scheme aren't in conflict
57 + urlPort := toURL.Port()
58 + if toURL.Scheme == "http" && urlPort == "443" {
59 + return "", "", fmt.Errorf("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
60 + }
61 + if toURL.Scheme == "https" && urlPort == "80" {
62 + return "", "", fmt.Errorf("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
63 + }
64 + if toURL.Scheme == "h2c" && urlPort == "443" {
65 + return "", "", fmt.Errorf("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
66 + }
67 +
68 + // if port is missing, attempt to infer from scheme
69 + if toURL.Port() == "" {
70 + var toPort string
71 + switch toURL.Scheme {
72 + case "", "http", "h2c":
73 + toPort = "80"
74 + case "https":
75 + toPort = "443"
76 + }
77 + toURL.Host = net.JoinHostPort(toURL.Hostname(), toPort)
78 + }
79 +
80 + scheme, host, port = toURL.Scheme, toURL.Hostname(), toURL.Port()
81 + } else {
82 + // extract network manually, since caddy.ParseNetworkAddress() will always add one
83 + if idx := strings.Index(upstreamAddr, "/"); idx >= 0 {
84 + network = strings.ToLower(strings.TrimSpace(upstreamAddr[:idx]))
85 + upstreamAddr = upstreamAddr[idx+1:]
86 + }
87 + var err error
88 + host, port, err = net.SplitHostPort(upstreamAddr)
89 + if err != nil {
90 + host = upstreamAddr
91 + }
92 + // we can assume a port if only a hostname is specified, but use of a
93 + // placeholder without a port likely means a port will be filled in
94 + if port == "" && !strings.Contains(host, "{") {
95 + port = "80"
96 + }
97 + }
98 +
99 + // for simplest possible config, we only need to include
100 + // the network portion if the user specified one
101 + if network != "" {
102 + return caddy.JoinNetworkAddress(network, host, port), scheme, nil
103 + }
104 +
105 + // if the host is a placeholder, then we don't want to join with an empty port,
106 + // because that would just append an extra ':' at the end of the address.
107 + if port == "" && strings.Contains(host, "{") {
108 + return host, scheme, nil
109 + }
110 +
111 + return net.JoinHostPort(host, port), scheme, nil
112 +}
modules/caddyhttp/reverseproxy/caddyfile.go
+13 -95
@@ -18,7 +18,6 @@ import (
18 "log"
19 "net"
20 "net/http"
21 - "net/url"
21 "reflect"
22 "strconv"
23 "strings"
@@ -123,98 +122,6 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
122 // prefixed with "@" for use with "handle_response" blocks
123 h.responseMatchers = make(map[string]caddyhttp.ResponseMatcher)
124
126 - // TODO: the logic in this function is kind of sensitive, we need
127 - // to write tests before making any more changes to it
128 - upstreamDialAddress := func(upstreamAddr string) (string, error) {
129 - var network, scheme, host, port string
130 -
131 - if strings.Contains(upstreamAddr, "://") {
132 - // we get a parsing error if a placeholder is specified
133 - // so we return a more user-friendly error message instead
134 - // to explain what to do instead
135 - if strings.Contains(upstreamAddr, "{") {
136 - return "", d.Err("due to parsing difficulties, placeholders are not allowed when an upstream address contains a scheme")
137 - }
138 -
139 - toURL, err := url.Parse(upstreamAddr)
140 - if err != nil {
141 - return "", d.Errf("parsing upstream URL: %v", err)
142 - }
143 -
144 - // there is currently no way to perform a URL rewrite between choosing
145 - // a backend and proxying to it, so we cannot allow extra components
146 - // in backend URLs
147 - if toURL.Path != "" || toURL.RawQuery != "" || toURL.Fragment != "" {
148 - return "", d.Err("for now, URLs for proxy upstreams only support scheme, host, and port components")
149 - }
150 -
151 - // ensure the port and scheme aren't in conflict
152 - urlPort := toURL.Port()
153 - if toURL.Scheme == "http" && urlPort == "443" {
154 - return "", d.Err("upstream address has conflicting scheme (http://) and port (:443, the HTTPS port)")
155 - }
156 - if toURL.Scheme == "https" && urlPort == "80" {
157 - return "", d.Err("upstream address has conflicting scheme (https://) and port (:80, the HTTP port)")
158 - }
159 - if toURL.Scheme == "h2c" && urlPort == "443" {
160 - return "", d.Err("upstream address has conflicting scheme (h2c://) and port (:443, the HTTPS port)")
161 - }
162 -
163 - // if port is missing, attempt to infer from scheme
164 - if toURL.Port() == "" {
165 - var toPort string
166 - switch toURL.Scheme {
167 - case "", "http", "h2c":
168 - toPort = "80"
169 - case "https":
170 - toPort = "443"
171 - }
172 - toURL.Host = net.JoinHostPort(toURL.Hostname(), toPort)
173 - }
174 -
175 - scheme, host, port = toURL.Scheme, toURL.Hostname(), toURL.Port()
176 - } else {
177 - // extract network manually, since caddy.ParseNetworkAddress() will always add one
178 - if idx := strings.Index(upstreamAddr, "/"); idx >= 0 {
179 - network = strings.ToLower(strings.TrimSpace(upstreamAddr[:idx]))
180 - upstreamAddr = upstreamAddr[idx+1:]
181 - }
182 - var err error
183 - host, port, err = net.SplitHostPort(upstreamAddr)
184 - if err != nil {
185 - host = upstreamAddr
186 - }
187 - // we can assume a port if only a hostname is specified, but use of a
188 - // placeholder without a port likely means a port will be filled in
189 - if port == "" && !strings.Contains(host, "{") {
190 - port = "80"
191 - }
192 - }
193 -
194 - // the underlying JSON does not yet support different
195 - // transports (protocols or schemes) to each backend,
196 - // so we remember the last one we see and compare them
197 - if commonScheme != "" && scheme != commonScheme {
198 - return "", d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
199 - commonScheme, scheme)
200 - }
201 - commonScheme = scheme
202 -
203 - // for simplest possible config, we only need to include
204 - // the network portion if the user specified one
205 - if network != "" {
206 - return caddy.JoinNetworkAddress(network, host, port), nil
207 - }
208 -
209 - // if the host is a placeholder, then we don't want to join with an empty port,
210 - // because that would just append an extra ':' at the end of the address.
211 - if port == "" && strings.Contains(host, "{") {
212 - return host, nil
213 - }
214 -
215 - return net.JoinHostPort(host, port), nil
216 - }
217 -
125 // appendUpstream creates an upstream for address and adds
126 // it to the list. If the address starts with "srv+" it is
127 // treated as a SRV-based upstream, and any port will be
@@ -224,10 +131,21 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
131 if isSRV {
132 address = strings.TrimPrefix(address, "srv+")
133 }
227 - dialAddr, err := upstreamDialAddress(address)
134 +
135 + dialAddr, scheme, err := parseUpstreamDialAddress(address)
136 if err != nil {
229 - return err
137 + return d.WrapErr(err)
138 }
139 +
140 + // the underlying JSON does not yet support different
141 + // transports (protocols or schemes) to each backend,
142 + // so we remember the last one we see and compare them
143 + if commonScheme != "" && scheme != commonScheme {
144 + return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
145 + commonScheme, scheme)
146 + }
147 + commonScheme = scheme
148 +
149 if isSRV {
150 if host, _, err := net.SplitHostPort(dialAddr); err == nil {
151 dialAddr = host
modules/caddyhttp/reverseproxy/command.go
+4 -23
@@ -18,7 +18,6 @@ import (
18 "encoding/json"
19 "flag"
20 "fmt"
21 - "net"
21 "net/http"
22 "strconv"
23
@@ -104,32 +103,14 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
103 }
104
105 // set up the upstream address; assume missing information from given parts
107 - toAddr, err := httpcaddyfile.ParseAddress(to)
106 + toAddr, toScheme, err := parseUpstreamDialAddress(to)
107 if err != nil {
108 return caddy.ExitCodeFailedStartup, fmt.Errorf("invalid upstream address %s: %v", to, err)
109 }
111 - if toAddr.Path != "" {
112 - return caddy.ExitCodeFailedStartup, fmt.Errorf("paths are not allowed: %s", to)
113 - }
114 - if toAddr.Scheme == "" {
115 - if toAddr.Port == httpsPort {
116 - toAddr.Scheme = "https"
117 - } else {
118 - toAddr.Scheme = "http"
119 - }
120 - }
121 - if toAddr.Port == "" {
122 - if toAddr.Scheme == "http" {
123 - toAddr.Port = httpPort
124 - } else if toAddr.Scheme == "https" {
125 - toAddr.Port = httpsPort
126 - }
127 - }
110
111 // proceed to build the handler and server
130 -
112 ht := HTTPTransport{}
132 - if toAddr.Scheme == "https" {
113 + if toScheme == "https" {
114 ht.TLS = new(TLSConfig)
115 if insecure {
116 ht.TLS.InsecureSkipVerify = true
@@ -138,7 +119,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
119
120 handler := Handler{
121 TransportRaw: caddyconfig.JSONModuleObject(ht, "protocol", "http", nil),
141 - Upstreams: UpstreamPool{{Dial: net.JoinHostPort(toAddr.Host, toAddr.Port)}},
122 + Upstreams: UpstreamPool{{Dial: toAddr}},
123 }
124
125 if changeHost {
@@ -185,7 +166,7 @@ func cmdReverseProxy(fs caddycmd.Flags) (int, error) {
166 return caddy.ExitCodeFailedStartup, err
167 }
168
188 - fmt.Printf("Caddy proxying %s -> %s\n", fromAddr.String(), toAddr.String())
169 + fmt.Printf("Caddy proxying %s -> %s\n", fromAddr.String(), toAddr)
170
171 select {}
172 }