Files
-Fonu/internal/api/frpmulti_handler.go
T

1046 lines
35 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package api
import (
"encoding/json"
"errors"
"io"
"log/slog"
"net/http"
"strconv"
"strings"
"time"
"github.com/fonu/fonu/internal/auth"
"github.com/fonu/fonu/internal/frp"
)
// FRPMultiHandler 承载多服务端 FRP 模块的 HTTP 接口(/api/frpmulti/*),
// 包装 internal/frp 的 Service(0.1.25 核心迁移版)。
type FRPMultiHandler struct {
svc *frp.Service
auth *auth.Service
}
type frpServerRequest struct {
AgentURL string `json:"agent_url"`
AgentToken string `json:"agent_token"`
ClearAgentToken bool `json:"clear_agent_token"`
SSHHost string `json:"ssh_host"`
SSHPort int `json:"ssh_port"`
SSHUser string `json:"ssh_user"`
SSHAuth string `json:"ssh_auth"`
SSHSecret string `json:"ssh_secret"`
ClearSSHSecret bool `json:"clear_ssh_secret"`
MgmtEnabled *bool `json:"mgmt_enabled"`
Name string `json:"name"`
ServerAddr string `json:"server_addr"`
ServerPort int `json:"server_port"`
TLSEnabled *bool `json:"tls_enabled"`
TLSServerName string `json:"tls_server_name"`
Enabled *bool `json:"enabled"`
Token string `json:"token"`
ClearToken bool `json:"clear_token"`
Options frp.ServerOptions `json:"options"`
OIDCClientSecret string `json:"oidc_client_secret"`
ClearOIDCClientSecret bool `json:"clear_oidc_client_secret"`
DashboardPassword string `json:"dashboard_password"`
ClearDashboardPassword bool `json:"clear_dashboard_password"`
TLSCertificate string `json:"tls_certificate"`
TLSKey string `json:"tls_key"`
TLSTrustedCA string `json:"tls_trusted_ca"`
ClearTLSCertificate bool `json:"clear_tls_certificate"`
ClearTLSKey bool `json:"clear_tls_key"`
ClearTLSTrustedCA bool `json:"clear_tls_trusted_ca"`
}
type frpProxyRequest struct {
ServerID int64 `json:"server_id"`
Name string `json:"name"`
Type string `json:"type"`
LocalIP string `json:"local_ip"`
LocalPort int `json:"local_port"`
RemotePort *int `json:"remote_port"`
CustomDomains []string `json:"custom_domains"`
HostHeaderRewrite string `json:"host_header_rewrite"`
Options frp.ProxyOptions `json:"options"`
Enabled *bool `json:"enabled"`
Remark string `json:"remark"`
}
type frpDownloadProxyRequest struct {
Proxy string `json:"proxy"`
}
func NewFRPMultiHandler(svc *frp.Service, authSvc *auth.Service) *FRPMultiHandler {
return &FRPMultiHandler{svc: svc, auth: authSvc}
}
func (h *FRPMultiHandler) Status(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, h.svc.Status(r.Context()))
}
func (h *FRPMultiHandler) Runtime(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, h.svc.Runtime(r.Context()))
}
func (h *FRPMultiHandler) SetDownloadProxy(w http.ResponseWriter, r *http.Request) {
var request frpDownloadProxyRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return
}
runtime, err := h.svc.SetDownloadProxy(r.Context(), request.Proxy)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, runtime)
}
func (h *FRPMultiHandler) Releases(w http.ResponseWriter, r *http.Request) {
releases, err := h.svc.ListReleases(r.Context())
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, releases)
}
func (h *FRPMultiHandler) Binaries(w http.ResponseWriter, r *http.Request) {
binaries, err := h.svc.ListBinaries()
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取已下载 FRP 客户端失败")
return
}
writeJSON(w, http.StatusOK, binaries)
}
func (h *FRPMultiHandler) DownloadStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, h.svc.DownloadStatus())
}
func (h *FRPMultiHandler) DownloadRelease(w http.ResponseWriter, r *http.Request) {
version := r.PathValue("version")
task, err := h.svc.StartDownload(version)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusAccepted, task)
}
func (h *FRPMultiHandler) ActivateBinary(w http.ResponseWriter, r *http.Request) {
version := r.PathValue("version")
status, err := h.svc.ActivateBinary(r.Context(), version)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, status)
}
func (h *FRPMultiHandler) DeleteBinary(w http.ResponseWriter, r *http.Request) {
version := r.PathValue("version")
if err := h.svc.DeleteBinary(version); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *FRPMultiHandler) ListServers(w http.ResponseWriter, r *http.Request) {
servers, err := h.svc.ListServers(r.Context())
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取 FRP 服务端失败")
return
}
writeJSON(w, http.StatusOK, servers)
}
func (h *FRPMultiHandler) ServerDiagnostics(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
diagnostics, err := h.svc.ServerDiagnostics(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, diagnostics)
}
func (h *FRPMultiHandler) ServerDetail(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
detail, err := h.svc.ServerDetail(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, detail)
}
func (h *FRPMultiHandler) StartServer(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
server, err := h.svc.StartServer(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) StopServer(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
server, err := h.svc.StopServer(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) CreateServer(w http.ResponseWriter, r *http.Request) {
request, ok := decodeFRPServerRequest(w, r)
if !ok {
return
}
server, err := h.svc.CreateServer(r.Context(), request.serverInput())
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) UpdateServer(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
request, ok := decodeFRPServerRequest(w, r)
if !ok {
return
}
server, err := h.svc.UpdateServer(r.Context(), id, request.serverInput())
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) DeleteServer(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
if err := h.svc.DeleteServer(r.Context(), id); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *FRPMultiHandler) AgentProbe(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
result, err := h.svc.AgentProbe(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
func (h *FRPMultiHandler) AgentInstall(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
server, err := h.svc.AgentInstall(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) AgentUninstall(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
keepData := r.URL.Query().Get("keep_data") == "true"
output, err := h.svc.AgentUninstall(r.Context(), id, keepData)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "output": output})
}
func (h *FRPMultiHandler) AgentSSHDiagnose(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
output, err := h.svc.AgentSSHDiagnose(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"output": output})
}
func (h *FRPMultiHandler) AgentTest(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
if err := h.svc.AgentTest(r.Context(), id); err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (h *FRPMultiHandler) AgentTunnelRestart(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
server, err := h.svc.RestartAgentTunnel(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) SetAgentTransport(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
var request struct {
Transport string `json:"transport"`
Port int `json:"port"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return
}
var server frp.Server
switch strings.TrimSpace(request.Transport) {
case "tunnel":
server, err = h.svc.SwitchAgentToTunnel(r.Context(), id)
case "https-pin":
server, err = h.svc.EnableAgentHTTPS(r.Context(), id, request.Port)
default:
writeError(r, w, http.StatusBadRequest, "不支持的传输方式")
return
}
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
h.audit(r, id, "transport", "切换为 "+server.AgentTransport)
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) AgentTransportStatus(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
status, err := h.svc.AgentTransportStatus(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, status)
}
func (h *FRPMultiHandler) AgentTransportRotateCert(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
server, err := h.svc.RotateAgentCertificate(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
h.audit(r, id, "rotate-tls-cert", server.AgentTLSPin)
writeJSON(w, http.StatusOK, server)
}
func (h *FRPMultiHandler) AgentFirewallStatus(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
port, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("port")))
status, err := h.svc.AgentFirewallStatus(r.Context(), id, port)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, status)
}
func (h *FRPMultiHandler) AgentFirewallAllow(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
var request struct {
Port int `json:"port"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return
}
result, err := h.svc.AllowAgentFirewallPort(r.Context(), id, request.Port)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
h.audit(r, id, "firewall-allow", strconv.Itoa(request.Port))
writeJSON(w, http.StatusOK, result)
}
// audit 记录 Agent 传输配置变更;admin_id 取自登录会话,API Token 调用没有会话时记为 0。
func (h *FRPMultiHandler) audit(r *http.Request, serverID int64, action, detail string) {
adminID := 0
if sessionID, ok := r.Context().Value(sessionContextKey).(string); ok && sessionID != "" && h.auth != nil {
if id, err := h.auth.AdminIDForSession(r.Context(), sessionID); err == nil {
adminID = id
}
}
slog.Default().Info("审计:Agent 配置变更",
"module", "AUDIT",
"action", action,
"server_id", serverID,
"admin_id", adminID,
"detail", detail,
"remote_addr", r.RemoteAddr,
)
}
func (h *FRPMultiHandler) AgentStatus(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
status, err := h.svc.AgentStatus(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, status)
}
func (h *FRPMultiHandler) AgentRouteCandidates(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
proxies, err := h.svc.ListAgentRouteCandidates(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, proxies)
}
func (h *FRPMultiHandler) DeployAgentRoute(w http.ResponseWriter, r *http.Request) {
proxyID, err := parseID(r.PathValue("proxyID"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 规则 ID 无效")
return
}
if err := h.svc.DeployAgentRoute(r.Context(), proxyID); err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "proxy_id": proxyID})
}
func (h *FRPMultiHandler) RemoveAgentRoute(w http.ResponseWriter, r *http.Request) {
proxyID, err := parseID(r.PathValue("proxyID"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 规则 ID 无效")
return
}
if err := h.svc.RemoveAgentRoute(r.Context(), proxyID); err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "proxy_id": proxyID})
}
func (h *FRPMultiHandler) AgentRoutes(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
// 路由状态明细在独立的 /api/v1/routes 端点(status 里的 routes 仍是字符串数组),
// 从 agentClient 直取明细并透传。
client, ok := h.svc.AgentClient(r.Context(), id)
if !ok {
writeError(r, w, http.StatusBadGateway, "公网 agent 未配置或 Token 未保存")
return
}
details, err := client.RoutesDetail(r.Context())
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, details)
}
// agentRouteTarget 解析服务端与规则归属,返回该规则用于公网反代的首个域名。
// status 非 0 表示校验失败,调用方直接把 status 与 errMsg 写回响应。
func (h *FRPMultiHandler) agentRouteTarget(r *http.Request, serverIDRaw, proxyIDRaw string) (serverID, proxyID int64, domain string, status int, errMsg string) {
serverID, err := parseID(serverIDRaw)
if err != nil {
return 0, 0, "", http.StatusBadRequest, "FRP 服务端 ID 无效"
}
proxyID, err = parseID(proxyIDRaw)
if err != nil {
return 0, 0, "", http.StatusBadRequest, "FRP 规则 ID 无效"
}
proxy, err := h.svc.GetProxy(r.Context(), proxyID)
if err != nil || proxy.ServerID != serverID {
return 0, 0, "", http.StatusNotFound, "FRP 规则不存在"
}
if len(proxy.CustomDomains) == 0 {
return 0, 0, "", http.StatusBadRequest, "该规则没有自定义域名"
}
return serverID, proxyID, proxy.CustomDomains[0], 0, ""
}
// AgentRouteConf 返回指定穿透规则(首个域名)当前反代配置内容。
func (h *FRPMultiHandler) AgentRouteConf(w http.ResponseWriter, r *http.Request) {
serverID, proxyID, domain, status, msg := h.agentRouteTarget(r, r.PathValue("id"), r.PathValue("proxyID"))
if status != 0 {
writeError(r, w, status, msg)
return
}
client, ok := h.svc.AgentClient(r.Context(), serverID)
if !ok {
writeError(r, w, http.StatusBadGateway, "公网 agent 未配置或 Token 未保存")
return
}
content, err := client.RouteConf(r.Context(), domain)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"proxy_id": proxyID, "domain": domain, "content": content})
}
// RouteUptime 返回每条反代规则在最近窗口内的可用率与不可用区间。
// 数据来自后台巡检落库的「状态变更」序列,不做现场探测。
func (h *FRPMultiHandler) RouteUptime(w http.ResponseWriter, r *http.Request) {
hours := 24
if raw := strings.TrimSpace(r.URL.Query().Get("hours")); raw != "" {
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 24*7 {
hours = parsed
}
}
stats, err := h.svc.RouteUptime(r.Context(), time.Duration(hours)*time.Hour)
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取可用率失败", err)
return
}
writeJSON(w, http.StatusOK, map[string]any{"hours": hours, "items": stats})
}
// AgentRouteVersions 返回规则首个域名在 VPS 上的当前 vhost 与历史版本,供差异对比与回滚。
func (h *FRPMultiHandler) AgentRouteVersions(w http.ResponseWriter, r *http.Request) {
serverID, _, domain, status, msg := h.agentRouteTarget(r, r.PathValue("id"), r.PathValue("proxyID"))
if status != 0 {
writeError(r, w, status, msg)
return
}
client, ok := h.svc.AgentClient(r.Context(), serverID)
if !ok {
writeError(r, w, http.StatusBadGateway, "公网 agent 未配置或 Token 未保存")
return
}
result, err := client.RouteVersions(r.Context(), domain)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
// AgentRouteVersion 读取某个历史版本的内容(与当前 vhost 对比用)。
func (h *FRPMultiHandler) AgentRouteVersion(w http.ResponseWriter, r *http.Request) {
serverID, _, domain, status, msg := h.agentRouteTarget(r, r.PathValue("id"), r.PathValue("proxyID"))
if status != 0 {
writeError(r, w, status, msg)
return
}
client, ok := h.svc.AgentClient(r.Context(), serverID)
if !ok {
writeError(r, w, http.StatusBadGateway, "公网 agent 未配置或 Token 未保存")
return
}
result, err := client.RouteVersion(r.Context(), domain, r.PathValue("name"))
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
// AgentRollbackRouteVersion 让 VPS 上的 vhost 回退到指定版本(agent 侧校验不过会保持当前配置)。
func (h *FRPMultiHandler) AgentRollbackRouteVersion(w http.ResponseWriter, r *http.Request) {
serverID, _, domain, status, msg := h.agentRouteTarget(r, r.PathValue("id"), r.PathValue("proxyID"))
if status != 0 {
writeError(r, w, status, msg)
return
}
client, ok := h.svc.AgentClient(r.Context(), serverID)
if !ok {
writeError(r, w, http.StatusBadGateway, "公网 agent 未配置或 Token 未保存")
return
}
result, err := client.RollbackRouteVersion(r.Context(), domain, r.PathValue("name"))
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
// AgentInstallNginx 触发 VPS 上一键安装 Nginx(需 agent 以 root 运行)。
func (h *FRPMultiHandler) AgentInstallNginx(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
result, err := h.svc.InstallAgentNginx(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
// AgentRouteHealth 返回指定服务端公网反代规则的四段健康状态(DNS/证书/隧道/内网服务)。
func (h *FRPMultiHandler) AgentRouteHealth(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
health, err := h.svc.RouteHealth(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, health)
}
// AgentRouteHealthSummary 汇总所有已启用服务端的公网反代健康状态(仪表盘用,避免前端逐台请求)
func (h *FRPMultiHandler) AgentRouteHealthSummary(w http.ResponseWriter, r *http.Request) {
summary, err := h.svc.SummarizeRouteHealth(r.Context())
if err != nil {
writeError(r, w, http.StatusInternalServerError, "汇总公网反代健康失败")
return
}
writeJSON(w, http.StatusOK, summary)
}
// AgentRouteDrift 比对库里的期望与 VPS 上的实际配置,返回部署漂移清单
func (h *FRPMultiHandler) AgentRouteDrift(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
report, err := h.svc.RouteDrift(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, report)
}
// AgentRedeployRoutes 把该服务端上所有启用中的公网反代规则重新下发一遍
func (h *FRPMultiHandler) AgentRedeployRoutes(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
result, err := h.svc.RedeployAllRoutes(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
// AgentMetrics 返回 VPS 主机资源(需 agent ≥ 0.10.0)
func (h *FRPMultiHandler) AgentMetrics(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
metrics, err := h.svc.AgentMetrics(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, metrics)
}
// AgentNginxLogs 读取 VPS 上 Nginx 的 access / error 日志尾部(需 agent ≥ 0.10.0)
func (h *FRPMultiHandler) AgentNginxLogs(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
lines, _ := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("lines")))
logs, err := h.svc.AgentNginxLogs(r.Context(), id, strings.TrimSpace(r.URL.Query().Get("type")), lines)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, logs)
}
// AgentFRPSConfig 回读 VPS 上的 frps.toml(需 agent ≥ 0.10.0)
func (h *FRPMultiHandler) AgentFRPSConfig(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
config, err := h.svc.AgentFRPSConfig(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, config)
}
// AgentCerts 列出 VPS 证书目录下的域名与到期时间(需 agent ≥ 0.11.0)
func (h *FRPMultiHandler) AgentCerts(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
certs, err := h.svc.AgentCerts(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, certs)
}
// AgentNginxReload 显式校验并重载 VPS 上的 Nginx(需 agent ≥ 0.11.0)
func (h *FRPMultiHandler) AgentNginxReload(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
result, err := h.svc.AgentNginxReload(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
func (h *FRPMultiHandler) AgentFRPSAction(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
var body struct {
Action string `json:"action"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return
}
if err := h.svc.AgentFRPSAction(r.Context(), id, body.Action); err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "action": body.Action})
}
func (h *FRPMultiHandler) InstallFRPSOnAgent(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
var body struct {
Version string `json:"version"`
Proxy string `json:"proxy"`
}
// 允许空请求体(走默认版本),但 JSON 语法错误要报 400
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return
}
if body.Version == "" {
body.Version = "0.71.0"
}
result, err := h.svc.InstallFRPSOnAgent(r.Context(), id, body.Version, body.Proxy)
if err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, result)
}
func (h *FRPMultiHandler) PushFRPSConfigToAgent(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
if err := h.svc.PushFRPSConfigToAgent(r.Context(), id); err != nil {
writeError(r, w, http.StatusBadGateway, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (h *FRPMultiHandler) TestServer(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
if err := h.svc.Test(r.Context(), id); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"message": "FRP 配置校验通过"})
}
func (h *FRPMultiHandler) ListProxies(w http.ResponseWriter, r *http.Request) {
proxies, err := h.svc.ListProxies(r.Context())
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取 FRP 规则失败")
return
}
writeJSON(w, http.StatusOK, proxies)
}
// ListProxiesTraffic 返回各 frps 管理接口采集到的穿透规则流量(含速率)。
func (h *FRPMultiHandler) ListProxiesTraffic(w http.ResponseWriter, r *http.Request) {
traffic, err := h.svc.ListProxiesTraffic(r.Context())
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取 FRP 规则流量失败")
return
}
writeJSON(w, http.StatusOK, traffic)
}
func (h *FRPMultiHandler) CreateProxy(w http.ResponseWriter, r *http.Request) {
request, ok := decodeFRPProxyRequest(w, r)
if !ok {
return
}
proxy, err := h.svc.CreateProxy(r.Context(), request.proxyInput())
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, proxy)
}
func (h *FRPMultiHandler) UpdateProxy(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 规则 ID 无效")
return
}
request, ok := decodeFRPProxyRequest(w, r)
if !ok {
return
}
proxy, err := h.svc.UpdateProxy(r.Context(), id, request.proxyInput())
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, proxy)
}
func (h *FRPMultiHandler) DeleteProxy(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 规则 ID 无效")
return
}
if err := h.svc.DeleteProxy(r.Context(), id); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *FRPMultiHandler) Start(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Start(r.Context()); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, h.svc.Status(r.Context()))
}
func (h *FRPMultiHandler) Stop(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Stop(r.Context()); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, h.svc.Status(r.Context()))
}
func (h *FRPMultiHandler) Reload(w http.ResponseWriter, r *http.Request) {
if err := h.svc.Reload(r.Context()); err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, h.svc.Status(r.Context()))
}
func (h *FRPMultiHandler) Logs(w http.ResponseWriter, r *http.Request) {
lines, err := h.svc.ReadLogs(queryInt(r, "limit", 200), int64(queryInt(r, "server_id", 0)))
if err != nil {
writeError(r, w, http.StatusInternalServerError, "读取 FRP 日志失败")
return
}
writeJSON(w, http.StatusOK, lines)
}
// ProxyLogs 返回单条穿透规则的日志(按规则名过滤所属服务端 frpc 日志)。
func (h *FRPMultiHandler) ProxyLogs(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 规则 ID 无效")
return
}
lines, err := h.svc.ReadProxyLogs(r.Context(), id, queryInt(r, "limit", 200))
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, lines)
}
func (h *FRPMultiHandler) ExportConfig(w http.ResponseWriter, r *http.Request) {
content, err := h.svc.ExportMaskedConfig(r.Context())
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"content": content})
}
// FRPSServerConfig 返回指定服务端的 frps.toml 同源配置(bindPort/auth/webServer 与 frpc 侧一致)。
func (h *FRPMultiHandler) FRPSServerConfig(w http.ResponseWriter, r *http.Request) {
id, err := parseID(r.PathValue("id"))
if err != nil {
writeError(r, w, http.StatusBadRequest, "FRP 服务端 ID 无效")
return
}
content, err := h.svc.FRPSServerConfig(r.Context(), id)
if err != nil {
writeError(r, w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"content": content})
}
func decodeFRPServerRequest(w http.ResponseWriter, r *http.Request) (frpServerRequest, bool) {
var request frpServerRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return frpServerRequest{}, false
}
return request, true
}
func decodeFRPProxyRequest(w http.ResponseWriter, r *http.Request) (frpProxyRequest, bool) {
var request frpProxyRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
writeError(r, w, http.StatusBadRequest, "请求格式无效")
return frpProxyRequest{}, false
}
return request, true
}
func (request frpServerRequest) serverInput() frp.ServerInput {
return frp.ServerInput{
AgentURL: request.AgentURL, AgentToken: request.AgentToken, ClearAgentToken: request.ClearAgentToken,
SSHHost: request.SSHHost, SSHPort: request.SSHPort, SSHUser: request.SSHUser, SSHAuth: request.SSHAuth,
SSHSecret: request.SSHSecret, ClearSSHSecret: request.ClearSSHSecret, MgmtEnabled: boolDefault(request.MgmtEnabled, false),
Name: request.Name, ServerAddr: request.ServerAddr, ServerPort: request.ServerPort,
TLS: boolDefault(request.TLSEnabled, true), TLSServerName: request.TLSServerName,
Enabled: boolDefault(request.Enabled, true), Token: request.Token, ClearToken: request.ClearToken, Options: request.Options,
OIDCClientSecret: request.OIDCClientSecret, ClearOIDCClientSecret: request.ClearOIDCClientSecret,
DashboardPassword: request.DashboardPassword, ClearDashboardPassword: request.ClearDashboardPassword,
TLSCertificate: request.TLSCertificate, TLSKey: request.TLSKey, TLSTrustedCA: request.TLSTrustedCA,
ClearTLSCertificate: request.ClearTLSCertificate, ClearTLSKey: request.ClearTLSKey, ClearTLSTrustedCA: request.ClearTLSTrustedCA,
}
}
func (request frpProxyRequest) proxyInput() frp.ProxyInput {
return frp.ProxyInput{
ServerID: request.ServerID, Name: request.Name, Type: request.Type, LocalIP: request.LocalIP,
LocalPort: request.LocalPort, RemotePort: request.RemotePort, CustomDomains: request.CustomDomains,
HostHeaderRewrite: request.HostHeaderRewrite, Options: request.Options, Enabled: boolDefault(request.Enabled, true), Remark: request.Remark,
}
}