From 62c711c66e49e892d7d7ed0592823b842d3cff46 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 15 Jun 2024 02:08:17 +0300 Subject: [PATCH] core: add modular `network_proxy` support Co-authored-by: @ImpostorKeanu Signed-off-by: Mohammed Al Sahaf --- .../caddyhttp/reverseproxy/httptransport.go | 19 ++- modules/caddytls/acmeissuer.go | 19 ++- network.go | 128 ++++++++++++++++++ 3 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 network.go diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go index 80a498066..ab3a47d94 100644 --- a/modules/caddyhttp/reverseproxy/httptransport.go +++ b/modules/caddyhttp/reverseproxy/httptransport.go @@ -135,6 +135,9 @@ type HTTPTransport struct { // The pre-configured underlying HTTP transport. Transport *http.Transport `json:"-"` + // Forward proxy module + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy.source inline_key=from"` + h2cTransport *http2.Transport h3Transport *http3.RoundTripper // TODO: EXPERIMENTAL (May 2024) } @@ -297,7 +300,19 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } // negotiate any HTTP/SOCKS proxy for the HTTP transport - var proxy func(*http.Request) (*url.URL, error) + proxy := http.ProxyFromEnvironment + if len(h.NetworkProxyRaw) != 0 { + proxyMod, err := caddyCtx.LoadModule(h, "ForwardProxyRaw") + if err != nil { + return nil, fmt.Errorf("failed to load network_proxy module: %v", err) + } + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok { + proxy = m.ProxyFunc() + } else { + return nil, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") + } + } + if h.ForwardProxyURL != "" { pUrl, err := url.Parse(h.ForwardProxyURL) if err != nil { @@ -305,8 +320,6 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e } caddyCtx.Logger().Info("setting transport proxy url", zap.String("url", h.ForwardProxyURL)) proxy = http.ProxyURL(pUrl) - } else { - proxy = http.ProxyFromEnvironment } rt := &http.Transport{ diff --git a/modules/caddytls/acmeissuer.go b/modules/caddytls/acmeissuer.go index f46e296e3..38fae46eb 100644 --- a/modules/caddytls/acmeissuer.go +++ b/modules/caddytls/acmeissuer.go @@ -97,6 +97,9 @@ type ACMEIssuer struct { // be used. EXPERIMENTAL: Subject to change. CertificateLifetime caddy.Duration `json:"certificate_lifetime,omitempty"` + // Forward proxy module + NetworkProxyRaw json.RawMessage `json:"network_proxy,omitempty" caddy:"namespace=caddy.network_proxy.source inline_key=from"` + rootPool *x509.CertPool logger *zap.Logger @@ -170,7 +173,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { } var err error - iss.template, err = iss.makeIssuerTemplate() + iss.template, err = iss.makeIssuerTemplate(ctx) if err != nil { return err } @@ -178,7 +181,7 @@ func (iss *ACMEIssuer) Provision(ctx caddy.Context) error { return nil } -func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { +func (iss *ACMEIssuer) makeIssuerTemplate(ctx caddy.Context) (certmagic.ACMEIssuer, error) { template := certmagic.ACMEIssuer{ CA: iss.CA, TestCA: iss.TestCA, @@ -191,6 +194,18 @@ func (iss *ACMEIssuer) makeIssuerTemplate() (certmagic.ACMEIssuer, error) { Logger: iss.logger, } + if len(iss.NetworkProxyRaw) != 0 { + proxyMod, err := ctx.LoadModule(iss, "ForwardProxyRaw") + if err != nil { + return template, fmt.Errorf("failed to load network_proxy module: %v", err) + } + if m, ok := proxyMod.(caddy.ProxyFuncProducer); ok { + template.HTTPProxy = m.ProxyFunc() + } else { + return template, fmt.Errorf("network_proxy module is not `(func(*http.Request) (*url.URL, error))``") + } + } + if iss.Challenges != nil { if iss.Challenges.HTTP != nil { template.DisableHTTPChallenge = iss.Challenges.HTTP.Disabled diff --git a/network.go b/network.go new file mode 100644 index 000000000..72c612762 --- /dev/null +++ b/network.go @@ -0,0 +1,128 @@ +package caddy + +import ( + "errors" + "net/http" + "net/url" + "strings" + + "go.uber.org/zap" +) + +func init() { + RegisterModule(ProxyFromEnvironment{}) + RegisterModule(ProxyFromURL{}) +} + +type ProxyFuncProducer interface { + ProxyFunc() func(*http.Request) (*url.URL, error) +} + +type ProxyFromEnvironment struct{} + +// ProxyFunc implements ProxyFuncProducer. +func (p ProxyFromEnvironment) ProxyFunc() func(*http.Request) (*url.URL, error) { + return http.ProxyFromEnvironment +} + +// CaddyModule implements Module. +func (p ProxyFromEnvironment) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "caddy.network_proxy.source.environment", + New: func() Module { + return ProxyFromEnvironment{} + }, + } +} + +type ProxyFromURL struct { + URL string `json:"url"` + + ctx Context + logger *zap.Logger +} + +// CaddyModule implements Module. +func (p ProxyFromURL) CaddyModule() ModuleInfo { + return ModuleInfo{ + ID: "caddy.network_proxy.source.url", + New: func() Module { + return &ProxyFromURL{} + }, + } +} + +func (p *ProxyFromURL) Provision(ctx Context) error { + p.ctx = ctx + p.logger = ctx.Logger() + return nil +} + +// Validate implements Validator. +func (p ProxyFromURL) Validate() error { + if _, err := url.Parse(p.URL); err != nil { + return err + } + return nil +} + +// ProxyFunc implements ProxyFuncProducer. +func (p ProxyFromURL) ProxyFunc() func(*http.Request) (*url.URL, error) { + if strings.Contains(p.URL, "{") && strings.Contains(p.URL, "}") { + // courtesy of @ImpostorKeanu: https://github.com/caddyserver/caddy/pull/6397 + return func(r *http.Request) (*url.URL, error) { + // retrieve the replacer from context. + repl, ok := r.Context().Value(ReplacerCtxKey).(*Replacer) + if !ok { + err := errors.New("failed to obtain replacer from request") + p.logger.Error(err.Error()) + return nil, err + } + + // apply placeholders to the value + // note: h.ForwardProxyURL should never be empty at this point + s := repl.ReplaceAll(p.URL, "") + if s == "" { + p.logger.Error("forward_proxy_url was empty after applying placeholders", + zap.String("initial_value", p.URL), + zap.String("final_value", s), + zap.String("hint", "check for invalid placeholders")) + return nil, errors.New("empty value for forward_proxy_url") + } + + // parse the url + pUrl, err := url.Parse(s) + if err != nil { + p.logger.Warn("failed to derive transport proxy from forward_proxy_url") + pUrl = nil + } else if pUrl.Host == "" || strings.Split("", pUrl.Host)[0] == ":" { + // url.Parse does not return an error on these values: + // + // - http://:80 + // - pUrl.Host == ":80" + // - /some/path + // - pUrl.Host == "" + // + // Super edge cases, but humans are human. + err = errors.New("supplied forward_proxy_url is missing a host value") + pUrl = nil + } else { + p.logger.Debug("setting transport proxy url", zap.String("url", s)) + } + + return pUrl, err + } + } + return func(*http.Request) (*url.URL, error) { + return url.Parse(p.URL) + } +} + +var ( + _ Module = ProxyFromEnvironment{} + _ ProxyFuncProducer = ProxyFromEnvironment{} + _ Module = ProxyFromURL{} + _ Provisioner = &ProxyFromURL{} + _ Validator = ProxyFromURL{} + _ ProxyFuncProducer = ProxyFromURL{} +)