// 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 }