aodun.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. package service
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/tls"
  6. "encoding/json"
  7. "fmt"
  8. v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
  9. "github.com/spf13/viper"
  10. "go.uber.org/zap"
  11. "io"
  12. "net/http"
  13. "net/url"
  14. "strconv"
  15. "strings"
  16. "time"
  17. )
  18. // AoDunService 定义了与傲盾 API 交互的服务接口
  19. type AoDunService interface {
  20. DomainWhiteList(ctx context.Context, domain string, ip string, apiType string) error
  21. AddWhiteStaticList(ctx context.Context, isSmall bool, req []v1.IpInfo) error
  22. DelWhiteStaticList(ctx context.Context, isSmall bool, id string) error
  23. GetWhiteStaticList(ctx context.Context, isSmall bool, ip string) (int, error)
  24. }
  25. // aoDunService 是 AoDunService 接口的实现
  26. type aoDunService struct {
  27. *Service
  28. cfg *aoDunConfig
  29. httpClient *http.Client
  30. }
  31. // aoDunConfig 用于整合来自 viper 的所有配置
  32. type aoDunConfig struct {
  33. Url string
  34. ClientID string
  35. Username string
  36. Password string
  37. SmallUrl string
  38. SmallClientID string
  39. DomainUsername string
  40. DomainPassword string
  41. }
  42. // NewAoDunService 创建一个新的 AoDunService 实例
  43. func NewAoDunService(service *Service, conf *viper.Viper) AoDunService {
  44. cfg := &aoDunConfig{
  45. Url: conf.GetString("aodun.Url"),
  46. ClientID: conf.GetString("aodun.clientID"),
  47. Username: conf.GetString("aodun.username"),
  48. Password: conf.GetString("aodun.password"),
  49. SmallUrl: conf.GetString("aodunSmall.Url"),
  50. SmallClientID: conf.GetString("aodunSmall.clientID"),
  51. DomainUsername: conf.GetString("domainWhite.username"),
  52. DomainPassword: conf.GetString("domainWhite.password"),
  53. }
  54. tr := &http.Transport{
  55. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  56. MaxIdleConns: 100,
  57. IdleConnTimeout: 90 * time.Second,
  58. ForceAttemptHTTP2: true,
  59. }
  60. client := &http.Client{
  61. Transport: tr,
  62. Timeout: 15 * time.Second,
  63. }
  64. return &aoDunService{
  65. Service: service,
  66. cfg: cfg,
  67. httpClient: client,
  68. }
  69. }
  70. // getApiUrl 根据 isSmall 标志返回正确的 API 基础 URL
  71. func (s *aoDunService) getApiUrl(isSmall bool) string {
  72. if isSmall {
  73. return s.cfg.SmallUrl
  74. }
  75. return s.cfg.Url
  76. }
  77. // getClientID 根据 isSmall 标志返回正确的 ClientID
  78. func (s *aoDunService) getClientID(isSmall bool) string {
  79. if isSmall {
  80. return s.cfg.SmallClientID
  81. }
  82. return s.cfg.ClientID
  83. }
  84. // executeRequest 封装了发送 HTTP POST 请求、读取响应和 JSON 解码的通用逻辑
  85. func (s *aoDunService) executeRequest(ctx context.Context, url, tokenType, token string, requestBody, responsePayload interface{}, isSmall bool) error {
  86. jsonData, err := json.Marshal(requestBody)
  87. if err != nil {
  88. return fmt.Errorf("序列化请求数据失败 (isSmall: %t): %w", isSmall, err)
  89. }
  90. req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
  91. if err != nil {
  92. return fmt.Errorf("创建 HTTP 请求失败 (isSmall: %t): %w", isSmall, err)
  93. }
  94. req.Header.Set("Content-Type", "application/json")
  95. if token != "" {
  96. req.Header.Set("Authorization", tokenType+" "+token)
  97. }
  98. resp, err := s.httpClient.Do(req)
  99. if err != nil {
  100. return fmt.Errorf("发送 HTTP 请求失败 (isSmall: %t): %w", isSmall, err)
  101. }
  102. defer resp.Body.Close()
  103. body, err := io.ReadAll(resp.Body)
  104. if err != nil {
  105. return fmt.Errorf("读取响应体失败 (isSmall: %t): %w", isSmall, err)
  106. }
  107. if resp.StatusCode != http.StatusOK {
  108. return fmt.Errorf("HTTP 错误 (isSmall: %t): 状态码 %d, 响应: %s", isSmall, resp.StatusCode, string(body))
  109. }
  110. if err := json.Unmarshal(body, responsePayload); err != nil {
  111. return fmt.Errorf("反序列化响应 JSON 失败 (isSmall: %t, 内容: %s): %w", isSmall, string(body), err)
  112. }
  113. return nil
  114. }
  115. // sendAuthenticatedRequest 封装了需要认证的 API 请求的通用流程
  116. func (s *aoDunService) sendAuthenticatedRequest(ctx context.Context, isSmall bool, apiPath string, requestBody, responsePayload interface{}) error {
  117. tokenType, token, err := s.GetToken(ctx, isSmall)
  118. if err != nil {
  119. return err
  120. }
  121. apiURL := s.getApiUrl(isSmall) + apiPath
  122. return s.executeRequest(ctx, apiURL, tokenType, token, requestBody, responsePayload, isSmall)
  123. }
  124. // GetToken 获取认证令牌
  125. func (s *aoDunService) GetToken(ctx context.Context, isSmall bool) (string, string, error) {
  126. formData := map[string]interface{}{
  127. "ClientID": s.getClientID(isSmall),
  128. "GrantType": "password",
  129. "Username": s.cfg.Username,
  130. "Password": s.cfg.Password,
  131. }
  132. apiURL := s.getApiUrl(isSmall) + "/oauth/token"
  133. var res v1.GetTokenRespone
  134. if err := s.executeRequest(ctx, apiURL, "", "", formData, &res, isSmall); err != nil {
  135. return "", "", err
  136. }
  137. if res.Code != 0 {
  138. return "", "", fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
  139. }
  140. if res.AccessToken == "" {
  141. return "", "", fmt.Errorf("API 成功 (isSmall: %t, code 0) 但 access_token 为空", isSmall)
  142. }
  143. return res.TokenType, res.AccessToken, nil
  144. }
  145. // AddWhiteStaticList 添加 IP 到静态白名单
  146. func (s *aoDunService) AddWhiteStaticList(ctx context.Context, isSmall bool, req []v1.IpInfo) error {
  147. formData := map[string]interface{}{
  148. "action": "add",
  149. "bwflag": "white",
  150. "insert_bw_list": req,
  151. }
  152. var res v1.IpResponse
  153. err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
  154. if err != nil {
  155. return err
  156. }
  157. if res.Code != 0 {
  158. if strings.Contains(res.Msg, "操作部分成功,重复IP如下") {
  159. s.logger.Info(res.Msg, zap.String("isSmall", strconv.FormatBool(isSmall)))
  160. return nil
  161. }
  162. return fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
  163. }
  164. return nil
  165. }
  166. // GetWhiteStaticList 查询白名单 IP 并返回其 ID
  167. func (s *aoDunService) GetWhiteStaticList(ctx context.Context, isSmall bool, ip string) (int, error) {
  168. formData := map[string]interface{}{
  169. "action": "get",
  170. "bwflag": "white",
  171. "page": 1,
  172. "ids": ip,
  173. }
  174. var res v1.IpGetResponse
  175. err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
  176. if err != nil {
  177. return 0, err
  178. }
  179. if res.Code != 0 {
  180. return 0, fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
  181. }
  182. if len(res.Data) == 0 {
  183. return 0, fmt.Errorf("未找到 IP '%s' 相关的白名单记录 (isSmall: %t)", ip, isSmall)
  184. }
  185. return res.Data[0].ID, nil
  186. }
  187. // DelWhiteStaticList 根据 ID 从白名单中删除 IP
  188. func (s *aoDunService) DelWhiteStaticList(ctx context.Context, isSmall bool, id string) error {
  189. formData := map[string]interface{}{
  190. "action": "del",
  191. "bwflag": "white",
  192. "flag": 0,
  193. "ids": id,
  194. }
  195. var res v1.IpResponse
  196. err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
  197. if err != nil {
  198. return err
  199. }
  200. if res.Code != 0 {
  201. return fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
  202. }
  203. return nil
  204. }
  205. // sendDomainFormData 处理域名白名单的 application/x-www-form-urlencoded 请求
  206. func (s *aoDunService) sendDomainFormData(ctx context.Context, domain, ip, apiType string) ([]byte, error) {
  207. var apiURL string
  208. switch apiType {
  209. case "add":
  210. apiURL = "http://zapi.zzybgp.com/api/user/do_main"
  211. case "del":
  212. apiURL = "http://zapi.zzybgp.com/api/user/do_main/delete"
  213. default:
  214. return nil, fmt.Errorf("无效的 apiType: %s", apiType)
  215. }
  216. formData := url.Values{}
  217. formData.Set("username", s.cfg.DomainUsername)
  218. formData.Set("password", s.cfg.DomainPassword)
  219. formData.Add("do_main_list[name][]", domain)
  220. formData.Add("do_main_list[ip]", ip)
  221. req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(formData.Encode()))
  222. if err != nil {
  223. return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
  224. }
  225. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  226. resp, err := s.httpClient.Do(req)
  227. if err != nil {
  228. return nil, fmt.Errorf("发送 HTTP 请求失败: %w", err)
  229. }
  230. defer resp.Body.Close()
  231. body, err := io.ReadAll(resp.Body)
  232. if err != nil {
  233. return nil, fmt.Errorf("读取响应体失败: %w", err)
  234. }
  235. if resp.StatusCode != http.StatusOK {
  236. return nil, fmt.Errorf("HTTP 错误: 状态码 %d, 响应: %s", resp.StatusCode, string(body))
  237. }
  238. return body, nil
  239. }
  240. // DomainWhiteList 添加或删除域名白名单
  241. func (s *aoDunService) DomainWhiteList(ctx context.Context, domain, ip, apiType string) error {
  242. resBody, err := s.sendDomainFormData(ctx, domain, ip, apiType)
  243. if err != nil {
  244. return err
  245. }
  246. var res v1.DomainResponse
  247. if err := json.Unmarshal(resBody, &res); err != nil {
  248. return fmt.Errorf("反序列化响应 JSON 失败 (内容: %s): %w", string(resBody), err)
  249. }
  250. switch apiType {
  251. case "add":
  252. if res.Code != 200 {
  253. return fmt.Errorf("API 错误: code %d, msg '%s', info '%s'", res.Code, res.Msg, res.Info)
  254. }
  255. case "del":
  256. if res.Code != 600 {
  257. return fmt.Errorf("API 错误: code %d, msg '%s', info '%s'", res.Code, res.Msg, res.Info)
  258. }
  259. }
  260. return nil
  261. }