diff --git a/internal/api/captcha.go b/internal/api/captcha.go index 9cedb3d3e..9a0c6f3ca 100644 --- a/internal/api/captcha.go +++ b/internal/api/captcha.go @@ -41,7 +41,7 @@ func (c *CaptchaApi) VerifyCaptcha(ctx *gin.Context) { } 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()) + log.ZError(ctx, "captcha verify rpc failed", err, "captchaID", req.GetCaptchaID(), "clickCount", len(req.GetClickPoints())) apiresp.GinError(ctx, err) return } diff --git a/internal/api/router.go b/internal/api/router.go index 2e3cb02ad..328b83db2 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -9,10 +9,10 @@ import ( pbAuth "github.com/openimsdk/protocol/auth" pbcaptcha "github.com/openimsdk/protocol/captcha" "github.com/openimsdk/protocol/conversation" + pbcrypto "github.com/openimsdk/protocol/crypto" "github.com/openimsdk/protocol/group" "github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/relation" - pbcrypto "github.com/openimsdk/protocol/crypto" "github.com/openimsdk/protocol/rtc" "github.com/openimsdk/protocol/third" "github.com/openimsdk/protocol/user" @@ -334,8 +334,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co phoneGroup := r.Group("/phone") phoneGroup.POST("/get_sn_info", phoneSN.GetSNInfo) phoneGroup.POST("/set_sn_info", phoneSN.SetSNInfo) - } - { + } + { rc := NewRtcApi(rtc.NewRtcServiceClient(rtcConn)) rtcGroup := r.Group("/rtc") rtcGroup.POST("/signal_message_assemble", rc.SignalMessageAssemble) diff --git a/internal/msggateway/message_handler.go b/internal/msggateway/message_handler.go index 2706540a0..cb1b2ea27 100644 --- a/internal/msggateway/message_handler.go +++ b/internal/msggateway/message_handler.go @@ -188,6 +188,9 @@ func (g *GrpcHandler) SendSignalMessage(ctx context.Context, data *Req) ([]byte, } assembleReq.SignalReq = &signalReq } + + log.ZDebug(ctx, "SendSignalMessage", "assembleReq", assembleReq) + resp, err := g.rtcClient.RtcServiceClient.SignalMessageAssemble(ctx, &assembleReq) if err != nil { log.ZError(ctx, "SendSignalMessage", err, "r", err.Error()) diff --git a/internal/rpc/captcha/captcha.go b/internal/rpc/captcha/captcha.go index 206b376e5..4b5e6c833 100644 --- a/internal/rpc/captcha/captcha.go +++ b/internal/rpc/captcha/captcha.go @@ -2,6 +2,7 @@ package captcha import ( "context" + "encoding/json" "errors" "time" @@ -17,10 +18,17 @@ import ( "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" + "github.com/wenlng/go-captcha/v2/click" ) +// alphanumChars is the character pool for the click captcha. +// Visually ambiguous characters (I, O, 0, 1, l) are excluded. +var alphanumChars = []string{ + "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", + "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z", "2", "3", "4", "5", "6", "7", "8", "9", +} + type Config struct { RpcConfig config.Captcha MongodbConfig config.Mongo @@ -31,14 +39,14 @@ type Config struct { type server struct { pbcaptcha.UnimplementedCaptchaServer conf config.Captcha - capt slide.Captcha + capt click.Captcha collection *mongo.Collection } +// captchaDoc is the MongoDB document that stores the verification answer. type captchaDoc struct { CaptchaID string `bson:"captcha_id"` - X int `bson:"x"` - Y int `bson:"y"` + DotsJSON string `bson:"dots_json"` // JSON-encoded map[int]*click.Dot (answer dots) ExpiredAt time.Time `bson:"expired_at"` CreateTime time.Time `bson:"create_time"` VerifyTime time.Time `bson:"verify_time,omitempty"` @@ -66,17 +74,15 @@ func Start(ctx context.Context, cfg *Config, _ discovery.SvcDiscoveryRegistry, g return err } - resources, err := loadResources() + capt, err := buildClickCaptcha() if err != nil { - log.ZError(ctx, "captcha load resources failed", err) + log.ZError(ctx, "captcha build click captcha failed", err) return err } - builder := slide.NewBuilder() - builder.SetResources(resources...) s := &server{ conf: cfg.RpcConfig, - capt: builder.Make(), + capt: capt, collection: collection, } if s.conf.ExpireSeconds <= 0 { @@ -95,24 +101,31 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc log.ZError(ctx, "captcha generate failed", err) return nil, err } - block := captData.GetData() - masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(option.QualityNone) + + dots := captData.GetData() // answer dots: map[int]*click.Dot + masterImage, err := captData.GetMasterImage().ToBase64DataWithQuality(0) if err != nil { log.ZError(ctx, "captcha encode master image failed", err) return nil, err } - tileImage, err := captData.GetTileImage().ToBase64Data() + thumbImage, err := captData.GetThumbImage().ToBase64Data() if err != nil { - log.ZError(ctx, "captcha encode tile image failed", err) + log.ZError(ctx, "captcha encode thumb image failed", err) return nil, err } + + dotsJSON, err := json.Marshal(dots) + if err != nil { + log.ZError(ctx, "captcha marshal dots 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, + DotsJSON: string(dotsJSON), ExpiredAt: expiredAt, CreateTime: now, }) @@ -120,26 +133,26 @@ func (s *server) GenerateCaptcha(ctx context.Context, _ *pbcaptcha.GenerateCaptc log.ZError(ctx, "captcha insert mongodb failed", err, "captchaID", id) return nil, err } + + log.ZDebug(ctx, "captcha generated", "captchaID", id, "dotCount", len(dots), "expireAt", expiredAt.Unix()) return &pbcaptcha.GenerateCaptchaResp{ CaptchaID: id, MasterImage: masterImage, - TileImage: tileImage, - TileY: int32(block.DY), + ThumbImage: thumbImage, ExpireAt: expiredAt.Unix(), }, nil } func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptchaReq) (*pbcaptcha.VerifyCaptchaResp, error) { + log.ZDebug(ctx, "captcha verify request", "captchaID", req.CaptchaID, "clickCount", len(req.ClickPoints)) + 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, - }, - } + update := bson.M{"$set": bson.M{"verify_time": now}} + var doc captchaDoc err := s.collection.FindOneAndUpdate( ctx, @@ -159,9 +172,41 @@ func (s *server) VerifyCaptcha(ctx context.Context, req *pbcaptcha.VerifyCaptcha 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) + + // Unmarshal the stored answer dots. + var answerDots map[int]*click.Dot + if err := json.Unmarshal([]byte(doc.DotsJSON), &answerDots); err != nil { + log.ZError(ctx, "captcha unmarshal dots failed", err, "captchaID", req.CaptchaID) + return nil, servererrs.ErrDatabase.WrapMsg("internal captcha data error") + } + + success := validateClickPoints(req.ClickPoints, answerDots, s.conf.VerifyPadding) if !success { - log.ZError(ctx, "captcha validate failed", nil, "captchaID", req.CaptchaID, "x", req.X, "y", req.Y, "docX", doc.X, "docY", doc.Y) + log.ZError(ctx, "captcha validate failed", nil, + "captchaID", req.CaptchaID, + "clickCount", len(req.ClickPoints), + "answerCount", len(answerDots), + ) + } else { + log.ZDebug(ctx, "captcha validate success", "captchaID", req.CaptchaID) } return &pbcaptcha.VerifyCaptchaResp{Success: success}, nil } + +// validateClickPoints checks that each user click point falls within the +// bounding box of the corresponding answer dot (in order). +func validateClickPoints(points []*pbcaptcha.ClickPoint, dots map[int]*click.Dot, padding int) bool { + if len(points) != len(dots) { + return false + } + for i, pt := range points { + dot, ok := dots[i] + if !ok { + return false + } + if !click.Validate(int(pt.X), int(pt.Y), dot.X, dot.Y, dot.Width, dot.Height, padding) { + return false + } + } + return true +} diff --git a/internal/rpc/captcha/embed.go b/internal/rpc/captcha/embed.go index 9ec2e8d8f..c0c0372bf 100644 --- a/internal/rpc/captcha/embed.go +++ b/internal/rpc/captcha/embed.go @@ -2,12 +2,7 @@ package captcha import "embed" -// resourceFS embeds background images and tile images at compile time. -// Background images come from go-captcha-resources (sourcedata/images/image-{1..5}). -// Tile images come from go-captcha-resources (sourcedata/tiles/tile-{1..4}): -// overlay.png → GraphImage.OverlayImage -// shadow.png → GraphImage.ShadowImage -// mask.png → GraphImage.MaskImage +// resourceFS embeds background images for the click captcha at compile time. // -//go:embed resources/images/*.jpg resources/tiles/*/*.png +//go:embed resources/images/*.jpg var resourceFS embed.FS diff --git a/internal/rpc/captcha/resources.go b/internal/rpc/captcha/resources.go index 1264ef1fd..d02ce1c29 100644 --- a/internal/rpc/captcha/resources.go +++ b/internal/rpc/captcha/resources.go @@ -6,24 +6,42 @@ import ( _ "image/jpeg" _ "image/png" - "github.com/wenlng/go-captcha/v2/slide" + "github.com/golang/freetype" + "github.com/golang/freetype/truetype" + "github.com/wenlng/go-captcha/v2/base/option" + "github.com/wenlng/go-captcha/v2/click" + "golang.org/x/image/font/gofont/goregular" ) -// loadResources reads the embedded files and returns slide.Resource options -// ready to be passed to slide.NewBuilder().SetResources(...). -func loadResources() ([]slide.Resource, error) { - backgrounds, err := loadBackgrounds() +// buildClickCaptcha constructs a click.Captcha instance configured with +// alphanumeric characters, a bundled Go font, and the embedded background images. +func buildClickCaptcha() (click.Captcha, error) { + font, err := loadGoRegularFont() if err != nil { - return nil, fmt.Errorf("load captcha backgrounds: %w", err) + return nil, fmt.Errorf("load font: %w", err) } - graphImages, err := loadGraphImages() + backgrounds, err := loadBackgrounds() if err != nil { - return nil, fmt.Errorf("load captcha graph images: %w", err) + return nil, fmt.Errorf("load captcha backgrounds: %w", err) } - return []slide.Resource{ - slide.WithBackgrounds(backgrounds), - slide.WithGraphImages(graphImages), - }, nil + + builder := click.NewBuilder( + click.WithRangeLen(option.RangeVal{Min: 6, Max: 8}), + click.WithRangeVerifyLen(option.RangeVal{Min: 3, Max: 4}), + click.WithRangeSize(option.RangeVal{Min: 26, Max: 34}), + click.WithDisplayShadow(true), + ) + builder.SetResources( + click.WithChars(alphanumChars), + click.WithFonts([]*truetype.Font{font}), + click.WithBackgrounds(backgrounds), + ) + return builder.Make(), nil +} + +// loadGoRegularFont parses the bundled Go Regular TTF font. +func loadGoRegularFont() (*truetype.Font, error) { + return freetype.ParseFont(goregular.TTF) } // loadBackgrounds decodes the embedded JPEG background images. @@ -41,32 +59,6 @@ func loadBackgrounds() ([]image.Image, error) { return images, nil } -// loadGraphImages decodes the 4 sets of tile overlay/shadow/mask PNG images. -func loadGraphImages() ([]*slide.GraphImage, error) { - const count = 4 - graphs := make([]*slide.GraphImage, 0, count) - for i := 1; i <= count; i++ { - overlay, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/overlay.png", i)) - if err != nil { - return nil, fmt.Errorf("decode tile-%d overlay: %w", i, err) - } - shadow, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/shadow.png", i)) - if err != nil { - return nil, fmt.Errorf("decode tile-%d shadow: %w", i, err) - } - mask, err := decodeEmbedImage(fmt.Sprintf("resources/tiles/tile-%d/mask.png", i)) - if err != nil { - return nil, fmt.Errorf("decode tile-%d mask: %w", i, err) - } - graphs = append(graphs, &slide.GraphImage{ - OverlayImage: overlay, - ShadowImage: shadow, - MaskImage: mask, - }) - } - return graphs, nil -} - func decodeEmbedImage(path string) (image.Image, error) { f, err := resourceFS.Open(path) if err != nil { diff --git a/internal/rpc/captcha/resources/images/image-1.jpg b/internal/rpc/captcha/resources/images/image-1.jpg index f3be01297..a392ce012 100644 Binary files a/internal/rpc/captcha/resources/images/image-1.jpg and b/internal/rpc/captcha/resources/images/image-1.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-2.jpg b/internal/rpc/captcha/resources/images/image-2.jpg index ca35458f6..36e867881 100644 Binary files a/internal/rpc/captcha/resources/images/image-2.jpg and b/internal/rpc/captcha/resources/images/image-2.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-3.jpg b/internal/rpc/captcha/resources/images/image-3.jpg index 104aeb77a..77cdb21db 100644 Binary files a/internal/rpc/captcha/resources/images/image-3.jpg and b/internal/rpc/captcha/resources/images/image-3.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-4.jpg b/internal/rpc/captcha/resources/images/image-4.jpg index dc12c2bf4..5fea0bd09 100644 Binary files a/internal/rpc/captcha/resources/images/image-4.jpg and b/internal/rpc/captcha/resources/images/image-4.jpg differ diff --git a/internal/rpc/captcha/resources/images/image-5.jpg b/internal/rpc/captcha/resources/images/image-5.jpg index 4a11044a9..601702436 100644 Binary files a/internal/rpc/captcha/resources/images/image-5.jpg and b/internal/rpc/captcha/resources/images/image-5.jpg differ diff --git a/internal/rpc/rtc/signal.go b/internal/rpc/rtc/signal.go index b20e02d36..a3efb70c5 100644 --- a/internal/rpc/rtc/signal.go +++ b/internal/rpc/rtc/signal.go @@ -48,7 +48,7 @@ func (s *rtcServer) SignalMessageAssemble(ctx context.Context, req *rtc.SignalMe ) switch payload := req.SignalReq.Payload.(type) { case *rtc.SignalReq_Invite: - log.ZInfo(ctx, "SignalMessageAssemble", "payload", payload.Invite) + log.ZDebug(ctx, "SignalMessageAssemble", "payload", payload.Invite) r, err := s.handleInvite(ctx, payload.Invite, req.SignalReq) resp.Payload = &rtc.SignalResp_Invite{Invite: r} respErr = err diff --git a/openim-sdk-core b/openim-sdk-core new file mode 120000 index 000000000..889d82c06 --- /dev/null +++ b/openim-sdk-core @@ -0,0 +1 @@ +../openim-sdk-core-origin \ No newline at end of file diff --git a/protocol b/protocol index 9f0b38eb5..ed16bd0c4 160000 --- a/protocol +++ b/protocol @@ -1 +1 @@ -Subproject commit 9f0b38eb5c5015da3969d6711a140c3ba12956bb +Subproject commit ed16bd0c4049d722e7b605c3f314ee661b9bc4e1 diff --git a/start-config.yml b/start-config.yml index d06738143..52afa4cab 100644 --- a/start-config.yml +++ b/start-config.yml @@ -13,6 +13,7 @@ serviceBinaries: openim-rpc-msg: 1 openim-rpc-rtc: 1 openim-rpc-third: 1 + openim-rpc-crypto: 1 toolBinaries: - check-free-memory - check-component