399 lines
11 KiB
Go
399 lines
11 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"fmt"
|
||
"net/http"
|
||
"strconv"
|
||
"text/template"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"golang.org/x/crypto/bcrypt"
|
||
)
|
||
|
||
// SetupRouter 配置所有路由
|
||
func SetupRouter() *gin.Engine {
|
||
r := gin.Default()
|
||
|
||
// 静态文件(前端)
|
||
r.Static("/static", "./static")
|
||
|
||
// 健康检查
|
||
r.GET("/ping", func(c *gin.Context) {
|
||
c.String(200, "frpc-console 后端已启动 🎉")
|
||
})
|
||
|
||
// 前端入口
|
||
r.GET("/", func(c *gin.Context) {
|
||
c.File("./static/index.html")
|
||
})
|
||
|
||
// API 路由组
|
||
api := r.Group("/api")
|
||
{
|
||
// 登录(无需鉴权)
|
||
api.POST("/login", loginHandler)
|
||
|
||
// 需要鉴权的路由
|
||
auth := api.Group("/")
|
||
auth.Use(AuthMiddleware())
|
||
{
|
||
// 全局配置
|
||
auth.GET("/config", getConfigHandler)
|
||
auth.PUT("/config", updateConfigHandler)
|
||
|
||
// 隧道管理
|
||
auth.GET("/proxies", getProxiesHandler)
|
||
auth.GET("/proxy/:id", getProxyHandler)
|
||
auth.POST("/proxy", createProxyHandler)
|
||
auth.PUT("/proxy/:id", updateProxyHandler)
|
||
auth.DELETE("/proxy/:id", deleteProxyHandler)
|
||
|
||
// frpc 控制
|
||
auth.POST("/frpc/reload", reloadFrpcHandler)
|
||
auth.POST("/frpc/start", startFrpcHandler)
|
||
auth.POST("/frpc/stop", stopFrpcHandler)
|
||
auth.GET("/frpc/status", getFrpcStatusHandler)
|
||
|
||
// 🟢 导入 TOML(新增)
|
||
auth.POST("/import/toml", importTomlHandler)
|
||
auth.GET("/export/toml", ExportTomlHandler)
|
||
}
|
||
}
|
||
|
||
return r
|
||
}
|
||
|
||
// ========== 登录 ==========
|
||
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 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
|
||
cfg.TcpMux = true
|
||
|
||
if err := UpdateGlobalConfig(&cfg); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := GenerateFrpcConfig(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := ReloadFrpc(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
||
}
|
||
|
||
// ========== 隧道管理 ==========
|
||
func getProxiesHandler(c *gin.Context) {
|
||
proxies, err := GetProxies()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道列表失败"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": proxies})
|
||
}
|
||
|
||
func getProxyHandler(c *gin.Context) {
|
||
id, err := strconv.Atoi(c.Param("id"))
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||
return
|
||
}
|
||
p, err := GetProxy(id)
|
||
if err != nil {
|
||
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "隧道不存在"})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": p})
|
||
}
|
||
|
||
func createProxyHandler(c *gin.Context) {
|
||
var p Proxy
|
||
if err := c.ShouldBindJSON(&p); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||
return
|
||
}
|
||
p.Enabled = true
|
||
|
||
if err := CreateProxy(&p); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := generateAndReload(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道创建成功", "data": gin.H{"id": p.ID}})
|
||
}
|
||
|
||
func updateProxyHandler(c *gin.Context) {
|
||
id, err := strconv.Atoi(c.Param("id"))
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||
return
|
||
}
|
||
|
||
var p Proxy
|
||
if err := c.ShouldBindJSON(&p); err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
||
return
|
||
}
|
||
p.ID = id
|
||
|
||
if err := UpdateProxy(&p); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := generateAndReload(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道更新成功"})
|
||
}
|
||
|
||
func deleteProxyHandler(c *gin.Context) {
|
||
id, err := strconv.Atoi(c.Param("id"))
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
||
return
|
||
}
|
||
|
||
if err := DeleteProxy(id); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
if err := generateAndReload(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"})
|
||
}
|
||
|
||
// ========== frpc 控制 ==========
|
||
func reloadFrpcHandler(c *gin.Context) {
|
||
if err := GenerateFrpcConfig(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||
return
|
||
}
|
||
if err := ReloadFrpc(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "热加载成功"})
|
||
}
|
||
|
||
func startFrpcHandler(c *gin.Context) {
|
||
if err := StartFrpc(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "启动失败: " + err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 启动成功"})
|
||
}
|
||
|
||
func stopFrpcHandler(c *gin.Context) {
|
||
if err := StopFrpc(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "停止失败: " + err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 已停止"})
|
||
}
|
||
|
||
func getFrpcStatusHandler(c *gin.Context) {
|
||
running, err := GetFrpcStatus()
|
||
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}})
|
||
}
|
||
|
||
// ========== 🟢 导入 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
|
||
}
|
||
|
||
// 更新全局配置(保持当前 tcpMux 值)
|
||
cfg := parsed.ToGlobalConfig()
|
||
currentCfg, _ := GetGlobalConfig()
|
||
if currentCfg != nil {
|
||
cfg.TcpMux = currentCfg.TcpMux // 保持当前值不变
|
||
}
|
||
cfg.TcpMux = true // 强制开启
|
||
|
||
if err := UpdateGlobalConfig(cfg); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
// 清空并导入隧道
|
||
if _, err := DB.Exec("DELETE FROM proxies"); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "清空隧道失败"})
|
||
return
|
||
}
|
||
|
||
proxies := parsed.ToProxies()
|
||
for _, p := range proxies {
|
||
if err := CreateProxy(&p); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "导入隧道失败: " + err.Error()})
|
||
return
|
||
}
|
||
}
|
||
|
||
// 生成配置并热加载
|
||
if err := GenerateFrpcConfig(); err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
||
return
|
||
}
|
||
if err := ReloadFrpc(); err != nil {
|
||
// 导入成功但热加载失败,不影响整体结果
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 0,
|
||
"msg": fmt.Sprintf("导入成功!共 %d 条隧道,但热加载失败: %s", len(proxies), err.Error()),
|
||
})
|
||
return
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"code": 0,
|
||
"msg": fmt.Sprintf("导入成功!共 %d 条隧道", len(proxies)),
|
||
})
|
||
}
|
||
|
||
// ExportTomlHandler 导出 TOML 配置文件
|
||
func ExportTomlHandler(c *gin.Context) {
|
||
cfg, err := GetGlobalConfig()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
proxies, err := GetProxies()
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
var activeProxies []Proxy
|
||
for _, p := range proxies {
|
||
if p.Enabled {
|
||
activeProxies = append(activeProxies, p)
|
||
}
|
||
}
|
||
|
||
data := struct {
|
||
*GlobalConfig
|
||
Proxies []Proxy
|
||
}{
|
||
GlobalConfig: cfg,
|
||
Proxies: activeProxies,
|
||
}
|
||
|
||
// 解析模板(直接使用 frp.go 中的 frpcTemplateContent)
|
||
tmpl, err := template.New("frpc").Parse(FrpcTemplateContent)
|
||
if err != nil {
|
||
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "解析模板失败: " + err.Error()})
|
||
return
|
||
}
|
||
|
||
var buf bytes.Buffer
|
||
if err := tmpl.Execute(&buf, data); 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=frpc.toml")
|
||
c.String(http.StatusOK, buf.String())
|
||
}
|
||
|
||
// ========== 辅助函数 ==========
|
||
func generateAndReload() error {
|
||
if err := GenerateFrpcConfig(); err != nil {
|
||
return err
|
||
}
|
||
return ReloadFrpc()
|
||
}
|