feat: add fail2ban service
This commit is contained in:
parent
40d11f97f4
commit
dff505037f
|
@ -9,6 +9,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"gitea.urkob.com/urko/prosody-password/internal/api"
|
"gitea.urkob.com/urko/prosody-password/internal/api"
|
||||||
|
"gitea.urkob.com/urko/prosody-password/internal/services/fail2ban"
|
||||||
"gitea.urkob.com/urko/prosody-password/internal/services/prosody"
|
"gitea.urkob.com/urko/prosody-password/internal/services/prosody"
|
||||||
"gitea.urkob.com/urko/prosody-password/kit/config"
|
"gitea.urkob.com/urko/prosody-password/kit/config"
|
||||||
)
|
)
|
||||||
|
@ -23,7 +24,7 @@ func main() {
|
||||||
ctx, cancel := context.WithCancel(signalContext(context.Background()))
|
ctx, cancel := context.WithCancel(signalContext(context.Background()))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
restServer := api.NewRestServer(prosody.NewProsody(cfg.Domain))
|
restServer := api.NewRestServer(prosody.NewProsody(cfg.Domain), fail2ban.NewFail2Ban())
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if err := restServer.Start(cfg.ApiPort, cfg.Views); err != nil {
|
if err := restServer.Start(cfg.ApiPort, cfg.Views); err != nil {
|
||||||
|
|
|
@ -3,18 +3,21 @@ package handler
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"gitea.urkob.com/urko/prosody-password/internal/services/fail2ban"
|
||||||
"gitea.urkob.com/urko/prosody-password/internal/services/prosody"
|
"gitea.urkob.com/urko/prosody-password/internal/services/prosody"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewProsodyHandler(prosodyService *prosody.Prosody) ProsodyHandler {
|
|
||||||
return ProsodyHandler{
|
|
||||||
prosodyService: prosodyService,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProsodyHandler struct {
|
type ProsodyHandler struct {
|
||||||
prosodyService *prosody.Prosody
|
prosodyService *prosody.Prosody
|
||||||
|
fail2banSrv *fail2ban.Fail2Ban
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProsodyHandler(prosodyService *prosody.Prosody, fail2banSrv *fail2ban.Fail2Ban) ProsodyHandler {
|
||||||
|
return ProsodyHandler{
|
||||||
|
prosodyService: prosodyService,
|
||||||
|
fail2banSrv: fail2banSrv,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type changePasswordReq struct {
|
type changePasswordReq struct {
|
||||||
|
@ -30,6 +33,9 @@ func (handler ProsodyHandler) Post(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handler.prosodyService.ChangePassword(req.User, req.CurrentPassword, req.NewPassword); err != nil {
|
if err := handler.prosodyService.ChangePassword(req.User, req.CurrentPassword, req.NewPassword); err != nil {
|
||||||
|
for _, ip := range c.IPs() {
|
||||||
|
handler.fail2banSrv.FailedAttempt(ip)
|
||||||
|
}
|
||||||
return RenderError(c, fmt.Errorf("ChangePassword: %w", err), defaultErrMessage)
|
return RenderError(c, fmt.Errorf("ChangePassword: %w", err), defaultErrMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"gitea.urkob.com/urko/prosody-password/internal/api/handler"
|
"gitea.urkob.com/urko/prosody-password/internal/api/handler"
|
||||||
|
"gitea.urkob.com/urko/prosody-password/internal/services/fail2ban"
|
||||||
"gitea.urkob.com/urko/prosody-password/internal/services/prosody"
|
"gitea.urkob.com/urko/prosody-password/internal/services/prosody"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
@ -13,13 +15,16 @@ import (
|
||||||
type RestServer struct {
|
type RestServer struct {
|
||||||
app *fiber.App
|
app *fiber.App
|
||||||
prosodyService *prosody.Prosody
|
prosodyService *prosody.Prosody
|
||||||
|
fail2banSrv *fail2ban.Fail2Ban
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRestServer(
|
func NewRestServer(
|
||||||
prosodyService *prosody.Prosody,
|
prosodyService *prosody.Prosody,
|
||||||
|
fail2banSrv *fail2ban.Fail2Ban,
|
||||||
) *RestServer {
|
) *RestServer {
|
||||||
return &RestServer{
|
return &RestServer{
|
||||||
prosodyService: prosodyService,
|
prosodyService: prosodyService,
|
||||||
|
fail2banSrv: fail2banSrv,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,8 +44,14 @@ func (s *RestServer) Start(apiPort, views string) error {
|
||||||
|
|
||||||
s.loadViews()
|
s.loadViews()
|
||||||
|
|
||||||
prosodyHdl := handler.NewProsodyHandler(s.prosodyService)
|
prosodyHdl := handler.NewProsodyHandler(s.prosodyService, s.fail2banSrv)
|
||||||
s.app.Post("/changePassword", func(c *fiber.Ctx) error {
|
s.app.Post("/changePassword", func(c *fiber.Ctx) error {
|
||||||
|
for _, ip := range c.IPs() {
|
||||||
|
if !s.fail2banSrv.CanChangePassword(ip) {
|
||||||
|
return handler.RenderError(c, fmt.Errorf("id is empty"), "Too many tries, blocked for 1h")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return prosodyHdl.Post(c)
|
return prosodyHdl.Post(c)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package fail2ban
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
FailedAttempts int
|
||||||
|
BlockedUntil time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fail2Ban struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
clients map[string]*Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFail2Ban() *Fail2Ban {
|
||||||
|
return &Fail2Ban{
|
||||||
|
clients: make(map[string]*Client),
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fail2Ban) FailedAttempt(ip string) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
|
||||||
|
client, ok := f.clients[ip]
|
||||||
|
if !ok {
|
||||||
|
client = &Client{}
|
||||||
|
f.clients[ip] = client
|
||||||
|
}
|
||||||
|
|
||||||
|
client.FailedAttempts++
|
||||||
|
if client.FailedAttempts >= 3 {
|
||||||
|
client.BlockedUntil = time.Now().Add(10 * time.Minute)
|
||||||
|
client.FailedAttempts = 0
|
||||||
|
|
||||||
|
// Automatically unblock the client after 1 hour
|
||||||
|
time.AfterFunc(1*time.Hour, func() {
|
||||||
|
f.unblockClient(ip)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fail2Ban) unblockClient(ip string) {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
|
||||||
|
client, ok := f.clients[ip]
|
||||||
|
if ok {
|
||||||
|
client.BlockedUntil = time.Time{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Fail2Ban) CanChangePassword(ip string) bool {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
|
||||||
|
client, ok := f.clients[ip]
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().Before(client.BlockedUntil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
Loading…
Reference in New Issue