mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-21 08:32:16 -04:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
106bcae966 | ||
|
|
0a89666d1d | ||
|
|
3ad9168576 | ||
|
|
1eaa495e66 | ||
|
|
5e0cc687ea | ||
|
|
289536b145 | ||
|
|
d5e4936679 | ||
|
|
6e18e921b7 | ||
|
|
40c92eebf5 | ||
|
|
8adc0556ba | ||
|
|
e7824014ee | ||
|
|
6e0d48f7c1 | ||
|
|
a10fb64ffd | ||
|
|
a649f8a4a8 | ||
|
|
72018451b3 | ||
|
|
2e5b3c7924 | ||
|
|
c985595969 | ||
|
|
caa4840a61 | ||
|
|
a86ddd42d1 | ||
|
|
ce1a447e0a | ||
|
|
22c8b587c9 | ||
|
|
78c86b0e24 | ||
|
|
fa771cd4b2 | ||
|
|
4e94823f69 | ||
|
|
96521addd5 | ||
|
|
85ddad6da1 | ||
|
|
d54c334e1c | ||
|
|
49392003e0 | ||
|
|
5a5e0c7375 | ||
|
|
180092f47e | ||
|
|
43e168581c | ||
|
|
76a40e47c7 | ||
|
|
50b1997dbb | ||
|
|
d75398d71b | ||
|
|
7191e93932 | ||
|
|
ec1f7acbde | ||
|
|
a8a8d5793b | ||
|
|
dde28ebd1f | ||
|
|
cd5f04acaf | ||
|
|
5d1809aca6 | ||
|
|
46ae9af6a4 | ||
|
|
b9a357fc1c | ||
|
|
1e7c909baf | ||
|
|
e83857b3db | ||
|
|
5ab1607f97 | ||
|
|
4bb09e86dd | ||
|
|
9460fa969b | ||
|
|
157e041b28 | ||
|
|
b40391bb4e | ||
|
|
60cf3130e3 | ||
|
|
5557c32135 | ||
|
|
46e8647d35 | ||
|
|
eb1f925576 | ||
|
|
18b058f188 | ||
|
|
be89e798d2 | ||
|
|
20704eea8c | ||
|
|
6416c6fed3 | ||
|
|
a317809ff8 | ||
|
|
7ae9f72038 | ||
|
|
5b1bc29ad4 | ||
|
|
e937ee741c | ||
|
|
688aebbc4f | ||
|
|
6c2c2cf7cb | ||
|
|
1c3e4cdef7 | ||
|
|
d8a7fef6bd | ||
|
|
87f59bf498 | ||
|
|
818f7471dd | ||
|
|
b35658f32c | ||
|
|
48fb1a4b95 | ||
|
|
2e069ccf1d | ||
|
|
0d38e9385f | ||
|
|
b4310ad822 | ||
|
|
878cf4cc45 | ||
|
|
89faafe377 | ||
|
|
bf3f78f9f9 | ||
|
|
6bf82d7be1 | ||
|
|
77f1681c4c | ||
|
|
82e3d60db5 | ||
|
|
c65c8d63bd | ||
|
|
0f1ddfb9b0 | ||
|
|
1d466cdc83 | ||
|
|
0a6ef7ffbf | ||
|
|
e7ae5ac4cc | ||
|
|
701ae125bf | ||
|
|
b775798b65 | ||
|
|
166b0c7095 | ||
|
|
3240bb7d26 | ||
|
|
3047c83ee9 | ||
|
|
3b29a33849 | ||
|
|
860bc02e2e | ||
|
|
cd2d3c46cc | ||
|
|
e630dd9889 | ||
|
|
b2d96787b8 | ||
|
|
5b52255601 | ||
|
|
04c55028a1 | ||
|
|
e07e8da31c | ||
|
|
af2f3a3257 | ||
|
|
00efca4af4 | ||
|
|
3272612db2 | ||
|
|
5b7968c468 | ||
|
|
7ec39c1256 | ||
|
|
96857f3bae | ||
|
|
57c7d1be2d | ||
|
|
53b6f533a8 | ||
|
|
a82ed93169 | ||
|
|
d07fcc664b | ||
|
|
d013ceb869 | ||
|
|
d3506e9792 | ||
|
|
c0249672bf | ||
|
|
cfeb95872a | ||
|
|
091cf5f855 | ||
|
|
7001add533 | ||
|
|
9ccdbbd2d3 | ||
|
|
f8a3ab63c6 | ||
|
|
14033223d9 | ||
|
|
18161a6064 | ||
|
|
216b8ab1ae | ||
|
|
4af23a756b | ||
|
|
96c84a5a4f | ||
|
|
4564d16c06 | ||
|
|
6e4a56b3cf | ||
|
|
919ab65985 | ||
|
|
e023aae909 | ||
|
|
066bcdd3bf | ||
|
|
0a6c6b9bc7 | ||
|
|
8cdff8e4d3 | ||
|
|
bffc30264f | ||
|
|
4f141c20a0 | ||
|
|
582ce626c8 | ||
|
|
13b29aeba4 | ||
|
|
a5afca15d1 | ||
|
|
25ee692242 | ||
|
|
922146efd3 | ||
|
|
db9959cf59 | ||
|
|
50303aef7b | ||
|
|
137e372102 | ||
|
|
f300c59411 | ||
|
|
c23998bd09 |
@@ -12,7 +12,7 @@
|
||||
"workspaceFolder": "/workspace",
|
||||
"appPort": 8000,
|
||||
"extensions": [
|
||||
"ms-vscode.go",
|
||||
"golang.go",
|
||||
"IBM.output-colorizer",
|
||||
"eamodio.gitlens",
|
||||
"mhutchie.git-graph",
|
||||
@@ -23,7 +23,6 @@
|
||||
"mohsen1.prettify-json",
|
||||
"quicktype.quicktype",
|
||||
"spikespaz.vscode-smoothtype",
|
||||
"stkb.rewrap",
|
||||
"vscode-icons-team.vscode-icons"
|
||||
],
|
||||
"settings": {
|
||||
@@ -44,53 +43,6 @@
|
||||
"usePlaceholders": false
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": [
|
||||
"--fast",
|
||||
"--enable",
|
||||
"rowserrcheck",
|
||||
"--enable",
|
||||
"bodyclose",
|
||||
"--enable",
|
||||
"dogsled",
|
||||
"--enable",
|
||||
"dupl",
|
||||
"--enable",
|
||||
"gochecknoglobals",
|
||||
"--enable",
|
||||
"gochecknoinits",
|
||||
"--enable",
|
||||
"gocognit",
|
||||
"--enable",
|
||||
"goconst",
|
||||
"--enable",
|
||||
"gocritic",
|
||||
"--enable",
|
||||
"gocyclo",
|
||||
"--enable",
|
||||
"goimports",
|
||||
"--enable",
|
||||
"golint",
|
||||
"--enable",
|
||||
"gosec",
|
||||
"--enable",
|
||||
"interfacer",
|
||||
"--enable",
|
||||
"maligned",
|
||||
"--enable",
|
||||
"misspell",
|
||||
"--enable",
|
||||
"nakedret",
|
||||
"--enable",
|
||||
"prealloc",
|
||||
"--enable",
|
||||
"scopelint",
|
||||
"--enable",
|
||||
"unconvert",
|
||||
"--enable",
|
||||
"unparam",
|
||||
"--enable",
|
||||
"whitespace"
|
||||
],
|
||||
// Golang on save
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
@@ -112,6 +64,6 @@
|
||||
"go.testFlags": [
|
||||
"-v"
|
||||
],
|
||||
"go.testTimeout": "600s"
|
||||
"go.testTimeout": "5s"
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ services:
|
||||
image: qmcgaw/godevcontainer
|
||||
volumes:
|
||||
- ../:/workspace
|
||||
- ~/.ssh:/home/vscode/.ssh:ro
|
||||
- ~/.ssh:/root/.ssh:ro
|
||||
- ~/.ssh:/home/vscode/.ssh
|
||||
- ~/.ssh:/root/.ssh
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
cap_add:
|
||||
- SYS_PTRACE
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
docs
|
||||
readme
|
||||
.gitignore
|
||||
config.json
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
README.md
|
||||
ui/favicon.svg
|
||||
|
||||
43
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Report a bug
|
||||
title: 'Bug: ...'
|
||||
labels: ":bug: bug"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
YOU CAN CHAT THERE EVENTUALLY:
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
<!-- See the line at the top of your logs -->
|
||||
|
||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
||||
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature to add to this project
|
||||
title: 'Feature request: ...'
|
||||
labels: ":bulb: feature request"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
1. What's the feature?
|
||||
|
||||
2. Extra information?
|
||||
|
||||
<!--
|
||||
|
||||
YOU CAN CHAT THERE EVENTUALLY:
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
43
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Help
|
||||
about: Ask for help
|
||||
title: 'Help: ...'
|
||||
labels: ":pray: help wanted"
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
HAVE A CHAT FIRST!
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
<!-- See the line at the top of your logs -->
|
||||
|
||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
||||
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
109
.github/workflows/build.yml
vendored
109
.github/workflows/build.yml
vendored
@@ -1,12 +1,107 @@
|
||||
name: Docker build
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
jobs:
|
||||
build:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- name: Go mod tidy check
|
||||
run: docker build --target tidy .
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container \
|
||||
go test \
|
||||
-race \
|
||||
-coverpkg=./... \
|
||||
-coverprofile=coverage.txt \
|
||||
-covermode=atomic \
|
||||
./...
|
||||
|
||||
# We run this here to use the caching of the previous steps
|
||||
- name: Build final image
|
||||
run: docker build .
|
||||
|
||||
publish:
|
||||
needs: [verify]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: docker build .
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Set variables
|
||||
id: vars
|
||||
run: |
|
||||
BRANCH=${GITHUB_REF#refs/heads/}
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo ::set-output name=commit::$(git rev-parse --short HEAD)
|
||||
echo ::set-output name=build_date::$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
if [ "$TAG" != "$GITHUB_REF" ]; then
|
||||
echo ::set-output name=version::$TAG
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
elif [ "$BRANCH" = "master" ]; then
|
||||
echo ::set-output name=version::latest
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
else
|
||||
echo ::set-output name=version::$BRANCH
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,
|
||||
fi
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: ${{ steps.vars.outputs.platforms }}
|
||||
build-args: |
|
||||
BUILD_DATE=${{ steps.vars.outputs.build_date }}
|
||||
COMMIT=${{ steps.vars.outputs.commit }}
|
||||
VERSION=${{ steps.vars.outputs.version }}
|
||||
tags: qmcgaw/ddns-updater:${{ steps.vars.outputs.version }}
|
||||
push: true
|
||||
|
||||
- if: github.event.ref == 'refs/heads/master'
|
||||
name: Microbadger hook
|
||||
run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s=
|
||||
continue-on-error: true
|
||||
|
||||
40
.github/workflows/buildx-latest.yml
vendored
40
.github/workflows/buildx-latest.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Buildx latest
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg VERSION=latest \
|
||||
-t qmcgaw/ddns-updater:latest \
|
||||
--push \
|
||||
.
|
||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0
|
||||
40
.github/workflows/buildx-release.yml
vendored
40
.github/workflows/buildx-release.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Buildx release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-latest.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg VERSION=${GITHUB_REF##*/} \
|
||||
-t qmcgaw/ddns-updater:${GITHUB_REF##*/} \
|
||||
--push \
|
||||
.
|
||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0
|
||||
11
.github/workflows/greetings.yml
vendored
11
.github/workflows/greetings.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Greetings
|
||||
on: [pull_request, issues]
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Thanks for creating your first issue :+1: Feel free to use [Slack](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc) if you just need some quick help or want to chat'
|
||||
pr-message: 'Thank you so much for contributing, that means a lot to me :wink:'
|
||||
6
.github/workflows/labels.yml
vendored
6
.github/workflows/labels.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: labels
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
branches: [master]
|
||||
paths:
|
||||
- '.github/labels.yml'
|
||||
- '.github/workflows/labels.yml'
|
||||
- .github/labels.yml
|
||||
- .github/workflows/labels.yml
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
16
.github/workflows/misspell.yml
vendored
16
.github/workflows/misspell.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: Misspells
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
jobs:
|
||||
misspell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: reviewdog/action-misspell@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
locale: "US"
|
||||
level: error
|
||||
59
.github/workflows/security.yml
vendored
59
.github/workflows/security.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Security scan of Docker image
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
schedule:
|
||||
- cron: '0 9 * * *'
|
||||
jobs:
|
||||
security-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Check for scratch
|
||||
id: scratchCheck
|
||||
run: echo ::set-output name=scratch::$(cat Dockerfile | grep 'FROM scratch')
|
||||
- name: Build image
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
run: docker build -t image .
|
||||
- name: Phonito
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: phonito/phonito-scanner-action@master
|
||||
with:
|
||||
image: image
|
||||
fail-level: LOW
|
||||
phonito-token: ${{ secrets.PHONITO_TOKEN }}
|
||||
- name: Trivy
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: homoluctus/gitrivy@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
image: image
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
*.exe
|
||||
updater
|
||||
.vscode
|
||||
|
||||
@@ -4,33 +4,80 @@ linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: cmd/
|
||||
text: buildInfo is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: cmd/
|
||||
text: commit is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: cmd/
|
||||
text: buildDate is a global variable
|
||||
linters:
|
||||
- gochecknoglobals
|
||||
- path: cmd/updater/main.go
|
||||
text: "mnd: Magic number: 4, in <argument> detected"
|
||||
linters:
|
||||
- gomnd
|
||||
- path: cmd/updater/main.go
|
||||
text: "mnd: Magic number: 2, in <argument> detected"
|
||||
linters:
|
||||
- gomnd
|
||||
- path: internal/regex/regex.go
|
||||
text: "regexpMust: for const patterns like "
|
||||
linters:
|
||||
- gocritic
|
||||
- path: internal/settings/
|
||||
linters:
|
||||
- maligned
|
||||
- dupl
|
||||
- path: pkg/publicip/.*_test.go
|
||||
linters:
|
||||
- dupl
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- gci
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goheader
|
||||
- goimports
|
||||
- golint
|
||||
- gomnd
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- lll
|
||||
- maligned
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
@@ -44,6 +91,3 @@ run:
|
||||
skip-dirs:
|
||||
- .devcontainer
|
||||
- .github
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.26.x # use the fixed version to not introduce new linters unexpectedly
|
||||
|
||||
88
.vscode/settings.json
vendored
88
.vscode/settings.json
vendored
@@ -1,88 +0,0 @@
|
||||
{
|
||||
// General settings
|
||||
"files.eol": "\n",
|
||||
// Docker
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
},
|
||||
// Golang general settings
|
||||
"go.useLanguageServer": true,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.gotoSymbol.includeImports": true,
|
||||
"go.gotoSymbol.includeGoroot": true,
|
||||
"gopls": {
|
||||
"completeUnimported": true,
|
||||
"deepCompletion": true,
|
||||
"usePlaceholders": false
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": [
|
||||
"--fast",
|
||||
"--enable",
|
||||
"rowserrcheck",
|
||||
"--enable",
|
||||
"bodyclose",
|
||||
"--enable",
|
||||
"dogsled",
|
||||
"--enable",
|
||||
"dupl",
|
||||
"--enable",
|
||||
"gochecknoglobals",
|
||||
"--enable",
|
||||
"gochecknoinits",
|
||||
"--enable",
|
||||
"gocognit",
|
||||
"--enable",
|
||||
"goconst",
|
||||
"--enable",
|
||||
"gocritic",
|
||||
"--enable",
|
||||
"gocyclo",
|
||||
"--enable",
|
||||
"goimports",
|
||||
"--enable",
|
||||
"golint",
|
||||
"--enable",
|
||||
"gosec",
|
||||
"--enable",
|
||||
"interfacer",
|
||||
"--enable",
|
||||
"maligned",
|
||||
"--enable",
|
||||
"misspell",
|
||||
"--enable",
|
||||
"nakedret",
|
||||
"--enable",
|
||||
"prealloc",
|
||||
"--enable",
|
||||
"scopelint",
|
||||
"--enable",
|
||||
"unconvert",
|
||||
"--enable",
|
||||
"unparam",
|
||||
"--enable",
|
||||
"whitespace"
|
||||
],
|
||||
// Golang on save
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
// Golang testing
|
||||
"go.toolsEnvVars": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"gopls.env": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"go.testEnvVars": {},
|
||||
"go.testFlags": [
|
||||
"-v"
|
||||
],
|
||||
"go.testTimeout": "600s"
|
||||
}
|
||||
103
Dockerfile
103
Dockerfile
@@ -1,54 +1,103 @@
|
||||
ARG ALPINE_VERSION=3.11
|
||||
ARG GO_VERSION=1.14
|
||||
ARG ALPINE_VERSION=3.13
|
||||
ARG GO_VERSION=1.16
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS alpine
|
||||
FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates tzdata
|
||||
RUN mkdir /tmp/data && \
|
||||
chown 1000 /tmp/data && \
|
||||
chmod 700 /tmp/data
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
ARG GOLANGCI_LINT_VERSION=v1.26.0
|
||||
RUN apk --update add git
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
ENV CGO_ENABLED=0
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
|
||||
RUN apk --update add git
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY .golangci.yml .
|
||||
# Copy repository code and install Go dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download 2>&1
|
||||
RUN go mod download
|
||||
COPY pkg/ ./pkg/
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
COPY cmd/updater/main.go .
|
||||
RUN go test ./...
|
||||
RUN go build -ldflags="-s -w" -o app
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS test
|
||||
ENV CGO_ENABLED=1
|
||||
# g++ is installed for the -race detector in go test
|
||||
RUN apk --update add g++
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS lint
|
||||
ARG GOLANGCI_LINT_VERSION=v1.37.0
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
|
||||
sh -s -- -b /usr/local/bin ${GOLANGCI_LINT_VERSION}
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS tidy
|
||||
RUN git init && \
|
||||
git config user.email ci@localhost && \
|
||||
git config user.name ci && \
|
||||
git add -A && git commit -m ci && \
|
||||
sed -i '/\/\/ indirect/d' go.mod && \
|
||||
go mod tidy && \
|
||||
git diff --exit-code -- go.mod
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS build
|
||||
COPY --from=qmcgaw/xcputranslate:v0.4.0 /xcputranslate /usr/local/bin/xcputranslate
|
||||
ARG TARGETPLATFORM
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
RUN GOARCH="$(xcputranslate -targetplatform ${TARGETPLATFORM} -field arch)" \
|
||||
GOARM="$(xcputranslate -targetplatform ${TARGETPLATFORM} -field arm)" \
|
||||
go build -trimpath -ldflags="-s -w \
|
||||
-X 'main.version=$VERSION' \
|
||||
-X 'main.buildDate=$BUILD_DATE' \
|
||||
-X 'main.commit=$COMMIT' \
|
||||
" -o app cmd/updater/main.go
|
||||
|
||||
FROM scratch
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.revision=$VCS_REF \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.revision=$COMMIT \
|
||||
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.title="ddns-updater" \
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Namecheap, Cloudflare, GoDaddy, DuckDns, Dreamhost, DNSPod and NoIP"
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI"
|
||||
COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
|
||||
USER 1000
|
||||
ENTRYPOINT ["/updater/app"]
|
||||
ENV DELAY=10m \
|
||||
ROOT_URL=/ \
|
||||
LISTENING_PORT=8000 \
|
||||
LOG_ENCODING=console \
|
||||
LOG_LEVEL=info \
|
||||
NODE_ID=0 \
|
||||
ENV \
|
||||
# Core
|
||||
CONFIG= \
|
||||
PERIOD=5m \
|
||||
UPDATE_COOLDOWN_PERIOD=5m \
|
||||
IP_METHOD=all \
|
||||
IPV4_METHOD=all \
|
||||
IPV6_METHOD=all \
|
||||
HTTP_TIMEOUT=10s \
|
||||
|
||||
# Web UI
|
||||
LISTENING_PORT=8000 \
|
||||
ROOT_URL=/ \
|
||||
|
||||
# Backup
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data \
|
||||
|
||||
# Other
|
||||
LOG_LEVEL=info \
|
||||
LOG_CALLER=hidden \
|
||||
GOTIFY_URL= \
|
||||
GOTIFY_TOKEN= \
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data
|
||||
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
|
||||
TZ=
|
||||
COPY --from=alpine --chown=1000 /tmp/data /updater/data/
|
||||
COPY --from=build --chown=1000 /tmp/gobuild/app /updater/app
|
||||
COPY --chown=1000 ui/* /updater/ui/
|
||||
|
||||
394
README.md
394
README.md
@@ -1,8 +1,8 @@
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
|
||||
*Light container updating DNS A records periodically for GoDaddy, Namecheap, Cloudflare, Dreamhost, NoIP, DNSPod, Infomaniak, ddnss.de and DuckDNS*
|
||||
*Light container updating DNS A and/or AAAA records periodically for multiple DNS providers*
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg?sanitize=true">
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
@@ -17,12 +17,36 @@
|
||||
|
||||
## Features
|
||||
|
||||
- Updates periodically A records for different DNS providers: Namecheap, GoDaddy, Cloudflare, NoIP, Dreamhost, DuckDNS, DNSPod and Infomaniak (ask for more)
|
||||
- Updates periodically A records for different DNS providers:
|
||||
- Cloudflare
|
||||
- DDNSS.de
|
||||
- DigitalOcean
|
||||
- DonDominio
|
||||
- DNSOMatic
|
||||
- DNSPod
|
||||
- Dreamhost
|
||||
- DuckDNS
|
||||
- DynDNS
|
||||
- FreeDNS
|
||||
- Gandi
|
||||
- GoDaddy
|
||||
- Google
|
||||
- He.net
|
||||
- Infomaniak
|
||||
- Linode
|
||||
- LuaDNS
|
||||
- Namecheap
|
||||
- NoIP
|
||||
- OpenDNS
|
||||
- OVH
|
||||
- Selfhost.de
|
||||
- Strato.de
|
||||
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
|
||||
- Web User interface
|
||||
|
||||

|
||||
|
||||
- 12.3MB Docker image based on a Go static binary in a Scratch Docker image with ca-certificates and timezone data
|
||||
- 14MB Docker image based on a Go static binary in a Scratch Docker image with ca-certificates and timezone data
|
||||
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
|
||||
- Docker healthcheck verifying the DNS resolution of your domains
|
||||
- Highly configurable
|
||||
@@ -31,7 +55,8 @@
|
||||
|
||||
## Setup
|
||||
|
||||
1. To setup your domains initially, see the [Domain set up](#domain-set-up) section.
|
||||
The program reads the configuration from a JSON object, either from a file or from an environment variable.
|
||||
|
||||
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
|
||||
|
||||
```sh
|
||||
@@ -47,7 +72,7 @@
|
||||
|
||||
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
|
||||
|
||||
1. Modify the *data/config.json* file similarly to:
|
||||
1. Write a JSON configuration in *data/config.json*, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -56,162 +81,121 @@
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "duckduckgo",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See more information in the [configuration section](#configuration)
|
||||
You can find more information in the [configuration section](#configuration) to customize it.
|
||||
|
||||
1. Use the following command:
|
||||
1. Run the container with
|
||||
|
||||
```bash
|
||||
```sh
|
||||
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
1. (Optional) You can also set your JSON configuration as a single environment variable line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`), which takes precedence over config.json. Note however that if you don't bind mount the `/updater/data` directory, there won't be a persistent database file `/updater/updates.json` but it will still work.
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
### Next steps
|
||||
|
||||
1. You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
|
||||
## Configuration
|
||||
|
||||
Start by having the following content in *config.json*:
|
||||
Start by having the following content in *config.json*, or in your `CONFIG` environment variable:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
},
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following parameters are to be added in *config.json*
|
||||
For each setting, you need to fill in parameters.
|
||||
Check the documentation for your DNS provider:
|
||||
|
||||
For all record update configuration, you need the following:
|
||||
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
|
||||
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
|
||||
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
|
||||
- [DonDominio](https://github.com/qdm12/ddns-updater/blob/master/docs/dondominio.md)
|
||||
- [DNSOMatic](https://github.com/qdm12/ddns-updater/blob/master/docs/dnsomatic.md)
|
||||
- [DNSPod](https://github.com/qdm12/ddns-updater/blob/master/docs/dnspod.md)
|
||||
- [Dreamhost](https://github.com/qdm12/ddns-updater/blob/master/docs/dreamhost.md)
|
||||
- [DuckDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/duckdns.md)
|
||||
- [DynDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/dyndns.md)
|
||||
- [DynV6](https://github.com/qdm12/ddns-updater/blob/master/docs/dynv6.md)
|
||||
- [FreeDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/freedns.md)
|
||||
- [Gandi](https://github.com/qdm12/ddns-updater/blob/master/docs/gandi.md)
|
||||
- [GoDaddy](https://github.com/qdm12/ddns-updater/blob/master/docs/godaddy.md)
|
||||
- [Google](https://github.com/qdm12/ddns-updater/blob/master/docs/google.md)
|
||||
- [He.net](https://github.com/qdm12/ddns-updater/blob/master/docs/he.net.md)
|
||||
- [Infomaniak](https://github.com/qdm12/ddns-updater/blob/master/docs/infomaniak.md)
|
||||
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
|
||||
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
|
||||
- [Namecheap](https://github.com/qdm12/ddns-updater/blob/master/docs/namecheap.md)
|
||||
- [NoIP](https://github.com/qdm12/ddns-updater/blob/master/docs/noip.md)
|
||||
- [OpenDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/opendns.md)
|
||||
- [OVH](https://github.com/qdm12/ddns-updater/blob/master/docs/ovh.md)
|
||||
- [Selfhost.de](https://github.com/qdm12/ddns-updater/blob/master/docs/selfhost.de.md)
|
||||
- [Strato.de](https://github.com/qdm12/ddns-updater/blob/master/docs/strato.md)
|
||||
|
||||
- `"provider"` is the DNS provider and can be `"godaddy"`, `"namecheap"`, `"duckdns"`, `"dreamhost"`, `"cloudflare"`, `"noip"`, `"dnspod"` or `"ddnss"`
|
||||
- `"domain"`
|
||||
- `"ip_method"` is the method to obtain your public IP address and can be:
|
||||
- `"provider"` means the public IP is automatically determined by the DNS provider (**only for DuckDNs, Namecheap, Infomaniak and NoIP**), most reliable.
|
||||
- `"opendns"` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip) (reliable)
|
||||
- `"ifconfig"` using [https://ifconfig.io/ip](https://ifconfig.io/ip) (may be rate limited)
|
||||
- `"ipinfo"` using [https://ipinfo.io/ip](https://ipinfo.io/ip) (may be rate limited)
|
||||
- `"ipify"` using [https://api.ipify.org](https://api.ipify.org) (may be rate limited)
|
||||
- `"ipify6"` using [https://api6.ipify.org](https://api.ipify.org) for IPv6 only (may be rate limited)
|
||||
- `"ddnss"` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `"ddnss4"` using [https://ip4.ddnss.de/meineip.php](https://ip4.ddnss.de/meineip.php) for IPv4 only
|
||||
- `"ddnss6"` using [https://ip6.ddnss.de/meineip.php](https://ip6.ddnss.de/meineip.php) for IPv6 only
|
||||
- `"cycle"` to cycle between each external methods, in order to avoid being rate limited
|
||||
- You can also specify an HTTPS URL to obtain your public IP address (i.e. `"ip_method": "https://ipinfo.io/ip"`)
|
||||
Note that:
|
||||
|
||||
You can optionnally add the parameters:
|
||||
|
||||
- `"delay"` is the delay in seconds between each update. It defaults to the `DELAY` environment variable value.
|
||||
- `"no_dns_lookup"` can be `true` or `false` and allows, if `true`, to prevent the periodic Docker healthcheck from running a DNS lookup on your domain.
|
||||
|
||||
For each DNS provider exist some specific parameters you need to add, as described below:
|
||||
|
||||
Namecheap:
|
||||
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"password"`
|
||||
|
||||
Cloudflare:
|
||||
|
||||
- `"zone_identifier"` is the Zone ID of your site
|
||||
- `"identifier"` is the DNS record identifier as returned by the Cloudflare "List DNS Records" API (see below)
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following:
|
||||
- Email `"email"` and Global API Key `"key"`
|
||||
- User service key `"user_service_key"`
|
||||
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone.
|
||||
- *Optionally*, `"proxied"` can be `true` or `false` to use the proxy services of Cloudflare
|
||||
|
||||
GoDaddy:
|
||||
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"key"`
|
||||
- `"secret"`
|
||||
|
||||
DuckDNS:
|
||||
|
||||
- `"token"`
|
||||
|
||||
Dreamhost:
|
||||
|
||||
- `"key"`
|
||||
|
||||
NoIP:
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
DNSPOD:
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"`
|
||||
|
||||
Infomaniak:
|
||||
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
|
||||
DDNSS.de:
|
||||
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Environment variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `DELAY` | `10m` | Default delay between updates, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
|
||||
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
|
||||
| `LOG_ENCODING` | `console` | Format of logging, `json` or `console` |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `info`, `warning` or `error` |
|
||||
| `NODE_ID` | `0` | Node ID (for distributed systems), can be any integer |
|
||||
| `CONFIG` | | One line JSON object containing the entire config (takes precendence over config.json file) if specified |
|
||||
| `PERIOD` | `5m` | Default period of IP address check, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `IP_METHOD` | `all` | Comma separated methods to obtain the public IP address (ipv4 or ipv6). See the [IP Methods section](#IP-methods) |
|
||||
| `IPV4_METHOD` | `all` | Comma separated methods to obtain the public IPv4 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `IPV6_METHOD` | `all` | Comma separated methods to obtain the public IPv6 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
|
||||
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
|
||||
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
|
||||
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
|
||||
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
|
||||
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`. |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `info`, `warning` or `error` |
|
||||
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
|
||||
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
|
||||
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
|
||||
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
|
||||
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`.
|
||||
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
|
||||
|
||||
#### IP methods
|
||||
|
||||
By default, all ip methods are specified. The program will cycle between each. This allows you not to be blocked for making too many requests. You can otherwise pick one or more of the following, for each ip version:
|
||||
|
||||
- IPv4 or IPv6 (for most cases)
|
||||
- `opendns` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip)
|
||||
- `ifconfig` using [https://ifconfig.io/ip](https://ifconfig.io/ip)
|
||||
- `ipinfo` using [https://ipinfo.io/ip](https://ipinfo.io/ip)
|
||||
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
|
||||
- `ddnss` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
|
||||
- IPv4 only (useful for updating both ipv4 and ipv6)
|
||||
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- IPv6 only
|
||||
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
|
||||
|
||||
You can also specify one or more HTTPS URL to obtain your public IP address (i.e. `-e IPV6_METHOD=https://ipinfo.io/ip`).
|
||||
|
||||
### Host firewall
|
||||
|
||||
@@ -222,83 +206,39 @@ If you have a host firewall in place, this container needs the following ports:
|
||||
- UDP 53 outbound for outbound DNS resolution
|
||||
- TCP 8000 inbound (or other) for the WebUI
|
||||
|
||||
## Domain set up
|
||||
## Architecture
|
||||
|
||||
### Namecheap
|
||||
At program start and every period (5 minutes by default):
|
||||
|
||||
[](https://www.namecheap.com)
|
||||
1. Fetch your public IP address
|
||||
1. For each record:
|
||||
1. DNS resolve it to obtain its current IP address(es)
|
||||
- If the resolution fails, update the record with your public IP address by calling the DNS provider API and finish
|
||||
1. Check if your public IP address is within the resolved IP addresses
|
||||
- Yes: skip the update
|
||||
- No: update the record with your public IP address by calling the DNS provider API
|
||||
|
||||
1. Create a Namecheap account and buy a domain name - *example.com* as an example
|
||||
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
|
||||
💡 We do DNS resolution every period so it detects a change made to the record manually, for example on the DNS provider web UI
|
||||
💡 As DNS resolutions are essentially free and without rate limiting, these are great to avoid getting banned for too many requests.
|
||||
|
||||
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
|
||||
### Special case: Cloudflare
|
||||
|
||||
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
|
||||
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
|
||||
For Cloudflare records with the `proxied` option, the following is done.
|
||||
|
||||

|
||||
At program start and every period (5 minutes by default), for each record:
|
||||
|
||||
1. Select the following settings and create the *A + Dynamic DNS Record*:
|
||||
1. Fetch your public IP address
|
||||
1. For each record:
|
||||
1. Check the last IP address (persisted in `updates.json`) for that record
|
||||
- If it doesn't exist, update the record with your public IP address by calling the DNS provider API and finish
|
||||
1. Check if your public IP address matches the last IP address you updated the record with
|
||||
- Yes: skip the update
|
||||
- No: update the record with your public IP address by calling the DNS provider API
|
||||
|
||||

|
||||
This is the only way as doing a DNS resolution on the record will give the IP address of a Cloudflare server instead of your server.
|
||||
|
||||
1. Scroll down and turn on the switch for *DYNAMIC DNS*
|
||||
|
||||

|
||||
|
||||
1. The Dynamic DNS Password will appear, which is `0e4512a9c45a4fe88313bcc2234bf547` in this example.
|
||||
|
||||

|
||||
|
||||
***
|
||||
|
||||
### GoDaddy
|
||||
|
||||
[](https://godaddy.com)
|
||||
|
||||
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a Test key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a **Production** key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
Obtain the **key** and **secret** of that production key.
|
||||
|
||||
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
|
||||
|
||||
***
|
||||
|
||||
### DuckDNS
|
||||
|
||||
[](https://duckdns.org)
|
||||
|
||||
*See [duckdns website](https://duckdns.org)*
|
||||
|
||||
### Cloudflare
|
||||
|
||||
1. Make sure you have `curl` installed
|
||||
1. Obtain your API key from Cloudflare website ([see this](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-))
|
||||
1. Obtain your zone identifier for your domain name, from the domain's overview page written as *Zone ID*
|
||||
1. Find your **identifier** in the `id` field with
|
||||
|
||||
```sh
|
||||
ZONEID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
EMAIL=example@example.com
|
||||
APIKEY=aaaaaaaaaaaaaaaaaa
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONEID/dns_records" \
|
||||
-H "X-Auth-Email: $EMAIL" \
|
||||
-H "X-Auth-Key: $APIKEY"
|
||||
```
|
||||
|
||||
You can now fill in the necessary parameters in *config.json*
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
⚠️ This has the disadvantage that if the record is changed manually, the program will not detect it.
|
||||
We could do an API call to get the record IP address every period, but that would get you banned especially with a low period duration.
|
||||
|
||||
## Gotify
|
||||
|
||||
@@ -321,70 +261,32 @@ To set it up with DDNS updater:
|
||||
## Testing
|
||||
|
||||
- The automated healthcheck verifies all your records are up to date [using DNS lookups](https://github.com/qdm12/ddns-updater/blob/master/internal/healthcheck/healthcheck.go#L15)
|
||||
- You can check manually at:
|
||||
- GoDaddy: [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
|
||||
- You can also manually check, by:
|
||||
1. Going to your DNS management webpage
|
||||
1. Setting your record to `127.0.0.1`
|
||||
1. Run the container
|
||||
1. Refresh the DNS management webpage and verify the update happened
|
||||
|
||||
[](https://dcc.godaddy.com/manage/)
|
||||
## Development and contributing
|
||||
|
||||
You might want to try to change the IP address to `127.0.0.1` to see if the update actually occurs.
|
||||
- [Contribute with code](https://github.com/qdm12/ddns-updater/blob/master/docs/contributing.md)
|
||||
- [Github workflows to know what's building](https://github.com/qdm12/ddns-updater/actions)
|
||||
- [List of issues and feature requests](https://github.com/qdm12/ddns-updater/issues)
|
||||
- [Kanban board](https://github.com/qdm12/ddns-updater/projects/1)
|
||||
|
||||
## Development
|
||||
## License
|
||||
|
||||
1. Setup your environment
|
||||
|
||||
<details><summary>Using VSCode and Docker (easier)</summary><p>
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1: So you can discard it and update it easily!
|
||||
|
||||
</p></details>
|
||||
|
||||
<details><summary>Locally</summary><p>
|
||||
|
||||
1. Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads)
|
||||
1. Install Go dependencies with
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
1. Install [golangci-lint](https://github.com/golangci/golangci-lint#install)
|
||||
1. You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](https://github.com/qdm12/ddns-updater/master/.vscode/settings.json).
|
||||
|
||||
</p></details>
|
||||
|
||||
1. Commands available:
|
||||
|
||||
```sh
|
||||
# Build the binary
|
||||
go build cmd/app/main.go
|
||||
# Test the code
|
||||
go test ./...
|
||||
# Lint the code
|
||||
golangci-lint run
|
||||
# Build the Docker image
|
||||
docker build -t qmcgaw/ddns-updater .
|
||||
```
|
||||
|
||||
1. See [Contributing](https://github.com/qdm12/ddns-updater/master/.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
This repository is under an [MIT license](https://github.com/qdm12/ddns-updater/master/license)
|
||||
|
||||
## Used in external projects
|
||||
|
||||
- [Starttoaster/docker-traefik](https://github.com/Starttoaster/docker-traefik#home-networks-extra-credit-dynamic-dns)
|
||||
|
||||
## TODOs
|
||||
## Support
|
||||
|
||||
- [ ] Update dependencies
|
||||
- [ ] Mockgen instead of mockery
|
||||
- [ ] Other types or records
|
||||
- [ ] icon.ico for webpage
|
||||
- [ ] Record events log
|
||||
- [ ] Hot reload of config.json
|
||||
- [ ] Unit tests
|
||||
- [ ] ReactJS frontend
|
||||
- [ ] Live update of website
|
||||
- [ ] Change settings
|
||||
Sponsor me on [Github](https://github.com/sponsors/qdm12) or donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||
|
||||
[](https://github.com/sponsors/qdm12)
|
||||
[](https://www.paypal.me/qmcgaw)
|
||||
|
||||
Many thanks to J. Famiglietti for supporting me financially 🥇👍
|
||||
|
||||
@@ -2,63 +2,82 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/server"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/backup"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/handlers"
|
||||
"github.com/qdm12/ddns-updater/internal/healthcheck"
|
||||
"github.com/qdm12/ddns-updater/internal/health"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
recordslib "github.com/qdm12/ddns-updater/internal/records"
|
||||
"github.com/qdm12/ddns-updater/internal/server"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/trigger"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
pubiphttp "github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"github.com/qdm12/golibs/admin"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
)
|
||||
|
||||
var (
|
||||
buildInfo models.BuildInformation
|
||||
version = "unknown"
|
||||
commit = "unknown"
|
||||
buildDate = "an unknown date"
|
||||
)
|
||||
|
||||
func main() {
|
||||
buildInfo.Version = version
|
||||
buildInfo.Commit = commit
|
||||
buildInfo.BuildDate = buildDate
|
||||
os.Exit(_main(context.Background(), time.Now))
|
||||
// returns 1 on error
|
||||
// returns 2 on os signal
|
||||
}
|
||||
|
||||
type allParams struct {
|
||||
period time.Duration
|
||||
cooldown time.Duration
|
||||
httpIPOptions []pubiphttp.Option
|
||||
dir string
|
||||
dataDir string
|
||||
listeningPort uint16
|
||||
rootURL string
|
||||
backupPeriod time.Duration
|
||||
backupDirectory string
|
||||
}
|
||||
|
||||
func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
if health.IsClientMode(os.Args) {
|
||||
// Running the program in a separate instance through the Docker
|
||||
// built-in healthcheck, in an ephemeral fashion to query the
|
||||
// long running instance of the program about its status
|
||||
if err := libhealthcheck.Query(); err != nil {
|
||||
client := health.NewClient()
|
||||
if err := client.Query(ctx); err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
logger, err := setupLogger()
|
||||
|
||||
fmt.Println(splash.Splash(buildInfo))
|
||||
|
||||
// Setup logger
|
||||
paramsReader := params.NewReader(logging.New(logging.StdLog)) // use a temporary logger
|
||||
logLevel, logCaller, err := paramsReader.LoggerConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
paramsReader := params.NewReader(logger)
|
||||
|
||||
fmt.Println(splash.Splash(
|
||||
paramsReader.GetVersion(),
|
||||
paramsReader.GetVcsRef(),
|
||||
paramsReader.GetBuildDate()))
|
||||
logger := logging.New(logging.StdLog, logging.SetLevel(logLevel), logging.SetCaller(logCaller))
|
||||
paramsReader = params.NewReader(logger)
|
||||
|
||||
notify, err := setupGotify(paramsReader, logger)
|
||||
if err != nil {
|
||||
@@ -66,20 +85,20 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, err := getParams(paramsReader)
|
||||
p, err := getParams(paramsReader, logger)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewJSON(dataDir)
|
||||
persistentDB, err := persistence.NewJSON(p.dataDir)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
settings, warnings, err := paramsReader.GetSettings(dataDir + "/config.json")
|
||||
settings, warnings, err := paramsReader.JSONSettings(filepath.Join(p.dataDir, "config.json"))
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(2, w)
|
||||
@@ -92,68 +111,76 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
if len(settings) > 1 {
|
||||
logger.Info("Found %d settings to update records", len(settings))
|
||||
} else if len(settings) == 1 {
|
||||
logger.Info("Found single setting to update records")
|
||||
logger.Info("Found single setting to update record")
|
||||
}
|
||||
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
const connectivyCheckTimeout = 5 * time.Second
|
||||
for _, err := range connectivity.NewConnectivity(connectivyCheckTimeout).
|
||||
Checks(ctx, "google.com") {
|
||||
logger.Warn(err)
|
||||
}
|
||||
records := make([]models.Record, len(settings))
|
||||
idToPeriod := make(map[int]time.Duration)
|
||||
i := 0
|
||||
for id, setting := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", setting.Domain, setting.Host)
|
||||
events, err := persistentDB.GetEvents(setting.Domain, setting.Host)
|
||||
records := make([]recordslib.Record, len(settings))
|
||||
for i, s := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", s.Domain(), s.Host())
|
||||
events, err := persistentDB.GetEvents(s.Domain(), s.Host())
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
records[i] = models.NewRecord(setting, events)
|
||||
idToPeriod[id] = defaultPeriod
|
||||
if setting.Delay > 0 {
|
||||
idToPeriod[id] = setting.Delay
|
||||
}
|
||||
i++
|
||||
records[i] = recordslib.New(s, events)
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
HTTPTimeout, err := paramsReader.HTTPTimeout()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
client := network.NewClient(HTTPTimeout)
|
||||
defer client.Close()
|
||||
client := &http.Client{Timeout: HTTPTimeout}
|
||||
defer client.CloseIdleConnections()
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}()
|
||||
updater := update.NewUpdater(db, logger, client, notify)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
|
||||
ipGetter, err := pubiphttp.New(client, p.httpIPOptions...)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
updater := update.NewUpdater(db, client, notify, logger)
|
||||
runner := update.NewRunner(db, updater, ipGetter, p.cooldown, logger, timeNow)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
checkError := func(err error) {
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
forceUpdate := trigger.StartUpdates(ctx, updater, idToPeriod, checkError)
|
||||
forceUpdate()
|
||||
productionHandlerFunc := handlers.NewHandler(rootURL, dir, db, logger, forceUpdate, checkError).GetHandlerFunc()
|
||||
healthcheckHandlerFunc := libhealthcheck.GetHandler(func() error {
|
||||
return healthcheck.IsHealthy(db, net.LookupIP, logger)
|
||||
})
|
||||
logger.Info("Web UI listening at address 0.0.0.0:%s with root URL %s", listeningPort, rootURL)
|
||||
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))
|
||||
serverErrors := make(chan []error)
|
||||
go func() {
|
||||
serverErrors <- server.RunServers(ctx,
|
||||
server.Settings{Name: "production", Addr: "0.0.0.0:" + listeningPort, Handler: productionHandlerFunc},
|
||||
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
}()
|
||||
|
||||
go backupRunLoop(ctx, backupPeriod, dir, backupDirectory, logger, timeNow)
|
||||
go runner.Run(ctx, p.period)
|
||||
|
||||
// note: errors are logged within the goroutine,
|
||||
// no need to collect the resulting errors.
|
||||
go runner.ForceUpdate(ctx)
|
||||
|
||||
const healthServerAddr = "127.0.0.1:9999"
|
||||
isHealthy := health.MakeIsHealthy(db, net.LookupIP, logger)
|
||||
healthServer := health.NewServer(healthServerAddr,
|
||||
logger.NewChild(logging.SetPrefix("healthcheck server: ")),
|
||||
isHealthy)
|
||||
wg.Add(1)
|
||||
go healthServer.Run(ctx, wg)
|
||||
|
||||
address := fmt.Sprintf("0.0.0.0:%d", p.listeningPort)
|
||||
uiDir := p.dir + "/ui"
|
||||
server := server.New(ctx, address, p.rootURL, uiDir, db, logger.NewChild(logging.SetPrefix("http server: ")), runner)
|
||||
wg.Add(1)
|
||||
go server.Run(ctx, wg)
|
||||
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))
|
||||
|
||||
go backupRunLoop(ctx, p.backupPeriod, p.dir, p.backupDirectory,
|
||||
logger.NewChild(logging.SetPrefix("backup: ")), timeNow)
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals,
|
||||
@@ -162,16 +189,11 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
os.Interrupt,
|
||||
)
|
||||
select {
|
||||
case errors := <-serverErrors:
|
||||
for _, err := range errors {
|
||||
logger.Error(err)
|
||||
}
|
||||
return 1
|
||||
case signal := <-osSignals:
|
||||
message := fmt.Sprintf("Stopping program: caught OS signal %q", signal)
|
||||
logger.Warn(message)
|
||||
notify(2, message)
|
||||
return 2
|
||||
return 1
|
||||
case <-ctx.Done():
|
||||
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
|
||||
logger.Warn(message)
|
||||
@@ -179,23 +201,15 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogger() (logging.Logger, error) {
|
||||
paramsReader := params.NewReader(nil)
|
||||
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logging.NewLogger(encoding, level, nodeID)
|
||||
}
|
||||
|
||||
func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func(priority int, messageArgs ...interface{}), err error) {
|
||||
gotifyURL, err := paramsReader.GetGotifyURL()
|
||||
func setupGotify(paramsReader params.Reader, logger logging.Logger) (
|
||||
notify func(priority int, messageArgs ...interface{}), err error) {
|
||||
gotifyURL, err := paramsReader.GotifyURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if gotifyURL == nil {
|
||||
return func(priority int, messageArgs ...interface{}) {}, nil
|
||||
}
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
gotifyToken, err := paramsReader.GotifyToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -207,47 +221,67 @@ func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getParams(paramsReader params.Reader) (
|
||||
dir, dataDir,
|
||||
listeningPort, rootURL string,
|
||||
defaultPeriod time.Duration,
|
||||
backupPeriod time.Duration, backupDirectory string,
|
||||
err error) {
|
||||
dir, err = paramsReader.GetExeDir()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams, err error) {
|
||||
var warnings []string
|
||||
p.period, warnings, err = paramsReader.Period()
|
||||
for _, warning := range warnings {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
dataDir, err = paramsReader.GetDataDir(dir)
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
return p, err
|
||||
}
|
||||
listeningPort, _, err = paramsReader.GetListeningPort()
|
||||
p.cooldown, err = paramsReader.CooldownPeriod()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
rootURL, err = paramsReader.GetRootURL()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
defaultPeriod, err = paramsReader.GetDelay(libparams.Default("10m"))
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
return p, err
|
||||
}
|
||||
|
||||
backupPeriod, err = paramsReader.GetBackupPeriod()
|
||||
httpIPProviders, err := paramsReader.IPMethod()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
return p, err
|
||||
}
|
||||
backupDirectory, err = paramsReader.GetBackupDirectory()
|
||||
httpIP4Providers, err := paramsReader.IPv4Method()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
return p, err
|
||||
}
|
||||
return dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, nil
|
||||
httpIP6Providers, err := paramsReader.IPv6Method()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.httpIPOptions = []pubiphttp.Option{
|
||||
pubiphttp.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
|
||||
pubiphttp.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
|
||||
pubiphttp.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
|
||||
}
|
||||
|
||||
p.dir, err = paramsReader.ExeDir()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.dataDir, err = paramsReader.DataDir(p.dir)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.listeningPort, _, err = paramsReader.ListeningPort()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.rootURL, err = paramsReader.RootURL()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.backupPeriod, err = paramsReader.BackupPeriod()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.backupDirectory, err = paramsReader.BackupDirectory()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
|
||||
logger logging.Logger, timeNow func() time.Time) {
|
||||
logger = logger.WithPrefix("backup: ")
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
|
||||
@@ -4,28 +4,23 @@
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "google",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
},
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "example.info",
|
||||
"ip_method": "opendns",
|
||||
"key": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,15 +9,25 @@ services:
|
||||
volumes:
|
||||
- ./data:/updater/data
|
||||
environment:
|
||||
- DELAY=300s
|
||||
- ROOT_URL=/
|
||||
- LISTENING_PORT=8000
|
||||
- LOG_ENCODING=console
|
||||
- LOG_LEVEL=info
|
||||
- NODE_ID=0
|
||||
- CONFIG=
|
||||
- PERIOD=5m
|
||||
- UPDATE_COOLDOWN_PERIOD=5m
|
||||
- IP_METHOD=all
|
||||
- IPV4_METHOD=all
|
||||
- IPV6_METHOD=all
|
||||
- HTTP_TIMEOUT=10s
|
||||
|
||||
# Web UI
|
||||
- LISTENING_PORT=8000
|
||||
- ROOT_URL=/
|
||||
|
||||
# Backup
|
||||
- BACKUP_PERIOD=0 # 0 to disable
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
|
||||
# Other
|
||||
- LOG_LEVEL=info
|
||||
- LOG_CALLER=hidden
|
||||
- GOTIFY_URL=
|
||||
- GOTIFY_TOKEN=
|
||||
- BACKUP_PERIOD=0
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
restart: always
|
||||
|
||||
57
docs/cloudflare.md
Normal file
57
docs/cloudflare.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Cloudflare
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "cloudflare",
|
||||
"zone_identifier": "some id",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"zone_identifier"` is the Zone ID of your site
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following:
|
||||
- Email `"email"` and Global API Key `"key"`
|
||||
- User service key `"user_service_key"`
|
||||
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"proxied"` can be set to `true` to use the proxy services of Cloudflare
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), and defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Make sure you have `curl` installed
|
||||
1. Obtain your API key from Cloudflare website ([see this](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-))
|
||||
1. Obtain your zone identifier for your domain name, from the domain's overview page written as *Zone ID*
|
||||
1. Find your **identifier** in the `id` field with
|
||||
|
||||
```sh
|
||||
ZONEID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
EMAIL=example@example.com
|
||||
APIKEY=aaaaaaaaaaaaaaaaaa
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONEID/dns_records" \
|
||||
-H "X-Auth-Email: $EMAIL" \
|
||||
-H "X-Auth-Key: $APIKEY"
|
||||
```
|
||||
|
||||
You can now fill in the necessary parameters in *config.json*
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
52
docs/contributing.md
Normal file
52
docs/contributing.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Contributing
|
||||
|
||||
## Table of content
|
||||
|
||||
1. [Setup](#Setup)
|
||||
1. [Commands available](#Commands-available)
|
||||
1. [Guidelines](#Guidelines)
|
||||
|
||||
## Setup
|
||||
|
||||
### Using VSCode and Docker
|
||||
|
||||
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1:
|
||||
|
||||
### Locally
|
||||
|
||||
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](../.vscode/settings.json).
|
||||
|
||||
## Build and Run
|
||||
|
||||
```sh
|
||||
go build -o app cmd/updater/main.go
|
||||
./app
|
||||
```
|
||||
|
||||
## Commands available
|
||||
|
||||
- Test the code: `go test ./...`
|
||||
- Lint the code `golangci-lint run`
|
||||
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
|
||||
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
|
||||
|
||||
## Guidelines
|
||||
|
||||
The Go code is in the Go file [cmd/updater/main.go](](../cmd/updater/main.go) and the [internal directory](](../internal), you might want to start reading the main.go file.
|
||||
|
||||
See the [Contributing document](](../.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
34
docs/ddnss.de.md
Normal file
34
docs/ddnss.de.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# DDNSS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "ddnss",
|
||||
"provider_ip": true,
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "user",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
31
docs/digitalocean.md
Normal file
31
docs/digitalocean.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Digital Ocean
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "digitalocean",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"token"` is your token that you can create [here](https://cloud.digitalocean.com/settings/applications)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
32
docs/dnsomatic.md
Normal file
32
docs/dnsomatic.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# DNS-O-Matic
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dnsomatic",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
31
docs/dnspod.md
Normal file
31
docs/dnspod.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# DNSPod
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dnspod",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
33
docs/dondominio.md
Normal file
33
docs/dondominio.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Don Dominio
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dondominio",
|
||||
"domain": "domain.com",
|
||||
"name": "something",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"name"` is the name server associated with the domain
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
29
docs/dreamhost.md
Normal file
29
docs/dreamhost.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Dreamhost
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "domain.com",
|
||||
"key": "key",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
35
docs/duckdns.md
Normal file
35
docs/duckdns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# DuckDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"host": "host",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"host"` is your host, for example `subdomain` for `subdomain.duckdns.org`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://duckdns.org)
|
||||
|
||||
*See the [duckdns website](https://duckdns.org)*
|
||||
35
docs/dyndns.md
Normal file
35
docs/dyndns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# DynDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
33
docs/dynv6.md
Normal file
33
docs/dynv6.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# DynV6
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dynv6",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"` that you can obtain [here](https://dynv6.com/keys#token)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
31
docs/freedns.md
Normal file
31
docs/freedns.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# FreeDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "freedns",
|
||||
"domain": "domain.com",
|
||||
"host": "host",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host (subdomain)
|
||||
- `"token"` is the randomized update token you use to update your record
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
37
docs/gandi.md
Normal file
37
docs/gandi.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Gandi
|
||||
|
||||
This provider uses Gandi v5 API
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "gandi",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"ttl": 3600,
|
||||
"ip_version": "ipv4",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` which can be a subdomain, `@` or a wildcard `*`
|
||||
- `"key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"ttl"` default is `3600`
|
||||
|
||||
## Domain setup
|
||||
|
||||
[Gandi Documentation Website](https://docs.gandi.net/en/domain_names/advanced_users/api.html#gandi-s-api)
|
||||
61
docs/godaddy.md
Normal file
61
docs/godaddy.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# GoDaddy
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"key"`
|
||||
- `"secret"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://godaddy.com)
|
||||
|
||||
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a Test key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a **Production** key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
Obtain the **key** and **secret** of that production key.
|
||||
|
||||
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
|
||||
|
||||
## Testing
|
||||
|
||||
1. Go to [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
|
||||
|
||||
[](https://dcc.godaddy.com/manage/)
|
||||
|
||||
1. Change the IP address to `127.0.0.1`
|
||||
1. Run the ddns-updater
|
||||
1. Refresh the Godaddy webpage to check the update occurred.
|
||||
42
docs/google.md
Normal file
42
docs/google.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Google
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
Thanks to [@gauravspatel](https://github.com/gauravspatel) for #124
|
||||
|
||||
1. Enable dynamic DNS in the *synthetic records* section of DNS management.
|
||||
1. The username and password is generated once you create the dynamic DNS entry.
|
||||
|
||||
### Wildcard entries
|
||||
|
||||
If you want to create a **wildcard entry**, you have to create a custom **CNAME** record with key `"*"` and value `"@"`.
|
||||
31
docs/he.net.md
Normal file
31
docs/he.net.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# He.net
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "he",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"` (untested)
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
34
docs/infomaniak.md
Normal file
34
docs/infomaniak.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Infomaniak
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "infomaniak",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
34
docs/linode.md
Normal file
34
docs/linode.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Linode
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "linode",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Create a personal access token with `domains` set, with read and write privileges, ideally that never expires. You can refer to [@AnujRNair's comment](https://github.com/qdm12/ddns-updater/pull/144#discussion_r559292678) and to [Linode's guide](https://www.linode.com/docs/products/tools/cloud-manager/guides/cloud-api-keys).
|
||||
1. The program will create the A or AAAA record for you if it doesn't exist already.
|
||||
37
docs/luadns.md
Normal file
37
docs/luadns.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# LuaDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "luadns",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"email": "email",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"email"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Go to [api.luadns.com/settings](https://api.luadns.com/settings)
|
||||
1. Enable API access
|
||||
1. Obtain your API token and replace it in the parameters as the value for `token`
|
||||
57
docs/namecheap.md
Normal file
57
docs/namecheap.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Namecheap
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
Note that Namecheap only supports ipv4 addresses for now.
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://www.namecheap.com)
|
||||
|
||||
1. Create a Namecheap account and buy a domain name - *example.com* as an example
|
||||
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
|
||||
|
||||
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
|
||||
|
||||
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
|
||||
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
|
||||
|
||||

|
||||
|
||||
1. Select the following settings and create the *A + Dynamic DNS Record*:
|
||||
|
||||

|
||||
|
||||
1. Scroll down and turn on the switch for *DYNAMIC DNS*
|
||||
|
||||

|
||||
|
||||
1. The Dynamic DNS Password will appear, which is `0e4512a9c45a4fe88313bcc2234bf547` in this example.
|
||||
|
||||

|
||||
34
docs/noip.md
Normal file
34
docs/noip.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# NoIP
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "noip",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
35
docs/opendns.md
Normal file
35
docs/opendns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# OpenDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
51
docs/ovh.md
Normal file
51
docs/ovh.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# OVH
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "ovh",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
|
||||
#### Using DynHost
|
||||
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
#### OR Using ZoneDNS
|
||||
|
||||
- `"api_endpoint"` default value is `"ovh-eu"`
|
||||
- `"app_key"`
|
||||
- `"app_secret"`
|
||||
- `"consumer_key"`
|
||||
|
||||
The ZoneDNS implementation allows you to update any record name including *.yourdomain.tld
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
- `"mode"` select between two modes, OVH's dynamic hosting service (`"dynamic"`) or OVH's API (`"api"`). Default is `"dynamic"`
|
||||
|
||||
## Domain setup
|
||||
|
||||
- If you use DynHost: [docs.ovh.com/ie/en/domains/hosting_dynhost](https://docs.ovh.com/ie/en/domains/hosting_dynhost/)
|
||||
- If you use the ZoneDNS API: [docs.ovh.com/gb/en/customer/first-steps-with-ovh-api](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/)
|
||||
33
docs/selfhost.de.md
Normal file
33
docs/selfhost.de.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Selfhost.de
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "selfhost.de",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"` is your DynDNS username
|
||||
- `"password"` is your DynDNS password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
35
docs/strato.md
Normal file
35
docs/strato.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Strato
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "strato",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"password"` is your dyndns password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
|
||||
See [their article](https://www.strato.com/faq/en_us/domain/this-is-how-easy-it-is-to-set-up-dyndns-for-your-domains/)
|
||||
12
go.mod
12
go.mod
@@ -1,11 +1,13 @@
|
||||
module github.com/qdm12/ddns-updater
|
||||
|
||||
go 1.13
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi v1.5.1
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible
|
||||
github.com/miekg/dns v1.1.40
|
||||
github.com/ovh/go-ovh v1.1.0
|
||||
github.com/qdm12/golibs v0.0.0-20210215133151-c711ebd3e56a
|
||||
github.com/stretchr/testify v1.6.1
|
||||
)
|
||||
|
||||
90
go.sum
90
go.sum
@@ -1,5 +1,3 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
@@ -9,11 +7,14 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
|
||||
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
|
||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||
@@ -41,22 +42,16 @@ github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
|
||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
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/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o=
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible h1:gaQFbK2+uSxOR4iGZprJAbpmtqTrHhSdgOyIMD6Oidc=
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
@@ -64,76 +59,71 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
|
||||
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
|
||||
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
||||
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151 h1:5q8oyhJqgQyW5v427CDC34SobllqiJCLLfS3Z4EeLCI=
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/qdm12/golibs v0.0.0-20210215133151-c711ebd3e56a h1:DxO9jvcQDtWgKSzxL95828kQxO6WCocP9PPpmIqGMRs=
|
||||
github.com/qdm12/golibs v0.0.0-20210215133151-c711ebd3e56a/go.mod h1:y0qNgur9dTkHK2Bb5tK0UCtYyvEiK08flVIglROmnBg=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
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-20190222072716-a9d3bda3a223/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
HTMLFail models.HTML = `<font color="red"><b>Failure</b></font>`
|
||||
HTMLSuccess models.HTML = `<font color="green"><b>Success</b></font>`
|
||||
HTMLUpdate models.HTML = `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
HTMLUpdating models.HTML = `<font color="orange"><b>Updating</b></font>`
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO have a struct model containing URL, name for each provider
|
||||
HTMLNamecheap models.HTML = "<a href=\"https://namecheap.com\">Namecheap</a>"
|
||||
HTMLGodaddy models.HTML = "<a href=\"https://godaddy.com\">GoDaddy</a>"
|
||||
HTMLDuckDNS models.HTML = "<a href=\"https://duckdns.org\">DuckDNS</a>"
|
||||
HTMLDreamhost models.HTML = "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>"
|
||||
HTMLCloudflare models.HTML = "<a href=\"https://www.cloudflare.com\">Cloudflare</a>"
|
||||
HTMLNoIP models.HTML = "<a href=\"https://www.noip.com/\">NoIP</a>"
|
||||
HTMLDNSPod models.HTML = "<a href=\"https://www.dnspod.cn/\">DNSPod</a>"
|
||||
HTMLInfomaniak models.HTML = "<a href=\"https://www.infomaniak.com/\">Infomaniak</a>"
|
||||
HTMLDdnssde models.HTML = "<a href=\"https://ddnss.de/\">DDNSS.de</a>"
|
||||
)
|
||||
|
||||
const (
|
||||
HTMLGoogle models.HTML = "<a href=\"https://google.com/search?q=ip\">Google</a>"
|
||||
HTMLOpenDNS models.HTML = "<a href=\"https://diagnostic.opendns.com/myip\">OpenDNS</a>"
|
||||
HTMLIfconfig models.HTML = "<a href=\"https://ifconfig.io\">ifconfig.io</a>"
|
||||
HTMLIpinfo models.HTML = "<a href=\"https://ipinfo.io\">ipinfo.io</a>"
|
||||
HTMLIpify models.HTML = "<a href=\"https://api.ipify.org\">api.ipify.org</a>"
|
||||
HTMLIpify6 models.HTML = "<a href=\"https://api6.ipify.org\">api6.ipify.org</a>"
|
||||
HTMLDdnss models.HTML = "<a href=\"https://ddnss.de/meineip.php\">ddnss.de</a>"
|
||||
HTMLDdnss4 models.HTML = "<a href=\"https://ip4.ddnss.de/meineip.php\">ip4.ddnss.de</a>"
|
||||
HTMLDdnss6 models.HTML = "<a href=\"https://ip6.ddnss.de/meineip.php\">ip6.ddns.de</a>"
|
||||
HTMLCycle models.HTML = "Cycling"
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
IPv4 models.IPVersion = "ipv4"
|
||||
IPv6 models.IPVersion = "ipv6"
|
||||
)
|
||||
@@ -1,53 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
PROVIDER models.IPMethod = "provider"
|
||||
OPENDNS models.IPMethod = "opendns"
|
||||
IFCONFIG models.IPMethod = "ifconfig"
|
||||
IPINFO models.IPMethod = "ipinfo"
|
||||
IPIFY models.IPMethod = "ipify"
|
||||
IPIFY6 models.IPMethod = "ipify6"
|
||||
CYCLE models.IPMethod = "cycle"
|
||||
DDNSS models.IPMethod = "ddnss"
|
||||
DDNSS4 models.IPMethod = "ddnss4"
|
||||
DDNSS6 models.IPMethod = "ddnss6"
|
||||
// Retro compatibility only
|
||||
GOOGLE models.IPMethod = "google"
|
||||
)
|
||||
|
||||
func IPMethodMapping() map[models.IPMethod]string {
|
||||
return map[models.IPMethod]string{
|
||||
PROVIDER: string(PROVIDER),
|
||||
CYCLE: string(CYCLE),
|
||||
OPENDNS: "https://diagnostic.opendns.com/myip",
|
||||
IFCONFIG: "https://ifconfig.io/ip",
|
||||
IPINFO: "https://ipinfo.io/ip",
|
||||
IPIFY: "https://api.ipify.org",
|
||||
IPIFY6: "https://api6.ipify.org",
|
||||
DDNSS: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS4: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS6: "https://ip6.ddnss.de/meineip.php",
|
||||
}
|
||||
}
|
||||
|
||||
func IPMethodChoices() (choices []models.IPMethod) {
|
||||
for choice := range IPMethodMapping() {
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
return choices
|
||||
}
|
||||
|
||||
func IPMethodExternalChoices() (choices []models.IPMethod) {
|
||||
for _, choice := range IPMethodChoices() {
|
||||
switch choice {
|
||||
case PROVIDER, CYCLE:
|
||||
default:
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
}
|
||||
return choices
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_IPMethodChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "provider", "cycle", "opendns", "ifconfig", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
|
||||
func Test_IPMethodExternalChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodExternalChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "ifconfig", "opendns", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
@@ -2,29 +2,59 @@ package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
// All possible provider values
|
||||
// All possible provider values.
|
||||
const (
|
||||
GODADDY models.Provider = "godaddy"
|
||||
NAMECHEAP models.Provider = "namecheap"
|
||||
DUCKDNS models.Provider = "duckdns"
|
||||
DREAMHOST models.Provider = "dreamhost"
|
||||
CLOUDFLARE models.Provider = "cloudflare"
|
||||
NOIP models.Provider = "noip"
|
||||
DNSPOD models.Provider = "dnspod"
|
||||
INFOMANIAK models.Provider = "infomaniak"
|
||||
DDNSSDE models.Provider = "ddnss"
|
||||
CLOUDFLARE models.Provider = "cloudflare"
|
||||
DIGITALOCEAN models.Provider = "digitalocean"
|
||||
DDNSSDE models.Provider = "ddnss"
|
||||
DONDOMINIO models.Provider = "dondominio"
|
||||
DNSOMATIC models.Provider = "dnsomatic"
|
||||
DNSPOD models.Provider = "dnspod"
|
||||
DUCKDNS models.Provider = "duckdns"
|
||||
DYN models.Provider = "dyn"
|
||||
DYNV6 models.Provider = "dynv6"
|
||||
DREAMHOST models.Provider = "dreamhost"
|
||||
FREEDNS models.Provider = "freedns"
|
||||
GANDI models.Provider = "gandi"
|
||||
GODADDY models.Provider = "godaddy"
|
||||
GOOGLE models.Provider = "google"
|
||||
HE models.Provider = "he"
|
||||
INFOMANIAK models.Provider = "infomaniak"
|
||||
LINODE models.Provider = "linode"
|
||||
LUADNS models.Provider = "luadns"
|
||||
NAMECHEAP models.Provider = "namecheap"
|
||||
NOIP models.Provider = "noip"
|
||||
OPENDNS models.Provider = "opendns"
|
||||
OVH models.Provider = "ovh"
|
||||
SELFHOSTDE models.Provider = "selfhost.de"
|
||||
STRATO models.Provider = "strato"
|
||||
)
|
||||
|
||||
func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
GODADDY,
|
||||
NAMECHEAP,
|
||||
DUCKDNS,
|
||||
DREAMHOST,
|
||||
CLOUDFLARE,
|
||||
NOIP,
|
||||
DNSPOD,
|
||||
INFOMANIAK,
|
||||
DIGITALOCEAN,
|
||||
DDNSSDE,
|
||||
DONDOMINIO,
|
||||
DNSOMATIC,
|
||||
DNSPOD,
|
||||
DUCKDNS,
|
||||
DYN,
|
||||
DYNV6,
|
||||
DREAMHOST,
|
||||
FREEDNS,
|
||||
GANDI,
|
||||
GODADDY,
|
||||
GOOGLE,
|
||||
HE,
|
||||
INFOMANIAK,
|
||||
LINODE,
|
||||
LUADNS,
|
||||
NAMECHEAP,
|
||||
NOIP,
|
||||
OVH,
|
||||
OPENDNS,
|
||||
SELFHOSTDE,
|
||||
STRATO,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "regexp"
|
||||
|
||||
const (
|
||||
goDaddyKey = `[A-Za-z0-9]{10,14}\_[A-Za-z0-9]{22}`
|
||||
godaddySecret = `[A-Za-z0-9]{22}` // #nosec
|
||||
duckDNSToken = `[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}` // #nosec
|
||||
namecheapPassword = `[a-f0-9]{32}` // #nosec
|
||||
dreamhostKey = `[a-zA-Z0-9]{16}`
|
||||
cloudflareKey = `[a-zA-Z0-9]+`
|
||||
cloudflareUserServiceKey = `v1\.0.+`
|
||||
cloudflareToken = `[a-zA-Z0-9_]{40}` // #nosec
|
||||
)
|
||||
|
||||
func MatchGodaddyKey(s string) bool {
|
||||
return regexp.MustCompile("^" + goDaddyKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchGodaddySecret(s string) bool {
|
||||
return regexp.MustCompile("^" + godaddySecret + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDuckDNSToken(s string) bool {
|
||||
return regexp.MustCompile("^" + duckDNSToken + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchNamecheapPassword(s string) bool {
|
||||
return regexp.MustCompile("^" + namecheapPassword + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDreamhostKey(s string) bool {
|
||||
return regexp.MustCompile("^" + dreamhostKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareUserServiceKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareUserServiceKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareToken(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareToken + "$").MatchString(s)
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// Announcement is a message announcement
|
||||
Announcement = "Smaller Docker image based on Scratch (12.3MB)"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd
|
||||
AnnouncementExpiration = "2020-04-20"
|
||||
// Announcement is a message announcement.
|
||||
Announcement = "Support for he.net"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd.
|
||||
AnnouncementExpiration = "2020-10-15"
|
||||
)
|
||||
|
||||
const (
|
||||
// IssueLink is the link for users to use to create issues
|
||||
// IssueLink is the link for users to use to create issues.
|
||||
IssueLink = "https://github.com/qdm12/ddns-updater/issues/new"
|
||||
)
|
||||
|
||||
@@ -7,4 +7,5 @@ const (
|
||||
SUCCESS models.Status = "success"
|
||||
UPTODATE models.Status = "up to date"
|
||||
UPDATING models.Status = "updating"
|
||||
UNSET models.Status = "unset"
|
||||
)
|
||||
|
||||
@@ -5,26 +5,26 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
Close() error
|
||||
Insert(record models.Record) (id int)
|
||||
Select(id int) (record models.Record, err error)
|
||||
SelectAll() (records []models.Record)
|
||||
Update(id int, record models.Record) error
|
||||
// From persistence database
|
||||
Select(id int) (record records.Record, err error)
|
||||
SelectAll() (records []records.Record)
|
||||
// Using persistence database
|
||||
Update(id int, record records.Record) error
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
}
|
||||
|
||||
type database struct {
|
||||
data []models.Record
|
||||
data []records.Record
|
||||
sync.RWMutex
|
||||
persistentDB persistence.Database
|
||||
}
|
||||
|
||||
// NewDatabase creates a new in memory database
|
||||
func NewDatabase(data []models.Record, persistentDB persistence.Database) Database {
|
||||
// NewDatabase creates a new in memory database.
|
||||
func NewDatabase(data []records.Record, persistentDB persistence.Database) Database {
|
||||
return &database{
|
||||
data: data,
|
||||
persistentDB: persistentDB,
|
||||
|
||||
@@ -3,17 +3,10 @@ package data
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) Insert(record models.Record) (id int) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
db.data = append(db.data, record)
|
||||
return len(db.data) - 1
|
||||
}
|
||||
|
||||
func (db *database) Select(id int) (record models.Record, err error) {
|
||||
func (db *database) Select(id int) (record records.Record, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
if id < 0 {
|
||||
@@ -25,7 +18,7 @@ func (db *database) Select(id int) (record models.Record, err error) {
|
||||
return db.data[id], nil
|
||||
}
|
||||
|
||||
func (db *database) SelectAll() (records []models.Record) {
|
||||
func (db *database) SelectAll() (records []records.Record) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
return db.data
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
return db.persistentDB.GetEvents(domain, host)
|
||||
}
|
||||
|
||||
func (db *database) Update(id int, record models.Record) error {
|
||||
func (db *database) Update(id int, record records.Record) error {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
if id < 0 {
|
||||
@@ -25,8 +26,8 @@ func (db *database) Update(id int, record models.Record) error {
|
||||
// new IP address added
|
||||
if newCount > currentCount {
|
||||
if err := db.persistentDB.StoreNewIP(
|
||||
record.Settings.Domain,
|
||||
record.Settings.Host,
|
||||
record.Settings.Domain(),
|
||||
record.Settings.Host(),
|
||||
record.History.GetCurrentIP(),
|
||||
record.History.GetSuccessTime(),
|
||||
); err != nil {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/html"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
// Handler contains a handler function
|
||||
type Handler interface {
|
||||
GetHandlerFunc() http.HandlerFunc
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
rootURL string
|
||||
uiDir string
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
forceUpdate func()
|
||||
onError func(err error)
|
||||
getTime func() time.Time
|
||||
}
|
||||
|
||||
// NewHandler returns a Handler object
|
||||
func NewHandler(rootURL, uiDir string, db data.Database, logger logging.Logger,
|
||||
forceUpdate func(), onError func(err error)) Handler {
|
||||
return &handler{
|
||||
rootURL: rootURL,
|
||||
uiDir: uiDir,
|
||||
db: db,
|
||||
logger: logger,
|
||||
forceUpdate: forceUpdate,
|
||||
onError: onError,
|
||||
getTime: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHandlerFunc returns a router with all the necessary routes configured
|
||||
func (h *handler) GetHandlerFunc() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("received HTTP request at %s", r.RequestURI)
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/":
|
||||
// TODO: Forms to change existing updates or add some
|
||||
t := template.Must(template.ParseFiles(h.uiDir + "/ui/index.html"))
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := html.ConvertRecord(record, h.getTime())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := t.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
h.logger.Warn(err)
|
||||
fmt.Fprint(w, "An error occurred creating this webpage")
|
||||
}
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/update":
|
||||
h.logger.Info("Update started manually")
|
||||
h.forceUpdate()
|
||||
http.Redirect(w, r, h.rootURL, 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
54
internal/health/check.go
Normal file
54
internal/health/check.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type lookupIPFunc func(host string) ([]net.IP, error)
|
||||
|
||||
func MakeIsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) func() error {
|
||||
return func() (err error) {
|
||||
return isHealthy(db, lookupIP)
|
||||
}
|
||||
}
|
||||
|
||||
// isHealthy checks all the records were updated successfully and returns an error if not.
|
||||
func isHealthy(db data.Database, lookupIP lookupIPFunc) (err error) {
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%s", record.String())
|
||||
} else if record.Settings.Proxied() {
|
||||
continue
|
||||
}
|
||||
hostname := record.Settings.BuildDomainName()
|
||||
lookedUpIPs, err := lookupIP(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("no database set IP address found for %s", hostname)
|
||||
}
|
||||
found := false
|
||||
lookedUpIPsString := make([]string, len(lookedUpIPs))
|
||||
for i, lookedUpIP := range lookedUpIPs {
|
||||
lookedUpIPsString[i] = lookedUpIP.String()
|
||||
if lookedUpIP.Equal(currentIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s",
|
||||
hostname, strings.Join(lookedUpIPsString, ","), currentIP)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
53
internal/health/client.go
Normal file
53
internal/health/client.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func IsClientMode(args []string) bool {
|
||||
return len(args) > 1 && args[1] == "healthcheck"
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Query(ctx context.Context) error
|
||||
}
|
||||
|
||||
type client struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func NewClient() Client {
|
||||
const timeout = 5 * time.Second
|
||||
return &client{
|
||||
Client: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
// Query sends an HTTP request to the other instance of
|
||||
// the program, and to its internal healthcheck server.
|
||||
func (c *client) Query(ctx context.Context) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:9999", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %s", resp.Status, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf(string(b))
|
||||
}
|
||||
31
internal/health/handler.go
Normal file
31
internal/health/handler.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
func newHandler(logger logging.Logger, healthcheck func() error) http.Handler {
|
||||
return &handler{
|
||||
logger: logger,
|
||||
healthcheck: healthcheck,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
logger logging.Logger
|
||||
healthcheck func() error
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || (r.RequestURI != "" && r.RequestURI != "/") {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err := h.healthcheck(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
53
internal/health/server.go
Normal file
53
internal/health/server.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
Run(ctx context.Context, wg *sync.WaitGroup)
|
||||
}
|
||||
|
||||
type server struct {
|
||||
address string
|
||||
logger logging.Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewServer(address string, logger logging.Logger, healthcheck func() error) Server {
|
||||
handler := newHandler(logger, healthcheck)
|
||||
return &server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.logger.Warn("shutting down (context canceled)")
|
||||
defer s.logger.Warn("shut down")
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
s.logger.Error("failed shutting down: %s", err)
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on %s", s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err)
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type lookupIPFunc func(host string) ([]net.IP, error)
|
||||
|
||||
// IsHealthy checks all the records were updated successfully and returns an error if not
|
||||
func IsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logger.Warn("unhealthy: %s", err)
|
||||
}
|
||||
}()
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%s", record.String())
|
||||
} else if record.Settings.NoDNSLookup {
|
||||
continue
|
||||
}
|
||||
lookedUpIPs, err := lookupIP(record.Settings.BuildDomainName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("no set IP address found")
|
||||
}
|
||||
for _, lookedUpIP := range lookedUpIPs {
|
||||
if !lookedUpIP.Equal(currentIP) {
|
||||
return fmt.Errorf(
|
||||
"lookup IP address of %s is %s instead of %s",
|
||||
record.Settings.BuildDomainName(), lookedUpIP, currentIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
const NotAvailable = "N/A"
|
||||
row := models.HTMLRow{
|
||||
Domain: convertDomain(record.Settings.BuildDomainName()),
|
||||
Host: models.HTML(record.Settings.Host),
|
||||
Provider: convertProvider(record.Settings.Provider),
|
||||
IPMethod: convertIPMethod(record.Settings.IPMethod, record.Settings.Provider),
|
||||
}
|
||||
message := record.Message
|
||||
if record.Status == constants.UPTODATE {
|
||||
message = "no IP change for " + record.History.GetDurationSinceSuccess(now)
|
||||
}
|
||||
if len(message) > 0 {
|
||||
message = fmt.Sprintf("(%s)", message)
|
||||
}
|
||||
if len(record.Status) == 0 {
|
||||
row.Status = NotAvailable
|
||||
} else {
|
||||
row.Status = models.HTML(fmt.Sprintf("%s %s, %s",
|
||||
convertStatus(record.Status),
|
||||
message,
|
||||
time.Since(record.Time).Round(time.Second).String()+" ago"))
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP != nil {
|
||||
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
|
||||
} else {
|
||||
row.CurrentIP = NotAvailable
|
||||
}
|
||||
previousIPs := record.History.GetPreviousIPs()
|
||||
row.PreviousIPs = NotAvailable
|
||||
if len(previousIPs) > 0 {
|
||||
var previousIPsStr []string
|
||||
const maxPreviousIPs = 2
|
||||
for i, previousIP := range previousIPs {
|
||||
if i == maxPreviousIPs {
|
||||
previousIPsStr = append(previousIPsStr, fmt.Sprintf("and %d more", len(previousIPs)-i))
|
||||
break
|
||||
}
|
||||
previousIPsStr = append(previousIPsStr, previousIP.String())
|
||||
}
|
||||
row.PreviousIPs = models.HTML(strings.Join(previousIPsStr, ", "))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func convertStatus(status models.Status) models.HTML {
|
||||
switch status {
|
||||
case constants.SUCCESS:
|
||||
return constants.HTMLSuccess
|
||||
case constants.FAIL:
|
||||
return constants.HTMLFail
|
||||
case constants.UPTODATE:
|
||||
return constants.HTMLUpdate
|
||||
case constants.UPDATING:
|
||||
return constants.HTMLUpdating
|
||||
default:
|
||||
return "Unknown status"
|
||||
}
|
||||
}
|
||||
|
||||
func convertProvider(provider models.Provider) models.HTML {
|
||||
switch provider {
|
||||
case constants.NAMECHEAP:
|
||||
return constants.HTMLNamecheap
|
||||
case constants.GODADDY:
|
||||
return constants.HTMLGodaddy
|
||||
case constants.DUCKDNS:
|
||||
return constants.HTMLDuckDNS
|
||||
case constants.DREAMHOST:
|
||||
return constants.HTMLDreamhost
|
||||
case constants.CLOUDFLARE:
|
||||
return constants.HTMLCloudflare
|
||||
case constants.NOIP:
|
||||
return constants.HTMLNoIP
|
||||
case constants.DNSPOD:
|
||||
return constants.HTMLDNSPod
|
||||
case constants.INFOMANIAK:
|
||||
return constants.HTMLInfomaniak
|
||||
case constants.DDNSSDE:
|
||||
return constants.HTMLDdnssde
|
||||
default:
|
||||
s := string(provider)
|
||||
if strings.HasPrefix("https://", s) {
|
||||
shorterName := strings.TrimPrefix(s, "https://")
|
||||
shorterName = strings.TrimSuffix(shorterName, "/")
|
||||
return models.HTML(fmt.Sprintf("<a href=\"%s\">%s</a>", s, shorterName))
|
||||
}
|
||||
return models.HTML(string(provider))
|
||||
}
|
||||
}
|
||||
|
||||
func convertIPMethod(ipMethod models.IPMethod, provider models.Provider) models.HTML {
|
||||
// TODO map to icons
|
||||
switch ipMethod {
|
||||
case constants.PROVIDER:
|
||||
return convertProvider(provider)
|
||||
case constants.OPENDNS:
|
||||
return constants.HTMLOpenDNS
|
||||
case constants.IFCONFIG:
|
||||
return constants.HTMLIfconfig
|
||||
case constants.IPINFO:
|
||||
return constants.HTMLIpinfo
|
||||
case constants.IPIFY:
|
||||
return constants.HTMLIpify
|
||||
case constants.IPIFY6:
|
||||
return constants.HTMLIpify6
|
||||
case constants.DDNSS:
|
||||
return constants.HTMLDdnss
|
||||
case constants.DDNSS4:
|
||||
return constants.HTMLDdnss4
|
||||
case constants.DDNSS6:
|
||||
return constants.HTMLDdnss6
|
||||
case constants.CYCLE:
|
||||
return constants.HTMLCycle
|
||||
default:
|
||||
return models.HTML(string(ipMethod))
|
||||
}
|
||||
}
|
||||
|
||||
func convertDomain(domain string) models.HTML {
|
||||
return models.HTML("<a href=\"http://" + domain + "\">" + domain + "</a>")
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
// Provider is a possible DNS provider.
|
||||
Provider string
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
IPMethod string
|
||||
// Status is the record config status
|
||||
// Status is the record config status.
|
||||
Status string
|
||||
// HTML is for constants HTML strings
|
||||
// HTML is for constants HTML strings.
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6
|
||||
IPVersion string
|
||||
)
|
||||
|
||||
7
internal/models/build.go
Normal file
7
internal/models/build.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
type BuildInformation struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
BuildDate string `json:"buildDate"`
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// History contains current and previous IP address for a particular record
|
||||
// with the latest success time
|
||||
// with the latest success time.
|
||||
type History []HistoryEvent // current and previous ips
|
||||
|
||||
type HistoryEvent struct { // current and previous ips
|
||||
@@ -23,13 +23,14 @@ func (h History) GetPreviousIPs() []net.IP {
|
||||
return nil
|
||||
}
|
||||
IPs := make([]net.IP, len(h)-1)
|
||||
for i := len(h) - 2; i >= 0; i-- {
|
||||
const two = 2
|
||||
for i := len(h) - two; i >= 0; i-- {
|
||||
IPs[i] = h[i].IP
|
||||
}
|
||||
return IPs
|
||||
}
|
||||
|
||||
// GetCurrentIP returns the current IP address (latest in history)
|
||||
// GetCurrentIP returns the current IP address (latest in history).
|
||||
func (h History) GetCurrentIP() net.IP {
|
||||
if len(h) < 1 {
|
||||
return nil
|
||||
@@ -37,7 +38,7 @@ func (h History) GetCurrentIP() net.IP {
|
||||
return h[len(h)-1].IP
|
||||
}
|
||||
|
||||
// GetSuccessTime returns the latest success update time
|
||||
// GetSuccessTime returns the latest success update time.
|
||||
func (h History) GetSuccessTime() time.Time {
|
||||
if len(h) < 1 {
|
||||
return time.Time{}
|
||||
@@ -50,6 +51,7 @@ func (h History) GetDurationSinceSuccess(now time.Time) string {
|
||||
return "N/A"
|
||||
}
|
||||
duration := now.Sub(h[len(h)-1].Time)
|
||||
const hoursInDay = 24
|
||||
switch {
|
||||
case duration < time.Minute:
|
||||
return fmt.Sprintf("%ds", int(duration.Round(time.Second).Seconds()))
|
||||
@@ -58,7 +60,7 @@ func (h History) GetDurationSinceSuccess(now time.Time) string {
|
||||
case duration < 24*time.Hour:
|
||||
return fmt.Sprintf("%dh", int(duration.Round(time.Hour).Hours()))
|
||||
default:
|
||||
return fmt.Sprintf("%dd", int(duration.Round(time.Hour*24).Hours()/24))
|
||||
return fmt.Sprintf("%dd", int(duration.Round(time.Hour*hoursInDay).Hours()/hoursInDay))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ type HTMLRow struct {
|
||||
Domain HTML
|
||||
Host HTML
|
||||
Provider HTML
|
||||
IPMethod HTML
|
||||
IPVersion HTML
|
||||
Status HTML
|
||||
CurrentIP HTML
|
||||
PreviousIPs HTML
|
||||
|
||||
9
internal/models/ipmethod.go
Normal file
9
internal/models/ipmethod.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
// IPMethod is a method to obtain your public IP address.
|
||||
type IPMethod struct {
|
||||
Name string
|
||||
URL string
|
||||
IPv4 bool
|
||||
IPv6 bool
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Record contains all the information to update and display a DNS record
|
||||
type Record struct { // internal
|
||||
Settings Settings // fixed
|
||||
History History // past information
|
||||
Status Status
|
||||
Message string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// NewRecord returns a new Record with settings and some history
|
||||
func NewRecord(settings Settings, events []HistoryEvent) Record {
|
||||
return Record{
|
||||
Settings: settings,
|
||||
History: events,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Record) String() string {
|
||||
status := string(r.Status)
|
||||
if len(r.Message) > 0 {
|
||||
status += " (" + r.Message + ")"
|
||||
}
|
||||
return fmt.Sprintf("%s: %s %s; %s", r.Settings.String(), status, r.Time.Format("2006-01-02 15:04:05 MST"), r.History.String())
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Settings contains the elements to update the DNS record
|
||||
// nolint: maligned
|
||||
type Settings struct {
|
||||
Domain string
|
||||
Host string
|
||||
Provider Provider
|
||||
IPMethod IPMethod
|
||||
IPVersion IPVersion
|
||||
Delay time.Duration
|
||||
NoDNSLookup bool
|
||||
// Provider dependent fields
|
||||
Password string // Namecheap, Infomaniak, DDNSS and NoIP only
|
||||
Key string // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string // GoDaddy only
|
||||
Token string // Cloudflare and DuckDNS only
|
||||
Email string // Cloudflare only
|
||||
UserServiceKey string // Cloudflare only
|
||||
ZoneIdentifier string // Cloudflare only
|
||||
Identifier string // Cloudflare only
|
||||
Proxied bool // Cloudflare only
|
||||
TTL uint // Cloudflare only
|
||||
Username string // NoIP, Infomaniak, DDNSS only
|
||||
}
|
||||
|
||||
func (settings *Settings) String() string {
|
||||
b, _ := json.Marshal(
|
||||
struct {
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Provider string `json:"provider"`
|
||||
}{
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
string(settings.Provider),
|
||||
},
|
||||
)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// BuildDomainName builds the domain name from the domain and the host of the settings
|
||||
func (settings *Settings) BuildDomainName() string {
|
||||
switch settings.Host {
|
||||
case "@":
|
||||
return settings.Domain
|
||||
case "*":
|
||||
return "any." + settings.Domain
|
||||
default:
|
||||
return settings.Host + "." + settings.Domain
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
// GetPublicIP downloads a webpage and extracts the IP address from it
|
||||
func GetPublicIP(client network.Client, url string, ipVersion models.IPVersion) (ip net.IP, err error) {
|
||||
content, status, err := client.GetContent(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: %s", ipVersion, url, err)
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d", ipVersion, url, status)
|
||||
}
|
||||
verifier := verification.NewVerifier()
|
||||
regexSearch := verifier.SearchIPv4
|
||||
if ipVersion == constants.IPv6 {
|
||||
regexSearch = verifier.SearchIPv6
|
||||
}
|
||||
ips := regexSearch(string(content))
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no public %s address found at %s", ipVersion, url)
|
||||
} else if len(ips) > 1 {
|
||||
return nil, fmt.Errorf("multiple public %s addresses found at %s: %s", ipVersion, url, strings.Join(ips, " "))
|
||||
}
|
||||
ip = net.ParseIP(ips[0])
|
||||
if ip == nil { // in case the regex is not restrictive enough
|
||||
return nil, fmt.Errorf("Public IP address %q found at %s is not valid", ips[0], url)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network/mock_network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetPublicIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
IPVersion models.IPVersion
|
||||
mockContent []byte
|
||||
mockStatus int
|
||||
mockErr error
|
||||
ip net.IP
|
||||
err error
|
||||
}{
|
||||
"network error": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockErr: fmt.Errorf("error"),
|
||||
err: fmt.Errorf("cannot get public ipv4 address from https://getmyip.com: error"),
|
||||
},
|
||||
"bad status": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockStatus: http.StatusUnauthorized,
|
||||
err: fmt.Errorf("cannot get public ipv4 address from https://getmyip.com: HTTP status code 401"),
|
||||
},
|
||||
"no IPs in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte(""),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("no public ipv4 address found at https://getmyip.com"),
|
||||
},
|
||||
"multiple IPs in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("10.10.10.10 50.50.50.50"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("multiple public ipv4 addresses found at https://getmyip.com: 10.10.10.10 50.50.50.50"),
|
||||
},
|
||||
"single IP in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("10.10.10.10"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{10, 10, 10, 10},
|
||||
},
|
||||
"single IPv6 in content": {
|
||||
IPVersion: constants.IPv6,
|
||||
mockContent: []byte("::fe"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfe},
|
||||
},
|
||||
}
|
||||
const URL = "https://getmyip.com"
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
client := mock_network.NewMockClient(mockCtrl)
|
||||
client.EXPECT().GetContent(URL).Return(tc.mockContent, tc.mockStatus, tc.mockErr).Times(1)
|
||||
ip, err := GetPublicIP(client, URL, tc.IPVersion)
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// BuildHTTPPut is used for GoDaddy and Cloudflare only
|
||||
func BuildHTTPPut(url string, body interface{}) (request *http.Request, err error) {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err = http.NewRequest(http.MethodPut, url, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
return request, nil
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func settingsGeneralChecks(settings models.Settings, matchDomain func(s string) bool) error {
|
||||
switch {
|
||||
case !ipMethodIsValid(settings.IPMethod):
|
||||
return fmt.Errorf("IP method %q is not recognized", settings.IPMethod)
|
||||
case settings.IPVersion != constants.IPv4 && settings.IPVersion != constants.IPv6:
|
||||
return fmt.Errorf("IP version %q is not recognized", settings.IPVersion)
|
||||
case !matchDomain(settings.Domain):
|
||||
return fmt.Errorf("invalid domain name format")
|
||||
case len(settings.Host) == 0:
|
||||
return fmt.Errorf("host cannot be empty")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func settingsIPVersionChecks(ipVersion models.IPVersion, ipMethod models.IPMethod, provider models.Provider) error {
|
||||
switch ipVersion {
|
||||
case constants.IPv4:
|
||||
switch ipMethod {
|
||||
case constants.IPIFY6, constants.DDNSS6:
|
||||
return fmt.Errorf("IP method %s is only for IPv6 addresses", ipMethod)
|
||||
}
|
||||
case constants.IPv6:
|
||||
switch ipMethod {
|
||||
case constants.IPIFY, constants.DDNSS4:
|
||||
return fmt.Errorf("IP method %s is only for IPv4 addresses", ipMethod)
|
||||
}
|
||||
switch provider {
|
||||
case constants.GODADDY, constants.CLOUDFLARE, constants.DNSPOD, constants.DREAMHOST, constants.DUCKDNS, constants.NOIP:
|
||||
return fmt.Errorf("IPv6 support for %s is not supported yet", provider)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsIPMethodChecks(ipMethod models.IPMethod, provider models.Provider) error {
|
||||
if ipMethod == constants.PROVIDER {
|
||||
switch provider {
|
||||
case constants.GODADDY, constants.DREAMHOST, constants.CLOUDFLARE, constants.DNSPOD, constants.DDNSSDE:
|
||||
return fmt.Errorf("unsupported IP update method %q", ipMethod)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsNamecheapChecks(password string) error {
|
||||
if !constants.MatchNamecheapPassword(password) {
|
||||
return fmt.Errorf("invalid password format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsGoDaddyChecks(key, secret string) error {
|
||||
switch {
|
||||
case !constants.MatchGodaddyKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !constants.MatchGodaddySecret(secret):
|
||||
return fmt.Errorf("invalid secret format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDuckDNSChecks(token, host string) error {
|
||||
switch {
|
||||
case !constants.MatchDuckDNSToken(token):
|
||||
return fmt.Errorf("invalid token format")
|
||||
case host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDreamhostChecks(key, host string) error {
|
||||
switch {
|
||||
case !constants.MatchDreamhostKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsCloudflareChecks(key, email, userServiceKey, token, zoneIdentifier, identifier string, ttl uint, matchEmail func(s string) bool) error {
|
||||
switch {
|
||||
case len(key) > 0: // email and key must be provided
|
||||
switch {
|
||||
case !constants.MatchCloudflareKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !matchEmail(email):
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
case len(userServiceKey) > 0: // only user service key
|
||||
if !constants.MatchCloudflareKey(key) {
|
||||
return fmt.Errorf("invalid user service key format")
|
||||
}
|
||||
default: // API token only
|
||||
if !constants.MatchCloudflareToken(token) {
|
||||
return fmt.Errorf("invalid API token key format")
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(zoneIdentifier) == 0:
|
||||
return fmt.Errorf("zone identifier cannot be empty")
|
||||
case len(identifier) == 0:
|
||||
return fmt.Errorf("identifier cannot be empty")
|
||||
case ttl == 0:
|
||||
return fmt.Errorf("TTL cannot be left to 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsNoIPChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(username) > 50:
|
||||
return fmt.Errorf("username cannot be longer than 50 characters")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDNSPodChecks(token string) error {
|
||||
if len(token) == 0 {
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsInfomaniakChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDdnssdeChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *reader) isConsistent(settings models.Settings) error {
|
||||
if err := settingsGeneralChecks(settings, r.verifier.MatchDomain); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingsIPVersionChecks(settings.IPVersion, settings.IPMethod, settings.Provider); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingsIPMethodChecks(settings.IPMethod, settings.Provider); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Checks for each DNS provider
|
||||
switch settings.Provider {
|
||||
case constants.NAMECHEAP:
|
||||
if err := settingsNamecheapChecks(settings.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.GODADDY:
|
||||
if err := settingsGoDaddyChecks(settings.Key, settings.Secret); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DUCKDNS:
|
||||
if err := settingsDuckDNSChecks(settings.Token, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DREAMHOST:
|
||||
if err := settingsDreamhostChecks(settings.Key, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.CLOUDFLARE:
|
||||
if err := settingsCloudflareChecks(settings.Key, settings.Email, settings.UserServiceKey, settings.Token, settings.ZoneIdentifier, settings.Identifier, settings.TTL, r.verifier.MatchEmail); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.NOIP:
|
||||
if err := settingsNoIPChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DNSPOD:
|
||||
if err := settingsDNSPodChecks(settings.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.INFOMANIAK:
|
||||
if err := settingsInfomaniakChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DDNSSDE:
|
||||
if err := settingsDdnssdeChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("provider %q is not supported", settings.Provider)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipMethodIsValid(ipMethod models.IPMethod) bool {
|
||||
for _, possibility := range constants.IPMethodChoices() {
|
||||
if ipMethod == possibility {
|
||||
return true
|
||||
}
|
||||
}
|
||||
url, err := url.Parse(string(ipMethod))
|
||||
if err != nil || url == nil || url.Scheme != "https" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ipMethodIsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
valid bool
|
||||
}{
|
||||
"empty method": {
|
||||
ipMethod: "",
|
||||
valid: false,
|
||||
},
|
||||
"non existing method": {
|
||||
ipMethod: "abc",
|
||||
valid: false,
|
||||
},
|
||||
"existing method": {
|
||||
ipMethod: "opendns",
|
||||
valid: true,
|
||||
},
|
||||
"http url": {
|
||||
ipMethod: "http://ipinfo.io/ip",
|
||||
valid: false,
|
||||
},
|
||||
"https url": {
|
||||
ipMethod: "https://ipinfo.io/ip",
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid := ipMethodIsValid(tc.ipMethod)
|
||||
assert.Equal(t, tc.valid, valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,89 +3,174 @@ package params
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
// nolint: maligned
|
||||
type settingsType struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
IPMethod string `json:"ip_method"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
Delay uint64 `json:"delay"`
|
||||
NoDNSLookup bool `json:"no_dns_lookup"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"` // Namecheap, NoIP only
|
||||
Key string `json:"key"` // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string `json:"secret"` // GoDaddy only
|
||||
Token string `json:"token"` // DuckDNS and Cloudflare only
|
||||
Email string `json:"email"` // Cloudflare only
|
||||
Username string `json:"username"` // NoIP only
|
||||
UserServiceKey string `json:"user_service_key"` // Cloudflare only
|
||||
ZoneIdentifier string `json:"zone_identifier"` // Cloudflare only
|
||||
Identifier string `json:"identifier"` // Cloudflare only
|
||||
Proxied bool `json:"proxied"` // Cloudflare only
|
||||
TTL uint `json:"ttl"` // Cloudflare only
|
||||
type commonSettings struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
// Retro values for warnings
|
||||
IPMethod *string `json:"ip_method,omitempty"`
|
||||
Delay *uint64 `json:"delay,omitempty"`
|
||||
}
|
||||
|
||||
// GetSettings obtain the update settings from config.json
|
||||
func (r *reader) GetSettings(filePath string) (settings []models.Settings, warnings []string, err error) {
|
||||
// JSONSettings obtain the update settings from the JSON content, first trying from the environment variable CONFIG
|
||||
// and then from the file config.json.
|
||||
func (r *reader) JSONSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
allSettings, warnings, err = r.getSettingsFromEnv()
|
||||
if allSettings != nil || warnings != nil || err != nil {
|
||||
return allSettings, warnings, err
|
||||
}
|
||||
return r.getSettingsFromFile(filePath)
|
||||
}
|
||||
|
||||
// getSettingsFromFile obtain the update settings from config.json.
|
||||
func (r *reader) getSettingsFromFile(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
bytes, err := r.readFile(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var config struct {
|
||||
Settings []settingsType `json:"settings"`
|
||||
return extractAllSettings(bytes)
|
||||
}
|
||||
|
||||
// getSettingsFromEnv obtain the update settings from the environment variable CONFIG.
|
||||
func (r *reader) getSettingsFromEnv() (allSettings []settings.Settings, warnings []string, err error) {
|
||||
s, err := r.env.Get("CONFIG")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(s) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
if err := json.Unmarshal(bytes, &config); err != nil {
|
||||
return extractAllSettings([]byte(s))
|
||||
}
|
||||
|
||||
func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
config := struct {
|
||||
CommonSettings []commonSettings `json:"settings"`
|
||||
}{}
|
||||
rawConfig := struct {
|
||||
Settings []json.RawMessage `json:"settings"`
|
||||
}{}
|
||||
if err := json.Unmarshal(jsonBytes, &config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, s := range config.Settings {
|
||||
switch models.Provider(s.Provider) {
|
||||
case constants.DREAMHOST, constants.DUCKDNS:
|
||||
s.Host = "@" // only choice available
|
||||
}
|
||||
ipMethod := models.IPMethod(s.IPMethod)
|
||||
// Retro compatibility
|
||||
if ipMethod == constants.GOOGLE {
|
||||
r.logger.Warn("IP Method %q is no longer valid, please change it. Defaulting it to %s", constants.GOOGLE, constants.CYCLE)
|
||||
ipMethod = constants.CYCLE
|
||||
}
|
||||
ipVersion := models.IPVersion(s.IPVersion)
|
||||
if len(ipVersion) == 0 {
|
||||
ipVersion = constants.IPv4 // default
|
||||
}
|
||||
setting := models.Settings{
|
||||
Provider: models.Provider(s.Provider),
|
||||
Domain: s.Domain,
|
||||
Host: s.Host,
|
||||
IPMethod: ipMethod,
|
||||
IPVersion: ipVersion,
|
||||
Delay: time.Second * time.Duration(s.Delay),
|
||||
NoDNSLookup: s.NoDNSLookup,
|
||||
Password: s.Password,
|
||||
Key: s.Key,
|
||||
Secret: s.Secret,
|
||||
Token: s.Token,
|
||||
Email: s.Email,
|
||||
Username: s.Username,
|
||||
UserServiceKey: s.UserServiceKey,
|
||||
ZoneIdentifier: s.ZoneIdentifier,
|
||||
Identifier: s.Identifier,
|
||||
Proxied: s.Proxied,
|
||||
TTL: s.TTL,
|
||||
}
|
||||
if err := r.isConsistent(setting); err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("%s for settings %s", err, setting.String()))
|
||||
continue
|
||||
}
|
||||
settings = append(settings, setting)
|
||||
if err := json.Unmarshal(jsonBytes, &rawConfig); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(settings) == 0 {
|
||||
return nil, warnings, fmt.Errorf("no settings found in config.json")
|
||||
matcher, err := regex.NewMatcher()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return settings, warnings, nil
|
||||
for i, common := range config.CommonSettings {
|
||||
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i], matcher)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
allSettings = append(allSettings, newSettings...)
|
||||
}
|
||||
if len(allSettings) == 0 {
|
||||
warnings = append(warnings, "no settings found in JSON data")
|
||||
}
|
||||
return allSettings, warnings, nil
|
||||
}
|
||||
|
||||
// TODO remove gocyclo.
|
||||
//nolint:gocyclo
|
||||
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage, matcher regex.Matcher) (
|
||||
settingsSlice []settings.Settings, warnings []string, err error) {
|
||||
provider := models.Provider(common.Provider)
|
||||
if provider == constants.DUCKDNS { // only hosts, no domain
|
||||
if len(common.Domain) > 0 { // retro compatibility
|
||||
if len(common.Host) == 0 {
|
||||
common.Host = strings.TrimSuffix(common.Domain, ".duckdns.org")
|
||||
warnings = append(warnings,
|
||||
fmt.Sprintf("DuckDNS record should have %q specified as host instead of %q as domain",
|
||||
common.Host, common.Domain))
|
||||
} else {
|
||||
warnings = append(warnings,
|
||||
fmt.Sprintf("ignoring domain %q because host %q is specified for DuckDNS record",
|
||||
common.Domain, common.Host))
|
||||
}
|
||||
}
|
||||
}
|
||||
hosts := strings.Split(common.Host, ",")
|
||||
|
||||
if len(common.IPVersion) == 0 {
|
||||
common.IPVersion = ipversion.IP4or6.String()
|
||||
}
|
||||
ipVersion, err := ipversion.Parse(common.IPVersion)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var settingsConstructor settings.Constructor
|
||||
switch provider {
|
||||
case constants.CLOUDFLARE:
|
||||
settingsConstructor = settings.NewCloudflare
|
||||
case constants.DIGITALOCEAN:
|
||||
settingsConstructor = settings.NewDigitalOcean
|
||||
case constants.DDNSSDE:
|
||||
settingsConstructor = settings.NewDdnss
|
||||
case constants.DONDOMINIO:
|
||||
settingsConstructor = settings.NewDonDominio
|
||||
case constants.DNSOMATIC:
|
||||
settingsConstructor = settings.NewDNSOMatic
|
||||
case constants.DNSPOD:
|
||||
settingsConstructor = settings.NewDNSPod
|
||||
case constants.DREAMHOST:
|
||||
settingsConstructor = settings.NewDreamhost
|
||||
case constants.DUCKDNS:
|
||||
settingsConstructor = settings.NewDuckdns
|
||||
case constants.FREEDNS:
|
||||
settingsConstructor = settings.NewFreedns
|
||||
case constants.GANDI:
|
||||
settingsConstructor = settings.NewGandi
|
||||
case constants.GODADDY:
|
||||
settingsConstructor = settings.NewGodaddy
|
||||
case constants.GOOGLE:
|
||||
settingsConstructor = settings.NewGoogle
|
||||
case constants.HE:
|
||||
settingsConstructor = settings.NewHe
|
||||
case constants.INFOMANIAK:
|
||||
settingsConstructor = settings.NewInfomaniak
|
||||
case constants.LINODE:
|
||||
settingsConstructor = settings.NewLinode
|
||||
case constants.LUADNS:
|
||||
settingsConstructor = settings.NewLuaDNS
|
||||
case constants.NAMECHEAP:
|
||||
settingsConstructor = settings.NewNamecheap
|
||||
case constants.NOIP:
|
||||
settingsConstructor = settings.NewNoip
|
||||
case constants.DYN:
|
||||
settingsConstructor = settings.NewDyn
|
||||
case constants.SELFHOSTDE:
|
||||
settingsConstructor = settings.NewSelfhostde
|
||||
case constants.STRATO:
|
||||
settingsConstructor = settings.NewStrato
|
||||
case constants.OVH:
|
||||
settingsConstructor = settings.NewOVH
|
||||
case constants.DYNV6:
|
||||
settingsConstructor = settings.NewDynV6
|
||||
case constants.OPENDNS:
|
||||
settingsConstructor = settings.NewOpendns
|
||||
default:
|
||||
return nil, warnings, fmt.Errorf("provider %q is not supported", provider)
|
||||
}
|
||||
settingsSlice = make([]settings.Settings, len(hosts))
|
||||
for i, host := range hosts {
|
||||
settingsSlice[i], err = settingsConstructor(rawSettings, common.Domain, host, ipVersion, matcher)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
}
|
||||
return settingsSlice, warnings, nil
|
||||
}
|
||||
|
||||
@@ -1,104 +1,215 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
GetSettings(filePath string) (settings []models.Settings, warnings []string, err error)
|
||||
GetDataDir(currentDir string) (string, error)
|
||||
GetListeningPort() (listeningPort, warning string, err error)
|
||||
GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error)
|
||||
GetGotifyURL(setters ...libparams.GetEnvSetter) (URL *url.URL, err error)
|
||||
GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error)
|
||||
GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error)
|
||||
GetDelay(setters ...libparams.GetEnvSetter) (duration time.Duration, err error)
|
||||
GetExeDir() (dir string, err error)
|
||||
GetHTTPTimeout() (duration time.Duration, err error)
|
||||
GetBackupPeriod() (duration time.Duration, err error)
|
||||
GetBackupDirectory() (directory string, err error)
|
||||
// JSON
|
||||
JSONSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error)
|
||||
|
||||
// Version getters
|
||||
GetVersion() string
|
||||
GetBuildDate() string
|
||||
GetVcsRef() string
|
||||
// Core
|
||||
Period() (period time.Duration, warnings []string, err error)
|
||||
IPMethod() (providers []http.Provider, err error)
|
||||
IPv4Method() (providers []http.Provider, err error)
|
||||
IPv6Method() (providers []http.Provider, err error)
|
||||
HTTPTimeout() (duration time.Duration, err error)
|
||||
CooldownPeriod() (duration time.Duration, err error)
|
||||
|
||||
// File paths
|
||||
ExeDir() (dir string, err error)
|
||||
DataDir(currentDir string) (string, error)
|
||||
|
||||
// Web UI
|
||||
ListeningPort() (listeningPort uint16, warning string, err error)
|
||||
RootURL() (rootURL string, err error)
|
||||
|
||||
// Backup
|
||||
BackupPeriod() (duration time.Duration, err error)
|
||||
BackupDirectory() (directory string, err error)
|
||||
|
||||
// Other
|
||||
LoggerConfig() (level logging.Level, caller logging.Caller, err error)
|
||||
GotifyURL() (URL *url.URL, err error)
|
||||
GotifyToken() (token string, err error)
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
envParams libparams.EnvParams
|
||||
verifier verification.Verifier
|
||||
logger logging.Logger
|
||||
readFile func(filename string) ([]byte, error)
|
||||
env params.Env
|
||||
os params.OS
|
||||
readFile func(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewReader(logger logging.Logger) Reader {
|
||||
return &reader{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
verifier: verification.NewVerifier(),
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
env: params.NewEnv(),
|
||||
os: params.NewOS(),
|
||||
readFile: ioutil.ReadFile,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDataDir obtains the data directory from the environment
|
||||
// variable DATADIR
|
||||
func (r *reader) GetDataDir(currentDir string) (string, error) {
|
||||
return r.envParams.GetEnv("DATADIR", libparams.Default(currentDir+"/data"))
|
||||
// variable DATADIR.
|
||||
func (r *reader) DataDir(currentDir string) (string, error) {
|
||||
return r.env.Get("DATADIR", params.Default(currentDir+"/data"))
|
||||
}
|
||||
|
||||
func (r *reader) GetListeningPort() (listeningPort, warning string, err error) {
|
||||
return r.envParams.GetListeningPort()
|
||||
func (r *reader) ListeningPort() (listeningPort uint16, warning string, err error) {
|
||||
return r.env.ListeningPort("LISTENING_PORT", params.Default("8000"))
|
||||
}
|
||||
|
||||
func (r *reader) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
|
||||
return r.envParams.GetLoggerConfig()
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyURL(setters ...libparams.GetEnvSetter) (url *url.URL, err error) {
|
||||
return r.envParams.GetGotifyURL()
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error) {
|
||||
return r.envParams.GetGotifyToken()
|
||||
}
|
||||
|
||||
func (r *reader) GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error) {
|
||||
return r.envParams.GetRootURL()
|
||||
}
|
||||
|
||||
func (r *reader) GetDelay(setters ...libparams.GetEnvSetter) (period time.Duration, err error) {
|
||||
// Backward compatibility
|
||||
n, err := r.envParams.GetEnvInt("DELAY", libparams.Compulsory()) // TODO change to PERIOD
|
||||
if err == nil { // integer only, treated as seconds
|
||||
r.logger.Warn("The value for the duration period of the updater does not have a time unit, you might want to set it to \"%ds\" instead of \"%d\"", n, n)
|
||||
return time.Duration(n) * time.Second, nil
|
||||
func (r *reader) LoggerConfig() (level logging.Level, caller logging.Caller, err error) {
|
||||
caller, err = r.env.LogCaller("LOG_CALLER", params.Default("hidden"))
|
||||
if err != nil {
|
||||
return level, caller, err
|
||||
}
|
||||
return r.envParams.GetDuration("DELAY", setters...)
|
||||
|
||||
level, err = r.env.LogLevel("LOG_LEVEL", params.Default("info"))
|
||||
if err != nil {
|
||||
return level, caller, err
|
||||
}
|
||||
|
||||
return level, caller, nil
|
||||
}
|
||||
|
||||
func (r *reader) GetExeDir() (dir string, err error) {
|
||||
return r.envParams.GetExeDir()
|
||||
func (r *reader) GotifyURL() (url *url.URL, err error) {
|
||||
return r.env.URL("GOTIFY_URL")
|
||||
}
|
||||
|
||||
func (r *reader) GetHTTPTimeout() (duration time.Duration, err error) {
|
||||
return r.envParams.GetHTTPTimeout(libparams.Default("10s"))
|
||||
func (r *reader) GotifyToken() (token string, err error) {
|
||||
return r.env.Get("GOTIFY_TOKEN",
|
||||
params.CaseSensitiveValue(),
|
||||
params.Compulsory(),
|
||||
params.Unset())
|
||||
}
|
||||
|
||||
func (r *reader) GetBackupPeriod() (duration time.Duration, err error) {
|
||||
s, err := r.envParams.GetEnv("BACKUP_PERIOD", libparams.Default("0"))
|
||||
func (r *reader) RootURL() (rootURL string, err error) {
|
||||
return r.env.RootURL("ROOT_URL")
|
||||
}
|
||||
|
||||
func (r *reader) Period() (period time.Duration, warnings []string, err error) {
|
||||
// Backward compatibility
|
||||
n, err := r.env.Int("DELAY", params.Compulsory())
|
||||
if err == nil { // integer only, treated as seconds
|
||||
return time.Duration(n) * time.Second,
|
||||
[]string{
|
||||
"the environment variable DELAY should be changed to PERIOD",
|
||||
fmt.Sprintf(`the value for the duration period of the updater does not have a time unit, you might want to set it to "%ds" instead of "%d"`, n, n), //nolint:lll
|
||||
}, nil
|
||||
}
|
||||
period, err = r.env.Duration("DELAY", params.Compulsory())
|
||||
if err == nil {
|
||||
return period,
|
||||
[]string{
|
||||
"the environment variable DELAY should be changed to PERIOD",
|
||||
}, nil
|
||||
}
|
||||
period, err = r.env.Duration("PERIOD", params.Default("10m"))
|
||||
return period, nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
ErrIPMethodInvalid = errors.New("ip method is not valid")
|
||||
ErrIPMethodVersion = errors.New("ip method not valid for IP version")
|
||||
)
|
||||
|
||||
// IPMethod obtains the HTTP method for IP v4 or v6 to obtain your public IP address.
|
||||
func (r *reader) IPMethod() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("IP_METHOD", ipversion.IP4or6)
|
||||
}
|
||||
|
||||
// IPMethod obtains the HTTP method for IP v4 to obtain your public IP address.
|
||||
func (r *reader) IPv4Method() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("IPV4_METHOD", ipversion.IP4)
|
||||
}
|
||||
|
||||
// IPMethod obtains the HTTP method for IP v6 to obtain your public IP address.
|
||||
func (r *reader) IPv6Method() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("IPV6_METHOD", ipversion.IP6)
|
||||
}
|
||||
|
||||
func (r *reader) httpIPMethod(envKey string, version ipversion.IPVersion) (
|
||||
providers []http.Provider, err error) {
|
||||
s, err := r.env.Get(envKey, params.Default("cycle"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
availableProviders := http.ListProvidersForVersion(version)
|
||||
choices := make(map[http.Provider]struct{}, len(availableProviders))
|
||||
for _, provider := range availableProviders {
|
||||
choices[provider] = struct{}{}
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
|
||||
for _, field := range fields {
|
||||
// Retro-compatibility.
|
||||
switch field {
|
||||
case "ipify6":
|
||||
field = "ipify"
|
||||
case "noip4", "noip6", "noip8245_4", "noip8245_6":
|
||||
field = "noip"
|
||||
case "cycle":
|
||||
field = "all"
|
||||
}
|
||||
|
||||
if field == "all" {
|
||||
return availableProviders, nil
|
||||
}
|
||||
|
||||
// Custom URL check
|
||||
url, err := url.Parse(field)
|
||||
if err == nil && url != nil && url.Scheme == "https" {
|
||||
providers = append(providers, http.CustomProvider(url))
|
||||
continue
|
||||
}
|
||||
|
||||
provider := http.Provider(field)
|
||||
if _, ok := choices[provider]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPMethodInvalid, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPMethodVersion, version)
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (r *reader) ExeDir() (dir string, err error) {
|
||||
return r.os.ExeDir()
|
||||
}
|
||||
|
||||
func (r *reader) HTTPTimeout() (duration time.Duration, err error) {
|
||||
return r.env.Duration("HTTP_TIMEOUT", params.Default("10s"))
|
||||
}
|
||||
|
||||
func (r *reader) BackupPeriod() (duration time.Duration, err error) {
|
||||
s, err := r.env.Get("BACKUP_PERIOD", params.Default("0"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
func (r *reader) GetBackupDirectory() (directory string, err error) {
|
||||
return r.envParams.GetEnv("BACKUP_DIRECTORY", libparams.Default("./data"))
|
||||
func (r *reader) BackupDirectory() (directory string, err error) {
|
||||
return r.env.Path("BACKUP_DIRECTORY", params.Default("./data"))
|
||||
}
|
||||
|
||||
func (r *reader) CooldownPeriod() (duration time.Duration, err error) {
|
||||
return r.env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
func (r *reader) GetVersion() string {
|
||||
version, _ := r.envParams.GetEnv("VERSION", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return version
|
||||
}
|
||||
|
||||
func (r *reader) GetBuildDate() string {
|
||||
buildDate, _ := r.envParams.GetEnv("BUILD_DATE", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
|
||||
func (r *reader) GetVcsRef() string {
|
||||
buildDate, _ := r.envParams.GetEnv("VCS_REF", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func NewDatabase(dataDir string) (*Database, error) {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.Check(); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%s validation error: %w", db.filepath, err)
|
||||
}
|
||||
return &db, nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (db *Database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err
|
||||
}
|
||||
|
||||
// GetEvents gets all the IP addresses history for a certain domain and host, in the order
|
||||
// from oldest to newest
|
||||
// from oldest to newest.
|
||||
func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
@@ -44,7 +44,7 @@ func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetAllDomainsHosts gets all the domains and hosts from the database
|
||||
// GetAllDomainsHosts gets all the domains and hosts from the database.
|
||||
func (db *Database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
68
internal/records/html.go
Normal file
68
internal/records/html.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package records
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func (r *Record) HTML(now time.Time) models.HTMLRow {
|
||||
const NotAvailable = "N/A"
|
||||
row := r.Settings.HTML()
|
||||
message := r.Message
|
||||
if r.Status == constants.UPTODATE {
|
||||
message = "no IP change for " + r.History.GetDurationSinceSuccess(now)
|
||||
}
|
||||
if len(message) > 0 {
|
||||
message = fmt.Sprintf("(%s)", message)
|
||||
}
|
||||
if len(r.Status) == 0 {
|
||||
row.Status = NotAvailable
|
||||
} else {
|
||||
row.Status = models.HTML(fmt.Sprintf("%s %s, %s",
|
||||
convertStatus(r.Status),
|
||||
message,
|
||||
time.Since(r.Time).Round(time.Second).String()+" ago"))
|
||||
}
|
||||
currentIP := r.History.GetCurrentIP()
|
||||
if currentIP != nil {
|
||||
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
|
||||
} else {
|
||||
row.CurrentIP = NotAvailable
|
||||
}
|
||||
previousIPs := r.History.GetPreviousIPs()
|
||||
row.PreviousIPs = NotAvailable
|
||||
if len(previousIPs) > 0 {
|
||||
var previousIPsStr []string
|
||||
const maxPreviousIPs = 2
|
||||
for i, previousIP := range previousIPs {
|
||||
if i == maxPreviousIPs {
|
||||
previousIPsStr = append(previousIPsStr, fmt.Sprintf("and %d more", len(previousIPs)-i))
|
||||
break
|
||||
}
|
||||
previousIPsStr = append(previousIPsStr, previousIP.String())
|
||||
}
|
||||
row.PreviousIPs = models.HTML(strings.Join(previousIPsStr, ", "))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func convertStatus(status models.Status) models.HTML {
|
||||
switch status {
|
||||
case constants.SUCCESS:
|
||||
return `<font color="green"><b>Success</b></font>`
|
||||
case constants.FAIL:
|
||||
return `<font color="red"><b>Failure</b></font>`
|
||||
case constants.UPTODATE:
|
||||
return `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
case constants.UPDATING:
|
||||
return `<font color="orange"><b>Updating</b></font>`
|
||||
case constants.UNSET:
|
||||
return `<font color="purple"><b>Unset</b></font>`
|
||||
default:
|
||||
return "Unknown status"
|
||||
}
|
||||
}
|
||||
38
internal/records/records.go
Normal file
38
internal/records/records.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package records
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
)
|
||||
|
||||
// Record contains all the information to update and display a DNS record.
|
||||
type Record struct { // internal
|
||||
Settings settings.Settings // fixed
|
||||
History models.History // past information
|
||||
Status models.Status
|
||||
Message string
|
||||
Time time.Time
|
||||
LastBan *time.Time // nil means no last ban
|
||||
}
|
||||
|
||||
// New returns a new Record with settings and some history.
|
||||
func New(settings settings.Settings, events []models.HistoryEvent) Record {
|
||||
return Record{
|
||||
Settings: settings,
|
||||
History: events,
|
||||
Status: constants.UNSET,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Record) String() string {
|
||||
status := string(r.Status)
|
||||
if len(r.Message) > 0 {
|
||||
status += " (" + r.Message + ")"
|
||||
}
|
||||
return fmt.Sprintf("%s: %s %s; %s",
|
||||
r.Settings, status, r.Time.Format("2006-01-02 15:04:05 MST"), r.History)
|
||||
}
|
||||
73
internal/regex/regex.go
Normal file
73
internal/regex/regex.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package regex
|
||||
|
||||
import "regexp"
|
||||
|
||||
type Matcher interface {
|
||||
GandiKey(s string) bool
|
||||
GodaddyKey(s string) bool
|
||||
DuckDNSToken(s string) bool
|
||||
NamecheapPassword(s string) bool
|
||||
DreamhostKey(s string) bool
|
||||
CloudflareKey(s string) bool
|
||||
CloudflareUserServiceKey(s string) bool
|
||||
DNSOMaticUsername(s string) bool
|
||||
DNSOMaticPassword(s string) bool
|
||||
}
|
||||
|
||||
type matcher struct {
|
||||
goDaddyKey, duckDNSToken, namecheapPassword, dreamhostKey, cloudflareKey,
|
||||
cloudflareUserServiceKey, dnsOMaticUsername, dnsOMaticPassword, gandiKey *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewMatcher() (m Matcher, err error) {
|
||||
matcher := &matcher{}
|
||||
matcher.gandiKey, err = regexp.Compile(`^[A-Za-z0-9]{24}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.goDaddyKey, err = regexp.Compile(`^[A-Za-z0-9]{8,14}\_[A-Za-z0-9]{21,22}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.duckDNSToken, err = regexp.Compile(`^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.namecheapPassword, err = regexp.Compile(`^[a-f0-9]{32}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.dreamhostKey, err = regexp.Compile(`^[a-zA-Z0-9]{16}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.cloudflareKey, err = regexp.Compile(`^[a-zA-Z0-9]+$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.cloudflareUserServiceKey, err = regexp.Compile(`^v1\.0.+$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.dnsOMaticUsername, err = regexp.Compile(`^[a-zA-Z0-9._-]{3,25}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.dnsOMaticPassword, err = regexp.Compile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{5,19}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
func (m *matcher) GandiKey(s string) bool { return m.gandiKey.MatchString(s) }
|
||||
func (m *matcher) GodaddyKey(s string) bool { return m.goDaddyKey.MatchString(s) }
|
||||
func (m *matcher) DuckDNSToken(s string) bool { return m.duckDNSToken.MatchString(s) }
|
||||
func (m *matcher) NamecheapPassword(s string) bool { return m.namecheapPassword.MatchString(s) }
|
||||
func (m *matcher) DreamhostKey(s string) bool { return m.dreamhostKey.MatchString(s) }
|
||||
func (m *matcher) CloudflareKey(s string) bool { return m.cloudflareKey.MatchString(s) }
|
||||
func (m *matcher) CloudflareUserServiceKey(s string) bool {
|
||||
return m.cloudflareUserServiceKey.MatchString(s)
|
||||
}
|
||||
func (m *matcher) DNSOMaticUsername(s string) bool { return m.dnsOMaticUsername.MatchString(s) }
|
||||
func (m *matcher) DNSOMaticPassword(s string) bool { return m.dnsOMaticPassword.MatchString(s) }
|
||||
35
internal/server/error.go
Normal file
35
internal/server/error.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type errJSONWrapper struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, status int, errString string) {
|
||||
w.WriteHeader(status)
|
||||
if errString == "" {
|
||||
errString = http.StatusText(status)
|
||||
}
|
||||
body := errJSONWrapper{Error: errString}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
type errorsJSONWrapper struct {
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
func httpErrors(w http.ResponseWriter, status int, errors []error) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
errs := make([]string, len(errors))
|
||||
for i := range errors {
|
||||
errs[i] = errors[i].Error()
|
||||
}
|
||||
|
||||
body := errorsJSONWrapper{Errors: errs}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
24
internal/server/fileserver.go
Normal file
24
internal/server/fileserver.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
func fileServer(router chi.Router, path string, root http.FileSystem) {
|
||||
if path != "/" && path[len(path)-1] != '/' {
|
||||
router.Get(path,
|
||||
http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
|
||||
path += "/"
|
||||
}
|
||||
path += "*"
|
||||
|
||||
router.Get(path, func(w http.ResponseWriter, r *http.Request) {
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
|
||||
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
|
||||
fs.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
50
internal/server/handler.go
Normal file
50
internal/server/handler.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
)
|
||||
|
||||
type handlers struct {
|
||||
ctx context.Context
|
||||
// Objects
|
||||
db data.Database
|
||||
runner update.Runner
|
||||
indexTemplate *template.Template
|
||||
// Mockable functions
|
||||
timeNow func() time.Time
|
||||
}
|
||||
|
||||
func newHandler(ctx context.Context, rootURL, uiDir string,
|
||||
db data.Database, runner update.Runner) http.Handler {
|
||||
indexTemplate := template.Must(template.ParseFiles(uiDir + "/index.html"))
|
||||
|
||||
handlers := &handlers{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
indexTemplate: indexTemplate,
|
||||
// TODO build information
|
||||
timeNow: time.Now,
|
||||
runner: runner,
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.Logger, middleware.CleanPath)
|
||||
|
||||
router.Get(rootURL+"/", handlers.index)
|
||||
|
||||
router.Get(rootURL+"/update", handlers.update)
|
||||
|
||||
// UI file server for other paths
|
||||
fileServer(router, rootURL+"/", http.Dir(uiDir))
|
||||
|
||||
return router
|
||||
}
|
||||
18
internal/server/index.go
Normal file
18
internal/server/index.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func (h *handlers) index(w http.ResponseWriter, r *http.Request) {
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := record.HTML(h.timeNow())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := h.indexTemplate.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "failed generating webpage: "+err.Error())
|
||||
}
|
||||
}
|
||||
56
internal/server/server.go
Normal file
56
internal/server/server.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
Run(ctx context.Context, wg *sync.WaitGroup)
|
||||
}
|
||||
|
||||
type server struct {
|
||||
address string
|
||||
logger logging.Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func New(ctx context.Context, address, rootURL, uiDir string, db data.Database, logger logging.Logger,
|
||||
runner update.Runner) Server {
|
||||
handler := newHandler(ctx, rootURL, uiDir, db, runner)
|
||||
return &server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.logger.Warn("shutting down (context canceled)")
|
||||
defer s.logger.Warn("shut down")
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
s.logger.Error("failed shutting down: %s", err)
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on %s", s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err)
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
18
internal/server/update.go
Normal file
18
internal/server/update.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *handlers) update(w http.ResponseWriter, r *http.Request) {
|
||||
start := h.timeNow()
|
||||
errors := h.runner.ForceUpdate(h.ctx)
|
||||
duration := h.timeNow().Sub(start)
|
||||
if len(errors) > 0 {
|
||||
httpErrors(w, http.StatusInternalServerError, errors)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
message := "All records updated successfully in " + duration.String()
|
||||
_, _ = w.Write([]byte(message))
|
||||
}
|
||||
293
internal/settings/cloudflare.go
Normal file
293
internal/settings/cloudflare.go
Normal file
@@ -0,0 +1,293 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type cloudflare struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
key string
|
||||
token string
|
||||
email string
|
||||
userServiceKey string
|
||||
zoneIdentifier string
|
||||
proxied bool
|
||||
ttl uint
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewCloudflare(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
Token string `json:"token"`
|
||||
Email string `json:"email"`
|
||||
UserServiceKey string `json:"user_service_key"`
|
||||
ZoneIdentifier string `json:"zone_identifier"`
|
||||
Proxied bool `json:"proxied"`
|
||||
TTL uint `json:"ttl"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &cloudflare{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
key: extraSettings.Key,
|
||||
token: extraSettings.Token,
|
||||
email: extraSettings.Email,
|
||||
userServiceKey: extraSettings.UserServiceKey,
|
||||
zoneIdentifier: extraSettings.ZoneIdentifier,
|
||||
proxied: extraSettings.Proxied,
|
||||
ttl: extraSettings.TTL,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := c.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *cloudflare) isValid() error {
|
||||
switch {
|
||||
case len(c.key) > 0: // email and key must be provided
|
||||
switch {
|
||||
case !c.matcher.CloudflareKey(c.key):
|
||||
return ErrMalformedKey
|
||||
case !verification.NewVerifier().MatchEmail(c.email):
|
||||
return ErrMalformedEmail
|
||||
}
|
||||
case len(c.userServiceKey) > 0: // only user service key
|
||||
if !c.matcher.CloudflareKey(c.key) {
|
||||
return ErrMalformedUserServiceKey
|
||||
}
|
||||
default: // API token only
|
||||
}
|
||||
switch {
|
||||
case len(c.zoneIdentifier) == 0:
|
||||
return ErrEmptyZoneIdentifier
|
||||
case c.ttl == 0:
|
||||
return ErrEmptyTTL
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cloudflare) String() string {
|
||||
return toString(c.domain, c.host, constants.CLOUDFLARE, c.ipVersion)
|
||||
}
|
||||
|
||||
func (c *cloudflare) Domain() string {
|
||||
return c.domain
|
||||
}
|
||||
|
||||
func (c *cloudflare) Host() string {
|
||||
return c.host
|
||||
}
|
||||
|
||||
func (c *cloudflare) IPVersion() ipversion.IPVersion {
|
||||
return c.ipVersion
|
||||
}
|
||||
|
||||
func (c *cloudflare) Proxied() bool {
|
||||
return c.proxied
|
||||
}
|
||||
|
||||
func (c *cloudflare) BuildDomainName() string {
|
||||
return buildDomainName(c.host, c.domain)
|
||||
}
|
||||
|
||||
func (c *cloudflare) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", c.BuildDomainName(), c.BuildDomainName())),
|
||||
Host: models.HTML(c.Host()),
|
||||
Provider: "<a href=\"https://www.cloudflare.com\">Cloudflare</a>",
|
||||
IPVersion: models.HTML(c.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cloudflare) setHeaders(request *http.Request) {
|
||||
setUserAgent(request)
|
||||
setContentType(request, "application/json")
|
||||
setAccept(request, "application/json")
|
||||
switch {
|
||||
case len(c.token) > 0:
|
||||
setAuthBearer(request, c.token)
|
||||
case len(c.userServiceKey) > 0:
|
||||
request.Header.Set("X-Auth-User-Service-Key", c.userServiceKey)
|
||||
case len(c.email) > 0 && len(c.key) > 0:
|
||||
request.Header.Set("X-Auth-Email", c.email)
|
||||
request.Header.Set("X-Auth-Key", c.key)
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain domain ID.
|
||||
// See https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records.
|
||||
func (c *cloudflare) getRecordID(ctx context.Context, client *http.Client, newIP net.IP) (
|
||||
identifier string, upToDate bool, err error) {
|
||||
recordType := A
|
||||
if newIP.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.cloudflare.com",
|
||||
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records", c.zoneIdentifier),
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("type", recordType)
|
||||
values.Set("name", c.BuildDomainName())
|
||||
values.Set("page", "1")
|
||||
values.Set("per_page", "1")
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
c.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return "", false, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
listRecordsResponse := struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []string `json:"errors"`
|
||||
Result []struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
if err := decoder.Decode(&listRecordsResponse); err != nil {
|
||||
return "", false, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(listRecordsResponse.Errors) > 0:
|
||||
return "", false, fmt.Errorf("%w: %s",
|
||||
ErrUnsuccessfulResponse, strings.Join(listRecordsResponse.Errors, ","))
|
||||
case !listRecordsResponse.Success:
|
||||
return "", false, ErrUnsuccessfulResponse
|
||||
case len(listRecordsResponse.Result) == 0:
|
||||
return "", false, ErrNoResultReceived
|
||||
case len(listRecordsResponse.Result) > 1:
|
||||
return "", false, fmt.Errorf("%w: %d instead of 1",
|
||||
ErrNumberOfResultsReceived, len(listRecordsResponse.Result))
|
||||
case listRecordsResponse.Result[0].Content == newIP.String(): // up to date
|
||||
return "", true, nil
|
||||
}
|
||||
return listRecordsResponse.Result[0].ID, false, nil
|
||||
}
|
||||
|
||||
func (c *cloudflare) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
identifier, upToDate, err := c.getRecordID(ctx, client, ip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", ErrGetRecordID, err)
|
||||
} else if upToDate {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.cloudflare.com",
|
||||
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records/%s", c.zoneIdentifier, identifier),
|
||||
}
|
||||
|
||||
requestData := struct {
|
||||
Type string `json:"type"` // A or AAAA depending on ip address given
|
||||
Name string `json:"name"` // DNS record name i.e. example.com
|
||||
Content string `json:"content"` // ip address
|
||||
Proxied bool `json:"proxied"` // whether the record is receiving the performance and security benefits of Cloudflare
|
||||
TTL uint `json:"ttl"`
|
||||
}{
|
||||
Type: recordType,
|
||||
Name: c.BuildDomainName(),
|
||||
Content: ip.String(),
|
||||
Proxied: c.proxied,
|
||||
TTL: c.ttl,
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
if err := encoder.Encode(requestData); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrRequestEncode, err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode > http.StatusUnsupportedMediaType {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var parsedJSON struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"errors"`
|
||||
Result struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err := decoder.Decode(&parsedJSON); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
if !parsedJSON.Success {
|
||||
var errStr string
|
||||
for _, e := range parsedJSON.Errors {
|
||||
errStr += fmt.Sprintf("error %d: %s; ", e.Code, e.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsuccessfulResponse, errStr)
|
||||
}
|
||||
|
||||
newIP = net.ParseIP(parsedJSON.Result.Content)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, parsedJSON.Result.Content)
|
||||
} else if !newIP.Equal(ip) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
13
internal/settings/constants.go
Normal file
13
internal/settings/constants.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package settings
|
||||
|
||||
const (
|
||||
badauth = "badauth"
|
||||
success = "success"
|
||||
nohost = "nohost"
|
||||
notfqdn = "notfqdn"
|
||||
badagent = "badagent"
|
||||
abuse = "abuse"
|
||||
nineoneone = "911"
|
||||
A = "A"
|
||||
AAAA = "AAAA"
|
||||
)
|
||||
151
internal/settings/ddnss.go
Normal file
151
internal/settings/ddnss.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type ddnss struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDdnss(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &ddnss{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *ddnss) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return ErrEmptyUsername
|
||||
case len(d.password) == 0:
|
||||
return ErrEmptyPassword
|
||||
case d.host == "*":
|
||||
return ErrHostWildcard
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ddnss) String() string {
|
||||
return toString(d.domain, d.host, constants.DDNSSDE, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *ddnss) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *ddnss) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *ddnss) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *ddnss) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *ddnss) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *ddnss) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://ddnss.de/\">DDNSS.de</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ddnss) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.ddnss.de",
|
||||
Path: "/upd.php",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("user", d.username)
|
||||
values.Set("pwd", d.password)
|
||||
values.Set("host", d.BuildDomainName())
|
||||
if !d.useProviderIP {
|
||||
if ip.To4() == nil { // ipv6
|
||||
values.Set("ip6", ip.String())
|
||||
} else {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyDataToSingleLine(s))
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(s, "badysys"):
|
||||
return nil, ErrInvalidSystemParam
|
||||
case strings.Contains(s, badauth):
|
||||
return nil, ErrAuth
|
||||
case strings.Contains(s, notfqdn):
|
||||
return nil, ErrHostnameNotExists
|
||||
case strings.Contains(s, "Updated 1 hostname"):
|
||||
return ip, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownResponse, s)
|
||||
}
|
||||
}
|
||||
206
internal/settings/digitalocean.go
Normal file
206
internal/settings/digitalocean.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type digitalOcean struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
}
|
||||
|
||||
func NewDigitalOcean(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &digitalOcean{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
token: extraSettings.Token,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *digitalOcean) isValid() error {
|
||||
if len(d.token) == 0 {
|
||||
return ErrEmptyToken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *digitalOcean) String() string {
|
||||
return toString(d.domain, d.host, constants.DIGITALOCEAN, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *digitalOcean) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *digitalOcean) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *digitalOcean) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *digitalOcean) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *digitalOcean) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *digitalOcean) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.digitalocean.com/\">DigitalOcean</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *digitalOcean) setHeaders(request *http.Request) {
|
||||
setUserAgent(request)
|
||||
setContentType(request, "application/json")
|
||||
setAccept(request, "application/json")
|
||||
setAuthBearer(request, d.token)
|
||||
}
|
||||
|
||||
func (d *digitalOcean) getRecordID(ctx context.Context, recordType string, client *http.Client) (
|
||||
recordID int, err error) {
|
||||
values := url.Values{}
|
||||
values.Set("name", d.BuildDomainName())
|
||||
values.Set("type", recordType)
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.digitalocean.com",
|
||||
Path: "/v2/domains/" + d.domain + "/records",
|
||||
RawQuery: values.Encode(),
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return 0, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var result struct {
|
||||
DomainRecords []struct {
|
||||
ID int `json:"id"`
|
||||
} `json:"domain_records"`
|
||||
}
|
||||
if err = decoder.Decode(&result); err != nil {
|
||||
return 0, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
if len(result.DomainRecords) == 0 {
|
||||
return 0, ErrNotFound
|
||||
} else if result.DomainRecords[0].ID == 0 {
|
||||
return 0, ErrDomainIDNotFound
|
||||
}
|
||||
|
||||
return result.DomainRecords[0].ID, nil
|
||||
}
|
||||
|
||||
func (d *digitalOcean) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil { // IPv6
|
||||
recordType = AAAA
|
||||
}
|
||||
|
||||
recordID, err := d.getRecordID(ctx, recordType, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", ErrGetRecordID, err)
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.digitalocean.com",
|
||||
Path: fmt.Sprintf("/v2/domains/%s/records/%d", d.domain, recordID),
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
requestData := struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Data string `json:"data"`
|
||||
}{
|
||||
Type: recordType,
|
||||
Name: d.host,
|
||||
Data: ip.String(),
|
||||
}
|
||||
if err := encoder.Encode(requestData); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrRequestEncode, err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var responseData struct {
|
||||
DomainRecord struct {
|
||||
Data string `json:"data"`
|
||||
} `json:"domain_record"`
|
||||
}
|
||||
if err := decoder.Decode(&responseData); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
newIP = net.ParseIP(responseData.DomainRecord.Data)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", responseData.DomainRecord.Data)
|
||||
} else if !newIP.Equal(ip) {
|
||||
return nil, fmt.Errorf("updated IP address %s is not the IP address %s sent for update", newIP, ip)
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
175
internal/settings/dnsomatic.go
Normal file
175
internal/settings/dnsomatic.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type dnsomatic struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDNSOMatic(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &dnsomatic{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dnsomatic) isValid() error {
|
||||
switch {
|
||||
case !d.matcher.DNSOMaticUsername(d.username):
|
||||
return fmt.Errorf("%w: %s", ErrMalformedUsername, d.username)
|
||||
case !d.matcher.DNSOMaticPassword(d.password):
|
||||
return ErrMalformedPassword
|
||||
case len(d.username) == 0:
|
||||
return ErrEmptyUsername
|
||||
case len(d.password) == 0:
|
||||
return ErrEmptyPassword
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dnsomatic) String() string {
|
||||
return toString(d.domain, d.host, constants.DNSOMATIC, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *dnsomatic) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dnsomatic) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dnsomatic) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dnsomatic) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *dnsomatic) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dnsomatic) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dnsomatic.com/\">dnsomatic</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnsomatic) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
// Multiple hosts can be updated in one query, see https://www.dnsomatic.com/docs/api
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "updates.dnsomatic.com",
|
||||
Path: "/nic/update",
|
||||
User: url.UserPassword(d.username, d.password),
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", d.BuildDomainName())
|
||||
if !d.useProviderIP {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
values.Set("wildcard", "NOCHG")
|
||||
if d.host == "*" {
|
||||
values.Set("wildcard", "ON")
|
||||
}
|
||||
values.Set("mx", "NOCHG")
|
||||
values.Set("backmx", "NOCHG")
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, s)
|
||||
}
|
||||
|
||||
switch s {
|
||||
case nohost, notfqdn:
|
||||
return nil, ErrHostnameNotExists
|
||||
case badauth:
|
||||
return nil, ErrAuth
|
||||
case badagent:
|
||||
return nil, ErrBannedUserAgent
|
||||
case abuse:
|
||||
return nil, ErrAbuse
|
||||
case "dnserr", nineoneone:
|
||||
return nil, fmt.Errorf("%w: %s", ErrDNSServerSide, s)
|
||||
}
|
||||
if strings.Contains(s, "nochg") || strings.Contains(s, "good") {
|
||||
ipsV4 := verification.NewVerifier().SearchIPv4(s)
|
||||
ipsV6 := verification.NewVerifier().SearchIPv6(s)
|
||||
ips := append(ipsV4, ipsV6...)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ips[0])
|
||||
}
|
||||
if !d.useProviderIP && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnknownResponse, s)
|
||||
}
|
||||
206
internal/settings/dnspod.go
Normal file
206
internal/settings/dnspod.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type dnspod struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
}
|
||||
|
||||
func NewDNSPod(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &dnspod{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
token: extraSettings.Token,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dnspod) isValid() error {
|
||||
if len(d.token) == 0 {
|
||||
return ErrEmptyToken
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dnspod) String() string {
|
||||
return toString(d.domain, d.host, constants.DNSPOD, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *dnspod) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dnspod) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dnspod) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dnspod) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *dnspod) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dnspod) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dnspod.cn/\">DNSPod</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnspod) setHeaders(request *http.Request) {
|
||||
setContentType(request, "application/x-www-form-urlencoded")
|
||||
setAccept(request, "application/json")
|
||||
setUserAgent(request)
|
||||
}
|
||||
|
||||
func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dnsapi.cn",
|
||||
Path: "/Record.List",
|
||||
}
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("login_token", d.token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("length", "200")
|
||||
values.Set("sub_domain", d.host)
|
||||
values.Set("record_type", recordType)
|
||||
buffer := bytes.NewBufferString(values.Encode())
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var recordResp struct {
|
||||
Records []struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Line string `json:"line"`
|
||||
} `json:"records"`
|
||||
}
|
||||
if err := decoder.Decode(&recordResp); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
var recordID, recordLine string
|
||||
for _, record := range recordResp.Records {
|
||||
if record.Type == A && record.Name == d.host {
|
||||
receivedIP := net.ParseIP(record.Value)
|
||||
if ip.Equal(receivedIP) {
|
||||
return ip, nil
|
||||
}
|
||||
recordID = record.ID
|
||||
recordLine = record.Line
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(recordID) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
u.Path = "/Record.Ddns"
|
||||
values = url.Values{}
|
||||
values.Set("login_token", d.token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("record_id", recordID)
|
||||
values.Set("value", ip.String())
|
||||
values.Set("record_line", recordLine)
|
||||
values.Set("sub_domain", d.host)
|
||||
buffer = bytes.NewBufferString(values.Encode())
|
||||
|
||||
request, err = http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.setHeaders(request)
|
||||
|
||||
response, err = client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder = json.NewDecoder(response.Body)
|
||||
var ddnsResp struct {
|
||||
Record struct {
|
||||
ID int64 `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Name string `json:"name"`
|
||||
} `json:"record"`
|
||||
}
|
||||
if err := decoder.Decode(&ddnsResp); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
ipStr := ddnsResp.Record.Value
|
||||
receivedIP := net.ParseIP(ipStr)
|
||||
if receivedIP == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ipStr)
|
||||
} else if !ip.Equal(receivedIP) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, receivedIP.String())
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
173
internal/settings/dondominio.go
Normal file
173
internal/settings/dondominio.go
Normal file
@@ -0,0 +1,173 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type donDominio struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
name string
|
||||
}
|
||||
|
||||
func NewDonDominio(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(host) == 0 {
|
||||
host = "@" // default
|
||||
}
|
||||
d := &donDominio{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
name: extraSettings.Name,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *donDominio) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return ErrEmptyUsername
|
||||
case len(d.password) == 0:
|
||||
return ErrEmptyPassword
|
||||
case len(d.name) == 0:
|
||||
return ErrEmptyName
|
||||
case d.host != "@":
|
||||
return ErrHostOnlyAt
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *donDominio) String() string {
|
||||
return toString(d.domain, d.host, constants.DONDOMINIO, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *donDominio) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *donDominio) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *donDominio) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *donDominio) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *donDominio) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *donDominio) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dondominio.com/\">DonDominio</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *donDominio) setHeaders(request *http.Request) {
|
||||
setUserAgent(request)
|
||||
setContentType(request, "application/x-www-form-urlencoded")
|
||||
setAccept(request, "application/json")
|
||||
}
|
||||
|
||||
func (d *donDominio) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "simple-api.dondominio.net",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("apiuser", d.username)
|
||||
values.Set("apipasswd", d.password)
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("name", d.name)
|
||||
isIPv4 := ip.To4() != nil
|
||||
if isIPv4 {
|
||||
values.Set("ipv4", ip.String())
|
||||
} else {
|
||||
values.Set("ipv6", ip.String())
|
||||
}
|
||||
buffer := strings.NewReader(values.Encode())
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var responseData struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorCodeMessage string `json:"errorCodeMsg"`
|
||||
ResponseData struct {
|
||||
GlueRecords []struct {
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
} `json:"gluerecords"`
|
||||
} `json:"responseData"`
|
||||
}
|
||||
if err := decoder.Decode(&responseData); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
if !responseData.Success {
|
||||
return nil, fmt.Errorf("%w: %s (error code %d)",
|
||||
ErrUnsuccessfulResponse, responseData.ErrorCodeMessage, responseData.ErrorCode)
|
||||
}
|
||||
ipString := responseData.ResponseData.GlueRecords[0].IPv4
|
||||
if !isIPv4 {
|
||||
ipString = responseData.ResponseData.GlueRecords[0].IPv6
|
||||
}
|
||||
newIP = net.ParseIP(ipString)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ipString)
|
||||
} else if !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
290
internal/settings/dreamhost.go
Normal file
290
internal/settings/dreamhost.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type dreamhost struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
key string
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDreamhost(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(host) == 0 {
|
||||
host = "@" // default
|
||||
}
|
||||
d := &dreamhost{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
key: extraSettings.Key,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) isValid() error {
|
||||
switch {
|
||||
case !d.matcher.DreamhostKey(d.key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case d.host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) String() string {
|
||||
return toString(d.domain, d.host, constants.DREAMHOST, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *dreamhost) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dreamhost) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dreamhost) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dreamhost) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (d *dreamhost) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dreamhost) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dreamhost) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
|
||||
records, err := d.getRecords(ctx, client)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrListRecords, err)
|
||||
}
|
||||
|
||||
var oldIP net.IP
|
||||
for _, data := range records.Data {
|
||||
if data.Type == recordType && data.Record == d.BuildDomainName() {
|
||||
if data.Editable == "0" {
|
||||
return nil, ErrRecordNotEditable
|
||||
}
|
||||
oldIP = net.ParseIP(data.Value)
|
||||
if ip.Equal(oldIP) { // success, nothing to change
|
||||
return ip, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if oldIP != nil { // Found editable record with a different IP address, so remove it
|
||||
if err := d.removeRecord(ctx, client, oldIP); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrRemoveRecord, err)
|
||||
}
|
||||
}
|
||||
if err := d.createRecord(ctx, client, ip); err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrCreateRecord, err)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
type (
|
||||
dreamHostRecords struct {
|
||||
Result string `json:"result"`
|
||||
Data []struct {
|
||||
Editable string `json:"editable"`
|
||||
Type string `json:"type"`
|
||||
Record string `json:"record"`
|
||||
Value string `json:"value"`
|
||||
} `json:"data"`
|
||||
}
|
||||
dreamhostReponse struct {
|
||||
Result string `json:"result"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
func (d *dreamhost) defaultURLValues() (values url.Values) {
|
||||
uuid := make([]byte, 16)
|
||||
_, _ = io.ReadFull(rand.Reader, uuid)
|
||||
//nolint:gomnd
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||
//nolint:gomnd
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||
values = url.Values{}
|
||||
values.Set("key", d.key)
|
||||
values.Set("unique_id", string(uuid))
|
||||
values.Set("format", "json")
|
||||
return values
|
||||
}
|
||||
|
||||
func (d *dreamhost) getRecords(ctx context.Context, client *http.Client) (
|
||||
records dreamHostRecords, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := d.defaultURLValues()
|
||||
values.Set("cmd", "dns-list_records")
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return records, err
|
||||
}
|
||||
setUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return records, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return records, fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&records); err != nil {
|
||||
return records, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
if records.Result != success {
|
||||
return records, fmt.Errorf("%w: %s", ErrUnsuccessfulResponse, records.Result)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) removeRecord(ctx context.Context, client *http.Client, ip net.IP) error {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := d.defaultURLValues()
|
||||
values.Set("cmd", "dns-remove_record")
|
||||
values.Set("record", d.domain)
|
||||
values.Set("type", recordType)
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
var dhResponse dreamhostReponse
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&dhResponse); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
if dhResponse.Result != success { // this should not happen
|
||||
return fmt.Errorf("%w: %s - %s",
|
||||
ErrUnsuccessfulResponse, dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) createRecord(ctx context.Context, client *http.Client, ip net.IP) error {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := d.defaultURLValues()
|
||||
values.Set("cmd", "dns-add_record")
|
||||
values.Set("record", d.domain)
|
||||
values.Set("type", recordType)
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("%w: %d: %s",
|
||||
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
var dhResponse dreamhostReponse
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
if err := decoder.Decode(&dhResponse); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
|
||||
}
|
||||
|
||||
if dhResponse.Result != success {
|
||||
return fmt.Errorf("%w: %s - %s",
|
||||
ErrUnsuccessfulResponse, dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user