Merge branch 'master' into acme-database

This commit is contained in:
Mohammed Al Sahaf
2024-02-24 02:12:25 +03:00
committed by GitHub
153 changed files with 303 additions and 233 deletions
+9
View File
@@ -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
}
+12 -24
View File
@@ -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
+11 -8
View File
@@ -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,
})
}
+3 -17
View File
@@ -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)
}
+4 -95
View File
@@ -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)
)
+1 -27
View File
@@ -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()
+11 -3
View File
@@ -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 {
+21
View File
@@ -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":