main.go 11 KB

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