mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-22 00:52:42 -04:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||||
- If you don't use Linux or WSL 2, share your home directory `~/` and the directory of your project with Docker Desktop
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
||||
- Ensure your host has the following and that they are accessible by Docker:
|
||||
- `~/.ssh` directory
|
||||
- `~/.gitconfig` file (can be empty)
|
||||
|
||||
## Setup
|
||||
|
||||
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.
|
||||
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
|
||||
|
||||
## 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
|
||||
USER root
|
||||
RUN apk add curl
|
||||
USER vscode
|
||||
```
|
||||
|
||||
Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`.
|
||||
|
||||
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 `/home/vscode/.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).
|
||||
|
||||
### 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,79 @@
|
||||
{
|
||||
"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": "source ~/.windows.sh && go mod download && go mod tidy",
|
||||
"workspaceFolder": "/workspace",
|
||||
// "overrideCommand": "",
|
||||
"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
|
||||
// "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",
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
},
|
||||
"editor.codeActionsOnSaveTimeout": 3000,
|
||||
"go.useLanguageServer": true,
|
||||
"[go]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
// Optional: Disable snippets, as they conflict with completion ranking.
|
||||
"editor.snippetSuggestions": "none"
|
||||
},
|
||||
"[go.mod]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
},
|
||||
},
|
||||
"gopls": {
|
||||
"usePlaceholders": false,
|
||||
"staticcheck": true
|
||||
},
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.gotoSymbol.includeImports": true,
|
||||
"go.gotoSymbol.includeGoroot": true,
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"go.toolsEnvVars": {
|
||||
"GOFLAGS": "-tags=",
|
||||
"CGO_ENABLED": 1 // for the race detector
|
||||
},
|
||||
"gopls.env": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"go.testEnvVars": {
|
||||
"": "",
|
||||
},
|
||||
"go.testFlags": ["-v", "-race"],
|
||||
"go.testTimeout": "10s",
|
||||
"go.coverOnSingleTest": true,
|
||||
"go.coverOnSingleTestFile": true,
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,31 @@ version: "3.7"
|
||||
|
||||
services:
|
||||
vscode:
|
||||
image: qmcgaw/godevcontainer
|
||||
build: .
|
||||
image: godevcontainer
|
||||
volumes:
|
||||
- ../:/workspace
|
||||
- ~/.ssh:/home/vscode/.ssh:ro
|
||||
- ~/.ssh:/root/.ssh:ro
|
||||
# Docker
|
||||
- ~/.docker:/root/.docker:z
|
||||
# Docker socket to access Docker server
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# SSH directory for Linux, OSX and WSL
|
||||
- ~/.ssh:/root/.ssh:z
|
||||
# For Windows without WSL, a copy will be made
|
||||
# from /tmp/.ssh to ~/.ssh to fix permissions
|
||||
# - ~/.ssh:/tmp/.ssh:ro
|
||||
# Shell history persistence
|
||||
- ~/.zsh_history:/root/.zsh_history:z
|
||||
# Git config
|
||||
- ~/.gitconfig:/root/.gitconfig:z
|
||||
# Kubernetes
|
||||
- ~/.kube:/root/.kube:z
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
|
||||
|
||||
128
.github/workflows/build.yml
vendored
128
.github/workflows/build.yml
vendored
@@ -1,31 +1,107 @@
|
||||
name: Docker build
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-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@v2
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- name: Go mod tidy check
|
||||
run: docker build --target tidy .
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container \
|
||||
go test \
|
||||
-race \
|
||||
-coverpkg=./... \
|
||||
-coverprofile=coverage.txt \
|
||||
-covermode=atomic \
|
||||
./...
|
||||
|
||||
# We run this here to use the caching of the previous steps
|
||||
- name: Build final image
|
||||
run: docker build .
|
||||
|
||||
publish:
|
||||
needs: [verify]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: docker build .
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Set variables
|
||||
id: vars
|
||||
run: |
|
||||
BRANCH=${GITHUB_REF#refs/heads/}
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo ::set-output name=commit::$(git rev-parse --short HEAD)
|
||||
echo ::set-output name=build_date::$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
if [ "$TAG" != "$GITHUB_REF" ]; then
|
||||
echo ::set-output name=version::$TAG
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
elif [ "$BRANCH" = "master" ]; then
|
||||
echo ::set-output name=version::latest
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
else
|
||||
echo ::set-output name=version::$BRANCH
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,
|
||||
fi
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: ${{ steps.vars.outputs.platforms }}
|
||||
build-args: |
|
||||
BUILD_DATE=${{ steps.vars.outputs.build_date }}
|
||||
COMMIT=${{ steps.vars.outputs.commit }}
|
||||
VERSION=${{ steps.vars.outputs.version }}
|
||||
tags: qmcgaw/ddns-updater:${{ steps.vars.outputs.version }}
|
||||
push: true
|
||||
|
||||
- if: github.event.ref == 'refs/heads/master'
|
||||
name: Microbadger hook
|
||||
run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s=
|
||||
continue-on-error: true
|
||||
|
||||
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
|
||||
6
.github/workflows/labels.yml
vendored
6
.github/workflows/labels.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: labels
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
branches: [master]
|
||||
paths:
|
||||
- '.github/labels.yml'
|
||||
- '.github/workflows/labels.yml'
|
||||
- .github/labels.yml
|
||||
- .github/workflows/labels.yml
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
*.exe
|
||||
updater
|
||||
.vscode
|
||||
|
||||
@@ -4,40 +4,68 @@ linters-settings:
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- path: cmd/updater/main.go
|
||||
text: "mnd: Magic number: 4, in <argument> detected"
|
||||
linters:
|
||||
- gomnd
|
||||
- path: cmd/updater/main.go
|
||||
text: "mnd: Magic number: 2, in <argument> detected"
|
||||
linters:
|
||||
- gomnd
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- asciicheck
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- gci
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godot
|
||||
- goheader
|
||||
- goimports
|
||||
- golint
|
||||
- gomnd
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
# - goerr113 # TODO
|
||||
- gosimple
|
||||
- govet
|
||||
- importas
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- maligned
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- nestif
|
||||
- nilerr
|
||||
- noctx
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- exportloopref
|
||||
- sqlclosecheck
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- thelper
|
||||
- tparallel
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- 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"
|
||||
}
|
||||
88
Dockerfile
88
Dockerfile
@@ -1,41 +1,73 @@
|
||||
ARG ALPINE_VERSION=3.12
|
||||
ARG GO_VERSION=1.15
|
||||
ARG ALPINE_VERSION=3.13
|
||||
ARG GO_VERSION=1.16
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS alpine
|
||||
FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates tzdata
|
||||
RUN mkdir /tmp/data && \
|
||||
chown 1000 /tmp/data && \
|
||||
chmod 700 /tmp/data
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
ARG GOLANGCI_LINT_VERSION=v1.31.0
|
||||
RUN apk --update add git
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
ENV CGO_ENABLED=0
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
|
||||
RUN apk --update add git
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY .golangci.yml .
|
||||
# Copy repository code and install Go dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
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
|
||||
ENV CGO_ENABLED=1
|
||||
# g++ is installed for the -race detector in go test
|
||||
RUN apk --update add g++
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS lint
|
||||
ARG GOLANGCI_LINT_VERSION=v1.40.1
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
|
||||
sh -s -- -b /usr/local/bin ${GOLANGCI_LINT_VERSION}
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS tidy
|
||||
RUN git init && \
|
||||
git config user.email ci@localhost && \
|
||||
git config user.name ci && \
|
||||
git add -A && git commit -m ci && \
|
||||
sed -i '/\/\/ indirect/d' go.mod && \
|
||||
go mod tidy && \
|
||||
git diff --exit-code -- go.mod
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS build
|
||||
COPY --from=qmcgaw/xcputranslate:v0.4.0 /xcputranslate /usr/local/bin/xcputranslate
|
||||
ARG TARGETPLATFORM
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
RUN GOARCH="$(xcputranslate -targetplatform ${TARGETPLATFORM} -field arch)" \
|
||||
GOARM="$(xcputranslate -targetplatform ${TARGETPLATFORM} -field arm)" \
|
||||
go build -trimpath -ldflags="-s -w \
|
||||
-X 'main.version=$VERSION' \
|
||||
-X 'main.buildDate=$BUILD_DATE' \
|
||||
-X 'main.commit=$COMMIT' \
|
||||
" -o app cmd/updater/main.go
|
||||
|
||||
FROM scratch
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION
|
||||
ENV VERSION=$VERSION \
|
||||
BUILD_DATE=$BUILD_DATE \
|
||||
VCS_REF=$VCS_REF
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.revision=$VCS_REF \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.revision=$COMMIT \
|
||||
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.title="ddns-updater" \
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP"
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI"
|
||||
COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
EXPOSE 8000
|
||||
@@ -46,9 +78,12 @@ 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 \
|
||||
HTTP_TIMEOUT=10s \
|
||||
|
||||
# Web UI
|
||||
@@ -60,11 +95,10 @@ ENV \
|
||||
BACKUP_DIRECTORY=/updater/data \
|
||||
|
||||
# Other
|
||||
LOG_ENCODING=console \
|
||||
LOG_LEVEL=info \
|
||||
NODE_ID=-1 \
|
||||
LOG_CALLER=hidden \
|
||||
GOTIFY_URL= \
|
||||
GOTIFY_TOKEN= \
|
||||
TZ=
|
||||
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
|
||||
COPY --chown=1000 ui/* /updater/ui/
|
||||
COPY --from=alpine --chown=1000 /tmp/data /updater/data/
|
||||
COPY --from=build --chown=1000 /tmp/gobuild/app /updater/app
|
||||
|
||||
339
README.md
339
README.md
@@ -1,8 +1,8 @@
|
||||
# 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*
|
||||
*Light container updating DNS A and/or AAAA records periodically for multiple DNS providers*
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg?sanitize=true">
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
@@ -17,7 +17,34 @@
|
||||
|
||||
## 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)
|
||||
- Updates periodically A records for different DNS providers:
|
||||
- Cloudflare
|
||||
- DDNSS.de
|
||||
- DigitalOcean
|
||||
- DonDominio
|
||||
- DNSOMatic
|
||||
- DNSPod
|
||||
- Dreamhost
|
||||
- DuckDNS
|
||||
- DynDNS
|
||||
- FreeDNS
|
||||
- Gandi
|
||||
- GoDaddy
|
||||
- Google
|
||||
- He.net
|
||||
- Infomaniak
|
||||
- Linode
|
||||
- LuaDNS
|
||||
- Namecheap
|
||||
- NoIP
|
||||
- Njalla
|
||||
- OpenDNS
|
||||
- OVH
|
||||
- Selfhost.de
|
||||
- [Spdyn](spdyn.de)
|
||||
- Strato.de
|
||||
- Variomedia.de
|
||||
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
|
||||
- Web User interface
|
||||
|
||||

|
||||
@@ -31,51 +58,7 @@
|
||||
|
||||
## 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
|
||||
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:
|
||||
|
||||
@@ -92,13 +75,32 @@ Note that this CONFIG environment variable takes precedence over the 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:
|
||||
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:
|
||||
@@ -126,120 +128,40 @@ Start by having the following content in *config.json*, or in your `CONFIG` envi
|
||||
}
|
||||
```
|
||||
|
||||
The following parameters are to be added:
|
||||
For each setting, you need to fill in parameters.
|
||||
Check the documentation for your DNS provider:
|
||||
|
||||
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:
|
||||
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
|
||||
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
|
||||
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
|
||||
- [DonDominio](https://github.com/qdm12/ddns-updater/blob/master/docs/dondominio.md)
|
||||
- [DNSOMatic](https://github.com/qdm12/ddns-updater/blob/master/docs/dnsomatic.md)
|
||||
- [DNSPod](https://github.com/qdm12/ddns-updater/blob/master/docs/dnspod.md)
|
||||
- [Dreamhost](https://github.com/qdm12/ddns-updater/blob/master/docs/dreamhost.md)
|
||||
- [DuckDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/duckdns.md)
|
||||
- [DynDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/dyndns.md)
|
||||
- [DynV6](https://github.com/qdm12/ddns-updater/blob/master/docs/dynv6.md)
|
||||
- [FreeDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/freedns.md)
|
||||
- [Gandi](https://github.com/qdm12/ddns-updater/blob/master/docs/gandi.md)
|
||||
- [GoDaddy](https://github.com/qdm12/ddns-updater/blob/master/docs/godaddy.md)
|
||||
- [Google](https://github.com/qdm12/ddns-updater/blob/master/docs/google.md)
|
||||
- [He.net](https://github.com/qdm12/ddns-updater/blob/master/docs/he.net.md)
|
||||
- [Infomaniak](https://github.com/qdm12/ddns-updater/blob/master/docs/infomaniak.md)
|
||||
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
|
||||
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
|
||||
- [Namecheap](https://github.com/qdm12/ddns-updater/blob/master/docs/namecheap.md)
|
||||
- [NoIP](https://github.com/qdm12/ddns-updater/blob/master/docs/noip.md)
|
||||
- [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)
|
||||
- [Selfhost.de](https://github.com/qdm12/ddns-updater/blob/master/docs/selfhost.de.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)
|
||||
|
||||
- `"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.
|
||||
Note that:
|
||||
|
||||
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",`.
|
||||
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
@@ -247,44 +169,53 @@ DonDominio:
|
||||
| --- | --- | --- |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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 |
|
||||
| `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 |
|
||||
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`. |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `debug`, `info`, `warning` or `error` |
|
||||
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
|
||||
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
|
||||
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
|
||||
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
|
||||
|
||||
#### IP methods
|
||||
#### Public IP
|
||||
|
||||
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.
|
||||
By default, all public IP fetching types are used and cycled (over DNS and over HTTPs).
|
||||
|
||||
- IPv4 or IPv6 (for most cases)
|
||||
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)
|
||||
- `"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`)
|
||||
- `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
|
||||
|
||||
@@ -295,9 +226,39 @@ If you have a host firewall in place, this container needs the following ports:
|
||||
- UDP 53 outbound for outbound DNS resolution
|
||||
- TCP 8000 inbound (or other) for the WebUI
|
||||
|
||||
## Domain set up
|
||||
## Architecture
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
## Gotify
|
||||
|
||||
@@ -326,11 +287,9 @@ To set it up with DDNS updater:
|
||||
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)
|
||||
- [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)
|
||||
|
||||
@@ -2,74 +2,95 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/backup"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/handlers"
|
||||
"github.com/qdm12/ddns-updater/internal/healthcheck"
|
||||
"github.com/qdm12/ddns-updater/internal/health"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
recordslib "github.com/qdm12/ddns-updater/internal/records"
|
||||
"github.com/qdm12/ddns-updater/internal/server"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
|
||||
pubiphttp "github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"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"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
buildInfo models.BuildInformation
|
||||
version = "unknown"
|
||||
commit = "unknown"
|
||||
buildDate = "an unknown date"
|
||||
)
|
||||
|
||||
func main() {
|
||||
buildInfo.Version = version
|
||||
buildInfo.Commit = commit
|
||||
buildInfo.BuildDate = buildDate
|
||||
os.Exit(_main(context.Background(), time.Now))
|
||||
// returns 1 on error
|
||||
// returns 2 on os signal
|
||||
}
|
||||
|
||||
type allParams struct {
|
||||
period time.Duration
|
||||
ipMethod models.IPMethod
|
||||
ipv4Method models.IPMethod
|
||||
ipv6Method models.IPMethod
|
||||
cooldown time.Duration
|
||||
httpTimeout time.Duration
|
||||
ipv6Mask net.IPMask
|
||||
httpSettings publicip.HTTPSettings
|
||||
dnsSettings publicip.DNSSettings
|
||||
dir string
|
||||
dataDir string
|
||||
listeningPort string
|
||||
listeningPort uint16
|
||||
rootURL string
|
||||
healthAddress string
|
||||
backupPeriod time.Duration
|
||||
backupDirectory string
|
||||
}
|
||||
|
||||
func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
if health.IsClientMode(os.Args) {
|
||||
// Running the program in a separate instance through the Docker
|
||||
// built-in healthcheck, in an ephemeral fashion to query the
|
||||
// long running instance of the program about its status
|
||||
if err := libhealthcheck.Query(); err != nil {
|
||||
client := health.NewClient()
|
||||
paramsReader := params.NewReader(nil) // nil logger as no retro compat use of it
|
||||
address, _, err := paramsReader.HealthServerAddress()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
if err := client.Query(ctx, address); err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
logger, err := setupLogger()
|
||||
|
||||
fmt.Println(splash.Splash(buildInfo))
|
||||
|
||||
// Setup logger
|
||||
paramsReader := params.NewReader(logging.New(logging.Settings{})) // use a temporary logger
|
||||
logLevel, logCaller, err := paramsReader.LoggerConfig()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
paramsReader := params.NewReader(logger)
|
||||
|
||||
fmt.Println(splash.Splash(
|
||||
paramsReader.GetVersion(),
|
||||
paramsReader.GetVcsRef(),
|
||||
paramsReader.GetBuildDate()))
|
||||
logger := logging.NewParent(logging.Settings{Level: logLevel, Caller: logCaller})
|
||||
paramsReader = params.NewReader(logger)
|
||||
|
||||
notify, err := setupGotify(paramsReader, logger)
|
||||
if err != nil {
|
||||
@@ -90,7 +111,8 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
settings, warnings, err := paramsReader.GetSettings(p.dataDir + "/config.json")
|
||||
configFilepath := filepath.Join(p.dataDir, "config.json")
|
||||
settings, warnings, err := paramsReader.JSONSettings(configFilepath, logger)
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(2, w)
|
||||
@@ -100,14 +122,24 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
if len(settings) > 1 {
|
||||
logger.Info("Found %d settings to update records", len(settings))
|
||||
} else if len(settings) == 1 {
|
||||
|
||||
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 %d settings to update records", len(settings))
|
||||
}
|
||||
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
|
||||
client := &http.Client{Timeout: p.httpTimeout}
|
||||
|
||||
connectivity := connectivity.NewConnectivity(net.DefaultResolver, client)
|
||||
for _, err := range connectivity.Checks(ctx, "github.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())
|
||||
@@ -119,42 +151,53 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
}
|
||||
records[i] = recordslib.New(s, events)
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
client := network.NewClient(HTTPTimeout)
|
||||
defer client.Close()
|
||||
|
||||
defer client.CloseIdleConnections()
|
||||
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)
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
|
||||
p.httpSettings.Client = client
|
||||
|
||||
ipGetter, err := publicip.NewFetcher(p.dnsSettings, p.httpSettings)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
updater := update.NewUpdater(db, client, notify, logger)
|
||||
runner := update.NewRunner(db, updater, ipGetter, p.ipv6Mask, p.cooldown, 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)
|
||||
go runner.Run(ctx, p.period)
|
||||
|
||||
// note: errors are logged within the goroutine,
|
||||
// no need to collect the resulting errors.
|
||||
go runner.ForceUpdate(ctx)
|
||||
|
||||
isHealthy := health.MakeIsHealthy(db, net.LookupIP, logger)
|
||||
healthServer := health.NewServer(p.healthAddress,
|
||||
logger.NewChild(logging.Settings{Prefix: "healthcheck server: "}),
|
||||
isHealthy)
|
||||
wg.Add(1)
|
||||
go healthServer.Run(ctx, wg)
|
||||
|
||||
address := fmt.Sprintf("0.0.0.0:%d", p.listeningPort)
|
||||
serverLogger := logger.NewChild(logging.Settings{Prefix: "http server: "})
|
||||
server := server.New(ctx, address, p.rootURL, db, serverLogger, runner)
|
||||
wg.Add(1)
|
||||
go server.Run(ctx, wg)
|
||||
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))
|
||||
|
||||
go backupRunLoop(ctx, p.backupPeriod, p.dir, p.backupDirectory,
|
||||
logger.NewChild(logging.Settings{Prefix: "backup: "}), timeNow)
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals,
|
||||
@@ -163,16 +206,11 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
os.Interrupt,
|
||||
)
|
||||
select {
|
||||
case errors := <-serverErrors:
|
||||
for _, err := range errors {
|
||||
logger.Error(err)
|
||||
}
|
||||
return 1
|
||||
case signal := <-osSignals:
|
||||
message := fmt.Sprintf("Stopping program: caught OS signal %q", signal)
|
||||
logger.Warn(message)
|
||||
notify(2, message)
|
||||
return 2
|
||||
return 1
|
||||
case <-ctx.Done():
|
||||
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
|
||||
logger.Warn(message)
|
||||
@@ -180,23 +218,15 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogger() (logging.Logger, error) {
|
||||
paramsReader := params.NewReader(nil)
|
||||
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logging.NewLogger(encoding, level, nodeID)
|
||||
}
|
||||
|
||||
func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func(priority int, messageArgs ...interface{}), err error) {
|
||||
gotifyURL, err := paramsReader.GetGotifyURL()
|
||||
func setupGotify(paramsReader params.Reader, logger logging.Logger) (
|
||||
notify func(priority int, messageArgs ...interface{}), err error) {
|
||||
gotifyURL, err := paramsReader.GotifyURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if gotifyURL == nil {
|
||||
return func(priority int, messageArgs ...interface{}) {}, nil
|
||||
}
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
gotifyToken, err := paramsReader.GotifyToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -210,46 +240,88 @@ func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func
|
||||
|
||||
func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams, err error) {
|
||||
var warnings []string
|
||||
p.period, warnings, err = paramsReader.GetPeriod()
|
||||
p.period, warnings, err = paramsReader.Period()
|
||||
for _, warning := range warnings {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.ipMethod, err = paramsReader.GetIPMethod()
|
||||
p.cooldown, err = paramsReader.CooldownPeriod()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.ipv4Method, err = paramsReader.GetIPv4Method()
|
||||
|
||||
p.ipv6Mask, err = paramsReader.IPv6Prefix()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.ipv6Method, err = paramsReader.GetIPv6Method()
|
||||
|
||||
p.httpSettings.Enabled, p.dnsSettings.Enabled, err = paramsReader.PublicIPFetchers()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.dir, err = paramsReader.GetExeDir()
|
||||
|
||||
p.httpTimeout, err = paramsReader.HTTPTimeout()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.dataDir, err = paramsReader.GetDataDir(p.dir)
|
||||
|
||||
httpIPProviders, err := paramsReader.PublicIPHTTPProviders()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.listeningPort, _, err = paramsReader.GetListeningPort()
|
||||
httpIP4Providers, err := paramsReader.PublicIPv4HTTPProviders()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.rootURL, err = paramsReader.GetRootURL()
|
||||
httpIP6Providers, err := paramsReader.PublicIPv6HTTPProviders()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.backupPeriod, err = paramsReader.GetBackupPeriod()
|
||||
p.httpSettings.Options = []pubiphttp.Option{
|
||||
pubiphttp.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
|
||||
pubiphttp.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
|
||||
pubiphttp.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
|
||||
}
|
||||
|
||||
dnsIPProviders, err := paramsReader.PublicIPDNSProviders()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.backupDirectory, err = paramsReader.GetBackupDirectory()
|
||||
p.dnsSettings.Options = []dns.Option{
|
||||
dns.SetProviders(dnsIPProviders[0], dnsIPProviders[1:]...),
|
||||
}
|
||||
|
||||
p.dir, err = paramsReader.ExeDir()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.dataDir, err = paramsReader.DataDir(p.dir)
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.listeningPort, _, err = paramsReader.ListeningPort()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.rootURL, err = paramsReader.RootURL()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
var warning string
|
||||
p.healthAddress, warning, err = paramsReader.HealthServerAddress()
|
||||
if warning != "" {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.backupPeriod, err = paramsReader.BackupPeriod()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.backupDirectory, err = paramsReader.BackupDirectory()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
@@ -258,7 +330,6 @@ func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams,
|
||||
|
||||
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
|
||||
logger logging.Logger, timeNow func() time.Time) {
|
||||
logger = logger.WithPrefix("backup: ")
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
|
||||
@@ -11,9 +11,12 @@ 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
|
||||
- HTTP_TIMEOUT=10s
|
||||
|
||||
# Web UI
|
||||
@@ -25,9 +28,8 @@ services:
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
|
||||
# Other
|
||||
- LOG_ENCODING=console
|
||||
- LOG_LEVEL=info
|
||||
- NODE_ID=-1 # -1 to disable
|
||||
- LOG_CALLER=hidden
|
||||
- GOTIFY_URL=
|
||||
- GOTIFY_TOKEN=
|
||||
restart: always
|
||||
|
||||
57
docs/cloudflare.md
Normal file
57
docs/cloudflare.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Cloudflare
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "cloudflare",
|
||||
"zone_identifier": "some id",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"ttl": 600,
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"zone_identifier"` is the Zone ID of your site
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following:
|
||||
- Email `"email"` and Global API Key `"key"`
|
||||
- User service key `"user_service_key"`
|
||||
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"proxied"` can be set to `true` to use the proxy services of Cloudflare
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), and defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Make sure you have `curl` installed
|
||||
1. Obtain your API key from Cloudflare website ([see this](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-))
|
||||
1. Obtain your zone identifier for your domain name, from the domain's overview page written as *Zone ID*
|
||||
1. Find your **identifier** in the `id` field with
|
||||
|
||||
```sh
|
||||
ZONEID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
EMAIL=example@example.com
|
||||
APIKEY=aaaaaaaaaaaaaaaaaa
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONEID/dns_records" \
|
||||
-H "X-Auth-Email: $EMAIL" \
|
||||
-H "X-Auth-Key: $APIKEY"
|
||||
```
|
||||
|
||||
You can now fill in the necessary parameters in *config.json*
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
52
docs/contributing.md
Normal file
52
docs/contributing.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Contributing
|
||||
|
||||
## Table of content
|
||||
|
||||
1. [Setup](#Setup)
|
||||
1. [Commands available](#Commands-available)
|
||||
1. [Guidelines](#Guidelines)
|
||||
|
||||
## Setup
|
||||
|
||||
### Using VSCode and Docker
|
||||
|
||||
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1:
|
||||
|
||||
### Locally
|
||||
|
||||
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](../.vscode/settings.json).
|
||||
|
||||
## Build and Run
|
||||
|
||||
```sh
|
||||
go build -o app cmd/updater/main.go
|
||||
./app
|
||||
```
|
||||
|
||||
## Commands available
|
||||
|
||||
- Test the code: `go test ./...`
|
||||
- Lint the code `golangci-lint run`
|
||||
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
|
||||
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
|
||||
|
||||
## Guidelines
|
||||
|
||||
The Go code is in the Go file [cmd/updater/main.go](](../cmd/updater/main.go) and the [internal directory](](../internal), you might want to start reading the main.go file.
|
||||
|
||||
See the [Contributing document](](../.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
34
docs/ddnss.de.md
Normal file
34
docs/ddnss.de.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# DDNSS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "ddnss",
|
||||
"provider_ip": true,
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "user",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
31
docs/digitalocean.md
Normal file
31
docs/digitalocean.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Digital Ocean
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "digitalocean",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"token"` is your token that you can create [here](https://cloud.digitalocean.com/settings/applications)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
32
docs/dnsomatic.md
Normal file
32
docs/dnsomatic.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# DNS-O-Matic
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dnsomatic",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
31
docs/dnspod.md
Normal file
31
docs/dnspod.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# DNSPod
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dnspod",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "yourtoken",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
33
docs/dondominio.md
Normal file
33
docs/dondominio.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Don Dominio
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dondominio",
|
||||
"domain": "domain.com",
|
||||
"name": "something",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"name"` is the name server associated with the domain
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
29
docs/dreamhost.md
Normal file
29
docs/dreamhost.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Dreamhost
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "domain.com",
|
||||
"key": "key",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
35
docs/duckdns.md
Normal file
35
docs/duckdns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# DuckDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"host": "host",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"host"` is your host, for example `subdomain` for `subdomain.duckdns.org`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://duckdns.org)
|
||||
|
||||
*See the [duckdns website](https://duckdns.org)*
|
||||
35
docs/dyndns.md
Normal file
35
docs/dyndns.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# DynDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dyn",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
33
docs/dynv6.md
Normal file
33
docs/dynv6.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# DynV6
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dynv6",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"` that you can obtain [here](https://dynv6.com/keys#token)
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
31
docs/freedns.md
Normal file
31
docs/freedns.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# FreeDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "freedns",
|
||||
"domain": "domain.com",
|
||||
"host": "host",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host (subdomain)
|
||||
- `"token"` is the randomized update token you use to update your record
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
37
docs/gandi.md
Normal file
37
docs/gandi.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Gandi
|
||||
|
||||
This provider uses Gandi v5 API
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "gandi",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"ttl": 3600,
|
||||
"ip_version": "ipv4",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` which can be a subdomain, `@` or a wildcard `*`
|
||||
- `"key"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"ttl"` default is `3600`
|
||||
|
||||
## Domain setup
|
||||
|
||||
[Gandi Documentation Website](https://docs.gandi.net/en/domain_names/advanced_users/api.html#gandi-s-api)
|
||||
61
docs/godaddy.md
Normal file
61
docs/godaddy.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# GoDaddy
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"key": "key",
|
||||
"secret": "secret",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"key"`
|
||||
- `"secret"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://godaddy.com)
|
||||
|
||||
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a Test key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a **Production** key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
Obtain the **key** and **secret** of that production key.
|
||||
|
||||
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
|
||||
|
||||
## Testing
|
||||
|
||||
1. Go to [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
|
||||
|
||||
[](https://dcc.godaddy.com/manage/)
|
||||
|
||||
1. Change the IP address to `127.0.0.1`
|
||||
1. Run the ddns-updater
|
||||
1. Refresh the Godaddy webpage to check the update occurred.
|
||||
42
docs/google.md
Normal file
42
docs/google.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Google
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
Thanks to [@gauravspatel](https://github.com/gauravspatel) for #124
|
||||
|
||||
1. Enable dynamic DNS in the *synthetic records* section of DNS management.
|
||||
1. The username and password is generated once you create the dynamic DNS entry.
|
||||
|
||||
### Wildcard entries
|
||||
|
||||
If you want to create a **wildcard entry**, you have to create a custom **CNAME** record with key `"*"` and value `"@"`.
|
||||
31
docs/he.net.md
Normal file
31
docs/he.net.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# He.net
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "he",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"` (untested)
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
34
docs/infomaniak.md
Normal file
34
docs/infomaniak.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Infomaniak
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "infomaniak",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
## Domain setup
|
||||
34
docs/linode.md
Normal file
34
docs/linode.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Linode
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "linode",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Create a personal access token with `domains` set, with read and write privileges, ideally that never expires. You can refer to [@AnujRNair's comment](https://github.com/qdm12/ddns-updater/pull/144#discussion_r559292678) and to [Linode's guide](https://www.linode.com/docs/products/tools/cloud-manager/guides/cloud-api-keys).
|
||||
1. The program will create the A or AAAA record for you if it doesn't exist already.
|
||||
37
docs/luadns.md
Normal file
37
docs/luadns.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# LuaDNS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "luadns",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"email": "email",
|
||||
"token": "token",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
|
||||
- `"email"`
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
|
||||
1. Go to [api.luadns.com/settings](https://api.luadns.com/settings)
|
||||
1. Enable API access
|
||||
1. Obtain your API token and replace it in the parameters as the value for `token`
|
||||
57
docs/namecheap.md
Normal file
57
docs/namecheap.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Namecheap
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"provider_ip": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"password"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
Note that Namecheap only supports ipv4 addresses for now.
|
||||
|
||||
## Domain setup
|
||||
|
||||
[](https://www.namecheap.com)
|
||||
|
||||
1. Create a Namecheap account and buy a domain name - *example.com* as an example
|
||||
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
|
||||
|
||||
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
|
||||
|
||||
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
|
||||
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
|
||||
|
||||

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

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

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

|
||||
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"`
|
||||
- `"app_secret"`
|
||||
- `"consumer_key"`
|
||||
|
||||
The ZoneDNS implementation allows you to update any record name including *.yourdomain.tld
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
- `"mode"` select between two modes, OVH's dynamic hosting service (`"dynamic"`) or OVH's API (`"api"`). Default is `"dynamic"`
|
||||
|
||||
## Domain setup
|
||||
|
||||
- If you use DynHost: [docs.ovh.com/ie/en/domains/hosting_dynhost](https://docs.ovh.com/ie/en/domains/hosting_dynhost/)
|
||||
- If you use the ZoneDNS API: [docs.ovh.com/gb/en/customer/first-steps-with-ovh-api](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/)
|
||||
33
docs/selfhost.de.md
Normal file
33
docs/selfhost.de.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Selfhost.de
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "selfhost.de",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"` is your DynDNS username
|
||||
- `"password"` is your DynDNS password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
|
||||
## Domain setup
|
||||
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/)
|
||||
12
go.mod
12
go.mod
@@ -1,11 +1,13 @@
|
||||
module github.com/qdm12/ddns-updater
|
||||
|
||||
go 1.15
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/go-chi/chi v1.5.4
|
||||
github.com/golang/mock v1.5.0
|
||||
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/miekg/dns v1.1.42
|
||||
github.com/ovh/go-ovh v1.1.0
|
||||
github.com/qdm12/golibs v0.0.0-20210514224620-c025cb0da211
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
||||
|
||||
124
go.sum
124
go.sum
@@ -1,5 +1,3 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
|
||||
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
@@ -10,10 +8,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.11.0 h1:l4iX0RqNnx/pU7rY2DB/I+znuYY0K3x6Ywac6EIr0PA=
|
||||
github.com/fatih/color v1.11.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
|
||||
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
|
||||
@@ -37,105 +37,83 @@ github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi88
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq8iS6U=
|
||||
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
|
||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
|
||||
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
|
||||
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
|
||||
github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
|
||||
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
||||
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
|
||||
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a h1:IyS72qFm+iXipadmUKXmpJScKXXK2GrD8yYfxXsnIYs=
|
||||
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/qdm12/golibs v0.0.0-20210514224620-c025cb0da211 h1:tpjavgiEPlyZtsXO1xMzN1JpeDwhCMnn4c9dFVtl0i0=
|
||||
github.com/qdm12/golibs v0.0.0-20210514224620-c025cb0da211/go.mod h1:Is1wBOULKFH6NFPVBR+ksPWkm8njbKyQkOinLLfFAuE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g=
|
||||
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -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 +1,13 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// Announcement is a message announcement
|
||||
// Announcement is a message announcement.
|
||||
Announcement = "Support for he.net"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd.
|
||||
AnnouncementExpiration = "2020-10-15"
|
||||
)
|
||||
|
||||
const (
|
||||
// IssueLink is the link for users to use to create issues
|
||||
// IssueLink is the link for users to use to create issues.
|
||||
IssueLink = "https://github.com/qdm12/ddns-updater/issues/new"
|
||||
)
|
||||
|
||||
@@ -10,11 +10,10 @@ import (
|
||||
|
||||
type Database interface {
|
||||
Close() error
|
||||
Insert(record records.Record) (id int)
|
||||
Select(id int) (record records.Record, err error)
|
||||
SelectAll() (records []records.Record)
|
||||
// Using persistence database
|
||||
Update(id int, record records.Record) error
|
||||
// From persistence database
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
}
|
||||
|
||||
@@ -24,7 +23,7 @@ type database struct {
|
||||
persistentDB persistence.Database
|
||||
}
|
||||
|
||||
// NewDatabase creates a new in memory database
|
||||
// NewDatabase creates a new in memory database.
|
||||
func NewDatabase(data []records.Record, persistentDB persistence.Database) Database {
|
||||
return &database{
|
||||
data: data,
|
||||
|
||||
@@ -6,13 +6,6 @@ import (
|
||||
"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
|
||||
}
|
||||
|
||||
func (db *database) Select(id int) (record records.Record, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package healthcheck
|
||||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,18 +12,19 @@ import (
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
func MakeIsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) func() error {
|
||||
return func() (err error) {
|
||||
return isHealthy(db, lookupIP)
|
||||
}
|
||||
}
|
||||
|
||||
// isHealthy checks all the records were updated successfully and returns an error if not.
|
||||
func isHealthy(db data.Database, lookupIP lookupIPFunc) (err error) {
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%s", record.String())
|
||||
} else if !record.Settings.DNSLookup() {
|
||||
} else if record.Settings.Proxied() {
|
||||
continue
|
||||
}
|
||||
hostname := record.Settings.BuildDomainName()
|
||||
@@ -45,7 +46,8 @@ func IsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) (
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s", hostname, strings.Join(lookedUpIPsString, ","), currentIP)
|
||||
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s",
|
||||
hostname, strings.Join(lookedUpIPsString, ","), currentIP)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
53
internal/health/client.go
Normal file
53
internal/health/client.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func IsClientMode(args []string) bool {
|
||||
return len(args) > 1 && args[1] == "healthcheck"
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Query(ctx context.Context, address string) error
|
||||
}
|
||||
|
||||
type client struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func NewClient() Client {
|
||||
const timeout = 5 * time.Second
|
||||
return &client{
|
||||
Client: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
// Query sends an HTTP request to the other instance of
|
||||
// the program, and to its internal healthcheck server.
|
||||
func (c *client) Query(ctx context.Context, address string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://"+address, 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("%s: %s", resp.Status, err)
|
||||
}
|
||||
|
||||
return fmt.Errorf(string(b))
|
||||
}
|
||||
31
internal/health/handler.go
Normal file
31
internal/health/handler.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
func newHandler(logger logging.Logger, healthcheck func() error) http.Handler {
|
||||
return &handler{
|
||||
logger: logger,
|
||||
healthcheck: healthcheck,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
logger logging.Logger
|
||||
healthcheck func() error
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || (r.RequestURI != "" && r.RequestURI != "/") {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err := h.healthcheck(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
53
internal/health/server.go
Normal file
53
internal/health/server.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
Run(ctx context.Context, wg *sync.WaitGroup)
|
||||
}
|
||||
|
||||
type server struct {
|
||||
address string
|
||||
logger logging.Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewServer(address string, logger logging.Logger, healthcheck func() error) Server {
|
||||
handler := newHandler(logger, healthcheck)
|
||||
return &server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.logger.Warn("shutting down (context canceled)")
|
||||
defer s.logger.Warn("shut down")
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
s.logger.Error("failed shutting down: %s", err)
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on %s", s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err)
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,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
|
||||
}
|
||||
41
internal/params/ipprefix.go
Normal file
41
internal/params/ipprefix.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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/params/ipprefix_test.go
Normal file
62
internal/params/ipprefix_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package params
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,58 +2,69 @@ package params
|
||||
|
||||
import (
|
||||
"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/internal/settings/log"
|
||||
"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, logger log.Logger) (
|
||||
allSettings []settings.Settings, warnings []string, err error) {
|
||||
allSettings, warnings, err = r.getSettingsFromEnv(logger)
|
||||
if allSettings != nil || warnings != nil || err != nil {
|
||||
return allSettings, warnings, err
|
||||
}
|
||||
return r.getSettingsFromFile(filePath)
|
||||
return r.getSettingsFromFile(filePath, logger)
|
||||
}
|
||||
|
||||
// getSettingsFromFile obtain the update settings from config.json
|
||||
func (r *reader) getSettingsFromFile(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
// getSettingsFromFile obtain the update settings from config.json.
|
||||
func (r *reader) getSettingsFromFile(filePath string, logger log.Logger) (
|
||||
allSettings []settings.Settings, warnings []string, err error) {
|
||||
bytes, err := r.readFile(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil, err
|
||||
}
|
||||
const mode = fs.FileMode(0600)
|
||||
return nil, nil, r.writeFile(filePath, []byte(`{}`), mode)
|
||||
}
|
||||
return extractAllSettings(bytes)
|
||||
return extractAllSettings(bytes, logger)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (r *reader) getSettingsFromEnv(logger log.Logger) (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 {
|
||||
} else if s == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return extractAllSettings([]byte(s))
|
||||
return extractAllSettings([]byte(s), logger)
|
||||
}
|
||||
|
||||
func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
func extractAllSettings(jsonBytes []byte, logger log.Logger) (
|
||||
allSettings []settings.Settings, warnings []string, err error) {
|
||||
config := struct {
|
||||
CommonSettings []commonSettings `json:"settings"`
|
||||
}{}
|
||||
@@ -66,83 +77,52 @@ func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warn
|
||||
if err := json.Unmarshal(jsonBytes, &rawConfig); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
matcher, err := regex.NewMatcher()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
matcher := regex.NewMatcher()
|
||||
|
||||
for i, common := range config.CommonSettings {
|
||||
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i], matcher)
|
||||
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i], matcher, logger)
|
||||
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,
|
||||
matcher regex.Matcher, logger log.Logger) (
|
||||
settingsSlice []settings.Settings, warnings []string, err error) {
|
||||
provider := models.Provider(common.Provider)
|
||||
if provider == constants.DUCKDNS { // only hosts, no domain
|
||||
if provider == constants.DuckDNS { // only hosts, no domain
|
||||
if len(common.Domain) > 0 { // retro compatibility
|
||||
if len(common.Host) == 0 {
|
||||
common.Host = strings.TrimSuffix(common.Domain, ".duckdns.org")
|
||||
warnings = append(warnings, fmt.Sprintf("DuckDNS record should have %q specified as host instead of %q as domain", common.Host, common.Domain))
|
||||
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 len(common.IPVersion) == 0 {
|
||||
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, matcher, logger)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
|
||||
@@ -1,203 +1,300 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
"github.com/qdm12/ddns-updater/internal/settings/log"
|
||||
"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/logging"
|
||||
"github.com/qdm12/golibs/params"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
const https = "https"
|
||||
const (
|
||||
all = "all"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
// JSON
|
||||
GetSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error)
|
||||
JSONSettings(filePath string, logger log.Logger) (allSettings []settings.Settings, warnings []string, err error)
|
||||
|
||||
// Core
|
||||
GetPeriod() (period time.Duration, warnings []string, err error)
|
||||
GetIPMethod() (method models.IPMethod, err error)
|
||||
GetIPv4Method() (method models.IPMethod, err error)
|
||||
GetIPv6Method() (method models.IPMethod, err error)
|
||||
GetHTTPTimeout() (duration time.Duration, err error)
|
||||
Period() (period time.Duration, warnings []string, err error)
|
||||
PublicIPFetchers() (http, dns bool, err error)
|
||||
PublicIPHTTPProviders() (providers []http.Provider, err error)
|
||||
PublicIPv4HTTPProviders() (providers []http.Provider, err error)
|
||||
PublicIPv6HTTPProviders() (providers []http.Provider, err error)
|
||||
PublicIPDNSProviders() (providers []dns.Provider, err error)
|
||||
HTTPTimeout() (duration time.Duration, err error)
|
||||
CooldownPeriod() (duration time.Duration, err error)
|
||||
IPv6Prefix() (ipv6Mask net.IPMask, err error)
|
||||
|
||||
// File paths
|
||||
GetExeDir() (dir string, err error)
|
||||
GetDataDir(currentDir string) (string, error)
|
||||
ExeDir() (dir string, err error)
|
||||
DataDir(currentDir string) (string, error)
|
||||
|
||||
// Web UI
|
||||
GetListeningPort() (listeningPort, warning string, err error)
|
||||
GetRootURL() (rootURL string, err error)
|
||||
ListeningPort() (listeningPort uint16, warning string, err error)
|
||||
RootURL() (rootURL string, err error)
|
||||
|
||||
// Healthcheck
|
||||
HealthServerAddress() (address, warning string, err error)
|
||||
|
||||
// Backup
|
||||
GetBackupPeriod() (duration time.Duration, err error)
|
||||
GetBackupDirectory() (directory string, err error)
|
||||
BackupPeriod() (duration time.Duration, err error)
|
||||
BackupDirectory() (directory string, err error)
|
||||
|
||||
// Other
|
||||
GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error)
|
||||
GetGotifyURL() (URL *url.URL, err error)
|
||||
GetGotifyToken() (token string, err error)
|
||||
|
||||
// Version getters
|
||||
GetVersion() string
|
||||
GetBuildDate() string
|
||||
GetVcsRef() string
|
||||
LoggerConfig() (level logging.Level, caller logging.Caller, err error)
|
||||
GotifyURL() (URL *url.URL, err error)
|
||||
GotifyToken() (token string, err error)
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
envParams libparams.EnvParams
|
||||
verifier verification.Verifier
|
||||
env params.Env
|
||||
os params.OS
|
||||
readFile func(filename string) ([]byte, error)
|
||||
writeFile func(filename string, data []byte, perm fs.FileMode) (err error)
|
||||
retroFn func(oldKey, newKey string)
|
||||
}
|
||||
|
||||
func NewReader(logger logging.Logger) Reader {
|
||||
return &reader{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
verifier: verification.NewVerifier(),
|
||||
env: params.NewEnv(),
|
||||
os: params.NewOS(),
|
||||
readFile: ioutil.ReadFile,
|
||||
writeFile: os.WriteFile,
|
||||
retroFn: func(oldKey, newKey string) {
|
||||
logger.Warn("You are using the old environment variable %s, please consider switching to %s instead", oldKey, newKey)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetDataDir obtains the data directory from the environment
|
||||
// variable DATADIR
|
||||
func (r *reader) GetDataDir(currentDir string) (string, error) {
|
||||
return r.envParams.GetEnv("DATADIR", libparams.Default(currentDir+"/data"))
|
||||
// variable DATADIR.
|
||||
func (r *reader) DataDir(currentDir string) (string, error) {
|
||||
return r.env.Get("DATADIR", params.Default(currentDir+"/data"))
|
||||
}
|
||||
|
||||
func (r *reader) GetListeningPort() (listeningPort, warning string, err error) {
|
||||
return r.envParams.GetListeningPort()
|
||||
func (r *reader) ListeningPort() (listeningPort uint16, warning string, err error) {
|
||||
return r.env.ListeningPort("LISTENING_PORT", params.Default("8000"))
|
||||
}
|
||||
|
||||
func (r *reader) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
|
||||
return r.envParams.GetLoggerConfig()
|
||||
func (r *reader) LoggerConfig() (level logging.Level, caller logging.Caller, err error) {
|
||||
caller, err = r.env.LogCaller("LOG_CALLER", params.Default("hidden"))
|
||||
if err != nil {
|
||||
return level, caller, err
|
||||
}
|
||||
|
||||
level, err = r.env.LogLevel("LOG_LEVEL", params.Default("info"))
|
||||
if err != nil {
|
||||
return level, caller, err
|
||||
}
|
||||
|
||||
return level, caller, nil
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyURL() (url *url.URL, err error) {
|
||||
return r.envParams.GetGotifyURL()
|
||||
func (r *reader) GotifyURL() (url *url.URL, err error) {
|
||||
return r.env.URL("GOTIFY_URL")
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyToken() (token string, err error) {
|
||||
return r.envParams.GetGotifyToken()
|
||||
func (r *reader) GotifyToken() (token string, err error) {
|
||||
return r.env.Get("GOTIFY_TOKEN",
|
||||
params.CaseSensitiveValue(),
|
||||
params.Compulsory(),
|
||||
params.Unset())
|
||||
}
|
||||
|
||||
func (r *reader) GetRootURL() (rootURL string, err error) {
|
||||
return r.envParams.GetRootURL()
|
||||
func (r *reader) RootURL() (rootURL string, err error) {
|
||||
return r.env.RootURL("ROOT_URL")
|
||||
}
|
||||
|
||||
func (r *reader) GetPeriod() (period time.Duration, warnings []string, err error) {
|
||||
func (r *reader) HealthServerAddress() (address, warning string, err error) {
|
||||
return r.env.ListeningAddress("HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999"))
|
||||
}
|
||||
|
||||
func (r *reader) Period() (period time.Duration, warnings []string, err error) {
|
||||
// Backward compatibility
|
||||
n, err := r.envParams.GetEnvInt("DELAY", libparams.Compulsory())
|
||||
n, err := r.env.Int("DELAY", params.Compulsory())
|
||||
if err == nil { // integer only, treated as seconds
|
||||
return time.Duration(n) * time.Second,
|
||||
[]string{
|
||||
"the environment variable DELAY should be changed to PERIOD",
|
||||
fmt.Sprintf("the value for the duration period of the updater does not have a time unit, you might want to set it to \"%ds\" instead of \"%d\"", n, n),
|
||||
fmt.Sprintf(`the value for the duration period of the updater does not have a time unit, you might want to set it to "%ds" instead of "%d"`, n, n), //nolint:lll
|
||||
}, nil
|
||||
}
|
||||
period, err = r.envParams.GetDuration("DELAY", libparams.Compulsory())
|
||||
period, err = r.env.Duration("DELAY", params.Compulsory())
|
||||
if err == nil {
|
||||
return period,
|
||||
[]string{
|
||||
"the environment variable DELAY should be changed to PERIOD",
|
||||
}, nil
|
||||
}
|
||||
period, err = r.envParams.GetDuration("PERIOD", libparams.Default("10m"))
|
||||
period, err = r.env.Duration("PERIOD", params.Default("10m"))
|
||||
return period, nil, err
|
||||
}
|
||||
|
||||
func (r *reader) GetIPMethod() (method models.IPMethod, err error) {
|
||||
s, err := r.envParams.GetEnv("IP_METHOD", params.Default("cycle"))
|
||||
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
|
||||
|
||||
func (r *reader) PublicIPFetchers() (http, dns bool, err error) {
|
||||
s, err := r.env.Get("PUBLICIP_FETCHERS", params.Default(all))
|
||||
if err != nil {
|
||||
return method, err
|
||||
return false, false, err
|
||||
}
|
||||
for _, choice := range constants.IPMethods() {
|
||||
if choice.Name == s {
|
||||
return choice, nil
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
for i, field := range fields {
|
||||
switch strings.ToLower(field) {
|
||||
case all:
|
||||
return true, true, nil
|
||||
case "http":
|
||||
http = true
|
||||
case "dns":
|
||||
dns = true
|
||||
default:
|
||||
return false, false, fmt.Errorf(
|
||||
"%w: %q at position %d of %d",
|
||||
ErrInvalidFetcher, field, i+1, len(fields))
|
||||
}
|
||||
}
|
||||
url, err := url.Parse(s)
|
||||
if err != nil || url == nil || url.Scheme != https {
|
||||
return method, fmt.Errorf("ip method %q is not valid", s)
|
||||
}
|
||||
return models.IPMethod{
|
||||
Name: s,
|
||||
URL: s,
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
}, nil
|
||||
|
||||
return http, dns, nil
|
||||
}
|
||||
|
||||
func (r *reader) GetIPv4Method() (method models.IPMethod, err error) {
|
||||
s, err := r.envParams.GetEnv("IPV4_METHOD", params.Default("cycle"))
|
||||
// PublicIPHTTPProviders obtains the HTTP providers to obtain your public IPv4 and/or IPv6 address.
|
||||
func (r *reader) PublicIPDNSProviders() (providers []dns.Provider, err error) {
|
||||
s, err := r.env.Get("PUBLICIP_DNS_PROVIDERS", params.Default(all))
|
||||
if err != nil {
|
||||
return method, err
|
||||
return nil, err
|
||||
}
|
||||
for _, choice := range constants.IPMethods() {
|
||||
if choice.Name == s {
|
||||
if s != "cycle" && !choice.IPv4 {
|
||||
return method, fmt.Errorf("ip method %s does not support IPv4", s)
|
||||
}
|
||||
return choice, nil
|
||||
|
||||
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)
|
||||
if err := dns.ValidateProvider(providers[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
url, err := url.Parse(s)
|
||||
if err != nil || url == nil || url.Scheme != https {
|
||||
return method, fmt.Errorf("ipv4 method %q is not valid", s)
|
||||
}
|
||||
return models.IPMethod{
|
||||
Name: s,
|
||||
URL: s,
|
||||
IPv4: true,
|
||||
}, nil
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (r *reader) GetIPv6Method() (method models.IPMethod, err error) {
|
||||
s, err := r.envParams.GetEnv("IPV6_METHOD", params.Default("cycle"))
|
||||
// PublicIPHTTPProviders obtains the HTTP providers to obtain your public IPv4 or IPv6 address.
|
||||
func (r *reader) PublicIPHTTPProviders() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("PUBLICIP_HTTP_PROVIDERS", "IP_METHOD", ipversion.IP4or6)
|
||||
}
|
||||
|
||||
// PublicIPv4HTTPProviders obtains the HTTP providers to obtain your public IPv4 address.
|
||||
func (r *reader) PublicIPv4HTTPProviders() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("PUBLICIPV4_HTTP_PROVIDERS", "IPV4_METHOD", ipversion.IP4)
|
||||
}
|
||||
|
||||
// PublicIPv6HTTPProviders obtains the HTTP providers to obtain your public IPv6 address.
|
||||
func (r *reader) PublicIPv6HTTPProviders() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("PUBLICIPV6_HTTP_PROVIDERS", "IPV6_METHOD", ipversion.IP6)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidPublicIPHTTPProvider = errors.New("invalid public IP HTTP provider")
|
||||
)
|
||||
|
||||
func (r *reader) httpIPMethod(envKey, retroKey string, version ipversion.IPVersion) (
|
||||
providers []http.Provider, err error) {
|
||||
retroKeyOption := params.RetroKeys([]string{retroKey}, r.retroFn)
|
||||
s, err := r.env.Get(envKey, params.Default("cycle"), retroKeyOption)
|
||||
if err != nil {
|
||||
return method, err
|
||||
return nil, err
|
||||
}
|
||||
for _, choice := range constants.IPMethods() {
|
||||
if choice.Name == s {
|
||||
if s != "cycle" && !choice.IPv6 {
|
||||
return method, fmt.Errorf("ip method %s does not support IPv6", s)
|
||||
}
|
||||
return choice, nil
|
||||
|
||||
availableProviders := http.ListProvidersForVersion(version)
|
||||
choices := make(map[http.Provider]struct{}, len(availableProviders))
|
||||
for _, provider := range availableProviders {
|
||||
choices[provider] = struct{}{}
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
|
||||
for _, field := range fields {
|
||||
// Retro-compatibility.
|
||||
switch field {
|
||||
case "ipify6":
|
||||
field = "ipify"
|
||||
case "noip4", "noip6", "noip8245_4", "noip8245_6":
|
||||
field = "noip"
|
||||
case "cycle":
|
||||
field = all
|
||||
}
|
||||
|
||||
if field == all {
|
||||
return availableProviders, nil
|
||||
}
|
||||
|
||||
// Custom URL check
|
||||
url, err := url.Parse(field)
|
||||
if err == nil && url != nil && url.Scheme == "https" {
|
||||
providers = append(providers, http.CustomProvider(url))
|
||||
continue
|
||||
}
|
||||
|
||||
provider := http.Provider(field)
|
||||
if _, ok := choices[provider]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidPublicIPHTTPProvider, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
url, err := url.Parse(s)
|
||||
if err != nil || url == nil || url.Scheme != https {
|
||||
return method, fmt.Errorf("ipv6 method %q is not valid", s)
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, fmt.Errorf("%w: for IP version %s", ErrInvalidPublicIPHTTPProvider, version)
|
||||
}
|
||||
return models.IPMethod{
|
||||
Name: s,
|
||||
URL: s,
|
||||
IPv6: true,
|
||||
}, nil
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (r *reader) GetExeDir() (dir string, err error) {
|
||||
return r.envParams.GetExeDir()
|
||||
func (r *reader) ExeDir() (dir string, err error) {
|
||||
return r.os.ExeDir()
|
||||
}
|
||||
|
||||
func (r *reader) GetHTTPTimeout() (duration time.Duration, err error) {
|
||||
return r.envParams.GetHTTPTimeout(libparams.Default("10s"))
|
||||
func (r *reader) HTTPTimeout() (duration time.Duration, err error) {
|
||||
return r.env.Duration("HTTP_TIMEOUT", params.Default("10s"))
|
||||
}
|
||||
|
||||
func (r *reader) GetBackupPeriod() (duration time.Duration, err error) {
|
||||
s, err := r.envParams.GetEnv("BACKUP_PERIOD", libparams.Default("0"))
|
||||
func (r *reader) BackupPeriod() (duration time.Duration, err error) {
|
||||
s, err := r.env.Get("BACKUP_PERIOD", params.Default("0"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
func (r *reader) GetBackupDirectory() (directory string, err error) {
|
||||
return r.envParams.GetEnv("BACKUP_DIRECTORY", libparams.Default("./data"))
|
||||
func (r *reader) BackupDirectory() (directory string, err error) {
|
||||
return r.env.Path("BACKUP_DIRECTORY", params.Default("./data"))
|
||||
}
|
||||
|
||||
func (r *reader) CooldownPeriod() (duration time.Duration, err error) {
|
||||
return r.env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
|
||||
}
|
||||
|
||||
func (r *reader) IPv6Prefix() (ipv6Mask net.IPMask, err error) {
|
||||
s, err := r.env.Get("IPV6_PREFIX", params.Default("/128"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipv6DecimalPrefixToMask(s)
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
func (r *reader) GetVersion() string {
|
||||
version, _ := r.envParams.GetEnv("VERSION", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return version
|
||||
}
|
||||
|
||||
func (r *reader) GetBuildDate() string {
|
||||
buildDate, _ := r.envParams.GetEnv("BUILD_DATE", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
|
||||
func (r *reader) GetVcsRef() string {
|
||||
buildDate, _ := r.envParams.GetEnv("VCS_REF", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
@@ -51,7 +51,7 @@ func NewDatabase(dataDir string) (*Database, error) {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.Check(); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%s validation error: %w", db.filepath, err)
|
||||
}
|
||||
return &db, nil
|
||||
}
|
||||
@@ -74,7 +74,7 @@ func (db *Database) Check() error {
|
||||
case event.IP == nil:
|
||||
return fmt.Errorf("IP %d of %d is empty for record %s", i+1, len(record.Events), record)
|
||||
case event.Time.IsZero():
|
||||
return fmt.Errorf("Time of IP %d of %d is empty for record %s", i+1, len(record.Events), record)
|
||||
return fmt.Errorf("time of IP %d of %d is empty for record %s", i+1, len(record.Events), record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (db *Database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err
|
||||
}
|
||||
|
||||
// GetEvents gets all the IP addresses history for a certain domain and host, in the order
|
||||
// from oldest to newest
|
||||
// from oldest to newest.
|
||||
func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
@@ -44,7 +44,7 @@ func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetAllDomainsHosts gets all the domains and hosts from the database
|
||||
// GetAllDomainsHosts gets all the domains and hosts from the database.
|
||||
func (db *Database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
|
||||
@@ -9,16 +9,17 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
)
|
||||
|
||||
// Record contains all the information to update and display a DNS record
|
||||
// Record contains all the information to update and display a DNS record.
|
||||
type Record struct { // internal
|
||||
Settings settings.Settings // fixed
|
||||
History models.History // past information
|
||||
Status models.Status
|
||||
Message string
|
||||
Time time.Time
|
||||
LastBan *time.Time // nil means no last ban
|
||||
}
|
||||
|
||||
// New returns a new Record with settings and some history
|
||||
// New returns a new Record with settings and some history.
|
||||
func New(settings settings.Settings, events []models.HistoryEvent) Record {
|
||||
return Record{
|
||||
Settings: settings,
|
||||
@@ -32,5 +33,6 @@ func (r *Record) String() string {
|
||||
if len(r.Message) > 0 {
|
||||
status += " (" + r.Message + ")"
|
||||
}
|
||||
return fmt.Sprintf("%s: %s %s; %s", r.Settings.String(), status, r.Time.Format("2006-01-02 15:04:05 MST"), r.History.String())
|
||||
return fmt.Sprintf("%s: %s %s; %s",
|
||||
r.Settings, status, r.Time.Format("2006-01-02 15:04:05 MST"), r.History)
|
||||
}
|
||||
|
||||
@@ -3,55 +3,50 @@ package regex
|
||||
import "regexp"
|
||||
|
||||
type Matcher interface {
|
||||
GandiKey(s string) bool
|
||||
GodaddyKey(s string) bool
|
||||
GodaddySecret(s string) bool
|
||||
DuckDNSToken(s string) bool
|
||||
NamecheapPassword(s string) bool
|
||||
DreamhostKey(s string) bool
|
||||
CloudflareKey(s string) bool
|
||||
CloudflareUserServiceKey(s string) bool
|
||||
DNSOMaticUsername(s string) bool
|
||||
DNSOMaticPassword(s string) bool
|
||||
}
|
||||
|
||||
type matcher struct {
|
||||
goDaddyKey, goDaddySecret, duckDNSToken, namecheapPassword, dreamhostKey, cloudflareKey, cloudflareUserServiceKey *regexp.Regexp
|
||||
goDaddyKey, duckDNSToken, namecheapPassword, dreamhostKey, cloudflareKey,
|
||||
cloudflareUserServiceKey, dnsOMaticUsername, dnsOMaticPassword, gandiKey *regexp.Regexp
|
||||
}
|
||||
|
||||
//nolint:gocritic
|
||||
func NewMatcher() (m Matcher, err error) {
|
||||
matcher := &matcher{}
|
||||
matcher.goDaddyKey, err = regexp.Compile(`^[A-Za-z0-9]{10,14}\_[A-Za-z0-9]{22}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var (
|
||||
gandiKey = regexp.MustCompile(`^[A-Za-z0-9]{24}$`)
|
||||
goDaddyKey = regexp.MustCompile(`^[A-Za-z0-9]{8,14}\_[A-Za-z0-9]{21,22}$`)
|
||||
duckDNSToken = regexp.MustCompile(`^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$`)
|
||||
namecheapPassword = regexp.MustCompile(`^[a-f0-9]{32}$`)
|
||||
dreamhostKey = regexp.MustCompile(`^[a-zA-Z0-9]{16}$`)
|
||||
cloudflareKey = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
|
||||
cloudflareUserServiceKey = regexp.MustCompile(`^v1\.0.+$`)
|
||||
dnsOMaticUsername = regexp.MustCompile(`^[a-zA-Z0-9._-]{3,25}$`)
|
||||
dnsOMaticPassword = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{5,19}$`)
|
||||
)
|
||||
|
||||
func NewMatcher() Matcher {
|
||||
return &matcher{
|
||||
gandiKey: gandiKey,
|
||||
goDaddyKey: goDaddyKey,
|
||||
duckDNSToken: duckDNSToken,
|
||||
namecheapPassword: namecheapPassword,
|
||||
dreamhostKey: dreamhostKey,
|
||||
cloudflareKey: cloudflareKey,
|
||||
cloudflareUserServiceKey: cloudflareUserServiceKey,
|
||||
dnsOMaticUsername: dnsOMaticUsername,
|
||||
dnsOMaticPassword: dnsOMaticPassword,
|
||||
}
|
||||
matcher.goDaddySecret, err = regexp.Compile(`^[A-Za-z0-9]{22}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.duckDNSToken, err = regexp.Compile(`^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.namecheapPassword, err = regexp.Compile(`^[a-f0-9]{32}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.dreamhostKey, err = regexp.Compile(`^[a-zA-Z0-9]{16}$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.cloudflareKey, err = regexp.Compile(`^[a-zA-Z0-9]+$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matcher.cloudflareUserServiceKey, err = regexp.Compile(`^v1\.0.+$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
func (m *matcher) GandiKey(s string) bool { return m.gandiKey.MatchString(s) }
|
||||
func (m *matcher) GodaddyKey(s string) bool { return m.goDaddyKey.MatchString(s) }
|
||||
func (m *matcher) GodaddySecret(s string) bool { return m.goDaddySecret.MatchString(s) }
|
||||
func (m *matcher) DuckDNSToken(s string) bool { return m.duckDNSToken.MatchString(s) }
|
||||
func (m *matcher) NamecheapPassword(s string) bool { return m.namecheapPassword.MatchString(s) }
|
||||
func (m *matcher) DreamhostKey(s string) bool { return m.dreamhostKey.MatchString(s) }
|
||||
@@ -59,3 +54,5 @@ func (m *matcher) CloudflareKey(s string) bool { return m.cloudflareKey.Matc
|
||||
func (m *matcher) CloudflareUserServiceKey(s string) bool {
|
||||
return m.cloudflareUserServiceKey.MatchString(s)
|
||||
}
|
||||
func (m *matcher) DNSOMaticUsername(s string) bool { return m.dnsOMaticUsername.MatchString(s) }
|
||||
func (m *matcher) DNSOMaticPassword(s string) bool { return m.dnsOMaticPassword.MatchString(s) }
|
||||
|
||||
35
internal/server/error.go
Normal file
35
internal/server/error.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type errJSONWrapper struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, status int, errString string) {
|
||||
w.WriteHeader(status)
|
||||
if errString == "" {
|
||||
errString = http.StatusText(status)
|
||||
}
|
||||
body := errJSONWrapper{Error: errString}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
type errorsJSONWrapper struct {
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
func httpErrors(w http.ResponseWriter, status int, errors []error) {
|
||||
w.WriteHeader(status)
|
||||
|
||||
errs := make([]string, len(errors))
|
||||
for i := range errors {
|
||||
errs[i] = errors[i].Error()
|
||||
}
|
||||
|
||||
body := errorsJSONWrapper{Errors: errs}
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
51
internal/server/handler.go
Normal file
51
internal/server/handler.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
)
|
||||
|
||||
type handlers struct {
|
||||
ctx context.Context
|
||||
// Objects
|
||||
db data.Database
|
||||
runner update.Runner
|
||||
indexTemplate *template.Template
|
||||
// Mockable functions
|
||||
timeNow func() time.Time
|
||||
}
|
||||
|
||||
//go:embed ui/*
|
||||
var uiFS embed.FS //nolint:gochecknoglobals
|
||||
|
||||
func newHandler(ctx context.Context, rootURL string,
|
||||
db data.Database, runner update.Runner) http.Handler {
|
||||
indexTemplate := template.Must(template.ParseFS(uiFS, "ui/index.html"))
|
||||
|
||||
handlers := &handlers{
|
||||
ctx: ctx,
|
||||
db: db,
|
||||
indexTemplate: indexTemplate,
|
||||
// TODO build information
|
||||
timeNow: time.Now,
|
||||
runner: runner,
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Use(middleware.Logger)
|
||||
|
||||
router.Get(rootURL+"/", handlers.index)
|
||||
|
||||
router.Get(rootURL+"/update", handlers.update)
|
||||
|
||||
return router
|
||||
}
|
||||
18
internal/server/index.go
Normal file
18
internal/server/index.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func (h *handlers) index(w http.ResponseWriter, r *http.Request) {
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := record.HTML(h.timeNow())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := h.indexTemplate.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "failed generating webpage: "+err.Error())
|
||||
}
|
||||
}
|
||||
56
internal/server/server.go
Normal file
56
internal/server/server.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
Run(ctx context.Context, wg *sync.WaitGroup)
|
||||
}
|
||||
|
||||
type server struct {
|
||||
address string
|
||||
logger logging.Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func New(ctx context.Context, address, rootURL string, db data.Database, logger logging.Logger,
|
||||
runner update.Runner) Server {
|
||||
handler := newHandler(ctx, rootURL, db, runner)
|
||||
return &server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) Run(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.logger.Warn("shutting down (context canceled)")
|
||||
defer s.logger.Warn("shut down")
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
s.logger.Error("failed shutting down: %s", err)
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on %s", s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err)
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
internal/server/ui/favicon.ico
Normal file
BIN
internal/server/ui/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
internal/server/ui/favicon.svg
Normal file
1
internal/server/ui/favicon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="レイヤー_1" data-name="レイヤー 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><defs><style>.cls-1{fill:#6edbd1;}.cls-1,.cls-10,.cls-3,.cls-4,.cls-5{stroke:#000;}.cls-1,.cls-10,.cls-3,.cls-4,.cls-5,.cls-7{stroke-linecap:round;}.cls-1,.cls-10,.cls-5,.cls-7{stroke-width:3px;}.cls-1,.cls-10,.cls-2,.cls-3,.cls-4,.cls-5,.cls-6,.cls-7{fill-rule:evenodd;}.cls-3,.cls-4,.cls-5,.cls-6{fill:#fff;}.cls-3{stroke-width:2.91px;}.cls-4{stroke-width:2.82px;}.cls-10,.cls-7{fill:#f6d2a2;}.cls-7{stroke:#231f20;}.cls-8{fill:#febd15;}.cls-9{font-size:130.48px;font-family:Arial-BoldMT, Arial;font-weight:700;}</style></defs><title>ddnsgopher.icon</title><path class="cls-1" d="M27.31,93.21C-21.42,79.51,14.81,17.51,54,43Z"/><path class="cls-1" d="M243.83,38.36c38.57-27.09,73.43,34,28.6,49.42Z"/><path class="cls-2" d="M29.34,76.09c-6.43-3.4-11.14-8-7.21-15.46C25.77,53.75,32.53,54.5,39,57.9Z"/><path class="cls-2" d="M262.27,69.62c6.43-3.4,11.15-8,7.21-15.45-3.64-6.89-10.4-6.14-16.83-2.74Z"/><path class="cls-1" d="M286.18,300.15c.34-23.18-2.14-47.14-3.42-69-2.1-35.74.3-72-3.43-107.23-.21-2,4.06-5.13,3.81-7.14-.22-1.71-4.95-2.29-5.2-4A197.91,197.91,0,0,0,265.7,66.46c-18.2-35.8-49.18-48.33-84.54-52.25-3.11-.34-3.88-4.71-7-4.93-2.08-.15-6.55,3.82-8.65,3.72-6-.28-12-.39-18.12-.39-24.14,1.91-47.5,5.51-67.46,14.08-3.81,1.63-9.76-1.51-13.31.51-3.15,1.8-3.94,8.72-6.86,10.86-14,10.27-25.13,24.63-31.93,44.86-2.76,8.23,1.52,21-5.94,24.76-12,6.15-1.55,12.56-1.87,18.86-1.9,38,6.3,76.48,4.75,114.88-.66,16.19-3.48,40.24-4.54,58.73"/><path class="cls-3" d="M156,78.1c8.76,50.92,92,37.46,80-13.89C225.27,18.15,153,30.9,156,78.1"/><path class="cls-4" d="M56.49,86.34c11.35,44.33,82.33,33,79.65-11.49-3.2-53.25-90.5-43-79.65,11.49"/><ellipse cx="150.96" cy="159.38" rx="25.39" ry="32.86"/><path class="cls-5" d="M166,139.21c0,6.62,1.5,14,.25,21.09-1.69,3.2-5,3.53-7.87,4.83a11.59,11.59,0,0,1-8.86-6.92c-1-8,.38-15.82.64-23.86Z"/><ellipse cx="98.47" cy="77.62" rx="11.99" ry="12.98"/><path class="cls-6" d="M104.62,77.34c0,3.36-2.33,6.08-5.21,6.08s-5.2-2.72-5.2-6.08,2.33-6.08,5.2-6.08S104.62,74,104.62,77.34Z"/><ellipse cx="198.89" cy="74.38" rx="11.79" ry="12.98"/><path class="cls-6" d="M206.22,74.21c0,4.15-2.83,7.51-6.32,7.51s-6.32-3.36-6.32-7.51,2.83-7.5,6.32-7.5S206.22,70.07,206.22,74.21Z"/><path class="cls-5" d="M131.75,138.49c-5.23,12.67,2.91,38,17.1,19.32-1-8,.38-15.82.63-23.86Z"/><path class="cls-7" d="M133.54,114.08c-9.75.83-17.72,12.42-12.65,21.59,6.71,12.14,21.69-1.08,31,.16,10.73.22,19.53,11.35,28.16,2,9.59-10.39-4.13-20.5-14.86-25Z"/><path class="cls-2" d="M132.31,113.57c-.72-16.88,31.47-19,35.27-4.86s-33.69,17.38-35.27,4.86Z"/><polygon id="lighting_bolt" data-name="lighting bolt" class="cls-8" points="10.65 292.5 171.49 238.88 150.71 214.26 295.68 150.26 190.09 219.74 219.64 250.92 10.65 292.5"/><text class="cls-9" transform="translate(15.68 289.59)">DNS</text><path class="cls-10" d="M28.18,222.19s10.24,19.32.26,18.47c-3.07,12.48-15.52,1.79-15.52,1.79s-4.76-5.64-5-12.91C7.61,219.53,19.84,209.64,28.18,222.19Z"/><path class="cls-10" d="M289,218.12s-21.86-.54-16.65,8c-9.79,8.31,5.33,14.68,5.33,14.68s5.22,3.92,13.78-1.3S302.25,223.86,289,218.12Z"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
@@ -2,6 +2,7 @@
|
||||
|
||||
<head>
|
||||
<title>DDNS Updater</title>
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
<style>
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
18
internal/server/update.go
Normal file
18
internal/server/update.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *handlers) update(w http.ResponseWriter, r *http.Request) {
|
||||
start := h.timeNow()
|
||||
errors := h.runner.ForceUpdate(h.ctx)
|
||||
duration := h.timeNow().Sub(start)
|
||||
if len(errors) > 0 {
|
||||
httpErrors(w, http.StatusInternalServerError, errors)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
message := "All records updated successfully in " + duration.String()
|
||||
_, _ = w.Write([]byte(message))
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
netlib "github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type cloudflare struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
key string
|
||||
token string
|
||||
email string
|
||||
userServiceKey string
|
||||
zoneIdentifier string
|
||||
proxied bool
|
||||
ttl uint
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewCloudflare(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
Token string `json:"token"`
|
||||
Email string `json:"email"`
|
||||
UserServiceKey string `json:"user_service_key"`
|
||||
ZoneIdentifier string `json:"zone_identifier"`
|
||||
Proxied bool `json:"proxied"`
|
||||
TTL uint `json:"ttl"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &cloudflare{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
key: extraSettings.Key,
|
||||
token: extraSettings.Token,
|
||||
email: extraSettings.Email,
|
||||
userServiceKey: extraSettings.UserServiceKey,
|
||||
zoneIdentifier: extraSettings.ZoneIdentifier,
|
||||
proxied: extraSettings.Proxied,
|
||||
ttl: extraSettings.TTL,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := c.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *cloudflare) isValid() error {
|
||||
switch {
|
||||
case len(c.key) > 0: // email and key must be provided
|
||||
switch {
|
||||
case !c.matcher.CloudflareKey(c.key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !verification.NewVerifier().MatchEmail(c.email):
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
case len(c.userServiceKey) > 0: // only user service key
|
||||
if !c.matcher.CloudflareKey(c.key) {
|
||||
return fmt.Errorf("invalid user service key format")
|
||||
}
|
||||
default: // API token only
|
||||
}
|
||||
switch {
|
||||
case len(c.zoneIdentifier) == 0:
|
||||
return fmt.Errorf("zone identifier cannot be empty")
|
||||
case c.ttl == 0:
|
||||
return fmt.Errorf("TTL cannot be left to 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *cloudflare) String() string {
|
||||
return toString(c.domain, c.host, constants.CLOUDFLARE, c.ipVersion)
|
||||
}
|
||||
|
||||
func (c *cloudflare) Domain() string {
|
||||
return c.domain
|
||||
}
|
||||
|
||||
func (c *cloudflare) Host() string {
|
||||
return c.host
|
||||
}
|
||||
|
||||
func (c *cloudflare) IPVersion() models.IPVersion {
|
||||
return c.ipVersion
|
||||
}
|
||||
|
||||
func (c *cloudflare) DNSLookup() bool {
|
||||
return c.dnsLookup
|
||||
}
|
||||
|
||||
func (c *cloudflare) BuildDomainName() string {
|
||||
return buildDomainName(c.host, c.domain)
|
||||
}
|
||||
|
||||
func (c *cloudflare) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", c.BuildDomainName(), c.BuildDomainName())),
|
||||
Host: models.HTML(c.Host()),
|
||||
Provider: "<a href=\"https://www.cloudflare.com\">Cloudflare</a>",
|
||||
IPVersion: models.HTML(c.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func setHeaders(r *http.Request, token, userServiceKey, email, key string) {
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
switch {
|
||||
case len(token) > 0:
|
||||
r.Header.Set("Authorization", "Bearer "+token)
|
||||
case len(userServiceKey) > 0:
|
||||
r.Header.Set("X-Auth-User-Service-Key", userServiceKey)
|
||||
case len(email) > 0 && len(key) > 0:
|
||||
r.Header.Set("X-Auth-Email", email)
|
||||
r.Header.Set("X-Auth-Key", key)
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain domain identifier
|
||||
// See https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
|
||||
func (c *cloudflare) getRecordIdentifier(client netlib.Client, newIP net.IP) (identifier string, upToDate bool, err error) {
|
||||
recordType := A
|
||||
if newIP.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.cloudflare.com",
|
||||
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records", c.zoneIdentifier),
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("type", recordType)
|
||||
values.Set("name", c.BuildDomainName())
|
||||
values.Set("page", "1")
|
||||
values.Set("per_page", "1")
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
setHeaders(r, c.token, c.userServiceKey, c.email, c.key)
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
} else if status != http.StatusOK {
|
||||
return "", false, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
listRecordsResponse := struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []string `json:"errors"`
|
||||
Result []struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
} `json:"result"`
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &listRecordsResponse); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
switch {
|
||||
case len(listRecordsResponse.Errors) > 0:
|
||||
return "", false, fmt.Errorf(strings.Join(listRecordsResponse.Errors, ","))
|
||||
case !listRecordsResponse.Success:
|
||||
return "", false, fmt.Errorf("request to Cloudflare not successful")
|
||||
case len(listRecordsResponse.Result) == 0:
|
||||
return "", false, fmt.Errorf("received no result from Cloudflare")
|
||||
case len(listRecordsResponse.Result) > 1:
|
||||
return "", false, fmt.Errorf("received %d results instead of 1 from Cloudflare", len(listRecordsResponse.Result))
|
||||
case listRecordsResponse.Result[0].Content == newIP.String(): // up to date
|
||||
return "", true, nil
|
||||
}
|
||||
return listRecordsResponse.Result[0].ID, false, nil
|
||||
}
|
||||
|
||||
func (c *cloudflare) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
identifier, upToDate, err := c.getRecordIdentifier(client, ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if upToDate {
|
||||
return ip, nil
|
||||
}
|
||||
type cloudflarePutBody struct {
|
||||
Type string `json:"type"` // A or AAAA depending on ip address given
|
||||
Name string `json:"name"` // DNS record name i.e. example.com
|
||||
Content string `json:"content"` // ip address
|
||||
Proxied bool `json:"proxied"` // whether the record is receiving the performance and security benefits of Cloudflare
|
||||
TTL uint `json:"ttl"`
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.cloudflare.com",
|
||||
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records/%s", c.zoneIdentifier, identifier),
|
||||
}
|
||||
r, err := network.BuildHTTPPut(
|
||||
u.String(),
|
||||
cloudflarePutBody{
|
||||
Type: recordType,
|
||||
Name: c.host,
|
||||
Content: ip.String(),
|
||||
Proxied: c.proxied,
|
||||
TTL: c.ttl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setHeaders(r, c.token, c.userServiceKey, c.email, c.key)
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status > http.StatusUnsupportedMediaType {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var parsedJSON struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"errors"`
|
||||
Result struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &parsedJSON); err != nil {
|
||||
return nil, err
|
||||
} else if !parsedJSON.Success {
|
||||
var errStr string
|
||||
for _, e := range parsedJSON.Errors {
|
||||
errStr += fmt.Sprintf("error %d: %s; ", e.Code, e.Message)
|
||||
}
|
||||
return nil, fmt.Errorf(errStr)
|
||||
}
|
||||
newIP = net.ParseIP(parsedJSON.Result.Content)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("new IP %q is malformed", parsedJSON.Result.Content)
|
||||
} else if !newIP.Equal(ip) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package settings
|
||||
|
||||
const (
|
||||
badauth = "badauth"
|
||||
success = "success"
|
||||
nohost = "nohost"
|
||||
A = "A"
|
||||
AAAA = "AAAA"
|
||||
)
|
||||
11
internal/settings/constants/dyndns.go
Normal file
11
internal/settings/constants/dyndns.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
Badauth = "badauth"
|
||||
Success = "success"
|
||||
Nohost = "nohost"
|
||||
Notfqdn = "notfqdn"
|
||||
Badagent = "badagent"
|
||||
Abuse = "abuse"
|
||||
Nineoneone = "911"
|
||||
)
|
||||
6
internal/settings/constants/ip.go
Normal file
6
internal/settings/constants/ip.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
A = "A"
|
||||
AAAA = "AAAA"
|
||||
)
|
||||
66
internal/settings/constants/providers.go
Normal file
66
internal/settings/constants/providers.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
// All possible provider values.
|
||||
const (
|
||||
Cloudflare models.Provider = "cloudflare"
|
||||
DdnssDe models.Provider = "ddnss"
|
||||
DigitalOcean models.Provider = "digitalocean"
|
||||
DnsOMatic models.Provider = "dnsomatic"
|
||||
DNSPod models.Provider = "dnspod"
|
||||
DonDominio models.Provider = "dondominio"
|
||||
Dreamhost models.Provider = "dreamhost"
|
||||
DuckDNS models.Provider = "duckdns"
|
||||
Dyn models.Provider = "dyn"
|
||||
DynV6 models.Provider = "dynv6"
|
||||
FreeDNS models.Provider = "freedns"
|
||||
Gandi models.Provider = "gandi"
|
||||
GoDaddy models.Provider = "godaddy"
|
||||
Google models.Provider = "google"
|
||||
HE models.Provider = "he"
|
||||
Infomaniak models.Provider = "infomaniak"
|
||||
Linode models.Provider = "linode"
|
||||
LuaDNS models.Provider = "luadns"
|
||||
Namecheap models.Provider = "namecheap"
|
||||
Njalla models.Provider = "njalla"
|
||||
NoIP models.Provider = "noip"
|
||||
OpenDNS models.Provider = "opendns"
|
||||
OVH models.Provider = "ovh"
|
||||
SelfhostDe models.Provider = "selfhost.de"
|
||||
Spdyn models.Provider = "spdyn"
|
||||
Strato models.Provider = "strato"
|
||||
Variomedia models.Provider = "variomedia"
|
||||
)
|
||||
|
||||
func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
Cloudflare,
|
||||
DdnssDe,
|
||||
DigitalOcean,
|
||||
DnsOMatic,
|
||||
DNSPod,
|
||||
DonDominio,
|
||||
Dreamhost,
|
||||
DuckDNS,
|
||||
Dyn,
|
||||
DynV6,
|
||||
FreeDNS,
|
||||
Gandi,
|
||||
GoDaddy,
|
||||
Google,
|
||||
HE,
|
||||
Infomaniak,
|
||||
Linode,
|
||||
LuaDNS,
|
||||
Namecheap,
|
||||
Njalla,
|
||||
NoIP,
|
||||
OpenDNS,
|
||||
OVH,
|
||||
SelfhostDe,
|
||||
Spdyn,
|
||||
Strato,
|
||||
Variomedia,
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type ddnss struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDdnss(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &ddnss{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *ddnss) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(d.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case d.host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ddnss) String() string {
|
||||
return toString(d.domain, d.host, constants.DDNSSDE, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *ddnss) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *ddnss) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *ddnss) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *ddnss) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *ddnss) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *ddnss) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://ddnss.de/\">DDNSS.de</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ddnss) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.ddnss.de",
|
||||
Path: "/upd.php",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("user", d.username)
|
||||
values.Set("pwd", d.password)
|
||||
fqdn := d.domain
|
||||
if d.host != "@" {
|
||||
fqdn = d.host + "." + d.domain
|
||||
}
|
||||
values.Set("host", fqdn)
|
||||
if !d.useProviderIP {
|
||||
if ip.To4() == nil { // ipv6
|
||||
values.Set("ip6", ip.String())
|
||||
} else {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("received status %d with message: %s", status, s)
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(s, "badysys"):
|
||||
return nil, fmt.Errorf("ddnss.de: invalid system parameter")
|
||||
case strings.Contains(s, badauth):
|
||||
return nil, fmt.Errorf("ddnss.de: bad authentication")
|
||||
case strings.Contains(s, "notfqdn"):
|
||||
return nil, fmt.Errorf("ddnss.de: hostname %q does not exist", fqdn)
|
||||
case strings.Contains(s, "Updated 1 hostname"):
|
||||
return ip, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown response received from ddnss.de: %s", s)
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
type dnspod struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
token string
|
||||
}
|
||||
|
||||
func NewDNSPod(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &dnspod{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
token: extraSettings.Token,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dnspod) isValid() error {
|
||||
if len(d.token) == 0 {
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dnspod) String() string {
|
||||
return toString(d.domain, d.host, constants.DNSPOD, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *dnspod) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dnspod) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dnspod) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dnspod) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *dnspod) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dnspod) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dnspod.cn/\">DNSPod</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnspod) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dnsapi.cn",
|
||||
Path: "/Record.List",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("login_token", d.token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("length", "200")
|
||||
values.Set("sub_domain", d.host)
|
||||
values.Set("record_type", recordType)
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var recordResp struct {
|
||||
Records []struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Line string `json:"line"`
|
||||
} `json:"records"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &recordResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var recordID, recordLine string
|
||||
for _, record := range recordResp.Records {
|
||||
if record.Type == A && record.Name == d.host {
|
||||
receivedIP := net.ParseIP(record.Value)
|
||||
if ip.Equal(receivedIP) {
|
||||
return ip, nil
|
||||
}
|
||||
recordID = record.ID
|
||||
recordLine = record.Line
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(recordID) == 0 {
|
||||
return nil, fmt.Errorf("record not found")
|
||||
}
|
||||
|
||||
u.Path = "/Record.Ddns"
|
||||
values = url.Values{}
|
||||
values.Set("login_token", d.token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("record_id", recordID)
|
||||
values.Set("value", ip.String())
|
||||
values.Set("record_line", recordLine)
|
||||
values.Set("sub_domain", d.host)
|
||||
u.RawQuery = values.Encode()
|
||||
r, err = http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err = client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var ddnsResp struct {
|
||||
Record struct {
|
||||
ID int64 `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Name string `json:"name"`
|
||||
} `json:"record"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &ddnsResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivedIP := net.ParseIP(ddnsResp.Record.Value)
|
||||
if !ip.Equal(receivedIP) {
|
||||
return nil, fmt.Errorf("ip not set")
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
netlib "github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type donDominio struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
name string
|
||||
}
|
||||
|
||||
func NewDonDominio(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(host) == 0 {
|
||||
host = "@" // default
|
||||
}
|
||||
d := &donDominio{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
name: extraSettings.Name,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *donDominio) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(d.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case len(d.name) == 0:
|
||||
return fmt.Errorf("name cannot be empty")
|
||||
case d.host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *donDominio) String() string {
|
||||
return toString(d.domain, d.host, constants.DONDOMINIO, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *donDominio) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *donDominio) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *donDominio) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *donDominio) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *donDominio) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *donDominio) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dondominio.com/\">DonDominio</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *donDominio) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "simple-api.dondominio.net",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("apiuser", d.username)
|
||||
values.Set("apipasswd", d.password)
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("name", d.name)
|
||||
isIPv4 := ip.To4() != nil
|
||||
if isIPv4 {
|
||||
values.Set("ipv4", ip.String())
|
||||
} else {
|
||||
values.Set("ipv6", ip.String())
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentid.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorCodeMessage string `json:"errorCodeMsg"`
|
||||
ResponseData struct {
|
||||
GlueRecords []struct {
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
} `json:"gluerecords"`
|
||||
} `json:"responseData"`
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !response.Success {
|
||||
return nil, fmt.Errorf("%s (error code %d)", response.ErrorCodeMessage, response.ErrorCode)
|
||||
}
|
||||
ipString := response.ResponseData.GlueRecords[0].IPv4
|
||||
if !isIPv4 {
|
||||
ipString = response.ResponseData.GlueRecords[0].IPv6
|
||||
}
|
||||
newIP = net.ParseIP(ipString)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ipString)
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
type dreamhost struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
key string
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDreamhost(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(host) == 0 {
|
||||
host = "@" // default
|
||||
}
|
||||
d := &dreamhost{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
key: extraSettings.Key,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) isValid() error {
|
||||
switch {
|
||||
case !d.matcher.DreamhostKey(d.key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case d.host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) String() string {
|
||||
return toString(d.domain, d.host, constants.DREAMHOST, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *dreamhost) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dreamhost) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dreamhost) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dreamhost) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *dreamhost) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dreamhost) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dreamhost) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
records, err := listDreamhostRecords(client, d.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var oldIP net.IP
|
||||
for _, data := range records.Data {
|
||||
if data.Type == recordType && data.Record == d.BuildDomainName() {
|
||||
if data.Editable == "0" {
|
||||
return nil, fmt.Errorf("record data is not editable")
|
||||
}
|
||||
oldIP = net.ParseIP(data.Value)
|
||||
if ip.Equal(oldIP) { // success, nothing to change
|
||||
return ip, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if oldIP != nil { // Found editable record with a different IP address, so remove it
|
||||
if err := removeDreamhostRecord(client, d.key, d.domain, oldIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ip, addDreamhostRecord(client, d.key, d.domain, ip)
|
||||
}
|
||||
|
||||
type (
|
||||
dreamHostRecords struct {
|
||||
Result string `json:"result"`
|
||||
Data []struct {
|
||||
Editable string `json:"editable"`
|
||||
Type string `json:"type"`
|
||||
Record string `json:"record"`
|
||||
Value string `json:"value"`
|
||||
} `json:"data"`
|
||||
}
|
||||
dreamhostReponse struct {
|
||||
Result string `json:"result"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
func makeDreamhostDefaultValues(key string) (values url.Values) { //nolint:unparam
|
||||
values.Set("key", key)
|
||||
values.Set("unique_id", uuid.New().String())
|
||||
values.Set("format", "json")
|
||||
return values
|
||||
}
|
||||
|
||||
func listDreamhostRecords(client network.Client, key string) (records dreamHostRecords, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-list_records")
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return records, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return records, err
|
||||
} else if status != http.StatusOK {
|
||||
return records, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
if err := json.Unmarshal(content, &records); err != nil {
|
||||
return records, err
|
||||
} else if records.Result != success {
|
||||
return records, fmt.Errorf(records.Result)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func removeDreamhostRecord(client network.Client, key, domain string, ip net.IP) error { //nolint:dupl
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-remove_record")
|
||||
values.Set("record", domain)
|
||||
values.Set("type", recordType)
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != success { // this should not happen
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addDreamhostRecord(client network.Client, key, domain string, ip net.IP) error { //nolint:dupl
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-add_record")
|
||||
values.Set("record", domain)
|
||||
values.Set("type", recordType)
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != success {
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type duckdns struct {
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
token string
|
||||
useProviderIP bool
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDuckdns(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &duckdns{
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
token: extraSettings.Token,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *duckdns) isValid() error {
|
||||
if !d.matcher.DuckDNSToken(d.token) {
|
||||
return fmt.Errorf("invalid token format")
|
||||
}
|
||||
switch d.host {
|
||||
case "@", "*":
|
||||
return fmt.Errorf("host cannot be @ or * and must be a subdomain for DuckDNS")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *duckdns) String() string {
|
||||
return toString("duckdns..org", d.host, constants.DUCKDNS, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *duckdns) Domain() string {
|
||||
return "duckdns.org"
|
||||
}
|
||||
|
||||
func (d *duckdns) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *duckdns) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *duckdns) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *duckdns) BuildDomainName() string {
|
||||
return buildDomainName(d.host, "duckdns.org")
|
||||
}
|
||||
|
||||
func (d *duckdns) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://duckdns.org\">DuckDNS</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *duckdns) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.duckdns.org",
|
||||
Path: "/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("verbose", "true")
|
||||
values.Set("domains", d.host)
|
||||
values.Set("token", d.token)
|
||||
u.RawQuery = values.Encode()
|
||||
if !d.useProviderIP {
|
||||
if ip.To4() == nil {
|
||||
values.Set("ip6", ip.String())
|
||||
} else {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
s := string(content)
|
||||
switch {
|
||||
case len(s) < 2:
|
||||
return nil, fmt.Errorf("response %q is too short", s)
|
||||
case s[0:2] == "KO":
|
||||
return nil, fmt.Errorf("invalid domain token combination")
|
||||
case s[0:2] == "OK":
|
||||
ips := verification.NewVerifier().SearchIPv4(s)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
|
||||
}
|
||||
if ip != nil && !newIP.Equal(ip) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid response %q", s)
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type dyn struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDyn(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &dyn{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dyn) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(d.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case d.host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dyn) String() string {
|
||||
return fmt.Sprintf("[domain: %s | host: %s | provider: Dyn]", d.domain, d.host)
|
||||
}
|
||||
|
||||
func (d *dyn) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dyn) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dyn) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dyn) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *dyn) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dyn) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://dyn.com/\">Dyn DNS</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dyn) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
User: url.UserPassword(d.username, d.password),
|
||||
Host: "members.dyndns.org",
|
||||
Path: "/v3/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
switch d.host {
|
||||
case "@":
|
||||
values.Set("hostname", d.domain)
|
||||
default:
|
||||
values.Set("hostname", fmt.Sprintf("%s.%s", d.host, d.domain))
|
||||
}
|
||||
if !d.useProviderIP {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
s := string(content)
|
||||
switch {
|
||||
case strings.HasPrefix(s, "notfqdn"):
|
||||
return nil, fmt.Errorf("fully qualified domain name is not valid")
|
||||
case strings.HasPrefix(s, "badrequest"):
|
||||
return nil, fmt.Errorf("bad request")
|
||||
case strings.HasPrefix(s, "good"):
|
||||
return ip, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown response: %s", s)
|
||||
}
|
||||
}
|
||||
14
internal/settings/errors/intermediary.go
Normal file
14
internal/settings/errors/intermediary.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrCreateRecord = errors.New("cannot create record")
|
||||
ErrGetDomainID = errors.New("cannot get domain ID")
|
||||
ErrGetRecordID = errors.New("cannot get record ID")
|
||||
ErrGetRecordInZone = errors.New("cannot get record in zone") // LuaDNS
|
||||
ErrGetZoneID = errors.New("cannot get zone ID") // LuaDNS
|
||||
ErrListRecords = errors.New("cannot list records") // Dreamhost
|
||||
ErrRemoveRecord = errors.New("cannot remove record") // Dreamhost
|
||||
ErrUpdateRecord = errors.New("cannot update record")
|
||||
)
|
||||
34
internal/settings/errors/update.go
Normal file
34
internal/settings/errors/update.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrAbuse = errors.New("banned due to abuse")
|
||||
ErrAccountInactive = errors.New("account is inactive")
|
||||
ErrAuth = errors.New("bad authentication")
|
||||
ErrBadHTTPStatus = errors.New("bad HTTP status")
|
||||
ErrBadRequest = errors.New("bad request sent")
|
||||
ErrBannedUserAgent = errors.New("user agend is banned")
|
||||
ErrConflictingRecord = errors.New("conflicting record")
|
||||
ErrDNSServerSide = errors.New("server side DNS error")
|
||||
ErrDomainDisabled = errors.New("record disabled")
|
||||
ErrDomainIDNotFound = errors.New("ID not found in domain record")
|
||||
ErrFeatureUnavailable = errors.New("feature is not available to the user")
|
||||
ErrHostnameNotExists = errors.New("hostname does not exist")
|
||||
ErrInvalidSystemParam = errors.New("invalid system parameter")
|
||||
ErrIPReceivedMalformed = errors.New("malformed IP address received")
|
||||
ErrIPReceivedMismatch = errors.New("mismatching IP address received")
|
||||
ErrMalformedIPSent = errors.New("malformed IP address sent")
|
||||
ErrNoResultReceived = errors.New("no result received")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrNumberOfResultsReceived = errors.New("wrong number of results received")
|
||||
ErrPrivateIPSent = errors.New("private IP cannot be routed")
|
||||
ErrRecordNotEditable = errors.New("record is not editable") // Dreamhost
|
||||
ErrRecordNotFound = errors.New("record not found")
|
||||
ErrRequestEncode = errors.New("cannot encode request")
|
||||
ErrRequestMarshal = errors.New("cannot marshal request body")
|
||||
ErrUnknownResponse = errors.New("unknown response received")
|
||||
ErrUnmarshalResponse = errors.New("cannot unmarshal update response")
|
||||
ErrUnsuccessfulResponse = errors.New("unsuccessful response")
|
||||
ErrZoneNotFound = errors.New("zone not found") // LuaDNS
|
||||
)
|
||||
27
internal/settings/errors/validation.go
Normal file
27
internal/settings/errors/validation.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package errors
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrEmptyAppKey = errors.New("empty app key")
|
||||
ErrEmptyConsumerKey = errors.New("empty consumer key")
|
||||
ErrEmptyEmail = errors.New("empty email")
|
||||
ErrEmptyKey = errors.New("empty key")
|
||||
ErrEmptyName = errors.New("empty name")
|
||||
ErrEmptyPassword = errors.New("empty password")
|
||||
ErrEmptySecret = errors.New("empty secret")
|
||||
ErrEmptyToken = errors.New("empty token")
|
||||
ErrEmptyTTL = errors.New("TTL is not set")
|
||||
ErrEmptyUsername = errors.New("empty username")
|
||||
ErrEmptyZoneIdentifier = errors.New("empty zone identifier")
|
||||
ErrHostOnlyAt = errors.New(`host can only be "@"`)
|
||||
ErrHostOnlySubdomain = errors.New("host can only be a subdomain")
|
||||
ErrHostWildcard = errors.New(`host cannot be a "*"`)
|
||||
ErrIPv6NotSupported = errors.New("IPv6 is not supported by this provider")
|
||||
ErrMalformedEmail = errors.New("malformed email address")
|
||||
ErrMalformedKey = errors.New("malformed key")
|
||||
ErrMalformedPassword = errors.New("malformed password")
|
||||
ErrMalformedToken = errors.New("malformed token")
|
||||
ErrMalformedUsername = errors.New("malformed username")
|
||||
ErrMalformedUserServiceKey = errors.New("malformed user service key")
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user