proxy.go 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. package handler
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "net/http"
  7. "net/url"
  8. "strconv"
  9. "sync"
  10. "time"
  11. "spider/internal/model"
  12. "spider/internal/store"
  13. "spider/internal/task"
  14. "github.com/gin-gonic/gin"
  15. "golang.org/x/net/proxy"
  16. )
  17. // ProxyHandler handles proxy CRUD and testing.
  18. type ProxyHandler struct {
  19. store *store.Store
  20. taskMgr *task.Manager
  21. }
  22. // List handles GET /proxies
  23. func (h *ProxyHandler) List(c *gin.Context) {
  24. page, pageSize, offset := parsePage(c)
  25. query := h.store.DB.Model(&model.Proxy{})
  26. if status := c.Query("status"); status != "" {
  27. query = query.Where("status = ?", status)
  28. }
  29. if enabled := c.Query("enabled"); enabled != "" {
  30. query = query.Where("enabled = ?", enabled == "true")
  31. }
  32. if search := c.Query("search"); search != "" {
  33. like := "%" + search + "%"
  34. query = query.Where("name LIKE ? OR host LIKE ? OR region LIKE ?", like, like, like)
  35. }
  36. var total int64
  37. query.Count(&total)
  38. var proxies []model.Proxy
  39. if err := query.Order("id DESC").Limit(pageSize).Offset(offset).Find(&proxies).Error; err != nil {
  40. Fail(c, 500, err.Error())
  41. return
  42. }
  43. PageOK(c, proxies, total, page, pageSize)
  44. }
  45. // ListEnabled handles GET /proxies/enabled — returns only enabled proxies (for task dropdown)
  46. func (h *ProxyHandler) ListEnabled(c *gin.Context) {
  47. var proxies []model.Proxy
  48. h.store.DB.Where("enabled = ?", true).Order("name ASC").Find(&proxies)
  49. OK(c, proxies)
  50. }
  51. // Create handles POST /proxies
  52. func (h *ProxyHandler) Create(c *gin.Context) {
  53. var body struct {
  54. Name string `json:"name" binding:"required"`
  55. Protocol string `json:"protocol" binding:"required"`
  56. Host string `json:"host" binding:"required"`
  57. Port int `json:"port" binding:"required"`
  58. Username string `json:"username"`
  59. Password string `json:"password"`
  60. Region string `json:"region"`
  61. Remark string `json:"remark"`
  62. }
  63. if err := c.ShouldBindJSON(&body); err != nil {
  64. Fail(c, 400, err.Error())
  65. return
  66. }
  67. allowed := map[string]bool{"http": true, "https": true, "socks5": true}
  68. if !allowed[body.Protocol] {
  69. Fail(c, 400, "协议必须是 http/https/socks5")
  70. return
  71. }
  72. p := model.Proxy{
  73. Name: body.Name,
  74. Protocol: body.Protocol,
  75. Host: body.Host,
  76. Port: body.Port,
  77. Username: body.Username,
  78. Password: body.Password,
  79. Region: body.Region,
  80. Remark: body.Remark,
  81. Enabled: true,
  82. Status: "unknown",
  83. }
  84. if err := h.store.DB.Create(&p).Error; err != nil {
  85. Fail(c, 500, err.Error())
  86. return
  87. }
  88. LogAudit(h.store, c, "create", "proxy", fmt.Sprintf("%d", p.ID), gin.H{"name": p.Name})
  89. OK(c, p)
  90. }
  91. // Update handles PUT /proxies/:id
  92. func (h *ProxyHandler) Update(c *gin.Context) {
  93. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  94. if err != nil {
  95. Fail(c, 400, "invalid id")
  96. return
  97. }
  98. var p model.Proxy
  99. if err := h.store.DB.First(&p, id).Error; err != nil {
  100. Fail(c, 404, "代理不存在")
  101. return
  102. }
  103. var body struct {
  104. Name *string `json:"name"`
  105. Protocol *string `json:"protocol"`
  106. Host *string `json:"host"`
  107. Port *int `json:"port"`
  108. Username *string `json:"username"`
  109. Password *string `json:"password"`
  110. Region *string `json:"region"`
  111. Remark *string `json:"remark"`
  112. Enabled *bool `json:"enabled"`
  113. }
  114. if err := c.ShouldBindJSON(&body); err != nil {
  115. Fail(c, 400, err.Error())
  116. return
  117. }
  118. updates := map[string]any{}
  119. if body.Name != nil {
  120. updates["name"] = *body.Name
  121. }
  122. if body.Protocol != nil {
  123. updates["protocol"] = *body.Protocol
  124. }
  125. if body.Host != nil {
  126. updates["host"] = *body.Host
  127. }
  128. if body.Port != nil {
  129. updates["port"] = *body.Port
  130. }
  131. if body.Username != nil {
  132. updates["username"] = *body.Username
  133. }
  134. if body.Password != nil {
  135. updates["password"] = *body.Password
  136. }
  137. if body.Region != nil {
  138. updates["region"] = *body.Region
  139. }
  140. if body.Remark != nil {
  141. updates["remark"] = *body.Remark
  142. }
  143. if body.Enabled != nil {
  144. updates["enabled"] = *body.Enabled
  145. }
  146. h.store.DB.Model(&p).Updates(updates)
  147. h.store.DB.First(&p, id)
  148. LogAudit(h.store, c, "update", "proxy", fmt.Sprintf("%d", id), updates)
  149. OK(c, p)
  150. }
  151. // Delete handles DELETE /proxies/:id
  152. func (h *ProxyHandler) Delete(c *gin.Context) {
  153. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  154. if err != nil {
  155. Fail(c, 400, "invalid id")
  156. return
  157. }
  158. if err := h.store.DB.Delete(&model.Proxy{}, id).Error; err != nil {
  159. Fail(c, 500, err.Error())
  160. return
  161. }
  162. LogAudit(h.store, c, "delete", "proxy", fmt.Sprintf("%d", id), nil)
  163. OK(c, gin.H{"message": "已删除"})
  164. }
  165. // Test handles POST /proxies/:id/test — tests proxy connectivity
  166. func (h *ProxyHandler) Test(c *gin.Context) {
  167. id, err := strconv.ParseUint(c.Param("id"), 10, 64)
  168. if err != nil {
  169. Fail(c, 400, "invalid id")
  170. return
  171. }
  172. var p model.Proxy
  173. if err := h.store.DB.First(&p, id).Error; err != nil {
  174. Fail(c, 404, "代理不存在")
  175. return
  176. }
  177. proxyURL := p.ProxyURL()
  178. status := "ok"
  179. errMsg := ""
  180. if p.Protocol == "socks5" {
  181. // Test SOCKS5 by dialing through it
  182. auth := &proxy.Auth{}
  183. if p.Username != "" {
  184. auth.User = p.Username
  185. auth.Password = p.Password
  186. } else {
  187. auth = nil
  188. }
  189. dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", p.Host, p.Port), auth, proxy.Direct)
  190. if err != nil {
  191. status = "fail"
  192. errMsg = err.Error()
  193. } else {
  194. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  195. defer cancel()
  196. conn, err := dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", "www.google.com:80")
  197. if err != nil {
  198. status = "fail"
  199. errMsg = err.Error()
  200. } else {
  201. conn.Close()
  202. }
  203. }
  204. } else {
  205. // Test HTTP/HTTPS proxy
  206. pURL, _ := url.Parse(proxyURL)
  207. client := &http.Client{
  208. Timeout: 10 * time.Second,
  209. Transport: &http.Transport{
  210. Proxy: http.ProxyURL(pURL),
  211. DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
  212. },
  213. }
  214. resp, err := client.Get("https://httpbin.org/ip")
  215. if err != nil {
  216. status = "fail"
  217. errMsg = err.Error()
  218. } else {
  219. resp.Body.Close()
  220. if resp.StatusCode != 200 {
  221. status = "fail"
  222. errMsg = fmt.Sprintf("HTTP %d", resp.StatusCode)
  223. }
  224. }
  225. }
  226. now := time.Now()
  227. h.store.DB.Model(&p).Updates(map[string]any{
  228. "status": status,
  229. "last_checked_at": now,
  230. })
  231. result := gin.H{"status": status, "proxy_url": proxyURL}
  232. if errMsg != "" {
  233. result["error"] = errMsg
  234. }
  235. OK(c, result)
  236. }
  237. // TestAll handles POST /proxies/test-all — tests all enabled proxies in parallel.
  238. func (h *ProxyHandler) TestAll(c *gin.Context) {
  239. var proxies []model.Proxy
  240. h.store.DB.Where("enabled = ?", true).Find(&proxies)
  241. if len(proxies) == 0 {
  242. Fail(c, 404, "没有已启用的代理")
  243. return
  244. }
  245. type testResult struct {
  246. ID uint `json:"id"`
  247. Name string `json:"name"`
  248. Status string `json:"status"`
  249. Error string `json:"error,omitempty"`
  250. }
  251. results := make([]testResult, len(proxies))
  252. var wg sync.WaitGroup
  253. for i, p := range proxies {
  254. wg.Add(1)
  255. go func(idx int, px model.Proxy) {
  256. defer wg.Done()
  257. tr := testResult{ID: px.ID, Name: px.Name}
  258. if px.Protocol == "socks5" {
  259. auth := &proxy.Auth{}
  260. if px.Username != "" {
  261. auth.User = px.Username
  262. auth.Password = px.Password
  263. } else {
  264. auth = nil
  265. }
  266. dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("%s:%d", px.Host, px.Port), auth, proxy.Direct)
  267. if err != nil {
  268. tr.Status = "fail"
  269. tr.Error = err.Error()
  270. } else {
  271. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  272. defer cancel()
  273. conn, err := dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", "www.google.com:80")
  274. if err != nil {
  275. tr.Status = "fail"
  276. tr.Error = err.Error()
  277. } else {
  278. conn.Close()
  279. tr.Status = "ok"
  280. }
  281. }
  282. } else {
  283. pURL, _ := url.Parse(px.ProxyURL())
  284. client := &http.Client{
  285. Timeout: 10 * time.Second,
  286. Transport: &http.Transport{
  287. Proxy: http.ProxyURL(pURL),
  288. DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
  289. },
  290. }
  291. resp, err := client.Get("https://httpbin.org/ip")
  292. if err != nil {
  293. tr.Status = "fail"
  294. tr.Error = err.Error()
  295. } else {
  296. resp.Body.Close()
  297. if resp.StatusCode != 200 {
  298. tr.Status = "fail"
  299. tr.Error = fmt.Sprintf("HTTP %d", resp.StatusCode)
  300. } else {
  301. tr.Status = "ok"
  302. }
  303. }
  304. }
  305. // Update DB
  306. now := time.Now()
  307. h.store.DB.Model(&model.Proxy{}).Where("id = ?", px.ID).Updates(map[string]any{
  308. "status": tr.Status,
  309. "last_checked_at": now,
  310. })
  311. results[idx] = tr
  312. }(i, p)
  313. }
  314. wg.Wait()
  315. okCount := 0
  316. failCount := 0
  317. for _, r := range results {
  318. if r.Status == "ok" {
  319. okCount++
  320. } else {
  321. failCount++
  322. }
  323. }
  324. OK(c, gin.H{
  325. "total": len(results),
  326. "ok": okCount,
  327. "fail": failCount,
  328. "results": results,
  329. })
  330. }
  331. // PoolStatus handles GET /proxies/pool-status — returns live proxy pool health.
  332. func (h *ProxyHandler) PoolStatus(c *gin.Context) {
  333. if h.taskMgr == nil {
  334. OK(c, gin.H{"active": false, "message": "任务管理器未初始化"})
  335. return
  336. }
  337. pool := h.taskMgr.GetProxyPool()
  338. if pool == nil {
  339. OK(c, gin.H{"active": false, "message": "当前没有使用代理池"})
  340. return
  341. }
  342. entries := pool.AllEntries()
  343. OK(c, gin.H{
  344. "active": true,
  345. "total": pool.Size(),
  346. "active_count": pool.ActiveCount(),
  347. "proxies": entries,
  348. })
  349. }