gameShieldCrawler.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/rand"
  6. "crypto/tls"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "github.com/PuerkitoBio/goquery"
  11. "github.com/spf13/viper"
  12. "github.com/tidwall/gjson"
  13. "io"
  14. "mime/multipart"
  15. "net/http"
  16. "net/url"
  17. "strings"
  18. )
  19. type CrawlerService interface {
  20. GetLoginCookie(ctx context.Context) (string, error)
  21. GetFormTokens(ctx context.Context, loginUrl string, cookieHeader string) (map[string]string, error)
  22. SendFormData(ctx context.Context, url string, cookie string, formData map[string]interface{}) ([]byte, error)
  23. GetField(ctx context.Context, appName string) (map[string]interface{}, error)
  24. GetKey(ctx context.Context, appName string) (string, error)
  25. DeleteRule(ctx context.Context, ruleID int, ruleUrl string) (string, error)
  26. }
  27. type CrawlerConfig struct {
  28. Username string
  29. Password string
  30. URL string
  31. KeyURL string
  32. }
  33. func NewCrawlerService(
  34. service *Service,
  35. parser ParserService,
  36. conf *viper.Viper,
  37. ) CrawlerService {
  38. return &crawlerService{
  39. Service: service,
  40. parser: parser,
  41. config: &CrawlerConfig{
  42. Username: conf.GetString("crawler.username"),
  43. Password: conf.GetString("crawler.password"),
  44. URL: conf.GetString("crawler.Url"),
  45. KeyURL: conf.GetString("crawler.keyUrl"),
  46. },
  47. }
  48. }
  49. type crawlerService struct {
  50. *Service
  51. parser ParserService
  52. config *CrawlerConfig
  53. }
  54. // 生成随机字符串
  55. func randomHex(n int) string {
  56. b := make([]byte, n)
  57. _, err := rand.Read(b)
  58. if err != nil {
  59. panic(err)
  60. }
  61. return hex.EncodeToString(b)
  62. }
  63. // 获取登录cookie
  64. func (service *crawlerService) GetLoginCookie(ctx context.Context) (string, error) {
  65. data := url.Values{}
  66. data.Set("username", service.config.Username)
  67. data.Set("password", service.config.Password)
  68. loginUrl := service.config.URL + "admin/signin"
  69. req, err := http.NewRequestWithContext(ctx, "POST", loginUrl, strings.NewReader(data.Encode()))
  70. if err != nil {
  71. return "", fmt.Errorf("操作失败: %v", err)
  72. }
  73. // 添加关键请求头
  74. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  75. req.Header.Set("Expect", "")
  76. req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36")
  77. req.Header.Set("Referer", loginUrl)
  78. req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
  79. client := &http.Client{
  80. CheckRedirect: func(req *http.Request, via []*http.Request) error {
  81. return http.ErrUseLastResponse // 禁止自动跳转
  82. },
  83. Transport: &http.Transport{
  84. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  85. },
  86. }
  87. resp, err := client.Do(req)
  88. if err != nil {
  89. return "", fmt.Errorf("%v", err)
  90. }
  91. defer resp.Body.Close()
  92. // 输出响应体,调试用
  93. _, err = io.ReadAll(resp.Body)
  94. if err != nil {
  95. return "", fmt.Errorf("读取响应失败: %v", err)
  96. }
  97. // 提取原始 Header 中的 Set-Cookie 字段
  98. rawCookies := resp.Header["Set-Cookie"]
  99. var cookieStr strings.Builder
  100. for _, cookie := range rawCookies {
  101. parts := strings.SplitN(cookie, ";", 2)
  102. if len(parts) > 0 {
  103. cookieStr.WriteString(parts[0] + "; ")
  104. }
  105. }
  106. cookieHeader := strings.TrimRight(cookieStr.String(), "; ")
  107. if cookieHeader == "" {
  108. return "", fmt.Errorf("获取 Cookie 失败")
  109. }
  110. return cookieHeader, nil
  111. }
  112. // 获取表单令牌
  113. func (service *crawlerService) GetFormTokens(ctx context.Context, loginUrl string, cookieHeader string) (map[string]string, error) {
  114. req, err := http.NewRequestWithContext(ctx, "GET", loginUrl, nil)
  115. if err != nil {
  116. return nil, fmt.Errorf("创建请求失败: %v", err)
  117. }
  118. // 设置请求头,包括 Cookie 和 PJAX 头
  119. req.Header.Set("Cookie", cookieHeader)
  120. req.Header.Set("X-PJAX", "true")
  121. req.Header.Set("X-PJAX-Container", "#pjax-container")
  122. req.Header.Set("X-Requested-With", "XMLHttpRequest")
  123. req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36")
  124. // HTTP 客户端,跳过 SSL 验证
  125. client := &http.Client{
  126. Transport: &http.Transport{
  127. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  128. },
  129. }
  130. // 发送请求
  131. resp, err := client.Do(req)
  132. if err != nil {
  133. return nil, fmt.Errorf("请求失败: %v", err)
  134. }
  135. defer resp.Body.Close()
  136. // 使用 goquery 解析 HTML
  137. doc, err := goquery.NewDocumentFromReader(resp.Body)
  138. if err != nil {
  139. return nil, fmt.Errorf("解析 HTML 失败: %v", err)
  140. }
  141. // 提取隐藏字段
  142. previous := doc.Find(`input[name="__go_admin_previous_"]`).AttrOr("value", "默认值")
  143. t := doc.Find(`input[name="__go_admin_t_"]`).AttrOr("value", "默认值")
  144. return map[string]string{
  145. "previous": previous,
  146. "t": t,
  147. }, nil
  148. }
  149. // 发送 POST 请求
  150. func (service *crawlerService) SendFormData(ctx context.Context, url string, cookie string, formData map[string]interface{}) ([]byte, error) {
  151. var buf bytes.Buffer
  152. writer := multipart.NewWriter(&buf)
  153. // 遍历字段添加到 multipart 表单中
  154. for key, val := range formData {
  155. valueStr := fmt.Sprintf("%v", val) // 转为字符串
  156. if err := writer.WriteField(key, valueStr); err != nil {
  157. return nil, fmt.Errorf("写入字段失败: %v", err)
  158. }
  159. }
  160. // 关闭 writer 以完成结尾边界写入
  161. if err := writer.Close(); err != nil {
  162. return nil, fmt.Errorf("关闭 multipart writer 失败: %v", err)
  163. }
  164. // 构造请求
  165. req, err := http.NewRequestWithContext(ctx, "POST", url, &buf)
  166. if err != nil {
  167. return nil, fmt.Errorf("创建请求失败: %v", err)
  168. }
  169. // 设置请求头
  170. req.Header.Set("Content-Type", writer.FormDataContentType())
  171. req.Header.Set("X-PJAX", "true")
  172. req.Header.Set("X-PJAX-Container", "#pjax-container")
  173. req.Header.Set("X-Requested-With", "XMLHttpRequest")
  174. req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)")
  175. req.Header.Set("Accept", "text/html, */*; q=0.01")
  176. req.Header.Set("Accept-Encoding", "gzip, deflate")
  177. req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7")
  178. req.Header.Set("Cookie", cookie)
  179. req.Header.Set("Expect", "")
  180. // 跳过 SSL 验证的 HTTP 客户端
  181. client := &http.Client{
  182. Transport: &http.Transport{
  183. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  184. },
  185. }
  186. // 发起请求
  187. resp, err := client.Do(req)
  188. if err != nil {
  189. return nil, fmt.Errorf("请求发送失败: %v", err)
  190. }
  191. defer resp.Body.Close()
  192. // 读取响应
  193. res, err := io.ReadAll(resp.Body)
  194. if err != nil {
  195. return nil, fmt.Errorf("读取响应失败: %v", err)
  196. }
  197. return res, nil
  198. }
  199. // 获取 rule_id
  200. func (service *crawlerService) GetField(ctx context.Context, appName string) (map[string]interface{}, error) {
  201. keyURL := service.config.KeyURL + appName
  202. resp, err := http.Get(keyURL)
  203. if err != nil {
  204. return nil, fmt.Errorf("请求失败:%w", err)
  205. }
  206. defer resp.Body.Close()
  207. body, err := io.ReadAll(resp.Body)
  208. if err != nil {
  209. return nil, fmt.Errorf("读取响应体失败:%w", err)
  210. }
  211. // 先用 gjson 拿到 data.raw 对应的原始 JSON
  212. result := gjson.GetBytes(body, "data.raw")
  213. if !result.Exists() {
  214. return nil, fmt.Errorf("响应中缺少 data.raw")
  215. }
  216. // 再把这一段反序列化到 map
  217. var rawMap map[string]interface{}
  218. if err := json.Unmarshal([]byte(result.Raw), &rawMap); err != nil {
  219. return nil, fmt.Errorf("解析 data.raw 失败:%w", err)
  220. }
  221. return rawMap, nil
  222. }
  223. func (service *crawlerService) GetKey(ctx context.Context, appName string) (string, error) {
  224. resp, err := http.Get(service.config.KeyURL + appName)
  225. if err != nil {
  226. return "", fmt.Errorf("请求失败:%w", err)
  227. }
  228. defer resp.Body.Close()
  229. body, err := io.ReadAll(resp.Body)
  230. if err != nil {
  231. return "", fmt.Errorf("读取响应体失败:%w", err)
  232. }
  233. // 2. 直接从 JSON 路径 data.key 拿字符串
  234. result := gjson.GetBytes(body, "data.key")
  235. if !result.Exists() {
  236. return "", fmt.Errorf("响应中缺少 data.key")
  237. }
  238. return result.String(), nil
  239. }
  240. func (service *crawlerService) DeleteRule(ctx context.Context, ruleID int, ruleUrl string) (string, error) {
  241. // 1. 登录,拿到 Cookie
  242. cookie, err := service.GetLoginCookie(ctx)
  243. if err != nil {
  244. return "", fmt.Errorf("login failed: %w", err)
  245. }
  246. // 2. 构造删除请求 URL 和表单
  247. deleteURL := service.config.URL + ruleUrl
  248. formData := map[string]interface{}{
  249. "id": ruleID,
  250. }
  251. // 3. 发表单(multipart 也支持 x-www-form-urlencoded,你这里用已有的 SendFormData)
  252. respBody, err := service.SendFormData(ctx, deleteURL, cookie, formData)
  253. if err != nil {
  254. return "", err
  255. }
  256. res, err := service.parser.GetMessage(ctx, respBody)
  257. if err != nil {
  258. return "", err
  259. }
  260. return res, nil
  261. }