diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index a094026..12a6026 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -18,7 +18,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.24' cache: true - name: Build all platforms @@ -26,33 +26,33 @@ jobs: mkdir -p dist # Linux amd64 - GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" \ -o dist/server-linux-amd64 ./cmd/server - GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" \ -o dist/client-linux-amd64 ./cmd/client # Linux arm64 - GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" \ -o dist/server-linux-arm64 ./cmd/server - GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" \ -o dist/client-linux-arm64 ./cmd/client # Darwin amd64 - GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" \ -o dist/server-darwin-amd64 ./cmd/server - GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" \ -o dist/client-darwin-amd64 ./cmd/client # Darwin arm64 - GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" \ -o dist/server-darwin-arm64 ./cmd/server - GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" \ -o dist/client-darwin-arm64 ./cmd/client # Windows amd64 - GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" \ -o dist/server-windows-amd64.exe ./cmd/server - GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" \ + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" \ -o dist/client-windows-amd64.exe ./cmd/client - name: List artifacts diff --git a/cmd/server/main.go b/cmd/server/main.go index 063065e..8495b90 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -40,7 +40,7 @@ func main() { // 启动 Web 控制台 if cfg.Web.Enabled { - ws := app.NewWebServer(clientStore, server) + ws := app.NewWebServer(clientStore, server, cfg, *configPath) addr := fmt.Sprintf("%s:%d", cfg.Web.BindAddr, cfg.Web.BindPort) go func() { diff --git a/go.mod b/go.mod index 9a3207f..b3196ac 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,22 @@ module github.com/gotunnel -go 1.21 +go 1.24.0 require ( - github.com/google/uuid v1.4.0 + github.com/google/uuid v1.6.0 github.com/hashicorp/yamux v0.1.1 gopkg.in/yaml.v3 v3.0.1 + modernc.org/sqlite v1.41.0 ) -require github.com/mattn/go-sqlite3 v1.14.32 // indirect +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect + golang.org/x/sys v0.36.0 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect +) diff --git a/go.sum b/go.sum index 980f813..0ac7b58 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,55 @@ -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= -github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck= +modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/internal/server/app/app.go b/internal/server/app/app.go index 7e3e23b..dace5b7 100644 --- a/internal/server/app/app.go +++ b/internal/server/app/app.go @@ -7,6 +7,7 @@ import ( "log" "net/http" + "github.com/gotunnel/internal/server/config" "github.com/gotunnel/internal/server/db" "github.com/gotunnel/internal/server/router" ) @@ -45,13 +46,17 @@ func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { type WebServer struct { ClientStore db.ClientStore Server router.ServerInterface + Config *config.ServerConfig + ConfigPath string } // NewWebServer 创建Web服务 -func NewWebServer(cs db.ClientStore, srv router.ServerInterface) *WebServer { +func NewWebServer(cs db.ClientStore, srv router.ServerInterface, cfg *config.ServerConfig, cfgPath string) *WebServer { return &WebServer{ ClientStore: cs, Server: srv, + Config: cfg, + ConfigPath: cfgPath, } } @@ -110,3 +115,18 @@ func (w *WebServer) GetClientStore() db.ClientStore { func (w *WebServer) GetServer() router.ServerInterface { return w.Server } + +// GetConfig 获取配置 +func (w *WebServer) GetConfig() *config.ServerConfig { + return w.Config +} + +// GetConfigPath 获取配置文件路径 +func (w *WebServer) GetConfigPath() string { + return w.ConfigPath +} + +// SaveConfig 保存配置 +func (w *WebServer) SaveConfig() error { + return config.SaveServerConfig(w.ConfigPath, w.Config) +} diff --git a/internal/server/config/config.go b/internal/server/config/config.go index a572aff..c13c07c 100644 --- a/internal/server/config/config.go +++ b/internal/server/config/config.go @@ -1,6 +1,8 @@ package config import ( + "crypto/rand" + "encoding/hex" "os" "gopkg.in/yaml.v3" @@ -33,17 +35,24 @@ type WebSettings struct { // LoadServerConfig 加载服务端配置 func LoadServerConfig(path string) (*ServerConfig, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - var cfg ServerConfig - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, err + + // 尝试读取配置文件,不存在则使用默认配置 + data, err := os.ReadFile(path) + if err == nil { + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } } // 设置默认值 + setDefaults(&cfg) + + return &cfg, nil +} + +// setDefaults 设置默认值 +func setDefaults(cfg *ServerConfig) { if cfg.Server.BindAddr == "" { cfg.Server.BindAddr = "0.0.0.0" } @@ -60,13 +69,33 @@ func LoadServerConfig(path string) (*ServerConfig, error) { cfg.Server.DBPath = "gotunnel.db" } - // Web 默认值 + // Web 默认启用 if cfg.Web.BindAddr == "" { cfg.Web.BindAddr = "0.0.0.0" } if cfg.Web.BindPort == 0 { cfg.Web.BindPort = 7500 + cfg.Web.Enabled = true } - return &cfg, nil + // Token 未配置时自动生成 32 位 + if cfg.Server.Token == "" { + cfg.Server.Token = generateToken(32) + } +} + +// generateToken 生成随机 token +func generateToken(length int) string { + bytes := make([]byte, length/2) + rand.Read(bytes) + return hex.EncodeToString(bytes) +} + +// SaveServerConfig 保存服务端配置 +func SaveServerConfig(path string, cfg *ServerConfig) error { + data, err := yaml.Marshal(cfg) + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) } diff --git a/internal/server/db/sqlite.go b/internal/server/db/sqlite.go index 2e4367e..77b7d08 100644 --- a/internal/server/db/sqlite.go +++ b/internal/server/db/sqlite.go @@ -5,7 +5,7 @@ import ( "encoding/json" "sync" - _ "github.com/mattn/go-sqlite3" + _ "modernc.org/sqlite" "github.com/gotunnel/pkg/protocol" ) @@ -18,7 +18,7 @@ type SQLiteStore struct { // NewSQLiteStore 创建 SQLite 存储 func NewSQLiteStore(dbPath string) (*SQLiteStore, error) { - db, err := sql.Open("sqlite3", dbPath) + db, err := sql.Open("sqlite", dbPath) if err != nil { return nil, err } diff --git a/internal/server/router/api.go b/internal/server/router/api.go index 4bcc54b..a3e2bd6 100644 --- a/internal/server/router/api.go +++ b/internal/server/router/api.go @@ -4,6 +4,7 @@ import ( "encoding/json" "net/http" + "github.com/gotunnel/internal/server/config" "github.com/gotunnel/internal/server/db" "github.com/gotunnel/pkg/protocol" ) @@ -32,12 +33,16 @@ type ServerInterface interface { type AppInterface interface { GetClientStore() db.ClientStore GetServer() ServerInterface + GetConfig() *config.ServerConfig + GetConfigPath() string + SaveConfig() error } // APIHandler API处理器 type APIHandler struct { clientStore db.ClientStore server ServerInterface + app AppInterface } // RegisterRoutes 注册所有 API 路由 @@ -45,12 +50,14 @@ func RegisterRoutes(r *Router, app AppInterface) { h := &APIHandler{ clientStore: app.GetClientStore(), server: app.GetServer(), + app: app, } api := r.Group("/api") api.HandleFunc("/status", h.handleStatus) api.HandleFunc("/clients", h.handleClients) api.HandleFunc("/client/", h.handleClient) + api.HandleFunc("/config", h.handleConfig) api.HandleFunc("/config/reload", h.handleReload) } @@ -198,6 +205,100 @@ func (h *APIHandler) deleteClient(rw http.ResponseWriter, clientID string) { h.jsonResponse(rw, map[string]string{"status": "ok"}) } +func (h *APIHandler) handleConfig(rw http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + h.getConfig(rw) + case http.MethodPut: + h.updateConfig(rw, r) + default: + http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func (h *APIHandler) getConfig(rw http.ResponseWriter) { + cfg := h.app.GetConfig() + h.jsonResponse(rw, map[string]interface{}{ + "server": map[string]interface{}{ + "bind_addr": cfg.Server.BindAddr, + "bind_port": cfg.Server.BindPort, + "token": cfg.Server.Token, + "heartbeat_sec": cfg.Server.HeartbeatSec, + "heartbeat_timeout": cfg.Server.HeartbeatTimeout, + }, + "web": map[string]interface{}{ + "enabled": cfg.Web.Enabled, + "bind_addr": cfg.Web.BindAddr, + "bind_port": cfg.Web.BindPort, + "username": cfg.Web.Username, + "password": cfg.Web.Password, + }, + }) +} + +func (h *APIHandler) updateConfig(rw http.ResponseWriter, r *http.Request) { + var req struct { + Server *struct { + BindAddr string `json:"bind_addr"` + BindPort int `json:"bind_port"` + Token string `json:"token"` + HeartbeatSec int `json:"heartbeat_sec"` + HeartbeatTimeout int `json:"heartbeat_timeout"` + } `json:"server"` + Web *struct { + Enabled bool `json:"enabled"` + BindAddr string `json:"bind_addr"` + BindPort int `json:"bind_port"` + Username string `json:"username"` + Password string `json:"password"` + } `json:"web"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(rw, err.Error(), http.StatusBadRequest) + return + } + + cfg := h.app.GetConfig() + + // 更新 Server 配置 + if req.Server != nil { + if req.Server.BindAddr != "" { + cfg.Server.BindAddr = req.Server.BindAddr + } + if req.Server.BindPort > 0 { + cfg.Server.BindPort = req.Server.BindPort + } + if req.Server.Token != "" { + cfg.Server.Token = req.Server.Token + } + if req.Server.HeartbeatSec > 0 { + cfg.Server.HeartbeatSec = req.Server.HeartbeatSec + } + if req.Server.HeartbeatTimeout > 0 { + cfg.Server.HeartbeatTimeout = req.Server.HeartbeatTimeout + } + } + + // 更新 Web 配置 + if req.Web != nil { + cfg.Web.Enabled = req.Web.Enabled + if req.Web.BindAddr != "" { + cfg.Web.BindAddr = req.Web.BindAddr + } + if req.Web.BindPort > 0 { + cfg.Web.BindPort = req.Web.BindPort + } + cfg.Web.Username = req.Web.Username + cfg.Web.Password = req.Web.Password + } + + if err := h.app.SaveConfig(); err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + h.jsonResponse(rw, map[string]string{"status": "ok"}) +} + func (h *APIHandler) handleReload(rw http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)