Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: add modular network_proxy support #6399

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport http {
forward_proxy none
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"network_proxy": {
"from": "none"
},
"protocol": "http"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
:8884
reverse_proxy 127.0.0.1:65535 {
transport http {
forward_proxy url http://localhost:8080
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8884"
],
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"transport": {
"network_proxy": {
"from": "url",
"url": "http://localhost:8080"
},
"protocol": "http"
},
"upstreams": [
{
"dial": "127.0.0.1:65535"
}
]
}
]
}
]
}
}
}
}
}
16 changes: 16 additions & 0 deletions modules/caddyhttp/reverseproxy/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,22 @@ func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
h.ForwardProxyURL = d.Val()

case "forward_proxy":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's "network_proxy" should that be the option name in Caddyfile?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was on the fence. I've changed them all to network_proxy.

if !d.NextArg() {
return d.ArgErr()
}
modStem := d.Val()
modID := "caddy.network_proxy.source." + modStem
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
// TODO: should we validate here?
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
// ca, ok := unm.(caddy.ProxyFuncProducer)
// if !ok {
// return d.Errf("module %s is not a caddy.ProxyFuncProducer", modID)
// }
h.NetworkProxyRaw = caddyconfig.JSONModuleObject(unm, "from", modStem, nil)
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
case "dial_timeout":
if !d.NextArg() {
return d.ArgErr()
Expand Down
19 changes: 16 additions & 3 deletions modules/caddyhttp/reverseproxy/httptransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ type HTTPTransport struct {
// The pre-configured underlying HTTP transport.
Transport *http.Transport `json:"-"`

// Forward proxy module
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs expanded godoc

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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)
}
Expand Down Expand Up @@ -326,16 +329,26 @@ 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 {
return nil, fmt.Errorf("failed to parse transport proxy url: %v", err)
}
caddyCtx.Logger().Info("setting transport proxy url", zap.String("url", h.ForwardProxyURL))
proxy = http.ProxyURL(pUrl)
} else {
proxy = http.ProxyFromEnvironment
}

rt := &http.Transport{
Expand Down
19 changes: 17 additions & 2 deletions modules/caddytls/acmeissuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -170,15 +173,15 @@ 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
}

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,
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions modules/caddytls/proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package caddytls

import _ "github.com/caddyserver/caddy/v2/modules/internal/network"
142 changes: 142 additions & 0 deletions modules/internal/network/networkproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package network

import (
"errors"
"net/http"
"net/url"
"strings"

"go.uber.org/zap"

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)

func init() {
caddy.RegisterModule(ProxyFromURL{})
caddy.RegisterModule(ProxyFromNone{})
}

type ProxyFromURL struct {
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved
URL string `json:"url"`

ctx caddy.Context
logger *zap.Logger
}

// CaddyModule implements Module.
func (p ProxyFromURL) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.network_proxy.source.url",
New: func() caddy.Module {
return &ProxyFromURL{}
},
}
}

func (p *ProxyFromURL) Provision(ctx caddy.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(caddy.ReplacerCtxKey).(*caddy.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(r *http.Request) (*url.URL, error) {
return url.Parse(p.URL)
}
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (p *ProxyFromURL) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next()
d.Next()
p.URL = d.Val()
return nil
}

type ProxyFromNone struct{}
mohammed90 marked this conversation as resolved.
Show resolved Hide resolved

func (p ProxyFromNone) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{
ID: "caddy.network_proxy.source.none",
New: func() caddy.Module {
return &ProxyFromNone{}
},
}
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (p ProxyFromNone) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return nil
}

// ProxyFunc implements ProxyFuncProducer.
func (p ProxyFromNone) ProxyFunc() func(*http.Request) (*url.URL, error) {
return nil
}

var (
_ caddy.Module = ProxyFromURL{}
_ caddy.Provisioner = &ProxyFromURL{}
_ caddy.Validator = ProxyFromURL{}
_ caddy.ProxyFuncProducer = ProxyFromURL{}
_ caddyfile.Unmarshaler = &ProxyFromURL{}

_ caddy.Module = ProxyFromNone{}
_ caddy.ProxyFuncProducer = ProxyFromNone{}
_ caddyfile.Unmarshaler = ProxyFromNone{}
)
10 changes: 10 additions & 0 deletions network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package caddy

import (
"net/http"
"net/url"
)

type ProxyFuncProducer interface {
ProxyFunc() func(*http.Request) (*url.URL, error)
}
Loading