mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-22 17:12:14 -04:00
Compare commits
278 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6f72d97bc | ||
|
|
5fff4f4a56 | ||
|
|
41d848f9a7 | ||
|
|
e42feb933f | ||
|
|
e7ebfc4b9a | ||
|
|
23043b73d9 | ||
|
|
4fb7526ce7 | ||
|
|
43d405cfd8 | ||
|
|
b0493e9c1c | ||
|
|
28509cfb36 | ||
|
|
522cc7c9c6 | ||
|
|
519b9c4380 | ||
|
|
464a557adb | ||
|
|
5ae4668296 | ||
|
|
934c67d710 | ||
|
|
9e6d456fc9 | ||
|
|
15c6ad4b01 | ||
|
|
3339c838fa | ||
|
|
18751973c1 | ||
|
|
49b779813a | ||
|
|
f779a2159e | ||
|
|
43f47fdee0 | ||
|
|
0e3703b8f2 | ||
|
|
4a2a6d83d5 | ||
|
|
1aebdef825 | ||
|
|
8819e5f030 | ||
|
|
228fc507a7 | ||
|
|
eaa7200734 | ||
|
|
bfbd68f9d5 | ||
|
|
83a9a2d999 | ||
|
|
8a18aec420 | ||
|
|
3fd82ab89f | ||
|
|
2165b69137 | ||
|
|
3ad972b718 | ||
|
|
e197478638 | ||
|
|
3b0fae84e5 | ||
|
|
46b1d649f0 | ||
|
|
bd4b87032f | ||
|
|
59bffe99ba | ||
|
|
d8de9d25ad | ||
|
|
ba8cd00326 | ||
|
|
9671407502 | ||
|
|
3a3c892385 | ||
|
|
cd37ab1646 | ||
|
|
7c815924ff | ||
|
|
675c4ae64e | ||
|
|
71b9b92288 | ||
|
|
23ec8d7907 | ||
|
|
89711cd76d | ||
|
|
f09ca2dd0a | ||
|
|
251112697a | ||
|
|
7fcd0f902b | ||
|
|
88474c8a3a | ||
|
|
5674102019 | ||
|
|
41a64bbd55 | ||
|
|
f793454476 | ||
|
|
bcbf0938c1 | ||
|
|
1561babd76 | ||
|
|
45c684232d | ||
|
|
d725c7c856 | ||
|
|
bc081ef3c1 | ||
|
|
a688255270 | ||
|
|
0655f1a080 | ||
|
|
b3a7669d2a | ||
|
|
f455c2a880 | ||
|
|
584597d5cb | ||
|
|
1f9eb467ca | ||
|
|
b1a69e028e | ||
|
|
39c3732202 | ||
|
|
b2634a15fa | ||
|
|
5b426afc57 | ||
|
|
f5b76f214e | ||
|
|
e4f0c0d561 | ||
|
|
0f28807240 | ||
|
|
d1fbbbe9d4 | ||
|
|
ae7f10448d | ||
|
|
01334ad91f | ||
|
|
fa15abb781 | ||
|
|
87ffce17b1 | ||
|
|
7382c5bb92 | ||
|
|
5b657dfa92 | ||
|
|
ecda99a217 | ||
|
|
7fccf2c979 | ||
|
|
14cd7b0a47 | ||
|
|
d0f03c8ae1 | ||
|
|
7acc9b931a | ||
|
|
ea215dec6b | ||
|
|
87f06eeb28 | ||
|
|
963da5aab7 | ||
|
|
9e73f99ee1 | ||
|
|
01f4044404 | ||
|
|
904c59d6ac | ||
|
|
242c47bbed | ||
|
|
00a1d25847 | ||
|
|
8b327f81c7 | ||
|
|
b29d6bb9d2 | ||
|
|
cdbebc2323 | ||
|
|
335c82b4be | ||
|
|
28e2d00198 | ||
|
|
c584f9b3d5 | ||
|
|
51dd14cb66 | ||
|
|
41e61ddaa6 | ||
|
|
ee69feaaea | ||
|
|
9252bca505 | ||
|
|
8f8d187a32 | ||
|
|
d837acd3a2 | ||
|
|
ad57321ea3 | ||
|
|
e20a9b8634 | ||
|
|
5847e9fe8f | ||
|
|
e608a016db | ||
|
|
050b34a9bd | ||
|
|
96cf980b36 | ||
|
|
3fc3b115f8 | ||
|
|
6a3789f78c | ||
|
|
cd6d46e146 | ||
|
|
6fdbaa1b97 | ||
|
|
4684dfc9a3 | ||
|
|
9b094fff7a | ||
|
|
a4af81dddb | ||
|
|
e735ef335d | ||
|
|
3ece0c967e | ||
|
|
523bbee5b8 | ||
|
|
86f559c0b3 | ||
|
|
4be365574d | ||
|
|
10a952f7b9 | ||
|
|
3606f7a461 | ||
|
|
ae24ab8d56 | ||
|
|
bc1a5b206f | ||
|
|
2dceab7452 | ||
|
|
1e74dc6179 | ||
|
|
fe00994522 | ||
|
|
d22dc41903 | ||
|
|
76b1f5f0df | ||
|
|
c5de1358dd | ||
|
|
4133dbfdc7 | ||
|
|
c4f50992c2 | ||
|
|
f8b67e5261 | ||
|
|
1b154df8aa | ||
|
|
f9edf75540 | ||
|
|
ae748fb80d | ||
|
|
edb6c491bf | ||
|
|
2f196b4886 | ||
|
|
2d7d016650 | ||
|
|
1da19b3d6d | ||
|
|
0c520eddda | ||
|
|
050825d399 | ||
|
|
53212df518 | ||
|
|
6fe4743bc0 | ||
|
|
4cdc71b45c | ||
|
|
36a6275bd2 | ||
|
|
ee66170a87 | ||
|
|
bb62a9d9e1 | ||
|
|
e04c1f83df | ||
|
|
e19cabc894 | ||
|
|
682821efd0 | ||
|
|
276c1c02fd | ||
|
|
9b8ec3298b | ||
|
|
cc670b3939 | ||
|
|
0c3d258620 | ||
|
|
d39ecd6fd3 | ||
|
|
43dd02dbd5 | ||
|
|
381af0cd90 | ||
|
|
2bede070de | ||
|
|
09b810732f | ||
|
|
398566850d | ||
|
|
201df818d3 | ||
|
|
b1a3740059 | ||
|
|
ae978e007b | ||
|
|
3688208030 | ||
|
|
da4341fbba | ||
|
|
1705afeada | ||
|
|
844904aa7b | ||
|
|
803232e670 | ||
|
|
2f7ee832b8 | ||
|
|
943d1486b3 | ||
|
|
d727feefc4 | ||
|
|
aee3022a11 | ||
|
|
864a696680 | ||
|
|
34623e1b81 | ||
|
|
c0e57c6e1d | ||
|
|
1ff629dc17 | ||
|
|
23f4897365 | ||
|
|
e3472476e2 | ||
|
|
869cf52c7d | ||
|
|
8b2e83a69e | ||
|
|
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 |
5
.devcontainer/.dockerignore
Normal file
5
.devcontainer/.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.dockerignore
|
||||
devcontainer.json
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
README.md
|
||||
1
.devcontainer/Dockerfile
Normal file
1
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1 @@
|
||||
FROM qmcgaw/godevcontainer
|
||||
69
.devcontainer/README.md
Normal file
69
.devcontainer/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Development container
|
||||
|
||||
Development container that can be used with VSCode.
|
||||
|
||||
It works on Linux, Windows and OSX.
|
||||
|
||||
## Requirements
|
||||
|
||||
- [VS code](https://code.visualstudio.com/download) installed
|
||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
||||
|
||||
## Setup
|
||||
|
||||
1. Create the following files on your host if you don't have them:
|
||||
|
||||
```sh
|
||||
touch ~/.gitconfig ~/.zsh_history
|
||||
```
|
||||
|
||||
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
|
||||
|
||||
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is accessible by Docker.
|
||||
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
|
||||
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
|
||||
|
||||
## Customization
|
||||
|
||||
### Customize the image
|
||||
|
||||
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
|
||||
|
||||
```Dockerfile
|
||||
FROM qmcgaw/godevcontainer
|
||||
RUN apk add curl
|
||||
```
|
||||
|
||||
To rebuild the image, either:
|
||||
|
||||
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
|
||||
- With a terminal, go to this directory and `docker-compose build`
|
||||
|
||||
### Customize VS code settings
|
||||
|
||||
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
|
||||
|
||||
### Entrypoint script
|
||||
|
||||
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
|
||||
|
||||
### Publish a port
|
||||
|
||||
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml). You can also now do it directly with VSCode without restarting the container.
|
||||
|
||||
### Run other services
|
||||
|
||||
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
|
||||
|
||||
```yml
|
||||
database:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: password
|
||||
```
|
||||
|
||||
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
|
||||
1. In the VS code command palette, rebuild the container.
|
||||
@@ -1,116 +1,75 @@
|
||||
{
|
||||
"name": "ddns-dev",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"postCreateCommand": "go mod download",
|
||||
"workspaceFolder": "/workspace",
|
||||
"appPort": 8000,
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"IBM.output-colorizer",
|
||||
"eamodio.gitlens",
|
||||
"mhutchie.git-graph",
|
||||
"davidanson.vscode-markdownlint",
|
||||
"shardulm94.trailing-spaces",
|
||||
"alefragnani.Bookmarks",
|
||||
"Gruntfuggly.todo-tree",
|
||||
"mohsen1.prettify-json",
|
||||
"quicktype.quicktype",
|
||||
"spikespaz.vscode-smoothtype",
|
||||
"vscode-icons-team.vscode-icons"
|
||||
],
|
||||
"settings": {
|
||||
// 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=integration"
|
||||
},
|
||||
"gopls.env": {
|
||||
"GOFLAGS": "-tags=integration"
|
||||
},
|
||||
"go.testEnvVars": {},
|
||||
"go.testFlags": [
|
||||
"-v"
|
||||
],
|
||||
"go.testTimeout": "600s"
|
||||
}
|
||||
{
|
||||
"name": "ddns-dev",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
|
||||
"workspaceFolder": "/workspace",
|
||||
// "overrideCommand": "",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"golang.go",
|
||||
"eamodio.gitlens", // IDE Git information
|
||||
"davidanson.vscode-markdownlint",
|
||||
"ms-azuretools.vscode-docker", // Docker integration and linting
|
||||
"shardulm94.trailing-spaces", // Show trailing spaces
|
||||
"Gruntfuggly.todo-tree", // Highlights TODO comments
|
||||
"bierner.emojisense", // Emoji sense for markdown
|
||||
"stkb.rewrap", // rewrap comments after n characters on one line
|
||||
"vscode-icons-team.vscode-icons", // Better file extension icons
|
||||
"github.vscode-pull-request-github", // Github interaction
|
||||
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
|
||||
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
|
||||
"IBM.output-colorizer", // Colorize your output/test logs
|
||||
"github.copilot" // AI code completion
|
||||
// "mohsen1.prettify-json", // Prettify JSON data
|
||||
// "zxh404.vscode-proto3", // Supports Proto syntax
|
||||
// "jrebocho.vscode-random", // Generates random values
|
||||
// "alefragnani.Bookmarks", // Manage bookmarks
|
||||
// "quicktype.quicktype", // Paste JSON as code
|
||||
// "spikespaz.vscode-smoothtype", // smooth cursor animation
|
||||
],
|
||||
"settings": {
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": true,
|
||||
"go.buildTags": "",
|
||||
"go.toolsEnvVars": {
|
||||
"CGO_ENABLED": "0"
|
||||
},
|
||||
"go.useLanguageServer": true,
|
||||
"go.testEnvVars": {
|
||||
"CGO_ENABLED": "1"
|
||||
},
|
||||
"go.testFlags": [
|
||||
"-v",
|
||||
"-race"
|
||||
],
|
||||
"go.testTimeout": "10s",
|
||||
"go.coverOnSingleTest": true,
|
||||
"go.coverOnSingleTestFile": true,
|
||||
"go.coverOnTestPackage": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintOnSave": "package",
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true,
|
||||
"vulncheck": "Imports"
|
||||
},
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,26 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
vscode:
|
||||
image: qmcgaw/godevcontainer
|
||||
build: .
|
||||
volumes:
|
||||
- ../:/workspace
|
||||
- ~/.ssh:/home/vscode/.ssh:ro
|
||||
- ~/.ssh:/root/.ssh:ro
|
||||
# Docker socket to access Docker server
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# SSH directory for Linux, OSX and WSL
|
||||
# On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
|
||||
# created in the container. On Windows, files are copied
|
||||
# from /mnt/ssh to ~/.ssh to fix permissions.
|
||||
- ~/.ssh:/mnt/ssh
|
||||
# Shell history persistence
|
||||
- ~/.zsh_history:/root/.zsh_history
|
||||
# Git config
|
||||
- ~/.gitconfig:/root/.gitconfig
|
||||
environment:
|
||||
- TZ=
|
||||
cap_add:
|
||||
# For debugging with dlv
|
||||
- SYS_PTRACE
|
||||
security_opt:
|
||||
# For debugging with dlv
|
||||
- seccomp:unconfined
|
||||
entrypoint: zsh -c "while sleep 1000; do :; done"
|
||||
entrypoint: [ "zsh", "-c", "while sleep 1000; do :; done" ]
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
docs
|
||||
readme
|
||||
.gitignore
|
||||
config.json
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
README.md
|
||||
ui/favicon.svg
|
||||
|
||||
48
.github/ISSUE_TEMPLATE/bug.md
vendored
48
.github/ISSUE_TEMPLATE/bug.md
vendored
@@ -7,46 +7,26 @@ 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?
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
- [ ] Yes
|
||||
- [x] No
|
||||
|
||||
2. What DNS service provider(s) are you using?
|
||||
|
||||
- [x] Cloudflare
|
||||
- [ ] DDNSS.de
|
||||
- [ ] DonDominio
|
||||
- [ ] DNSPod
|
||||
- [ ] Dreamhost
|
||||
- [ ] DuckDNS
|
||||
- [ ] DynDNS
|
||||
- [ ] GoDaddy
|
||||
- [ ] Google
|
||||
- [ ] He.net
|
||||
- [ ] Infomaniak
|
||||
- [ ] Namecheap
|
||||
- [ ] NoIP
|
||||
|
||||
3. What's the version of the program?
|
||||
|
||||
**See the line at the top of your logs**
|
||||
<!-- 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 run
|
||||
- [x] Docker Compose
|
||||
- [ ] Kubernetes
|
||||
- [ ] Docker stack
|
||||
- [ ] Docker swarm
|
||||
- [ ] Podman
|
||||
- [ ] Other:
|
||||
|
||||
5. Extra information
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
|
||||
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
10
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -9,6 +9,12 @@ assignees: qdm12
|
||||
|
||||
1. What's the feature?
|
||||
|
||||
2. Why do you need this feature?
|
||||
2. Extra information?
|
||||
|
||||
3. Extra information?
|
||||
<!--
|
||||
|
||||
YOU CAN CHAT THERE EVENTUALLY:
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
|
||||
52
.github/ISSUE_TEMPLATE/help.md
vendored
52
.github/ISSUE_TEMPLATE/help.md
vendored
@@ -7,46 +7,26 @@ 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?
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
- [ ] Yes
|
||||
- [x] No
|
||||
|
||||
2. What DNS service provider(s) are you using?
|
||||
|
||||
- [x] Cloudflare
|
||||
- [ ] DDNSS.de
|
||||
- [ ] DonDominio
|
||||
- [ ] DNSPod
|
||||
- [ ] Dreamhost
|
||||
- [ ] DuckDNS
|
||||
- [ ] DynDNS
|
||||
- [ ] GoDaddy
|
||||
- [ ] Google
|
||||
- [ ] He.net
|
||||
- [ ] Infomaniak
|
||||
- [ ] Namecheap
|
||||
- [ ] NoIP
|
||||
|
||||
3. What's the version of the program?
|
||||
|
||||
**See the line at the top of your logs**
|
||||
<!-- 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 run
|
||||
- [x] Docker Compose
|
||||
- [ ] Kubernetes
|
||||
- [ ] Docker stack
|
||||
- [ ] Docker swarm
|
||||
- [ ] Podman
|
||||
- [ ] Other:
|
||||
|
||||
5. Extra information
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
@@ -54,9 +34,9 @@ Logs:
|
||||
|
||||
```
|
||||
|
||||
Configuration file:
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```yml
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
|
||||
129
.github/workflows/build.yml
vendored
129
.github/workflows/build.yml
vendored
@@ -1,31 +1,108 @@
|
||||
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-ignore:
|
||||
- .devcontainer
|
||||
- .github/ISSUE_TEMPLATE
|
||||
- .github/workflows/build-branch.yml
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/buildx-latest.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/CODEOWNERS
|
||||
- .github/CONTRIBUTING.md
|
||||
- .github/FUNDING.yml
|
||||
- .github/labels.yml
|
||||
- .vscode
|
||||
- readme
|
||||
- .gitignore
|
||||
- config.json
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
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@v3
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- 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
|
||||
|
||||
# We run this here to use the caching of the previous steps
|
||||
- name: Build final image
|
||||
run: docker build .
|
||||
|
||||
publish:
|
||||
needs: [verify]
|
||||
if: |
|
||||
github.repository == 'qdm12/ddns-updater' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'release' ||
|
||||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: docker build .
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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@v3
|
||||
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 }}
|
||||
ghcr.io/${{ github.repository_owner }}/ddns-updater:${{ steps.vars.outputs.version }}
|
||||
push: true
|
||||
|
||||
47
.github/workflows/buildx-branch.yml
vendored
47
.github/workflows/buildx-branch.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: Buildx branch
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
- '*/*'
|
||||
- '!master'
|
||||
paths-ignore:
|
||||
- .devcontainer
|
||||
- .github/ISSUE_TEMPLATE
|
||||
- .github/workflows/build.yml
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/buildx-latest.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/CODEOWNERS
|
||||
- .github/CONTRIBUTING.md
|
||||
- .github/FUNDING.yml
|
||||
- .github/labels.yml
|
||||
- .vscode
|
||||
- readme
|
||||
- .gitignore
|
||||
- config.json
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
- 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
|
||||
44
.github/workflows/buildx-latest.yml
vendored
44
.github/workflows/buildx-latest.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: Buildx latest
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .devcontainer
|
||||
- .github/ISSUE_TEMPLATE
|
||||
- .github/workflows/build.yml
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/buildx-branch.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/CODEOWNERS
|
||||
- .github/CONTRIBUTING.md
|
||||
- .github/FUNDING.yml
|
||||
- .github/labels.yml
|
||||
- .vscode
|
||||
- readme
|
||||
- .gitignore
|
||||
- config.json
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
- 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
|
||||
44
.github/workflows/buildx-release.yml
vendored
44
.github/workflows/buildx-release.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: Buildx release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
paths-ignore:
|
||||
- .devcontainer
|
||||
- .github/ISSUE_TEMPLATE
|
||||
- .github/workflows/build.yml
|
||||
- .github/workflows/buildx-branch.yml
|
||||
- .github/workflows/buildx-latest.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/CODEOWNERS
|
||||
- .github/CONTRIBUTING.md
|
||||
- .github/FUNDING.yml
|
||||
- .github/labels.yml
|
||||
- .vscode
|
||||
- readme
|
||||
- .gitignore
|
||||
- config.json
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
- 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
|
||||
4
.github/workflows/dockerhub-description.yml
vendored
4
.github/workflows/dockerhub-description.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Docker Hub Description
|
||||
uses: peter-evans/dockerhub-description@v2.1.0
|
||||
uses: peter-evans/dockerhub-description@v3.1.0
|
||||
env:
|
||||
DOCKERHUB_USERNAME: qmcgaw
|
||||
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
14
.github/workflows/labels.yml
vendored
14
.github/workflows/labels.yml
vendored
@@ -1,18 +1,18 @@
|
||||
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
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
- name: Labeler
|
||||
if: success()
|
||||
uses: crazy-max/ghaction-github-labeler@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
uses: crazy-max/ghaction-github-labeler@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1 @@
|
||||
*.exe
|
||||
updater
|
||||
.vscode
|
||||
data
|
||||
@@ -4,40 +4,81 @@ linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- containedctx
|
||||
- dupl
|
||||
- goerr113
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
# - cyclop
|
||||
- asasalint
|
||||
- asciicheck
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- containedctx
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- dupword
|
||||
- durationcheck
|
||||
- errchkjson
|
||||
- errname
|
||||
- errorlint
|
||||
- execinquery
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- forcetypeassert
|
||||
- gci
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goerr113
|
||||
- goheader
|
||||
- goimports
|
||||
- golint
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- maligned
|
||||
- grouper
|
||||
- importas
|
||||
- interfacebloat
|
||||
- ireturn
|
||||
- lll
|
||||
- maintidx
|
||||
- makezero
|
||||
- misspell
|
||||
- musttag
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- nilnil
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- reassign
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- sqlclosecheck
|
||||
- tenv
|
||||
- thelper
|
||||
- tparallel
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
|
||||
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"
|
||||
}
|
||||
111
Dockerfile
111
Dockerfile
@@ -1,70 +1,93 @@
|
||||
ARG ALPINE_VERSION=3.12
|
||||
ARG GO_VERSION=1.15
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
ARG ALPINE_VERSION=3.18
|
||||
ARG GO_VERSION=1.20
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.52.2
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates tzdata
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
ARG GOLANGCI_LINT_VERSION=v1.31.0
|
||||
RUN apk --update add git
|
||||
ENV CGO_ENABLED=0
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY .golangci.yml .
|
||||
ENV CGO_ENABLED=0
|
||||
RUN apk --update add git g++
|
||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||
# Copy repository code and install Go dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY pkg/ ./pkg/
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
COPY cmd/updater/main.go .
|
||||
RUN go test ./...
|
||||
RUN go build -trimpath -ldflags="-s -w" -o app
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS test
|
||||
# Note on the go race detector:
|
||||
# - we set CGO_ENABLED=1 to have it enabled
|
||||
# - we installed g++ to support the race detector
|
||||
ENV CGO_ENABLED=1
|
||||
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS lint
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS build
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
ARG TARGETPLATFORM
|
||||
RUN GOARCH="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field arch)" \
|
||||
GOARM="$(xcputranslate translate -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
|
||||
ENV VERSION=$VERSION \
|
||||
BUILD_DATE=$BUILD_DATE \
|
||||
VCS_REF=$VCS_REF
|
||||
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.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 Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP"
|
||||
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
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
USER ${UID}:${GID}
|
||||
ENTRYPOINT ["/updater/app"]
|
||||
ENV \
|
||||
# Core
|
||||
CONFIG= \
|
||||
PERIOD=5m \
|
||||
IP_METHOD=cycle \
|
||||
IPV4_METHOD=cycle \
|
||||
IPV6_METHOD=cycle \
|
||||
UPDATE_COOLDOWN_PERIOD=5m \
|
||||
PUBLICIP_FETCHERS=all \
|
||||
PUBLICIP_HTTP_PROVIDERS=all \
|
||||
PUBLICIPV4_HTTP_PROVIDERS=all \
|
||||
PUBLICIPV6_HTTP_PROVIDERS=all \
|
||||
PUBLICIP_DNS_PROVIDERS=all \
|
||||
PUBLICIP_DNS_TIMEOUT=3s \
|
||||
HTTP_TIMEOUT=10s \
|
||||
|
||||
DATADIR=/updater/data \
|
||||
RESOLVER_ADDRESS= \
|
||||
RESOLVER_TIMEOUT=5s \
|
||||
# Web UI
|
||||
LISTENING_PORT=8000 \
|
||||
ROOT_URL=/ \
|
||||
|
||||
# Backup
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data \
|
||||
|
||||
# Other
|
||||
LOG_ENCODING=console \
|
||||
LOG_LEVEL=info \
|
||||
NODE_ID=-1 \
|
||||
GOTIFY_URL= \
|
||||
GOTIFY_TOKEN= \
|
||||
LOG_CALLER=hidden \
|
||||
SHOUTRRR_ADDRESSES= \
|
||||
TZ=
|
||||
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
|
||||
COPY --chown=1000 ui/* /updater/ui/
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
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"
|
||||
COPY --from=build --chown=${UID}:${GID} /tmp/gobuild/app /updater/app
|
||||
|
||||
699
README.md
699
README.md
@@ -1,353 +1,346 @@
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
|
||||
*Light container updating DNS A records periodically for Cloudflare, DDNSS.de, DonDominio, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP*
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://microbadger.com/images/qmcgaw/ddns-updater)
|
||||
[](https://microbadger.com/images/qmcgaw/ddns-updater)
|
||||
|
||||
[](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
|
||||
## Features
|
||||
|
||||
- Updates periodically A records for different DNS providers: Cloudflare, DDNSS.de, DonDominio, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP ([create an issue](https://github.com/qdm12/ddns-updater/issues/new/choose) for more)
|
||||
- Web User interface
|
||||
|
||||

|
||||
|
||||
- 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
|
||||
- Sends notifications to your Android phone, see the [**Gotify**](#Gotify) section (it's free, open source and self hosted 🆒)
|
||||
- Compatible with `amd64`, `386`, `arm64`, `arm32v7` (Raspberry Pis) CPU architectures.
|
||||
|
||||
## Setup
|
||||
|
||||
The program reads the configuration from a JSON configuration file.
|
||||
|
||||
1. First, create a JSON configuration starting from, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
1. You can find more information in the [configuration section](#configuration) to customize it.
|
||||
1. You can either use a bind mounted file or put all your JSON in a single line with the `CONFIG` environment variable, see the two subsections below for each
|
||||
|
||||
### Using the CONFIG variable
|
||||
|
||||
1. Remove all 'new lines' in order to put your entire JSON in a single line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`)
|
||||
1. Set the `CONFIG` environment variable to your single line configuration
|
||||
1. Use the following command:
|
||||
|
||||
```sh
|
||||
docker run -d -p 8000:8000/tcp -e CONFIG='{"settings": [{"provider": "namecheap", ...}]}' qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
Note that this CONFIG environment variable takes precedence over the config.json file if it is set.
|
||||
|
||||
### Using a file
|
||||
|
||||
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
|
||||
|
||||
```sh
|
||||
mkdir data
|
||||
touch data/config.json
|
||||
# Owned by user ID of Docker container (1000)
|
||||
chown -R 1000 data
|
||||
# all access (for creating json database file data/updates.json)
|
||||
chmod 700 data
|
||||
# read access only
|
||||
chmod 400 data/config.json
|
||||
```
|
||||
|
||||
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
|
||||
|
||||
1. Place your JSON configuration in `data/config.json`
|
||||
1. Use the following command:
|
||||
|
||||
```sh
|
||||
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
### Next steps
|
||||
|
||||
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*, or in your `CONFIG` environment variable:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "",
|
||||
},
|
||||
{
|
||||
"provider": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following parameters are to be added:
|
||||
|
||||
For all record update configuration, you have to specify the DNS provider with `"provider"` which can be `"cloudflare"`, `"ddnss"`, `"dondominio"`, `"dnspod"`, `"dreamhost"`, `"duckdns"`, `"dyn"`, `"godaddy"`, `"google"`, `"he"`, `"infomaniak"`, `"namecheap"` or `"noip"`.
|
||||
You can optionnally add the parameters:
|
||||
|
||||
- `"no_dns_lookup"` can be `true` or `false` and allows, if `true`, to prevent the program from doing assumptions from DNS lookups returning an IP address not matching your public IP address (in example for proxied records on Cloudflare).
|
||||
- `"provider_ip"` can be `true` or `false`. It is only available for the providers `ddnss`, `duckdns`, `he`, `infomaniak`, `namecheap`, `noip` and `dyndns`. It allows to let your DNS provider to 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.
|
||||
|
||||
For each DNS provider exist some specific parameters you need to add, as described below:
|
||||
|
||||
Namecheap:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"password"`
|
||||
|
||||
Cloudflare:
|
||||
|
||||
- `"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.
|
||||
- *Optionally*, `"proxied"` can be `true` or `false` to use the proxy services of Cloudflare
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
GoDaddy:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"key"`
|
||||
- `"secret"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
DuckDNS:
|
||||
|
||||
- `"domain"` is your fqdn, for example `subdomain.duckdns.org`
|
||||
- `"token"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
Dreamhost:
|
||||
|
||||
- `"domain"`
|
||||
- `"key"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
NoIP:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
DNSPOD:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
HE.net:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"` (untested)
|
||||
- `"password"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
Infomaniak:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
DDNSS.de:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
DYNDNS:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
Google:
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
DonDominio:
|
||||
|
||||
- `"domain"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"name"` is the name server associated with the domain
|
||||
|
||||
### Additional notes
|
||||
|
||||
- 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 |
|
||||
| --- | --- | --- |
|
||||
| `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` | `cycle` | Method to obtain the public IP address (ipv4 or ipv6). See the [IP Methods section](#IP-methods) |
|
||||
| `IPV4_METHOD` | `cycle` | Method to obtain the public IPv4 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `IPV6_METHOD` | `cycle` | Method to obtain the public IPv6 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `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_ENCODING` | `console` | Format of logging, `json` or `console` |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `info`, `warning` or `error` |
|
||||
| `NODE_ID` | `-1` | Node ID (for distributed systems), can be any integer |
|
||||
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
|
||||
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
|
||||
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
|
||||
|
||||
#### IP methods
|
||||
|
||||
By default, all ip methods are cycled through between all ip methods available for the specified ip version, if any. This allows you not to be blocked for making too many requests. You can otherwise pick one of the following.
|
||||
|
||||
- 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)
|
||||
- `"ddnss4"` using [https://ip4.ddnss.de/meineip.php](https://ip4.ddnss.de/meineip.php)
|
||||
- `"noip4"` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- `"noip8245_4"` using [http://ip1.dynupdate.no-ip.com:8245](http://ip1.dynupdate.no-ip.com:8245)
|
||||
- IPv6 only
|
||||
- `ipify6` using [https://api6.ipify.org](https://api6.ipify.org)
|
||||
- `"ddnss6"` using [https://ip6.ddnss.de/meineip.php](https://ip6.ddnss.de/meineip.php)
|
||||
- `"noip6"` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- `"noip8245_6"` using [http://ip1.dynupdate.no-ip.com:8245](http://ip1.dynupdate.no-ip.com:8245)
|
||||
|
||||
You can also specify an HTTPS URL to obtain your public IP address (i.e. `-e IPV6_METHOD=https://ipinfo.io/ip`)
|
||||
|
||||
### Host firewall
|
||||
|
||||
If you have a host firewall in place, this container needs the following ports:
|
||||
|
||||
- TCP 443 outbound for outbound HTTPS
|
||||
- TCP 80 outbound if you use a local unsecured HTTP connection to your Gotify server
|
||||
- UDP 53 outbound for outbound DNS resolution
|
||||
- TCP 8000 inbound (or other) for the WebUI
|
||||
|
||||
## Domain set up
|
||||
|
||||
Instructions to setup your domain for this program are available for DuckDNS, Cloudflare, GoDaddy and Namecheap on the [Github Wiki](https://github.com/qdm12/ddns-updater/wiki).
|
||||
|
||||
## Gotify
|
||||
|
||||
[](https://gotify.net)
|
||||
|
||||
[**Gotify**](https://gotify.net) is a simple server for sending and receiving messages, and it is **free**, **private** and **open source**
|
||||
|
||||
- It has an [Android app](https://play.google.com/store/apps/details?id=com.github.gotify) to receive notifications
|
||||
- The app does not drain your battery 👍
|
||||
- The notification server is self hosted, see [how to set it up with Docker](https://gotify.net/docs/install)
|
||||
- The notifications only go through your own server (ideally through HTTPS though)
|
||||
|
||||
To set it up with DDNS updater:
|
||||
|
||||
1. Go to the Web GUI of Gotify
|
||||
1. Login with the admin credentials
|
||||
1. Create an app and copy the generated token to the environment variable `GOTIFYTOKEN` (for this container)
|
||||
1. Set the `GOTIFYURL` variable to the URL of your Gotify server address (i.e. `http://127.0.0.1:8080` or `https://bla.com/gotify`)
|
||||
|
||||
## 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 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
|
||||
|
||||
Better testing instructions are written in the [Wiki for GoDaddy](https://github.com/qdm12/ddns-updater/wiki/GoDaddy#testing)
|
||||
|
||||
## Development and contributing
|
||||
|
||||
- Contribute with code: see [the Wiki](https://github.com/qdm12/ddns-updater/wiki/Contributing)
|
||||
- [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)
|
||||
|
||||
## License
|
||||
|
||||
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)
|
||||
|
||||
## Support
|
||||
|
||||
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 🥇👍
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
|
||||
Light container updating DNS A and/or AAAA records periodically for multiple DNS providers
|
||||
|
||||
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg">
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/actions/workflows/build.yml)
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
|
||||

|
||||

|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater/tags?page=1&ordering=last_updated)
|
||||

|
||||

|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater/tags)
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/commits/main)
|
||||
[](https://github.com/qdm12/ddns-updater/graphs/contributors)
|
||||
[](https://github.com/qdm12/ddns-updater/pulls?q=is%3Apr+is%3Aclosed)
|
||||
[](https://github.com/qdm12/ddns-updater/issues)
|
||||
[](https://github.com/qdm12/ddns-updater/issues?q=is%3Aissue+is%3Aclosed)
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater)
|
||||

|
||||

|
||||

|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/master/LICENSE)
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- Updates periodically A records for different DNS providers:
|
||||
- Aliyun
|
||||
- AllInkl
|
||||
- Cloudflare
|
||||
- DD24
|
||||
- DDNSS.de
|
||||
- DigitalOcean
|
||||
- DonDominio
|
||||
- DNSOMatic
|
||||
- DNSPod
|
||||
- Dreamhost
|
||||
- DuckDNS
|
||||
- DynDNS
|
||||
- Dynu
|
||||
- FreeDNS
|
||||
- Gandi
|
||||
- GCP
|
||||
- GoDaddy
|
||||
- Google
|
||||
- He.net
|
||||
- Infomaniak
|
||||
- INWX
|
||||
- Linode
|
||||
- LuaDNS
|
||||
- Namecheap
|
||||
- NoIP
|
||||
- Njalla
|
||||
- OpenDNS
|
||||
- OVH
|
||||
- Porkbun
|
||||
- Selfhost.de
|
||||
- Servercow.de
|
||||
- Spdyn
|
||||
- Strato.de
|
||||
- Variomedia.de
|
||||
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
|
||||
- Web User interface
|
||||
|
||||

|
||||
|
||||
- 11MB Docker image based on a Go static binary in a Scratch Docker image
|
||||
- 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
|
||||
- Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/services/overview/) using `SHOUTRRR_ADDRESSES`
|
||||
- Compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures.
|
||||
|
||||
## Setup
|
||||
|
||||
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
|
||||
mkdir data
|
||||
touch data/config.json
|
||||
# Owned by user ID of Docker container (1000)
|
||||
chown -R 1000 data
|
||||
# all access (for creating json database file data/updates.json)
|
||||
chmod 700 data
|
||||
# read access only
|
||||
chmod 400 data/config.json
|
||||
```
|
||||
|
||||
If you want to use another user ID, [build the image yourself](#build-the-image) with `--build-arg UID=<your-uid>`. You could also just run the container as root with `--user="0"` but this is not advised security wise.
|
||||
1. Write a JSON configuration in *data/config.json*, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
You can find more information in the [configuration section](#configuration) to customize it.
|
||||
|
||||
1. Run the container with
|
||||
|
||||
```sh
|
||||
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
1. ⚠️ If you use IPv6, you might need to set `-e IPV6_PREFIX=/64` (`/64` is your prefix, depending on your ISP)
|
||||
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.
|
||||
|
||||
### Next steps
|
||||
|
||||
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).
|
||||
|
||||
### GHCR
|
||||
|
||||
Images are also added to the Github Container Registry. To use the GHCR container replace `qmcgaw/ddns-updater` to `ghcr.io/qdm12/ddns-updater`, further details are available [here](https://github.com/qdm12/ddns-updater/pkgs/container/ddns-updater)
|
||||
|
||||
## Configuration
|
||||
|
||||
Start by having the following content in *config.json*, or in your `CONFIG` environment variable:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "",
|
||||
},
|
||||
{
|
||||
"provider": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For each setting, you need to fill in parameters.
|
||||
Check the documentation for your DNS provider:
|
||||
|
||||
- [Aliyun](https://github.com/qdm12/ddns-updater/blob/master/docs/aliyun.md)
|
||||
- [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)
|
||||
- [DD24](https://github.com/qdm12/ddns-updater/blob/master/docs/domaindiscount24.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)
|
||||
- [Dynu](https://github.com/qdm12/ddns-updater/blob/master/docs/dynu.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)
|
||||
- [GCP](https://github.com/qdm12/ddns-updater/blob/master/docs/gcp.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)
|
||||
- [INWX](https://github.com/qdm12/ddns-updater/blob/master/docs/inwx.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)
|
||||
- [Njalla](https://github.com/qdm12/ddns-updater/blob/master/docs/njalla.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)
|
||||
- [Porkbun](https://github.com/qdm12/ddns-updater/blob/master/docs/porkbun.md)
|
||||
- [Selfhost.de](https://github.com/qdm12/ddns-updater/blob/master/docs/selfhost.de.md)
|
||||
- [Servercow.de](https://github.com/qdm12/ddns-updater/blob/master/docs/servercow.md)
|
||||
- [Spdyn](https://github.com/qdm12/ddns-updater/blob/master/docs/spdyn.md)
|
||||
- [Strato.de](https://github.com/qdm12/ddns-updater/blob/master/docs/strato.md)
|
||||
- [Variomedia.de](https://github.com/qdm12/ddns-updater/blob/master/docs/variomedia.md)
|
||||
|
||||
Note that:
|
||||
|
||||
- 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 |
|
||||
| --- | --- | --- |
|
||||
| `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) |
|
||||
| `IPV6_PREFIX` | `/128` | IPv6 prefix used to mask your public IPv6 address and your record IPv6 address. Ranges from `/0` to `/128` depending on your ISP. |
|
||||
| `PUBLICIP_FETCHERS` | `all` | Comma separated fetcher types to obtain the public IP address from `http` and `dns` |
|
||||
| `PUBLICIP_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (ipv4 or ipv6). See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIPV4_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv4 address only. See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIPV6_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv6 address only. See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIP_DNS_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (IPv4 and/or IPv6). See the [Public IP section](#public-ip) |
|
||||
| `PUBLICIP_DNS_TIMEOUT` | `3s` | Public IP DNS query timeout |
|
||||
| `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) |
|
||||
| `HEALTH_SERVER_ADDRESS` | `127.0.0.1:9999` | Health server listening address |
|
||||
| `DATADIR` | `/updater/data` | Directory to read and write data files from internally |
|
||||
| `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`. |
|
||||
| `RESOLVER_ADDRESS` | Your network DNS | A plaintext DNS address to use, such as `1.1.1.1:53`. This is useful for split dns, see [#389](https://github.com/qdm12/ddns-updater/issues/389) |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `debug`, `info`, `warning` or `error` |
|
||||
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
|
||||
| `SHOUTRRR_ADDRESSES` | | (optional) Comma separated list of [Shoutrrr addresses](https://containrrr.dev/shoutrrr/services/overview/) (notification services) |
|
||||
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
|
||||
|
||||
#### Public IP
|
||||
|
||||
By default, all public IP fetching types are used and cycled (over DNS and over HTTPs).
|
||||
|
||||
On top of that, for each fetching method, all echo services available are cycled on each request.
|
||||
|
||||
This allows you not to be blocked for making too many requests.
|
||||
|
||||
You can otherwise customize it with the following:
|
||||
|
||||
- `PUBLICIP_HTTP_PROVIDERS` gets your public IPv4 or IPv6 address. It can be one or more of the following:
|
||||
- `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)
|
||||
- `ddnss` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIPV4_HTTP_PROVIDERS` gets your public IPv4 address only. It can be one or more of the following:
|
||||
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIPV6_HTTP_PROVIDERS` gets your public IPv6 address only. It can be one or more of the following:
|
||||
- `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 an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIP_DNS_PROVIDERS` gets your public IPv4 address only or IPv6 address only or one of them (see #136). It can be one or more of the following:
|
||||
- `google`
|
||||
- `cloudflare`
|
||||
|
||||
### Host firewall
|
||||
|
||||
If you have a host firewall in place, this container needs the following ports:
|
||||
|
||||
- TCP 443 outbound for outbound HTTPS
|
||||
- UDP 53 outbound for outbound DNS resolution
|
||||
- TCP 8000 inbound (or other) for the WebUI
|
||||
|
||||
## Architecture
|
||||
|
||||
At program start and every period (5 minutes by default):
|
||||
|
||||
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
|
||||
|
||||
💡 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.
|
||||
|
||||
### Special case: Cloudflare
|
||||
|
||||
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. 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.
|
||||
|
||||
⚠️ 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.
|
||||
|
||||
## 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 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
|
||||
|
||||
## Build the image
|
||||
|
||||
You can build the image yourself with:
|
||||
|
||||
```sh
|
||||
docker build -t qmcgaw/ddns-updater https://github.com/qdm12/ddns-updater.git
|
||||
```
|
||||
|
||||
You can use optional build arguments with `--build-arg KEY=VALUE` from the table below:
|
||||
|
||||
| Build argument | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `UID` | `1000` | User ID running the container |
|
||||
| `GID` | `1000` | User group ID running the container |
|
||||
| `VERSION` | `unknown` | Version of the program and Docker image |
|
||||
| `BUILD_DATE` | `an unknown date` | Build date of the program and Docker image |
|
||||
| `COMMIT` | `unknown` | Commit hash of the program and Docker image |
|
||||
|
||||
## Development and contributing
|
||||
|
||||
- [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)
|
||||
|
||||
## License
|
||||
|
||||
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)
|
||||
|
||||
## Support
|
||||
|
||||
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,277 +2,303 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
_ "github.com/breml/rootcerts"
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/qdm12/ddns-updater/internal/backup"
|
||||
"github.com/qdm12/ddns-updater/internal/config"
|
||||
"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"
|
||||
jsonparams "github.com/qdm12/ddns-updater/internal/params"
|
||||
persistence "github.com/qdm12/ddns-updater/internal/persistence/json"
|
||||
recordslib "github.com/qdm12/ddns-updater/internal/records"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/resolver"
|
||||
"github.com/qdm12/ddns-updater/internal/server"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"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"
|
||||
"github.com/qdm12/golibs/server"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/golibs/connectivity"
|
||||
"github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/goshutdown"
|
||||
"github.com/qdm12/gosplash"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
version = "unknown"
|
||||
commit = "unknown"
|
||||
buildDate = "an unknown date"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(_main(context.Background(), time.Now))
|
||||
// returns 1 on error
|
||||
// returns 2 on os signal
|
||||
buildInfo := models.BuildInformation{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: buildDate,
|
||||
}
|
||||
env := params.New()
|
||||
logger := log.New()
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
errorCh <- _main(ctx, env, os.Args, logger, buildInfo, time.Now)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stop()
|
||||
logger.Warn("Caught OS signal, shutting down")
|
||||
case err := <-errorCh:
|
||||
stop()
|
||||
close(errorCh)
|
||||
if err == nil { // expected exit such as healthcheck
|
||||
os.Exit(0)
|
||||
}
|
||||
logger.Error(err.Error())
|
||||
cancel()
|
||||
}
|
||||
|
||||
const shutdownGracePeriod = 5 * time.Second
|
||||
timer := time.NewTimer(shutdownGracePeriod)
|
||||
select {
|
||||
case err := <-errorCh:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
logger.Info("Shutdown successful")
|
||||
case <-timer.C:
|
||||
logger.Warn("Shutdown timed out")
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
type allParams struct {
|
||||
period time.Duration
|
||||
ipMethod models.IPMethod
|
||||
ipv4Method models.IPMethod
|
||||
ipv6Method models.IPMethod
|
||||
dir string
|
||||
dataDir string
|
||||
listeningPort string
|
||||
rootURL string
|
||||
backupPeriod time.Duration
|
||||
backupDirectory string
|
||||
}
|
||||
var (
|
||||
errShoutrrrSetup = errors.New("failed setting up Shoutrrr")
|
||||
)
|
||||
|
||||
func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
func _main(ctx context.Context, env params.Interface, args []string, logger log.LoggerInterface,
|
||||
buildInfo models.BuildInformation, timeNow func() time.Time) (err error) {
|
||||
if health.IsClientMode(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 {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
logger, err := setupLogger()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
paramsReader := params.NewReader(logger)
|
||||
|
||||
fmt.Println(splash.Splash(
|
||||
paramsReader.GetVersion(),
|
||||
paramsReader.GetVcsRef(),
|
||||
paramsReader.GetBuildDate()))
|
||||
|
||||
notify, err := setupGotify(paramsReader, logger)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
p, err := getParams(paramsReader, logger)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewJSON(p.dataDir)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
settings, warnings, err := paramsReader.GetSettings(p.dataDir + "/config.json")
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(2, w)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
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 record")
|
||||
}
|
||||
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
logger.Warn(err)
|
||||
}
|
||||
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())
|
||||
client := health.NewClient()
|
||||
var healthConfig config.Health
|
||||
_, err := healthConfig.Get(env)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
return err
|
||||
}
|
||||
records[i] = recordslib.New(s, events)
|
||||
return client.Query(ctx, healthConfig.Port)
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2021-07-22T00:00:00Z")
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
return err
|
||||
}
|
||||
client := network.NewClient(HTTPTimeout)
|
||||
defer client.Close()
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}()
|
||||
updater := update.NewUpdater(db, client, notify)
|
||||
ipGetter := update.NewIPGetter(client, p.ipMethod, p.ipv4Method, p.ipv6Method)
|
||||
runner := update.NewRunner(db, updater, ipGetter, logger, timeNow)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
forceUpdate := runner.Run(ctx, p.period)
|
||||
forceUpdate()
|
||||
productionHandlerFunc := handlers.MakeHandler(p.rootURL, p.dir+"/ui", db, logger, forceUpdate, timeNow)
|
||||
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 %q", p.listeningPort, p.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:" + p.listeningPort, Handler: productionHandlerFunc},
|
||||
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
}()
|
||||
|
||||
go backupRunLoop(ctx, p.backupPeriod, p.dir, p.backupDirectory, logger, timeNow)
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
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
|
||||
case <-ctx.Done():
|
||||
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
|
||||
logger.Warn(message)
|
||||
return 1
|
||||
splashSettings := gosplash.Settings{
|
||||
User: "qdm12",
|
||||
Repository: "ddns-updater",
|
||||
Emails: []string{"quentin.mcgaw@gmail.com"},
|
||||
Version: buildInfo.Version,
|
||||
Commit: buildInfo.Commit,
|
||||
BuildDate: buildInfo.BuildDate,
|
||||
Announcement: "",
|
||||
AnnounceExp: announcementExp,
|
||||
// Sponsor information
|
||||
PaypalUser: "qmcgaw",
|
||||
GithubSponsor: "qdm12",
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogger() (logging.Logger, error) {
|
||||
paramsReader := params.NewReader(nil)
|
||||
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for _, line := range gosplash.MakeLines(splashSettings) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if gotifyURL == nil {
|
||||
return func(priority int, messageArgs ...interface{}) {}, nil
|
||||
}
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gotify := admin.NewGotify(*gotifyURL, gotifyToken, &http.Client{Timeout: time.Second})
|
||||
return func(priority int, messageArgs ...interface{}) {
|
||||
if err := gotify.Notify("DDNS Updater", priority, messageArgs...); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams, err error) {
|
||||
var warnings []string
|
||||
p.period, warnings, err = paramsReader.GetPeriod()
|
||||
var config config.Config
|
||||
warnings, err := config.Get(env)
|
||||
for _, warning := range warnings {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
return p, err
|
||||
return err
|
||||
}
|
||||
p.ipMethod, err = paramsReader.GetIPMethod()
|
||||
|
||||
// Setup logger
|
||||
options := []log.Option{log.SetLevel(config.Logger.Level)}
|
||||
if config.Logger.Caller {
|
||||
options = append(options, log.SetCallerFile(true), log.SetCallerLine(true))
|
||||
}
|
||||
logger.Patch(options...)
|
||||
|
||||
sender, err := shoutrrr.CreateSender(config.Shoutrrr.Addresses...)
|
||||
if err != nil {
|
||||
return p, err
|
||||
return fmt.Errorf("%w: %w", errShoutrrrSetup, err)
|
||||
}
|
||||
p.ipv4Method, err = paramsReader.GetIPv4Method()
|
||||
notify := func(message string) {
|
||||
errs := sender.Send(message, &config.Shoutrrr.Params)
|
||||
for i, err := range errs {
|
||||
if err != nil {
|
||||
destination := strings.Split(config.Shoutrrr.Addresses[i], ":")[0]
|
||||
logger.Error(destination + ": " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewDatabase(config.Paths.DataDir)
|
||||
if err != nil {
|
||||
return p, err
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
jsonReader := jsonparams.NewReader(logger)
|
||||
settings, warnings, err := jsonReader.JSONSettings(config.Paths.JSON)
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(w)
|
||||
}
|
||||
p.ipv6Method, err = paramsReader.GetIPv6Method()
|
||||
if err != nil {
|
||||
return p, err
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
p.dir, err = paramsReader.GetExeDir()
|
||||
|
||||
L := len(settings)
|
||||
switch L {
|
||||
case 0:
|
||||
logger.Warn("Found no setting to update record")
|
||||
case 1:
|
||||
logger.Info("Found single setting to update record")
|
||||
default:
|
||||
logger.Info("Found " + fmt.Sprint(len(settings)) + " settings to update records")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: config.Client.Timeout}
|
||||
|
||||
connectivity := connectivity.NewHTTPSGetChecker(client, http.StatusOK)
|
||||
err = connectivity.Check(ctx, "https://github.com")
|
||||
if err != nil {
|
||||
return p, err
|
||||
logger.Warn(err.Error())
|
||||
}
|
||||
p.dataDir, err = paramsReader.GetDataDir(p.dir)
|
||||
|
||||
records := make([]recordslib.Record, len(settings))
|
||||
for i, s := range settings {
|
||||
logger.Info("Reading history from database: domain " +
|
||||
s.Domain() + " host " + s.Host())
|
||||
events, err := persistentDB.GetEvents(s.Domain(), s.Host())
|
||||
if err != nil {
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
records[i] = recordslib.New(s, events)
|
||||
}
|
||||
|
||||
defer client.CloseIdleConnections()
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
defer func() {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
config.PubIP.HTTPSettings.Client = client
|
||||
|
||||
ipGetter, err := publicip.NewFetcher(config.PubIP.DNSSettings, config.PubIP.HTTPSettings)
|
||||
if err != nil {
|
||||
return p, err
|
||||
return err
|
||||
}
|
||||
p.listeningPort, _, err = paramsReader.GetListeningPort()
|
||||
|
||||
resolver, err := resolver.New(config.Resolver)
|
||||
if err != nil {
|
||||
return p, err
|
||||
return fmt.Errorf("creating resolver: %w", err)
|
||||
}
|
||||
p.rootURL, err = paramsReader.GetRootURL()
|
||||
|
||||
updater := update.NewUpdater(db, client, notify, logger)
|
||||
runner := update.NewRunner(db, updater, ipGetter, config.Update.Period,
|
||||
config.IPv6.Mask, config.Update.Cooldown, logger, resolver, timeNow)
|
||||
|
||||
runnerHandler, runnerCtx, runnerDone := goshutdown.NewGoRoutineHandler("runner")
|
||||
go runner.Run(runnerCtx, runnerDone)
|
||||
|
||||
// note: errors are logged within the goroutine,
|
||||
// no need to collect the resulting errors.
|
||||
go runner.ForceUpdate(ctx)
|
||||
|
||||
isHealthy := health.MakeIsHealthy(db, resolver)
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck server"))
|
||||
healthServer := health.NewServer(config.Health.ServerAddress,
|
||||
healthLogger, isHealthy)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler("health server")
|
||||
go healthServer.Run(healthServerCtx, healthServerDone)
|
||||
|
||||
address := ":" + strconv.Itoa(int(config.Server.Port))
|
||||
serverLogger := logger.New(log.SetComponent("http server"))
|
||||
server := server.New(ctx, address, config.Server.RootURL, db, serverLogger, runner)
|
||||
serverHandler, serverCtx, serverDone := goshutdown.NewGoRoutineHandler("server")
|
||||
go server.Run(serverCtx, serverDone)
|
||||
notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
|
||||
|
||||
backupHandler, backupCtx, backupDone := goshutdown.NewGoRoutineHandler("backup")
|
||||
backupLogger := logger.New(log.SetComponent("backup"))
|
||||
go backupRunLoop(backupCtx, backupDone, config.Backup.Period, config.Paths.DataDir, config.Backup.Directory,
|
||||
backupLogger, timeNow)
|
||||
|
||||
shutdownGroup := goshutdown.NewGroupHandler("")
|
||||
shutdownGroup.Add(runnerHandler, healthServerHandler, serverHandler, backupHandler)
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
err = shutdownGroup.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
return p, err
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
p.backupPeriod, err = paramsReader.GetBackupPeriod()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.backupDirectory, err = paramsReader.GetBackupDirectory()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
return p, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
|
||||
logger logging.Logger, timeNow func() time.Time) {
|
||||
logger = logger.WithPrefix("backup: ")
|
||||
type InfoErroer interface {
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
}
|
||||
|
||||
func backupRunLoop(ctx context.Context, done chan<- struct{}, backupPeriod time.Duration,
|
||||
dataDir, outputDir string, logger InfoErroer, timeNow func() time.Time) {
|
||||
defer close(done)
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
}
|
||||
logger.Info("each %s; writing zip files to directory %s", backupPeriod, outputDir)
|
||||
logger.Info("each " + backupPeriod.String() +
|
||||
"; writing zip files to directory " + outputDir)
|
||||
ziper := backup.NewZiper()
|
||||
timer := time.NewTimer(backupPeriod)
|
||||
for {
|
||||
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
|
||||
if err := ziper.ZipFiles(
|
||||
filepath,
|
||||
fmt.Sprintf("%s/data/updates.json", exeDir),
|
||||
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
|
||||
logger.Error(err)
|
||||
fileName := "ddns-updater-backup-" + strconv.Itoa(int(timeNow().UnixNano())) + ".zip"
|
||||
zipFilepath := filepath.Join(outputDir, fileName)
|
||||
err := ziper.ZipFiles(
|
||||
zipFilepath,
|
||||
filepath.Join(dataDir, "updates.json"),
|
||||
filepath.Join(dataDir, "config.json"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
|
||||
@@ -11,9 +11,13 @@ services:
|
||||
environment:
|
||||
- CONFIG=
|
||||
- PERIOD=5m
|
||||
- IP_METHOD=cycle
|
||||
- IPV4_METHOD=cycle
|
||||
- IPV6_METHOD=cycle
|
||||
- UPDATE_COOLDOWN_PERIOD=5m
|
||||
- PUBLICIP_FETCHERS=all
|
||||
- PUBLICIP_HTTP_PROVIDERS=all
|
||||
- PUBLICIPV4_HTTP_PROVIDERS=all
|
||||
- PUBLICIPV6_HTTP_PROVIDERS=all
|
||||
- PUBLICIP_DNS_PROVIDERS=all
|
||||
- PUBLICIP_DNS_TIMEOUT=3s
|
||||
- HTTP_TIMEOUT=10s
|
||||
|
||||
# Web UI
|
||||
@@ -25,9 +29,7 @@ services:
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
|
||||
# Other
|
||||
- LOG_ENCODING=console
|
||||
- LOG_LEVEL=info
|
||||
- NODE_ID=-1 # -1 to disable
|
||||
- GOTIFY_URL=
|
||||
- GOTIFY_TOKEN=
|
||||
- LOG_CALLER=hidden
|
||||
- SHOUTRRR_ADDRESSES=
|
||||
restart: always
|
||||
|
||||
33
docs/aliyun.md
Normal file
33
docs/aliyun.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Aliyun
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "aliyun",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"access_key_id": "your access_key_id",
|
||||
"access_secret": "your access_secret",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"access_key_id"`
|
||||
- `"access_secret"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
28
docs/allinkl.md
Normal file
28
docs/allinkl.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# All-Inkl
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "allinkl",
|
||||
"domain": "domain.com",
|
||||
"host": "host",
|
||||
"username": "dynXXXXXXX",
|
||||
"password": "password"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host (subdomain)
|
||||
- `"username"` username (usually starts with dyn followed by numbers)
|
||||
- `"password"` password in plain text
|
||||
|
||||
## Domain setup
|
||||
40
docs/cloudflare.md
Normal file
40
docs/cloudflare.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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, from the domain overview page written as *Zone ID*
|
||||
- `"domain"`
|
||||
- `"host"` is your host. It should be left to `"@"`, since subdomain and wildcards (`"*"`) are not really supported by Cloudflare it seems.
|
||||
See [this issue comment for context](https://github.com/qdm12/ddns-updater/issues/243#issuecomment-928313949). This is left as is for compatibility.
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following ([how to find API keys](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-)):
|
||||
- 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`
|
||||
|
||||
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.
|
||||
29
docs/dd24.md
Normal file
29
docs/dd24.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Domain Discount 24
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dd24",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"password"` is your password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
38
docs/ddnss.de.md
Normal file
38
docs/ddnss.de.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# DDNSS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "ddnss",
|
||||
"provider_ip": true,
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "user",
|
||||
"password": "password",
|
||||
"dual_stack": false,
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"dual_stack"` can be set to `true` **if you have turn on dual stack for your record** to update both IPv4 and IPv6 addresses. Note it is ignored if `"provider_ip": true`. More precisely:
|
||||
- if it is `false`, the updates are done using the `ip` parameter and only one IP address can be set (ipv4 or ipv6, whichever is last sent).
|
||||
- if it is `true`, the updates are done using the `ip` and `ip6` parameters, for IPv4 and IPv6 respectively, and both can be set on the same record
|
||||
- `"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
|
||||
31
docs/dreamhost.md
Normal file
31
docs/dreamhost.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Dreamhost
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`. It defaults to `"@"`.
|
||||
- `"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 (**NOT** your 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",
|
||||
"client_key": "client_key",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"client_key"`
|
||||
|
||||
### 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
|
||||
37
docs/dynu.md
Normal file
37
docs/dynu.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Dynu
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dynu",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"group": "group",
|
||||
"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"` could be plain text or password in MD5 or SHA256 format (There's also an option for setting a password for IP Update only)
|
||||
|
||||
### 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.
|
||||
- `"group"` specify the Group for which you want to set the IP (will update any domains and subdomains in the same group)
|
||||
|
||||
## 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
|
||||
33
docs/freedns.md
Normal file
33
docs/freedns.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 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
|
||||
|
||||
This integration uses FreeDNS's v2 dynamic dns interface, which is not shown by default when you select `Dynamic DNS` from the side menu. Instead you must go to https://freedns.afraid.org/dynamic/v2/ and enable dynamic DNS for the subdomains you wish and you will then see a url like `https://sync.afraid.org/u/token/` for each enabled subdomain.
|
||||
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)
|
||||
37
docs/gcp.md
Normal file
37
docs/gcp.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# GCP
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "gpc",
|
||||
"project": "my-project-id",
|
||||
"zone": "zone",
|
||||
"credentials": {
|
||||
"type": "service_account",
|
||||
"project_id": "my-project-id",
|
||||
// ...
|
||||
},
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"project"` is the id of your Google Cloud project
|
||||
- `"zone"` is the zone, that your DNS record is located in
|
||||
- `"credentials"` is the JSON credentials for your Google Cloud project. This is usually downloaded as a JSON file, which you can copy paste the content as the value of the `"credentials"` key. More information on how to get it is available [here](https://cloud.google.com/docs/authentication/getting-started). Please ensure your service account has all necessary permissions to create/update/list/get DNS records within your project.
|
||||
- `"domain"` is the TLD of you DNS record (without a trailing dot)
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4`
|
||||
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": "google",
|
||||
"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
|
||||
32
docs/inwx.md
Normal file
32
docs/inwx.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# OpenDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dyn",
|
||||
"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"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
35
docs/njalla.md
Normal file
35
docs/njalla.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Njalla
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "njalla",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"key"` is the key for your record
|
||||
|
||||
### 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 [https://njal.la/docs/ddns](https://njal.la/docs/ddns/)
|
||||
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"` which you can create at [eu.api.ovh.com/createApp](https://eu.api.ovh.com/createApp/)
|
||||
- `"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/)
|
||||
35
docs/porkbun.md
Normal file
35
docs/porkbun.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Porkbun
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "porkbun",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"api_key": "sk1_7d119e3f656b00ae042980302e1425a04163c476efec1833q3cb0w54fc6f5022",
|
||||
"secret_api_key": "pk1_5299b57125c8f3cdf347d2fe0e713311ee3a1e11f11a14942b26472593e35368",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"*"` or `"@"`
|
||||
- `"apikey"`
|
||||
- `"secretapikey"`
|
||||
- `"ttl"` optional integer value corresponding to a number of seconds
|
||||
|
||||
## Domain setup
|
||||
|
||||
- Create an API key at [porkbun.com/account/api](https://porkbun.com/account/api)
|
||||
- From the [Domain Management page](https://porkbun.com/account/domainsSpeedy), toggle on **API ACCESS** for your domain.
|
||||
|
||||
💁 [Official setup documentation](https://kb.porkbun.com/article/190-getting-started-with-the-porkbun-dns-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
|
||||
37
docs/servercow.md
Normal file
37
docs/servercow.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Servercow
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "servercow",
|
||||
"domain": "domain.com",
|
||||
"host": "",
|
||||
"username": "servercow_username",
|
||||
"password": "servercow_password",
|
||||
"ttl": 600,
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsury parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be `""`, a subdomain or `"*"` generally
|
||||
- `"username"` is the username for your DNS API User
|
||||
- `"password"` is the password for your DNS API User
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ttl"` can be set to an integer value for record TTL in seconds (if not set the default is 120)
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), and defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
See [their article](https://cp.servercow.de/en/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/)
|
||||
41
docs/spdyn.md
Normal file
41
docs/spdyn.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Spdyn.de
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "spdyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"user": "user",
|
||||
"password": "password",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
|
||||
#### Using user and password
|
||||
|
||||
- `"user"` is the name of a user who can update this host
|
||||
- `"password"` is the password of a user who can update this host
|
||||
|
||||
#### Using update tokens
|
||||
|
||||
- `"token"` is your update 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 (**not IPv6**)automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
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/)
|
||||
37
docs/variomedia.md
Normal file
37
docs/variomedia.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Variomedia
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "variomedia",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"email": "email@domain.com",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"email"`
|
||||
- `"password"` is your DNS settings password, not your account 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 [dyndns.variomedia.de](https://dyndns.variomedia.de/)
|
||||
44
go.mod
44
go.mod
@@ -1,11 +1,43 @@
|
||||
module github.com/qdm12/ddns-updater
|
||||
|
||||
go 1.15
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible
|
||||
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/breml/rootcerts v0.2.11
|
||||
github.com/containrrr/shoutrrr v0.7.0
|
||||
github.com/go-chi/chi v1.5.4
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/miekg/dns v1.1.42
|
||||
github.com/qdm12/golibs v0.0.0-20210822203818-5c568b0777b6
|
||||
github.com/qdm12/goshutdown v0.3.0
|
||||
github.com/qdm12/gosplash v0.1.0
|
||||
github.com/qdm12/log v0.1.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
google.golang.org/api v0.102.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e // indirect
|
||||
google.golang.org/grpc v1.50.1 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -6,25 +6,27 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type Ziper interface {
|
||||
var _ FileZiper = (*Ziper)(nil)
|
||||
|
||||
type FileZiper interface {
|
||||
ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
}
|
||||
|
||||
type ziper struct {
|
||||
type Ziper struct {
|
||||
createFile func(name string) (*os.File, error)
|
||||
openFile func(name string) (*os.File, error)
|
||||
ioCopy func(dst io.Writer, src io.Reader) (written int64, err error)
|
||||
}
|
||||
|
||||
func NewZiper() Ziper {
|
||||
return &ziper{
|
||||
func NewZiper() *Ziper {
|
||||
return &Ziper{
|
||||
createFile: os.Create,
|
||||
openFile: os.Open,
|
||||
ioCopy: io.Copy,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
func (z *Ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
f, err := z.createFile(outputFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -33,14 +35,15 @@ func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
w := zip.NewWriter(f)
|
||||
defer w.Close()
|
||||
for _, filepath := range inputFilepaths {
|
||||
if err := z.addFile(w, filepath); err != nil {
|
||||
err = z.addFile(w, filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
func (z *Ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
f, err := z.openFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
27
internal/config/backup.go
Normal file
27
internal/config/backup.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Backup struct {
|
||||
Period time.Duration
|
||||
Directory string
|
||||
}
|
||||
|
||||
func (b *Backup) get(env params.Interface) (err error) {
|
||||
b.Period, err = env.Duration("BACKUP_PERIOD", params.Default("0"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable BACKUP_PERIOD", err)
|
||||
}
|
||||
|
||||
b.Directory, err = env.Path("BACKUP_DIRECTORY", params.Default("./data"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable BACKUP_DIRECTORY", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
21
internal/config/client.go
Normal file
21
internal/config/client.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *Client) get(env params.Interface) (err error) {
|
||||
c.Timeout, err = env.Duration("HTTP_TIMEOUT", params.Default("10s"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable HTTP_TIMEOUT", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
86
internal/config/config.go
Normal file
86
internal/config/config.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/resolver"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Client Client
|
||||
Update Update
|
||||
PubIP PubIP
|
||||
Resolver resolver.Settings
|
||||
IPv6 IPv6
|
||||
Server Server
|
||||
Health Health
|
||||
Paths Paths
|
||||
Backup Backup
|
||||
Logger Logger
|
||||
Shoutrrr Shoutrrr
|
||||
}
|
||||
|
||||
func (c *Config) Get(env params.Interface) (warnings []string, err error) {
|
||||
err = c.Client.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err := c.Update.get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
newWarnings, err := c.PubIP.get(env)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
c.Resolver, err = readResolver()
|
||||
if err != nil {
|
||||
return warnings, fmt.Errorf("reading resolver settings: %w", err)
|
||||
}
|
||||
|
||||
err = c.IPv6.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err = c.Server.get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err = c.Health.Get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
err = c.Paths.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
err = c.Backup.get(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
c.Logger, err = readLog()
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
newWarnings, err = c.Shoutrrr.get(env)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
32
internal/config/health.go
Normal file
32
internal/config/health.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Health struct {
|
||||
ServerAddress string
|
||||
Port uint16 // obtained from ServerAddress
|
||||
}
|
||||
|
||||
func (h *Health) Get(env params.Interface) (warning string, err error) {
|
||||
h.ServerAddress, warning, err = env.ListeningAddress(
|
||||
"HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999"))
|
||||
if err != nil {
|
||||
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
|
||||
}
|
||||
_, portStr, err := net.SplitHostPort(h.ServerAddress)
|
||||
if err != nil {
|
||||
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return warning, fmt.Errorf("%w: for environment variable HEALTH_SERVER_ADDRESS", err)
|
||||
}
|
||||
h.Port = uint16(port)
|
||||
return warning, nil
|
||||
}
|
||||
60
internal/config/ipv6.go
Normal file
60
internal/config/ipv6.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type IPv6 struct {
|
||||
Mask net.IPMask
|
||||
}
|
||||
|
||||
func (i *IPv6) get(env params.Interface) (err error) {
|
||||
maskStr, err := env.Get("IPV6_PREFIX", params.Default("/128"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
|
||||
}
|
||||
i.Mask, err = ipv6DecimalPrefixToMask(maskStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrParsePrefix = errors.New("cannot parse IP prefix")
|
||||
|
||||
func ipv6DecimalPrefixToMask(prefixDecimal string) (ipMask net.IPMask, err error) {
|
||||
if prefixDecimal == "" {
|
||||
return nil, fmt.Errorf("%w: empty prefix", ErrParsePrefix)
|
||||
}
|
||||
|
||||
prefixDecimal = strings.TrimPrefix(prefixDecimal, "/")
|
||||
|
||||
const bits = 8 * net.IPv6len
|
||||
|
||||
ones, consumed, ok := decimalToInteger(prefixDecimal)
|
||||
if !ok || consumed != len(prefixDecimal) || ones < 0 || ones > bits {
|
||||
return nil, fmt.Errorf("%w: %s", ErrParsePrefix, prefixDecimal)
|
||||
}
|
||||
|
||||
return net.CIDRMask(ones, bits), nil
|
||||
}
|
||||
|
||||
func decimalToInteger(s string) (ones int, i int, ok bool) {
|
||||
const big = 0xFFFFFF // Bigger than we need, not too big to worry about overflow
|
||||
const ten = 10
|
||||
|
||||
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
|
||||
ones = ones*ten + int(s[i]-'0')
|
||||
if ones >= big {
|
||||
return big, i, false
|
||||
}
|
||||
}
|
||||
|
||||
return ones, i, true
|
||||
}
|
||||
62
internal/config/ipv6_test.go
Normal file
62
internal/config/ipv6_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ipv6DecimalPrefixToMask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
prefixDecimal string
|
||||
ipMask net.IPMask
|
||||
err error
|
||||
}{
|
||||
"empty": {
|
||||
err: fmt.Errorf("cannot parse IP prefix: empty prefix"),
|
||||
},
|
||||
"malformed": {
|
||||
prefixDecimal: "malformed",
|
||||
err: fmt.Errorf("cannot parse IP prefix: malformed"),
|
||||
},
|
||||
"with leading slash": {
|
||||
prefixDecimal: "/78",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
"without leading slash": {
|
||||
prefixDecimal: "78",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
"full IPv6 mask": {
|
||||
prefixDecimal: "/128",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
|
||||
},
|
||||
"zero IPv6 mask": {
|
||||
prefixDecimal: "/0",
|
||||
ipMask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipMask, err := ipv6DecimalPrefixToMask(testCase.prefixDecimal)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.ipMask, ipMask)
|
||||
})
|
||||
}
|
||||
}
|
||||
73
internal/config/logger.go
Normal file
73
internal/config/logger.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
Caller bool
|
||||
Level log.Level
|
||||
}
|
||||
|
||||
var (
|
||||
ErrLogCallerNotValid = errors.New("LOG_CALLER value is not valid")
|
||||
)
|
||||
|
||||
func readLog() (settings Logger, err error) {
|
||||
callerString := os.Getenv("LOG_CALLER")
|
||||
switch callerString {
|
||||
case "":
|
||||
case "hidden":
|
||||
case "short":
|
||||
settings.Caller = true
|
||||
default:
|
||||
return settings, fmt.Errorf("%w: "+
|
||||
`%q must be one of "", "hidden" or "short"`,
|
||||
ErrLogCallerNotValid, callerString)
|
||||
}
|
||||
|
||||
settings.Level, err = readLogLevel()
|
||||
if err != nil {
|
||||
return settings, err
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
|
||||
func readLogLevel() (level log.Level, err error) {
|
||||
s := os.Getenv("LOG_LEVEL")
|
||||
if s == "" {
|
||||
return log.LevelInfo, nil
|
||||
}
|
||||
|
||||
level, err = parseLogLevel(s)
|
||||
if err != nil {
|
||||
return level, fmt.Errorf("environment variable LOG_LEVEL: %w", err)
|
||||
}
|
||||
|
||||
return level, nil
|
||||
}
|
||||
|
||||
var ErrLogLevelUnknown = errors.New("log level is unknown")
|
||||
|
||||
func parseLogLevel(s string) (level log.Level, err error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "debug":
|
||||
return log.LevelDebug, nil
|
||||
case "info":
|
||||
return log.LevelInfo, nil
|
||||
case "warning":
|
||||
return log.LevelWarn, nil
|
||||
case "error":
|
||||
return log.LevelError, nil
|
||||
default:
|
||||
return level, fmt.Errorf(
|
||||
"%w: %q is not valid and can be one of debug, info, warning or error",
|
||||
ErrLogLevelUnknown, s)
|
||||
}
|
||||
}
|
||||
23
internal/config/paths.go
Normal file
23
internal/config/paths.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Paths struct {
|
||||
DataDir string
|
||||
JSON string // obtained from DataDir
|
||||
}
|
||||
|
||||
func (p *Paths) get(env params.Interface) (err error) {
|
||||
p.DataDir, err = env.Path("DATADIR", params.Default("./data"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable DATADIR", err)
|
||||
}
|
||||
|
||||
p.JSON = filepath.Join(p.DataDir, "config.json")
|
||||
return nil
|
||||
}
|
||||
197
internal/config/pubip.go
Normal file
197
internal/config/pubip.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
const all = "all"
|
||||
|
||||
type PubIP struct {
|
||||
HTTPSettings publicip.HTTPSettings
|
||||
DNSSettings publicip.DNSSettings
|
||||
}
|
||||
|
||||
func (p *PubIP) get(env params.Interface) (warnings []string, err error) {
|
||||
err = p.getFetchers(env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpIPProviders, warning, err := p.getIPHTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
httpIP4Providers, warning, err := p.getIPv4HTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
httpIP6Providers, warning, err := p.getIPv6HTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
p.HTTPSettings.Options = []http.Option{
|
||||
http.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
|
||||
http.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
|
||||
http.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
|
||||
}
|
||||
|
||||
dnsIPProviders, err := p.getDNSProviders(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
dnsTimeout, err := env.Duration("PUBLICIP_DNS_TIMEOUT", params.Default("3s"))
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
p.DNSSettings.Options = []dns.Option{
|
||||
dns.SetTimeout(dnsTimeout),
|
||||
dns.SetProviders(dnsIPProviders[0], dnsIPProviders[1:]...),
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
|
||||
|
||||
func (p *PubIP) getFetchers(env params.Interface) (err error) {
|
||||
s, err := env.Get("PUBLICIP_FETCHERS", params.Default(all))
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: for environment variable PUBLICIP_FETCHERS", err)
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
for i, field := range fields {
|
||||
switch strings.ToLower(field) {
|
||||
case all:
|
||||
p.HTTPSettings.Enabled = true
|
||||
p.DNSSettings.Enabled = true
|
||||
case "http":
|
||||
p.HTTPSettings.Enabled = true
|
||||
case "dns":
|
||||
p.DNSSettings.Enabled = true
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"%w: %q at position %d of %d",
|
||||
ErrInvalidFetcher, field, i+1, len(fields))
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getDNSProviders obtains the DNS providers to obtain your public IPv4 and/or IPv6 address.
|
||||
func (p *PubIP) getDNSProviders(env params.Interface) (providers []dns.Provider, err error) {
|
||||
s, err := env.Get("PUBLICIP_DNS_PROVIDERS", params.Default(all))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: for environment variable PUBLICIP_DNS_PROVIDERS", err)
|
||||
}
|
||||
|
||||
availableProviders := dns.ListProviders()
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
providers = make([]dns.Provider, len(fields))
|
||||
for i, field := range fields {
|
||||
if field == all {
|
||||
return availableProviders, nil
|
||||
}
|
||||
|
||||
providers[i] = dns.Provider(field)
|
||||
err = dns.ValidateProvider(providers[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// getHTTPProviders obtains the HTTP providers to obtain your public IPv4 or IPv6 address.
|
||||
func (p *PubIP) getIPHTTPProviders(env params.Interface) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIP_HTTP_PROVIDERS", "IP_METHOD", ipversion.IP4or6)
|
||||
}
|
||||
|
||||
// getIPv4HTTPProviders obtains the HTTP providers to obtain your public IPv4 address.
|
||||
func (p *PubIP) getIPv4HTTPProviders(env params.Interface) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIPV4_HTTP_PROVIDERS", "IPV4_METHOD", ipversion.IP4)
|
||||
}
|
||||
|
||||
// getIPv6HTTPProviders obtains the HTTP providers to obtain your public IPv6 address.
|
||||
func (p *PubIP) getIPv6HTTPProviders(env params.Interface) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIPV6_HTTP_PROVIDERS", "IPV6_METHOD", ipversion.IP6)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidPublicIPHTTPProvider = errors.New("invalid public IP HTTP provider")
|
||||
)
|
||||
|
||||
func httpIPMethod(env params.Interface, envKey, retroKey string, version ipversion.IPVersion) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
retroKeyOption := params.RetroKeys([]string{retroKey}, func(oldKey, newKey string) {
|
||||
warning = "You are using an old environment variable " + oldKey +
|
||||
" please change it to " + newKey
|
||||
})
|
||||
s, err := env.Get(envKey, params.Default("cycle"), retroKeyOption)
|
||||
if err != nil {
|
||||
return nil, warning, fmt.Errorf("%w: for environment variable %s", err, envKey)
|
||||
}
|
||||
|
||||
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, warning, 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, warning, fmt.Errorf("%w: %s", ErrInvalidPublicIPHTTPProvider, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, warning, fmt.Errorf("%w: for IP version %s", ErrInvalidPublicIPHTTPProvider, version)
|
||||
}
|
||||
|
||||
return providers, warning, nil
|
||||
}
|
||||
27
internal/config/resolver.go
Normal file
27
internal/config/resolver.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/resolver"
|
||||
)
|
||||
|
||||
func readResolver() (settings resolver.Settings, err error) {
|
||||
address := os.Getenv("RESOLVER_ADDRESS")
|
||||
if address != "" {
|
||||
settings.Address = &address
|
||||
}
|
||||
|
||||
timeoutString := os.Getenv("RESOLVER_TIMEOUT")
|
||||
if timeoutString != "" {
|
||||
timeout, err := time.ParseDuration(timeoutString)
|
||||
if err != nil {
|
||||
return settings, fmt.Errorf("environment variable RESOLVER_TIMEOUT: %w", err)
|
||||
}
|
||||
settings.Timeout = timeout
|
||||
}
|
||||
|
||||
return settings, nil
|
||||
}
|
||||
26
internal/config/server.go
Normal file
26
internal/config/server.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Port uint16
|
||||
RootURL string
|
||||
}
|
||||
|
||||
func (s *Server) get(env params.Interface) (warning string, err error) {
|
||||
s.RootURL, err = env.RootURL("ROOT_URL")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable ROOT_URL", err)
|
||||
}
|
||||
|
||||
s.Port, warning, err = env.ListeningPort("LISTENING_PORT", params.Default("8000"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable LISTENING_PORT", err)
|
||||
}
|
||||
|
||||
return warning, err
|
||||
}
|
||||
71
internal/config/shoutrrr.go
Normal file
71
internal/config/shoutrrr.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Shoutrrr struct {
|
||||
Addresses []string
|
||||
Params types.Params
|
||||
}
|
||||
|
||||
func (s *Shoutrrr) get(env params.Interface) (warnings []string, err error) {
|
||||
s.Addresses, err = env.CSV("SHOUTRRR_ADDRESSES", params.CaseSensitiveValue())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: for environment variable SHOUTRRR_ADDRESSES", err)
|
||||
}
|
||||
|
||||
// Retro-compatibility: GOTIFY_URL and GOTIFY_TOKEN
|
||||
gotifyURL, err := env.URL("GOTIFY_URL")
|
||||
if err != nil || gotifyURL != nil {
|
||||
const warning = "You should use the environment variable SHOUTRRR_ADDRESSES instead of GOTIFY_URL and GOTIFY_TOKEN"
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: for environment variable GOTIFY_URL", err)
|
||||
} else if gotifyURL != nil {
|
||||
gotifyToken, err := env.Get("GOTIFY_TOKEN", params.CaseSensitiveValue(),
|
||||
params.Compulsory(), params.Unset())
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
gotifyShoutrrrAddress := gotifyURLTokenToShoutrrr(gotifyURL, gotifyToken)
|
||||
s.Addresses = append(s.Addresses, gotifyShoutrrrAddress)
|
||||
}
|
||||
|
||||
_, err = shoutrrr.CreateSender(s.Addresses...)
|
||||
if err != nil {
|
||||
return warnings, fmt.Errorf("for environment variable SHOUTRRR_ADDRESSES: %w", err) // validation step
|
||||
}
|
||||
|
||||
str, err := env.Get("SHOUTRRR_PARAMS", params.Default("title=DDNS Updater"), params.CaseSensitiveValue())
|
||||
if err != nil {
|
||||
return warnings, fmt.Errorf("%w: for environment variable SHOUTRRR_PARAMS", err)
|
||||
}
|
||||
|
||||
keyValues := strings.Split(str, ",")
|
||||
s.Params = make(map[string]string, len(keyValues))
|
||||
for _, keyValue := range keyValues {
|
||||
fields := strings.Split(keyValue, "=")
|
||||
key, value := fields[0], fields[1]
|
||||
s.Params[key] = value
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
func gotifyURLTokenToShoutrrr(url *url.URL, token string) (address string) {
|
||||
hostAndPath := path.Join(url.Host, url.Path)
|
||||
address = "gotify://" + hostAndPath + "/" + token
|
||||
if url.Scheme == "http" {
|
||||
address += "?DisableTLS=Yes"
|
||||
}
|
||||
return address
|
||||
}
|
||||
55
internal/config/update.go
Normal file
55
internal/config/update.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Update struct {
|
||||
Period time.Duration
|
||||
Cooldown time.Duration
|
||||
}
|
||||
|
||||
func (u *Update) get(env params.Interface) (warning string, err error) {
|
||||
warning, err = u.getPeriod(env)
|
||||
if err != nil {
|
||||
return warning, err
|
||||
}
|
||||
|
||||
u.Cooldown, err = env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable UPDATE_COOLDOWN_PERIOD", err)
|
||||
}
|
||||
|
||||
return warning, nil
|
||||
}
|
||||
|
||||
func (u *Update) getPeriod(env params.Interface) (warning string, err error) {
|
||||
// Backward compatibility: DELAY
|
||||
s, err := env.Get("DELAY", params.Compulsory())
|
||||
if err == nil {
|
||||
warning = "the environment variable DELAY should be changed to PERIOD"
|
||||
// Backward compatibility: integer only, treated as seconds
|
||||
n, err := strconv.Atoi(s)
|
||||
if err == nil {
|
||||
u.Period = time.Duration(n) * time.Second
|
||||
return warning, nil
|
||||
}
|
||||
|
||||
period, err := time.ParseDuration(s)
|
||||
if err == nil {
|
||||
u.Period = period
|
||||
return warning, nil
|
||||
}
|
||||
}
|
||||
|
||||
u.Period, err = env.Duration("PERIOD", params.Default("10m"))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: for environment variable PERIOD", err)
|
||||
}
|
||||
|
||||
return "", err
|
||||
}
|
||||
8
internal/config/utils.go
Normal file
8
internal/config/utils.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
func appendIfNotEmpty(slice []string, s string) (newSlice []string) {
|
||||
if s == "" {
|
||||
return slice
|
||||
}
|
||||
return append(slice, s)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
IPv4 models.IPVersion = "ipv4"
|
||||
IPv6 models.IPVersion = "ipv6"
|
||||
IPv4OrIPv6 models.IPVersion = "ipv4 or ipv6"
|
||||
)
|
||||
@@ -1,77 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func IPMethods() []models.IPMethod {
|
||||
return []models.IPMethod{
|
||||
{
|
||||
Name: "cycle",
|
||||
},
|
||||
{
|
||||
Name: "opendns",
|
||||
URL: "https://diagnostic.opendns.com/myip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "ifconfig",
|
||||
URL: "https://ifconfig.io/ip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "ipinfo",
|
||||
URL: "https://ipinfo.io/ip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "ipify",
|
||||
URL: "https://api.ipify.org",
|
||||
IPv4: true,
|
||||
},
|
||||
{
|
||||
Name: "ipify6",
|
||||
URL: "https://api6.ipify.org",
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "ddnss4",
|
||||
URL: "https://ip4.ddnss.de/meineip.php",
|
||||
IPv4: true,
|
||||
},
|
||||
{
|
||||
Name: "ddnss6",
|
||||
URL: "https://ip6.ddnss.de/meineip.php",
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "google",
|
||||
URL: "https://domains.google.com/checkip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "noip4",
|
||||
URL: "http://ip1.dynupdate.no-ip.com",
|
||||
IPv4: true,
|
||||
},
|
||||
{
|
||||
Name: "noip6",
|
||||
URL: "http://ip1.dynupdate6.no-ip.com",
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "noip8245_4",
|
||||
URL: "http://ip1.dynupdate.no-ip.com:8245",
|
||||
IPv4: true,
|
||||
},
|
||||
{
|
||||
Name: "noip8245_6",
|
||||
URL: "http://ip1.dynupdate6.no-ip.com:8245",
|
||||
IPv6: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
// All possible provider values
|
||||
const (
|
||||
CLOUDFLARE models.Provider = "cloudflare"
|
||||
DDNSSDE models.Provider = "ddnss"
|
||||
DONDOMINIO models.Provider = "dondominio"
|
||||
DNSPOD models.Provider = "dnspod"
|
||||
DUCKDNS models.Provider = "duckdns"
|
||||
DYN models.Provider = "dyn"
|
||||
DREAMHOST models.Provider = "dreamhost"
|
||||
GODADDY models.Provider = "godaddy"
|
||||
GOOGLE models.Provider = "google"
|
||||
HE models.Provider = "he"
|
||||
INFOMANIAK models.Provider = "infomaniak"
|
||||
NAMECHEAP models.Provider = "namecheap"
|
||||
NOIP models.Provider = "noip"
|
||||
)
|
||||
|
||||
func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
CLOUDFLARE,
|
||||
DDNSSDE,
|
||||
DONDOMINIO,
|
||||
DNSPOD,
|
||||
DUCKDNS,
|
||||
DYN,
|
||||
DREAMHOST,
|
||||
GODADDY,
|
||||
GOOGLE,
|
||||
HE,
|
||||
INFOMANIAK,
|
||||
NAMECHEAP,
|
||||
NOIP,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// 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 = "https://github.com/qdm12/ddns-updater/issues/new"
|
||||
)
|
||||
@@ -1,17 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AnnouncementExpiration(t *testing.T) {
|
||||
t.Parallel()
|
||||
if len(AnnouncementExpiration) == 0 {
|
||||
return
|
||||
}
|
||||
_, err := time.Parse("2006-01-02", AnnouncementExpiration)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -3,37 +3,19 @@ package data
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"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 records.Record) (id int)
|
||||
Select(id int) (record records.Record, err error)
|
||||
SelectAll() (records []records.Record)
|
||||
Update(id int, record records.Record) error
|
||||
// From persistence database
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
}
|
||||
|
||||
type database struct {
|
||||
type Database struct {
|
||||
data []records.Record
|
||||
sync.RWMutex
|
||||
persistentDB persistence.Database
|
||||
persistentDB PersistentDatabase
|
||||
}
|
||||
|
||||
// NewDatabase creates a new in memory database
|
||||
func NewDatabase(data []records.Record, persistentDB persistence.Database) Database {
|
||||
return &database{
|
||||
// NewDatabase creates a new in memory database.
|
||||
func NewDatabase(data []records.Record, persistentDB PersistentDatabase) *Database {
|
||||
return &Database{
|
||||
data: data,
|
||||
persistentDB: persistentDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *database) Close() error {
|
||||
db.Lock() // ensure write operation finishes
|
||||
defer db.Unlock()
|
||||
return db.persistentDB.Close()
|
||||
}
|
||||
|
||||
15
internal/data/interfaces.go
Normal file
15
internal/data/interfaces.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
type PersistentDatabase interface {
|
||||
Close() error
|
||||
StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error)
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
Check() error
|
||||
}
|
||||
@@ -1,31 +1,24 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) Insert(record records.Record) (id int) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
db.data = append(db.data, record)
|
||||
return len(db.data) - 1
|
||||
}
|
||||
var ErrRecordNotFound = errors.New("record not found")
|
||||
|
||||
func (db *database) Select(id int) (record records.Record, err error) {
|
||||
func (db *Database) Select(id uint) (record records.Record, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
if id < 0 {
|
||||
return record, fmt.Errorf("id %d cannot be lower than 0", id)
|
||||
}
|
||||
if id > len(db.data)-1 {
|
||||
return record, fmt.Errorf("no record config found for id %d", id)
|
||||
if int(id) > len(db.data)-1 {
|
||||
return record, fmt.Errorf("%w: for id %d", ErrRecordNotFound, id)
|
||||
}
|
||||
return db.data[id], nil
|
||||
}
|
||||
|
||||
func (db *database) SelectAll() (records []records.Record) {
|
||||
func (db *Database) SelectAll() (records []records.Record) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
return db.data
|
||||
|
||||
@@ -7,18 +7,15 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
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 records.Record) error {
|
||||
func (db *Database) Update(id uint, record records.Record) (err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
if id < 0 {
|
||||
return fmt.Errorf("id %d cannot be lower than 0", id)
|
||||
}
|
||||
if id > len(db.data)-1 {
|
||||
return fmt.Errorf("no record config found for id %d", id)
|
||||
if int(id) > len(db.data)-1 {
|
||||
return fmt.Errorf("%w: for id %d", ErrRecordNotFound, id)
|
||||
}
|
||||
currentCount := len(db.data[id].History)
|
||||
newCount := len(record.History)
|
||||
@@ -36,3 +33,9 @@ func (db *database) Update(id int, record records.Record) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Database) Close() (err error) {
|
||||
db.Lock() // ensure write operation finishes
|
||||
defer db.Unlock()
|
||||
return db.persistentDB.Close()
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
// MakeHandler returns a router with all the necessary routes configured
|
||||
func MakeHandler(rootURL, uiDir string, db data.Database, logger logging.Logger, forceUpdate func(), timeNow func() time.Time) http.HandlerFunc {
|
||||
logger = logger.WithPrefix("http server: ")
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
logger.Info("HTTP %s %s", r.Method, r.RequestURI)
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.RequestURI == rootURL+"/":
|
||||
t := template.Must(template.ParseFiles(uiDir + "/index.html"))
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range db.SelectAll() {
|
||||
row := record.HTML(timeNow())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := t.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
logger.Warn(err)
|
||||
fmt.Fprint(w, "An error occurred creating this webpage")
|
||||
}
|
||||
case r.Method == http.MethodGet && r.RequestURI == rootURL+"/update":
|
||||
logger.Info("Update started manually")
|
||||
forceUpdate()
|
||||
http.Redirect(w, r, rootURL, 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
57
internal/health/check.go
Normal file
57
internal/health/check.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
)
|
||||
|
||||
func MakeIsHealthy(db AllSelecter, resolver LookupIPer) func() error {
|
||||
return func() (err error) {
|
||||
return isHealthy(db, resolver)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrRecordUpdateFailed = errors.New("record update failed")
|
||||
ErrRecordIPNotSet = errors.New("record IP not set")
|
||||
ErrLookupMismatch = errors.New("lookup IP addresses do not match")
|
||||
)
|
||||
|
||||
// isHealthy checks all the records were updated successfully and returns an error if not.
|
||||
func isHealthy(db AllSelecter, resolver LookupIPer) (err error) {
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%w: %s", ErrRecordUpdateFailed, record.String())
|
||||
} else if record.Settings.Proxied() {
|
||||
continue
|
||||
}
|
||||
hostname := record.Settings.BuildDomainName()
|
||||
lookedUpIPs, err := resolver.LookupIP(context.Background(), "ip", hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("%w: for hostname %s", ErrRecordIPNotSet, 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("%w: %s instead of %s for %s",
|
||||
ErrLookupMismatch, strings.Join(lookedUpIPsString, ","), currentIP, hostname)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
53
internal/health/client.go
Normal file
53
internal/health/client.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func IsClientMode(args []string) bool {
|
||||
return len(args) > 1 && args[1] == "healthcheck"
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func NewClient() *Client {
|
||||
const timeout = 5 * time.Second
|
||||
return &Client{
|
||||
Client: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
var ErrUnhealthy = errors.New("program is unhealthy")
|
||||
|
||||
// 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, port uint16) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+strconv.Itoa(int(port)), 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 := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading body from response with status %s: %w", resp.Status, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf("%w: %s", ErrUnhealthy, string(b))
|
||||
}
|
||||
28
internal/health/handler.go
Normal file
28
internal/health/handler.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func newHandler(healthcheck func() error) http.Handler {
|
||||
return &handler{
|
||||
healthcheck: healthcheck,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
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
|
||||
}
|
||||
err := h.healthcheck()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
22
internal/health/interfaces.go
Normal file
22
internal/health/interfaces.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
type AllSelecter interface {
|
||||
SelectAll() (records []records.Record)
|
||||
}
|
||||
|
||||
type LookupIPer interface {
|
||||
LookupIP(ctx context.Context, network, host string) (ips []net.IP, err error)
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Info(s string)
|
||||
Warn(s string)
|
||||
Error(s string)
|
||||
}
|
||||
52
internal/health/server.go
Normal file
52
internal/health/server.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
address string
|
||||
logger Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewServer(address string, logger Logger, healthcheck func() error) *Server {
|
||||
handler := newHandler(healthcheck)
|
||||
return &Server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
server := http.Server{
|
||||
Addr: s.address,
|
||||
Handler: s.handler,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
}
|
||||
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()
|
||||
err := server.Shutdown(shutdownCtx)
|
||||
if err != nil {
|
||||
s.logger.Error("failed shutting down: " + err.Error())
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on " + s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err.Error())
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package healthcheck
|
||||
|
||||
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)
|
||||
|
||||
// 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.DNSLookup() {
|
||||
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
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
// Provider is a possible DNS provider.
|
||||
Provider 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package models
|
||||
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
// IPMethod is a method to obtain your public IP address.
|
||||
type IPMethod struct {
|
||||
Name string
|
||||
URL string
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"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: %w", ipVersion, err)
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d", ipVersion, url, status)
|
||||
}
|
||||
s := string(content)
|
||||
switch ipVersion {
|
||||
case constants.IPv4:
|
||||
return searchIP(constants.IPv4, s)
|
||||
case constants.IPv6:
|
||||
return searchIP(constants.IPv6, s)
|
||||
case constants.IPv4OrIPv6:
|
||||
var ipv4Err, ipv6Err error
|
||||
ip, ipv4Err = searchIP(constants.IPv4, s)
|
||||
if ipv4Err != nil {
|
||||
ip, ipv6Err = searchIP(constants.IPv6, s)
|
||||
}
|
||||
if ipv6Err != nil {
|
||||
return nil, fmt.Errorf("%s, %s", ipv4Err, ipv6Err)
|
||||
}
|
||||
return ip, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("ip version %q not supported", ipVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func searchIP(version models.IPVersion, s string) (ip net.IP, err error) {
|
||||
verifier := verification.NewVerifier()
|
||||
var regexSearch func(s string) []string
|
||||
switch version {
|
||||
case constants.IPv4:
|
||||
regexSearch = verifier.SearchIPv4
|
||||
case constants.IPv6:
|
||||
regexSearch = verifier.SearchIPv6
|
||||
default:
|
||||
return nil, fmt.Errorf("ip version %q is not supported for regex search", version)
|
||||
}
|
||||
ips := regexSearch(s)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no public %s address found", version)
|
||||
}
|
||||
uniqueIPs := make(map[string]struct{})
|
||||
for _, ipString := range ips {
|
||||
uniqueIPs[ipString] = struct{}{}
|
||||
}
|
||||
netIPs := []net.IP{}
|
||||
for ipString := range uniqueIPs {
|
||||
netIP := net.ParseIP(ipString)
|
||||
if netIP == nil || netIPIsPrivate(netIP) {
|
||||
// in case the regex is not restrictive enough
|
||||
// or the IP address is private
|
||||
continue
|
||||
}
|
||||
netIPs = append(netIPs, netIP)
|
||||
}
|
||||
switch len(netIPs) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("no public %s address found", version)
|
||||
case 1:
|
||||
return netIPs[0], nil
|
||||
default:
|
||||
sort.Slice(netIPs, func(i, j int) bool {
|
||||
return bytes.Compare(netIPs[i], netIPs[j]) < 0
|
||||
})
|
||||
ips = make([]string, len(netIPs))
|
||||
for i := range netIPs {
|
||||
ips[i] = netIPs[i].String()
|
||||
}
|
||||
return nil, fmt.Errorf("multiple public %s addresses found: %s", version, strings.Join(ips, " "))
|
||||
}
|
||||
}
|
||||
|
||||
func netIPIsPrivate(netIP net.IP) bool {
|
||||
for _, privateCIDRBlock := range [8]string{
|
||||
"127.0.0.1/8", // localhost
|
||||
"10.0.0.0/8", // 24-bit block
|
||||
"172.16.0.0/12", // 20-bit block
|
||||
"192.168.0.0/16", // 16-bit block
|
||||
"169.254.0.0/16", // link local address
|
||||
"::1/128", // localhost IPv6
|
||||
"fc00::/7", // unique local address IPv6
|
||||
"fe80::/10", // link local address IPv6
|
||||
} {
|
||||
_, CIDR, _ := net.ParseCIDR(privateCIDRBlock)
|
||||
if CIDR.Contains(netIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,155 +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: 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"),
|
||||
},
|
||||
"ipv4 address": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("55.55.55.55"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"ipv6 address": {
|
||||
IPVersion: constants.IPv6,
|
||||
mockContent: []byte("ad07:e846:51ac:6cd0:0000:0000:0000:0000"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"ipv4 or ipv6 found ipv4": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
mockContent: []byte("55.55.55.55"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"ipv4 or ipv6 found ipv6": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
mockContent: []byte("ad07:e846:51ac:6cd0:0000:0000:0000:0000"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"ipv4 or ipv6 not found": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
mockContent: []byte("abc"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("no public ipv4 address found, no public ipv6 address found"),
|
||||
},
|
||||
"unsupported ip version": {
|
||||
IPVersion: models.IPVersion("x"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("ip version \"x\" not supported"),
|
||||
},
|
||||
}
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_searchIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
IPVersion models.IPVersion
|
||||
s string
|
||||
ip net.IP
|
||||
err error
|
||||
}{
|
||||
"unsupported ip version": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
err: fmt.Errorf("ip version \"ipv4 or ipv6\" is not supported for regex search"),
|
||||
},
|
||||
"no content": {
|
||||
IPVersion: constants.IPv4,
|
||||
err: fmt.Errorf("no public ipv4 address found"),
|
||||
},
|
||||
"single ipv4 address": {
|
||||
IPVersion: constants.IPv4,
|
||||
s: "abcd 55.55.55.55 abcd",
|
||||
ip: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"single ipv6 address": {
|
||||
IPVersion: constants.IPv6,
|
||||
s: "abcd bd07:e846:51ac:6cd0:0000:0000:0000:0000 abcd",
|
||||
ip: net.IP{0xbd, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"single private ipv4 address": {
|
||||
IPVersion: constants.IPv4,
|
||||
s: "abcd 10.0.0.3 abcd",
|
||||
err: fmt.Errorf("no public ipv4 address found"),
|
||||
},
|
||||
"single private ipv6 address": {
|
||||
IPVersion: constants.IPv6,
|
||||
s: "abcd ::1 abcd",
|
||||
err: fmt.Errorf("no public ipv6 address found"),
|
||||
},
|
||||
"2 ipv4 addresses": {
|
||||
IPVersion: constants.IPv4,
|
||||
s: "55.55.55.55 56.56.56.56",
|
||||
err: fmt.Errorf("multiple public ipv4 addresses found: 55.55.55.55 56.56.56.56"),
|
||||
},
|
||||
"2 ipv6 addresses": {
|
||||
IPVersion: constants.IPv6,
|
||||
s: "bd07:e846:51ac:6cd0:0000:0000:0000:0000 ad07:e846:51ac:6cd0:0000:0000:0000:0000",
|
||||
err: fmt.Errorf("multiple public ipv6 addresses found: ad07:e846:51ac:6cd0:: bd07:e846:51ac:6cd0::"), //nolint:golint
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ip, err := searchIP(tc.IPVersion, tc.s)
|
||||
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
|
||||
}
|
||||
7
internal/params/env.go
Normal file
7
internal/params/env.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package params
|
||||
|
||||
import "github.com/qdm12/golibs/params"
|
||||
|
||||
type envInterface interface {
|
||||
Get(key string, options ...params.OptionSetter) (value string, err error)
|
||||
}
|
||||
@@ -1,148 +1,168 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"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/internal/settings/constants"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
// nolint: maligned
|
||||
type commonSettings struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
NoDNSLookup bool `json:"no_dns_lookup"`
|
||||
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 the JSON content, first trying from the environment variable CONFIG
|
||||
// and then from the file config.json
|
||||
func (r *reader) GetSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
allSettings, warnings, err = r.getSettingsFromEnv()
|
||||
// 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(filePath)
|
||||
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) {
|
||||
var errWriteConfigToFile = errors.New("cannot write configuration to file")
|
||||
|
||||
// getSettingsFromFile obtain the update settings from config.json.
|
||||
func (r *Reader) getSettingsFromFile(filePath string) (
|
||||
allSettings []settings.Settings, warnings []string, err error) {
|
||||
r.logger.Info("reading JSON config from file " + filePath)
|
||||
bytes, err := r.readFile(filePath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
r.logger.Info("file not found, creating an empty settings file")
|
||||
|
||||
const mode = fs.FileMode(0600)
|
||||
|
||||
err = r.writeFile(filePath, []byte(`{}`), mode)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %w", errWriteConfigToFile, err)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
r.logger.Debug("config read: " + string(bytes))
|
||||
|
||||
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.envParams.GetEnv("CONFIG")
|
||||
// getSettingsFromEnv obtain the update settings from the environment variable CONFIG.
|
||||
// If the settings are valid, they are written to the filePath.
|
||||
func (r *Reader) getSettingsFromEnv(filePath string) (
|
||||
allSettings []settings.Settings, warnings []string, err error) {
|
||||
s, err := r.env.Get("CONFIG", params.CaseSensitiveValue())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(s) == 0 {
|
||||
return nil, nil, fmt.Errorf("%w: for environment variable CONFIG", err)
|
||||
} else if s == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return extractAllSettings([]byte(s))
|
||||
r.logger.Info("reading JSON config from environment variable CONFIG")
|
||||
r.logger.Debug("config read: " + s)
|
||||
|
||||
b := []byte(s)
|
||||
|
||||
allSettings, warnings, err = extractAllSettings(b)
|
||||
if err != nil {
|
||||
return allSettings, warnings, fmt.Errorf("configuration given: %w", err)
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
err = json.Indent(buffer, b, "", " ")
|
||||
if err != nil {
|
||||
return allSettings, warnings, fmt.Errorf("%w: %w", errWriteConfigToFile, err)
|
||||
}
|
||||
const mode = fs.FileMode(0600)
|
||||
err = r.writeFile(filePath, buffer.Bytes(), mode)
|
||||
if err != nil {
|
||||
return allSettings, warnings, fmt.Errorf("%w: %w", errWriteConfigToFile, err)
|
||||
}
|
||||
|
||||
return allSettings, warnings, nil
|
||||
}
|
||||
|
||||
func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
var (
|
||||
errUnmarshalCommon = errors.New("cannot unmarshal common settings")
|
||||
errUnmarshalRaw = errors.New("cannot unmarshal raw configuration")
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
if err := json.Unmarshal(jsonBytes, &rawConfig); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
matcher, err := regex.NewMatcher()
|
||||
err = json.Unmarshal(jsonBytes, &config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("%w: %w", errUnmarshalCommon, err)
|
||||
}
|
||||
err = json.Unmarshal(jsonBytes, &rawConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("%w: %w", errUnmarshalRaw, err)
|
||||
}
|
||||
|
||||
for i, common := range config.CommonSettings {
|
||||
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i], matcher)
|
||||
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i])
|
||||
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
|
||||
}
|
||||
|
||||
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage, matcher regex.Matcher) (settingsSlice []settings.Settings, warnings []string, err error) {
|
||||
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage) (
|
||||
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 {
|
||||
if provider == constants.DuckDNS { // only hosts, no domain
|
||||
if common.Domain != "" { // retro compatibility
|
||||
if common.Host == "" {
|
||||
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))
|
||||
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))
|
||||
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, ",")
|
||||
for _, host := range hosts {
|
||||
if len(host) == 0 {
|
||||
return nil, warnings, fmt.Errorf("host cannot be empty")
|
||||
}
|
||||
|
||||
if common.IPVersion == "" {
|
||||
common.IPVersion = ipversion.IP4or6.String()
|
||||
}
|
||||
ipVersion := models.IPVersion(common.IPVersion)
|
||||
if len(ipVersion) == 0 {
|
||||
ipVersion = constants.IPv4OrIPv6 // default
|
||||
}
|
||||
if ipVersion != constants.IPv4OrIPv6 && ipVersion != constants.IPv4 && ipVersion != constants.IPv6 {
|
||||
return nil, warnings, fmt.Errorf("ip version %q is not valid", ipVersion)
|
||||
}
|
||||
var settingsConstructor settings.Constructor
|
||||
switch provider {
|
||||
case constants.CLOUDFLARE:
|
||||
settingsConstructor = settings.NewCloudflare
|
||||
case constants.DDNSSDE:
|
||||
settingsConstructor = settings.NewDdnss
|
||||
case constants.DONDOMINIO:
|
||||
settingsConstructor = settings.NewDonDominio
|
||||
case constants.DNSPOD:
|
||||
settingsConstructor = settings.NewDNSPod
|
||||
case constants.DREAMHOST:
|
||||
settingsConstructor = settings.NewDreamhost
|
||||
case constants.DUCKDNS:
|
||||
settingsConstructor = settings.NewDuckdns
|
||||
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.NAMECHEAP:
|
||||
settingsConstructor = settings.NewNamecheap
|
||||
case constants.NOIP:
|
||||
settingsConstructor = settings.NewNoip
|
||||
case constants.DYN:
|
||||
settingsConstructor = settings.NewDyn
|
||||
default:
|
||||
return nil, warnings, fmt.Errorf("provider %q is not supported", provider)
|
||||
ipVersion, err := ipversion.Parse(common.IPVersion)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
settingsSlice = make([]settings.Settings, len(hosts))
|
||||
for i, host := range hosts {
|
||||
settingsSlice[i], err = settingsConstructor(rawSettings, common.Domain, host, ipVersion, common.NoDNSLookup, matcher)
|
||||
settingsSlice[i], err = settings.New(provider, rawSettings, common.Domain,
|
||||
host, ipVersion)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user