diff --git a/go.mod b/go.mod index b008f12..594cab7 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 4db0ac1..e1ed21b 100644 --- a/go.sum +++ b/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= diff --git a/pkg/internal/captcha/index.go b/pkg/internal/captcha/index.go new file mode 100644 index 0000000..462af28 --- /dev/null +++ b/pkg/internal/captcha/index.go @@ -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 +} diff --git a/pkg/internal/captcha/turnstile.go b/pkg/internal/captcha/turnstile.go new file mode 100644 index 0000000..257f508 --- /dev/null +++ b/pkg/internal/captcha/turnstile.go @@ -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 +} diff --git a/pkg/internal/grpc/captcha.go b/pkg/internal/grpc/captcha.go new file mode 100644 index 0000000..f85086d --- /dev/null +++ b/pkg/internal/grpc/captcha.go @@ -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 +} diff --git a/pkg/internal/grpc/server.go b/pkg/internal/grpc/server.go index 58448a1..99bba6f 100644 --- a/pkg/internal/grpc/server.go +++ b/pkg/internal/grpc/server.go @@ -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) diff --git a/pkg/internal/web/api/captcha.go b/pkg/internal/web/api/captcha.go new file mode 100644 index 0000000..e4928b0 --- /dev/null +++ b/pkg/internal/web/api/captcha.go @@ -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) +} diff --git a/pkg/internal/web/api/index.go b/pkg/internal/web/api/index.go index ba727e1..a39b6e9 100644 --- a/pkg/internal/web/api/index.go +++ b/pkg/internal/web/api/index.go @@ -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{ diff --git a/pkg/internal/web/server.go b/pkg/internal/web/server.go index fc1481a..50c402a 100644 --- a/pkg/internal/web/server.go +++ b/pkg/internal/web/server.go @@ -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{ diff --git a/pkg/proto/captcha.pb.go b/pkg/proto/captcha.pb.go new file mode 100644 index 0000000..7a4dcb3 --- /dev/null +++ b/pkg/proto/captcha.pb.go @@ -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 +} diff --git a/pkg/proto/captcha.proto b/pkg/proto/captcha.proto new file mode 100644 index 0000000..071d613 --- /dev/null +++ b/pkg/proto/captcha.proto @@ -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; +} diff --git a/pkg/proto/captcha_grpc.pb.go b/pkg/proto/captcha_grpc.pb.go new file mode 100644 index 0000000..f6f0c82 --- /dev/null +++ b/pkg/proto/captcha_grpc.pb.go @@ -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", +} diff --git a/settings.toml b/settings.toml index ba9ef4e..d47eff3 100644 --- a/settings.toml +++ b/settings.toml @@ -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 diff --git a/templates/captcha.tmpl b/templates/captcha.tmpl index dc7b940..3955bb6 100644 --- a/templates/captcha.tmpl +++ b/templates/captcha.tmpl @@ -3,7 +3,7 @@
-