From 26c93e5ea39ff71cbcee574881479c9af6ebe5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=8B=E7=94=B0=E5=BC=98?= <1240092443@qq.com> Date: Sat, 11 Jan 2025 17:42:44 +0800 Subject: [PATCH] =?UTF-8?q?add:=E6=96=B0=E5=A2=9E=E8=BF=9C=E7=A8=8B?= =?UTF-8?q?=E4=BB=A3=E7=90=86frpc=E6=8E=A5=E5=8F=A3=EF=BC=8C=E5=89=8D?= =?UTF-8?q?=E7=AB=AFui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components.d.ts | 15 + frontend/src/components/builtin/FileList.vue | 1 + .../src/components/setting/NetProxy copy.vue | 204 +++++ frontend/src/components/setting/NetProxy.vue | 744 +++++++++++++----- frontend/src/stores/proxy.ts | 182 +++++ godo/cmd/main.go | 7 + godo/go.mod | 1 + godo/go.sum | 2 + godo/proxy/frpc.go | 435 +++++++++- 9 files changed, 1355 insertions(+), 236 deletions(-) create mode 100644 frontend/src/components/setting/NetProxy copy.vue create mode 100644 frontend/src/stores/proxy.ts diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 37699f1..2e1dbfa 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -49,6 +49,7 @@ declare module 'vue' { Computer: typeof import('./src/components/computer/Computer.vue')['default'] ComputerNavBar: typeof import('./src/components/computer/ComputerNavBar.vue')['default'] ContextMenu: typeof import('./src/components/builtin/ContextMenu.vue')['default'] + copy: typeof import('./src/components/setting/NetProxy copy.vue')['default'] CreateUrl: typeof import('./src/components/builtin/CreateUrl.vue')['default'] CustomIcon: typeof import('./src/components/taskbar/CustomIcon.vue')['default'] DateNote: typeof import('./src/components/builtin/DateNote.vue')['default'] @@ -72,28 +73,41 @@ declare module 'vue' { ElCarousel: typeof import('element-plus/es')['ElCarousel'] ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem'] ElCol: typeof import('element-plus/es')['ElCol'] + ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] ElContainer: typeof import('element-plus/es')['ElContainer'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElFor: typeof import('element-plus/es')['ElFor'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElHeader: typeof import('element-plus/es')['ElHeader'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElImage: typeof import('element-plus/es')['ElImage'] + ElIn: typeof import('element-plus/es')['ElIn'] ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElItem: typeof import('element-plus/es')['ElItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPageHeader: typeof import('element-plus/es')['ElPageHeader'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopover: typeof import('element-plus/es')['ElPopover'] ElProgress: typeof import('element-plus/es')['ElProgress'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSpace: typeof import('element-plus/es')['ElSpace'] ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] ElTag: typeof import('element-plus/es')['ElTag'] ElText: typeof import('element-plus/es')['ElText'] + ElTo: typeof import('element-plus/es')['ElTo'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] Error: typeof import('./src/components/taskbar/Error.vue')['default'] FileIcon: typeof import('./src/components/builtin/FileIcon.vue')['default'] @@ -127,6 +141,7 @@ declare module 'vue' { MusicViewer: typeof import('./src/components/builtin/MusicViewer.vue')['default'] NasClient: typeof import('./src/components/setting/NasClient.vue')['default'] NetProxy: typeof import('./src/components/setting/NetProxy.vue')['default'] + 'NetProxy copy': typeof import('./src/components/setting/NetProxy copy.vue')['default'] NetWork: typeof import('./src/components/taskbar/NetWork.vue')['default'] NetworkPop: typeof import('./src/components/taskbar/NetworkPop.vue')['default'] NotFound: typeof import('./src/components/window/NotFound.vue')['default'] diff --git a/frontend/src/components/builtin/FileList.vue b/frontend/src/components/builtin/FileList.vue index b5083c2..7e33a01 100644 --- a/frontend/src/components/builtin/FileList.vue +++ b/frontend/src/components/builtin/FileList.vue @@ -52,6 +52,7 @@ } " > +
diff --git a/frontend/src/components/setting/NetProxy copy.vue b/frontend/src/components/setting/NetProxy copy.vue new file mode 100644 index 0000000..95cf01f --- /dev/null +++ b/frontend/src/components/setting/NetProxy copy.vue @@ -0,0 +1,204 @@ + + \ No newline at end of file diff --git a/frontend/src/components/setting/NetProxy.vue b/frontend/src/components/setting/NetProxy.vue index 95cf01f..2e125d7 100644 --- a/frontend/src/components/setting/NetProxy.vue +++ b/frontend/src/components/setting/NetProxy.vue @@ -1,204 +1,546 @@ + \ No newline at end of file +
+ + + + + + + + + + + + + + + + + + + + + {{ type }} + + + + + + + + + + + + + + + + 保存 + 取消 + + + +
+ + + diff --git a/frontend/src/stores/proxy.ts b/frontend/src/stores/proxy.ts new file mode 100644 index 0000000..69dbe18 --- /dev/null +++ b/frontend/src/stores/proxy.ts @@ -0,0 +1,182 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; + +export interface ProxyItem { + port: string; + domain: string; + type: string; + name: string; + serverAddr: string; + serverPort: string; + httpAuth: boolean; + authUsername: string; + authPassword: string; + https2http: boolean; + https2httpCaFile: string; + https2httpKeyFile: string; + remotePort?: string; + stcpModel?: string; + secretKey?: string; + visitedName?: string; + bindAddr?: string; + bindPort?: string; + fallbackTo?: string; + fallbackTimeoutMs?: number; + keepAlive?: boolean; +} + +export const useProxyStore = defineStore('proxyStore', () => { + const localKey = "godoos_net_proxy"; + + const proxies = ref(getProxies()); + const customDomains = ref([""]); + const proxyData = ref(createNewProxyData()); + + function createNewProxyData(): ProxyItem { + return { + type: "http", + name: "", + port: "", + domain: "", + serverAddr: "", + serverPort: "", + httpAuth: false, + authUsername: "", + authPassword: "", + https2http: false, + https2httpCaFile: "", + https2httpKeyFile: "", + remotePort: "", + stcpModel: "", + secretKey: "", + visitedName: "", + bindAddr: "", + bindPort: "", + fallbackTo: "", + fallbackTimeoutMs: 0, + keepAlive: false, + }; + } + + function getProxies(): ProxyItem[] { + const proxies = localStorage.getItem(localKey); + return proxies ? JSON.parse(proxies) : []; + } + + function saveProxies(proxies: ProxyItem[]) { + localStorage.setItem(localKey, JSON.stringify(proxies)); + } + + const addProxy = (proxy: ProxyItem) => { + proxies.value.push(proxy); + saveProxies(proxies.value); + }; + + const updateProxy = (updatedProxy: ProxyItem) => { + const index = proxies.value.findIndex(p => p.id === updatedProxy.id); + if (index !== -1) { + proxies.value[index] = updatedProxy; + saveProxies(proxies.value); + } + }; + + const deleteProxy = (id: number) => { + proxies.value = proxies.value.filter(p => p.id !== id); + saveProxies(proxies.value); + }; + + const addCustomDomain = () => { + customDomains.value.push(""); + }; + + const removeCustomDomain = (index: number) => { + if (customDomains.value.length > 1) { + customDomains.value.splice(index, 1); + } + }; + + const resetProxyData = () => { + proxyData.value = createNewProxyData(); + }; + + const createFrpcConfig = async () => { + const url = "http://localhost:56780/frpc/create"; + const requestData = { + config: { + ServerAddr: proxyData.value.serverAddr, + ServerPort: Number(proxyData.value.serverPort), + AuthMethod: "token", + AuthToken: "your-auth-token", + User: "your-username", + MetaToken: "your-meta-token", + TransportHeartbeatInterval: 30, + TransportHeartbeatTimeout: 90, + LogLevel: "info", + LogMaxDays: 3, + WebPort: 7500, + TlsConfigEnable: true, + TlsConfigCertFile: "/path/to/cert/file", + TlsConfigKeyFile: "/path/to/key/file", + TlsConfigTrustedCaFile: "/path/to/ca/file", + TlsConfigServerName: "server-name", + ProxyConfigEnable: true, + ProxyConfigProxyUrl: "http://proxy.example.com" + }, + proxies: [ + { + Name: proxyData.value.name, + Type: proxyData.value.type, + LocalIp: proxyData.value.serverAddr, + LocalPort: Number(proxyData.value.port), + RemotePort: Number(proxyData.value.remotePort), + CustomDomains: customDomains.value, + Subdomain: proxyData.value.domain, + BasicAuth: proxyData.value.httpAuth, + HttpUser: proxyData.value.authUsername, + HttpPassword: proxyData.value.authPassword, + StcpModel: proxyData.value.stcpModel, + ServerName: "server1", + BindAddr: proxyData.value.bindAddr, + BindPort: Number(proxyData.value.bindPort), + FallbackTo: proxyData.value.fallbackTo, + FallbackTimeoutMs: Number(proxyData.value.fallbackTimeoutMs), + SecretKey: proxyData.value.secretKey + } + ] + }; + + try { + const response = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(requestData) + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + console.log("Response data:", data); + // 可以在这里添加成功通知 + } catch (error) { + console.error("Error:", error); + // 可以在这里添加错误通知 + } + }; + + return { + proxies, + customDomains, + proxyData, + addProxy, + updateProxy, + deleteProxy, + addCustomDomain, + removeCustomDomain, + resetProxyData, + createFrpcConfig, + }; +}); \ No newline at end of file diff --git a/godo/cmd/main.go b/godo/cmd/main.go index 92e2758..0610eae 100644 --- a/godo/cmd/main.go +++ b/godo/cmd/main.go @@ -168,6 +168,13 @@ func OsStart() { proxyRouter.HandleFunc("/local/get", proxy.GetLocalProxyHandler).Methods(http.MethodGet) proxyRouter.HandleFunc("/local/list", proxy.GetLocalProxiesHandler).Methods(http.MethodGet) proxyRouter.HandleFunc("/local/update", proxy.UpdateLocalProxyHandler).Methods(http.MethodPost) + // 注册frpc路由 + frpcRouter := router.PathPrefix("/frpc").Subrouter() + frpcRouter.HandleFunc("/create", proxy.CreateFrpcHandler).Methods(http.MethodPost) + frpcRouter.HandleFunc("/delete", proxy.DeleteFrpcConfigHandler).Methods(http.MethodGet) // 删除frpc配置 + frpcRouter.HandleFunc("/get", proxy.GetFrpcConfigHandler).Methods(http.MethodGet) // 获取frpc配置 + frpcRouter.HandleFunc("/list", proxy.GetFrpcProxiesHandler).Methods(http.MethodGet) // 获取frpc代理列表 + frpcRouter.HandleFunc("/update", proxy.UpdateFrpcConfigHandler).Methods(http.MethodPost) // 更新frpc配置 // 注册根路径的处理函数 distFS, _ := fs.Sub(deps.Frontendassets, "dist") diff --git a/godo/go.mod b/godo/go.mod index 68081cc..52abe2d 100644 --- a/godo/go.mod +++ b/godo/go.mod @@ -46,4 +46,5 @@ require ( github.com/tklauser/numcpus v0.9.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect golang.org/x/crypto v0.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 ) diff --git a/godo/go.sum b/godo/go.sum index 69dd19f..a8361de 100644 --- a/godo/go.sum +++ b/godo/go.sum @@ -150,6 +150,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= diff --git a/godo/proxy/frpc.go b/godo/proxy/frpc.go index 2c5083f..bc119d4 100644 --- a/godo/proxy/frpc.go +++ b/godo/proxy/frpc.go @@ -1,51 +1,74 @@ package proxy import ( + "encoding/json" "fmt" + "godo/libs" + "net/http" + "os" + "path/filepath" "strings" + + "gopkg.in/ini.v1" ) // FrpConfig 结构体用于存储 FRP 配置 type FrpConfig struct { - ServerAddr string - ServerPort int - AuthMethod string - AuthToken string - User string - MetaToken string - TransportHeartbeatInterval int - TransportHeartbeatTimeout int - LogLevel string - LogMaxDays int - WebPort int - TlsConfigEnable bool - TlsConfigCertFile string - TlsConfigKeyFile string - TlsConfigTrustedCaFile string - TlsConfigServerName string - ProxyConfigEnable bool - ProxyConfigProxyUrl string + ServerAddr string // 服务器地址 + ServerPort int // 服务器端口 + AuthMethod string // 认证方法,例如 "token" 或 "multiuser" + AuthToken string // 认证令牌,当 AuthMethod 为 "token" 时使用 + User string // 用户名,当 AuthMethod 为 "multiuser" 时使用 + MetaToken string // 元数据令牌,当 AuthMethod 为 "multiuser" 时使用 + TransportHeartbeatInterval int // 传输心跳间隔(秒) + TransportHeartbeatTimeout int // 传输心跳超时(秒) + LogLevel string // 日志级别,例如 "info"、"warn"、"error" + LogMaxDays int // 日志保留天数 + WebPort int // Web 管理界面的端口 + TlsConfigEnable bool // 是否启用 TLS 配置 + TlsConfigCertFile string // TLS 证书文件路径 + TlsConfigKeyFile string // TLS 密钥文件路径 + TlsConfigTrustedCaFile string // TLS 信任的 CA 文件路径 + TlsConfigServerName string // TLS 服务器名称 + ProxyConfigEnable bool // 是否启用代理配置 + ProxyConfigProxyUrl string // 代理 URL } // Proxy 结构体用于存储代理配置 type Proxy struct { - Name string - Type string - LocalIp string - LocalPort int - RemotePort int - CustomDomains []string - Subdomain string - BasicAuth bool - HttpUser string - HttpPassword string - StcpModel string - ServerName string - BindAddr string - BindPort int - FallbackTo string - FallbackTimeoutMs int - SecretKey string + Name string `json:"name"` // 代理名称 + Type string `json:"type"` // 代理类型,例如 "tcp"、"http"、"stcp" + LocalIp string `json:"localIp"` // 本地 IP 地址 + LocalPort int `json:"localPort"` // 本地端口 + RemotePort int `json:"remotePort"` // 远程端口 + CustomDomains []string `json:"customDomains"` // 自定义域名列表 + Subdomain string `json:"subdomain"` // 子域名 + BasicAuth bool `json:"basicAuth"` // 是否启用基本认证 + HttpUser string `json:"httpUser"` // HTTP 基本认证用户名 + HttpPassword string `json:"httpPassword"` // HTTP 基本认证密码 + StcpModel string `json:"stcpModel"` // STCP 模式,例如 "visitors" 或 "visited" + ServerName string `json:"serverName"` // 服务器名称 + BindAddr string `json:"bindAddr"` // 绑定地址 + BindPort int `json:"bindPort"` // 绑定端口 + FallbackTo string `json:"fallbackTo"` // 回退到的目标 + FallbackTimeoutMs int `json:"fallbackTimeoutMs"` // 回退超时时间(毫秒) + SecretKey string `json:"secretKey"` // 密钥,用于加密通信 +} + +type Response struct { + Code string `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` + Success bool `json:"success"` +} + +func NewResponse(code string, message string, data interface{}) Response { + return Response{ + Code: code, + Message: message, + Data: data, + Success: true, + } } // isRangePort 检查端口是否为范围端口 @@ -283,3 +306,345 @@ httpProxy = "%s" return ini } + +// CreateFrpcHandler 创建frpc配置 +func CreateFrpcHandler(w http.ResponseWriter, r *http.Request) { + var requestData struct { + Config FrpConfig `json:"config"` + Proxies []Proxy `json:"proxies"` + } + + err := json.NewDecoder(r.Body).Decode(&requestData) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + // 读取现有配置 + path := libs.GetAppExecDir() + filePath := filepath.Join(path, "frpc", "frpc.ini") + + var existingProxies []Proxy + if iniContent, err := os.ReadFile(filePath); err == nil { + cfg, err := ini.Load(iniContent) + if err == nil { + for _, section := range cfg.Sections() { + if section.Name() == "common" { + continue + } + proxy := Proxy{ + Name: section.Name(), + Type: section.Key("type").String(), + LocalIp: section.Key("localIP").String(), + LocalPort: section.Key("localPort").MustInt(), + RemotePort: section.Key("remotePort").MustInt(), + CustomDomains: strings.Split(section.Key("custom_domains").String(), ","), + Subdomain: section.Key("subdomain").String(), + BasicAuth: section.Key("httpUser").String() != "", + HttpUser: section.Key("httpUser").String(), + HttpPassword: section.Key("httpPassword").String(), + StcpModel: section.Key("role").String(), + ServerName: section.Key("serverName").String(), + BindAddr: section.Key("bindAddr").String(), + BindPort: section.Key("bindPort").MustInt(), + FallbackTo: section.Key("fallbackTo").String(), + FallbackTimeoutMs: section.Key("fallbackTimeoutMs").MustInt(), + SecretKey: section.Key("sk").String(), + } + existingProxies = append(existingProxies, proxy) + } + } + } + + // 合并现有代理和新代理 + proxyMap := make(map[string]Proxy) + for _, proxy := range existingProxies { + proxyMap[proxy.Name] = proxy + } + + for _, proxy := range requestData.Proxies { + proxyMap[proxy.Name] = proxy // 新代理会覆盖同 Name 的旧代理 + } + + var allProxies []Proxy + for _, proxy := range proxyMap { + allProxies = append(allProxies, proxy) + } + + // 生成新的配置内容 + iniContent := GenFrpcIniConfig(requestData.Config, allProxies) + + if _, err := os.Stat(filepath.Join(path, "frpc")); os.IsNotExist(err) { + os.MkdirAll(filepath.Join(path, "frpc"), 0755) + } + + err = os.WriteFile(filePath, []byte(iniContent), 0644) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(NewResponse("0", "Configuration created successfully", nil)) +} + +// GetFrpcConfigHandler 处理获取单个 FRPC 配置的 HTTP 请求 +func GetFrpcConfigHandler(w http.ResponseWriter, r *http.Request) { + // 获取请求参数中的 name + name := r.URL.Query().Get("name") + if name == "" { + http.Error(w, "Name parameter is required", http.StatusBadRequest) + return + } + + // 使用 os.ReadFile 读取配置文件内容 + path := libs.GetAppExecDir() + filePath := filepath.Join(path, "frpc", "frpc.ini") + + iniContent, err := os.ReadFile(filePath) + if err != nil { + http.Error(w, "Failed to read configuration file", http.StatusInternalServerError) + return + } + + // 解析 ini 文件 + cfg, err := ini.Load(iniContent) + if err != nil { + http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError) + return + } + + // 提取 [common] 部分 + commonSection := cfg.Section("common") + frpConfig := FrpConfig{ + ServerAddr: commonSection.Key("serverAddr").String(), + ServerPort: commonSection.Key("serverPort").MustInt(), + AuthMethod: commonSection.Key("authenticationMethod").String(), + AuthToken: commonSection.Key("token").String(), + User: commonSection.Key("user").String(), + MetaToken: commonSection.Key("metaToken").String(), + TransportHeartbeatInterval: commonSection.Key("heartbeatInterval").MustInt(), + TransportHeartbeatTimeout: commonSection.Key("heartbeatTimeout").MustInt(), + LogLevel: commonSection.Key("logLevel").String(), + LogMaxDays: commonSection.Key("logMaxDays").MustInt(), + WebPort: commonSection.Key("adminPort").MustInt(), + TlsConfigEnable: commonSection.Key("tlsEnable").MustBool(), + TlsConfigCertFile: commonSection.Key("tlsCertFile").String(), + TlsConfigKeyFile: commonSection.Key("tlsKeyFile").String(), + TlsConfigTrustedCaFile: commonSection.Key("tlsTrustedCaFile").String(), + TlsConfigServerName: commonSection.Key("tlsServerName").String(), + ProxyConfigEnable: commonSection.Key("httpProxy").String() != "", + ProxyConfigProxyUrl: commonSection.Key("httpProxy").String(), + } + + // 查找指定 name 的代理 + var proxy *Proxy + for _, section := range cfg.Sections() { + if section.Name() == "common" { + continue + } + if section.Name() == name { + proxy = &Proxy{ + Name: section.Name(), + Type: section.Key("type").String(), + LocalIp: section.Key("localIP").String(), + LocalPort: section.Key("localPort").MustInt(), + RemotePort: section.Key("remotePort").MustInt(), + // 继续提取其他字段... + } + break + } + } + + if proxy == nil { + http.Error(w, "Proxy not found", http.StatusNotFound) + return + } + + // 创建响应 + responseData := struct { + Config FrpConfig `json:"config"` + Proxy Proxy `json:"proxy"` + }{ + Config: frpConfig, + Proxy: *proxy, + } + + response := NewResponse("0", "Configuration retrieved successfully", responseData) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// GetFrpcProxiesHandler 获取 FRPC 代理列表 +func GetFrpcProxiesHandler(w http.ResponseWriter, r *http.Request) { + // 使用 os.ReadFile 读取配置文件内容 + path := libs.GetAppExecDir() + filePath := filepath.Join(path, "frpc", "frpc.ini") + + iniContent, err := os.ReadFile(filePath) + if err != nil { + http.Error(w, "Failed to read configuration file", http.StatusInternalServerError) + return + } + + // 解析 ini 文件 + cfg, err := ini.Load(iniContent) + if err != nil { + http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError) + return + } + + // 提取代理部分 + var proxies []Proxy + for _, section := range cfg.Sections() { + if section.Name() == "common" { + continue + } + proxy := Proxy{ + Name: section.Name(), + Type: section.Key("type").String(), + LocalIp: section.Key("localIP").String(), + LocalPort: section.Key("localPort").MustInt(), + RemotePort: section.Key("remotePort").MustInt(), + CustomDomains: strings.Split(section.Key("custom_domains").String(), ","), + Subdomain: section.Key("subdomain").String(), + BasicAuth: section.Key("httpUser").String() != "", + HttpUser: section.Key("httpUser").String(), + HttpPassword: section.Key("httpPassword").String(), + StcpModel: section.Key("role").String(), + ServerName: section.Key("serverName").String(), + BindAddr: section.Key("bindAddr").String(), + BindPort: section.Key("bindPort").MustInt(), + FallbackTo: section.Key("fallbackTo").String(), + FallbackTimeoutMs: section.Key("fallbackTimeoutMs").MustInt(), + SecretKey: section.Key("sk").String(), + } + proxies = append(proxies, proxy) + } + + // 创建响应 + response := NewResponse("0", "Proxies retrieved successfully", proxies) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func DeleteFrpcConfigHandler(w http.ResponseWriter, r *http.Request) { + // 获取请求参数中的 name + name := r.URL.Query().Get("name") + if name == "" { + http.Error(w, "Name parameter is required", http.StatusBadRequest) + return + } + + // 使用 os.ReadFile 读取配置文件内容 + path := libs.GetAppExecDir() + filePath := filepath.Join(path, "frpc", "frpc.ini") + + iniContent, err := os.ReadFile(filePath) + if err != nil { + http.Error(w, "Failed to read configuration file", http.StatusInternalServerError) + return + } + + // 解析 ini 文件 + cfg, err := ini.Load(iniContent) + if err != nil { + http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError) + return + } + + // 检查是否存在指定 name 的代理 + if cfg.Section(name) == nil { + http.Error(w, "Proxy not found", http.StatusNotFound) + return + } + + // 删除指定 name 的代理 + cfg.DeleteSection(name) + + // 将更新后的配置写回文件 + err = cfg.SaveTo(filePath) + if err != nil { + http.Error(w, "Failed to save configuration file", http.StatusInternalServerError) + return + } + + // 创建响应 + response := NewResponse("0", "Configuration deleted successfully", nil) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +func UpdateFrpcConfigHandler(w http.ResponseWriter, r *http.Request) { + // 获取请求参数中的 name + name := r.URL.Query().Get("name") + if name == "" { + http.Error(w, "Name parameter is required", http.StatusBadRequest) + return + } + + // 解析请求体中的更新数据 + var updateData Proxy + err := json.NewDecoder(r.Body).Decode(&updateData) + if err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // 使用 os.ReadFile 读取配置文件内容 + path := libs.GetAppExecDir() + filePath := filepath.Join(path, "frpc", "frpc.ini") + + iniContent, err := os.ReadFile(filePath) + if err != nil { + http.Error(w, "Failed to read configuration file", http.StatusInternalServerError) + return + } + + // 解析 ini 文件 + cfg, err := ini.Load(iniContent) + if err != nil { + http.Error(w, "Failed to parse configuration file", http.StatusInternalServerError) + return + } + + // 查找指定 name 的代理 + section := cfg.Section(name) + if section == nil { + http.Error(w, "Proxy not found", http.StatusNotFound) + return + } + + // 更新代理配置 + section.Key("type").SetValue(updateData.Type) + section.Key("localIP").SetValue(updateData.LocalIp) + section.Key("localPort").SetValue(fmt.Sprintf("%d", updateData.LocalPort)) + section.Key("remotePort").SetValue(fmt.Sprintf("%d", updateData.RemotePort)) + section.Key("custom_domains").SetValue(strings.Join(updateData.CustomDomains, ",")) + section.Key("subdomain").SetValue(updateData.Subdomain) + section.Key("httpUser").SetValue(updateData.HttpUser) + section.Key("httpPassword").SetValue(updateData.HttpPassword) + section.Key("role").SetValue(updateData.StcpModel) + section.Key("serverName").SetValue(updateData.ServerName) + section.Key("bindAddr").SetValue(updateData.BindAddr) + section.Key("bindPort").SetValue(fmt.Sprintf("%d", updateData.BindPort)) + section.Key("fallbackTo").SetValue(updateData.FallbackTo) + section.Key("fallbackTimeoutMs").SetValue(fmt.Sprintf("%d", updateData.FallbackTimeoutMs)) + section.Key("sk").SetValue(updateData.SecretKey) + + // 将更新后的配置写回文件 + err = cfg.SaveTo(filePath) + if err != nil { + http.Error(w, "Failed to save configuration file", http.StatusInternalServerError) + return + } + + // 创建响应 + response := NewResponse("0", "Configuration updated successfully", nil) + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +}