mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-16 21:56:40 -05: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 (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
weakrand "math/rand"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(HTTPBasicAuth{})
|
||||
|
||||
weakrand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// HTTPBasicAuth facilitates HTTP basic authentication.
|
||||
|
@ -38,6 +44,17 @@ type HTTPBasicAuth struct {
|
|||
// The name of the realm. Default: restricted
|
||||
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:"-"`
|
||||
Hash Comparer `json:"-"`
|
||||
}
|
||||
|
@ -99,6 +116,11 @@ func (hba *HTTPBasicAuth) Provision(ctx caddy.Context) error {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -109,13 +131,11 @@ func (hba HTTPBasicAuth) Authenticate(w http.ResponseWriter, req *http.Request)
|
|||
return hba.promptForCredentials(w, nil)
|
||||
}
|
||||
|
||||
plaintextPassword := []byte(plaintextPasswordStr)
|
||||
|
||||
account, accountExists := hba.Accounts[username]
|
||||
// don't return early if account does not exist; we want
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
// browsers show a message that says something like:
|
||||
// "The website says: <realm>"
|
||||
|
@ -138,6 +195,47 @@ func (hba HTTPBasicAuth) promptForCredentials(w http.ResponseWriter, err error)
|
|||
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
|
||||
// a plaintext password with a hashed password
|
||||
// in constant-time. Comparers should hash the
|
||||
|
|
|
@ -35,6 +35,7 @@ func init() {
|
|||
// If no hash algorithm is supplied, bcrypt will be assumed.
|
||||
func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||
var ba HTTPBasicAuth
|
||||
ba.HashCache = new(Cache)
|
||||
|
||||
for h.Next() {
|
||||
var cmp Comparer
|
||||
|
|
Loading…
Reference in a new issue