192 lines
6.1 KiB
Go
192 lines
6.1 KiB
Go
// 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
|
||
}
|