From 8a5a647dcc8d2af74cd6a3d5267902f5bfe77bab Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Sat, 6 Jun 2026 16:13:30 +0800 Subject: [PATCH] feat(proxy): add proxy switch endpoint to enable/disable proxies, and all `enabled` parameter to display the status of each proxy when fetching the proxies list --- README.md | 4 +- docs/api.md | 51 ++++++++++- handlers/instance.go | 2 +- handlers/proxy.go | 207 +++++++++++++++++++++++++++++++++++++++++-- router.go | 1 + 5 files changed, 253 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1d2a944..0ef137d 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,9 @@ For detailed API documentation, please see [docs/api.md](docs/api.md) - [ ] Develop an agent software to handle windows service management - [ ] Refactor all log output level to be more clear - [x] Add frpc instance watchdog -- [ ] Add per-proxy status display +- [x] Add per-proxy status display - [ ] Add proxy edit submit action -- [ ] Add per-proxy status control +- [x] Add per-proxy status control - [x] Fix reboot failed when deployed as systemd service - [x] Enhance speed of listing instances - [x] Increase speed of fetching instance logs diff --git a/docs/api.md b/docs/api.md index 245bafc..83c4542 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1255,6 +1255,51 @@ X-Timestamp: 1704067200000 --- +## Proxy Switch + +**Endpoint:** `/frpcAct/proxyMgr/switch` +**Method:** POST +**Auth Required:** Yes (token) +**Permission Level:** Admin + +Switch the status of a proxy. + +**Request Headers:** +``` +X-Token: your_token +X-Timestamp: 1704067200000 +``` + +| Header | Type | Required | Description | +|--------|------|----------|-------------| +| X-Token | string | Yes | Authentication token | +| X-Timestamp | int64 | Yes | Client timestamp in milliseconds | + +**Request Body:** +```json +{ + "instanceID": 1, + "proxyName": "test", + "action": 1 +} + +| Field | Type | Description | +|-------|------|-------------| +| instanceID | int | Instance ID | +| proxyName | string | Name of the created proxy | +| action | string | Enable or disable the current proxy. 0 is disable, 1 is enable | + +**Response:** +```json +{ + "success": true, + "message": "Proxy status has been switched to off." +} +``` + + +--- + ## List Proxies **Endpoint:** `/frpcAct/proxyMgr/list` @@ -1298,14 +1343,16 @@ X-Timestamp: 1704067200000 "type": "tcp", "localIP": "127.0.0.1", "localPort": "22", - "remotePort": "6000" + "remotePort": "6000", + "enabled": true }, { "name": "web_proxy", "type": "http", "localIP": "127.0.0.1", "localPort": "8080", - "remotePort": "80" + "remotePort": "80", + "enabled": false } ] } diff --git a/handlers/instance.go b/handlers/instance.go index fc308fc..ffdf441 100644 --- a/handlers/instance.go +++ b/handlers/instance.go @@ -452,7 +452,7 @@ func getStringFromMap(m map[string]interface{}, key string) string { return "" } -func getNumFromMap(m map[string]interface{}, key string) int { +func getIntFromMap(m map[string]interface{}, key string) int { if v, ok := m[key].(int); ok { return v } diff --git a/handlers/proxy.go b/handlers/proxy.go index 2a24ede..4391ae6 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strconv" + "strings" "super-frpc/config" "super-frpc/database" @@ -57,8 +58,8 @@ func CreateProxyHandler(w http.ResponseWriter, r *http.Request) { Name: getStringFromMap(proxyInfoMap, "name"), Type: getStringFromMap(proxyInfoMap, "type"), LocalIP: getStringFromMap(proxyInfoMap, "localIP"), - LocalPort: getNumFromMap(proxyInfoMap, "localPort"), - RemotePort: getNumFromMap(proxyInfoMap, "remotePort"), + LocalPort: getIntFromMap(proxyInfoMap, "localPort"), + RemotePort: getIntFromMap(proxyInfoMap, "remotePort"), } if proxyInfo.Name == "" || proxyInfo.Type == "" || proxyInfo.LocalIP == "" || @@ -155,8 +156,8 @@ func ModifyProxyHandler(w http.ResponseWriter, r *http.Request) { Name: getStringFromMap(proxyInfoMap, "name"), Type: getStringFromMap(proxyInfoMap, "type"), LocalIP: getStringFromMap(proxyInfoMap, "localIP"), - LocalPort: getNumFromMap(proxyInfoMap, "localPort"), - RemotePort: getNumFromMap(proxyInfoMap, "remotePort"), + LocalPort: getIntFromMap(proxyInfoMap, "localPort"), + RemotePort: getIntFromMap(proxyInfoMap, "remotePort"), } if proxyInfo.OldName == "" { @@ -336,18 +337,23 @@ func ListProxiesHandler(w http.ResponseWriter, r *http.Request) { return } - proxyList := make([]map[string]interface{}, len(cfg.Proxies)) - for i, proxy := range cfg.Proxies { + contentStr := string(configContent) + proxyList := make([]map[string]interface{}, 0, len(cfg.Proxies)) + for _, proxy := range cfg.Proxies { proxyData := map[string]interface{}{ "name": proxy["name"], "type": proxy["type"], "localIP": proxy["localIP"], "localPort": proxy["localPort"], "remotePort": proxy["remotePort"], + "enabled": true, } - proxyList[i] = proxyData + proxyList = append(proxyList, proxyData) } + disabledProxies := parseDisabledProxies(strings.Split(contentStr, "\n")) + proxyList = append(proxyList, disabledProxies...) + utils.SendSuccessResponse(w, "Proxies listed successfully", map[string]interface{}{ "instanceID": instance.ID, "proxyCount": len(proxyList), @@ -355,3 +361,190 @@ func ListProxiesHandler(w http.ResponseWriter, r *http.Request) { }) postLog.Info(fmt.Sprintf("[ListProxiesHandler] Retrieved %d proxies for instance %d", len(proxyList), instance.ID)) } + +func SwitchProxyHandler(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("[SwitchProxyHandler] Auth failed: %v", err)) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + postLog.Error(fmt.Sprintf("[SwitchProxyHandler] 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("[SwitchProxyHandler] Failed to unmarshal request body: %v", err)) + utils.SendErrorResponse(w, http.StatusBadRequest, "Invalid request format") + return + } + + instanceID := getIntFromMap(reqMap, "instanceID") + if instanceID == 0 { + postLog.Error("[SwitchProxyHandler] instanceID is required") + utils.SendErrorResponse(w, http.StatusBadRequest, "instanceID is required") + return + } + + proxyName := getStringFromMap(reqMap, "proxyName") + if proxyName == "" { + postLog.Error("[SwitchProxyHandler] proxyName is required") + utils.SendErrorResponse(w, http.StatusBadRequest, "proxyName is required") + return + } + + action := getIntFromMap(reqMap, "action") + if action != 0 && action != 1 { + postLog.Error("[SwitchProxyHandler] action must be 0 or 1") + utils.SendErrorResponse(w, http.StatusBadRequest, "action must be 0 (disable) or 1 (enable)") + return + } + + var instance database.FrpcInstance + instance, err = database.DBQueryFrpcInstanceByID(instanceID) + if err != nil { + postLog.Error(fmt.Sprintf("[SwitchProxyHandler] Failed to query instance: %v", err)) + utils.SendErrorResponse(w, http.StatusInternalServerError, "Failed to query instance") + return + } + + configContent, err := os.ReadFile(instance.ConfigPath) + if err != nil { + postLog.Error(fmt.Sprintf("[SwitchProxyHandler] Failed to read config file %s: %v", instance.ConfigPath, err)) + utils.SendErrorResponse(w, http.StatusInternalServerError, "Failed to read config file") + return + } + + lines := strings.Split(string(configContent), "\n") + startLine, endLine := findProxyBlock(lines, proxyName) + if startLine < 0 { + postLog.Error(fmt.Sprintf("[SwitchProxyHandler] Proxy %s not found in config", proxyName)) + utils.SendErrorResponse(w, http.StatusNotFound, "Proxy not found in config") + return + } + + if action == 0 { + for i := startLine; i <= endLine; i++ { + if !strings.HasPrefix(lines[i], "#") { + lines[i] = "#" + lines[i] + } + } + } else { + for i := startLine; i <= endLine; i++ { + lines[i] = strings.TrimPrefix(lines[i], "#") + } + } + + updatedContent := strings.Join(lines, "\n") + if err := os.WriteFile(instance.ConfigPath, []byte(updatedContent), 0644); err != nil { + postLog.Error(fmt.Sprintf("[SwitchProxyHandler] Failed to write config file %s: %v", instance.ConfigPath, err)) + utils.SendErrorResponse(w, http.StatusInternalServerError, "Failed to write config file") + return + } + + statusText := "off" + if action == 1 { + statusText = "on" + } + utils.SendSuccessResponse(w, "Proxy status has been switched to "+statusText+".", map[string]interface{}{ + "instanceID": instance.ID, + "proxyName": proxyName, + "status": statusText, + }) + postLog.Info(fmt.Sprintf("[SwitchProxyHandler] Proxy %s switched %s for instance %d", proxyName, statusText, instance.ID)) +} + +func findProxyBlock(lines []string, proxyName string) (int, int) { + targetLine := -1 + for i, line := range lines { + trimmed := strings.TrimSpace(line) + uncommented := strings.TrimPrefix(trimmed, "#") + uncommented = strings.TrimSpace(uncommented) + if strings.HasPrefix(uncommented, "name ") && strings.Contains(uncommented, proxyName) { + targetLine = i + break + } + } + + if targetLine < 0 { + return -1, -1 + } + + startLine := targetLine + for i := targetLine; i >= 0; i-- { + trimmed := strings.TrimSpace(lines[i]) + uncommented := strings.TrimPrefix(trimmed, "#") + uncommented = strings.TrimSpace(uncommented) + if uncommented == "[[proxies]]" { + startLine = i + break + } + } + + endLine := targetLine + for i := targetLine + 1; i < len(lines); i++ { + trimmed := strings.TrimSpace(lines[i]) + uncommented := strings.TrimPrefix(trimmed, "#") + uncommented = strings.TrimSpace(uncommented) + if uncommented == "[[proxies]]" { + endLine = i - 1 + break + } + endLine = i + } + + return startLine, endLine +} + +func parseDisabledProxies(lines []string) []map[string]interface{} { + var disabled []map[string]interface{} + + for i := 0; i < len(lines); i++ { + trimmed := strings.TrimSpace(lines[i]) + if trimmed != "#[[proxies]]" { + continue + } + + proxy := map[string]interface{}{"enabled": false} + for j := i + 1; j < len(lines); j++ { + line := lines[j] + if !strings.HasPrefix(strings.TrimSpace(line), "#") { + break + } + uncommented := strings.TrimPrefix(strings.TrimSpace(line), "#") + uncommented = strings.TrimSpace(uncommented) + if uncommented == "" || strings.HasPrefix(uncommented, "[[") { + break + } + parts := strings.SplitN(uncommented, "=", 2) + if len(parts) != 2 { + continue + } + key := strings.TrimSpace(parts[0]) + val := strings.TrimSpace(parts[1]) + proxy[key] = parseTOMLValue(val) + } + + if proxy["name"] != nil && proxy["name"] != "" { + disabled = append(disabled, proxy) + } + } + + return disabled +} + +func parseTOMLValue(val string) interface{} { + if len(val) >= 2 && val[0] == '"' && val[len(val)-1] == '"' { + return val[1 : len(val)-1] + } + if n, err := strconv.Atoi(val); err == nil { + return n + } + return val +} diff --git a/router.go b/router.go index 1e88ed3..048a494 100644 --- a/router.go +++ b/router.go @@ -50,6 +50,7 @@ func setupRoutes() { http.HandleFunc("/api/frpcAct/proxyMgr/modify", handlers.ModifyProxyHandler) http.HandleFunc("/api/frpcAct/proxyMgr/delete", handlers.DeleteProxyHandler) http.HandleFunc("/api/frpcAct/proxyMgr/list", handlers.ListProxiesHandler) + http.HandleFunc("/api/frpcAct/proxyMgr/switch", handlers.SwitchProxyHandler) http.HandleFunc("/", web.ServeStatic)