server use sqlite
All checks were successful
Build Multi-Platform Binaries / build (push) Successful in 11m35s

This commit is contained in:
Flik
2025-12-25 19:48:11 +08:00
parent 790d3b682a
commit 7100362cd7
23 changed files with 688 additions and 421 deletions

View File

@@ -1,89 +0,0 @@
package config
import (
"os"
"github.com/gotunnel/pkg/protocol"
"gopkg.in/yaml.v3"
)
// ServerConfig 服务端配置
type ServerConfig struct {
Server ServerSettings `yaml:"server"`
Web WebSettings `yaml:"web"`
Clients []ClientConfig `yaml:"clients"`
}
// ServerSettings 服务端设置
type ServerSettings struct {
BindAddr string `yaml:"bind_addr"`
BindPort int `yaml:"bind_port"`
Token string `yaml:"token"`
HeartbeatSec int `yaml:"heartbeat_sec"`
HeartbeatTimeout int `yaml:"heartbeat_timeout"`
}
// WebSettings Web控制台设置
type WebSettings struct {
Enabled bool `yaml:"enabled"`
BindAddr string `yaml:"bind_addr"`
BindPort int `yaml:"bind_port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
// ClientConfig 客户端配置(服务端维护)
type ClientConfig struct {
ID string `yaml:"id"`
Rules []protocol.ProxyRule `yaml:"rules"`
}
// 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
}
// 设置默认值
if cfg.Server.HeartbeatSec == 0 {
cfg.Server.HeartbeatSec = 30
}
if cfg.Server.HeartbeatTimeout == 0 {
cfg.Server.HeartbeatTimeout = 90
}
// Web 默认值
if cfg.Web.BindAddr == "" {
cfg.Web.BindAddr = "0.0.0.0"
}
if cfg.Web.BindPort == 0 {
cfg.Web.BindPort = 7500
}
return &cfg, nil
}
// GetClientRules 获取指定客户端的代理规则
func (c *ServerConfig) GetClientRules(clientID string) []protocol.ProxyRule {
for _, client := range c.Clients {
if client.ID == clientID {
return client.Rules
}
}
return nil
}
// SaveServerConfig 保存服务端配置
func SaveServerConfig(path string, cfg *ServerConfig) error {
data, err := yaml.Marshal(cfg)
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}

View File

@@ -63,7 +63,6 @@ type ErrorMessage struct {
// WriteMessage 写入消息到 writer
func WriteMessage(w io.Writer, msg *Message) error {
// 消息格式: [1字节类型][4字节长度][payload]
header := make([]byte, 5)
header[0] = msg.Type
binary.BigEndian.PutUint32(header[1:], uint32(len(msg.Payload)))
@@ -89,7 +88,7 @@ func ReadMessage(r io.Reader) (*Message, error) {
msgType := header[0]
length := binary.BigEndian.Uint32(header[1:])
if length > 1024*1024 { // 最大 1MB
if length > 1024*1024 {
return nil, errors.New("message too large")
}

30
pkg/relay/relay.go Normal file
View File

@@ -0,0 +1,30 @@
package relay
import (
"net"
"sync"
)
// Relay 双向数据转发
func Relay(c1, c2 net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
copy := func(dst, src net.Conn) {
defer wg.Done()
buf := make([]byte, 32*1024)
for {
n, err := src.Read(buf)
if n > 0 {
dst.Write(buf[:n])
}
if err != nil {
return
}
}
}
go copy(c1, c2)
go copy(c2, c1)
wg.Wait()
}

View File

@@ -1,182 +0,0 @@
package tunnel
import (
"fmt"
"log"
"net"
"sync"
"time"
"github.com/gotunnel/pkg/protocol"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
)
// Client 隧道客户端
type Client struct {
ServerAddr string
Token string
ID string
session *yamux.Session
rules []protocol.ProxyRule
mu sync.RWMutex
}
// NewClient 创建客户端
func NewClient(serverAddr, token, id string) *Client {
if id == "" {
id = uuid.New().String()[:8]
}
return &Client{
ServerAddr: serverAddr,
Token: token,
ID: id,
}
}
// Run 启动客户端(带断线重连)
func (c *Client) Run() error {
for {
if err := c.connect(); err != nil {
log.Printf("[Client] Connect error: %v", err)
log.Printf("[Client] Reconnecting in 5s...")
time.Sleep(5 * time.Second)
continue
}
c.handleSession()
log.Printf("[Client] Disconnected, reconnecting...")
time.Sleep(3 * time.Second)
}
}
// connect 连接到服务端并认证
func (c *Client) connect() error {
conn, err := net.DialTimeout("tcp", c.ServerAddr, 10*time.Second)
if err != nil {
return err
}
// 发送认证
authReq := protocol.AuthRequest{ClientID: c.ID, Token: c.Token}
msg, _ := protocol.NewMessage(protocol.MsgTypeAuth, authReq)
if err := protocol.WriteMessage(conn, msg); err != nil {
conn.Close()
return err
}
// 读取响应
resp, err := protocol.ReadMessage(conn)
if err != nil {
conn.Close()
return err
}
var authResp protocol.AuthResponse
resp.ParsePayload(&authResp)
if !authResp.Success {
conn.Close()
return fmt.Errorf("auth failed: %s", authResp.Message)
}
log.Printf("[Client] Authenticated as %s", c.ID)
// 建立 Yamux 会话
session, err := yamux.Client(conn, nil)
if err != nil {
conn.Close()
return err
}
c.mu.Lock()
c.session = session
c.mu.Unlock()
return nil
}
// handleSession 处理会话
func (c *Client) handleSession() {
defer c.session.Close()
for {
stream, err := c.session.Accept()
if err != nil {
return
}
go c.handleStream(stream)
}
}
// handleStream 处理流
func (c *Client) handleStream(stream net.Conn) {
defer stream.Close()
msg, err := protocol.ReadMessage(stream)
if err != nil {
return
}
switch msg.Type {
case protocol.MsgTypeProxyConfig:
c.handleProxyConfig(msg)
case protocol.MsgTypeNewProxy:
c.handleNewProxy(stream, msg)
case protocol.MsgTypeHeartbeat:
c.handleHeartbeat(stream)
}
}
// handleProxyConfig 处理代理配置
func (c *Client) handleProxyConfig(msg *protocol.Message) {
var cfg protocol.ProxyConfig
msg.ParsePayload(&cfg)
c.mu.Lock()
c.rules = cfg.Rules
c.mu.Unlock()
log.Printf("[Client] Received %d proxy rules", len(cfg.Rules))
for _, r := range cfg.Rules {
log.Printf("[Client] %s: %s:%d", r.Name, r.LocalIP, r.LocalPort)
}
}
// handleNewProxy 处理新代理请求
func (c *Client) handleNewProxy(stream net.Conn, msg *protocol.Message) {
var req protocol.NewProxyRequest
msg.ParsePayload(&req)
// 查找对应规则
var rule *protocol.ProxyRule
c.mu.RLock()
for _, r := range c.rules {
if r.RemotePort == req.RemotePort {
rule = &r
break
}
}
c.mu.RUnlock()
if rule == nil {
log.Printf("[Client] Unknown port %d", req.RemotePort)
return
}
// 连接本地服务
localAddr := fmt.Sprintf("%s:%d", rule.LocalIP, rule.LocalPort)
localConn, err := net.DialTimeout("tcp", localAddr, 5*time.Second)
if err != nil {
log.Printf("[Client] Connect %s error: %v", localAddr, err)
return
}
// 双向转发
relay(stream, localConn)
}
// handleHeartbeat 处理心跳
func (c *Client) handleHeartbeat(stream net.Conn) {
msg := &protocol.Message{Type: protocol.MsgTypeHeartbeatAck}
protocol.WriteMessage(stream, msg)
}

View File

@@ -1,362 +0,0 @@
package tunnel
import (
"fmt"
"log"
"net"
"sync"
"time"
"github.com/gotunnel/pkg/config"
"github.com/gotunnel/pkg/protocol"
"github.com/gotunnel/pkg/utils"
"github.com/hashicorp/yamux"
)
// Server 隧道服务端
type Server struct {
config *config.ServerConfig
portManager *utils.PortManager
clients map[string]*ClientSession
mu sync.RWMutex
}
// ClientSession 客户端会话
type ClientSession struct {
ID string
Session *yamux.Session
Rules []protocol.ProxyRule
Listeners map[int]net.Listener
LastPing time.Time
mu sync.Mutex
}
// NewServer 创建服务端
func NewServer(cfg *config.ServerConfig) *Server {
return &Server{
config: cfg,
portManager: utils.NewPortManager(),
clients: make(map[string]*ClientSession),
}
}
// Run 启动服务端
func (s *Server) Run() error {
addr := fmt.Sprintf("%s:%d", s.config.Server.BindAddr, s.config.Server.BindPort)
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to listen on %s: %v", addr, err)
}
defer ln.Close()
log.Printf("[Server] Listening on %s", addr)
for {
conn, err := ln.Accept()
if err != nil {
log.Printf("[Server] Accept error: %v", err)
continue
}
go s.handleConnection(conn)
}
}
// handleConnection 处理客户端连接
func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close()
// 设置认证超时
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
// 读取认证消息
msg, err := protocol.ReadMessage(conn)
if err != nil {
log.Printf("[Server] Read auth error: %v", err)
return
}
if msg.Type != protocol.MsgTypeAuth {
log.Printf("[Server] Expected auth, got %d", msg.Type)
return
}
var authReq protocol.AuthRequest
if err := msg.ParsePayload(&authReq); err != nil {
log.Printf("[Server] Parse auth error: %v", err)
return
}
// 验证 Token
if authReq.Token != s.config.Server.Token {
s.sendAuthResponse(conn, false, "invalid token")
return
}
// 获取客户端配置
rules := s.config.GetClientRules(authReq.ClientID)
if rules == nil {
s.sendAuthResponse(conn, false, "client not configured")
return
}
conn.SetReadDeadline(time.Time{})
if err := s.sendAuthResponse(conn, true, "ok"); err != nil {
return
}
log.Printf("[Server] Client %s authenticated", authReq.ClientID)
s.setupClientSession(conn, authReq.ClientID, rules)
}
// setupClientSession 建立客户端会话
func (s *Server) setupClientSession(conn net.Conn, clientID string, rules []protocol.ProxyRule) {
session, err := yamux.Server(conn, nil)
if err != nil {
log.Printf("[Server] Yamux error: %v", err)
return
}
cs := &ClientSession{
ID: clientID,
Session: session,
Rules: rules,
Listeners: make(map[int]net.Listener),
LastPing: time.Now(),
}
s.registerClient(cs)
defer s.unregisterClient(cs)
if err := s.sendProxyConfig(session, rules); err != nil {
log.Printf("[Server] Send config error: %v", err)
return
}
s.startProxyListeners(cs)
go s.heartbeatLoop(cs)
<-session.CloseChan()
log.Printf("[Server] Client %s disconnected", clientID)
}
// sendAuthResponse 发送认证响应
func (s *Server) sendAuthResponse(conn net.Conn, success bool, message string) error {
resp := protocol.AuthResponse{Success: success, Message: message}
msg, _ := protocol.NewMessage(protocol.MsgTypeAuthResp, resp)
return protocol.WriteMessage(conn, msg)
}
// sendProxyConfig 发送代理配置
func (s *Server) sendProxyConfig(session *yamux.Session, rules []protocol.ProxyRule) error {
stream, err := session.Open()
if err != nil {
return err
}
defer stream.Close()
cfg := protocol.ProxyConfig{Rules: rules}
msg, _ := protocol.NewMessage(protocol.MsgTypeProxyConfig, cfg)
return protocol.WriteMessage(stream, msg)
}
// registerClient 注册客户端
func (s *Server) registerClient(cs *ClientSession) {
s.mu.Lock()
defer s.mu.Unlock()
s.clients[cs.ID] = cs
}
// unregisterClient 注销客户端
func (s *Server) unregisterClient(cs *ClientSession) {
s.mu.Lock()
defer s.mu.Unlock()
// 关闭所有监听器
cs.mu.Lock()
for port, ln := range cs.Listeners {
ln.Close()
s.portManager.Release(port)
}
cs.mu.Unlock()
delete(s.clients, cs.ID)
}
// startProxyListeners 启动代理监听
func (s *Server) startProxyListeners(cs *ClientSession) {
for _, rule := range cs.Rules {
if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil {
log.Printf("[Server] Port %d error: %v", rule.RemotePort, err)
continue
}
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", rule.RemotePort))
if err != nil {
log.Printf("[Server] Listen %d error: %v", rule.RemotePort, err)
s.portManager.Release(rule.RemotePort)
continue
}
cs.mu.Lock()
cs.Listeners[rule.RemotePort] = ln
cs.mu.Unlock()
log.Printf("[Server] Proxy %s: :%d -> %s:%d",
rule.Name, rule.RemotePort, rule.LocalIP, rule.LocalPort)
go s.acceptProxyConns(cs, ln, rule)
}
}
// acceptProxyConns 接受代理连接
func (s *Server) acceptProxyConns(cs *ClientSession, ln net.Listener, rule protocol.ProxyRule) {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go s.handleProxyConn(cs, conn, rule)
}
}
// handleProxyConn 处理代理连接
func (s *Server) handleProxyConn(cs *ClientSession, conn net.Conn, rule protocol.ProxyRule) {
defer conn.Close()
// 打开到客户端的流
stream, err := cs.Session.Open()
if err != nil {
log.Printf("[Server] Open stream error: %v", err)
return
}
defer stream.Close()
// 发送新代理请求
req := protocol.NewProxyRequest{RemotePort: rule.RemotePort}
msg, _ := protocol.NewMessage(protocol.MsgTypeNewProxy, req)
if err := protocol.WriteMessage(stream, msg); err != nil {
return
}
// 双向转发
relay(conn, stream)
}
// relay 双向数据转发
func relay(c1, c2 net.Conn) {
var wg sync.WaitGroup
wg.Add(2)
copy := func(dst, src net.Conn) {
defer wg.Done()
buf := make([]byte, 32*1024)
for {
n, err := src.Read(buf)
if n > 0 {
dst.Write(buf[:n])
}
if err != nil {
return
}
}
}
go copy(c1, c2)
go copy(c2, c1)
wg.Wait()
}
// heartbeatLoop 心跳检测循环
func (s *Server) heartbeatLoop(cs *ClientSession) {
ticker := time.NewTicker(time.Duration(s.config.Server.HeartbeatSec) * time.Second)
defer ticker.Stop()
timeout := time.Duration(s.config.Server.HeartbeatTimeout) * time.Second
for {
select {
case <-ticker.C:
cs.mu.Lock()
if time.Since(cs.LastPing) > timeout {
cs.mu.Unlock()
log.Printf("[Server] Client %s heartbeat timeout", cs.ID)
cs.Session.Close()
return
}
cs.mu.Unlock()
// 发送心跳
stream, err := cs.Session.Open()
if err != nil {
return
}
msg := &protocol.Message{Type: protocol.MsgTypeHeartbeat}
protocol.WriteMessage(stream, msg)
stream.Close()
case <-cs.Session.CloseChan():
return
}
}
}
// GetClientStatus 获取客户端状态
func (s *Server) GetClientStatus(clientID string) (online bool, lastPing string) {
s.mu.RLock()
defer s.mu.RUnlock()
if cs, ok := s.clients[clientID]; ok {
cs.mu.Lock()
defer cs.mu.Unlock()
return true, cs.LastPing.Format(time.RFC3339)
}
return false, ""
}
// GetAllClientStatus 获取所有客户端状态
func (s *Server) GetAllClientStatus() map[string]struct {
Online bool
LastPing string
} {
s.mu.RLock()
defer s.mu.RUnlock()
result := make(map[string]struct {
Online bool
LastPing string
})
for id, cs := range s.clients {
cs.mu.Lock()
result[id] = struct {
Online bool
LastPing string
}{
Online: true,
LastPing: cs.LastPing.Format(time.RFC3339),
}
cs.mu.Unlock()
}
return result
}
// ReloadConfig 重新加载配置
func (s *Server) ReloadConfig() error {
// 目前仅返回nil后续可实现热重载
return nil
}
// SetConfig 设置配置
func (s *Server) SetConfig(cfg *config.ServerConfig) {
s.mu.Lock()
defer s.mu.Unlock()
s.config = cfg
}
// GetConfig 获取配置
func (s *Server) GetConfig() *config.ServerConfig {
s.mu.RLock()
defer s.mu.RUnlock()
return s.config
}

View File

@@ -1,322 +0,0 @@
package webserver
import (
"embed"
"encoding/json"
"fmt"
"io"
"io/fs"
"log"
"net/http"
"sync"
"github.com/gotunnel/pkg/config"
)
//go:embed dist/*
var staticFiles embed.FS
// spaHandler SPA路由处理器
type spaHandler struct {
fs http.FileSystem
}
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
f, err := h.fs.Open(path)
if err != nil {
f, err = h.fs.Open("index.html")
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
}
defer f.Close()
stat, _ := f.Stat()
if stat.IsDir() {
f, err = h.fs.Open(path + "/index.html")
if err != nil {
f, _ = h.fs.Open("index.html")
}
}
http.ServeContent(w, r, path, stat.ModTime(), f.(io.ReadSeeker))
}
// ClientStatus 客户端状态
type ClientStatus struct {
ID string `json:"id"`
Online bool `json:"online"`
LastPing string `json:"last_ping,omitempty"`
RuleCount int `json:"rule_count"`
}
// ServerInterface 服务端接口
type ServerInterface interface {
GetClientStatus(clientID string) (online bool, lastPing string)
GetAllClientStatus() map[string]struct {
Online bool
LastPing string
}
ReloadConfig() error
}
// WebServer Web控制台服务
type WebServer struct {
config *config.ServerConfig
configPath string
server ServerInterface
mu sync.RWMutex
}
// NewWebServer 创建Web服务
func NewWebServer(cfg *config.ServerConfig, configPath string, srv ServerInterface) *WebServer {
return &WebServer{
config: cfg,
configPath: configPath,
server: srv,
}
}
// Run 启动Web服务
func (w *WebServer) Run(addr string) error {
mux := http.NewServeMux()
mux.HandleFunc("/api/status", w.handleStatus)
mux.HandleFunc("/api/clients", w.handleClients)
mux.HandleFunc("/api/client/", w.handleClient)
mux.HandleFunc("/api/config/reload", w.handleReload)
staticFS, err := fs.Sub(staticFiles, "dist")
if err != nil {
return err
}
mux.Handle("/", spaHandler{fs: http.FS(staticFS)})
log.Printf("[Web] Console listening on %s", addr)
return http.ListenAndServe(addr, mux)
}
// handleStatus 获取服务状态
func (w *WebServer) handleStatus(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
w.mu.RLock()
defer w.mu.RUnlock()
status := map[string]interface{}{
"server": map[string]interface{}{
"bind_addr": w.config.Server.BindAddr,
"bind_port": w.config.Server.BindPort,
},
"client_count": len(w.config.Clients),
}
w.jsonResponse(rw, status)
}
// handleClients 获取所有客户端
func (w *WebServer) handleClients(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
w.getClients(rw)
case http.MethodPost:
w.addClient(rw, r)
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (w *WebServer) getClients(rw http.ResponseWriter) {
w.mu.RLock()
defer w.mu.RUnlock()
var clients []ClientStatus
statusMap := w.server.GetAllClientStatus()
for _, c := range w.config.Clients {
cs := ClientStatus{ID: c.ID, RuleCount: len(c.Rules)}
if s, ok := statusMap[c.ID]; ok {
cs.Online = s.Online
cs.LastPing = s.LastPing
}
clients = append(clients, cs)
}
w.jsonResponse(rw, clients)
}
func (w *WebServer) addClient(rw http.ResponseWriter, r *http.Request) {
var client config.ClientConfig
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
if client.ID == "" {
http.Error(rw, "client id required", http.StatusBadRequest)
return
}
w.mu.Lock()
for _, c := range w.config.Clients {
if c.ID == client.ID {
w.mu.Unlock()
http.Error(rw, "client already exists", http.StatusConflict)
return
}
}
w.config.Clients = append(w.config.Clients, client)
w.mu.Unlock()
if err := w.saveConfig(); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
w.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (w *WebServer) handleClient(rw http.ResponseWriter, r *http.Request) {
clientID := r.URL.Path[len("/api/client/"):]
if clientID == "" {
http.Error(rw, "client id required", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
w.getClient(rw, clientID)
case http.MethodPut:
w.updateClient(rw, r, clientID)
case http.MethodDelete:
w.deleteClient(rw, clientID)
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (w *WebServer) getClient(rw http.ResponseWriter, clientID string) {
w.mu.RLock()
defer w.mu.RUnlock()
for _, c := range w.config.Clients {
if c.ID == clientID {
online, lastPing := w.server.GetClientStatus(clientID)
w.jsonResponse(rw, map[string]interface{}{
"id": c.ID, "rules": c.Rules,
"online": online, "last_ping": lastPing,
})
return
}
}
http.Error(rw, "client not found", http.StatusNotFound)
}
func (w *WebServer) updateClient(rw http.ResponseWriter, r *http.Request, clientID string) {
var client config.ClientConfig
if err := json.NewDecoder(r.Body).Decode(&client); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
w.mu.Lock()
found := false
for i, c := range w.config.Clients {
if c.ID == clientID {
client.ID = clientID
w.config.Clients[i] = client
found = true
break
}
}
w.mu.Unlock()
if !found {
http.Error(rw, "client not found", http.StatusNotFound)
return
}
if err := w.saveConfig(); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
w.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (w *WebServer) deleteClient(rw http.ResponseWriter, clientID string) {
w.mu.Lock()
found := false
for i, c := range w.config.Clients {
if c.ID == clientID {
w.config.Clients = append(w.config.Clients[:i], w.config.Clients[i+1:]...)
found = true
break
}
}
w.mu.Unlock()
if !found {
http.Error(rw, "client not found", http.StatusNotFound)
return
}
if err := w.saveConfig(); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
w.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (w *WebServer) handleReload(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if err := w.server.ReloadConfig(); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
w.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (w *WebServer) saveConfig() error {
w.mu.RLock()
defer w.mu.RUnlock()
return config.SaveServerConfig(w.configPath, w.config)
}
func (w *WebServer) jsonResponse(rw http.ResponseWriter, data interface{}) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(data)
}
// RunWithAuth 启动带认证的Web服务
func (w *WebServer) RunWithAuth(addr, username, password string) error {
mux := http.NewServeMux()
mux.HandleFunc("/api/status", w.handleStatus)
mux.HandleFunc("/api/clients", w.handleClients)
mux.HandleFunc("/api/client/", w.handleClient)
mux.HandleFunc("/api/config/reload", w.handleReload)
staticFS, err := fs.Sub(staticFiles, "dist")
if err != nil {
return fmt.Errorf("failed to load static files: %v", err)
}
mux.Handle("/", spaHandler{fs: http.FS(staticFS)})
handler := &authMiddleware{username, password, mux}
log.Printf("[Web] Console listening on %s (auth enabled)", addr)
return http.ListenAndServe(addr, handler)
}
type authMiddleware struct {
username, password string
handler http.Handler
}
func (a *authMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok || user != a.username || pass != a.password {
w.Header().Set("WWW-Authenticate", `Basic realm="GoTunnel"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
a.handler.ServeHTTP(w, r)
}