1
0
mirror of https://github.com/taigrr/godns synced 2025-01-18 04:03:25 -08:00

Merge remote-tracking branch 'upstream/master'

This commit is contained in:
hguandl 2018-07-04 10:52:50 +08:00
commit 5344c3c393
24 changed files with 976 additions and 800 deletions

5
.gitignore vendored
View File

@ -28,10 +28,13 @@ _testmain.go
config.json config.json
*.log *.log
*.swp *.swp
godns *.gz
cmd/godns/godns
vendor/* vendor/*
/.idea /.idea
/godns.iml /godns.iml
/godns.ipr /godns.ipr
/godns.iws /godns.iws
.current_ip
.DS_Store

View File

@ -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

View File

@ -1,7 +1,13 @@
language: go language: go
go: go:
- 1.7 - 1.7.x
- 1.8 - 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: script:
- cp ./config_sample.json ./config.json - cp ./config_sample.json ./config.json

163
README.md
View File

@ -5,25 +5,52 @@
██║ ██║██║ ██║██║ ██║██║╚██╗██║╚════██║ ██║ ██║██║ ██║██║ ██║██║╚██╗██║╚════██║
╚██████╔╝╚██████╔╝██████╔╝██║ ╚████║███████║ ╚██████╔╝╚██████╔╝██████╔╝██║ ╚████║███████║
╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
Latest release: V1.1
``` ```
[![Build Status](https://travis-ci.org/TimothyYe/godns.svg?branch=master)](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). 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 ## 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: * 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: * Go into the godns directory, get related library and then build it:
```bash ```bash
cd godns cd cmd/godns
go get go get -v
go build go build
``` ```
### Download from releases
Download compiled binaries from [releases](https://github.com/TimothyYe/godns/releases)
## Get help ## Get help
```bash ```bash
@ -53,11 +84,100 @@ Usage of ./godns:
* Get [config_sample.json](https://github.com/timothyye/godns/blob/master/config_sample.json) from Github. * Get [config_sample.json](https://github.com/timothyye/godns/blob/master/config_sample.json) from Github.
* Rename it to **config.json**. * Rename it to **config.json**.
* Configure your domain/sub-domain info, username and password of DNSPod account. * Configure your provider, domain/sub-domain info, username and password, etc.
* Configure log file path, max size of log file, max count of log file. * Configure the SMTP options if you want, a mail notification will sent to your mailbox once the IP is changed.
* Configure user id, group id for safety.
* Save it in the same directory of GoDNS, or use -c=your_conf_path command. * 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:
<img src="https://github.com/TimothyYe/godns/blob/master/snapshots/he1.png?raw=true" width="640" />
Fill your own DDNS key or generate a random DDNS key for this new created "A record":
<img src="https://github.com/TimothyYe/godns/blob/master/snapshots/he2.png?raw=true" width="640" />
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:
<img src="https://github.com/TimothyYe/godns/blob/master/snapshots/mail.png?raw=true" />
### 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 ## Run it as a daemon manually
```bash ```bash
@ -85,24 +205,17 @@ sudo systemctl enable godns
sudo systemctl start godns sudo systemctl start godns
``` ```
## Run it in docker ## Run it with docker
Now godns supports to run in docker. Now godns supports to run in docker.
* Pull godns image from docker hub: * Get [config_sample.json](https://github.com/timothyye/godns/blob/master/config_sample.json) from Github.
```bash * Rename it to **config.json**.
docker pull timothyye/godns:1.0 * Run GoDNS with docker:
```
* Run godns in container and pass config parameters to it via enviroment variables:
```bash ```bash
docker run -d --name godns --restart=always \ docker run -d --name godns --restart=always \
-e EMAIL=your_dnspod_account \ -v /path/to/config.json:/usr/local/godns/config.json timothyye/godns:latest
-e PASSWORD=your_dnspod_password \
-e DOMAINS="your_domain1,your_domain2" DOCKER_IMAGE_ID
``` ```
## Enjoy it! ## Enjoy it!

View File

@ -1,4 +1,4 @@
FROM alpine:latest FROM timothyye/alpine:3.6-glibc
MAINTAINER Timothy MAINTAINER Timothy
RUN apk add --update ca-certificates RUN apk add --update ca-certificates
RUN mkdir -p /usr/local/godns RUN mkdir -p /usr/local/godns
@ -6,4 +6,4 @@ COPY godns /usr/local/godns
RUN chmod +x /usr/local/godns/godns RUN chmod +x /usr/local/godns/godns
RUN rm -rf /var/cache/apk/* RUN rm -rf /var/cache/apk/*
WORKDIR /usr/local/godns WORKDIR /usr/local/godns
ENTRYPOINT ["./godns", "-d"] ENTRYPOINT ["./godns", "-c", "/usr/local/godns/config.json"]

34
cmd/godns/Makefile Normal file
View File

@ -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

70
cmd/godns/godns.go Normal file
View File

@ -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)
}
}
}

View File

@ -1,17 +1,32 @@
{ {
"provider": "DNSPod",
"email": "example@gmail.com", "email": "example@gmail.com",
"password": "", "password": "",
"login_token": "", "login_token": "",
"domains": [{ "domains": [
"domain_name":"example.com", {
"sub_domains":["www","test"] "domain_name": "example.com",
},{ "sub_domains": [
"domain_name":"example2.com", "www",
"sub_domains":["www","test"] "test"
]
},
{
"domain_name": "example2.com",
"sub_domains": [
"www",
"test"
]
} }
], ],
"ip_url": "http://members.3322.org/dyndns/getip", "ip_url": "http://members.3322.org/dyndns/getip",
"log_path":"./godns.log", "socks5_proxy": "",
"log_size":16, "notify": {
"log_num":3 "enabled": false,
} "smtp_server": "",
"smtp_username": "",
"smtp_password": "",
"smtp_port": 25,
"send_to": ""
}
}

View File

@ -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
}

View File

@ -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)
}
}

134
godns.go
View File

@ -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)
}
}

270
handler/dnspod_handler.go Normal file
View File

@ -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
}

23
handler/handler.go Normal file
View File

@ -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
}

104
handler/he_handler.go Normal file
View File

@ -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))
}
}
}

292
logger.go
View File

@ -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()
}

View File

@ -1,33 +1,43 @@
package main package godns
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strings"
) )
// Domain struct
type Domain struct { type Domain struct {
DomainName string `json:"domain_name"` DomainName string `json:"domain_name"`
SubDomains []string `json:"sub_domains"` SubDomains []string `json:"sub_domains"`
} }
//Settings struct // Notify struct for SMTP notification
type Settings struct { type Notify struct {
Email string `json:"email"` Enabled bool `json:"enabled"`
Password string `json:"password"` SMTPServer string `json:"smtp_server"`
LoginToken string `json:"login_token"` SMTPUsername string `json:"smtp_username"`
Domains []Domain `json:"domains"` SMTPPassword string `json:"smtp_password"`
IPUrl string `json:"ip_url"` SMTPPort int `json:"smtp_port"`
LogPath string `json:"log_path"` SendTo string `json:"send_to"`
LogSize int `json:"log_size"`
LogNum int `json:"log_num"`
Socks5Proxy string `json:"socks5_proxy"`
} }
//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 { func LoadSettings(configPath string, settings *Settings) error {
//LoadSettings from config file // LoadSettings from config file
file, err := ioutil.ReadFile(configPath) file, err := ioutil.ReadFile(configPath)
if err != nil { if err != nil {
fmt.Println("Error occurs while reading config file, please make sure config file exists!") 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 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
}

View File

@ -1,4 +1,4 @@
package main package godns
import ( import (
"testing" "testing"
@ -13,6 +13,11 @@ func TestLoadSetting(t *testing.T) {
} }
if settings.IPUrl == "" { 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")
} }
} }

BIN
snapshots/he1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

BIN
snapshots/he2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

BIN
snapshots/mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
snapshots/notify.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

105
template.go Normal file
View File

@ -0,0 +1,105 @@
package godns
var mailTemplate = `
<html>
<body>
<div role="section">
<div style="background-color: #281557;">
<div class="layout one-col" style="Margin: 0 auto;max-width: 600px;min-width: 320px; width: 320px;width: calc(28000% - 167400px);overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner" style="border-collapse: collapse;display: table;width: 100%;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" role="presentation"><tr class="layout-full-width" style="background-color: #281557;"><td class="layout__edges">&nbsp;</td><td style="width: 600px" class="w560"><![endif]-->
<div class="column" style="max-width: 600px;min-width: 320px; width: 320px;width: calc(28000% - 167400px);text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;font-family: sans-serif;">
<div style="Margin-left: 20px;Margin-right: 20px;">
<div style="mso-line-height-rule: exactly;line-height: 10px;font-size: 1px;">&nbsp;</div>
</div>
</div>
<!--[if (mso)|(IE)]></td><td class="layout__edges">&nbsp;</td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="background-color: #281557;">
<div class="layout one-col" style="Margin: 0 auto;max-width: 600px;min-width: 320px; width: 320px;width: calc(28000% - 167400px);overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner" style="border-collapse: collapse;display: table;width: 100%;">
<!--[if (mso)|(IE)]><table width="100%" cellpadding="0" cellspacing="0" role="presentation"><tr class="layout-full-width" style="background-color: #281557;"><td class="layout__edges">&nbsp;</td><td style="width: 600px" class="w560"><![endif]-->
<div class="column" style="max-width: 600px;min-width: 320px; width: 320px;width: calc(28000% - 167400px);text-align: left;color: #8e959c;font-size: 14px;line-height: 21px;font-family: sans-serif;">
<div style="Margin-left: 20px;Margin-right: 20px;">
<div style="mso-line-height-rule: exactly;line-height: 50px;font-size: 1px;">&nbsp;</div>
</div>
<div style="Margin-left: 20px;Margin-right: 20px;">
<div style="mso-line-height-rule: exactly;mso-text-raise: 4px;">
<h1 class="size-28" style="Margin-top: 0;Margin-bottom: 0;font-style: normal;font-weight: normal;color: #000;font-size: 24px;line-height: 32px;font-family: avenir,sans-serif;text-align: center;"
lang="x-size-28">
<span class="font-avenir">
<span style="color:#ffffff">Your IP address is changed to</span>
</span>
</h1>
<h1 class="size-48" style="Margin-top: 20px;Margin-bottom: 0;font-style: normal;font-weight: normal;color: #000;font-size: 36px;line-height: 43px;font-family: avenir,sans-serif;text-align: center;"
lang="x-size-48">
<span class="font-avenir">
<strong>
<span style="color:#ffffff">{{ .CurrentIP }}</span>
</strong>
</span>
</h1>
<h2 class="size-28" style="Margin-top: 20px;Margin-bottom: 16px;font-style: normal;font-weight: normal;color: #e31212;font-size: 24px;line-height: 32px;font-family: Avenir,sans-serif;text-align: center;"
lang="x-size-28">
<font color="#ffffff">
<strong>Domain {{ .Domain }} is updated</strong>
</font>
</h2>
</div>
</div>
<div style="Margin-left: 20px;Margin-right: 20px;">
<div style="mso-line-height-rule: exactly;line-height: 15px;font-size: 1px;">&nbsp;</div>
</div>
<div style="Margin-left: 20px;Margin-right: 20px;">
<div style="mso-line-height-rule: exactly;line-height: 35px;font-size: 1px;">&nbsp;</div>
</div>
</div>
<!--[if (mso)|(IE)]></td><td class="layout__edges">&nbsp;</td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="mso-line-height-rule: exactly;line-height: 20px;font-size: 20px;">&nbsp;</div>
<div style="mso-line-height-rule: exactly;" role="contentinfo">
<div class="layout email-footer" style="Margin: 0 auto;max-width: 600px;min-width: 320px; width: 320px;width: calc(28000% - 167400px);overflow-wrap: break-word;word-wrap: break-word;word-break: break-word;">
<div class="layout__inner" style="border-collapse: collapse;display: table;width: 100%;">
<!--[if (mso)|(IE)]><table align="center" cellpadding="0" cellspacing="0" role="presentation"><tr class="layout-email-footer"><td style="width: 400px;" valign="top" class="w360"><![endif]-->
<div class="column wide" style="text-align: left;font-size: 12px;line-height: 19px;color: #adb3b9;font-family: sans-serif;Float: left;max-width: 400px;min-width: 320px; width: 320px;width: calc(8000% - 47600px);">
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 10px;Margin-bottom: 10px;">
<div style="font-size: 12px;line-height: 19px;">
</div>
<div style="font-size: 12px;line-height: 19px;Margin-top: 18px;">
</div>
<!--[if mso]>&nbsp;<![endif]-->
</div>
</div>
<!--[if (mso)|(IE)]></td><td style="width: 200px;" valign="top" class="w160"><![endif]-->
<div class="column narrow" style="text-align: left;font-size: 12px;line-height: 19px;color: #adb3b9;font-family: sans-serif;Float: left;max-width: 320px;min-width: 200px; width: 320px;width: calc(72200px - 12000%);">
<div style="Margin-left: 20px;Margin-right: 20px;Margin-top: 10px;Margin-bottom: 10px;">
</div>
</div>
<!--[if (mso)|(IE)]></td></tr></table><![endif]-->
</div>
</div>
</div>
<div style="mso-line-height-rule: exactly;line-height: 40px;font-size: 40px;">&nbsp;</div>
</body>
</div>
</html>
`

145
utils.go
View File

@ -1,51 +1,130 @@
package main package godns
import ( import (
"bytes"
"errors" "errors"
"flag" "html/template"
"fmt" "io/ioutil"
"log" "log"
"runtime" "net/http"
"strings" "golang.org/x/net/proxy"
"gopkg.in/gomail.v2"
) )
func identifyPanic() string { var (
var name, file string // Logo for GoDNS
var line int Logo = `
var pc [16]uintptr
n := runtime.Callers(3, pc[:])
for _, pc := range pc[:n] {
fn := runtime.FuncForPC(pc)
if fn == nil {
continue
}
file, line = fn.FileLine(pc)
name = fn.Name() GoDNS V%s
if !strings.HasPrefix(name, "runtime.") { https://github.com/TimothyYe/godns
break
`
)
const (
// PanicMax is the max allowed panic times
PanicMax = 5
// INTERVAL is minute
INTERVAL = 5
// DNSPOD for dnspod.cn
DNSPOD = "DNSPod"
// HE for he.net
HE = "HE"
)
// GetCurrentIP gets public IP from internet
func GetCurrentIP(configuration *Settings) (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 {
log.Println("can't connect to the proxy:", err)
return "", err
} }
httpTransport := &http.Transport{}
client.Transport = httpTransport
httpTransport.Dial = dialer.Dial
} }
switch { response, err := client.Get(configuration.IPUrl)
case name != "":
return fmt.Sprintf("%v:%v", name, line) if err != nil {
case file != "": log.Println("Cannot get IP...")
return fmt.Sprintf("%v:%v", file, line) return "", err
} }
return fmt.Sprintf("pc:%x", pc) defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
return string(body), nil
} }
func usage() { // CheckSettings check the format of settings
log.Println("[command] -c=[config file path]") func CheckSettings(config *Settings) error {
flag.PrintDefaults() if config.Provider == DNSPOD {
} if (config.Email == "" || config.Password == "") && config.LoginToken == "" {
return errors.New("email/password or login token cannot be empty")
func checkSettings(config *Settings) error { }
if (config.Email == "" || config.Password == "") && config.LoginToken == "" { } else if config.Provider == HE {
return errors.New("Input email/password or login token cannot be empty!") if config.Password == "" {
return errors.New("password cannot be empty")
}
} else {
return errors.New("please provide supported DNS provider: DNSPod/HE")
} }
return nil return nil
} }
// SendNotify sends mail notify if IP is changed
func SendNotify(configuration *Settings, domain, currentIP string) error {
m := gomail.NewMessage()
m.SetHeader("From", configuration.Notify.SMTPUsername)
m.SetHeader("To", configuration.Notify.SendTo)
m.SetHeader("Subject", "GoDNS Notification")
log.Println("currentIP:", currentIP)
log.Println("domain:", domain)
m.SetBody("text/html", buildTemplate(currentIP, domain))
d := gomail.NewPlainDialer(configuration.Notify.SMTPServer, configuration.Notify.SMTPPort, configuration.Notify.SMTPUsername, configuration.Notify.SMTPPassword)
// Send the email config by sendlist .
if err := d.DialAndSend(m); err != nil {
log.Println("Send email notification with error:", err.Error())
return err
}
return nil
}
func buildTemplate(currentIP, domain string) string {
t := template.New("notification template")
t.Parse(mailTemplate)
data := struct {
CurrentIP string
Domain string
}{
currentIP,
domain,
}
var tpl bytes.Buffer
if err := t.Execute(&tpl, data); err != nil {
log.Println(err.Error())
return ""
}
return tpl.String()
}

49
utils_test.go Normal file
View File

@ -0,0 +1,49 @@
package godns
import (
"testing"
)
func TestGetCurrentIP(t *testing.T) {
conf := &Settings{IPUrl: "http://members.3322.org/dyndns/getip"}
ip, _ := GetCurrentIP(conf)
if ip == "" {
t.Log("IP is empty...")
} else {
t.Log("IP is:" + ip)
}
conf = &Settings{Socks5Proxy: "localhost:8899", IPUrl: "http://members.3322.org/dyndns/getip"}
ip, err := GetCurrentIP(conf)
if ip != "" && err == nil {
t.Error("should return error")
}
}
func TestCheckSettings(t *testing.T) {
settingError := &Settings{}
if err := CheckSettings(settingError); err == nil {
t.Error("setting is invalid, should return error")
}
settingDNSPod := &Settings{Provider: "DNSPod", LoginToken: "aaa"}
if err := CheckSettings(settingDNSPod); err == nil {
t.Log("setting with login token, passed")
} else {
t.Error("setting with login token, should be passed")
}
settingDNSPod = &Settings{Provider: "DNSPod"}
if err := CheckSettings(settingDNSPod); err == nil {
t.Error("setting with invalid parameters, should be failed")
}
settingHE := &Settings{Provider: "HE", Password: ""}
if err := CheckSettings(settingHE); err != nil {
t.Log("HE setting without password, passed")
} else {
t.Error("HE setting without password, should be faild")
}
}