feat: add fail2ban service

This commit is contained in:
Urko 2023-07-07 23:31:32 +02:00
parent 40d11f97f4
commit dff505037f
4 changed files with 97 additions and 8 deletions

View File

@ -9,6 +9,7 @@ import (
"syscall"
"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/kit/config"
)
@ -23,7 +24,7 @@ func main() {
ctx, cancel := context.WithCancel(signalContext(context.Background()))
defer cancel()
restServer := api.NewRestServer(prosody.NewProsody(cfg.Domain))
restServer := api.NewRestServer(prosody.NewProsody(cfg.Domain), fail2ban.NewFail2Ban())
go func() {
if err := restServer.Start(cfg.ApiPort, cfg.Views); err != nil {

View File

@ -3,18 +3,21 @@ package handler
import (
"fmt"
"gitea.urkob.com/urko/prosody-password/internal/services/fail2ban"
"gitea.urkob.com/urko/prosody-password/internal/services/prosody"
"github.com/gofiber/fiber/v2"
)
func NewProsodyHandler(prosodyService *prosody.Prosody) ProsodyHandler {
return ProsodyHandler{
prosodyService: prosodyService,
}
}
type ProsodyHandler struct {
prosodyService *prosody.Prosody
fail2banSrv *fail2ban.Fail2Ban
}
func NewProsodyHandler(prosodyService *prosody.Prosody, fail2banSrv *fail2ban.Fail2Ban) ProsodyHandler {
return ProsodyHandler{
prosodyService: prosodyService,
fail2banSrv: fail2banSrv,
}
}
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 {
for _, ip := range c.IPs() {
handler.fail2banSrv.FailedAttempt(ip)
}
return RenderError(c, fmt.Errorf("ChangePassword: %w", err), defaultErrMessage)
}

View File

@ -1,9 +1,11 @@
package api
import (
"fmt"
"log"
"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"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
@ -13,13 +15,16 @@ import (
type RestServer struct {
app *fiber.App
prosodyService *prosody.Prosody
fail2banSrv *fail2ban.Fail2Ban
}
func NewRestServer(
prosodyService *prosody.Prosody,
fail2banSrv *fail2ban.Fail2Ban,
) *RestServer {
return &RestServer{
prosodyService: prosodyService,
fail2banSrv: fail2banSrv,
}
}
@ -39,8 +44,14 @@ func (s *RestServer) Start(apiPort, views string) error {
s.loadViews()
prosodyHdl := handler.NewProsodyHandler(s.prosodyService)
prosodyHdl := handler.NewProsodyHandler(s.prosodyService, s.fail2banSrv)
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)
})

View File

@ -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
}