diff --git a/.gitignore b/.gitignore
index 1310df3..114cbf7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,10 +28,13 @@ _testmain.go
config.json
*.log
*.swp
-godns
+*.gz
+cmd/godns/godns
vendor/*
/.idea
/godns.iml
/godns.ipr
/godns.iws
+.current_ip
+.DS_Store
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
deleted file mode 100644
index 122cdfb..0000000
--- a/.gitlab-ci.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-before_script:
- - export GOPATH=$(pwd)
- - export GOBIN=$GOPATH/bin
-
-stages:
- - build
- - test
-
-build-my-project:
- image: golang:1.6.2
- stage: build
- script:
- - mkdir $GOPATH/bin
- - go get
- - go build
-
-test-my-project:
- image: golang:1.6.2
- stage: test
- script:
- - mkdir $GOPATH/bin
- - go get
- - go build
- - go test
diff --git a/.travis.yml b/.travis.yml
index 04220fe..9775110 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,13 @@
language: go
go:
- - 1.7
- - 1.8
+ - 1.7.x
+ - 1.8.x
+ - 1.9.x
+
+install:
+ - go get -v
+ - go get -v github.com/bitly/go-simplejson
+ - go get -v github.com/fatih/color
script:
- cp ./config_sample.json ./config.json
diff --git a/README.md b/README.md
index f7f4e16..49d5ad0 100644
--- a/README.md
+++ b/README.md
@@ -5,25 +5,52 @@
██║ ██║██║ ██║██║ ██║██║╚██╗██║╚════██║
╚██████╔╝╚██████╔╝██████╔╝██║ ╚████║███████║
╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
-
-Latest release: V1.1
```
-[](https://travis-ci.org/TimothyYe/godns)
+[![Release][7]][8] [![MIT licensed][9]][10] [![Build Status][1]][2] [![Docker][3]][4] [![Go Report Card][11]][12] [![Cover.Run][15]][16] [![GoDoc][13]][14]
-GoDNS is a dynamic DNS (DDNS) tool, it is based on my early open source project: [DynDNS](https://github.com/TimothyYe/DynDNS).
+[1]: https://travis-ci.org/TimothyYe/godns.svg?branch=master
+[2]: https://travis-ci.org/TimothyYe/godns
+[3]: https://images.microbadger.com/badges/image/timothyye/godns.svg
+[4]: https://microbadger.com/images/timothyye/godns
+[7]: http://github-release-version.herokuapp.com/github/timothyye/godns/release.svg?style=flat
+[8]: https://github.com/TimothyYe/godns/releases
+[9]: https://img.shields.io/badge/license-Apache-blue.svg
+[10]: LICENSE
+[11]: https://goreportcard.com/badge/github.com/timothyye/godns
+[12]: https://goreportcard.com/report/github.com/timothyye/godns
+[13]: https://godoc.org/github.com/TimothyYe/godns?status.svg
+[14]: https://godoc.org/github.com/TimothyYe/godns
+[15]: https://img.shields.io/badge/cover.run-88.2%25-green.svg
+[16]: https://cover.run/go/github.com/timothyye/godns
+
+GoDNS is a dynamic DNS (DDNS) client tool, it is based on my early open source project: [DynDNS](https://github.com/TimothyYe/DynDNS).
Now I rewrite [DynDNS](https://github.com/TimothyYe/DynDNS) by Golang and call it [GoDNS](https://github.com/TimothyYe/godns).
+## Supported DNS Provider
+* DNSPod ([https://www.dnspod.cn/](https://www.dnspod.cn/))
+* HE.net (Hurricane Electric) ([https://dns.he.net/](https://dns.he.net/))
+
+## Supported Platforms
+* Linux
+* MacOS
+* ARM Linux (Raspberry Pi, etc...)
+* Windows
+
+## MIPS32 platform
+
+For MIPS32 platform, please checkout the [mips32](https://github.com/TimothyYe/godns/tree/mips32) branch, this branch is contributed by [hguandl](https://github.com/hguandl), in this branch, the support for mips32 is added, which means it could run properly on Openwrt and LEDE.
+
## Pre-condition
-* GoDNS relies on [DNSPod](http://dnspod.cn) and its API.
+* Register and own a domain.
-* To use GoDNS, you need a domain hosted on [DNSPod](http://dnspod.cn).
+* Domain's nameserver points to [DNSPod](https://www.dnspod.cn/) or [HE.net](https://dns.he.net/).
-## Build it
+## Get it
-### Get & build it from source code
+### Build it from source code
* Get source code from Github:
@@ -33,11 +60,15 @@ git clone https://github.com/timothyye/godns.git
* Go into the godns directory, get related library and then build it:
```bash
-cd godns
-go get
+cd cmd/godns
+go get -v
go build
```
+### Download from releases
+
+Download compiled binaries from [releases](https://github.com/TimothyYe/godns/releases)
+
## Get help
```bash
@@ -53,11 +84,100 @@ 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 domain/sub-domain info, username and password of DNSPod account.
-* Configure log file path, max size of log file, max count of log file.
-* Configure user id, group id for safety.
+* Configure your provider, domain/sub-domain 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.
+### Config example for DNSPod
+
+For DNSPod, you need to provide email & password, and config all the domains & subdomains.
+
+```json
+{
+ "provider": "DNSPod",
+ "email": "example@gmail.com",
+ "password": "YourPassword",
+ "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",
+ "log_path": "./godns.log",
+ "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.
+
+```json
+{
+ "provider": "HE",
+ "email": "",
+ "password": "YourPassword",
+ "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",
+ "log_path":"/users/timothy/workspace/src/godns/godns.log",
+ "socks5_proxy": ""
+}
+```
+
+### HE.net DDNS configuration
+
+Add a new "A record", make sure that "Enable entry for dynamic dns" is checked:
+
+
+
+Fill your own DDNS key or generate a random DDNS key for this new created "A record":
+
+
+
+Remember the DDNS key and fill it as password to the config.json.
+
+__NOTICE__: If you have multiple domains or subdomains, make sure their DDNS key are the same.
+
+### Email notification support
+
+Update config file and provide your SMTP options, a notification mail will be sent to your mailbox once the IP is changed and updated.
+
+```json
+ "notify": {
+ "enabled": true,
+ "smtp_server": "smtp.example.com",
+ "smtp_username": "user",
+ "smtp_password": "password",
+ "smtp_port": 25,
+ "send_to": "my_mail@example.com"
+ }
+```
+
+Notification mail example:
+
+
+
+### SOCKS5 proxy support
+
+You can also use SOCKS5 proxy, just fill SOCKS5 address to the ```socks5_proxy``` item:
+
+```json
+"socks5_proxy": "127.0.0.1:7070"
+```
+
+Now all the queries will go through the specified SOCKS5 proxy.
+
## Run it as a daemon manually
```bash
@@ -85,24 +205,17 @@ sudo systemctl enable godns
sudo systemctl start godns
```
-## Run it in docker
+## Run it with docker
Now godns supports to run in docker.
-* Pull godns image from docker hub:
-```bash
-docker pull timothyye/godns:1.0
-```
-
-* Run godns in container and pass config parameters to it via enviroment variables:
+* Get [config_sample.json](https://github.com/timothyye/godns/blob/master/config_sample.json) from Github.
+* Rename it to **config.json**.
+* Run GoDNS with docker:
```bash
docker run -d --name godns --restart=always \
--e EMAIL=your_dnspod_account \
--e PASSWORD=your_dnspod_password \
--e DOMAINS="your_domain1,your_domain2" DOCKER_IMAGE_ID
+-v /path/to/config.json:/usr/local/godns/config.json timothyye/godns:latest
```
-
-
## Enjoy it!
diff --git a/Dockerfile b/cmd/godns/Dockerfile
similarity index 68%
rename from Dockerfile
rename to cmd/godns/Dockerfile
index 5d5bb94..5fe2664 100644
--- a/Dockerfile
+++ b/cmd/godns/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:latest
+FROM timothyye/alpine:3.6-glibc
MAINTAINER Timothy
RUN apk add --update ca-certificates
RUN mkdir -p /usr/local/godns
@@ -6,4 +6,4 @@ COPY godns /usr/local/godns
RUN chmod +x /usr/local/godns/godns
RUN rm -rf /var/cache/apk/*
WORKDIR /usr/local/godns
-ENTRYPOINT ["./godns", "-d"]
+ENTRYPOINT ["./godns", "-c", "/usr/local/godns/config.json"]
diff --git a/cmd/godns/Makefile b/cmd/godns/Makefile
new file mode 100644
index 0000000..274156f
--- /dev/null
+++ b/cmd/godns/Makefile
@@ -0,0 +1,34 @@
+# Binary name
+BINARY=godns
+# Builds the project
+build:
+ go build -o ${BINARY} -ldflags "-X main.Version=${VERSION}"
+# Installs our project: copies binaries
+install:
+ go install
+release:
+ # Clean
+ go clean
+ rm -rf *.gz
+ # Build for mac
+ go build -o ${BINARY} -ldflags "-X main.Version=${VERSION}"
+ tar czvf ${BINARY}-mac64-${VERSION}.tar.gz ./${BINARY}
+ # Build for linux
+ go clean
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BINARY} -ldflags "-X main.Version=${VERSION}"
+ tar czvf ${BINARY}-linux64-${VERSION}.tar.gz ./${BINARY}
+ # Build for arm
+ go clean
+ CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o ${BINARY} -ldflags "-X main.Version=${VERSION}"
+ tar czvf ${BINARY}-arm64-${VERSION}.tar.gz ./${BINARY}
+ # Build for win
+ go clean
+ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ${BINARY}.exe -ldflags "-X main.Version=${VERSION}"
+ tar czvf ${BINARY}-win64-${VERSION}.tar.gz ./${BINARY}.exe
+ go clean
+# Cleans our projects: deletes binaries
+clean:
+ go clean
+ rm -rf *.gz
+
+.PHONY: clean build
diff --git a/cmd/godns/godns.go b/cmd/godns/godns.go
new file mode 100644
index 0000000..6cc795b
--- /dev/null
+++ b/cmd/godns/godns.go
@@ -0,0 +1,70 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "os"
+
+ "log"
+
+ "github.com/TimothyYe/godns"
+ "github.com/TimothyYe/godns/handler"
+ "github.com/fatih/color"
+)
+
+var (
+ configuration godns.Settings
+ optConf = flag.String("c", "./config.json", "Specify a config file")
+ optHelp = flag.Bool("h", false, "Show help")
+
+ // Version is current version of GoDNS
+ Version = "0.1"
+)
+
+func main() {
+ flag.Parse()
+ if *optHelp {
+ color.Cyan(godns.Logo, Version)
+ flag.Usage()
+ return
+ }
+
+ // Load settings from configurations file
+ if err := godns.LoadSettings(*optConf, &configuration); err != nil {
+ fmt.Println(err.Error())
+ os.Exit(1)
+ }
+
+ if err := godns.CheckSettings(&configuration); err != nil {
+ fmt.Println("Settings is invalid! ", err.Error())
+ os.Exit(1)
+ }
+
+ // Init log settings
+ log.SetPrefix("【GoDNS】")
+ log.Println("GoDNS started, entering main loop...")
+ dnsLoop()
+}
+
+func dnsLoop() {
+ panicChan := make(chan godns.Domain)
+
+ log.Println("Creating DNS handler with provider:", configuration.Provider)
+ handler := handler.CreateHandler(configuration.Provider)
+ handler.SetConfiguration(&configuration)
+ for i, _ := range configuration.Domains {
+ go handler.DomainLoop(&configuration.Domains[i], panicChan)
+ }
+
+ panicCount := 0
+ for {
+ failDomain := <-panicChan
+ log.Println("Got panic in goroutine, will start a new one... :", panicCount)
+ go handler.DomainLoop(&failDomain, panicChan)
+
+ panicCount++
+ if panicCount >= godns.PanicMax {
+ os.Exit(1)
+ }
+ }
+}
diff --git a/config_sample.json b/config_sample.json
index 3e1f521..7553c94 100644
--- a/config_sample.json
+++ b/config_sample.json
@@ -1,17 +1,32 @@
{
+ "provider": "DNSPod",
"email": "example@gmail.com",
"password": "",
"login_token": "",
- "domains": [{
- "domain_name":"example.com",
- "sub_domains":["www","test"]
- },{
- "domain_name":"example2.com",
- "sub_domains":["www","test"]
+ "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",
- "log_path":"./godns.log",
- "log_size":16,
- "log_num":3
-}
+ "socks5_proxy": "",
+ "notify": {
+ "enabled": false,
+ "smtp_server": "",
+ "smtp_username": "",
+ "smtp_password": "",
+ "smtp_port": 25,
+ "send_to": ""
+ }
+}
\ No newline at end of file
diff --git a/dns_handler.go b/dns_handler.go
deleted file mode 100644
index 5bef4ca..0000000
--- a/dns_handler.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "log"
- "net/http"
- "net/url"
- "strconv"
- "strings"
- "golang.org/x/net/proxy"
- "github.com/bitly/go-simplejson"
-)
-
-func getCurrentIP(url string) (string, error) {
- response, err := http.Get(url)
-
- if err != nil {
- log.Println("Cannot get IP...")
- return "", err
- }
-
- defer response.Body.Close()
-
- body, _ := ioutil.ReadAll(response.Body)
- return string(body), nil
-}
-
-func generateHeader(content url.Values) url.Values {
- header := url.Values{}
- if configuration.LoginToken != "" {
- header.Add("login_token", configuration.LoginToken)
- } else {
- header.Add("login_email", configuration.Email)
- header.Add("login_password", configuration.Password)
- }
- header.Add("format", "json")
- header.Add("lang", "en")
- header.Add("error_on_empty", "no")
-
- if content != nil {
- for k, _ := range content {
- header.Add(k, content.Get(k))
- }
- }
-
- return header
-}
-
-func apiVersion() {
- postData("/Info.Version", nil)
-}
-
-func getDomain(name string) int64 {
-
- var ret int64
- values := url.Values{}
- values.Add("type", "all")
- values.Add("offset", "0")
- values.Add("length", "20")
-
- response, err := postData("/Domain.List", values)
-
- if err != nil {
- log.Println("Failed to get domain list...")
- return -1
- }
-
- sjson, parseErr := simplejson.NewJson([]byte(response))
-
- if parseErr != nil {
- log.Println(parseErr)
- return -1
- }
-
- if sjson.Get("status").Get("code").MustString() == "1" {
- domains, _ := sjson.Get("domains").Array()
-
- for _, d := range domains {
- m := d.(map[string]interface{})
- if m["name"] == name {
- id := m["id"]
-
- switch t := id.(type) {
- case json.Number:
- ret, _ = t.Int64()
- }
-
- break
- }
- }
- if len(domains) == 0 {
- log.Println("domains slice is empty.")
- }
- } else {
- log.Println("get_domain:status code:", sjson.Get("status").Get("code").MustString())
- }
-
- return ret
-}
-
-func getSubDomain(domainID int64, name string) (string, string) {
- log.Println("debug:", domainID, name)
- var ret, ip string
- value := url.Values{}
- value.Add("domain_id", strconv.FormatInt(domainID, 10))
- value.Add("offset", "0")
- value.Add("length", "1")
- value.Add("sub_domain", name)
-
- response, err := postData("/Record.List", value)
-
- if err != nil {
- log.Println("Failed to get domain list")
- return "", ""
- }
-
- sjson, parseErr := simplejson.NewJson([]byte(response))
-
- if parseErr != nil {
- log.Println(parseErr)
- return "", ""
- }
-
- if sjson.Get("status").Get("code").MustString() == "1" {
- records, _ := sjson.Get("records").Array()
-
- for _, d := range records {
- m := d.(map[string]interface{})
- if m["name"] == name {
- ret = m["id"].(string)
- ip = m["value"].(string)
- break
- }
- }
- if len(records) == 0 {
- log.Println("records slice is empty.")
- }
- } else {
- log.Println("get_subdomain:status code:", sjson.Get("status").Get("code").MustString())
- }
-
- return ret, ip
-}
-
-func updateIP(domainID int64, subDomainID string, subDomainName string, ip string) {
- value := url.Values{}
- value.Add("domain_id", strconv.FormatInt(domainID, 10))
- value.Add("record_id", subDomainID)
- value.Add("sub_domain", subDomainName)
- value.Add("record_type", "A")
- value.Add("record_line", "默认")
- value.Add("value", ip)
-
- response, err := postData("/Record.Modify", value)
-
- if err != nil {
- log.Println("Failed to update record to new IP!")
- log.Println(err)
- return
- }
-
- sjson, parseErr := simplejson.NewJson([]byte(response))
-
- if parseErr != nil {
- log.Println(parseErr)
- return
- }
-
- if sjson.Get("status").Get("code").MustString() == "1" {
- log.Println("New IP updated!")
- }
-
-}
-
-func postData(url string, content url.Values) (string, error) {
- client := &http.Client{}
-
- if configuration.Socks5Proxy != "" {
-
- log.Println("use socks5 proxy:" + configuration.Socks5Proxy)
-
- dialer, err := proxy.SOCKS5("tcp", configuration.Socks5Proxy, nil, proxy.Direct)
- if err != nil {
- fmt.Println("can't connect to the proxy:", err)
- return "", err
- }
-
- httpTransport := &http.Transport{}
- client.Transport = httpTransport
- httpTransport.Dial = dialer.Dial
- }
-
- values := generateHeader(content)
- req, _ := http.NewRequest("POST", "https://dnsapi.cn" + url, strings.NewReader(values.Encode()))
-
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- req.Header.Set("User-Agent", fmt.Sprintf("GoDNS/0.1 (%s)", configuration.Email))
-
- response, err := client.Do(req)
-
- if err != nil {
- log.Println("Post failed...")
- log.Println(err)
- return "", err
- }
-
- defer response.Body.Close()
- resp, _ := ioutil.ReadAll(response.Body)
-
- return string(resp), nil
-}
diff --git a/dns_handler_test.go b/dns_handler_test.go
deleted file mode 100644
index 21f3ee1..0000000
--- a/dns_handler_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package main
-
-import (
- "testing"
-)
-
-func testGetCurrentIP(t *testing.T) {
- ip, _ := getCurrentIP("http://members.3322.org/dyndns/getip")
-
- if ip == "" {
- t.Log("IP is empty...")
- } else {
- t.Log("IP is:" + ip)
- }
-}
diff --git a/godns.go b/godns.go
deleted file mode 100644
index 3af621d..0000000
--- a/godns.go
+++ /dev/null
@@ -1,134 +0,0 @@
-package main
-
-import (
- "flag"
- "fmt"
- "log"
- "os"
- "runtime/debug"
- "strings"
- "time"
-)
-
-const (
- PANIC_MAX = 5
- INTERVAL = 5 //Minute
-)
-
-var (
- configuration Settings
- optConf = flag.String("c", "./config.json", "Specify a config file")
- optDocker = flag.Bool("d", false, "Run it as docker mode")
- optHelp = flag.Bool("h", false, "Show help")
- panicCount = 0
-)
-
-func main() {
- flag.Parse()
- if *optHelp {
- flag.Usage()
- return
- }
-
- if *optDocker {
- //Load settings from ENV
- configuration = Settings{
- Email: os.Getenv("EMAIL"),
- Password: os.Getenv("PASSWORD"),
- LoginToken: os.Getenv("TOKEN"),
- IPUrl: "http://members.3322.org/dyndns/getip",
- LogPath: "./godns.log",
- LogSize: 16,
- LogNum: 3,
- }
-
- if err := LoadDomains(os.Getenv("DOMAINS"), &configuration.Domains); err != nil {
- fmt.Println(err.Error())
- log.Println(err.Error())
- os.Exit(1)
- }
- } else {
- //Load settings from configurations file
- if err := LoadSettings(*optConf, &configuration); err != nil {
- fmt.Println(err.Error())
- log.Println(err.Error())
- os.Exit(1)
- }
- }
-
- if err := checkSettings(&configuration); err != nil {
- log.Println("Settings is invalid! ", err.Error())
- os.Exit(1)
- }
-
- if err := InitLogger(configuration.LogPath, configuration.LogSize, configuration.LogNum); err != nil {
- log.Println("InitLogger error:", err.Error())
- os.Exit(1)
- }
-
- dnsLoop()
-}
-
-func dnsLoop() {
-
- for _, domain := range configuration.Domains {
- go DomainLoop(&domain)
- }
-
- select {}
-}
-
-func DomainLoop(domain *Domain) {
- defer func() {
- if err := recover(); err != nil {
- panicCount++
- log.Printf("Recovered in %v: %v\n", err, debug.Stack())
- fmt.Println(identifyPanic())
- log.Print(identifyPanic())
- if panicCount < PANIC_MAX {
- log.Println("Got panic in goroutine, will start a new one... :", panicCount)
- go DomainLoop(domain)
- } else {
- os.Exit(1)
- }
- }
- }()
-
- for {
-
- domainID := getDomain(domain.DomainName)
-
- if domainID == -1 {
- continue
- }
-
- currentIP, err := getCurrentIP(configuration.IPUrl)
-
- if err != nil {
- log.Println("get_currentIP:", err)
- continue
- }
- log.Println("currentIp is:", currentIP)
-
- for _, subDomain := range domain.SubDomains {
-
- subDomainID, ip := getSubDomain(domainID, subDomain)
-
- if subDomainID == "" || ip == "" {
- log.Printf("domain: %s.%s subDomainID: %s ip: %s\n", subDomain, domain.DomainName, subDomainID, ip)
- continue
- }
-
- //Continue to check the IP of sub-domain
- if len(ip) > 0 && !strings.Contains(currentIP, ip) {
- log.Printf("%s.%s Start to update record IP...\n", subDomain, domain.DomainName)
- updateIP(domainID, subDomainID, subDomain, currentIP)
- } else {
- log.Printf("%s.%s Current IP is same as domain IP, no need to update...\n", subDomain, domain.DomainName)
- }
- }
-
- //Interval is 5 minutes
- time.Sleep(time.Minute * INTERVAL)
- }
-}
diff --git a/handler/dnspod_handler.go b/handler/dnspod_handler.go
new file mode 100644
index 0000000..06a4381
--- /dev/null
+++ b/handler/dnspod_handler.go
@@ -0,0 +1,270 @@
+package handler
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "runtime/debug"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/TimothyYe/godns"
+ "github.com/bitly/go-simplejson"
+ "golang.org/x/net/proxy"
+)
+
+// DNSPodHandler struct definition
+type DNSPodHandler struct {
+ Configuration *godns.Settings
+}
+
+// SetConfiguration pass dns settings and store it to handler instance
+func (handler *DNSPodHandler) SetConfiguration(conf *godns.Settings) {
+ handler.Configuration = conf
+}
+
+// DomainLoop the main logic loop
+func (handler *DNSPodHandler) 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
+ }
+ }()
+
+ for {
+ log.Printf("Checking IP for domain %s \r\n", domain.DomainName)
+ domainID := handler.GetDomain(domain.DomainName)
+
+ if domainID == -1 {
+ continue
+ }
+
+ currentIP, err := godns.GetCurrentIP(handler.Configuration)
+
+ if err != nil {
+ log.Println("get_currentIP:", err)
+ continue
+ }
+ log.Println("currentIP is:", currentIP)
+
+ for _, subDomain := range domain.SubDomains {
+
+ subDomainID, ip := handler.GetSubDomain(domainID, subDomain)
+
+ if subDomainID == "" || ip == "" {
+ log.Printf("domain: %s.%s subDomainID: %s ip: %s\n", subDomain, domain.DomainName, subDomainID, ip)
+ continue
+ }
+
+ // Continue to check the IP of sub-domain
+ 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)
+
+ // 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)
+ }
+
+ } else {
+ log.Printf("%s.%s Current IP is same as domain IP, no need to update...\n", subDomain, domain.DomainName)
+ }
+ }
+
+ // 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)
+ }
+}
+
+// GenerateHeader generates the request header for DNSPod API
+func (handler *DNSPodHandler) GenerateHeader(content url.Values) url.Values {
+ header := url.Values{}
+ if handler.Configuration.LoginToken != "" {
+ header.Add("login_token", handler.Configuration.LoginToken)
+ } else {
+ header.Add("login_email", handler.Configuration.Email)
+ header.Add("login_password", handler.Configuration.Password)
+ }
+ header.Add("format", "json")
+ header.Add("lang", "en")
+ header.Add("error_on_empty", "no")
+
+ if content != nil {
+ for k := range content {
+ header.Add(k, content.Get(k))
+ }
+ }
+
+ return header
+}
+
+// GetDomain returns specific domain by name
+func (handler *DNSPodHandler) GetDomain(name string) int64 {
+
+ var ret int64
+ values := url.Values{}
+ values.Add("type", "all")
+ values.Add("offset", "0")
+ values.Add("length", "20")
+
+ response, err := handler.PostData("/Domain.List", values)
+
+ if err != nil {
+ log.Println("Failed to get domain list...")
+ return -1
+ }
+
+ sjson, parseErr := simplejson.NewJson([]byte(response))
+
+ if parseErr != nil {
+ log.Println(parseErr)
+ return -1
+ }
+
+ if sjson.Get("status").Get("code").MustString() == "1" {
+ domains, _ := sjson.Get("domains").Array()
+
+ for _, d := range domains {
+ m := d.(map[string]interface{})
+ if m["name"] == name {
+ id := m["id"]
+
+ switch t := id.(type) {
+ case json.Number:
+ ret, _ = t.Int64()
+ }
+
+ break
+ }
+ }
+ if len(domains) == 0 {
+ log.Println("domains slice is empty.")
+ }
+ } else {
+ log.Println("get_domain:status code:", sjson.Get("status").Get("code").MustString())
+ }
+
+ return ret
+}
+
+// GetSubDomain returns subdomain by domain id
+func (handler *DNSPodHandler) GetSubDomain(domainID int64, name string) (string, string) {
+ log.Println("debug:", domainID, name)
+ var ret, ip string
+ value := url.Values{}
+ value.Add("domain_id", strconv.FormatInt(domainID, 10))
+ value.Add("offset", "0")
+ value.Add("length", "1")
+ value.Add("sub_domain", name)
+
+ response, err := handler.PostData("/Record.List", value)
+
+ if err != nil {
+ log.Println("Failed to get domain list")
+ return "", ""
+ }
+
+ sjson, parseErr := simplejson.NewJson([]byte(response))
+
+ if parseErr != nil {
+ log.Println(parseErr)
+ return "", ""
+ }
+
+ if sjson.Get("status").Get("code").MustString() == "1" {
+ records, _ := sjson.Get("records").Array()
+
+ for _, d := range records {
+ m := d.(map[string]interface{})
+ if m["name"] == name {
+ ret = m["id"].(string)
+ ip = m["value"].(string)
+ break
+ }
+ }
+ if len(records) == 0 {
+ log.Println("records slice is empty.")
+ }
+ } else {
+ log.Println("get_subdomain:status code:", sjson.Get("status").Get("code").MustString())
+ }
+
+ return ret, ip
+}
+
+// UpdateIP update subdomain with current IP
+func (handler *DNSPodHandler) UpdateIP(domainID int64, subDomainID string, subDomainName string, ip string) {
+ value := url.Values{}
+ value.Add("domain_id", strconv.FormatInt(domainID, 10))
+ value.Add("record_id", subDomainID)
+ value.Add("sub_domain", subDomainName)
+ value.Add("record_type", "A")
+ value.Add("record_line", "默认")
+ value.Add("value", ip)
+
+ response, err := handler.PostData("/Record.Modify", value)
+
+ if err != nil {
+ log.Println("Failed to update record to new IP!")
+ log.Println(err)
+ return
+ }
+
+ sjson, parseErr := simplejson.NewJson([]byte(response))
+
+ if parseErr != nil {
+ log.Println(parseErr)
+ return
+ }
+
+ if sjson.Get("status").Get("code").MustString() == "1" {
+ log.Println("New IP updated!")
+ }
+
+}
+
+// PostData post data and invoke DNSPod API
+func (handler *DNSPodHandler) PostData(url string, content url.Values) (string, error) {
+ client := &http.Client{}
+
+ if handler.Configuration.Socks5Proxy != "" {
+
+ log.Println("use socks5 proxy:" + handler.Configuration.Socks5Proxy)
+
+ dialer, err := proxy.SOCKS5("tcp", handler.Configuration.Socks5Proxy, nil, proxy.Direct)
+ if err != nil {
+ fmt.Println("can't connect to the proxy:", err)
+ return "", err
+ }
+
+ httpTransport := &http.Transport{}
+ client.Transport = httpTransport
+ httpTransport.Dial = dialer.Dial
+ }
+
+ values := handler.GenerateHeader(content)
+ req, _ := http.NewRequest("POST", "https://dnsapi.cn"+url, strings.NewReader(values.Encode()))
+
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.Header.Set("User-Agent", fmt.Sprintf("GoDNS/0.1 (%s)", handler.Configuration.Email))
+
+ response, err := client.Do(req)
+
+ if err != nil {
+ log.Println("Post failed...")
+ log.Println(err)
+ return "", err
+ }
+
+ defer response.Body.Close()
+ resp, _ := ioutil.ReadAll(response.Body)
+
+ return string(resp), nil
+}
diff --git a/handler/handler.go b/handler/handler.go
new file mode 100644
index 0000000..1bc202e
--- /dev/null
+++ b/handler/handler.go
@@ -0,0 +1,23 @@
+package handler
+
+import "github.com/TimothyYe/godns"
+
+// IHandler is the interface for all DNS handlers
+type IHandler interface {
+ SetConfiguration(*godns.Settings)
+ DomainLoop(domain *godns.Domain, panicChan chan<- godns.Domain)
+}
+
+// CreateHandler creates dns handler by different providers
+func CreateHandler(provider string) IHandler {
+ var handler IHandler
+
+ switch provider {
+ case godns.DNSPOD:
+ handler = IHandler(&DNSPodHandler{})
+ case godns.HE:
+ handler = IHandler(&HEHandler{})
+ }
+
+ return handler
+}
diff --git a/handler/he_handler.go b/handler/he_handler.go
new file mode 100644
index 0000000..c34e02c
--- /dev/null
+++ b/handler/he_handler.go
@@ -0,0 +1,104 @@
+package handler
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/url"
+ "runtime/debug"
+ "strings"
+ "time"
+
+ "github.com/TimothyYe/godns"
+
+ "golang.org/x/net/proxy"
+)
+
+var (
+ // HEUrl the API address for he.net
+ HEUrl = "https://dyn.dns.he.net/nic/update"
+)
+
+// HEHandler struct
+type HEHandler struct {
+ Configuration *godns.Settings
+}
+
+// SetConfiguration pass dns settings and store it to handler instance
+func (handler *HEHandler) SetConfiguration(conf *godns.Settings) {
+ handler.Configuration = conf
+}
+
+// DomainLoop the main logic loop
+func (handler *HEHandler) 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
+ }
+ }()
+
+ for {
+ currentIP, err := godns.GetCurrentIP(handler.Configuration)
+
+ if err != nil {
+ log.Println("get_currentIP:", err)
+ continue
+ }
+ log.Println("currentIP is:", currentIP)
+
+ for _, subDomain := range domain.SubDomains {
+ log.Printf("%s.%s Start to update record IP...\n", subDomain, domain.DomainName)
+ handler.UpdateIP(domain.DomainName, subDomain, currentIP)
+
+ // 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)
+ }
+}
+
+// UpdateIP update subdomain with current IP
+func (handler *HEHandler) UpdateIP(domain, subDomain, currentIP string) {
+ values := url.Values{}
+ values.Add("hostname", fmt.Sprintf("%s.%s", subDomain, domain))
+ values.Add("password", handler.Configuration.Password)
+ values.Add("myip", currentIP)
+
+ client := &http.Client{}
+
+ if handler.Configuration.Socks5Proxy != "" {
+ log.Println("use socks5 proxy:" + handler.Configuration.Socks5Proxy)
+ dialer, err := proxy.SOCKS5("tcp", handler.Configuration.Socks5Proxy, nil, proxy.Direct)
+ if err != nil {
+ log.Println("can't connect to the proxy:", err)
+ return
+ }
+
+ httpTransport := &http.Transport{}
+ client.Transport = httpTransport
+ httpTransport.Dial = dialer.Dial
+ }
+
+ req, _ := http.NewRequest("POST", HEUrl, strings.NewReader(values.Encode()))
+ resp, err := client.Do(req)
+
+ if err != nil {
+ log.Println("Request error...")
+ log.Println("Err:", err.Error())
+ } else {
+ body, _ := ioutil.ReadAll(resp.Body)
+ if resp.StatusCode == http.StatusOK {
+ log.Println("Update IP success:", string(body))
+ } else {
+ log.Println("Update IP failed:", string(body))
+ }
+ }
+}
diff --git a/logger.go b/logger.go
deleted file mode 100644
index a6474c1..0000000
--- a/logger.go
+++ /dev/null
@@ -1,292 +0,0 @@
-package main
-
-import (
- "bufio"
- "bytes"
- "errors"
- "io"
- "io/ioutil"
- "log"
- "os"
- "runtime/debug"
- "strconv"
- "strings"
- "sync"
- "time"
-)
-
-const (
- L_INFO int = iota
- L_WARNING
- L_DEBUG
- PRE_INFO = "[ INFO]"
- PRE_WARNING = "[WARNING]"
- PRE_DEBUG = "[ DEBUG]"
-)
-
-type Logger struct {
- DEV_MODE bool
- fd *os.File
- size int
- num int
- level int
- mu sync.Mutex
- muSplit sync.Mutex
- flushInterval int64 //Second
- flushSize int
- buf *bytes.Buffer
- log *log.Logger
-}
-
-func NewLogger(logfile string, size, num int, level int, flushInterval int64, flushSize int) (logger *Logger, err error) {
- if size < 1 || num < 1 || level < L_INFO || len(logfile) < 1 {
- err = errors.New("NewLogWriter:param error.")
- return
- }
- logger = &Logger{size: size * 1024, num: num, level: level, DEV_MODE: false}
- logger.fd, err = os.OpenFile(logfile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModeAppend|0666)
- if err != nil {
- logger = nil
- return
- }
- log.SetOutput(logger)
- if flushInterval > 0 && flushSize > 0 {
- logger.buf = new(bytes.Buffer)
- logger.log = log.New(logger.buf, "", log.LstdFlags)
-
- go func(interval int64, logger *Logger) {
- defer func() {
- if r := recover(); r != nil {
- log.Printf("logger Tick, Recovered in %v:\n %s", r, debug.Stack())
- }
- }()
- c := time.Tick(time.Duration(interval) * time.Second)
- for _ = range c {
- logger.Flush()
- }
- }(flushInterval, logger)
- }
- return
-}
-
-func InitLogger(logfile string, size, num int) (err error) {
- logger, err := NewLogger(logfile, size, num, L_INFO, -1, -1)
- if logger != nil {
- logger.level = L_INFO - 1
- }
- return
-}
-
-//immplement write
-func (this *Logger) Write(p []byte) (n int, err error) {
- if this.DEV_MODE {
- n, err = os.Stdout.Write(p)
- return
- }
- n, err = this.fd.Write(p)
- if err == nil {
- fi, e := this.fd.Stat()
- if e != nil {
- err = e
- return
- }
- if fi.Size() > int64(this.size) {
- this.muSplit.Lock()
- defer this.muSplit.Unlock()
-
- fname := fi.Name()
- strings.HasSuffix(fname, ".log")
- fbase := fname[:len(fname)-3]
-
- oldBs := make([]byte, 0, this.size)
- newBs := []byte{}
- fd, e := os.Open(fname)
- if e != nil {
- err = e
- return
- }
- rd := bufio.NewReader(fd)
- for {
- line, e := rd.ReadBytes('\n')
- if e == io.EOF {
- break
- }
- if e != nil {
- err = e
- return
- }
- if len(oldBs)+len(line) > this.size {
- newBs = append(newBs, line...)
- } else {
- oldBs = append(oldBs, line...)
- }
- }
- fd.Close()
-
- _, err = this.saveLog(1, fbase, oldBs)
- if err != nil {
- return
- }
- err = this.fd.Close()
- if err != nil {
- return
- }
- err = os.Remove(fname)
- if err != nil {
- return
- }
- this.fd, err = os.OpenFile(fname, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModeAppend|0666)
- if err != nil {
- return
- }
- _, err = this.fd.Write(newBs)
- if err != nil {
- return
- }
- }
- }
- return
-}
-
-func (this *Logger) saveLog(index int, fbase string, data []byte) (n int, err error) {
- fn := fbase + strconv.Itoa(index) + ".log"
- _, err = os.Stat(fn)
- if index < this.num && err == nil {
- var b []byte
- b, err = ioutil.ReadFile(fn)
- if err != nil {
- return
- }
- n, err = this.saveLog(index+1, fbase, b)
- if err != nil {
- return
- }
- }
-
- fd, err := os.OpenFile(fn, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, os.ModePerm|0666)
- if err != nil {
- return
- }
- defer fd.Close()
- n, err = fd.Write(data)
- return
-}
-
-//flush buf data to std log
-func (this *Logger) Flush() {
- if this.buf.Len() > 0 {
- this.mu.Lock()
- defer this.mu.Unlock()
-
- log.SetFlags(0)
- log.Print(this.buf)
- log.SetFlags(log.LstdFlags)
- this.buf.Reset()
- }
-}
-
-//clean prefix and check buf size
-func (this *Logger) clean() {
- this.log.SetPrefix("")
- if this.buf.Len()/1024 > this.flushSize {
- go this.Flush()
- }
-}
-
-func (this *Logger) setPrefix(lv int) bool {
- if lv > this.level {
- return false
- }
-
- switch lv {
- case L_INFO:
- this.log.SetPrefix(PRE_INFO)
- case L_WARNING:
- this.log.SetPrefix(PRE_WARNING)
- case L_DEBUG:
- this.log.SetPrefix(PRE_DEBUG)
- default:
- return false
- }
- return true
-}
-
-func (this *Logger) logPrint(lv int, args ...interface{}) {
- this.mu.Lock()
- defer this.mu.Unlock()
-
- if !this.setPrefix(lv) {
- return
- }
- this.log.Print(args...)
- this.clean()
-}
-
-func (this *Logger) logPrintln(lv int, args ...interface{}) {
- this.mu.Lock()
- defer this.mu.Unlock()
-
- if !this.setPrefix(lv) {
- return
- }
- this.log.Println(args...)
- this.clean()
-}
-
-func (this *Logger) logPrintf(lv int, format string, args ...interface{}) {
- this.mu.Lock()
- defer this.mu.Unlock()
-
- if !this.setPrefix(lv) {
- return
- }
- this.log.Printf(format, args...)
- this.clean()
-}
-
-//close fd
-func (this *Logger) Close() {
- if this.fd != nil {
- this.Flush()
- this.fd.Close()
- }
-}
-
-func (this *Logger) Info(args ...interface{}) {
- this.logPrint(L_INFO, args...)
-}
-
-func (this *Logger) Infoln(args ...interface{}) {
- this.logPrintln(L_INFO, args...)
-}
-
-func (this *Logger) Infof(format string, args ...interface{}) {
- this.logPrintf(L_INFO, format, args...)
-}
-
-func (this *Logger) Warning(args ...interface{}) {
- this.logPrint(L_WARNING, args...)
-}
-
-func (this *Logger) Warningln(args ...interface{}) {
- this.logPrintln(L_WARNING, args...)
-}
-
-func (this *Logger) Warningf(format string, args ...interface{}) {
- this.logPrintf(L_WARNING, format, args...)
-}
-
-func (this *Logger) Debug(args ...interface{}) {
- this.logPrint(L_DEBUG, args...)
- this.Flush()
-}
-
-func (this *Logger) Debugln(args ...interface{}) {
- this.logPrintln(L_DEBUG, args...)
- this.Flush()
-}
-
-func (this *Logger) Debugf(format string, args ...interface{}) {
- this.logPrintf(L_DEBUG, format, args...)
- this.Flush()
-}
diff --git a/settings.go b/settings.go
index 54b4936..c287628 100644
--- a/settings.go
+++ b/settings.go
@@ -1,33 +1,43 @@
-package main
+package godns
import (
"encoding/json"
"fmt"
"io/ioutil"
- "strings"
)
+// Domain struct
type Domain struct {
DomainName string `json:"domain_name"`
SubDomains []string `json:"sub_domains"`
}
-//Settings struct
-type Settings struct {
- Email string `json:"email"`
- Password string `json:"password"`
- LoginToken string `json:"login_token"`
- Domains []Domain `json:"domains"`
- IPUrl string `json:"ip_url"`
- LogPath string `json:"log_path"`
- LogSize int `json:"log_size"`
- LogNum int `json:"log_num"`
- Socks5Proxy string `json:"socks5_proxy"`
+// Notify struct for SMTP notification
+type Notify struct {
+ Enabled bool `json:"enabled"`
+ SMTPServer string `json:"smtp_server"`
+ SMTPUsername string `json:"smtp_username"`
+ SMTPPassword string `json:"smtp_password"`
+ SMTPPort int `json:"smtp_port"`
+ SendTo string `json:"send_to"`
}
-//LoadSettings -- Load settings from config file
+// Settings struct
+type Settings struct {
+ Provider string `json:"provider"`
+ Email string `json:"email"`
+ Password string `json:"password"`
+ LoginToken string `json:"login_token"`
+ Domains []Domain `json:"domains"`
+ IPUrl string `json:"ip_url"`
+ LogPath string `json:"log_path"`
+ Socks5Proxy string `json:"socks5_proxy"`
+ Notify Notify `json:"notify"`
+}
+
+// LoadSettings -- Load settings from config file
func LoadSettings(configPath string, settings *Settings) error {
- //LoadSettings from config file
+ // LoadSettings from config file
file, err := ioutil.ReadFile(configPath)
if err != nil {
fmt.Println("Error occurs while reading config file, please make sure config file exists!")
@@ -42,35 +52,3 @@ func LoadSettings(configPath string, settings *Settings) error {
return nil
}
-
-//LoadDomains -- Load domains from domains string
-func LoadDomains(domainsOrginStr string, domains *[]Domain) error {
-
- domainsMap := make(map[string]*Domain)
- domainsArray := strings.Split(domainsOrginStr, ",")
- for _, host := range domainsArray {
- dotCount := strings.Count(host, ".")
- if dotCount < 2 {
- continue
- }
- len := len(host)
- pos := strings.Index(host, ".")
- subDomain := host[0:pos]
- domainName := host[pos+1 : len]
-
- if d, exist := domainsMap[domainName]; exist {
- d.SubDomains = append(d.SubDomains, subDomain)
- } else {
- d := new(Domain)
- d.DomainName = domainName
- d.SubDomains = append(d.SubDomains, subDomain)
- domainsMap[domainName] = d
- }
- }
-
- for _, d := range domainsMap {
- *domains = append(*domains, *d)
- }
-
- return nil
-}
diff --git a/settings_test.go b/settings_test.go
index 3c9dd54..2f606f4 100644
--- a/settings_test.go
+++ b/settings_test.go
@@ -1,4 +1,4 @@
-package main
+package godns
import (
"testing"
@@ -13,6 +13,11 @@ func TestLoadSetting(t *testing.T) {
}
if settings.IPUrl == "" {
- t.Error("Cannot load ip_url from config file")
+ t.Error("cannot load ip_url from config file")
+ }
+
+ err = LoadSettings("./file/does/not/exists", &settings)
+ if err == nil {
+ t.Error("file doesn't exist, should return error")
}
}
diff --git a/snapshots/he1.png b/snapshots/he1.png
new file mode 100644
index 0000000..9dd40f3
Binary files /dev/null and b/snapshots/he1.png differ
diff --git a/snapshots/he2.png b/snapshots/he2.png
new file mode 100644
index 0000000..0163c96
Binary files /dev/null and b/snapshots/he2.png differ
diff --git a/snapshots/mail.png b/snapshots/mail.png
new file mode 100644
index 0000000..b3df9b0
Binary files /dev/null and b/snapshots/mail.png differ
diff --git a/snapshots/notify.png b/snapshots/notify.png
new file mode 100644
index 0000000..aaa5307
Binary files /dev/null and b/snapshots/notify.png differ
diff --git a/template.go b/template.go
new file mode 100644
index 0000000..9b66792
--- /dev/null
+++ b/template.go
@@ -0,0 +1,105 @@
+package godns
+
+var mailTemplate = `
+
+