diff --git a/README.md b/README.md index 0043272..8417c51 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ ## 特性 - 支持Mac、Windows、Linux系统,支持ARM、x86架构 -- 支持的域名服务商 `阿里云` `腾讯云` `Dnspod` `Cloudflare` `华为云` `Callback` `百度云` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `时代互联` `Eranet` +- 支持的域名服务商 `阿里云` `腾讯云` `Dnspod` `Cloudflare` `华为云` `Callback` `百度云` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `时代互联` `Eranet` `Gcore` - 支持接口/网卡/[命令](https://github.com/jeessy2/ddns-go/wiki/通过命令获取IP参考)获取IP - 支持以服务的方式运行 - 默认间隔5分钟同步一次 diff --git a/README_EN.md b/README_EN.md index 50c8ae0..f2ed3e7 100644 --- a/README_EN.md +++ b/README_EN.md @@ -16,7 +16,7 @@ Automatically obtain your public IPv4 or IPv6 address and resolve it to the corr ## Features - Support Mac, Windows, Linux system, support ARM, x86 architecture -- Support domain service providers `Aliyun` `Tencent` `Dnspod` `Cloudflare` `Huawei` `Callback` `Baidu` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `Nowcn` `Eranet` +- Support domain service providers `Aliyun` `Tencent` `Dnspod` `Cloudflare` `Huawei` `Callback` `Baidu` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `Nowcn` `Eranet` `Gcore` - Support interface / netcard / command to get IP - Support running as a service - Default interval is 5 minutes diff --git a/dns/gcore.go b/dns/gcore.go new file mode 100644 index 0000000..12aae3b --- /dev/null +++ b/dns/gcore.go @@ -0,0 +1,301 @@ +package dns + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/jeessy2/ddns-go/v6/config" + "github.com/jeessy2/ddns-go/v6/util" +) + +const gcoreAPIEndpoint = "https://api.gcore.com/dns/v2" + +// Gcore Gcore DNS实现 +type Gcore struct { + DNS config.DNS + Domains config.Domains + TTL int +} + +// GcoreZoneResponse zones返回结果 +type GcoreZoneResponse struct { + Zones []GcoreZone `json:"zones"` + TotalAmount int `json:"total_amount"` +} + +// GcoreZone 域名信息 +type GcoreZone struct { + ID int `json:"id"` + Name string `json:"name"` +} + +// GcoreRRSetListResponse RRSet列表返回结果 +type GcoreRRSetListResponse struct { + RRSets []GcoreRRSet `json:"rrsets"` + TotalAmount int `json:"total_amount"` +} + +// GcoreRRSet RRSet记录实体 +type GcoreRRSet struct { + Name string `json:"name"` + Type string `json:"type"` + TTL int `json:"ttl"` + ResourceRecords []GcoreResourceRecord `json:"resource_records"` + Meta map[string]interface{} `json:"meta,omitempty"` +} + +// GcoreResourceRecord 资源记录 +type GcoreResourceRecord struct { + Content []interface{} `json:"content"` + Enabled bool `json:"enabled"` + ID int `json:"id,omitempty"` + Meta map[string]interface{} `json:"meta,omitempty"` +} + +// GcoreInputRRSet 输入的RRSet +type GcoreInputRRSet struct { + TTL int `json:"ttl"` + ResourceRecords []GcoreInputResourceRecord `json:"resource_records"` + Meta map[string]interface{} `json:"meta,omitempty"` +} + +// GcoreInputResourceRecord 输入的资源记录 +type GcoreInputResourceRecord struct { + Content []interface{} `json:"content"` + Enabled bool `json:"enabled"` + Meta map[string]interface{} `json:"meta,omitempty"` +} + +// Init 初始化 +func (gc *Gcore) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) { + gc.Domains.Ipv4Cache = ipv4cache + gc.Domains.Ipv6Cache = ipv6cache + gc.DNS = dnsConf.DNS + gc.Domains.GetNewIp(dnsConf) + if dnsConf.TTL == "" { + // 默认 120 秒(免费版最低值) + gc.TTL = 120 + } else { + ttl, err := strconv.Atoi(dnsConf.TTL) + if err != nil { + gc.TTL = 120 + } else { + gc.TTL = ttl + } + } +} + +// AddUpdateDomainRecords 添加或更新 IPv4 / IPv6 记录 +func (gc *Gcore) AddUpdateDomainRecords() config.Domains { + gc.addUpdateDomainRecords("A") + gc.addUpdateDomainRecords("AAAA") + return gc.Domains +} + +func (gc *Gcore) addUpdateDomainRecords(recordType string) { + ipAddr, domains := gc.Domains.GetNewIpResult(recordType) + + if ipAddr == "" { + return + } + + for _, domain := range domains { + // get zone + zoneInfo, err := gc.getZoneByDomain(domain) + if err != nil { + util.Log("查询域名信息发生异常! %s", err) + domain.UpdateStatus = config.UpdatedFailed + continue + } + + if zoneInfo == nil { + util.Log("在DNS服务商中未找到根域名: %s", domain.DomainName) + domain.UpdateStatus = config.UpdatedFailed + continue + } + + // 查询现有记录 + existingRecord, err := gc.getRRSet(zoneInfo.Name, domain.GetSubDomain(), recordType) + if err != nil { + util.Log("查询域名信息发生异常! %s", err) + domain.UpdateStatus = config.UpdatedFailed + continue + } + + if existingRecord != nil { + // 更新现有记录 + gc.updateRecord(zoneInfo.Name, domain, recordType, ipAddr, existingRecord) + } else { + // 创建新记录 + gc.createRecord(zoneInfo.Name, domain, recordType, ipAddr) + } + } +} + +// 获取域名对应的Zone信息 +func (gc *Gcore) getZoneByDomain(domain *config.Domain) (*GcoreZone, error) { + var result GcoreZoneResponse + params := url.Values{} + params.Set("name", domain.DomainName) + + err := gc.request( + "GET", + fmt.Sprintf("%s/zones?%s", gcoreAPIEndpoint, params.Encode()), + nil, + &result, + ) + + if err != nil { + return nil, err + } + + if len(result.Zones) > 0 { + return &result.Zones[0], nil + } + + return nil, nil +} + +// 获取指定的RRSet记录 +func (gc *Gcore) getRRSet(zoneName, recordName, recordType string) (*GcoreRRSet, error) { + var result GcoreRRSetListResponse + + err := gc.request( + "GET", + fmt.Sprintf("%s/zones/%s/rrsets", gcoreAPIEndpoint, zoneName), + nil, + &result, + ) + + if err != nil { + return nil, err + } + + // 查找匹配的记录 + fullRecordName := recordName + if recordName != "" && recordName != "@" { + fullRecordName = recordName + "." + zoneName + } else { + fullRecordName = zoneName + } + + for _, rrset := range result.RRSets { + if rrset.Name == fullRecordName && rrset.Type == recordType { + return &rrset, nil + } + } + + return nil, nil +} + +// 创建新记录 +func (gc *Gcore) createRecord(zoneName string, domain *config.Domain, recordType string, ipAddr string) { + recordName := domain.GetSubDomain() + if recordName == "" || recordName == "@" { + recordName = zoneName + } else { + recordName = recordName + "." + zoneName + } + + inputRRSet := GcoreInputRRSet{ + TTL: gc.TTL, + ResourceRecords: []GcoreInputResourceRecord{ + { + Content: []interface{}{ipAddr}, + Enabled: true, + }, + }, + } + + var result interface{} + err := gc.request( + "POST", + fmt.Sprintf("%s/zones/%s/%s/%s", gcoreAPIEndpoint, zoneName, recordName, recordType), + inputRRSet, + &result, + ) + + if err != nil { + util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err) + domain.UpdateStatus = config.UpdatedFailed + return + } + + util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr) + domain.UpdateStatus = config.UpdatedSuccess +} + +// 更新现有记录 +func (gc *Gcore) updateRecord(zoneName string, domain *config.Domain, recordType string, ipAddr string, existingRecord *GcoreRRSet) { + // 检查IP是否相同 + if len(existingRecord.ResourceRecords) > 0 && len(existingRecord.ResourceRecords[0].Content) > 0 { + if existingRecord.ResourceRecords[0].Content[0] == ipAddr { + util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain) + return + } + } + + recordName := domain.GetSubDomain() + if recordName == "" || recordName == "@" { + recordName = zoneName + } else { + recordName = recordName + "." + zoneName + } + + inputRRSet := GcoreInputRRSet{ + TTL: gc.TTL, + ResourceRecords: []GcoreInputResourceRecord{ + { + Content: []interface{}{ipAddr}, + Enabled: true, + }, + }, + } + + var result interface{} + err := gc.request( + "PUT", + fmt.Sprintf("%s/zones/%s/%s/%s", gcoreAPIEndpoint, zoneName, recordName, recordType), + inputRRSet, + &result, + ) + + if err != nil { + util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err) + domain.UpdateStatus = config.UpdatedFailed + return + } + + util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr) + domain.UpdateStatus = config.UpdatedSuccess +} + +// request 统一请求接口 +func (gc *Gcore) request(method string, url string, data interface{}, result interface{}) (err error) { + jsonStr := make([]byte, 0) + if data != nil { + jsonStr, _ = json.Marshal(data) + } + + req, err := http.NewRequest( + method, + url, + bytes.NewBuffer(jsonStr), + ) + if err != nil { + return + } + + req.Header.Set("Authorization", "APIKey "+gc.DNS.Secret) + req.Header.Set("Content-Type", "application/json") + + client := util.CreateHTTPClient() + resp, err := client.Do(req) + err = util.GetHTTPResponse(resp, err, result) + + return +} diff --git a/dns/index.go b/dns/index.go index 987ec20..f4e833d 100644 --- a/dns/index.go +++ b/dns/index.go @@ -27,6 +27,7 @@ var ( tencentCloudEndPoint, dynadotEndpoint, dynv6Endpoint, + gcoreAPIEndpoint, } Ipcache = [][2]util.IpCache{} @@ -94,6 +95,8 @@ func RunOnce() { dnsSelected = &Nowcn{} case "eranet": dnsSelected = &Eranet{} + case "gcore": + dnsSelected = &Gcore{} default: dnsSelected = &Alidns{} } diff --git a/static/constant.js b/static/constant.js index 52af9f0..e60922f 100644 --- a/static/constant.js +++ b/static/constant.js @@ -216,6 +216,17 @@ const DNS_PROVIDERS = { "zh-cn": "获取 api-key", } }, + gcore: { + name: { + "en": "Gcore", + }, + idLabel: "", + secretLabel: "API Token", + helpHtml: { + "en": "Create API Token", + "zh-cn": "创建 API Token", + } + }, }; const SVG_CODE = {