@@ -0,0 +1,34 @@
|
||||
package ratelimit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Redis struct{ client *redis.Client }
|
||||
|
||||
func NewRedis(client *redis.Client) *Redis { return &Redis{client: client} }
|
||||
|
||||
var loginCounter = redis.NewScript(`local n = redis.call('INCR', KEYS[1]); if n == 1 then redis.call('EXPIRE', KEYS[1], 900) end; return n`)
|
||||
|
||||
// Login permits at most 20 attempts per IP and 10 per login in a 15-minute window.
|
||||
// Redis errors fail closed. Neither credential nor IP is stored in plaintext keys.
|
||||
func (r *Redis) Login(ctx context.Context, ip, login string) (bool, error) {
|
||||
for _, item := range []struct {
|
||||
kind, value string
|
||||
limit int64
|
||||
}{{"ip", ip, 20}, {"name", login, 10}} {
|
||||
key := fmt.Sprintf("veyra:login:%s:%x", item.kind, sha256.Sum256([]byte(item.value)))
|
||||
n, err := loginCounter.Run(ctx, r.client, []string{key}).Int64()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if n > item.limit {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user