main.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. package main
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "log"
  7. "math/rand"
  8. "os"
  9. "regexp"
  10. "runtime"
  11. "sort"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/denverdino/aliyungo/common"
  16. dns "github.com/honwen/aliyun-ddns-cli/alidns"
  17. "github.com/honwen/golibs/cip"
  18. "github.com/honwen/golibs/domain"
  19. "github.com/urfave/cli"
  20. )
  21. // AccessKey from https://ak-console.aliyun.com/#/accesskey
  22. type AccessKey struct {
  23. ID string
  24. Secret string
  25. client *dns.Client
  26. managedDomains []string
  27. }
  28. func (ak *AccessKey) getClient() *dns.Client {
  29. if len(ak.ID) <= 0 && len(ak.Secret) <= 0 {
  30. return nil
  31. }
  32. if ak.client == nil {
  33. ak.client = dns.NewClient(ak.ID, ak.Secret)
  34. ak.client.SetEndpoint(dns.DNSDefaultEndpointNew)
  35. }
  36. return ak.client
  37. }
  38. func (ak AccessKey) String() string {
  39. return fmt.Sprintf("Access Key: [ ID: %s ;\t Secret: %s ]", ak.ID, ak.Secret)
  40. }
  41. func (ak *AccessKey) ListManagedDomains() (domains []string, err error) {
  42. var resp []dns.DomainType
  43. resp, err = ak.getClient().DescribeDomains(
  44. &dns.DescribeDomainsArgs{
  45. Pagination: common.Pagination{PageSize: 50},
  46. })
  47. if err != nil {
  48. return
  49. }
  50. domains = make([]string, len(resp))
  51. for i, v := range resp {
  52. domains[i] = v.DomainName
  53. }
  54. return
  55. }
  56. func (ak *AccessKey) AutocheckDomainRR(rr, domain string) (r, d string, err error) {
  57. if contains(ak.managedDomains, domain) {
  58. return rr, domain, nil
  59. } else {
  60. if !strings.Contains(rr, `.`) {
  61. return "", "", fmt.Errorf("Domain [%s.%s] Not Managed", rr, domain)
  62. } else {
  63. rrs := strings.Split(rr, `.`)
  64. for i := len(rrs) - 1; i > 0; i-- {
  65. d = strings.Join(append(rrs[i:], domain), `.`)
  66. if contains(ak.managedDomains, d) {
  67. r = strings.Join(rrs[:i], `.`)
  68. return
  69. }
  70. }
  71. }
  72. }
  73. return "", "", fmt.Errorf("Domain [%s.%s] Not Managed", rr, domain)
  74. }
  75. func (ak *AccessKey) ListRecord(domain string) (dnsRecords []dns.RecordTypeNew, err error) {
  76. var resp *dns.DescribeDomainRecordsNewResponse
  77. for idx := 1; idx <= 99; idx++ {
  78. resp, err = ak.getClient().DescribeDomainRecordsNew(
  79. &dns.DescribeDomainRecordsNewArgs{
  80. DomainName: domain,
  81. Pagination: common.Pagination{PageNumber: idx, PageSize: 50},
  82. })
  83. if err != nil {
  84. return
  85. }
  86. dnsRecords = append(dnsRecords, resp.DomainRecords.Record...)
  87. if len(dnsRecords) >= resp.PaginationResult.TotalCount {
  88. return
  89. }
  90. }
  91. return
  92. }
  93. func (ak *AccessKey) DelRecord(rr, domain string) (err error) {
  94. var target *dns.RecordTypeNew
  95. if dnsRecords, err := ak.ListRecord(domain); err == nil {
  96. for i := range dnsRecords {
  97. if dnsRecords[i].RR == rr {
  98. target = &dnsRecords[i]
  99. _, err = ak.getClient().DeleteDomainRecord(
  100. &dns.DeleteDomainRecordArgs{
  101. RecordId: target.RecordId,
  102. },
  103. )
  104. }
  105. }
  106. } else {
  107. return err
  108. }
  109. return
  110. }
  111. func (ak *AccessKey) UpdateRecord(recordID, rr, dmType, value string, ttl int) (err error) {
  112. _, err = ak.getClient().UpdateDomainRecord(
  113. &dns.UpdateDomainRecordArgs{
  114. RecordId: recordID,
  115. RR: rr,
  116. Value: value,
  117. Type: dmType,
  118. TTL: json.Number(fmt.Sprint(ttl)),
  119. })
  120. return
  121. }
  122. func (ak *AccessKey) AddRecord(domain, rr, dmType, value string, ttl int) (err error) {
  123. _, err = ak.getClient().AddDomainRecord(
  124. &dns.AddDomainRecordArgs{
  125. DomainName: domain,
  126. RR: rr,
  127. Type: dmType,
  128. Value: value,
  129. TTL: json.Number(fmt.Sprint(ttl)),
  130. })
  131. return err
  132. }
  133. func (ak *AccessKey) CheckAndUpdateRecord(rr, domain, ipaddr, recordType string, ttl int) (err error) {
  134. fulldomain := strings.Join([]string{rr, domain}, `.`)
  135. if reslove(fulldomain) == ipaddr {
  136. return // Skip
  137. }
  138. targetCnt := 0
  139. var target *dns.RecordTypeNew
  140. if dnsRecords, err := ak.ListRecord(domain); err == nil {
  141. for i := range dnsRecords {
  142. if dnsRecords[i].RR == rr && dnsRecords[i].Type == recordType {
  143. target = &dnsRecords[i]
  144. targetCnt++
  145. }
  146. }
  147. } else {
  148. return err
  149. }
  150. if targetCnt > 1 {
  151. ak.DelRecord(rr, domain)
  152. target = nil
  153. }
  154. if target == nil {
  155. err = ak.AddRecord(domain, rr, recordType, ipaddr, ttl)
  156. } else if target.Value != ipaddr {
  157. if target.Type != recordType {
  158. return fmt.Errorf("record type error! oldType=%s, targetType=%s", target.Type, recordType)
  159. }
  160. err = ak.UpdateRecord(target.RecordId, target.RR, target.Type, ipaddr, ttl)
  161. }
  162. if err != nil && strings.Contains(err.Error(), `DomainRecordDuplicate`) {
  163. ak.DelRecord(rr, domain)
  164. return ak.CheckAndUpdateRecord(rr, domain, ipaddr, recordType, ttl)
  165. }
  166. return err
  167. }
  168. var (
  169. accessKey AccessKey
  170. VersionString = "MISSING build version [git hash]"
  171. )
  172. func init() {
  173. rand.Seed(time.Now().UnixNano())
  174. }
  175. func main() {
  176. app := cli.NewApp()
  177. app.Name = "aliddns"
  178. app.Usage = "aliyun-ddns-cli"
  179. app.Version = fmt.Sprintf("Git:[%s] (%s)", strings.ToUpper(VersionString), runtime.Version())
  180. app.Commands = []cli.Command{
  181. {
  182. Name: "list",
  183. Category: "DDNS",
  184. Usage: "List AliYun's DNS DomainRecords Record",
  185. Flags: []cli.Flag{
  186. cli.StringFlag{
  187. Name: "domain, d",
  188. Usage: "Specific `DomainName`. like aliyun.com",
  189. },
  190. },
  191. Action: func(c *cli.Context) error {
  192. if err := appInit(c, true); err != nil {
  193. return err
  194. }
  195. // fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"))
  196. domain := c.String("domain")
  197. if !contains(accessKey.managedDomains, domain) {
  198. return fmt.Errorf("Domain [%s] Not Managed", domain)
  199. }
  200. if dnsRecords, err := accessKey.ListRecord(domain); err != nil {
  201. fmt.Printf("%+v", err)
  202. } else {
  203. for _, v := range dnsRecords {
  204. fmt.Printf("%20s %-16s %s\n", v.RR+`.`+v.DomainName, fmt.Sprintf("%s(TTL:%4s)", v.Type, v.TTL), v.Value)
  205. }
  206. }
  207. return nil
  208. },
  209. },
  210. {
  211. Name: "delete",
  212. Category: "DDNS",
  213. Usage: "Delete AliYun's DNS DomainRecords Record",
  214. Flags: []cli.Flag{
  215. cli.StringFlag{
  216. Name: "domain, d",
  217. Usage: "Specific `FullDomainName`. like ddns.aliyun.com",
  218. },
  219. },
  220. Action: func(c *cli.Context) error {
  221. if err := appInit(c, true); err != nil {
  222. return err
  223. }
  224. // fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"))
  225. rr, domain, err := accessKey.AutocheckDomainRR(domain.SplitDomainToRR(c.String("domain")))
  226. if err != nil {
  227. return err
  228. }
  229. if err := accessKey.DelRecord(rr, domain); err != nil {
  230. fmt.Printf("%+v", err)
  231. } else {
  232. fmt.Println(c.String("domain"), "Deleted")
  233. }
  234. return nil
  235. },
  236. },
  237. {
  238. Name: "update",
  239. Category: "DDNS",
  240. Usage: "Update AliYun's DNS DomainRecords Record, Create Record if not exist",
  241. Flags: []cli.Flag{
  242. cli.StringFlag{
  243. Name: "domain, d",
  244. Usage: "Specific `DomainName`. like ddns.aliyun.com",
  245. },
  246. cli.StringFlag{
  247. Name: "ipaddr, i",
  248. Usage: "Specific `IP`. like 1.2.3.4",
  249. },
  250. cli.IntFlag{
  251. Name: "ttl, t",
  252. Value: 600,
  253. Usage: "The resolution effective time (in `seconds`)",
  254. },
  255. },
  256. Action: func(c *cli.Context) error {
  257. if err := appInit(c, true); err != nil {
  258. return err
  259. }
  260. // fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"), c.String("ipaddr"))
  261. rr, domain, err := accessKey.AutocheckDomainRR(domain.SplitDomainToRR(c.String("domain")))
  262. if err != nil {
  263. return err
  264. }
  265. recordType := "A"
  266. if c.GlobalBool("ipv6") {
  267. recordType = "AAAA"
  268. }
  269. if err := accessKey.CheckAndUpdateRecord(rr, domain, c.String("ipaddr"), recordType, c.Int("ttl")); err != nil {
  270. log.Printf("%+v", err)
  271. } else {
  272. log.Println(c.String("domain"), c.String("ipaddr"), ip2locCN(c.String("ipaddr")))
  273. }
  274. return nil
  275. },
  276. },
  277. {
  278. Name: "auto-update",
  279. Category: "DDNS",
  280. Usage: "Auto-Update AliYun's DNS DomainRecords Record, Get IP using its getip",
  281. Flags: []cli.Flag{
  282. cli.StringFlag{
  283. Name: "domain, d",
  284. Usage: "Specific `DomainName`. like ddns.aliyun.com",
  285. },
  286. cli.StringFlag{
  287. Name: "redo, r",
  288. Value: "",
  289. Usage: "redo Auto-Update, every N `Seconds`; Disable if N less than 10; End with [Rr] enable random delay: [N, 2N]",
  290. },
  291. cli.IntFlag{
  292. Name: "ttl, t",
  293. Value: 600,
  294. Usage: "The resolution effective time (in `seconds`)",
  295. },
  296. },
  297. Action: func(c *cli.Context) error {
  298. if err := appInit(c, true); err != nil {
  299. return err
  300. }
  301. // fmt.Println(c.Command.Name, "task: ", accessKey, c.String("domain"), c.Int64("redo"))
  302. rr, domain, err := accessKey.AutocheckDomainRR(domain.SplitDomainToRR(c.String("domain")))
  303. if err != nil {
  304. return err
  305. }
  306. recordType := "A"
  307. if c.GlobalBool("ipv6") {
  308. recordType = "AAAA"
  309. }
  310. redoDurtionStr := c.String("redo")
  311. if len(redoDurtionStr) > 0 && !regexp.MustCompile(`\d+[Rr]?$`).MatchString(redoDurtionStr) {
  312. return errors.New(`redo format: [0-9]+[Rr]?$`)
  313. }
  314. randomDelay := regexp.MustCompile(`\d+[Rr]$`).MatchString(redoDurtionStr)
  315. redoDurtion := 0
  316. if randomDelay {
  317. redoDurtion, _ = strconv.Atoi(redoDurtionStr[:len(redoDurtionStr)-1])
  318. } else {
  319. redoDurtion, _ = strconv.Atoi(redoDurtionStr)
  320. }
  321. // Print Version if exist
  322. if redoDurtion > 0 && !strings.HasPrefix(VersionString, "MISSING") {
  323. fmt.Fprintf(os.Stderr, "%s %s\n", strings.ToUpper(c.App.Name), c.App.Version)
  324. }
  325. for {
  326. autoip := myip()
  327. if len(autoip) == 0 {
  328. log.Printf("# Err-CheckAndUpdateRecord: [%s]", "IP is empty, PLZ check network")
  329. } else {
  330. if err := accessKey.CheckAndUpdateRecord(rr, domain, autoip, recordType, c.Int("ttl")); err != nil {
  331. log.Printf("# Err-CheckAndUpdateRecord: [%+v]", err)
  332. } else {
  333. log.Println(c.String("domain"), autoip, ip2locCN(autoip))
  334. }
  335. }
  336. if redoDurtion < 10 {
  337. break // Disable if N less than 10
  338. }
  339. if randomDelay {
  340. time.Sleep(time.Duration(redoDurtion+rand.Intn(redoDurtion)) * time.Second)
  341. } else {
  342. time.Sleep(time.Duration(redoDurtion) * time.Second)
  343. }
  344. }
  345. return nil
  346. },
  347. },
  348. {
  349. Name: "getip",
  350. Category: "GET-IP",
  351. Usage: fmt.Sprintf(" Get IP Combine 10+ different Web-API"),
  352. Action: func(c *cli.Context) error {
  353. if err := appInit(c, false); err != nil {
  354. return err
  355. }
  356. // fmt.Println(c.Command.Name, "task: ", c.Command.Usage)
  357. ip := myip()
  358. fmt.Println(ip, ip2locCN(ip))
  359. return nil
  360. },
  361. },
  362. {
  363. Name: "resolve",
  364. Category: "GET-IP",
  365. Usage: fmt.Sprintf(" Get DNS-IPv4 Combine 4+ DNS Upstream"),
  366. Flags: []cli.Flag{
  367. cli.StringFlag{
  368. Name: "domain, d",
  369. Usage: "Specific `DomainName`. like ddns.aliyun.com",
  370. },
  371. },
  372. Action: func(c *cli.Context) error {
  373. if err := appInit(c, false); err != nil {
  374. return err
  375. }
  376. // fmt.Println(c.Command.Name, "task: ", c.Command.Usage)
  377. ip := reslove(c.String("domain"))
  378. fmt.Println(ip, ip2locCN(ip))
  379. return nil
  380. },
  381. },
  382. }
  383. app.Flags = []cli.Flag{
  384. cli.StringFlag{
  385. Name: "access-key-id, id",
  386. Usage: "AliYun's Access Key ID",
  387. },
  388. cli.StringFlag{
  389. Name: "access-key-secret, secret",
  390. Usage: "AliYun's Access Key Secret",
  391. },
  392. cli.StringSliceFlag{
  393. Name: "ipapi, api",
  394. Usage: "Web-API to Get IP, like: http://myip.ipip.net",
  395. },
  396. cli.BoolFlag{
  397. Name: "ipv6, 6",
  398. Usage: "IPv6",
  399. },
  400. }
  401. app.Action = func(c *cli.Context) error {
  402. return appInit(c, true)
  403. }
  404. app.Run(os.Args)
  405. }
  406. func appInit(c *cli.Context, checkAccessKey bool) error {
  407. akids := []string{c.GlobalString("access-key-id"), os.Getenv("AKID"), os.Getenv("AccessKeyID")}
  408. akscts := []string{c.GlobalString("access-key-secret"), os.Getenv("AKSCT"), os.Getenv("AccessKeySecret")}
  409. sort.Sort(sort.Reverse(sort.StringSlice(akids)))
  410. sort.Sort(sort.Reverse(sort.StringSlice(akscts)))
  411. accessKey.ID = akids[0]
  412. accessKey.Secret = akscts[0]
  413. if checkAccessKey && accessKey.getClient() == nil {
  414. cli.ShowAppHelp(c)
  415. return errors.New("access-key is empty")
  416. }
  417. if domains, err := accessKey.ListManagedDomains(); err == nil {
  418. // log.Println(domains)
  419. accessKey.managedDomains = domains
  420. } else {
  421. cli.ShowAppHelp(c)
  422. return errors.New("No Managed Domains")
  423. }
  424. if c.GlobalBool("ipv6") {
  425. funcs["myip"] = cip.MyIPv6
  426. funcs["reslove"] = cip.ResloveIPv6
  427. }
  428. ipapi := []string{}
  429. for _, api := range c.GlobalStringSlice("ipapi") {
  430. if !regexp.MustCompile(`^https?://.*`).MatchString(api) {
  431. api = "http://" + api
  432. }
  433. if regexp.MustCompile(`(https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]`).MatchString(api) {
  434. ipapi = append(ipapi, api)
  435. }
  436. }
  437. if len(ipapi) > 0 {
  438. regx := regexp.MustCompile(cip.RegxIPv4)
  439. if c.GlobalBoolT("ipv6") {
  440. regx = regexp.MustCompile(cip.RegxIPv6)
  441. }
  442. funcs["myip"] = func() string {
  443. return cip.FastWGetWithVailder(ipapi, func(s string) string {
  444. return regx.FindString((s))
  445. })
  446. }
  447. }
  448. return nil
  449. }