✨ Captcha Gateway
This commit is contained in:
33
pkg/internal/captcha/index.go
Normal file
33
pkg/internal/captcha/index.go
Normal file
@ -0,0 +1,33 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type TemplateData struct {
|
||||
ApiKey string `json:"api_key"`
|
||||
}
|
||||
|
||||
func GetTemplateData() TemplateData {
|
||||
return TemplateData{
|
||||
ApiKey: viper.GetString("captcha.api_key"),
|
||||
}
|
||||
}
|
||||
|
||||
type CaptchaAdapter interface {
|
||||
Validate(token, ip string) bool
|
||||
}
|
||||
|
||||
var adapters = map[string]CaptchaAdapter{
|
||||
"turnstile": &TurnstileAdapter{},
|
||||
}
|
||||
|
||||
func Validate(token, ip string) bool {
|
||||
provider := viper.GetString("captcha.provider")
|
||||
if adapter, ok := adapters[provider]; ok {
|
||||
return adapter.Validate(token, ip)
|
||||
}
|
||||
log.Error().Msg("Unable to handle captcha validate request due to unsupported provider.")
|
||||
return false
|
||||
}
|
46
pkg/internal/captcha/turnstile.go
Normal file
46
pkg/internal/captcha/turnstile.go
Normal file
@ -0,0 +1,46 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type TurnstileAdapter struct{}
|
||||
|
||||
type turnstileResponse struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorCodes []string `json:"error-codes"`
|
||||
}
|
||||
|
||||
func (a *TurnstileAdapter) Validate(token, ip string) bool {
|
||||
url := "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
||||
data := map[string]string{
|
||||
"secret": viper.GetString("captcha.api_secret"),
|
||||
"response": token,
|
||||
"remoteip": ip,
|
||||
}
|
||||
|
||||
jsonData, _ := json.Marshal(data)
|
||||
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error sending request to Turnstile...")
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result turnstileResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
log.Error().Err(err).Msg("Error decoding response from Turnstile...")
|
||||
return false
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
log.Warn().Strs("errors", result.ErrorCodes).Msg("An captcha validation request failed...")
|
||||
}
|
||||
|
||||
return result.Success
|
||||
}
|
14
pkg/internal/grpc/captcha.go
Normal file
14
pkg/internal/grpc/captcha.go
Normal file
@ -0,0 +1,14 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/internal/captcha"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
)
|
||||
|
||||
func (v *Server) CheckCaptcha(_ context.Context, req *proto.CheckCaptchaRequest) (*proto.CheckCaptchaResponse, error) {
|
||||
return &proto.CheckCaptchaResponse{
|
||||
IsValid: captcha.Validate(req.Token, req.RemoteIp),
|
||||
}, nil
|
||||
}
|
@ -19,6 +19,7 @@ type Server struct {
|
||||
proto.UnimplementedDatabaseServiceServer
|
||||
proto.UnimplementedStreamServiceServer
|
||||
proto.UnimplementedAllocatorServiceServer
|
||||
proto.UnimplementedCaptchaServiceServer
|
||||
health.UnimplementedHealthServer
|
||||
|
||||
srv *grpc.Server
|
||||
@ -33,6 +34,7 @@ func NewServer() *Server {
|
||||
proto.RegisterDatabaseServiceServer(server.srv, server)
|
||||
proto.RegisterStreamServiceServer(server.srv, server)
|
||||
proto.RegisterAllocatorServiceServer(server.srv, server)
|
||||
proto.RegisterCaptchaServiceServer(server.srv, server)
|
||||
health.RegisterHealthServer(server.srv, server)
|
||||
|
||||
reflection.Register(server.srv)
|
||||
|
25
pkg/internal/web/api/captcha.go
Normal file
25
pkg/internal/web/api/captcha.go
Normal file
@ -0,0 +1,25 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/internal/captcha"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/internal/web/exts"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func renderCaptcha(c *fiber.Ctx) error {
|
||||
return c.Render("captcha", captcha.GetTemplateData())
|
||||
}
|
||||
|
||||
func validateCaptcha(c *fiber.Ctx) error {
|
||||
var body struct {
|
||||
CaptchaToken string `json:"captcha_tk"`
|
||||
}
|
||||
if err := exts.BindAndValidate(c, &body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !captcha.Validate(body.CaptchaToken, c.IP()) {
|
||||
return c.SendStatus(fiber.StatusNotAcceptable)
|
||||
}
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
@ -12,6 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func MapControllers(app *fiber.App) {
|
||||
app.Get("/captcha", renderCaptcha)
|
||||
app.Post("/captcha", validateCaptcha)
|
||||
app.Get("/check-ip", getClientIP)
|
||||
app.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/idempotency"
|
||||
"github.com/gofiber/fiber/v2/middleware/limiter"
|
||||
"github.com/gofiber/template/html/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
@ -21,6 +22,8 @@ type WebApp struct {
|
||||
}
|
||||
|
||||
func NewServer() *WebApp {
|
||||
engine := html.New(viper.GetString("templates_dir"), ".tmpl")
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
EnableIPValidation: true,
|
||||
@ -32,6 +35,7 @@ func NewServer() *WebApp {
|
||||
BodyLimit: 512 * 1024 * 1024 * 1024, // 512 TiB
|
||||
ReadBufferSize: 5 * 1024 * 1024, // 5MB for large JWT
|
||||
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
|
||||
Views: engine,
|
||||
})
|
||||
|
||||
app.Use(fiberzerolog.New(fiberzerolog.Config{
|
||||
|
Reference in New Issue
Block a user