Skip to content

Commit fa3ecab

Browse files
author
Julien Pivotto
authored
Merge pull request #32 from roidelapluie/fastauth
Cache basic authentications
2 parents 89922ed + 2d92119 commit fa3ecab

File tree

6 files changed

+244
-10
lines changed

6 files changed

+244
-10
lines changed

web/cache.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2021 The Prometheus Authors
2+
// This code is partly borrowed from Caddy:
3+
// Copyright 2015 Matthew Holt and The Caddy Authors
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package web
17+
18+
import (
19+
weakrand "math/rand"
20+
"sync"
21+
"time"
22+
)
23+
24+
var cacheSize = 100
25+
26+
func init() {
27+
weakrand.Seed(time.Now().UnixNano())
28+
}
29+
30+
type cache struct {
31+
cache map[string]bool
32+
mtx sync.Mutex
33+
}
34+
35+
// newCache returns a cache that contains a mapping of plaintext passwords
36+
// to their hashes (with random eviction). This can greatly improve the
37+
// performance of traffic-heavy servers that use secure password hashing
38+
// algorithms, with the downside that plaintext passwords will be stored in
39+
// memory for a longer time (this should not be a problem as long as your
40+
// machine is not compromised, at which point all bets are off, since basicauth
41+
// necessitates plaintext passwords being received over the wire anyway).
42+
func newCache(size int) *cache {
43+
return &cache{
44+
cache: make(map[string]bool, size),
45+
}
46+
}
47+
48+
func (c *cache) get(key string) (bool, bool) {
49+
c.mtx.Lock()
50+
defer c.mtx.Unlock()
51+
v, ok := c.cache[key]
52+
return v, ok
53+
}
54+
55+
func (c *cache) set(key string, value bool) {
56+
c.mtx.Lock()
57+
defer c.mtx.Unlock()
58+
c.makeRoom()
59+
c.cache[key] = value
60+
}
61+
62+
func (c *cache) makeRoom() {
63+
if len(c.cache) < cacheSize {
64+
return
65+
}
66+
// We delete more than just 1 entry so that we don't have
67+
// to do this on every request; assuming the capacity of
68+
// the cache is on a long tail, we can save a lot of CPU
69+
// time by doing a whole bunch of deletions now and then
70+
// we won't have to do them again for a while.
71+
numToDelete := len(c.cache) / 10
72+
if numToDelete < 1 {
73+
numToDelete = 1
74+
}
75+
for deleted := 0; deleted <= numToDelete; deleted++ {
76+
// Go maps are "nondeterministic" not actually random,
77+
// so although we could just chop off the "front" of the
78+
// map with less code, this is a heavily skewed eviction
79+
// strategy; generating random numbers is cheap and
80+
// ensures a much better distribution.
81+
rnd := weakrand.Intn(len(c.cache))
82+
i := 0
83+
for key := range c.cache {
84+
if i == rnd {
85+
delete(c.cache, key)
86+
break
87+
}
88+
i++
89+
}
90+
}
91+
}

web/cache_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2021 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package web
15+
16+
import (
17+
"fmt"
18+
"testing"
19+
)
20+
21+
// TestCacheSize validates that makeRoom function caps the size of the cache
22+
// appropriately.
23+
func TestCacheSize(t *testing.T) {
24+
cache := newCache(100)
25+
expectedSize := 0
26+
for i := 0; i < 200; i++ {
27+
cache.set(fmt.Sprintf("foo%d", i), true)
28+
expectedSize++
29+
if expectedSize > 100 {
30+
expectedSize = 90
31+
}
32+
33+
if gotSize := len(cache.cache); gotSize != expectedSize {
34+
t.Fatalf("iter %d: cache size invalid: expected %d, got %d", i, expectedSize, gotSize)
35+
}
36+
}
37+
}

web/tls_config.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,17 +201,19 @@ func Serve(l net.Listener, server *http.Server, tlsConfigPath string, logger log
201201
if server.Handler != nil {
202202
handler = server.Handler
203203
}
204-
server.Handler = &userAuthRoundtrip{
205-
tlsConfigPath: tlsConfigPath,
206-
logger: logger,
207-
handler: handler,
208-
}
209204

210205
c, err := getConfig(tlsConfigPath)
211206
if err != nil {
212207
return err
213208
}
214209

210+
server.Handler = &userAuthRoundtrip{
211+
tlsConfigPath: tlsConfigPath,
212+
logger: logger,
213+
handler: handler,
214+
cache: newCache(len(c.Users)),
215+
}
216+
215217
config, err := ConfigToTLSConfig(&c.TLSConfig)
216218
switch err {
217219
case nil:

web/tls_config_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -382,16 +382,14 @@ func (test *TestInputs) Test(t *testing.T) {
382382
w.Write([]byte("Hello World!"))
383383
}),
384384
}
385-
defer func() {
386-
server.Close()
387-
}()
385+
t.Cleanup(func() { server.Close() })
388386
go func() {
389387
defer func() {
390388
if recover() != nil {
391389
recordConnectionError(errors.New("Panic starting server"))
392390
}
393391
}()
394-
err := Listen(server, test.YAMLConfigPath, testlogger)
392+
err := ListenAndServe(server, test.YAMLConfigPath, testlogger)
395393
recordConnectionError(err)
396394
}()
397395

web/users.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// Copyright 2020 The Prometheus Authors
2+
// This code is partly borrowed from Caddy:
3+
// Copyright 2015 Matthew Holt and The Caddy Authors
24
// Licensed under the Apache License, Version 2.0 (the "License");
35
// you may not use this file except in compliance with the License.
46
// You may obtain a copy of the License at
@@ -14,7 +16,9 @@
1416
package web
1517

1618
import (
19+
"encoding/hex"
1720
"net/http"
21+
"sync"
1822

1923
"github.com/go-kit/kit/log"
2024
"golang.org/x/crypto/bcrypt"
@@ -40,6 +44,10 @@ type userAuthRoundtrip struct {
4044
tlsConfigPath string
4145
handler http.Handler
4246
logger log.Logger
47+
cache *cache
48+
// bcryptMtx is there to ensure that bcrypt.CompareHashAndPassword is run
49+
// only once in parallel as this is CPU expansive.
50+
bcryptMtx sync.Mutex
4351
}
4452

4553
func (u *userAuthRoundtrip) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -58,7 +66,20 @@ func (u *userAuthRoundtrip) ServeHTTP(w http.ResponseWriter, r *http.Request) {
5866
user, pass, auth := r.BasicAuth()
5967
if auth {
6068
if hashedPassword, ok := c.Users[user]; ok {
61-
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass)); err == nil {
69+
cacheKey := hex.EncodeToString(append(append([]byte(user), []byte(hashedPassword)...), []byte(pass)...))
70+
authOk, ok := u.cache.get(cacheKey)
71+
72+
if !ok {
73+
// This user, hashedPassword, password is not cached.
74+
u.bcryptMtx.Lock()
75+
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass))
76+
u.bcryptMtx.Unlock()
77+
78+
authOk = err == nil
79+
u.cache.set(cacheKey, authOk)
80+
}
81+
82+
if authOk {
6283
u.handler.ServeHTTP(w, r)
6384
return
6485
}

web/users_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2021 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package web
15+
16+
import (
17+
"context"
18+
"net/http"
19+
"sync"
20+
"testing"
21+
)
22+
23+
// TestBasicAuthCache validates that the cache is working by calling a password
24+
// protected endpoint multiple times.
25+
func TestBasicAuthCache(t *testing.T) {
26+
server := &http.Server{
27+
Addr: port,
28+
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
29+
w.Write([]byte("Hello World!"))
30+
}),
31+
}
32+
33+
done := make(chan struct{})
34+
t.Cleanup(func() {
35+
if err := server.Shutdown(context.Background()); err != nil {
36+
t.Fatal(err)
37+
}
38+
<-done
39+
})
40+
41+
go func() {
42+
ListenAndServe(server, "testdata/tls_config_users_noTLS.good.yml", testlogger)
43+
close(done)
44+
}()
45+
46+
login := func(username, password string, code int) {
47+
client := &http.Client{}
48+
req, err := http.NewRequest("GET", "http://localhost"+port, nil)
49+
if err != nil {
50+
t.Fatal(err)
51+
}
52+
req.SetBasicAuth(username, password)
53+
r, err := client.Do(req)
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
if r.StatusCode != code {
58+
t.Fatalf("bad return code, expected %d, got %d", code, r.StatusCode)
59+
}
60+
}
61+
62+
// Initial logins, checking that it just works.
63+
login("alice", "alice123", 200)
64+
login("alice", "alice1234", 401)
65+
66+
var (
67+
start = make(chan struct{})
68+
wg sync.WaitGroup
69+
)
70+
wg.Add(300)
71+
for i := 0; i < 150; i++ {
72+
go func() {
73+
<-start
74+
login("alice", "alice123", 200)
75+
wg.Done()
76+
}()
77+
go func() {
78+
<-start
79+
login("alice", "alice1234", 401)
80+
wg.Done()
81+
}()
82+
}
83+
close(start)
84+
wg.Wait()
85+
}

0 commit comments

Comments
 (0)