507 lines
14 KiB
Go
507 lines
14 KiB
Go
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": "密码修改成功"})
|
||
}
|
||
|
||
// ========== 配置 Handler(frps 专用) ==========
|
||
|
||
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 = true(frps 推荐)
|
||
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())
|
||
}
|