Files
GoTunnel/cmd/server/main.go
Flik d1058f9e89
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 38s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m47s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m48s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 1m29s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 1m53s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 1m14s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 1m40s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m36s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 1m51s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m17s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m50s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m18s
feat(app): 添加流量统计功能和服务器配置管理
- 在WebServer中添加TrafficStore存储接口
- 将Web配置从根级别移动到Server.Web子结构下
- 移除Web配置中的BindAddr字段并调整默认值逻辑
- 在前端HomeView中替换模拟流量数据显示真实统计数据
- 添加流量统计API接口(/traffic/stats和/traffic/hourly)
- 实现SQLite数据库流量统计表创建和CRUD操作
- 在Relay包中添加带流量统计的数据转发功能
- 在设置页面添加服务器配置编辑和保存功能
- 创建流量统计处理器和相关数据模型定义
2026-01-22 19:53:40 +08:00

197 lines
5.0 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
// @title GoTunnel API
// @version 1.0
// @description GoTunnel 内网穿透服务器 API
// @host localhost:7500
// @BasePath /
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description JWT Bearer token
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
_ "github.com/gotunnel/docs" // Swagger docs
"github.com/gotunnel/internal/server/app"
"github.com/gotunnel/internal/server/config"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/tunnel"
"github.com/gotunnel/pkg/crypto"
"github.com/gotunnel/pkg/plugin"
"github.com/gotunnel/pkg/plugin/sign"
"github.com/gotunnel/pkg/version"
)
// 版本信息(通过 ldflags 注入)
var Version string
func init() {
version.SetVersion(Version)
}
func main() {
configPath := flag.String("c", "server.yaml", "config file path")
flag.Parse()
// 加载 YAML 配置
cfg, err := config.LoadServerConfig(*configPath)
if err != nil {
log.Fatalf("Load config error: %v", err)
}
// 初始化数据库
clientStore, err := db.NewSQLiteStore(cfg.Server.DBPath)
if err != nil {
log.Fatalf("Init database error: %v", err)
}
defer clientStore.Close()
// 打印 token便于客户端连接
log.Printf("[Server] Token: %s", cfg.Server.Token)
// 创建隧道服务
server := tunnel.NewServer(
clientStore,
cfg.Server.BindAddr,
cfg.Server.BindPort,
cfg.Server.Token,
cfg.Server.HeartbeatSec,
cfg.Server.HeartbeatTimeout,
)
// 配置 TLS默认启用
if !cfg.Server.TLSDisabled {
tlsConfig, err := crypto.GenerateTLSConfig()
if err != nil {
log.Fatalf("Generate TLS config error: %v", err)
}
server.SetTLSConfig(tlsConfig)
log.Printf("[Server] TLS enabled")
}
// 初始化插件系统(用于客户端 JS 插件管理)
registry := plugin.NewRegistry()
server.SetPluginRegistry(registry)
server.SetJSPluginStore(clientStore) // 设置 JS 插件存储,用于客户端重连时恢复插件
// 加载 JS 插件配置
if len(cfg.JSPlugins) > 0 {
jsPlugins := loadJSPlugins(cfg.JSPlugins)
server.LoadJSPlugins(jsPlugins)
}
// 启动 Web 控制台
if cfg.Server.Web.Enabled {
// 强制生成 Web 凭据(如果未配置)
if config.GenerateWebCredentials(cfg) {
log.Printf("[Web] Auto-generated credentials - Username: %s, Password: %s",
cfg.Server.Web.Username, cfg.Server.Web.Password)
log.Printf("[Web] Please save these credentials and update your config file")
// 保存配置以持久化凭据
if err := config.SaveServerConfig(*configPath, cfg); err != nil {
log.Printf("[Web] Warning: failed to save config: %v", err)
}
}
ws := app.NewWebServer(clientStore, server, cfg, *configPath, clientStore)
addr := fmt.Sprintf("%s:%d", cfg.Server.BindAddr, cfg.Server.Web.BindPort)
go func() {
// 始终使用 JWT 认证
err := ws.RunWithJWT(addr, cfg.Server.Web.Username, cfg.Server.Web.Password, cfg.Server.Token)
if err != nil {
log.Printf("[Web] Server error: %v", err)
}
}()
log.Printf("[Web] Console running at http://%s (authentication required)", addr)
}
// 优雅关闭信号处理
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-quit
log.Printf("[Server] Received shutdown signal")
server.Shutdown(30 * time.Second)
os.Exit(0)
}()
log.Fatal(server.Run())
}
// loadJSPlugins 加载 JS 插件文件
func loadJSPlugins(configs []config.JSPluginConfig) []tunnel.JSPluginEntry {
var plugins []tunnel.JSPluginEntry
for _, cfg := range configs {
source, err := os.ReadFile(cfg.Path)
if err != nil {
log.Printf("[JSPlugin] Failed to load %s: %v", cfg.Path, err)
continue
}
// 加载签名文件
sigPath := cfg.SigPath
if sigPath == "" {
sigPath = cfg.Path + ".sig"
}
signature, err := os.ReadFile(sigPath)
if err != nil {
log.Printf("[JSPlugin] Failed to load signature for %s: %v", cfg.Name, err)
continue
}
// 服务端也验证签名,防止配置文件被篡改
if err := verifyPluginSignature(cfg.Name, string(source), string(signature)); err != nil {
log.Printf("[JSPlugin] Signature verification failed for %s: %v", cfg.Name, err)
continue
}
plugins = append(plugins, tunnel.JSPluginEntry{
Name: cfg.Name,
Source: string(source),
Signature: string(signature),
AutoPush: cfg.AutoPush,
Config: cfg.Config,
AutoStart: cfg.AutoStart,
})
log.Printf("[JSPlugin] Loaded: %s from %s (verified)", cfg.Name, cfg.Path)
}
return plugins
}
// verifyPluginSignature 验证插件签名
func verifyPluginSignature(name, source, signature string) error {
// 解码签名
signed, err := sign.DecodeSignedPlugin(signature)
if err != nil {
return fmt.Errorf("decode signature: %w", err)
}
// 获取公钥
pubKey, err := sign.GetPublicKeyByID(signed.Payload.KeyID)
if err != nil {
return err
}
// 验证插件名称
if signed.Payload.Name != name {
return fmt.Errorf("name mismatch: %s vs %s", signed.Payload.Name, name)
}
// 验证签名
return sign.VerifyPlugin(pubKey, signed, source)
}