Files
regplatformm/internal/handler/proxy_pool_handler.go
T
xiaolajiaoyyds 6f71df4849 Initial open-source release
多平台账号批量注册系统,支持 OpenAI / Grok / Kiro / Gemini 平台的全自动注册。

Features:
- 用户名/密码注册登录(首个用户自动管理员)
- JWT 认证 + bcrypt 密码加密
- 积分系统与多用户管理
- HuggingFace Space 弹性 Worker 节点管理
- Cloudflare Worker 负载均衡
- 实时日志推送(SSE/WebSocket)
- 管理后台(用户管理、系统设置、代理池、卡池)

Tech Stack: Go 1.25 / Gin / GORM / PostgreSQL / Vue 3 / Vite / TailwindCSS
2026-03-22 03:44:42 +08:00

573 lines
17 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 handler
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
"github.com/xiaolajiaoyyds/regplatformm/internal/model"
"github.com/xiaolajiaoyyds/regplatformm/internal/pkg/netutil"
"github.com/xiaolajiaoyyds/regplatformm/internal/service"
"gorm.io/gorm"
)
// ProxyPoolHandler 全局代理池管理(管理员)
type ProxyPoolHandler struct {
db *gorm.DB
proxyPool *service.ProxyPool
}
// NewProxyPoolHandler 创建代理池管理处理器
func NewProxyPoolHandler(db *gorm.DB, proxyPool *service.ProxyPool) *ProxyPoolHandler {
return &ProxyPoolHandler{db: db, proxyPool: proxyPool}
}
// ── 请求结构体 ──────────────────────────────────────────────────────
type createPoolProxyReq struct {
Name string `json:"name" binding:"max=100"`
Protocol string `json:"protocol" binding:"required,oneof=http https socks5"`
Host string `json:"host" binding:"required,max=200"`
Port int `json:"port" binding:"required,min=1,max=65535"`
Username string `json:"username" binding:"max=100"`
Password string `json:"password" binding:"max=200"`
Country string `json:"country" binding:"max=10"`
}
// ── CRUD ────────────────────────────────────────────────────────────
// List 获取代理池列表(GET /api/admin/proxy-pool,支持分页)
func (h *ProxyPoolHandler) List(c *gin.Context) {
query := h.db.Where("protocol IN ?", []string{"http", "https", "socks5"}).Order("created_at DESC")
// 可选过滤:healthy / unhealthy
if filter := c.Query("filter"); filter == "healthy" {
query = query.Where("is_healthy = ?", true)
} else if filter == "unhealthy" {
query = query.Where("is_healthy = ?", false)
}
// 分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 500 {
pageSize = 50
}
// 总数(Session 克隆防止 Count 污染后续 Find 查询)
var total int64
query.Session(&gorm.Session{}).Model(&model.ProxyPoolEntry{}).Count(&total)
// 分页查询
var entries []model.ProxyPoolEntry
if err := query.Offset((page - 1) * pageSize).Limit(pageSize).Find(&entries).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "查询失败"})
return
}
// 脱敏密码
for i := range entries {
if entries[i].Password != "" {
entries[i].Password = "******"
}
}
c.JSON(http.StatusOK, gin.H{
"items": entries,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// Create 添加代理到池(POST /api/admin/proxy-pool)
func (h *ProxyPoolHandler) Create(c *gin.Context) {
var req createPoolProxyReq
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// SSRF 防护:拒绝私网/保留地址
if netutil.IsPrivateHost(req.Host) {
c.JSON(http.StatusBadRequest, gin.H{"detail": "不允许添加内网地址的代理"})
return
}
// 去重检查
var count int64
if err := h.db.Model(&model.ProxyPoolEntry{}).Where("host = ? AND port = ?", req.Host, req.Port).Count(&count).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "查询失败"})
return
}
if count > 0 {
c.JSON(http.StatusConflict, gin.H{"detail": fmt.Sprintf("代理 %s:%d 已存在", req.Host, req.Port)})
return
}
entry := model.ProxyPoolEntry{
Name: req.Name,
Protocol: req.Protocol,
Host: req.Host,
Port: req.Port,
Username: req.Username,
Password: req.Password,
Country: req.Country,
Source: "manual",
}
if err := h.db.Create(&entry).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "创建失败"})
return
}
h.proxyPool.Reload()
if entry.Password != "" {
entry.Password = "******"
}
c.JSON(http.StatusOK, entry)
}
// Delete 删除代理(DELETE /api/admin/proxy-pool/:id)
func (h *ProxyPoolHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的 ID"})
return
}
result := h.db.Where("id = ?", id).Delete(&model.ProxyPoolEntry{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "删除失败"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"detail": "代理不存在"})
return
}
h.proxyPool.Reload()
c.JSON(http.StatusOK, gin.H{"message": "已删除"})
}
// BatchDelete 批量删除(POST /api/admin/proxy-pool/batch-delete)
func (h *ProxyPoolHandler) BatchDelete(c *gin.Context) {
var req struct {
IDs []uint `json:"ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
if len(req.IDs) > 500 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "单次最多删除 500 条"})
return
}
result := h.db.Where("id IN ?", req.IDs).Delete(&model.ProxyPoolEntry{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "删除失败"})
return
}
h.proxyPool.Reload()
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("已删除 %d 条", result.RowsAffected)})
}
// ── 批量导入 ────────────────────────────────────────────────────────
// Import 批量导入代理(POST /api/admin/proxy-pool/import)
// 支持格式:每行一个,格式 protocol://[user:pass@]host:port 或 host:port(默认 socks5)
func (h *ProxyPoolHandler) Import(c *gin.Context) {
var req struct {
Proxies string `json:"proxies" binding:"required"` // 换行分隔的代理列表
Protocol string `json:"protocol"` // 默认协议(未指定时使用)
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
defaultProto := req.Protocol
if defaultProto == "" {
defaultProto = "socks5"
}
// 协议白名单校验
allowedProtos := map[string]bool{"http": true, "https": true, "socks5": true}
if !allowedProtos[defaultProto] {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("不支持的协议: %s,仅支持 http/https/socks5", defaultProto)})
return
}
const maxImportLines = 1000
scanner := bufio.NewScanner(strings.NewReader(req.Proxies))
var imported, skipped, failed int
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if imported+skipped+failed >= maxImportLines {
failed++
continue
}
entry, err := parseProxyLine(line, defaultProto)
if err != nil {
log.Debug().Str("line", line).Err(err).Msg("代理导入解析失败")
failed++
continue
}
// 协议白名单(解析后二次校验)
if !allowedProtos[entry.Protocol] {
failed++
continue
}
// SSRF 防护:拒绝私网/保留地址
if netutil.IsPrivateHost(entry.Host) {
log.Warn().Str("host", entry.Host).Msg("代理导入拒绝内网地址")
failed++
continue
}
// 去重检查
var count int64
h.db.Model(&model.ProxyPoolEntry{}).Where("host = ? AND port = ?", entry.Host, entry.Port).Count(&count)
if count > 0 {
skipped++
continue
}
entry.Source = "imported"
if err := h.db.Create(entry).Error; err != nil {
failed++
continue
}
imported++
}
h.proxyPool.Reload()
c.JSON(http.StatusOK, gin.H{
"imported": imported,
"skipped": skipped,
"failed": failed,
"message": fmt.Sprintf("导入完成:成功 %d,重复跳过 %d,失败 %d", imported, skipped, failed),
})
}
// parseProxyLine 解析单行代理地址
// 支持格式:
// - socks5://user:pass@host:port
// - host:port(使用默认协议)
// - host:port:user:pass(使用默认协议)
func parseProxyLine(line, defaultProto string) (*model.ProxyPoolEntry, error) {
entry := &model.ProxyPoolEntry{}
// 带协议前缀
if strings.Contains(line, "://") {
parts := strings.SplitN(line, "://", 2)
entry.Protocol = parts[0]
rest := parts[1]
// 提取认证信息
if atIdx := strings.LastIndex(rest, "@"); atIdx > 0 {
authPart := rest[:atIdx]
rest = rest[atIdx+1:]
if colonIdx := strings.Index(authPart, ":"); colonIdx > 0 {
entry.Username = authPart[:colonIdx]
entry.Password = authPart[colonIdx+1:]
}
}
host, port, err := parseHostPort(rest)
if err != nil {
return nil, err
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("端口超出范围: %d", port)
}
entry.Host = host
entry.Port = port
} else {
// 无协议前缀
entry.Protocol = defaultProto
parts := strings.Split(line, ":")
switch len(parts) {
case 2:
// host:port
port, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("端口格式错误: %s", parts[1])
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("端口超出范围: %d", port)
}
entry.Host = parts[0]
entry.Port = port
case 4:
// host:port:user:pass
port, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("端口格式错误: %s", parts[1])
}
if port < 1 || port > 65535 {
return nil, fmt.Errorf("端口超出范围: %d", port)
}
entry.Host = parts[0]
entry.Port = port
entry.Username = parts[2]
entry.Password = parts[3]
default:
return nil, fmt.Errorf("无法解析: %s", line)
}
}
return entry, nil
}
// parseHostPort 解析 host:port(支持 IPv6 如 [::1]:8080)
func parseHostPort(s string) (string, int, error) {
host, portStr, err := net.SplitHostPort(s)
if err != nil {
return "", 0, fmt.Errorf("无效 host:port: %s", s)
}
port, err := strconv.Atoi(portStr)
if err != nil {
return "", 0, fmt.Errorf("端口格式错误: %s", portStr)
}
return host, port, nil
}
// ── 健康检查 ────────────────────────────────────────────────────────
// 健康检查冷却:防止频繁触发导致大量出站连接
var (
healthCheckMu sync.Mutex
healthCheckLastTime time.Time
)
const healthCheckCooldown = 30 * time.Second // 最短间隔 30 秒
// HealthCheck 触发代理池健康检查(POST /api/admin/proxy-pool/health-check)
// 支持选择性检查:请求体含 ids 字段时仅检查指定代理(跳过冷却),为空时检查全部
func (h *ProxyPoolHandler) HealthCheck(c *gin.Context) {
var req struct {
IDs []uint `json:"ids"`
}
// 忽略绑定错误,允许空 body(兼容旧调用)
_ = c.ShouldBindJSON(&req)
// 指定 ID 列表:小批量检查,跳过全局冷却
if len(req.IDs) > 0 {
if len(req.IDs) > 500 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "单次最多检查 500 个代理"})
return
}
total, healthy, unhealthy := h.proxyPool.HealthCheckByIDs(req.IDs)
c.JSON(http.StatusOK, gin.H{
"total": total,
"healthy": healthy,
"unhealthy": unhealthy,
"message": fmt.Sprintf("检查完成:共 %d 个,健康 %d,不健康 %d", total, healthy, unhealthy),
})
return
}
// 全量检查:受 30 秒冷却限制
healthCheckMu.Lock()
if time.Since(healthCheckLastTime) < healthCheckCooldown {
remaining := healthCheckCooldown - time.Since(healthCheckLastTime)
healthCheckMu.Unlock()
c.JSON(http.StatusTooManyRequests, gin.H{
"detail": fmt.Sprintf("健康检查冷却中,请 %d 秒后再试", int(remaining.Seconds())+1),
"retry_after": int(remaining.Seconds()) + 1,
})
return
}
healthCheckLastTime = time.Now()
healthCheckMu.Unlock()
total, healthy, unhealthy := h.proxyPool.HealthCheckAll()
c.JSON(http.StatusOK, gin.H{
"total": total,
"healthy": healthy,
"unhealthy": unhealthy,
"message": fmt.Sprintf("检查完成:共 %d 个,健康 %d,不健康 %d", total, healthy, unhealthy),
})
}
// Stats 代理池统计(GET /api/admin/proxy-pool/stats)
func (h *ProxyPoolHandler) Stats(c *gin.Context) {
stats := h.proxyPool.PoolStats()
c.JSON(http.StatusOK, stats)
}
// PurgeUnhealthy 清除所有不健康代理(POST /api/admin/proxy-pool/purge)
func (h *ProxyPoolHandler) PurgeUnhealthy(c *gin.Context) {
result := h.db.Where("is_healthy = ?", false).Delete(&model.ProxyPoolEntry{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "清除失败"})
return
}
h.proxyPool.Reload()
c.JSON(http.StatusOK, gin.H{
"deleted": result.RowsAffected,
"message": fmt.Sprintf("已清除 %d 个不健康代理", result.RowsAffected),
})
}
// ResetHealth 重置指定代理的健康状态(POST /api/admin/proxy-pool/:id/reset)
func (h *ProxyPoolHandler) ResetHealth(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "无效的 ID"})
return
}
now := time.Now()
result := h.db.Model(&model.ProxyPoolEntry{}).Where("id = ?", id).Updates(map[string]interface{}{
"is_healthy": true,
"fail_count": 0,
"last_checked_at": now,
})
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"detail": "代理不存在"})
return
}
h.proxyPool.Reload()
c.JSON(http.StatusOK, gin.H{"message": "已重置健康状态"})
}
// ── 从 URL 抓取 ────────────────────────────────────────────────────
// FetchURL 从 URL 抓取代理列表并导入(POST /api/admin/proxy-pool/fetch-url)
func (h *ProxyPoolHandler) FetchURL(c *gin.Context) {
var req struct {
URL string `json:"url" binding:"required"`
Protocol string `json:"protocol"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
defaultProto := req.Protocol
if defaultProto == "" {
defaultProto = "socks5"
}
allowedProtos := map[string]bool{"http": true, "https": true, "socks5": true}
if !allowedProtos[defaultProto] {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("不支持的协议: %s,仅支持 http/https/socks5", defaultProto)})
return
}
// SSRF 防护:校验 URL 协议白名单
if err := netutil.ValidateURLScheme(req.URL); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// SSRF 防护:校验 URL 主机不指向内网
if err := netutil.ValidateURLHost(req.URL); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// 使用 SSRF 安全客户端下载
client := netutil.NewSSRFSafeClient(30 * time.Second)
httpReq, err := http.NewRequest("GET", req.URL, nil)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("无效的 URL: %s", err.Error())})
return
}
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
httpReq.Header.Set("Accept", "text/plain, */*")
resp, err := client.Do(httpReq)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"detail": fmt.Sprintf("抓取失败: %s", err.Error())})
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
c.JSON(http.StatusBadGateway, gin.H{"detail": fmt.Sprintf("远程返回 HTTP %d", resp.StatusCode)})
return
}
// 限制读取大小(5MB)
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"detail": fmt.Sprintf("读取内容失败: %s", err.Error())})
return
}
// 逐行解析并导入
const maxLines = 2000
scanner := bufio.NewScanner(strings.NewReader(string(body)))
var imported, skipped, failed int
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
continue
}
if imported+skipped+failed >= maxLines {
failed++
continue
}
entry, err := parseProxyLine(line, defaultProto)
if err != nil {
failed++
continue
}
// 协议白名单
if !allowedProtos[entry.Protocol] {
failed++
continue
}
// SSRF 防护
if netutil.IsPrivateHost(entry.Host) {
failed++
continue
}
// 去重
var count int64
h.db.Model(&model.ProxyPoolEntry{}).Where("host = ? AND port = ?", entry.Host, entry.Port).Count(&count)
if count > 0 {
skipped++
continue
}
entry.Source = "fetched"
if err := h.db.Create(entry).Error; err != nil {
failed++
continue
}
imported++
}
h.proxyPool.Reload()
c.JSON(http.StatusOK, gin.H{
"imported": imported,
"skipped": skipped,
"failed": failed,
"message": fmt.Sprintf("抓取完成:成功 %d,重复跳过 %d,失败 %d", imported, skipped, failed),
})
}