mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-26 13:43:47 +03:00
Merge branch 'master' into interface-network-type
This commit is contained in:
commit
9ebd7fa221
12 changed files with 178 additions and 77 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -150,6 +150,7 @@ jobs:
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
mkdir -p ~/.ssh && echo -e "${SSH_KEY//_/\\n}" > ~/.ssh/id_ecdsa && chmod og-rwx ~/.ssh/id_ecdsa
|
||||||
|
|
||||||
# short sha is enough?
|
# short sha is enough?
|
||||||
|
|
|
@ -84,7 +84,6 @@ func TestLoadUnorderedJSON(t *testing.T) {
|
||||||
"servers": {
|
"servers": {
|
||||||
"s_server": {
|
"s_server": {
|
||||||
"listen": [
|
"listen": [
|
||||||
":9443",
|
|
||||||
":9080"
|
":9080"
|
||||||
],
|
],
|
||||||
"routes": [
|
"routes": [
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
:8884
|
||||||
|
|
||||||
|
reverse_proxy 127.0.0.1:65535 {
|
||||||
|
health_uri /health
|
||||||
|
health_request_body "test body"
|
||||||
|
}
|
||||||
|
----------
|
||||||
|
{
|
||||||
|
"apps": {
|
||||||
|
"http": {
|
||||||
|
"servers": {
|
||||||
|
"srv0": {
|
||||||
|
"listen": [
|
||||||
|
":8884"
|
||||||
|
],
|
||||||
|
"routes": [
|
||||||
|
{
|
||||||
|
"handle": [
|
||||||
|
{
|
||||||
|
"handler": "reverse_proxy",
|
||||||
|
"health_checks": {
|
||||||
|
"active": {
|
||||||
|
"body": "test body",
|
||||||
|
"uri": "/health"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"upstreams": [
|
||||||
|
{
|
||||||
|
"dial": "127.0.0.1:65535"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
cmd/cobra.go
26
cmd/cobra.go
|
@ -8,9 +8,10 @@ import (
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var defaultFactory = newRootCommandFactory(func() *cobra.Command {
|
||||||
Use: "caddy",
|
return &cobra.Command{
|
||||||
Long: `Caddy is an extensible server platform written in Go.
|
Use: "caddy",
|
||||||
|
Long: `Caddy is an extensible server platform written in Go.
|
||||||
|
|
||||||
At its core, Caddy merely manages configuration. Modules are plugged
|
At its core, Caddy merely manages configuration. Modules are plugged
|
||||||
in statically at compile-time to provide useful functionality. Caddy's
|
in statically at compile-time to provide useful functionality. Caddy's
|
||||||
|
@ -91,23 +92,26 @@ package installers: https://caddyserver.com/docs/install
|
||||||
Instructions for running Caddy in production are also available:
|
Instructions for running Caddy in production are also available:
|
||||||
https://caddyserver.com/docs/running
|
https://caddyserver.com/docs/running
|
||||||
`,
|
`,
|
||||||
Example: ` $ caddy run
|
Example: ` $ caddy run
|
||||||
$ caddy run --config caddy.json
|
$ caddy run --config caddy.json
|
||||||
$ caddy reload --config caddy.json
|
$ caddy reload --config caddy.json
|
||||||
$ caddy stop`,
|
$ caddy stop`,
|
||||||
|
|
||||||
// kind of annoying to have all the help text printed out if
|
// kind of annoying to have all the help text printed out if
|
||||||
// caddy has an error provisioning its modules, for instance...
|
// caddy has an error provisioning its modules, for instance...
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
Version: onlyVersionText(),
|
Version: onlyVersionText(),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const fullDocsFooter = `Full documentation is available at:
|
const fullDocsFooter = `Full documentation is available at:
|
||||||
https://caddyserver.com/docs/command-line`
|
https://caddyserver.com/docs/command-line`
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
defaultFactory.Use(func(rootCmd *cobra.Command) {
|
||||||
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
rootCmd.SetVersionTemplate("{{.Version}}\n")
|
||||||
|
rootCmd.SetHelpTemplate(rootCmd.HelpTemplate() + "\n" + fullDocsFooter + "\n")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func onlyVersionText() string {
|
func onlyVersionText() string {
|
||||||
|
|
28
cmd/commandfactory.go
Normal file
28
cmd/commandfactory.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package caddycmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rootCommandFactory struct {
|
||||||
|
constructor func() *cobra.Command
|
||||||
|
options []func(*cobra.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRootCommandFactory(fn func() *cobra.Command) *rootCommandFactory {
|
||||||
|
return &rootCommandFactory{
|
||||||
|
constructor: fn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *rootCommandFactory) Use(fn func(cmd *cobra.Command)) {
|
||||||
|
f.options = append(f.options, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *rootCommandFactory) Build() *cobra.Command {
|
||||||
|
o := f.constructor()
|
||||||
|
for _, v := range f.options {
|
||||||
|
v(o)
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
100
cmd/commands.go
100
cmd/commands.go
|
@ -438,43 +438,44 @@ EXPERIMENTAL: May be changed or removed.
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
RegisterCommand(Command{
|
defaultFactory.Use(func(rootCmd *cobra.Command) {
|
||||||
Name: "manpage",
|
RegisterCommand(Command{
|
||||||
Usage: "--directory <path>",
|
Name: "manpage",
|
||||||
Short: "Generates the manual pages for Caddy commands",
|
Usage: "--directory <path>",
|
||||||
Long: `
|
Short: "Generates the manual pages for Caddy commands",
|
||||||
|
Long: `
|
||||||
Generates the manual pages for Caddy commands into the designated directory
|
Generates the manual pages for Caddy commands into the designated directory
|
||||||
tagged into section 8 (System Administration).
|
tagged into section 8 (System Administration).
|
||||||
|
|
||||||
The manual page files are generated into the directory specified by the
|
The manual page files are generated into the directory specified by the
|
||||||
argument of --directory. If the directory does not exist, it will be created.
|
argument of --directory. If the directory does not exist, it will be created.
|
||||||
`,
|
`,
|
||||||
CobraFunc: func(cmd *cobra.Command) {
|
CobraFunc: func(cmd *cobra.Command) {
|
||||||
cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
|
cmd.Flags().StringP("directory", "o", "", "The output directory where the manpages are generated")
|
||||||
cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
|
cmd.RunE = WrapCommandFuncForCobra(func(fl Flags) (int, error) {
|
||||||
dir := strings.TrimSpace(fl.String("directory"))
|
dir := strings.TrimSpace(fl.String("directory"))
|
||||||
if dir == "" {
|
if dir == "" {
|
||||||
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
return caddy.ExitCodeFailedQuit, fmt.Errorf("designated output directory and specified section are required")
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
return caddy.ExitCodeFailedQuit, err
|
return caddy.ExitCodeFailedQuit, err
|
||||||
}
|
}
|
||||||
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
|
if err := doc.GenManTree(rootCmd, &doc.GenManHeader{
|
||||||
Title: "Caddy",
|
Title: "Caddy",
|
||||||
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
|
Section: "8", // https://en.wikipedia.org/wiki/Man_page#Manual_sections
|
||||||
}, dir); err != nil {
|
}, dir); err != nil {
|
||||||
return caddy.ExitCodeFailedQuit, err
|
return caddy.ExitCodeFailedQuit, err
|
||||||
}
|
}
|
||||||
return caddy.ExitCodeSuccess, nil
|
return caddy.ExitCodeSuccess, nil
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
|
// source: https://github.com/spf13/cobra/blob/main/shell_completions.md
|
||||||
rootCmd.AddCommand(&cobra.Command{
|
rootCmd.AddCommand(&cobra.Command{
|
||||||
Use: "completion [bash|zsh|fish|powershell]",
|
Use: "completion [bash|zsh|fish|powershell]",
|
||||||
Short: "Generate completion script",
|
Short: "Generate completion script",
|
||||||
Long: fmt.Sprintf(`To load completions:
|
Long: fmt.Sprintf(`To load completions:
|
||||||
|
|
||||||
Bash:
|
Bash:
|
||||||
|
|
||||||
|
@ -513,23 +514,24 @@ argument of --directory. If the directory does not exist, it will be created.
|
||||||
PS> %[1]s completion powershell > %[1]s.ps1
|
PS> %[1]s completion powershell > %[1]s.ps1
|
||||||
# and source this file from your PowerShell profile.
|
# and source this file from your PowerShell profile.
|
||||||
`, rootCmd.Root().Name()),
|
`, rootCmd.Root().Name()),
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
switch args[0] {
|
switch args[0] {
|
||||||
case "bash":
|
case "bash":
|
||||||
return cmd.Root().GenBashCompletion(os.Stdout)
|
return cmd.Root().GenBashCompletion(os.Stdout)
|
||||||
case "zsh":
|
case "zsh":
|
||||||
return cmd.Root().GenZshCompletion(os.Stdout)
|
return cmd.Root().GenZshCompletion(os.Stdout)
|
||||||
case "fish":
|
case "fish":
|
||||||
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
return cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||||
case "powershell":
|
case "powershell":
|
||||||
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
return cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unrecognized shell: %s", args[0])
|
return fmt.Errorf("unrecognized shell: %s", args[0])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,7 +565,9 @@ func RegisterCommand(cmd Command) {
|
||||||
if !commandNameRegex.MatchString(cmd.Name) {
|
if !commandNameRegex.MatchString(cmd.Name) {
|
||||||
panic("invalid command name")
|
panic("invalid command name")
|
||||||
}
|
}
|
||||||
rootCmd.AddCommand(caddyCmdToCobra(cmd))
|
defaultFactory.Use(func(rootCmd *cobra.Command) {
|
||||||
|
rootCmd.AddCommand(caddyCmdToCobra(cmd))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
|
var commandNameRegex = regexp.MustCompile(`^[a-z0-9]$|^([a-z0-9]+-?[a-z0-9]*)+[a-z0-9]$`)
|
||||||
|
|
|
@ -72,7 +72,7 @@ func Main() {
|
||||||
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
|
caddy.Log().Warn("failed to set GOMAXPROCS", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := defaultFactory.Build().Execute(); err != nil {
|
||||||
var exitError *exitError
|
var exitError *exitError
|
||||||
if errors.As(err, &exitError) {
|
if errors.As(err, &exitError) {
|
||||||
os.Exit(exitError.ExitCode)
|
os.Exit(exitError.ExitCode)
|
||||||
|
|
|
@ -40,7 +40,7 @@ type ListenerWrapper struct {
|
||||||
Allow []string `json:"allow,omitempty"`
|
Allow []string `json:"allow,omitempty"`
|
||||||
allow []netip.Prefix
|
allow []netip.Prefix
|
||||||
|
|
||||||
// Denby is an optional list of CIDR ranges to
|
// Deny is an optional list of CIDR ranges to
|
||||||
// deny PROXY headers from.
|
// deny PROXY headers from.
|
||||||
Deny []string `json:"deny,omitempty"`
|
Deny []string `json:"deny,omitempty"`
|
||||||
deny []netip.Prefix
|
deny []netip.Prefix
|
||||||
|
|
|
@ -69,19 +69,20 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||||
// lb_retry_match <request-matcher>
|
// lb_retry_match <request-matcher>
|
||||||
//
|
//
|
||||||
// # active health checking
|
// # active health checking
|
||||||
// health_uri <uri>
|
// health_uri <uri>
|
||||||
// health_port <port>
|
// health_port <port>
|
||||||
// health_interval <interval>
|
// health_interval <interval>
|
||||||
// health_passes <num>
|
// health_passes <num>
|
||||||
// health_fails <num>
|
// health_fails <num>
|
||||||
// health_timeout <duration>
|
// health_timeout <duration>
|
||||||
// health_status <status>
|
// health_status <status>
|
||||||
// health_body <regexp>
|
// health_body <regexp>
|
||||||
|
// health_method <value>
|
||||||
|
// health_request_body <value>
|
||||||
// health_follow_redirects
|
// health_follow_redirects
|
||||||
// health_headers {
|
// health_headers {
|
||||||
// <field> [<values...>]
|
// <field> [<values...>]
|
||||||
// }
|
// }
|
||||||
// health_method <value>
|
|
||||||
//
|
//
|
||||||
// # passive health checking
|
// # passive health checking
|
||||||
// fail_duration <duration>
|
// fail_duration <duration>
|
||||||
|
@ -425,6 +426,18 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
}
|
}
|
||||||
h.HealthChecks.Active.Method = d.Val()
|
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":
|
case "health_interval":
|
||||||
if !d.NextArg() {
|
if !d.NextArg() {
|
||||||
return d.ArgErr()
|
return d.ArgErr()
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
@ -93,6 +94,9 @@ type ActiveHealthChecks struct {
|
||||||
// The HTTP method to use for health checks (default "GET").
|
// The HTTP method to use for health checks (default "GET").
|
||||||
Method string `json:"method,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
|
|
||||||
|
// The body to send with the health check request.
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
|
||||||
// Whether to follow HTTP redirects in response to active health checks (default off).
|
// Whether to follow HTTP redirects in response to active health checks (default off).
|
||||||
FollowRedirects bool `json:"follow_redirects,omitempty"`
|
FollowRedirects bool `json:"follow_redirects,omitempty"`
|
||||||
|
|
||||||
|
@ -396,6 +400,16 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
|
||||||
u.Path = h.HealthChecks.Active.Path
|
u.Path = h.HealthChecks.Active.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replacer used for both body and headers. Only globals (env vars, system info, etc.) are available
|
||||||
|
repl := caddy.NewReplacer()
|
||||||
|
|
||||||
|
// if body is provided, create a reader for it, otherwise nil
|
||||||
|
var requestBody io.Reader
|
||||||
|
if h.HealthChecks.Active.Body != "" {
|
||||||
|
// set body, using replacer
|
||||||
|
requestBody = strings.NewReader(repl.ReplaceAll(h.HealthChecks.Active.Body, ""))
|
||||||
|
}
|
||||||
|
|
||||||
// attach dialing information to this request, as well as context values that
|
// attach dialing information to this request, as well as context values that
|
||||||
// may be expected by handlers of this request
|
// may be expected by handlers of this request
|
||||||
ctx := h.ctx.Context
|
ctx := h.ctx.Context
|
||||||
|
@ -403,15 +417,14 @@ func (h *Handler) doActiveHealthCheck(dialInfo DialInfo, hostAddr string, networ
|
||||||
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
|
ctx = context.WithValue(ctx, caddyhttp.VarsCtxKey, map[string]any{
|
||||||
dialInfoVarKey: dialInfo,
|
dialInfoVarKey: dialInfo,
|
||||||
})
|
})
|
||||||
req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), nil)
|
req, err := http.NewRequestWithContext(ctx, h.HealthChecks.Active.Method, u.String(), requestBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("making request: %v", err)
|
return fmt.Errorf("making request: %v", err)
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
|
ctx = context.WithValue(ctx, caddyhttp.OriginalRequestCtxKey, *req)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
// set headers, using a replacer with only globals (env vars, system info, etc.)
|
// set headers, using replacer
|
||||||
repl := caddy.NewReplacer()
|
|
||||||
repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
|
repl.Set("http.reverse_proxy.active.target_upstream", networkAddr)
|
||||||
for key, vals := range h.HealthChecks.Active.Headers {
|
for key, vals := range h.HealthChecks.Active.Headers {
|
||||||
key = repl.ReplaceAll(key, "")
|
key = repl.ReplaceAll(key, "")
|
||||||
|
|
|
@ -979,7 +979,7 @@ func (h *Handler) finalizeResponse(
|
||||||
// we'll just log the error and abort the stream here and panic just as
|
// we'll just log the error and abort the stream here and panic just as
|
||||||
// the standard lib's proxy to propagate the stream error.
|
// the standard lib's proxy to propagate the stream error.
|
||||||
// see issue https://github.com/caddyserver/caddy/issues/5951
|
// see issue https://github.com/caddyserver/caddy/issues/5951
|
||||||
logger.Error("aborting with incomplete response", zap.Error(err))
|
logger.Warn("aborting with incomplete response", zap.Error(err))
|
||||||
// no extra logging from stdlib
|
// no extra logging from stdlib
|
||||||
panic(http.ErrAbortHandler)
|
panic(http.ErrAbortHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,8 +105,7 @@ func (e StaticError) ServeHTTP(w http.ResponseWriter, r *http.Request, _ Handler
|
||||||
}
|
}
|
||||||
statusCode = intVal
|
statusCode = intVal
|
||||||
}
|
}
|
||||||
|
return Error(statusCode, fmt.Errorf("%s", repl.ReplaceKnown(e.Error, "")))
|
||||||
return Error(statusCode, fmt.Errorf("%s", e.Error))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface guard
|
// Interface guard
|
||||||
|
|
Loading…
Reference in a new issue