From 3a708d2e079e65854afd93cf7231f5e7470bc199 Mon Sep 17 00:00:00 2001 From: suguer Date: Thu, 26 Jun 2025 09:05:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A1=B9=E7=9B=AE=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E4=BA=86=E5=AF=B9=E6=97=B6=E4=BB=A3=E4=BA=92=E8=81=94www.now.c?= =?UTF-8?q?n=E7=9A=84=E5=AE=98=E6=96=B9=E6=94=AF=E6=8C=81=20(#1492)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 项目新增了对时代互联www.now.cn的官方支持 * fix: 补充文档说明 * fix: 补充执行过程日志 * refactor: 清理已实现的DNS相关功能中的TODO注释 --------- Co-authored-by: dsuzejian --- README.md | 2 +- README_EN.md | 2 +- dns/index.go | 2 + dns/nowcn.go | 209 +++++++++++++++++++++++++++++++++++++++++++++ static/constant.js | 38 ++++++--- 5 files changed, 238 insertions(+), 15 deletions(-) create mode 100644 dns/nowcn.go diff --git a/README.md b/README.md index 4971d13..56e6c6c 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` +- 支持的域名服务商 `阿里云` `腾讯云` `Dnspod` `Cloudflare` `华为云` `Callback` `百度云` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `时代互联` - 支持接口/网卡/[命令](https://github.com/jeessy2/ddns-go/wiki/通过命令获取IP参考)获取IP - 支持以服务的方式运行 - 默认间隔5分钟同步一次 diff --git a/README_EN.md b/README_EN.md index 9c0e0d4..ae173ad 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` +- Support domain service providers `Aliyun` `Tencent` `Dnspod` `Cloudflare` `Huawei` `Callback` `Baidu` `Porkbun` `GoDaddy` `Namecheap` `NameSilo` `Dynadot` `DNSLA` `Nowcn` - Support interface / netcard / command to get IP - Support running as a service - Default interval is 5 minutes diff --git a/dns/index.go b/dns/index.go index da08f7e..2ef9a03 100644 --- a/dns/index.go +++ b/dns/index.go @@ -90,6 +90,8 @@ func RunOnce() { dnsSelected = &Dynv6{} case "spaceship": dnsSelected = &Spaceship{} + case "nowcn": + dnsSelected = &Nowcn{} default: dnsSelected = &Alidns{} } diff --git a/dns/nowcn.go b/dns/nowcn.go new file mode 100644 index 0000000..9d81963 --- /dev/null +++ b/dns/nowcn.go @@ -0,0 +1,209 @@ +package dns + +import ( + "fmt" + "net/url" + "strconv" + "strings" + + "github.com/jeessy2/ddns-go/v6/config" + "github.com/jeessy2/ddns-go/v6/util" +) + +const ( + nowcnRecordListAPI string = "https://todapi.now.cn:2443/api/dns/describe-record-index.json" + nowcnRecordModifyURL string = "https://todapi.now.cn:2443/api/dns/update-domain-record.json" + nowcnRecordCreateAPI string = "https://todapi.now.cn:2443/api/dns/add-domain-record.json" +) + +// https://www.todaynic.com/partner/mode_Http_Api_detail.php?target_id=d15d8028-7c4f-4a5c-9d15-3a4481c4178e +// Nowcn nowcn DNS实现 +type Nowcn struct { + DNS config.DNS + Domains config.Domains + TTL string +} + +// NowcnRecord DNS记录结构 +type NowcnRecord struct { + ID int `json:"id"` + Domain string + Host string + Type string + Value string + State int + // Name string + // Enabled string +} + +// NowcnRecordListResp 记录列表响应 +type NowcnRecordListResp struct { + NowcnStatus + Data []NowcnRecord +} + +// NowcnStatus API响应状态 +type NowcnStatus struct { + RequestId string `json:"RequestId"` + Id int `json:"Id"` + Error string `json:"error"` +} + +// Init 初始化 +func (nowcn *Nowcn) Init(dnsConf *config.DnsConfig, ipv4cache *util.IpCache, ipv6cache *util.IpCache) { + nowcn.Domains.Ipv4Cache = ipv4cache + nowcn.Domains.Ipv6Cache = ipv6cache + nowcn.DNS = dnsConf.DNS + nowcn.Domains.GetNewIp(dnsConf) + if dnsConf.TTL == "" { + // 默认600s + nowcn.TTL = "600" + } else { + nowcn.TTL = dnsConf.TTL + } +} + +// AddUpdateDomainRecords 添加或更新IPv4/IPv6记录 +func (nowcn *Nowcn) AddUpdateDomainRecords() config.Domains { + nowcn.addUpdateDomainRecords("A") + nowcn.addUpdateDomainRecords("AAAA") + return nowcn.Domains +} + +func (nowcn *Nowcn) addUpdateDomainRecords(recordType string) { + ipAddr, domains := nowcn.Domains.GetNewIpResult(recordType) + + if ipAddr == "" { + return + } + + for _, domain := range domains { + result, err := nowcn.getRecordList(domain, recordType) + if err != nil { + util.Log("查询域名信息发生异常! %s", err) + domain.UpdateStatus = config.UpdatedFailed + return + } + + if len(result.Data) > 0 { + // 默认第一个 + recordSelected := result.Data[0] + params := domain.GetCustomParams() + if params.Has("Id") { + for i := 0; i < len(result.Data); i++ { + if strconv.Itoa(result.Data[i].ID) == params.Get("Id") { + recordSelected = result.Data[i] + } + } + } + // 更新 + nowcn.modify(recordSelected, domain, recordType, ipAddr) + } else { + // 新增 + nowcn.create(domain, recordType, ipAddr) + } + } +} + +// create 创建DNS记录 +func (nowcn *Nowcn) create(domain *config.Domain, recordType string, ipAddr string) { + param := map[string]any{ + "Domain": domain.DomainName, + "Host": domain.GetSubDomain(), + "Type": recordType, + "Value": ipAddr, + "Ttl": nowcn.TTL, + } + res, err := nowcn.request(nowcnRecordCreateAPI, param) + if err != nil { + util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, err.Error()) + domain.UpdateStatus = config.UpdatedFailed + } else if res.Error != "" { + util.Log("新增域名解析 %s 失败! 异常信息: %s", domain, res.Error) + domain.UpdateStatus = config.UpdatedFailed + } else { + util.Log("新增域名解析 %s 成功! IP: %s", domain, ipAddr) + domain.UpdateStatus = config.UpdatedSuccess + } +} + +// modify 修改DNS记录 +func (nowcn *Nowcn) modify(record NowcnRecord, domain *config.Domain, recordType string, ipAddr string) { + // 相同不修改 + if record.Value == ipAddr { + util.Log("你的IP %s 没有变化, 域名 %s", ipAddr, domain) + return + } + param := map[string]any{ + "Id": record.ID, + "Domain": domain.DomainName, + "Host": domain.GetSubDomain(), + "Type": recordType, + "Value": ipAddr, + "Ttl": nowcn.TTL, + } + res, err := nowcn.request(nowcnRecordModifyURL, param) + if err != nil { + util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, err.Error()) + domain.UpdateStatus = config.UpdatedFailed + } else if res.Error != "" { + util.Log("更新域名解析 %s 失败! 异常信息: %s", domain, res.Error) + domain.UpdateStatus = config.UpdatedFailed + } else { + util.Log("更新域名解析 %s 成功! IP: %s", domain, ipAddr) + domain.UpdateStatus = config.UpdatedSuccess + } +} + +// request 发送HTTP请求 +func (nowcn *Nowcn) request(apiAddr string, param map[string]any) (status NowcnStatus, err error) { + param["auth-userid"] = nowcn.DNS.ID + param["api-key"] = nowcn.DNS.Secret + + fullURL := apiAddr + "?" + nowcn.queryParams(param) + client := util.CreateHTTPClient() + resp, err := client.Get(fullURL) + + // 处理响应 + err = util.GetHTTPResponse(resp, err, &status) + + return +} + +// getRecordList 获取域名记录列表 +func (nowcn *Nowcn) getRecordList(domain *config.Domain, typ string) (result NowcnRecordListResp, err error) { + param := map[string]any{ + "Domain": domain.DomainName, + "auth-userid": nowcn.DNS.ID, + "api-key": nowcn.DNS.Secret, + } + fullURL := nowcnRecordListAPI + "?" + nowcn.queryParams(param) + client := util.CreateHTTPClient() + resp, err := client.Get(fullURL) + var response NowcnRecordListResp + result = NowcnRecordListResp{ + Data: make([]NowcnRecord, 0), + } + err = util.GetHTTPResponse(resp, err, &response) + for _, v := range response.Data { + if v.Host == domain.GetSubDomain() { + result.Data = append(result.Data, v) + break + } + + } + return +} + +func (nowcn *Nowcn) queryParams(param map[string]any) string { + var queryParams []string + for key, value := range param { + // 只对键进行URL编码,值保持原样(特别是@符号) + encodedKey := url.QueryEscape(key) + valueStr := fmt.Sprintf("%v", value) + // 对值进行选择性编码,保留@符号 + encodedValue := strings.ReplaceAll(url.QueryEscape(valueStr), "%40", "@") + queryParams = append(queryParams, encodedKey+"="+encodedValue) + } + return strings.Join(queryParams, "&") +} diff --git a/static/constant.js b/static/constant.js index da13bd5..92e70ec 100644 --- a/static/constant.js +++ b/static/constant.js @@ -165,8 +165,8 @@ const DNS_PROVIDERS = { idLabel: "", secretLabel: "Token", helpHtml: { - "en": "Create Token", - "zh-cn": "创建令牌", + "en": "Create Token", + "zh-cn": "创建令牌", } }, spaceship: { @@ -180,18 +180,30 @@ const DNS_PROVIDERS = { "zh-cn": "创建 API 密钥", } }, - dnsla: { - name: { - "en": "Dnsla", - "zh-cn": "Dnsla", - }, - idLabel: "APIID", - secretLabel: "API密钥", - helpHtml: { - "en": "Create AccessKey", - "zh-cn": "创建 AccessKey", - } + dnsla: { + name: { + "en": "Dnsla", + "zh-cn": "Dnsla", }, + idLabel: "APIID", + secretLabel: "API密钥", + helpHtml: { + "en": "Create AccessKey", + "zh-cn": "创建 AccessKey", + } + }, + nowcn: { + name: { + "en": "Nowcn", + "zh-cn": "时代互联", + }, + idLabel: "auth-userid", + secretLabel: "api-key", + helpHtml: { + "en": "api-key", + "zh-cn": "获取 api-key", + } + }, }; const SVG_CODE = {