mirror of
https://github.com/isboyjc/GoProxy.git
synced 2026-07-13 08:23:53 +08:00
feat: ✨ init
This commit is contained in:
1109
webui/dashboard.go
1109
webui/dashboard.go
File diff suppressed because it is too large
Load Diff
100
webui/html.go
100
webui/html.go
@@ -5,31 +5,45 @@ const loginHTML = `<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ProxyGo - 登录</title>
|
||||
<title>GoProxy — 身份验证</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#0f172a;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
||||
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:40px;width:360px;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
|
||||
h1{font-size:24px;font-weight:700;margin-bottom:8px;color:#f1f5f9}
|
||||
.sub{color:#94a3b8;font-size:14px;margin-bottom:32px}
|
||||
label{display:block;font-size:13px;color:#94a3b8;margin-bottom:6px}
|
||||
input[type=password]{width:100%;padding:10px 14px;background:#0f172a;border:1px solid #334155;border-radius:8px;color:#f1f5f9;font-size:15px;outline:none;transition:border 0.2s}
|
||||
input[type=password]:focus{border-color:#6366f1}
|
||||
button{width:100%;margin-top:20px;padding:11px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:background 0.2s}
|
||||
button:hover{background:#4f46e5}
|
||||
.logo{font-size:32px;margin-bottom:16px}
|
||||
body{background:#0a0a0a;color:#00ff41;font-family:JetBrains Mono,monospace;display:flex;align-items:center;justify-content:center;min-height:100vh;position:relative}
|
||||
body::before{content:'';position:fixed;top:0;left:0;width:100%;height:100%;background:repeating-linear-gradient(0deg,rgba(0,255,65,0.03) 0px,transparent 1px,transparent 2px,rgba(0,255,65,0.03) 3px);pointer-events:none;z-index:9999}
|
||||
body::after{content:'';position:fixed;top:0;left:0;width:100%;height:100%;background:radial-gradient(ellipse at center,rgba(0,255,65,0.08) 0%,transparent 70%);pointer-events:none;z-index:9998}
|
||||
.card{border:1px solid #00ff41;padding:64px;width:440px;background:#111;box-shadow:0 0 40px rgba(0,255,65,0.3);position:relative;z-index:1}
|
||||
h1{font-size:32px;font-weight:700;margin-bottom:8px;letter-spacing:0.15em;text-transform:uppercase;color:#00ff41;text-shadow:0 0 15px #00ff41}
|
||||
.sub{color:#00cc33;font-size:12px;margin-bottom:48px;font-family:JetBrains Mono,monospace;letter-spacing:0.08em;text-transform:uppercase}
|
||||
label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:0.1em;color:#00cc33;margin-bottom:8px;font-weight:600}
|
||||
input[type=password]{width:100%;padding:16px;background:#0d0d0d;border:1px solid #1a3a1a;color:#00ff41;font-size:16px;font-family:JetBrains Mono,monospace;outline:none;transition:all 0.2s}
|
||||
input[type=password]:focus{border-color:#00ff41;background:#111;box-shadow:0 0 10px rgba(0,255,65,0.3)}
|
||||
button{width:100%;margin-top:24px;padding:16px;background:#00ff41;color:#000;border:1px solid #00ff41;font-size:12px;font-weight:700;cursor:pointer;transition:all 0.2s;text-transform:uppercase;letter-spacing:0.1em;font-family:JetBrains Mono,monospace;box-shadow:0 0 15px rgba(0,255,65,0.5)}
|
||||
button:hover{box-shadow:0 0 25px rgba(0,255,65,0.8);transform:translateY(-1px)}
|
||||
.logo{font-size:64px;margin-bottom:24px;line-height:1;font-weight:700;letter-spacing:0.1em;color:#00ff41;text-shadow:0 0 20px #00ff41}
|
||||
.tip{color:#888;font-size:10px;margin-top:24px;line-height:1.6;letter-spacing:0.05em;text-align:center}
|
||||
.tip a{color:#00ff41;text-decoration:none;border-bottom:1px solid transparent;transition:all 0.2s}
|
||||
.tip a:hover{border-bottom-color:#00ff41;text-shadow:0 0 8px #00ff41}
|
||||
.github{position:absolute;top:20px;right:20px;color:#00ff41;opacity:0.6;transition:all 0.3s}
|
||||
.github:hover{opacity:1;transform:scale(1.1);filter:drop-shadow(0 0 8px #00ff41)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="https://github.com/isboyjc/ProxyGo" target="_blank" class="github" title="GitHub">
|
||||
<svg width="32" height="32" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="card">
|
||||
<div class="logo">⚡</div>
|
||||
<h1>ProxyGo</h1>
|
||||
<p class="sub">代理池管理系统</p>
|
||||
<div class="logo">[GP]</div>
|
||||
<h1>GoProxy</h1>
|
||||
<p class="sub">// Intelligent Proxy Pool</p>
|
||||
<form method="POST" action="/login">
|
||||
<label>管理密码</label>
|
||||
<input type="password" name="password" placeholder="请输入密码" autofocus>
|
||||
<button type="submit">登录</button>
|
||||
<label>> Password</label>
|
||||
<input type="password" name="password" placeholder="****************" autofocus>
|
||||
<button type="submit">[ AUTHENTICATE ]</button>
|
||||
</form>
|
||||
<p class="tip">访客模式可<a href="/">查看数据</a>,管理员登录后可完全控制</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
@@ -39,33 +53,47 @@ const loginHTMLWithError = `<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ProxyGo - 登录</title>
|
||||
<title>GoProxy — 身份验证</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{background:#0f172a;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
||||
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:40px;width:360px;box-shadow:0 20px 60px rgba(0,0,0,0.5)}
|
||||
h1{font-size:24px;font-weight:700;margin-bottom:8px;color:#f1f5f9}
|
||||
.sub{color:#94a3b8;font-size:14px;margin-bottom:32px}
|
||||
label{display:block;font-size:13px;color:#94a3b8;margin-bottom:6px}
|
||||
input[type=password]{width:100%;padding:10px 14px;background:#0f172a;border:1px solid #334155;border-radius:8px;color:#f1f5f9;font-size:15px;outline:none;transition:border 0.2s}
|
||||
input[type=password]:focus{border-color:#6366f1}
|
||||
button{width:100%;margin-top:20px;padding:11px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:600;cursor:pointer;transition:background 0.2s}
|
||||
button:hover{background:#4f46e5}
|
||||
.logo{font-size:32px;margin-bottom:16px}
|
||||
.error{background:#450a0a;border:1px solid #7f1d1d;color:#fca5a5;padding:10px 14px;border-radius:8px;font-size:13px;margin-bottom:16px}
|
||||
body{background:#0a0a0a;color:#00ff41;font-family:JetBrains Mono,monospace;display:flex;align-items:center;justify-content:center;min-height:100vh;position:relative}
|
||||
body::before{content:'';position:fixed;top:0;left:0;width:100%;height:100%;background:repeating-linear-gradient(0deg,rgba(0,255,65,0.03) 0px,transparent 1px,transparent 2px,rgba(0,255,65,0.03) 3px);pointer-events:none;z-index:9999}
|
||||
body::after{content:'';position:fixed;top:0;left:0;width:100%;height:100%;background:radial-gradient(ellipse at center,rgba(0,255,65,0.08) 0%,transparent 70%);pointer-events:none;z-index:9998}
|
||||
.card{border:1px solid #00ff41;padding:64px;width:440px;background:#111;box-shadow:0 0 40px rgba(0,255,65,0.3);position:relative;z-index:1}
|
||||
h1{font-size:32px;font-weight:700;margin-bottom:8px;letter-spacing:0.15em;text-transform:uppercase;color:#00ff41;text-shadow:0 0 15px #00ff41}
|
||||
.sub{color:#00cc33;font-size:12px;margin-bottom:48px;font-family:JetBrains Mono,monospace;letter-spacing:0.08em;text-transform:uppercase}
|
||||
label{display:block;font-size:10px;text-transform:uppercase;letter-spacing:0.1em;color:#00cc33;margin-bottom:8px;font-weight:600}
|
||||
input[type=password]{width:100%;padding:16px;background:#0d0d0d;border:1px solid #1a3a1a;color:#00ff41;font-size:16px;font-family:JetBrains Mono,monospace;outline:none;transition:all 0.2s}
|
||||
input[type=password]:focus{border-color:#00ff41;background:#111;box-shadow:0 0 10px rgba(0,255,65,0.3)}
|
||||
button{width:100%;margin-top:24px;padding:16px;background:#00ff41;color:#000;border:1px solid #00ff41;font-size:12px;font-weight:700;cursor:pointer;transition:all 0.2s;text-transform:uppercase;letter-spacing:0.1em;font-family:JetBrains Mono,monospace;box-shadow:0 0 15px rgba(0,255,65,0.5)}
|
||||
button:hover{box-shadow:0 0 25px rgba(0,255,65,0.8);transform:translateY(-1px)}
|
||||
.logo{font-size:64px;margin-bottom:24px;line-height:1;font-weight:700;letter-spacing:0.1em;color:#00ff41;text-shadow:0 0 20px #00ff41}
|
||||
.error{background:#ff0033;color:#fff;padding:16px;font-size:11px;margin-bottom:24px;font-family:JetBrains Mono,monospace;font-weight:600;border:1px solid #ff0033;box-shadow:0 0 15px rgba(255,0,51,0.5);text-transform:uppercase;letter-spacing:0.05em}
|
||||
.tip{color:#888;font-size:10px;margin-top:24px;line-height:1.6;letter-spacing:0.05em;text-align:center}
|
||||
.tip a{color:#00ff41;text-decoration:none;border-bottom:1px solid transparent;transition:all 0.2s}
|
||||
.tip a:hover{border-bottom-color:#00ff41;text-shadow:0 0 8px #00ff41}
|
||||
.github{position:absolute;top:20px;right:20px;color:#00ff41;opacity:0.6;transition:all 0.3s}
|
||||
.github:hover{opacity:1;transform:scale(1.1);filter:drop-shadow(0 0 8px #00ff41)}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="https://github.com/isboyjc/ProxyGo" target="_blank" class="github" title="GitHub">
|
||||
<svg width="32" height="32" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="card">
|
||||
<div class="logo">⚡</div>
|
||||
<h1>ProxyGo</h1>
|
||||
<p class="sub">代理池管理系统</p>
|
||||
<div class="error">密码错误,请重试</div>
|
||||
<div class="logo">[GP]</div>
|
||||
<h1>GoProxy</h1>
|
||||
<p class="sub">// Intelligent Proxy Pool</p>
|
||||
<div class="error">[!] ACCESS DENIED - INVALID PASSWORD</div>
|
||||
<form method="POST" action="/login">
|
||||
<label>管理密码</label>
|
||||
<input type="password" name="password" placeholder="请输入密码" autofocus>
|
||||
<button type="submit">登录</button>
|
||||
<label>> Password</label>
|
||||
<input type="password" name="password" placeholder="****************" autofocus>
|
||||
<button type="submit">[ AUTHENTICATE ]</button>
|
||||
</form>
|
||||
<p class="tip">访客模式可<a href="/">查看数据</a>,管理员登录后可完全控制</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
292
webui/server.go
292
webui/server.go
@@ -9,9 +9,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"proxy-pool/config"
|
||||
"proxy-pool/logger"
|
||||
"proxy-pool/storage"
|
||||
"goproxy/config"
|
||||
"goproxy/logger"
|
||||
"goproxy/pool"
|
||||
"goproxy/storage"
|
||||
"goproxy/validator"
|
||||
)
|
||||
|
||||
// 简单内存 session
|
||||
@@ -44,12 +46,19 @@ 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, ft FetchTrigger, cc chan<- struct{}) *Server {
|
||||
return &Server{storage: s, cfg: cfg, fetchTrigger: ft, configChanged: cc}
|
||||
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() {
|
||||
@@ -57,12 +66,22 @@ func (s *Server) Start() {
|
||||
mux.HandleFunc("/", s.handleIndex)
|
||||
mux.HandleFunc("/login", s.handleLogin)
|
||||
mux.HandleFunc("/logout", s.handleLogout)
|
||||
mux.HandleFunc("/api/stats", s.authMiddleware(s.apiStats))
|
||||
mux.HandleFunc("/api/proxies", s.authMiddleware(s.apiProxies))
|
||||
|
||||
// 只读 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/logs", s.authMiddleware(s.apiLogs))
|
||||
mux.HandleFunc("/api/config", s.authMiddleware(s.apiConfig))
|
||||
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() {
|
||||
@@ -72,6 +91,7 @@ func (s *Server) Start() {
|
||||
}()
|
||||
}
|
||||
|
||||
// authMiddleware 管理员权限中间件(必须登录)
|
||||
func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !validSession(r) {
|
||||
@@ -86,11 +106,16 @@ func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if !validSession(r) {
|
||||
http.Redirect(w, r, "/login", http.StatusFound)
|
||||
return
|
||||
// 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)
|
||||
}
|
||||
@@ -129,6 +154,20 @@ func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
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")
|
||||
@@ -173,6 +212,60 @@ func (s *Server) apiDeleteProxy(w http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
@@ -182,54 +275,173 @@ func (s *Server) apiFetch(w http.ResponseWriter, r *http.Request) {
|
||||
jsonOK(w, map[string]string{"status": "fetch started"})
|
||||
}
|
||||
|
||||
func (s *Server) apiLogs(w http.ResponseWriter, r *http.Request) {
|
||||
lines := logger.GetLines(200)
|
||||
jsonOK(w, map[string]interface{}{"lines": lines})
|
||||
}
|
||||
|
||||
func (s *Server) apiConfig(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
if r.Method == http.MethodGet {
|
||||
jsonOK(w, map[string]interface{}{
|
||||
"fetch_interval": cfg.FetchInterval,
|
||||
"check_interval": cfg.CheckInterval,
|
||||
"validate_concurrency": cfg.ValidateConcurrency,
|
||||
"validate_timeout": cfg.ValidateTimeout,
|
||||
})
|
||||
return
|
||||
}
|
||||
func (s *Server) apiRefreshLatency(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
jsonError(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
FetchInterval int `json:"fetch_interval"`
|
||||
CheckInterval int `json:"check_interval"`
|
||||
ValidateConcurrency int `json:"validate_concurrency"`
|
||||
ValidateTimeout int `json:"validate_timeout"`
|
||||
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.FetchInterval <= 0 || req.CheckInterval <= 0 || req.ValidateConcurrency <= 0 || req.ValidateTimeout <= 0 {
|
||||
jsonError(w, "all values must be positive", http.StatusBadRequest)
|
||||
|
||||
// 验证配置有效性
|
||||
if req.PoolMaxSize <= 0 || req.PoolHTTPRatio <= 0 || req.PoolHTTPRatio > 1 {
|
||||
jsonError(w, "invalid pool config", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := config.Save(req.FetchInterval, req.CheckInterval, req.ValidateConcurrency, req.ValidateTimeout); err != nil {
|
||||
|
||||
// 记录旧配置
|
||||
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:
|
||||
}
|
||||
log.Printf("[config] updated: fetch=%dm check=%dm concurrency=%d timeout=%ds",
|
||||
req.FetchInterval, req.CheckInterval, req.ValidateConcurrency, req.ValidateTimeout)
|
||||
|
||||
// 如果池子大小或比例变更,调整池子
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user