mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-30 18:45:18 -04:00
Merge branch 'master' into acme-database
This commit is contained in:
@@ -642,6 +642,15 @@ func (app *App) Stop() error {
|
||||
finishedShutdown.Wait()
|
||||
}
|
||||
|
||||
// run stop callbacks now that the server shutdowns are complete
|
||||
for name, s := range app.Servers {
|
||||
for _, stopHook := range s.onStopFuncs {
|
||||
if err := stopHook(ctx); err != nil {
|
||||
app.logger.Error("server stop hook", zap.String("server", name), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,6 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
|
||||
|
||||
acct.Username = repl.ReplaceAll(acct.Username, "")
|
||||
acct.Password = repl.ReplaceAll(acct.Password, "")
|
||||
acct.Salt = repl.ReplaceAll(acct.Salt, "")
|
||||
|
||||
if acct.Username == "" || acct.Password == "" {
|
||||
return fmt.Errorf("account %d: username and password are required", i)
|
||||
@@ -127,13 +126,6 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if acct.Salt != "" {
|
||||
acct.salt, err = base64.StdEncoding.DecodeString(acct.Salt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("base64-decoding salt: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
hba.Accounts[acct.Username] = acct
|
||||
}
|
||||
hba.AccountList = nil // allow GC to deallocate
|
||||
@@ -172,7 +164,7 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
|
||||
|
||||
func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) {
|
||||
compare := func() (bool, error) {
|
||||
return hba.Hash.Compare(account.password, plaintextPassword, account.salt)
|
||||
return hba.Hash.Compare(account.password, plaintextPassword)
|
||||
}
|
||||
|
||||
// if no caching is enabled, simply return the result of hashing + comparing
|
||||
@@ -181,7 +173,7 @@ func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []by
|
||||
}
|
||||
|
||||
// compute a cache key that is unique for these input parameters
|
||||
cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...))
|
||||
cacheKey := hex.EncodeToString(append(account.password, plaintextPassword...))
|
||||
|
||||
// fast track: if the result of the input is already cached, use it
|
||||
hba.HashCache.mu.RLock()
|
||||
@@ -231,7 +223,7 @@ type Cache struct {
|
||||
mu *sync.RWMutex
|
||||
g *singleflight.Group
|
||||
|
||||
// map of concatenated hashed password + plaintext password + salt, to result
|
||||
// map of concatenated hashed password + plaintext password, to result
|
||||
cache map[string]bool
|
||||
}
|
||||
|
||||
@@ -274,37 +266,33 @@ func (c *Cache) makeRoom() {
|
||||
// comparison.
|
||||
type Comparer interface {
|
||||
// Compare returns true if the result of hashing
|
||||
// plaintextPassword with salt is hashedPassword,
|
||||
// false otherwise. An error is returned only if
|
||||
// plaintextPassword is hashedPassword, false
|
||||
// otherwise. An error is returned only if
|
||||
// there is a technical/configuration error.
|
||||
Compare(hashedPassword, plaintextPassword, salt []byte) (bool, error)
|
||||
Compare(hashedPassword, plaintextPassword []byte) (bool, error)
|
||||
}
|
||||
|
||||
// Hasher is a type that can generate a secure hash
|
||||
// given a plaintext and optional salt (for algorithms
|
||||
// that require a salt). Hashing modules which implement
|
||||
// given a plaintext. Hashing modules which implement
|
||||
// this interface can be used with the hash-password
|
||||
// subcommand as well as benefitting from anti-timing
|
||||
// features. A hasher also returns a fake hash which
|
||||
// can be used for timing side-channel mitigation.
|
||||
type Hasher interface {
|
||||
Hash(plaintext, salt []byte) ([]byte, error)
|
||||
Hash(plaintext []byte) ([]byte, error)
|
||||
FakeHash() []byte
|
||||
}
|
||||
|
||||
// Account contains a username, password, and salt (if applicable).
|
||||
// Account contains a username and password.
|
||||
type Account struct {
|
||||
// A user's username.
|
||||
Username string `json:"username"`
|
||||
|
||||
// The user's hashed password, base64-encoded.
|
||||
// The user's hashed password, in Modular Crypt Format (with `$` prefix)
|
||||
// or base64-encoded.
|
||||
Password string `json:"password"`
|
||||
|
||||
// The user's password salt, base64-encoded; for
|
||||
// algorithms where external salt is needed.
|
||||
Salt string `json:"salt,omitempty"`
|
||||
|
||||
password, salt []byte
|
||||
password []byte
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
|
||||
@@ -22,13 +22,14 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile)
|
||||
httpcaddyfile.RegisterHandlerDirective("basicauth", parseCaddyfile) // deprecated
|
||||
httpcaddyfile.RegisterHandlerDirective("basic_auth", parseCaddyfile)
|
||||
}
|
||||
|
||||
// parseCaddyfile sets up the handler from Caddyfile tokens. Syntax:
|
||||
//
|
||||
// basicauth [<matcher>] [<hash_algorithm> [<realm>]] {
|
||||
// <username> <hashed_password_base64> [<salt_base64>]
|
||||
// basic_auth [<matcher>] [<hash_algorithm> [<realm>]] {
|
||||
// <username> <hashed_password>
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
@@ -36,6 +37,11 @@ func init() {
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
h.Next() // consume directive name
|
||||
|
||||
// "basicauth" is deprecated, replaced by "basic_auth"
|
||||
if h.Val() == "basicauth" {
|
||||
caddy.Log().Named("config.adapter.caddyfile").Warn("the 'basicauth' directive is deprecated, please use 'basic_auth' instead!")
|
||||
}
|
||||
|
||||
var ba HTTPBasicAuth
|
||||
ba.HashCache = new(Cache)
|
||||
|
||||
@@ -58,8 +64,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
switch hashName {
|
||||
case "bcrypt":
|
||||
cmp = BcryptHash{}
|
||||
case "scrypt":
|
||||
cmp = ScryptHash{}
|
||||
default:
|
||||
return nil, h.Errf("unrecognized hash algorithm: %s", hashName)
|
||||
}
|
||||
@@ -69,8 +73,8 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
for h.NextBlock(0) {
|
||||
username := h.Val()
|
||||
|
||||
var b64Pwd, b64Salt string
|
||||
h.Args(&b64Pwd, &b64Salt)
|
||||
var b64Pwd string
|
||||
h.Args(&b64Pwd)
|
||||
if h.NextArg() {
|
||||
return nil, h.ArgErr()
|
||||
}
|
||||
@@ -82,7 +86,6 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error)
|
||||
ba.AccountList = append(ba.AccountList, Account{
|
||||
Username: username,
|
||||
Password: b64Pwd,
|
||||
Salt: b64Salt,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ package caddyauth
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -33,7 +32,7 @@ import (
|
||||
func init() {
|
||||
caddycmd.RegisterCommand(caddycmd.Command{
|
||||
Name: "hash-password",
|
||||
Usage: "[--algorithm <name>] [--salt <string>] [--plaintext <password>]",
|
||||
Usage: "[--plaintext <password>] [--algorithm <name>]",
|
||||
Short: "Hashes a password and writes base64",
|
||||
Long: `
|
||||
Convenient way to hash a plaintext password. The resulting
|
||||
@@ -43,17 +42,10 @@ hash is written to stdout as a base64 string.
|
||||
Caddy is attached to a controlling tty, the plaintext will
|
||||
not be echoed.
|
||||
|
||||
--algorithm may be bcrypt or scrypt. If scrypt, the default
|
||||
parameters are used.
|
||||
|
||||
Use the --salt flag for algorithms which require a salt to
|
||||
be provided (scrypt).
|
||||
|
||||
Note that scrypt is deprecated. Please use 'bcrypt' instead.
|
||||
--algorithm currently only supports 'bcrypt', and is the default.
|
||||
`,
|
||||
CobraFunc: func(cmd *cobra.Command) {
|
||||
cmd.Flags().StringP("plaintext", "p", "", "The plaintext password")
|
||||
cmd.Flags().StringP("salt", "s", "", "The password salt")
|
||||
cmd.Flags().StringP("algorithm", "a", "bcrypt", "Name of the hash algorithm")
|
||||
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdHashPassword)
|
||||
},
|
||||
@@ -65,7 +57,6 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
|
||||
|
||||
algorithm := fs.String("algorithm")
|
||||
plaintext := []byte(fs.String("plaintext"))
|
||||
salt := []byte(fs.String("salt"))
|
||||
|
||||
if len(plaintext) == 0 {
|
||||
fd := int(os.Stdin.Fd())
|
||||
@@ -117,13 +108,8 @@ func cmdHashPassword(fs caddycmd.Flags) (int, error) {
|
||||
var hashString string
|
||||
switch algorithm {
|
||||
case "bcrypt":
|
||||
hash, err = BcryptHash{}.Hash(plaintext, nil)
|
||||
hash, err = BcryptHash{}.Hash(plaintext)
|
||||
hashString = string(hash)
|
||||
case "scrypt":
|
||||
def := ScryptHash{}
|
||||
def.SetDefaults()
|
||||
hash, err = def.Hash(plaintext, salt)
|
||||
hashString = base64.StdEncoding.EncodeToString(hash)
|
||||
default:
|
||||
return caddy.ExitCodeFailedStartup, fmt.Errorf("unrecognized hash algorithm: %s", algorithm)
|
||||
}
|
||||
|
||||
@@ -15,18 +15,13 @@
|
||||
package caddyauth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(BcryptHash{})
|
||||
caddy.RegisterModule(ScryptHash{})
|
||||
}
|
||||
|
||||
// BcryptHash implements the bcrypt hash.
|
||||
@@ -41,7 +36,7 @@ func (BcryptHash) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
|
||||
// Compare compares passwords.
|
||||
func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) {
|
||||
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
|
||||
err := bcrypt.CompareHashAndPassword(hashed, plaintext)
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
return false, nil
|
||||
@@ -53,7 +48,7 @@ func (BcryptHash) Compare(hashed, plaintext, _ []byte) (bool, error) {
|
||||
}
|
||||
|
||||
// Hash hashes plaintext using a random salt.
|
||||
func (BcryptHash) Hash(plaintext, _ []byte) ([]byte, error) {
|
||||
func (BcryptHash) Hash(plaintext []byte) ([]byte, error) {
|
||||
return bcrypt.GenerateFromPassword(plaintext, 14)
|
||||
}
|
||||
|
||||
@@ -64,94 +59,8 @@ func (BcryptHash) FakeHash() []byte {
|
||||
return []byte("$2a$14$X3ulqf/iGxnf1k6oMZ.RZeJUoqI9PX2PM4rS5lkIKJXduLGXGPrt6")
|
||||
}
|
||||
|
||||
// ScryptHash implements the scrypt KDF as a hash.
|
||||
//
|
||||
// DEPRECATED, please use 'bcrypt' instead.
|
||||
type ScryptHash struct {
|
||||
// scrypt's N parameter. If unset or 0, a safe default is used.
|
||||
N int `json:"N,omitempty"`
|
||||
|
||||
// scrypt's r parameter. If unset or 0, a safe default is used.
|
||||
R int `json:"r,omitempty"`
|
||||
|
||||
// scrypt's p parameter. If unset or 0, a safe default is used.
|
||||
P int `json:"p,omitempty"`
|
||||
|
||||
// scrypt's key length parameter (in bytes). If unset or 0, a
|
||||
// safe default is used.
|
||||
KeyLength int `json:"key_length,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (ScryptHash) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.authentication.hashes.scrypt",
|
||||
New: func() caddy.Module { return new(ScryptHash) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up s.
|
||||
func (s *ScryptHash) Provision(ctx caddy.Context) error {
|
||||
s.SetDefaults()
|
||||
ctx.Logger().Warn("use of 'scrypt' is deprecated, please use 'bcrypt' instead")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaults sets safe default parameters, but does
|
||||
// not overwrite existing values. Each default parameter
|
||||
// is set independently; it does not check to ensure
|
||||
// that r*p < 2^30. The defaults chosen are those as
|
||||
// recommended in 2019 by
|
||||
// https://godoc.org/golang.org/x/crypto/scrypt.
|
||||
func (s *ScryptHash) SetDefaults() {
|
||||
if s.N == 0 {
|
||||
s.N = 32768
|
||||
}
|
||||
if s.R == 0 {
|
||||
s.R = 8
|
||||
}
|
||||
if s.P == 0 {
|
||||
s.P = 1
|
||||
}
|
||||
if s.KeyLength == 0 {
|
||||
s.KeyLength = 32
|
||||
}
|
||||
}
|
||||
|
||||
// Compare compares passwords.
|
||||
func (s ScryptHash) Compare(hashed, plaintext, salt []byte) (bool, error) {
|
||||
ourHash, err := scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if hashesMatch(hashed, ourHash) {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Hash hashes plaintext using the given salt.
|
||||
func (s ScryptHash) Hash(plaintext, salt []byte) ([]byte, error) {
|
||||
return scrypt.Key(plaintext, salt, s.N, s.R, s.P, s.KeyLength)
|
||||
}
|
||||
|
||||
// FakeHash returns a fake hash.
|
||||
func (ScryptHash) FakeHash() []byte {
|
||||
// hashed with the following command:
|
||||
// caddy hash-password --plaintext "antitiming" --salt "fakesalt" --algorithm "scrypt"
|
||||
bytes, _ := base64.StdEncoding.DecodeString("kFbjiVemlwK/ZS0tS6/UQqEDeaNMigyCs48KEsGUse8=")
|
||||
return bytes
|
||||
}
|
||||
|
||||
func hashesMatch(pwdHash1, pwdHash2 []byte) bool {
|
||||
return subtle.ConstantTimeCompare(pwdHash1, pwdHash2) == 1
|
||||
}
|
||||
|
||||
// Interface guards
|
||||
var (
|
||||
_ Comparer = (*BcryptHash)(nil)
|
||||
_ Comparer = (*ScryptHash)(nil)
|
||||
_ Hasher = (*BcryptHash)(nil)
|
||||
_ Hasher = (*ScryptHash)(nil)
|
||||
_ caddy.Provisioner = (*ScryptHash)(nil)
|
||||
_ Comparer = (*BcryptHash)(nil)
|
||||
_ Hasher = (*BcryptHash)(nil)
|
||||
)
|
||||
|
||||
@@ -783,7 +783,7 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
|
||||
// regardless, and we should expect client disconnection in low-latency streaming
|
||||
// scenarios (see issue #4922)
|
||||
if h.FlushInterval == -1 {
|
||||
req = req.WithContext(ignoreClientGoneContext{req.Context()})
|
||||
req = req.WithContext(context.WithoutCancel(req.Context()))
|
||||
}
|
||||
|
||||
// do the round-trip; emit debug log with values we know are
|
||||
@@ -1419,32 +1419,6 @@ type handleResponseContext struct {
|
||||
isFinalized bool
|
||||
}
|
||||
|
||||
// ignoreClientGoneContext is a special context.Context type
|
||||
// intended for use when doing a RoundTrip where you don't
|
||||
// want a client disconnection to cancel the request during
|
||||
// the roundtrip.
|
||||
// This context clears cancellation, error, and deadline methods,
|
||||
// but still allows values to pass through from its embedded
|
||||
// context.
|
||||
//
|
||||
// TODO: This can be replaced with context.WithoutCancel once
|
||||
// the minimum required version of Go is 1.21.
|
||||
type ignoreClientGoneContext struct {
|
||||
context.Context
|
||||
}
|
||||
|
||||
func (c ignoreClientGoneContext) Deadline() (deadline time.Time, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c ignoreClientGoneContext) Done() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c ignoreClientGoneContext) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// proxyHandleResponseContextCtxKey is the context key for the active proxy handler
|
||||
// so that handle_response routes can inherit some config options
|
||||
// from the proxy handler.
|
||||
|
||||
@@ -655,12 +655,22 @@ func (s CookieHashSelection) Select(pool UpstreamPool, req *http.Request, w http
|
||||
if err != nil {
|
||||
return upstream
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
cookie := &http.Cookie{
|
||||
Name: s.Name,
|
||||
Value: sha,
|
||||
Path: "/",
|
||||
Secure: false,
|
||||
})
|
||||
}
|
||||
isProxyHttps := false
|
||||
if trusted, ok := caddyhttp.GetVar(req.Context(), caddyhttp.TrustedProxyVarKey).(bool); ok && trusted {
|
||||
xfp, xfpOk, _ := lastHeaderValue(req.Header, "X-Forwarded-Proto")
|
||||
isProxyHttps = xfpOk && xfp == "https"
|
||||
}
|
||||
if req.TLS != nil || isProxyHttps {
|
||||
cookie.Secure = true
|
||||
cookie.SameSite = http.SameSiteNoneMode
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
return upstream
|
||||
}
|
||||
|
||||
|
||||
@@ -658,6 +658,9 @@ func TestCookieHashPolicy(t *testing.T) {
|
||||
if cookieServer1.Name != "lb" {
|
||||
t.Error("cookieHashPolicy should set a cookie with name lb")
|
||||
}
|
||||
if cookieServer1.Secure {
|
||||
t.Error("cookieHashPolicy should set cookie Secure attribute to false when request is not secure")
|
||||
}
|
||||
if h != pool[0] {
|
||||
t.Error("Expected cookieHashPolicy host to be the first only available host.")
|
||||
}
|
||||
@@ -687,6 +690,57 @@ func TestCookieHashPolicy(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieHashPolicyWithSecureRequest(t *testing.T) {
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
cookieHashPolicy := CookieHashSelection{}
|
||||
if err := cookieHashPolicy.Provision(ctx); err != nil {
|
||||
t.Errorf("Provision error: %v", err)
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
pool := testPool()
|
||||
pool[0].Dial = "localhost:8080"
|
||||
pool[1].Dial = "localhost:8081"
|
||||
pool[2].Dial = "localhost:8082"
|
||||
pool[0].setHealthy(true)
|
||||
pool[1].setHealthy(false)
|
||||
pool[2].setHealthy(false)
|
||||
|
||||
// Create a test server that serves HTTPS requests
|
||||
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
h := cookieHashPolicy.Select(pool, r, w)
|
||||
if h != pool[0] {
|
||||
t.Error("Expected cookieHashPolicy host to be the first only available host.")
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Make a new HTTPS request to the test server
|
||||
client := ts.Client()
|
||||
request, err := http.NewRequest(http.MethodGet, ts.URL+"/test", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check if the cookie set is Secure and has SameSiteNone mode
|
||||
cookies := response.Cookies()
|
||||
if len(cookies) == 0 {
|
||||
t.Fatal("Expected a cookie to be set")
|
||||
}
|
||||
cookie := cookies[0]
|
||||
if !cookie.Secure {
|
||||
t.Error("Expected cookie Secure attribute to be true when request is secure")
|
||||
}
|
||||
if cookie.SameSite != http.SameSiteNoneMode {
|
||||
t.Error("Expected cookie SameSite attribute to be None when request is secure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCookieHashPolicyWithFirstFallback(t *testing.T) {
|
||||
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
|
||||
defer cancel()
|
||||
|
||||
@@ -253,6 +253,7 @@ type Server struct {
|
||||
connStateFuncs []func(net.Conn, http.ConnState)
|
||||
connContextFuncs []func(ctx context.Context, c net.Conn) context.Context
|
||||
onShutdownFuncs []func()
|
||||
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
|
||||
}
|
||||
|
||||
// ServeHTTP is the entry point for all HTTP requests.
|
||||
@@ -301,11 +302,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// enable full-duplex for HTTP/1, ensuring the entire
|
||||
// request body gets consumed before writing the response
|
||||
if s.EnableFullDuplex {
|
||||
if s.EnableFullDuplex && r.ProtoMajor == 1 {
|
||||
//nolint:bodyclose
|
||||
err := http.NewResponseController(w).EnableFullDuplex()
|
||||
if err != nil {
|
||||
s.accessLogger.Warn("failed to enable full duplex", zap.Error(err))
|
||||
s.logger.Warn("failed to enable full duplex", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,11 +631,18 @@ func (s *Server) RegisterConnContext(f func(ctx context.Context, c net.Conn) con
|
||||
s.connContextFuncs = append(s.connContextFuncs, f)
|
||||
}
|
||||
|
||||
// RegisterOnShutdown registers f to be invoked on server shutdown.
|
||||
// RegisterOnShutdown registers f to be invoked when the server begins to shut down.
|
||||
func (s *Server) RegisterOnShutdown(f func()) {
|
||||
s.onShutdownFuncs = append(s.onShutdownFuncs, f)
|
||||
}
|
||||
|
||||
// RegisterOnStop registers f to be invoked after the server has shut down completely.
|
||||
//
|
||||
// EXPERIMENTAL: Subject to change or removal.
|
||||
func (s *Server) RegisterOnStop(f func(context.Context) error) {
|
||||
s.onStopFuncs = append(s.onStopFuncs, f)
|
||||
}
|
||||
|
||||
// HTTPErrorConfig determines how to handle errors
|
||||
// from the HTTP handlers.
|
||||
type HTTPErrorConfig struct {
|
||||
|
||||
@@ -169,6 +169,27 @@ func (IPMaskFilter) CaddyModule() caddy.ModuleInfo {
|
||||
// UnmarshalCaddyfile sets up the module from Caddyfile tokens.
|
||||
func (m *IPMaskFilter) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
d.Next() // consume filter name
|
||||
|
||||
args := d.RemainingArgs()
|
||||
if len(args) > 2 {
|
||||
return d.Errf("too many arguments")
|
||||
}
|
||||
if len(args) > 0 {
|
||||
val, err := strconv.Atoi(args[0])
|
||||
if err != nil {
|
||||
return d.Errf("error parsing %s: %v", args[0], err)
|
||||
}
|
||||
m.IPv4MaskRaw = val
|
||||
|
||||
if len(args) > 1 {
|
||||
val, err := strconv.Atoi(args[1])
|
||||
if err != nil {
|
||||
return d.Errf("error parsing %s: %v", args[1], err)
|
||||
}
|
||||
m.IPv6MaskRaw = val
|
||||
}
|
||||
}
|
||||
|
||||
for d.NextBlock(0) {
|
||||
switch d.Val() {
|
||||
case "ipv4":
|
||||
|
||||
Reference in New Issue
Block a user