Files
GoProxy/webui/server.go

463 lines
13 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 webui
import (
"crypto/sha256"
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"goproxy/config"
"goproxy/logger"
"goproxy/pool"
"goproxy/storage"
"goproxy/validator"
)
// 简单内存 session
var (
sessions = make(map[string]time.Time)
sessionsMu sync.Mutex
)
func newSession() string {
token := fmt.Sprintf("%x", sha256.Sum256([]byte(fmt.Sprintf("%d", time.Now().UnixNano()))))
sessionsMu.Lock()
sessions[token] = time.Now().Add(24 * time.Hour)
sessionsMu.Unlock()
return token
}
func validSession(r *http.Request) bool {
cookie, err := r.Cookie("session")
if err != nil {
return false
}
sessionsMu.Lock()
expiry, ok := sessions[cookie.Value]
sessionsMu.Unlock()
return ok && time.Now().Before(expiry)
}
type FetchTrigger func()
type Server struct {
storage *storage.Storage
cfg *config.Config
poolMgr *pool.Manager
fetchTrigger FetchTrigger
configChanged chan<- struct{}
}
func New(s *storage.Storage, cfg *config.Config, pm *pool.Manager, ft FetchTrigger, cc chan<- struct{}) *Server {
return &Server{
storage: s,
cfg: cfg,
poolMgr: pm,
fetchTrigger: ft,
configChanged: cc,
}
}
func (s *Server) Start() {
mux := http.NewServeMux()
// 添加日志中间件
loggedMux := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("[webui] %s %s | Host: %s | RemoteAddr: %s",
r.Method, r.URL.Path, r.Host, r.RemoteAddr)
mux.ServeHTTP(w, r)
})
mux.HandleFunc("/", s.handleIndex)
mux.HandleFunc("/login", s.handleLogin)
mux.HandleFunc("/logout", s.handleLogout)
// 只读 API访客可访问
mux.HandleFunc("/api/stats", s.readOnlyMiddleware(s.apiStats))
mux.HandleFunc("/api/proxies", s.readOnlyMiddleware(s.apiProxies))
mux.HandleFunc("/api/logs", s.readOnlyMiddleware(s.apiLogs))
mux.HandleFunc("/api/pool/status", s.readOnlyMiddleware(s.apiPoolStatus))
mux.HandleFunc("/api/pool/quality", s.readOnlyMiddleware(s.apiQualityDistribution))
mux.HandleFunc("/api/config", s.readOnlyMiddleware(s.apiConfig))
mux.HandleFunc("/api/auth/check", s.apiAuthCheck) // 检查登录状态
// 管理员 API需要登录
mux.HandleFunc("/api/proxy/delete", s.authMiddleware(s.apiDeleteProxy))
mux.HandleFunc("/api/proxy/refresh", s.authMiddleware(s.apiRefreshProxy))
mux.HandleFunc("/api/fetch", s.authMiddleware(s.apiFetch))
mux.HandleFunc("/api/refresh-latency", s.authMiddleware(s.apiRefreshLatency))
mux.HandleFunc("/api/config/save", s.authMiddleware(s.apiConfigSave))
log.Printf("WebUI listening on %s", s.cfg.WebUIPort)
go func() {
if err := http.ListenAndServe(s.cfg.WebUIPort, loggedMux); err != nil {
log.Fatalf("webui: %v", err)
}
}()
}
// authMiddleware 管理员权限中间件(必须登录)
func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !validSession(r) {
if len(r.URL.Path) >= 4 && r.URL.Path[:4] == "/api" {
jsonError(w, "unauthorized", http.StatusUnauthorized)
return
}
http.Redirect(w, r, "/login", http.StatusFound)
return
}
next(w, r)
}
}
// readOnlyMiddleware 只读中间件(访客可访问,但会标记是否为管理员)
func (s *Server) readOnlyMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 访客和管理员都可以访问,通过 validSession 判断权限
next(w, r)
}
}
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
// 允许访客访问(只读模式),管理员登录后有完整权限
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, dashboardHTML)
}
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, loginHTML)
return
}
password := r.FormValue("password")
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(password)))
if hash != s.cfg.WebUIPasswordHash {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, loginHTMLWithError)
return
}
token := newSession()
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
Path: "/",
Expires: time.Now().Add(24 * time.Hour),
HttpOnly: true,
})
http.Redirect(w, r, "/", http.StatusFound)
}
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie("session"); err == nil {
sessionsMu.Lock()
delete(sessions, cookie.Value)
sessionsMu.Unlock()
}
http.SetCookie(w, &http.Cookie{Name: "session", Value: "", Path: "/", MaxAge: -1})
http.Redirect(w, r, "/login", http.StatusFound)
}
// apiAuthCheck 检查当前用户是否为管理员
func (s *Server) apiAuthCheck(w http.ResponseWriter, r *http.Request) {
isAdmin := validSession(r)
jsonOK(w, map[string]interface{}{
"isAdmin": isAdmin,
"mode": func() string {
if isAdmin {
return "admin"
}
return "guest"
}(),
})
}
func (s *Server) apiStats(w http.ResponseWriter, r *http.Request) {
total, _ := s.storage.Count()
httpCount, _ := s.storage.CountByProtocol("http")
socks5Count, _ := s.storage.CountByProtocol("socks5")
jsonOK(w, map[string]interface{}{
"total": total,
"http": httpCount,
"socks5": socks5Count,
"port": s.cfg.ProxyPort,
})
}
func (s *Server) apiProxies(w http.ResponseWriter, r *http.Request) {
protocol := r.URL.Query().Get("protocol")
var proxies []storage.Proxy
var err error
if protocol != "" {
proxies, err = s.storage.GetByProtocol(protocol)
} else {
proxies, err = s.storage.GetAll()
}
if err != nil {
jsonError(w, err.Error(), http.StatusInternalServerError)
return
}
jsonOK(w, proxies)
}
func (s *Server) apiDeleteProxy(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
jsonError(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Address string `json:"address"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Address == "" {
jsonError(w, "invalid request", http.StatusBadRequest)
return
}
s.storage.Delete(req.Address)
jsonOK(w, map[string]string{"status": "deleted"})
}
func (s *Server) apiRefreshProxy(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
jsonError(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
Address string `json:"address"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Address == "" {
jsonError(w, "invalid request", http.StatusBadRequest)
return
}
// 从数据库获取代理信息
proxies, err := s.storage.GetAll()
if err != nil {
jsonError(w, "failed to get proxy", http.StatusInternalServerError)
return
}
var targetProxy *storage.Proxy
for i := range proxies {
if proxies[i].Address == req.Address {
targetProxy = &proxies[i]
break
}
}
if targetProxy == nil {
jsonError(w, "proxy not found", http.StatusNotFound)
return
}
// 异步验证并更新
go func() {
cfg := config.Get()
v := validator.New(1, cfg.ValidateTimeout, cfg.ValidateURL)
log.Printf("[webui] refreshing proxy: %s", req.Address)
valid, latency, exitIP, exitLocation := v.ValidateOne(*targetProxy)
if valid {
latencyMs := int(latency.Milliseconds())
s.storage.UpdateExitInfo(req.Address, exitIP, exitLocation, latencyMs)
log.Printf("[webui] proxy refreshed: %s latency=%dms grade=%s", req.Address, latencyMs, storage.CalculateQualityGrade(latencyMs))
} else {
s.storage.Delete(req.Address)
log.Printf("[webui] proxy validation failed, removed: %s", req.Address)
}
}()
jsonOK(w, map[string]string{"status": "refresh started"})
}
func (s *Server) apiFetch(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
jsonError(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
go s.fetchTrigger()
jsonOK(w, map[string]string{"status": "fetch started"})
}
func (s *Server) apiRefreshLatency(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
jsonError(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
go func() {
log.Println("[webui] refreshing latency for all proxies...")
proxies, err := s.storage.GetAll()
if err != nil {
log.Printf("[webui] get proxies error: %v", err)
return
}
if len(proxies) == 0 {
log.Println("[webui] no proxies to refresh")
return
}
cfg := config.Get()
validate := validator.New(cfg.ValidateConcurrency, cfg.ValidateTimeout, cfg.ValidateURL)
log.Printf("[webui] refreshing latency for %d proxies...", len(proxies))
updated := 0
for r := range validate.ValidateStream(proxies) {
if r.Valid {
latencyMs := int(r.Latency.Milliseconds())
s.storage.UpdateExitInfo(r.Proxy.Address, r.ExitIP, r.ExitLocation, latencyMs)
updated++
} else {
s.storage.Delete(r.Proxy.Address)
}
}
log.Printf("[webui] latency refresh done: updated=%d", updated)
}()
jsonOK(w, map[string]string{"status": "refresh started"})
}
func (s *Server) apiLogs(w http.ResponseWriter, r *http.Request) {
lines := logger.GetLines(100)
jsonOK(w, map[string]interface{}{"lines": lines})
}
// apiConfig 获取配置
func (s *Server) apiConfig(w http.ResponseWriter, r *http.Request) {
cfg := config.Get()
httpSlots, socks5Slots := cfg.CalculateSlots()
jsonOK(w, map[string]interface{}{
// 池子配置
"pool_max_size": cfg.PoolMaxSize,
"pool_http_ratio": cfg.PoolHTTPRatio,
"pool_min_per_protocol": cfg.PoolMinPerProtocol,
"pool_http_slots": httpSlots,
"pool_socks5_slots": socks5Slots,
// 延迟配置
"max_latency_ms": cfg.MaxLatencyMs,
"max_latency_emergency": cfg.MaxLatencyEmergency,
"max_latency_healthy": cfg.MaxLatencyHealthy,
// 验证配置
"validate_concurrency": cfg.ValidateConcurrency,
"validate_timeout": cfg.ValidateTimeout,
// 健康检查配置
"health_check_interval": cfg.HealthCheckInterval,
"health_check_batch_size": cfg.HealthCheckBatchSize,
// 优化配置
"optimize_interval": cfg.OptimizeInterval,
"replace_threshold": cfg.ReplaceThreshold,
})
}
// apiConfigSave 保存配置
func (s *Server) apiConfigSave(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
jsonError(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
PoolMaxSize int `json:"pool_max_size"`
PoolHTTPRatio float64 `json:"pool_http_ratio"`
PoolMinPerProtocol int `json:"pool_min_per_protocol"`
MaxLatencyMs int `json:"max_latency_ms"`
MaxLatencyEmergency int `json:"max_latency_emergency"`
MaxLatencyHealthy int `json:"max_latency_healthy"`
ValidateConcurrency int `json:"validate_concurrency"`
ValidateTimeout int `json:"validate_timeout"`
HealthCheckInterval int `json:"health_check_interval"`
HealthCheckBatchSize int `json:"health_check_batch_size"`
OptimizeInterval int `json:"optimize_interval"`
ReplaceThreshold float64 `json:"replace_threshold"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonError(w, "invalid request", http.StatusBadRequest)
return
}
// 验证配置有效性
if req.PoolMaxSize <= 0 || req.PoolHTTPRatio <= 0 || req.PoolHTTPRatio > 1 {
jsonError(w, "invalid pool config", http.StatusBadRequest)
return
}
// 记录旧配置
oldCfg := config.Get()
oldSize := oldCfg.PoolMaxSize
oldRatio := oldCfg.PoolHTTPRatio
// 更新配置
newCfg := *oldCfg
newCfg.PoolMaxSize = req.PoolMaxSize
newCfg.PoolHTTPRatio = req.PoolHTTPRatio
newCfg.PoolMinPerProtocol = req.PoolMinPerProtocol
newCfg.MaxLatencyMs = req.MaxLatencyMs
newCfg.MaxLatencyEmergency = req.MaxLatencyEmergency
newCfg.MaxLatencyHealthy = req.MaxLatencyHealthy
newCfg.ValidateConcurrency = req.ValidateConcurrency
newCfg.ValidateTimeout = req.ValidateTimeout
newCfg.HealthCheckInterval = req.HealthCheckInterval
newCfg.HealthCheckBatchSize = req.HealthCheckBatchSize
newCfg.OptimizeInterval = req.OptimizeInterval
newCfg.ReplaceThreshold = req.ReplaceThreshold
if err := config.Save(&newCfg); err != nil {
jsonError(w, "save config error: "+err.Error(), http.StatusInternalServerError)
return
}
// 通知配置变更
select {
case s.configChanged <- struct{}{}:
default:
}
// 如果池子大小或比例变更,调整池子
if oldSize != req.PoolMaxSize || oldRatio != req.PoolHTTPRatio {
go s.poolMgr.AdjustForConfigChange(oldSize, oldRatio)
}
log.Printf("[config] 配置已更新: 池子=%d HTTP=%.0f%% 延迟=%dms",
req.PoolMaxSize, req.PoolHTTPRatio*100, req.MaxLatencyMs)
jsonOK(w, map[string]string{"status": "saved"})
}
// apiPoolStatus 获取池子状态
func (s *Server) apiPoolStatus(w http.ResponseWriter, r *http.Request) {
status, err := s.poolMgr.GetStatus()
if err != nil {
jsonError(w, err.Error(), http.StatusInternalServerError)
return
}
jsonOK(w, status)
}
// apiQualityDistribution 获取质量分布
func (s *Server) apiQualityDistribution(w http.ResponseWriter, r *http.Request) {
dist, err := s.storage.GetQualityDistribution()
if err != nil {
jsonError(w, err.Error(), http.StatusInternalServerError)
return
}
jsonOK(w, dist)
}
func jsonOK(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(data)
}
func jsonError(w http.ResponseWriter, msg string, code int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}