From 92a0e24db71d033bf489871a8ed4e7eb60469708 Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Wed, 25 Mar 2026 20:00:34 +0800 Subject: [PATCH] refactor(frpc): restructure instance management and config handling - Move instance-related structs and functions to config.go - Remove serverAddr, serverPort, and authMethod from database schema - Implement new config parsing and encoding with nested key support - Update service management to use instanceID instead of username/name - Add GetServiceNameByInstanceID helper function - Update API documentation for auth.method field change --- README.md | 1 + config.go | 276 ++++++++++++++++++++++++++++++++++++++++++++++++ database.go | 43 +++++--- docs/api.md | 10 +- frpAct.go | 246 +++++++++++++++--------------------------- frpcProxyAct.go | 60 ----------- go.mod | 2 +- os.go | 134 ++++++++++++++++------- 8 files changed, 491 insertions(+), 281 deletions(-) diff --git a/README.md b/README.md index a209d1c..15bbff8 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ For detailed API documentation, please see [docs/api.md](docs/api.md) - [ ] Add frpc instance running status management API - [ ] Add frpc instance log display API - [ ] Fix random database lock when processing logs +- [ ] Add frpc createdBy storage and display ## License diff --git a/config.go b/config.go index d83af94..772c7dd 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,10 @@ import ( "errors" "fmt" "os" + "strconv" + "strings" + + "github.com/BurntSushi/toml" ) type Config struct { @@ -15,6 +19,40 @@ type Config struct { Debug bool `json:"debug"` } +type InstanceInfo struct { + Name string `json:"name"` + ServerAddr string `json:"serverAddr"` + ServerPort string `json:"serverPort"` + AuthMethod string `json:"auth_method"` + RunUser string `json:"runUser"` + Additional map[string]interface{} `json:"additionalProperties"` +} + +type FrpcProxyInfo struct { + Name string `json:"name"` + Type string `json:"type"` + LocalIP string `json:"local_ip"` + LocalPort string `json:"local_port"` + RemotePort string `json:"remote_port"` +} + +type CreateInstanceRequest struct { + InstanceInfo InstanceInfo `json:"instanceInfo"` + BootAtStart bool `json:"bootAtStart"` + RunUser string `json:"runUser"` + Additional map[string]interface{} `json:"additionalProperties"` +} + +type CreateProxyRequest struct { + InstanceID string `json:"instanceID"` + ProxyInfo FrpcProxyInfo `json:"proxyInfo"` +} + +type FrpcConfig struct { + Global map[string]interface{} `toml:"-"` + Proxies []map[string]interface{} `toml:"proxies"` +} + var globalConfig *Config func LoadConfig(configPath string) (*Config, error) { @@ -72,3 +110,241 @@ func SaveConfig(configPath string, config *Config) error { globalConfig = config return nil } + +func decodeFrpcConfig(configContent string) (FrpcConfig, error) { + var config FrpcConfig + + var rawMap map[string]interface{} + meta, err := toml.Decode(configContent, &rawMap) + if err != nil { + return config, fmt.Errorf("failed to parse config: %w", err) + } + + config.Global = make(map[string]interface{}) + config.Proxies = make([]map[string]interface{}, 0) + + getNestedValue := func(m map[string]interface{}, key string) (interface{}, bool) { + parts := strings.Split(key, ".") + current := m + for i, part := range parts { + if i == len(parts)-1 { + val, ok := current[part] + return val, ok + } + next, ok := current[part].(map[string]interface{}) + if !ok { + return nil, false + } + current = next + } + return nil, false + } + + for _, key := range meta.Keys() { + keyStr := key.String() + if keyStr == "proxies" { + if proxies, ok := rawMap["proxies"].([]map[string]interface{}); ok { + config.Proxies = proxies + } else if proxies, ok := rawMap["proxies"].([]interface{}); ok { + for _, p := range proxies { + if pm, ok := p.(map[string]interface{}); ok { + config.Proxies = append(config.Proxies, pm) + } + } + } + } else if !strings.HasPrefix(keyStr, "proxies.") { + if val, ok := getNestedValue(rawMap, keyStr); ok { + config.Global[keyStr] = val + } + } + } + + return config, nil +} + +func encodeFrpcConfig(config FrpcConfig) (string, error) { + var buf strings.Builder + + if len(config.Global) > 0 { + for key, value := range config.Global { + switch v := value.(type) { + case string: + buf.WriteString(fmt.Sprintf("%s = %q\n", key, v)) + case int: + buf.WriteString(fmt.Sprintf("%s = %d\n", key, v)) + case int64: + buf.WriteString(fmt.Sprintf("%s = %d\n", key, v)) + case float64: + buf.WriteString(fmt.Sprintf("%s = %v\n", key, v)) + case bool: + buf.WriteString(fmt.Sprintf("%s = %v\n", key, v)) + default: + data, err := json.Marshal(v) + if err != nil { + return "", fmt.Errorf("failed to marshal value for key %s: %w", key, err) + } + buf.WriteString(fmt.Sprintf("%s = %s\n", key, string(data))) + } + } + buf.WriteString("\n") + } + + if len(config.Proxies) > 0 { + for _, proxy := range config.Proxies { + buf.WriteString("[[proxies]]\n") + for key, value := range proxy { + switch v := value.(type) { + case string: + buf.WriteString(fmt.Sprintf("%s = %q\n", key, v)) + case int: + buf.WriteString(fmt.Sprintf("%s = %d\n", key, v)) + case int64: + buf.WriteString(fmt.Sprintf("%s = %d\n", key, v)) + case float64: + buf.WriteString(fmt.Sprintf("%s = %v\n", key, v)) + case bool: + buf.WriteString(fmt.Sprintf("%s = %v\n", key, v)) + default: + data, err := json.Marshal(v) + if err != nil { + return "", fmt.Errorf("failed to marshal value for key %s: %w", key, err) + } + buf.WriteString(fmt.Sprintf("%s = %s\n", key, string(data))) + } + } + buf.WriteString("\n") + } + } + + return strings.TrimSuffix(buf.String(), "\n"), nil +} + +func generateFrpcConfig(info InstanceInfo) string { + config := FrpcConfig{ + Global: make(map[string]interface{}), + } + + config.Global["serverAddr"] = info.ServerAddr + config.Global["serverPort"] = info.ServerPort + config.Global["auth.method"] = info.AuthMethod + + for key, value := range info.Additional { + config.Global[key] = value + } + + result, err := encodeFrpcConfig(config) + if err != nil { + return "" + } + + return result +} + +func addFrpcProxy(configContent string, info FrpcProxyInfo) (string, error) { + config, err := decodeFrpcConfig(configContent) + if err != nil { + return "", fmt.Errorf("failed to parse config: %w", err) + } + + proxy := map[string]interface{}{ + "name": info.Name, + "type": info.Type, + "localIP": info.LocalIP, + "localPort": info.LocalPort, + "remotePort": info.RemotePort, + } + + config.Proxies = append(config.Proxies, proxy) + + result, err := encodeFrpcConfig(config) + if err != nil { + return "", fmt.Errorf("failed to write config: %w", err) + } + + return result, nil +} + +func removeFrpcProxy(configContent string, proxyName string) (string, error) { + config, err := decodeFrpcConfig(configContent) + if err != nil { + return "", fmt.Errorf("failed to parse config: %w", err) + } + + var found bool + var newProxies []map[string]interface{} + for _, proxy := range config.Proxies { + if name, ok := proxy["name"].(string); ok && name == proxyName { + found = true + continue + } + newProxies = append(newProxies, proxy) + } + + if !found { + return "", fmt.Errorf("proxy %s not found", proxyName) + } + + config.Proxies = newProxies + + result, err := encodeFrpcConfig(config) + if err != nil { + return "", fmt.Errorf("failed to write config: %w", err) + } + + return result, nil +} + +func getKeyText(configPath, key, section string) (value string, err error) { + configContent, err := os.ReadFile(configPath) + if err != nil { + return "", fmt.Errorf("failed to read config file: %w", err) + } + + config, err := decodeFrpcConfig(string(configContent)) + if err != nil { + return "", fmt.Errorf("failed to parse config: %w", err) + } + + if config.Global == nil { + return "", fmt.Errorf("config file has no global fields") + } + + if v, ok := config.Global[key]; ok { + value = fmt.Sprintf("%v", v) + } + + return value, nil +} + +func setKeyText(configPath, key, section string, value string) error { + configContent, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + config, err := decodeFrpcConfig(string(configContent)) + if err != nil { + return fmt.Errorf("failed to parse config: %w", err) + } + + if config.Global == nil { + config.Global = make(map[string]interface{}) + } + + if intVal, err := strconv.Atoi(value); err == nil { + config.Global[key] = intVal + } else { + config.Global[key] = value + } + + result, err := encodeFrpcConfig(config) + if err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + + if err := os.WriteFile(configPath, []byte(result), 0644); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + return nil +} diff --git a/database.go b/database.go index 694abd1..8c11476 100644 --- a/database.go +++ b/database.go @@ -31,9 +31,6 @@ type FrpcInstance struct { ID int UserID int Name string - ServerAddr string - ServerPort string - AuthMethod string BootAtStart bool RunUser string ConfigPath string @@ -275,9 +272,6 @@ func InitFrpcDatabase(dbPath string) error { id INTEGER PRIMARY KEY AUTOINCREMENT, userID INTEGER NOT NULL, name TEXT NOT NULL, - serverAddr TEXT NOT NULL, - serverPort TEXT NOT NULL, - auth_method TEXT NOT NULL, bootAtStart INTEGER NOT NULL DEFAULT 0, runUser TEXT NOT NULL DEFAULT 'root', configPath TEXT NOT NULL, @@ -353,8 +347,8 @@ func DBQuerySpecificUser(userID int) (User, error) { // Query user by ID } func DBAddFrpcInstance(instance FrpcInstance) error { - _, err := frpcDB.Exec("INSERT INTO frpcInstances (userID, name, serverAddr, serverPort, auth_method, bootAtStart, runUser, configPath, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - instance.UserID, instance.Name, instance.ServerAddr, instance.ServerPort, instance.AuthMethod, instance.BootAtStart, instance.RunUser, instance.ConfigPath, time.Now().Format(time.RFC3339)) + _, err := frpcDB.Exec("INSERT INTO frpcInstances (userID, name, bootAtStart, runUser, configPath, createdAt) VALUES (?, ?, ?, ?, ?, ?)", + instance.UserID, instance.Name, instance.BootAtStart, instance.RunUser, instance.ConfigPath, time.Now().Format(time.RFC3339)) if err != nil { return fmt.Errorf("failed to insert frpc instance: %w", err) } @@ -364,8 +358,8 @@ func DBAddFrpcInstance(instance FrpcInstance) error { func DBQueryFrpcInstanceByID(instanceID int) (FrpcInstance, error) { var instance FrpcInstance var createdAtStr string - err := frpcDB.QueryRow("SELECT id, userID, name, serverAddr, serverPort, auth_method, bootAtStart, runUser, configPath, createdAt FROM frpcInstances WHERE id = ?", instanceID).Scan( - &instance.ID, &instance.UserID, &instance.Name, &instance.ServerAddr, &instance.ServerPort, &instance.AuthMethod, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr) + err := frpcDB.QueryRow("SELECT id, userID, name, bootAtStart, runUser, configPath, createdAt FROM frpcInstances WHERE id = ?", instanceID).Scan( + &instance.ID, &instance.UserID, &instance.Name, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr) if err != nil { return instance, fmt.Errorf("failed to query frpc instance: %w", err) } @@ -376,8 +370,8 @@ func DBQueryFrpcInstanceByID(instanceID int) (FrpcInstance, error) { func DBQueryFrpcInstance(userID int, instanceName string) (FrpcInstance, error) { var instance FrpcInstance var createdAtStr string - err := frpcDB.QueryRow("SELECT id, userID, name, serverAddr, serverPort, auth_method, bootAtStart, runUser, configPath, createdAt FROM frpcInstances WHERE userID = ? AND name = ?", userID, instanceName).Scan( - &instance.ID, &instance.UserID, &instance.Name, &instance.ServerAddr, &instance.ServerPort, &instance.AuthMethod, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr) + err := frpcDB.QueryRow("SELECT id, userID, name, bootAtStart, runUser, configPath, createdAt FROM frpcInstances WHERE userID = ? AND name = ?", userID, instanceName).Scan( + &instance.ID, &instance.UserID, &instance.Name, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr) if err != nil { return instance, fmt.Errorf("failed to query frpc instance: %w", err) } @@ -394,8 +388,8 @@ func DBRemoveFrpcInstanceByID(instanceID int) error { } func DBUpdateFrpcInstance(instance FrpcInstance) error { - _, err := frpcDB.Exec("UPDATE frpcInstances SET name = ?, serverAddr = ?, serverPort = ?, auth_method = ?, bootAtStart = ?, runUser = ?, configPath = ? WHERE id = ?", - instance.Name, instance.ServerAddr, instance.ServerPort, instance.AuthMethod, instance.BootAtStart, instance.RunUser, instance.ConfigPath, instance.ID) + _, err := frpcDB.Exec("UPDATE frpcInstances SET bootAtStart = ?, runUser = ?, configPath = ? WHERE id = ?", + instance.BootAtStart, instance.RunUser, instance.ConfigPath, instance.ID) if err != nil { return fmt.Errorf("failed to update frpc instance: %w", err) } @@ -404,8 +398,7 @@ func DBUpdateFrpcInstance(instance FrpcInstance) error { func DBListFrpcInstances() ([]FrpcInstance, error) { rows, err := frpcDB.Query(` - SELECT fi.id, fi.userID, fi.name, fi.serverAddr, fi.serverPort, fi.auth_method, - fi.bootAtStart, fi.runUser, fi.configPath, fi.createdAt, u.username + SELECT fi.id, fi.userID, fi.name, fi.bootAtStart, fi.runUser, fi.configPath, fi.createdAt, u.username FROM frpcInstances fi JOIN userLogin u ON fi.userID = u.userID `) @@ -418,7 +411,7 @@ func DBListFrpcInstances() ([]FrpcInstance, error) { for rows.Next() { var instance FrpcInstance var createdAtStr string - if err := rows.Scan(&instance.ID, &instance.UserID, &instance.Name, &instance.ServerAddr, &instance.ServerPort, &instance.AuthMethod, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr, &instance.CreatedBy); err != nil { + if err := rows.Scan(&instance.ID, &instance.UserID, &instance.Name, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr, &instance.CreatedBy); err != nil { return nil, fmt.Errorf("failed to scan frpc instance: %w", err) } instance.CreatedAt, _ = time.Parse(time.RFC3339, createdAtStr) @@ -431,3 +424,19 @@ func DBListFrpcInstances() ([]FrpcInstance, error) { return instances, nil } + +func GetServiceNameByInstanceID(instanceID int) (string, error) { + instance, err := DBQueryFrpcInstanceByID(instanceID) + if err != nil { + return "", fmt.Errorf("failed to query frpc instance: %w", err) + } + + user, err := GetUserByID(instance.UserID) + if err != nil { + return "", fmt.Errorf("failed to get user info: %w", err) + } + + serviceName := fmt.Sprintf("superfrpc_%s_%s", user.Username, instance.Name) + postLog.Debug(fmt.Sprintf("[GetServiceNameByInstanceID] instanceID: %d, serviceName: %s", instanceID, serviceName)) + return serviceName, nil +} diff --git a/docs/api.md b/docs/api.md index e80846d..db8f255 100644 --- a/docs/api.md +++ b/docs/api.md @@ -552,8 +552,8 @@ Modify fields in the `[common]` section of the frpc configuration file. Only `[c "modifiedData": { "server_addr": "192.168.1.1", "server_port": "7000", - "auth_method": "token", - "auth_token": "my_secret_token" + "auth.method": "token", + "auth.token": "my_secret_token" } } ``` @@ -1149,7 +1149,7 @@ X-Timestamp: 1704067200000 "name": "my_frpc", "serverAddr": "127.0.0.1", "serverPort": "7000", - "auth_method": "token", + "auth.method": "token", "bootAtStart": true, "runUser": "root", "configPath": "./configs/superfrpc_user_my_frpc.toml", @@ -1185,14 +1185,14 @@ X-Timestamp: 1704067200000 | name | string | Instance name | | serverAddr | string | frps server address (admin/superuser only) | | serverPort | string | frps server port (admin/superuser only) | -| auth_method | string | Authentication method (admin/superuser only) | +| auth.method | string | Authentication method (admin/superuser only) | | bootAtStart | bool | Auto-start on system boot | | runUser | string | User to run the frpc instance as | | configPath | string | Path to the configuration file | | createdAt | string | Instance creation time (ISO 8601 format) | | createdBy | string | Username of the user who created this instance | -> Note: Visitor users do not see sensitive information (serverAddr, serverPort, auth_method). +> Note: Visitor users do not see sensitive information (serverAddr, serverPort, auth.method). --- diff --git a/frpAct.go b/frpAct.go index 7b370f4..ae834d1 100644 --- a/frpAct.go +++ b/frpAct.go @@ -9,43 +9,10 @@ import ( "os" "path/filepath" "strconv" - "strings" "super-frpc/postLog" "time" - - "github.com/BurntSushi/toml" ) -type InstanceInfo struct { - Name string `json:"name"` - ServerAddr string `json:"serverAddr"` - ServerPort string `json:"serverPort"` - AuthMethod string `json:"auth_method"` - RunUser string `json:"runUser"` - Additional map[string]interface{} `json:"additionalProperties"` -} - -type FrpcProxyInfo struct { - Name string `json:"name"` - Type string `json:"type"` - LocalIP string `json:"local_ip"` - LocalPort string `json:"local_port"` - RemotePort string `json:"remote_port"` -} - -type CreateInstanceRequest struct { - InstanceInfo InstanceInfo `json:"instanceInfo"` - BootAtStart bool `json:"bootAtStart"` - RunUser string `json:"runUser"` - Additional map[string]interface{} `json:"additionalProperties"` -} - -type FrpcConfig struct { - Common map[string]interface{} `toml:"common"` - Proxies []map[string]interface{} `toml:"proxies"` - Additional map[string]interface{} `toml:"-"` -} - var frpcDB *sql.DB func CloseFrpcDatabase() error { @@ -165,9 +132,6 @@ func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) { instance := FrpcInstance{ UserID: userID, Name: req.InstanceInfo.Name, - ServerAddr: req.InstanceInfo.ServerAddr, - ServerPort: req.InstanceInfo.ServerPort, - AuthMethod: req.InstanceInfo.AuthMethod, BootAtStart: req.BootAtStart, RunUser: runUser, ConfigPath: configPath, @@ -180,16 +144,24 @@ func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - if err := createBootService(user.Username, req.InstanceInfo.Name, configPath, runUser); err != nil { - frpcDB.Exec("DELETE FROM frpcInstances WHERE userID = ? AND name = ?", userID, req.InstanceInfo.Name) - os.Remove(configPath) - postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to create boot service for instance %s: %v", req.InstanceInfo.Name, err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to create boot service") - return - } + createdInstance, err := DBQueryFrpcInstance(userID, req.InstanceInfo.Name) + if err != nil { + os.Remove(configPath) + postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to query created instance: %v", err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to query created instance") + return + } + + if err := createBootService(createdInstance.ID); err != nil { + frpcDB.Exec("DELETE FROM frpcInstances WHERE userID = ? AND name = ?", userID, req.InstanceInfo.Name) + os.Remove(configPath) + postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to create boot service for instance %s: %v", req.InstanceInfo.Name, err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to create boot service") + return + } if req.BootAtStart { - if err := setBootAtStart(user.Username, req.InstanceInfo.Name); err != nil { + if err := setBootAtStart(createdInstance.ID); err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to set boot at start for instance %s: %v", req.InstanceInfo.Name, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to set boot at start") return @@ -250,15 +222,7 @@ func DeleteInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := GetUserByID(userID) - if err != nil { - postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to get user info: %v", err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info") - return - } - - var instance FrpcInstance - instance, err = DBQueryFrpcInstanceByID(instanceID) + instance, err := DBQueryFrpcInstanceByID(instanceID) if err == sql.ErrNoRows { SendErrorResponse(w, http.StatusNotFound, "Instance not found") postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] User %d tried to delete a not existed instance: %d", userID, instanceID)) @@ -270,7 +234,7 @@ func DeleteInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - if err := removeBootService(user.Username, instance.Name); err != nil { + if err := removeBootService(instanceID); err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to remove boot service for instance %s: %v", instance.Name, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to remove boot service") return @@ -395,40 +359,40 @@ func ModifyInstanceHandler(w http.ResponseWriter, r *http.Request) { func handleConfigFileModify(w http.ResponseWriter, instance FrpcInstance, modifiedData map[string]interface{}, username string) { configPath := instance.ConfigPath - configContent, err := os.ReadFile(configPath) - if err != nil { - postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to read config file %s: %v", configPath, err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to read config file") - return - } + for key, value := range modifiedData { + configKey := key + if key == "auth_method" { + configKey = "auth.method" + } + if key == "auth_token" { + configKey = "auth.token" + } - updatedConfig, err := updateCommonSection(string(configContent), modifiedData) - if err != nil { - postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to update common section: %v", err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to update config file") - return - } - - if err := os.WriteFile(configPath, []byte(updatedConfig), 0644); err != nil { - postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to write config file %s: %v", configPath, err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to write config file") - return - } - - if v, ok := modifiedData["server_addr"].(string); ok && v != "" { - instance.ServerAddr = v - } - if v, ok := modifiedData["server_port"].(string); ok && v != "" { - instance.ServerPort = v - } - if v, ok := modifiedData["auth_method"].(string); ok && v != "" { - instance.AuthMethod = v - } - - if err := DBUpdateFrpcInstance(instance); err != nil { - postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to update instance in database: %v", err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to update instance in database") - return + var configValue string + if key == "serverPort" { + switch v := value.(type) { + case float64: + configValue = strconv.Itoa(int(v)) + case int: + configValue = strconv.Itoa(v) + case string: + var intVal int + if _, err := fmt.Sscanf(v, "%d", &intVal); err == nil { + configValue = strconv.Itoa(intVal) + } else { + configValue = v + } + default: + configValue = fmt.Sprintf("%v", value) + } + } else { + configValue = fmt.Sprintf("%v", value) + } + if err := setKeyText(configPath, configKey, "", configValue); err != nil { + postLog.Error(fmt.Sprintf("[handleConfigFileModify] Failed to set key %s: %v", key, err)) + SendErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("Failed to set key %s", key)) + return + } } SendSuccessResponse(w, "Config file modified successfully", map[string]interface{}{ @@ -439,24 +403,6 @@ func handleConfigFileModify(w http.ResponseWriter, instance FrpcInstance, modifi postLog.Info(fmt.Sprintf("[handleConfigFileModify] Config file for instance %s modified successfully", instance.Name)) } -func updateCommonSection(configContent string, modifiedData map[string]interface{}) (string, error) { - var config FrpcConfig - if _, err := toml.Decode(configContent, &config); err != nil { - return "", fmt.Errorf("failed to parse config: %w", err) - } - - for key, value := range modifiedData { - config.Common[key] = value - } - - var buf strings.Builder - if err := toml.NewEncoder(&buf).Encode(config); err != nil { - return "", fmt.Errorf("failed to write config: %w", err) - } - - return buf.String(), nil -} - func handleSystemConfigModify(w http.ResponseWriter, r *http.Request, instance FrpcInstance, modifiedData map[string]interface{}, user *User) { newName := instance.Name newRunUser := instance.RunUser @@ -512,12 +458,12 @@ func handleSystemConfigModify(w http.ResponseWriter, r *http.Request, instance F } if newBootAtStart { - if err := removeBootAtStart(user.Username, instance.Name); err != nil { + if err := removeBootAtStart(instance.ID); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to remove boot at start: %v", err)) bootServiceError = fmt.Sprintf("Failed to remove boot at start: %v", err) } instance.Name = newName - if err := setBootAtStart(user.Username, newName); err != nil { + if err := setBootAtStart(instance.ID); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to set boot at start: %v", err)) if bootServiceError != "" { bootServiceError += "; " @@ -525,7 +471,7 @@ func handleSystemConfigModify(w http.ResponseWriter, r *http.Request, instance F bootServiceError += fmt.Sprintf("Failed to set boot at start: %v", err) } } else { - if err := removeBootAtStart(user.Username, instance.Name); err != nil { + if err := removeBootAtStart(instance.ID); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to remove boot at start: %v", err)) bootServiceError = fmt.Sprintf("Failed to remove boot at start: %v", err) } @@ -546,30 +492,9 @@ func handleSystemConfigModify(w http.ResponseWriter, r *http.Request, instance F SendSuccessResponse(w, "System config modified successfully", data) } -func generateFrpcConfig(info InstanceInfo) string { - config := FrpcConfig{ - Common: make(map[string]interface{}), - } - - config.Common["server_addr"] = info.ServerAddr - config.Common["server_port"] = info.ServerPort - config.Common["auth_method"] = info.AuthMethod - - for key, value := range info.Additional { - config.Common[key] = value - } - - var buf strings.Builder - if err := toml.NewEncoder(&buf).Encode(config); err != nil { - return "" - } - - return buf.String() -} - func GetUserInstances(userID int) ([]FrpcInstance, error) { rows, err := frpcDB.Query(` - SELECT id, userID, name, serverAddr, serverPort, auth_method, bootAtStart, runUser, configPath, createdAt + SELECT id, userID, name, bootAtStart, runUser, configPath, createdAt FROM frpcInstances WHERE userID = ? `, userID) if err != nil { @@ -582,8 +507,7 @@ func GetUserInstances(userID int) ([]FrpcInstance, error) { var instance FrpcInstance var createdAtStr string if err := rows.Scan( - &instance.ID, &instance.UserID, &instance.Name, &instance.ServerAddr, &instance.ServerPort, - &instance.AuthMethod, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr, + &instance.ID, &instance.UserID, &instance.Name, &instance.BootAtStart, &instance.RunUser, &instance.ConfigPath, &createdAtStr, ); err != nil { return nil, err } @@ -642,9 +566,15 @@ func ListInstancesHandler(w http.ResponseWriter, r *http.Request) { } if userType == "admin" || userType == "superuser" { - instanceData["serverAddr"] = inst.ServerAddr - instanceData["serverPort"] = inst.ServerPort - instanceData["auth_method"] = inst.AuthMethod + serverAddr, err := getKeyText(inst.ConfigPath, "serverAddr", "") + serverPort, err := getKeyText(inst.ConfigPath, "serverPort", "") + authMethod, err := getKeyText(inst.ConfigPath, "auth.method", "") + if err != nil { + postLog.Error(fmt.Sprintf("[ListInstancesHandler] Failed to read config for instance %d: %v", inst.ID, err)) + } + instanceData["serverAddr"] = serverAddr + instanceData["serverPort"] = serverPort + instanceData["auth_method"] = authMethod } instanceList[i] = instanceData @@ -725,16 +655,14 @@ func StartInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := GetUserByID(instance.UserID) + initType := GetInitSystem() + serviceName, err := GetServiceNameByInstanceID(instanceID) if err != nil { - postLog.Error(fmt.Sprintf("[StartInstanceHandler] Failed to get user info: %v", err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info") + postLog.Error(fmt.Sprintf("[StartInstanceHandler] Failed to get service name: %v", err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to get service name") return } - initType := GetInitSystem() - serviceName := fmt.Sprintf("superfrpc_%s_%s", user.Username, instance.Name) - switch initType { case "windows": if err := StartWindowsService(serviceName); err != nil { @@ -841,16 +769,14 @@ func StopInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := GetUserByID(instance.UserID) + initType := GetInitSystem() + serviceName, err := GetServiceNameByInstanceID(instanceID) if err != nil { - postLog.Error(fmt.Sprintf("[StopInstanceHandler] Failed to get user info: %v", err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info") + postLog.Error(fmt.Sprintf("[StopInstanceHandler] Failed to get service name: %v", err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to get service name") return } - initType := GetInitSystem() - serviceName := fmt.Sprintf("superfrpc_%s_%s", user.Username, instance.Name) - switch initType { case "windows": if err := StopWindowsService(serviceName); err != nil { @@ -957,16 +883,14 @@ func RestartInstanceHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := GetUserByID(instance.UserID) + initType := GetInitSystem() + serviceName, err := GetServiceNameByInstanceID(instanceID) if err != nil { - postLog.Error(fmt.Sprintf("[RestartInstanceHandler] Failed to get user info: %v", err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info") + postLog.Error(fmt.Sprintf("[RestartInstanceHandler] Failed to get service name: %v", err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to get service name") return } - initType := GetInitSystem() - serviceName := fmt.Sprintf("superfrpc_%s_%s", user.Username, instance.Name) - switch initType { case "windows": if err := RestartWindowsService(serviceName); err != nil { @@ -1047,16 +971,14 @@ func GetInstanceStatusHandler(w http.ResponseWriter, r *http.Request) { return } - user, err := GetUserByID(instance.UserID) - if err != nil { - postLog.Error(fmt.Sprintf("[GetInstanceStatusHandler] Failed to get user info: %v", err)) - SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info") - return - } - initType := GetInitSystem() - serviceName := fmt.Sprintf("superfrpc_%s_%s", user.Username, instance.Name) + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + postLog.Error(fmt.Sprintf("[GetInstanceStatusHandler] Failed to get service name: %v", err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to get service name") + return + } responseData := map[string]interface{}{ "name": instance.Name, @@ -1065,7 +987,7 @@ func GetInstanceStatusHandler(w http.ResponseWriter, r *http.Request) { "isRunning": false, } - isRunning := IsInstanceRunning(instance.Name) + isRunning := IsInstanceRunning(instanceID) responseData["isRunning"] = isRunning SendSuccessResponse(w, "Instance status retrieved successfully", responseData) diff --git a/frpcProxyAct.go b/frpcProxyAct.go index 3af7054..f223e96 100644 --- a/frpcProxyAct.go +++ b/frpcProxyAct.go @@ -7,17 +7,11 @@ import ( "net/http" "os" "strconv" - "strings" "super-frpc/postLog" "github.com/BurntSushi/toml" ) -type CreateProxyRequest struct { - InstanceID string `json:"instanceID"` - ProxyInfo FrpcProxyInfo `json:"proxyInfo"` -} - func CreateProxyHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method") @@ -129,30 +123,6 @@ func CreateProxyHandler(w http.ResponseWriter, r *http.Request) { postLog.Info(fmt.Sprintf("[CreateProxyHandler] Proxy %s created successfully for instance %d", proxyInfo.Name, instance.ID)) } -func addFrpcProxy(configContent string, info FrpcProxyInfo) (string, error) { - var config FrpcConfig - if _, err := toml.Decode(configContent, &config); err != nil { - return "", fmt.Errorf("failed to parse config: %w", err) - } - - proxy := map[string]interface{}{ - "name": info.Name, - "type": info.Type, - "localIP": info.LocalIP, - "localPort": info.LocalPort, - "remotePort": info.RemotePort, - } - - config.Proxies = append(config.Proxies, proxy) - - var buf strings.Builder - if err := toml.NewEncoder(&buf).Encode(config); err != nil { - return "", fmt.Errorf("failed to write config: %w", err) - } - - return buf.String(), nil -} - func DeleteProxyHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method") @@ -249,36 +219,6 @@ func DeleteProxyHandler(w http.ResponseWriter, r *http.Request) { postLog.Info(fmt.Sprintf("[DeleteProxyHandler] Proxy %s deleted successfully from instance %d", proxyName, instance.ID)) } -func removeFrpcProxy(configContent string, proxyName string) (string, error) { - var config FrpcConfig - if _, err := toml.Decode(configContent, &config); err != nil { - return "", fmt.Errorf("failed to parse config: %w", err) - } - - var found bool - var newProxies []map[string]interface{} - for _, proxy := range config.Proxies { - if name, ok := proxy["name"].(string); ok && name == proxyName { - found = true - continue - } - newProxies = append(newProxies, proxy) - } - - if !found { - return "", fmt.Errorf("proxy %s not found", proxyName) - } - - config.Proxies = newProxies - - var buf strings.Builder - if err := toml.NewEncoder(&buf).Encode(config); err != nil { - return "", fmt.Errorf("failed to write config: %w", err) - } - - return buf.String(), nil -} - func ListProxiesHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method") diff --git a/go.mod b/go.mod index 844f0d5..d18f013 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.24.0 require ( github.com/BurntSushi/toml v1.4.0 github.com/gorilla/websocket v1.5.3 - golang.org/x/sys v0.37.0 modernc.org/sqlite v1.46.1 ) @@ -16,6 +15,7 @@ require ( github.com/ncruces/go-strftime v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/sys v0.37.0 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/os.go b/os.go index 4bff9de..28b45ac 100644 --- a/os.go +++ b/os.go @@ -44,28 +44,47 @@ func GetInitSystem() string { return "unknown" } -func createBootService(username, instanceName, configPath, runUser string) error { +func createBootService(instanceID int) error { + instance, err := DBQueryFrpcInstanceByID(instanceID) + if err != nil { + return fmt.Errorf("failed to query frpc instance: %w", err) + } + initType := GetInitSystem() switch initType { case "systemd": - return createSystemdService(username, instanceName, configPath, runUser) + return createSystemdService(instanceID, instance.ConfigPath, instance.RunUser) case "init.d": - return createInitDService(username, instanceName, configPath, runUser) + return createInitDService(instanceID, instance.ConfigPath, instance.RunUser) case "windows": - return createWindowsBootService(username, instanceName, configPath) + return createWindowsBootService(instanceID, instance.ConfigPath) default: return fmt.Errorf("unsupported init system: %s", initType) } } -func createSystemdService(username, instanceName, configPath, runUser string) error { +func createSystemdService(instanceID int, configPath, runUser string) error { frpcPath, err := GetFrpcPath() if err != nil { return err } - serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName) + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + return err + } + + instance, err := DBQueryFrpcInstanceByID(instanceID) + if err != nil { + return fmt.Errorf("failed to query frpc instance: %w", err) + } + + user, err := GetUserByID(instance.UserID) + if err != nil { + return fmt.Errorf("failed to get user info: %w", err) + } + serviceContent := fmt.Sprintf(`[Unit] Description=superfrpc_%s_%s After=network.target @@ -79,7 +98,7 @@ RestartSec=5 [Install] WantedBy=multi-user.target -`, username, instanceName, frpcPath, configPath, runUser) +`, user.Username, instance.Name, frpcPath, configPath, runUser) servicePath := filepath.Join("/etc/systemd/system", serviceName+".service") if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil { @@ -100,13 +119,26 @@ WantedBy=multi-user.target return nil } -func createInitDService(username, instanceName, configPath, runUser string) error { +func createInitDService(instanceID int, configPath, runUser string) error { frpcPath, err := GetFrpcPath() if err != nil { return err } - serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName) + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + return err + } + + instance, err := DBQueryFrpcInstanceByID(instanceID) + if err != nil { + return fmt.Errorf("failed to query frpc instance: %w", err) + } + + user, err := GetUserByID(instance.UserID) + if err != nil { + return fmt.Errorf("failed to get user info: %w", err) + } var serviceContent string runUserArg := "" @@ -162,7 +194,7 @@ case "$1" in esac exit 0 -`, serviceName, username, instanceName, serviceName, frpcPath, configPath, runUserArg) +`, serviceName, user.Username, instance.Name, serviceName, frpcPath, configPath, runUserArg) } else { serviceContent = fmt.Sprintf(`#!/bin/sh @@ -213,7 +245,7 @@ case "$1" in esac exit 0 -`, serviceName, username, instanceName, serviceName, frpcPath, configPath) +`, serviceName, user.Username, instance.Name, serviceName, frpcPath, configPath) } servicePath := filepath.Join("/etc/init.d", serviceName) @@ -230,13 +262,17 @@ exit 0 return nil } -func createWindowsBootService(username, instanceName, configPath string) error { +func createWindowsBootService(instanceID int, configPath string) error { frpcPath, err := GetFrpcPath() if err != nil { return err } - serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName) + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + return err + } + command := fmt.Sprintf("\"%s\" -c \"%s\"", frpcPath, configPath) cmd := exec.Command("sc", "create", serviceName, "binPath=", command, "start=", "auto") @@ -249,9 +285,12 @@ func createWindowsBootService(username, instanceName, configPath string) error { return nil } -func setBootAtStart(username, instanceName string) error { +func setBootAtStart(instanceID int) error { initType := GetInitSystem() - serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName) + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + return err + } switch initType { case "windows": @@ -280,11 +319,14 @@ func setBootAtStart(username, instanceName string) error { } } -func removeBootAtStart(username, instanceName string) error { +func removeBootAtStart(instanceID int) error { initType := GetInitSystem() - serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName) + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + return err + } - switch initType{ + switch initType { case "windows": cmd := exec.Command("sc", "config", serviceName, "start=", "disabled") output, err := cmd.CombinedOutput() @@ -311,10 +353,12 @@ func removeBootAtStart(username, instanceName string) error { } } -func removeBootService(username, instanceName string) error { +func removeBootService(instanceID int) error { initType := GetInitSystem() - - serviceName := fmt.Sprintf("superfrpc_%s_%s", username, instanceName) + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + return err + } switch initType { case "systemd": @@ -347,11 +391,23 @@ func removeBootService(username, instanceName string) error { return nil } -func IsInstanceRunning(instanceName string) bool { +func IsInstanceRunning(instanceID int) bool { initType := GetInitSystem() + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + postLog.Error(fmt.Sprintf("[IsInstanceRunning] Failed to get service name: %v", err)) + return false + } + + instance, err := DBQueryFrpcInstanceByID(instanceID) + if err != nil { + postLog.Error(fmt.Sprintf("[IsInstanceRunning] Failed to query instance: %v", err)) + return false + } + switch initType { case "windows": - cmd := exec.Command("sc", "query", instanceName) + cmd := exec.Command("sc", "query", serviceName) output, err := cmd.CombinedOutput() postLog.Debug(fmt.Sprintf("[IsInstanceRunning] Sc query output: %s", output)) @@ -362,15 +418,15 @@ func IsInstanceRunning(instanceName string) bool { outputStr := string(output) if strings.Contains(outputStr, "RUNNING") { - postLog.Info(fmt.Sprintf("[IsInstanceRunning] Windows service %s is running", instanceName)) + postLog.Info(fmt.Sprintf("[IsInstanceRunning] Windows service %s is running", instance.Name)) return true } else { - postLog.Info(fmt.Sprintf("[IsInstanceRunning] Windows service %s is stopped", instanceName)) + postLog.Info(fmt.Sprintf("[IsInstanceRunning] Windows service %s is stopped", instance.Name)) return false } case "systemd": - cmd := exec.Command("systemctl", "is-active", instanceName) + cmd := exec.Command("systemctl", "is-active", serviceName) output, err := cmd.CombinedOutput() if err != nil { @@ -380,17 +436,17 @@ func IsInstanceRunning(instanceName string) bool { status := strings.TrimSpace(string(output)) if status == "active" { - postLog.Info(fmt.Sprintf("[IsInstanceRunning] Systemd service %s is running", instanceName)) + postLog.Info(fmt.Sprintf("[IsInstanceRunning] Systemd service %s is running", serviceName)) return true } else { - postLog.Info(fmt.Sprintf("[IsInstanceRunning] Systemd service %s is stopped", instanceName)) + postLog.Info(fmt.Sprintf("[IsInstanceRunning] Systemd service %s is stopped", serviceName)) return false } case "init.d": - servicePath := fmt.Sprintf("/etc/init.d/%s", instanceName) + servicePath := fmt.Sprintf("/etc/init.d/%s", serviceName) if _, err := os.Stat(servicePath); err != nil { - postLog.Info(fmt.Sprintf("[IsInstanceRunning] Init.d service %s not found", instanceName)) + postLog.Info(fmt.Sprintf("[IsInstanceRunning] Init.d service %s not found", serviceName)) return false } @@ -415,11 +471,17 @@ func IsInstanceRunning(instanceName string) bool { } } -func GetInstancePid(instanceName string) int { +func GetInstancePid(instanceID int) int { initType := GetInitSystem() + serviceName, err := GetServiceNameByInstanceID(instanceID) + if err != nil { + postLog.Error(fmt.Sprintf("[GetInstancePid] Failed to get service name: %v", err)) + return 0 + } + switch initType { case "windows": - cmd := exec.Command("sc", "query", instanceName, "info") + cmd := exec.Command("sc", "query", serviceName, "info") output, err := cmd.CombinedOutput() if err != nil { postLog.Error(fmt.Sprintf("[GetInstancePid] Failed to get Windows service info: %s, output: %s", err, output)) @@ -441,7 +503,7 @@ func GetInstancePid(instanceName string) int { } return 0 case "systemd": - cmd := exec.Command("systemctl", "show", instanceName, "--property", "MainPID") + cmd := exec.Command("systemctl", "show", serviceName, "--property", "MainPID") output, err := cmd.CombinedOutput() if err != nil { postLog.Error(fmt.Sprintf("[GetInstancePid] Failed to get PID from systemd: %s, output: %s", err, output)) @@ -460,9 +522,9 @@ func GetInstancePid(instanceName string) int { } return 0 case "init.d": - servicePath := fmt.Sprintf("/etc/init.d/%s", instanceName) + servicePath := fmt.Sprintf("/etc/init.d/%s", serviceName) if _, err := os.Stat(servicePath); err != nil { - postLog.Error(fmt.Sprintf("[GetInstancePid] Init.d service %s not found", instanceName)) + postLog.Error(fmt.Sprintf("[GetInstancePid] Init.d service %s not found", serviceName)) return 0 } cmd := exec.Command(servicePath, "status") @@ -483,7 +545,7 @@ func GetInstancePid(instanceName string) int { } } } - pidFile := fmt.Sprintf("/var/run/%s.pid", instanceName) + pidFile := fmt.Sprintf("/var/run/%s.pid", serviceName) if content, err := os.ReadFile(pidFile); err == nil { pidStr := strings.TrimSpace(string(content)) pid, err := strconv.Atoi(pidStr)