@marcel91200 / caddy Mirror · read-only / commits / 17357303
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 12, 02:00 UTC

core: add modular `network_proxy` support (#6399)

* core: add modular `network_proxy` support Co-authored-by: @ImpostorKeanu Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com> * move modules around Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com> * add caddyfile implementation Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com> * address feedbcak * Apply suggestions from code review Co-authored-by: Francis Lavoie <lavofr@gmail.com> * adapt ForwardProxyURL to use the NetworkProxyRaw Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com> * remove redundant `url` in log Co-authored-by: Matt Holt <mholt@users.noreply.github.com> * code review Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com> * remove `.source` from the module ID Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com> --------- Signed-off-by: Mohammed Al Sahaf <msaa1990@gmail.com> Co-authored-by: Francis Lavoie <lavofr@gmail.com> Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

Mohammed Al Sahaf committed Mar 21, 2025 at 20:06 UTC 173573035c7484bb4aad4498a90bf5a1cf1bb5be
8 files changed +347 -12
caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_forward_proxy_url.txt new
+41
@@ -0,0 +1,41 @@
1 +:8884
2 +reverse_proxy 127.0.0.1:65535 {
3 + transport http {
4 + forward_proxy_url http://localhost:8080
5 + }
6 +}
7 +----------
8 +{
9 + "apps": {
10 + "http": {
11 + "servers": {
12 + "srv0": {
13 + "listen": [
14 + ":8884"
15 + ],
16 + "routes": [
17 + {
18 + "handle": [
19 + {
20 + "handler": "reverse_proxy",
21 + "transport": {
22 + "network_proxy": {
23 + "from": "url",
24 + "url": "http://localhost:8080"
25 + },
26 + "protocol": "http"
27 + },
28 + "upstreams": [
29 + {
30 + "dial": "127.0.0.1:65535"
31 + }
32 + ]
33 + }
34 + ]
35 + }
36 + ]
37 + }
38 + }
39 + }
40 + }
41 +}
caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_none_proxy.txt new
+40
@@ -0,0 +1,40 @@
1 +:8884
2 +reverse_proxy 127.0.0.1:65535 {
3 + transport http {
4 + network_proxy none
5 + }
6 +}
7 +----------
8 +{
9 + "apps": {
10 + "http": {
11 + "servers": {
12 + "srv0": {
13 + "listen": [
14 + ":8884"
15 + ],
16 + "routes": [
17 + {
18 + "handle": [
19 + {
20 + "handler": "reverse_proxy",
21 + "transport": {
22 + "network_proxy": {
23 + "from": "none"
24 + },
25 + "protocol": "http"
26 + },
27 + "upstreams": [
28 + {
29 + "dial": "127.0.0.1:65535"
30 + }
31 + ]
32 + }
33 + ]
34 + }
35 + ]
36 + }
37 + }
38 + }
39 + }
40 +}
caddytest/integration/caddyfile_adapt/reverse_proxy_http_transport_url_proxy.txt new
+41
@@ -0,0 +1,41 @@
1 +:8884
2 +reverse_proxy 127.0.0.1:65535 {
3 + transport http {
4 + network_proxy url http://localhost:8080
5 + }
6 +}
7 +----------
8 +{
9 + "apps": {
10 + "http": {
11 + "servers": {
12 + "srv0": {
13 + "listen": [
14 + ":8884"
15 + ],
16 + "routes": [
17 + {
18 + "handle": [
19 + {
20 + "handler": "reverse_proxy",
21 + "transport": {
22 + "network_proxy": {
23 + "from": "url",
24 + "url": "http://localhost:8080"
25 + },
26 + "protocol": "http"
27 + },
28 + "upstreams": [
29 + {
30 + "dial": "127.0.0.1:65535"
31 + }
32 + ]
33 + }
34 + ]
35 + }
36 + ]
37 + }
38 + }
39 + }
40 + }
41 +}
modules.go
+10
@@ -18,6 +18,8 @@ import (
18 "bytes"
19 "encoding/json"
20 "fmt"
21 + "net/http"
22 + "net/url"
23 "reflect"
24 "sort"
25 "strings"
@@ -360,6 +362,14 @@ func isModuleMapType(typ reflect.Type) bool {
362 isJSONRawMessage(typ.Elem())
363 }
364
365 +// ProxyFuncProducer is implemented by modules which produce a
366 +// function that returns a URL to use as network proxy. Modules
367 +// in the namespace `caddy.network_proxy` must implement this
368 +// interface.
369 +type ProxyFuncProducer interface {
370 + ProxyFunc() func(*http.Request) (*url.URL, error)
371 +}
372 +
373 var (
374 modules = make(map[string]ModuleInfo)
375 modulesMu sync.RWMutex
modules/caddyhttp/reverseproxy/caddyfile.go
+22 -2
@@ -33,6 +33,7 @@ import (
33 "github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
34 "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
35 "github.com/caddyserver/caddy/v2/modules/caddytls"
36 + "github.com/caddyserver/caddy/v2/modules/internal/network"
37 )
38
39 func init() {
@@ -979,7 +980,9 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error
980 // read_buffer <size>
981 // write_buffer <size>
982 // max_response_header <size>
982 -// forward_proxy_url <url>
983 +// network_proxy <module> {
984 +// ...
985 +// }
986 // dial_timeout <duration>
987 // dial_fallback_delay <duration>
988 // response_header_timeout <duration>
@@ -990,6 +993,9 @@ func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error
993 // tls_insecure_skip_verify
994 // tls_timeout <duration>
995 // tls_trusted_ca_certs <cert_files...>
996 +// tls_trust_pool <module> {
997 +// ...
998 +// }
999 // tls_server_name <sni>
1000 // tls_renegotiation <level>
1001 // tls_except_ports <ports...>
@@ -1068,10 +1074,24 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
1074 }
1075
1076 case "forward_proxy_url":
1077 + caddy.Log().Warn("The 'forward_proxy_url' field is deprecated. Use 'network_proxy <url>' instead.")
1078 if !d.NextArg() {
1079 return d.ArgErr()
1080 }
1074 - h.ForwardProxyURL = d.Val()
1081 + u := network.ProxyFromURL{URL: d.Val()}
1082 + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil)
1083 +
1084 + case "network_proxy":
1085 + if !d.NextArg() {
1086 + return d.ArgErr()
1087 + }
1088 + modStem := d.Val()
1089 + modID := "caddy.network_proxy." + modStem
1090 + unm, err := caddyfile.UnmarshalModule(d, modID)
1091 + if err != nil {
1092 + return err
1093 + }
1094 + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(unm, "from", modStem, nil)
1095
1096 case "dial_timeout":
1097 if !d.NextArg() {
modules/caddyhttp/reverseproxy/httptransport.go
+32 -8
@@ -24,7 +24,6 @@ import (
24 weakrand "math/rand"
25 "net"
26 "net/http"
27 - "net/url"
27 "os"
28 "reflect"
29 "slices"
@@ -38,8 +37,10 @@ import (
37 "golang.org/x/net/http2"
38
39 "github.com/caddyserver/caddy/v2"
40 + "github.com/caddyserver/caddy/v2/caddyconfig"
41 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
42 "github.com/caddyserver/caddy/v2/modules/caddytls"
43 + "github.com/caddyserver/caddy/v2/modules/internal/network"
44 )
45
46 func init() {
@@ -90,6 +91,7 @@ type HTTPTransport struct {
91 // forward_proxy_url -> upstream
92 //
93 // Default: http.ProxyFromEnvironment
94 + // DEPRECATED: Use NetworkProxyRaw|`network_proxy` instead. Subject to removal.
95 ForwardProxyURL string `json:"forward_proxy_url,omitempty"`
96
97 // How long to wait before timing out trying to connect to
@@ -141,6 +143,22 @@ type HTTPTransport struct {
143 // The pre-configured underlying HTTP transport.
144 Transport *http.Transport `json:"-"`
145
146 + // The module that provides the network (forward) proxy
147 + // URL that the HTTP transport will use to proxy
148 + // requests to the upstream. See [http.Transport.Proxy](https://pkg.go.dev/net/http#Transport.Proxy)
149 + // for information regarding supported protocols.
150 + //
151 + // Providing a value to this parameter results in requests
152 + // flowing through the reverse_proxy in the following way:
153 + //
154 + // User Agent ->
155 + // reverse_proxy ->
156 + // [proxy provided by the module] -> upstream
157 + //
158 + // If nil, defaults to reading the `HTTP_PROXY`,
159 + // `HTTPS_PROXY`, and `NO_PROXY` environment variables.
160 + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"`
161 +
162 h2cTransport *http2.Transport
163 h3Transport *http3.Transport // TODO: EXPERIMENTAL (May 2024)
164 }
@@ -328,16 +346,22 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
346 }
347
348 // negotiate any HTTP/SOCKS proxy for the HTTP transport
331 - var proxy func(*http.Request) (*url.URL, error)
349 + proxy := http.ProxyFromEnvironment
350 if h.ForwardProxyURL != "" {
333 - pUrl, err := url.Parse(h.ForwardProxyURL)
351 + caddyCtx.Logger().Warn("forward_proxy_url is deprecated; use network_proxy instead")
352 + u := network.ProxyFromURL{URL: h.ForwardProxyURL}
353 + h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil)
354 + }
355 + if len(h.NetworkProxyRaw) != 0 {
356 + proxyMod, err := caddyCtx.LoadModule(h, "ForwardProxyRaw")
357 if err != nil {
335 - return nil, fmt.Errorf("failed to parse transport proxy url: %v", err)
358 + return nil, fmt.Errorf("failed to load network_proxy module: %v", err)
359 + }
360 + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok {
361 + proxy = m.ProxyFunc()
362 + } else {
363 + return nil, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``")
364 }
337 - caddyCtx.Logger().Info("setting transport proxy url", zap.String("url", h.ForwardProxyURL))
338 - proxy = http.ProxyURL(pUrl)
339 - } else {
340 - proxy = http.ProxyFromEnvironment
365 }
366
367 rt := &http.Transport{
modules/caddytls/acmeissuer.go
+17 -2
@@ -106,6 +106,9 @@ type ACMEIssuer struct {
106 // be used. EXPERIMENTAL: Subject to change.
107 CertificateLifetime caddy.Duration `json:"certificate_lifetime,omitempty"`
108
109 + // Forward proxy module
110 + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy inline_key=from"`
111 +
112 rootPool *x509.CertPool
113 logger *zap.Logger
114
@@ -194,7 +197,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
197 }
198
199 var err error
197 - iss.template, err = iss.makeIssuerTemplate()
200 + iss.template, err = iss.makeIssuerTemplate(ctx)
201 if err != nil {
202 return err
203 }
@@ -202,7 +205,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error {
205 return nil
206 }
207
205 -func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) {
208 +func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssuer, error) {
209 template := certmagic.ACMEIssuer{
210 CA: iss.CA,
211 TestCA: iss.TestCA,
@@ -216,6 +219,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) {
219 Logger: iss.logger,
220 }
221
222 + if len(iss.NetworkProxyRaw) != 0 {
223 + proxyMod, err := ctx.LoadModule(iss, "ForwardProxyRaw")
224 + if err != nil {
225 + return template, fmt.Errorf("failed to load network_proxy module: %v", err)
226 + }
227 + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok {
228 + template.HTTPProxy = m.ProxyFunc()
229 + } else {
230 + return template, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``")
231 + }
232 + }
233 +
234 if iss.Challenges != nil {
235 if iss.Challenges.HTTP != nil {
236 template.DisableHTTPChallenge = iss.Challenges.HTTP.Disabled
modules/internal/network/networkproxy.go new
+144
@@ -0,0 +1,144 @@
1 +package network
2 +
3 +import (
4 + "errors"
5 + "net/http"
6 + "net/url"
7 + "strings"
8 +
9 + "go.uber.org/zap"
10 +
11 + "github.com/caddyserver/caddy/v2"
12 + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
13 +)
14 +
15 +func init() {
16 + caddy.RegisterModule(ProxyFromURL{})
17 + caddy.RegisterModule(ProxyFromNone{})
18 +}
19 +
20 +// The "url" proxy source uses the defined URL as the proxy
21 +type ProxyFromURL struct {
22 + URL string `json:"url"`
23 +
24 + ctx caddy.Context
25 + logger *zap.Logger
26 +}
27 +
28 +// CaddyModule implements Module.
29 +func (p ProxyFromURL) CaddyModule() caddy.ModuleInfo {
30 + return caddy.ModuleInfo{
31 + ID: "caddy.network_proxy.url",
32 + New: func() caddy.Module {
33 + return &ProxyFromURL{}
34 + },
35 + }
36 +}
37 +
38 +func (p *ProxyFromURL) Provision(ctx caddy.Context) error {
39 + p.ctx = ctx
40 + p.logger = ctx.Logger()
41 + return nil
42 +}
43 +
44 +// Validate implements Validator.
45 +func (p ProxyFromURL) Validate() error {
46 + if _, err := url.Parse(p.URL); err != nil {
47 + return err
48 + }
49 + return nil
50 +}
51 +
52 +// ProxyFunc implements ProxyFuncProducer.
53 +func (p ProxyFromURL) ProxyFunc() func(*http.Request) (*url.URL, error) {
54 + if strings.Contains(p.URL, "{") && strings.Contains(p.URL, "}") {
55 + // courtesy of @ImpostorKeanu: https://github.com/caddyserver/caddy/pull/6397
56 + return func(r *http.Request) (*url.URL, error) {
57 + // retrieve the replacer from context.
58 + repl, ok := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
59 + if !ok {
60 + err := errors.New("failed to obtain replacer from request")
61 + p.logger.Error(err.Error())
62 + return nil, err
63 + }
64 +
65 + // apply placeholders to the value
66 + // note: h.ForwardProxyURL should never be empty at this point
67 + s := repl.ReplaceAll(p.URL, "")
68 + if s == "" {
69 + p.logger.Error("network_proxy URL was empty after applying placeholders",
70 + zap.String("initial_value", p.URL),
71 + zap.String("final_value", s),
72 + zap.String("hint", "check for invalid placeholders"))
73 + return nil, errors.New("empty value for network_proxy URL")
74 + }
75 +
76 + // parse the url
77 + pUrl, err := url.Parse(s)
78 + if err != nil {
79 + p.logger.Warn("failed to derive transport proxy from network_proxy URL")
80 + pUrl = nil
81 + } else if pUrl.Host == "" || strings.Split("", pUrl.Host)[0] == ":" {
82 + // url.Parse does not return an error on these values:
83 + //
84 + // - http://:80
85 + // - pUrl.Host == ":80"
86 + // - /some/path
87 + // - pUrl.Host == ""
88 + //
89 + // Super edge cases, but humans are human.
90 + err = errors.New("supplied network_proxy URL is missing a host value")
91 + pUrl = nil
92 + } else {
93 + p.logger.Debug("setting transport proxy url", zap.String("url", s))
94 + }
95 +
96 + return pUrl, err
97 + }
98 + }
99 + return func(r *http.Request) (*url.URL, error) {
100 + return url.Parse(p.URL)
101 + }
102 +}
103 +
104 +// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
105 +func (p *ProxyFromURL) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
106 + d.Next()
107 + d.Next()
108 + p.URL = d.Val()
109 + return nil
110 +}
111 +
112 +// The "none" proxy source module disables the use of network proxy.
113 +type ProxyFromNone struct{}
114 +
115 +func (p ProxyFromNone) CaddyModule() caddy.ModuleInfo {
116 + return caddy.ModuleInfo{
117 + ID: "caddy.network_proxy.none",
118 + New: func() caddy.Module {
119 + return &ProxyFromNone{}
120 + },
121 + }
122 +}
123 +
124 +// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
125 +func (p ProxyFromNone) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
126 + return nil
127 +}
128 +
129 +// ProxyFunc implements ProxyFuncProducer.
130 +func (p ProxyFromNone) ProxyFunc() func(*http.Request) (*url.URL, error) {
131 + return nil
132 +}
133 +
134 +var (
135 + _ caddy.Module = ProxyFromURL{}
136 + _ caddy.Provisioner = (*ProxyFromURL)(nil)
137 + _ caddy.Validator = ProxyFromURL{}
138 + _ caddy.ProxyFuncProducer = ProxyFromURL{}
139 + _ caddyfile.Unmarshaler = (*ProxyFromURL)(nil)
140 +
141 + _ caddy.Module = ProxyFromNone{}
142 + _ caddy.ProxyFuncProducer = ProxyFromNone{}
143 + _ caddyfile.Unmarshaler = ProxyFromNone{}
144 +)