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/net v0.6.0 // indirect
|
||||||
golang.org/x/sys v0.5.0 // indirect
|
golang.org/x/sys v0.5.0 // indirect
|
||||||
golang.org/x/text v0.7.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
|
google.golang.org/protobuf v1.28.1 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // 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.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 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
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-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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
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
|
// Use of this source code is governed by an ISC
|
||||||
// license that can be found in the LICENSE file.
|
// 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 {
|
if password != s.cfg.AdminPass {
|
||||||
s.log.Warnf("Failed login attempt from %s", c.ClientIP())
|
s.log.Warnf("Failed login attempt from %s", c.ClientIP())
|
||||||
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
|
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
|
||||||
"WebApiCache": s.cache.getData(),
|
"WebApiCache": s.cache.getData(),
|
||||||
"WebApiCfg": s.cfg,
|
"WebApiCfg": s.cfg,
|
||||||
"IncorrectPassword": true,
|
"FailedLoginMsg": "Incorrect password",
|
||||||
})
|
})
|
||||||
return
|
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
|
// Use of this source code is governed by an ISC
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
@ -10,6 +10,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/decred/dcrd/blockchain/stake/v4"
|
"github.com/decred/dcrd/blockchain/stake/v4"
|
||||||
"github.com/decred/vspd/rpc"
|
"github.com/decred/vspd/rpc"
|
||||||
@ -18,8 +20,43 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/jrick/wsrpc/v2"
|
"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
|
// withSession middleware adds a gorilla session to the request context for
|
||||||
// downstream handlers to make use of. Sessions are used by admin pages to
|
// downstream handlers to make use of. Sessions are used by admin pages to
|
||||||
// maintain authentication status.
|
// maintain authentication status.
|
||||||
|
|||||||
@ -4,9 +4,9 @@
|
|||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
<form class="py-3" action="/admin" method="post">
|
<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>
|
<button class="btn btn-primary" type="submit">Login</button>
|
||||||
</form>
|
</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
|
// Use of this source code is governed by an ISC
|
||||||
// license that can be found in the LICENSE file.
|
// 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(
|
login := router.Group("/admin").Use(
|
||||||
s.withSession(cookieStore),
|
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(
|
admin := router.Group("/admin").Use(
|
||||||
s.withWalletClients(wallets), s.withSession(cookieStore), s.requireAdmin,
|
s.withWalletClients(wallets), s.withSession(cookieStore), s.requireAdmin,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user