mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-20 16:13:59 +08:00
418 lines
12 KiB
Go
418 lines
12 KiB
Go
package middleware
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/1Panel-dev/1Panel/core/app/model"
|
|
"github.com/1Panel-dev/1Panel/core/app/repo"
|
|
"github.com/1Panel-dev/1Panel/core/cmd/server/docs"
|
|
"github.com/1Panel-dev/1Panel/core/constant"
|
|
"github.com/1Panel-dev/1Panel/core/global"
|
|
psessionUtils "github.com/1Panel-dev/1Panel/core/init/session/psession"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/glebarez/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
headerNeedOperationResolve = "X-Need-Op-Resolve"
|
|
headerOperationResolved = "X-Op-Resolved"
|
|
)
|
|
|
|
func OperationLog() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
c.Request.Header.Del(headerNeedOperationResolve)
|
|
|
|
if strings.Contains(c.Request.URL.Path, "search") || c.Request.Method == http.MethodGet {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
source := loadLogInfo(c.Request.URL.Path)
|
|
pathItem := strings.TrimPrefix(c.Request.URL.Path, "/api/v2")
|
|
pathItem = strings.TrimPrefix(pathItem, "/api/v2/core")
|
|
currentNodeItem := c.Request.Header.Get("CurrentNode")
|
|
currentNode, _ := url.QueryUnescape(currentNodeItem)
|
|
record := &model.OperationLog{
|
|
Source: source,
|
|
Node: currentNode,
|
|
IP: c.ClientIP(),
|
|
Method: strings.ToLower(c.Request.Method),
|
|
Path: pathItem,
|
|
UserAgent: c.Request.UserAgent(),
|
|
}
|
|
swagger := make(map[string]operationJson)
|
|
if err := json.Unmarshal(docs.XLogJson, &swagger); err != nil {
|
|
c.Next()
|
|
return
|
|
}
|
|
operationDic, hasPath := swagger[record.Path]
|
|
if !hasPath {
|
|
if fullPath := normalizeOperationPath(c.FullPath()); fullPath != "" {
|
|
operationDic, hasPath = swagger[fullPath]
|
|
}
|
|
}
|
|
if !hasPath {
|
|
c.Next()
|
|
return
|
|
}
|
|
if len(operationDic.FormatZH) == 0 {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
formatMap := make(map[string]interface{})
|
|
if len(operationDic.BodyKeys) != 0 {
|
|
body, err := io.ReadAll(c.Request.Body)
|
|
if err == nil {
|
|
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
|
}
|
|
bodyMap := make(map[string]interface{})
|
|
if strings.Contains(c.Request.Header.Get("Content-Type"), "multipart/form-data") {
|
|
bodyMap, _ = parseMultipart(body, c.Request.Header.Get("Content-Type"))
|
|
} else {
|
|
decoder := json.NewDecoder(bytes.NewReader(body))
|
|
decoder.UseNumber()
|
|
_ = decoder.Decode(&bodyMap)
|
|
}
|
|
for _, key := range operationDic.BodyKeys {
|
|
if _, ok := bodyMap[key]; ok {
|
|
formatMap[key] = bodyMap[key]
|
|
}
|
|
}
|
|
}
|
|
needAgentResolve := len(operationDic.BeforeFunctions) != 0 && len(currentNode) != 0 && currentNode != "local" && !strings.HasPrefix(record.Path, "/core")
|
|
allowCoreFallback := strings.HasPrefix(record.Path, "/core/xpack") || !ShouldProxyToAgent(c.Request.URL.Path) || len(currentNode) == 0 || currentNode == "local"
|
|
if needAgentResolve {
|
|
c.Request.Header.Set(headerNeedOperationResolve, "1")
|
|
defer func() {
|
|
c.Request.Header.Del(headerNeedOperationResolve)
|
|
}()
|
|
}
|
|
|
|
writer := responseBodyWriter{
|
|
ResponseWriter: c.Writer,
|
|
body: &bytes.Buffer{},
|
|
captureBody: !strings.Contains(strings.ToLower(c.Request.URL.Path), "download"),
|
|
}
|
|
c.Writer = &writer
|
|
now := time.Now()
|
|
|
|
c.Next()
|
|
|
|
record.User = LoadOperationUser(c)
|
|
|
|
if len(operationDic.BeforeFunctions) != 0 {
|
|
if needAgentResolve {
|
|
mergeResolvedData(writer.resolvedHeader, formatMap)
|
|
}
|
|
|
|
if allowCoreFallback && !hasAllResolvedData(formatMap, operationDic.BeforeFunctions) {
|
|
dbItem, err := newDB(record.Path)
|
|
if err == nil {
|
|
resolveByDB(dbItem, formatMap, operationDic.BeforeFunctions)
|
|
closeDB(dbItem)
|
|
}
|
|
}
|
|
}
|
|
fillOperationDetail(&operationDic, formatMap)
|
|
record.DetailEN = strings.ReplaceAll(operationDic.FormatEN, "[]", "")
|
|
record.DetailZH = strings.ReplaceAll(operationDic.FormatZH, "[]", "")
|
|
|
|
datas := writer.body.Bytes()
|
|
logRepo := repo.NewILogRepo()
|
|
if c.Request.Header.Get("Content-Encoding") == "gzip" {
|
|
buf := bytes.NewReader(writer.body.Bytes())
|
|
reader, err := gzip.NewReader(buf)
|
|
if err != nil {
|
|
record.Status = constant.StatusFailed
|
|
record.Message = fmt.Sprintf("gzip new reader failed, err: %v", err)
|
|
latency := time.Since(now)
|
|
record.Latency = latency
|
|
|
|
if err := logRepo.CreateOperationLog(record); err != nil {
|
|
global.LOG.Errorf("create operation record failed, err: %v", err)
|
|
}
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
datas, _ = io.ReadAll(reader)
|
|
}
|
|
var res response
|
|
contentType := strings.ToLower(c.Writer.Header().Get("Content-Type"))
|
|
isJSONResponse := strings.Contains(contentType, "application/json")
|
|
if isJSONResponse {
|
|
_ = json.Unmarshal(datas, &res)
|
|
if res.Code == 200 {
|
|
record.Status = constant.StatusSuccess
|
|
} else {
|
|
record.Status = constant.StatusFailed
|
|
record.Message = res.Message
|
|
}
|
|
} else {
|
|
statusCode := c.Writer.Status()
|
|
if statusCode >= 200 && statusCode < 400 {
|
|
record.Status = constant.StatusSuccess
|
|
} else {
|
|
record.Status = constant.StatusFailed
|
|
record.Message = http.StatusText(statusCode)
|
|
}
|
|
}
|
|
|
|
latency := time.Since(now)
|
|
record.Latency = latency
|
|
|
|
if err := logRepo.CreateOperationLog(record); err != nil {
|
|
global.LOG.Errorf("create operation record failed, err: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func LoadOperationUser(c *gin.Context) string {
|
|
sessionUser, ok := c.Get(psessionUtils.GinContextSessionUserKey)
|
|
if ok {
|
|
psession, ok := sessionUser.(psessionUtils.SessionUser)
|
|
if ok {
|
|
return psession.Name
|
|
}
|
|
}
|
|
apiUsername, ok := c.Get("API_AUTH_USERNAME")
|
|
if !ok {
|
|
return ""
|
|
}
|
|
username, ok := apiUsername.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return username
|
|
}
|
|
|
|
func fillOperationDetail(operationDic *operationJson, formatMap map[string]interface{}) {
|
|
for key, value := range formatMap {
|
|
if !strings.Contains(operationDic.FormatEN, "["+key+"]") {
|
|
continue
|
|
}
|
|
t := reflect.TypeOf(value)
|
|
if t == nil || (t.Kind() != reflect.Array && t.Kind() != reflect.Slice) {
|
|
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", fmt.Sprintf("[%v]", value))
|
|
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", fmt.Sprintf("[%v]", value))
|
|
continue
|
|
}
|
|
|
|
val := reflect.ValueOf(value)
|
|
length := val.Len()
|
|
elements := make([]string, 0, length)
|
|
for i := 0; i < length; i++ {
|
|
elements = append(elements, fmt.Sprintf("%v", val.Index(i).Interface()))
|
|
}
|
|
replaced := fmt.Sprintf("[%v]", strings.Join(elements, ","))
|
|
operationDic.FormatZH = strings.ReplaceAll(operationDic.FormatZH, "["+key+"]", replaced)
|
|
operationDic.FormatEN = strings.ReplaceAll(operationDic.FormatEN, "["+key+"]", replaced)
|
|
}
|
|
}
|
|
|
|
type operationJson struct {
|
|
API string `json:"api"`
|
|
Method string `json:"method"`
|
|
BodyKeys []string `json:"bodyKeys"`
|
|
ParamKeys []string `json:"paramKeys"`
|
|
BeforeFunctions []functionInfo `json:"beforeFunctions"`
|
|
FormatZH string `json:"formatZH"`
|
|
FormatEN string `json:"formatEN"`
|
|
}
|
|
type functionInfo struct {
|
|
InputColumn string `json:"input_column"`
|
|
InputValue string `json:"input_value"`
|
|
IsList bool `json:"isList"`
|
|
DB string `json:"db"`
|
|
OutputColumn string `json:"output_column"`
|
|
OutputValue string `json:"output_value"`
|
|
}
|
|
|
|
type response struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type responseBodyWriter struct {
|
|
gin.ResponseWriter
|
|
body *bytes.Buffer
|
|
resolvedHeader string
|
|
captureBody bool
|
|
}
|
|
|
|
func (r *responseBodyWriter) sanitizeResolvedHeader() {
|
|
if len(r.resolvedHeader) == 0 {
|
|
r.resolvedHeader = r.ResponseWriter.Header().Get(headerOperationResolved)
|
|
}
|
|
r.ResponseWriter.Header().Del(headerOperationResolved)
|
|
}
|
|
|
|
func (r *responseBodyWriter) WriteHeader(code int) {
|
|
r.sanitizeResolvedHeader()
|
|
r.ResponseWriter.WriteHeader(code)
|
|
}
|
|
|
|
func (r *responseBodyWriter) WriteHeaderNow() {
|
|
r.sanitizeResolvedHeader()
|
|
r.ResponseWriter.WriteHeaderNow()
|
|
}
|
|
|
|
func (r *responseBodyWriter) Write(b []byte) (int, error) {
|
|
r.sanitizeResolvedHeader()
|
|
if r.captureBody {
|
|
r.body.Write(b)
|
|
}
|
|
return r.ResponseWriter.Write(b)
|
|
}
|
|
|
|
func loadLogInfo(path string) string {
|
|
path = replaceStr(path, "/api/v2", "/core", "/xpack", "/enterprise")
|
|
if !strings.Contains(path, "/") {
|
|
return ""
|
|
}
|
|
pathArrays := strings.Split(path, "/")
|
|
if len(pathArrays) < 2 {
|
|
return ""
|
|
}
|
|
return pathArrays[1]
|
|
}
|
|
|
|
func normalizeOperationPath(reqPath string) string {
|
|
pathItem := strings.TrimPrefix(reqPath, "/api/v2")
|
|
pathItem = strings.TrimPrefix(pathItem, "/api/v2/core")
|
|
return pathItem
|
|
}
|
|
|
|
func newDB(pathItem string) (*gorm.DB, error) {
|
|
dbFile := ""
|
|
switch {
|
|
case strings.HasPrefix(pathItem, "/core/xpack") || strings.HasPrefix(pathItem, "/xpack"):
|
|
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db")
|
|
case strings.HasPrefix(pathItem, "/core/enterprise") || strings.HasPrefix(pathItem, "/enterprise"):
|
|
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/enterprise.db")
|
|
case strings.HasPrefix(pathItem, "/core"):
|
|
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/core.db")
|
|
default:
|
|
dbFile = path.Join(global.CONF.Base.InstallDir, "1panel/db/agent.db")
|
|
}
|
|
|
|
db, _ := gorm.Open(sqlite.Open(dbFile), &gorm.Config{
|
|
DisableForeignKeyConstraintWhenMigrating: true,
|
|
})
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sqlDB.SetMaxOpenConns(4)
|
|
sqlDB.SetMaxIdleConns(1)
|
|
sqlDB.SetConnMaxIdleTime(15 * time.Minute)
|
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
|
return db, nil
|
|
}
|
|
|
|
func closeDB(db *gorm.DB) {
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
return
|
|
}
|
|
_ = sqlDB.Close()
|
|
}
|
|
|
|
func resolveByDB(dbItem *gorm.DB, values map[string]interface{}, beforeFunctions []functionInfo) {
|
|
for _, funcs := range beforeFunctions {
|
|
for key, value := range values {
|
|
if funcs.InputValue != key {
|
|
continue
|
|
}
|
|
var names []string
|
|
if funcs.IsList {
|
|
sql := fmt.Sprintf("SELECT %s FROM %s where %s in (?);", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
|
_ = dbItem.Raw(sql, value).Scan(&names)
|
|
} else {
|
|
sql := fmt.Sprintf("select %s from %s where %s = ?;", funcs.OutputColumn, funcs.DB, funcs.InputColumn)
|
|
_ = dbItem.Raw(sql, value).Scan(&names)
|
|
}
|
|
values[funcs.OutputValue] = strings.Join(names, ",")
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func replaceStr(val string, rep ...string) string {
|
|
for _, item := range rep {
|
|
val = strings.ReplaceAll(val, item, "")
|
|
}
|
|
return val
|
|
}
|
|
|
|
func parseMultipart(formData []byte, contentType string) (map[string]interface{}, error) {
|
|
d, params, err := mime.ParseMediaType(contentType)
|
|
if err != nil || d != "multipart/form-data" {
|
|
return nil, http.ErrNotMultipart
|
|
}
|
|
boundary, ok := params["boundary"]
|
|
if !ok {
|
|
return nil, http.ErrMissingBoundary
|
|
}
|
|
reader := multipart.NewReader(bytes.NewReader(formData), boundary)
|
|
ret := make(map[string]interface{})
|
|
|
|
f, err := reader.ReadForm(32 << 20)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for k, v := range f.Value {
|
|
if len(v) > 0 {
|
|
ret[k] = v[0]
|
|
}
|
|
}
|
|
for k, v := range f.File {
|
|
if len(v) > 0 {
|
|
ret[k] = v[0].Filename
|
|
}
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func mergeResolvedData(headerVal string, values map[string]interface{}) {
|
|
if len(headerVal) == 0 {
|
|
return
|
|
}
|
|
data, err := base64.RawURLEncoding.DecodeString(headerVal)
|
|
if err != nil {
|
|
return
|
|
}
|
|
resolved := make(map[string]string)
|
|
if err := json.Unmarshal(data, &resolved); err != nil {
|
|
return
|
|
}
|
|
for key, value := range resolved {
|
|
values[key] = value
|
|
}
|
|
}
|
|
|
|
func hasAllResolvedData(values map[string]interface{}, beforeFunctions []functionInfo) bool {
|
|
for _, item := range beforeFunctions {
|
|
if _, ok := values[item.OutputValue]; ok {
|
|
continue
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|