✨ Captcha Gateway
This commit is contained in:
		
							
								
								
									
										5
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
									
									
									
									
								
							@@ -9,7 +9,9 @@ require (
 | 
			
		||||
	github.com/gofiber/contrib/fiberzerolog v1.0.2
 | 
			
		||||
	github.com/gofiber/contrib/websocket v1.3.2
 | 
			
		||||
	github.com/gofiber/fiber/v2 v2.52.6
 | 
			
		||||
	github.com/gofiber/template/html/v2 v2.1.3
 | 
			
		||||
	github.com/golang-jwt/jwt/v5 v5.2.1
 | 
			
		||||
	github.com/google/uuid v1.6.0
 | 
			
		||||
	github.com/json-iterator/go v1.1.12
 | 
			
		||||
	github.com/lib/pq v1.10.9
 | 
			
		||||
	github.com/nats-io/nats.go v1.37.0
 | 
			
		||||
@@ -39,9 +41,10 @@ require (
 | 
			
		||||
	github.com/go-playground/locales v0.14.1 // indirect
 | 
			
		||||
	github.com/go-playground/universal-translator v0.18.1 // indirect
 | 
			
		||||
	github.com/go-sql-driver/mysql v1.8.1 // indirect
 | 
			
		||||
	github.com/gofiber/template v1.8.3 // indirect
 | 
			
		||||
	github.com/gofiber/utils v1.1.0 // indirect
 | 
			
		||||
	github.com/gogo/protobuf v1.3.2 // indirect
 | 
			
		||||
	github.com/golang/protobuf v1.5.4 // indirect
 | 
			
		||||
	github.com/google/uuid v1.6.0 // indirect
 | 
			
		||||
	github.com/hashicorp/hcl v1.0.0 // indirect
 | 
			
		||||
	github.com/jackc/pgpassfile v1.0.0 // indirect
 | 
			
		||||
	github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								go.sum
									
									
									
									
									
								
							@@ -42,6 +42,12 @@ github.com/gofiber/contrib/websocket v1.3.2 h1:AUq5PYeKwK50s0nQrnluuINYeep1c4nRC
 | 
			
		||||
github.com/gofiber/contrib/websocket v1.3.2/go.mod h1:07u6QGMsvX+sx7iGNCl5xhzuUVArWwLQ3tBIH24i+S8=
 | 
			
		||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
 | 
			
		||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
 | 
			
		||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
 | 
			
		||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
 | 
			
		||||
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
 | 
			
		||||
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
 | 
			
		||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
 | 
			
		||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 | 
			
		||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 | 
			
		||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 | 
			
		||||
@@ -154,8 +160,8 @@ github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 | 
			
		||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
			
		||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 | 
			
		||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 | 
			
		||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 | 
			
		||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
 | 
			
		||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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{
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										193
									
								
								pkg/proto/captcha.pb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								pkg/proto/captcha.pb.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,193 @@
 | 
			
		||||
// Code generated by protoc-gen-go. DO NOT EDIT.
 | 
			
		||||
// versions:
 | 
			
		||||
// 	protoc-gen-go v1.35.1
 | 
			
		||||
// 	protoc        v5.28.3
 | 
			
		||||
// source: captcha.proto
 | 
			
		||||
 | 
			
		||||
package proto
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
 | 
			
		||||
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
 | 
			
		||||
	reflect "reflect"
 | 
			
		||||
	sync "sync"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// Verify that this generated code is sufficiently up-to-date.
 | 
			
		||||
	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
 | 
			
		||||
	// Verify that runtime/protoimpl is sufficiently up-to-date.
 | 
			
		||||
	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type CheckCaptchaRequest struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	Token    string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
 | 
			
		||||
	RemoteIp string `protobuf:"bytes,2,opt,name=remote_ip,json=remoteIp,proto3" json:"remote_ip,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaRequest) Reset() {
 | 
			
		||||
	*x = CheckCaptchaRequest{}
 | 
			
		||||
	mi := &file_captcha_proto_msgTypes[0]
 | 
			
		||||
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
	ms.StoreMessageInfo(mi)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaRequest) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*CheckCaptchaRequest) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaRequest) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_captcha_proto_msgTypes[0]
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use CheckCaptchaRequest.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*CheckCaptchaRequest) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_captcha_proto_rawDescGZIP(), []int{0}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaRequest) GetToken() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.Token
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaRequest) GetRemoteIp() string {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.RemoteIp
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type CheckCaptchaResponse struct {
 | 
			
		||||
	state         protoimpl.MessageState
 | 
			
		||||
	sizeCache     protoimpl.SizeCache
 | 
			
		||||
	unknownFields protoimpl.UnknownFields
 | 
			
		||||
 | 
			
		||||
	IsValid bool `protobuf:"varint,1,opt,name=is_valid,json=isValid,proto3" json:"is_valid,omitempty"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaResponse) Reset() {
 | 
			
		||||
	*x = CheckCaptchaResponse{}
 | 
			
		||||
	mi := &file_captcha_proto_msgTypes[1]
 | 
			
		||||
	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
	ms.StoreMessageInfo(mi)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaResponse) String() string {
 | 
			
		||||
	return protoimpl.X.MessageStringOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (*CheckCaptchaResponse) ProtoMessage() {}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaResponse) ProtoReflect() protoreflect.Message {
 | 
			
		||||
	mi := &file_captcha_proto_msgTypes[1]
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 | 
			
		||||
		if ms.LoadMessageInfo() == nil {
 | 
			
		||||
			ms.StoreMessageInfo(mi)
 | 
			
		||||
		}
 | 
			
		||||
		return ms
 | 
			
		||||
	}
 | 
			
		||||
	return mi.MessageOf(x)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Deprecated: Use CheckCaptchaResponse.ProtoReflect.Descriptor instead.
 | 
			
		||||
func (*CheckCaptchaResponse) Descriptor() ([]byte, []int) {
 | 
			
		||||
	return file_captcha_proto_rawDescGZIP(), []int{1}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (x *CheckCaptchaResponse) GetIsValid() bool {
 | 
			
		||||
	if x != nil {
 | 
			
		||||
		return x.IsValid
 | 
			
		||||
	}
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var File_captcha_proto protoreflect.FileDescriptor
 | 
			
		||||
 | 
			
		||||
var file_captcha_proto_rawDesc = []byte{
 | 
			
		||||
	0x0a, 0x0d, 0x63, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
 | 
			
		||||
	0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x48, 0x0a, 0x13, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43,
 | 
			
		||||
	0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a,
 | 
			
		||||
	0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f,
 | 
			
		||||
	0x6b, 0x65, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x69, 0x70,
 | 
			
		||||
	0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x49, 0x70,
 | 
			
		||||
	0x22, 0x31, 0x0a, 0x14, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61,
 | 
			
		||||
	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x73, 0x5f, 0x76,
 | 
			
		||||
	0x61, 0x6c, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73, 0x56, 0x61,
 | 
			
		||||
	0x6c, 0x69, 0x64, 0x32, 0x5b, 0x0a, 0x0e, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x53, 0x65,
 | 
			
		||||
	0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x61,
 | 
			
		||||
	0x70, 0x74, 0x63, 0x68, 0x61, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68,
 | 
			
		||||
	0x65, 0x63, 0x6b, 0x43, 0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
 | 
			
		||||
	0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43,
 | 
			
		||||
	0x61, 0x70, 0x74, 0x63, 0x68, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
 | 
			
		||||
	0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
 | 
			
		||||
	0x74, 0x6f, 0x33,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	file_captcha_proto_rawDescOnce sync.Once
 | 
			
		||||
	file_captcha_proto_rawDescData = file_captcha_proto_rawDesc
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func file_captcha_proto_rawDescGZIP() []byte {
 | 
			
		||||
	file_captcha_proto_rawDescOnce.Do(func() {
 | 
			
		||||
		file_captcha_proto_rawDescData = protoimpl.X.CompressGZIP(file_captcha_proto_rawDescData)
 | 
			
		||||
	})
 | 
			
		||||
	return file_captcha_proto_rawDescData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var file_captcha_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
 | 
			
		||||
var file_captcha_proto_goTypes = []any{
 | 
			
		||||
	(*CheckCaptchaRequest)(nil),  // 0: proto.CheckCaptchaRequest
 | 
			
		||||
	(*CheckCaptchaResponse)(nil), // 1: proto.CheckCaptchaResponse
 | 
			
		||||
}
 | 
			
		||||
var file_captcha_proto_depIdxs = []int32{
 | 
			
		||||
	0, // 0: proto.CaptchaService.CheckCaptcha:input_type -> proto.CheckCaptchaRequest
 | 
			
		||||
	1, // 1: proto.CaptchaService.CheckCaptcha:output_type -> proto.CheckCaptchaResponse
 | 
			
		||||
	1, // [1:2] is the sub-list for method output_type
 | 
			
		||||
	0, // [0:1] is the sub-list for method input_type
 | 
			
		||||
	0, // [0:0] is the sub-list for extension type_name
 | 
			
		||||
	0, // [0:0] is the sub-list for extension extendee
 | 
			
		||||
	0, // [0:0] is the sub-list for field type_name
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() { file_captcha_proto_init() }
 | 
			
		||||
func file_captcha_proto_init() {
 | 
			
		||||
	if File_captcha_proto != nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	type x struct{}
 | 
			
		||||
	out := protoimpl.TypeBuilder{
 | 
			
		||||
		File: protoimpl.DescBuilder{
 | 
			
		||||
			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 | 
			
		||||
			RawDescriptor: file_captcha_proto_rawDesc,
 | 
			
		||||
			NumEnums:      0,
 | 
			
		||||
			NumMessages:   2,
 | 
			
		||||
			NumExtensions: 0,
 | 
			
		||||
			NumServices:   1,
 | 
			
		||||
		},
 | 
			
		||||
		GoTypes:           file_captcha_proto_goTypes,
 | 
			
		||||
		DependencyIndexes: file_captcha_proto_depIdxs,
 | 
			
		||||
		MessageInfos:      file_captcha_proto_msgTypes,
 | 
			
		||||
	}.Build()
 | 
			
		||||
	File_captcha_proto = out.File
 | 
			
		||||
	file_captcha_proto_rawDesc = nil
 | 
			
		||||
	file_captcha_proto_goTypes = nil
 | 
			
		||||
	file_captcha_proto_depIdxs = nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										18
									
								
								pkg/proto/captcha.proto
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								pkg/proto/captcha.proto
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
syntax = "proto3";
 | 
			
		||||
 | 
			
		||||
option go_package = ".;proto";
 | 
			
		||||
 | 
			
		||||
package proto;
 | 
			
		||||
 | 
			
		||||
service CaptchaService {
 | 
			
		||||
  rpc CheckCaptcha(CheckCaptchaRequest) returns (CheckCaptchaResponse) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message CheckCaptchaRequest {
 | 
			
		||||
  string token = 1;
 | 
			
		||||
  string remote_ip = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message CheckCaptchaResponse {
 | 
			
		||||
  bool is_valid = 1;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										121
									
								
								pkg/proto/captcha_grpc.pb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								pkg/proto/captcha_grpc.pb.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
 | 
			
		||||
// versions:
 | 
			
		||||
// - protoc-gen-go-grpc v1.5.1
 | 
			
		||||
// - protoc             v5.28.3
 | 
			
		||||
// source: captcha.proto
 | 
			
		||||
 | 
			
		||||
package proto
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	context "context"
 | 
			
		||||
	grpc "google.golang.org/grpc"
 | 
			
		||||
	codes "google.golang.org/grpc/codes"
 | 
			
		||||
	status "google.golang.org/grpc/status"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// This is a compile-time assertion to ensure that this generated file
 | 
			
		||||
// is compatible with the grpc package it is being compiled against.
 | 
			
		||||
// Requires gRPC-Go v1.64.0 or later.
 | 
			
		||||
const _ = grpc.SupportPackageIsVersion9
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	CaptchaService_CheckCaptcha_FullMethodName = "/proto.CaptchaService/CheckCaptcha"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CaptchaServiceClient is the client API for CaptchaService service.
 | 
			
		||||
//
 | 
			
		||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
 | 
			
		||||
type CaptchaServiceClient interface {
 | 
			
		||||
	CheckCaptcha(ctx context.Context, in *CheckCaptchaRequest, opts ...grpc.CallOption) (*CheckCaptchaResponse, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type captchaServiceClient struct {
 | 
			
		||||
	cc grpc.ClientConnInterface
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewCaptchaServiceClient(cc grpc.ClientConnInterface) CaptchaServiceClient {
 | 
			
		||||
	return &captchaServiceClient{cc}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *captchaServiceClient) CheckCaptcha(ctx context.Context, in *CheckCaptchaRequest, opts ...grpc.CallOption) (*CheckCaptchaResponse, error) {
 | 
			
		||||
	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
 | 
			
		||||
	out := new(CheckCaptchaResponse)
 | 
			
		||||
	err := c.cc.Invoke(ctx, CaptchaService_CheckCaptcha_FullMethodName, in, out, cOpts...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CaptchaServiceServer is the server API for CaptchaService service.
 | 
			
		||||
// All implementations must embed UnimplementedCaptchaServiceServer
 | 
			
		||||
// for forward compatibility.
 | 
			
		||||
type CaptchaServiceServer interface {
 | 
			
		||||
	CheckCaptcha(context.Context, *CheckCaptchaRequest) (*CheckCaptchaResponse, error)
 | 
			
		||||
	mustEmbedUnimplementedCaptchaServiceServer()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UnimplementedCaptchaServiceServer must be embedded to have
 | 
			
		||||
// forward compatible implementations.
 | 
			
		||||
//
 | 
			
		||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
 | 
			
		||||
// pointer dereference when methods are called.
 | 
			
		||||
type UnimplementedCaptchaServiceServer struct{}
 | 
			
		||||
 | 
			
		||||
func (UnimplementedCaptchaServiceServer) CheckCaptcha(context.Context, *CheckCaptchaRequest) (*CheckCaptchaResponse, error) {
 | 
			
		||||
	return nil, status.Errorf(codes.Unimplemented, "method CheckCaptcha not implemented")
 | 
			
		||||
}
 | 
			
		||||
func (UnimplementedCaptchaServiceServer) mustEmbedUnimplementedCaptchaServiceServer() {}
 | 
			
		||||
func (UnimplementedCaptchaServiceServer) testEmbeddedByValue()                        {}
 | 
			
		||||
 | 
			
		||||
// UnsafeCaptchaServiceServer may be embedded to opt out of forward compatibility for this service.
 | 
			
		||||
// Use of this interface is not recommended, as added methods to CaptchaServiceServer will
 | 
			
		||||
// result in compilation errors.
 | 
			
		||||
type UnsafeCaptchaServiceServer interface {
 | 
			
		||||
	mustEmbedUnimplementedCaptchaServiceServer()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RegisterCaptchaServiceServer(s grpc.ServiceRegistrar, srv CaptchaServiceServer) {
 | 
			
		||||
	// If the following call pancis, it indicates UnimplementedCaptchaServiceServer was
 | 
			
		||||
	// embedded by pointer and is nil.  This will cause panics if an
 | 
			
		||||
	// unimplemented method is ever invoked, so we test this at initialization
 | 
			
		||||
	// time to prevent it from happening at runtime later due to I/O.
 | 
			
		||||
	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
 | 
			
		||||
		t.testEmbeddedByValue()
 | 
			
		||||
	}
 | 
			
		||||
	s.RegisterService(&CaptchaService_ServiceDesc, srv)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func _CaptchaService_CheckCaptcha_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
 | 
			
		||||
	in := new(CheckCaptchaRequest)
 | 
			
		||||
	if err := dec(in); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if interceptor == nil {
 | 
			
		||||
		return srv.(CaptchaServiceServer).CheckCaptcha(ctx, in)
 | 
			
		||||
	}
 | 
			
		||||
	info := &grpc.UnaryServerInfo{
 | 
			
		||||
		Server:     srv,
 | 
			
		||||
		FullMethod: CaptchaService_CheckCaptcha_FullMethodName,
 | 
			
		||||
	}
 | 
			
		||||
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
 | 
			
		||||
		return srv.(CaptchaServiceServer).CheckCaptcha(ctx, req.(*CheckCaptchaRequest))
 | 
			
		||||
	}
 | 
			
		||||
	return interceptor(ctx, in, info, handler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CaptchaService_ServiceDesc is the grpc.ServiceDesc for CaptchaService service.
 | 
			
		||||
// It's only intended for direct use with grpc.RegisterService,
 | 
			
		||||
// and not to be introspected or modified (even as a copy)
 | 
			
		||||
var CaptchaService_ServiceDesc = grpc.ServiceDesc{
 | 
			
		||||
	ServiceName: "proto.CaptchaService",
 | 
			
		||||
	HandlerType: (*CaptchaServiceServer)(nil),
 | 
			
		||||
	Methods: []grpc.MethodDesc{
 | 
			
		||||
		{
 | 
			
		||||
			MethodName: "CheckCaptcha",
 | 
			
		||||
			Handler:    _CaptchaService_CheckCaptcha_Handler,
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	Streams:  []grpc.StreamDesc{},
 | 
			
		||||
	Metadata: "captcha.proto",
 | 
			
		||||
}
 | 
			
		||||
@@ -2,6 +2,8 @@ bind = "0.0.0.0:8001"
 | 
			
		||||
grpc_bind = "0.0.0.0:7001"
 | 
			
		||||
domain = "localhost"
 | 
			
		||||
 | 
			
		||||
templates_dir = "./templates"
 | 
			
		||||
 | 
			
		||||
rate_limit = 120
 | 
			
		||||
rate_limit_advance = 60
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Cloudflare Turnstile</title>
 | 
			
		||||
    <title>Solar Network Captcha</title>
 | 
			
		||||
    <link
 | 
			
		||||
      href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap"
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
@@ -54,7 +54,7 @@
 | 
			
		||||
        <h1>reCaptcha</h1>
 | 
			
		||||
        <div
 | 
			
		||||
          class="cf-turnstile"
 | 
			
		||||
          data-sitekey="{{ .APIKey }}"
 | 
			
		||||
          data-sitekey="{{ .ApiKey }}"
 | 
			
		||||
          data-callback="onSuccess"
 | 
			
		||||
        ></div>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user