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

Compare commits

..

No commits in common. "master" and "V1.2" have entirely different histories.
master ... V1.2

43 changed files with 856 additions and 3297 deletions

12
.github/FUNDING.yml vendored
View File

@ -1,12 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: https://www.paypal.me/timothyye

View File

@ -1,20 +0,0 @@
name: Go
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.15.7
uses: actions/setup-go@v1
with:
go-version: 1.15.7
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Build
run: cd cmd/godns && go build -v .

9
.gitignore vendored
View File

@ -1,6 +1,3 @@
# System files on macOS
.DS_Store
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
@ -29,13 +26,9 @@ config.json
*.swp
*.gz
godns
godns.exe
config.json
cmd/godns/config.json
vendor/*
/.idea
/godns.iml
/godns.ipr
/godns.iws
.current_ip
.DS_Store

24
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,24 @@
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

11
.travis.yml Normal file
View File

@ -0,0 +1,11 @@
language: go
go:
- 1.7
- 1.8
install:
go get -v
script:
- cp ./config_sample.json ./config.json
- go test -v ./...

View File

@ -1,76 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at i@xiaozhou.net. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1,9 +1,9 @@
FROM golang:alpine AS builder
RUN mkdir /godns
ADD . /godns/
WORKDIR /godns
RUN CGO_ENABLED=0 go build -o godns cmd/godns/godns.go
FROM gcr.io/distroless/base
COPY --from=builder /godns/godns /godns
ENTRYPOINT ["/godns"]
FROM timothyye/alpine:3.6-glibc
MAINTAINER Timothy
RUN apk add --update ca-certificates
RUN mkdir -p /usr/local/godns
COPY godns /usr/local/godns
RUN chmod +x /usr/local/godns/godns
RUN rm -rf /var/cache/apk/*
WORKDIR /usr/local/godns
ENTRYPOINT ["./godns", "-c", "/usr/local/godns/config.json"]

View File

@ -2,43 +2,28 @@
BINARY=godns
# Builds the project
build:
GO111MODULE=on go build -ldflags "-X main.Version=${VERSION}" -o ${BINARY} cmd/godns/godns.go
go build -o ${BINARY}
# Installs our project: copies binaries
install:
GO111MODULE=on go install
image:
# Build docker image
go clean
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t timothyye/godns:${VERSION} . --push
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t timothyye/godns:latest . --push
go install
release:
# Clean
go clean
rm -rf *.gz
# Build for mac
GO111MODULE=on go build -ldflags "-s -w -X main.Version=${VERSION}" cmd/godns/godns.go
go build
tar czvf ${BINARY}-mac64-${VERSION}.tar.gz ./${BINARY}
# Build for linux
go clean
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -ldflags "-s -w -X main.Version=${VERSION}" cmd/godns/godns.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
tar czvf ${BINARY}-linux64-${VERSION}.tar.gz ./${BINARY}
# Build for arm
go clean
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GO111MODULE=on go build -ldflags "-s -w -X main.Version=${VERSION}" cmd/godns/godns.go
tar czvf ${BINARY}-arm64-${VERSION}.tar.gz ./${BINARY}
go clean
CGO_ENABLED=0 GOOS=linux GOARCH=arm GO111MODULE=on go build -ldflags "-s -w -X main.Version=${VERSION}" cmd/godns/godns.go
tar czvf ${BINARY}-arm-${VERSION}.tar.gz ./${BINARY}
# Build for win
go clean
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 GO111MODULE=on go build -ldflags "-s -w -X main.Version=${VERSION}" cmd/godns/godns.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
tar czvf ${BINARY}-win64-${VERSION}.tar.gz ./${BINARY}.exe
make image
go clean
# Cleans our projects: deletes binaries
clean:
go clean
rm -rf ./godns
rm -rf ./godns.exe
rm -rf *.gz
.PHONY: clean build

622
README.md
View File

@ -7,613 +7,113 @@
╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
```
[![Apache licensed][9]][10] [![Docker][3]][4] [![Go Report Card][11]][12] [![Cover.Run][15]][16] [![GoDoc][13]][14]
[![Release][7]][8] [![MIT licensed][9]][10] [![Build Status][1]][2] [![Downloads][5]][6] [![Docker][3]][4] [![Go Report Card][11]][12]
[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
[5]: https://img.shields.io/badge/downloads-1.95MB-brightgreen.svg
[6]: https://github.com/TimothyYe/godns/releases
[7]: https://img.shields.io/badge/release-v1.2-brightgreen.svg
[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](https://github.com/TimothyYe/godns) is a dynamic DNS (DDNS) client tool. It is a rewrite in [Go](https://golang.org) of my early [DynDNS](https://github.com/TimothyYe/DynDNS) open source project.
GoDNS is a dynamic DNS (DDNS) tool, it is based on my early open source project: [DynDNS](https://github.com/TimothyYe/DynDNS).
Currently supports updating A records for subdomains. Doesn't support updating of root domains.
Now I rewrite [DynDNS](https://github.com/TimothyYe/DynDNS) by Golang and call it [GoDNS](https://github.com/TimothyYe/godns).
---
- [Supported DNS Providers](#supported-dns-providers)
- [Supported Platforms](#supported-platforms)
- [Pre-conditions](#pre-conditions)
- [Installation](#installation)
- [Usage](#usage)
- [Configuration](#configuration)
- [Overview](#overview)
- [Configuration properties](#configuration-properties)
- [Configuration examples](#configuration-examples)
- [Cloudflare](#cloudflare)
- [DNSPod](#dnspod)
- [Dreamhost](#dreamhost)
- [Google Domains](#google-domains)
- [AliDNS](#alidns)
- [DuckDNS](#duckdns)
- [No-IP](#no-ip)
- [HE.net](#henet)
- [Notifications](#notifications)
- [Email](#email)
- [Telegram](#telegram)
- [Slack](#slack)
- [Miscellaneous topics](#miscellaneous-topics)
- [IPv6 support](#ipv6-support)
- [Network interface IP address](#network-interface-ip-address)
- [SOCKS5 proxy support](#socks5-proxy-support)
- [Running GoDNS](#running-godns)
- [As a manual daemon](#as-a-manual-daemon)
- [As a managed daemon (with upstart)](#as-a-managed-daemon-with-upstart)
- [As a managed daemon (with systemd)](#as-a-managed-daemon-with-systemd)
- [As a Docker container](#as-a-docker-container)
- [As a Windows service](#as-a-windows-service)
- [Special Thanks](#special-thanks)
## MIPS32 platform
---
## Supported DNS Providers
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.
| Provider | IPv4 support | IPv6 support | Root Domain | Subdomains |
| ------------------------------------- | :----------------: | :----------------: | :----------------: | :----------------: |
| [Cloudflare][cloudflare] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| [Google Domains][google.domains] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [DNSPod][dnspod] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [HE.net (Hurricane Electric)][he.net] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [AliDNS][alidns] | :white_check_mark: | :x: | :x: | :white_check_mark: |
| [DuckDNS][duckdns] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [Dreamhost][dreamhost] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [No-IP][no-ip] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
## Pre-condition
[cloudflare]: https://cloudflare.com
[google.domains]: https://domains.google
[dnspod]: https://www.dnspod.cn
[he.net]: https://dns.he.net
[alidns]: https://help.aliyun.com/product/29697.html
[duckdns]: https://www.duckdns.org
[dreamhost]: https://www.dreamhost.com
[no-ip]: https://www.noip.com
* GoDNS relies on [DNSPod](http://dnspod.cn) and its API.
Tip: You can follow this [issue](https://github.com/TimothyYe/godns/issues/76) to view the current status of DDNS for root domains.
* To use GoDNS, you need a domain hosted on [DNSPod](http://dnspod.cn).
## Supported Platforms
## Build it
* Linux
* MacOS
* ARM Linux (Raspberry Pi, etc.)
* Windows
* MIPS32 platform
### Get & build it from source code
To compile binaries for MIPS (mips or mipsle), run:
```bash
GOOS=linux GOARCH=mips/mipsle GOMIPS=softfloat go build -a
```
The binary can run on routers as well.
## Pre-conditions
To use GoDNS, it is assumed:
* You registered (now own) a domain
* Domain was delegated to a supported [DNS provider](#supported-dns-providers) (i.e. it has nameserver `NS` records pointing at a supported provider)
Alternatively, you can sign in to [DuckDNS](https://www.duckdns.org) (with a social account) and get a subdomain on the duckdns.org domain for free.
## Installation
Build GoDNS by running (from the root of the repository):
* Get source code from Github:
```bash
cd cmd/godns # go to the GoDNS directory
go get -v # get dependencies
go build # build
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
go build
```
You can also download a compiled binary from the [releases](https://github.com/TimothyYe/godns/releases).
## Usage
Print usage/help by running:
## Get help
```bash
$ ./godns -h
Usage of ./godns:
-c string
Specify a config file (default "./config.json")
-d Run it as docker mode
-h Show help
```
## Configuration
## Config it
### Overview
* 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.
* Save it in the same directory of GoDNS, or use -c=your_conf_path command.
* Make a copy of [config_sample.json](./config_sample.json) and name it `config.json`
* Configure your provider, domain/subdomain info, credentials, etc.
* Configure a notification medium (e.g. SMTP to receive emails) to get notified when your IP address changes
* Place the file in the same directory of GoDNS or use the `-c=path/to/your/file.json` option
### Configuration properties
* `provider` — One of the [supported provider to use](#supported-dns-providers): `Cloudflare`, `Google`, `DNSPod`, `AliDNS`, `HE`, `DuckDNS` or `Dreamhost`.
* `email` — Email or account name of the DNS provider.
* `password` — Password of the DNS provider.
* `login_token` — API token of the DNS provider.
* `domains` — Domains list, with your sub domains.
* `ip_url` — A URL for fetching one's public IPv4 address.
* `ipv6_url` — A URL for fetching one's public IPv6 address.
* `ip_type` — Switch deciding if IPv4 or IPv6 should be used (when [supported](#supported-dns-providers)). Available values: `IPv4` or `IPv6`.
* `interval` — How often (in seconds) the public IP should be updated.
* `socks5_proxy` — Socks5 proxy server.
* `resolver` — Address of a public DNS server to use. For instance to use [Google's public DNS](https://developers.google.com/speed/public-dns/docs/using), you can set `8.8.8.8` when using GoDNS in IPv4 mode or `2001:4860:4860::8888` in IPv6 mode.
### Configuration examples
#### Cloudflare
For Cloudflare, you need to provide the email & Global API Key as password (or to use the API token) and config all the domains & subdomains.
<details>
<summary>Using email & Global API Key</summary>
```json
{
"provider": "Cloudflare",
"email": "you@example.com",
"password": "Global API Key",
"domains": [{
"domain_name": "example.com",
"sub_domains": ["www","test"]
},{
"domain_name": "example2.com",
"sub_domains": ["www","test"]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
<details>
<summary>Using the API Token</summary>
```json
{
"provider": "Cloudflare",
"login_token": "API Token",
"domains": [{
"domain_name": "example.com",
"sub_domains": ["www","test"]
},{
"domain_name": "example2.com",
"sub_domains": ["www","test"]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
#### DNSPod
For DNSPod, you need to provide your API Token(you can create it [here](https://www.dnspod.cn/console/user/security)), and config all the domains & subdomains.
<details>
<summary>Example</summary>
```json
{
"provider": "DNSPod",
"login_token": "your_id,your_token",
"domains": [{
"domain_name": "example.com",
"sub_domains": ["www","test"]
},{
"domain_name": "example2.com",
"sub_domains": ["www","test"]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"ip_type": "IPV4",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
#### Dreamhost
For Dreamhost, you need to provide your API Token(you can create it [here](https://panel.dreamhost.com/?tree=home.api)), and config all the domains & subdomains.
<details>
<summary>Example</summary>
```json
{
"provider": "Dreamhost",
"login_token": "your_api_key",
"domains": [{
"domain_name": "example.com",
"sub_domains": ["www","test"]
},{
"domain_name": "example2.com",
"sub_domains": ["www","test"]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"ip_type": "IPV4",
"interval": 300,
"resolver": "ns1.dreamhost.com",
"socks5_proxy": ""
}
```
</details>
#### Google Domains
For Google Domains, you need to provide email & password, and config all the domains & subdomains.
<details>
<summary>Example</summary>
```json
{
"provider": "Google",
"email": "Your_Username",
"password": "Your_Password",
"domains": [{
"domain_name": "example.com",
"sub_domains": ["www","test"]
},{
"domain_name": "example2.com",
"sub_domains": ["www","test"]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
#### AliDNS
For AliDNS, you need to provide `AccessKeyID` & `AccessKeySecret` as `email` & `password`, and config all the domains & subdomains.
<details>
<summary>Example</summary>
```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"]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
#### DuckDNS
For DuckDNS, only need to provide the `token`, config 1 default domain & subdomains.
<details>
<summary>Example</summary>
```json
{
"provider": "DuckDNS",
"password": "",
"login_token": "3aaaaaaaa-f411-4198-a5dc-8381cac61b87",
"domains": [
{
"domain_name": "www.duckdns.org",
"sub_domains": [
"myname"
]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
#### No-IP
<details>
<summary>Example</summary>
```json
{
"provider": "NoIP",
"email": "mail@example.com",
"password": "YourPassword",
"domains": [
{
"domain_name": "ddns.net",
"sub_domains": ["timothyye6"]
}
],
"ip_type": "IPv4",
"ip_url": "https://myip.biturl.top",
"resolver": "8.8.8.8",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
#### HE.net
For HE, email is not needed, just fill DDNS key to password, and config all the domains & subdomains.
<details>
<summary>Example</summary>
```json
{
"provider": "HE",
"password": "YourPassword",
"login_token": "",
"domains": [{
"domain_name": "example.com",
"sub_domains": ["www","test"]
},{
"domain_name": "example2.com",
"sub_domains": ["www","test"]
}
],
"resolver": "8.8.8.8",
"ip_url": "https://myip.biturl.top",
"interval": 300,
"socks5_proxy": ""
}
```
</details>
<details>
<summary>Provider configuration</summary>
Add a new "A record" and make sure that "Enable entry for dynamic dns" is checked:
<img src="./snapshots/he1.png" width="640" />
Fill in your own DDNS key or generate a random DDNS key for this new created "A record":
<img src="./snapshots/he2.png" width="640" />
Remember the DDNS key and set it in the `password` property in the configuration file.
__NOTICE__: If you have multiple domains or subdomains, make sure their DDNS key are the same.
</details>
### Notifications
GoDNS can send a notification each time the IP changes.
#### Email
Emails are sent over [SMTP](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol). Update your configuration with the following snippet:
```json
"notify": {
"mail": {
"enabled": true,
"smtp_server": "smtp.example.com",
"smtp_username": "user",
"smtp_password": "password",
"smtp_port": 25,
"send_to": "my_mail@example.com"
}
}
```
Each time the IP changes, you will receive an email like that:
<img src="https://github.com/TimothyYe/godns/blob/master/snapshots/mail.png?raw=true" />
#### Telegram
To receive a [Telegram](https://telegram.org/) message each time the IP changes, update your configuration with the following snippet:
```json
"notify": {
"telegram": {
"enabled": true,
"bot_api_key": "11111:aaaa-bbbb",
"chat_id": "-123456",
"message_template": "Domain *{{ .Domain }}* is updated to %0A{{ .CurrentIP }}",
"use_proxy": false
},
}
```
The `message_template` property supports [markdown](https://www.markdownguide.org). New lines needs to be escaped with `%0A`.
#### Slack
To receive a [Slack](https://slack.com) message each time the IP changes, update your configuration with the following snippet:
```json
"notify": {
"slack": {
"enabled": true,
"bot_api_token": "xoxb-xxx",
"channel": "your_channel",
"message_template": "Domain *{{ .Domain }}* is updated to \n{{ .CurrentIP }}",
"use_proxy": false
},
}
```
The `message_template` property supports [markdown](https://www.markdownguide.org). New lines needs to be escaped with `\n`.
### Miscellaneous topics
#### IPv6 support
Most of the [providers](#supported-dns-providers) support IPv6.
To enable the `IPv6` support of GoDNS, there are two solutions to choose from:
1. Use an online service to lookup the external IPv6
For that:
- Set the `ip_type` as `IPv6`, and make sure the `ipv6_url` is configured
- Create an `AAAA` record instead of an `A` record in your DNS provider
<details>
<summary>Configuration example</summary>
```json
{
"domains": [
{
"domain_name": "example.com",
"sub_domains": [
"ipv6"
]
}
],
"resolver": "2001:4860:4860::8888",
"ipv6_url": "https://api-ipv6.ip.sb/ip",
"ip_type": "IPv6"
}
```
</details>
2. Let GoDNS find the IPv6 of the network interface of the machine it is running on (more on that [later](#network-interface-ip-address)).
For this to happen, just leave `ip_url` and `ipv6_url` empty.
Note that the network interface must be configured with an IPv6 for this to work.
#### Network interface IP address
For some reasons if you want to get the IP address associated to a network interface (instead of performing an online lookup), you can specify it in the configuration file this way:
```json
...
"ip_url": "",
"ip_interface": "interface-name",
...
```
With `interface-name` replaced by the name of the network interface, e.g. `eth0` on Linux or `Local Area Connection` on Windows.
Note: If `ip_url` is also specified, it will be used to perform an online lookup first and the network interface IP will be used as a fallback in case of failure.
#### SOCKS5 proxy support
You can make all remote calls go through a [SOCKS5 proxy](https://en.wikipedia.org/wiki/SOCKS#SOCKS5) by specifying it in the configuration file this way:
```json
...
"socks5_proxy": "127.0.0.1:7070"
"use_proxy": true
...
```
## Running GoDNS
There are few ways to run GoDNS.
### As a manual daemon
## Run it as a daemon manually
```bash
nohup ./godns &
```
Note: when the program stops, it will not be restarted.
## Run it as a daemon, manage it via Upstart
### As a managed daemon (with upstart)
1. Install `upstart` first (if not available already)
2. Copy `./upstart/godns.conf` to `/etc/init` (and tweak it to your needs)
3. Start the service:
```bash
sudo start godns
```
### As a managed daemon (with systemd)
1. Install `systemd` first (it not available already)
2. Copy `./systemd/godns.service` to `/lib/systemd/system` (and tweak it to your needs)
3. Start the service:
```bash
sudo systemctl enable godns
sudo systemctl start godns
```
### As a Docker container
With `/path/to/config.json` your local configuration file, run:
* Install `upstart` first
* Copy `./upstart/godns.conf` to `/etc/init`
* Start it as a system service:
```bash
docker run \
-d --name godns --restart=always \
-v /path/to/config.json:/config.json \
timothyye/godns:latest
sudo start godns
```
### As a Windows service
## Run it as a daemon, manage it via Systemd
1. Download the latest version of [NSSM](https://nssm.cc/download)
* Modify `./systemd/godns.service` and config it.
* Copy `./systemd/godns.service` to `/lib/systemd/system`
* Start it as a systemd service:
2. In an administrative prompt, from the folder where NSSM was downloaded, e.g. `C:\Downloads\nssm\` **win64**, run:
```
nssm install YOURSERVICENAME
```
3. Follow the interface to configure the service. In the "Application" tab just indicate where the `godns.exe` file is. Optionally you can also define a description on the "Details" tab and define a log file on the "I/O" tab. Finish by clicking on the "Install service" button.
4. The service will now start along Windows.
Note: you can uninstall the service by running:
```
nssm remove YOURSERVICENAME
```bash
sudo systemctl enable godns
sudo systemctl start godns
```
## Special Thanks
## Run it in docker
<img src="https://i.imgur.com/xhe5RLZ.jpg" width="80px" align="right" />
Now godns supports to run in docker.
Thanks JetBrains for sponsoring this project with [free open source license](https://www.jetbrains.com/community/opensource/).
* Pull godns image from docker hub:
```bash
docker pull timothyye/godns:1.2
```
> I like GoLand, it is an amazing and productive tool.
* Run godns in container and pass config parameters to it via enviroment variables:
```bash
docker run -d --name godns --restart=always \
-v /path/to/config.json:/usr/local/godns/config.json timothyye/godns:1.2
```
## Enjoy it!

View File

@ -1 +0,0 @@
theme: jekyll-theme-cayman

View File

@ -1,70 +0,0 @@
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)
h := handler.CreateHandler(configuration.Provider)
h.SetConfiguration(&configuration)
for i := range configuration.Domains {
go h.DomainLoop(&configuration.Domains[i], panicChan)
}
panicCount := 0
for {
failDomain := <-panicChan
log.Println("Got panic in goroutine, will start a new one... :", panicCount)
go h.DomainLoop(&failDomain, panicChan)
panicCount++
if panicCount >= godns.PanicMax {
os.Exit(1)
}
}
}

View File

@ -1,47 +1,18 @@
{
"provider": "DNSPod",
"email": "example@gmail.com",
"password": "",
"login_token": "",
"domains": [
{
"domains": [{
"domain_name": "example.com",
"sub_domains": [
"www",
"test"
]
},
{
"sub_domains": ["www","test"]
},{
"domain_name": "example2.com",
"sub_domains": [
"www",
"test"
]
"sub_domains": ["www","test"]
}
],
"ip_url": "https://myip.biturl.top",
"ipv6_url": "https://api-ipv6.ip.sb/ip",
"ip_type": "IPv4",
"interval": 300,
"resolver": "8.8.8.8",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.111 Safari/537.36",
"ip_interface": "eth0",
"socks5_proxy": "",
"use_proxy": false,
"notify": {
"telegram": {
"enabled": false,
"bot_api_key": "",
"chat_id": "",
"message_template": "",
"use_proxy": false
},
"mail": {
"enabled": false,
"smtp_server": "",
"smtp_username": "",
"smtp_password": "",
"smtp_port": 25,
"send_to": ""
}
}
"ip_url": "http://members.3322.org/dyndns/getip",
"log_path": "./godns.log",
"log_size": 16,
"log_num": 3,
"socks5_proxy": ""
}

214
dns_handler.go Normal file
View File

@ -0,0 +1,214 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/bitly/go-simplejson"
"golang.org/x/net/proxy"
)
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
}

15
dns_handler_test.go Normal file
View File

@ -0,0 +1,15 @@
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)
}
}

6
glide.lock generated Normal file
View File

@ -0,0 +1,6 @@
hash: 1da678bb0eda2c40266e5014e089aa36ebfa0a360d20552a77ef77a00b2d1bfb
updated: 2016-05-25T15:08:31.725668117+08:00
imports:
- name: github.com/bitly/go-simplejson
version: aabad6e819789e569bd6aabf444c935aa9ba1e44
devImports: []

3
glide.yaml Normal file
View File

@ -0,0 +1,3 @@
package: github.com/timothyye/godns
import:
- package: github.com/bitly/go-simplejson

18
go.mod
View File

@ -1,18 +0,0 @@
module github.com/TimothyYe/godns
require (
github.com/bitly/go-simplejson v0.5.0
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/bogdanovich/dns_resolver v0.0.0-20170211073258-a8e42bc6a5b6
github.com/fatih/color v1.7.0
github.com/google/uuid v1.1.1
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.4 // indirect
github.com/miekg/dns v1.1.29
golang.org/x/net v0.0.0-20190923162816-aa69164e4478
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
go 1.13

46
go.sum
View File

@ -1,46 +0,0 @@
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/bogdanovich/dns_resolver v0.0.0-20170211073258-a8e42bc6a5b6 h1:oV1V+uwP+sjmdSkvMxsl/l+HE+N8wbL49wCXZPel25M=
github.com/bogdanovich/dns_resolver v0.0.0-20170211073258-a8e42bc6a5b6/go.mod h1:txOV61Nn+21z77KUMkNsp8lTHoOFTtqotltQAFenS9I=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc h1:Yx9JGxI1SBhVLFjpAkWMaO1TF+xyqtHLjZpvQboJGiM=
golang.org/x/net v0.0.0-20190110200230-915654e7eabc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190226215855-775f8194d0f9 h1:N26gncmS+iqc/W/SKhX3ElI5pkt72XYoRLgi5Z70LSc=
golang.org/x/sys v0.0.0-20190226215855-775f8194d0f9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPTs2tR8uOySCbBP7BN/M=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=

116
godns.go Normal file
View File

@ -0,0 +1,116 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime/debug"
"strings"
"time"
)
const (
// PANIC_MAX is the max allowed panic times
PANIC_MAX = 5
// INTERVAL is minute
INTERVAL = 5
)
var (
configuration Settings
optConf = flag.String("c", "./config.json", "Specify a config file")
optHelp = flag.Bool("h", false, "Show help")
panicCount = 0
)
func main() {
flag.Parse()
if *optHelp {
flag.Usage()
return
}
//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)
}
}

View File

@ -1,19 +0,0 @@
{
"Deadline": "2m",
"Enable": [
"goimports",
"golint",
"ineffassign",
"misspell",
"vet",
"varcheck",
"deadcode",
"structcheck"
],
"Exclude": [
"snapshots/",
"upstart/",
"systemd/"
],
"LineLength": 100
}

View File

@ -1,166 +0,0 @@
package alidns
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"errors"
"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)
}
// NewAliDNS function creates instance of AliDNS and return
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": "DescribeSubDomainRecords",
"SubDomain": fmt.Sprintf("%s.%s", rr, domain),
}
urlPath := d.genRequestURL(parms)
body, err := getHTTPBody(urlPath)
if err != nil {
fmt.Printf("GetDomainRecords error.%+v\n", err)
} else {
if err := json.Unmarshal(body, resp); err != nil {
fmt.Printf("GetDomainRecords error. %+v\n", err)
return nil
}
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)
if urlPath == "" {
return errors.New("failed to generate request URL")
}
_, err := getHTTPBody(urlPath)
if err != nil {
fmt.Printf("UpdateDomainRecord error.%+v\n", err)
}
return err
}
func (d *AliDNS) genRequestURL(parms map[string]string) string {
var 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+"&"))
if _, err := mac.Write([]byte(s)); err != nil {
return ""
}
sign := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return fmt.Sprintf("%s?%s&Signature=%s", baseURL, path, url.QueryEscape(sign))
}

View File

@ -1,85 +0,0 @@
package alidns
import (
"fmt"
"log"
"runtime/debug"
"time"
"github.com/TimothyYe/godns"
)
// Handler struct
type Handler struct {
Configuration *godns.Settings
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
}
// DomainLoop the main logic loop
func (handler *Handler) 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
}
}()
looping := false
aliDNS := NewAliDNS(handler.Configuration.Email, handler.Configuration.Password)
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
currentIP, err := godns.GetCurrentIP(handler.Configuration)
if err != nil {
log.Println("Failed to get current IP:", err)
continue
}
log.Println("currentIP is:", currentIP)
for _, subDomain := range domain.SubDomains {
hostname := subDomain + "." + domain.DomainName
lastIP, err := godns.ResolveDNS(hostname, handler.Configuration.Resolver, handler.Configuration.IPType)
if err != nil {
log.Println(err)
continue
}
//check against currently known IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
lastIP = currentIP
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 notification
if err := godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP); err != nil {
log.Printf("Failed to send notification")
}
}
}
}
}

View File

@ -1,266 +0,0 @@
package cloudflare
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"runtime/debug"
"strings"
"time"
"github.com/TimothyYe/godns"
)
// Handler struct definition
type Handler struct {
Configuration *godns.Settings
API string
}
// DNSRecordResponse struct
type DNSRecordResponse struct {
Records []DNSRecord `json:"result"`
Success bool `json:"success"`
}
// DNSRecordUpdateResponse struct
type DNSRecordUpdateResponse struct {
Record DNSRecord `json:"result"`
Success bool `json:"success"`
}
// DNSRecord for Cloudflare API
type DNSRecord struct {
ID string `json:"id"`
IP string `json:"content"`
Name string `json:"name"`
Proxied bool `json:"proxied"`
Type string `json:"type"`
ZoneID string `json:"zone_id"`
TTL int32 `json:"ttl"`
}
// SetIP updates DNSRecord.IP
func (r *DNSRecord) SetIP(ip string) {
r.IP = ip
}
// ZoneResponse is a wrapper for Zones
type ZoneResponse struct {
Zones []Zone `json:"result"`
Success bool `json:"success"`
}
// Zone object with id and name
type Zone struct {
ID string `json:"id"`
Name string `json:"name"`
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
handler.API = "https://api.cloudflare.com/client/v4"
}
// DomainLoop the main logic loop
func (handler *Handler) 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
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
currentIP, err := godns.GetCurrentIP(handler.Configuration)
if err != nil {
log.Println("Error in GetCurrentIP:", err)
continue
}
log.Println("Current IP 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 {
log.Println("Checking IP for domain", domain.DomainName)
zoneID := handler.getZone(domain.DomainName)
if zoneID != "" {
records := handler.getDNSRecords(zoneID)
// update records
for _, rec := range records {
if !recordTracked(domain, &rec) {
log.Println("Skiping record:", rec.Name)
continue
}
if rec.IP != currentIP {
log.Printf("IP mismatch: Current(%+v) vs Cloudflare(%+v)\r\n", currentIP, rec.IP)
lastIP = handler.updateRecord(rec, currentIP)
// Send notification
if err := godns.SendNotify(handler.Configuration, rec.Name, currentIP); err != nil {
log.Println("Failed to send notification")
}
} else {
log.Printf("Record OK: %+v - %+v\r\n", rec.Name, rec.IP)
}
}
} else {
log.Println("Failed to find zone for domain:", domain.DomainName)
}
}
}
}
// Check if record is present in domain conf
func recordTracked(domain *godns.Domain, record *DNSRecord) bool {
if record.Name == domain.DomainName {
return true
}
for _, subDomain := range domain.SubDomains {
sd := fmt.Sprintf("%s.%s", subDomain, domain.DomainName)
if record.Name == sd {
return true
}
}
return false
}
// Create a new request with auth in place and optional proxy
func (handler *Handler) newRequest(method, url string, body io.Reader) (*http.Request, *http.Client) {
client := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
if client == nil {
log.Println("cannot create HTTP client")
}
req, _ := http.NewRequest(method, handler.API+url, body)
req.Header.Set("Content-Type", "application/json")
if handler.Configuration.Email != "" && handler.Configuration.Password != "" {
req.Header.Set("X-Auth-Email", handler.Configuration.Email)
req.Header.Set("X-Auth-Key", handler.Configuration.Password)
} else if handler.Configuration.LoginToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", handler.Configuration.LoginToken))
}
return req, client
}
// Find the correct zone via domain name
func (handler *Handler) getZone(domain string) string {
var z ZoneResponse
req, client := handler.newRequest("GET", fmt.Sprintf("/zones?name=%s", domain), nil)
resp, err := client.Do(req)
if err != nil {
log.Println("Request error:", err.Error())
return ""
}
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &z)
if err != nil {
log.Printf("Decoder error: %+v\n", err)
log.Printf("Response body: %+v\n", string(body))
return ""
}
if z.Success != true {
log.Printf("Response failed: %+v\n", string(body))
return ""
}
for _, zone := range z.Zones {
if zone.Name == domain {
return zone.ID
}
}
return ""
}
// Get all DNS A records for a zone
func (handler *Handler) getDNSRecords(zoneID string) []DNSRecord {
var empty []DNSRecord
var r DNSRecordResponse
var recordType string
if handler.Configuration.IPType == "" || strings.ToUpper(handler.Configuration.IPType) == godns.IPV4 {
recordType = "A"
} else if strings.ToUpper(handler.Configuration.IPType) == godns.IPV6 {
recordType = "AAAA"
}
log.Println("Querying records with type:", recordType)
req, client := handler.newRequest("GET", fmt.Sprintf("/zones/"+zoneID+"/dns_records?type=%s&page=1&per_page=500", recordType), nil)
resp, err := client.Do(req)
if err != nil {
log.Println("Request error:", err.Error())
return empty
}
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &r)
if err != nil {
log.Printf("Decoder error: %+v\n", err)
log.Printf("Response body: %+v\n", string(body))
return empty
}
if r.Success != true {
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("Response failed: %+v\n", string(body))
return empty
}
return r.Records
}
// Update DNS A Record with new IP
func (handler *Handler) updateRecord(record DNSRecord, newIP string) string {
var r DNSRecordUpdateResponse
record.SetIP(newIP)
var lastIP string
j, _ := json.Marshal(record)
req, client := handler.newRequest("PUT",
"/zones/"+record.ZoneID+"/dns_records/"+record.ID,
bytes.NewBuffer(j),
)
resp, err := client.Do(req)
if err != nil {
log.Println("Request error:", err.Error())
return ""
}
body, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(body, &r)
if err != nil {
log.Printf("Decoder error: %+v\n", err)
log.Printf("Response body: %+v\n", string(body))
return ""
}
if r.Success != true {
body, _ := ioutil.ReadAll(resp.Body)
log.Printf("Response failed: %+v\n", string(body))
} else {
log.Printf("Record updated: %+v - %+v", record.Name, record.IP)
lastIP = record.IP
}
return lastIP
}

View File

@ -1,167 +0,0 @@
package cloudflare
import (
"encoding/json"
"strings"
"testing"
"github.com/TimothyYe/godns"
)
func TestResponseToJSON(t *testing.T) {
s := strings.NewReader(`
{
"errors": [],
"messages": [],
"result": [
{
"id": "mk2b6fa491c12445a4376666a32429e1",
"name": "example.com",
"status": "active"
}
],
"result_info": {
"count": 1,
"page": 1,
"per_page": 20,
"total_count": 1,
"total_pages": 1
},
"success": true
}`)
var resp ZoneResponse
err := json.NewDecoder(s).Decode(&resp)
if err != nil {
t.Error(err.Error())
}
if resp.Success != true {
t.Errorf("Success Error: %#v != true ", resp.Success)
}
if resp.Zones[0].ID != "mk2b6fa491c12445a4376666a32429e1" {
t.Errorf("ID Error: %#v != mk2b6fa491c12445a4376666a32429e1 ", resp.Zones[0].ID)
}
if resp.Zones[0].Name != "example.com" {
t.Errorf("Name Error: %#v != example.com", resp.Zones[0].Name)
}
}
func TestDNSResponseToJSON(t *testing.T) {
s := strings.NewReader(`
{
"errors": [],
"messages": [],
"result": [
{
"content": "127.0.0.1",
"id": "F11cc63e02a42d38174b8e7c548a7b6f",
"name": "example.com",
"type": "A",
"zone_id": "mk2b6fa491c12445a4376666a32429e1",
"zone_name": "example.com"
}
],
"success": true
}`)
var resp DNSRecordResponse
err := json.NewDecoder(s).Decode(&resp)
if err != nil {
t.Error(err.Error())
}
if resp.Success != true {
t.Errorf("Success Error: %#v != true ", resp.Success)
}
if resp.Records[0].ID != "F11cc63e02a42d38174b8e7c548a7b6f" {
t.Errorf("ID Error: %#v != F11cc63e02a42d38174b8e7c548a7b6f ", resp.Records[0].ID)
}
if resp.Records[0].Name != "example.com" {
t.Errorf("Name Error: %#v != example.com", resp.Records[0].Name)
}
}
func TestDNSUpdateResponseToJSON(t *testing.T) {
s := strings.NewReader(`
{
"result": {
"id": "F11cc63e02a42d38174b8e7c548a7b6f",
"type": "A",
"name": "example.com",
"content": "127.0.0.1",
"proxiable": true,
"proxied": true,
"ttl": 1,
"locked": false,
"zone_id": "mk2b6fa491c12445a4376666a32429e1",
"zone_name": "example.com",
"modified_on": "2018-10-12T14:29:53.205191Z",
"created_on": "2018-10-12T14:29:53.205191Z",
"meta": {
"auto_added": false,
"managed_by_apps": false,
"managed_by_argo_tunnel": false
}
},
"success": true,
"errors": [],
"messages": []
}`)
var resp DNSRecordUpdateResponse
err := json.NewDecoder(s).Decode(&resp)
if err != nil {
t.Error(err.Error())
}
if resp.Success != true {
t.Errorf("Success Error: %#v != true ", resp.Success)
}
if resp.Record.ID != "F11cc63e02a42d38174b8e7c548a7b6f" {
t.Errorf("ID Error: %#v != F11cc63e02a42d38174b8e7c548a7b6f ", resp.Record.ID)
}
if resp.Record.Name != "example.com" {
t.Errorf("Name Error: %#v != example.com", resp.Record.Name)
}
}
func TestRecordTracked(t *testing.T) {
s := strings.NewReader(`
{
"errors": [],
"messages": [],
"result": [
{
"content": "127.0.0.1",
"id": "F11cc63e02a42d38174b8e7c548a7b6f",
"name": "example.com",
"type": "A",
"zone_id": "mk2b6fa491c12445a4376666a32429e1",
"zone_name": "example.com"
},
{
"content": "127.0.0.1",
"id": "G00cc63e02a42d38174b8e7c548a7b6f",
"name": "www.example.com",
"type": "A",
"zone_id": "mk2b6fa491c12445a4376666a32429e1",
"zone_name": "www.example.com"
}
],
"success": true
}`)
var resp DNSRecordResponse
err := json.NewDecoder(s).Decode(&resp)
if err != nil {
t.Error(err.Error())
}
domain := &godns.Domain{
DomainName: "example.com",
SubDomains: []string{"www"},
}
for _, rec := range resp.Records {
if recordTracked(domain, &rec) {
t.Logf("Record founded: %+v\r\n", rec.Name)
}
}
}

View File

@ -1,292 +0,0 @@
package dnspod
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"runtime/debug"
"strconv"
"strings"
"time"
"github.com/TimothyYe/godns"
"github.com/bitly/go-simplejson"
)
// Handler struct definition
type Handler struct {
Configuration *godns.Settings
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
}
// DomainLoop the main logic loop
func (handler *Handler) 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
}
}()
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
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 {
hostname := subDomain + "." + domain.DomainName
lastIP, err := godns.ResolveDNS(hostname, handler.Configuration.Resolver, handler.Configuration.IPType)
if err != nil {
log.Println(err)
continue
}
//check against currently known IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
lastIP = currentIP
subDomainID, ip := handler.GetSubDomain(domainID, subDomain)
if subDomainID == "" || ip == "" {
log.Printf("Domain or subdomain not configured yet. domain: %s.%s subDomainID: %s ip: %s\n", subDomain, domain.DomainName, subDomainID, ip)
continue
}
// 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)
// Send notification
if err := godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP); err != nil {
log.Println("Failed to send notification")
}
} else {
log.Printf("%s.%s Current IP is same as domain IP, no need to update...\n", subDomain, domain.DomainName)
}
}
}
}
}
// GenerateHeader generates the request header for DNSPod API
func (handler *Handler) GenerateHeader(content url.Values) url.Values {
header := url.Values{}
if handler.Configuration.LoginToken != "" {
header.Add("login_token", handler.Configuration.LoginToken)
}
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 *Handler) 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 *Handler) GetSubDomain(domainID int64, name string) (string, string) {
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)
if handler.Configuration.IPType == "" || strings.ToUpper(handler.Configuration.IPType) == godns.IPV4 {
value.Add("record_type", "A")
} else if strings.ToUpper(handler.Configuration.IPType) == godns.IPV6 {
value.Add("record_type", "AAAA")
} else {
log.Println("Error: must specify \"ip_type\" in config for DNSPod.")
return "", ""
}
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 *Handler) 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)
if strings.ToUpper(handler.Configuration.IPType) == godns.IPV4 {
value.Add("record_type", "A")
} else if strings.ToUpper(handler.Configuration.IPType) == godns.IPV6 {
value.Add("record_type", "AAAA")
} else {
log.Println("Error: must specify \"ip_type\" in config for DNSPod.")
return
}
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!")
} else {
log.Println("Failed to update IP record:", sjson.Get("status").Get("message").MustString())
}
}
// PostData post data and invoke DNSPod API
func (handler *Handler) PostData(url string, content url.Values) (string, error) {
client := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
if client == nil {
return "", errors.New("failed to create HTTP client")
}
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)", ""))
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,138 +0,0 @@
package dreamhost
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"runtime/debug"
"strings"
"time"
"github.com/TimothyYe/godns"
"github.com/google/uuid"
)
var (
// DreamhostURL the API address for dreamhost.com
DreamhostURL = "https://api.dreamhost.com"
)
// Handler struct
type Handler struct {
Configuration *godns.Settings
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
}
// DomainLoop the main logic loop
func (handler *Handler) 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
}
}()
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
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 {
hostname := subDomain + "." + domain.DomainName
lastIP, err := godns.ResolveDNS(hostname, handler.Configuration.Resolver, handler.Configuration.IPType)
if err != nil {
log.Println(err)
continue
}
//check against currently known IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
log.Printf("%s.%s Start to update record IP...\n", subDomain, domain.DomainName)
handler.UpdateIP(hostname, currentIP, lastIP)
// Send notification
if err := godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP); err != nil {
log.Println("Failed to send notification")
}
}
}
}
}
// UpdateIP update subdomain with current IP
func (handler *Handler) UpdateIP(hostname, currentIP, lastIP string) {
handler.updateDNS(lastIP, currentIP, hostname, "remove")
handler.updateDNS(lastIP, currentIP, hostname, "add")
}
// updateDNS can add or remove DNS records.
func (handler *Handler) updateDNS(dns, ip, hostname, action string) {
ipType := "A"
if strings.ToUpper(handler.Configuration.IPType) == godns.IPV6 {
ipType = "AAAA"
}
// Generates UUID
uid, _ := uuid.NewRandom()
values := url.Values{}
values.Add("record", hostname)
values.Add("key", handler.Configuration.LoginToken)
values.Add("type", ipType)
values.Add("unique_id", uid.String())
switch action {
case "remove":
// Build URL query (remove)
values.Add("cmd", "dns-remove_record")
values.Add("value", dns)
case "add":
// Build URL query (add)
values.Add("cmd", "dns-add_record")
values.Add("value", ip)
default:
log.Fatalf("Unknown action %s\n", action)
}
client := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
req, _ := http.NewRequest("POST", DreamhostURL, strings.NewReader(values.Encode()))
req.SetBasicAuth(handler.Configuration.Email, handler.Configuration.Password)
if handler.Configuration.UserAgent != "" {
req.Header.Add("User-Agent", handler.Configuration.UserAgent)
}
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))
}
}
}

View File

@ -1,102 +0,0 @@
package duck
import (
"fmt"
"io/ioutil"
"log"
"runtime/debug"
"strings"
"time"
"github.com/TimothyYe/godns"
)
var (
// DuckUrl the API address for Duck DNS
DuckUrl = "https://www.duckdns.org/update?domains=%s&token=%s&%s"
)
// Handler struct
type Handler struct {
Configuration *godns.Settings
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
}
// DomainLoop the main logic loop
func (handler *Handler) 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
}
}()
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
currentIP, err := godns.GetCurrentIP(handler.Configuration)
if err != nil {
log.Println("get_currentIP:", err)
continue
}
log.Println("currentIP is:", currentIP)
client := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
var ip string
if strings.ToUpper(handler.Configuration.IPType) == godns.IPV4 {
ip = fmt.Sprintf("ip=%s", currentIP)
} else if strings.ToUpper(handler.Configuration.IPType) == godns.IPV6 {
ip = fmt.Sprintf("ipv6=%s", currentIP)
}
for _, subDomain := range domain.SubDomains {
hostname := subDomain + "." + domain.DomainName
lastIP, err := godns.ResolveDNS(hostname, handler.Configuration.Resolver, handler.Configuration.IPType)
if err != nil {
log.Println(err)
continue
}
//check against currently known IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
// update IP with HTTP GET request
resp, err := client.Get(fmt.Sprintf(DuckUrl, subDomain, handler.Configuration.LoginToken, ip))
if err != nil {
// handle error
log.Print("Failed to update sub domain:", subDomain)
continue
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil || string(body) != "OK" {
log.Println("Failed to update the IP")
continue
} else {
log.Print("IP updated to:", currentIP)
}
// Send notification
if err := godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP); err != nil {
log.Println("Failed to send notification")
}
}
}
}
}

View File

@ -1,113 +0,0 @@
package google
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"runtime/debug"
"strings"
"time"
"github.com/TimothyYe/godns"
)
var (
// GoogleURL the API address for Google Domains
GoogleURL = "https://%s:%s@domains.google.com/nic/update?hostname=%s.%s&myip=%s"
)
// Handler struct
type Handler struct {
Configuration *godns.Settings
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
}
// DomainLoop the main logic loop
func (handler *Handler) 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
}
}()
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
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 {
hostname := subDomain + "." + domain.DomainName
lastIP, err := godns.ResolveDNS(hostname, handler.Configuration.Resolver, handler.Configuration.IPType)
if err != nil {
log.Println(err)
continue
}
//check against currently known IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
log.Printf("%s.%s Start to update record IP...\n", subDomain, domain.DomainName)
handler.UpdateIP(domain.DomainName, subDomain, currentIP)
// Send notification
if err := godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP); err != nil {
log.Println("Failed to send notification")
}
}
}
}
}
// UpdateIP update subdomain with current IP
func (handler *Handler) UpdateIP(domain, subDomain, currentIP string) {
client := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
resp, err := client.Get(fmt.Sprintf(GoogleURL,
handler.Configuration.Email,
handler.Configuration.Password,
subDomain,
domain,
currentIP))
if err != nil {
// handle error
log.Print("Failed to update sub domain:", subDomain)
return
}
defer resp.Body.Close()
if err != nil {
log.Println("Request error...")
log.Println("Err:", err.Error())
} else {
body, _ := ioutil.ReadAll(resp.Body)
if resp.StatusCode == http.StatusOK {
if strings.Contains(string(body), "good") {
log.Println("Update IP success:", string(body))
} else if strings.Contains(string(body), "nochg") {
log.Println("IP not changed:", string(body))
}
} else {
log.Println("Update IP failed:", string(body))
}
}
}

View File

@ -1,45 +0,0 @@
package handler
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/dreamhost"
"github.com/TimothyYe/godns/handler/duck"
"github.com/TimothyYe/godns/handler/google"
"github.com/TimothyYe/godns/handler/he"
"github.com/TimothyYe/godns/handler/noip"
)
// 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.CLOUDFLARE:
handler = IHandler(&cloudflare.Handler{})
case godns.DNSPOD:
handler = IHandler(&dnspod.Handler{})
case godns.DREAMHOST:
handler = IHandler(&dreamhost.Handler{})
case godns.HE:
handler = IHandler(&he.Handler{})
case godns.ALIDNS:
handler = IHandler(&alidns.Handler{})
case godns.GOOGLE:
handler = IHandler(&google.Handler{})
case godns.DUCK:
handler = IHandler(&duck.Handler{})
case godns.NOIP:
handler = IHandler(&noip.Handler{})
}
return handler
}

View File

@ -1,107 +0,0 @@
package he
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"net/url"
"runtime/debug"
"strings"
"time"
"github.com/TimothyYe/godns"
)
var (
// HEUrl the API address for he.net
HEUrl = "https://dyn.dns.he.net/nic/update"
)
// Handler struct
type Handler struct {
Configuration *godns.Settings
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
}
// DomainLoop the main logic loop
func (handler *Handler) 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
}
}()
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
currentIP, err := godns.GetCurrentIP(handler.Configuration)
if err != nil {
log.Println("get_currentIP:", err)
continue
}
log.Println("currentIP is:", currentIP)
//check against locally cached IP, if no change, skip update
for _, subDomain := range domain.SubDomains {
hostname := subDomain + "." + domain.DomainName
lastIP, err := godns.ResolveDNS(hostname, handler.Configuration.Resolver, handler.Configuration.IPType)
if err != nil {
log.Println(err)
continue
}
//check against currently known IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
log.Printf("%s.%s Start to update record IP...\n", subDomain, domain.DomainName)
handler.UpdateIP(domain.DomainName, subDomain, currentIP)
// Send notification
if err := godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP); err != nil {
log.Println("Failed to send notification")
}
}
}
}
}
// UpdateIP update subdomain with current IP
func (handler *Handler) 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 := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
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))
}
}
}

View File

@ -1,114 +0,0 @@
package noip
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"runtime/debug"
"strings"
"time"
"github.com/TimothyYe/godns"
)
var (
// NoIPUrl the API address for NoIP
NoIPUrl = "https://%s:%s@dynupdate.no-ip.com/nic/update?hostname=%s&%s"
)
// Handler struct
type Handler struct {
Configuration *godns.Settings
}
// SetConfiguration pass dns settings and store it to handler instance
func (handler *Handler) SetConfiguration(conf *godns.Settings) {
handler.Configuration = conf
}
// DomainLoop the main logic loop
func (handler *Handler) 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
}
}()
looping := false
for {
if looping {
// Sleep with interval
log.Printf("Going to sleep, will start next checking in %d seconds...\r\n", handler.Configuration.Interval)
time.Sleep(time.Second * time.Duration(handler.Configuration.Interval))
}
looping = true
currentIP, err := godns.GetCurrentIP(handler.Configuration)
if err != nil {
log.Println("get_currentIP:", err)
continue
}
log.Println("currentIP is:", currentIP)
client := godns.GetHttpClient(handler.Configuration, handler.Configuration.UseProxy)
var ip string
if strings.ToUpper(handler.Configuration.IPType) == godns.IPV4 {
ip = fmt.Sprintf("myip=%s", currentIP)
} else if strings.ToUpper(handler.Configuration.IPType) == godns.IPV6 {
ip = fmt.Sprintf("myipv6=%s", currentIP)
}
for _, subDomain := range domain.SubDomains {
hostname := subDomain + "." + domain.DomainName
lastIP, err := godns.ResolveDNS(hostname, handler.Configuration.Resolver, handler.Configuration.IPType)
if err != nil {
log.Println(err)
continue
}
//check against currently known IP, if no change, skip update
if currentIP == lastIP {
log.Printf("IP is the same as cached one. Skip update.\n")
} else {
req, _ := http.NewRequest("GET", fmt.Sprintf(
NoIPUrl,
handler.Configuration.Email,
handler.Configuration.Password,
hostname,
ip), nil)
if handler.Configuration.UserAgent != "" {
req.Header.Add("User-Agent", handler.Configuration.UserAgent)
}
// update IP with HTTP GET request
resp, err := client.Do(req)
if err != nil {
// handle error
log.Print("Failed to update sub domain:", subDomain)
continue
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil || !strings.Contains(string(body), "good") {
log.Println("Failed to update the IP")
continue
} else {
log.Print("IP updated to:", currentIP)
}
// Send notification
if err := godns.SendNotify(handler.Configuration, fmt.Sprintf("%s.%s", subDomain, domain.DomainName), currentIP); err != nil {
log.Println("Failed to send notification")
}
}
}
}
}

310
logger.go Normal file
View File

@ -0,0 +1,310 @@
package main
import (
"bufio"
"bytes"
"errors"
"io"
"io/ioutil"
"log"
"os"
"runtime/debug"
"strconv"
"strings"
"sync"
"time"
)
const (
// L_INFO log level
L_INFO int = iota
// L_WARNING log level
L_WARNING
// L_DEBUG log level
L_DEBUG
// PRE_INFO log level
PRE_INFO = "[ INFO]"
// PRE_WARNING log level
PRE_WARNING = "[WARNING]"
// PRE_DEBUG log level
PRE_DEBUG = "[ DEBUG]"
)
// Logger struct
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
}
// NewLogger returns a new created 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
}
// InitLogger initialize logger with specified log filename & size
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 (logger *Logger) Write(p []byte) (n int, err error) {
if logger.DEV_MODE {
n, err = os.Stdout.Write(p)
return
}
n, err = logger.fd.Write(p)
if err == nil {
fi, e := logger.fd.Stat()
if e != nil {
err = e
return
}
if fi.Size() > int64(logger.size) {
logger.muSplit.Lock()
defer logger.muSplit.Unlock()
fname := fi.Name()
strings.HasSuffix(fname, ".log")
fbase := fname[:len(fname)-3]
oldBs := make([]byte, 0, logger.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) > logger.size {
newBs = append(newBs, line...)
} else {
oldBs = append(oldBs, line...)
}
}
fd.Close()
_, err = logger.saveLog(1, fbase, oldBs)
if err != nil {
return
}
err = logger.fd.Close()
if err != nil {
return
}
err = os.Remove(fname)
if err != nil {
return
}
logger.fd, err = os.OpenFile(fname, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModeAppend|0666)
if err != nil {
return
}
_, err = logger.fd.Write(newBs)
if err != nil {
return
}
}
}
return
}
func (logger *Logger) saveLog(index int, fbase string, data []byte) (n int, err error) {
fn := fbase + strconv.Itoa(index) + ".log"
_, err = os.Stat(fn)
if index < logger.num && err == nil {
var b []byte
b, err = ioutil.ReadFile(fn)
if err != nil {
return
}
n, err = logger.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 (logger *Logger) Flush() {
if logger.buf.Len() > 0 {
logger.mu.Lock()
defer logger.mu.Unlock()
log.SetFlags(0)
log.Print(logger.buf)
log.SetFlags(log.LstdFlags)
logger.buf.Reset()
}
}
// Clean prefix and check buf size
func (logger *Logger) clean() {
logger.log.SetPrefix("")
if logger.buf.Len()/1024 > logger.flushSize {
go logger.Flush()
}
}
func (logger *Logger) setPrefix(lv int) bool {
if lv > logger.level {
return false
}
switch lv {
case L_INFO:
logger.log.SetPrefix(PRE_INFO)
case L_WARNING:
logger.log.SetPrefix(PRE_WARNING)
case L_DEBUG:
logger.log.SetPrefix(PRE_DEBUG)
default:
return false
}
return true
}
func (logger *Logger) logPrint(lv int, args ...interface{}) {
logger.mu.Lock()
defer logger.mu.Unlock()
if !logger.setPrefix(lv) {
return
}
logger.log.Print(args...)
logger.clean()
}
func (logger *Logger) logPrintln(lv int, args ...interface{}) {
logger.mu.Lock()
defer logger.mu.Unlock()
if !logger.setPrefix(lv) {
return
}
logger.log.Println(args...)
logger.clean()
}
func (logger *Logger) logPrintf(lv int, format string, args ...interface{}) {
logger.mu.Lock()
defer logger.mu.Unlock()
if !logger.setPrefix(lv) {
return
}
logger.log.Printf(format, args...)
logger.clean()
}
// Close fd
func (logger *Logger) Close() {
if logger.fd != nil {
logger.Flush()
logger.fd.Close()
}
}
// Info output info log
func (logger *Logger) Info(args ...interface{}) {
logger.logPrint(L_INFO, args...)
}
// Infoln output info log with newline
func (logger *Logger) Infoln(args ...interface{}) {
logger.logPrintln(L_INFO, args...)
}
// Infof output formatted info log
func (logger *Logger) Infof(format string, args ...interface{}) {
logger.logPrintf(L_INFO, format, args...)
}
// Warning output warning log
func (logger *Logger) Warning(args ...interface{}) {
logger.logPrint(L_WARNING, args...)
}
//Warningln output warning log with newline
func (logger *Logger) Warningln(args ...interface{}) {
logger.logPrintln(L_WARNING, args...)
}
// Warningf output formatted warning log
func (logger *Logger) Warningf(format string, args ...interface{}) {
logger.logPrintf(L_WARNING, format, args...)
}
// Debug output debug log
func (logger *Logger) Debug(args ...interface{}) {
logger.logPrint(L_DEBUG, args...)
logger.Flush()
}
// Debugln output debug log with newline
func (logger *Logger) Debugln(args ...interface{}) {
logger.logPrintln(L_DEBUG, args...)
logger.Flush()
}
// Debugf output formatted debug log
func (logger *Logger) Debugf(format string, args ...interface{}) {
logger.logPrintf(L_DEBUG, format, args...)
logger.Flush()
}

View File

@ -1,110 +0,0 @@
// Package resolver is a simple dns resolver
// based on miekg/dns
package resolver
import (
"errors"
"math/rand"
"net"
"os"
"strings"
"time"
"github.com/miekg/dns"
)
// DNSResolver represents a dns resolver
type DNSResolver struct {
Servers []string
RetryTimes int
r *rand.Rand
}
// New initializes DnsResolver.
func New(servers []string) *DNSResolver {
for i := range servers {
servers[i] = net.JoinHostPort(servers[i], "53")
}
return &DNSResolver{servers, len(servers) * 2, rand.New(rand.NewSource(time.Now().UnixNano()))}
}
// NewFromResolvConf initializes DnsResolver from resolv.conf like file.
func NewFromResolvConf(path string) (*DNSResolver, error) {
if _, err := os.Stat(path); os.IsNotExist(err) {
return &DNSResolver{}, errors.New("no such file or directory: " + path)
}
config, err := dns.ClientConfigFromFile(path)
if err != nil {
return &DNSResolver{}, err
}
var servers []string
for _, ipAddress := range config.Servers {
servers = append(servers, net.JoinHostPort(ipAddress, "53"))
}
return &DNSResolver{servers, len(servers) * 2, rand.New(rand.NewSource(time.Now().UnixNano()))}, err
}
// LookupHost returns IP addresses of provied host.
// In case of timeout retries query RetryTimes times.
func (r *DNSResolver) LookupHost(host string, dnsType uint16) ([]net.IP, error) {
return r.lookupHost(host, dnsType, r.RetryTimes)
}
func (r *DNSResolver) lookupHost(host string, dnsType uint16, triesLeft int) ([]net.IP, error) {
m1 := new(dns.Msg)
m1.Id = dns.Id()
m1.RecursionDesired = true
m1.Question = make([]dns.Question, 1)
switch dnsType {
case dns.TypeA:
m1.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: dns.TypeA, Qclass: dns.ClassINET}
case dns.TypeAAAA:
m1.Question[0] = dns.Question{Name: dns.Fqdn(host), Qtype: dns.TypeAAAA, Qclass: dns.ClassINET}
}
in, err := dns.Exchange(m1, r.Servers[r.r.Intn(len(r.Servers))])
var result []net.IP
if err != nil {
if strings.HasSuffix(err.Error(), "i/o timeout") && triesLeft > 0 {
triesLeft--
return r.lookupHost(host, dnsType, triesLeft)
}
return result, err
}
if in != nil && in.Rcode != dns.RcodeSuccess {
return result, errors.New(dns.RcodeToString[in.Rcode])
}
if dnsType == dns.TypeA {
if len(in.Answer) > 0 {
for _, record := range in.Answer {
if t, ok := record.(*dns.A); ok {
result = append(result, t.A)
}
}
} else {
return result, errors.New("empty result")
}
}
if dnsType == dns.TypeAAAA {
if len(in.Answer) > 0 {
for _, record := range in.Answer {
if t, ok := record.(*dns.AAAA); ok {
result = append(result, t.AAAA)
}
}
} else {
return result, errors.New("Cannot resolve this domain, please make sure the IP type is right")
}
}
return result, err
}

View File

@ -1,45 +0,0 @@
package resolver
import (
"fmt"
"reflect"
"testing"
"github.com/miekg/dns"
)
func TestNew(t *testing.T) {
servers := []string{"8.8.8.8", "8.8.4.4"}
expectedServers := []string{"8.8.8.8:53", "8.8.4.4:53"}
resolver := New(servers)
if !reflect.DeepEqual(resolver.Servers, expectedServers) {
t.Error("resolver.Servers: ", resolver.Servers, "should be equal to", expectedServers)
}
}
func TestLookupHost_ValidServer(t *testing.T) {
resolver := New([]string{"8.8.8.8", "8.8.4.4"})
result, err := resolver.LookupHost("google-public-dns-a.google.com", dns.TypeA)
if err != nil {
fmt.Println(err.Error())
t.Error("Should succeed dns lookup")
}
if result[0].String() != "8.8.8.8" {
t.Error("google-public-dns-a.google.com should be resolved to 8.8.8.8")
}
}
func TestLookupHostIPv6_ValidServer(t *testing.T) {
resolver := New([]string{"2001:4860:4860::8888", "2001:4860:4860::8844"})
result, err := resolver.LookupHost("google-public-dns-a.google.com", dns.TypeAAAA)
if err != nil {
fmt.Println(err.Error())
t.Error("Should succeed dns lookup")
}
if result[0].String() != "2001:4860:4860::8888" {
t.Error("result should be: 2001:4860:4860::8888")
}
}

View File

@ -1,75 +1,34 @@
package godns
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
)
// Domain struct
//Domain struct
type Domain struct {
DomainName string `json:"domain_name"`
SubDomains []string `json:"sub_domains"`
}
// Notify struct for slack notification
type SlackNotify struct {
Enabled bool `json:"enabled"`
BotApiToken string `json:"bot_api_token"`
Channel string `json:"channel"`
MsgTemplate string `json:"message_template"`
UseProxy bool `json:"use_proxy"`
}
// Notify struct for telegram notification
type TelegramNotify struct {
Enabled bool `json:"enabled"`
BotApiKey string `json:"bot_api_key"`
ChatId string `json:"chat_id"`
MsgTemplate string `json:"message_template"`
UseProxy bool `json:"use_proxy"`
}
// Notify struct for SMTP notification
type MailNotify 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"`
}
// Notify struct
type Notify struct {
Telegram TelegramNotify `json:"telegram"`
Mail MailNotify `json:"mail"`
Slack SlackNotify `json:"slack"`
}
// Settings struct
//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"`
IPV6Url string `json:"ipv6_url"`
Interval int `json:"interval"`
UserAgent string `json:"user_agent,omitempty"`
LogPath string `json:"log_path"`
LogSize int `json:"log_size"`
LogNum int `json:"log_num"`
Socks5Proxy string `json:"socks5_proxy"`
Notify Notify `json:"notify"`
IPInterface string `json:"ip_interface"`
IPType string `json:"ip_type"`
Resolver string `json:"resolver"`
UseProxy bool `json:"use_proxy"`
}
// LoadSettings -- Load settings from config file
//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!")
@ -82,9 +41,36 @@ func LoadSettings(configPath string, settings *Settings) error {
return err
}
if settings.Interval == 0 {
// set default interval as 5 minutes if interval is 0
settings.Interval = 5 * 60
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 godns
package main
import (
"testing"
@ -13,11 +13,6 @@ func TestLoadSetting(t *testing.T) {
}
if settings.IPUrl == "" {
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")
t.Error("Cannot load ip_url from config file")
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,105 +0,0 @@
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 has been 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>
`

471
utils.go
View File

@ -1,462 +1,51 @@
package godns
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"html/template"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"runtime"
"strings"
dnsResolver "github.com/TimothyYe/godns/resolver"
"github.com/miekg/dns"
"golang.org/x/net/proxy"
"gopkg.in/gomail.v2"
)
var (
// Logo for GoDNS
Logo = `
func identifyPanic() string {
var name, file string
var line int
var pc [16]uintptr
GoDNS V%s
https://github.com/TimothyYe/godns
`
)
const (
// PanicMax is the max allowed panic times
PanicMax = 5
// DNSPOD for dnspod.cn
DNSPOD = "DNSPod"
// HE for he.net
HE = "HE"
// CLOUDFLARE for cloudflare.com
CLOUDFLARE = "Cloudflare"
// ALIDNS for AliDNS
ALIDNS = "AliDNS"
// GOOGLE for Google Domains
GOOGLE = "Google"
// DUCK for Duck DNS
DUCK = "DuckDNS"
// DREAMHOST for Dreamhost
DREAMHOST = "Dreamhost"
// NOIP for NoIP
NOIP = "NoIP"
// IPV4 for IPV4 mode
IPV4 = "IPV4"
// IPV6 for IPV6 mode
IPV6 = "IPV6"
)
//GetIPFromInterface gets IP address from the specific interface
func GetIPFromInterface(configuration *Settings) (string, error) {
ifaces, err := net.InterfaceByName(configuration.IPInterface)
if err != nil {
log.Println("can't get network device "+configuration.IPInterface+":", err)
return "", err
}
addrs, err := ifaces.Addrs()
if err != nil {
log.Println("can't get address from "+configuration.IPInterface+":", err)
return "", err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil {
n := runtime.Callers(3, pc[:])
for _, pc := range pc[:n] {
fn := runtime.FuncForPC(pc)
if fn == nil {
continue
}
if !(ip.IsGlobalUnicast() &&
!(ip.IsUnspecified() ||
ip.IsMulticast() ||
ip.IsLoopback() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsInterfaceLocalMulticast())) {
continue
}
if isIPv4(ip.String()) {
if strings.ToUpper(configuration.IPType) != IPV4 {
continue
}
} else {
if strings.ToUpper(configuration.IPType) != IPV6 {
continue
}
}
if ip.String() != "" {
return ip.String(), nil
file, line = fn.FileLine(pc)
name = fn.Name()
if !strings.HasPrefix(name, "runtime.") {
break
}
}
return "", errors.New("can't get a vaild address from " + configuration.IPInterface)
switch {
case name != "":
return fmt.Sprintf("%v:%v", name, line)
case file != "":
return fmt.Sprintf("%v:%v", file, line)
}
return fmt.Sprintf("pc:%x", pc)
}
func isIPv4(ip string) bool {
return strings.Count(ip, ":") < 2
func usage() {
log.Println("[command] -c=[config file path]")
flag.PrintDefaults()
}
// GetHttpClient creates the HTTP client and return it
func GetHttpClient(configuration *Settings, useProxy bool) *http.Client {
client := &http.Client{}
if useProxy && 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 nil
}
httpTransport := &http.Transport{}
client.Transport = httpTransport
httpTransport.Dial = dialer.Dial
}
return client
}
//GetCurrentIP gets an IP from either internet or specific interface, depending on configuration
func GetCurrentIP(configuration *Settings) (string, error) {
var err error
if configuration.IPUrl != "" || configuration.IPV6Url != "" {
ip, err := GetIPOnline(configuration)
if err != nil {
log.Println("get ip online failed. Fallback to get ip from interface if possible.")
} else {
return ip, nil
}
}
if configuration.IPInterface != "" {
ip, err := GetIPFromInterface(configuration)
if err != nil {
log.Println("get ip from interface failed. There is no more ways to try.")
} else {
return ip, nil
}
}
return "", err
}
// GetIPOnline gets public IP from internet
func GetIPOnline(configuration *Settings) (string, error) {
client := &http.Client{}
var response *http.Response
var err error
if configuration.IPType == "" || strings.ToUpper(configuration.IPType) == IPV4 {
response, err = client.Get(configuration.IPUrl)
} else {
response, err = client.Get(configuration.IPV6Url)
}
if err != nil {
log.Println("Cannot get IP...")
return "", err
}
defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
return strings.Trim(string(body), "\n"), nil
}
// CheckSettings check the format of settings
func CheckSettings(config *Settings) error {
switch config.Provider {
case DNSPOD:
if config.Password == "" && config.LoginToken == "" {
return errors.New("password or login token cannot be empty")
}
case HE:
if config.Password == "" {
return errors.New("password cannot be empty")
}
case CLOUDFLARE:
if config.LoginToken == "" {
if config.Email == "" {
return errors.New("email cannot be empty")
}
if config.Password == "" {
return errors.New("password cannot be empty")
}
}
case ALIDNS:
if config.Email == "" {
return errors.New("email cannot be empty")
}
if config.Password == "" {
return errors.New("password cannot be empty")
}
case DUCK:
if config.LoginToken == "" {
return errors.New("login token cannot be empty")
}
case GOOGLE:
fallthrough
case NOIP:
if config.Email == "" {
return errors.New("email cannot be empty")
}
if config.Password == "" {
return errors.New("password cannot be empty")
}
case DREAMHOST:
if config.LoginToken == "" {
return errors.New("login token cannot be empty")
}
default:
return errors.New("please provide supported DNS provider: DNSPod/HE/AliDNS/Cloudflare/GoogleDomain/DuckDNS/Dreamhost")
func checkSettings(config *Settings) error {
if (config.Email == "" || config.Password == "") && config.LoginToken == "" {
return errors.New("Input email/password or login token cannot be empty!")
}
return nil
}
// SendTelegramNotify sends notify if IP is changed
func SendTelegramNotify(configuration *Settings, domain, currentIP string) error {
if !configuration.Notify.Telegram.Enabled {
return nil
}
if configuration.Notify.Telegram.BotApiKey == "" {
return errors.New("bot api key cannot be empty")
}
if configuration.Notify.Telegram.ChatId == "" {
return errors.New("chat id cannot be empty")
}
client := GetHttpClient(configuration, configuration.Notify.Telegram.UseProxy)
tpl := configuration.Notify.Telegram.MsgTemplate
if tpl == "" {
tpl = "_Your IP address is changed to_%0A%0A*{{ .CurrentIP }}*%0A%0ADomain *{{ .Domain }}* is updated"
}
msg := buildTemplate(currentIP, domain, tpl)
reqURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&parse_mode=Markdown&text=%s",
configuration.Notify.Telegram.BotApiKey,
configuration.Notify.Telegram.ChatId,
msg)
var response *http.Response
var err error
response, err = client.Get(reqURL)
if err != nil {
return err
}
defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
type ResponseParameters struct {
MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
RetryAfter int `json:"retry_after"` // optional
}
type APIResponse struct {
Ok bool `json:"ok"`
Result json.RawMessage `json:"result"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
Parameters *ResponseParameters `json:"parameters"`
}
var resp APIResponse
err = json.Unmarshal(body, &resp)
if err != nil {
fmt.Println("error:", err)
return errors.New("failed to parse response")
}
if !resp.Ok {
return errors.New(resp.Description)
}
return nil
}
// SendMailNotify sends mail notify if IP is changed
func SendMailNotify(configuration *Settings, domain, currentIP string) error {
if !configuration.Notify.Mail.Enabled {
return nil
}
log.Print("Sending notification to:", configuration.Notify.Mail.SendTo)
m := gomail.NewMessage()
m.SetHeader("From", configuration.Notify.Mail.SMTPUsername)
m.SetHeader("To", configuration.Notify.Mail.SendTo)
m.SetHeader("Subject", "GoDNS Notification")
log.Println("currentIP:", currentIP)
log.Println("domain:", domain)
m.SetBody("text/html", buildTemplate(currentIP, domain, mailTemplate))
d := gomail.NewDialer(configuration.Notify.Mail.SMTPServer, configuration.Notify.Mail.SMTPPort, configuration.Notify.Mail.SMTPUsername, configuration.Notify.Mail.SMTPPassword)
// Send the email config by sendlist .
if err := d.DialAndSend(m); err != nil {
return err
}
return nil
}
// SendSlack sends slack if IP is changed
func SendSlackNotify(configuration *Settings, domain, currentIP string) error {
if !configuration.Notify.Slack.Enabled {
return nil
}
if configuration.Notify.Slack.BotApiToken == "" {
return errors.New("bot api token cannot be empty")
}
if configuration.Notify.Slack.Channel == "" {
return errors.New("channel cannot be empty")
}
client := GetHttpClient(configuration, configuration.Notify.Slack.UseProxy)
tpl := configuration.Notify.Slack.MsgTemplate
if tpl == "" {
tpl = "_Your IP address is changed to_\n\n*{{ .CurrentIP }}*\n\nDomain *{{ .Domain }}* is updated"
}
msg := buildTemplate(currentIP, domain, tpl)
var response *http.Response
var err error
formData := url.Values{
"token": {configuration.Notify.Slack.BotApiToken},
"channel": {configuration.Notify.Slack.Channel},
"text": {msg},
}
response, err = client.PostForm("https://slack.com/api/chat.postMessage", formData)
if err != nil {
return err
}
defer response.Body.Close()
body, _ := ioutil.ReadAll(response.Body)
type ResponseParameters struct {
MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
RetryAfter int `json:"retry_after"` // optional
}
type APIResponse struct {
Ok bool `json:"ok"`
Result json.RawMessage `json:"result"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
Parameters *ResponseParameters `json:"parameters"`
}
var resp APIResponse
err = json.Unmarshal(body, &resp)
if err != nil {
fmt.Println("error:", err)
return errors.New("failed to parse response")
}
if !resp.Ok {
return errors.New(resp.Description)
}
return nil
}
// SendNotify sends notify if IP is changed
func SendNotify(configuration *Settings, domain, currentIP string) error {
err := SendTelegramNotify(configuration, domain, currentIP)
if err != nil {
log.Println("Send telegram notification with error:", err.Error())
}
err = SendMailNotify(configuration, domain, currentIP)
if err != nil {
log.Println("Send email notification with error:", err.Error())
}
err = SendSlackNotify(configuration, domain, currentIP)
if err != nil {
log.Println("Send slack notification with error:", err.Error())
}
return nil
}
func buildTemplate(currentIP, domain string, tplsrc string) string {
t := template.New("notification template")
if _, err := t.Parse(tplsrc); err != nil {
log.Println("Failed to parse template")
return ""
}
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()
}
// ResolveDNS will query DNS for a given hostname.
func ResolveDNS(hostname, resolver, ipType string) (string, error) {
var dnsType uint16
if ipType == "" || strings.ToUpper(ipType) == IPV4 {
dnsType = dns.TypeA
} else {
dnsType = dns.TypeAAAA
}
// If no DNS server is set in config file, falls back to default resolver.
if resolver == "" {
dnsAdress, err := net.LookupHost(hostname)
if err != nil {
return "<nil>", err
}
return dnsAdress[0], nil
}
res := dnsResolver.New([]string{resolver})
// In case of i/o timeout
res.RetryTimes = 5
ip, err := res.LookupHost(hostname, dnsType)
if err != nil {
return "<nil>", err
}
return ip[0].String(), nil
}

View File

@ -1,42 +0,0 @@
package godns
import (
"testing"
)
func TestGetCurrentIP(t *testing.T) {
conf := &Settings{IPUrl: "https://myip.biturl.top"}
ip, _ := GetCurrentIP(conf)
if ip == "" {
t.Log("IP is empty...")
} else {
t.Log("IP is:" + ip)
}
}
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")
}
}