webapi: Rate limit for admin login requests.
Only allow 3 requests per second. Return "429 Too Many Requests" when this rate is exceeded.
This commit is contained in:
parent
d00ba70f94
commit
62803147f0
1
go.mod
1
go.mod
@ -59,6 +59,7 @@ require (
|
||||
golang.org/x/net v0.6.0 // indirect
|
||||
golang.org/x/sys v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@ -242,6 +242,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020-2022 The Decred developers
|
||||
// Copyright (c) 2020-2023 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@ -200,9 +200,9 @@ func (s *Server) adminLogin(c *gin.Context) {
|
||||
if password != s.cfg.AdminPass {
|
||||
s.log.Warnf("Failed login attempt from %s", c.ClientIP())
|
||||
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
|
||||
"WebApiCache": s.cache.getData(),
|
||||
"WebApiCfg": s.cfg,
|
||||
"IncorrectPassword": true,
|
||||
"WebApiCache": s.cache.getData(),
|
||||
"WebApiCfg": s.cfg,
|
||||
"FailedLoginMsg": "Incorrect password",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020-2022 The Decred developers
|
||||
// Copyright (c) 2020-2023 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@ -10,6 +10,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/decred/dcrd/blockchain/stake/v4"
|
||||
"github.com/decred/vspd/rpc"
|
||||
@ -18,8 +20,43 @@ import (
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/jrick/wsrpc/v2"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// rateLimit middleware limits how many requests each client IP can submit per
|
||||
// second. If the limit is exceeded the limitExceeded handler will be executed
|
||||
// and the context will be aborted.
|
||||
func rateLimit(limit rate.Limit, limitExceeded gin.HandlerFunc) gin.HandlerFunc {
|
||||
var limitersMtx sync.Mutex
|
||||
limiters := make(map[string]*rate.Limiter)
|
||||
|
||||
// Clear limiters every hour so they arent accumulating infinitely.
|
||||
go func() {
|
||||
for {
|
||||
<-time.After(time.Hour)
|
||||
limitersMtx.Lock()
|
||||
limiters = make(map[string]*rate.Limiter)
|
||||
limitersMtx.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
return func(c *gin.Context) {
|
||||
limitersMtx.Lock()
|
||||
defer limitersMtx.Unlock()
|
||||
|
||||
// Create a limiter for this IP if one does not exist.
|
||||
if _, ok := limiters[c.ClientIP()]; !ok {
|
||||
limiters[c.ClientIP()] = rate.NewLimiter(limit, 1)
|
||||
}
|
||||
|
||||
// Check if this IP exceeds limit.
|
||||
if !limiters[c.ClientIP()].Allow() {
|
||||
limitExceeded(c)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// withSession middleware adds a gorilla session to the request context for
|
||||
// downstream handlers to make use of. Sessions are used by admin pages to
|
||||
// maintain authentication status.
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
<h1>Login</h1>
|
||||
<form class="py-3" action="/admin" method="post">
|
||||
|
||||
<input type="password" name="password" required placeholder="Enter password">
|
||||
<input type="password" name="password" autofocus required placeholder="Enter password">
|
||||
|
||||
<p class="my-1 orange" style="visibility:{{ if .IncorrectPassword }}visible{{ else }}hidden{{ end }};">Incorrect password</p>
|
||||
<p class="my-1 orange" style="visibility:{{ if .FailedLoginMsg }}visible{{ else }}hidden{{ end }};">{{ .FailedLoginMsg }}</p>
|
||||
|
||||
<button class="btn btn-primary" type="submit">Login</button>
|
||||
</form>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020-2022 The Decred developers
|
||||
// Copyright (c) 2020-2023 The Decred developers
|
||||
// Use of this source code is governed by an ISC
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
@ -249,7 +249,17 @@ func (s *Server) router(cookieSecret []byte, dcrd rpc.DcrdConnect, wallets rpc.W
|
||||
login := router.Group("/admin").Use(
|
||||
s.withSession(cookieStore),
|
||||
)
|
||||
login.POST("", s.adminLogin)
|
||||
|
||||
// Limit login attempts to 3 per second.
|
||||
loginRateLmiter := rateLimit(3, func(c *gin.Context) {
|
||||
s.log.Warnf("Login rate limit exceeded by %s", c.ClientIP())
|
||||
c.HTML(http.StatusTooManyRequests, "login.html", gin.H{
|
||||
"WebApiCache": s.cache.getData(),
|
||||
"WebApiCfg": s.cfg,
|
||||
"FailedLoginMsg": "Rate limit exceeded",
|
||||
})
|
||||
})
|
||||
login.POST("", loginRateLmiter, s.adminLogin)
|
||||
|
||||
admin := router.Group("/admin").Use(
|
||||
s.withWalletClients(wallets), s.withSession(cookieStore), s.requireAdmin,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user