Files
frps-console/api.go
T

507 lines
14 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 main
import (
"bytes"
"embed"
"encoding/json"
"io/fs"
"net"
"net/http"
"strconv"
"text/template"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
//go:embed static/*
var staticFS embed.FS
func SetupRouter() *gin.Engine {
r := gin.Default()
// 从 embed 读取前端静态文件
staticSubFS, _ := fs.Sub(staticFS, "static")
r.StaticFS("/static", http.FS(staticSubFS))
// 根路由
r.GET("/", func(c *gin.Context) {
content, err := staticFS.ReadFile("static/index.html")
if err != nil {
c.String(500, "加载前端页面失败")
return
}
c.Data(http.StatusOK, "text/html; charset=utf-8", content)
})
// 健康检查
r.GET("/ping", func(c *gin.Context) {
c.String(200, "frps-console 后端已启动 🎉")
})
api := r.Group("/api")
{
// ---- 公开路由(不需要认证) ----
api.GET("/check/users", checkUsersHandler)
api.POST("/register", registerHandler)
api.POST("/login", loginHandler)
api.GET("/ping", pingHandler) // 新增:frps 延迟检测
// ---- 需要认证的路由 ----
auth := api.Group("/")
auth.Use(AuthMiddleware())
{
auth.GET("/config", getConfigHandler)
auth.PUT("/config", updateConfigHandler)
// frps 特有:从 Dashboard API 读取客户端和代理列表
auth.GET("/clients", getClientsHandler)
auth.GET("/proxies", getProxiesHandler)
auth.POST("/frps/reload", reloadFrpsHandler)
auth.POST("/frps/start", startFrpsHandler)
auth.POST("/frps/stop", stopFrpsHandler)
auth.GET("/frps/status", getFrpsStatusHandler)
auth.GET("/frps/log", getFrpsLogHandler)
auth.POST("/import/toml", importTomlHandler)
auth.GET("/export/toml", ExportTomlHandler)
auth.PUT("/user/password", changePasswordHandler)
}
}
return r
}
// ========== 认证 Handler(与 frps 相同) ==========
func checkUsersHandler(c *gin.Context) {
count, err := CountUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{"hasUsers": count > 0, "count": count},
})
}
func registerHandler(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
return
}
count, err := CountUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
return
}
if count > 0 {
c.JSON(http.StatusForbidden, gin.H{"code": 1, "msg": "已存在管理员账户,请登录"})
return
}
if len(req.Username) < 5 {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "用户名至少 5 位"})
return
}
if !ValidatePassword(req.Password) {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母和数字"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
return
}
if err := CreateUser(req.Username, string(hash)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建用户失败: " + err.Error()})
return
}
token, err := GenerateJWT(req.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "注册成功",
"data": gin.H{"token": token},
})
}
func loginHandler(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
return
}
user, err := GetUserByUsername(req.Username)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
return
}
token, err := GenerateJWT(user.Username)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "登录成功",
"data": gin.H{"token": token},
})
}
func changePasswordHandler(c *gin.Context) {
var req struct {
OldPassword string `json:"oldPassword"`
NewPassword string `json:"newPassword"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
return
}
username, exists := c.Get("username")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未登录"})
return
}
user, err := GetUserByUsername(username.(string))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "用户不存在"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "当前密码错误"})
return
}
if !ValidatePassword(req.NewPassword) {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母和数字"})
return
}
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
return
}
if err := UpdatePassword(username.(string), string(hash)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新密码失败"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"})
}
// ========== 配置 Handlerfrps 专用) ==========
func getConfigHandler(c *gin.Context) {
cfg, err := GetGlobalConfig()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": cfg})
}
func updateConfigHandler(c *gin.Context) {
var cfg GlobalConfig
if err := c.ShouldBindJSON(&cfg); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
return
}
// 强制 tcpMux = truefrps 推荐)
cfg.TcpMux = true
if err := UpdateGlobalConfig(&cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
return
}
if err := GenerateFrpsConfig(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()})
return
}
if err := ReloadFrps(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
}
// ========== frps Dashboard API 读取 ==========
type FrpsDashboardClient struct {
ID int `json:"id"`
Name string `json:"name"`
OS string `json:"os"`
Arch string `json:"arch"`
Version string `json:"version"`
Status string `json:"status"`
ConnTime string `json:"connTime"`
}
type FrpsDashboardProxy struct {
Name string `json:"name"`
Type string `json:"type"`
Status string `json:"status"`
LocalAddr string `json:"localAddr"`
RemoteAddr string `json:"remoteAddr"`
}
func getClientsHandler(c *gin.Context) {
clients, err := fetchClientsFromDashboard()
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 0, "data": []interface{}{}, "msg": "无法连接 frps Dashboard: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": clients})
}
func getProxiesHandler(c *gin.Context) {
proxies, err := fetchProxiesFromDashboard()
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 0, "data": []interface{}{}, "msg": "无法连接 frps Dashboard: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": proxies})
}
func fetchClientsFromDashboard() ([]FrpsDashboardClient, error) {
// TODO: 从配置读取 Dashboard 地址(当前硬编码)
resp, err := http.Get("http://127.0.0.1:7500/api/v2/clients")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Data []FrpsDashboardClient `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Data, nil
}
func fetchProxiesFromDashboard() ([]FrpsDashboardProxy, error) {
resp, err := http.Get("http://127.0.0.1:7500/api/v2/proxies")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Data []FrpsDashboardProxy `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result.Data, nil
}
// ========== frps 进程管理 Handler ==========
func reloadFrpsHandler(c *gin.Context) {
if err := GenerateFrpsConfig(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
return
}
if err := ReloadFrps(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "热加载成功"})
}
func startFrpsHandler(c *gin.Context) {
if err := StartFrps(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "启动失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frps 启动成功"})
}
func stopFrpsHandler(c *gin.Context) {
if err := StopFrps(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "停止失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frps 已停止"})
}
func getFrpsStatusHandler(c *gin.Context) {
running, err := GetFrpsStatus()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询状态失败"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}})
}
// ========== 日志 Handler ==========
func getFrpsLogHandler(c *gin.Context) {
// readTailLog 在 frps.go 中定义,读取 ./data/frps.log
lines, err := readTailLog("./data/frps.log", 200)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"lines": []string{},
"total": 0,
"error": err.Error(),
},
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"data": gin.H{
"lines": lines,
"total": len(lines),
},
})
}
// ========== Ping Handler(检测 frps 服务连通性) ==========
func pingHandler(c *gin.Context) {
target := c.Query("target")
if target == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 target 参数"})
return
}
cfg, err := GetGlobalConfig()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
return
}
port := cfg.BindPort // frps 使用 bindPort
address := net.JoinHostPort(target, strconv.Itoa(port))
start := time.Now()
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
if err != nil {
c.JSON(http.StatusOK, gin.H{"code": 1, "msg": "ping 失败", "latency": -1})
return
}
conn.Close()
latency := time.Since(start).Milliseconds()
c.JSON(http.StatusOK, gin.H{"code": 0, "latency": latency})
}
// ========== 导入/导出 TOML ==========
func importTomlHandler(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请选择文件"})
return
}
f, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"})
return
}
defer f.Close()
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(f); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"})
return
}
parsed, err := ParseToml(buf.String())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "解析 TOML 失败: " + err.Error()})
return
}
cfg := parsed.ToGlobalConfig()
cfg.TcpMux = true
if err := UpdateGlobalConfig(cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
return
}
// frps 没有隧道表,无需清空 proxies,但可以清空 clients 表(如果有)
// 目前 clients 表只做记录,可忽略
if err := GenerateFrpsConfig(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
return
}
if err := ReloadFrps(); err != nil {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "导入成功,但热加载失败: " + err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "导入成功"})
}
func ExportTomlHandler(c *gin.Context) {
cfg, err := GetGlobalConfig()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败: " + err.Error()})
return
}
// 使用 frps 模板
tmpl, err := template.New("frps").Parse(FrpsTemplateContent)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "解析模板失败: " + err.Error()})
return
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, cfg); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "渲染模板失败: " + err.Error()})
return
}
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=frps.toml")
c.String(http.StatusOK, buf.String())
}