From 0e578c422f5bcd8dd57c7a8ab3fe114e9752050c Mon Sep 17 00:00:00 2001 From: NanamiAdmin Date: Tue, 31 Mar 2026 23:03:51 +0800 Subject: [PATCH] feat(proxy): add modify proxy API endpoint and functionality Implement proxy modification feature including: - New modifyFrpcProxy function in config.go - New ModifyProxyHandler in frpcProxyAct.go - New API endpoint in router.go - Updated API documentation in docs/api.md --- README.md | 1 + config.go | 33 +++++++++++++++++ docs/api.md | 85 +++++++++++++++++++++++++++++++++++++++++++ frpcProxyAct.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ router.go | 1 + 5 files changed, 215 insertions(+) diff --git a/README.md b/README.md index f78dfd0..e6c32ad 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ For detailed API documentation, please see [docs/api.md](docs/api.md) - [x] Add frpc instance log display API - [x] Fix random database lock when processing logs - [ ] Add frpc createdBy storage and display +- [x] Add frpc proxy management API - [x] Fix backend can still start frpc instance when it is already running - [ ] Develop an agent software to handle windows service management - [ ] Refactor all log output level to be more clear diff --git a/config.go b/config.go index 772c7dd..58ad055 100644 --- a/config.go +++ b/config.go @@ -294,6 +294,39 @@ func removeFrpcProxy(configContent string, proxyName string) (string, error) { return result, nil } +func modifyFrpcProxy(configContent string, info FrpcProxyInfo) (string, error) { + config, err := decodeFrpcConfig(configContent) + if err != nil { + return "", fmt.Errorf("failed to parse config: %w", err) + } + + var found bool + for i, proxy := range config.Proxies { + if name, ok := proxy["name"].(string); ok && name == info.Name { + config.Proxies[i] = map[string]interface{}{ + "name": info.Name, + "type": info.Type, + "localIP": info.LocalIP, + "localPort": info.LocalPort, + "remotePort": info.RemotePort, + } + found = true + break + } + } + + if !found { + return "", fmt.Errorf("proxy %s not found", info.Name) + } + + 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 { diff --git a/docs/api.md b/docs/api.md index 5bede36..f668955 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1042,6 +1042,91 @@ remote_port = 6000 --- +## Modify Proxy + +**Endpoint:** `/frpcAct/proxyMgr/modify` +**Method:** POST +**Content-Type:** application/json +**Auth Required:** Yes (token) +**Permission Level:** Admin + +Modify an existing proxy configuration for an frpc instance. The proxy configuration will be updated in the instance's config file. + +**Request Headers:** +``` +X-Token: your_token +X-Timestamp: 1704067200000 +``` + +**Request Body:** +```json +{ + "instanceID": "1", + "proxyInfo": { + "name": "ssh_proxy", + "type": "tcp", + "localIP": "127.0.0.1", + "localPort": "22", + "remotePort": "6001" + } +} +``` + +| Header | Type | Required | Description | +|--------|------|----------|-------------| +| X-Token | string | Yes | Authentication token | +| X-Timestamp | int64 | Yes | Client timestamp in milliseconds | + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| instanceID | string | Yes | Instance ID (the ID of the frpc instance) | +| proxyInfo.name | string | Yes | Proxy name (used to identify which proxy to modify) | +| proxyInfo.type | string | Yes | Proxy type (e.g., tcp, udp, http, https) | +| proxyInfo.localIP | string | Yes | Local IP address to forward to | +| proxyInfo.localPort | string | Yes | Local port to forward from | +| proxyInfo.remotePort | string | Yes | Remote port on frps to expose | + +**Response:** +```json +{ + "success": true, + "message": "Proxy modified successfully", + "data": { + "instanceID": 1, + "configPath": "./configs/superfrpc_user_my_frpc.toml", + "proxyName": "ssh_proxy" + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| instanceID | int | Instance ID | +| configPath | string | Path to the configuration file | +| proxyName | string | Name of the modified proxy | + +**Config File Format:** + +The proxy configuration in the config file will be updated to the following format: + +```toml +[[proxies]] +name = ssh_proxy +type = tcp +local_ip = 127.0.0.1 +local_port = 22 +remote_port = 6001 +``` + +> **Note:** +> - The proxy configuration is updated in the existing config file +> - The instance must already exist before modifying a proxy +> - This endpoint does not modify the database, only the config file +> - The frpc service needs to be restarted for changes to take effect +> - The proxy name is used to identify which proxy to modify; if the proxy is not found, an error will be returned + +--- + ## Delete Proxy **Endpoint:** `/frpcAct/proxyMgr/delete` diff --git a/frpcProxyAct.go b/frpcProxyAct.go index c5a2cc2..0de8678 100644 --- a/frpcProxyAct.go +++ b/frpcProxyAct.go @@ -107,6 +107,101 @@ 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 ModifyProxyHandler(w http.ResponseWriter, r *http.Request) { + userID, err := Auth(w, r, http.MethodPost, "superuser", "admin") + if err != nil { + SendErrorResponse(w, http.StatusUnauthorized, err.Error()) + postLog.Warning(fmt.Sprintf("[ModifyProxyHandler] Auth failed: %v", err)) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + postLog.Error(fmt.Sprintf("[ModifyProxyHandler] 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("[ModifyProxyHandler] Failed to unmarshal request body: %v", err)) + SendErrorResponse(w, http.StatusBadRequest, "Invalid request format") + return + } + + instanceID := getStringFromMap(reqMap, "instanceID") + if instanceID == "" { + postLog.Error("[ModifyProxyHandler] instanceID is required") + SendErrorResponse(w, http.StatusBadRequest, "instanceID is required") + return + } + + proxyInfoMap, ok := reqMap["proxyInfo"].(map[string]interface{}) + if !ok { + postLog.Error("[ModifyProxyHandler] Invalid proxyInfo format") + SendErrorResponse(w, http.StatusBadRequest, "Invalid proxyInfo format") + return + } + + proxyInfo := FrpcProxyInfo{ + Name: getStringFromMap(proxyInfoMap, "name"), + Type: getStringFromMap(proxyInfoMap, "type"), + LocalIP: getStringFromMap(proxyInfoMap, "localIP"), + LocalPort: getStringFromMap(proxyInfoMap, "localPort"), + RemotePort: getStringFromMap(proxyInfoMap, "remotePort"), + } + + if proxyInfo.Name == "" || proxyInfo.Type == "" || proxyInfo.LocalIP == "" || + proxyInfo.LocalPort == "" || proxyInfo.RemotePort == "" { + postLog.Error("[ModifyProxyHandler] Missing required fields in proxyInfo") + SendErrorResponse(w, http.StatusBadRequest, "Missing required fields in proxyInfo") + return + } + + var instance FrpcInstance + instanceIDInt, _ := strconv.Atoi(instanceID) + instance, err = DBQueryFrpcInstanceByID(instanceIDInt) + if err != nil { + postLog.Error(fmt.Sprintf("[ModifyProxyHandler] Failed to query instance: %v", err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to query instance") + return + } + + if instance.UserID != userID { + postLog.Error(fmt.Sprintf("[ModifyProxyHandler] Instance not found for user %d", userID)) + SendErrorResponse(w, http.StatusNotFound, "Instance not found") + return + } + + configContent, err := os.ReadFile(instance.ConfigPath) + if err != nil { + postLog.Error(fmt.Sprintf("[ModifyProxyHandler] Failed to read config file %s: %v", instance.ConfigPath, err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to read config file") + return + } + + updatedContent, err := modifyFrpcProxy(string(configContent), proxyInfo) + if err != nil { + postLog.Error(fmt.Sprintf("[ModifyProxyHandler] Failed to modify proxy: %v", err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to modify proxy") + return + } + + if err := os.WriteFile(instance.ConfigPath, []byte(updatedContent), 0644); err != nil { + postLog.Error(fmt.Sprintf("[ModifyProxyHandler] Failed to write config file %s: %v", instance.ConfigPath, err)) + SendErrorResponse(w, http.StatusInternalServerError, "Failed to write config file") + return + } + + SendSuccessResponse(w, "Proxy modified successfully", map[string]interface{}{ + "instanceID": instance.ID, + "configPath": instance.ConfigPath, + "proxyName": proxyInfo.Name, + }) + postLog.Info(fmt.Sprintf("[ModifyProxyHandler] Proxy %s modified successfully for instance %d", proxyInfo.Name, instance.ID)) +} + func DeleteProxyHandler(w http.ResponseWriter, r *http.Request) { userID, err := Auth(w, r, http.MethodPost, "superuser", "admin") if err != nil { diff --git a/router.go b/router.go index a735a05..fc055d7 100644 --- a/router.go +++ b/router.go @@ -38,6 +38,7 @@ func setupRoutes() { http.HandleFunc("/frpcAct/instanceMgr/getInfo", GetInstanceInfoHandler) http.HandleFunc("/frpcAct/instanceMgr/logs", frpLogger.NewInstanceLogHandler(ValidateTokenFromMap).ServeHTTP) http.HandleFunc("/frpcAct/proxyMgr/create", CreateProxyHandler) + http.HandleFunc("/frpcAct/proxyMgr/modify", ModifyProxyHandler) http.HandleFunc("/frpcAct/proxyMgr/delete", DeleteProxyHandler) http.HandleFunc("/frpcAct/proxyMgr/list", ListProxiesHandler)