diff --git a/config/config.go b/config/config.go index a784a85..e1c2e84 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "super-frpc/global" + "super-frpc/postLog" "github.com/BurntSushi/toml" ) @@ -94,6 +95,8 @@ func LoadConfig(configPath string, getInitSystem func() string) error { return fmt.Errorf("failed to create config directory: %w", err) } + postLog.Debug(fmt.Sprintf("Loaded config: %v", global.CurrentConfig)) + return nil } @@ -101,13 +104,13 @@ func GetConfig() *global.Config { return &global.CurrentConfig } -func SaveConfig(configPath string) error { +func SaveConfig() error { data, err := json.MarshalIndent(global.CurrentConfig, "", " ") if err != nil { return fmt.Errorf("failed to marshal config: %w", err) } - if err := os.WriteFile(configPath, data, 0644); err != nil { + if err := os.WriteFile(*global.ConfigPath, data, 0644); err != nil { return fmt.Errorf("failed to write config file: %w", err) } diff --git a/docs/api.md b/docs/api.md index f668955..d27698f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1573,6 +1573,134 @@ Log messages are sent as JSON objects: --- +## Get Settings + +**Endpoint:** `/settings/get` +**Method:** GET +**Auth Required:** Yes (token) +**Permission Level:** Superuser, Admin + +Get system settings. If no `key` parameter is provided, returns all settings. If `key` is provided, returns only that specific setting. + +**Request Headers:** +``` +X-Token: your_token +X-Timestamp: 1704067200000 +``` + +**Query Parameters (optional):** +``` +key=ListenAddr +``` + +| Header | Type | Required | Description | +|--------|------|----------|-------------| +| X-Token | string | Yes | Authentication token | +| X-Timestamp | int64 | Yes | Client timestamp in milliseconds | + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| key | string | No | Specific setting key to retrieve | + +**Response (all settings):** +```json +{ + "success": true, + "message": "Settings retrieved successfully", + "data": { + "ListenAddr": "0.0.0.0", + "ListenPort": "8080", + "FrpcPath": "/usr/bin/frpc", + "InstancePath": "./configs", + "Debug": true, + "Watchdog.Enabled": true, + "Watchdog.Port": 12380, + "Notification.Enabled": true, + "Notification.Method": "webhook", + "Webhook.Method": "POST", + "Webhook.URL": "https://example.com/webhook", + "Webhook.Headers": { + "Content-Type": "application/json" + }, + "Webhook.Body": { + "text": "Super-frpc notification" + } + } +} +``` + +**Response (single setting):** +```json +{ + "success": true, + "message": "Setting retrieved successfully", + "data": { + "key": "ListenAddr", + "value": "0.0.0.0" + } +} +``` + +**Error Response (unknown key):** +```json +{ + "success": false, + "message": "Unknown setting key: InvalidKey" +} +``` + +--- + +## Set Settings + +**Endpoint:** `/settings/set` +**Method:** POST +**Content-Type:** application/json +**Auth Required:** Yes (token) +**Permission Level:** Superuser, Admin + +Update system settings. Multiple settings can be updated in a single request. + +**Request Headers:** +``` +X-Token: your_token +X-Timestamp: 1704067200000 +``` + +**Request Body:** +```json +{ + "ListenAddr": "127.0.0.1", + "ListenPort": "9090", + "Debug": false +} +``` + +| Header | Type | Required | Description | +|--------|------|----------|-------------| +| X-Token | string | Yes | Authentication token | +| X-Timestamp | int64 | Yes | Client timestamp in milliseconds | + +**Response:** +```json +{ + "success": true, + "message": "Config saved successfully" +} +``` + +**Error Response:** +```json +{ + "success": false, + "message": "Failed to save config" +} +``` + +> **Note:** Settings are saved to the config file immediately after update. Unknown keys will be logged as warnings but will not cause the request to fail. + +--- + ## User Permissions | Permission | superuser | admin | visitor | @@ -1584,6 +1712,8 @@ Log messages are sent as JSON objects: | Manage users | ✓ | ✗ | ✗ | | List active sessions | ✓ | ✓ | ✗ | | View logs | ✓ | ✓ | ✓ | +| Change settings | ✓ | ✓ | ✗ | +| Get settings | ✓ | ✓ | ✗ | --- diff --git a/global/global.go b/global/global.go index 4e8c165..39e3c5f 100644 --- a/global/global.go +++ b/global/global.go @@ -4,6 +4,10 @@ import ( "database/sql" ) +var ConfigPath *string +var DBPath_data *string +var DBPath_log *string + type SoftwareInfo struct { Name string Version string @@ -49,13 +53,23 @@ type Config struct { Enabled bool `json:"enabled"` Port int `json:"port"` } `json:"watchdog"` + Notification struct { + Enabled bool `json:"enabled"` + Method string `json:"method"` + } `json:"notification"` + Webhook struct { + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers"` + Body map[string]string `json:"body"` + } `json:"webhook"` } var CurrentConfig = Config{ ListenAddr: "0.0.0.0", ListenPort: "7000", - FrpcPath: "/usr/bin/frpc", - InstancePath: "/var/lib/frpc", + FrpcPath: "", + InstancePath: "", Debug: false, Watchdog: struct { Enabled bool `json:"enabled"` @@ -64,4 +78,26 @@ var CurrentConfig = Config{ Enabled: false, Port: 0, }, + Notification: struct { + Enabled bool `json:"enabled"` + Method string `json:"method"` + }{ + Enabled: false, + Method: "webhook", + }, + Webhook: struct { + Method string `json:"method"` + URL string `json:"url"` + Headers map[string]string `json:"headers"` + Body map[string]string `json:"body"` + }{ + Method: "", + URL: "", + Headers: map[string]string{ + "Content-Type": "", + }, + Body: map[string]string{ + "text": "", + }, + }, } diff --git a/handlers/settings.go b/handlers/settings.go index 513450a..85a754e 100644 --- a/handlers/settings.go +++ b/handlers/settings.go @@ -1,9 +1,13 @@ package handlers import ( + "encoding/json" "fmt" + "io" "net/http" + "super-frpc/config" + "super-frpc/global" "super-frpc/postLog" "super-frpc/utils" ) @@ -15,4 +19,113 @@ func GetSettingsHandler(w http.ResponseWriter, r *http.Request) { postLog.Warning(fmt.Sprintf("[GetSettingsHandler] Auth failed: %v", err)) return } + + key := r.URL.Query().Get("key") + + if key != "" { + var value interface{} + switch key { + case "ListenAddr": + value = global.CurrentConfig.ListenAddr + case "ListenPort": + value = global.CurrentConfig.ListenPort + case "FrpcPath": + value = global.CurrentConfig.FrpcPath + case "InstancePath": + value = global.CurrentConfig.InstancePath + case "Debug": + value = global.CurrentConfig.Debug + case "Watchdog.Enabled": + value = global.CurrentConfig.Watchdog.Enabled + case "Watchdog.Port": + value = global.CurrentConfig.Watchdog.Port + case "Notification.Enabled": + value = global.CurrentConfig.Notification.Enabled + case "Notification.Method": + value = global.CurrentConfig.Notification.Method + case "Webhook.Method": + value = global.CurrentConfig.Webhook.Method + case "Webhook.URL": + value = global.CurrentConfig.Webhook.URL + case "Webhook.Headers": + value = global.CurrentConfig.Webhook.Headers + case "Webhook.Body": + value = global.CurrentConfig.Webhook.Body + default: + utils.SendErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("Unknown setting key: %s", key)) + return + } + + utils.SendSuccessResponse(w, "Setting retrieved successfully", map[string]interface{}{ + "key": key, + "value": value, + }) + } else { + settings := map[string]interface{}{ + "ListenAddr": global.CurrentConfig.ListenAddr, + "ListenPort": global.CurrentConfig.ListenPort, + "FrpcPath": global.CurrentConfig.FrpcPath, + "InstancePath": global.CurrentConfig.InstancePath, + "Debug": global.CurrentConfig.Debug, + "Watchdog.Enabled": global.CurrentConfig.Watchdog.Enabled, + "Watchdog.Port": global.CurrentConfig.Watchdog.Port, + "Notification.Enabled": global.CurrentConfig.Notification.Enabled, + "Notification.Method": global.CurrentConfig.Notification.Method, + "Webhook.Method": global.CurrentConfig.Webhook.Method, + "Webhook.URL": global.CurrentConfig.Webhook.URL, + "Webhook.Headers": global.CurrentConfig.Webhook.Headers, + "Webhook.Body": global.CurrentConfig.Webhook.Body, + } + + utils.SendSuccessResponse(w, "Settings retrieved successfully", settings) + } +} + +func SetSettingsHandler(w http.ResponseWriter, r *http.Request) { + _, err := utils.Auth(w, r, http.MethodPost, "superuser", "admin") + if err != nil { + utils.SendErrorResponse(w, http.StatusUnauthorized, err.Error()) + postLog.Warning(fmt.Sprintf("[SetSettingsHandler] Auth failed: %v", err)) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + postLog.Error(fmt.Sprintf("[SetSettingsHandler] Failed to read request body: %v", err)) + utils.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("[SetSettingsHandler] Failed to unmarshal request body: %v", err)) + utils.SendErrorResponse(w, http.StatusBadRequest, "Invalid request format") + return + } + + for key, value := range reqMap { + postLog.Debug(fmt.Sprintf("[SetSettingsHandler] Modify setting: %s = %v", key, value)) + switch key { + case "ListenAddr": + if v, ok := value.(string); ok { + global.CurrentConfig.ListenAddr = v + } + case "ListenPort": + if v, ok := value.(float64); ok { + global.CurrentConfig.ListenPort = fmt.Sprintf("%d", int(v)) + } + default: + postLog.Warning(fmt.Sprintf("[SetSettingsHandler] Unknown setting key: %s", key)) + } + } + + err = config.SaveConfig() + if err != nil { + postLog.Error(fmt.Sprintf("[SetSettingsHandler] Failed to save config: %v", err)) + utils.SendErrorResponse(w, http.StatusInternalServerError, "Failed to save config") + return + } + postLog.Debug("[SetSettingsHandler] Config saved successfully") + utils.SendSuccessResponse(w, "Config saved successfully", nil) } diff --git a/main.go b/main.go index 7c8c1d4..570e179 100644 --- a/main.go +++ b/main.go @@ -22,41 +22,44 @@ import ( func main() { postLog.Info(fmt.Sprintf("%s %s (Build %d.%s) by %s", global.Software.Name, global.Software.Version, global.Software.BuildVer, global.Software.BuildType, global.Software.Developer)) - configPath := flag.String("config", "./config.json", "path to config file") - dbPath_data := flag.String("db", "./database.db", "path to database file") - dbPath_log := flag.String("log", "./logs.db", "path to logs database file") + global.ConfigPath = flag.String("config", "./config.json", "path to config file") + global.DBPath_data = flag.String("db", "./database.db", "path to database file") + global.DBPath_log = flag.String("log", "./logs.db", "path to logs database file") flag.Parse() - if _, err := os.Stat(*configPath); os.IsNotExist(err) { + if _, err := os.Stat(*global.ConfigPath); os.IsNotExist(err) { // Load the default config configData, err := json.MarshalIndent(global.CurrentConfig, "", " ") if err != nil { postLog.Warning(fmt.Sprintf("Failed to marshal default config: %v", err)) - } else if err := os.WriteFile(*configPath, configData, 0644); err != nil { + } else if err := os.WriteFile(*global.ConfigPath, configData, 0644); err != nil { postLog.Warning(fmt.Sprintf("Failed to create default config file: %v", err)) } - postLog.Info(fmt.Sprintf("Created default config file at %s", *configPath)) + postLog.Info(fmt.Sprintf("Created default config file at %s", *global.ConfigPath)) } - err := config.LoadConfig(*configPath, service.GetInitSystem) + err := config.LoadConfig(*global.ConfigPath, service.GetInitSystem) if err != nil { postLog.Fatal(fmt.Sprintf("Failed to load config: %v", err)) } + if global.Software.BuildType == "debug" { // If debug build, set debug mode to true + global.CurrentConfig.Debug = true + } postLog.SetDebugMode(global.CurrentConfig.Debug) global.Is.Debug = global.CurrentConfig.Debug - if err := postLog.InitLogsDatabase(*dbPath_log); err != nil { + if err := postLog.InitLogsDatabase(*global.DBPath_log); err != nil { postLog.Fatal(fmt.Sprintf("Failed to initialize logs database: %v", err)) } postLog.Info("Logs database initialized successfully") - if err := database.InitDatabase(*dbPath_data, *dbPath_log); err != nil { + if err := database.InitDatabase(*global.DBPath_data, *global.DBPath_log); err != nil { postLog.Fatal(fmt.Sprintf("Failed to initialize database: %v", err)) } postLog.Info("Database initialized successfully") - if err := database.InitFrpcDatabase(*dbPath_data); err != nil { + if err := database.InitFrpcDatabase(*global.DBPath_data); err != nil { postLog.Fatal(fmt.Sprintf("Failed to initialize frpc database: %v", err)) } diff --git a/router.go b/router.go index 81b791a..71fd2c9 100644 --- a/router.go +++ b/router.go @@ -11,10 +11,12 @@ import ( ) func setupRoutes() { - http.HandleFunc("/system/getStatus", GetStatusHandler) - http.HandleFunc("/system/getSoftwareInfo", GetSoftwareInfoHandler) systemLogHandler := postLog.NewLogSocketHandler(postLog.GetLogBroadcaster()) http.HandleFunc("/system/getLogs", systemLogHandler.Handle) + http.HandleFunc("/system/getStatus", GetStatusHandler) + http.HandleFunc("/system/getSoftwareInfo", GetSoftwareInfoHandler) + http.HandleFunc("/system/settings/get", handlers.GetSettingsHandler) + http.HandleFunc("/system/settings/set", handlers.SetSettingsHandler) http.HandleFunc("/register", handlers.RegisterHandler) http.HandleFunc("/login", handlers.LoginHandler)