diff --git a/README.md b/README.md index ce2ce004..22afa8b8 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Now I rewrite [DynDNS](https://github.com/TimothyYe/DynDNS) by Golang and call i * Cloudflare ([https://cloudflare.com](https://cloudflare.com)) * DNSPod ([https://www.dnspod.cn/](https://www.dnspod.cn/)) * HE.net (Hurricane Electric) ([https://dns.he.net/](https://dns.he.net/)) +* AliDNS ([https://help.aliyun.com/product/29697.html](https://help.aliyun.com/product/29697.html)) ## Supported Platforms * Linux @@ -52,7 +53,7 @@ And the binary can run well on routers. * Register and own a domain. -* Domain's nameserver points to [DNSPod](https://www.dnspod.cn/) or [HE.net](https://dns.he.net/) or [Cloudflare](https://www.cloudflare.com/). +* Domain's nameserver points to [DNSPod](https://www.dnspod.cn/) or [HE.net](https://dns.he.net/) or [Cloudflare](https://www.cloudflare.com/) or [AliDNS](https://dc.console.aliyun.com). ## Get it @@ -89,7 +90,7 @@ Usage of ./godns: * Get [config_sample.json](https://github.com/timothyye/godns/blob/master/config_sample.json) from Github. * Rename it to **config.json**. -* Configure your provider, domain/sub-domain info, username and password, etc. +* Configure your provider, domain/subdomain info, username and password, etc. * Configure the SMTP options if you want, a mail notification will sent to your mailbox once the IP is changed. * Save it in the same directory of GoDNS, or use -c=your_conf_path command. @@ -136,6 +137,28 @@ For DNSPod, you need to provide email & password, and config all the domains & "socks5_proxy": "" } ``` +### Config example for AliDNS + +For AliDNS, you need to provide `AccessKeyID` & `AccessKeySecret` as `email` & `password`, and config all the domains & subdomains. + +```json +{ + "provider": "AliDNS", + "email": "AccessKeyID", + "password": "AccessKeySecret", + "login_token": "", + "domains": [{ + "domain_name": "example.com", + "sub_domains": ["www","test"] + },{ + "domain_name": "example2.com", + "sub_domains": ["www","test"] + } + ], + "ip_url": "http://members.3322.org/dyndns/getip", + "socks5_proxy": "" +} +``` ### Config example for HE.net For HE, email is not needed, just fill DDNS key to password, and config all the domains & subdomains. diff --git a/handler/alidns/alidns.go b/handler/alidns/alidns.go new file mode 100644 index 00000000..13ef5ad1 --- /dev/null +++ b/handler/alidns/alidns.go @@ -0,0 +1,157 @@ +package alidns + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +// AliDNS token +type AliDNS struct { + AccessKeyID string + AccessKeySecret string +} + +var ( + publicParm = map[string]string{ + "AccessKeyId": "", + "Format": "JSON", + "Version": "2015-01-09", + "SignatureMethod": "HMAC-SHA1", + "Timestamp": "", + "SignatureVersion": "1.0", + "SignatureNonce": "", + } + baseURL = "http://alidns.aliyuncs.com/" + instance *AliDNS + once sync.Once +) + +type domainRecordsResp struct { + RequestID string `json:"RequestId"` + TotalCount int + PageNumber int + PageSize int + DomainRecords domainRecords +} + +type domainRecords struct { + Record []DomainRecord +} + +// DomainRecord struct +type DomainRecord struct { + DomainName string + RecordID string `json:"RecordId"` + RR string + Type string + Value string + Line string + Priority int + TTL int + Status string + Locked bool +} + +func getHTTPBody(url string) ([]byte, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(resp.Body) + if resp.StatusCode == http.StatusOK { + return body, err + } + return nil, fmt.Errorf("Status %d, Error:%s", resp.StatusCode, body) +} + +func NewAliDNS(key, secret string) *AliDNS { + once.Do(func() { + instance = &AliDNS{ + AccessKeyID: key, + AccessKeySecret: secret, + } + }) + return instance +} + +// GetDomainRecords gets all the doamin records according to input subdomain key +func (d *AliDNS) GetDomainRecords(domain, rr string) []DomainRecord { + resp := &domainRecordsResp{} + parms := map[string]string{ + "Action": "DescribeDomainRecords", + "DomainName": domain, + "RRKeyWord": rr, + } + urlPath := d.genRequestURL(parms) + body, err := getHTTPBody(urlPath) + if err != nil { + fmt.Printf("GetDomainRecords error.%+v\n", err) + } else { + json.Unmarshal(body, resp) + return resp.DomainRecords.Record + } + return nil +} + +// UpdateDomainRecord updates domain record +func (d *AliDNS) UpdateDomainRecord(r DomainRecord) error { + parms := map[string]string{ + "Action": "UpdateDomainRecord", + "RecordId": r.RecordID, + "RR": r.RR, + "Type": r.Type, + "Value": r.Value, + "TTL": strconv.Itoa(r.TTL), + "Line": r.Line, + } + + urlPath := d.genRequestURL(parms) + _, err := getHTTPBody(urlPath) + if err != nil { + fmt.Printf("UpdateDomainRecord error.%+v\n", err) + } + return err +} + +func (d *AliDNS) genRequestURL(parms map[string]string) string { + pArr := []string{} + ps := map[string]string{} + for k, v := range publicParm { + ps[k] = v + } + for k, v := range parms { + ps[k] = v + } + now := time.Now().UTC() + ps["AccessKeyId"] = d.AccessKeyID + ps["SignatureNonce"] = strconv.Itoa(int(now.UnixNano()) + rand.Intn(99999)) + ps["Timestamp"] = now.Format("2006-01-02T15:04:05Z") + + for k, v := range ps { + pArr = append(pArr, fmt.Sprintf("%s=%s", k, v)) + } + sort.Strings(pArr) + path := strings.Join(pArr, "&") + + s := "GET&%2F&" + url.QueryEscape(path) + s = strings.Replace(s, "%3A", "%253A", -1) + s = strings.Replace(s, "%40", "%2540", -1) + s = strings.Replace(s, "%2A", "%252A", -1) + mac := hmac.New(sha1.New, []byte(d.AccessKeySecret+"&")) + + mac.Write([]byte(s)) + sign := base64.StdEncoding.EncodeToString(mac.Sum(nil)) + return fmt.Sprintf("%s?%s&Signature=%s", baseURL, path, url.QueryEscape(sign)) +} diff --git a/handler/alidns/alidns_handler.go b/handler/alidns/alidns_handler.go new file mode 100644 index 00000000..a88555e1 --- /dev/null +++ b/handler/alidns/alidns_handler.go @@ -0,0 +1,77 @@ +package alidns + +import ( + "fmt" + "log" + "runtime/debug" + "time" + + "github.com/TimothyYe/godns" +) + +// AliDNSHandler struct +type AliDNSHandler struct { + Configuration *godns.Settings +} + +// SetConfiguration pass dns settings and store it to handler instance +func (handler *AliDNSHandler) SetConfiguration(conf *godns.Settings) { + handler.Configuration = conf +} + +// DomainLoop the main logic loop +func (handler *AliDNSHandler) DomainLoop(domain *godns.Domain, panicChan chan<- godns.Domain) { + defer func() { + if err := recover(); err != nil { + log.Printf("Recovered in %v: %v\n", err, debug.Stack()) + panicChan <- *domain + } + }() + + var lastIP string + aliDNS := NewAliDNS(handler.Configuration.Email, handler.Configuration.Password) + + for { + currentIP, err := godns.GetCurrentIP(handler.Configuration) + + if err != nil { + log.Println("Failed to get current IP:", err) + continue + } + log.Println("currentIP is:", currentIP) + + //check against locally cached IP, if no change, skip update + if currentIP == lastIP { + log.Printf("IP is the same as cached one. Skip update.\n") + } else { + lastIP = currentIP + + for _, subDomain := range domain.SubDomains { + log.Printf("%s.%s Start to update record IP...\n", subDomain, domain.DomainName) + records := aliDNS.GetDomainRecords(domain.DomainName, subDomain) + if records == nil || len(records) == 0 { + log.Printf("Cannot get subdomain %s from AliDNS.\r\n", subDomain) + continue + } + + records[0].Value = currentIP + if err := aliDNS.UpdateDomainRecord(records[0]); err != nil { + log.Printf("Failed to update IP for subdomain:%s\r\n", subDomain) + continue + } else { + log.Printf("IP updated for subdomain:%s\r\n", subDomain) + } + + // Send mail notification if notify is enabled + if handler.Configuration.Notify.Enabled { + log.Print("Sending notification to:", handler.Configuration.Notify.SendTo) + godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP) + } + } + } + // Interval is 5 minutes + log.Printf("Going to sleep, will start next checking in %d minutes...\r\n", godns.INTERVAL) + time.Sleep(time.Minute * godns.INTERVAL) + } + +} diff --git a/handler/cloudflare_handler.go b/handler/cloudflare/cloudflare_handler.go similarity index 99% rename from handler/cloudflare_handler.go rename to handler/cloudflare/cloudflare_handler.go index 8c51ba4b..636e2b4f 100644 --- a/handler/cloudflare_handler.go +++ b/handler/cloudflare/cloudflare_handler.go @@ -1,4 +1,4 @@ -package handler +package cloudflare import ( "bytes" diff --git a/handler/cloudflare_handler_test.go b/handler/cloudflare/cloudflare_handler_test.go similarity index 99% rename from handler/cloudflare_handler_test.go rename to handler/cloudflare/cloudflare_handler_test.go index fde94646..76128280 100644 --- a/handler/cloudflare_handler_test.go +++ b/handler/cloudflare/cloudflare_handler_test.go @@ -1,4 +1,4 @@ -package handler +package cloudflare import ( "encoding/json" diff --git a/handler/dnspod_handler.go b/handler/dnspod/dnspod_handler.go similarity index 98% rename from handler/dnspod_handler.go rename to handler/dnspod/dnspod_handler.go index ee03c5ec..fdbb1700 100644 --- a/handler/dnspod_handler.go +++ b/handler/dnspod/dnspod_handler.go @@ -1,4 +1,4 @@ -package handler +package dnspod import ( "encoding/json" @@ -13,7 +13,7 @@ import ( "time" "github.com/TimothyYe/godns" - "github.com/bitly/go-simplejson" + simplejson "github.com/bitly/go-simplejson" "golang.org/x/net/proxy" ) @@ -68,7 +68,7 @@ func (handler *DNSPodHandler) DomainLoop(domain *godns.Domain, panicChan chan<- continue } - // Continue to check the IP of sub-domain + // Continue to check the IP of subdomain if len(ip) > 0 && strings.TrimRight(currentIP, "\n") != strings.TrimRight(ip, "\n") { log.Printf("%s.%s Start to update record IP...\n", subDomain, domain.DomainName) handler.UpdateIP(domainID, subDomainID, subDomain, currentIP) diff --git a/handler/handler.go b/handler/handler.go index e10ca78d..a6899df9 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -1,6 +1,12 @@ package handler -import "github.com/TimothyYe/godns" +import ( + "github.com/TimothyYe/godns" + "github.com/TimothyYe/godns/handler/alidns" + "github.com/TimothyYe/godns/handler/cloudflare" + "github.com/TimothyYe/godns/handler/dnspod" + "github.com/TimothyYe/godns/handler/he" +) // IHandler is the interface for all DNS handlers type IHandler interface { @@ -14,11 +20,13 @@ func CreateHandler(provider string) IHandler { switch provider { case godns.CLOUDFLARE: - handler = IHandler(&CloudflareHandler{}) + handler = IHandler(&cloudflare.CloudflareHandler{}) case godns.DNSPOD: - handler = IHandler(&DNSPodHandler{}) + handler = IHandler(&dnspod.DNSPodHandler{}) case godns.HE: - handler = IHandler(&HEHandler{}) + handler = IHandler(&he.HEHandler{}) + case godns.ALIDNS: + handler = IHandler(&alidns.AliDNSHandler{}) } return handler diff --git a/handler/he_handler.go b/handler/he/he_handler.go similarity index 99% rename from handler/he_handler.go rename to handler/he/he_handler.go index b558510d..e8b782a7 100644 --- a/handler/he_handler.go +++ b/handler/he/he_handler.go @@ -1,4 +1,4 @@ -package handler +package he import ( "fmt" diff --git a/utils.go b/utils.go index 4a003693..8962251a 100644 --- a/utils.go +++ b/utils.go @@ -11,7 +11,7 @@ import ( "strings" "golang.org/x/net/proxy" - "gopkg.in/gomail.v2" + gomail "gopkg.in/gomail.v2" ) var ( @@ -42,6 +42,8 @@ const ( HE = "HE" // CLOUDFLARE for cloudflare.com CLOUDFLARE = "Cloudflare" + // ALIDNS for AliDNS + ALIDNS = "AliDNS" ) //GetIPFromInterface gets IP address from the specific interface @@ -178,6 +180,13 @@ func CheckSettings(config *Settings) error { if config.Password == "" { return errors.New("password cannot be empty") } + } else if config.Provider == ALIDNS { + if config.Email == "" { + return errors.New("email cannot be empty") + } + if config.Password == "" { + return errors.New("password cannot be empty") + } } else { return errors.New("please provide supported DNS provider: DNSPod/HE") }