471 lines
13 KiB
Go
471 lines
13 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"math/rand"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/denverdino/aliyungo/common"
|
|
dns "github.com/honwen/aliyun-ddns-cli/alidns"
|
|
"github.com/honwen/golibs/cip"
|
|
"github.com/honwen/golibs/domain"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
// AccessKey from https://ak-console.aliyun.com/#/accesskey
|
|
type AccessKey struct {
|
|
ID string
|
|
Secret string
|
|
client *dns.Client
|
|
managedDomains []string
|
|
}
|
|
|
|
func (ak *AccessKey) getClient() *dns.Client {
|
|
if len(ak.ID) <= 0 && len(ak.Secret) <= 0 {
|
|
return nil
|
|
}
|
|
if ak.client == nil {
|
|
ak.client = dns.NewClient(ak.ID, ak.Secret)
|
|
ak.client.SetEndpoint(dns.DNSDefaultEndpointNew)
|
|
}
|
|
return ak.client
|
|
}
|
|
|
|
func (ak AccessKey) String() string {
|
|
return fmt.Sprintf("Access Key: [ ID: %s ;\t Secret: %s ]", ak.ID, ak.Secret)
|
|
}
|
|
|
|
func (ak *AccessKey) ListManagedDomains() (domains []string, err error) {
|
|
var resp []dns.DomainType
|
|
resp, err = ak.getClient().DescribeDomains(
|
|
&dns.DescribeDomainsArgs{
|
|
Pagination: common.Pagination{PageSize: 50},
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
domains = make([]string, len(resp))
|
|
for i, v := range resp {
|
|
domains[i] = v.DomainName
|
|
}
|
|
return
|
|
}
|
|
|
|
func (ak *AccessKey) AutocheckDomainRR(rr, domain string) (r, d string, err error) {
|
|
if contains(ak.managedDomains, domain) {
|
|
return rr, domain, nil
|
|
} else {
|
|
if !strings.Contains(rr, `.`) {
|
|
return "", "", fmt.Errorf("Domain [%s.%s] Not Managed", rr, domain)
|
|
} else {
|
|
rrs := strings.Split(rr, `.`)
|
|
for i := len(rrs) - 1; i > 0; i-- {
|
|
d = strings.Join(append(rrs[i:], domain), `.`)
|
|
if contains(ak.managedDomains, d) {
|
|
r = strings.Join(rrs[:i], `.`)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "", "", fmt.Errorf("Domain [%s.%s] Not Managed", rr, domain)
|
|
}
|
|
|
|
func (ak *AccessKey) ListRecord(domain string) (dnsRecords []dns.RecordTypeNew, err error) {
|
|
var resp *dns.DescribeDomainRecordsNewResponse
|
|
for idx := 1; idx <= 99; idx++ {
|
|
resp, err = ak.getClient().DescribeDomainRecordsNew(
|
|
&dns.DescribeDomainRecordsNewArgs{
|
|
DomainName: domain,
|
|
Pagination: common.Pagination{PageNumber: idx, PageSize: 50},
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
dnsRecords = append(dnsRecords, resp.DomainRecords.Record...)
|
|
if len(dnsRecords) >= resp.PaginationResult.TotalCount {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (ak *AccessKey) DelRecord(rr, domain string) (err error) {
|
|
var target *dns.RecordTypeNew
|
|
if dnsRecords, err := ak.ListRecord(domain); err == nil {
|
|
for i := range dnsRecords {
|
|
if dnsRecords[i].RR == rr {
|
|
target = &dnsRecords[i]
|
|
_, err = ak.getClient().DeleteDomainRecord(
|
|
&dns.DeleteDomainRecordArgs{
|
|
RecordId: target.RecordId,
|
|
},
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
return
|
|
}
|
|
|
|
func (ak *AccessKey) UpdateRecord(recordID, rr, dmType, value string, ttl int) (err error) {
|
|
_, err = ak.getClient().UpdateDomainRecord(
|
|
&dns.UpdateDomainRecordArgs{
|
|
RecordId: recordID,
|
|
RR: rr,
|
|
Value: value,
|
|
Type: dmType,
|
|
TTL: json.Number(fmt.Sprint(ttl)),
|
|
})
|
|
return
|
|
}
|
|
|
|
func (ak *AccessKey) AddRecord(domain, rr, dmType, value string, ttl int) (err error) {
|
|
_, err = ak.getClient().AddDomainRecord(
|
|
&dns.AddDomainRecordArgs{
|
|
DomainName: domain,
|
|
RR: rr,
|
|
Type: dmType,
|
|
Value: value,
|
|
TTL: json.Number(fmt.Sprint(ttl)),
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (ak *AccessKey) CheckAndUpdateRecord(rr, domain, ipaddr, recordType string, ttl int) (err error) {
|
|
fulldomain := strings.Join([]string{rr, domain}, `.`)
|
|
if reslove(fulldomain) == ipaddr {
|
|
return // Skip
|
|
}
|
|
targetCnt := 0
|
|
var target *dns.RecordTypeNew
|
|
if dnsRecords, err := ak.ListRecord(domain); err == nil {
|
|
for i := range dnsRecords {
|
|
if dnsRecords[i].RR == rr && dnsRecords[i].Type == recordType {
|
|
target = &dnsRecords[i]
|
|
targetCnt++
|
|
}
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
|
|
if targetCnt > 1 {
|
|
ak.DelRecord(rr, domain)
|
|
target = nil
|
|
}
|
|
|
|
if target == nil {
|
|
err = ak.AddRecord(domain, rr, recordType, ipaddr, ttl)
|
|
} else if target.Value != ipaddr {
|
|
if target.Type != recordType {
|
|
return fmt.Errorf("record type error! oldType=%s, targetType=%s", target.Type, recordType)
|
|
}
|
|
err = ak.UpdateRecord(target.RecordId, target.RR, target.Type, ipaddr, ttl)
|
|
}
|
|
if err != nil && strings.Contains(err.Error(), `DomainRecordDuplicate`) {
|
|
ak.DelRecord(rr, domain)
|
|
return ak.CheckAndUpdateRecord(rr, domain, ipaddr, recordType, ttl)
|
|
}
|
|
return err
|
|
}
|
|
|
|
var (
|
|
accessKey AccessKey
|
|
VersionString = "MISSING build version [git hash]"
|
|
)
|
|
|
|
func init() {
|
|
rand.Seed(time.Now().UnixNano())
|
|
}
|
|
|
|
func main() {
|
|
app := cli.NewApp()
|
|
app.Name = "aliddns"
|
|
app.Usage = "aliyun-ddns-cli"
|
|
app.Version = fmt.Sprintf("Git:[%s] (%s)", strings.ToUpper(VersionString), runtime.Version())
|
|
app.Commands = []cli.Command{
|
|
{
|
|
Name: "list",
|
|
Category: "DDNS",
|
|
Usage: "List AliYun's DNS DomainRecords Record",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "domain, d",
|
|
Usage: "Specific `DomainName`. like aliyun.com",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := appInit(c, true); err != nil {
|
|
return err
|
|
}
|
|
// fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"))
|
|
domain := c.String("domain")
|
|
if !contains(accessKey.managedDomains, domain) {
|
|
return fmt.Errorf("Domain [%s] Not Managed", domain)
|
|
}
|
|
if dnsRecords, err := accessKey.ListRecord(domain); err != nil {
|
|
fmt.Printf("%+v", err)
|
|
} else {
|
|
for _, v := range dnsRecords {
|
|
fmt.Printf("%20s %-16s %s\n", v.RR+`.`+v.DomainName, fmt.Sprintf("%s(TTL:%4s)", v.Type, v.TTL), v.Value)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "delete",
|
|
Category: "DDNS",
|
|
Usage: "Delete AliYun's DNS DomainRecords Record",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "domain, d",
|
|
Usage: "Specific `FullDomainName`. like ddns.aliyun.com",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := appInit(c, true); err != nil {
|
|
return err
|
|
}
|
|
// fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"))
|
|
rr, domain, err := accessKey.AutocheckDomainRR(domain.SplitDomainToRR(c.String("domain")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := accessKey.DelRecord(rr, domain); err != nil {
|
|
fmt.Printf("%+v", err)
|
|
} else {
|
|
fmt.Println(c.String("domain"), "Deleted")
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "update",
|
|
Category: "DDNS",
|
|
Usage: "Update AliYun's DNS DomainRecords Record, Create Record if not exist",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "domain, d",
|
|
Usage: "Specific `DomainName`. like ddns.aliyun.com",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "ipaddr, i",
|
|
Usage: "Specific `IP`. like 1.2.3.4",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "ttl, t",
|
|
Value: 600,
|
|
Usage: "The resolution effective time (in `seconds`)",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := appInit(c, true); err != nil {
|
|
return err
|
|
}
|
|
// fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"), c.String("ipaddr"))
|
|
rr, domain, err := accessKey.AutocheckDomainRR(domain.SplitDomainToRR(c.String("domain")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recordType := "A"
|
|
if c.GlobalBool("ipv6") {
|
|
recordType = "AAAA"
|
|
}
|
|
if err := accessKey.CheckAndUpdateRecord(rr, domain, c.String("ipaddr"), recordType, c.Int("ttl")); err != nil {
|
|
log.Printf("%+v", err)
|
|
} else {
|
|
log.Println(c.String("domain"), c.String("ipaddr"), ip2locCN(c.String("ipaddr")))
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "auto-update",
|
|
Category: "DDNS",
|
|
Usage: "Auto-Update AliYun's DNS DomainRecords Record, Get IP using its getip",
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "domain, d",
|
|
Usage: "Specific `DomainName`. like ddns.aliyun.com",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "redo, r",
|
|
Value: "",
|
|
Usage: "redo Auto-Update, every N `Seconds`; Disable if N less than 10; End with [Rr] enable random delay: [N, 2N]",
|
|
},
|
|
cli.IntFlag{
|
|
Name: "ttl, t",
|
|
Value: 600,
|
|
Usage: "The resolution effective time (in `seconds`)",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := appInit(c, true); err != nil {
|
|
return err
|
|
}
|
|
// fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"), c.Int64("redo"))
|
|
rr, domain, err := accessKey.AutocheckDomainRR(domain.SplitDomainToRR(c.String("domain")))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
recordType := "A"
|
|
if c.GlobalBool("ipv6") {
|
|
recordType = "AAAA"
|
|
}
|
|
redoDurtionStr := c.String("redo")
|
|
if len(redoDurtionStr) > 0 && !regexp.MustCompile(`\d+[Rr]?$`).MatchString(redoDurtionStr) {
|
|
return errors.New(`redo format: [0-9]+[Rr]?$`)
|
|
}
|
|
randomDelay := regexp.MustCompile(`\d+[Rr]$`).MatchString(redoDurtionStr)
|
|
redoDurtion := 0
|
|
if randomDelay {
|
|
redoDurtion, _ = strconv.Atoi(redoDurtionStr[:len(redoDurtionStr)-1])
|
|
} else {
|
|
redoDurtion, _ = strconv.Atoi(redoDurtionStr)
|
|
}
|
|
// Print Version if exist
|
|
if redoDurtion > 0 && !strings.HasPrefix(VersionString, "MISSING") {
|
|
fmt.Fprintf(os.Stderr, "%s %s\n", strings.ToUpper(c.App.Name), c.App.Version)
|
|
}
|
|
for {
|
|
autoip := myip()
|
|
if len(autoip) == 0 {
|
|
log.Printf("# Err-CheckAndUpdateRecord: [%s]", "IP is empty, PLZ check network")
|
|
} else {
|
|
if err := accessKey.CheckAndUpdateRecord(rr, domain, autoip, recordType, c.Int("ttl")); err != nil {
|
|
log.Printf("# Err-CheckAndUpdateRecord: [%+v]", err)
|
|
} else {
|
|
log.Println(c.String("domain"), autoip, ip2locCN(autoip))
|
|
}
|
|
}
|
|
if redoDurtion < 10 {
|
|
break // Disable if N less than 10
|
|
}
|
|
if randomDelay {
|
|
time.Sleep(time.Duration(redoDurtion+rand.Intn(redoDurtion)) * time.Second)
|
|
} else {
|
|
time.Sleep(time.Duration(redoDurtion) * time.Second)
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "getip",
|
|
Category: "GET-IP",
|
|
Usage: fmt.Sprintf(" Get IP Combine 10+ different Web-API"),
|
|
Action: func(c *cli.Context) error {
|
|
if err := appInit(c, false); err != nil {
|
|
return err
|
|
}
|
|
// fmt.Println(c.Command.Name, "task: ", c.Command.Usage)
|
|
ip := myip()
|
|
fmt.Println(ip, ip2locCN(ip))
|
|
return nil
|
|
},
|
|
},
|
|
{
|
|
Name: "resolve",
|
|
Category: "GET-IP",
|
|
Usage: fmt.Sprintf(" Get DNS-IPv4 Combine 4+ DNS Upstream"),
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "domain, d",
|
|
Usage: "Specific `DomainName`. like ddns.aliyun.com",
|
|
},
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
if err := appInit(c, false); err != nil {
|
|
return err
|
|
}
|
|
// fmt.Println(c.Command.Name, "task: ", c.Command.Usage)
|
|
ip := reslove(c.String("domain"))
|
|
fmt.Println(ip, ip2locCN(ip))
|
|
return nil
|
|
},
|
|
},
|
|
}
|
|
app.Flags = []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "access-key-id, id",
|
|
Usage: "AliYun's Access Key ID",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "access-key-secret, secret",
|
|
Usage: "AliYun's Access Key Secret",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "ipapi, api",
|
|
Usage: "Web-API to Get IP, like: http://myip.ipip.net",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "ipv6, 6",
|
|
Usage: "IPv6",
|
|
},
|
|
}
|
|
app.Action = func(c *cli.Context) error {
|
|
return appInit(c, true)
|
|
}
|
|
app.Run(os.Args)
|
|
}
|
|
|
|
func appInit(c *cli.Context, checkAccessKey bool) error {
|
|
akids := []string{c.GlobalString("access-key-id"), os.Getenv("AKID"), os.Getenv("AccessKeyID")}
|
|
akscts := []string{c.GlobalString("access-key-secret"), os.Getenv("AKSCT"), os.Getenv("AccessKeySecret")}
|
|
sort.Sort(sort.Reverse(sort.StringSlice(akids)))
|
|
sort.Sort(sort.Reverse(sort.StringSlice(akscts)))
|
|
accessKey.ID = akids[0]
|
|
accessKey.Secret = akscts[0]
|
|
if checkAccessKey && accessKey.getClient() == nil {
|
|
cli.ShowAppHelp(c)
|
|
return errors.New("access-key is empty")
|
|
}
|
|
if domains, err := accessKey.ListManagedDomains(); err == nil {
|
|
// log.Println(domains)
|
|
accessKey.managedDomains = domains
|
|
} else {
|
|
cli.ShowAppHelp(c)
|
|
return errors.New("No Managed Domains")
|
|
}
|
|
|
|
if c.GlobalBool("ipv6") {
|
|
funcs["myip"] = cip.MyIPv6
|
|
funcs["reslove"] = cip.ResloveIPv6
|
|
}
|
|
|
|
ipapi := []string{}
|
|
for _, api := range c.GlobalStringSlice("ipapi") {
|
|
if !regexp.MustCompile(`^https?://.*`).MatchString(api) {
|
|
api = "http://" + api
|
|
}
|
|
if regexp.MustCompile(`(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]`).MatchString(api) {
|
|
ipapi = append(ipapi, api)
|
|
}
|
|
}
|
|
if len(ipapi) > 0 {
|
|
regx := regexp.MustCompile(cip.RegxIPv4)
|
|
if c.GlobalBoolT("ipv6") {
|
|
regx = regexp.MustCompile(cip.RegxIPv6)
|
|
}
|
|
funcs["myip"] = func() string {
|
|
return cip.FastWGetWithVailder(ipapi, func(s string) string {
|
|
return regx.FindString((s))
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|