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 := normalizeOperationPath(c.Request.URL.Path) 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 }