mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-22 17:12:14 -04:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe00994522 | ||
|
|
d22dc41903 | ||
|
|
76b1f5f0df | ||
|
|
c5de1358dd | ||
|
|
4133dbfdc7 | ||
|
|
c4f50992c2 | ||
|
|
f8b67e5261 | ||
|
|
1b154df8aa | ||
|
|
f9edf75540 | ||
|
|
ae748fb80d | ||
|
|
edb6c491bf | ||
|
|
2f196b4886 | ||
|
|
2d7d016650 | ||
|
|
1da19b3d6d | ||
|
|
0c520eddda | ||
|
|
050825d399 | ||
|
|
53212df518 | ||
|
|
6fe4743bc0 | ||
|
|
4cdc71b45c | ||
|
|
36a6275bd2 | ||
|
|
ee66170a87 | ||
|
|
bb62a9d9e1 | ||
|
|
e04c1f83df | ||
|
|
e19cabc894 | ||
|
|
682821efd0 | ||
|
|
276c1c02fd | ||
|
|
9b8ec3298b | ||
|
|
cc670b3939 | ||
|
|
0c3d258620 | ||
|
|
d39ecd6fd3 | ||
|
|
43dd02dbd5 | ||
|
|
381af0cd90 | ||
|
|
2bede070de | ||
|
|
09b810732f | ||
|
|
398566850d | ||
|
|
201df818d3 | ||
|
|
b1a3740059 | ||
|
|
ae978e007b | ||
|
|
3688208030 | ||
|
|
da4341fbba | ||
|
|
1705afeada | ||
|
|
844904aa7b | ||
|
|
803232e670 | ||
|
|
2f7ee832b8 | ||
|
|
943d1486b3 | ||
|
|
d727feefc4 | ||
|
|
aee3022a11 | ||
|
|
864a696680 | ||
|
|
34623e1b81 | ||
|
|
c0e57c6e1d | ||
|
|
1ff629dc17 | ||
|
|
23f4897365 | ||
|
|
e3472476e2 | ||
|
|
869cf52c7d | ||
|
|
8b2e83a69e | ||
|
|
106bcae966 | ||
|
|
0a89666d1d | ||
|
|
3ad9168576 | ||
|
|
1eaa495e66 | ||
|
|
5e0cc687ea | ||
|
|
289536b145 | ||
|
|
d5e4936679 | ||
|
|
6e18e921b7 | ||
|
|
40c92eebf5 | ||
|
|
8adc0556ba | ||
|
|
e7824014ee | ||
|
|
6e0d48f7c1 | ||
|
|
a10fb64ffd | ||
|
|
a649f8a4a8 | ||
|
|
72018451b3 | ||
|
|
2e5b3c7924 | ||
|
|
c985595969 | ||
|
|
caa4840a61 | ||
|
|
a86ddd42d1 | ||
|
|
ce1a447e0a | ||
|
|
22c8b587c9 | ||
|
|
78c86b0e24 | ||
|
|
fa771cd4b2 | ||
|
|
4e94823f69 | ||
|
|
96521addd5 | ||
|
|
85ddad6da1 | ||
|
|
d54c334e1c | ||
|
|
49392003e0 | ||
|
|
5a5e0c7375 | ||
|
|
180092f47e | ||
|
|
43e168581c | ||
|
|
76a40e47c7 | ||
|
|
50b1997dbb | ||
|
|
d75398d71b | ||
|
|
7191e93932 | ||
|
|
ec1f7acbde | ||
|
|
a8a8d5793b | ||
|
|
dde28ebd1f | ||
|
|
cd5f04acaf | ||
|
|
5d1809aca6 | ||
|
|
46ae9af6a4 | ||
|
|
b9a357fc1c | ||
|
|
1e7c909baf | ||
|
|
e83857b3db | ||
|
|
5ab1607f97 | ||
|
|
4bb09e86dd | ||
|
|
9460fa969b | ||
|
|
157e041b28 | ||
|
|
b40391bb4e | ||
|
|
60cf3130e3 | ||
|
|
5557c32135 | ||
|
|
46e8647d35 | ||
|
|
eb1f925576 | ||
|
|
18b058f188 | ||
|
|
be89e798d2 | ||
|
|
20704eea8c | ||
|
|
6416c6fed3 | ||
|
|
a317809ff8 | ||
|
|
7ae9f72038 | ||
|
|
5b1bc29ad4 | ||
|
|
e937ee741c | ||
|
|
688aebbc4f | ||
|
|
6c2c2cf7cb | ||
|
|
1c3e4cdef7 | ||
|
|
d8a7fef6bd | ||
|
|
87f59bf498 | ||
|
|
818f7471dd | ||
|
|
b35658f32c | ||
|
|
48fb1a4b95 | ||
|
|
2e069ccf1d | ||
|
|
0d38e9385f | ||
|
|
b4310ad822 | ||
|
|
878cf4cc45 | ||
|
|
89faafe377 | ||
|
|
bf3f78f9f9 | ||
|
|
6bf82d7be1 | ||
|
|
77f1681c4c | ||
|
|
82e3d60db5 | ||
|
|
c65c8d63bd | ||
|
|
0f1ddfb9b0 | ||
|
|
1d466cdc83 | ||
|
|
0a6ef7ffbf | ||
|
|
e7ae5ac4cc | ||
|
|
701ae125bf | ||
|
|
b775798b65 | ||
|
|
166b0c7095 | ||
|
|
3240bb7d26 | ||
|
|
3047c83ee9 | ||
|
|
3b29a33849 | ||
|
|
860bc02e2e | ||
|
|
cd2d3c46cc | ||
|
|
e630dd9889 | ||
|
|
b2d96787b8 | ||
|
|
5b52255601 | ||
|
|
04c55028a1 | ||
|
|
e07e8da31c | ||
|
|
af2f3a3257 | ||
|
|
00efca4af4 | ||
|
|
3272612db2 | ||
|
|
5b7968c468 | ||
|
|
7ec39c1256 | ||
|
|
96857f3bae | ||
|
|
57c7d1be2d | ||
|
|
53b6f533a8 | ||
|
|
a82ed93169 | ||
|
|
d07fcc664b | ||
|
|
d013ceb869 | ||
|
|
d3506e9792 | ||
|
|
c0249672bf | ||
|
|
cfeb95872a | ||
|
|
091cf5f855 | ||
|
|
7001add533 | ||
|
|
9ccdbbd2d3 | ||
|
|
f8a3ab63c6 | ||
|
|
14033223d9 | ||
|
|
18161a6064 | ||
|
|
216b8ab1ae | ||
|
|
4af23a756b | ||
|
|
96c84a5a4f | ||
|
|
4564d16c06 | ||
|
|
6e4a56b3cf | ||
|
|
919ab65985 | ||
|
|
e023aae909 | ||
|
|
066bcdd3bf | ||
|
|
0a6c6b9bc7 | ||
|
|
8cdff8e4d3 | ||
|
|
bffc30264f | ||
|
|
4f141c20a0 | ||
|
|
582ce626c8 | ||
|
|
13b29aeba4 | ||
|
|
a5afca15d1 | ||
|
|
25ee692242 | ||
|
|
922146efd3 | ||
|
|
db9959cf59 | ||
|
|
50303aef7b | ||
|
|
137e372102 | ||
|
|
f300c59411 | ||
|
|
c23998bd09 |
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,117 +1,79 @@
|
||||
{
|
||||
"name": "ddns-dev",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml"
|
||||
],
|
||||
"service": "vscode",
|
||||
"runServices": [
|
||||
"vscode"
|
||||
],
|
||||
"shutdownAction": "stopCompose",
|
||||
"postCreateCommand": "go mod download",
|
||||
"workspaceFolder": "/workspace",
|
||||
"appPort": 8000,
|
||||
"extensions": [
|
||||
"ms-vscode.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",
|
||||
"stkb.rewrap",
|
||||
"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
|
||||
|
||||
43
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Report a bug
|
||||
title: 'Bug: ...'
|
||||
labels: ":bug: bug"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
YOU CAN CHAT THERE EVENTUALLY:
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
<!-- See the line at the top of your logs -->
|
||||
|
||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
||||
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature to add to this project
|
||||
title: 'Feature request: ...'
|
||||
labels: ":bulb: feature request"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
1. What's the feature?
|
||||
|
||||
2. Extra information?
|
||||
|
||||
<!--
|
||||
|
||||
YOU CAN CHAT THERE EVENTUALLY:
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
43
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Help
|
||||
about: Ask for help
|
||||
title: 'Help: ...'
|
||||
labels: ":pray: help wanted"
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
HAVE A CHAT FIRST!
|
||||
|
||||
https://github.com/qdm12/ddns-updater/discussions
|
||||
|
||||
-->
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent: Yes/No
|
||||
2. DNS provider(s) you use: Answer here
|
||||
3. Program version:
|
||||
|
||||
<!-- See the line at the top of your logs -->
|
||||
|
||||
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
|
||||
|
||||
4. What are you using to run the container: docker-compose
|
||||
5. Extra information (optional)
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
103
.github/workflows/build.yml
vendored
103
.github/workflows/build.yml
vendored
@@ -1,12 +1,101 @@
|
||||
name: Docker build
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths:
|
||||
- .github/workflows/build.yml
|
||||
- cmd/**
|
||||
- internal/**
|
||||
- pkg/**
|
||||
- .dockerignore
|
||||
- .golangci.yml
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
|
||||
jobs:
|
||||
build:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Linting
|
||||
run: docker build --target lint .
|
||||
|
||||
- name: Go mod tidy check
|
||||
run: docker build --target tidy .
|
||||
|
||||
- name: Build test image
|
||||
run: docker build --target test -t test-container .
|
||||
|
||||
- name: Run tests in test container
|
||||
run: |
|
||||
touch coverage.txt
|
||||
docker run --rm \
|
||||
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
|
||||
test-container
|
||||
|
||||
# We run this here to use the caching of the previous steps
|
||||
- name: Build final image
|
||||
run: docker build .
|
||||
|
||||
publish:
|
||||
needs: [verify]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build image
|
||||
run: docker build .
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Set variables
|
||||
id: vars
|
||||
run: |
|
||||
BRANCH=${GITHUB_REF#refs/heads/}
|
||||
TAG=${GITHUB_REF#refs/tags/}
|
||||
echo ::set-output name=commit::$(git rev-parse --short HEAD)
|
||||
echo ::set-output name=build_date::$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
if [ "$TAG" != "$GITHUB_REF" ]; then
|
||||
echo ::set-output name=version::$TAG
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
elif [ "$BRANCH" = "master" ]; then
|
||||
echo ::set-output name=version::latest
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
|
||||
else
|
||||
echo ::set-output name=version::$BRANCH
|
||||
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7
|
||||
fi
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: ${{ steps.vars.outputs.platforms }}
|
||||
build-args: |
|
||||
BUILD_DATE=${{ steps.vars.outputs.build_date }}
|
||||
COMMIT=${{ steps.vars.outputs.commit }}
|
||||
VERSION=${{ steps.vars.outputs.version }}
|
||||
tags: qmcgaw/ddns-updater:${{ steps.vars.outputs.version }}
|
||||
push: true
|
||||
|
||||
- if: github.event.ref == 'refs/heads/master'
|
||||
name: Microbadger hook
|
||||
run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s=
|
||||
continue-on-error: true
|
||||
|
||||
40
.github/workflows/buildx-latest.yml
vendored
40
.github/workflows/buildx-latest.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Buildx latest
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg VERSION=latest \
|
||||
-t qmcgaw/ddns-updater:latest \
|
||||
--push \
|
||||
.
|
||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0
|
||||
40
.github/workflows/buildx-release.yml
vendored
40
.github/workflows/buildx-release.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: Buildx release
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-latest.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--progress plain \
|
||||
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
|
||||
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
|
||||
--build-arg VCS_REF=`git rev-parse --short HEAD` \
|
||||
--build-arg VERSION=${GITHUB_REF##*/} \
|
||||
-t qmcgaw/ddns-updater:${GITHUB_REF##*/} \
|
||||
--push \
|
||||
.
|
||||
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0
|
||||
11
.github/workflows/greetings.yml
vendored
11
.github/workflows/greetings.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Greetings
|
||||
on: [pull_request, issues]
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Thanks for creating your first issue :+1: Feel free to use [Slack](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc) if you just need some quick help or want to chat'
|
||||
pr-message: 'Thank you so much for contributing, that means a lot to me :wink:'
|
||||
6
.github/workflows/labels.yml
vendored
6
.github/workflows/labels.yml
vendored
@@ -1,10 +1,10 @@
|
||||
name: labels
|
||||
on:
|
||||
push:
|
||||
branches: ["master"]
|
||||
branches: [master]
|
||||
paths:
|
||||
- '.github/labels.yml'
|
||||
- '.github/workflows/labels.yml'
|
||||
- .github/labels.yml
|
||||
- .github/workflows/labels.yml
|
||||
jobs:
|
||||
labeler:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
16
.github/workflows/misspell.yml
vendored
16
.github/workflows/misspell.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: Misspells
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
jobs:
|
||||
misspell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: reviewdog/action-misspell@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
locale: "US"
|
||||
level: error
|
||||
59
.github/workflows/security.yml
vendored
59
.github/workflows/security.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Security scan of Docker image
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
schedule:
|
||||
- cron: '0 9 * * *'
|
||||
jobs:
|
||||
security-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Check for scratch
|
||||
id: scratchCheck
|
||||
run: echo ::set-output name=scratch::$(cat Dockerfile | grep 'FROM scratch')
|
||||
- name: Build image
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
run: docker build -t image .
|
||||
- name: Phonito
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: phonito/phonito-scanner-action@master
|
||||
with:
|
||||
image: image
|
||||
fail-level: LOW
|
||||
phonito-token: ${{ secrets.PHONITO_TOKEN }}
|
||||
- name: Trivy
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: homoluctus/gitrivy@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
image: image
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
*.exe
|
||||
updater
|
||||
.vscode
|
||||
|
||||
@@ -4,46 +4,71 @@ 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:
|
||||
skip-dirs:
|
||||
- .devcontainer
|
||||
- .github
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.26.x # use the fixed version to not introduce new linters unexpectedly
|
||||
|
||||
88
.vscode/settings.json
vendored
88
.vscode/settings.json
vendored
@@ -1,88 +0,0 @@
|
||||
{
|
||||
// General settings
|
||||
"files.eol": "\n",
|
||||
// Docker
|
||||
"remote.extensionKind": {
|
||||
"ms-azuretools.vscode-docker": "workspace"
|
||||
},
|
||||
// Golang general settings
|
||||
"go.useLanguageServer": true,
|
||||
"go.autocompleteUnimportedPackages": true,
|
||||
"go.gotoSymbol.includeImports": true,
|
||||
"go.gotoSymbol.includeGoroot": true,
|
||||
"gopls": {
|
||||
"completeUnimported": true,
|
||||
"deepCompletion": true,
|
||||
"usePlaceholders": false
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": [
|
||||
"--fast",
|
||||
"--enable",
|
||||
"rowserrcheck",
|
||||
"--enable",
|
||||
"bodyclose",
|
||||
"--enable",
|
||||
"dogsled",
|
||||
"--enable",
|
||||
"dupl",
|
||||
"--enable",
|
||||
"gochecknoglobals",
|
||||
"--enable",
|
||||
"gochecknoinits",
|
||||
"--enable",
|
||||
"gocognit",
|
||||
"--enable",
|
||||
"goconst",
|
||||
"--enable",
|
||||
"gocritic",
|
||||
"--enable",
|
||||
"gocyclo",
|
||||
"--enable",
|
||||
"goimports",
|
||||
"--enable",
|
||||
"golint",
|
||||
"--enable",
|
||||
"gosec",
|
||||
"--enable",
|
||||
"interfacer",
|
||||
"--enable",
|
||||
"maligned",
|
||||
"--enable",
|
||||
"misspell",
|
||||
"--enable",
|
||||
"nakedret",
|
||||
"--enable",
|
||||
"prealloc",
|
||||
"--enable",
|
||||
"scopelint",
|
||||
"--enable",
|
||||
"unconvert",
|
||||
"--enable",
|
||||
"unparam",
|
||||
"--enable",
|
||||
"whitespace"
|
||||
],
|
||||
// Golang on save
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true
|
||||
}
|
||||
},
|
||||
// Golang testing
|
||||
"go.toolsEnvVars": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"gopls.env": {
|
||||
"GOFLAGS": "-tags="
|
||||
},
|
||||
"go.testEnvVars": {},
|
||||
"go.testFlags": [
|
||||
"-v"
|
||||
],
|
||||
"go.testTimeout": "600s"
|
||||
}
|
||||
133
Dockerfile
133
Dockerfile
@@ -1,54 +1,109 @@
|
||||
ARG ALPINE_VERSION=3.11
|
||||
ARG GO_VERSION=1.14
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
ARG ALPINE_VERSION=3.13
|
||||
ARG GO_VERSION=1.16
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.41.1
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates tzdata
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
ARG GOLANGCI_LINT_VERSION=v1.26.0
|
||||
RUN apk --update add git
|
||||
ENV CGO_ENABLED=0
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
|
||||
FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates
|
||||
RUN mkdir /tmp/data && \
|
||||
chown 1000 /tmp/data && \
|
||||
chmod 700 /tmp/data
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY .golangci.yml .
|
||||
ENV CGO_ENABLED=0
|
||||
RUN apk --update add git g++
|
||||
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
|
||||
COPY --from=golangci-lint /bin /go/bin/golangci-lint
|
||||
# Copy repository code and install Go dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download 2>&1
|
||||
RUN go mod download
|
||||
COPY pkg/ ./pkg/
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
COPY cmd/updater/main.go .
|
||||
RUN go test ./...
|
||||
RUN go build -ldflags="-s -w" -o app
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS test
|
||||
# Note on the go race detector:
|
||||
# - we set CGO_ENABLED=1 to have it enabled
|
||||
# - we installed g++ to support the race detector
|
||||
ENV CGO_ENABLED=1
|
||||
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS lint
|
||||
COPY .golangci.yml ./
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS 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
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
ARG TARGETPLATFORM
|
||||
RUN GOARCH="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field arch)" \
|
||||
GOARM="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field arm)" \
|
||||
go build -trimpath -ldflags="-s -w \
|
||||
-X 'main.version=$VERSION' \
|
||||
-X 'main.buildDate=$BUILD_DATE' \
|
||||
-X 'main.commit=$COMMIT' \
|
||||
" -o app cmd/updater/main.go
|
||||
|
||||
FROM scratch
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.revision=$VCS_REF \
|
||||
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.title="ddns-updater" \
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Namecheap, Cloudflare, GoDaddy, DuckDns, Dreamhost, DNSPod and NoIP"
|
||||
COPY --from=alpine --chown=1000 /tmp/data /updater/data/
|
||||
COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
|
||||
USER 1000
|
||||
ENTRYPOINT ["/updater/app"]
|
||||
ENV DELAY=10m \
|
||||
ROOT_URL=/ \
|
||||
LISTENING_PORT=8000 \
|
||||
LOG_ENCODING=console \
|
||||
LOG_LEVEL=info \
|
||||
NODE_ID=0 \
|
||||
ENV \
|
||||
# Core
|
||||
CONFIG= \
|
||||
PERIOD=5m \
|
||||
UPDATE_COOLDOWN_PERIOD=5m \
|
||||
PUBLICIP_FETCHERS=all \
|
||||
PUBLICIP_HTTP_PROVIDERS=all \
|
||||
PUBLICIPV4_HTTP_PROVIDERS=all \
|
||||
PUBLICIPV6_HTTP_PROVIDERS=all \
|
||||
PUBLICIP_DNS_PROVIDERS=all \
|
||||
PUBLICIP_DNS_TIMEOUT=3s \
|
||||
HTTP_TIMEOUT=10s \
|
||||
GOTIFY_URL= \
|
||||
GOTIFY_TOKEN= \
|
||||
DATADIR=/updater/data \
|
||||
|
||||
# Web UI
|
||||
LISTENING_PORT=8000 \
|
||||
ROOT_URL=/ \
|
||||
|
||||
# Backup
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data
|
||||
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
|
||||
COPY --chown=1000 ui/* /updater/ui/
|
||||
BACKUP_DIRECTORY=/updater/data \
|
||||
|
||||
# Other
|
||||
LOG_LEVEL=info \
|
||||
LOG_CALLER=hidden \
|
||||
SHOUTRRR_ADDRESSES= \
|
||||
TZ=
|
||||
ARG VERSION=unknown
|
||||
ARG BUILD_DATE="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
LABEL \
|
||||
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
|
||||
org.opencontainers.image.version=$VERSION \
|
||||
org.opencontainers.image.created=$BUILD_DATE \
|
||||
org.opencontainers.image.revision=$COMMIT \
|
||||
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.title="ddns-updater" \
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI"
|
||||
COPY --from=build --chown=1000 /tmp/gobuild/app /updater/app
|
||||
|
||||
442
README.md
442
README.md
@@ -1,8 +1,8 @@
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
|
||||
*Light container updating DNS A records periodically for GoDaddy, Namecheap, Cloudflare, Dreamhost, NoIP, DNSPod, Infomaniak, ddnss.de and DuckDNS*
|
||||
*Light container updating DNS A and/or AAAA records periodically for multiple DNS providers*
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg?sanitize=true">
|
||||
|
||||
[](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
@@ -17,21 +17,50 @@
|
||||
|
||||
## Features
|
||||
|
||||
- Updates periodically A records for different DNS providers: Namecheap, GoDaddy, Cloudflare, NoIP, Dreamhost, DuckDNS, DNSPod and Infomaniak (ask for more)
|
||||
- Updates periodically A records for different DNS providers:
|
||||
- Cloudflare
|
||||
- DD24
|
||||
- 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
|
||||
- Strato.de
|
||||
- Variomedia.de
|
||||
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
|
||||
- Web User interface
|
||||
|
||||

|
||||
|
||||
- 12.3MB Docker image based on a Go static binary in a Scratch Docker image with ca-certificates and timezone data
|
||||
- 14MB Docker image based on a Go static binary in a Scratch Docker image with ca-certificates and timezone data
|
||||
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
|
||||
- Docker healthcheck verifying the DNS resolution of your domains
|
||||
- Highly configurable
|
||||
- Sends notifications to your Android phone, see the [**Gotify**](#Gotify) section (it's free, open source and self hosted 🆒)
|
||||
- Compatible with `amd64`, `386`, `arm64`, `arm32v7` (Raspberry Pis) CPU architectures.
|
||||
- Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/services/overview/) using `SHOUTRRR_ADDRESSES`
|
||||
- Compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures.
|
||||
|
||||
## Setup
|
||||
|
||||
1. To setup your domains initially, see the [Domain set up](#domain-set-up) section.
|
||||
The program reads the configuration from a JSON object, either from a file or from an environment variable.
|
||||
|
||||
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
|
||||
|
||||
```sh
|
||||
@@ -47,7 +76,7 @@
|
||||
|
||||
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
|
||||
|
||||
1. Modify the *data/config.json* file similarly to:
|
||||
1. Write a JSON configuration in *data/config.json*, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -56,335 +85,212 @@
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "duckduckgo",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See more information in the [configuration section](#configuration)
|
||||
You can find more information in the [configuration section](#configuration) to customize it.
|
||||
|
||||
1. Use the following command:
|
||||
1. Run the container with
|
||||
|
||||
```bash
|
||||
```sh
|
||||
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
1. ⚠️ 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.
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
### Next steps
|
||||
|
||||
1. You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
|
||||
## Configuration
|
||||
|
||||
Start by having the following content in *config.json*:
|
||||
Start by having the following content in *config.json*, or in your `CONFIG` environment variable:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
},
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following parameters are to be added in *config.json*
|
||||
For each setting, you need to fill in parameters.
|
||||
Check the documentation for your DNS provider:
|
||||
|
||||
For all record update configuration, you need the following:
|
||||
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
|
||||
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
|
||||
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
|
||||
- [DD24](https://github.com/qdm12/ddns-updater/blob/master/docs/domaindiscount24.md)
|
||||
- [DonDominio](https://github.com/qdm12/ddns-updater/blob/master/docs/dondominio.md)
|
||||
- [DNSOMatic](https://github.com/qdm12/ddns-updater/blob/master/docs/dnsomatic.md)
|
||||
- [DNSPod](https://github.com/qdm12/ddns-updater/blob/master/docs/dnspod.md)
|
||||
- [Dreamhost](https://github.com/qdm12/ddns-updater/blob/master/docs/dreamhost.md)
|
||||
- [DuckDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/duckdns.md)
|
||||
- [DynDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/dyndns.md)
|
||||
- [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)
|
||||
|
||||
- `"provider"` is the DNS provider and can be `"godaddy"`, `"namecheap"`, `"duckdns"`, `"dreamhost"`, `"cloudflare"`, `"noip"`, `"dnspod"` or `"ddnss"`
|
||||
- `"domain"`
|
||||
- `"ip_method"` is the method to obtain your public IP address and can be:
|
||||
- `"provider"` means the public IP is automatically determined by the DNS provider (**only for DuckDNs, Namecheap, Infomaniak and NoIP**), most reliable.
|
||||
- `"opendns"` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip) (reliable)
|
||||
- `"ifconfig"` using [https://ifconfig.io/ip](https://ifconfig.io/ip) (may be rate limited)
|
||||
- `"ipinfo"` using [https://ipinfo.io/ip](https://ipinfo.io/ip) (may be rate limited)
|
||||
- `"ipify"` using [https://api.ipify.org](https://api.ipify.org) (may be rate limited)
|
||||
- `"ipify6"` using [https://api6.ipify.org](https://api.ipify.org) for IPv6 only (may be rate limited)
|
||||
- `"ddnss"` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `"ddnss4"` using [https://ip4.ddnss.de/meineip.php](https://ip4.ddnss.de/meineip.php) for IPv4 only
|
||||
- `"ddnss6"` using [https://ip6.ddnss.de/meineip.php](https://ip6.ddnss.de/meineip.php) for IPv6 only
|
||||
- `"cycle"` to cycle between each external methods, in order to avoid being rate limited
|
||||
- You can also specify an HTTPS URL to obtain your public IP address (i.e. `"ip_method": "https://ipinfo.io/ip"`)
|
||||
Note that:
|
||||
|
||||
You can optionnally add the parameters:
|
||||
|
||||
- `"delay"` is the delay in seconds between each update. It defaults to the `DELAY` environment variable value.
|
||||
- `"no_dns_lookup"` can be `true` or `false` and allows, if `true`, to prevent the periodic Docker healthcheck from running a DNS lookup on your domain.
|
||||
|
||||
For each DNS provider exist some specific parameters you need to add, as described below:
|
||||
|
||||
Namecheap:
|
||||
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"password"`
|
||||
|
||||
Cloudflare:
|
||||
|
||||
- `"zone_identifier"` is the Zone ID of your site
|
||||
- `"identifier"` is the DNS record identifier as returned by the Cloudflare "List DNS Records" API (see below)
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following:
|
||||
- Email `"email"` and Global API Key `"key"`
|
||||
- User service key `"user_service_key"`
|
||||
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone.
|
||||
- *Optionally*, `"proxied"` can be `true` or `false` to use the proxy services of Cloudflare
|
||||
|
||||
GoDaddy:
|
||||
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"key"`
|
||||
- `"secret"`
|
||||
|
||||
DuckDNS:
|
||||
|
||||
- `"token"`
|
||||
|
||||
Dreamhost:
|
||||
|
||||
- `"key"`
|
||||
|
||||
NoIP:
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
DNSPOD:
|
||||
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"token"`
|
||||
|
||||
Infomaniak:
|
||||
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
|
||||
DDNSS.de:
|
||||
|
||||
- `"user"`
|
||||
- `"password"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Environment variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `DELAY` | `10m` | Default delay between updates, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
|
||||
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
|
||||
| `LOG_ENCODING` | `console` | Format of logging, `json` or `console` |
|
||||
| `LOG_LEVEL` | `info` | Level of logging, `info`, `warning` or `error` |
|
||||
| `NODE_ID` | `0` | Node ID (for distributed systems), can be any integer |
|
||||
| `CONFIG` | | One line JSON object containing the entire config (takes precendence over config.json file) if specified |
|
||||
| `PERIOD` | `5m` | Default period of IP address check, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `IPV6_PREFIX` | `/128` | IPv6 prefix used to mask your public IPv6 address and your record IPv6 address. Ranges from `/0` to `/128` depending on your ISP. |
|
||||
| `PUBLICIP_FETCHERS` | `all` | Comma separated fetcher types to obtain the public IP address from `http` and `dns` |
|
||||
| `PUBLICIP_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (ipv4 or ipv6). See the [Public IP section](#Public-IP) |
|
||||
| `PUBLICIPV4_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv4 address only. See the [Public IP section](#Public-IP) |
|
||||
| `PUBLICIPV6_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv6 address only. See the [Public IP section](#Public-IP) |
|
||||
| `PUBLICIP_DNS_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (IPv4 and/or IPv6). See the [Public IP section](#Public-IP) |
|
||||
| `PUBLICIP_DNS_TIMEOUT` | `3s` | Public IP DNS query timeout |
|
||||
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
|
||||
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
|
||||
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
|
||||
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
|
||||
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
|
||||
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
|
||||
| `HEALTH_SERVER_ADDRESS` | `127.0.0.1:9999` | Health server listening address |
|
||||
| `DATADIR` | `/updater/data` | Directory to read and write data files from internally |
|
||||
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
|
||||
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`.
|
||||
| `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` |
|
||||
| `SHOUTRRR_ADDRESSES` | | (optional) Comma separated list of [Shoutrrr addresses](https://containrrr.dev/shoutrrr/services/overview/) (notification services) |
|
||||
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
|
||||
|
||||
#### Public IP
|
||||
|
||||
By default, all public IP fetching types are used and cycled (over DNS and over HTTPs).
|
||||
|
||||
On top of that, for each fetching method, all echo services available are cycled on each request.
|
||||
|
||||
This allows you not to be blocked for making too many requests.
|
||||
|
||||
You can otherwise customize it with the following:
|
||||
|
||||
- `PUBLICIP_HTTP_PROVIDERS` gets your public IPv4 or IPv6 address. It can be one or more of the following:
|
||||
- `opendns` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip)
|
||||
- `ifconfig` using [https://ifconfig.io/ip](https://ifconfig.io/ip)
|
||||
- `ipinfo` using [https://ipinfo.io/ip](https://ipinfo.io/ip)
|
||||
- `ddnss` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIPV4_HTTP_PROVIDERS` gets your public IPv4 address only. It can be one or more of the following:
|
||||
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIPV6_HTTP_PROVIDERS` gets your public IPv6 address only. It can be one or more of the following:
|
||||
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
|
||||
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
|
||||
- `PUBLICIP_DNS_PROVIDERS` gets your public IPv4 address only or IPv6 address only or one of them (see #136). It can be one or more of the following:
|
||||
- `google`
|
||||
- `cloudflare`
|
||||
|
||||
### Host firewall
|
||||
|
||||
If you have a host firewall in place, this container needs the following ports:
|
||||
|
||||
- TCP 443 outbound for outbound HTTPS
|
||||
- TCP 80 outbound if you use a local unsecured HTTP connection to your Gotify server
|
||||
- UDP 53 outbound for outbound DNS resolution
|
||||
- TCP 8000 inbound (or other) for the WebUI
|
||||
|
||||
## Domain set up
|
||||
## Architecture
|
||||
|
||||
### Namecheap
|
||||
At program start and every period (5 minutes by default):
|
||||
|
||||
[](https://www.namecheap.com)
|
||||
1. Fetch your public IP address
|
||||
1. For each record:
|
||||
1. DNS resolve it to obtain its current IP address(es)
|
||||
- If the resolution fails, update the record with your public IP address by calling the DNS provider API and finish
|
||||
1. Check if your public IP address is within the resolved IP addresses
|
||||
- Yes: skip the update
|
||||
- No: update the record with your public IP address by calling the DNS provider API
|
||||
|
||||
1. Create a Namecheap account and buy a domain name - *example.com* as an example
|
||||
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
|
||||
💡 We do DNS resolution every period so it detects a change made to the record manually, for example on the DNS provider web UI
|
||||
💡 As DNS resolutions are essentially free and without rate limiting, these are great to avoid getting banned for too many requests.
|
||||
|
||||
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
|
||||
### Special case: Cloudflare
|
||||
|
||||
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
|
||||
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
|
||||
For Cloudflare records with the `proxied` option, the following is done.
|
||||
|
||||

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

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

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

|
||||
|
||||
***
|
||||
|
||||
### GoDaddy
|
||||
|
||||
[](https://godaddy.com)
|
||||
|
||||
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a Test key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a **Production** key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
Obtain the **key** and **secret** of that production key.
|
||||
|
||||
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
|
||||
|
||||
***
|
||||
|
||||
### DuckDNS
|
||||
|
||||
[](https://duckdns.org)
|
||||
|
||||
*See [duckdns website](https://duckdns.org)*
|
||||
|
||||
### Cloudflare
|
||||
|
||||
1. Make sure you have `curl` installed
|
||||
1. Obtain your API key from Cloudflare website ([see this](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-))
|
||||
1. Obtain your zone identifier for your domain name, from the domain's overview page written as *Zone ID*
|
||||
1. Find your **identifier** in the `id` field with
|
||||
|
||||
```sh
|
||||
ZONEID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
EMAIL=example@example.com
|
||||
APIKEY=aaaaaaaaaaaaaaaaaa
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONEID/dns_records" \
|
||||
-H "X-Auth-Email: $EMAIL" \
|
||||
-H "X-Auth-Key: $APIKEY"
|
||||
```
|
||||
|
||||
You can now fill in the necessary parameters in *config.json*
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
|
||||
## Gotify
|
||||
|
||||
[](https://gotify.net)
|
||||
|
||||
[**Gotify**](https://gotify.net) is a simple server for sending and receiving messages, and it is **free**, **private** and **open source**
|
||||
|
||||
- It has an [Android app](https://play.google.com/store/apps/details?id=com.github.gotify) to receive notifications
|
||||
- The app does not drain your battery 👍
|
||||
- The notification server is self hosted, see [how to set it up with Docker](https://gotify.net/docs/install)
|
||||
- The notifications only go through your own server (ideally through HTTPS though)
|
||||
|
||||
To set it up with DDNS updater:
|
||||
|
||||
1. Go to the Web GUI of Gotify
|
||||
1. Login with the admin credentials
|
||||
1. Create an app and copy the generated token to the environment variable `GOTIFYTOKEN` (for this container)
|
||||
1. Set the `GOTIFYURL` variable to the URL of your Gotify server address (i.e. `http://127.0.0.1:8080` or `https://bla.com/gotify`)
|
||||
⚠️ This has the disadvantage that if the record is changed manually, the program will not detect it.
|
||||
We could do an API call to get the record IP address every period, but that would get you banned especially with a low period duration.
|
||||
|
||||
## Testing
|
||||
|
||||
- The automated healthcheck verifies all your records are up to date [using DNS lookups](https://github.com/qdm12/ddns-updater/blob/master/internal/healthcheck/healthcheck.go#L15)
|
||||
- You can check manually at:
|
||||
- GoDaddy: [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
|
||||
- You can also manually check, by:
|
||||
1. Going to your DNS management webpage
|
||||
1. Setting your record to `127.0.0.1`
|
||||
1. Run the container
|
||||
1. Refresh the DNS management webpage and verify the update happened
|
||||
|
||||
[](https://dcc.godaddy.com/manage/)
|
||||
## Development and contributing
|
||||
|
||||
You might want to try to change the IP address to `127.0.0.1` to see if the update actually occurs.
|
||||
- [Contribute with code](https://github.com/qdm12/ddns-updater/blob/master/docs/contributing.md)
|
||||
- [Github workflows to know what's building](https://github.com/qdm12/ddns-updater/actions)
|
||||
- [List of issues and feature requests](https://github.com/qdm12/ddns-updater/issues)
|
||||
- [Kanban board](https://github.com/qdm12/ddns-updater/projects/1)
|
||||
|
||||
## Development
|
||||
## License
|
||||
|
||||
1. Setup your environment
|
||||
|
||||
<details><summary>Using VSCode and Docker (easier)</summary><p>
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1: So you can discard it and update it easily!
|
||||
|
||||
</p></details>
|
||||
|
||||
<details><summary>Locally</summary><p>
|
||||
|
||||
1. Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads)
|
||||
1. Install Go dependencies with
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
1. Install [golangci-lint](https://github.com/golangci/golangci-lint#install)
|
||||
1. You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](https://github.com/qdm12/ddns-updater/master/.vscode/settings.json).
|
||||
|
||||
</p></details>
|
||||
|
||||
1. Commands available:
|
||||
|
||||
```sh
|
||||
# Build the binary
|
||||
go build cmd/app/main.go
|
||||
# Test the code
|
||||
go test ./...
|
||||
# Lint the code
|
||||
golangci-lint run
|
||||
# Build the Docker image
|
||||
docker build -t qmcgaw/ddns-updater .
|
||||
```
|
||||
|
||||
1. See [Contributing](https://github.com/qdm12/ddns-updater/master/.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
This repository is under an [MIT license](https://github.com/qdm12/ddns-updater/master/license)
|
||||
|
||||
## Used in external projects
|
||||
|
||||
- [Starttoaster/docker-traefik](https://github.com/Starttoaster/docker-traefik#home-networks-extra-credit-dynamic-dns)
|
||||
|
||||
## TODOs
|
||||
## Support
|
||||
|
||||
- [ ] Update dependencies
|
||||
- [ ] Mockgen instead of mockery
|
||||
- [ ] Other types or records
|
||||
- [ ] icon.ico for webpage
|
||||
- [ ] Record events log
|
||||
- [ ] Hot reload of config.json
|
||||
- [ ] Unit tests
|
||||
- [ ] ReactJS frontend
|
||||
- [ ] Live update of website
|
||||
- [ ] Change settings
|
||||
Sponsor me on [Github](https://github.com/sponsors/qdm12) or donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||
|
||||
[](https://github.com/sponsors/qdm12)
|
||||
[](https://www.paypal.me/qmcgaw)
|
||||
|
||||
Many thanks to J. Famiglietti for supporting me financially 🥇👍
|
||||
|
||||
@@ -2,252 +2,249 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/server"
|
||||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/qdm12/ddns-updater/internal/backup"
|
||||
"github.com/qdm12/ddns-updater/internal/config"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/handlers"
|
||||
"github.com/qdm12/ddns-updater/internal/healthcheck"
|
||||
"github.com/qdm12/ddns-updater/internal/health"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
jsonparams "github.com/qdm12/ddns-updater/internal/params"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
recordslib "github.com/qdm12/ddns-updater/internal/records"
|
||||
"github.com/qdm12/ddns-updater/internal/server"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/trigger"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
"github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/goshutdown"
|
||||
)
|
||||
|
||||
//nolint:gochecknoglobals
|
||||
var (
|
||||
version = "unknown"
|
||||
commit = "unknown"
|
||||
buildDate = "an unknown date"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(_main(context.Background(), time.Now))
|
||||
// returns 1 on error
|
||||
// returns 2 on os signal
|
||||
buildInfo := models.BuildInformation{
|
||||
Version: version,
|
||||
Commit: commit,
|
||||
BuildDate: buildDate,
|
||||
}
|
||||
env := params.NewEnv()
|
||||
logger := logging.NewParent(logging.Settings{Writer: os.Stdout})
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
errorCh := make(chan error)
|
||||
go func() {
|
||||
errorCh <- _main(ctx, env, os.Args, logger, buildInfo, time.Now)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
stop()
|
||||
logger.Warn("Caught OS signal, shutting down")
|
||||
case err := <-errorCh:
|
||||
stop()
|
||||
close(errorCh)
|
||||
if err == nil { // expected exit such as healthcheck
|
||||
os.Exit(0)
|
||||
}
|
||||
logger.Error(err)
|
||||
cancel()
|
||||
}
|
||||
|
||||
const shutdownGracePeriod = 5 * time.Second
|
||||
timer := time.NewTimer(shutdownGracePeriod)
|
||||
select {
|
||||
case err := <-errorCh:
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
logger.Info("Shutdown successful")
|
||||
case <-timer.C:
|
||||
logger.Warn("Shutdown timed out")
|
||||
}
|
||||
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
func _main(ctx context.Context, env params.Env, args []string, logger logging.ParentLogger,
|
||||
buildInfo models.BuildInformation, timeNow func() time.Time) (err error) {
|
||||
if health.IsClientMode(args) {
|
||||
// Running the program in a separate instance through the Docker
|
||||
// built-in healthcheck, in an ephemeral fashion to query the
|
||||
// long running instance of the program about its status
|
||||
if err := libhealthcheck.Query(); err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
client := health.NewClient()
|
||||
var healthConfig config.Health
|
||||
_, err := healthConfig.Get(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return 0
|
||||
}
|
||||
logger, err := setupLogger()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
paramsReader := params.NewReader(logger)
|
||||
|
||||
fmt.Println(splash.Splash(
|
||||
paramsReader.GetVersion(),
|
||||
paramsReader.GetVcsRef(),
|
||||
paramsReader.GetBuildDate()))
|
||||
|
||||
notify, err := setupGotify(paramsReader, logger)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return 1
|
||||
if err := client.Query(ctx, healthConfig.Port); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, err := getParams(paramsReader)
|
||||
fmt.Println(splash.Splash(buildInfo))
|
||||
|
||||
var config config.Config
|
||||
warnings, err := config.Get(env)
|
||||
for _, warning := range warnings {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
return err
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewJSON(dataDir)
|
||||
// Setup logger
|
||||
loggerSettings := logging.Settings{
|
||||
Level: config.Logger.Level,
|
||||
Caller: config.Logger.Caller}
|
||||
logger = logging.NewParent(loggerSettings)
|
||||
|
||||
sender, err := shoutrrr.CreateSender(config.Shoutrrr.Addresses...)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
return err
|
||||
}
|
||||
settings, warnings, err := paramsReader.GetSettings(dataDir + "/config.json")
|
||||
notify := func(message string) {
|
||||
errs := sender.Send(message, &config.Shoutrrr.Params)
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewJSON(config.Paths.DataDir)
|
||||
if err != nil {
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
jsonReader := jsonparams.NewReader(logger)
|
||||
settings, warnings, err := jsonReader.JSONSettings(config.Paths.JSON, logger)
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(2, w)
|
||||
notify(w)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
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))
|
||||
} else if len(settings) == 1 {
|
||||
logger.Info("Found single setting to update records")
|
||||
}
|
||||
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
|
||||
client := &http.Client{Timeout: config.Client.Timeout}
|
||||
|
||||
connectivity := connectivity.NewConnectivity(net.DefaultResolver, client)
|
||||
for _, err := range connectivity.Checks(ctx, "github.com") {
|
||||
logger.Warn(err)
|
||||
}
|
||||
records := make([]models.Record, len(settings))
|
||||
idToPeriod := make(map[int]time.Duration)
|
||||
i := 0
|
||||
for id, setting := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", setting.Domain, setting.Host)
|
||||
events, err := persistentDB.GetEvents(setting.Domain, setting.Host)
|
||||
|
||||
records := make([]recordslib.Record, len(settings))
|
||||
for i, s := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", s.Domain(), s.Host())
|
||||
events, err := persistentDB.GetEvents(s.Domain(), s.Host())
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
records[i] = models.NewRecord(setting, events)
|
||||
idToPeriod[id] = defaultPeriod
|
||||
if setting.Delay > 0 {
|
||||
idToPeriod[id] = setting.Delay
|
||||
}
|
||||
i++
|
||||
records[i] = recordslib.New(s, events)
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
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, logger, client, notify)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
checkError := func(err error) {
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
forceUpdate := trigger.StartUpdates(ctx, updater, idToPeriod, checkError)
|
||||
forceUpdate()
|
||||
productionHandlerFunc := handlers.NewHandler(rootURL, dir, db, logger, forceUpdate, checkError).GetHandlerFunc()
|
||||
healthcheckHandlerFunc := libhealthcheck.GetHandler(func() error {
|
||||
return healthcheck.IsHealthy(db, net.LookupIP, logger)
|
||||
})
|
||||
logger.Info("Web UI listening at address 0.0.0.0:%s with root URL %s", listeningPort, rootURL)
|
||||
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))
|
||||
serverErrors := make(chan []error)
|
||||
go func() {
|
||||
serverErrors <- server.RunServers(ctx,
|
||||
server.Settings{Name: "production", Addr: "0.0.0.0:" + listeningPort, Handler: productionHandlerFunc},
|
||||
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
}()
|
||||
|
||||
go backupRunLoop(ctx, backupPeriod, dir, backupDirectory, logger, timeNow)
|
||||
config.PubIP.HTTPSettings.Client = client
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
os.Interrupt,
|
||||
)
|
||||
select {
|
||||
case errors := <-serverErrors:
|
||||
for _, err := range errors {
|
||||
logger.Error(err)
|
||||
}
|
||||
return 1
|
||||
case signal := <-osSignals:
|
||||
message := fmt.Sprintf("Stopping program: caught OS signal %q", signal)
|
||||
logger.Warn(message)
|
||||
notify(2, message)
|
||||
return 2
|
||||
case <-ctx.Done():
|
||||
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
|
||||
logger.Warn(message)
|
||||
return 1
|
||||
ipGetter, err := publicip.NewFetcher(config.PubIP.DNSSettings, config.PubIP.HTTPSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updater := update.NewUpdater(db, client, notify, logger)
|
||||
runner := update.NewRunner(db, updater, ipGetter, config.Update.Period,
|
||||
config.IPv6.Mask, config.Update.Cooldown, logger, timeNow)
|
||||
|
||||
runnerHandler, runnerCtx, runnerDone := goshutdown.NewGoRoutineHandler(
|
||||
"runner", goshutdown.GoRoutineSettings{})
|
||||
go runner.Run(runnerCtx, runnerDone)
|
||||
|
||||
// note: errors are logged within the goroutine,
|
||||
// no need to collect the resulting errors.
|
||||
go runner.ForceUpdate(ctx)
|
||||
|
||||
isHealthy := health.MakeIsHealthy(db, net.LookupIP, logger)
|
||||
healthServer := health.NewServer(config.Health.ServerAddress,
|
||||
logger.NewChild(logging.Settings{Prefix: "healthcheck server: "}),
|
||||
isHealthy)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
|
||||
"health server", goshutdown.GoRoutineSettings{})
|
||||
go healthServer.Run(healthServerCtx, healthServerDone)
|
||||
|
||||
address := ":" + strconv.Itoa(int(config.Server.Port))
|
||||
serverLogger := logger.NewChild(logging.Settings{Prefix: "http server: "})
|
||||
server := server.New(ctx, address, config.Server.RootURL, db, serverLogger, runner)
|
||||
serverHandler, serverCtx, serverDone := goshutdown.NewGoRoutineHandler(
|
||||
"server", goshutdown.GoRoutineSettings{})
|
||||
go server.Run(serverCtx, serverDone)
|
||||
notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
|
||||
|
||||
backupHandler, backupCtx, backupDone := goshutdown.NewGoRoutineHandler(
|
||||
"backup", goshutdown.GoRoutineSettings{})
|
||||
go backupRunLoop(backupCtx, backupDone, config.Backup.Period, config.Paths.DataDir, config.Backup.Directory,
|
||||
logger.NewChild(logging.Settings{Prefix: "backup: "}), timeNow)
|
||||
|
||||
shutdownGroup := goshutdown.NewGroupHandler("", goshutdown.GroupSettings{})
|
||||
shutdownGroup.Add(runnerHandler, healthServerHandler, serverHandler, backupHandler)
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
if err := shutdownGroup.Shutdown(context.Background()); err != nil {
|
||||
notify(err.Error())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if gotifyURL == nil {
|
||||
return func(priority int, messageArgs ...interface{}) {}, nil
|
||||
}
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gotify := admin.NewGotify(*gotifyURL, gotifyToken, &http.Client{Timeout: time.Second})
|
||||
return func(priority int, messageArgs ...interface{}) {
|
||||
if err := gotify.Notify("DDNS Updater", priority, messageArgs...); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getParams(paramsReader params.Reader) (
|
||||
dir, dataDir,
|
||||
listeningPort, rootURL string,
|
||||
defaultPeriod time.Duration,
|
||||
backupPeriod time.Duration, backupDirectory string,
|
||||
err error) {
|
||||
dir, err = paramsReader.GetExeDir()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
dataDir, err = paramsReader.GetDataDir(dir)
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
listeningPort, _, err = paramsReader.GetListeningPort()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
rootURL, err = paramsReader.GetRootURL()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
defaultPeriod, err = paramsReader.GetDelay(libparams.Default("10m"))
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
|
||||
backupPeriod, err = paramsReader.GetBackupPeriod()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
backupDirectory, err = paramsReader.GetBackupDirectory()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
return dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, nil
|
||||
}
|
||||
|
||||
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
|
||||
logger logging.Logger, timeNow func() time.Time) {
|
||||
logger = logger.WithPrefix("backup: ")
|
||||
func backupRunLoop(ctx context.Context, done chan<- struct{}, backupPeriod time.Duration,
|
||||
dataDir, outputDir string, logger logging.Logger, timeNow func() time.Time) {
|
||||
defer close(done)
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
@@ -256,11 +253,13 @@ func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outp
|
||||
ziper := backup.NewZiper()
|
||||
timer := time.NewTimer(backupPeriod)
|
||||
for {
|
||||
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
|
||||
fileName := "ddns-updater-backup-" + strconv.Itoa(int(timeNow().UnixNano())) + ".zip"
|
||||
zipFilepath := filepath.Join(outputDir, fileName)
|
||||
if err := ziper.ZipFiles(
|
||||
filepath,
|
||||
fmt.Sprintf("%s/data/updates.json", exeDir),
|
||||
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
|
||||
zipFilepath,
|
||||
filepath.Join(dataDir, "updates.json"),
|
||||
filepath.Join(dataDir, "config.json"),
|
||||
); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
select {
|
||||
|
||||
@@ -4,28 +4,23 @@
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "google",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
},
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "example.info",
|
||||
"ip_method": "opendns",
|
||||
"key": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,15 +9,27 @@ services:
|
||||
volumes:
|
||||
- ./data:/updater/data
|
||||
environment:
|
||||
- DELAY=300s
|
||||
- ROOT_URL=/
|
||||
- LISTENING_PORT=8000
|
||||
- LOG_ENCODING=console
|
||||
- LOG_LEVEL=info
|
||||
- NODE_ID=0
|
||||
- CONFIG=
|
||||
- PERIOD=5m
|
||||
- UPDATE_COOLDOWN_PERIOD=5m
|
||||
- PUBLICIP_FETCHERS=all
|
||||
- PUBLICIP_HTTP_PROVIDERS=all
|
||||
- PUBLICIPV4_HTTP_PROVIDERS=all
|
||||
- PUBLICIPV6_HTTP_PROVIDERS=all
|
||||
- PUBLICIP_DNS_PROVIDERS=all
|
||||
- PUBLICIP_DNS_TIMEOUT=3s
|
||||
- HTTP_TIMEOUT=10s
|
||||
- GOTIFY_URL=
|
||||
- GOTIFY_TOKEN=
|
||||
- BACKUP_PERIOD=0
|
||||
|
||||
# Web UI
|
||||
- LISTENING_PORT=8000
|
||||
- ROOT_URL=/
|
||||
|
||||
# Backup
|
||||
- BACKUP_PERIOD=0 # 0 to disable
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
|
||||
# Other
|
||||
- LOG_LEVEL=info
|
||||
- LOG_CALLER=hidden
|
||||
- SHOUTRRR_ADDRESSES=
|
||||
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.
|
||||
29
docs/dd24.md
Normal file
29
docs/dd24.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Domain Discount 24
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "dd24",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"password"` is your password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
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"` which you can create at [eu.api.ovh.com/createApp](https://eu.api.ovh.com/createApp/)
|
||||
- `"app_secret"`
|
||||
- `"consumer_key"`
|
||||
|
||||
The ZoneDNS implementation allows you to update any record name including *.yourdomain.tld
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
|
||||
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
- `"mode"` select between two modes, OVH's dynamic hosting service (`"dynamic"`) or OVH's API (`"api"`). Default is `"dynamic"`
|
||||
|
||||
## Domain setup
|
||||
|
||||
- If you use DynHost: [docs.ovh.com/ie/en/domains/hosting_dynhost](https://docs.ovh.com/ie/en/domains/hosting_dynhost/)
|
||||
- If you use the ZoneDNS API: [docs.ovh.com/gb/en/customer/first-steps-with-ovh-api](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/)
|
||||
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/)
|
||||
15
go.mod
15
go.mod
@@ -1,11 +1,14 @@
|
||||
module github.com/qdm12/ddns-updater
|
||||
|
||||
go 1.13
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/containrrr/shoutrrr v0.4.4
|
||||
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/miekg/dns v1.1.42
|
||||
github.com/qdm12/golibs v0.0.0-20210514224620-c025cb0da211
|
||||
github.com/qdm12/goshutdown v0.1.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
)
|
||||
|
||||
398
go.sum
398
go.sum
@@ -1,139 +1,383 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
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=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
|
||||
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
|
||||
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
|
||||
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
|
||||
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY=
|
||||
github.com/containrrr/shoutrrr v0.4.4/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
|
||||
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
|
||||
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
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-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
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=
|
||||
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/errors v0.17.2 h1:azEQ8Fnx0jmtFF2fxsnmd6I0x6rsweUF63qqSO1NmKk=
|
||||
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
|
||||
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
|
||||
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
|
||||
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
|
||||
github.com/go-openapi/loads v0.17.0 h1:H22nMs3GDQk4SwAaFQ+jLNw+0xoFeCueawhZlv8MBYs=
|
||||
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
|
||||
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
|
||||
github.com/go-openapi/runtime v0.17.2 h1:/ZK67ikFhQAMFFH/aPu2MaGH7QjP4wHBvHYOVIzDAw0=
|
||||
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
|
||||
github.com/go-openapi/spec v0.17.0 h1:XNvrt8FlSVP8T1WuhbAFF6QDhJc0zsoWzX4wXARhhpE=
|
||||
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
|
||||
github.com/go-openapi/strfmt v0.17.0 h1:1isAxYf//QDTnVzbLAMrUK++0k1EjeLJU/gTOR0o3Mc=
|
||||
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
|
||||
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
|
||||
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/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
|
||||
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
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/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
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/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
|
||||
github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I=
|
||||
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA=
|
||||
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
|
||||
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o=
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible h1:gaQFbK2+uSxOR4iGZprJAbpmtqTrHhSdgOyIMD6Oidc=
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
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/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
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/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
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.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
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/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
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/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
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/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ=
|
||||
github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
|
||||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||
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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151 h1:5q8oyhJqgQyW5v427CDC34SobllqiJCLLfS3Z4EeLCI=
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
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/qdm12/goshutdown v0.1.0 h1:lmwnygdXtnr2pa6VqfR/bm8077/BnBef1+7CP96B7Sw=
|
||||
github.com/qdm12/goshutdown v0.1.0/go.mod h1:/LP3MWLqI+wGH/ijfaUG+RHzBbKXIiVKnrg5vXOCf6Q=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
|
||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
|
||||
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/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20190306220234-b354f8bf4d9e/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
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-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/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/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/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=
|
||||
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
|
||||
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
|
||||
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
|
||||
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
|
||||
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
|
||||
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
|
||||
21
internal/config/backup.go
Normal file
21
internal/config/backup.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Backup struct {
|
||||
Period time.Duration
|
||||
Directory string
|
||||
}
|
||||
|
||||
func (b *Backup) get(env params.Env) (err error) {
|
||||
b.Period, err = env.Duration("BACKUP_PERIOD", params.Default("0"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Directory, err = env.Path("BACKUP_DIRECTORY", params.Default("./data"))
|
||||
return err
|
||||
}
|
||||
16
internal/config/client.go
Normal file
16
internal/config/client.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *Client) get(env params.Env) (err error) {
|
||||
c.Timeout, err = env.Duration("HTTP_TIMEOUT", params.Default("10s"))
|
||||
return err
|
||||
}
|
||||
72
internal/config/config.go
Normal file
72
internal/config/config.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Client Client
|
||||
Update Update
|
||||
PubIP PubIP
|
||||
IPv6 IPv6
|
||||
Server Server
|
||||
Health Health
|
||||
Paths Paths
|
||||
Backup Backup
|
||||
Logger Logger
|
||||
Shoutrrr Shoutrrr
|
||||
}
|
||||
|
||||
func (c *Config) Get(env params.Env) (warnings []string, err error) {
|
||||
if err := c.Client.get(env); err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err := c.Update.get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
newWarnings, err := c.PubIP.get(env)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
if err := c.IPv6.get(env); err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err = c.Server.get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
warning, err = c.Health.Get(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
if err := c.Paths.get(env); err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
if err := c.Backup.get(env); err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
if err := c.Logger.get(env); err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
newWarnings, err = c.Shoutrrr.get(env)
|
||||
warnings = append(warnings, newWarnings...)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
31
internal/config/health.go
Normal file
31
internal/config/health.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Health struct {
|
||||
ServerAddress string
|
||||
Port uint16 // obtained from ServerAddress
|
||||
}
|
||||
|
||||
func (h *Health) Get(env params.Env) (warning string, err error) {
|
||||
h.ServerAddress, warning, err = env.ListeningAddress(
|
||||
"HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999"))
|
||||
if err != nil {
|
||||
return warning, err
|
||||
}
|
||||
_, portStr, err := net.SplitHostPort(h.ServerAddress)
|
||||
if err != nil {
|
||||
return warning, err
|
||||
}
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return warning, err
|
||||
}
|
||||
h.Port = uint16(port)
|
||||
return warning, nil
|
||||
}
|
||||
56
internal/config/ipv6.go
Normal file
56
internal/config/ipv6.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type IPv6 struct {
|
||||
Mask net.IPMask
|
||||
}
|
||||
|
||||
func (i *IPv6) get(env params.Env) (err error) {
|
||||
maskStr, err := env.Get("IPV6_PREFIX", params.Default("/128"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.Mask, err = ipv6DecimalPrefixToMask(maskStr)
|
||||
return err
|
||||
}
|
||||
|
||||
var ErrParsePrefix = errors.New("cannot parse IP prefix")
|
||||
|
||||
func ipv6DecimalPrefixToMask(prefixDecimal string) (ipMask net.IPMask, err error) {
|
||||
if prefixDecimal == "" {
|
||||
return nil, fmt.Errorf("%w: empty prefix", ErrParsePrefix)
|
||||
}
|
||||
|
||||
prefixDecimal = strings.TrimPrefix(prefixDecimal, "/")
|
||||
|
||||
const bits = 8 * net.IPv6len
|
||||
|
||||
ones, consumed, ok := decimalToInteger(prefixDecimal)
|
||||
if !ok || consumed != len(prefixDecimal) || ones < 0 || ones > bits {
|
||||
return nil, fmt.Errorf("%w: %s", ErrParsePrefix, prefixDecimal)
|
||||
}
|
||||
|
||||
return net.CIDRMask(ones, bits), nil
|
||||
}
|
||||
|
||||
func decimalToInteger(s string) (ones int, i int, ok bool) {
|
||||
const big = 0xFFFFFF // Bigger than we need, not too big to worry about overflow
|
||||
const ten = 10
|
||||
|
||||
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
|
||||
ones = ones*ten + int(s[i]-'0')
|
||||
if ones >= big {
|
||||
return big, i, false
|
||||
}
|
||||
}
|
||||
|
||||
return ones, i, true
|
||||
}
|
||||
62
internal/config/ipv6_test.go
Normal file
62
internal/config/ipv6_test.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ipv6DecimalPrefixToMask(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
prefixDecimal string
|
||||
ipMask net.IPMask
|
||||
err error
|
||||
}{
|
||||
"empty": {
|
||||
err: fmt.Errorf("cannot parse IP prefix: empty prefix"),
|
||||
},
|
||||
"malformed": {
|
||||
prefixDecimal: "malformed",
|
||||
err: fmt.Errorf("cannot parse IP prefix: malformed"),
|
||||
},
|
||||
"with leading slash": {
|
||||
prefixDecimal: "/78",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
"without leading slash": {
|
||||
prefixDecimal: "78",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
"full IPv6 mask": {
|
||||
prefixDecimal: "/128",
|
||||
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
|
||||
},
|
||||
"zero IPv6 mask": {
|
||||
prefixDecimal: "/0",
|
||||
ipMask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ipMask, err := ipv6DecimalPrefixToMask(testCase.prefixDecimal)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.ipMask, ipMask)
|
||||
})
|
||||
}
|
||||
}
|
||||
21
internal/config/logger.go
Normal file
21
internal/config/logger.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
Caller logging.Caller
|
||||
Level logging.Level
|
||||
}
|
||||
|
||||
func (l *Logger) get(env params.Env) (err error) {
|
||||
l.Caller, err = env.LogCaller("LOG_CALLER", params.Default("hidden"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.Level, err = env.LogLevel("LOG_LEVEL", params.Default("info"))
|
||||
return err
|
||||
}
|
||||
22
internal/config/paths.go
Normal file
22
internal/config/paths.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Paths struct {
|
||||
DataDir string
|
||||
JSON string // obtained from DataDir
|
||||
}
|
||||
|
||||
func (p *Paths) get(env params.Env) (err error) {
|
||||
p.DataDir, err = env.Path("DATADIR", params.Default("./data"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.JSON = filepath.Join(p.DataDir, "config.json")
|
||||
return nil
|
||||
}
|
||||
195
internal/config/pubip.go
Normal file
195
internal/config/pubip.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
const all = "all"
|
||||
|
||||
type PubIP struct {
|
||||
HTTPSettings publicip.HTTPSettings
|
||||
DNSSettings publicip.DNSSettings
|
||||
}
|
||||
|
||||
func (p *PubIP) get(env params.Env) (warnings []string, err error) {
|
||||
if err = p.getFetchers(env); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpIPProviders, warning, err := p.getIPHTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
httpIP4Providers, warning, err := p.getIPv4HTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
httpIP6Providers, warning, err := p.getIPv6HTTPProviders(env)
|
||||
warnings = appendIfNotEmpty(warnings, warning)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
p.HTTPSettings.Options = []http.Option{
|
||||
http.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
|
||||
http.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
|
||||
http.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
|
||||
}
|
||||
|
||||
dnsIPProviders, err := p.getDNSProviders(env)
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
dnsTimeout, err := env.Duration("PUBLICIP_DNS_TIMEOUT", params.Default("3s"))
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
p.DNSSettings.Options = []dns.Option{
|
||||
dns.SetTimeout(dnsTimeout),
|
||||
dns.SetProviders(dnsIPProviders[0], dnsIPProviders[1:]...),
|
||||
}
|
||||
|
||||
return warnings, nil
|
||||
}
|
||||
|
||||
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
|
||||
|
||||
func (p *PubIP) getFetchers(env params.Env) (err error) {
|
||||
s, err := env.Get("PUBLICIP_FETCHERS", params.Default(all))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
for i, field := range fields {
|
||||
switch strings.ToLower(field) {
|
||||
case all:
|
||||
p.HTTPSettings.Enabled = true
|
||||
p.DNSSettings.Enabled = true
|
||||
case "http":
|
||||
p.HTTPSettings.Enabled = true
|
||||
case "dns":
|
||||
p.DNSSettings.Enabled = true
|
||||
default:
|
||||
err = fmt.Errorf(
|
||||
"%w: %q at position %d of %d",
|
||||
ErrInvalidFetcher, field, i+1, len(fields))
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getDNSProviders obtains the DNS providers to obtain your public IPv4 and/or IPv6 address.
|
||||
func (p *PubIP) getDNSProviders(env params.Env) (providers []dns.Provider, err error) {
|
||||
s, err := env.Get("PUBLICIP_DNS_PROVIDERS", params.Default(all))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
availableProviders := dns.ListProviders()
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
providers = make([]dns.Provider, len(fields))
|
||||
for i, field := range fields {
|
||||
if field == all {
|
||||
return availableProviders, nil
|
||||
}
|
||||
|
||||
providers[i] = dns.Provider(field)
|
||||
if err := dns.ValidateProvider(providers[i]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
// getHTTPProviders obtains the HTTP providers to obtain your public IPv4 or IPv6 address.
|
||||
func (p *PubIP) getIPHTTPProviders(env params.Env) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIP_HTTP_PROVIDERS", "IP_METHOD", ipversion.IP4or6)
|
||||
}
|
||||
|
||||
// getIPv4HTTPProviders obtains the HTTP providers to obtain your public IPv4 address.
|
||||
func (p *PubIP) getIPv4HTTPProviders(env params.Env) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIPV4_HTTP_PROVIDERS", "IPV4_METHOD", ipversion.IP4)
|
||||
}
|
||||
|
||||
// getIPv6HTTPProviders obtains the HTTP providers to obtain your public IPv6 address.
|
||||
func (p *PubIP) getIPv6HTTPProviders(env params.Env) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
return httpIPMethod(env, "PUBLICIPV6_HTTP_PROVIDERS", "IPV6_METHOD", ipversion.IP6)
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidPublicIPHTTPProvider = errors.New("invalid public IP HTTP provider")
|
||||
)
|
||||
|
||||
func httpIPMethod(env params.Env, envKey, retroKey string, version ipversion.IPVersion) (
|
||||
providers []http.Provider, warning string, err error) {
|
||||
retroKeyOption := params.RetroKeys([]string{retroKey}, func(oldKey, newKey string) {
|
||||
warning = "You are using an old environment variable " + oldKey +
|
||||
" please change it to " + newKey
|
||||
})
|
||||
s, err := env.Get(envKey, params.Default("cycle"), retroKeyOption)
|
||||
if err != nil {
|
||||
return nil, warning, err
|
||||
}
|
||||
|
||||
availableProviders := http.ListProvidersForVersion(version)
|
||||
choices := make(map[http.Provider]struct{}, len(availableProviders))
|
||||
for _, provider := range availableProviders {
|
||||
choices[provider] = struct{}{}
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
|
||||
for _, field := range fields {
|
||||
// Retro-compatibility.
|
||||
switch field {
|
||||
case "ipify6":
|
||||
field = "ipify"
|
||||
case "noip4", "noip6", "noip8245_4", "noip8245_6":
|
||||
field = "noip"
|
||||
case "cycle":
|
||||
field = all
|
||||
}
|
||||
|
||||
if field == all {
|
||||
return availableProviders, warning, nil
|
||||
}
|
||||
|
||||
// Custom URL check
|
||||
url, err := url.Parse(field)
|
||||
if err == nil && url != nil && url.Scheme == "https" {
|
||||
providers = append(providers, http.CustomProvider(url))
|
||||
continue
|
||||
}
|
||||
|
||||
provider := http.Provider(field)
|
||||
if _, ok := choices[provider]; !ok {
|
||||
return nil, warning, fmt.Errorf("%w: %s", ErrInvalidPublicIPHTTPProvider, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, warning, fmt.Errorf("%w: for IP version %s", ErrInvalidPublicIPHTTPProvider, version)
|
||||
}
|
||||
|
||||
return providers, warning, nil
|
||||
}
|
||||
19
internal/config/server.go
Normal file
19
internal/config/server.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Port uint16
|
||||
RootURL string
|
||||
}
|
||||
|
||||
func (s *Server) get(env params.Env) (warning string, err error) {
|
||||
s.RootURL, err = env.RootURL("ROOT_URL")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.Port, warning, err = env.ListeningPort("LISTENING_PORT", params.Default("8000"))
|
||||
return warning, err
|
||||
}
|
||||
69
internal/config/shoutrrr.go
Normal file
69
internal/config/shoutrrr.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/containrrr/shoutrrr"
|
||||
"github.com/containrrr/shoutrrr/pkg/types"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Shoutrrr struct {
|
||||
Addresses []string
|
||||
Params types.Params
|
||||
}
|
||||
|
||||
func (s *Shoutrrr) get(env params.Env) (warnings []string, err error) {
|
||||
s.Addresses, err = env.CSV("SHOUTRRR_ADDRESSES")
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
// Retro-compatibility: GOTIFY_URL and GOTIFY_TOKEN
|
||||
gotifyURL, err := env.URL("GOTIFY_URL")
|
||||
if err != nil || gotifyURL != nil {
|
||||
const warning = "You should use the environment variable SHOUTRRR_ADDRESSES instead of GOTIFY_URL and GOTIFY_TOKEN"
|
||||
warnings = append(warnings, warning)
|
||||
}
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
} else if gotifyURL != nil {
|
||||
gotifyToken, err := env.Get("GOTIFY_TOKEN", params.CaseSensitiveValue(),
|
||||
params.Compulsory(), params.Unset())
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
gotifyShoutrrrAddress := gotifyURLTokenToShoutrrr(gotifyURL, gotifyToken)
|
||||
s.Addresses = append(s.Addresses, gotifyShoutrrrAddress)
|
||||
}
|
||||
|
||||
if _, err = shoutrrr.CreateSender(s.Addresses...); err != nil {
|
||||
return warnings, err // validation step
|
||||
}
|
||||
|
||||
str, err := env.Get("SHOUTRRR_PARAMS", params.Default("title=DDNS Updater"))
|
||||
if err != nil {
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
keyValues := strings.Split(str, ",")
|
||||
s.Params = make(map[string]string, len(keyValues))
|
||||
for _, keyValue := range keyValues {
|
||||
fields := strings.Split(keyValue, "=")
|
||||
key, value := fields[0], fields[1]
|
||||
s.Params[key] = value
|
||||
}
|
||||
|
||||
return warnings, err
|
||||
}
|
||||
|
||||
func gotifyURLTokenToShoutrrr(url *url.URL, token string) (address string) {
|
||||
hostAndPath := path.Join(url.Host, url.Path)
|
||||
address = "gotify://" + hostAndPath + "/" + token
|
||||
if url.Scheme == "http" {
|
||||
address += "?DisableTLS=Yes"
|
||||
}
|
||||
return address
|
||||
}
|
||||
46
internal/config/update.go
Normal file
46
internal/config/update.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Update struct {
|
||||
Period time.Duration
|
||||
Cooldown time.Duration
|
||||
}
|
||||
|
||||
func (u *Update) get(env params.Env) (warning string, err error) {
|
||||
warning, err = u.getPeriod(env)
|
||||
if err != nil {
|
||||
return warning, err
|
||||
}
|
||||
|
||||
u.Cooldown, err = env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
|
||||
return warning, err
|
||||
}
|
||||
|
||||
func (u *Update) getPeriod(env params.Env) (warning string, err error) {
|
||||
// Backward compatibility: DELAY
|
||||
s, err := env.Get("DELAY", params.Compulsory())
|
||||
if err == nil {
|
||||
warning = "the environment variable DELAY should be changed to PERIOD"
|
||||
// Backward compatibility: integer only, treated as seconds
|
||||
n, err := strconv.Atoi(s)
|
||||
if err == nil {
|
||||
u.Period = time.Duration(n) * time.Second
|
||||
return warning, nil
|
||||
}
|
||||
|
||||
period, err := time.ParseDuration(s)
|
||||
if err == nil {
|
||||
u.Period = period
|
||||
return warning, nil
|
||||
}
|
||||
}
|
||||
|
||||
u.Period, err = env.Duration("PERIOD", params.Default("10m"))
|
||||
return "", err
|
||||
}
|
||||
8
internal/config/utils.go
Normal file
8
internal/config/utils.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
func appendIfNotEmpty(slice []string, s string) (newSlice []string) {
|
||||
if s == "" {
|
||||
return slice
|
||||
}
|
||||
return append(slice, s)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
HTMLFail models.HTML = `<font color="red"><b>Failure</b></font>`
|
||||
HTMLSuccess models.HTML = `<font color="green"><b>Success</b></font>`
|
||||
HTMLUpdate models.HTML = `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
HTMLUpdating models.HTML = `<font color="orange"><b>Updating</b></font>`
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO have a struct model containing URL, name for each provider
|
||||
HTMLNamecheap models.HTML = "<a href=\"https://namecheap.com\">Namecheap</a>"
|
||||
HTMLGodaddy models.HTML = "<a href=\"https://godaddy.com\">GoDaddy</a>"
|
||||
HTMLDuckDNS models.HTML = "<a href=\"https://duckdns.org\">DuckDNS</a>"
|
||||
HTMLDreamhost models.HTML = "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>"
|
||||
HTMLCloudflare models.HTML = "<a href=\"https://www.cloudflare.com\">Cloudflare</a>"
|
||||
HTMLNoIP models.HTML = "<a href=\"https://www.noip.com/\">NoIP</a>"
|
||||
HTMLDNSPod models.HTML = "<a href=\"https://www.dnspod.cn/\">DNSPod</a>"
|
||||
HTMLInfomaniak models.HTML = "<a href=\"https://www.infomaniak.com/\">Infomaniak</a>"
|
||||
HTMLDdnssde models.HTML = "<a href=\"https://ddnss.de/\">DDNSS.de</a>"
|
||||
)
|
||||
|
||||
const (
|
||||
HTMLGoogle models.HTML = "<a href=\"https://google.com/search?q=ip\">Google</a>"
|
||||
HTMLOpenDNS models.HTML = "<a href=\"https://diagnostic.opendns.com/myip\">OpenDNS</a>"
|
||||
HTMLIfconfig models.HTML = "<a href=\"https://ifconfig.io\">ifconfig.io</a>"
|
||||
HTMLIpinfo models.HTML = "<a href=\"https://ipinfo.io\">ipinfo.io</a>"
|
||||
HTMLIpify models.HTML = "<a href=\"https://api.ipify.org\">api.ipify.org</a>"
|
||||
HTMLIpify6 models.HTML = "<a href=\"https://api6.ipify.org\">api6.ipify.org</a>"
|
||||
HTMLDdnss models.HTML = "<a href=\"https://ddnss.de/meineip.php\">ddnss.de</a>"
|
||||
HTMLDdnss4 models.HTML = "<a href=\"https://ip4.ddnss.de/meineip.php\">ip4.ddnss.de</a>"
|
||||
HTMLDdnss6 models.HTML = "<a href=\"https://ip6.ddnss.de/meineip.php\">ip6.ddns.de</a>"
|
||||
HTMLCycle models.HTML = "Cycling"
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
IPv4 models.IPVersion = "ipv4"
|
||||
IPv6 models.IPVersion = "ipv6"
|
||||
)
|
||||
@@ -1,53 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
PROVIDER models.IPMethod = "provider"
|
||||
OPENDNS models.IPMethod = "opendns"
|
||||
IFCONFIG models.IPMethod = "ifconfig"
|
||||
IPINFO models.IPMethod = "ipinfo"
|
||||
IPIFY models.IPMethod = "ipify"
|
||||
IPIFY6 models.IPMethod = "ipify6"
|
||||
CYCLE models.IPMethod = "cycle"
|
||||
DDNSS models.IPMethod = "ddnss"
|
||||
DDNSS4 models.IPMethod = "ddnss4"
|
||||
DDNSS6 models.IPMethod = "ddnss6"
|
||||
// Retro compatibility only
|
||||
GOOGLE models.IPMethod = "google"
|
||||
)
|
||||
|
||||
func IPMethodMapping() map[models.IPMethod]string {
|
||||
return map[models.IPMethod]string{
|
||||
PROVIDER: string(PROVIDER),
|
||||
CYCLE: string(CYCLE),
|
||||
OPENDNS: "https://diagnostic.opendns.com/myip",
|
||||
IFCONFIG: "https://ifconfig.io/ip",
|
||||
IPINFO: "https://ipinfo.io/ip",
|
||||
IPIFY: "https://api.ipify.org",
|
||||
IPIFY6: "https://api6.ipify.org",
|
||||
DDNSS: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS4: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS6: "https://ip6.ddnss.de/meineip.php",
|
||||
}
|
||||
}
|
||||
|
||||
func IPMethodChoices() (choices []models.IPMethod) {
|
||||
for choice := range IPMethodMapping() {
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
return choices
|
||||
}
|
||||
|
||||
func IPMethodExternalChoices() (choices []models.IPMethod) {
|
||||
for _, choice := range IPMethodChoices() {
|
||||
switch choice {
|
||||
case PROVIDER, CYCLE:
|
||||
default:
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
}
|
||||
return choices
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_IPMethodChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "provider", "cycle", "opendns", "ifconfig", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
|
||||
func Test_IPMethodExternalChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodExternalChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "ifconfig", "opendns", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
// All possible provider values
|
||||
const (
|
||||
GODADDY models.Provider = "godaddy"
|
||||
NAMECHEAP models.Provider = "namecheap"
|
||||
DUCKDNS models.Provider = "duckdns"
|
||||
DREAMHOST models.Provider = "dreamhost"
|
||||
CLOUDFLARE models.Provider = "cloudflare"
|
||||
NOIP models.Provider = "noip"
|
||||
DNSPOD models.Provider = "dnspod"
|
||||
INFOMANIAK models.Provider = "infomaniak"
|
||||
DDNSSDE models.Provider = "ddnss"
|
||||
)
|
||||
|
||||
func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
GODADDY,
|
||||
NAMECHEAP,
|
||||
DUCKDNS,
|
||||
DREAMHOST,
|
||||
CLOUDFLARE,
|
||||
NOIP,
|
||||
DNSPOD,
|
||||
INFOMANIAK,
|
||||
DDNSSDE,
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "regexp"
|
||||
|
||||
const (
|
||||
goDaddyKey = `[A-Za-z0-9]{10,14}\_[A-Za-z0-9]{22}`
|
||||
godaddySecret = `[A-Za-z0-9]{22}` // #nosec
|
||||
duckDNSToken = `[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}` // #nosec
|
||||
namecheapPassword = `[a-f0-9]{32}` // #nosec
|
||||
dreamhostKey = `[a-zA-Z0-9]{16}`
|
||||
cloudflareKey = `[a-zA-Z0-9]+`
|
||||
cloudflareUserServiceKey = `v1\.0.+`
|
||||
cloudflareToken = `[a-zA-Z0-9_]{40}` // #nosec
|
||||
)
|
||||
|
||||
func MatchGodaddyKey(s string) bool {
|
||||
return regexp.MustCompile("^" + goDaddyKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchGodaddySecret(s string) bool {
|
||||
return regexp.MustCompile("^" + godaddySecret + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDuckDNSToken(s string) bool {
|
||||
return regexp.MustCompile("^" + duckDNSToken + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchNamecheapPassword(s string) bool {
|
||||
return regexp.MustCompile("^" + namecheapPassword + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDreamhostKey(s string) bool {
|
||||
return regexp.MustCompile("^" + dreamhostKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareUserServiceKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareUserServiceKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareToken(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareToken + "$").MatchString(s)
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// Announcement is a message announcement
|
||||
Announcement = "Smaller Docker image based on Scratch (12.3MB)"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd
|
||||
AnnouncementExpiration = "2020-04-20"
|
||||
// Announcement is a message announcement.
|
||||
Announcement = "Support for he.net"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd.
|
||||
AnnouncementExpiration = "2020-10-15"
|
||||
)
|
||||
|
||||
const (
|
||||
// IssueLink is the link for users to use to create issues
|
||||
// IssueLink is the link for users to use to create issues.
|
||||
IssueLink = "https://github.com/qdm12/ddns-updater/issues/new"
|
||||
)
|
||||
|
||||
@@ -7,4 +7,5 @@ const (
|
||||
SUCCESS models.Status = "success"
|
||||
UPTODATE models.Status = "up to date"
|
||||
UPDATING models.Status = "updating"
|
||||
UNSET models.Status = "unset"
|
||||
)
|
||||
|
||||
@@ -5,26 +5,26 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
Close() error
|
||||
Insert(record models.Record) (id int)
|
||||
Select(id int) (record models.Record, err error)
|
||||
SelectAll() (records []models.Record)
|
||||
Update(id int, record models.Record) error
|
||||
// From persistence database
|
||||
Select(id int) (record records.Record, err error)
|
||||
SelectAll() (records []records.Record)
|
||||
// Using persistence database
|
||||
Update(id int, record records.Record) error
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
}
|
||||
|
||||
type database struct {
|
||||
data []models.Record
|
||||
data []records.Record
|
||||
sync.RWMutex
|
||||
persistentDB persistence.Database
|
||||
}
|
||||
|
||||
// NewDatabase creates a new in memory database
|
||||
func NewDatabase(data []models.Record, persistentDB persistence.Database) Database {
|
||||
// NewDatabase creates a new in memory database.
|
||||
func NewDatabase(data []records.Record, persistentDB persistence.Database) Database {
|
||||
return &database{
|
||||
data: data,
|
||||
persistentDB: persistentDB,
|
||||
|
||||
@@ -3,17 +3,10 @@ package data
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) Insert(record models.Record) (id int) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
db.data = append(db.data, record)
|
||||
return len(db.data) - 1
|
||||
}
|
||||
|
||||
func (db *database) Select(id int) (record models.Record, err error) {
|
||||
func (db *database) Select(id int) (record records.Record, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
if id < 0 {
|
||||
@@ -25,7 +18,7 @@ func (db *database) Select(id int) (record models.Record, err error) {
|
||||
return db.data[id], nil
|
||||
}
|
||||
|
||||
func (db *database) SelectAll() (records []models.Record) {
|
||||
func (db *database) SelectAll() (records []records.Record) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
return db.data
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
return db.persistentDB.GetEvents(domain, host)
|
||||
}
|
||||
|
||||
func (db *database) Update(id int, record models.Record) error {
|
||||
func (db *database) Update(id int, record records.Record) error {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
if id < 0 {
|
||||
@@ -25,8 +26,8 @@ func (db *database) Update(id int, record models.Record) error {
|
||||
// new IP address added
|
||||
if newCount > currentCount {
|
||||
if err := db.persistentDB.StoreNewIP(
|
||||
record.Settings.Domain,
|
||||
record.Settings.Host,
|
||||
record.Settings.Domain(),
|
||||
record.Settings.Host(),
|
||||
record.History.GetCurrentIP(),
|
||||
record.History.GetSuccessTime(),
|
||||
); err != nil {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/html"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
// Handler contains a handler function
|
||||
type Handler interface {
|
||||
GetHandlerFunc() http.HandlerFunc
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
rootURL string
|
||||
uiDir string
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
forceUpdate func()
|
||||
onError func(err error)
|
||||
getTime func() time.Time
|
||||
}
|
||||
|
||||
// NewHandler returns a Handler object
|
||||
func NewHandler(rootURL, uiDir string, db data.Database, logger logging.Logger,
|
||||
forceUpdate func(), onError func(err error)) Handler {
|
||||
return &handler{
|
||||
rootURL: rootURL,
|
||||
uiDir: uiDir,
|
||||
db: db,
|
||||
logger: logger,
|
||||
forceUpdate: forceUpdate,
|
||||
onError: onError,
|
||||
getTime: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHandlerFunc returns a router with all the necessary routes configured
|
||||
func (h *handler) GetHandlerFunc() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h.logger.Info("received HTTP request at %s", r.RequestURI)
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/":
|
||||
// TODO: Forms to change existing updates or add some
|
||||
t := template.Must(template.ParseFiles(h.uiDir + "/ui/index.html"))
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := html.ConvertRecord(record, h.getTime())
|
||||
htmlData.Rows = append(htmlData.Rows, row)
|
||||
}
|
||||
if err := t.ExecuteTemplate(w, "index.html", htmlData); err != nil {
|
||||
h.logger.Warn(err)
|
||||
fmt.Fprint(w, "An error occurred creating this webpage")
|
||||
}
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/update":
|
||||
h.logger.Info("Update started manually")
|
||||
h.forceUpdate()
|
||||
http.Redirect(w, r, h.rootURL, 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
54
internal/health/check.go
Normal file
54
internal/health/check.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type lookupIPFunc func(host string) ([]net.IP, error)
|
||||
|
||||
func MakeIsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) func() error {
|
||||
return func() (err error) {
|
||||
return isHealthy(db, lookupIP)
|
||||
}
|
||||
}
|
||||
|
||||
// isHealthy checks all the records were updated successfully and returns an error if not.
|
||||
func isHealthy(db data.Database, lookupIP lookupIPFunc) (err error) {
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%s", record.String())
|
||||
} else if record.Settings.Proxied() {
|
||||
continue
|
||||
}
|
||||
hostname := record.Settings.BuildDomainName()
|
||||
lookedUpIPs, err := lookupIP(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("no database set IP address found for %s", hostname)
|
||||
}
|
||||
found := false
|
||||
lookedUpIPsString := make([]string, len(lookedUpIPs))
|
||||
for i, lookedUpIP := range lookedUpIPs {
|
||||
lookedUpIPsString[i] = lookedUpIP.String()
|
||||
if lookedUpIP.Equal(currentIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s",
|
||||
hostname, strings.Join(lookedUpIPsString, ","), currentIP)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
internal/health/client.go
Normal file
57
internal/health/client.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func IsClientMode(args []string) bool {
|
||||
return len(args) > 1 && args[1] == "healthcheck"
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Query(ctx context.Context, port uint16) error
|
||||
}
|
||||
|
||||
type client struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
func NewClient() Client {
|
||||
const timeout = 5 * time.Second
|
||||
return &client{
|
||||
Client: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
var ErrParseHealthServerAddress = errors.New("cannot parse health server address")
|
||||
|
||||
// Query sends an HTTP request to the other instance of
|
||||
// the program, and to its internal healthcheck server.
|
||||
func (c *client) Query(ctx context.Context, port uint16) error {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+strconv.Itoa(int(port)), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%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)
|
||||
}
|
||||
52
internal/health/server.go
Normal file
52
internal/health/server.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type Server interface {
|
||||
Run(ctx context.Context, done chan<- struct{})
|
||||
}
|
||||
|
||||
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, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
server := http.Server{Addr: s.address, Handler: s.handler}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.logger.Warn("shutting down (context canceled)")
|
||||
defer s.logger.Warn("shut down")
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||
s.logger.Error("failed shutting down: %s", err)
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on %s", s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err)
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type lookupIPFunc func(host string) ([]net.IP, error)
|
||||
|
||||
// IsHealthy checks all the records were updated successfully and returns an error if not
|
||||
func IsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logger.Warn("unhealthy: %s", err)
|
||||
}
|
||||
}()
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%s", record.String())
|
||||
} else if record.Settings.NoDNSLookup {
|
||||
continue
|
||||
}
|
||||
lookedUpIPs, err := lookupIP(record.Settings.BuildDomainName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("no set IP address found")
|
||||
}
|
||||
for _, lookedUpIP := range lookedUpIPs {
|
||||
if !lookedUpIP.Equal(currentIP) {
|
||||
return fmt.Errorf(
|
||||
"lookup IP address of %s is %s instead of %s",
|
||||
record.Settings.BuildDomainName(), lookedUpIP, currentIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
const NotAvailable = "N/A"
|
||||
row := models.HTMLRow{
|
||||
Domain: convertDomain(record.Settings.BuildDomainName()),
|
||||
Host: models.HTML(record.Settings.Host),
|
||||
Provider: convertProvider(record.Settings.Provider),
|
||||
IPMethod: convertIPMethod(record.Settings.IPMethod, record.Settings.Provider),
|
||||
}
|
||||
message := record.Message
|
||||
if record.Status == constants.UPTODATE {
|
||||
message = "no IP change for " + record.History.GetDurationSinceSuccess(now)
|
||||
}
|
||||
if len(message) > 0 {
|
||||
message = fmt.Sprintf("(%s)", message)
|
||||
}
|
||||
if len(record.Status) == 0 {
|
||||
row.Status = NotAvailable
|
||||
} else {
|
||||
row.Status = models.HTML(fmt.Sprintf("%s %s, %s",
|
||||
convertStatus(record.Status),
|
||||
message,
|
||||
time.Since(record.Time).Round(time.Second).String()+" ago"))
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP != nil {
|
||||
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
|
||||
} else {
|
||||
row.CurrentIP = NotAvailable
|
||||
}
|
||||
previousIPs := record.History.GetPreviousIPs()
|
||||
row.PreviousIPs = NotAvailable
|
||||
if len(previousIPs) > 0 {
|
||||
var previousIPsStr []string
|
||||
const maxPreviousIPs = 2
|
||||
for i, previousIP := range previousIPs {
|
||||
if i == maxPreviousIPs {
|
||||
previousIPsStr = append(previousIPsStr, fmt.Sprintf("and %d more", len(previousIPs)-i))
|
||||
break
|
||||
}
|
||||
previousIPsStr = append(previousIPsStr, previousIP.String())
|
||||
}
|
||||
row.PreviousIPs = models.HTML(strings.Join(previousIPsStr, ", "))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func convertStatus(status models.Status) models.HTML {
|
||||
switch status {
|
||||
case constants.SUCCESS:
|
||||
return constants.HTMLSuccess
|
||||
case constants.FAIL:
|
||||
return constants.HTMLFail
|
||||
case constants.UPTODATE:
|
||||
return constants.HTMLUpdate
|
||||
case constants.UPDATING:
|
||||
return constants.HTMLUpdating
|
||||
default:
|
||||
return "Unknown status"
|
||||
}
|
||||
}
|
||||
|
||||
func convertProvider(provider models.Provider) models.HTML {
|
||||
switch provider {
|
||||
case constants.NAMECHEAP:
|
||||
return constants.HTMLNamecheap
|
||||
case constants.GODADDY:
|
||||
return constants.HTMLGodaddy
|
||||
case constants.DUCKDNS:
|
||||
return constants.HTMLDuckDNS
|
||||
case constants.DREAMHOST:
|
||||
return constants.HTMLDreamhost
|
||||
case constants.CLOUDFLARE:
|
||||
return constants.HTMLCloudflare
|
||||
case constants.NOIP:
|
||||
return constants.HTMLNoIP
|
||||
case constants.DNSPOD:
|
||||
return constants.HTMLDNSPod
|
||||
case constants.INFOMANIAK:
|
||||
return constants.HTMLInfomaniak
|
||||
case constants.DDNSSDE:
|
||||
return constants.HTMLDdnssde
|
||||
default:
|
||||
s := string(provider)
|
||||
if strings.HasPrefix("https://", s) {
|
||||
shorterName := strings.TrimPrefix(s, "https://")
|
||||
shorterName = strings.TrimSuffix(shorterName, "/")
|
||||
return models.HTML(fmt.Sprintf("<a href=\"%s\">%s</a>", s, shorterName))
|
||||
}
|
||||
return models.HTML(string(provider))
|
||||
}
|
||||
}
|
||||
|
||||
func convertIPMethod(ipMethod models.IPMethod, provider models.Provider) models.HTML {
|
||||
// TODO map to icons
|
||||
switch ipMethod {
|
||||
case constants.PROVIDER:
|
||||
return convertProvider(provider)
|
||||
case constants.OPENDNS:
|
||||
return constants.HTMLOpenDNS
|
||||
case constants.IFCONFIG:
|
||||
return constants.HTMLIfconfig
|
||||
case constants.IPINFO:
|
||||
return constants.HTMLIpinfo
|
||||
case constants.IPIFY:
|
||||
return constants.HTMLIpify
|
||||
case constants.IPIFY6:
|
||||
return constants.HTMLIpify6
|
||||
case constants.DDNSS:
|
||||
return constants.HTMLDdnss
|
||||
case constants.DDNSS4:
|
||||
return constants.HTMLDdnss4
|
||||
case constants.DDNSS6:
|
||||
return constants.HTMLDdnss6
|
||||
case constants.CYCLE:
|
||||
return constants.HTMLCycle
|
||||
default:
|
||||
return models.HTML(string(ipMethod))
|
||||
}
|
||||
}
|
||||
|
||||
func convertDomain(domain string) models.HTML {
|
||||
return models.HTML("<a href=\"http://" + domain + "\">" + domain + "</a>")
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
// Provider is a possible DNS provider.
|
||||
Provider string
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
IPMethod string
|
||||
// Status is the record config status
|
||||
// Status is the record config status.
|
||||
Status string
|
||||
// HTML is for constants HTML strings
|
||||
// HTML is for constants HTML strings.
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6
|
||||
IPVersion string
|
||||
)
|
||||
|
||||
7
internal/models/build.go
Normal file
7
internal/models/build.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
type BuildInformation struct {
|
||||
Version string `json:"version"`
|
||||
Commit string `json:"commit"`
|
||||
BuildDate string `json:"buildDate"`
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// History contains current and previous IP address for a particular record
|
||||
// with the latest success time
|
||||
// with the latest success time.
|
||||
type History []HistoryEvent // current and previous ips
|
||||
|
||||
type HistoryEvent struct { // current and previous ips
|
||||
@@ -23,13 +23,14 @@ func (h History) GetPreviousIPs() []net.IP {
|
||||
return nil
|
||||
}
|
||||
IPs := make([]net.IP, len(h)-1)
|
||||
for i := len(h) - 2; i >= 0; i-- {
|
||||
const two = 2
|
||||
for i := len(h) - two; i >= 0; i-- {
|
||||
IPs[i] = h[i].IP
|
||||
}
|
||||
return IPs
|
||||
}
|
||||
|
||||
// GetCurrentIP returns the current IP address (latest in history)
|
||||
// GetCurrentIP returns the current IP address (latest in history).
|
||||
func (h History) GetCurrentIP() net.IP {
|
||||
if len(h) < 1 {
|
||||
return nil
|
||||
@@ -37,7 +38,7 @@ func (h History) GetCurrentIP() net.IP {
|
||||
return h[len(h)-1].IP
|
||||
}
|
||||
|
||||
// GetSuccessTime returns the latest success update time
|
||||
// GetSuccessTime returns the latest success update time.
|
||||
func (h History) GetSuccessTime() time.Time {
|
||||
if len(h) < 1 {
|
||||
return time.Time{}
|
||||
@@ -50,6 +51,7 @@ func (h History) GetDurationSinceSuccess(now time.Time) string {
|
||||
return "N/A"
|
||||
}
|
||||
duration := now.Sub(h[len(h)-1].Time)
|
||||
const hoursInDay = 24
|
||||
switch {
|
||||
case duration < time.Minute:
|
||||
return fmt.Sprintf("%ds", int(duration.Round(time.Second).Seconds()))
|
||||
@@ -58,7 +60,7 @@ func (h History) GetDurationSinceSuccess(now time.Time) string {
|
||||
case duration < 24*time.Hour:
|
||||
return fmt.Sprintf("%dh", int(duration.Round(time.Hour).Hours()))
|
||||
default:
|
||||
return fmt.Sprintf("%dd", int(duration.Round(time.Hour*24).Hours()/24))
|
||||
return fmt.Sprintf("%dd", int(duration.Round(time.Hour*hoursInDay).Hours()/hoursInDay))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ type HTMLRow struct {
|
||||
Domain HTML
|
||||
Host HTML
|
||||
Provider HTML
|
||||
IPMethod HTML
|
||||
IPVersion HTML
|
||||
Status HTML
|
||||
CurrentIP HTML
|
||||
PreviousIPs HTML
|
||||
|
||||
9
internal/models/ipmethod.go
Normal file
9
internal/models/ipmethod.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
// IPMethod is a method to obtain your public IP address.
|
||||
type IPMethod struct {
|
||||
Name string
|
||||
URL string
|
||||
IPv4 bool
|
||||
IPv6 bool
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Record contains all the information to update and display a DNS record
|
||||
type Record struct { // internal
|
||||
Settings Settings // fixed
|
||||
History History // past information
|
||||
Status Status
|
||||
Message string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// NewRecord returns a new Record with settings and some history
|
||||
func NewRecord(settings Settings, events []HistoryEvent) Record {
|
||||
return Record{
|
||||
Settings: settings,
|
||||
History: events,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Record) String() string {
|
||||
status := string(r.Status)
|
||||
if len(r.Message) > 0 {
|
||||
status += " (" + r.Message + ")"
|
||||
}
|
||||
return fmt.Sprintf("%s: %s %s; %s", r.Settings.String(), status, r.Time.Format("2006-01-02 15:04:05 MST"), r.History.String())
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Settings contains the elements to update the DNS record
|
||||
// nolint: maligned
|
||||
type Settings struct {
|
||||
Domain string
|
||||
Host string
|
||||
Provider Provider
|
||||
IPMethod IPMethod
|
||||
IPVersion IPVersion
|
||||
Delay time.Duration
|
||||
NoDNSLookup bool
|
||||
// Provider dependent fields
|
||||
Password string // Namecheap, Infomaniak, DDNSS and NoIP only
|
||||
Key string // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string // GoDaddy only
|
||||
Token string // Cloudflare and DuckDNS only
|
||||
Email string // Cloudflare only
|
||||
UserServiceKey string // Cloudflare only
|
||||
ZoneIdentifier string // Cloudflare only
|
||||
Identifier string // Cloudflare only
|
||||
Proxied bool // Cloudflare only
|
||||
TTL uint // Cloudflare only
|
||||
Username string // NoIP, Infomaniak, DDNSS only
|
||||
}
|
||||
|
||||
func (settings *Settings) String() string {
|
||||
b, _ := json.Marshal(
|
||||
struct {
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Provider string `json:"provider"`
|
||||
}{
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
string(settings.Provider),
|
||||
},
|
||||
)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// BuildDomainName builds the domain name from the domain and the host of the settings
|
||||
func (settings *Settings) BuildDomainName() string {
|
||||
switch settings.Host {
|
||||
case "@":
|
||||
return settings.Domain
|
||||
case "*":
|
||||
return "any." + settings.Domain
|
||||
default:
|
||||
return settings.Host + "." + settings.Domain
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
// GetPublicIP downloads a webpage and extracts the IP address from it
|
||||
func GetPublicIP(client network.Client, url string, ipVersion models.IPVersion) (ip net.IP, err error) {
|
||||
content, status, err := client.GetContent(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: %s", ipVersion, url, err)
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d", ipVersion, url, status)
|
||||
}
|
||||
verifier := verification.NewVerifier()
|
||||
regexSearch := verifier.SearchIPv4
|
||||
if ipVersion == constants.IPv6 {
|
||||
regexSearch = verifier.SearchIPv6
|
||||
}
|
||||
ips := regexSearch(string(content))
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no public %s address found at %s", ipVersion, url)
|
||||
} else if len(ips) > 1 {
|
||||
return nil, fmt.Errorf("multiple public %s addresses found at %s: %s", ipVersion, url, strings.Join(ips, " "))
|
||||
}
|
||||
ip = net.ParseIP(ips[0])
|
||||
if ip == nil { // in case the regex is not restrictive enough
|
||||
return nil, fmt.Errorf("Public IP address %q found at %s is not valid", ips[0], url)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network/mock_network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetPublicIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
IPVersion models.IPVersion
|
||||
mockContent []byte
|
||||
mockStatus int
|
||||
mockErr error
|
||||
ip net.IP
|
||||
err error
|
||||
}{
|
||||
"network error": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockErr: fmt.Errorf("error"),
|
||||
err: fmt.Errorf("cannot get public ipv4 address from https://getmyip.com: error"),
|
||||
},
|
||||
"bad status": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockStatus: http.StatusUnauthorized,
|
||||
err: fmt.Errorf("cannot get public ipv4 address from https://getmyip.com: HTTP status code 401"),
|
||||
},
|
||||
"no IPs in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte(""),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("no public ipv4 address found at https://getmyip.com"),
|
||||
},
|
||||
"multiple IPs in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("10.10.10.10 50.50.50.50"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("multiple public ipv4 addresses found at https://getmyip.com: 10.10.10.10 50.50.50.50"),
|
||||
},
|
||||
"single IP in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("10.10.10.10"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{10, 10, 10, 10},
|
||||
},
|
||||
"single IPv6 in content": {
|
||||
IPVersion: constants.IPv6,
|
||||
mockContent: []byte("::fe"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfe},
|
||||
},
|
||||
}
|
||||
const URL = "https://getmyip.com"
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
client := mock_network.NewMockClient(mockCtrl)
|
||||
client.EXPECT().GetContent(URL).Return(tc.mockContent, tc.mockStatus, tc.mockErr).Times(1)
|
||||
ip, err := GetPublicIP(client, URL, tc.IPVersion)
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// BuildHTTPPut is used for GoDaddy and Cloudflare only
|
||||
func BuildHTTPPut(url string, body interface{}) (request *http.Request, err error) {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err = http.NewRequest(http.MethodPut, url, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
return request, nil
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func settingsGeneralChecks(settings models.Settings, matchDomain func(s string) bool) error {
|
||||
switch {
|
||||
case !ipMethodIsValid(settings.IPMethod):
|
||||
return fmt.Errorf("IP method %q is not recognized", settings.IPMethod)
|
||||
case settings.IPVersion != constants.IPv4 && settings.IPVersion != constants.IPv6:
|
||||
return fmt.Errorf("IP version %q is not recognized", settings.IPVersion)
|
||||
case !matchDomain(settings.Domain):
|
||||
return fmt.Errorf("invalid domain name format")
|
||||
case len(settings.Host) == 0:
|
||||
return fmt.Errorf("host cannot be empty")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func settingsIPVersionChecks(ipVersion models.IPVersion, ipMethod models.IPMethod, provider models.Provider) error {
|
||||
switch ipVersion {
|
||||
case constants.IPv4:
|
||||
switch ipMethod {
|
||||
case constants.IPIFY6, constants.DDNSS6:
|
||||
return fmt.Errorf("IP method %s is only for IPv6 addresses", ipMethod)
|
||||
}
|
||||
case constants.IPv6:
|
||||
switch ipMethod {
|
||||
case constants.IPIFY, constants.DDNSS4:
|
||||
return fmt.Errorf("IP method %s is only for IPv4 addresses", ipMethod)
|
||||
}
|
||||
switch provider {
|
||||
case constants.GODADDY, constants.CLOUDFLARE, constants.DNSPOD, constants.DREAMHOST, constants.DUCKDNS, constants.NOIP:
|
||||
return fmt.Errorf("IPv6 support for %s is not supported yet", provider)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsIPMethodChecks(ipMethod models.IPMethod, provider models.Provider) error {
|
||||
if ipMethod == constants.PROVIDER {
|
||||
switch provider {
|
||||
case constants.GODADDY, constants.DREAMHOST, constants.CLOUDFLARE, constants.DNSPOD, constants.DDNSSDE:
|
||||
return fmt.Errorf("unsupported IP update method %q", ipMethod)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsNamecheapChecks(password string) error {
|
||||
if !constants.MatchNamecheapPassword(password) {
|
||||
return fmt.Errorf("invalid password format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsGoDaddyChecks(key, secret string) error {
|
||||
switch {
|
||||
case !constants.MatchGodaddyKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !constants.MatchGodaddySecret(secret):
|
||||
return fmt.Errorf("invalid secret format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDuckDNSChecks(token, host string) error {
|
||||
switch {
|
||||
case !constants.MatchDuckDNSToken(token):
|
||||
return fmt.Errorf("invalid token format")
|
||||
case host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDreamhostChecks(key, host string) error {
|
||||
switch {
|
||||
case !constants.MatchDreamhostKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsCloudflareChecks(key, email, userServiceKey, token, zoneIdentifier, identifier string, ttl uint, matchEmail func(s string) bool) error {
|
||||
switch {
|
||||
case len(key) > 0: // email and key must be provided
|
||||
switch {
|
||||
case !constants.MatchCloudflareKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !matchEmail(email):
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
case len(userServiceKey) > 0: // only user service key
|
||||
if !constants.MatchCloudflareKey(key) {
|
||||
return fmt.Errorf("invalid user service key format")
|
||||
}
|
||||
default: // API token only
|
||||
if !constants.MatchCloudflareToken(token) {
|
||||
return fmt.Errorf("invalid API token key format")
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(zoneIdentifier) == 0:
|
||||
return fmt.Errorf("zone identifier cannot be empty")
|
||||
case len(identifier) == 0:
|
||||
return fmt.Errorf("identifier cannot be empty")
|
||||
case ttl == 0:
|
||||
return fmt.Errorf("TTL cannot be left to 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsNoIPChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(username) > 50:
|
||||
return fmt.Errorf("username cannot be longer than 50 characters")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDNSPodChecks(token string) error {
|
||||
if len(token) == 0 {
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsInfomaniakChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDdnssdeChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *reader) isConsistent(settings models.Settings) error {
|
||||
if err := settingsGeneralChecks(settings, r.verifier.MatchDomain); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingsIPVersionChecks(settings.IPVersion, settings.IPMethod, settings.Provider); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingsIPMethodChecks(settings.IPMethod, settings.Provider); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Checks for each DNS provider
|
||||
switch settings.Provider {
|
||||
case constants.NAMECHEAP:
|
||||
if err := settingsNamecheapChecks(settings.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.GODADDY:
|
||||
if err := settingsGoDaddyChecks(settings.Key, settings.Secret); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DUCKDNS:
|
||||
if err := settingsDuckDNSChecks(settings.Token, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DREAMHOST:
|
||||
if err := settingsDreamhostChecks(settings.Key, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.CLOUDFLARE:
|
||||
if err := settingsCloudflareChecks(settings.Key, settings.Email, settings.UserServiceKey, settings.Token, settings.ZoneIdentifier, settings.Identifier, settings.TTL, r.verifier.MatchEmail); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.NOIP:
|
||||
if err := settingsNoIPChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DNSPOD:
|
||||
if err := settingsDNSPodChecks(settings.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.INFOMANIAK:
|
||||
if err := settingsInfomaniakChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DDNSSDE:
|
||||
if err := settingsDdnssdeChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("provider %q is not supported", settings.Provider)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipMethodIsValid(ipMethod models.IPMethod) bool {
|
||||
for _, possibility := range constants.IPMethodChoices() {
|
||||
if ipMethod == possibility {
|
||||
return true
|
||||
}
|
||||
}
|
||||
url, err := url.Parse(string(ipMethod))
|
||||
if err != nil || url == nil || url.Scheme != "https" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ipMethodIsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
valid bool
|
||||
}{
|
||||
"empty method": {
|
||||
ipMethod: "",
|
||||
valid: false,
|
||||
},
|
||||
"non existing method": {
|
||||
ipMethod: "abc",
|
||||
valid: false,
|
||||
},
|
||||
"existing method": {
|
||||
ipMethod: "opendns",
|
||||
valid: true,
|
||||
},
|
||||
"http url": {
|
||||
ipMethod: "http://ipinfo.io/ip",
|
||||
valid: false,
|
||||
},
|
||||
"https url": {
|
||||
ipMethod: "https://ipinfo.io/ip",
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid := ipMethodIsValid(tc.ipMethod)
|
||||
assert.Equal(t, tc.valid, valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,90 +2,130 @@ package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"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 settingsType struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
IPMethod string `json:"ip_method"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
Delay uint64 `json:"delay"`
|
||||
NoDNSLookup bool `json:"no_dns_lookup"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"` // Namecheap, NoIP only
|
||||
Key string `json:"key"` // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string `json:"secret"` // GoDaddy only
|
||||
Token string `json:"token"` // DuckDNS and Cloudflare only
|
||||
Email string `json:"email"` // Cloudflare only
|
||||
Username string `json:"username"` // NoIP only
|
||||
UserServiceKey string `json:"user_service_key"` // Cloudflare only
|
||||
ZoneIdentifier string `json:"zone_identifier"` // Cloudflare only
|
||||
Identifier string `json:"identifier"` // Cloudflare only
|
||||
Proxied bool `json:"proxied"` // Cloudflare only
|
||||
TTL uint `json:"ttl"` // Cloudflare only
|
||||
type commonSettings struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
// Retro values for warnings
|
||||
IPMethod *string `json:"ip_method,omitempty"`
|
||||
Delay *uint64 `json:"delay,omitempty"`
|
||||
}
|
||||
|
||||
// GetSettings obtain the update settings from config.json
|
||||
func (r *reader) GetSettings(filePath string) (settings []models.Settings, warnings []string, err error) {
|
||||
// JSONSettings obtain the update settings from the JSON content, first trying from the environment variable CONFIG
|
||||
// and then from the file config.json.
|
||||
func (r *reader) JSONSettings(filePath string, 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, logger)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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, logger)
|
||||
}
|
||||
|
||||
// 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 s == "" {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return extractAllSettings([]byte(s), logger)
|
||||
}
|
||||
|
||||
func extractAllSettings(jsonBytes []byte, logger log.Logger) (
|
||||
allSettings []settings.Settings, warnings []string, err error) {
|
||||
config := struct {
|
||||
CommonSettings []commonSettings `json:"settings"`
|
||||
}{}
|
||||
rawConfig := struct {
|
||||
Settings []json.RawMessage `json:"settings"`
|
||||
}{}
|
||||
if err := json.Unmarshal(jsonBytes, &config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := json.Unmarshal(jsonBytes, &rawConfig); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
matcher := regex.NewMatcher()
|
||||
|
||||
for i, common := range config.CommonSettings {
|
||||
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...)
|
||||
}
|
||||
|
||||
return allSettings, warnings, nil
|
||||
}
|
||||
|
||||
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 len(common.Domain) > 0 { // retro compatibility
|
||||
if len(common.Host) == 0 {
|
||||
common.Host = strings.TrimSuffix(common.Domain, ".duckdns.org")
|
||||
warnings = append(warnings,
|
||||
fmt.Sprintf("DuckDNS record should have %q specified as host instead of %q as domain",
|
||||
common.Host, common.Domain))
|
||||
} else {
|
||||
warnings = append(warnings,
|
||||
fmt.Sprintf("ignoring domain %q because host %q is specified for DuckDNS record",
|
||||
common.Domain, common.Host))
|
||||
}
|
||||
}
|
||||
}
|
||||
hosts := strings.Split(common.Host, ",")
|
||||
|
||||
if len(common.IPVersion) == 0 {
|
||||
common.IPVersion = ipversion.IP4or6.String()
|
||||
}
|
||||
ipVersion, err := ipversion.Parse(common.IPVersion)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var config struct {
|
||||
Settings []settingsType `json:"settings"`
|
||||
|
||||
settingsSlice = make([]settings.Settings, len(hosts))
|
||||
for i, host := range hosts {
|
||||
settingsSlice[i], err = settings.New(provider, rawSettings, common.Domain,
|
||||
host, ipVersion, matcher, logger)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(bytes, &config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, s := range config.Settings {
|
||||
switch models.Provider(s.Provider) {
|
||||
case constants.DREAMHOST, constants.DUCKDNS:
|
||||
s.Host = "@" // only choice available
|
||||
}
|
||||
ipMethod := models.IPMethod(s.IPMethod)
|
||||
// Retro compatibility
|
||||
if ipMethod == constants.GOOGLE {
|
||||
r.logger.Warn("IP Method %q is no longer valid, please change it. Defaulting it to %s", constants.GOOGLE, constants.CYCLE)
|
||||
ipMethod = constants.CYCLE
|
||||
}
|
||||
ipVersion := models.IPVersion(s.IPVersion)
|
||||
if len(ipVersion) == 0 {
|
||||
ipVersion = constants.IPv4 // default
|
||||
}
|
||||
setting := models.Settings{
|
||||
Provider: models.Provider(s.Provider),
|
||||
Domain: s.Domain,
|
||||
Host: s.Host,
|
||||
IPMethod: ipMethod,
|
||||
IPVersion: ipVersion,
|
||||
Delay: time.Second * time.Duration(s.Delay),
|
||||
NoDNSLookup: s.NoDNSLookup,
|
||||
Password: s.Password,
|
||||
Key: s.Key,
|
||||
Secret: s.Secret,
|
||||
Token: s.Token,
|
||||
Email: s.Email,
|
||||
Username: s.Username,
|
||||
UserServiceKey: s.UserServiceKey,
|
||||
ZoneIdentifier: s.ZoneIdentifier,
|
||||
Identifier: s.Identifier,
|
||||
Proxied: s.Proxied,
|
||||
TTL: s.TTL,
|
||||
}
|
||||
if err := r.isConsistent(setting); err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("%s for settings %s", err, setting.String()))
|
||||
continue
|
||||
}
|
||||
settings = append(settings, setting)
|
||||
}
|
||||
if len(settings) == 0 {
|
||||
return nil, warnings, fmt.Errorf("no settings found in config.json")
|
||||
}
|
||||
return settings, warnings, nil
|
||||
return settingsSlice, warnings, nil
|
||||
}
|
||||
|
||||
@@ -1,104 +1,30 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
"os"
|
||||
|
||||
"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/golibs/logging"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
GetSettings(filePath string) (settings []models.Settings, warnings []string, err error)
|
||||
GetDataDir(currentDir string) (string, error)
|
||||
GetListeningPort() (listeningPort, warning string, err error)
|
||||
GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error)
|
||||
GetGotifyURL(setters ...libparams.GetEnvSetter) (URL *url.URL, err error)
|
||||
GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error)
|
||||
GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error)
|
||||
GetDelay(setters ...libparams.GetEnvSetter) (duration time.Duration, err error)
|
||||
GetExeDir() (dir string, err error)
|
||||
GetHTTPTimeout() (duration time.Duration, err error)
|
||||
GetBackupPeriod() (duration time.Duration, err error)
|
||||
GetBackupDirectory() (directory string, err error)
|
||||
|
||||
// Version getters
|
||||
GetVersion() string
|
||||
GetBuildDate() string
|
||||
GetVcsRef() string
|
||||
JSONSettings(filePath string, logger log.Logger) (allSettings []settings.Settings, warnings []string, err error)
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
envParams libparams.EnvParams
|
||||
verifier verification.Verifier
|
||||
logger logging.Logger
|
||||
env params.Env
|
||||
readFile func(filename string) ([]byte, error)
|
||||
writeFile func(filename string, data []byte, perm fs.FileMode) (err error)
|
||||
}
|
||||
|
||||
func NewReader(logger logging.Logger) Reader {
|
||||
return &reader{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
verifier: verification.NewVerifier(),
|
||||
logger: logger,
|
||||
env: params.NewEnv(),
|
||||
readFile: ioutil.ReadFile,
|
||||
writeFile: os.WriteFile,
|
||||
}
|
||||
}
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
||||
func (r *reader) GetListeningPort() (listeningPort, warning string, err error) {
|
||||
return r.envParams.GetListeningPort()
|
||||
}
|
||||
|
||||
func (r *reader) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
|
||||
return r.envParams.GetLoggerConfig()
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyURL(setters ...libparams.GetEnvSetter) (url *url.URL, err error) {
|
||||
return r.envParams.GetGotifyURL()
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error) {
|
||||
return r.envParams.GetGotifyToken()
|
||||
}
|
||||
|
||||
func (r *reader) GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error) {
|
||||
return r.envParams.GetRootURL()
|
||||
}
|
||||
|
||||
func (r *reader) GetDelay(setters ...libparams.GetEnvSetter) (period time.Duration, err error) {
|
||||
// Backward compatibility
|
||||
n, err := r.envParams.GetEnvInt("DELAY", libparams.Compulsory()) // TODO change to PERIOD
|
||||
if err == nil { // integer only, treated as seconds
|
||||
r.logger.Warn("The value for the duration period of the updater does not have a time unit, you might want to set it to \"%ds\" instead of \"%d\"", n, n)
|
||||
return time.Duration(n) * time.Second, nil
|
||||
}
|
||||
return r.envParams.GetDuration("DELAY", setters...)
|
||||
}
|
||||
|
||||
func (r *reader) GetExeDir() (dir string, err error) {
|
||||
return r.envParams.GetExeDir()
|
||||
}
|
||||
|
||||
func (r *reader) GetHTTPTimeout() (duration time.Duration, err error) {
|
||||
return r.envParams.GetHTTPTimeout(libparams.Default("10s"))
|
||||
}
|
||||
|
||||
func (r *reader) GetBackupPeriod() (duration time.Duration, err error) {
|
||||
s, err := r.envParams.GetEnv("BACKUP_PERIOD", libparams.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"))
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user