aodun.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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, color string) error
  22. DelWhiteStaticList(ctx context.Context, isSmall bool, id string, color string) error
  23. GetWhiteStaticList(ctx context.Context, isSmall bool, ip string,serverIp string, color string) (int, error)
  24. AddBandwidthLimit(ctx context.Context, req v1.Bandwidth) error
  25. DelBandwidthLimit(ctx context.Context, req v1.Bandwidth) error
  26. }
  27. // aoDunService 是 AoDunService 接口的实现
  28. type aoDunService struct {
  29. *Service
  30. cfg *aoDunConfig
  31. httpClient *http.Client
  32. }
  33. // aoDunConfig 用于整合来自 viper 的所有配置
  34. type aoDunConfig struct {
  35. Url string
  36. ClientID string
  37. Username string
  38. Password string
  39. SmallUrl string
  40. SmallClientID string
  41. DomainUsername string
  42. DomainPassword string
  43. }
  44. // NewAoDunService 创建一个新的 AoDunService 实例
  45. func NewAoDunService(service *Service, conf *viper.Viper) AoDunService {
  46. cfg := &aoDunConfig{
  47. Url: conf.GetString("aodun.Url"),
  48. ClientID: conf.GetString("aodun.clientID"),
  49. Username: conf.GetString("aodun.username"),
  50. Password: conf.GetString("aodun.password"),
  51. SmallUrl: conf.GetString("aodunSmall.Url"),
  52. SmallClientID: conf.GetString("aodunSmall.clientID"),
  53. DomainUsername: conf.GetString("domainWhite.username"),
  54. DomainPassword: conf.GetString("domainWhite.password"),
  55. }
  56. tr := &http.Transport{
  57. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  58. MaxIdleConns: 100,
  59. IdleConnTimeout: 90 * time.Second,
  60. ForceAttemptHTTP2: true,
  61. }
  62. client := &http.Client{
  63. Transport: tr,
  64. Timeout: 15 * time.Second,
  65. }
  66. return &aoDunService{
  67. Service: service,
  68. cfg: cfg,
  69. httpClient: client,
  70. }
  71. }
  72. // getApiUrl 根据 isSmall 标志返回正确的 API 基础 URL
  73. func (s *aoDunService) getApiUrl(isSmall bool) string {
  74. if isSmall {
  75. return s.cfg.SmallUrl
  76. }
  77. return s.cfg.Url
  78. }
  79. // getClientID 根据 isSmall 标志返回正确的 ClientID
  80. func (s *aoDunService) getClientID(isSmall bool) string {
  81. if isSmall {
  82. return s.cfg.SmallClientID
  83. }
  84. return s.cfg.ClientID
  85. }
  86. // executeRequest 封装了发送 HTTP POST 请求、读取响应和 JSON 解码的通用逻辑
  87. func (s *aoDunService) executeRequest(ctx context.Context, url, tokenType, token string, requestBody, responsePayload interface{}, isSmall bool) error {
  88. jsonData, err := json.Marshal(requestBody)
  89. if err != nil {
  90. return fmt.Errorf("序列化请求数据失败 (isSmall: %t): %w", isSmall, err)
  91. }
  92. req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
  93. if err != nil {
  94. return fmt.Errorf("创建 HTTP 请求失败 (isSmall: %t): %w", isSmall, err)
  95. }
  96. req.Header.Set("Content-Type", "application/json")
  97. if token != "" {
  98. req.Header.Set("Authorization", tokenType+" "+token)
  99. }
  100. resp, err := s.httpClient.Do(req)
  101. if err != nil {
  102. return fmt.Errorf("发送 HTTP 请求失败 (isSmall: %t): %w", isSmall, err)
  103. }
  104. defer resp.Body.Close()
  105. body, err := io.ReadAll(resp.Body)
  106. if err != nil {
  107. return fmt.Errorf("读取响应体失败 (isSmall: %t): %w", isSmall, err)
  108. }
  109. if resp.StatusCode != http.StatusOK {
  110. return fmt.Errorf("HTTP 错误 (isSmall: %t): 状态码 %d, 响应: %s", isSmall, resp.StatusCode, string(body))
  111. }
  112. if err := json.Unmarshal(body, responsePayload); err != nil {
  113. return fmt.Errorf("反序列化响应 JSON 失败 (isSmall: %t, 内容: %s): %w", isSmall, string(body), err)
  114. }
  115. return nil
  116. }
  117. // sendAuthenticatedRequest 封装了需要认证的 API 请求的通用流程
  118. func (s *aoDunService) sendAuthenticatedRequest(ctx context.Context, isSmall bool, apiPath string, requestBody, responsePayload interface{}) error {
  119. tokenType, token, err := s.GetToken(ctx, isSmall)
  120. if err != nil {
  121. return err
  122. }
  123. apiURL := s.getApiUrl(isSmall) + apiPath
  124. return s.executeRequest(ctx, apiURL, tokenType, token, requestBody, responsePayload, isSmall)
  125. }
  126. // GetToken 获取认证令牌
  127. func (s *aoDunService) GetToken(ctx context.Context, isSmall bool) (string, string, error) {
  128. formData := map[string]interface{}{
  129. "ClientID": s.getClientID(isSmall),
  130. "GrantType": "password",
  131. "Username": s.cfg.Username,
  132. "Password": s.cfg.Password,
  133. }
  134. apiURL := s.getApiUrl(isSmall) + "/oauth/token"
  135. var res v1.GetTokenRespone
  136. if err := s.executeRequest(ctx, apiURL, "", "", formData, &res, isSmall); err != nil {
  137. return "", "", err
  138. }
  139. if res.Code != 0 {
  140. return "", "", fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
  141. }
  142. if res.AccessToken == "" {
  143. return "", "", fmt.Errorf("API 成功 (isSmall: %t, code 0) 但 access_token 为空", isSmall)
  144. }
  145. return res.TokenType, res.AccessToken, nil
  146. }
  147. // AddWhiteStaticList 添加 IP 到静态白名单
  148. func (s *aoDunService) AddWhiteStaticList(ctx context.Context, isSmall bool, req []v1.IpInfo,color string) error {
  149. formData := map[string]interface{}{
  150. "action": "add",
  151. "bwflag": color,
  152. "insert_bw_list": req,
  153. }
  154. var res v1.IpResponse
  155. err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
  156. if err != nil {
  157. return err
  158. }
  159. if res.Code != 0 {
  160. if strings.Contains(res.Msg, "操作部分成功,重复IP如下") {
  161. s.Logger.Info(res.Msg, zap.String("isSmall", strconv.FormatBool(isSmall)))
  162. return nil
  163. }
  164. return fmt.Errorf("API 错误 (isSmall: %t): color %s,code %d, msg '%s'", isSmall, color, res.Code, res.Msg)
  165. }
  166. return nil
  167. }
  168. // GetWhiteStaticList 查询白名单 IP 并返回其 ID
  169. func (s *aoDunService) GetWhiteStaticList(ctx context.Context, isSmall bool, ip string,serverIp string, color string) (int, error) {
  170. // 使用一个无限循环,直到API返回空数据页才停止
  171. for i := 0; ; i++ { // i++ 会持续请求下一页
  172. formData := map[string]interface{}{
  173. "action": "get",
  174. "bwflag": color,
  175. "page": i,
  176. "ip": ip,
  177. }
  178. var res v1.IpGetResponse
  179. err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
  180. if err != nil {
  181. return 0, err // 网络或请求本身出错,直接返回
  182. }
  183. if res.Code != 0 {
  184. // API返回了业务错误,直接返回
  185. return 0, fmt.Errorf("API 错误 (isSmall: %t): color %s,code %d, msg '%s'", isSmall, color, res.Code, res.Msg)
  186. }
  187. // 如果当前页的数据为空,说明已经没有更多数据了,可以跳出循环。
  188. // 这是分页查询结束的正确信号。
  189. if len(res.Data) == 0 {
  190. break
  191. }
  192. // 在当前页的数据中查找目标记录
  193. for _, v := range res.Data {
  194. if v.Remark == "宁波高防IP过白" && v.ServerIP == serverIp {
  195. // 找到了,立即返回ID
  196. return v.ID, nil
  197. }
  198. }
  199. // 可选:为了防止无限循环,可以加一个最大页数限制
  200. if i > 50 { // 比如最多查100页
  201. break
  202. }
  203. }
  204. // 如果循环正常结束(所有页都查完了),说明没有找到符合条件的记录
  205. return 0, fmt.Errorf("未找到 IP '%s' 相关的 '%s'名单记录 (备注: 宁波高防IP过白) (isSmall: %t)", ip, color, isSmall)
  206. }
  207. // DelWhiteStaticList 根据 ID 从白名单中删除 IP
  208. func (s *aoDunService) DelWhiteStaticList(ctx context.Context, isSmall bool, id string, color string) error {
  209. formData := map[string]interface{}{
  210. "action": "del",
  211. "bwflag": color,
  212. "flag": 0,
  213. "ids": id,
  214. }
  215. var res v1.IpResponse
  216. err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
  217. if err != nil {
  218. return err
  219. }
  220. if res.Code != 0 {
  221. return fmt.Errorf("API 错误 (isSmall: %t): color %s,code %d, msg '%s'", isSmall, color, res.Code, res.Msg)
  222. }
  223. return nil
  224. }
  225. // sendDomainFormData 处理域名白名单的 application/x-www-form-urlencoded 请求
  226. func (s *aoDunService) sendDomainFormData(ctx context.Context, domain, ip, apiType string) ([]byte, error) {
  227. var apiURL string
  228. switch apiType {
  229. case "add":
  230. apiURL = "http://zapi.zzybgp.com/api/user/do_main"
  231. case "del":
  232. apiURL = "http://zapi.zzybgp.com/api/user/do_main/delete"
  233. default:
  234. return nil, fmt.Errorf("无效的 apiType: %s", apiType)
  235. }
  236. formData := url.Values{}
  237. formData.Set("username", s.cfg.DomainUsername)
  238. formData.Set("password", s.cfg.DomainPassword)
  239. formData.Add("do_main_list[name][]", domain)
  240. formData.Add("do_main_list[ip]", ip)
  241. req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(formData.Encode()))
  242. if err != nil {
  243. return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
  244. }
  245. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  246. resp, err := s.httpClient.Do(req)
  247. if err != nil {
  248. return nil, fmt.Errorf("发送 HTTP 请求失败: %w", err)
  249. }
  250. defer resp.Body.Close()
  251. body, err := io.ReadAll(resp.Body)
  252. if err != nil {
  253. return nil, fmt.Errorf("读取响应体失败: %w", err)
  254. }
  255. if resp.StatusCode != http.StatusOK {
  256. return nil, fmt.Errorf("HTTP 错误: 状态码 %d, 响应: %s", resp.StatusCode, string(body))
  257. }
  258. return body, nil
  259. }
  260. // DomainWhiteList 添加或删除域名白名单
  261. func (s *aoDunService) DomainWhiteList(ctx context.Context, domain, ip, apiType string) error {
  262. resBody, err := s.sendDomainFormData(ctx, domain, ip, apiType)
  263. if err != nil {
  264. return err
  265. }
  266. var res v1.DomainResponse
  267. if err := json.Unmarshal(resBody, &res); err != nil {
  268. return fmt.Errorf("反序列化响应 JSON 失败 (内容: %s): %w", string(resBody), err)
  269. }
  270. switch apiType {
  271. case "add":
  272. if res.Code != 200 {
  273. return fmt.Errorf("API 错误: code %d, msg '%s', info '%s'", res.Code, res.Msg, res.Info)
  274. }
  275. case "del":
  276. if res.Code != 600 {
  277. return fmt.Errorf("API 错误: code %d, msg '%s', info '%s'", res.Code, res.Msg, res.Info)
  278. }
  279. }
  280. return nil
  281. }
  282. // AddBandwidthLimit 添加带宽限制
  283. func (s *aoDunService) AddBandwidthLimit(ctx context.Context, req v1.Bandwidth) error {
  284. var res v1.BandwidthResponse
  285. formData := map[string]interface{}{
  286. "server_ip_type": req.ServerIPType,
  287. "server_ip_start": req.ServerIPStart,
  288. "name": req.Name,
  289. "speedlimit_out": req.SpeedlimitOut,
  290. "client_ip_type": req.ClientIPType,
  291. "action": req.Action,
  292. "direction": req.Direction,
  293. "protocol": req.Protocol,
  294. }
  295. err := s.sendAuthenticatedRequest(ctx, true, "/v1.0/firewall/add_filter_rule", formData, &res)
  296. if err != nil {
  297. return err
  298. }
  299. if res.Err != 0 {
  300. return fmt.Errorf("API 错误: code %d, msg '%s'", res.Err, res.Msg)
  301. }
  302. if res.Msg != "操作成功" {
  303. return fmt.Errorf("API 错误: code %d, msg '%s'", res.Err, res.Msg)
  304. }
  305. return nil
  306. }
  307. // DelBandwidthLimit 删除带宽限制
  308. func (s *aoDunService) DelBandwidthLimit(ctx context.Context, req v1.Bandwidth) error {
  309. var res v1.BandwidthResponse
  310. formData := map[string]interface{}{
  311. "name": req.Name,
  312. }
  313. err := s.sendAuthenticatedRequest(ctx, true, "/v1.0/firewall/delete_filter_rule", formData, &res)
  314. if err != nil {
  315. return err
  316. }
  317. if res.Err != 0 {
  318. return fmt.Errorf("API 错误: code %d, msg '%s'", res.Err, res.Msg)
  319. }
  320. if res.Msg != "操作成功" {
  321. return fmt.Errorf("API 错误: code %d, msg '%s'", res.Err, res.Msg)
  322. }
  323. return nil
  324. }