feat: harden query read-only workflow
This commit is contained in:
1 parent
58bbc394e9
commit
617c15dbfe
29 files changed
+1417
-395
No files matched your search
@@ -840,6 +840,10 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
|
||||
}
|
||||
|
||||
cmd := strings.ToUpper(parts[0])
|
||||
keyName := ""
|
||||
if len(parts) > 1 {
|
||||
keyName = parts[1]
|
||||
}
|
||||
readOnly := map[string]bool{
|
||||
"GET": true, "MGET": true, "HGET": true, "HGETALL": true,
|
||||
"LRANGE": true, "SMEMBERS": true, "ZRANGE": true,
|
||||
@@ -967,6 +971,21 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
|
||||
return nil, fmt.Errorf("command failed: %v", err)
|
||||
}
|
||||
|
||||
keyMeta := gin.H{}
|
||||
if keyName != "" && cmd != "TYPE" && cmd != "TTL" && cmd != "PTTL" && cmd != "EXISTS" && cmd != "DBSIZE" && cmd != "INFO" {
|
||||
keyMeta["raw_key"] = keyName
|
||||
if t, err := client.Type(ctx, keyName).Result(); err == nil {
|
||||
keyMeta["raw_type"] = t
|
||||
}
|
||||
if ttl, err := client.TTL(ctx, keyName).Result(); err == nil {
|
||||
if ttl < 0 {
|
||||
keyMeta["raw_ttl"] = "-1"
|
||||
} else {
|
||||
keyMeta["raw_ttl"] = fmt.Sprintf("%d", int(ttl.Seconds()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Format result into rows/columns for frontend table display
|
||||
columns := []string{"key", "value"}
|
||||
var rows []interface{}
|
||||
@@ -1128,10 +1147,6 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
|
||||
columns = []string{"field", "value"}
|
||||
} else {
|
||||
// Single value result (GET, HGET, TYPE, TTL, etc.)
|
||||
keyName := ""
|
||||
if len(parts) > 1 {
|
||||
keyName = parts[1]
|
||||
}
|
||||
rawValue := fmt.Sprintf("%v", v)
|
||||
// Try to pretty-format JSON with Chinese characters preserved
|
||||
var prettyJSON string
|
||||
@@ -1147,20 +1162,6 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
|
||||
prettyJSON = strings.TrimRight(buf.String(), "\n")
|
||||
}
|
||||
}
|
||||
// ARDM-style: auto-fetch key type and TTL for KeyHeader
|
||||
var keyType, keyTTL string
|
||||
if keyName != "" && cmd != "TYPE" && cmd != "TTL" && cmd != "PTTL" && cmd != "EXISTS" && cmd != "DBSIZE" && cmd != "INFO" {
|
||||
if t, err := client.Type(ctx, keyName).Result(); err == nil {
|
||||
keyType = t
|
||||
}
|
||||
if ttl, err := client.TTL(ctx, keyName).Result(); err == nil {
|
||||
if ttl < 0 {
|
||||
keyTTL = "永不过期"
|
||||
} else {
|
||||
keyTTL = fmt.Sprintf("%vs", int(ttl.Seconds()))
|
||||
}
|
||||
}
|
||||
}
|
||||
result := gin.H{
|
||||
"columns": []string{},
|
||||
"rows": []interface{}{},
|
||||
@@ -1169,22 +1170,23 @@ func executeRedisQuery(instance *model.Instance, dbSelect string, command string
|
||||
"raw_key": keyName,
|
||||
"raw_text": rawValue,
|
||||
}
|
||||
if keyType != "" {
|
||||
result["raw_type"] = keyType
|
||||
}
|
||||
if keyTTL != "" {
|
||||
result["raw_ttl"] = keyTTL
|
||||
for k, v := range keyMeta {
|
||||
result[k] = v
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
return gin.H{
|
||||
response := gin.H{
|
||||
"columns": columns,
|
||||
"rows": rowsOrDefault(rows),
|
||||
"total": len(rows),
|
||||
"count": len(rows),
|
||||
}, nil
|
||||
}
|
||||
for k, v := range keyMeta {
|
||||
response[k] = v
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// executeMongoQuery — MongoDB query with pagination
|
||||
|
||||
@@ -396,25 +396,41 @@ func RedisInfo(c *gin.Context) {
|
||||
defer client.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
info, err := client.Info(ctx, "server", "memory", "clients", "stats", "keyspace").Result()
|
||||
info, err := client.Info(ctx).Result()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse info into structured map
|
||||
// Parse Redis INFO into both a flat map and section groups.
|
||||
result := make(map[string]interface{})
|
||||
grouped := make(map[string]map[string]interface{})
|
||||
currentSection := "General"
|
||||
sections := strings.Split(info, "\n# ")
|
||||
for _, section := range sections {
|
||||
lines := strings.Split(section, "\n")
|
||||
for _, line := range lines {
|
||||
for lineIndex, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
currentSection = strings.TrimSpace(strings.TrimPrefix(line, "#"))
|
||||
continue
|
||||
}
|
||||
if lineIndex == 0 && !strings.Contains(line, ":") {
|
||||
currentSection = strings.TrimSpace(line)
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
result[key] = value
|
||||
if _, ok := grouped[currentSection]; !ok {
|
||||
grouped[currentSection] = make(map[string]interface{})
|
||||
}
|
||||
grouped[currentSection][key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,8 +438,15 @@ func RedisInfo(c *gin.Context) {
|
||||
// Add db_size
|
||||
dbSize, _ := client.DBSize(ctx).Result()
|
||||
result["db_size"] = dbSize
|
||||
if _, ok := grouped["Keyspace"]; !ok {
|
||||
grouped["Keyspace"] = make(map[string]interface{})
|
||||
}
|
||||
grouped["Keyspace"]["db_size"] = dbSize
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"info": result,
|
||||
"sections": grouped,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/redis/:id/dbsize
|
||||
|
||||
Reference in new issue
Block a user