Skip to content

Commit

Permalink
home: fix authratelimiter
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeneOne1 committed Apr 27, 2021
1 parent 724db43 commit c73a407
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 175 deletions.
38 changes: 25 additions & 13 deletions internal/home/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (s *session) deserialize(data []byte) bool {
// Auth - global object
type Auth struct {
db *bbolt.DB
blocker *authBlocker
blocker *authRateLimiter
sessions map[string]*session
users []User
lock sync.Mutex
Expand All @@ -76,7 +76,7 @@ type User struct {
}

// InitAuth - create a global object
func InitAuth(dbFilename string, users []User, sessionTTL uint32, blocker *authBlocker) *Auth {
func InitAuth(dbFilename string, users []User, sessionTTL uint32, blocker *authRateLimiter) *Auth {
log.Info("Initializing auth module: %s", dbFilename)

a := &Auth{
Expand Down Expand Up @@ -333,19 +333,19 @@ func cookieExpiryFormat(exp time.Time) (formatted string) {
return exp.Format(cookieTimeFormat)
}

func (a *Auth) httpCookie(req loginJSON, ip net.IP) (cookie string, err error) {
func (a *Auth) httpCookie(req loginJSON, addr string) (cookie string, err error) {
blocker := a.blocker
u := a.UserFind(req.Name, req.Password)
if len(u.Name) == 0 {
if blocker != nil {
blocker.inc(ip)
blocker.inc(addr)
}

return "", err
}

if blocker != nil {
blocker.remove(ip)
blocker.remove(addr)
}

var sess []byte
Expand Down Expand Up @@ -417,39 +417,51 @@ func handleLogin(w http.ResponseWriter, r *http.Request) {
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
httpError(w, http.StatusBadRequest, "json decode: %s", err)

return
}

var ip net.IP
ip, err = realIP(r)
if err != nil {
log.Info("auth: getting real ip from request: %s", err)
var remoteAddr string
// The realIP couldn't be used here due to security issues.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2799.
//
// TODO(e.burkov): Use realIP when the issue will be fixed.
if remoteAddr, err = aghnet.SplitHost(r.RemoteAddr); err == nil {
httpError(w, http.StatusBadRequest, "auth: getting remote address: %s", err)

return
}

if blocker := Context.auth.blocker; blocker != nil {
left := blocker.check(ip)
left := blocker.check(remoteAddr)
if left > 0 {
w.Header().Set("Retry-After", fmt.Sprint(int64(left.Seconds())))
httpError(
w,
http.StatusTooManyRequests,
"auth: out of login attempts",
"auth: blocked for %d minute(s)",
0,
)

return
}
}

var cookie string
cookie, err = Context.auth.httpCookie(req, ip)
cookie, err = Context.auth.httpCookie(req, remoteAddr)
if err != nil {
httpError(w, http.StatusBadRequest, "crypto rand reader: %s", err)

return
}

if len(cookie) == 0 {
if ip == nil {
var ip net.IP
ip, err = realIP(r)
if err != nil {
log.Info("auth: getting real ip from request: %s", err)
} else if ip == nil {
// Technically shouldn't happen.
log.Info("auth: failed to login user %q from unknown ip", req.Name)
} else {
Expand Down
2 changes: 1 addition & 1 deletion internal/home/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ func TestAuthHTTP(t *testing.T) {
assert.True(t, handlerCalled)

// perform login
cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}, nil)
cookie, err := Context.auth.httpCookie(loginJSON{Name: "name", Password: "password"}, "")
assert.Nil(t, err)
assert.NotEmpty(t, cookie)

Expand Down
138 changes: 0 additions & 138 deletions internal/home/authblocker.go

This file was deleted.

119 changes: 119 additions & 0 deletions internal/home/authratelimiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package home

import (
"sync"
"time"
)

// flushAfter defines for how long will attempter be tracked.
const flushAfter = 1 * time.Minute

// attempter is an entry of authRateLimiter's cache.
type attempter struct {
until time.Time
num uint16
}

// authRateLimiter used to cache unsuccessful authentication attempts.
type authRateLimiter struct {
attempters map[string]attempter
blockDur time.Duration
attemptersLock sync.RWMutex
maxAttempts uint16
}

// newAuthRateLimiter returns properly initialized *authRateLimiter.
func newAuthRateLimiter(blockDur time.Duration, maxAttempts uint16) (ab *authRateLimiter) {
return &authRateLimiter{
attempters: make(map[string]attempter),
blockDur: blockDur,
maxAttempts: maxAttempts,
}
}

// flushLocked checks each tracked attempter removing expired ones. For
// internal use only.
func (ab *authRateLimiter) flushLocked(now time.Time) {
for k, v := range ab.attempters {
if now.After(v.until) {
delete(ab.attempters, k)
}
}
}

// flush stops tracking of attempters tracking period of which have ended.
func (ab *authRateLimiter) flush() {
now := time.Now()

ab.attemptersLock.Lock()
defer ab.attemptersLock.Unlock()

ab.flushLocked(now)
}

// checkLocked checks the attempter for it's state. For internal use only.
func (ab *authRateLimiter) checkLocked(attID string, now time.Time) (left time.Duration) {
a, ok := ab.attempters[attID]
if !ok {
return 0
}

if a.num < ab.maxAttempts {
return 0
}

return a.until.Sub(now)
}

// check returns the time left until unblocking. The nonpositive result should
// be interpreted as not blocked attempter.
func (ab *authRateLimiter) check(attID string) (left time.Duration) {
now := time.Now()
ab.flush()

ab.attemptersLock.RLock()
defer ab.attemptersLock.RUnlock()

return ab.checkLocked(attID, now)
}

// incLocked increments the number of unsuccessful attempts for attempter with
// ip and updates it's blocking moment if needed. For internal use only.
func (ab *authRateLimiter) incLocked(attID string, now time.Time) {
var until time.Time = now.Add(flushAfter)
var attempts uint16 = 1

a, ok := ab.attempters[attID]
if ok {
// The attempter will be tracked during at least 1 minute since
// very first unsuccessful attempt but not since each one.
until = a.until
attempts = a.num + 1
}
if attempts >= ab.maxAttempts {
until = now.Add(ab.blockDur)
}

ab.attempters[attID] = attempter{
num: attempts,
until: until,
}
}

// inc updates the tracked attempter.
func (ab *authRateLimiter) inc(attID string) {
now := time.Now()

ab.attemptersLock.Lock()
defer ab.attemptersLock.Unlock()

ab.incLocked(attID, now)
}

// remove stops any tracking and any blocking of an attempter.
func (ab *authRateLimiter) remove(attID string) {
ab.attemptersLock.Lock()
defer ab.attemptersLock.Unlock()

delete(ab.attempters, attID)
}
Loading

0 comments on commit c73a407

Please sign in to comment.