parent
bce5747164
commit
34ecfd283d
@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/cmd"
|
||||
"github.com/openimsdk/tools/system/program"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := cmd.NewCaptchaRpcCmd().Exec(); err != nil {
|
||||
program.ExitWithError(err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
rpc:
|
||||
registerIP: ""
|
||||
listenIP: 0.0.0.0
|
||||
autoSetPorts: false
|
||||
ports: [10520]
|
||||
|
||||
prometheus:
|
||||
enable: false
|
||||
ports: [12520]
|
||||
|
||||
verifyPadding: 8
|
||||
expireSeconds: 120
|
||||
@ -0,0 +1,49 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
pbcaptcha "github.com/openimsdk/protocol/captcha"
|
||||
"github.com/openimsdk/tools/a2r"
|
||||
"github.com/openimsdk/tools/apiresp"
|
||||
"github.com/openimsdk/tools/log"
|
||||
)
|
||||
|
||||
type CaptchaApi struct {
|
||||
Client pbcaptcha.CaptchaClient
|
||||
}
|
||||
|
||||
func NewCaptchaApi(client pbcaptcha.CaptchaClient) *CaptchaApi {
|
||||
return &CaptchaApi{Client: client}
|
||||
}
|
||||
|
||||
func (c *CaptchaApi) GenerateCaptcha(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbcaptcha.GenerateCaptchaReq](ctx)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha generate request parse failed", err)
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := c.Client.GenerateCaptcha(ctx, req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha generate rpc failed", err)
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
|
||||
func (c *CaptchaApi) VerifyCaptcha(ctx *gin.Context) {
|
||||
req, err := a2r.ParseRequestNotCheck[pbcaptcha.VerifyCaptchaReq](ctx)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha verify request parse failed", err)
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
resp, err := c.Client.VerifyCaptcha(ctx, req)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "x", req.GetX(), "y", req.GetY())
|
||||
apiresp.GinError(ctx, err)
|
||||
return
|
||||
}
|
||||
apiresp.GinSuccess(ctx, resp)
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/config"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/servererrs"
|
||||
pbcaptcha "github.com/openimsdk/protocol/captcha"
|
||||
"github.com/openimsdk/tools/db/mongoutil"
|
||||
"github.com/openimsdk/tools/discovery"
|
||||
"github.com/openimsdk/tools/log"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/wenlng/go-captcha/v2/base/option"
|
||||
"github.com/wenlng/go-captcha/v2/slide"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
RpcConfig config.Captcha
|
||||
MongodbConfig config.Mongo
|
||||
Share config.Share
|
||||
Discovery config.Discovery
|
||||
}
|
||||
|
||||
type server struct {
|
||||
pbcaptcha.UnimplementedCaptchaServer
|
||||
conf config.Captcha
|
||||
capt slide.Captcha
|
||||
collection *mongo.Collection
|
||||
}
|
||||
|
||||
type captchaDoc struct {
|
||||
CaptchaID string `bson:"captcha_id"`
|
||||
X int `bson:"x"`
|
||||
Y int `bson:"y"`
|
||||
ExpiredAt time.Time `bson:"expired_at"`
|
||||
CreateTime time.Time `bson:"create_time"`
|
||||
VerifyTime time.Time `bson:"verify_time,omitempty"`
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, cfg *Config, _ discovery.SvcDiscoveryRegistry, grpcServer *grpc.Server) error {
|
||||
mongoClient, err := mongoutil.NewMongoDB(ctx, cfg.MongodbConfig.Build())
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha connect mongodb failed", err)
|
||||
return err
|
||||
}
|
||||
collection := mongoClient.GetDB().Collection("captcha")
|
||||
_, err = collection.Indexes().CreateMany(ctx, []mongo.IndexModel{
|
||||
{
|
||||
Keys: bson.D{{Key: "captcha_id", Value: 1}},
|
||||
Options: options.Index().SetUnique(true),
|
||||
},
|
||||
{
|
||||
Keys: bson.D{{Key: "expired_at", Value: 1}},
|
||||
Options: options.Index().SetExpireAfterSeconds(0),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha create mongodb indexes failed", err)
|
||||
return err
|
||||
}
|
||||
|
||||
s := &server{
|
||||
conf: cfg.RpcConfig,
|
||||
capt: slide.NewBuilder().Make(),
|
||||
collection: collection,
|
||||
}
|
||||
if s.conf.ExpireSeconds <= 0 {
|
||||
s.conf.ExpireSeconds = 120
|
||||
}
|
||||
if s.conf.VerifyPadding <= 0 {
|
||||
s.conf.VerifyPadding = 8
|
||||
}
|
||||
pbcaptcha.RegisterCaptchaServer(grpcServer, s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptchaReq) (*pbcaptcha.GenerateCaptchaResp, error) {
|
||||
captData, err := s.capt.Generate()
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha generate failed", err)
|
||||
return nil, err
|
||||
}
|
||||
block := captData.GetData()
|
||||
masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(option.QualityNone)
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha encode master image failed", err)
|
||||
return nil, err
|
||||
}
|
||||
tileImage, err := captData.GetTileImage().ToBase64Data()
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha encode tile image failed", err)
|
||||
return nil, err
|
||||
}
|
||||
id := uuid.NewString()
|
||||
now := time.Now()
|
||||
expiredAt := now.Add(time.Duration(s.conf.ExpireSeconds) * time.Second)
|
||||
_, err = s.collection.InsertOne(ctx, captchaDoc{
|
||||
CaptchaID: id,
|
||||
X: block.X,
|
||||
Y: block.Y,
|
||||
ExpiredAt: expiredAt,
|
||||
CreateTime: now,
|
||||
})
|
||||
if err != nil {
|
||||
log.ZError(ctx, "captcha insert mongodb failed", err, "captchaID", id)
|
||||
return nil, err
|
||||
}
|
||||
return &pbcaptcha.GenerateCaptchaResp{
|
||||
CaptchaID: id,
|
||||
MasterImage: masterImage,
|
||||
TileImage: tileImage,
|
||||
ExpireAt: expiredAt.Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptchaReq) (*pbcaptcha.VerifyCaptchaResp, error) {
|
||||
now := time.Now()
|
||||
filter := bson.M{
|
||||
"captcha_id": req.CaptchaID,
|
||||
"verify_time": bson.M{"$exists": false},
|
||||
}
|
||||
update := bson.M{
|
||||
"$set": bson.M{
|
||||
"verify_time": now,
|
||||
},
|
||||
}
|
||||
var doc captchaDoc
|
||||
err := s.collection.FindOneAndUpdate(
|
||||
ctx,
|
||||
filter,
|
||||
update,
|
||||
options.FindOneAndUpdate().SetReturnDocument(options.Before),
|
||||
).Decode(&doc)
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
log.ZWarn(ctx, "captcha not found or already verified", err, "captchaID", req.CaptchaID)
|
||||
return nil, servererrs.ErrRecordNotFound.WrapMsg("captcha not found, expired, or already verified", "captchaID", req.CaptchaID)
|
||||
}
|
||||
log.ZError(ctx, "captcha verify query failed", err, "captchaID", req.CaptchaID)
|
||||
return nil, servererrs.ErrDatabase.WrapMsg("verify captcha query failed", "captchaID", req.CaptchaID)
|
||||
}
|
||||
if now.After(doc.ExpiredAt) {
|
||||
log.ZWarn(ctx, "captcha expired", nil, "captchaID", req.CaptchaID, "expiredAt", doc.ExpiredAt.Unix())
|
||||
return nil, servererrs.ErrFileUploadedExpired.WrapMsg("captcha expired", "captchaID", req.CaptchaID)
|
||||
}
|
||||
success := slide.Validate(int(req.X), int(req.Y), doc.X, doc.Y, s.conf.VerifyPadding)
|
||||
if !success {
|
||||
log.ZWarn(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y)
|
||||
}
|
||||
return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/openimsdk/open-im-server/v3/internal/rpc/captcha"
|
||||
"github.com/openimsdk/open-im-server/v3/pkg/common/startrpc"
|
||||
"github.com/openimsdk/open-im-server/v3/version"
|
||||
"github.com/openimsdk/tools/system/program"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type CaptchaRpcCmd struct {
|
||||
*RootCmd
|
||||
ctx context.Context
|
||||
configMap map[string]any
|
||||
captchaConfig *captcha.Config
|
||||
}
|
||||
|
||||
func NewCaptchaRpcCmd() *CaptchaRpcCmd {
|
||||
var captchaConfig captcha.Config
|
||||
ret := &CaptchaRpcCmd{captchaConfig: &captchaConfig}
|
||||
ret.configMap = map[string]any{
|
||||
OpenIMRPCCaptchaCfgFileName: &captchaConfig.RpcConfig,
|
||||
MongodbConfigFileName: &captchaConfig.MongodbConfig,
|
||||
ShareFileName: &captchaConfig.Share,
|
||||
DiscoveryConfigFilename: &captchaConfig.Discovery,
|
||||
}
|
||||
ret.RootCmd = NewRootCmd(program.GetProcessName(), WithConfigMap(ret.configMap))
|
||||
ret.ctx = context.WithValue(context.Background(), "version", version.Version)
|
||||
ret.Command.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
return ret.runE()
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *CaptchaRpcCmd) Exec() error {
|
||||
return c.Execute()
|
||||
}
|
||||
|
||||
func (c *CaptchaRpcCmd) runE() error {
|
||||
return startrpc.Start(c.ctx, &c.captchaConfig.Discovery, &c.captchaConfig.RpcConfig.Prometheus, c.captchaConfig.RpcConfig.RPC.ListenIP,
|
||||
c.captchaConfig.RpcConfig.RPC.RegisterIP, c.captchaConfig.RpcConfig.RPC.AutoSetPorts, c.captchaConfig.RpcConfig.RPC.Ports,
|
||||
c.Index(), c.captchaConfig.Share.RpcRegisterName.Captcha, &c.captchaConfig.Share, c.captchaConfig,
|
||||
nil,
|
||||
captcha.Start)
|
||||
}
|
||||
Loading…
Reference in new issue