package main import ( "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 } 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) 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) (err error) { _, err = ak.getClient().UpdateDomainRecord( &dns.UpdateDomainRecordArgs{ RecordId: recordID, RR: rr, Value: value, Type: dmType, }) return } func (ak *AccessKey) AddRecord(domain, rr, dmType, value string) (err error) { _, err = ak.getClient().AddDomainRecord( &dns.AddDomainRecordArgs{ DomainName: domain, RR: rr, Type: dmType, Value: value, }) return err } func (ak *AccessKey) CheckAndUpdateRecord(rr, domain, ipaddr, recordType string) (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) } 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) } if err != nil && strings.Contains(err.Error(), `DomainRecordDuplicate`) { ak.DelRecord(rr, domain) return ak.CheckAndUpdateRecord(rr, domain, ipaddr, recordType) } 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 := domain.SplitDomainToRR(c.String("domain")) if dnsRecords, err := accessKey.ListRecord(domain); err != nil { fmt.Printf("%+v", err) } else { for _, v := range dnsRecords { fmt.Printf("%20s %-8s %s\n", v.RR+`.`+v.DomainName, v.Type, 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")) if err := accessKey.DelRecord(domain.SplitDomainToRR(c.String("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", }, }, 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 := domain.SplitDomainToRR(c.String("domain")) recordType := "A" if c.GlobalBool("ipv6") { recordType = "AAAA" } if err := accessKey.CheckAndUpdateRecord(rr, domain, c.String("ipaddr"), recordType); 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]", }, }, 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 := domain.SplitDomainToRR(c.String("domain")) 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); 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 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 }