Files
caddy/modules/caddyhttp/reverseproxy/caddyfile.go
Francis LavoieandWeidiDeng 7ef5c60ed0 reverseproxy: Optionally detach stream (websockets) from config lifecycle (#7649)
* reverseproxy: Optionally detach stream (websockets) from config lifecycle

* lint

* Improved logging facilities

* Adjustments from Weidi's review

* record bytes read and written for response writers unless detached

* simplify streaming handling

* correctly close detached streams

* make handleUpgradeTunnel a standalone func

* fix tests

* fix tests

* change the log level if hijacking without writing a status code first

* only clean up connections when stopped

* Move type and const down to the bottom

* Rename to tunnelTracker, reflow some comments

* Add note about capturing h

* Rename to stream_detached

* More comments

* reverseproxy: Preserve half-close behavior after rebase

Keep master half-close propagation and its unit coverage in the refactored tunnel lifecycle. Adapt response finalization tests to the selected upstream argument.

AI-assisted-by: Codex (GPT-6)

* reverseproxy: Fix detached stream cleanup and recorder chains

Snapshot the tracker registry before closing connections, and unregister empty trackers outside their mutex. Mark trackers stopped at cleanup entry so upstream removal closes detached streams even with stream_close_delay, while attached streams retain their delay.

Detach every supported response writer in the middleware chain so inner recorders do not retain state or count detached traffic. Add regressions for all four failures and an access-log/encode/intercept reload integration test.

AI-assisted-by: Codex (GPT-6)

---------

Co-authored-by: WeidiDeng <weidi_deng@icloud.com>
2026-10-04 07:45:10 -04:00

1763 lines
46 KiB
Go

// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reverseproxy
import (
"fmt"
"net"
"net/http"
"reflect"
"strconv"
"strings"
"github.com/dustin/go-humanize"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/caddyserver/caddy/v2/internal"
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/headers"
"github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/caddyserver/caddy/v2/modules/internal/network"
)
func init() {
httpcaddyfile.RegisterHandlerDirective("reverse_proxy", parseCaddyfile)
httpcaddyfile.RegisterHandlerDirective("copy_response", parseCopyResponseCaddyfile)
httpcaddyfile.RegisterHandlerDirective("copy_response_headers", parseCopyResponseHeadersCaddyfile)
}
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
rp := new(Handler)
err := rp.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
err = rp.FinalizeUnmarshalCaddyfile(h)
if err != nil {
return nil, err
}
return rp, nil
}
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// reverse_proxy [<matcher>] [<upstreams...>] {
// # backends
// to <upstreams...>
// dynamic <name> [...]
//
// # load balancing
// lb_policy <name> [<options...>]
// lb_retries <retries>
// lb_try_duration <duration>
// lb_try_interval <interval>
// lb_retry_match <matcher>
//
// # active health checking
// health_uri <uri>
// health_port <port>
// health_interval <interval>
// health_passes <num>
// health_fails <num>
// health_timeout <duration>
// health_status <status>
// health_body <regexp>
// health_method <value>
// health_request_body <value>
// health_follow_redirects
// health_headers {
// <field> [<values...>]
// }
//
// # passive health checking
// fail_duration <duration>
// max_fails <num>
// unhealthy_status <status>
// unhealthy_latency <duration>
// unhealthy_request_count <num>
//
// # streaming
// flush_interval <duration>
// request_buffers <size>
// response_buffers <size>
// stream_buffer_size <size>
// stream_timeout <duration>
// stream_close_delay <duration>
// stream_detached
// stream_logs {
// level <debug|info|warn|error>
// logger_name <name|access>
// skip_handshake
// }
// verbose_logs
//
// # reporting
// proxy_status_name <name>
//
// # request manipulation
// trusted_proxies [private_ranges] <ranges...>
// header_up [+|-]<field> [<value|regexp> [<replacement>]]
// header_down [+|-]<field> [<value|regexp> [<replacement>]]
// method <method>
// rewrite <to>
//
// # round trip
// transport <name> {
// ...
// }
//
// # optionally intercept responses from upstream
// @name {
// status <code...>
// header <field> [<value>]
// }
// replace_status [<matcher>] <status_code>
// handle_response [<matcher>] {
// <directives...>
//
// # special directives only available in handle_response
// copy_response [<matcher>] [<status>] {
// status <status>
// }
// copy_response_headers [<matcher>] {
// include <fields...>
// exclude <fields...>
// }
// }
// }
//
// Proxy upstream addresses should be network dial addresses such
// as `host:port`, or a URL such as `scheme://host:port`. Scheme
// and port may be inferred from other parts of the address/URL; if
// either are missing, defaults to HTTP.
//
// The FinalizeUnmarshalCaddyfile method should be called after this
// to finalize parsing of "handle_response" blocks, if possible.
func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// currently, all backends must use the same scheme/protocol (the
// underlying JSON does not yet support per-backend transports)
var commonScheme string
// we'll wait until the very end of parsing before
// validating and encoding the transport
var transport http.RoundTripper
var transportModuleName string
// collect the response matchers defined as subdirectives
// prefixed with "@" for use with "handle_response" blocks
h.responseMatchers = make(map[string]caddyhttp.ResponseMatcher)
// appendUpstream creates an upstream for address and adds
// it to the list.
appendUpstream := func(address string) error {
pa, err := parseUpstreamDialAddress(address)
if err != nil {
return d.WrapErr(err)
}
// the underlying JSON does not yet support different
// transports (protocols or schemes) to each backend,
// so we remember the last one we see and compare them
switch pa.scheme {
case "wss":
return d.Errf("the scheme wss:// is only supported in browsers; use https:// instead")
case "ws":
return d.Errf("the scheme ws:// is only supported in browsers; use http:// instead")
case "https", "http", "h2c", "":
// Do nothing or handle the valid schemes
default:
return d.Errf("unsupported URL scheme %s://", pa.scheme)
}
if commonScheme != "" && pa.scheme != commonScheme {
return d.Errf("for now, all proxy upstreams must use the same scheme (transport protocol); expecting '%s://' but got '%s://'",
commonScheme, pa.scheme)
}
commonScheme = pa.scheme
// if the port of upstream address contains a placeholder, only wrap it with the `Upstream` struct,
// delaying actual resolution of the address until request time.
if pa.replaceablePort() {
h.Upstreams = append(h.Upstreams, &Upstream{Dial: pa.dialAddr()})
return nil
}
parsedAddr, err := caddy.ParseNetworkAddress(pa.dialAddr())
if err != nil {
return d.WrapErr(err)
}
if pa.isUnix() || !pa.rangedPort() {
// unix networks don't have ports
h.Upstreams = append(h.Upstreams, &Upstream{
Dial: pa.dialAddr(),
})
} else {
// expand a port range into multiple upstreams
for i := parsedAddr.StartPort; i <= parsedAddr.EndPort; i++ {
h.Upstreams = append(h.Upstreams, &Upstream{
Dial: caddy.JoinNetworkAddress("", parsedAddr.Host, fmt.Sprint(i)),
})
}
}
return nil
}
d.Next() // consume the directive name
for _, up := range d.RemainingArgs() {
err := appendUpstream(up)
if err != nil {
return fmt.Errorf("parsing upstream '%s': %w", up, err)
}
}
for d.NextBlock(0) {
// if the subdirective has an "@" prefix then we
// parse it as a response matcher for use with "handle_response"
if strings.HasPrefix(d.Val(), matcherPrefix) {
err := caddyhttp.ParseNamedResponseMatcher(d.NewFromNextSegment(), h.responseMatchers)
if err != nil {
return err
}
continue
}
switch d.Val() {
case "to":
args := d.RemainingArgs()
if len(args) == 0 {
return d.ArgErr()
}
for _, up := range args {
err := appendUpstream(up)
if err != nil {
return fmt.Errorf("parsing upstream '%s': %w", up, err)
}
}
case "dynamic":
if !d.NextArg() {
return d.ArgErr()
}
if h.DynamicUpstreams != nil {
return d.Err("dynamic upstreams already specified")
}
dynModule := d.Val()
modID := "http.reverse_proxy.upstreams." + dynModule
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
source, ok := unm.(UpstreamSource)
if !ok {
return d.Errf("module %s (%T) is not an UpstreamSource", modID, unm)
}
h.DynamicUpstreamsRaw = caddyconfig.JSONModuleObject(source, "source", dynModule, nil)
case "lb_policy":
if !d.NextArg() {
return d.ArgErr()
}
if h.LoadBalancing != nil && h.LoadBalancing.SelectionPolicyRaw != nil {
return d.Err("load balancing selection policy already specified")
}
name := d.Val()
modID := "http.reverse_proxy.selection_policies." + name
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
sel, ok := unm.(Selector)
if !ok {
return d.Errf("module %s (%T) is not a reverseproxy.Selector", modID, unm)
}
if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing)
}
h.LoadBalancing.SelectionPolicyRaw = caddyconfig.JSONModuleObject(sel, "policy", name, nil)
case "lb_retries":
if !d.NextArg() {
return d.ArgErr()
}
tries, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad lb_retries number '%s': %v", d.Val(), err)
}
if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing)
}
h.LoadBalancing.Retries = tries
case "lb_try_duration":
if !d.NextArg() {
return d.ArgErr()
}
if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing)
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad duration value %s: %v", d.Val(), err)
}
h.LoadBalancing.TryDuration = caddy.Duration(dur)
case "lb_try_interval":
if !d.NextArg() {
return d.ArgErr()
}
if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing)
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad interval value '%s': %v", d.Val(), err)
}
h.LoadBalancing.TryInterval = caddy.Duration(dur)
case "lb_retry_match":
matcherSet, err := caddyhttp.ParseCaddyfileNestedMatcherSet(d)
if err != nil {
return d.Errf("failed to parse lb_retry_match: %v", err)
}
if h.LoadBalancing == nil {
h.LoadBalancing = new(LoadBalancing)
}
h.LoadBalancing.RetryMatchRaw = append(h.LoadBalancing.RetryMatchRaw, matcherSet)
case "health_uri":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.URI = d.Val()
case "health_path":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.Path = d.Val()
caddy.Log().Named("config.adapter.caddyfile").Warn("the 'health_path' subdirective is deprecated, please use 'health_uri' instead!")
case "health_upstream":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
_, port, err := net.SplitHostPort(d.Val())
if err != nil {
return d.Errf("health_upstream is malformed '%s': %v", d.Val(), err)
}
_, err = strconv.Atoi(port)
if err != nil {
return d.Errf("bad port number '%s': %v", d.Val(), err)
}
h.HealthChecks.Active.Upstream = d.Val()
case "health_port":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
if h.HealthChecks.Active.Upstream != "" {
return d.Errf("the 'health_port' subdirective is ignored if 'health_upstream' is used!")
}
portNum, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad port number '%s': %v", d.Val(), err)
}
h.HealthChecks.Active.Port = portNum
case "health_headers":
healthHeaders := make(http.Header)
for nesting := d.Nesting(); d.NextBlock(nesting); {
key := d.Val()
values := d.RemainingArgs()
if len(values) == 0 {
values = append(values, "")
}
healthHeaders[key] = append(healthHeaders[key], values...)
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.Headers = healthHeaders
case "health_method":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.Method = d.Val()
case "health_request_body":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.Body = d.Val()
case "health_interval":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad interval value %s: %v", d.Val(), err)
}
h.HealthChecks.Active.Interval = caddy.Duration(dur)
case "health_timeout":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad timeout value %s: %v", d.Val(), err)
}
h.HealthChecks.Active.Timeout = caddy.Duration(dur)
case "health_status":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
val := d.Val()
if len(val) == 3 && strings.HasSuffix(val, "xx") {
val = val[:1]
}
statusNum, err := strconv.Atoi(val)
if err != nil {
return d.Errf("bad status value '%s': %v", d.Val(), err)
}
h.HealthChecks.Active.ExpectStatus = statusNum
case "health_body":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.ExpectBody = d.Val()
case "health_follow_redirects":
if d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
h.HealthChecks.Active.FollowRedirects = true
case "health_passes":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
passes, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("invalid passes count '%s': %v", d.Val(), err)
}
h.HealthChecks.Active.Passes = passes
case "health_fails":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Active == nil {
h.HealthChecks.Active = new(ActiveHealthChecks)
}
fails, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("invalid fails count '%s': %v", d.Val(), err)
}
h.HealthChecks.Active.Fails = fails
case "max_fails":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Passive == nil {
h.HealthChecks.Passive = new(PassiveHealthChecks)
}
maxFails, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("invalid maximum fail count '%s': %v", d.Val(), err)
}
h.HealthChecks.Passive.MaxFails = maxFails
case "fail_duration":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Passive == nil {
h.HealthChecks.Passive = new(PassiveHealthChecks)
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err)
}
h.HealthChecks.Passive.FailDuration = caddy.Duration(dur)
case "unhealthy_request_count":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Passive == nil {
h.HealthChecks.Passive = new(PassiveHealthChecks)
}
maxConns, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("invalid maximum connection count '%s': %v", d.Val(), err)
}
h.HealthChecks.Passive.UnhealthyRequestCount = maxConns
case "unhealthy_status":
args := d.RemainingArgs()
if len(args) == 0 {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Passive == nil {
h.HealthChecks.Passive = new(PassiveHealthChecks)
}
for _, arg := range args {
if len(arg) == 3 && strings.HasSuffix(arg, "xx") {
arg = arg[:1]
}
statusNum, err := strconv.Atoi(arg)
if err != nil {
return d.Errf("bad status value '%s': %v", d.Val(), err)
}
h.HealthChecks.Passive.UnhealthyStatus = append(h.HealthChecks.Passive.UnhealthyStatus, statusNum)
}
case "unhealthy_latency":
if !d.NextArg() {
return d.ArgErr()
}
if h.HealthChecks == nil {
h.HealthChecks = new(HealthChecks)
}
if h.HealthChecks.Passive == nil {
h.HealthChecks.Passive = new(PassiveHealthChecks)
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err)
}
h.HealthChecks.Passive.UnhealthyLatency = caddy.Duration(dur)
case "flush_interval":
if !d.NextArg() {
return d.ArgErr()
}
if fi, err := strconv.Atoi(d.Val()); err == nil {
h.FlushInterval = caddy.Duration(fi)
} else {
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err)
}
h.FlushInterval = caddy.Duration(dur)
}
case "request_buffers", "response_buffers", "stream_buffer_size":
subdir := d.Val()
if !d.NextArg() {
return d.ArgErr()
}
val := d.Val()
var size int64
if val == "unlimited" {
size = -1
} else {
usize, err := humanize.ParseBytes(val)
if err != nil {
return d.Errf("invalid byte size '%s': %v", val, err)
}
size = int64(usize)
}
if d.NextArg() {
return d.ArgErr()
}
switch subdir {
case "request_buffers":
h.RequestBuffers = size
case "response_buffers":
h.ResponseBuffers = size
case "stream_buffer_size":
h.StreamBufferSize = int(size)
}
case "proxy_status_name":
if !d.NextArg() {
return d.ArgErr()
}
h.ProxyStatusName = d.Val()
if d.NextArg() {
return d.ArgErr()
}
case "stream_timeout":
if !d.NextArg() {
return d.ArgErr()
}
if fi, err := strconv.Atoi(d.Val()); err == nil {
h.StreamTimeout = caddy.Duration(fi)
} else {
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err)
}
h.StreamTimeout = caddy.Duration(dur)
}
case "stream_close_delay":
if !d.NextArg() {
return d.ArgErr()
}
if fi, err := strconv.Atoi(d.Val()); err == nil {
h.StreamCloseDelay = caddy.Duration(fi)
} else {
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err)
}
h.StreamCloseDelay = caddy.Duration(dur)
}
case "stream_detached":
if d.NextArg() {
return d.ArgErr()
}
h.StreamDetached = true
case "stream_logs":
if d.NextArg() {
return d.ArgErr()
}
if h.StreamLogs == nil {
h.StreamLogs = new(StreamLogs)
}
nesting := d.Nesting()
for d.NextBlock(nesting) {
switch d.Val() {
case "level":
if !d.NextArg() {
return d.ArgErr()
}
h.StreamLogs.Level = d.Val()
if d.NextArg() {
return d.ArgErr()
}
case "logger_name":
if !d.NextArg() {
return d.ArgErr()
}
h.StreamLogs.LoggerName = d.Val()
if d.NextArg() {
return d.ArgErr()
}
case "skip_handshake":
if d.NextArg() {
return d.ArgErr()
}
h.StreamLogs.SkipHandshake = true
default:
return d.Errf("unrecognized stream_logs option: %s", d.Val())
}
}
case "trusted_proxies":
for d.NextArg() {
if d.Val() == "private_ranges" {
h.TrustedProxies = append(h.TrustedProxies, internal.PrivateRangesCIDR()...)
continue
}
h.TrustedProxies = append(h.TrustedProxies, d.Val())
}
case "header_up":
var err error
if h.Headers == nil {
h.Headers = new(headers.Handler)
}
if h.Headers.Request == nil {
h.Headers.Request = new(headers.HeaderOps)
}
args := d.RemainingArgs()
switch len(args) {
case 1:
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], "", nil)
case 2:
// some lint checks, I guess
if strings.EqualFold(args[0], "x-forwarded-for") && (args[1] == "{remote}" || args[1] == "{http.request.remote}" || args[1] == "{remote_host}" || args[1] == "{http.request.remote.host}") {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-For: the reverse proxy's default behavior is to pass headers to the upstream")
}
if strings.EqualFold(args[0], "x-forwarded-proto") && (args[1] == "{scheme}" || args[1] == "{http.request.scheme}") {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-Proto: the reverse proxy's default behavior is to pass headers to the upstream")
}
if strings.EqualFold(args[0], "x-forwarded-host") && (args[1] == "{host}" || args[1] == "{http.request.host}" || args[1] == "{hostport}" || args[1] == "{http.request.hostport}") {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up X-Forwarded-Host: the reverse proxy's default behavior is to pass headers to the upstream")
}
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], nil)
case 3:
err = headers.CaddyfileHeaderOp(h.Headers.Request, args[0], args[1], &args[2])
default:
return d.ArgErr()
}
if err != nil {
return d.Err(err.Error())
}
case "header_down":
var err error
if h.Headers == nil {
h.Headers = new(headers.Handler)
}
if h.Headers.Response == nil {
h.Headers.Response = &headers.RespHeaderOps{
HeaderOps: new(headers.HeaderOps),
}
}
args := d.RemainingArgs()
switch len(args) {
case 1:
err = headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], "", nil)
case 2:
err = headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], args[1], nil)
case 3:
err = headers.CaddyfileHeaderOp(h.Headers.Response.HeaderOps, args[0], args[1], &args[2])
default:
return d.ArgErr()
}
if err != nil {
return d.Err(err.Error())
}
case "method":
if !d.NextArg() {
return d.ArgErr()
}
if h.Rewrite == nil {
h.Rewrite = &rewrite.Rewrite{}
}
h.Rewrite.Method = d.Val()
if d.NextArg() {
return d.ArgErr()
}
case "rewrite":
if !d.NextArg() {
return d.ArgErr()
}
if h.Rewrite == nil {
h.Rewrite = &rewrite.Rewrite{}
}
h.Rewrite.URI = d.Val()
if d.NextArg() {
return d.ArgErr()
}
case "transport":
if !d.NextArg() {
return d.ArgErr()
}
if h.TransportRaw != nil {
return d.Err("transport already specified")
}
transportModuleName = d.Val()
modID := "http.reverse_proxy.transport." + transportModuleName
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
rt, ok := unm.(http.RoundTripper)
if !ok {
return d.Errf("module %s (%T) is not a RoundTripper", modID, unm)
}
transport = rt
case "handle_response":
// delegate the parsing of handle_response to the caller,
// since we need the httpcaddyfile.Helper to parse subroutes.
// See h.FinalizeUnmarshalCaddyfile
h.handleResponseSegments = append(h.handleResponseSegments, d.NewFromNextSegment())
case "replace_status":
args := d.RemainingArgs()
if len(args) != 1 && len(args) != 2 {
return d.Errf("must have one or two arguments: an optional response matcher, and a status code")
}
responseHandler := caddyhttp.ResponseHandler{}
if len(args) == 2 {
if !strings.HasPrefix(args[0], matcherPrefix) {
return d.Errf("must use a named response matcher, starting with '@'")
}
foundMatcher, ok := h.responseMatchers[args[0]]
if !ok {
return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
}
responseHandler.Match = &foundMatcher
responseHandler.StatusCode = caddyhttp.WeakString(args[1])
} else if len(args) == 1 {
responseHandler.StatusCode = caddyhttp.WeakString(args[0])
}
// make sure there's no block, cause it doesn't make sense
if nesting := d.Nesting(); d.NextBlock(nesting) {
return d.Errf("cannot define routes for 'replace_status', use 'handle_response' instead.")
}
h.HandleResponse = append(
h.HandleResponse,
responseHandler,
)
case "verbose_logs":
if h.VerboseLogs {
return d.Err("verbose_logs already specified")
}
h.VerboseLogs = true
default:
return d.Errf("unrecognized subdirective %s", d.Val())
}
}
// if the scheme inferred from the backends' addresses is
// HTTPS, we will need a non-nil transport to enable TLS,
// or if H2C, to set the transport versions.
if (commonScheme == "https" || commonScheme == "h2c") && transport == nil {
transport = new(HTTPTransport)
transportModuleName = "http"
}
// verify transport configuration, and finally encode it
if transport != nil {
if te, ok := transport.(TLSTransport); ok {
if commonScheme == "https" && !te.TLSEnabled() {
err := te.EnableTLS(new(TLSConfig))
if err != nil {
return err
}
}
// check if the user set 'header_up host upstream_hostport' when proxying to HTTPS
// this is unnecessary because it's the default behavior already
if te.TLSEnabled() && h.Headers != nil && h.Headers.Request != nil {
hostVal := h.Headers.Request.Set.Get("Host")
if hostVal == "{upstream_hostport}" || hostVal == "{http.reverse_proxy.upstream.hostport}" {
caddy.Log().Named("caddyfile").Warn("Unnecessary header_up Host: the reverse proxy's default behavior is to pass the configured upstream address to the upstream when proxying to HTTPS")
}
}
if commonScheme == "http" && te.TLSEnabled() {
return d.Errf("upstream address scheme is HTTP but transport is configured for HTTP+TLS (HTTPS)")
}
if h2ct, ok := transport.(H2CTransport); ok && commonScheme == "h2c" {
err := h2ct.EnableH2C()
if err != nil {
return err
}
}
} else if commonScheme == "https" {
return d.Errf("upstreams are configured for HTTPS but transport module does not support TLS: %T", transport)
}
// no need to encode empty default transport
if !reflect.DeepEqual(transport, new(HTTPTransport)) {
h.TransportRaw = caddyconfig.JSONModuleObject(transport, "protocol", transportModuleName, nil)
}
}
return nil
}
// FinalizeUnmarshalCaddyfile finalizes the Caddyfile parsing which
// requires having an httpcaddyfile.Helper to function, to parse subroutes.
func (h *Handler) FinalizeUnmarshalCaddyfile(helper httpcaddyfile.Helper) error {
for _, d := range h.handleResponseSegments {
// consume the "handle_response" token
d.Next()
args := d.RemainingArgs()
if len(args) > 1 {
return d.Errf("too many arguments for 'handle_response': only a single response matcher name is allowed, but got: %s", args)
}
var matcher *caddyhttp.ResponseMatcher
if len(args) == 1 {
// the first arg should always be a matcher.
if !strings.HasPrefix(args[0], matcherPrefix) {
return d.Errf("must use a named response matcher, starting with '@'")
}
foundMatcher, ok := h.responseMatchers[args[0]]
if !ok {
return d.Errf("no named response matcher defined with name '%s'", args[0][1:])
}
matcher = &foundMatcher
}
// parse the block as routes
handler, err := httpcaddyfile.ParseSegmentAsSubroute(helper.WithDispenser(d.NewFromNextSegment()))
if err != nil {
return err
}
subroute, ok := handler.(*caddyhttp.Subroute)
if !ok {
return helper.Errf("segment was not parsed as a subroute")
}
h.HandleResponse = append(
h.HandleResponse,
caddyhttp.ResponseHandler{
Match: matcher,
Routes: subroute.Routes,
},
)
}
// move the handle_response entries without a matcher to the end.
// we can't use sort.SliceStable because it will reorder the rest of the
// entries which may be undesirable because we don't have a good
// heuristic to use for sorting.
withoutMatchers := []caddyhttp.ResponseHandler{}
withMatchers := []caddyhttp.ResponseHandler{}
for _, hr := range h.HandleResponse {
if hr.Match == nil {
withoutMatchers = append(withoutMatchers, hr)
} else {
withMatchers = append(withMatchers, hr)
}
}
h.HandleResponse = append(withMatchers, withoutMatchers...)
// clean up the bits we only needed for adapting
h.handleResponseSegments = nil
h.responseMatchers = nil
return nil
}
// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
//
// transport http {
// read_buffer <size>
// write_buffer <size>
// max_response_header <size>
// network_proxy <module> {
// ...
// }
// dial_timeout <duration>
// dial_fallback_delay <duration>
// response_header_timeout <duration>
// expect_continue_timeout <duration>
// resolvers <resolvers...>
// tls
// tls_client_auth <automate_name> | <cert_file> <key_file>
// tls_insecure_skip_verify
// tls_timeout <duration>
// tls_trusted_ca_certs <cert_files...>
// tls_trust_pool <module> {
// ...
// }
// tls_server_name <sni>
// tls_renegotiation <level>
// tls_except_ports <ports...>
// keepalive [off|<duration>]
// keepalive_interval <interval>
// keepalive_idle_conns <max_count>
// keepalive_idle_conns_per_host <count>
// versions <versions...>
// compression off
// max_conns_per_host <count>
// max_idle_conns_per_host <count>
// }
func (h *HTTPTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume transport name
for d.NextBlock(0) {
switch d.Val() {
case "read_buffer":
if !d.NextArg() {
return d.ArgErr()
}
size, err := humanize.ParseBytes(d.Val())
if err != nil {
return d.Errf("invalid read buffer size '%s': %v", d.Val(), err)
}
h.ReadBufferSize = int(size)
case "write_buffer":
if !d.NextArg() {
return d.ArgErr()
}
size, err := humanize.ParseBytes(d.Val())
if err != nil {
return d.Errf("invalid write buffer size '%s': %v", d.Val(), err)
}
h.WriteBufferSize = int(size)
case "read_timeout":
if !d.NextArg() {
return d.ArgErr()
}
timeout, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("invalid read timeout duration '%s': %v", d.Val(), err)
}
h.ReadTimeout = caddy.Duration(timeout)
case "write_timeout":
if !d.NextArg() {
return d.ArgErr()
}
timeout, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("invalid write timeout duration '%s': %v", d.Val(), err)
}
h.WriteTimeout = caddy.Duration(timeout)
case "max_response_header":
if !d.NextArg() {
return d.ArgErr()
}
size, err := humanize.ParseBytes(d.Val())
if err != nil {
return d.Errf("invalid max response header size '%s': %v", d.Val(), err)
}
h.MaxResponseHeaderSize = int64(size)
case "proxy_protocol":
if !d.NextArg() {
return d.ArgErr()
}
switch proxyProtocol := d.Val(); proxyProtocol {
case "v1", "v2":
h.ProxyProtocol = proxyProtocol
default:
return d.Errf("invalid proxy protocol version '%s'", proxyProtocol)
}
case "forward_proxy_url":
caddy.Log().Warn("The 'forward_proxy_url' field is deprecated. Use 'network_proxy <url>' instead.")
if !d.NextArg() {
return d.ArgErr()
}
u := network.ProxyFromURL{URL: d.Val()}
h.NetworkProxyRaw = caddyconfig.JSONModuleObject(u, "from", "url", nil)
case "network_proxy":
if !d.NextArg() {
return d.ArgErr()
}
modStem := d.Val()
modID := "caddy.network_proxy." + modStem
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
h.NetworkProxyRaw = caddyconfig.JSONModuleObject(unm, "from", modStem, nil)
case "dial_timeout":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err)
}
h.DialTimeout = caddy.Duration(dur)
case "dial_fallback_delay":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad fallback delay value '%s': %v", d.Val(), err)
}
h.FallbackDelay = caddy.Duration(dur)
case "response_header_timeout":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err)
}
h.ResponseHeaderTimeout = caddy.Duration(dur)
case "expect_continue_timeout":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err)
}
h.ExpectContinueTimeout = caddy.Duration(dur)
case "resolvers":
if h.Resolver == nil {
h.Resolver = new(UpstreamResolver)
}
h.Resolver.Addresses = d.RemainingArgs()
if len(h.Resolver.Addresses) == 0 {
return d.Errf("must specify at least one resolver address")
}
case "tls":
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
case "tls_client_auth":
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
args := d.RemainingArgs()
switch len(args) {
case 1:
h.TLS.ClientCertificateAutomate = args[0]
case 2:
h.TLS.ClientCertificateFile = args[0]
h.TLS.ClientCertificateKeyFile = args[1]
default:
return d.ArgErr()
}
case "tls_insecure_skip_verify":
if d.NextArg() {
return d.ArgErr()
}
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
h.TLS.InsecureSkipVerify = true
case "tls_curves":
args := d.RemainingArgs()
if len(args) == 0 {
return d.ArgErr()
}
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
h.TLS.Curves = args
case "tls_timeout":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err)
}
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
h.TLS.HandshakeTimeout = caddy.Duration(dur)
case "tls_trusted_ca_certs":
caddy.Log().Warn("The 'tls_trusted_ca_certs' field is deprecated. Use the 'tls_trust_pool' field instead.")
args := d.RemainingArgs()
if len(args) == 0 {
return d.ArgErr()
}
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
if len(h.TLS.CARaw) != 0 {
return d.Err("cannot specify both 'tls_trust_pool' and 'tls_trusted_ca_certs")
}
h.TLS.RootCAPEMFiles = args
case "tls_server_name":
if !d.NextArg() {
return d.ArgErr()
}
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
h.TLS.ServerName = d.Val()
case "tls_renegotiation":
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
if !d.NextArg() {
return d.ArgErr()
}
switch renegotiation := d.Val(); renegotiation {
case "never", "once", "freely":
h.TLS.Renegotiation = renegotiation
default:
return d.ArgErr()
}
case "tls_except_ports":
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
h.TLS.ExceptPorts = d.RemainingArgs()
if len(h.TLS.ExceptPorts) == 0 {
return d.ArgErr()
}
case "keepalive":
if !d.NextArg() {
return d.ArgErr()
}
if h.KeepAlive == nil {
h.KeepAlive = new(KeepAlive)
}
if d.Val() == "off" {
var disable bool
h.KeepAlive.Enabled = &disable
break
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad duration value '%s': %v", d.Val(), err)
}
h.KeepAlive.IdleConnTimeout = caddy.Duration(dur)
case "keepalive_interval":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad interval value '%s': %v", d.Val(), err)
}
if h.KeepAlive == nil {
h.KeepAlive = new(KeepAlive)
}
h.KeepAlive.ProbeInterval = caddy.Duration(dur)
case "keepalive_idle_conns":
if !d.NextArg() {
return d.ArgErr()
}
num, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad integer value '%s': %v", d.Val(), err)
}
if h.KeepAlive == nil {
h.KeepAlive = new(KeepAlive)
}
h.KeepAlive.MaxIdleConns = num
case "keepalive_idle_conns_per_host":
if !d.NextArg() {
return d.ArgErr()
}
num, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad integer value '%s': %v", d.Val(), err)
}
if h.KeepAlive == nil {
h.KeepAlive = new(KeepAlive)
}
h.KeepAlive.MaxIdleConnsPerHost = num
case "versions":
h.Versions = d.RemainingArgs()
if len(h.Versions) == 0 {
return d.ArgErr()
}
case "compression":
if d.NextArg() {
if d.Val() == "off" {
var disable bool
h.Compression = &disable
}
}
case "max_conns_per_host":
if !d.NextArg() {
return d.ArgErr()
}
num, err := strconv.Atoi(d.Val())
if err != nil {
return d.Errf("bad integer value '%s': %v", d.Val(), err)
}
h.MaxConnsPerHost = num
case "tls_trust_pool":
if !d.NextArg() {
return d.ArgErr()
}
modStem := d.Val()
modID := "tls.ca_pool.source." + modStem
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
ca, ok := unm.(caddytls.CA)
if !ok {
return d.Errf("module %s is not a caddytls.CA", modID)
}
if h.TLS == nil {
h.TLS = new(TLSConfig)
}
if len(h.TLS.RootCAPEMFiles) != 0 {
return d.Err("cannot specify both 'tls_trust_pool' and 'tls_trusted_ca_certs'")
}
if h.TLS.CARaw != nil {
return d.Err("cannot specify \"tls_trust_pool\" twice in caddyfile")
}
h.TLS.CARaw = caddyconfig.JSONModuleObject(ca, "provider", modStem, nil)
case "local_address":
if !d.NextArg() {
return d.ArgErr()
}
h.LocalAddress = d.Val()
default:
return d.Errf("unrecognized subdirective %s", d.Val())
}
}
return nil
}
func parseCopyResponseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
crh := new(CopyResponseHandler)
err := crh.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
return crh, nil
}
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// copy_response [<matcher>] [<status>] {
// status <status>
// }
func (h *CopyResponseHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
args := d.RemainingArgs()
if len(args) == 1 {
if num, err := strconv.Atoi(args[0]); err == nil && num > 0 {
h.StatusCode = caddyhttp.WeakString(args[0])
return nil
}
}
for d.NextBlock(0) {
switch d.Val() {
case "status":
if !d.NextArg() {
return d.ArgErr()
}
h.StatusCode = caddyhttp.WeakString(d.Val())
default:
return d.Errf("unrecognized subdirective '%s'", d.Val())
}
}
return nil
}
func parseCopyResponseHeadersCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
crh := new(CopyResponseHeadersHandler)
err := crh.UnmarshalCaddyfile(h.Dispenser)
if err != nil {
return nil, err
}
return crh, nil
}
// UnmarshalCaddyfile sets up the handler from Caddyfile tokens. Syntax:
//
// copy_response_headers [<matcher>] {
// include <fields...>
// exclude <fields...>
// }
func (h *CopyResponseHeadersHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
args := d.RemainingArgs()
if len(args) > 0 {
return d.ArgErr()
}
for d.NextBlock(0) {
switch d.Val() {
case "include":
h.Include = append(h.Include, d.RemainingArgs()...)
case "exclude":
h.Exclude = append(h.Exclude, d.RemainingArgs()...)
default:
return d.Errf("unrecognized subdirective '%s'", d.Val())
}
}
return nil
}
// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
//
// dynamic srv [<name>] {
// service <service>
// proto <proto>
// name <name>
// refresh <interval>
// resolvers <resolvers...>
// dial_timeout <timeout>
// dial_fallback_delay <timeout>
// grace_period <duration>
// }
func (u *SRVUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume upstream source name
args := d.RemainingArgs()
if len(args) > 1 {
return d.ArgErr()
}
if len(args) > 0 {
u.Name = args[0]
}
for d.NextBlock(0) {
switch d.Val() {
case "service":
if !d.NextArg() {
return d.ArgErr()
}
if u.Service != "" {
return d.Errf("srv service has already been specified")
}
u.Service = d.Val()
case "proto":
if !d.NextArg() {
return d.ArgErr()
}
if u.Proto != "" {
return d.Errf("srv proto has already been specified")
}
u.Proto = d.Val()
case "name":
if !d.NextArg() {
return d.ArgErr()
}
if u.Name != "" {
return d.Errf("srv name has already been specified")
}
u.Name = d.Val()
case "refresh":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("parsing refresh interval duration: %v", err)
}
u.Refresh = caddy.Duration(dur)
case "resolvers":
if u.Resolver == nil {
u.Resolver = new(UpstreamResolver)
}
u.Resolver.Addresses = d.RemainingArgs()
if len(u.Resolver.Addresses) == 0 {
return d.Errf("must specify at least one resolver address")
}
case "dial_timeout":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err)
}
u.DialTimeout = caddy.Duration(dur)
case "dial_fallback_delay":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad delay value '%s': %v", d.Val(), err)
}
u.FallbackDelay = caddy.Duration(dur)
case "grace_period":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad grace period value '%s': %v", d.Val(), err)
}
u.GracePeriod = caddy.Duration(dur)
default:
return d.Errf("unrecognized srv option '%s'", d.Val())
}
}
return nil
}
// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
//
// dynamic a [<name> <port] {
// name <name>
// port <port>
// refresh <interval>
// resolvers <resolvers...>
// dial_timeout <timeout>
// dial_fallback_delay <timeout>
// versions ipv4|ipv6
// }
func (u *AUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume upstream source name
args := d.RemainingArgs()
if len(args) > 2 {
return d.ArgErr()
}
if len(args) > 0 {
u.Name = args[0]
if len(args) == 2 {
u.Port = args[1]
}
}
for d.NextBlock(0) {
switch d.Val() {
case "name":
if !d.NextArg() {
return d.ArgErr()
}
if u.Name != "" {
return d.Errf("a name has already been specified")
}
u.Name = d.Val()
case "port":
if !d.NextArg() {
return d.ArgErr()
}
if u.Port != "" {
return d.Errf("a port has already been specified")
}
u.Port = d.Val()
case "refresh":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("parsing refresh interval duration: %v", err)
}
u.Refresh = caddy.Duration(dur)
case "resolvers":
if u.Resolver == nil {
u.Resolver = new(UpstreamResolver)
}
u.Resolver.Addresses = d.RemainingArgs()
if len(u.Resolver.Addresses) == 0 {
return d.Errf("must specify at least one resolver address")
}
case "dial_timeout":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad timeout value '%s': %v", d.Val(), err)
}
u.DialTimeout = caddy.Duration(dur)
case "dial_fallback_delay":
if !d.NextArg() {
return d.ArgErr()
}
dur, err := caddy.ParseDuration(d.Val())
if err != nil {
return d.Errf("bad delay value '%s': %v", d.Val(), err)
}
u.FallbackDelay = caddy.Duration(dur)
case "versions":
args := d.RemainingArgs()
if len(args) == 0 {
return d.Errf("must specify at least one version")
}
if u.Versions == nil {
u.Versions = &IPVersions{}
}
trueBool := true
for _, arg := range args {
switch arg {
case "ipv4":
u.Versions.IPv4 = &trueBool
case "ipv6":
u.Versions.IPv6 = &trueBool
default:
return d.Errf("unsupported version: '%s'", arg)
}
}
default:
return d.Errf("unrecognized a option '%s'", d.Val())
}
}
return nil
}
// UnmarshalCaddyfile deserializes Caddyfile tokens into h.
//
// dynamic multi {
// <source> [...]
// }
func (u *MultiUpstreams) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume upstream source name
if d.NextArg() {
return d.ArgErr()
}
for d.NextBlock(0) {
dynModule := d.Val()
modID := "http.reverse_proxy.upstreams." + dynModule
unm, err := caddyfile.UnmarshalModule(d, modID)
if err != nil {
return err
}
source, ok := unm.(UpstreamSource)
if !ok {
return d.Errf("module %s (%T) is not an UpstreamSource", modID, unm)
}
u.SourcesRaw = append(u.SourcesRaw, caddyconfig.JSONModuleObject(source, "source", dynModule, nil))
}
return nil
}
const matcherPrefix = "@"
// Interface guards
var (
_ caddyfile.Unmarshaler = (*Handler)(nil)
_ caddyfile.Unmarshaler = (*HTTPTransport)(nil)
_ caddyfile.Unmarshaler = (*SRVUpstreams)(nil)
_ caddyfile.Unmarshaler = (*AUpstreams)(nil)
_ caddyfile.Unmarshaler = (*MultiUpstreams)(nil)
)