1 Commits

Author SHA1 Message Date
Quentin McGaw (desktop)
70edf86d39 Fix: SHOUTRRR_ADDRESSES case sensitivity 2021-09-12 01:14:22 +00:00
313 changed files with 7867 additions and 16018 deletions

View File

@@ -9,21 +9,17 @@ It works on Linux, Windows and OSX.
- [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. Create the following files on your host if you don't have them:
```sh
touch ~/.gitconfig ~/.zsh_history
```
Note that the development container will create the empty directories `~/.docker`, `~/.ssh` and `~/.kube` if you don't have them.
1. **For Docker on OSX or Windows without WSL**: ensure your home directory `~` is accessible by Docker.
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
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
@@ -33,9 +29,13 @@ You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image.
```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`
@@ -47,11 +47,11 @@ You can customize **settings** and **extensions** in the [devcontainer.json](dev
### Entrypoint script
You can bind mount a shell script to `/root/.welcome.sh` to replace the [current welcome script](https://github.com/qdm12/basedevcontainer/blob/master/shell/.welcome.sh).
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). You can also now do it directly with VSCode without restarting the container.
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml).
### Run other services

View File

@@ -1,75 +1,79 @@
{
"name": "ddns-dev",
"dockerComposeFile": [
"docker-compose.yml"
],
"dockerComposeFile": ["docker-compose.yml"],
"service": "vscode",
"runServices": [
"vscode"
],
"runServices": ["vscode"],
"shutdownAction": "stopCompose",
"postCreateCommand": "~/.windows.sh && go mod download && go mod tidy",
"postCreateCommand": "source ~/.windows.sh && go mod download && go mod tidy",
"workspaceFolder": "/workspace",
// "overrideCommand": "",
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"eamodio.gitlens", // IDE Git information
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker", // Docker integration and linting
"shardulm94.trailing-spaces", // Show trailing spaces
"Gruntfuggly.todo-tree", // Highlights TODO comments
"bierner.emojisense", // Emoji sense for markdown
"stkb.rewrap", // rewrap comments after n characters on one line
"vscode-icons-team.vscode-icons", // Better file extension icons
"github.vscode-pull-request-github", // Github interaction
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
"IBM.output-colorizer", // Colorize your output/test logs
"github.copilot" // AI code completion
// "mohsen1.prettify-json", // Prettify JSON data
// "zxh404.vscode-proto3", // Supports Proto syntax
// "jrebocho.vscode-random", // Generates random values
// "alefragnani.Bookmarks", // Manage bookmarks
// "quicktype.quicktype", // Paste JSON as code
// "spikespaz.vscode-smoothtype", // smooth cursor animation
],
"settings": {
"files.eol": "\n",
"editor.formatOnSave": true,
"go.buildTags": "",
"go.toolsEnvVars": {
"CGO_ENABLED": "0"
},
"go.useLanguageServer": true,
"go.testEnvVars": {
"CGO_ENABLED": "1"
},
"go.testFlags": [
"-v",
"-race"
],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
"go.coverOnTestPackage": true,
"go.lintTool": "golangci-lint",
"go.lintOnSave": "package",
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
"gopls": {
"usePlaceholders": false,
"staticcheck": true,
"vulncheck": "Imports"
},
"remote.extensionKind": {
"ms-azuretools.vscode-docker": "workspace"
}
}
}
"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,
}
}

View File

@@ -3,19 +3,24 @@ version: "3.7"
services:
vscode:
build: .
image: godevcontainer
volumes:
- ../:/workspace
# 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
# On Linux and OSX, a symlink /mnt/ssh <-> ~/.ssh is
# created in the container. On Windows, files are copied
# from /mnt/ssh to ~/.ssh to fix permissions.
- ~/.ssh:/mnt/ssh
- ~/.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
- ~/.zsh_history:/root/.zsh_history:z
# Git config
- ~/.gitconfig:/root/.gitconfig
- ~/.gitconfig:/root/.gitconfig:z
# Kubernetes
- ~/.kube:/root/.kube:z
environment:
- TZ=
cap_add:
@@ -24,4 +29,4 @@ services:
security_opt:
# For debugging with dlv
- seccomp:unconfined
entrypoint: [ "zsh", "-c", "while sleep 1000; do :; done" ]
entrypoint: zsh -c "while sleep 1000; do :; done"

View File

@@ -4,9 +4,9 @@
.vscode
docs
readme
!readme/*.go
.gitignore
config.json
docker-compose.yml
LICENSE
README.md
ui/favicon.svg

View File

@@ -1,8 +1,9 @@
---
name: Bug
about: Report a bug
title: 'Bug: FILL THIS TEXT OR ISSUE WILL BE CLOSED'
labels:
title: 'Bug: ...'
labels: ":bug: bug"
assignees: qdm12
---

View File

@@ -1,8 +1,9 @@
---
name: Feature request
about: Suggest a feature to add to this project
title: 'Feature request: FILL THIS TEXT OR ISSUE WILL BE CLOSED'
labels:
title: 'Feature request: ...'
labels: ":bulb: feature request"
assignees: qdm12
---

View File

@@ -1,8 +1,9 @@
---
name: Help
about: Ask for help
title: 'Help: FILL THIS TEXT OR ISSUE WILL BE CLOSED'
labels:
title: 'Help: ...'
labels: ":pray: help wanted"
assignees:
---

View File

@@ -1,15 +0,0 @@
version: 2
updates:
# Maintain dependencies for GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: docker
directory: /
schedule:
interval: "daily"
- package-ecosystem: gomod
directory: /
schedule:
interval: "daily"

113
.github/labels.yml vendored
View File

@@ -1,62 +1,51 @@
- name: "Status: 🗯️ Waiting for feedback"
color: "f7d692"
- name: "Status: 🔴 Blocked"
color: "f7d692"
description: "Blocked by another issue or pull request"
- name: "Status: 🔒 After next release"
color: "f7d692"
description: "Will be done after the next release"
- name: "Closed: ⚰️ Inactive"
color: "959a9c"
description: "No answer was received for weeks"
- name: "Closed: 👥 Duplicate"
color: "959a9c"
description: "Issue duplicates an existing issue"
- name: "Closed: 🗑️ Bad issue"
color: "959a9c"
- name: "Priority: 🚨 Urgent"
color: "03adfc"
- name: "Priority: 💤 Low priority"
color: "03adfc"
- name: "Complexity: ☣️ Hard to do"
color: "ff9efc"
- name: "Complexity: 🟩 Easy to do"
color: "ff9efc"
- name: "Category: Config problem 📝"
color: "ffc7ea"
- name: "Category: Healthcheck 🩺"
color: "ffc7ea"
- name: "Category: Documentation ✒️"
description: "A problem with the readme or in the docs/ directory"
color: "ffc7ea"
- name: "Category: Maintenance ⛓️"
description: "Anything related to code or other maintenance"
color: "ffc7ea"
- name: "Category: Good idea 🎯"
description: "This is a good idea, judged by the maintainers"
color: "ffc7ea"
- name: "Category: Motivated! 🙌"
description: "Your pumpness makes me pumped! The issue or PR shows great motivation!"
color: "ffc7ea"
- name: "Category: Foolproof settings 👼"
color: "ffc7ea"
- name: "Category: Label missing ❗"
color: "ffc7ea"
- name: "Category: Provider update ♻️"
color: "ffc7ea"
- name: "Category: Shoutrrr 📢"
color: "ffc7ea"
- name: "Category: IP fetching 📥"
color: "ffc7ea"
- name: "Category: Database 🗃️"
color: "ffc7ea"
- name: "Category: New provider 🆕"
color: "ffc7ea"
- name: "Category: Web UI 🖱️"
color: "ffc7ea"
- name: "Category: Wildcard 🃏"
color: "ffc7ea"
- name: ":robot: bot"
color: "69cde9"
description: ""
- name: ":bug: bug"
color: "b60205"
description: ""
- name: ":game_die: dependencies"
color: "0366d6"
description: ""
- name: ":memo: documentation"
color: "c5def5"
description: ""
- name: ":busts_in_silhouette: duplicate"
color: "cccccc"
description: ""
- name: ":sparkles: enhancement"
color: "0054ca"
description: ""
- name: ":bulb: feature request"
color: "0e8a16"
description: ""
- name: ":mega: feedback"
color: "03a9f4"
description: ""
- name: ":rocket: future maybe"
color: "fef2c0"
description: ""
- name: ":hatching_chick: good first issue"
color: "7057ff"
description: ""
- name: ":pray: help wanted"
color: "4caf50"
description: ""
- name: ":hand: hold"
color: "24292f"
description: ""
- name: ":no_entry_sign: invalid"
color: "e6e6e6"
description: ""
- name: ":interrobang: maybe bug"
color: "ff5722"
description: ""
- name: ":thinking: needs more info"
color: "795548"
description: ""
- name: ":question: question"
color: "3f51b5"
description: ""
- name: ":coffin: wontfix"
color: "ffffff"
description: ""

View File

@@ -1,35 +0,0 @@
name: No trigger file paths
on:
push:
branches:
- master
paths-ignore:
- .github/workflows/build.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
pull_request:
paths-ignore:
- .github/workflows/build.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: No trigger path triggered for required verify workflow.
run: exit 0

View File

@@ -1,11 +1,6 @@
name: CI
on:
release:
types:
- published
push:
branches:
- master
paths:
- .github/workflows/build.yml
- cmd/**
@@ -31,26 +26,16 @@ on:
jobs:
verify:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v3
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
exclude: |
*.md
- uses: actions/checkout@v2
- name: Linting
run: docker build --target lint .
- name: Mocks check
run: docker build --target mocks .
- name: Go mod tidy check
run: docker build --target tidy .
- name: Build test image
run: docker build --target test -t test-container .
@@ -62,102 +47,55 @@ jobs:
-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 -t final-image .
codeql:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v3
- uses: github/codeql-action/init@v3
with:
languages: go
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
run: docker build .
publish:
if: |
github.repository == 'qdm12/ddns-updater' &&
(
github.event_name == 'push' ||
github.event_name == 'release' ||
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]')
)
needs: [verify, codeql]
permissions:
actions: read
contents: write
packages: write
needs: [verify]
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # for gorelease last step
- uses: actions/checkout@v2
# extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
latest=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
images: |
ghcr.io/qdm12/ddns-updater
qmcgaw/ddns-updater
tags: |
type=ref,event=pr
type=semver,pattern={{major}}.{{minor}}.{{patch}}
type=semver,pattern=v{{major}}.{{minor}}.{{patch}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern=v{{major}}.{{minor}}
type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=semver,pattern=v{{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v2
- uses: docker/login-action@v1
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ github.token }}
- name: Short commit
id: shortcommit
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
- 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@v5.1.0
uses: docker/build-push-action@v2
with:
platforms: linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ steps.vars.outputs.platforms }}
build-args: |
CREATED=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
COMMIT=${{ steps.shortcommit.outputs.value }}
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
tags: ${{ steps.meta.outputs.tags }}
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_name == 'release'
uses: actions/setup-go@v2
with:
go-version: 1.21
- if: github.event_name == 'release'
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --clean --config .github/workflows/configs/.goreleaser.yaml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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

View File

@@ -1,30 +0,0 @@
before:
hooks:
- go mod download
builds:
- main: ./cmd/updater/main.go
flags:
- -trimpath
env:
- CGO_ENABLED=0
targets:
# See https://goreleaser.com/customization/build/
- linux_amd64
- linux_386
- linux_arm64
- linux_arm_7
- linux_arm_6
- linux_arm_5
- darwin_amd64
- darwin_arm64
- windows_amd64
- windows_386
- windows_arm64
archives:
- format: binary
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc

View File

@@ -1,35 +0,0 @@
{
"ignorePatterns": [
{
"pattern": "^http://localhost"
},
{
"pattern": "^https://api6.ipify.org$"
},
{
"pattern": "^http://ip1.dynupdate6.no-ip.com$"
},
{
"pattern": "^https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns$"
},
{
"pattern": "^https://www.godaddy.com"
},
{
"pattern": "^https://www.namecheap.com"
},
{
"pattern": "https://www.linode.com/docs/products/tools/api/guides/manage-api-tokens/"
},
{
"pattern": "https://ipv6.ipleak.net/json"
}
],
"timeout": "20s",
"retryOn429": false,
"fallbackRetryDelay": "30s",
"aliveStatusCodes": [
200,
206
]
}

View File

@@ -0,0 +1,19 @@
name: Docker Hub description
on:
push:
branches: [master]
paths:
- README.md
- .github/workflows/dockerhub-description.yml
jobs:
dockerHubDescription:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v2.1.0
env:
DOCKERHUB_USERNAME: qmcgaw
DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
DOCKERHUB_REPOSITORY: qmcgaw/ddns-updater

View File

@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v2
- name: Labeler
if: success()
uses: crazy-max/ghaction-github-labeler@v4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
uses: crazy-max/ghaction-github-labeler@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,21 +0,0 @@
name: Markdown
on:
push:
branches:
- master
paths-ignore:
- "**.md"
- .github/workflows/markdown.yml
pull_request:
paths-ignore:
- "**.md"
- .github/workflows/markdown.yml
jobs:
markdown:
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: No trigger path triggered for required markdown workflow.
run: exit 0

View File

@@ -1,48 +0,0 @@
name: Markdown
on:
push:
branches:
- master
paths:
- "**.md"
- .github/workflows/markdown.yml
pull_request:
paths:
- "**.md"
- .github/workflows/markdown.yml
jobs:
markdown:
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v14
with:
globs: "**.md"
config: .markdownlint.json
- uses: reviewdog/action-misspell@v1
with:
locale: "US"
level: error
pattern: |
*.md
- uses: gaurav-nelson/github-action-markdown-link-check@v1
with:
use-quiet-mode: yes
config-file: .github/workflows/configs/mlc-config.json
- uses: peter-evans/dockerhub-description@v3
if: github.repository == 'qdm12/ddns-updater' && github.event_name == 'push'
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
repository: qmcgaw/ddns-updater
short-description: Container to update DNS records periodically with WebUI for many DNS providers
readme-filepath: README.md
enable-url-completion: true

1
.gitignore vendored
View File

@@ -1 +0,0 @@
/data

View File

@@ -6,34 +6,27 @@ linters-settings:
issues:
exclude-rules:
- path: _test\.go
- path: cmd/updater/main.go
text: "mnd: Magic number: 4, in <argument> detected"
linters:
- containedctx
- dupl
- goerr113
- gomnd
- path: cmd/updater/main.go
text: "mnd: Magic number: 2, in <argument> detected"
linters:
- gomnd
linters:
disable-all: true
enable:
# - cyclop
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- decorder
- deadcode
- dogsled
- dupl
- dupword
- durationcheck
- errchkjson
- errname
- errorlint
- execinquery
- errcheck
- exhaustive
- exportloopref
- forcetypeassert
- gci
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gocognit
@@ -41,43 +34,37 @@ linters:
- gocritic
- gocyclo
- godot
- goerr113
- goheader
- goimports
- gomnd
- gomoddirectives
- goprintffuncname
- gosec
- grouper
# - goerr113 # TODO
- gosimple
- govet
- importas
- interfacebloat
- ireturn
- ineffassign
- lll
- maintidx
- makezero
- misspell
- musttag
- nakedret
- nestif
- nilerr
- nilnil
- noctx
- nolintlint
- nosprintfhostport
- paralleltest
- prealloc
- predeclared
- promlinter
- reassign
- revive
- rowserrcheck
- exportloopref
- sqlclosecheck
- tenv
- staticcheck
- structcheck
- thelper
- tparallel
- typecheck
- unconvert
- unparam
- usestdlibvars
- unused
- varcheck
- wastedassign
- whitespace

View File

@@ -1,8 +0,0 @@
{
"MD013": false,
"MD033": {
"allowed_elements": [
"img"
]
}
}

View File

@@ -1,22 +1,24 @@
ARG BUILDPLATFORM=linux/amd64
ARG ALPINE_VERSION=3.19
ARG GO_VERSION=1.21
ARG ALPINE_VERSION=3.13
ARG GO_VERSION=1.16
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.55.2
ARG MOCKGEN_VERSION=v1.6.0
ARG GOLANGCI_LINT_VERSION=v1.41.1
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:mockgen-${MOCKGEN_VERSION} AS mockgen
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
ENV CGO_ENABLED=0
# Note: findutils needed to have xargs support `-d` flag for mocks stage.
RUN apk --update add git g++ findutils
RUN apk --update add git g++
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
COPY --from=golangci-lint /bin /go/bin/golangci-lint
COPY --from=mockgen /bin /go/bin/mockgen
# Copy repository code and install Go dependencies
COPY go.mod go.sum ./
RUN go mod download
@@ -29,45 +31,40 @@ FROM --platform=$BUILDPLATFORM base AS test
# - we set CGO_ENABLED=1 to have it enabled
# - we installed g++ to support the race detector
ENV CGO_ENABLED=1
COPY readme/ ./readme/
COPY README.md ./README.md
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 mocks
FROM --platform=$BUILDPLATFORM base AS tidy
RUN git init && \
git config user.email ci@localhost && \
git config user.name ci && \
git config core.fileMode false && \
git add -A && \
git commit -m "snapshot" && \
grep -lr -E '^// Code generated by MockGen\. DO NOT EDIT\.$' . | xargs -r -d '\n' rm && \
go generate -run "mockgen" ./... && \
git diff --exit-code && \
rm -rf .git/
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 CREATED="an unknown date"
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.date=$CREATED' \
-X 'main.buildDate=$BUILD_DATE' \
-X 'main.commit=$COMMIT' \
" -o app cmd/updater/main.go
FROM scratch
COPY --from=alpine --chown=1000 /tmp/data /updater/data/
COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8000
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
ARG UID=1000
ARG GID=1000
USER ${UID}:${GID}
USER 1000
ENTRYPOINT ["/updater/app"]
ENV \
# Core
@@ -82,33 +79,31 @@ ENV \
PUBLICIP_DNS_TIMEOUT=3s \
HTTP_TIMEOUT=10s \
DATADIR=/updater/data \
RESOLVER_ADDRESS= \
RESOLVER_TIMEOUT=5s \
# Web UI
LISTENING_ADDRESS=:8000 \
LISTENING_PORT=8000 \
ROOT_URL=/ \
# Backup
BACKUP_PERIOD=0 \
BACKUP_DIRECTORY=/updater/data \
# Other
LOG_LEVEL=info \
LOG_CALLER=hidden \
SHOUTRRR_ADDRESSES= \
SHOUTRRR_DEFAULT_TITLE="DDNS Updater" \
TZ= \
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
HEALTH_HEALTHCHECKSIO_UUID=
TZ=
ARG VERSION=unknown
ARG CREATED="an unknown date"
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=$CREATED \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.revision=$COMMIT \
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.title="ddns-updater" \
org.opencontainers.image.description="Universal DNS updater with WebUI"
COPY --from=build --chown=${UID}:${GID} /tmp/gobuild/app /updater/app
COPY --from=build --chown=1000 /tmp/gobuild/app /updater/app

664
README.md
View File

@@ -1,368 +1,296 @@
# Lightweight universal DDNS Updater with Docker and web UI
Light container updating DNS A and/or AAAA records periodically for multiple DNS providers
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg">
[![Build status](https://github.com/qdm12/ddns-updater/actions/workflows/build.yml/badge.svg)](https://github.com/qdm12/ddns-updater/actions/workflows/build.yml)
[![dockeri.co](https://dockeri.co/image/qmcgaw/ddns-updater)](https://hub.docker.com/r/qmcgaw/ddns-updater)
![Last release](https://img.shields.io/github/release/qdm12/ddns-updater?label=Last%20release)
![Last Docker tag](https://img.shields.io/docker/v/qmcgaw/ddns-updater?sort=semver&label=Last%20Docker%20tag)
[![Last release size](https://img.shields.io/docker/image-size/qmcgaw/ddns-updater?sort=semver&label=Last%20released%20image)](https://hub.docker.com/r/qmcgaw/ddns-updater/tags?page=1&ordering=last_updated)
![GitHub last release date](https://img.shields.io/github/release-date/qdm12/ddns-updater?label=Last%20release%20date)
![Commits since release](https://img.shields.io/github/commits-since/qdm12/ddns-updater/latest?sort=semver)
[![Latest size](https://img.shields.io/docker/image-size/qmcgaw/ddns-updater/latest?label=Latest%20image)](https://hub.docker.com/r/qmcgaw/ddns-updater/tags)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/commits/main)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/graphs/contributors)
[![GitHub closed PRs](https://img.shields.io/github/issues-pr-closed/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/pulls?q=is%3Apr+is%3Aclosed)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/issues)
[![GitHub closed issues](https://img.shields.io/github/issues-closed/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/issues?q=is%3Aissue+is%3Aclosed)
[![Lines of code](https://img.shields.io/tokei/lines/github/qdm12/ddns-updater)](https://github.com/qdm12/ddns-updater)
![Code size](https://img.shields.io/github/languages/code-size/qdm12/ddns-updater)
![GitHub repo size](https://img.shields.io/github/repo-size/qdm12/ddns-updater)
![Go version](https://img.shields.io/github/go-mod/go-version/qdm12/ddns-updater)
[![MIT](https://img.shields.io/github/license/qdm12/ddns-updater)](LICENSE)
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=ddns-updater.readme)
## Features
- Updates periodically A records for different DNS providers:
- Aliyun
- AllInkl
- Cloudflare
- DD24
- DDNSS.de
- deSEC
- DigitalOcean
- DonDominio
- DNSOMatic
- DNSPod
- Dreamhost
- DuckDNS
- DynDNS
- Dynu
- EasyDNS
- FreeDNS
- Gandi
- GCP
- GoDaddy
- GoIP.de
- He.net
- Hetzner
- Infomaniak
- INWX
- Ionos
- Linode
- LuaDNS
- Name.com
- Namecheap
- Netcup
- NoIP
- Now-DNS
- Njalla
- OpenDNS
- OVH
- Porkbun
- Selfhost.de
- Servercow.de
- Spdyn
- Strato.de
- Variomedia.de
- Zoneedit
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
- Web User interface
![Web UI](https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/webui.png)
- 11MB Docker image based on a Go static binary in a Scratch Docker image
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
- Docker healthcheck verifying the DNS resolution of your domains
- Highly configurable
- Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/v0.8/services/overview/) using `SHOUTRRR_ADDRESSES`
- Compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures.
## Setup
The program reads the configuration from a JSON object, either from a file or from an environment variable.
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
```sh
mkdir data
touch data/config.json
# Owned by user ID of Docker container (1000)
chown -R 1000 data
# all access (for creating json database file data/updates.json)
chmod 700 data
# read access only
chmod 400 data/config.json
```
If you want to use another user ID, [build the image yourself](#build-the-image) with `--build-arg UID=<your-uid>`. You could also just run the container as root with `--user="0"` but this is not advised security wise.
1. Write a JSON configuration in *data/config.json*, for example:
```json
{
"settings": [
{
"provider": "namecheap",
"domain": "example.com",
"host": "@",
"password": "e5322165c1d74692bfa6d807100c0310"
}
]
}
```
You can find more information in the [configuration section](#configuration) to customize it.
1. Run the container with
```sh
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
```
1. (Optional) You can also set your JSON configuration as a single environment variable line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`), which takes precedence over config.json. Note however that if you don't bind mount the `/updater/data` directory, there won't be a persistent database file `/updater/updates.json` but it will still work.
### Next steps
#### Docker-Compose
You can also use [docker-compose.yml](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).
#### Kubernetes
Check out the [k8s directory](k8s) for an installation guide and examples.
### GHCR
Images are also added to the Github Container Registry. To use the GHCR container replace `qmcgaw/ddns-updater` to `ghcr.io/qdm12/ddns-updater`, further details are available [here](https://github.com/qdm12/ddns-updater/pkgs/container/ddns-updater)
## Configuration
Start by having the following content in *config.json*, or in your `CONFIG` environment variable:
```json
{
"settings": [
{
"provider": "",
},
{
"provider": "",
}
]
}
```
For each setting, you need to fill in parameters.
Check the documentation for your DNS provider:
- [Aliyun](docs/aliyun.md)
- [Cloudflare](docs/cloudflare.md)
- [Custom](docs/custom.md)
- [DDNSS.de](docs/ddnss.de.md)
- [deSEC](docs/desec.md)
- [DigitalOcean](docs/digitalocean.md)
- [DD24](docs/dd24.md)
- [DonDominio](docs/dondominio.md)
- [DNSOMatic](docs/dnsomatic.md)
- [DNSPod](docs/dnspod.md)
- [Dreamhost](docs/dreamhost.md)
- [DuckDNS](docs/duckdns.md)
- [DynDNS](docs/dyndns.md)
- [Dynu](docs/dynu.md)
- [DynV6](docs/dynv6.md)
- [EasyDNS](docs/easydns.md)
- [FreeDNS](docs/freedns.md)
- [Gandi](docs/gandi.md)
- [GCP](docs/gcp.md)
- [GoDaddy](docs/godaddy.md)
- [GoIP.de](docs/goip.md)
- [He.net](docs/he.net.md)
- [Infomaniak](docs/infomaniak.md)
- [INWX](docs/inwx.md)
- [Ionos](docs/ionos.md)
- [Linode](docs/linode.md)
- [LuaDNS](docs/luadns.md)
- [Name.com](docs/name.com.md)
- [Namecheap](docs/namecheap.md)
- [Netcup](docs/netcup.md)
- [NoIP](docs/noip.md)
- [Now-DNS](docs/nowdns.md)
- [Njalla](docs/njalla.md)
- [OpenDNS](docs/opendns.md)
- [OVH](docs/ovh.md)
- [Porkbun](docs/porkbun.md)
- [Selfhost.de](docs/selfhost.de.md)
- [Servercow.de](docs/servercow.md)
- [Spdyn](docs/spdyn.md)
- [Strato.de](docs/strato.md)
- [Variomedia.de](docs/variomedia.md)
- [Zoneedit](docs/zoneedit.md)
Note that:
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
### Environment variables
🆕 There are now flags equivalent for each variable below, for example `--ipv6-prefix`.
| Environment variable | Default | Description |
| --- | --- | --- |
| `CONFIG` | | One line JSON object containing the entire config (takes precedence over config.json file) if specified |
| `PERIOD` | `5m` | Default period of IP address check, following [this format](https://golang.org/pkg/time/#ParseDuration) |
| `PUBLICIP_FETCHERS` | `all` | Comma separated fetcher types to obtain the public IP address from `http` and `dns` |
| `PUBLICIP_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (ipv4 or ipv6). See the [Public IP section](#public-ip) |
| `PUBLICIPV4_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv4 address only. See the [Public IP section](#public-ip) |
| `PUBLICIPV6_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv6 address only. See the [Public IP section](#public-ip) |
| `PUBLICIP_DNS_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (IPv4 and/or IPv6). See the [Public IP section](#public-ip) |
| `PUBLICIP_DNS_TIMEOUT` | `3s` | Public IP DNS query timeout |
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
| `LISTENING_ADDRESS` | `: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 |
| `HEALTH_HEALTHCHECKSIO_UUID` | | UUID for [healthchecks.io](https://healthchecks.io) to send a heartbeat on every update check |
| `DATADIR` | `/updater/data` | Directory to read and write data files from internally |
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`. |
| `RESOLVER_ADDRESS` | Your network DNS | A plaintext DNS address to use, such as `1.1.1.1:53`. This is useful for split dns, see [#389](https://github.com/qdm12/ddns-updater/issues/389) |
| `LOG_LEVEL` | `info` | Level of logging, `debug`, `info`, `warning` or `error` |
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
| `SHOUTRRR_ADDRESSES` | | (optional) Comma separated list of [Shoutrrr addresses](https://containrrr.dev/shoutrrr/v0.8/services/overview/) (notification services) |
| `SHOUTRRR_DEFAULT_TITLE` | `DDNS Updater` | Default title for Shoutrrr notifications |
| `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:
- `ipify` using [https://api64.ipify.org](https://api64.ipify.org)
- `ifconfig` using [https://ifconfig.io/ip](https://ifconfig.io/ip)
- `ipinfo` using [https://ipinfo.io/ip](https://ipinfo.io/ip)
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
- `spdyn` using [https://checkip.spdyn.de](https://checkip.spdyn.de/)
- `ipleak` using [https://ipleak.net/json](https://ipleak.net/json)
- 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:
- `ipleak` using [https://ipv4.ipleak.net/json](https://ipv4.ipleak.net/json)
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
- 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:
- `ipleak` using [https://ipv6.ipleak.net/json](https://ipv6.ipleak.net/json)
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
- 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:
- `cloudflare`
- `opendns`
### Host firewall
If you have a host firewall in place, this container needs the following ports:
- TCP 443 outbound for outbound HTTPS
- UDP 53 outbound for outbound DNS resolution
- TCP 8000 inbound (or other) for the WebUI
## Architecture
At program start and every period (5 minutes by default):
1. Fetch your public IP address
1. For each record:
1. DNS resolve it to obtain its current IP address(es)
- If the resolution fails, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address is within the resolved IP addresses
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
💡 We do DNS resolution every period so it detects a change made to the record manually, for example on the DNS provider web UI
💡 As DNS resolutions are essentially free and without rate limiting, these are great to avoid getting banned for too many requests.
### Special case: Cloudflare
For Cloudflare records with the `proxied` option, the following is done.
At program start and every period (5 minutes by default), for each record:
1. Fetch your public IP address
1. For each record:
1. Check the last IP address (persisted in `updates.json`) for that record
- If it doesn't exist, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address matches the last IP address you updated the record with
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
This is the only way as doing a DNS resolution on the record will give the IP address of a Cloudflare server instead of your server.
⚠️ This has the disadvantage that if the record is changed manually, the program will not detect it.
We could do an API call to get the record IP address every period, but that would get you banned especially with a low period duration.
## Testing
- The automated healthcheck verifies all your records are up to date [using DNS lookups](internal/health/check.go#L42)
- You can also manually check, by:
1. Going to your DNS management webpage
1. Setting your record to `127.0.0.1`
1. Run the container
1. Refresh the DNS management webpage and verify the update happened
## Build the image
You can build the image yourself with:
```sh
docker build -t qmcgaw/ddns-updater https://github.com/qdm12/ddns-updater.git
```
You can use optional build arguments with `--build-arg KEY=VALUE` from the table below:
| Build argument | Default | Description |
| --- | --- | --- |
| `UID` | `1000` | User ID running the container |
| `GID` | `1000` | User group ID running the container |
| `VERSION` | `unknown` | Version of the program and Docker image |
| `CREATED` | `an unknown date` | Build date of the program and Docker image |
| `COMMIT` | `unknown` | Commit hash of the program and Docker image |
## Development and contributing
- [Contribute with code](docs/contributing.md)
- [Github workflows to know what's building](https://github.com/qdm12/ddns-updater/actions)
- [List of issues and feature requests](https://github.com/qdm12/ddns-updater/issues)
- [Kanban board](https://github.com/qdm12/ddns-updater/projects/1)
## License
This repository is under an [MIT license](LICENSE)
## Used in external projects
- [Starttoaster/docker-traefik](https://github.com/Starttoaster/docker-traefik#home-networks-extra-credit-dynamic-dns)
## Support
Sponsor me on [Github](https://github.com/sponsors/qdm12) or donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
Many thanks to J. Famiglietti for supporting me financially 🥇👍
# Lightweight universal DDNS Updater with Docker and web UI
*Light container updating DNS A and/or AAAA records periodically for multiple DNS providers*
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg?sanitize=true">
[![Build status](https://github.com/qdm12/ddns-updater/workflows/Buildx%20latest/badge.svg)](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
[![Docker Pulls](https://img.shields.io/docker/pulls/qmcgaw/ddns-updater.svg)](https://hub.docker.com/r/qmcgaw/ddns-updater)
[![Docker Stars](https://img.shields.io/docker/stars/qmcgaw/ddns-updater.svg)](https://hub.docker.com/r/qmcgaw/ddns-updater)
[![Image size](https://images.microbadger.com/badges/image/qmcgaw/ddns-updater.svg)](https://microbadger.com/images/qmcgaw/ddns-updater)
[![Image version](https://images.microbadger.com/badges/version/qmcgaw/ddns-updater.svg)](https://microbadger.com/images/qmcgaw/ddns-updater)
[![Join Slack channel](https://img.shields.io/badge/slack-@qdm12-yellow.svg?logo=slack)](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc)
[![GitHub last commit](https://img.shields.io/github/last-commit/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/issues)
[![GitHub commit activity](https://img.shields.io/github/commit-activity/y/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/issues)
[![GitHub issues](https://img.shields.io/github/issues/qdm12/ddns-updater.svg)](https://github.com/qdm12/ddns-updater/issues)
## Features
- 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
![Web UI](https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/webui.png)
- 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
- Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/services/overview/) using `SHOUTRRR_ADDRESSES`
- Compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures.
## Setup
The program reads the configuration from a JSON object, either from a file or from an environment variable.
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
```sh
mkdir data
touch data/config.json
# Owned by user ID of Docker container (1000)
chown -R 1000 data
# all access (for creating json database file data/updates.json)
chmod 700 data
# read access only
chmod 400 data/config.json
```
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
1. Write a JSON configuration in *data/config.json*, for example:
```json
{
"settings": [
{
"provider": "namecheap",
"domain": "example.com",
"host": "@",
"password": "e5322165c1d74692bfa6d807100c0310"
}
]
}
```
You can find more information in the [configuration section](#configuration) to customize it.
1. Run the container with
```sh
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
```
1. ⚠️ If you use IPv6, you might need to set `-e IPV6_PREFIX=/64` (`/64` is your prefix, depending on your ISP)
1. (Optional) You can also set your JSON configuration as a single environment variable line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`), which takes precedence over config.json. Note however that if you don't bind mount the `/updater/data` directory, there won't be a persistent database file `/updater/updates.json` but it will still work.
### Next steps
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
```sh
docker-compose up -d
```
You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
## Configuration
Start by having the following content in *config.json*, or in your `CONFIG` environment variable:
```json
{
"settings": [
{
"provider": "",
},
{
"provider": "",
}
]
}
```
For each setting, you need to fill in parameters.
Check the documentation for your DNS provider:
- [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)
Note that:
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
### Environment variables
| Environment variable | Default | Description |
| --- | --- | --- |
| `CONFIG` | | One line JSON object containing the entire config (takes precendence over config.json file) if specified |
| `PERIOD` | `5m` | Default period of IP address check, following [this format](https://golang.org/pkg/time/#ParseDuration) |
| `IPV6_PREFIX` | `/128` | IPv6 prefix used to mask your public IPv6 address and your record IPv6 address. Ranges from `/0` to `/128` depending on your ISP. |
| `PUBLICIP_FETCHERS` | `all` | Comma separated fetcher types to obtain the public IP address from `http` and `dns` |
| `PUBLICIP_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (ipv4 or ipv6). See the [Public IP section](#Public-IP) |
| `PUBLICIPV4_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv4 address only. See the [Public IP section](#Public-IP) |
| `PUBLICIPV6_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv6 address only. See the [Public IP section](#Public-IP) |
| `PUBLICIP_DNS_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (IPv4 and/or IPv6). See the [Public IP section](#Public-IP) |
| `PUBLICIP_DNS_TIMEOUT` | `3s` | Public IP DNS query timeout |
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
| `HEALTH_SERVER_ADDRESS` | `127.0.0.1:9999` | Health server listening address |
| `DATADIR` | `/updater/data` | Directory to read and write data files from internally |
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`. |
| `LOG_LEVEL` | `info` | Level of logging, `debug`, `info`, `warning` or `error` |
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
| `SHOUTRRR_ADDRESSES` | | (optional) Comma separated list of [Shoutrrr addresses](https://containrrr.dev/shoutrrr/services/overview/) (notification services) |
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
#### Public IP
By default, all public IP fetching types are used and cycled (over DNS and over HTTPs).
On top of that, for each fetching method, all echo services available are cycled on each request.
This allows you not to be blocked for making too many requests.
You can otherwise customize it with the following:
- `PUBLICIP_HTTP_PROVIDERS` gets your public IPv4 or IPv6 address. It can be one or more of the following:
- `opendns` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip)
- `ifconfig` using [https://ifconfig.io/ip](https://ifconfig.io/ip)
- `ipinfo` using [https://ipinfo.io/ip](https://ipinfo.io/ip)
- `ddnss` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
- `PUBLICIPV4_HTTP_PROVIDERS` gets your public IPv4 address only. It can be one or more of the following:
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
- `PUBLICIPV6_HTTP_PROVIDERS` gets your public IPv6 address only. It can be one or more of the following:
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
- `noip` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
- `PUBLICIP_DNS_PROVIDERS` gets your public IPv4 address only or IPv6 address only or one of them (see #136). It can be one or more of the following:
- `google`
- `cloudflare`
### Host firewall
If you have a host firewall in place, this container needs the following ports:
- TCP 443 outbound for outbound HTTPS
- UDP 53 outbound for outbound DNS resolution
- TCP 8000 inbound (or other) for the WebUI
## Architecture
At program start and every period (5 minutes by default):
1. Fetch your public IP address
1. For each record:
1. DNS resolve it to obtain its current IP address(es)
- If the resolution fails, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address is within the resolved IP addresses
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
💡 We do DNS resolution every period so it detects a change made to the record manually, for example on the DNS provider web UI
💡 As DNS resolutions are essentially free and without rate limiting, these are great to avoid getting banned for too many requests.
### Special case: Cloudflare
For Cloudflare records with the `proxied` option, the following is done.
At program start and every period (5 minutes by default), for each record:
1. Fetch your public IP address
1. For each record:
1. Check the last IP address (persisted in `updates.json`) for that record
- If it doesn't exist, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address matches the last IP address you updated the record with
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
This is the only way as doing a DNS resolution on the record will give the IP address of a Cloudflare server instead of your server.
⚠️ This has the disadvantage that if the record is changed manually, the program will not detect it.
We could do an API call to get the record IP address every period, but that would get you banned especially with a low period duration.
## Testing
- The automated healthcheck verifies all your records are up to date [using DNS lookups](https://github.com/qdm12/ddns-updater/blob/master/internal/healthcheck/healthcheck.go#L15)
- You can also manually check, by:
1. Going to your DNS management webpage
1. Setting your record to `127.0.0.1`
1. Run the container
1. Refresh the DNS management webpage and verify the update happened
## Development and contributing
- [Contribute with code](https://github.com/qdm12/ddns-updater/blob/master/docs/contributing.md)
- [Github workflows to know what's building](https://github.com/qdm12/ddns-updater/actions)
- [List of issues and feature requests](https://github.com/qdm12/ddns-updater/issues)
- [Kanban board](https://github.com/qdm12/ddns-updater/projects/1)
## License
This repository is under an [MIT license](https://github.com/qdm12/ddns-updater/master/license)
## Used in external projects
- [Starttoaster/docker-traefik](https://github.com/Starttoaster/docker-traefik#home-networks-extra-credit-dynamic-dns)
## Support
Sponsor me on [Github](https://github.com/sponsors/qdm12) or donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
[![https://github.com/sponsors/qdm12](https://raw.githubusercontent.com/qdm12/private-internet-access-docker/master/doc/sponsors.jpg)](https://github.com/sponsors/qdm12)
[![https://www.paypal.me/qmcgaw](https://raw.githubusercontent.com/qdm12/private-internet-access-docker/master/doc/paypal.jpg)](https://www.paypal.me/qmcgaw)
Many thanks to J. Famiglietti for supporting me financially 🥇👍

View File

@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"net"
"net/http"
"os"
"os/signal"
@@ -12,48 +13,40 @@ import (
"time"
_ "time/tzdata"
_ "github.com/breml/rootcerts"
"github.com/containrrr/shoutrrr"
"github.com/qdm12/ddns-updater/internal/backup"
"github.com/qdm12/ddns-updater/internal/config"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/health"
"github.com/qdm12/ddns-updater/internal/healthchecksio"
"github.com/qdm12/ddns-updater/internal/models"
jsonparams "github.com/qdm12/ddns-updater/internal/params"
persistence "github.com/qdm12/ddns-updater/internal/persistence/json"
"github.com/qdm12/ddns-updater/internal/persistence"
recordslib "github.com/qdm12/ddns-updater/internal/records"
"github.com/qdm12/ddns-updater/internal/resolver"
"github.com/qdm12/ddns-updater/internal/server"
"github.com/qdm12/ddns-updater/internal/shoutrrr"
"github.com/qdm12/ddns-updater/internal/splash"
"github.com/qdm12/ddns-updater/internal/update"
"github.com/qdm12/ddns-updater/pkg/publicip"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network/connectivity"
"github.com/qdm12/golibs/params"
"github.com/qdm12/goshutdown"
"github.com/qdm12/gosplash"
"github.com/qdm12/log"
)
//nolint:gochecknoglobals
var (
version = "unknown"
commit = "unknown"
date = "an unknown date"
version = "unknown"
commit = "unknown"
buildDate = "an unknown date"
)
func main() {
buildInfo := models.BuildInformation{
Version: version,
Commit: commit,
Date: date,
Version: version,
Commit: commit,
BuildDate: buildDate,
}
logger := log.New()
reader := reader.New(reader.Settings{
HandleDeprecatedKey: func(source, oldKey, newKey string) {
logger.Warnf("%q key %s is deprecated, please use %q instead",
source, oldKey, newKey)
},
})
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)
@@ -61,7 +54,7 @@ func main() {
errorCh := make(chan error)
go func() {
errorCh <- _main(ctx, reader, os.Args, logger, buildInfo, time.Now)
errorCh <- _main(ctx, env, os.Args, logger, buildInfo, time.Now)
}()
select {
@@ -74,7 +67,7 @@ func main() {
if err == nil { // expected exit such as healthcheck
os.Exit(0)
}
logger.Error(err.Error())
logger.Error(err)
cancel()
}
@@ -86,7 +79,7 @@ func main() {
<-timer.C
}
if err != nil {
logger.Error(err.Error())
logger.Error(err)
}
logger.Info("Shutdown successful")
case <-timer.C:
@@ -96,224 +89,178 @@ func main() {
os.Exit(1)
}
func _main(ctx context.Context, reader *reader.Reader, args []string, logger log.LoggerInterface,
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
var healthSettings config.Health
healthSettings.Read(reader)
healthSettings.SetDefaults()
err = healthSettings.Validate()
if err != nil {
return fmt.Errorf("health settings: %w", err)
}
client := health.NewClient()
return client.Query(ctx, *healthSettings.ServerAddress)
var healthConfig config.Health
_, err := healthConfig.Get(env)
if err != nil {
return err
}
if err := client.Query(ctx, healthConfig.Port); err != nil {
return err
}
return nil
}
announcementExp, err := time.Parse(time.RFC3339, "2023-07-15T00:00:00Z")
fmt.Println(splash.Splash(buildInfo))
var config config.Config
warnings, err := config.Get(env)
for _, warning := range warnings {
logger.Warn(warning)
}
if err != nil {
return err
}
splashSettings := gosplash.Settings{
User: "qdm12",
Repository: "ddns-updater",
Emails: []string{"quentin.mcgaw@gmail.com"},
Version: buildInfo.Version,
Commit: buildInfo.Commit,
BuildDate: buildInfo.Date,
Announcement: "Public IP dns provider GOOGLE, see https://github.com/qdm12/ddns-updater/issues/492",
AnnounceExp: announcementExp,
// Sponsor information
PaypalUser: "qmcgaw",
GithubSponsor: "qdm12",
}
for _, line := range gosplash.MakeLines(splashSettings) {
fmt.Println(line)
}
var config config.Config
err = config.Read(reader, logger)
// 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 {
return fmt.Errorf("reading settings: %w", err)
return err
}
config.SetDefaults()
err = config.Validate()
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 {
return fmt.Errorf("settings validation: %w", err)
}
logger.Patch(config.Logger.ToOptions()...)
logger.Info(config.String())
shoutrrrSettings := shoutrrr.Settings{
Addresses: config.Shoutrrr.Addresses,
DefaultTitle: config.Shoutrrr.DefaultTitle,
Logger: logger.New(log.SetComponent("shoutrrr")),
}
shoutrrrClient, err := shoutrrr.New(shoutrrrSettings)
if err != nil {
return fmt.Errorf("setting up Shoutrrr: %w", err)
}
persistentDB, err := persistence.NewDatabase(*config.Paths.DataDir)
if err != nil {
shoutrrrClient.Notify(err.Error())
notify(err.Error())
return err
}
jsonReader := jsonparams.NewReader(logger)
jsonFilepath := filepath.Join(*config.Paths.DataDir, "config.json")
providers, warnings, err := jsonReader.JSONProviders(jsonFilepath)
settings, warnings, err := jsonReader.JSONSettings(config.Paths.JSON, logger)
for _, w := range warnings {
logger.Warn(w)
shoutrrrClient.Notify(w)
notify(w)
}
if err != nil {
shoutrrrClient.Notify(err.Error())
notify(err.Error())
return err
}
L := len(providers)
L := len(settings)
switch L {
case 0:
logger.Warn("Found no setting to update record")
case 1:
logger.Info("Found single setting to update record")
default:
logger.Info("Found " + fmt.Sprint(len(providers)) + " settings to update records")
logger.Info("Found %d settings to update records", len(settings))
}
client := &http.Client{Timeout: config.Client.Timeout}
err = health.CheckHTTP(ctx, client)
if err != nil {
logger.Warn(err.Error())
connectivity := connectivity.NewConnectivity(net.DefaultResolver, client)
for _, err := range connectivity.Checks(ctx, "github.com") {
logger.Warn(err)
}
records := make([]recordslib.Record, len(providers))
for i, provider := range providers {
logger.Info("Reading history from database: domain " +
provider.Domain() + " host " + provider.Host() +
" " + provider.IPVersion().String())
events, err := persistentDB.GetEvents(provider.Domain(),
provider.Host(), provider.IPVersion())
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 {
shoutrrrClient.Notify(err.Error())
notify(err.Error())
return err
}
records[i] = recordslib.New(provider, events)
records[i] = recordslib.New(s, events)
}
defer client.CloseIdleConnections()
db := data.NewDatabase(records, persistentDB)
defer func() {
err := db.Close()
if err != nil {
logger.Error(err.Error())
if err := db.Close(); err != nil {
logger.Error(err)
}
}()
httpSettings := publicip.HTTPSettings{
Enabled: *config.PubIP.HTTPEnabled,
Client: client,
Options: config.PubIP.ToHTTPOptions(),
}
dnsSettings := publicip.DNSSettings{
Enabled: *config.PubIP.DNSEnabled,
Options: config.PubIP.ToDNSPOptions(),
}
config.PubIP.HTTPSettings.Client = client
ipGetter, err := publicip.NewFetcher(dnsSettings, httpSettings)
ipGetter, err := publicip.NewFetcher(config.PubIP.DNSSettings, config.PubIP.HTTPSettings)
if err != nil {
return err
}
resolverSettings := resolver.Settings{
Address: config.Resolver.Address,
Timeout: config.Resolver.Timeout,
}
resolver, err := resolver.New(resolverSettings)
if err != nil {
return fmt.Errorf("creating resolver: %w", err)
}
hioClient := healthchecksio.New(client, *config.Health.HealthchecksioUUID)
updater := update.NewUpdater(db, client, shoutrrrClient, logger, timeNow)
updater := update.NewUpdater(db, client, notify, logger)
runner := update.NewRunner(db, updater, ipGetter, config.Update.Period,
config.Update.Cooldown, logger, resolver, timeNow, hioClient)
config.IPv6.Mask, config.Update.Cooldown, logger, timeNow)
runnerHandler, runnerCtx, runnerDone := goshutdown.NewGoRoutineHandler("runner")
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, resolver)
healthLogger := logger.New(log.SetComponent("healthcheck server"))
healthServer := health.NewServer(*config.Health.ServerAddress,
healthLogger, isHealthy)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler("health server")
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)
serverLogger := logger.New(log.SetComponent("http server"))
server := server.New(ctx, config.Server.ListeningAddress, config.Server.RootURL,
db, serverLogger, runner)
serverHandler, serverCtx, serverDone := goshutdown.NewGoRoutineHandler("server")
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)
shoutrrrClient.Notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
backupHandler, backupCtx, backupDone := goshutdown.NewGoRoutineHandler("backup")
backupLogger := logger.New(log.SetComponent("backup"))
go backupRunLoop(backupCtx, backupDone, *config.Backup.Period, *config.Paths.DataDir,
*config.Backup.Directory, backupLogger, timeNow)
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("")
shutdownGroup := goshutdown.NewGroupHandler("", goshutdown.GroupSettings{})
shutdownGroup.Add(runnerHandler, healthServerHandler, serverHandler, backupHandler)
<-ctx.Done()
err = shutdownGroup.Shutdown(context.Background())
if err != nil {
shoutrrrClient.Notify(err.Error())
if err := shutdownGroup.Shutdown(context.Background()); err != nil {
notify(err.Error())
return err
}
return nil
}
type InfoErroer interface {
Info(s string)
Error(s string)
}
func backupRunLoop(ctx context.Context, done chan<- struct{}, backupPeriod time.Duration,
dataDir, outputDir string, logger InfoErroer, timeNow func() time.Time) {
dataDir, outputDir string, logger logging.Logger, timeNow func() time.Time) {
defer close(done)
if backupPeriod == 0 {
logger.Info("disabled")
return
}
logger.Info("each " + backupPeriod.String() +
"; writing zip files to directory " + outputDir)
logger.Info("each %s; writing zip files to directory %s", backupPeriod, outputDir)
ziper := backup.NewZiper()
timer := time.NewTimer(backupPeriod)
for {
fileName := "ddns-updater-backup-" + strconv.Itoa(int(timeNow().UnixNano())) + ".zip"
zipFilepath := filepath.Join(outputDir, fileName)
err := ziper.ZipFiles(
if err := ziper.ZipFiles(
zipFilepath,
filepath.Join(dataDir, "updates.json"),
filepath.Join(dataDir, "config.json"),
)
if err != nil {
logger.Error(err.Error())
); err != nil {
logger.Error(err)
}
select {
case <-timer.C:

View File

@@ -21,7 +21,7 @@ services:
- HTTP_TIMEOUT=10s
# Web UI
- LISTENING_ADDRESS=:8000
- LISTENING_PORT=8000
- ROOT_URL=/
# Backup

View File

@@ -1,35 +0,0 @@
# Aliyun
## Configuration
### Example
```json
{
"settings": [
{
"provider": "aliyun",
"domain": "domain.com",
"host": "@",
"access_key_id": "your access_key_id",
"access_secret": "your access_secret",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"access_key_id"`
- `"access_secret"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
## Domain setup

View File

@@ -1,35 +0,0 @@
# All-Inkl
## Configuration
### Example
```json
{
"settings": [
{
"provider": "allinkl",
"domain": "domain.com",
"host": "host",
"username": "dynXXXXXXX",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host (subdomain)
- `"username"` username (usually starts with dyn followed by numbers)
- `"password"` password in plain text
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
## Domain setup

View File

@@ -14,8 +14,7 @@
"host": "@",
"ttl": 600,
"token": "yourtoken",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -23,20 +22,36 @@
### Compulsory parameters
- `"zone_identifier"` is the Zone ID of your site, from the domain overview page written as *Zone ID*
- `"zone_identifier"` is the Zone ID of your site
- `"domain"`
- `"host"` is your host and can be `"@"`, a subdomain or the wildcard `"*"`.
See [this issue comment for context](https://github.com/qdm12/ddns-updater/issues/243#issuecomment-928313949). This is left as is for compatibility.
- `"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 ([how to find API keys](https://developers.cloudflare.com/fundamentals/api/get-started/)):
- 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
- 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) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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.

View File

@@ -2,9 +2,9 @@
## Table of content
1. [Setup](#setup)
1. [Commands available](#commands-available)
1. [Guidelines](#guidelines)
1. [Setup](#Setup)
1. [Commands available](#Commands-available)
1. [Guidelines](#Guidelines)
## Setup
@@ -29,7 +29,7 @@ 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).
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
@@ -47,6 +47,6 @@ go build -o app cmd/updater/main.go
## 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.
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.
See the [Contributing document](](../.github/CONTRIBUTING.md) for more information on how to contribute to this repository.

View File

@@ -1,42 +0,0 @@
# Custom provider
The custom provider allows to configure a URL with a few additional parameters to update your records.
For now it sends an HTTP GET request to the URL given with some additional parameters.
Feel free to open issues to extend its configuration options.
## Configuration
### Example
```json
{
"settings": [
{
"provider": "custom",
"domain": "example.com",
"host": "@",
"url": "https://example.com/update?domain=example.com&host=@&username=username&client_key=client_key",
"ipv4key": "ipv4",
"ipv6key": "ipv6",
"success_regex": "good",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"` is the domain name to update
- `"host"` is the host to update, which can be `"@"` (root), `"*"` or a subdomain
- `"url"` is the URL to update your records and should contain all the information EXCEPT the IP address to update
- `"ipv4key"` is the URL query parameter name for the IPv4 address, for example `ipv4` will be added to the URL with `&ipv4=1.2.3.4`.
- `"ipv6key"` is the URL query parameter name for the IPv6 address, for example `ipv6` will be added to the URL with `&ipv6=::aaff`. Even if you don't use IPv6, this must be set to something.
- `"success_regex"` is a regular expression to match the response from the server to determine if the update was successful. You can use [regex101.com](https://regex101.com/) to find the regular expression you want. For example `good` would match any response containing the word "good".
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

View File

@@ -12,8 +12,7 @@
"domain": "domain.com",
"host": "@",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -27,5 +26,4 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`

View File

@@ -14,9 +14,7 @@
"host": "@",
"username": "user",
"password": "password",
"dual_stack": false,
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -31,10 +29,6 @@
### Optional parameters
- `"dual_stack"` can be set to `true` **if you have turn on dual stack for your record** to update both IPv4 and IPv6 addresses. Note it is ignored if `"provider_ip": true`. More precisely:
- if it is `false`, the updates are done using the `ip` parameter and only one IP address can be set (ipv4 or ipv6, whichever is last sent).
- if it is `true`, the updates are done using the `ip` and `ip6` parameters, for IPv4 and IPv6 respectively, and both can be set on the same record
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -1,37 +0,0 @@
# deSEC
## Configuration
### Example
```json
{
"settings": [
{
"provider": "desec",
"domain": "dedyn.io",
"host": "host",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": false
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` can be `@` for the root domain or a subdomain or a wildcard subdomain (`*`), defaults to `@`
- `"token"` is your token that you can create [here](https://desec.io/tokens)
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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
[desec.io/domains](https://desec.io/domains)

View File

@@ -12,8 +12,7 @@
"domain": "domain.com",
"host": "@",
"token": "yourtoken",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -27,7 +26,6 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -11,11 +11,8 @@
"provider": "dnsomatic",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"provider_ip": true,
"ip_version": "ipv4",
"ipv6_suffix": ""
"token": "yourtoken",
"ip_version": "ipv4"
}
]
}
@@ -27,11 +24,9 @@
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"username"`
- `"password"`
- `"provider_ip"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -12,8 +12,7 @@
"domain": "domain.com",
"host": "@",
"token": "yourtoken",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -27,7 +26,6 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -10,12 +10,10 @@
{
"provider": "dondominio",
"domain": "domain.com",
"host": "@",
"name": "something",
"username": "username",
"key": "key",
"ip_version": "ipv4",
"ipv6_suffix": ""
"password": "password",
"ip_version": "ipv4"
}
]
}
@@ -24,16 +22,12 @@
### Compulsory parameters
- `"domain"`
- `"host"` is the subdomain to update which can be `@`, `*` or a subdomain
- `"name"` is the name of the service/hosting
- `"name"` is the name server associated with the domain
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup
See [dondominio.dev/en/dondns/docs/api/#before-start](https://dondominio.dev/en/dondns/docs/api/#before-start)

View File

@@ -10,10 +10,8 @@
{
"provider": "dreamhost",
"domain": "domain.com",
"host": "@",
"key": "key",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -26,8 +24,6 @@
### Optional parameters
- `"host"` is your host and can be a subdomain or `"@"`. It defaults to `"@"`.
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -12,7 +12,6 @@
"host": "host",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -26,12 +25,11 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (**NOT** your IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
- `"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
[![DuckDNS Website](../readme/duckdns.png)](https://www.duckdns.org/)
[![DuckDNS Website](../readme/duckdns.png)](https://duckdns.org)
*See the [duckdns website](https://www.duckdns.org/)*
*See the [duckdns website](https://duckdns.org)*

View File

@@ -12,9 +12,9 @@
"domain": "domain.com",
"host": "@",
"username": "username",
"client_key": "client_key",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": ""
"provider_ip": true
}
]
}
@@ -23,13 +23,13 @@
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"client_key"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -1,39 +0,0 @@
# Dynu
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dynu",
"domain": "domain.com",
"host": "@",
"group": "group",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"` could be plain text or password in MD5 or SHA256 format (There's also an option for setting a password for IP Update only)
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
- `"group"` specify the Group for which you want to set the IP (will update any domains and subdomains in the same group)
## Domain setup

View File

@@ -13,7 +13,6 @@
"host": "@",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -28,8 +27,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -1,37 +0,0 @@
# EasyDNS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "easydns",
"domain": "domain.com",
"host": "@",
"username": "username",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"username"`
- `"token"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -12,8 +12,7 @@
"domain": "domain.com",
"host": "host",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -27,10 +26,6 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup
This integration uses FreeDNS's v2 dynamic dns interface, which is not shown by default when you select `Dynamic DNS` from the side menu.
Instead you must go to [freedns.afraid.org/dynamic/v2/](https://freedns.afraid.org/dynamic/v2/) and enable dynamic DNS for the subdomains you wish and you will then see a url like `https://sync.afraid.org/u/token/` for each enabled subdomain.

View File

@@ -13,10 +13,9 @@ This provider uses Gandi v5 API
"provider": "gandi",
"domain": "domain.com",
"host": "@",
"personal_access_token": "token",
"key": "key",
"ttl": 3600,
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
@@ -26,14 +25,13 @@ This provider uses Gandi v5 API
- `"domain"`
- `"host"` which can be a subdomain, `@` or a wildcard `*`
- `"personal_access_token"`
- `"key"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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/rest_api/index.html)
[Gandi Documentation Website](https://docs.gandi.net/en/domain_names/advanced_users/api.html#gandi-s-api)

View File

@@ -1,39 +0,0 @@
# GCP
## Configuration
### Example
```json
{
"settings": [
{
"provider": "gcp",
"project": "my-project-id",
"zone": "zone",
"credentials": {
"type": "service_account",
"project_id": "my-project-id",
// ...
},
"domain": "domain.com",
"host": "@",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"project"` is the id of your Google Cloud project
- `"zone"` is the zone, that your DNS record is located in
- `"credentials"` is the JSON credentials for your Google Cloud project. This is usually downloaded as a JSON file, which you can copy paste the content as the value of the `"credentials"` key. More information on how to get it is available [here](https://cloud.google.com/docs/authentication/getting-started). Please ensure your service account has all necessary permissions to create/update/list/get DNS records within your project.
- `"domain"` is the TLD of you DNS record (without a trailing dot)
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

View File

@@ -13,8 +13,7 @@
"host": "@",
"key": "key",
"secret": "secret",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -29,12 +28,11 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup
[![GoDaddy Website](../readme/godaddy.png)](https://www.godaddy.com/en-ie)
[![GoDaddy Website](../readme/godaddy.png)](https://godaddy.com)
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.

View File

@@ -1,36 +0,0 @@
# GoIP.de
## Configuration
### Example
```json
{
"settings": [
{
"provider": "goip",
"domain": "goip.de",
"host": "mysubdomain",
"username": "username",
"password": "password",
"provider_ip": true,
"ip_version": "",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"host"` is the full FQDN of your ddns address. sample.goip.de or something.goip.it
- `"username"` is your goip.de username listed under "Routers"
- `"password"` is your router account password
### Optional parameters
- `"domain"` is the domain name which can be `goip.de` or `goip.it`, and defaults to `goip.de` if left unset.
- `"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. This is automatically disabled for an IPv6 public address since it is not supported.
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

42
docs/google.md Normal file
View 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 `"@"`.

View File

@@ -12,9 +12,7 @@
"domain": "domain.com",
"host": "@",
"password": "password",
"provider_ip": true,
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -28,8 +26,6 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -1,36 +0,0 @@
# Hetzner
## Configuration
### Example
```json
{
"settings": [
{
"provider": "hetzner",
"zone_identifier": "some id",
"domain": "domain.com",
"host": "@",
"ttl": 600,
"token": "yourtoken",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"zone_identifier"` is the Zone ID of your site, from the domain overview page written as *Zone ID*
- `"domain"`
- `"host"` is your host and can be `"@"`, a subdomain or the wildcard `"*"`.
- `"ttl"` optional integer value corresponding to a number of seconds
- One of the following ([how to find API keys](https://docs.hetzner.com/cloud/api/getting-started/generating-api-token)):
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

View File

@@ -11,10 +11,8 @@
"provider": "infomaniak",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -30,8 +28,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -1,35 +0,0 @@
# INWX
## Configuration
### Example
```json
{
"settings": [
{
"provider": "inwx",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### 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) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
## Domain setup

View File

@@ -1,31 +0,0 @@
# Ionos
## Configuration
### Example
```json
{
"settings": [
{
"provider": "ionos",
"domain": "domain.com",
"host": "@",
"api_key": "api_key",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"api_key"` is your API key, obtained from [creating an API key](https://www.ionos.com/help/domains/configuring-your-ip-address/set-up-dynamic-dns-with-company-name/#c181598)
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

View File

@@ -12,8 +12,7 @@
"domain": "domain.com",
"host": "@",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -27,10 +26,9 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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/api/guides/manage-api-tokens/).
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.

View File

@@ -13,8 +13,7 @@
"host": "@",
"email": "email",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -29,8 +28,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -1,37 +0,0 @@
# Name.com
<img src="../readme/name.svg" alt="drawing" width="25%"/>
## Configuration
### Example
```json
{
"settings": [
{
"provider": "name.com",
"domain": "domain.com",
"host": "@",
"username": "username",
"token": "token",
"ttl": 300,
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"username"` is your account username
- `"token"` which you can obtain from [www.name.com/account/settings/api](https://www.name.com/account/settings/api)
### Optional parameters
- `"ttl"` is the time this record can be cached for in seconds. Name.com allows a minimum TTL of 300, or 5 minutes. Name.com defaults to 300 if not provided.
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

View File

@@ -32,26 +32,26 @@ Note that Namecheap only supports ipv4 addresses for now.
## Domain setup
[![Namecheap Website](../readme/namecheap.png)](https://www.namecheap.com/)
[![Namecheap Website](../readme/namecheap.png)](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/](https://www.namecheap.com/myaccount/login/)
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*
![https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns](../readme/namecheap1.png)
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap1.png)
1. Select the following settings and create the *A + Dynamic DNS Record*:
![https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns](../readme/namecheap2.png)
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap2.png)
1. Scroll down and turn on the switch for *DYNAMIC DNS*
![https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns](../readme/namecheap3.png)
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap3.png)
1. The Dynamic DNS Password will appear, which is `0e4512a9c45a4fe88313bcc2234bf547` in this example.
![https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns](../readme/namecheap4.png)
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap4.png)

View File

@@ -1,39 +0,0 @@
# Netcup
## Configuration
Note: This implementation does not require a domain reseller account. The warning in the dashboard can be ignored.
Also keep in mind, that TTL, Expire, Retry and Refresh values of the given Domain are not updated. They can be manually set in the dashboard. For DDNS purposes low numbers should be used.
### Example
```json
{
"settings": [
{
"provider": "netcup",
"domain": "domain.com",
"host": "host",
"api_key": "xxxxx",
"password": "yyyyy",
"customer_number": "111111",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"` is your domain
- `"host"` is your host (subdomain) or `"@"` for the root of the domain. It cannot be the wildcard.
- `"api_key"` is your api key (generated in the [customercontrolpanel](https://www.customercontrolpanel.de))
- `"password"` is your api password (generated in the [customercontrolpanel](https://www.customercontrolpanel.de)). Netcup only allows one ApiPassword. This is not the account password. This password is used for all api keys.
- `"customer_number"` is your customer number (viewable in the [customercontrolpanel](https://www.customercontrolpanel.de) next to your name). As seen in the example above, provide the number as string value.
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

View File

@@ -13,7 +13,6 @@
"host": "@",
"key": "key",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -28,8 +27,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -11,10 +11,8 @@
"provider": "noip",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -30,8 +28,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -1,31 +0,0 @@
# Now-DNS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "nowdns",
"domain": "domain.com",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"` your full domain name (FQDN)
- `"username"` your email address
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.

View File

@@ -8,13 +8,12 @@
{
"settings": [
{
"provider": "opendns",
"provider": "dyn",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -30,8 +29,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -14,7 +14,6 @@
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -42,8 +41,7 @@ The ZoneDNS implementation allows you to update any record name including *.your
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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"`

View File

@@ -1,47 +0,0 @@
# Porkbun
## Configuration
### Example
```json
{
"settings": [
{
"provider": "porkbun",
"domain": "domain.com",
"host": "@",
"api_key": "sk1_7d119e3f656b00ae042980302e1425a04163c476efec1833q3cb0w54fc6f5022",
"secret_api_key": "pk1_5299b57125c8f3cdf347d2fe0e713311ee3a1e11f11a14942b26472593e35368",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory Parameters
- `"domain"`
- `"host"` is your host and can be a subdomain, `"*"` or `"@"`
- `"apikey"`
- `"secretapikey"`
- `"ttl"` optional integer value corresponding to a number of seconds
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
## Domain setup
- Create an API key at [porkbun.com/account/api](https://porkbun.com/account/api)
- From the [Domain Management page](https://porkbun.com/account/domainsSpeedy), toggle on **API ACCESS** for your domain.
💁 [Official setup documentation](https://kb.porkbun.com/article/190-getting-started-with-the-porkbun-dns-api)
## Record creation
In case you don't have an A or AAAA record for your host and domain combination, it will be created by DDNS-Updater.
However, to do so, the corresponding ALIAS record, that is automatically created by Porkbun, is automatically deleted to allow this.
More details is in [this comment by @everydaycombat](https://github.com/qdm12/ddns-updater/issues/546#issuecomment-1773960193).

View File

@@ -13,9 +13,7 @@
"host": "@",
"username": "username",
"password": "password",
"provider_ip": true,
"ip_version": "ipv4",
"ipv6_suffix": ""
"ip_version": "ipv4"
}
]
}
@@ -30,8 +28,6 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

View File

@@ -1,41 +0,0 @@
# Servercow
## Configuration
### Example
```json
{
"settings": [
{
"provider": "servercow",
"domain": "domain.com",
"host": "",
"username": "servercow_username",
"password": "servercow_password",
"ttl": 600,
"provider_ip": true,
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be `""`, a subdomain or `"*"` generally
- `"username"` is the username for your DNS API User
- `"password"` is the password for your DNS API User
### Optional parameters
- `"ttl"` can be set to an integer value for record TTL in seconds (if not set the default is 120)
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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://cp.servercow.de/en/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/)

View File

@@ -15,7 +15,6 @@
"password": "password",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -38,6 +37,5 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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.

View File

@@ -13,7 +13,6 @@
"host": "@",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -28,8 +27,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -14,7 +14,6 @@
"email": "email@domain.com",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
@@ -30,8 +29,7 @@
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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

View File

@@ -1,45 +0,0 @@
# Zoneedit
## Configuration
⚠️ zoneedit.com for some reason requires at least a 10 minutes period between update request sent.
DDNS-Updater only sends update requests when it detects your domain name IP address mismatches your current public IP address,
so it should be fine in most cases since this happens rarely (in hours/days). But in case it happens and you want to avoid this,
set the environment variable as `PERIOD=11m` to check your public IP address and update every 11 minutes only.
### Example
```json
{
"settings": [
{
"provider": "zoneedit",
"domain": "domain.com",
"host": "@",
"username": "username",
"token": "token",
"ip_version": "ipv4",
"ipv6_suffix": "",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"username"`
- `"token"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
- `"ipv6_suffix"` is the IPv6 interface identifiersuffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
- `"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
[support.zoneedit.com/en/knowledgebase/article/dynamic-dns](https://support.zoneedit.com/en/knowledgebase/article/dynamic-dns)

52
go.mod
View File

@@ -1,48 +1,14 @@
module github.com/qdm12/ddns-updater
go 1.21
go 1.16
require (
github.com/breml/rootcerts v0.2.14
github.com/containrrr/shoutrrr v0.8.0
github.com/go-chi/chi/v5 v5.0.11
github.com/golang/mock v1.6.0
github.com/miekg/dns v1.1.57
github.com/qdm12/gosettings v0.4.0-rc9
github.com/qdm12/goshutdown v0.3.0
github.com/qdm12/gosplash v0.1.0
github.com/qdm12/gotree v0.2.0
github.com/qdm12/log v0.1.0
github.com/stretchr/testify v1.8.4
golang.org/x/mod v0.14.0
google.golang.org/api v0.114.0
)
require (
cloud.google.com/go/compute v1.19.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.15.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/grpc v1.56.3 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 // indirect
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 // indirect
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
)

459
go.sum
View File

@@ -1,210 +1,383 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
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/breml/rootcerts v0.2.14 h1:Bu0Ullru+/GTr/S582LCzP1P57WgncIEFylXkBBXgEI=
github.com/breml/rootcerts v0.2.14/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
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/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
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/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
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/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/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/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
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/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
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-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
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.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
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/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/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.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/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.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/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.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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/qdm12/gosettings v0.4.0-rc9 h1:MEVPYQLZfzg3BJgp+DDuY6/9LPAWIlGvPtQ0BeCq9+4=
github.com/qdm12/gosettings v0.4.0-rc9/go.mod h1:uItKwGXibJp2pQ0am6MBKilpjfvYTGiH+zXHd10jFj8=
github.com/qdm12/goshutdown v0.3.0 h1:pqBpJkdwlZlfTEx4QHtS8u8CXx6pG0fVo6S1N0MpSEM=
github.com/qdm12/goshutdown v0.3.0/go.mod h1:EqZ46No00kCTZ5qzdd3qIzY6ayhMt24QI8Mh8LVQYmM=
github.com/qdm12/gosplash v0.1.0 h1:Sfl+zIjFZFP7b0iqf2l5UkmEY97XBnaKkH3FNY6Gf7g=
github.com/qdm12/gosplash v0.1.0/go.mod h1:+A3fWW4/rUeDXhY3ieBzwghKdnIPFJgD8K3qQkenJlw=
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
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-20190213061140-3a22650c66bd/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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
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-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
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.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
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/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
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-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
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/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/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=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69 h1:N0m3tKYbkRMmDobh/47ngz+AWeV7PcfXMDi8xu3Vrag=
kernel.org/pub/linux/libs/security/libcap/cap v1.2.69/go.mod h1:Tk5Ip2TuxaWGpccL7//rAsLRH6RQ/jfqTGxuN/+i/FQ=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69 h1:IdrOs1ZgwGw5CI+BH6GgVVlOt+LAXoPyh7enr8lfaXs=
kernel.org/pub/linux/libs/security/libcap/psx v1.2.69/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24=
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=

View File

@@ -6,27 +6,25 @@ import (
"os"
)
var _ FileZiper = (*Ziper)(nil)
type FileZiper interface {
type Ziper interface {
ZipFiles(outputFilepath string, inputFilepaths ...string) error
}
type Ziper struct {
type ziper struct {
createFile func(name string) (*os.File, error)
openFile func(name string) (*os.File, error)
ioCopy func(dst io.Writer, src io.Reader) (written int64, err error)
}
func NewZiper() *Ziper {
return &Ziper{
func NewZiper() Ziper {
return &ziper{
createFile: os.Create,
openFile: os.Open,
ioCopy: io.Copy,
}
}
func (z *Ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
f, err := z.createFile(outputFilepath)
if err != nil {
return err
@@ -35,15 +33,14 @@ func (z *Ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error
w := zip.NewWriter(f)
defer w.Close()
for _, filepath := range inputFilepaths {
err = z.addFile(w, filepath)
if err != nil {
if err := z.addFile(w, filepath); err != nil {
return err
}
}
return nil
}
func (z *Ziper) addFile(w *zip.Writer, filepath string) error {
func (z *ziper) addFile(w *zip.Writer, filepath string) error {
f, err := z.openFile(filepath)
if err != nil {
return err

View File

@@ -3,45 +3,19 @@ package config
import (
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"github.com/qdm12/golibs/params"
)
type Backup struct {
Period *time.Duration
Directory *string
Period time.Duration
Directory string
}
func (b *Backup) setDefaults() {
b.Period = gosettings.DefaultPointer(b.Period, 0)
b.Directory = gosettings.DefaultPointer(b.Directory, "./data")
}
func (b Backup) Validate() (err error) {
return nil
}
func (b Backup) String() string {
return b.toLinesNode().String()
}
func (b Backup) toLinesNode() *gotree.Node {
if *b.Period == 0 {
return gotree.New("Backup: disabled")
}
node := gotree.New("Backup")
node.Appendf("Period: %s", b.Period)
node.Appendf("Directory: %s", *b.Directory)
return node
}
func (b *Backup) read(reader *reader.Reader) (err error) {
b.Period, err = reader.DurationPtr("BACKUP_PERIOD")
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 = reader.Get("BACKUP_DIRECTORY")
return nil
b.Directory, err = env.Path("BACKUP_DIRECTORY", params.Default("./data"))
return err
}

View File

@@ -3,39 +3,14 @@ package config
import (
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"github.com/qdm12/golibs/params"
)
type Client struct {
Timeout time.Duration
}
func (c *Client) setDefaults() {
const defaultTimeout = 20 * time.Second
c.Timeout = gosettings.DefaultComparable(c.Timeout, defaultTimeout)
}
func (c Client) Validate() (err error) {
return nil
}
func (c Client) String() string {
return c.toLinesNode().String()
}
func (c Client) toLinesNode() *gotree.Node {
node := gotree.New("HTTP client")
node.Appendf("Timeout: %s", c.Timeout)
return node
}
func (c *Client) read(reader *reader.Reader) (err error) {
c.Timeout, err = reader.Duration("HTTP_TIMEOUT")
if err != nil {
return err
}
return nil
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
View 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
}

View File

@@ -1,48 +1,31 @@
package config
import (
"fmt"
"os"
"net"
"strconv"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
"github.com/qdm12/golibs/params"
)
type Health struct {
ServerAddress *string
HealthchecksioUUID *string
ServerAddress string
Port uint16 // obtained from ServerAddress
}
func (h *Health) SetDefaults() {
h.ServerAddress = gosettings.DefaultPointer(h.ServerAddress, "127.0.0.1:9999")
h.HealthchecksioUUID = gosettings.DefaultPointer(h.HealthchecksioUUID, "")
}
func (h Health) Validate() (err error) {
err = validate.ListeningAddress(*h.ServerAddress, os.Getuid())
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 fmt.Errorf("server listening address: %w", err)
return warning, err
}
return nil
}
func (h Health) String() string {
return h.toLinesNode().String()
}
func (h Health) toLinesNode() *gotree.Node {
node := gotree.New("Health")
node.Appendf("Server listening address: %s", *h.ServerAddress)
if *h.HealthchecksioUUID != "" {
node.Appendf("Healthchecks.io UUID: %s", *h.HealthchecksioUUID)
_, portStr, err := net.SplitHostPort(h.ServerAddress)
if err != nil {
return warning, err
}
return node
}
func (h *Health) Read(reader *reader.Reader) {
h.ServerAddress = reader.Get("HEALTH_SERVER_ADDRESS")
h.HealthchecksioUUID = reader.Get("HEALTH_HEALTHCHECKSIO_UUID")
port, err := strconv.Atoi(portStr)
if err != nil {
return warning, err
}
h.Port = uint16(port)
return warning, nil
}

View File

@@ -1,3 +0,0 @@
package config
const all = "all"

56
internal/config/ipv6.go Normal file
View 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
}

View 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)
})
}
}

View File

@@ -1,65 +1,21 @@
package config
import (
"fmt"
"strings"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
"github.com/qdm12/log"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
)
type Logger struct {
Level string
Caller string
Caller logging.Caller
Level logging.Level
}
func (l *Logger) setDefaults() {
l.Level = gosettings.DefaultComparable(l.Level, log.LevelInfo.String())
l.Caller = gosettings.DefaultComparable(l.Caller, "hidden")
}
func (l Logger) Validate() (err error) {
_, err = log.ParseLevel(l.Level)
func (l *Logger) get(env params.Env) (err error) {
l.Caller, err = env.LogCaller("LOG_CALLER", params.Default("hidden"))
if err != nil {
return fmt.Errorf("log level: %w", err)
return err
}
err = validate.IsOneOf(l.Caller, "hidden", "short")
if err != nil {
return fmt.Errorf("log caller: %w", err)
}
return nil
}
func (l Logger) ToOptions() (options []log.Option) {
level, _ := log.ParseLevel(l.Level)
options = append(options, log.SetLevel(level))
if l.Caller == "short" {
options = append(options, log.SetCallerFile(true), log.SetCallerLine(true))
}
return options
}
func (l Logger) String() string {
return l.toLinesNode().String()
}
func (l Logger) toLinesNode() *gotree.Node {
node := gotree.New("Logger")
node.Appendf("Level: %s", l.Level)
node.Appendf("Caller: %s", l.Caller)
return node
}
func (l *Logger) read(reader *reader.Reader) {
l.Level = reader.String("LOG_LEVEL")
// Retro compatibility
if strings.ToLower(l.Level) == "warning" {
l.Level = "warn"
}
l.Caller = reader.String("LOG_CALLER")
l.Level, err = env.LogLevel("LOG_LEVEL", params.Default("info"))
return err
}

View File

@@ -1,33 +1,22 @@
package config
import (
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"path/filepath"
"github.com/qdm12/golibs/params"
)
type Paths struct {
DataDir *string
DataDir string
JSON string // obtained from DataDir
}
func (p *Paths) setDefaults() {
p.DataDir = gosettings.DefaultPointer(p.DataDir, "./data")
}
func (p *Paths) get(env params.Env) (err error) {
p.DataDir, err = env.Path("DATADIR", params.Default("./data"))
if err != nil {
return err
}
func (p Paths) Validate() (err error) {
p.JSON = filepath.Join(p.DataDir, "config.json")
return nil
}
func (p Paths) String() string {
return p.toLinesNode().String()
}
func (p Paths) toLinesNode() *gotree.Node {
node := gotree.New("Paths")
node.Appendf("Data directory: %s", *p.DataDir)
return node
}
func (p *Paths) read(reader *reader.Reader) {
p.DataDir = reader.Get("DATADIR")
}

View File

@@ -5,326 +5,191 @@ import (
"fmt"
"net/url"
"strings"
"time"
"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/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
"github.com/qdm12/golibs/params"
)
const all = "all"
type PubIP struct {
HTTPEnabled *bool
HTTPIPProviders []string
HTTPIPv4Providers []string
HTTPIPv6Providers []string
DNSEnabled *bool
DNSProviders []string
DNSTimeout time.Duration
HTTPSettings publicip.HTTPSettings
DNSSettings publicip.DNSSettings
}
func (p *PubIP) setDefaults() {
p.HTTPEnabled = gosettings.DefaultPointer(p.HTTPEnabled, true)
p.HTTPIPProviders = gosettings.DefaultSlice(p.HTTPIPProviders, []string{all})
p.HTTPIPv4Providers = gosettings.DefaultSlice(p.HTTPIPv4Providers, []string{all})
p.HTTPIPv6Providers = gosettings.DefaultSlice(p.HTTPIPv6Providers, []string{all})
p.DNSEnabled = gosettings.DefaultPointer(p.DNSEnabled, true)
p.DNSProviders = gosettings.DefaultSlice(p.DNSProviders, []string{all})
const defaultDNSTimeout = 3 * time.Second
p.DNSTimeout = gosettings.DefaultComparable(p.DNSTimeout, defaultDNSTimeout)
}
func (p *PubIP) get(env params.Env) (warnings []string, err error) {
if err = p.getFetchers(env); err != nil {
return nil, err
}
func (p PubIP) Validate() (err error) {
err = p.validateHTTPIPProviders()
httpIPProviders, warning, err := p.getIPHTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return fmt.Errorf("HTTP IP providers: %w", err)
return warnings, err
}
err = p.validateHTTPIPv4Providers()
httpIP4Providers, warning, err := p.getIPv4HTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return fmt.Errorf("HTTP IPv4 providers: %w", err)
return warnings, err
}
err = p.validateHTTPIPv6Providers()
httpIP6Providers, warning, err := p.getIPv6HTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return fmt.Errorf("HTTP IPv6 providers: %w", err)
return warnings, err
}
err = p.validateDNSProviders()
if err != nil {
return fmt.Errorf("DNS providers: %w", err)
}
return nil
}
func (p *PubIP) String() string {
return p.toLinesNode().String()
}
func (p *PubIP) toLinesNode() (node *gotree.Node) {
node = gotree.New("Public IP fetching")
node.Appendf("HTTP enabled: %s", gosettings.BoolToYesNo(p.HTTPEnabled))
if *p.HTTPEnabled {
childNode := node.Appendf("HTTP IP providers")
for _, provider := range p.HTTPIPProviders {
childNode.Appendf(provider)
}
childNode = node.Appendf("HTTP IPv4 providers")
for _, provider := range p.HTTPIPv4Providers {
childNode.Appendf(provider)
}
childNode = node.Appendf("HTTP IPv6 providers")
for _, provider := range p.HTTPIPv6Providers {
childNode.Appendf(provider)
}
}
node.Appendf("DNS enabled: %s", gosettings.BoolToYesNo(p.DNSEnabled))
if *p.DNSEnabled {
node.Appendf("DNS timeout: %s", p.DNSTimeout)
childNode := node.Appendf("DNS over TLS providers")
for _, provider := range p.DNSProviders {
childNode.Appendf(provider)
}
}
return node
}
// ToHTTPOptions assumes the settings have been validated.
func (p *PubIP) ToHTTPOptions() (options []http.Option) {
httpIPProviders := stringsToHTTPProviders(p.HTTPIPProviders, ipversion.IP4or6)
httpIPv4Providers := stringsToHTTPProviders(p.HTTPIPv4Providers, ipversion.IP4)
httpIPv6Providers := stringsToHTTPProviders(p.HTTPIPv6Providers, ipversion.IP6)
return []http.Option{
p.HTTPSettings.Options = []http.Option{
http.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
http.SetProvidersIP4(httpIPv4Providers[0], httpIPv4Providers[1:]...),
http.SetProvidersIP6(httpIPv6Providers[0], httpIPv6Providers[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
}
func stringsToHTTPProviders(providers []string, ipVersion ipversion.IPVersion) (
updatedProviders []http.Provider) {
updatedProvidersSet := make(map[string]struct{}, len(providers))
for _, provider := range providers {
if provider != all {
updatedProvidersSet[provider] = struct{}{}
continue
}
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
allProviders := http.ListProvidersForVersion(ipVersion)
for _, provider := range allProviders {
updatedProvidersSet[string(provider)] = struct{}{}
}
}
updatedProviders = make([]http.Provider, 0, len(updatedProvidersSet))
for provider := range updatedProvidersSet {
updatedProviders = append(updatedProviders, http.Provider(provider))
}
return updatedProviders
}
// ToDNSPOptions assumes the settings have been validated.
func (p *PubIP) ToDNSPOptions() (options []dns.Option) {
uniqueProviders := make(map[string]struct{}, len(p.DNSProviders))
for _, provider := range p.DNSProviders {
if provider != all {
uniqueProviders[provider] = struct{}{}
}
allProviders := dns.ListProviders()
for _, provider := range allProviders {
uniqueProviders[string(provider)] = struct{}{}
}
}
providers := make([]dns.Provider, 0, len(uniqueProviders))
for providerString := range uniqueProviders {
providers = append(providers, dns.Provider(providerString))
}
return []dns.Option{
dns.SetTimeout(p.DNSTimeout),
dns.SetProviders(providers[0], providers[1:]...),
}
}
var (
ErrNoPublicIPDNSProvider = errors.New("no public IP DNS provider specified")
)
func (p PubIP) validateDNSProviders() (err error) {
if len(p.DNSProviders) == 0 {
return fmt.Errorf("%w", ErrNoPublicIPDNSProvider)
}
availableProviders := dns.ListProviders()
validChoices := make([]string, len(availableProviders)+1)
for i, provider := range availableProviders {
validChoices[i] = string(provider)
}
validChoices[len(validChoices)-1] = all
return validate.AreAllOneOf(p.DNSProviders, validChoices)
}
func (p PubIP) validateHTTPIPProviders() (err error) {
return validateHTTPIPProviders(p.HTTPIPProviders, ipversion.IP4or6)
}
func (p PubIP) validateHTTPIPv4Providers() (err error) {
return validateHTTPIPProviders(p.HTTPIPv4Providers, ipversion.IP4)
}
func (p PubIP) validateHTTPIPv6Providers() (err error) {
return validateHTTPIPProviders(p.HTTPIPv6Providers, ipversion.IP6)
}
var (
ErrNoPublicIPHTTPProvider = errors.New("no public IP HTTP provider specified")
)
func validateHTTPIPProviders(providerStrings []string,
version ipversion.IPVersion) (err error) {
if len(providerStrings) == 0 {
return fmt.Errorf("%w", ErrNoPublicIPHTTPProvider)
}
availableProviders := http.ListProvidersForVersion(version)
choices := make(map[string]struct{}, len(availableProviders)+1)
choices[all] = struct{}{}
for i := range availableProviders {
choices[string(availableProviders[i])] = struct{}{}
}
for _, providerString := range providerStrings {
if providerString == "noip" {
// NoIP is no longer supported because the echo service
// only works over plaintext HTTP and could be tempered with.
// Silently discard it and it will default to another HTTP IP
// echo service.
continue
}
// Custom URL check
url, err := url.Parse(providerString)
if err == nil && url != nil && url.Scheme == "https" {
continue
}
_, ok := choices[providerString]
if !ok {
return fmt.Errorf("%w: %s", validate.ErrValueNotOneOf, providerString)
}
}
return nil
}
func (p *PubIP) read(r *reader.Reader, warner Warner) (err error) {
p.HTTPEnabled, p.DNSEnabled, err = getFetchers(r)
func (p *PubIP) getFetchers(env params.Env) (err error) {
s, err := env.Get("PUBLICIP_FETCHERS", params.Default(all))
if err != nil {
return err
}
p.HTTPIPProviders = r.CSV("PUBLICIP_HTTP_PROVIDERS",
reader.RetroKeys("IP_METHOD"))
p.HTTPIPv4Providers = r.CSV("PUBLICIPV4_HTTP_PROVIDERS",
reader.RetroKeys("IPV4_METHOD"))
p.HTTPIPv6Providers = r.CSV("PUBLICIPV6_HTTP_PROVIDERS",
reader.RetroKeys("IPV6_METHOD"))
// Retro-compatibility
for i := range p.HTTPIPProviders {
p.HTTPIPProviders[i] = handleRetroProvider(p.HTTPIPProviders[i])
}
for i := range p.HTTPIPv4Providers {
p.HTTPIPv4Providers[i] = handleRetroProvider(p.HTTPIPv4Providers[i])
}
for i := range p.HTTPIPv6Providers {
p.HTTPIPv6Providers[i] = handleRetroProvider(p.HTTPIPv6Providers[i])
}
// Retro-compatibility for now defunct opendns http provider for ipv4 or ipv6
if len(p.HTTPIPProviders) > 0 { // check to avoid transforming `nil` to `[]`
httpIPProvidersTemp := make([]string, len(p.HTTPIPProviders))
copy(httpIPProvidersTemp, p.HTTPIPProviders)
p.HTTPIPProviders = make([]string, 0, len(p.HTTPIPProviders))
for _, provider := range httpIPProvidersTemp {
if provider != "opendns" {
p.HTTPIPProviders = append(p.HTTPIPProviders, provider)
}
}
}
p.DNSProviders = r.CSV("PUBLICIP_DNS_PROVIDERS")
// Retro-compatibility
for i, provider := range p.DNSProviders {
if provider == "google" {
warner.Warnf("dns provider google will be ignored " +
"since it is no longer supported, " +
"see https://github.com/qdm12/ddns-updater/issues/492")
p.DNSProviders[i] = p.DNSProviders[len(p.DNSProviders)-1]
p.DNSProviders = p.DNSProviders[:len(p.DNSProviders)-1]
}
}
p.DNSTimeout, err = r.Duration("PUBLICIP_DNS_TIMEOUT")
if err != nil {
return err
}
return nil
}
var ErrFetcherNotValid = errors.New("fetcher is not valid")
func getFetchers(reader *reader.Reader) (http, dns *bool, err error) {
// TODO change to use reader.BoolPtr with retro-compatibility
s := reader.String("PUBLICIP_FETCHERS")
if s == "" {
return nil, nil, nil
}
http, dns = new(bool), new(bool)
fields := strings.Split(s, ",")
for i, field := range fields {
switch strings.ToLower(field) {
case "all":
*http = true
*dns = true
case all:
p.HTTPSettings.Enabled = true
p.DNSSettings.Enabled = true
case "http":
*http = true
p.HTTPSettings.Enabled = true
case "dns":
*dns = true
p.DNSSettings.Enabled = true
default:
return nil, nil, fmt.Errorf(
err = fmt.Errorf(
"%w: %q at position %d of %d",
ErrFetcherNotValid, field, i+1, len(fields))
ErrInvalidFetcher, field, i+1, len(fields))
}
}
return http, dns, nil
return err
}
func handleRetroProvider(provider string) (updatedProvider string) {
switch provider {
case "ipify6":
return "ipify"
case "noip4", "noip6", "noip8245_4", "noip8245_6":
return "noip"
case "cycle":
return "all"
default:
return provider
// 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
}

View File

@@ -1,80 +0,0 @@
package config
import (
"errors"
"fmt"
"net"
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type Resolver struct {
Address *string
Timeout time.Duration
}
func (r *Resolver) setDefaults() {
r.Address = gosettings.DefaultPointer(r.Address, "")
const defaultTimeout = 5 * time.Second
r.Timeout = gosettings.DefaultComparable(r.Timeout, defaultTimeout)
}
var (
ErrAddressHostEmpty = errors.New("address host is empty")
ErrAddressPortEmpty = errors.New("address port is empty")
ErrTimeoutTooLow = errors.New("timeout is too low")
)
func (r Resolver) Validate() (err error) {
if *r.Address != "" {
host, port, err := net.SplitHostPort(*r.Address)
if err != nil {
return fmt.Errorf("splitting host and port from address: %w", err)
}
switch {
case host == "":
return fmt.Errorf("%w: in %s", ErrAddressHostEmpty, *r.Address)
case port == "":
return fmt.Errorf("%w: in %s", ErrAddressPortEmpty, *r.Address)
}
}
const minTimeout = 10 * time.Millisecond
if r.Timeout < minTimeout {
return fmt.Errorf("%w: %s is below the minimum %s",
ErrTimeoutTooLow, r.Timeout, minTimeout)
}
return nil
}
func (r Resolver) String() string {
return r.ToLinesNode().String()
}
func (r Resolver) ToLinesNode() *gotree.Node {
if *r.Address == "" {
return gotree.New("Resolver: use Go default resolver")
}
node := gotree.New("Resolver")
node.Appendf("Address: %s", *r.Address)
node.Appendf("Timeout: %s", r.Timeout)
return node
}
func (r *Resolver) read(reader *reader.Reader) (err error) {
r.Address = reader.Get("RESOLVER_ADDRESS")
if r.Address != nil { // conveniently add port 53 if not specified
_, port, err := net.SplitHostPort(*r.Address)
if err == nil && port == "" {
*r.Address += ":53"
}
}
r.Timeout, err = reader.Duration("RESOLVER_TIMEOUT")
return err
}

View File

@@ -1,10 +0,0 @@
package config
type Warner interface {
Warnf(format string, a ...interface{})
}
func handleDeprecated(warner Warner, oldKey, newKey string) {
warner.Warnf("You are using an old environment variable %s, please change it to %s",
oldKey, newKey)
}

View File

@@ -1,60 +1,19 @@
package config
import (
"fmt"
"os"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gosettings/validate"
"github.com/qdm12/gotree"
"github.com/qdm12/golibs/params"
)
type Server struct {
ListeningAddress string
RootURL string
Port uint16
RootURL string
}
func (s *Server) setDefaults() {
s.ListeningAddress = gosettings.DefaultComparable(s.ListeningAddress, ":8000")
s.RootURL = gosettings.DefaultComparable(s.RootURL, "/")
}
func (s Server) Validate() (err error) {
err = validate.ListeningAddress(s.ListeningAddress, os.Getuid())
func (s *Server) get(env params.Env) (warning string, err error) {
s.RootURL, err = env.RootURL("ROOT_URL")
if err != nil {
return fmt.Errorf("listening address: %w", err)
return "", err
}
// TODO validate RootURL
return nil
}
func (s Server) String() string {
return s.toLinesNode().String()
}
func (s Server) toLinesNode() *gotree.Node {
node := gotree.New("Server")
node.Appendf("Listening address: %s", s.ListeningAddress)
node.Appendf("Root URL: %s", s.RootURL)
return node
}
func (s *Server) read(reader *reader.Reader, warner Warner) (err error) {
s.RootURL = reader.String("ROOT_URL")
// Retro-compatibility
port, err := reader.Uint16Ptr("LISTENING_PORT") // TODO change to address
if err != nil {
handleDeprecated(warner, "LISTENING_PORT", "LISTENING_ADDRESS")
return err
} else if port != nil {
s.ListeningAddress = fmt.Sprintf(":%d", *port)
}
s.ListeningAddress = reader.String("LISTENING_ADDRESS")
return err
s.Port, warning, err = env.ListeningPort("LISTENING_PORT", params.Default("8000"))
return warning, err
}

View File

@@ -1,125 +0,0 @@
package config
import (
"fmt"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
)
type Config struct {
Client Client
Update Update
PubIP PubIP
Resolver Resolver
Server Server
Health Health
Paths Paths
Backup Backup
Logger Logger
Shoutrrr Shoutrrr
}
func (c *Config) SetDefaults() {
c.Client.setDefaults()
c.Update.setDefaults()
c.PubIP.setDefaults()
c.Resolver.setDefaults()
c.Server.setDefaults()
c.Health.SetDefaults()
c.Paths.setDefaults()
c.Backup.setDefaults()
c.Logger.setDefaults()
c.Shoutrrr.setDefaults()
}
func (c Config) Validate() (err error) {
type validator interface {
Validate() (err error)
}
toValidate := map[string]validator{
"client": &c.Client,
"update": &c.Update,
"public ip": &c.PubIP,
"resolver": &c.Resolver,
"server": &c.Server,
"health": &c.Health,
"paths": &c.Paths,
"backup": &c.Backup,
"logger": &c.Logger,
"shoutrrr": &c.Shoutrrr,
}
for name, v := range toValidate {
err = v.Validate()
if err != nil {
return fmt.Errorf("%s settings: %w", name, err)
}
}
return nil
}
func (c Config) String() string {
return c.toLinesNode().String()
}
func (c Config) toLinesNode() *gotree.Node {
node := gotree.New("Settings summary:")
node.AppendNode(c.Client.toLinesNode())
node.AppendNode(c.Update.toLinesNode())
node.AppendNode(c.PubIP.toLinesNode())
node.AppendNode(c.Resolver.ToLinesNode())
node.AppendNode(c.Server.toLinesNode())
node.AppendNode(c.Health.toLinesNode())
node.AppendNode(c.Paths.toLinesNode())
node.AppendNode(c.Backup.toLinesNode())
node.AppendNode(c.Logger.toLinesNode())
node.AppendNode(c.Shoutrrr.ToLinesNode())
return node
}
func (c *Config) Read(reader *reader.Reader,
warner Warner) (err error) {
err = c.Client.read(reader)
if err != nil {
return fmt.Errorf("reading client settings: %w", err)
}
err = c.Update.read(reader, warner)
if err != nil {
return fmt.Errorf("reading update settings: %w", err)
}
err = c.PubIP.read(reader, warner)
if err != nil {
return fmt.Errorf("reading public IP settings: %w", err)
}
err = c.Resolver.read(reader)
if err != nil {
return fmt.Errorf("reading resolver settings: %w", err)
}
err = c.Server.read(reader, warner)
if err != nil {
return fmt.Errorf("reading server settings: %w", err)
}
c.Health.Read(reader)
c.Paths.read(reader)
err = c.Backup.read(reader)
if err != nil {
return fmt.Errorf("reading backup settings: %w", err)
}
c.Logger.read(reader)
err = c.Shoutrrr.read(reader, warner)
if err != nil {
return fmt.Errorf("reading shoutrrr settings: %w", err)
}
return nil
}

View File

@@ -1,48 +0,0 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Settings_String(t *testing.T) {
t.Parallel()
var defaultSettings Config
defaultSettings.SetDefaults()
s := defaultSettings.String()
const expected = `Settings summary:
├── HTTP client
| └── Timeout: 20s
├── Update
| ├── Period: 10m0s
| └── Cooldown: 5m0s
├── Public IP fetching
| ├── HTTP enabled: yes
| ├── HTTP IP providers
| | └── all
| ├── HTTP IPv4 providers
| | └── all
| ├── HTTP IPv6 providers
| | └── all
| ├── DNS enabled: yes
| ├── DNS timeout: 3s
| └── DNS over TLS providers
| └── all
├── Resolver: use Go default resolver
├── Server
| ├── Listening address: :8000
| └── Root URL: /
├── Health
| └── Server listening address: 127.0.0.1:9999
├── Paths
| └── Data directory: ./data
├── Backup: disabled
└── Logger
├── Level: INFO
└── Caller: hidden`
assert.Equal(t, expected, s)
}

View File

@@ -1,80 +1,62 @@
package config
import (
"fmt"
"net/url"
"path"
"strings"
"github.com/containrrr/shoutrrr"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/qdm12/golibs/params"
)
type Shoutrrr struct {
Addresses []string
DefaultTitle string
Addresses []string
Params types.Params
}
func (s *Shoutrrr) setDefaults() {
s.Addresses = gosettings.DefaultSlice(s.Addresses, []string{})
s.DefaultTitle = gosettings.DefaultComparable(s.DefaultTitle, "DDNS Updater")
}
func (s Shoutrrr) Validate() (err error) {
_, err = shoutrrr.CreateSender(s.Addresses...)
func (s *Shoutrrr) get(env params.Env) (warnings []string, err error) {
s.Addresses, err = env.CSV("SHOUTRRR_ADDRESSES", params.CaseSensitiveValue())
if err != nil {
return fmt.Errorf("shoutrrr addresses: %w", err)
return warnings, err
}
return nil
}
func (s Shoutrrr) String() string {
return s.ToLinesNode().String()
}
func (s Shoutrrr) ToLinesNode() *gotree.Node {
if len(s.Addresses) == 0 {
return nil // no address means shoutrrr is disabled
}
node := gotree.New("Shoutrrr")
node.Appendf("Default title: %s", s.DefaultTitle)
childNode := node.Appendf("Addresses")
for _, address := range s.Addresses {
childNode.Appendf(address)
}
return node
}
func (s *Shoutrrr) read(r *reader.Reader, warner Warner) (err error) {
s.Addresses = r.CSV("SHOUTRRR_ADDRESSES", reader.ForceLowercase(false))
// Retro-compatibility: GOTIFY_URL and GOTIFY_TOKEN
gotifyURLString := r.Get("GOTIFY_URL", reader.ForceLowercase(false))
if gotifyURLString != nil {
handleDeprecated(warner, "GOTIFY_URL", "SHOUTRRR_ADDRESSES")
gotifyURL, err := url.Parse(*gotifyURLString)
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 fmt.Errorf("gotify URL: %w", err)
return warnings, err
}
gotifyToken := r.String("GOTIFY_TOKEN", reader.ForceLowercase(false))
handleDeprecated(warner, "GOTIFY_TOKEN", "SHOUTRRR_ADDRESSES")
gotifyShoutrrrAddress := gotifyURLTokenToShoutrrr(gotifyURL, gotifyToken)
s.Addresses = append(s.Addresses, gotifyShoutrrrAddress)
}
// Retro-compatibility
shoutrrrParamsCSV := r.Get("SHOUTRRR_PARAMS")
if shoutrrrParamsCSV != nil {
warner.Warnf("SHOUTRRR_PARAMS is disabled, you can use SHOUTRRR_DEFAULT_TITLE and SHOUTRRR_ADDRESSES")
if _, err = shoutrrr.CreateSender(s.Addresses...); err != nil {
return warnings, err // validation step
}
s.DefaultTitle = r.String("SHOUTRRR_DEFAULT_TITLE", reader.ForceLowercase(false))
return nil
str, err := env.Get("SHOUTRRR_PARAMS", params.Default("title=DDNS Updater"), params.CaseSensitiveValue())
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) {

View File

@@ -4,9 +4,7 @@ import (
"strconv"
"time"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
"github.com/qdm12/golibs/params"
)
type Update struct {
@@ -14,51 +12,35 @@ type Update struct {
Cooldown time.Duration
}
func (u *Update) setDefaults() {
const defaultPeriod = 10 * time.Minute
u.Period = gosettings.DefaultComparable(u.Period, defaultPeriod)
const defaultCooldown = 5 * time.Minute
u.Cooldown = gosettings.DefaultComparable(u.Cooldown, defaultCooldown)
}
func (u Update) Validate() (err error) {
return nil
}
func (u Update) String() string {
return u.toLinesNode().String()
}
func (u Update) toLinesNode() *gotree.Node {
node := gotree.New("Update")
node.Appendf("Period: %s", u.Period)
node.Appendf("Cooldown: %s", u.Cooldown)
return node
}
func (u *Update) read(reader *reader.Reader, warner Warner) (err error) {
u.Period, err = readUpdatePeriod(reader, warner)
func (u *Update) get(env params.Env) (warning string, err error) {
warning, err = u.getPeriod(env)
if err != nil {
return err
return warning, err
}
u.Cooldown, err = reader.Duration("UPDATE_COOLDOWN_PERIOD")
return err
u.Cooldown, err = env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
return warning, err
}
func readUpdatePeriod(r *reader.Reader, warner Warner) (period time.Duration, err error) {
// Retro-compatibility: DELAY variable name
delayStringPtr := r.Get("DELAY")
if delayStringPtr != nil {
handleDeprecated(warner, "DELAY", "PERIOD")
// Retro-compatibility: integer only, treated as seconds
delayInt, err := strconv.Atoi(*delayStringPtr)
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 {
return time.Duration(delayInt) * time.Second, nil
u.Period = time.Duration(n) * time.Second
return warning, nil
}
return time.ParseDuration(*delayStringPtr)
period, err := time.ParseDuration(s)
if err == nil {
u.Period = period
return warning, nil
}
}
return r.Duration("PERIOD")
u.Period, err = env.Duration("PERIOD", params.Default("10m"))
return "", err
}

8
internal/config/utils.go Normal file
View File

@@ -0,0 +1,8 @@
package config
func appendIfNotEmpty(slice []string, s string) (newSlice []string) {
if s == "" {
return slice
}
return append(slice, s)
}

View File

@@ -0,0 +1,13 @@
package constants
const (
// Announcement is a message announcement.
Announcement = "Support for he.net"
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd.
AnnouncementExpiration = "2020-10-15"
)
const (
// IssueLink is the link for users to use to create issues.
IssueLink = "https://github.com/qdm12/ddns-updater/issues/new"
)

View File

@@ -0,0 +1,17 @@
package constants
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_AnnouncementExpiration(t *testing.T) {
t.Parallel()
if len(AnnouncementExpiration) == 0 {
return
}
_, err := time.Parse("2006-01-02", AnnouncementExpiration)
assert.NoError(t, err)
}

View File

@@ -3,19 +3,36 @@ package data
import (
"sync"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/persistence"
"github.com/qdm12/ddns-updater/internal/records"
)
type Database struct {
type Database interface {
Close() error
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 []records.Record
sync.RWMutex
persistentDB PersistentDatabase
persistentDB persistence.Database
}
// NewDatabase creates a new in memory database.
func NewDatabase(data []records.Record, persistentDB PersistentDatabase) *Database {
return &Database{
func NewDatabase(data []records.Record, persistentDB persistence.Database) Database {
return &database{
data: data,
persistentDB: persistentDB,
}
}
func (db *database) Close() error {
db.Lock() // ensure write operation finishes
defer db.Unlock()
return db.persistentDB.Close()
}

View File

@@ -1,11 +0,0 @@
package data
import (
"net/netip"
"time"
)
type PersistentDatabase interface {
Close() error
StoreNewIP(domain, host string, ip netip.Addr, t time.Time) (err error)
}

View File

@@ -1,24 +1,24 @@
package data
import (
"errors"
"fmt"
"github.com/qdm12/ddns-updater/internal/records"
)
var ErrRecordNotFound = errors.New("record not found")
func (db *Database) Select(id uint) (record records.Record, err error) {
func (db *database) Select(id int) (record records.Record, err error) {
db.RLock()
defer db.RUnlock()
if int(id) > len(db.data)-1 {
return record, fmt.Errorf("%w: for id %d", ErrRecordNotFound, id)
if id < 0 {
return record, fmt.Errorf("id %d cannot be lower than 0", id)
}
if id > len(db.data)-1 {
return record, fmt.Errorf("no record config found for id %d", id)
}
return db.data[id], nil
}
func (db *Database) SelectAll() (records []records.Record) {
func (db *database) SelectAll() (records []records.Record) {
db.RLock()
defer db.RUnlock()
return db.data

View File

@@ -3,14 +3,22 @@ package data
import (
"fmt"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/records"
)
func (db *Database) Update(id uint, record records.Record) (err error) {
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
return db.persistentDB.GetEvents(domain, host)
}
func (db *database) Update(id int, record records.Record) error {
db.Lock()
defer db.Unlock()
if int(id) > len(db.data)-1 {
return fmt.Errorf("%w: for id %d", ErrRecordNotFound, id)
if id < 0 {
return fmt.Errorf("id %d cannot be lower than 0", id)
}
if id > len(db.data)-1 {
return fmt.Errorf("no record config found for id %d", id)
}
currentCount := len(db.data[id].History)
newCount := len(record.History)
@@ -18,8 +26,8 @@ func (db *Database) Update(id uint, record records.Record) (err error) {
// new IP address added
if newCount > currentCount {
if err := db.persistentDB.StoreNewIP(
record.Provider.Domain(),
record.Provider.Host(),
record.Settings.Domain(),
record.Settings.Host(),
record.History.GetCurrentIP(),
record.History.GetSuccessTime(),
); err != nil {
@@ -28,9 +36,3 @@ func (db *Database) Update(id uint, record records.Record) (err error) {
}
return nil
}
func (db *Database) Close() (err error) {
db.Lock() // ensure write operation finishes
defer db.Unlock()
return db.persistentDB.Close()
}

View File

@@ -1,69 +1,53 @@
package health
import (
"context"
"errors"
"fmt"
"net/netip"
"net"
"strings"
"github.com/qdm12/ddns-updater/internal/constants"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/golibs/logging"
)
func MakeIsHealthy(db AllSelecter, resolver LookupIPer) func() error {
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, resolver)
return isHealthy(db, lookupIP)
}
}
var (
ErrRecordUpdateFailed = errors.New("record update failed")
ErrRecordIPNotSet = errors.New("record IP not set")
ErrLookupMismatch = errors.New("lookup IP addresses do not match")
)
// isHealthy checks all the records were updated successfully and returns an error if not.
func isHealthy(db AllSelecter, resolver LookupIPer) (err error) {
func isHealthy(db data.Database, lookupIP lookupIPFunc) (err error) {
records := db.SelectAll()
for _, record := range records {
if record.Status == constants.FAIL {
return fmt.Errorf("%w: %s", ErrRecordUpdateFailed, record.String())
} else if record.Provider.Proxied() {
return fmt.Errorf("%s", record.String())
} else if record.Settings.Proxied() {
continue
}
hostname := record.Provider.BuildDomainName()
currentIP := record.History.GetCurrentIP()
if !currentIP.IsValid() {
return fmt.Errorf("%w: for hostname %s", ErrRecordIPNotSet, hostname)
}
lookedUpNetIPs, err := resolver.LookupIP(context.Background(), "ip", hostname)
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(lookedUpNetIPs))
for i, netIP := range lookedUpNetIPs {
var ip netip.Addr
switch {
case netIP == nil:
case netIP.To4() != nil:
ip = netip.AddrFrom4([4]byte(netIP.To4()))
default: // IPv6
ip = netip.AddrFrom16([16]byte(netIP.To16()))
}
if ip.Compare(currentIP) == 0 {
lookedUpIPsString := make([]string, len(lookedUpIPs))
for i, lookedUpIP := range lookedUpIPs {
lookedUpIPsString[i] = lookedUpIP.String()
if lookedUpIP.Equal(currentIP) {
found = true
break
}
lookedUpIPsString[i] = ip.String()
}
if !found {
return fmt.Errorf("%w: %s instead of %s for %s",
ErrLookupMismatch, strings.Join(lookedUpIPsString, ","), currentIP, hostname)
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s",
hostname, strings.Join(lookedUpIPsString, ","), currentIP)
}
}
return nil

View File

@@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"strconv"
"time"
)
@@ -14,28 +14,27 @@ func IsClientMode(args []string) bool {
return len(args) > 1 && args[1] == "healthcheck"
}
type Client struct {
type Client interface {
Query(ctx context.Context, port uint16) error
}
type client struct {
*http.Client
}
func NewClient() *Client {
func NewClient() Client {
const timeout = 5 * time.Second
return &Client{
return &client{
Client: &http.Client{Timeout: timeout},
}
}
var ErrUnhealthy = errors.New("program is unhealthy")
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, listeningAddress string) error {
_, port, err := net.SplitHostPort(listeningAddress)
if err != nil {
return fmt.Errorf("splitting host and port from address: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+port, nil)
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
}
@@ -51,8 +50,8 @@ func (c *Client) Query(ctx context.Context, listeningAddress string) error {
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading body from response with status %s: %w", resp.Status, err)
return fmt.Errorf("%s: %s", resp.Status, err)
}
return fmt.Errorf("%w: %s", ErrUnhealthy, string(b))
return fmt.Errorf(string(b))
}

View File

@@ -2,15 +2,19 @@ package health
import (
"net/http"
"github.com/qdm12/golibs/logging"
)
func newHandler(healthcheck func() error) http.Handler {
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
}
@@ -19,8 +23,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
err := h.healthcheck()
if err != nil {
if err := h.healthcheck(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

Some files were not shown because too many files have changed in this diff Show More