package main import ( "database/sql" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "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 { if frpcDB != nil { return frpcDB.Close() } return nil } func CreateInstanceHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method") postLog.Debug(fmt.Sprintf("[CreateInstanceHandler] Invalid request method: %s", r.Method)) return } body, err := io.ReadAll(r.Body) if err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to read request body: %v", err)) SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body") return } defer r.Body.Close() var reqMap map[string]interface{} if err := json.Unmarshal(body, &reqMap); err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to unmarshal request body: %v", err)) SendErrorResponse(w, http.StatusBadRequest, "Invalid request format") return } bootAtStart := false if bas, ok := reqMap["bootAtStart"]; ok { switch v := bas.(type) { case bool: bootAtStart = v case string: if v == "true" { bootAtStart = true } } } instanceInfoMap, ok := reqMap["instanceInfo"].(map[string]interface{}) if !ok { SendErrorResponse(w, http.StatusBadRequest, "Invalid instanceInfo format") return } instanceInfo := InstanceInfo{ Name: getStringFromMap(instanceInfoMap, "name"), ServerAddr: getStringFromMap(instanceInfoMap, "serverAddr"), ServerPort: getStringFromMap(instanceInfoMap, "serverPort"), AuthMethod: getStringFromMap(instanceInfoMap, "auth_method"), RunUser: getStringFromMap(reqMap, "runUser"), } if additional, ok := reqMap["additionalProperties"].(map[string]interface{}); ok { instanceInfo.Additional = additional } userID, _, err := ValidateRequestWithHeader(w, r) if err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to validate request header: %v", err)) SendErrorResponse(w, http.StatusUnauthorized, err.Error()) return } req := CreateInstanceRequest{ InstanceInfo: instanceInfo, BootAtStart: bootAtStart, RunUser: instanceInfo.RunUser, Additional: instanceInfo.Additional, } if err := CheckPermission(userID, "superuser", "admin"); err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to check permission: %v", err)) SendErrorResponse(w, http.StatusForbidden, err.Error()) return } if req.InstanceInfo.Name == "" || req.InstanceInfo.ServerAddr == "" || req.InstanceInfo.ServerPort == "" || req.InstanceInfo.AuthMethod == "" { SendErrorResponse(w, http.StatusBadRequest, "Missing required fields in instanceInfo") return } runUser := req.RunUser if runUser == "" { runUser = "root" } user, err := GetUserByID(userID) if err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to get user info: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info") return } configDir, err := GetConfigDir() if err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to get config directory: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to get config directory") return } configFileName := fmt.Sprintf("superfrpc_%s_%s.toml", user.Username, req.InstanceInfo.Name) configPath := filepath.Join(configDir, configFileName) configContent := generateFrpcConfig(req.InstanceInfo) if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil { postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to create config file %s: %v", configPath, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to create config file") return } 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, } if err := DBAddFrpcInstance(instance); err != nil { os.Remove(configPath) postLog.Error(fmt.Sprintf("[CreateInstanceHandler] Failed to save instance %s to database: %v", req.InstanceInfo.Name, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to save instance to database") return } if req.BootAtStart { 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 } } SendSuccessResponse(w, "Instance created successfully", map[string]interface{}{ "name": req.InstanceInfo.Name, "configPath": configPath, "bootAtStart": req.BootAtStart, }) postLog.Info(fmt.Sprintf("[CreateInstanceHandler] Instance %s created successfully: configPath=%s, bootAtStart=%v, runUser=%s, additionalProperties=%v", req.InstanceInfo.Name, configPath, req.BootAtStart, runUser, req.Additional)) } func DeleteInstanceHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method") return } body, err := io.ReadAll(r.Body) if err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to read request body: %v", err)) SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body") return } defer r.Body.Close() var reqMap map[string]interface{} if err := json.Unmarshal(body, &reqMap); err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to unmarshal request body: %v", err)) SendErrorResponse(w, http.StatusBadRequest, "Invalid request format") return } instanceName := getStringFromMap(reqMap, "instanceName") if instanceName == "" { SendErrorResponse(w, http.StatusBadRequest, "instanceName is required") return } userID, _, err := ValidateRequestWithHeader(w, r) if err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to validate request header: %v", err)) SendErrorResponse(w, http.StatusUnauthorized, err.Error()) return } if err := CheckPermission(userID, "superuser", "admin"); err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to check permission: %v", err)) SendErrorResponse(w, http.StatusForbidden, err.Error()) 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 = DBQueryFrpcInstance(userID, instanceName) 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: %s", userID, instanceName)) return } if err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to query instance: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to query instance") return } if instance.BootAtStart { if err := removeBootService(user.Username, instanceName); err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to remove boot service for instance %s: %v", instanceName, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to remove boot service") return } } if _, err := os.Stat(instance.ConfigPath); err == nil { if err := os.Remove(instance.ConfigPath); err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to remove config file %s: %v", instance.ConfigPath, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to remove config file") return } } if err := DBRemoveFrpcInstance(userID, instanceName); err != nil { postLog.Error(fmt.Sprintf("[DeleteInstanceHandler] Failed to delete instance %s from database: %v", instanceName, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to delete instance from database") return } SendSuccessResponse(w, "Instance deleted successfully", map[string]interface{}{ "name": instanceName, }) postLog.Info(fmt.Sprintf("[DeleteInstanceHandler] Instance %s deleted successfully", instanceName)) } func ModifyInstanceHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Invalid request method: %s", r.Method)) SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method") return } body, err := io.ReadAll(r.Body) if err != nil { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to read request body: %v", err)) SendErrorResponse(w, http.StatusBadRequest, "Failed to read request body") return } defer r.Body.Close() var reqMap map[string]interface{} if err := json.Unmarshal(body, &reqMap); err != nil { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to unmarshal request body: %v", err)) SendErrorResponse(w, http.StatusBadRequest, "Invalid request format") return } instanceName := getStringFromMap(reqMap, "instanceName") if instanceName == "" { SendErrorResponse(w, http.StatusBadRequest, "instanceName is required") return } instanceID := getStringFromMap(reqMap, "instanceID") if instanceID == "" { SendErrorResponse(w, http.StatusBadRequest, "instanceID is required") return } modifyType := getStringFromMap(reqMap, "type") if modifyType == "" { SendErrorResponse(w, http.StatusBadRequest, "type is required") return } if modifyType != "configFile" && modifyType != "systemConfig" { SendErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("Unknown modify type %s", modifyType)) return } modifiedData, ok := reqMap["modifiedData"].(map[string]interface{}) if !ok || modifiedData == nil { SendErrorResponse(w, http.StatusBadRequest, "modifiedData is required and must be an object") return } userID, _, err := ValidateRequestWithHeader(w, r) if err != nil { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to validate request header: %v", err)) SendErrorResponse(w, http.StatusUnauthorized, err.Error()) return } if err := CheckPermission(userID, "superuser", "admin"); err != nil { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to check permission: %v", err)) SendErrorResponse(w, http.StatusForbidden, err.Error()) return } user, err := GetUserByID(userID) if err != nil { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to get user info: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user info") return } instance, err := DBQueryFrpcInstance(userID, instanceName) if err == sql.ErrNoRows { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] User %d tried to modify a not existed instance: %s", userID, instanceName)) SendErrorResponse(w, http.StatusNotFound, "Instance not found") return } if err != nil { postLog.Error(fmt.Sprintf("[ModifyInstanceHandler] Failed to query instance: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to query instance") return } if fmt.Sprintf("%d", instance.ID) != instanceID { SendErrorResponse(w, http.StatusBadRequest, "instanceID does not match instanceName") return } if modifyType == "configFile" { handleConfigFileModify(w, instance, modifiedData, user.Username) } else { handleSystemConfigModify(w, r, instance, modifiedData, user) } } 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 } 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 } SendSuccessResponse(w, "Config file modified successfully", map[string]interface{}{ "instanceName": instance.Name, "instanceID": instance.ID, "configPath": configPath, }) 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 newBootAtStart := instance.BootAtStart oldBootAtStart := instance.BootAtStart var bootServiceError string if v, ok := modifiedData["name"].(string); ok && v != "" { newName = v } if v, ok := modifiedData["runUser"].(string); ok { newRunUser = v } if v, ok := modifiedData["bootAtStart"].(bool); ok { newBootAtStart = v } oldConfigPath := instance.ConfigPath var newConfigPath string if newName != instance.Name || newRunUser != instance.RunUser { configDir, err := GetConfigDir() if err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to get config directory: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to get config directory") return } newConfigFileName := fmt.Sprintf("superfrpc_%s_%s.toml", user.Username, newName) newConfigPath = filepath.Join(configDir, newConfigFileName) if oldConfigPath != newConfigPath { if _, err := os.Stat(oldConfigPath); err == nil { if err := os.Rename(oldConfigPath, newConfigPath); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to rename config file %s to %s: %v", oldConfigPath, newConfigPath, err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to rename config file") return } } } } else { newConfigPath = oldConfigPath } instance.Name = newName instance.RunUser = newRunUser instance.BootAtStart = newBootAtStart instance.ConfigPath = newConfigPath if err := DBUpdateFrpcInstance(instance); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to update instance in database: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to update instance in database") return } if oldBootAtStart && !newBootAtStart { if err := removeBootService(user.Username, instance.Name); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to remove boot service: %v", err)) bootServiceError = fmt.Sprintf("Failed to remove boot service: %v", err) } } else if !oldBootAtStart && newBootAtStart { if err := createBootService(user.Username, newName, newConfigPath, newRunUser); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to create boot service: %v", err)) bootServiceError = fmt.Sprintf("Failed to create boot service: %v", err) } } else if oldBootAtStart && newBootAtStart && (instance.Name != newName || instance.RunUser != newRunUser) { if err := removeBootService(user.Username, instance.Name); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to remove old boot service: %v", err)) bootServiceError = fmt.Sprintf("Failed to remove old boot service: %v", err) } if err := createBootService(user.Username, newName, newConfigPath, newRunUser); err != nil { postLog.Error(fmt.Sprintf("[handleSystemConfigModify] Failed to create new boot service: %v", err)) if bootServiceError != "" { bootServiceError += "; " } bootServiceError += fmt.Sprintf("Failed to create new boot service: %v", err) } } data := map[string]interface{}{ "instanceName": newName, "instanceID": instance.ID, "configPath": newConfigPath, "bootAtStart": newBootAtStart, "runUser": newRunUser, } if bootServiceError != "" { data["bootServiceError"] = bootServiceError } 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 FROM frpcInstances WHERE userID = ? `, userID) if err != nil { return nil, err } defer rows.Close() var instances []FrpcInstance 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, ); err != nil { return nil, err } instance.CreatedAt, _ = time.Parse(time.RFC3339, createdAtStr) instances = append(instances, instance) } return instances, nil } func getStringFromMap(m map[string]interface{}, key string) string { if v, ok := m[key].(string); ok { return v } return "" } func ListInstancesHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method") postLog.Debug(fmt.Sprintf("[ListInstancesHandler] Invalid request method: %s", r.Method)) return } userID, _, err := ValidateRequestWithHeader(w, r) if err != nil { postLog.Error(fmt.Sprintf("[ListInstancesHandler] Failed to validate request header: %v", err)) SendErrorResponse(w, http.StatusBadRequest, "Invalid request header") return } userType, err := GetUserType(userID) if err != nil { postLog.Error(fmt.Sprintf("[ListInstancesHandler] Failed to get user type: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to get user type") return } instances, err := GetUserInstances(userID) if err != nil { postLog.Error(fmt.Sprintf("[ListInstancesHandler] Failed to get user instances: %v", err)) SendErrorResponse(w, http.StatusInternalServerError, "Failed to get instances") return } instanceList := make([]map[string]interface{}, len(instances)) for i, inst := range instances { instanceData := map[string]interface{}{ "instanceID": inst.ID, "name": inst.Name, "bootAtStart": inst.BootAtStart, "runUser": inst.RunUser, "configPath": inst.ConfigPath, "createdAt": inst.CreatedAt, "createdBy": inst.CreatedBy, } if userType == "admin" || userType == "superuser" { instanceData["serverAddr"] = inst.ServerAddr instanceData["serverPort"] = inst.ServerPort instanceData["auth_method"] = inst.AuthMethod } instanceList[i] = instanceData } SendSuccessResponse(w, "Instances retrieved successfully", instanceList) postLog.Info(fmt.Sprintf("[ListInstancesHandler] Retrieved %d instances for user %d (type: %s)", len(instances), userID, userType)) }