Files
open-im-server-deploy/internal/rpc/msg/qrcode_detect.go
kim.dev.6789 e50142a3b9 复制项目
2026-01-14 22:16:44 +08:00

192 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright © 2023 OpenIM. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package msg
import (
"context"
"encoding/json"
"fmt"
_ "image/jpeg"
_ "image/png"
"io"
"net/http"
"os"
"time"
"git.imall.cloud/openim/open-im-server-deploy/pkg/common/servererrs"
"git.imall.cloud/openim/protocol/constant"
"git.imall.cloud/openim/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
)
// PictureElem 用于解析图片消息内容
type PictureElem struct {
SourcePicture struct {
URL string `json:"url"`
} `json:"sourcePicture"`
BigPicture struct {
URL string `json:"url"`
} `json:"bigPicture"`
SnapshotPicture struct {
URL string `json:"url"`
} `json:"snapshotPicture"`
}
// checkImageContainsQRCode 检测图片中是否包含二维码
// userType: 0-普通用户不能发送包含二维码的图片1-特殊用户(可以发送)
func (m *msgServer) checkImageContainsQRCode(ctx context.Context, msgData *sdkws.MsgData, userType int32) error {
// userType=1 的用户可以发送包含二维码的图片,不进行检测
if userType == 1 {
return nil
}
// 只检测图片类型的消息
if msgData.ContentType != constant.Picture {
return nil
}
// 解析图片消息内容
var pictureElem PictureElem
if err := json.Unmarshal(msgData.Content, &pictureElem); err != nil {
// 如果解析失败,记录警告但不拦截
log.ZWarn(ctx, "failed to parse picture message", err, "content", string(msgData.Content))
return nil
}
// 获取图片URL优先使用原图如果没有则使用大图
imageURL := pictureElem.SourcePicture.URL
if imageURL == "" {
imageURL = pictureElem.BigPicture.URL
}
if imageURL == "" {
imageURL = pictureElem.SnapshotPicture.URL
}
if imageURL == "" {
// 没有有效的图片URL无法检测
log.ZWarn(ctx, "no valid image URL found in picture message", nil, "pictureElem", pictureElem)
return nil
}
// 下载图片并检测二维码
hasQRCode, err := m.detectQRCodeInImage(ctx, imageURL, "")
if err != nil {
// 检测失败时,记录错误但不拦截(避免误拦截)
log.ZWarn(ctx, "QR code detection failed", err, "imageURL", imageURL)
return nil
}
if hasQRCode {
log.ZWarn(ctx, "检测到二维码,拒绝发送", nil, "imageURL", imageURL, "userType", userType)
return servererrs.ErrImageContainsQRCode.WrapMsg("userType=0的用户不能发送包含二维码的图片")
}
return nil
}
// detectQRCodeInImage 下载图片并检测是否包含二维码
func (m *msgServer) detectQRCodeInImage(ctx context.Context, imageURL string, logPrefix string) (bool, error) {
// 创建带超时的HTTP客户端
client := &http.Client{
Timeout: 5 * time.Second,
}
// 下载图片
req, err := http.NewRequestWithContext(ctx, http.MethodGet, imageURL, nil)
if err != nil {
log.ZError(ctx, "创建HTTP请求失败", err, "imageURL", imageURL)
return false, errs.WrapMsg(err, "failed to create request")
}
resp, err := client.Do(req)
if err != nil {
log.ZError(ctx, "下载图片失败", err, "imageURL", imageURL)
return false, errs.WrapMsg(err, "failed to download image")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.ZError(ctx, "下载图片状态码异常", nil, "statusCode", resp.StatusCode, "imageURL", imageURL)
return false, errs.WrapMsg(fmt.Errorf("unexpected status code: %d", resp.StatusCode), "failed to download image")
}
// 限制图片大小最大10MB
const maxImageSize = 10 * 1024 * 1024
limitedReader := io.LimitReader(resp.Body, maxImageSize+1)
// 创建临时文件
tmpFile, err := os.CreateTemp("", "qrcode_detect_*.tmp")
if err != nil {
log.ZError(ctx, "创建临时文件失败", err)
return false, errs.WrapMsg(err, "failed to create temp file")
}
tmpFilePath := tmpFile.Name()
// 确保检测完成后删除临时文件(无论成功还是失败)
defer func() {
// 确保文件已关闭后再删除
if tmpFile != nil {
_ = tmpFile.Close()
}
// 删除临时文件,忽略文件不存在的错误
if err := os.Remove(tmpFilePath); err != nil && !os.IsNotExist(err) {
log.ZWarn(ctx, "删除临时文件失败", err, "tmpFile", tmpFilePath)
}
}()
// 保存图片到临时文件
written, err := io.Copy(tmpFile, limitedReader)
if err != nil {
log.ZError(ctx, "保存图片到临时文件失败", err, "tmpFile", tmpFilePath)
return false, errs.WrapMsg(err, "failed to save image")
}
// 关闭文件以便后续读取
if err := tmpFile.Close(); err != nil {
log.ZError(ctx, "关闭临时文件失败", err, "tmpFile", tmpFilePath)
return false, errs.WrapMsg(err, "failed to close temp file")
}
// 检查文件大小
if written > maxImageSize {
log.ZWarn(ctx, "图片过大", nil, "size", written, "maxSize", maxImageSize)
return false, errs.WrapMsg(fmt.Errorf("image too large: %d bytes", written), "image size exceeds limit")
}
// 使用优化的并行解码器检测二维码
hasQRCode, err := m.detectQRCodeWithDecoder(ctx, tmpFilePath, "")
if err != nil {
log.ZError(ctx, "二维码检测失败", err, "tmpFile", tmpFilePath)
return false, err
}
return hasQRCode, nil
}
// detectQRCodeWithDecoder 使用优化的解码器检测二维码
func (m *msgServer) detectQRCodeWithDecoder(ctx context.Context, imagePath string, logPrefix string) (bool, error) {
// 使用Custom解码器已移除Quirc解码器依赖
customDecoder := &CustomQRDecoder{}
// 执行解码
hasQRCode, err := customDecoder.Decode(ctx, imagePath, logPrefix)
if err != nil {
log.ZError(ctx, "解码器检测失败", err, "decoder", customDecoder.Name())
return false, err
}
return hasQRCode, nil
}