123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- package service
- import (
- "bytes"
- "context"
- "crypto/tls"
- "encoding/json"
- "fmt"
- v1 "github.com/go-nunu/nunu-layout-advanced/api/v1"
- "github.com/spf13/viper"
- "go.uber.org/zap"
- "io"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "time"
- )
- // AoDunService 定义了与傲盾 API 交互的服务接口
- type AoDunService interface {
- DomainWhiteList(ctx context.Context, domain string, ip string, apiType string) error
- AddWhiteStaticList(ctx context.Context, isSmall bool, req []v1.IpInfo) error
- DelWhiteStaticList(ctx context.Context, isSmall bool, id string) error
- GetWhiteStaticList(ctx context.Context, isSmall bool, ip string) (int, error)
- }
- // aoDunService 是 AoDunService 接口的实现
- type aoDunService struct {
- *Service
- cfg *aoDunConfig
- httpClient *http.Client
- }
- // aoDunConfig 用于整合来自 viper 的所有配置
- type aoDunConfig struct {
- Url string
- ClientID string
- Username string
- Password string
- SmallUrl string
- SmallClientID string
- DomainUsername string
- DomainPassword string
- }
- // NewAoDunService 创建一个新的 AoDunService 实例
- func NewAoDunService(service *Service, conf *viper.Viper) AoDunService {
- cfg := &aoDunConfig{
- Url: conf.GetString("aodun.Url"),
- ClientID: conf.GetString("aodun.clientID"),
- Username: conf.GetString("aodun.username"),
- Password: conf.GetString("aodun.password"),
- SmallUrl: conf.GetString("aodunSmall.Url"),
- SmallClientID: conf.GetString("aodunSmall.clientID"),
- DomainUsername: conf.GetString("domainWhite.username"),
- DomainPassword: conf.GetString("domainWhite.password"),
- }
- tr := &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
- MaxIdleConns: 100,
- IdleConnTimeout: 90 * time.Second,
- ForceAttemptHTTP2: true,
- }
- client := &http.Client{
- Transport: tr,
- Timeout: 15 * time.Second,
- }
- return &aoDunService{
- Service: service,
- cfg: cfg,
- httpClient: client,
- }
- }
- // getApiUrl 根据 isSmall 标志返回正确的 API 基础 URL
- func (s *aoDunService) getApiUrl(isSmall bool) string {
- if isSmall {
- return s.cfg.SmallUrl
- }
- return s.cfg.Url
- }
- // getClientID 根据 isSmall 标志返回正确的 ClientID
- func (s *aoDunService) getClientID(isSmall bool) string {
- if isSmall {
- return s.cfg.SmallClientID
- }
- return s.cfg.ClientID
- }
- // executeRequest 封装了发送 HTTP POST 请求、读取响应和 JSON 解码的通用逻辑
- func (s *aoDunService) executeRequest(ctx context.Context, url, tokenType, token string, requestBody, responsePayload interface{}, isSmall bool) error {
- jsonData, err := json.Marshal(requestBody)
- if err != nil {
- return fmt.Errorf("序列化请求数据失败 (isSmall: %t): %w", isSmall, err)
- }
- req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
- if err != nil {
- return fmt.Errorf("创建 HTTP 请求失败 (isSmall: %t): %w", isSmall, err)
- }
- req.Header.Set("Content-Type", "application/json")
- if token != "" {
- req.Header.Set("Authorization", tokenType+" "+token)
- }
- resp, err := s.httpClient.Do(req)
- if err != nil {
- return fmt.Errorf("发送 HTTP 请求失败 (isSmall: %t): %w", isSmall, err)
- }
- defer resp.Body.Close()
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return fmt.Errorf("读取响应体失败 (isSmall: %t): %w", isSmall, err)
- }
- if resp.StatusCode != http.StatusOK {
- return fmt.Errorf("HTTP 错误 (isSmall: %t): 状态码 %d, 响应: %s", isSmall, resp.StatusCode, string(body))
- }
- if err := json.Unmarshal(body, responsePayload); err != nil {
- return fmt.Errorf("反序列化响应 JSON 失败 (isSmall: %t, 内容: %s): %w", isSmall, string(body), err)
- }
- return nil
- }
- // sendAuthenticatedRequest 封装了需要认证的 API 请求的通用流程
- func (s *aoDunService) sendAuthenticatedRequest(ctx context.Context, isSmall bool, apiPath string, requestBody, responsePayload interface{}) error {
- tokenType, token, err := s.GetToken(ctx, isSmall)
- if err != nil {
- return err
- }
- apiURL := s.getApiUrl(isSmall) + apiPath
- return s.executeRequest(ctx, apiURL, tokenType, token, requestBody, responsePayload, isSmall)
- }
- // GetToken 获取认证令牌
- func (s *aoDunService) GetToken(ctx context.Context, isSmall bool) (string, string, error) {
- formData := map[string]interface{}{
- "ClientID": s.getClientID(isSmall),
- "GrantType": "password",
- "Username": s.cfg.Username,
- "Password": s.cfg.Password,
- }
- apiURL := s.getApiUrl(isSmall) + "/oauth/token"
- var res v1.GetTokenRespone
- if err := s.executeRequest(ctx, apiURL, "", "", formData, &res, isSmall); err != nil {
- return "", "", err
- }
- if res.Code != 0 {
- return "", "", fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
- }
- if res.AccessToken == "" {
- return "", "", fmt.Errorf("API 成功 (isSmall: %t, code 0) 但 access_token 为空", isSmall)
- }
- return res.TokenType, res.AccessToken, nil
- }
- // AddWhiteStaticList 添加 IP 到静态白名单
- func (s *aoDunService) AddWhiteStaticList(ctx context.Context, isSmall bool, req []v1.IpInfo) error {
- formData := map[string]interface{}{
- "action": "add",
- "bwflag": "white",
- "insert_bw_list": req,
- }
- var res v1.IpResponse
- err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
- if err != nil {
- return err
- }
- if res.Code != 0 {
- if strings.Contains(res.Msg, "操作部分成功,重复IP如下") {
- s.logger.Info(res.Msg, zap.String("isSmall", strconv.FormatBool(isSmall)))
- return nil
- }
- return fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
- }
- return nil
- }
- // GetWhiteStaticList 查询白名单 IP 并返回其 ID
- func (s *aoDunService) GetWhiteStaticList(ctx context.Context, isSmall bool, ip string) (int, error) {
- formData := map[string]interface{}{
- "action": "get",
- "bwflag": "white",
- "page": 1,
- "ids": ip,
- }
- var res v1.IpGetResponse
- err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
- if err != nil {
- return 0, err
- }
- if res.Code != 0 {
- return 0, fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
- }
- if len(res.Data) == 0 {
- return 0, fmt.Errorf("未找到 IP '%s' 相关的白名单记录 (isSmall: %t)", ip, isSmall)
- }
- return res.Data[0].ID, nil
- }
- // DelWhiteStaticList 根据 ID 从白名单中删除 IP
- func (s *aoDunService) DelWhiteStaticList(ctx context.Context, isSmall bool, id string) error {
- formData := map[string]interface{}{
- "action": "del",
- "bwflag": "white",
- "flag": 0,
- "ids": id,
- }
- var res v1.IpResponse
- err := s.sendAuthenticatedRequest(ctx, isSmall, "/v1.0/firewall/static_bw_list", formData, &res)
- if err != nil {
- return err
- }
- if res.Code != 0 {
- return fmt.Errorf("API 错误 (isSmall: %t): code %d, msg '%s'", isSmall, res.Code, res.Msg)
- }
- return nil
- }
- // sendDomainFormData 处理域名白名单的 application/x-www-form-urlencoded 请求
- func (s *aoDunService) sendDomainFormData(ctx context.Context, domain, ip, apiType string) ([]byte, error) {
- var apiURL string
- switch apiType {
- case "add":
- apiURL = "http://zapi.zzybgp.com/api/user/do_main"
- case "del":
- apiURL = "http://zapi.zzybgp.com/api/user/do_main/delete"
- default:
- return nil, fmt.Errorf("无效的 apiType: %s", apiType)
- }
- formData := url.Values{}
- formData.Set("username", s.cfg.DomainUsername)
- formData.Set("password", s.cfg.DomainPassword)
- formData.Add("do_main_list[name][]", domain)
- formData.Add("do_main_list[ip]", ip)
- req, err := http.NewRequestWithContext(ctx, "POST", apiURL, strings.NewReader(formData.Encode()))
- if err != nil {
- return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
- }
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- resp, err := s.httpClient.Do(req)
- if err != nil {
- return nil, fmt.Errorf("发送 HTTP 请求失败: %w", err)
- }
- defer resp.Body.Close()
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("读取响应体失败: %w", err)
- }
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("HTTP 错误: 状态码 %d, 响应: %s", resp.StatusCode, string(body))
- }
- return body, nil
- }
- // DomainWhiteList 添加或删除域名白名单
- func (s *aoDunService) DomainWhiteList(ctx context.Context, domain, ip, apiType string) error {
- resBody, err := s.sendDomainFormData(ctx, domain, ip, apiType)
- if err != nil {
- return err
- }
- var res v1.DomainResponse
- if err := json.Unmarshal(resBody, &res); err != nil {
- return fmt.Errorf("反序列化响应 JSON 失败 (内容: %s): %w", string(resBody), err)
- }
- switch apiType {
- case "add":
- if res.Code != 200 {
- return fmt.Errorf("API 错误: code %d, msg '%s', info '%s'", res.Code, res.Msg, res.Info)
- }
- case "del":
- if res.Code != 600 {
- return fmt.Errorf("API 错误: code %d, msg '%s', info '%s'", res.Code, res.Msg, res.Info)
- }
- }
- return nil
- }
|