You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
459 lines
14 KiB
459 lines
14 KiB
package gateway
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/core"
|
|
"yunyan/lego/core/cbase"
|
|
"yunyan/lego/utils/crypto/aes"
|
|
_ "yunyan/swagger"
|
|
"yunyan/utils"
|
|
|
|
"yunyan/lego/sys/log"
|
|
"yunyan/lego/sys/pools"
|
|
"yunyan/pb"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
swaggerfiles "github.com/swaggo/files"
|
|
ginSwagger "github.com/swaggo/gin-swagger"
|
|
)
|
|
|
|
// writeReply 统一处理 RPC 响应写回 HTTP:
|
|
// 1. 根据业务服务上报的 reply.Encrypted 字段判断是否加密
|
|
// 2. 调用 AES-CBC 对整个 body 加密并 base64,改写 reply.Body
|
|
// 3. 设置 X-Encrypted: 1 响应头,便于前端协议层识别并解密
|
|
func (this *WSServiceComp) writeReply(c *gin.Context, reply *pb.Rpc_GatewayHttpRouteResp) {
|
|
if reply.Encrypted && this.options.EncryptKey != "" && len(reply.Body) > 0 {
|
|
cipher := aes.AesEncryptCBC(string(reply.Body), this.options.EncryptKey)
|
|
reply.Body = []byte(cipher)
|
|
c.Header("X-Encrypted", "1")
|
|
}
|
|
c.Data(http.StatusOK, reply.ContentType, reply.Body)
|
|
}
|
|
|
|
type WSServiceComp struct {
|
|
cbase.ModuleCompBase
|
|
options *Options
|
|
module *Gateway // 网关
|
|
gin *gin.Engine
|
|
tslock sync.Mutex // 锁
|
|
userTsMap map[string]int64 //用户ts映射
|
|
}
|
|
|
|
// Init websocket服务初始化
|
|
func (this *WSServiceComp) Init(service core.IService, module core.IModule, comp core.IModuleComp, options core.IModuleOptions) (err error) {
|
|
err = this.ModuleCompBase.Init(service, module, comp, options)
|
|
this.options = options.(*Options)
|
|
this.module = module.(*Gateway)
|
|
this.userTsMap = make(map[string]int64)
|
|
this.gin = gin.Default()
|
|
this.gin.MaxMultipartMemory = 500 << 20 // 设置上传文件大小为32M
|
|
this.gin.Use(cors()) //设置允许跨域
|
|
this.gin.Static("/avatar", "./avatar")
|
|
this.gin.Static("/console", "./console")
|
|
this.gin.Static("/docs", "./docs")
|
|
this.gin.Any("/api/:param1/:param2", this.api)
|
|
this.gin.Any("/web/:param1/:param2", this.web)
|
|
this.gin.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
|
|
go this.gin.Run(fmt.Sprintf(":%d", this.options.ListenPort))
|
|
return
|
|
}
|
|
|
|
// 后台
|
|
func (this *WSServiceComp) web(c *gin.Context) {
|
|
var (
|
|
param1, param2 string
|
|
body []byte
|
|
params string
|
|
tokenString string
|
|
uid string
|
|
file *multipart.FileHeader
|
|
meta map[string]string = make(map[string]string)
|
|
args *pb.Rpc_GatewayHttpRouteReq = pools.GetForType(httpReqTyoe).(*pb.Rpc_GatewayHttpRouteReq)
|
|
reply *pb.Rpc_GatewayHttpRouteResp = pools.GetForType(httpRespTyoe).(*pb.Rpc_GatewayHttpRouteResp)
|
|
err error
|
|
)
|
|
|
|
defer func() {
|
|
pools.PutForType(httpReqTyoe, args)
|
|
pools.PutForType(httpRespTyoe, reply)
|
|
}()
|
|
meta[comm.SessionMeta_IP] = c.ClientIP()
|
|
meta["route"] = "web" // 标记为后台管理请求
|
|
param1 = c.Param("param1")
|
|
param2 = c.Param("param2")
|
|
tokenString = c.Request.Header.Get("Authorization")
|
|
if tokenString != "" {
|
|
if parsedClaims, err := parseToken(tokenString, []byte(this.options.TokenKey)); err == nil {
|
|
uid = parsedClaims.ID
|
|
meta[comm.SessionMeta_UserId] = uid
|
|
meta["identity"] = parsedClaims.Subject // 身份: 1=超管 2=管理员 3=代理
|
|
}
|
|
}
|
|
log.Debugln("ContentType:", c.ContentType())
|
|
switch c.Request.Method {
|
|
case http.MethodGet:
|
|
params := make(map[string]interface{})
|
|
for key, values := range c.Request.URL.Query() {
|
|
if len(values) > 0 {
|
|
params[key] = values[0]
|
|
}
|
|
}
|
|
body, err = json.Marshal(params)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: err.Error(),
|
|
})
|
|
this.module.Errorln(err)
|
|
return
|
|
}
|
|
case http.MethodPost:
|
|
if c.ContentType() == "multipart/form-data" {
|
|
params := make(map[string]interface{})
|
|
if form, err := c.MultipartForm(); err == nil {
|
|
// 遍历表单参数
|
|
for key, values := range form.Value {
|
|
for _, value := range values {
|
|
// fmt.Printf("Key: %s, Value: %s\n", key, value)
|
|
params[key] = value
|
|
}
|
|
}
|
|
}
|
|
body, err = json.Marshal(params)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: err.Error(),
|
|
})
|
|
this.module.Errorln(err)
|
|
return
|
|
}
|
|
} else {
|
|
if body, err = c.GetRawData(); err != nil {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: err.Error(),
|
|
})
|
|
this.module.Errorln(err)
|
|
return
|
|
}
|
|
}
|
|
|
|
}
|
|
args.MsgName = param2
|
|
args.Message = body
|
|
args.Meta = meta
|
|
stime := time.Now()
|
|
if args.MsgName == "" {
|
|
this.module.Error("[UserResponse]",
|
|
log.Field{Key: "serviceTag", Value: this.module.service.GetId()},
|
|
log.Field{Key: "servicePath", Value: param1},
|
|
log.Field{Key: "req", Value: string(body)},
|
|
)
|
|
return
|
|
}
|
|
if uid == "" && (args.MsgName != "api_login" && args.MsgName != "api_getsiteinfo") { //登录和验证 放过去
|
|
this.module.Error("[ApiResponse]",
|
|
log.Field{Key: "req", Value: params},
|
|
log.Field{Key: "err", Value: "no login"},
|
|
)
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_NoLogin,
|
|
Message: pb.ErrorCode_NoLogin.String(),
|
|
})
|
|
return
|
|
}
|
|
if c.ContentType() == "multipart/form-data" {
|
|
//检查是否有文件上传
|
|
file, err = c.FormFile("file")
|
|
if err != nil {
|
|
if err != http.ErrMissingFile {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "文件读取失败: " + err.Error()})
|
|
return
|
|
}
|
|
err = nil
|
|
} else { //临时处理
|
|
// 打开上传的文件
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法打开上传文件"})
|
|
return
|
|
}
|
|
defer src.Close()
|
|
// 构造临时文件路径
|
|
tempDir := os.TempDir()
|
|
timestamp := time.Now().UnixNano()
|
|
tempPath := filepath.Join(tempDir, fmt.Sprintf("upload_%d_%s", timestamp, file.Filename))
|
|
// 创建目标文件
|
|
dst, err := os.Create(tempPath)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建临时文件失败"})
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
// 拷贝内容到临时文件
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存临时文件失败"})
|
|
return
|
|
}
|
|
args.Meta["file_name"] = file.Filename
|
|
args.Meta["file_path"] = tempPath //传递临时文件地址
|
|
// 发送处理通知
|
|
}
|
|
}
|
|
|
|
if err = this.module.Service().RpcCall(c, param1, string(comm.Rpc_GatewayHttpRoute), args, reply); err != nil {
|
|
this.module.Error("[ApiResponse]",
|
|
log.Field{Key: "ip", Value: c.ClientIP()},
|
|
log.Field{Key: "req", Value: params},
|
|
log.Field{Key: "err", Value: err.Error()},
|
|
)
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_SystemError,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
this.module.Debug("[API]",
|
|
log.Field{Key: "t", Value: time.Since(stime).Milliseconds()},
|
|
log.Field{Key: "ip", Value: c.ClientIP()},
|
|
log.Field{Key: "req", Value: body},
|
|
log.Field{Key: "reply", Value: reply.String()},
|
|
)
|
|
this.writeReply(c, reply)
|
|
}
|
|
|
|
// 业务
|
|
func (this *WSServiceComp) api(c *gin.Context) {
|
|
var (
|
|
param1, param2 string
|
|
body []byte
|
|
signString, tsString, tokenString string
|
|
uid string
|
|
ts int64
|
|
meta map[string]string = make(map[string]string)
|
|
file *multipart.FileHeader
|
|
args *pb.Rpc_GatewayHttpRouteReq = pools.GetForType(httpReqTyoe).(*pb.Rpc_GatewayHttpRouteReq)
|
|
reply *pb.Rpc_GatewayHttpRouteResp = pools.GetForType(httpRespTyoe).(*pb.Rpc_GatewayHttpRouteResp)
|
|
err error
|
|
)
|
|
|
|
defer func() {
|
|
pools.PutForType(httpReqTyoe, args)
|
|
pools.PutForType(httpRespTyoe, reply)
|
|
}()
|
|
meta[comm.SessionMeta_IP] = c.ClientIP()
|
|
param1 = c.Param("param1")
|
|
param2 = c.Param("param2")
|
|
if this.options.IsSign {
|
|
tsString = c.Request.Header.Get("Ts")
|
|
signString = c.Request.Header.Get("Sign")
|
|
|
|
if tsString == "" || signString == "" {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: "Sign is empty",
|
|
})
|
|
this.module.Errorln("Sign is empty")
|
|
return
|
|
}
|
|
// 验证签名
|
|
if signString != ParamSign(this.options.SignKey, tsString) {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: "Sign error",
|
|
})
|
|
this.module.Errorln("Sign error")
|
|
return
|
|
}
|
|
|
|
}
|
|
|
|
tokenString = c.Request.Header.Get("Authorization")
|
|
if tokenString != "" {
|
|
if parsedClaims, err := parseToken(tokenString, []byte(this.options.TokenKey)); err == nil {
|
|
uid = parsedClaims.ID
|
|
meta[comm.SessionMeta_UserId] = uid
|
|
|
|
if this.options.IsSign {
|
|
//防止消息被复用
|
|
ts = utils.ToInt64(tsString)
|
|
this.tslock.Lock()
|
|
if ts <= this.userTsMap[uid] {
|
|
this.module.Error("[ApiResponse] Ts error",
|
|
log.Field{Key: "ts", Value: ts},
|
|
log.Field{Key: "lastts", Value: this.userTsMap[uid]},
|
|
)
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: "Ts error",
|
|
})
|
|
this.module.Errorln("Ts error")
|
|
this.tslock.Unlock()
|
|
return
|
|
}
|
|
this.userTsMap[uid] = ts
|
|
this.tslock.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
if c.Request.Method == http.MethodGet {
|
|
params := make(map[string]interface{})
|
|
for key, values := range c.Request.URL.Query() {
|
|
if len(values) > 0 {
|
|
params[key] = values[0]
|
|
}
|
|
}
|
|
body, err = json.Marshal(params)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: err.Error(),
|
|
})
|
|
this.module.Errorln(err)
|
|
return
|
|
}
|
|
} else if c.Request.Method == http.MethodPost {
|
|
if c.ContentType() == "multipart/form-data" {
|
|
params := make(map[string]interface{})
|
|
if form, err := c.MultipartForm(); err == nil {
|
|
// 遍历表单参数
|
|
for key, values := range form.Value {
|
|
for _, value := range values {
|
|
// fmt.Printf("Key: %s, Value: %s\n", key, value)
|
|
params[key] = value
|
|
}
|
|
}
|
|
}
|
|
body, err = json.Marshal(params)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: err.Error(),
|
|
})
|
|
this.module.Errorln(err)
|
|
return
|
|
}
|
|
} else if c.ContentType() == "application/x-www-form-urlencoded" {
|
|
params := make(map[string]interface{})
|
|
if err = c.Request.ParseForm(); err == nil {
|
|
for key, values := range c.Request.PostForm {
|
|
if len(values) > 0 {
|
|
params[key] = values[0]
|
|
}
|
|
}
|
|
}
|
|
body, err = json.Marshal(params)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: err.Error(),
|
|
})
|
|
this.module.Errorln(err)
|
|
return
|
|
}
|
|
} else {
|
|
if body, err = c.GetRawData(); err != nil {
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_ReqParameterError,
|
|
Message: err.Error(),
|
|
})
|
|
this.module.Errorln(err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
args.MsgName = param2
|
|
args.Message = body
|
|
args.Meta = meta
|
|
stime := time.Now()
|
|
if args.MsgName == "" {
|
|
this.module.Error("[UserResponse]",
|
|
log.Field{Key: "serviceTag", Value: this.module.service.GetId()},
|
|
log.Field{Key: "servicePath", Value: param1},
|
|
log.Field{Key: "req", Value: string(body)},
|
|
)
|
|
return
|
|
}
|
|
isWhite := isInWhiteList(args.MsgName, this.options.WhiteList)
|
|
if uid == "" && !isWhite {
|
|
this.module.Error("[ApiResponse]",
|
|
log.Field{Key: "req", Value: string(body)},
|
|
log.Field{Key: "err", Value: "no login"},
|
|
)
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_NoLogin,
|
|
Message: pb.ErrorCode_NoLogin.String(),
|
|
})
|
|
return
|
|
}
|
|
if c.ContentType() == "multipart/form-data" {
|
|
//检查是否有文件上传
|
|
file, err = c.FormFile("file")
|
|
if err != nil {
|
|
if err != http.ErrMissingFile {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "文件读取失败: " + err.Error()})
|
|
return
|
|
}
|
|
err = nil
|
|
} else { //临时处理
|
|
// 打开上传的文件
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "无法打开上传文件"})
|
|
return
|
|
}
|
|
defer src.Close()
|
|
// 构造临时文件路径
|
|
tempDir := os.TempDir()
|
|
timestamp := time.Now().UnixNano()
|
|
tempPath := filepath.Join(tempDir, fmt.Sprintf("upload_%d_%s", timestamp, file.Filename))
|
|
// 创建目标文件
|
|
dst, err := os.Create(tempPath)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建临时文件失败"})
|
|
return
|
|
}
|
|
defer dst.Close()
|
|
// 拷贝内容到临时文件
|
|
if _, err := io.Copy(dst, src); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存临时文件失败"})
|
|
return
|
|
}
|
|
args.Meta["file_name"] = file.Filename
|
|
args.Meta["file_path"] = tempPath //传递临时文件地址
|
|
// 发送处理通知
|
|
}
|
|
}
|
|
if err = this.module.Service().RpcCall(c, param1, string(comm.Rpc_GatewayHttpRoute), args, reply); err != nil {
|
|
this.module.Error("[ApiResponse]",
|
|
log.Field{Key: "ip", Value: c.ClientIP()},
|
|
log.Field{Key: "req", Value: string(body)},
|
|
log.Field{Key: "err", Value: err.Error()},
|
|
)
|
|
c.JSON(http.StatusOK, &comm.HttpResult{
|
|
Code: pb.ErrorCode_SystemError,
|
|
Message: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
this.module.Debug("[ApiResponse]",
|
|
log.Field{Key: "name", Value: args.MsgName},
|
|
log.Field{Key: "t", Value: time.Since(stime).Milliseconds()},
|
|
log.Field{Key: "ip", Value: c.ClientIP()},
|
|
log.Field{Key: "req", Value: string(body)},
|
|
// log.Field{Key: "reply", Value: reply.String()},
|
|
)
|
|
this.writeReply(c, reply)
|
|
}
|
|
|