1.4.5.2
This commit is contained in:
537
EdgeAPI/internal/dnsclients/provider_gname.go
Normal file
537
EdgeAPI/internal/dnsclients/provider_gname.go
Normal file
@@ -0,0 +1,537 @@
|
||||
package dnsclients
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
teaconst "github.com/TeaOSLab/EdgeAPI/internal/const"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/dnstypes"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/dnsclients/gname"
|
||||
"github.com/TeaOSLab/EdgeAPI/internal/errors"
|
||||
"github.com/iwind/TeaGo/maps"
|
||||
"github.com/iwind/TeaGo/types"
|
||||
)
|
||||
|
||||
const (
|
||||
GnameDefaultRoute = "0" // 默认线路代码,根据文档为 "0"
|
||||
GnameAPIEndpoint = "https://api.gname.com" // Gname API 基础地址
|
||||
)
|
||||
|
||||
// gnameNormalizeRouteCode 将 Gname 列表接口返回的线路(可能是名称或代码)统一为 AddRecord 使用的代码,
|
||||
// 避免 oldRecordsMap 的 key(route@value)与任务生成的 key 不一致导致重复添加、触发「相同主机记录及相同记录值」错误。
|
||||
func gnameNormalizeRouteCode(xl string) string {
|
||||
if xl == "" {
|
||||
return GnameDefaultRoute
|
||||
}
|
||||
// Gname 列表可能返回线路名称或代码,与 GetRoutes 中的 Name/Code 对应
|
||||
switch xl {
|
||||
case "中国大陆":
|
||||
return "asia-cn"
|
||||
case "非中国大陆":
|
||||
return "non-cn"
|
||||
case "默认":
|
||||
return "0"
|
||||
case "搜索引擎":
|
||||
return "seo"
|
||||
case "搜索引擎-Google":
|
||||
return "seo-google"
|
||||
case "搜索引擎-Baidu":
|
||||
return "seo-baidu"
|
||||
case "搜索引擎-Bing":
|
||||
return "seo-bing"
|
||||
case "亚洲":
|
||||
return "asia"
|
||||
case "新加坡":
|
||||
return "asia-sg"
|
||||
case "欧洲":
|
||||
return "eu"
|
||||
case "美洲":
|
||||
return "na-usa"
|
||||
}
|
||||
// 已是代码或未知值,原样返回
|
||||
return xl
|
||||
}
|
||||
|
||||
var gnameHTTPClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// GnameProvider Gname DNS 提供商
|
||||
// 参考文档:https://www.gname.vip/domain/api/jiexi/add
|
||||
type GnameProvider struct {
|
||||
BaseProvider
|
||||
ProviderId int64
|
||||
|
||||
// Gname API 认证信息
|
||||
appid string // APPID (Application Client Id)
|
||||
secret string // API Secret (用于签名)
|
||||
}
|
||||
|
||||
// Auth 认证
|
||||
func (this *GnameProvider) Auth(params maps.Map) error {
|
||||
this.appid = params.GetString("appid")
|
||||
if len(this.appid) == 0 {
|
||||
// 兼容旧参数名
|
||||
this.appid = params.GetString("apiKey")
|
||||
}
|
||||
if len(this.appid) == 0 {
|
||||
return errors.New("'appid' or 'apiKey' should not be empty")
|
||||
}
|
||||
|
||||
this.secret = params.GetString("secret")
|
||||
if len(this.secret) == 0 {
|
||||
// 兼容旧参数名
|
||||
this.secret = params.GetString("apiSecret")
|
||||
}
|
||||
if len(this.secret) == 0 {
|
||||
return errors.New("'secret' or 'apiSecret' should not be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaskParams 对参数进行掩码
|
||||
func (this *GnameProvider) MaskParams(params maps.Map) {
|
||||
if params == nil {
|
||||
return
|
||||
}
|
||||
if params.Has("secret") {
|
||||
params["secret"] = MaskString(params.GetString("secret"))
|
||||
}
|
||||
if params.Has("apiSecret") {
|
||||
params["apiSecret"] = MaskString(params.GetString("apiSecret"))
|
||||
}
|
||||
}
|
||||
|
||||
// GetDomains 获取所有域名列表
|
||||
func (this *GnameProvider) GetDomains() (domains []string, err error) {
|
||||
var resp = &gname.BaseResponse{}
|
||||
|
||||
err = this.doAPI(http.MethodPost, "/api/domain/list", map[string]string{}, resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Code != 1 {
|
||||
return nil, errors.New("Failed: " + resp.Msg)
|
||||
}
|
||||
|
||||
// 解析响应数据 - 支持多种可能的数据格式
|
||||
if resp.Data != nil {
|
||||
// 方式1: 直接是字符串数组
|
||||
if dataArray, ok := resp.Data.([]interface{}); ok {
|
||||
for _, item := range dataArray {
|
||||
switch v := item.(type) {
|
||||
case string:
|
||||
// 直接是域名字符串
|
||||
if len(v) > 0 {
|
||||
domains = append(domains, v)
|
||||
}
|
||||
case map[string]interface{}:
|
||||
// 是对象,尝试多个可能的字段名
|
||||
if domainName, ok := v["domain"].(string); ok && len(domainName) > 0 {
|
||||
domains = append(domains, domainName)
|
||||
} else if domainName, ok := v["name"].(string); ok && len(domainName) > 0 {
|
||||
domains = append(domains, domainName)
|
||||
} else if domainName, ok := v["ym"].(string); ok && len(domainName) > 0 {
|
||||
domains = append(domains, domainName)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if dataMap, ok := resp.Data.(map[string]interface{}); ok {
|
||||
// 方式2: 数据在某个字段中(如 data.list, data.domains 等)
|
||||
if list, ok := dataMap["list"].([]interface{}); ok {
|
||||
for _, item := range list {
|
||||
if domainName, ok := item.(string); ok && len(domainName) > 0 {
|
||||
domains = append(domains, domainName)
|
||||
} else if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
if domainName, ok := itemMap["domain"].(string); ok && len(domainName) > 0 {
|
||||
domains = append(domains, domainName)
|
||||
} else if domainName, ok := itemMap["name"].(string); ok && len(domainName) > 0 {
|
||||
domains = append(domains, domainName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if domainsList, ok := dataMap["domains"].([]interface{}); ok {
|
||||
for _, item := range domainsList {
|
||||
if domainName, ok := item.(string); ok && len(domainName) > 0 {
|
||||
domains = append(domains, domainName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetRecords 获取域名解析记录列表
|
||||
func (this *GnameProvider) GetRecords(domain string) (records []*dnstypes.Record, err error) {
|
||||
var resp = &gname.BaseResponse{}
|
||||
err = this.doAPI(http.MethodPost, "/api/resolution/list", map[string]string{
|
||||
"ym": domain, // 域名
|
||||
}, resp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Code != 1 {
|
||||
return nil, errors.New("Failed: " + resp.Msg)
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
if resp.Data != nil {
|
||||
if dataList, ok := resp.Data.([]interface{}); ok {
|
||||
for _, item := range dataList {
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
record := &dnstypes.Record{}
|
||||
|
||||
if id, ok := itemMap["id"].(float64); ok {
|
||||
record.Id = types.String(int64(id))
|
||||
}
|
||||
if zj, ok := itemMap["zj"].(string); ok {
|
||||
record.Name = zj // 主机记录
|
||||
}
|
||||
if lx, ok := itemMap["lx"].(string); ok {
|
||||
record.Type = lx // 记录类型
|
||||
}
|
||||
if jlz, ok := itemMap["jlz"].(string); ok {
|
||||
record.Value = jlz // 记录值
|
||||
}
|
||||
if ttl, ok := itemMap["ttl"].(float64); ok {
|
||||
record.TTL = int32(ttl)
|
||||
}
|
||||
if xl, ok := itemMap["xl"].(string); ok {
|
||||
record.Route = gnameNormalizeRouteCode(xl) // 线路:统一为代码,与 AddRecord 一致
|
||||
} else {
|
||||
record.Route = GnameDefaultRoute
|
||||
}
|
||||
|
||||
// 修正 CNAME 记录值
|
||||
if record.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(record.Value, ".") {
|
||||
record.Value += "."
|
||||
}
|
||||
|
||||
records = append(records, record)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetRoutes 读取域名支持的线路数据
|
||||
func (this *GnameProvider) GetRoutes(domain string) (routes []*dnstypes.Route, err error) {
|
||||
// 根据 Gname API 文档,线路列表
|
||||
routes = []*dnstypes.Route{
|
||||
{Name: "默认", Code: "0"},
|
||||
{Name: "搜索引擎", Code: "seo"},
|
||||
{Name: "搜索引擎-Google", Code: "seo-google"},
|
||||
{Name: "搜索引擎-Baidu", Code: "seo-baidu"},
|
||||
{Name: "搜索引擎-Bing", Code: "seo-bing"},
|
||||
{Name: "亚洲", Code: "asia"},
|
||||
{Name: "中国大陆", Code: "asia-cn"},
|
||||
{Name: "非中国大陆", Code: "non-cn"},
|
||||
{Name: "新加坡", Code: "asia-sg"},
|
||||
{Name: "欧洲", Code: "eu"},
|
||||
{Name: "美洲", Code: "na-usa"},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// QueryRecord 查询单个记录
|
||||
func (this *GnameProvider) QueryRecord(domain string, name string, recordType dnstypes.RecordType) (*dnstypes.Record, error) {
|
||||
records, err := this.QueryRecords(domain, name, recordType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return records[0], nil
|
||||
}
|
||||
|
||||
// QueryRecords 查询多个记录
|
||||
func (this *GnameProvider) QueryRecords(domain string, name string, recordType dnstypes.RecordType) ([]*dnstypes.Record, error) {
|
||||
// 先获取所有记录,然后过滤
|
||||
allRecords, err := this.GetRecords(domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result = []*dnstypes.Record{}
|
||||
for _, record := range allRecords {
|
||||
if record.Name == name && record.Type == recordType {
|
||||
result = append(result, record)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AddRecord 设置记录
|
||||
// 参考文档:https://www.gname.vip/domain/api/jiexi/add
|
||||
func (this *GnameProvider) AddRecord(domain string, newRecord *dnstypes.Record) error {
|
||||
// 修正 CNAME 记录
|
||||
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
|
||||
newRecord.Value += "."
|
||||
}
|
||||
|
||||
// 设置默认 TTL
|
||||
var ttl = newRecord.TTL
|
||||
if ttl <= 0 {
|
||||
ttl = 600
|
||||
}
|
||||
|
||||
// 使用 Gname API 文档中的参数名
|
||||
params := map[string]string{
|
||||
"ym": domain, // 域名
|
||||
"lx": newRecord.Type, // 记录类型:A,CNAME,MX,URL,TXT
|
||||
"zj": newRecord.Name, // 主机记录
|
||||
"jlz": newRecord.Value, // 记录值
|
||||
"ttl": types.String(ttl), // TTL值
|
||||
"xl": GnameDefaultRoute, // 线路,默认使用 "0"
|
||||
}
|
||||
|
||||
// 如果有线路支持
|
||||
if len(newRecord.Route) > 0 && newRecord.Route != this.DefaultRoute() {
|
||||
params["xl"] = newRecord.Route
|
||||
}
|
||||
|
||||
// MX 记录需要 MX 值
|
||||
if newRecord.Type == "MX" {
|
||||
params["mx"] = "10" // 默认 MX 优先级
|
||||
}
|
||||
|
||||
var resp = &gname.BaseResponse{}
|
||||
err := this.doAPI(http.MethodPost, "/api/resolution/add", params, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查响应状态:code=1 表示成功,-1 表示失败
|
||||
if resp.Code != 1 {
|
||||
return errors.New("Failed: " + resp.Msg)
|
||||
}
|
||||
|
||||
// 如果返回了记录ID,可以保存到 newRecord.Id
|
||||
if resp.Data != nil {
|
||||
if id, ok := resp.Data.(float64); ok {
|
||||
newRecord.Id = types.String(int64(id))
|
||||
} else if idStr, ok := resp.Data.(string); ok {
|
||||
newRecord.Id = idStr
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRecord 修改记录
|
||||
func (this *GnameProvider) UpdateRecord(domain string, record *dnstypes.Record, newRecord *dnstypes.Record) error {
|
||||
// 修正 CNAME 记录
|
||||
if newRecord.Type == dnstypes.RecordTypeCNAME && !strings.HasSuffix(newRecord.Value, ".") {
|
||||
newRecord.Value += "."
|
||||
}
|
||||
|
||||
// 设置默认 TTL
|
||||
var ttl = newRecord.TTL
|
||||
if ttl <= 0 {
|
||||
ttl = 600
|
||||
}
|
||||
|
||||
// 使用 Gname API 文档中的参数名
|
||||
params := map[string]string{
|
||||
"ym": domain, // 域名
|
||||
"id": record.Id, // 记录ID
|
||||
"lx": newRecord.Type, // 记录类型
|
||||
"zj": newRecord.Name, // 主机记录
|
||||
"jlz": newRecord.Value, // 记录值
|
||||
"ttl": types.String(ttl), // TTL值
|
||||
"xl": GnameDefaultRoute, // 线路
|
||||
}
|
||||
|
||||
// 如果有线路支持
|
||||
if len(newRecord.Route) > 0 && newRecord.Route != this.DefaultRoute() {
|
||||
params["xl"] = newRecord.Route
|
||||
}
|
||||
|
||||
// MX 记录需要 MX 值
|
||||
if newRecord.Type == "MX" {
|
||||
params["mx"] = "10" // 默认 MX 优先级
|
||||
}
|
||||
|
||||
var resp = &gname.BaseResponse{}
|
||||
err := this.doAPI(http.MethodPost, "/api/resolution/modify", params, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查响应状态:code=1 表示成功,-1 表示失败
|
||||
if resp.Code != 1 {
|
||||
return errors.New("Failed: " + resp.Msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRecord 删除记录
|
||||
func (this *GnameProvider) DeleteRecord(domain string, record *dnstypes.Record) error {
|
||||
params := map[string]string{
|
||||
"ym": domain, // 域名
|
||||
"id": record.Id, // 记录ID
|
||||
}
|
||||
|
||||
var resp = &gname.BaseResponse{}
|
||||
err := this.doAPI(http.MethodPost, "/api/resolution/delete", params, resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查响应状态:code=1 表示成功,-1 表示失败
|
||||
if resp.Code != 1 {
|
||||
return errors.New("Failed: " + resp.Msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DefaultRoute 默认线路
|
||||
func (this *GnameProvider) DefaultRoute() string {
|
||||
return "0" // 根据文档,默认线路代码为 "0"
|
||||
}
|
||||
|
||||
// doAPI 发送 API 请求
|
||||
// 根据 Gname API 文档,所有请求都需要包含 appid, gntime, gntoken 参数
|
||||
func (this *GnameProvider) doAPI(method string, apiPath string, params map[string]string, respPtr interface{}) error {
|
||||
apiURL := GnameAPIEndpoint + apiPath
|
||||
method = strings.ToUpper(method)
|
||||
|
||||
// 添加公共参数
|
||||
if params == nil {
|
||||
params = map[string]string{}
|
||||
}
|
||||
params["appid"] = this.appid
|
||||
params["gntime"] = types.String(time.Now().Unix())
|
||||
|
||||
// 生成签名
|
||||
params["gntoken"] = this.generateSignature(params)
|
||||
|
||||
// 构建请求体(POST 方式)
|
||||
var bodyReader io.Reader = nil
|
||||
if method == http.MethodPost {
|
||||
var formData = url.Values{}
|
||||
for k, v := range params {
|
||||
formData.Set(k, v)
|
||||
}
|
||||
bodyReader = strings.NewReader(formData.Encode())
|
||||
} else {
|
||||
// GET 方式使用查询参数
|
||||
var query = url.Values{}
|
||||
for k, v := range params {
|
||||
query.Set(k, v)
|
||||
}
|
||||
apiURL += "?" + query.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, apiURL, bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("User-Agent", teaconst.ProductName+"/"+teaconst.Version)
|
||||
if method == http.MethodPost {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
resp, err := gnameHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode == 0 {
|
||||
return errors.New("invalid response status '" + strconv.Itoa(resp.StatusCode) + "', response '" + string(data) + "'")
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New("response error: " + string(data))
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
if respPtr != nil {
|
||||
err = json.Unmarshal(data, respPtr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode json failed: %w: %s", err, string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateSignature 生成签名
|
||||
// 根据 Gname API 文档的签名方法:
|
||||
// 1. 将所有参数(除了 gntoken)按字母顺序排序
|
||||
// 2. 拼接成字符串:key1=value1&key2=value2&...
|
||||
// 3. 加上密钥:字符串 + secret
|
||||
// 4. MD5 加密并转大写
|
||||
func (this *GnameProvider) generateSignature(params map[string]string) string {
|
||||
// 复制参数(排除 gntoken)
|
||||
var signParams = map[string]string{}
|
||||
for k, v := range params {
|
||||
if k != "gntoken" {
|
||||
signParams[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// 按字母顺序排序
|
||||
var keys = []string{}
|
||||
for k := range signParams {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 拼接字符串
|
||||
var source strings.Builder
|
||||
for i, key := range keys {
|
||||
if i > 0 {
|
||||
source.WriteString("&")
|
||||
}
|
||||
source.WriteString(key)
|
||||
source.WriteString("=")
|
||||
source.WriteString(url.QueryEscape(signParams[key]))
|
||||
}
|
||||
|
||||
// 加上密钥
|
||||
source.WriteString(this.secret)
|
||||
|
||||
// MD5 加密并转大写
|
||||
md := md5.Sum([]byte(source.String()))
|
||||
return strings.ToUpper(hex.EncodeToString(md[:]))
|
||||
}
|
||||
Reference in New Issue
Block a user