mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-28 14:43:48 +03:00
Cache capacity is currently hard-coded at 1000 with random eviction. It is enabled by default from Caddyfile configurations because I assume this is the most common preference.
This commit is contained in:
parent
fdf2a77feb
commit
9a7756c6e4
2 changed files with 102 additions and 3 deletions
|
@ -16,15 +16,21 @@ package caddyauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
weakrand "math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
caddy.RegisterModule(HTTPBasicAuth{})
|
caddy.RegisterModule(HTTPBasicAuth{})
|
||||||
|
|
||||||
|
weakrand.Seed(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTTPBasicAuth facilitates HTTP basic authentication.
|
// HTTPBasicAuth facilitates HTTP basic authentication.
|
||||||
|
@ -38,6 +44,17 @@ type HTTPBasicAuth struct {
|
||||||
// The name of the realm. Default: restricted
|
// The name of the realm. Default: restricted
|
||||||
Realm string `json:"realm,omitempty"`
|
Realm string `json:"realm,omitempty"`
|
||||||
|
|
||||||
|
// If non-nil, a mapping of plaintext passwords to their
|
||||||
|
// hashes will be cached in memory (with random eviction).
|
||||||
|
// This can greatly improve the performance of traffic-heavy
|
||||||
|
// servers that use secure password hashing algorithms, with
|
||||||
|
// the downside that plaintext passwords will be stored in
|
||||||
|
// memory for a longer time (this should not be a problem
|
||||||
|
// as long as your machine is not compromised, at which point
|
||||||
|
// all bets are off, since basicauth necessitates plaintext
|
||||||
|
// passwords being received over the wire anyway).
|
||||||
|
HashCache *Cache `json:"hash_cache,omitempty"`
|
||||||
|
|
||||||
Accounts map[string]Account `json:"-"`
|
Accounts map[string]Account `json:"-"`
|
||||||
Hash Comparer `json:"-"`
|
Hash Comparer `json:"-"`
|
||||||
}
|
}
|
||||||
|
@ -99,6 +116,11 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
|
||||||
}
|
}
|
||||||
hba.AccountList = nil // allow GC to deallocate
|
hba.AccountList = nil // allow GC to deallocate
|
||||||
|
|
||||||
|
if hba.HashCache != nil {
|
||||||
|
hba.HashCache.cache = make(map[string]bool)
|
||||||
|
hba.HashCache.mu = new(sync.Mutex)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,13 +131,11 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
|
||||||
return hba.promptForCredentials(w, nil)
|
return hba.promptForCredentials(w, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
plaintextPassword := []byte(plaintextPasswordStr)
|
|
||||||
|
|
||||||
account, accountExists := hba.Accounts[username]
|
account, accountExists := hba.Accounts[username]
|
||||||
// don't return early if account does not exist; we want
|
// don't return early if account does not exist; we want
|
||||||
// to try to avoid side-channels that leak existence
|
// to try to avoid side-channels that leak existence
|
||||||
|
|
||||||
same, err := hba.Hash.Compare(account.password, plaintextPassword, account.salt)
|
same, err := hba.correctPassword(account, []byte(plaintextPasswordStr))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return hba.promptForCredentials(w, err)
|
return hba.promptForCredentials(w, err)
|
||||||
}
|
}
|
||||||
|
@ -126,6 +146,43 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
|
||||||
return User{ID: username}, true, nil
|
return User{ID: username}, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hba HTTPBasicAuth) correctPassword(account Account, plaintextPassword []byte) (bool, error) {
|
||||||
|
compare := func() (bool, error) {
|
||||||
|
return hba.Hash.Compare(account.password, plaintextPassword, account.salt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no caching is enabled, simply return the result of hashing + comparing
|
||||||
|
if hba.HashCache == nil {
|
||||||
|
return compare()
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute a cache key that is unique for these input parameters
|
||||||
|
cacheKey := hex.EncodeToString(append(append(account.password, account.salt...), plaintextPassword...))
|
||||||
|
|
||||||
|
// fast track: if the result of the input is already cached, use it
|
||||||
|
hba.HashCache.mu.Lock()
|
||||||
|
same, ok := hba.HashCache.cache[cacheKey]
|
||||||
|
if ok {
|
||||||
|
hba.HashCache.mu.Unlock()
|
||||||
|
return same, nil
|
||||||
|
}
|
||||||
|
hba.HashCache.mu.Unlock()
|
||||||
|
|
||||||
|
// slow track: do the expensive op, then add it to the cache
|
||||||
|
same, err := compare()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
hba.HashCache.mu.Lock()
|
||||||
|
if len(hba.HashCache.cache) >= 1000 {
|
||||||
|
hba.HashCache.makeRoom() // keep cache size under control
|
||||||
|
}
|
||||||
|
hba.HashCache.cache[cacheKey] = same
|
||||||
|
hba.HashCache.mu.Unlock()
|
||||||
|
|
||||||
|
return same, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error) (User, bool, error) {
|
func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error) (User, bool, error) {
|
||||||
// browsers show a message that says something like:
|
// browsers show a message that says something like:
|
||||||
// "The website says: <realm>"
|
// "The website says: <realm>"
|
||||||
|
@ -138,6 +195,47 @@ func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error)
|
||||||
return User{}, false, err
|
return User{}, false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache enables caching of basic auth results. This is especially
|
||||||
|
// helpful for secure password hashes which can be expensive to
|
||||||
|
// compute on every HTTP request.
|
||||||
|
type Cache struct {
|
||||||
|
mu *sync.Mutex
|
||||||
|
|
||||||
|
// map of concatenated hashed password + plaintext password + salt, to result
|
||||||
|
cache map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeRoom deletes about 1/10 of the items in the cache
|
||||||
|
// in order to keep its size under control. It must not be
|
||||||
|
// called without a lock on c.mu.
|
||||||
|
func (c *Cache) makeRoom() {
|
||||||
|
// we delete more than just 1 entry so that we don't have
|
||||||
|
// to do this on every request; assuming the capacity of
|
||||||
|
// the cache is on a long tail, we can save a lot of CPU
|
||||||
|
// time by doing a whole bunch of deletions now and then
|
||||||
|
// we won't have to do them again for a while
|
||||||
|
numToDelete := len(c.cache) / 10
|
||||||
|
if numToDelete < 1 {
|
||||||
|
numToDelete = 1
|
||||||
|
}
|
||||||
|
for deleted := 0; deleted <= numToDelete; deleted++ {
|
||||||
|
// Go maps are "nondeterministic" not actually random,
|
||||||
|
// so although we could just chop off the "front" of the
|
||||||
|
// map with less code, this is a heavily skewed eviction
|
||||||
|
// strategy; generating random numbers is cheap and
|
||||||
|
// ensures a much better distribution.
|
||||||
|
rnd := weakrand.Intn(len(c.cache))
|
||||||
|
i := 0
|
||||||
|
for key := range c.cache {
|
||||||
|
if i == rnd {
|
||||||
|
delete(c.cache, key)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Comparer is a type that can securely compare
|
// Comparer is a type that can securely compare
|
||||||
// a plaintext password with a hashed password
|
// a plaintext password with a hashed password
|
||||||
// in constant-time. Comparers should hash the
|
// in constant-time. Comparers should hash the
|
||||||
|
|
|
@ -35,6 +35,7 @@ func init() {
|
||||||
// If no hash algorithm is supplied, bcrypt will be assumed.
|
// If no hash algorithm is supplied, bcrypt will be assumed.
|
||||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
var ba HTTPBasicAuth
|
var ba HTTPBasicAuth
|
||||||
|
ba.HashCache = new(Cache)
|
||||||
|
|
||||||
for h.Next() {
|
for h.Next() {
|
||||||
var cmp Comparer
|
var cmp Comparer
|
||||||
|
|
Loading…
Reference in a new issue