mirror of
https://github.com/caddyserver/caddy.git
synced 2026-09-15 07:11:10 -04:00
* network_proxy: reject proxy URLs that resolve to a port with no host
The host check after placeholder replacement had its arguments swapped:
strings.Split("", pUrl.Host)[0] == ":"
This splits the empty string using pUrl.Host as the separator, so it
returns [""] and element 0 is always "", never ":". The comparison was
dead for every possible host value, which meant the "http://:80" case
named in the comment directly below it was never rejected. Such a URL
was handed back as the proxy, and the failure surfaced later as a
confusing dial error instead of the intended message.
Only the pUrl.Host == "" half of the condition ever did anything, so
"/some/path" was still caught.
Use url.URL.Hostname(), which returns the host with any port stripped
and is "" for exactly the two cases the comment describes -- ":80" and
"" -- while leaving IPv6 literals such as "[::1]:80" and userinfo forms
intact. That collapses both clauses into one expression.
Also adds tests for this function; the package previously had none.
* network_proxy: cover userinfo-only and IPv6 hosts in the tests
Differential-tested the old predicate against the new one across 36 URL
forms. Every behavioural change is in the same direction -- previously
accepted, now rejected -- and all of them are host-less. Nothing that was
rejected before is accepted now, and no legitimate host form changes.
Two of those forms were worth pinning down in the test:
- "http://user:pass@:8080" parses with Host ":8080", so it is just as
host-less as "http://:80". The comment in the source doesn't name
this variant, but it was accepted before and is rejected now.
- IPv6 literals are full of colons, so a repair that split Host on ":"
rather than using Hostname() could plausibly reject them. Added
"[::1]:8080" and "[2001:db8::1]" as regression guards; both are
accepted before and after, which is the point.
On unfixed master the port-only and userinfo cases both fail; the IPv6
cases pass on both sides.
145 lines
3.7 KiB
Go
145 lines
3.7 KiB
Go
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{})
|
|
}
|
|
|
|
// The "url" proxy source uses the defined URL as the proxy
|
|
type ProxyFromURL struct {
|
|
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.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("network_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 network_proxy URL")
|
|
}
|
|
|
|
// parse the url
|
|
pUrl, err := url.Parse(s)
|
|
if err != nil {
|
|
p.logger.Warn("failed to derive transport proxy from network_proxy URL")
|
|
pUrl = nil
|
|
} else if pUrl.Hostname() == "" {
|
|
// 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 network_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
|
|
}
|
|
|
|
// The "none" proxy source module disables the use of network proxy.
|
|
type ProxyFromNone struct{}
|
|
|
|
func (p ProxyFromNone) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "caddy.network_proxy.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)(nil)
|
|
_ caddy.Validator = ProxyFromURL{}
|
|
_ caddy.ProxyFuncProducer = ProxyFromURL{}
|
|
_ caddyfile.Unmarshaler = (*ProxyFromURL)(nil)
|
|
|
|
_ caddy.Module = ProxyFromNone{}
|
|
_ caddy.ProxyFuncProducer = ProxyFromNone{}
|
|
_ caddyfile.Unmarshaler = ProxyFromNone{}
|
|
)
|