Move the package into src/
Thirty-seven entries in the root, most of them .go files. The assets had to come along: //go:embed cannot reach outside its own directory, so templates/, static/ and migrations/ live beside the code that embeds them, and testdata/ beside the test that reads it. storage/ stays put — runtime data, not source. go build now needs -o. Without it the output would be named after the package directory and collide with src/ itself.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Login attempts are limited per email address, which is the thing under attack — and the only key
|
||||
// available without parsing X-Forwarded-For and maintaining a trusted-proxy list.
|
||||
//
|
||||
// ponytail: in memory, dies with the process. A restart clearing the counters is not an attack an
|
||||
// attacker can mount. A shared store is the upgrade if this ever runs as more than one process.
|
||||
const (
|
||||
loginMaxFailures = 10
|
||||
loginWindow = 15 * time.Minute
|
||||
loginLockout = 15 * time.Minute
|
||||
)
|
||||
|
||||
type attempts struct {
|
||||
count int
|
||||
first time.Time
|
||||
until time.Time // zero unless locked
|
||||
}
|
||||
|
||||
type limiter struct {
|
||||
mu sync.Mutex
|
||||
by map[string]*attempts
|
||||
}
|
||||
|
||||
// locked reports whether the key is currently refused. It does not count as an attempt.
|
||||
func (l *limiter) locked(key string) bool {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
a := l.by[key]
|
||||
return a != nil && time.Now().Before(a.until)
|
||||
}
|
||||
|
||||
func (l *limiter) fail(key string) {
|
||||
now := time.Now()
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
if l.by == nil {
|
||||
l.by = map[string]*attempts{}
|
||||
}
|
||||
l.sweep(now)
|
||||
|
||||
a := l.by[key]
|
||||
if a == nil || now.Sub(a.first) > loginWindow {
|
||||
l.by[key] = &attempts{count: 1, first: now}
|
||||
return
|
||||
}
|
||||
a.count++
|
||||
if a.count >= loginMaxFailures {
|
||||
a.until = now.Add(loginLockout)
|
||||
}
|
||||
}
|
||||
|
||||
// A correct password clears the record: the limit is on guessing, not on the account. Locking the
|
||||
// account itself would let anyone lock its owner out by trying.
|
||||
func (l *limiter) succeed(key string) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
delete(l.by, key)
|
||||
}
|
||||
|
||||
// Called under the lock, on failures only — there is nothing to grow the map otherwise.
|
||||
func (l *limiter) sweep(now time.Time) {
|
||||
for k, a := range l.by {
|
||||
if now.Sub(a.first) > loginWindow && now.After(a.until) {
|
||||
delete(l.by, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user