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.
 
 
 
 
 
 

500 lines
16 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. 密文是 base64 文本而非 JSON,把 Content-Type 改成 text/plain,避免客户端/中间层按 application/json 自动解析密文而报错
// 4. 设置 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)
reply.ContentType = "text/plain; charset=utf-8"
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")
// 会议纪要分享落地页(share.ymaikj.com 反代到本端口)。catch-all,理由见 share_page.go。
this.gin.GET("/share/*any", this.sharePage)
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
}
// console 运维接口(重载/重置业务配置)只允许经 /api/ 入口携带 HMAC 签名调用。
// 本入口(/web/)只校验"有没有用户登录"、不验签,若放行则任何登录用户都能重置生产配置——
// 而业务侧的权限拦截器对这些路由是豁免的(其身份由网关验签背书),故必须在此堵死。
if comm.IsConsoleOnlyRoute(args.MsgName) {
this.module.Error("[UserResponse] console 运维接口不允许从 /web 入口调用",
log.Field{Key: "msg", Value: args.MsgName},
log.Field{Key: "ip", Value: c.ClientIP()},
)
c.JSON(http.StatusOK, &comm.HttpResult{
Code: pb.ErrorCode_ReqParameterError,
Message: "forbidden",
})
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
}
// console 运维接口(如重载业务配置):不走用户登录,改用 ${FIELD_ENCRYPT_KEY} 的 HMAC 签名放行。
// 必须在登录判定之前处理——它本就没有用户身份;签名不合法则直接拒绝,不给任何降级路径。
if comm.IsConsoleOnlyRoute(args.MsgName) {
// 参数(如模块名)走请求头并已并入签名,故不必为这类接口新增 pb 字段;
// 验签通过后搬进 Meta,业务侧 handler 用 session.GetMateToString 取。
consoleArg := c.Request.Header.Get(comm.ConsoleArgHeader)
if err := comm.VerifyConsoleCall(
os.Getenv("FIELD_ENCRYPT_KEY"),
c.Request.Header.Get(comm.ConsoleTsHeader),
c.Request.Header.Get(comm.ConsoleSignHeader),
args.MsgName, consoleArg, time.Now(),
); err != nil {
this.module.Error("[ApiResponse] console 运维接口验签失败",
log.Field{Key: "msg", Value: args.MsgName},
log.Field{Key: "ip", Value: c.ClientIP()},
log.Field{Key: "err", Value: err.Error()},
)
c.JSON(http.StatusOK, &comm.HttpResult{
Code: pb.ErrorCode_ReqParameterError,
Message: "console sign invalid",
})
return
}
args.Meta[comm.SessionMeta_ConsoleArg] = consoleArg
} else if isWhite := isInWhiteList(args.MsgName, this.options.WhiteList); 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)
}