| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- package handler
- import (
- "context"
- "fmt"
- "net"
- "net/http"
- "net/url"
- "strconv"
- "sync"
- "time"
- "spider/internal/model"
- "spider/internal/store"
- "spider/internal/task"
- "github.com/gin-gonic/gin"
- "golang.org/x/net/proxy"
- )
- // ProxyHandler handles proxy CRUD and testing.
- type ProxyHandler struct {
- store *store.Store
- taskMgr *task.Manager
- }
- // List handles GET /proxies
- func (h *ProxyHandler) List(c *gin.Context) {
- page, pageSize, offset := parsePage(c)
- query := h.store.DB.Model(&model.Proxy{})
- if status := c.Query("status"); status != "" {
- query = query.Where("status = ?", status)
- }
- if enabled := c.Query("enabled"); enabled != "" {
- query = query.Where("enabled = ?", enabled == "true")
- }
- if search := c.Query("search"); search != "" {
- like := "%" + search + "%"
- query = query.Where("name LIKE ? OR host LIKE ? OR region LIKE ?", like, like, like)
- }
- var total int64
- query.Count(&total)
- var proxies []model.Proxy
- if err := query.Order("id DESC").Limit(pageSize).Offset(offset).Find(&proxies).Error; err != nil {
- Fail(c, 500, err.Error())
- return
- }
- PageOK(c, proxies, total, page, pageSize)
- }
- // ListEnabled handles GET /proxies/enabled — returns only enabled proxies (for task dropdown)
- func (h *ProxyHandler) ListEnabled(c *gin.Context) {
- var proxies []model.Proxy
- h.store.DB.Where("enabled = ?", true).Order("name ASC").Find(&proxies)
- OK(c, proxies)
- }
- // Create handles POST /proxies
- func (h *ProxyHandler) Create(c *gin.Context) {
- var body struct {
- Name string `json:"name" binding:"required"`
- Protocol string `json:"protocol" binding:"required"`
- Host string `json:"host" binding:"required"`
- Port int `json:"port" binding:"required"`
- Username string `json:"username"`
- Password string `json:"password"`
- Region string `json:"region"`
- Remark string `json:"remark"`
- }
- if err := c.ShouldBindJSON(&body); err != nil {
- Fail(c, 400, err.Error())
- return
- }
- allowed := map[string]bool{"http": true, "https": true, "socks5": true}
- if !allowed[body.Protocol] {
- Fail(c, 400, "协议必须是 http/https/socks5")
- return
- }
- p := model.Proxy{
- Name: body.Name,
- Protocol: body.Protocol,
- Host: body.Host,
- Port: body.Port,
- Username: body.Username,
- Password: body.Password,
- Region: body.Region,
- Remark: body.Remark,
- Enabled: true,
- Status: "unknown",
- }
- if err := h.store.DB.Create(&p).Error; err != nil {
- Fail(c, 500, err.Error())
- return
- }
- LogAudit(h.store, c, "create", "proxy", fmt.Sprintf("%d", p.ID), gin.H{"name": p.Name})
- OK(c, p)
- }
- // Update handles PUT /proxies/:id
- func (h *ProxyHandler) Update(c *gin.Context) {
- id, err := strconv.ParseUint(c.Param("id"), 10, 64)
- if err != nil {
- Fail(c, 400, "invalid id")
- return
- }
- var p model.Proxy
- if err := h.store.DB.First(&p, id).Error; err != nil {
- Fail(c, 404, "代理不存在")
- return
- }
- var body struct {
- Name *string `json:"name"`
- Protocol *string `json:"protocol"`
- Host *string `json:"host"`
- Port *int `json:"port"`
- Username *string `json:"username"`
- Password *string `json:"password"`
- Region *string `json:"region"`
- Remark *string `json:"remark"`
- Enabled *bool `json:"enabled"`
- }
- if err := c.ShouldBindJSON(&body); err != nil {
- Fail(c, 400, err.Error())
- return
- }
- updates := map[string]any{}
- if body.Name != nil {
- updates["name"] = *body.Name
- }
- if body.Protocol != nil {
- updates["protocol"] = *body.Protocol
- }
- if body.Host != nil {
- updates["host"] = *body.Host
- }
- if body.Port != nil {
- updates["port"] = *body.Port
- }
- if body.Username != nil {
- updates["username"] = *body.Username
- }
- if body.Password != nil {
- updates["password"] = *body.Password
- }
- if body.Region != nil {
- updates["region"] = *body.Region
- }
- if body.Remark != nil {
- updates["remark"] = *body.Remark
- }
- if body.Enabled != nil {
- updates["enabled"] = *body.Enabled
- }
- h.store.DB.Model(&p).Updates(updates)
- h.store.DB.First(&p, id)
- LogAudit(h.store, c, "update", "proxy", fmt.Sprintf("%d", id), updates)
- OK(c, p)
- }
- // Delete handles DELETE /proxies/:id
- func (h *ProxyHandler) Delete(c *gin.Context) {
- id, err := strconv.ParseUint(c.Param("id"), 10, 64)
- if err != nil {
- Fail(c, 400, "invalid id")
- return
- }
- if err := h.store.DB.Delete(&model.Proxy{}, id).Error; err != nil {
- Fail(c, 500, err.Error())
- return
- }
- LogAudit(h.store, c, "delete", "proxy", fmt.Sprintf("%d", id), nil)
- OK(c, gin.H{"message": "已删除"})
- }
- // Test handles POST /proxies/:id/test — tests proxy connectivity
- func (h *ProxyHandler) Test(c *gin.Context) {
- id, err := strconv.ParseUint(c.Param("id"), 10, 64)
- if err != nil {
- Fail(c, 400, "invalid id")
- return
- }
- var p model.Proxy
- if err := h.store.DB.First(&p, id).Error; err != nil {
- Fail(c, 404, "代理不存在")
- return
- }
- proxyURL := p.ProxyURL()
- status := "ok"
- errMsg := ""
- if p.Protocol == "socks5" {
- // Test SOCKS5 by dialing through it
- auth := &proxy.Auth{}
- if p.Username != "" {
- auth.User = p.Username
- auth.Password = p.Password
- } else {
- auth = nil
- }
- dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", p.Host, p.Port), auth, proxy.Direct)
- if err != nil {
- status = "fail"
- errMsg = err.Error()
- } else {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, err := dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", "www.google.com:80")
- if err != nil {
- status = "fail"
- errMsg = err.Error()
- } else {
- conn.Close()
- }
- }
- } else {
- // Test HTTP/HTTPS proxy
- pURL, _ := url.Parse(proxyURL)
- client := &http.Client{
- Timeout: 10 * time.Second,
- Transport: &http.Transport{
- Proxy: http.ProxyURL(pURL),
- DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
- },
- }
- resp, err := client.Get("https://httpbin.org/ip")
- if err != nil {
- status = "fail"
- errMsg = err.Error()
- } else {
- resp.Body.Close()
- if resp.StatusCode != 200 {
- status = "fail"
- errMsg = fmt.Sprintf("HTTP %d", resp.StatusCode)
- }
- }
- }
- now := time.Now()
- h.store.DB.Model(&p).Updates(map[string]any{
- "status": status,
- "last_checked_at": now,
- })
- result := gin.H{"status": status, "proxy_url": proxyURL}
- if errMsg != "" {
- result["error"] = errMsg
- }
- OK(c, result)
- }
- // TestAll handles POST /proxies/test-all — tests all enabled proxies in parallel.
- func (h *ProxyHandler) TestAll(c *gin.Context) {
- var proxies []model.Proxy
- h.store.DB.Where("enabled = ?", true).Find(&proxies)
- if len(proxies) == 0 {
- Fail(c, 404, "没有已启用的代理")
- return
- }
- type testResult struct {
- ID uint `json:"id"`
- Name string `json:"name"`
- Status string `json:"status"`
- Error string `json:"error,omitempty"`
- }
- results := make([]testResult, len(proxies))
- var wg sync.WaitGroup
- for i, p := range proxies {
- wg.Add(1)
- go func(idx int, px model.Proxy) {
- defer wg.Done()
- tr := testResult{ID: px.ID, Name: px.Name}
- if px.Protocol == "socks5" {
- auth := &proxy.Auth{}
- if px.Username != "" {
- auth.User = px.Username
- auth.Password = px.Password
- } else {
- auth = nil
- }
- dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", px.Host, px.Port), auth, proxy.Direct)
- if err != nil {
- tr.Status = "fail"
- tr.Error = err.Error()
- } else {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, err := dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", "www.google.com:80")
- if err != nil {
- tr.Status = "fail"
- tr.Error = err.Error()
- } else {
- conn.Close()
- tr.Status = "ok"
- }
- }
- } else {
- pURL, _ := url.Parse(px.ProxyURL())
- client := &http.Client{
- Timeout: 10 * time.Second,
- Transport: &http.Transport{
- Proxy: http.ProxyURL(pURL),
- DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
- },
- }
- resp, err := client.Get("https://httpbin.org/ip")
- if err != nil {
- tr.Status = "fail"
- tr.Error = err.Error()
- } else {
- resp.Body.Close()
- if resp.StatusCode != 200 {
- tr.Status = "fail"
- tr.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
- } else {
- tr.Status = "ok"
- }
- }
- }
- // Update DB
- now := time.Now()
- h.store.DB.Model(&model.Proxy{}).Where("id = ?", px.ID).Updates(map[string]any{
- "status": tr.Status,
- "last_checked_at": now,
- })
- results[idx] = tr
- }(i, p)
- }
- wg.Wait()
- okCount := 0
- failCount := 0
- for _, r := range results {
- if r.Status == "ok" {
- okCount++
- } else {
- failCount++
- }
- }
- OK(c, gin.H{
- "total": len(results),
- "ok": okCount,
- "fail": failCount,
- "results": results,
- })
- }
- // PoolStatus handles GET /proxies/pool-status — returns live proxy pool health.
- func (h *ProxyHandler) PoolStatus(c *gin.Context) {
- if h.taskMgr == nil {
- OK(c, gin.H{"active": false, "message": "任务管理器未初始化"})
- return
- }
- pool := h.taskMgr.GetProxyPool()
- if pool == nil {
- OK(c, gin.H{"active": false, "message": "当前没有使用代理池"})
- return
- }
- entries := pool.AllEntries()
- OK(c, gin.H{
- "active": true,
- "total": pool.Size(),
- "active_count": pool.ActiveCount(),
- "proxies": entries,
- })
- }
|