mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-23 01:22:18 -04:00
Compare commits
90 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4e1cca222 | ||
|
|
79efca2bf4 | ||
|
|
f17be83112 | ||
|
|
2a9e41c646 | ||
|
|
8664aa0e5c | ||
|
|
4c840f4bb6 | ||
|
|
7230fea962 | ||
|
|
8aa6aba8b7 | ||
|
|
1d8690fb14 | ||
|
|
2922d441d2 | ||
|
|
607ca8f0b8 | ||
|
|
d37f05766b | ||
|
|
ca85596e19 | ||
|
|
cf184070b8 | ||
|
|
012a6dddcd | ||
|
|
d3b689d0ef | ||
|
|
a1a9fc0b62 | ||
|
|
2f88d44af7 | ||
|
|
5d74d11835 | ||
|
|
2f2bef3d6c | ||
|
|
3494cfbcbb | ||
|
|
b7986dd6b9 | ||
|
|
3644e5cc21 | ||
|
|
2f252a3ec1 | ||
|
|
270014ccda | ||
|
|
b70a91582c | ||
|
|
5b384cbf18 | ||
|
|
ff3b661d66 | ||
|
|
b3134c7770 | ||
|
|
b9bbbf26d2 | ||
|
|
dfd352b817 | ||
|
|
a84a1d664a | ||
|
|
25472f43c3 | ||
|
|
aa4a1f3813 | ||
|
|
76afd8361e | ||
|
|
cc995a79c8 | ||
|
|
776206eec8 | ||
|
|
c1bf7a49c1 | ||
|
|
987138dfc1 | ||
|
|
85780dc6e1 | ||
|
|
130ab008b5 | ||
|
|
20792e9460 | ||
|
|
8e802d45ae | ||
|
|
2b02ac154e | ||
|
|
8e09cd6342 | ||
|
|
542e89536c | ||
|
|
093e8154f3 | ||
|
|
b131c3d90b | ||
|
|
d3541da812 | ||
|
|
937a249ffa | ||
|
|
11575ee82a | ||
|
|
ae4ab39421 | ||
|
|
3e6336fcbc | ||
|
|
6d4bb626aa | ||
|
|
9b142c99dc | ||
|
|
7d627c8581 | ||
|
|
b72711b0de | ||
|
|
8f34c766c5 | ||
|
|
ae2bcd55c8 | ||
|
|
dcc66fb857 | ||
|
|
b31d848d96 | ||
|
|
99d670f7f9 | ||
|
|
a5cc9da33c | ||
|
|
877531c3d9 | ||
|
|
bfdae74925 | ||
|
|
c09e01c6c7 | ||
|
|
6a6b1a8ebb | ||
|
|
346f4aa7f7 | ||
|
|
5e0e3f4702 | ||
|
|
d009ef3d3e | ||
|
|
1706443ecb | ||
|
|
ec4411e12d | ||
|
|
1697697b81 | ||
|
|
cb3075ea32 | ||
|
|
4499d87e05 | ||
|
|
e676983dba | ||
|
|
5d550980c1 | ||
|
|
983f41743d | ||
|
|
7bbc860225 | ||
|
|
9835d5a5d7 | ||
|
|
463ad6ac93 | ||
|
|
133956f082 | ||
|
|
7aa14c8737 | ||
|
|
845770f441 | ||
|
|
8f709d7c90 | ||
|
|
2af4a1bf82 | ||
|
|
0bb717c55a | ||
|
|
d07ce88aa3 | ||
|
|
860820087f | ||
|
|
e793c4926b |
@@ -7,7 +7,7 @@ It works on Linux, Windows and OSX.
|
||||
## Requirements
|
||||
|
||||
- [VS code](https://code.visualstudio.com/download) installed
|
||||
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [VS code dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
|
||||
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) installed
|
||||
|
||||
@@ -23,7 +23,7 @@ It works on Linux, Windows and OSX.
|
||||
|
||||
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. Select `Dev Containers: Open Folder in Container...` and choose the project directory.
|
||||
|
||||
## Customization
|
||||
|
||||
|
||||
81
.github/CONTRIBUTING.md
vendored
81
.github/CONTRIBUTING.md
vendored
@@ -1,17 +1,92 @@
|
||||
# Contributing
|
||||
|
||||
Contributions are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [open source license of this project](../LICENSE).
|
||||
## Table of content
|
||||
|
||||
1. [Submitting a pull request](#submitting-a-pull-request)
|
||||
1. [Development setup](#development-setup)
|
||||
1. [Commands available](#commands-available)
|
||||
1. [Add a new DNS provider](#add-a-new-dns-provider)
|
||||
1. [License](#license)
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
1. [Fork](https://github.com/qdm12/ddns-updater/fork) and clone the repository
|
||||
1. Create a new branch `git checkout -b my-branch-name`
|
||||
1. Modify the code
|
||||
1. Ensure the docker build succeeds `docker build .`
|
||||
1. Commit your modifications
|
||||
1. Push to your fork and [submit a pull request](https://github.com/qdm12/ddns-updater/compare)
|
||||
|
||||
## Resources
|
||||
Additional resources:
|
||||
|
||||
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
|
||||
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
|
||||
|
||||
## Development setup
|
||||
|
||||
### Using VSCode and Docker
|
||||
|
||||
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [dev containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Dev Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1:
|
||||
|
||||
### Locally
|
||||
|
||||
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go).
|
||||
|
||||
## Commands available
|
||||
|
||||
- Test the code: `go test ./...`
|
||||
- Lint the code `golangci-lint run`
|
||||
- Build the program: `go build -o app cmd/updater/main.go`
|
||||
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
|
||||
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
|
||||
|
||||
## Add a new DNS provider
|
||||
|
||||
An "example" DNS provider is present in the code, you can simply copy paste it modify it to your needs.
|
||||
In more detailed steps:
|
||||
|
||||
1. Copy the directory [`internal/provider/providers/example`](../internal/provider/providers/example) to `internal/provider/providers/yourprovider` where `yourprovider` is the name of the DNS provider you want to add, in a single word without spaces, dashes or underscores.
|
||||
1. Modify the `internal/provider/providers/yourprovider/provider.go` file to fit the requirements of your DNS provider. There are many `// TODO` comments you can follow and **need to remove** when done.
|
||||
1. Add the provider name constant to the `ProviderChoices` function in [`internal/provider/constants/providers.go`](../internal/provider/constants/providers.go). For example:
|
||||
|
||||
```go
|
||||
func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
// ...
|
||||
Example,
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Add a case for your provider in the `switch` statement in the `New` function in [`internal/provider/provider.go`](../internal/provider/provider.go). For example:
|
||||
|
||||
```go
|
||||
case constants.Example:
|
||||
return example.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
```
|
||||
|
||||
1. Copy the file [`docs/example.md`](../docs/example.md) to `docs/yourprovider.md` and modify it to fit the configuration and domain setup of your DNS provider. There are a few `<!-- ... -->` comments indicating what to change, please **remove them** when done.
|
||||
1. In the [README.md](../README.md):
|
||||
1. Add your provider name to the list of providers supported `- Your provider`
|
||||
1. Add your provider name and link to its document to the second list: `- [Your provider](docs/yourprovider.md)`
|
||||
1. Make sure to run the actual program (in Docker or directly) and check it updates your DNS records as expected, of course 😉 You can do this by setting a record to `127.0.0.1` manually and then run the updater to see if the update succeeds.
|
||||
1. Profit 🎉 Don't forget to [open a pull request](https://github.com/qdm12/ddns-updater/compare)
|
||||
|
||||
## License
|
||||
|
||||
Contributions are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [open source license of this project](../LICENSE).
|
||||
|
||||
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
env:
|
||||
DOCKER_BUILDKIT: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: reviewdog/action-misspell@v1
|
||||
with:
|
||||
@@ -72,7 +72,10 @@ jobs:
|
||||
contents: read
|
||||
security-events: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "^1.22"
|
||||
- uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: go
|
||||
@@ -94,7 +97,7 @@ jobs:
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # for gorelease last step
|
||||
|
||||
@@ -119,15 +122,15 @@ jobs:
|
||||
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@v2
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: qmcgaw
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- uses: docker/login-action@v2
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -138,7 +141,7 @@ jobs:
|
||||
run: echo "::set-output name=value::$(git rev-parse --short HEAD)"
|
||||
|
||||
- name: Build and push final image
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
uses: docker/build-push-action@v5
|
||||
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 }}
|
||||
@@ -150,12 +153,12 @@ jobs:
|
||||
push: true
|
||||
|
||||
- if: github.event_name == 'release'
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.21
|
||||
go-version: 1.22
|
||||
|
||||
- if: github.event_name == 'release'
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
version: latest
|
||||
args: release --clean --config .github/workflows/configs/.goreleaser.yaml
|
||||
|
||||
8
.github/workflows/configs/mlc-config.json
vendored
8
.github/workflows/configs/mlc-config.json
vendored
@@ -22,7 +22,13 @@
|
||||
"pattern": "https://www.linode.com/docs/products/tools/api/guides/manage-api-tokens/"
|
||||
},
|
||||
{
|
||||
"pattern": "https://ipv6.ipleak.net/json"
|
||||
"pattern": "https://(ip|ipv|v)6.+"
|
||||
},
|
||||
{
|
||||
"pattern": "https://github.com/qdm12/ddns-updater/pkgs/container/ddns-updater"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://www.duckdns.org/$"
|
||||
}
|
||||
],
|
||||
"timeout": "20s",
|
||||
|
||||
4
.github/workflows/labels.yml
vendored
4
.github/workflows/labels.yml
vendored
@@ -10,9 +10,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Labeler
|
||||
if: success()
|
||||
uses: crazy-max/ghaction-github-labeler@v4
|
||||
uses: crazy-max/ghaction-github-labeler@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
4
.github/workflows/markdown.yml
vendored
4
.github/workflows/markdown.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v14
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
with:
|
||||
globs: "**.md"
|
||||
config: .markdownlint.json
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
use-quiet-mode: yes
|
||||
config-file: .github/workflows/configs/mlc-config.json
|
||||
|
||||
- uses: peter-evans/dockerhub-description@v3
|
||||
- uses: peter-evans/dockerhub-description@v4
|
||||
if: github.repository == 'qdm12/ddns-updater' && github.event_name == 'push'
|
||||
with:
|
||||
username: qmcgaw
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
linters-settings:
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
@@ -11,6 +9,12 @@ issues:
|
||||
- containedctx
|
||||
- dupl
|
||||
- goerr113
|
||||
- text: Function `exitHealthchecksio` should pass the context parameter
|
||||
linters:
|
||||
- contextcheck
|
||||
- source: "See https://"
|
||||
linters:
|
||||
- lll
|
||||
|
||||
linters:
|
||||
enable:
|
||||
@@ -20,6 +24,7 @@ linters:
|
||||
- bidichk
|
||||
- bodyclose
|
||||
- containedctx
|
||||
- contextcheck
|
||||
- decorder
|
||||
- dogsled
|
||||
- dupl
|
||||
@@ -36,6 +41,7 @@ linters:
|
||||
- gocheckcompilerdirectives
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gochecksumtype
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
@@ -48,8 +54,10 @@ linters:
|
||||
- gomoddirectives
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosmopolitan
|
||||
- grouper
|
||||
- importas
|
||||
- inamedparam
|
||||
- interfacebloat
|
||||
- ireturn
|
||||
- lll
|
||||
@@ -65,13 +73,17 @@ linters:
|
||||
- nolintlint
|
||||
- nosprintfhostport
|
||||
- paralleltest
|
||||
- perfsprint
|
||||
- prealloc
|
||||
- predeclared
|
||||
- promlinter
|
||||
- protogetter
|
||||
- reassign
|
||||
- revive
|
||||
- rowserrcheck
|
||||
- sloglint
|
||||
- sqlclosecheck
|
||||
- tagalign
|
||||
- tenv
|
||||
- thelper
|
||||
- tparallel
|
||||
@@ -80,8 +92,4 @@ linters:
|
||||
- usestdlibvars
|
||||
- wastedassign
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- .devcontainer
|
||||
- .github
|
||||
- zerologlint
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -1,8 +1,8 @@
|
||||
ARG BUILDPLATFORM=linux/amd64
|
||||
ARG ALPINE_VERSION=3.19
|
||||
ARG GO_VERSION=1.21
|
||||
ARG GO_VERSION=1.22
|
||||
ARG XCPUTRANSLATE_VERSION=v0.6.0
|
||||
ARG GOLANGCI_LINT_VERSION=v1.55.2
|
||||
ARG GOLANGCI_LINT_VERSION=v1.56.2
|
||||
ARG MOCKGEN_VERSION=v1.6.0
|
||||
|
||||
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
|
||||
@@ -50,6 +50,8 @@ RUN git init && \
|
||||
rm -rf .git/
|
||||
|
||||
FROM --platform=$BUILDPLATFORM base AS build
|
||||
RUN mkdir -p /tmp/data && \
|
||||
touch /tmp/isdocker
|
||||
ARG VERSION=unknown
|
||||
ARG CREATED="an unknown date"
|
||||
ARG COMMIT=unknown
|
||||
@@ -64,11 +66,14 @@ RUN GOARCH="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field a
|
||||
|
||||
FROM scratch
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/ddns-updater", "healthcheck"]
|
||||
ARG UID=1000
|
||||
ARG GID=1000
|
||||
USER ${UID}:${GID}
|
||||
ENTRYPOINT ["/updater/app"]
|
||||
WORKDIR /updater
|
||||
ENTRYPOINT ["/updater/ddns-updater"]
|
||||
COPY --from=build --chown=${UID}:${GID} /tmp/data /updater/data
|
||||
COPY --from=build --chown=${UID}:${GID} /tmp/isdocker /updater/isdocker
|
||||
ENV \
|
||||
# Core
|
||||
CONFIG= \
|
||||
@@ -82,9 +87,11 @@ ENV \
|
||||
PUBLICIP_DNS_TIMEOUT=3s \
|
||||
HTTP_TIMEOUT=10s \
|
||||
DATADIR=/updater/data \
|
||||
CONFIG_FILEPATH=/updater/data/config.json \
|
||||
RESOLVER_ADDRESS= \
|
||||
RESOLVER_TIMEOUT=5s \
|
||||
# Web UI
|
||||
SERVER_ENABLED=yes \
|
||||
LISTENING_ADDRESS=:8000 \
|
||||
ROOT_URL=/ \
|
||||
# Backup
|
||||
@@ -97,6 +104,7 @@ ENV \
|
||||
SHOUTRRR_DEFAULT_TITLE="DDNS Updater" \
|
||||
TZ= \
|
||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999 \
|
||||
HEALTH_HEALTHCHECKSIO_BASE_URL=https://hc-ping.com \
|
||||
HEALTH_HEALTHCHECKSIO_UUID=
|
||||
ARG VERSION=unknown
|
||||
ARG CREATED="an unknown date"
|
||||
@@ -111,4 +119,4 @@ LABEL \
|
||||
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=${UID}:${GID} /tmp/gobuild/app /updater/ddns-updater
|
||||
|
||||
155
README.md
155
README.md
@@ -1,6 +1,6 @@
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
# Lightweight universal DDNS Updater program
|
||||
|
||||
Light container updating DNS A and/or AAAA records periodically for multiple DNS providers
|
||||
Program to keep DNS A and/or AAAA records updated for multiple DNS providers
|
||||
|
||||
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg">
|
||||
|
||||
@@ -30,11 +30,25 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
|
||||
[](LICENSE)
|
||||

|
||||
|
||||
## Versioned documentation
|
||||
|
||||
This readme and the [docs/](docs/) directory are **versioned** to match the program version:
|
||||
|
||||
| Version | Readme link | Docs link |
|
||||
| --- | --- | --- |
|
||||
| Latest | [README](https://github.com/qdm12/ddns-updater/blob/master/README.md) | [docs/](https://github.com/qdm12/ddns-updater/tree/master/docs) |
|
||||
| `v2.7` | [README](https://github.com/qdm12/ddns-updater/blob/v2.7.1/README.md) | [docs/](https://github.com/qdm12/ddns-updater/blob/v2.7.1/docs) |
|
||||
| `v2.6` | [README](https://github.com/qdm12/ddns-updater/blob/v2.6.1/README.md) | [docs/](https://github.com/qdm12/ddns-updater/blob/v2.6.1/docs) |
|
||||
| `v2.5` | [README](https://github.com/qdm12/ddns-updater/blob/v2.5/README.md) | [docs/](https://github.com/qdm12/ddns-updater/blob/v2.5/docs) |
|
||||
|
||||
## Features
|
||||
|
||||
- Available as a Docker image [`qmcgaw/ddns-updater`](https://hub.docker.com/r/qmcgaw/ddns-updater) and [`ghcr.io/qdm12/ddns-updater`]((https://github.com/qdm12/ddns-updater/pkgs/container/ddns-updater))
|
||||
- 🆕 Available as [zero-dependency binaries for Linux, Windows and MacOS](https://github.com/qdm12/ddns-updater/releases)
|
||||
- Updates periodically A records for different DNS providers:
|
||||
- Aliyun
|
||||
- AllInkl
|
||||
- Changeip
|
||||
- Cloudflare
|
||||
- DD24
|
||||
- DDNSS.de
|
||||
@@ -69,6 +83,7 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
|
||||
- OpenDNS
|
||||
- OVH
|
||||
- Porkbun
|
||||
- Route53
|
||||
- Selfhost.de
|
||||
- Servercow.de
|
||||
- Spdyn
|
||||
@@ -76,36 +91,74 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
|
||||
- Variomedia.de
|
||||
- Zoneedit
|
||||
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
|
||||
- Web User interface
|
||||
- Web user interface (Desktop)
|
||||
|
||||

|
||||

|
||||
|
||||
- Web user interface (Mobile)
|
||||
|
||||

|
||||
|
||||
- 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.
|
||||
- Container (Docker/K8s) specific features:
|
||||
- Lightweight 12MB Docker image based on the Scratch Docker image
|
||||
- Docker healthcheck verifying the DNS resolution of your domains
|
||||
- Images compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures
|
||||
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
|
||||
|
||||
## Setup
|
||||
|
||||
The program reads the configuration from a JSON object, either from a file or from an environment variable.
|
||||
### Binary programs
|
||||
|
||||
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
|
||||
1. Download the pre-built program for your platform from the assets of a release in the [releases page](https://github.com/qdm12/ddns-updater/releases). Note this is only available from [release v2.6.0](https://github.com/qdm12/ddns-updater/releases/tag/v2.6.0).
|
||||
1. For Linux and MacOS, make the program executable with `chmod +x ddns-updater`.
|
||||
1. In the directory where the program is saved, create a directory `data`.
|
||||
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 program with `./ddns-updater` (`./ddns-updater.exe` on Windows) or by double-clicking on it.
|
||||
1. The following is **optional**.
|
||||
- You can customize the program behavior using either [environment variables](#environment-variables) or flags. For flags, there is a flag corresponding to each environment variable, where it's all lowercase and underscores are replaced with dashes. For example the environment variable `LOG_LEVEL` translates into `--log-level`.
|
||||
|
||||
### Container
|
||||
|
||||
[➡️ Qnap guide by @Araminta](https://github.com/qdm12/ddns-updater/issues/708)
|
||||
|
||||
1. Create a directory, for example, *data* which is:
|
||||
- owned by user id `1000`, which is the built-in user ID of the ddns-updater container
|
||||
- has user read+write+execute permissions
|
||||
|
||||
```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
|
||||
chown 1000 data
|
||||
chmod u+r+w+x data
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
1. Similarly, create a *data/config.json* file which is:
|
||||
- owned by user id `1000`
|
||||
- has user read permissions
|
||||
|
||||
```sh
|
||||
touch data/config.json
|
||||
chmod u+r data/config.json
|
||||
```
|
||||
|
||||
1. Edit *data/config.json*, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -128,27 +181,13 @@ The program reads the configuration from a JSON object, either from a file or fr
|
||||
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)
|
||||
1. The following is **optional**.
|
||||
- You can customize the program behavior using [environment variables](#environment-variables)
|
||||
- You can use [docker-compose.yml](docker-compose.yml) with `docker-compose up -d`
|
||||
- **Kubernetes**: check out the [k8s directory](k8s) for an installation guide and examples.
|
||||
- Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags)
|
||||
- You can update the image with `docker pull qmcgaw/ddns-updater`
|
||||
- You can 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.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -171,6 +210,8 @@ For each setting, you need to fill in parameters.
|
||||
Check the documentation for your DNS provider:
|
||||
|
||||
- [Aliyun](docs/aliyun.md)
|
||||
- [Allinkl](docs/allinkl.md)
|
||||
- [ChangeIP](docs/changeip.md)
|
||||
- [Cloudflare](docs/cloudflare.md)
|
||||
- [Custom](docs/custom.md)
|
||||
- [DDNSS.de](docs/ddnss.de.md)
|
||||
@@ -219,7 +260,7 @@ Note that:
|
||||
|
||||
### Environment variables
|
||||
|
||||
🆕 There are now flags equivalent for each variable below, for example `--ipv6-prefix`.
|
||||
🆕 There are now flags equivalent for each variable below, for example `--log-level`.
|
||||
|
||||
| Environment variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
@@ -233,14 +274,17 @@ Note that:
|
||||
| `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 |
|
||||
| `SERVER_ENABLED` | `yes` | Enable the web server and web UI |
|
||||
| `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 |
|
||||
| `HEALTH_HEALTHCHECKSIO_BASE_URL` | `https://hc-ping.com` | Base URL for the [healthchecks.io](https://healthchecks.io) server |
|
||||
| `HEALTH_HEALTHCHECKSIO_UUID` | | UUID to idenfity with the [healthchecks.io](https://healthchecks.io) server |
|
||||
| `DATADIR` | `/updater/data` | Directory to read and write data files from internally |
|
||||
| `CONFIG_FILEPATH` | `/updater/data/config.json` | Path to the JSON configuration file |
|
||||
| `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) |
|
||||
| `RESOLVER_ADDRESS` | Your network DNS | A plaintext DNS address to use to resolve your domain names defined in your settings only. For example it can be `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) |
|
||||
@@ -261,18 +305,33 @@ You can otherwise customize it with 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`
|
||||
- `icanhazip` using [https://icanhazip.com](https://icanhazip.com)
|
||||
- `ident` using [https://ident.me](https://ident.me)
|
||||
- `nnev` using [https://ip.nnev.de](https://ip.nnev.de)
|
||||
- `wtfismyip` using [https://wtfismyip.com/text](https://wtfismyip.com/text)
|
||||
- `seeip` using [https://api.seeip.org](https://api.seeip.org)
|
||||
- `changeip` using [https://ip.changeip.com](https://ip.changeip.com)
|
||||
- You can also specify an HTTPS URL with prefix `url:` for example `url: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`
|
||||
- `icanhazip` using [https://ipv4.icanhazip.com](https://ipv4.icanhazip.com)
|
||||
- `ident` using [https://v4.ident.me](https://v4.ident.me)
|
||||
- `nnev` using [https://ip4.nnev.de](https://ip4.nnev.de)
|
||||
- `wtfismyip` using [https://ipv4.wtfismyip.com/text](https://ipv4.wtfismyip.com/text)
|
||||
- `seeip` using [https://ipv4.seeip.org](https://ipv4.seeip.org)
|
||||
- You can also specify an HTTPS URL with prefix `url:` for example `url: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`
|
||||
- `icanhazip` using [https://ipv6.icanhazip.com](https://ipv6.icanhazip.com)
|
||||
- `ident` using [https://v6.ident.me](https://v6.ident.me)
|
||||
- `nnev` using [https://ip6.nnev.de](https://ip6.nnev.de)
|
||||
- `wtfismyip` using [https://ipv6.wtfismyip.com/text](https://ipv6.wtfismyip.com/text)
|
||||
- `seeip` using [https://ipv6.seeip.org](https://ipv6.seeip.org)
|
||||
- You can also specify an HTTPS URL with prefix `url:` for example `url: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`
|
||||
@@ -348,7 +407,7 @@ You can use optional build arguments with `--build-arg KEY=VALUE` from the table
|
||||
|
||||
## Development and contributing
|
||||
|
||||
- [Contribute with code](docs/contributing.md)
|
||||
- [Contribute with code](.github/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)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -19,16 +18,18 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/health"
|
||||
"github.com/qdm12/ddns-updater/internal/healthchecksio"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/noop"
|
||||
jsonparams "github.com/qdm12/ddns-updater/internal/params"
|
||||
persistence "github.com/qdm12/ddns-updater/internal/persistence/json"
|
||||
"github.com/qdm12/ddns-updater/internal/provider"
|
||||
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/update"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/goservices"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/goshutdown"
|
||||
"github.com/qdm12/gosplash"
|
||||
"github.com/qdm12/log"
|
||||
)
|
||||
@@ -98,58 +99,35 @@ func main() {
|
||||
|
||||
func _main(ctx context.Context, reader *reader.Reader, args []string, logger log.LoggerInterface,
|
||||
buildInfo models.BuildInformation, timeNow func() time.Time) (err error) {
|
||||
if health.IsClientMode(args) {
|
||||
// Running the program in a separate instance through the Docker
|
||||
// built-in healthcheck, in an ephemeral fashion to query the
|
||||
// long running instance of the program about its status
|
||||
if len(args) > 1 {
|
||||
switch args[1] {
|
||||
case "version", "-version", "--version":
|
||||
fmt.Println(buildInfo.VersionString())
|
||||
return nil
|
||||
case "healthcheck":
|
||||
// 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)
|
||||
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)
|
||||
}
|
||||
|
||||
client := health.NewClient()
|
||||
return client.Query(ctx, *healthSettings.ServerAddress)
|
||||
}
|
||||
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2023-07-15T00:00:00Z")
|
||||
printSplash(buildInfo)
|
||||
|
||||
config, err := readConfig(reader, logger)
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading settings: %w", err)
|
||||
}
|
||||
config.SetDefaults()
|
||||
err = config.Validate()
|
||||
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,
|
||||
@@ -168,8 +146,7 @@ func _main(ctx context.Context, reader *reader.Reader, args []string, logger log
|
||||
}
|
||||
|
||||
jsonReader := jsonparams.NewReader(logger)
|
||||
jsonFilepath := filepath.Join(*config.Paths.DataDir, "config.json")
|
||||
providers, warnings, err := jsonReader.JSONProviders(jsonFilepath)
|
||||
providers, warnings, err := jsonReader.JSONProviders(*config.Paths.Config)
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
shoutrrrClient.Notify(w)
|
||||
@@ -179,45 +156,22 @@ func _main(ctx context.Context, reader *reader.Reader, args []string, logger log
|
||||
return err
|
||||
}
|
||||
|
||||
L := len(providers)
|
||||
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")
|
||||
}
|
||||
logProvidersCount(len(providers), logger)
|
||||
|
||||
client := &http.Client{Timeout: config.Client.Timeout}
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
err = health.CheckHTTP(ctx, client)
|
||||
if err != nil {
|
||||
logger.Warn(err.Error())
|
||||
}
|
||||
|
||||
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())
|
||||
if err != nil {
|
||||
shoutrrrClient.Notify(err.Error())
|
||||
return err
|
||||
}
|
||||
records[i] = recordslib.New(provider, events)
|
||||
records, err := readRecords(providers, persistentDB, logger, shoutrrrClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading records: %w", err)
|
||||
}
|
||||
|
||||
defer client.CloseIdleConnections()
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
defer func() {
|
||||
err := db.Close()
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
httpSettings := publicip.HTTPSettings{
|
||||
Enabled: *config.PubIP.HTTPEnabled,
|
||||
@@ -243,84 +197,173 @@ func _main(ctx context.Context, reader *reader.Reader, args []string, logger log
|
||||
return fmt.Errorf("creating resolver: %w", err)
|
||||
}
|
||||
|
||||
hioClient := healthchecksio.New(client, *config.Health.HealthchecksioUUID)
|
||||
hioClient := healthchecksio.New(client, config.Health.HealthchecksioBaseURL,
|
||||
*config.Health.HealthchecksioUUID)
|
||||
|
||||
updater := update.NewUpdater(db, client, shoutrrrClient, logger, timeNow)
|
||||
runner := update.NewRunner(db, updater, ipGetter, config.Update.Period,
|
||||
updaterService := update.NewService(db, updater, ipGetter, config.Update.Period,
|
||||
config.Update.Cooldown, logger, resolver, timeNow, hioClient)
|
||||
|
||||
runnerHandler, runnerCtx, runnerDone := goshutdown.NewGoRoutineHandler("runner")
|
||||
go runner.Run(runnerCtx, runnerDone)
|
||||
healthServer, err := createHealthServer(db, resolver, logger, *config.Health.ServerAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating health server: %w", err)
|
||||
}
|
||||
|
||||
server, err := createServer(ctx, config.Server, logger, db, updaterService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating server: %w", err)
|
||||
}
|
||||
|
||||
var backupService goservices.Service
|
||||
backupLogger := logger.New(log.SetComponent("backup"))
|
||||
backupService = backup.New(*config.Backup.Period, *config.Paths.DataDir,
|
||||
*config.Backup.Directory, backupLogger)
|
||||
backupService, err = goservices.NewRestarter(goservices.RestarterSettings{Service: backupService})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating backup restarter: %w", err)
|
||||
}
|
||||
|
||||
servicesSequence, err := goservices.NewSequence(goservices.SequenceSettings{
|
||||
ServicesStart: []goservices.Service{db, updaterService, healthServer, server, backupService},
|
||||
ServicesStop: []goservices.Service{server, healthServer, updaterService, backupService, db},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating services sequence: %w", err)
|
||||
}
|
||||
|
||||
runError, startErr := servicesSequence.Start(ctx)
|
||||
if startErr != nil {
|
||||
return fmt.Errorf("starting services: %w", startErr)
|
||||
}
|
||||
|
||||
// note: errors are logged within the goroutine,
|
||||
// no need to collect the resulting errors.
|
||||
go runner.ForceUpdate(ctx)
|
||||
go updaterService.ForceUpdate(ctx)
|
||||
|
||||
isHealthy := health.MakeIsHealthy(db, resolver)
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck server"))
|
||||
healthServer := health.NewServer(*config.Health.ServerAddress,
|
||||
healthLogger, isHealthy)
|
||||
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler("health server")
|
||||
go healthServer.Run(healthServerCtx, healthServerDone)
|
||||
|
||||
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")
|
||||
go server.Run(serverCtx, serverDone)
|
||||
shoutrrrClient.Notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
|
||||
|
||||
backupHandler, backupCtx, backupDone := goshutdown.NewGoRoutineHandler("backup")
|
||||
backupLogger := logger.New(log.SetComponent("backup"))
|
||||
go backupRunLoop(backupCtx, backupDone, *config.Backup.Period, *config.Paths.DataDir,
|
||||
*config.Backup.Directory, backupLogger, timeNow)
|
||||
|
||||
shutdownGroup := goshutdown.NewGroupHandler("")
|
||||
shutdownGroup.Add(runnerHandler, healthServerHandler, serverHandler, backupHandler)
|
||||
|
||||
<-ctx.Done()
|
||||
|
||||
err = shutdownGroup.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case err = <-runError:
|
||||
exitHealthchecksio(hioClient, logger, healthchecksio.Exit1)
|
||||
shoutrrrClient.Notify(err.Error())
|
||||
return err
|
||||
return fmt.Errorf("exiting due to critical error: %w", err)
|
||||
}
|
||||
|
||||
err = servicesSequence.Stop()
|
||||
if err != nil {
|
||||
exitHealthchecksio(hioClient, logger, healthchecksio.Exit1)
|
||||
shoutrrrClient.Notify(err.Error())
|
||||
return fmt.Errorf("stopping failed: %w", err)
|
||||
}
|
||||
|
||||
exitHealthchecksio(hioClient, logger, healthchecksio.Exit0)
|
||||
return nil
|
||||
}
|
||||
|
||||
type InfoErroer interface {
|
||||
Info(s string)
|
||||
Error(s string)
|
||||
func printSplash(buildInfo models.BuildInformation) {
|
||||
announcementExp, err := time.Parse(time.RFC3339, "2024-10-15T00:00:00Z")
|
||||
if err != nil {
|
||||
panic(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 http provider GOOGLE is no longer working",
|
||||
AnnounceExp: announcementExp,
|
||||
// Sponsor information
|
||||
PaypalUser: "qmcgaw",
|
||||
GithubSponsor: "qdm12",
|
||||
}
|
||||
for _, line := range gosplash.MakeLines(splashSettings) {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
|
||||
func backupRunLoop(ctx context.Context, done chan<- struct{}, backupPeriod time.Duration,
|
||||
dataDir, outputDir string, logger InfoErroer, timeNow func() time.Time) {
|
||||
defer close(done)
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
func readConfig(reader *reader.Reader, logger log.LoggerInterface) (
|
||||
config config.Config, err error) {
|
||||
err = config.Read(reader, logger)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("reading settings: %w", err)
|
||||
}
|
||||
logger.Info("each " + backupPeriod.String() +
|
||||
"; writing zip files to directory " + 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(
|
||||
zipFilepath,
|
||||
filepath.Join(dataDir, "updates.json"),
|
||||
filepath.Join(dataDir, "config.json"),
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
timer.Reset(backupPeriod)
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
config.SetDefaults()
|
||||
err = config.Validate()
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("settings validation: %w", err)
|
||||
}
|
||||
|
||||
logger.Patch(config.Logger.ToOptions()...)
|
||||
logger.Info(config.String())
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func logProvidersCount(providersCount int, logger log.LeveledLogger) {
|
||||
switch providersCount {
|
||||
case 0:
|
||||
logger.Warn("Found no setting to update record")
|
||||
case 1:
|
||||
logger.Info("Found single setting to update record")
|
||||
default:
|
||||
logger.Info("Found " + strconv.Itoa(providersCount) + " settings to update records")
|
||||
}
|
||||
}
|
||||
|
||||
func readRecords(providers []provider.Provider, persistentDB *persistence.Database,
|
||||
logger log.LoggerInterface, shoutrrrClient *shoutrrr.Client) (
|
||||
records []recordslib.Record, err error) {
|
||||
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())
|
||||
if err != nil {
|
||||
shoutrrrClient.Notify(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
records[i] = recordslib.New(provider, events)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func exitHealthchecksio(hioClient *healthchecksio.Client,
|
||||
logger log.LoggerInterface, state healthchecksio.State) {
|
||||
const timeout = 3 * time.Second
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
err := hioClient.Ping(ctx, state)
|
||||
if err != nil {
|
||||
logger.Error(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:ireturn
|
||||
func createHealthServer(db health.AllSelecter, resolver health.LookupIPer,
|
||||
logger log.LoggerInterface, serverAddress string) (
|
||||
healthServer goservices.Service, err error) {
|
||||
if !health.IsDocker() {
|
||||
return noop.New("healthcheck server"), nil
|
||||
}
|
||||
isHealthy := health.MakeIsHealthy(db, resolver)
|
||||
healthLogger := logger.New(log.SetComponent("healthcheck server"))
|
||||
return health.NewServer(serverAddress, healthLogger, isHealthy)
|
||||
}
|
||||
|
||||
//nolint:ireturn
|
||||
func createServer(ctx context.Context, config config.Server,
|
||||
logger log.LoggerInterface, db server.Database,
|
||||
updaterService server.UpdateForcer) (
|
||||
service goservices.Service, err error) {
|
||||
if !*config.Enabled {
|
||||
return noop.New("server"), nil
|
||||
}
|
||||
serverLogger := logger.New(log.SetComponent("http server"))
|
||||
return server.New(ctx, config.ListeningAddress, config.RootURL,
|
||||
db, serverLogger, updaterService)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -30,6 +30,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
35
docs/changeip.md
Normal file
35
docs/changeip.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# ChangeIP
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "changeip",
|
||||
"domain": "domain.com",
|
||||
"host": "host",
|
||||
"username": "dynXXXXXXX",
|
||||
"password": "password",
|
||||
"ip_version": "ipv4",
|
||||
"ipv6_suffix": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host (subdomain)
|
||||
- `"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 identifier suffix 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
|
||||
@@ -37,6 +37,6 @@ See [this issue comment for context](https://github.com/qdm12/ddns-updater/issue
|
||||
|
||||
- `"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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Contributing
|
||||
|
||||
## Table of content
|
||||
|
||||
1. [Setup](#setup)
|
||||
1. [Commands available](#commands-available)
|
||||
1. [Guidelines](#guidelines)
|
||||
|
||||
## Setup
|
||||
|
||||
### Using VSCode and Docker
|
||||
|
||||
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1:
|
||||
|
||||
### Locally
|
||||
|
||||
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
|
||||
|
||||
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go).
|
||||
|
||||
## Build and Run
|
||||
|
||||
```sh
|
||||
go build -o app cmd/updater/main.go
|
||||
./app
|
||||
```
|
||||
|
||||
## Commands available
|
||||
|
||||
- Test the code: `go test ./...`
|
||||
- Lint the code `golangci-lint run`
|
||||
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
|
||||
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
|
||||
|
||||
## Guidelines
|
||||
|
||||
The Go code is in the Go file [cmd/updater/main.go](../cmd/updater/main.go) and the [internal directory](../internal), you might want to start reading the main.go file.
|
||||
|
||||
See the [Contributing document](../.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
@@ -39,4 +39,4 @@ Feel free to open issues to extend its configuration options.
|
||||
### 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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -28,4 +28,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
- 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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -29,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -28,6 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -32,6 +32,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -28,6 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"provider": "dondominio",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"name": "something",
|
||||
"username": "username",
|
||||
"key": "key",
|
||||
"ip_version": "ipv4",
|
||||
@@ -25,14 +24,13 @@
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is the subdomain to update which can be `@`, `*` or a subdomain
|
||||
- `"name"` is the name of the service/hosting
|
||||
- `"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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
|
||||
- `"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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -27,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
## Domain setup
|
||||
|
||||
@@ -30,6 +30,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -32,7 +32,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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)
|
||||
|
||||
|
||||
@@ -29,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -31,7 +31,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
43
docs/example.md
Normal file
43
docs/example.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Example.com
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
<!-- UPDATE THIS JSON EXAMPLE -->
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "example",
|
||||
"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 `"@"` or the wildcard `"*"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
|
||||
<!-- UPDATE THIS IF NEEDED -->
|
||||
|
||||
### 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 identifier suffix 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.
|
||||
|
||||
<!-- UPDATE THIS IF NEEDED -->
|
||||
|
||||
## Domain setup
|
||||
|
||||
<!-- FILL THIS UP WITH A FEW NUMBERED STEPS -->
|
||||
@@ -28,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ This provider uses Gandi v5 API
|
||||
### 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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
- `"ttl"` default is `3600`
|
||||
|
||||
## Domain setup
|
||||
|
||||
@@ -36,4 +36,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -30,7 +30,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"host"` is the full FQDN of your ddns address. sample.goip.de or something.goip.it
|
||||
- `"host"` is the host of your domain, for example `"example"` for `example.goip.de`.
|
||||
- `"username"` is your goip.de username listed under "Routers"
|
||||
- `"password"` is your router account password
|
||||
|
||||
@@ -33,4 +33,4 @@
|
||||
- `"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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -29,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -33,4 +33,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -25,13 +25,17 @@
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"username"`
|
||||
- `"password"`
|
||||
- `"username"` for dyndns (**not** your infomaniak admin username!)
|
||||
- `"password"` for dyndns (**not** your infomaniak admin 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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
Follow [this guide](https://www.infomaniak.com/en/support/faq/2357/getting-started-guide-dyndns-with-an-infomaniak-domain) to set up your subdomain including `username` and `password` for use in the configuration. **do not use your infomaniak admin username and password in the configuration!**
|
||||
|
||||
If you only plan on using IPv4, add your current IPv4 Address. If you only plan on using IPv6, add your current IPv6 Address. If you plan to use dual-stack (IPv4 and IPv6) addresses, it does not matter what ip-address you put in the dialog.
|
||||
|
||||
@@ -30,6 +30,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -28,4 +28,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -28,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
|
||||
@@ -30,7 +30,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
|
||||
@@ -34,4 +34,4 @@
|
||||
|
||||
- `"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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -36,4 +36,4 @@ Also keep in mind, that TTL, Expire, Retry and Refresh values of the given Domai
|
||||
### 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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -29,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -31,7 +31,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -28,4 +28,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
|
||||
@@ -31,7 +31,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -43,7 +43,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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.
|
||||
- `"mode"` select between two modes, OVH's dynamic hosting service (`"dynamic"`) or OVH's API (`"api"`). Default is `"dynamic"`
|
||||
|
||||
|
||||
@@ -31,7 +31,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
|
||||
58
docs/route53.md
Normal file
58
docs/route53.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# AWS
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "route53",
|
||||
"domain": "domain.com",
|
||||
"host": "@",
|
||||
"ip_version": "ipv4",
|
||||
"ipv6_suffix": "",
|
||||
"access_key": "ffffffffffffffffffff",
|
||||
"secret_key": "ffffffffffffffffffffffffffffffffffffffff",
|
||||
"zone_id": "A30888735ZF12K83Z6F00",
|
||||
"ttl": 300
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"` or the wildcard `"*"`
|
||||
- `"access_key"` is the `AWS_ACCESS_KEY`
|
||||
- `"secret_key"` is the `AWS_SECRET_ACCESS_KEY`
|
||||
- `"zone_id"` is identification of your hosted 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.
|
||||
- `"ttl"` amount of time, in seconds, that you want DNS recursive resolvers to cache information about this record. Defaults to `300`.
|
||||
|
||||
## Domain setup
|
||||
|
||||
Amazon has [an extensive documentation on registering or tranfering your domain to route53](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/Welcome.html).
|
||||
|
||||
### User permissions
|
||||
|
||||
Create a policy to grant access to change record sets, you can use a wildcard `*` in case you want to grant access to all your hosted zones.
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "route53:ChangeResourceRecordSets",
|
||||
"Resource": "arn:aws:route53:::hostedzone/A30888735ZF12K83Z6F00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -31,7 +31,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
- `"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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -39,5 +39,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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 IPv6**)automatically when you send an update request, without sending the new IP address detected by the program in the request.
|
||||
|
||||
@@ -29,7 +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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -31,7 +31,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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
@@ -37,7 +37,7 @@ set the environment variable as `PERIOD=11m` to check your public IP address and
|
||||
### 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.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix 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
|
||||
|
||||
44
go.mod
44
go.mod
@@ -1,47 +1,39 @@
|
||||
module github.com/qdm12/ddns-updater
|
||||
|
||||
go 1.21
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/breml/rootcerts v0.2.14
|
||||
github.com/breml/rootcerts v0.2.18
|
||||
github.com/chmike/domain v1.0.1
|
||||
github.com/containrrr/shoutrrr v0.8.0
|
||||
github.com/go-chi/chi/v5 v5.0.11
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
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/miekg/dns v1.1.61
|
||||
github.com/qdm12/goservices v0.1.0
|
||||
github.com/qdm12/gosettings v0.4.4-rc1
|
||||
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
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/mod v0.18.0
|
||||
golang.org/x/oauth2 v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.19.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // 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/go-logr/logr v1.4.1 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // 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
|
||||
golang.org/x/net v0.26.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.21.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // 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
|
||||
|
||||
179
go.sum
179
go.sum
@@ -1,72 +1,30 @@
|
||||
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=
|
||||
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/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=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/breml/rootcerts v0.2.18 h1:KjZaNT7AX/akUjzpStuwTMQs42YHlPyc6NmdwShVba0=
|
||||
github.com/breml/rootcerts v0.2.18/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
|
||||
github.com/chmike/domain v1.0.1 h1:ug6h3a7LLAfAecBAysbCXWxP1Jo8iBKWNVDxcs1BNzA=
|
||||
github.com/chmike/domain v1.0.1/go.mod h1:h558M2qGKpYRUxHHNyey6puvXkZBjvjmseOla/d1VGQ=
|
||||
github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec=
|
||||
github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o=
|
||||
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-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
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/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/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/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.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/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/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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/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=
|
||||
@@ -74,19 +32,18 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
||||
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/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
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/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/goservices v0.1.0 h1:9sODefm/yuIGS7ynCkEnNlMTAYn9GzPhtcK4F69JWvc=
|
||||
github.com/qdm12/goservices v0.1.0/go.mod h1:/JOFsAnHFiSjyoXxa5FlfX903h20K5u/3rLzCjYVMck=
|
||||
github.com/qdm12/gosettings v0.4.4-rc1 h1:VT+6O6ww3Cn5v5/LgY2zlXoiCkZzbaLDWaA8ufQoOLY=
|
||||
github.com/qdm12/gosettings v0.4.4-rc1/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
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=
|
||||
@@ -94,116 +51,56 @@ github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL
|
||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||
github.com/qdm12/log v0.1.0/go.mod h1:Vchi5M8uBvHfPNIblN4mjXn/oSbiWguQIbsgF1zdQPI=
|
||||
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/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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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=
|
||||
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/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/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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-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/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/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/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/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/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
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-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/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
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/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/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/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=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/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=
|
||||
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=
|
||||
|
||||
5
internal/backup/interfaces.go
Normal file
5
internal/backup/interfaces.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package backup
|
||||
|
||||
type Logger interface {
|
||||
Info(message string)
|
||||
}
|
||||
97
internal/backup/service.go
Normal file
97
internal/backup/service.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
// Injected fields
|
||||
backupPeriod time.Duration
|
||||
dataDir string
|
||||
outputDir string
|
||||
logger Logger
|
||||
|
||||
// Internal fields
|
||||
stopCh chan<- struct{}
|
||||
done <-chan struct{}
|
||||
}
|
||||
|
||||
func New(backupPeriod time.Duration,
|
||||
dataDir, outputDir string, logger Logger) *Service {
|
||||
return &Service{
|
||||
logger: logger,
|
||||
backupPeriod: backupPeriod,
|
||||
dataDir: dataDir,
|
||||
outputDir: outputDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) String() string {
|
||||
return "backup"
|
||||
}
|
||||
|
||||
func makeZipFileName() string {
|
||||
return "ddns-updater-backup-" + strconv.Itoa(int(time.Now().UnixNano())) + ".zip"
|
||||
}
|
||||
|
||||
func (s *Service) Start(ctx context.Context) (runError <-chan error, startErr error) {
|
||||
ready := make(chan struct{})
|
||||
runErrorCh := make(chan error)
|
||||
stopCh := make(chan struct{})
|
||||
s.stopCh = stopCh
|
||||
done := make(chan struct{})
|
||||
s.done = done
|
||||
go run(ready, runErrorCh, stopCh, done,
|
||||
s.outputDir, s.dataDir, s.backupPeriod, s.logger)
|
||||
select {
|
||||
case <-ready:
|
||||
case <-ctx.Done():
|
||||
return nil, s.Stop()
|
||||
}
|
||||
return runErrorCh, nil
|
||||
}
|
||||
|
||||
func run(ready chan<- struct{}, runError chan<- error, stopCh <-chan struct{},
|
||||
done chan<- struct{}, outputDir, dataDir string, backupPeriod time.Duration,
|
||||
logger Logger) {
|
||||
defer close(done)
|
||||
|
||||
if backupPeriod == 0 {
|
||||
close(ready)
|
||||
logger.Info("disabled")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("each " + backupPeriod.String() +
|
||||
"; writing zip files to directory " + outputDir)
|
||||
timer := time.NewTimer(backupPeriod)
|
||||
close(ready)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-stopCh:
|
||||
_ = timer.Stop()
|
||||
return
|
||||
}
|
||||
err := zipFiles(
|
||||
filepath.Join(outputDir, makeZipFileName()),
|
||||
filepath.Join(dataDir, "config.json"),
|
||||
filepath.Join(dataDir, "updates.json"),
|
||||
)
|
||||
if err != nil {
|
||||
runError <- err
|
||||
return
|
||||
}
|
||||
timer.Reset(backupPeriod)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Stop() (err error) {
|
||||
close(s.stopCh)
|
||||
<-s.done
|
||||
return nil
|
||||
}
|
||||
@@ -6,28 +6,8 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
var _ FileZiper = (*Ziper)(nil)
|
||||
|
||||
type FileZiper interface {
|
||||
ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
}
|
||||
|
||||
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{
|
||||
createFile: os.Create,
|
||||
openFile: os.Open,
|
||||
ioCopy: io.Copy,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *Ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
f, err := z.createFile(outputFilepath)
|
||||
func zipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
f, err := os.Create(outputFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -35,7 +15,7 @@ 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)
|
||||
err = addFile(w, filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -43,8 +23,8 @@ func (z *Ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *Ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
f, err := z.openFile(filepath)
|
||||
func addFile(w *zip.Writer, filepath string) error {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,6 +45,6 @@ func (z *Ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = z.ioCopy(ioWriter, f)
|
||||
_, err = io.Copy(ioWriter, f)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
@@ -11,12 +12,14 @@ import (
|
||||
)
|
||||
|
||||
type Health struct {
|
||||
ServerAddress *string
|
||||
HealthchecksioUUID *string
|
||||
ServerAddress *string
|
||||
HealthchecksioBaseURL string
|
||||
HealthchecksioUUID *string
|
||||
}
|
||||
|
||||
func (h *Health) SetDefaults() {
|
||||
h.ServerAddress = gosettings.DefaultPointer(h.ServerAddress, "127.0.0.1:9999")
|
||||
h.HealthchecksioBaseURL = gosettings.DefaultComparable(h.HealthchecksioBaseURL, "https://hc-ping.com")
|
||||
h.HealthchecksioUUID = gosettings.DefaultPointer(h.HealthchecksioUUID, "")
|
||||
}
|
||||
|
||||
@@ -26,6 +29,11 @@ func (h Health) Validate() (err error) {
|
||||
return fmt.Errorf("server listening address: %w", err)
|
||||
}
|
||||
|
||||
_, err = url.Parse(h.HealthchecksioBaseURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("healthchecks.io base URL: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -37,6 +45,7 @@ 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 base URL: %s", h.HealthchecksioBaseURL)
|
||||
node.Appendf("Healthchecks.io UUID: %s", *h.HealthchecksioUUID)
|
||||
}
|
||||
return node
|
||||
@@ -44,5 +53,6 @@ func (h Health) toLinesNode() *gotree.Node {
|
||||
|
||||
func (h *Health) Read(reader *reader.Reader) {
|
||||
h.ServerAddress = reader.Get("HEALTH_SERVER_ADDRESS")
|
||||
h.HealthchecksioBaseURL = reader.String("HEALTH_HEALTHCHECKSIO_BASE_URL")
|
||||
h.HealthchecksioUUID = reader.Get("HEALTH_HEALTHCHECKSIO_UUID")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/qdm12/gosettings"
|
||||
"github.com/qdm12/gosettings/reader"
|
||||
"github.com/qdm12/gotree"
|
||||
@@ -8,10 +10,13 @@ import (
|
||||
|
||||
type Paths struct {
|
||||
DataDir *string
|
||||
Config *string
|
||||
}
|
||||
|
||||
func (p *Paths) setDefaults() {
|
||||
p.DataDir = gosettings.DefaultPointer(p.DataDir, "./data")
|
||||
defaultConfig := filepath.Join(*p.DataDir, "config.json")
|
||||
p.Config = gosettings.DefaultPointer(p.Config, defaultConfig)
|
||||
}
|
||||
|
||||
func (p Paths) Validate() (err error) {
|
||||
@@ -25,9 +30,11 @@ func (p Paths) String() string {
|
||||
func (p Paths) toLinesNode() *gotree.Node {
|
||||
node := gotree.New("Paths")
|
||||
node.Appendf("Data directory: %s", *p.DataDir)
|
||||
node.Appendf("Config file: %s", *p.Config)
|
||||
return node
|
||||
}
|
||||
|
||||
func (p *Paths) read(reader *reader.Reader) {
|
||||
p.DataDir = reader.Get("DATADIR")
|
||||
p.Config = reader.Get("CONFIG_FILEPATH")
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ func (p PubIP) validateHTTPIPv6Providers() (err error) {
|
||||
|
||||
var (
|
||||
ErrNoPublicIPHTTPProvider = errors.New("no public IP HTTP provider specified")
|
||||
ErrURLIsNotValidHTTPS = errors.New("URL is not valid or not HTTPS")
|
||||
)
|
||||
|
||||
func validateHTTPIPProviders(providerStrings []string,
|
||||
@@ -215,8 +216,11 @@ func validateHTTPIPProviders(providerStrings []string,
|
||||
}
|
||||
|
||||
// Custom URL check
|
||||
url, err := url.Parse(providerString)
|
||||
if err == nil && url != nil && url.Scheme == "https" {
|
||||
if strings.HasPrefix(providerString, "url:") {
|
||||
url, err := url.Parse(providerString[4:])
|
||||
if err != nil || url.Scheme != "https" {
|
||||
return fmt.Errorf("%w: %s", ErrURLIsNotValidHTTPS, providerString)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -259,7 +263,12 @@ func (p *PubIP) read(r *reader.Reader, warner Warner) (err error) {
|
||||
copy(httpIPProvidersTemp, p.HTTPIPProviders)
|
||||
p.HTTPIPProviders = make([]string, 0, len(p.HTTPIPProviders))
|
||||
for _, provider := range httpIPProvidersTemp {
|
||||
if provider != "opendns" {
|
||||
switch provider {
|
||||
case "opendns": // no longer available, for a long time
|
||||
case "google": // found no longer working on 2024.09.17
|
||||
warner.Warnf("http provider google will be ignored " +
|
||||
"since it is no longer supported by Google")
|
||||
default:
|
||||
p.HTTPIPProviders = append(p.HTTPIPProviders, provider)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ import (
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Enabled *bool
|
||||
ListeningAddress string
|
||||
RootURL string
|
||||
}
|
||||
|
||||
func (s *Server) setDefaults() {
|
||||
s.Enabled = gosettings.DefaultPointer(s.Enabled, true)
|
||||
s.ListeningAddress = gosettings.DefaultComparable(s.ListeningAddress, ":8000")
|
||||
s.RootURL = gosettings.DefaultComparable(s.RootURL, "/")
|
||||
}
|
||||
@@ -36,6 +38,9 @@ func (s Server) String() string {
|
||||
}
|
||||
|
||||
func (s Server) toLinesNode() *gotree.Node {
|
||||
if !*s.Enabled {
|
||||
return gotree.New("Server: disabled")
|
||||
}
|
||||
node := gotree.New("Server")
|
||||
node.Appendf("Listening address: %s", s.ListeningAddress)
|
||||
node.Appendf("Root URL: %s", s.RootURL)
|
||||
@@ -43,6 +48,11 @@ func (s Server) toLinesNode() *gotree.Node {
|
||||
}
|
||||
|
||||
func (s *Server) read(reader *reader.Reader, warner Warner) (err error) {
|
||||
s.Enabled, err = reader.BoolPtr("SERVER_ENABLED")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.RootURL = reader.String("ROOT_URL")
|
||||
|
||||
// Retro-compatibility
|
||||
|
||||
@@ -39,7 +39,8 @@ func Test_Settings_String(t *testing.T) {
|
||||
├── Health
|
||||
| └── Server listening address: 127.0.0.1:9999
|
||||
├── Paths
|
||||
| └── Data directory: ./data
|
||||
| ├── Data directory: ./data
|
||||
| └── Config file: data/config.json
|
||||
├── Backup: disabled
|
||||
└── Logger
|
||||
├── Level: INFO
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
@@ -19,3 +20,17 @@ func NewDatabase(data []records.Record, persistentDB PersistentDatabase) *Databa
|
||||
persistentDB: persistentDB,
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Database) String() string {
|
||||
return "database"
|
||||
}
|
||||
|
||||
func (db *Database) Start(_ context.Context) (_ <-chan error, err error) {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (db *Database) Stop() (err error) {
|
||||
db.Lock() // ensure write operation finishes
|
||||
defer db.Unlock()
|
||||
return db.persistentDB.Close()
|
||||
}
|
||||
|
||||
@@ -28,9 +28,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()
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
)
|
||||
|
||||
func MakeIsHealthy(db AllSelecter, resolver LookupIPer) func() error {
|
||||
return func() (err error) {
|
||||
return isHealthy(db, resolver)
|
||||
func MakeIsHealthy(db AllSelecter, resolver LookupIPer) func(ctx context.Context) error {
|
||||
return func(ctx context.Context) (err error) {
|
||||
return isHealthy(ctx, db, resolver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
)
|
||||
|
||||
// isHealthy checks all the records were updated successfully and returns an error if not.
|
||||
func isHealthy(db AllSelecter, resolver LookupIPer) (err error) {
|
||||
func isHealthy(ctx context.Context, db AllSelecter, resolver LookupIPer) (err error) {
|
||||
records := db.SelectAll()
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
@@ -39,7 +39,7 @@ func isHealthy(db AllSelecter, resolver LookupIPer) (err error) {
|
||||
return fmt.Errorf("%w: for hostname %s", ErrRecordIPNotSet, hostname)
|
||||
}
|
||||
|
||||
lookedUpNetIPs, err := resolver.LookupIP(context.Background(), "ip", hostname)
|
||||
lookedUpNetIPs, err := resolver.LookupIP(ctx, "ip", hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -10,10 +10,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func IsClientMode(args []string) bool {
|
||||
return len(args) > 1 && args[1] == "healthcheck"
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*http.Client
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func newHandler(healthcheck func() error) http.Handler {
|
||||
func newHandler(healthcheck func(context.Context) error) http.Handler {
|
||||
return &handler{
|
||||
healthcheck: healthcheck,
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
healthcheck func() error
|
||||
healthcheck func(context.Context) error
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -19,7 +20,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
err := h.healthcheck()
|
||||
err := h.healthcheck(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
8
internal/health/isdocker.go
Normal file
8
internal/health/isdocker.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package health
|
||||
|
||||
import "os"
|
||||
|
||||
func IsDocker() (ok bool) {
|
||||
_, err := os.Stat("isdocker")
|
||||
return err == nil
|
||||
}
|
||||
@@ -2,51 +2,17 @@ package health
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/goservices/httpserver"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
address string
|
||||
logger Logger
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewServer(address string, logger Logger, healthcheck func() error) *Server {
|
||||
handler := newHandler(healthcheck)
|
||||
return &Server{
|
||||
address: address,
|
||||
logger: logger,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Run(ctx context.Context, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
server := http.Server{
|
||||
Addr: s.address,
|
||||
Handler: s.handler,
|
||||
ReadHeaderTimeout: time.Second,
|
||||
ReadTimeout: time.Second,
|
||||
}
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
s.logger.Warn("shutting down (context canceled)")
|
||||
defer s.logger.Warn("shut down")
|
||||
const shutdownGraceDuration = 2 * time.Second
|
||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
|
||||
defer cancel()
|
||||
err := server.Shutdown(shutdownCtx)
|
||||
if err != nil {
|
||||
s.logger.Error("failed shutting down: " + err.Error())
|
||||
}
|
||||
}()
|
||||
for ctx.Err() == nil {
|
||||
s.logger.Info("listening on " + s.address)
|
||||
err := server.ListenAndServe()
|
||||
if err != nil && ctx.Err() == nil { // server crashed
|
||||
s.logger.Error(err.Error())
|
||||
s.logger.Info("restarting")
|
||||
}
|
||||
}
|
||||
func NewServer(address string, logger Logger, healthcheck func(context.Context) error) (
|
||||
server *httpserver.Server, err error) {
|
||||
name := "health"
|
||||
return httpserver.New(httpserver.Settings{
|
||||
Handler: newHandler(healthcheck),
|
||||
Name: &name,
|
||||
Address: &address,
|
||||
Logger: logger,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,15 +9,17 @@ import (
|
||||
|
||||
// New creates a new healthchecks.io client.
|
||||
// If passed an empty uuid string, it acts as no-op implementation.
|
||||
func New(httpClient *http.Client, uuid string) *Client {
|
||||
func New(httpClient *http.Client, baseURL, uuid string) *Client {
|
||||
return &Client{
|
||||
httpClient: httpClient,
|
||||
baseURL: baseURL,
|
||||
uuid: uuid,
|
||||
}
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
uuid string
|
||||
}
|
||||
|
||||
@@ -25,13 +27,27 @@ var (
|
||||
ErrStatusCode = errors.New("bad status code")
|
||||
)
|
||||
|
||||
func (c *Client) Ping(ctx context.Context) (err error) {
|
||||
type State string
|
||||
|
||||
const (
|
||||
Ok State = "ok"
|
||||
Start State = "start"
|
||||
Fail State = "fail"
|
||||
Exit0 State = "0"
|
||||
Exit1 State = "1"
|
||||
)
|
||||
|
||||
func (c *Client) Ping(ctx context.Context, state State) (err error) {
|
||||
if c.uuid == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||
"https://hc-ping.com/"+c.uuid, nil)
|
||||
url := c.baseURL + "/" + c.uuid
|
||||
if state != Ok {
|
||||
url += "/" + string(state)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
@@ -5,3 +5,14 @@ type BuildInformation struct {
|
||||
Commit string `json:"commit"`
|
||||
Date string `json:"buildDate"`
|
||||
}
|
||||
|
||||
func (b BuildInformation) VersionString() string {
|
||||
if b.Version != "latest" {
|
||||
return b.Version
|
||||
}
|
||||
const commitShortHashLength = 7
|
||||
if len(b.Commit) != commitShortHashLength {
|
||||
return "latest"
|
||||
}
|
||||
return b.Version + "-" + b.Commit[:7]
|
||||
}
|
||||
|
||||
@@ -18,16 +18,16 @@ type HistoryEvent struct { // current and previous ips
|
||||
|
||||
// GetPreviousIPs returns an antichronological list of previous
|
||||
// IP addresses if there is any.
|
||||
func (h History) GetPreviousIPs() []netip.Addr {
|
||||
func (h History) GetPreviousIPs() (previousIPs []netip.Addr) {
|
||||
if len(h) <= 1 {
|
||||
return nil
|
||||
}
|
||||
IPs := make([]netip.Addr, len(h)-1)
|
||||
const two = 2
|
||||
for i := len(h) - two; i >= 0; i-- {
|
||||
IPs[i] = h[i].IP
|
||||
previousIPs = make([]netip.Addr, len(h)-1)
|
||||
mostRecentPreviousIPIndex := len(h) - 2 //nolint:gomnd
|
||||
for i := range previousIPs {
|
||||
previousIPs[i] = h[mostRecentPreviousIPIndex-i].IP
|
||||
}
|
||||
return IPs
|
||||
return previousIPs
|
||||
}
|
||||
|
||||
// GetCurrentIP returns the current IP address (latest in history).
|
||||
|
||||
@@ -1,12 +1,58 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_GetPreviousIPs(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCases := map[string]struct {
|
||||
h History
|
||||
previousIPs []netip.Addr
|
||||
}{
|
||||
"empty_history": {
|
||||
h: History{},
|
||||
},
|
||||
"single_event": {
|
||||
h: History{
|
||||
{IP: netip.MustParseAddr("1.2.3.4")},
|
||||
},
|
||||
},
|
||||
"two_events": {
|
||||
h: History{
|
||||
{IP: netip.MustParseAddr("1.2.3.4")},
|
||||
{IP: netip.MustParseAddr("5.6.7.8")}, // last one
|
||||
},
|
||||
previousIPs: []netip.Addr{
|
||||
netip.MustParseAddr("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
"three_events": {
|
||||
h: History{
|
||||
{IP: netip.MustParseAddr("1.2.3.4")},
|
||||
{IP: netip.MustParseAddr("5.6.7.8")},
|
||||
{IP: netip.MustParseAddr("9.6.7.8")}, // last one
|
||||
},
|
||||
previousIPs: []netip.Addr{
|
||||
netip.MustParseAddr("5.6.7.8"),
|
||||
netip.MustParseAddr("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
previousIPs := testCase.h.GetPreviousIPs()
|
||||
assert.Equal(t, testCase.previousIPs, previousIPs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_GetDurationSinceSuccess(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
|
||||
25
internal/noop/service.go
Normal file
25
internal/noop/service.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package noop
|
||||
|
||||
import "context"
|
||||
|
||||
type Service struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func New(name string) *Service {
|
||||
return &Service{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) String() string {
|
||||
return s.name + " (no-op)"
|
||||
}
|
||||
|
||||
func (s *Service) Start(_ context.Context) (_ <-chan error, _ error) {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
func (s *Service) Stop() (stopErr error) {
|
||||
return nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/chmike/domain"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/provider"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
@@ -140,6 +141,7 @@ func extractAllSettings(jsonBytes []byte) (
|
||||
|
||||
var (
|
||||
ErrProviderNoLongerSupported = errors.New("provider no longer supported")
|
||||
ErrDomainBlank = errors.New("domain cannot be blank for provider")
|
||||
)
|
||||
|
||||
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage,
|
||||
@@ -149,6 +151,17 @@ func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage,
|
||||
return nil, nil, fmt.Errorf("%w: %s", ErrProviderNoLongerSupported, common.Provider)
|
||||
}
|
||||
|
||||
if common.Domain == "" && (common.Provider != "duckdns" && common.Provider != "goip") {
|
||||
return nil, nil, fmt.Errorf("%w: for provider %s", ErrDomainBlank, common.Provider)
|
||||
}
|
||||
|
||||
if common.Domain != "" {
|
||||
err = domain.Check(common.Domain)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("validating domain: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
providerName := models.Provider(common.Provider)
|
||||
if providerName == constants.DuckDNS { // only hosts, no domain
|
||||
if common.Domain != "" { // retro compatibility
|
||||
@@ -187,6 +200,7 @@ func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage,
|
||||
|
||||
providers = make([]provider.Provider, len(hosts))
|
||||
for i, host := range hosts {
|
||||
host = strings.TrimSpace(host)
|
||||
providers[i], err = provider.New(providerName, rawSettings, common.Domain,
|
||||
host, ipVersion, ipv6Suffix)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
@@ -19,12 +19,12 @@ func Test_makeIPv6Suffix(t *testing.T) {
|
||||
errMessage string
|
||||
}{
|
||||
"empty": {
|
||||
errWrapped: fmt.Errorf(`IPv6 prefix format is incorrect: ` +
|
||||
errWrapped: errors.New(`IPv6 prefix format is incorrect: ` +
|
||||
`cannot parse "" as uint8`),
|
||||
},
|
||||
"malformed": {
|
||||
prefixBitsString: "malformed",
|
||||
errWrapped: fmt.Errorf(`IPv6 prefix format is incorrect: ` +
|
||||
errWrapped: errors.New(`IPv6 prefix format is incorrect: ` +
|
||||
`cannot parse "malformed" as uint8`),
|
||||
},
|
||||
"with_leading_slash": {
|
||||
|
||||
@@ -6,6 +6,7 @@ import "github.com/qdm12/ddns-updater/internal/models"
|
||||
const (
|
||||
Aliyun models.Provider = "aliyun"
|
||||
AllInkl models.Provider = "allinkl"
|
||||
Changeip models.Provider = "changeip"
|
||||
Cloudflare models.Provider = "cloudflare"
|
||||
Custom models.Provider = "custom"
|
||||
Dd24 models.Provider = "dd24"
|
||||
@@ -21,6 +22,7 @@ const (
|
||||
Dynu models.Provider = "dynu"
|
||||
DynV6 models.Provider = "dynv6"
|
||||
EasyDNS models.Provider = "easydns"
|
||||
Example models.Provider = "example"
|
||||
FreeDNS models.Provider = "freedns"
|
||||
Gandi models.Provider = "gandi"
|
||||
GCP models.Provider = "gcp"
|
||||
@@ -42,6 +44,7 @@ const (
|
||||
OpenDNS models.Provider = "opendns"
|
||||
OVH models.Provider = "ovh"
|
||||
Porkbun models.Provider = "porkbun"
|
||||
Route53 models.Provider = "route53"
|
||||
SelfhostDe models.Provider = "selfhost.de"
|
||||
Servercow models.Provider = "servercow"
|
||||
Spdyn models.Provider = "spdyn"
|
||||
@@ -54,6 +57,7 @@ func ProviderChoices() []models.Provider {
|
||||
return []models.Provider{
|
||||
Aliyun,
|
||||
AllInkl,
|
||||
Changeip,
|
||||
Cloudflare,
|
||||
Dd24,
|
||||
DdnssDe,
|
||||
@@ -68,6 +72,7 @@ func ProviderChoices() []models.Provider {
|
||||
Dynu,
|
||||
DynV6,
|
||||
EasyDNS,
|
||||
Example,
|
||||
FreeDNS,
|
||||
Gandi,
|
||||
GCP,
|
||||
@@ -88,6 +93,7 @@ func ProviderChoices() []models.Provider {
|
||||
OpenDNS,
|
||||
OVH,
|
||||
Porkbun,
|
||||
Route53,
|
||||
SelfhostDe,
|
||||
Spdyn,
|
||||
Strato,
|
||||
|
||||
@@ -4,26 +4,30 @@ import "errors"
|
||||
|
||||
var (
|
||||
ErrAccessKeyIDNotSet = errors.New("access key id is not set")
|
||||
ErrAccessKeyNotSet = errors.New("access key is not set")
|
||||
ErrAccessKeySecretNotSet = errors.New("key secret is not set")
|
||||
ErrAPIKeyNotSet = errors.New("API key is not set")
|
||||
ErrAPISecretNotSet = errors.New("API secret is not set")
|
||||
ErrAppKeyNotSet = errors.New("app key is not set")
|
||||
ErrConsumerKeyNotSet = errors.New("consumer key is not set")
|
||||
ErrCredentialsNotSet = errors.New("credentials are not set")
|
||||
ErrCredentialsNotValid = errors.New("credentials are not valid")
|
||||
ErrCustomerNumberNotSet = errors.New("customer number is not set")
|
||||
ErrDomainNotSet = errors.New("domain is not set")
|
||||
ErrEmailNotSet = errors.New("email is not set")
|
||||
ErrEmailNotValid = errors.New("email address is not valid")
|
||||
ErrGCPProjectNotSet = errors.New("GCP project is not set")
|
||||
ErrDomainNotValid = errors.New("domain is not valid")
|
||||
ErrHostNotSet = errors.New("host is not set")
|
||||
ErrHostOnlySubdomain = errors.New("host can only be a subdomain")
|
||||
ErrHostWildcard = errors.New(`host cannot be a "*"`)
|
||||
ErrIPv4KeyNotSet = errors.New("IPv4 key is not set")
|
||||
ErrIPv6KeyNotSet = errors.New("IPv6 key is not set")
|
||||
ErrKeyNotSet = errors.New("key is not set")
|
||||
ErrKeyNotValid = errors.New("key is not valid")
|
||||
ErrNameNotSet = errors.New("name is not set")
|
||||
ErrPasswordNotSet = errors.New("password is not set")
|
||||
ErrPasswordNotValid = errors.New("password is not valid")
|
||||
ErrSecretKeyNotSet = errors.New("secret key is not set")
|
||||
ErrSecretNotSet = errors.New("secret is not set")
|
||||
ErrSuccessRegexNotSet = errors.New("success regex is not set")
|
||||
ErrTokenNotSet = errors.New("token is not set")
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/aliyun"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/allinkl"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/changeip"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/cloudflare"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/custom"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/dd24"
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/dynu"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/dynv6"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/easydns"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/example"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/freedns"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/gandi"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/gcp"
|
||||
@@ -48,6 +50,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/opendns"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/ovh"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/porkbun"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/route53"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/selfhostde"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/servercow"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/spdyn"
|
||||
@@ -79,6 +82,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
|
||||
return aliyun.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.AllInkl:
|
||||
return allinkl.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Changeip:
|
||||
return changeip.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Cloudflare:
|
||||
return cloudflare.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Custom:
|
||||
@@ -109,6 +114,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
|
||||
return dynv6.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.EasyDNS:
|
||||
return easydns.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Example:
|
||||
return example.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.FreeDNS:
|
||||
return freedns.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Gandi:
|
||||
@@ -151,6 +158,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
|
||||
return ovh.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Porkbun:
|
||||
return porkbun.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Route53:
|
||||
return route53.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.SelfhostDe:
|
||||
return selfhostde.New(data, domain, host, ipVersion, ipv6Suffix)
|
||||
case constants.Servercow:
|
||||
|
||||
@@ -3,9 +3,9 @@ package aliyun
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
@@ -23,7 +23,7 @@ func newURLValues(accessKeyID string) (values url.Values) {
|
||||
values.Set("SignatureMethod", "HMAC-SHA1")
|
||||
values.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05Z"))
|
||||
values.Set("SignatureVersion", "1.0")
|
||||
values.Set("SignatureNonce", fmt.Sprint(randInt64))
|
||||
values.Set("SignatureNonce", strconv.FormatInt(randInt64, 10))
|
||||
return values
|
||||
}
|
||||
|
||||
|
||||
147
internal/provider/providers/changeip/provider.go
Normal file
147
internal/provider/providers/changeip/provider.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package changeip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
ipv6Suffix netip.Prefix
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
type settings struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
|
||||
p *Provider, err error) {
|
||||
var providerSpecificSettings settings
|
||||
err = json.Unmarshal(data, &providerSpecificSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
|
||||
}
|
||||
err = validateSettings(domain, host, providerSpecificSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating settings: %w", err)
|
||||
}
|
||||
return &Provider{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
ipv6Suffix: ipv6Suffix,
|
||||
username: providerSpecificSettings.Username,
|
||||
password: providerSpecificSettings.Password,
|
||||
useProviderIP: providerSpecificSettings.UseProviderIP,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateSettings(domain, host string, settings settings) error {
|
||||
switch {
|
||||
case domain == "":
|
||||
return fmt.Errorf("%w", errors.ErrDomainNotSet)
|
||||
case host == "":
|
||||
return fmt.Errorf("%w", errors.ErrHostNotSet)
|
||||
case settings.Username == "":
|
||||
return fmt.Errorf("%w", errors.ErrUsernameNotSet)
|
||||
case settings.Password == "":
|
||||
return fmt.Errorf("%w", errors.ErrPasswordNotSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return utils.ToString(p.domain, p.host, constants.Changeip, p.ipVersion)
|
||||
}
|
||||
|
||||
func (p *Provider) Domain() string {
|
||||
return p.domain
|
||||
}
|
||||
|
||||
func (p *Provider) Host() string {
|
||||
return p.host
|
||||
}
|
||||
|
||||
func (p *Provider) IPVersion() ipversion.IPVersion {
|
||||
return p.ipVersion
|
||||
}
|
||||
|
||||
func (p *Provider) IPv6Suffix() netip.Prefix {
|
||||
return p.ipv6Suffix
|
||||
}
|
||||
|
||||
func (p *Provider) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Provider) BuildDomainName() string {
|
||||
return utils.BuildDomainName(p.host, p.domain)
|
||||
}
|
||||
|
||||
func (p *Provider) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
|
||||
Host: p.Host(),
|
||||
Provider: "<a href=\"https://www.changeip.com\">changeip.com</a>",
|
||||
IPVersion: p.ipVersion.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "nic.ChangeIP.com",
|
||||
Path: "/nic/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
|
||||
useProviderIP := p.useProviderIP && (ip.Is4() || !p.ipv6Suffix.IsValid())
|
||||
if !useProviderIP {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
request.SetBasicAuth(p.username, p.password)
|
||||
headers.SetUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("doing http request: %w", err)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
defer response.Body.Close()
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
|
||||
response.StatusCode, utils.BodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
@@ -31,7 +31,7 @@ type Provider struct {
|
||||
userServiceKey string
|
||||
zoneIdentifier string
|
||||
proxied bool
|
||||
ttl uint
|
||||
ttl uint32
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
@@ -44,7 +44,7 @@ func New(data json.RawMessage, domain, host string,
|
||||
UserServiceKey string `json:"user_service_key"`
|
||||
ZoneIdentifier string `json:"zone_identifier"`
|
||||
Proxied bool `json:"proxied"`
|
||||
TTL uint `json:"ttl"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{}
|
||||
err = json.Unmarshal(data, &extraSettings)
|
||||
if err != nil {
|
||||
@@ -78,7 +78,7 @@ var (
|
||||
|
||||
func (p *Provider) isValid() error {
|
||||
switch {
|
||||
case p.key != "": // email and key must be provided
|
||||
case p.email != "", p.key != "": // email and key must be provided
|
||||
switch {
|
||||
case !keyRegex.MatchString(p.key):
|
||||
return fmt.Errorf("%w: key %q does not match regex %q",
|
||||
@@ -225,7 +225,7 @@ func (p *Provider) getRecordID(ctx context.Context, client *http.Client, newIP n
|
||||
return listRecordsResponse.Result[0].ID, false, nil
|
||||
}
|
||||
|
||||
func (p *Provider) CreateRecord(ctx context.Context, client *http.Client, ip netip.Addr) (recordID string, err error) {
|
||||
func (p *Provider) createRecord(ctx context.Context, client *http.Client, ip netip.Addr) (recordID string, err error) {
|
||||
recordType := constants.A
|
||||
|
||||
if ip.Is6() {
|
||||
@@ -243,7 +243,7 @@ func (p *Provider) CreateRecord(ctx context.Context, client *http.Client, ip net
|
||||
Name string `json:"name"` // DNS record name i.e. example.com
|
||||
Content string `json:"content"` // ip address
|
||||
Proxied bool `json:"proxied"` // whether the record is receiving the performance and security benefits of Cloudflare
|
||||
TTL uint `json:"ttl"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Type: recordType,
|
||||
Name: utils.BuildURLQueryHostname(p.host, p.domain),
|
||||
@@ -314,7 +314,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
|
||||
switch {
|
||||
case stderrors.Is(err, errors.ErrReceivedNoResult):
|
||||
identifier, err = p.CreateRecord(ctx, client, ip)
|
||||
identifier, err = p.createRecord(ctx, client, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating record: %w", err)
|
||||
}
|
||||
@@ -335,7 +335,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
Name string `json:"name"` // DNS record name i.e. example.com
|
||||
Content string `json:"content"` // ip address
|
||||
Proxied bool `json:"proxied"` // whether the record is receiving the performance and security benefits of Cloudflare
|
||||
TTL uint `json:"ttl"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Type: recordType,
|
||||
Name: utils.BuildURLQueryHostname(p.host, p.domain),
|
||||
|
||||
@@ -122,8 +122,10 @@ func (p *Provider) HTML() models.HTMLRow {
|
||||
}
|
||||
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
values := url.Values{}
|
||||
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
|
||||
values, err := url.ParseQuery(p.url.RawQuery)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("parsing URL query: %w", err)
|
||||
}
|
||||
ipKey := p.ipv4Key
|
||||
if ip.Is6() {
|
||||
ipKey = p.ipv6Key
|
||||
|
||||
@@ -24,7 +24,6 @@ type Provider struct {
|
||||
ipv6Suffix netip.Prefix
|
||||
username string
|
||||
key string
|
||||
name string
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
@@ -34,7 +33,6 @@ func New(data json.RawMessage, domain, host string,
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"` // retro-compatibility
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
err = json.Unmarshal(data, &extraSettings)
|
||||
if err != nil {
|
||||
@@ -54,7 +52,6 @@ func New(data json.RawMessage, domain, host string,
|
||||
ipv6Suffix: ipv6Suffix,
|
||||
username: extraSettings.Username,
|
||||
key: extraSettings.Key,
|
||||
name: extraSettings.Name,
|
||||
}
|
||||
err = p.isValid()
|
||||
if err != nil {
|
||||
@@ -69,8 +66,6 @@ func (p *Provider) isValid() error {
|
||||
return fmt.Errorf("%w", errors.ErrUsernameNotSet)
|
||||
case p.key == "":
|
||||
return fmt.Errorf("%w", errors.ErrKeyNotSet)
|
||||
case p.name == "":
|
||||
return fmt.Errorf("%w", errors.ErrNameNotSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
182
internal/provider/providers/example/provider.go
Normal file
182
internal/provider/providers/example/provider.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package example
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
domain string
|
||||
host string
|
||||
// TODO: remove ipVersion and ipv6Suffix if the provider does not support IPv6.
|
||||
// Usually they do support IPv6 though.
|
||||
ipVersion ipversion.IPVersion
|
||||
ipv6Suffix netip.Prefix
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
|
||||
provider *Provider, err error) {
|
||||
var providerSpecificSettings settings
|
||||
err = json.Unmarshal(data, &providerSpecificSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
err = validateSettings(providerSpecificSettings, domain, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
ipv6Suffix: ipv6Suffix,
|
||||
username: providerSpecificSettings.Username,
|
||||
password: providerSpecificSettings.Password,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO adapt to the provider specific settings.
|
||||
type settings struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func validateSettings(providerSpecificSettings settings, domain, host string) error {
|
||||
// TODO: update this switch to be as restrictive as possible
|
||||
// to fail early for the user. Use errors already defined
|
||||
// in the internal/provider/errors package, or add your own
|
||||
// if really necessary. When returning an error, always use
|
||||
// fmt.Errorf (to enforce the caller to use errors.Is()).
|
||||
switch {
|
||||
case domain == "":
|
||||
return fmt.Errorf("%w", errors.ErrDomainNotSet)
|
||||
case host == "":
|
||||
return fmt.Errorf("%w", errors.ErrHostNotSet)
|
||||
// TODO: does the provider support wildcard hosts? If not, disallow * hosts
|
||||
// case host == "*":
|
||||
// return fmt.Errorf("%w", errors.ErrHostWildcard)
|
||||
case providerSpecificSettings.Username == "":
|
||||
return fmt.Errorf("%w", errors.ErrUsernameNotSet)
|
||||
case providerSpecificSettings.Password == "":
|
||||
return fmt.Errorf("%w", errors.ErrPasswordNotSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
// TODO update the name of the provider and add it to the
|
||||
// internal/provider/constants package.
|
||||
return utils.ToString(p.domain, p.host, constants.Dyn, p.ipVersion)
|
||||
}
|
||||
|
||||
func (p *Provider) Domain() string {
|
||||
return p.domain
|
||||
}
|
||||
|
||||
func (p *Provider) Host() string {
|
||||
return p.host
|
||||
}
|
||||
|
||||
func (p *Provider) IPVersion() ipversion.IPVersion {
|
||||
return p.ipVersion
|
||||
}
|
||||
|
||||
func (p *Provider) IPv6Suffix() netip.Prefix {
|
||||
return p.ipv6Suffix
|
||||
}
|
||||
|
||||
func (p *Provider) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Provider) BuildDomainName() string {
|
||||
return utils.BuildDomainName(p.host, p.domain)
|
||||
}
|
||||
|
||||
func (p *Provider) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
|
||||
Host: p.Host(),
|
||||
// TODO: update the provider name and link below
|
||||
Provider: "<a href=\"https://dyn.com/\">Dyn DNS</a>",
|
||||
IPVersion: p.ipVersion.String(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: update this function to match the provider's API
|
||||
// Ideally add a comment with a link to their API documentation.
|
||||
// If the provider API allows it, create the record if it does not exist.
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
User: url.UserPassword(p.username, p.password),
|
||||
Host: "example.com",
|
||||
Path: "/nic/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
|
||||
values.Set("myip", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
// TODO: there are other helping functions in the headers package to set request headers
|
||||
// if you need them.
|
||||
headers.SetUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// TODO handle the encoding of the response body properly. Often it can be JSON,
|
||||
// see other provider code for examples on how to decode JSON.
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
// TODO handle every possible status codes from the provider API.
|
||||
// If undocumented, try them out by sending bogus HTTP requests to see
|
||||
// what status codes they return, for example with `curl`.
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
|
||||
}
|
||||
|
||||
// TODO handle every possible response bodies from the provider API.
|
||||
// If undocumented, try them out by sending bogus HTTP requests to see
|
||||
// what response bodies they return, for example with `curl`.
|
||||
switch {
|
||||
case strings.HasPrefix(s, constants.Notfqdn):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
|
||||
case strings.HasPrefix(s, "badrequest"):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
|
||||
case strings.HasPrefix(s, "good"):
|
||||
return ip, nil
|
||||
default:
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ type Provider struct {
|
||||
host string
|
||||
ipVersion ipversion.IPVersion
|
||||
ipv6Suffix netip.Prefix
|
||||
ttl int
|
||||
ttl uint32
|
||||
// Authentication, either use the personal access token
|
||||
// or the deprecated API key.
|
||||
// See https://api.gandi.net/docs/authentication/
|
||||
@@ -38,7 +38,7 @@ func New(data json.RawMessage, domain, host string,
|
||||
extraSettings := struct {
|
||||
PersonalAccessToken string `json:"personal_access_token"`
|
||||
APIKey string `json:"key"`
|
||||
TTL int `json:"ttl"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{}
|
||||
err = json.Unmarshal(data, &extraSettings)
|
||||
if err != nil {
|
||||
@@ -118,14 +118,14 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
const defaultTTL = 3600
|
||||
const defaultTTL uint32 = 3600
|
||||
ttl := defaultTTL
|
||||
if p.ttl != 0 {
|
||||
ttl = p.ttl
|
||||
}
|
||||
requestData := struct {
|
||||
Values [1]string `json:"rrset_values"`
|
||||
TTL int `json:"rrset_ttl"`
|
||||
TTL uint32 `json:"rrset_ttl"`
|
||||
}{
|
||||
Values: [1]string{ip.Unmap().String()},
|
||||
TTL: ttl,
|
||||
|
||||
147
internal/provider/providers/gcp/api.go
Normal file
147
internal/provider/providers/gcp/api.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
)
|
||||
|
||||
type recordResourceSet struct {
|
||||
// Name is the fqdn.
|
||||
Name string `json:"name"`
|
||||
// Rrdatas, as defined in RFC 1035 (section 5) and RFC 1034 (section 3.6.1)
|
||||
Rrdatas []string `json:"rrdatas,omitempty"`
|
||||
// TTL is the number of seconds that this RRSet can be cached by resolvers.
|
||||
TTL uint32 `json:"ttl"`
|
||||
// Type is the identifier of a record type. For example A or AAAA.
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func (p *Provider) getRRSet(ctx context.Context, client *http.Client, fqdn, recordType string) (
|
||||
rrSet *recordResourceSet, err error) {
|
||||
urlPath := fmt.Sprintf("/dns/v1/projects/%s/managedZones/%s/rrsets/%s/%s",
|
||||
p.project, p.zone, fqdn, recordType)
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, makeAPIURL(urlPath), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers.SetUserAgent(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case response.StatusCode == http.StatusNoContent:
|
||||
err = response.Body.Close()
|
||||
return nil, err
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
errMessage := decodeError(response.Body)
|
||||
return nil, fmt.Errorf("%w: %s", errors.ErrRecordResourceSetNotFound, errMessage)
|
||||
case response.StatusCode >= http.StatusOK &&
|
||||
response.StatusCode < http.StatusMultipleChoices:
|
||||
rrSet = new(recordResourceSet)
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
err = decoder.Decode(&rrSet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json decoding rrset: %w", err)
|
||||
}
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
return rrSet, nil
|
||||
default:
|
||||
errMessage := decodeError(response.Body)
|
||||
return nil, fmt.Errorf("%w: %s", errors.ErrHTTPStatusNotValid, errMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) createRRSet(ctx context.Context, client *http.Client, fqdn, recordType string,
|
||||
ip netip.Addr) (err error) {
|
||||
urlPath := fmt.Sprintf("/dns/v1/projects/%s/managedZones/%s/rrsets", p.project, p.zone)
|
||||
body := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(body)
|
||||
rrSet := &recordResourceSet{
|
||||
Name: fqdn,
|
||||
Rrdatas: []string{ip.String()},
|
||||
Type: recordType,
|
||||
}
|
||||
err = encoder.Encode(rrSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, makeAPIURL(urlPath), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers.SetUserAgent(request)
|
||||
headers.SetContentType(request, "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.StatusCode >= http.StatusOK &&
|
||||
response.StatusCode < http.StatusMultipleChoices {
|
||||
return response.Body.Close()
|
||||
}
|
||||
errMessage := decodeError(response.Body)
|
||||
return fmt.Errorf("%w: %s", errors.ErrHTTPStatusNotValid, errMessage)
|
||||
}
|
||||
|
||||
func (p *Provider) patchRRSet(ctx context.Context, client *http.Client, fqdn, recordType string,
|
||||
ip netip.Addr) (err error) {
|
||||
urlPath := fmt.Sprintf("/dns/v1/projects/%s/managedZones/%s/rrsets/%s/%s",
|
||||
p.project, p.zone, fqdn, recordType)
|
||||
body := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(body)
|
||||
rrSet := &recordResourceSet{
|
||||
Name: fqdn,
|
||||
Rrdatas: []string{ip.String()},
|
||||
Type: recordType,
|
||||
}
|
||||
err = encoder.Encode(rrSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPatch, makeAPIURL(urlPath), body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headers.SetUserAgent(request)
|
||||
headers.SetContentType(request, "application/json")
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.StatusCode >= http.StatusOK &&
|
||||
response.StatusCode < http.StatusMultipleChoices {
|
||||
return response.Body.Close()
|
||||
}
|
||||
errMessage := decodeError(response.Body)
|
||||
return fmt.Errorf("%w: %s", errors.ErrHTTPStatusNotValid, errMessage)
|
||||
}
|
||||
|
||||
func makeAPIURL(path string) string {
|
||||
urlValues := make(url.Values)
|
||||
urlValues.Set("alt", "json")
|
||||
urlValues.Set("prettyPrint", "false")
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dns.googleapis.com",
|
||||
Path: path,
|
||||
RawQuery: urlValues.Encode(),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
82
internal/provider/providers/gcp/error.go
Normal file
82
internal/provider/providers/gcp/error.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
)
|
||||
|
||||
type gcpErrorData struct {
|
||||
gcpError `json:"error"`
|
||||
}
|
||||
|
||||
type gcpError struct {
|
||||
// Code is the HTTP response status code and will always be populated.
|
||||
Code int `json:"code"`
|
||||
// Message is the server response message and is only populated when
|
||||
// explicitly referenced by the JSON server response.
|
||||
Message string `json:"message"`
|
||||
// Details can be JSON encoded for readable details.
|
||||
Details []any `json:"details"`
|
||||
Errors []errorItem `json:"errors"`
|
||||
}
|
||||
|
||||
// errorItem is a detailed error code & message from the Google API frontend.
|
||||
type errorItem struct {
|
||||
// Reason is the typed error code. For example: "some_example".
|
||||
Reason string `json:"reason"`
|
||||
// Message is the human-readable description of the error.
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e gcpError) String() string {
|
||||
elements := make([]string, 0, 3+len(e.Errors)) //nolint:gomnd
|
||||
if e.Code != 0 {
|
||||
element := fmt.Sprintf("status %d", e.Code)
|
||||
elements = append(elements, element)
|
||||
}
|
||||
if e.Message != "" {
|
||||
elements = append(elements, e.Message)
|
||||
}
|
||||
|
||||
if len(e.Details) > 0 {
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
err := encoder.Encode(e.Details)
|
||||
if err == nil {
|
||||
elements = append(elements, "details: "+buffer.String())
|
||||
}
|
||||
}
|
||||
|
||||
for _, errorItem := range e.Errors {
|
||||
element := "reason: " + errorItem.Reason
|
||||
if errorItem.Message != "" && errorItem.Message != e.Message {
|
||||
element += ", message: " + errorItem.Message
|
||||
}
|
||||
elements = append(elements, element)
|
||||
}
|
||||
|
||||
if len(elements) == 0 {
|
||||
return "[no error details]"
|
||||
}
|
||||
|
||||
return strings.Join(elements, "; ")
|
||||
}
|
||||
|
||||
func decodeError(body io.ReadCloser) (message string) {
|
||||
b, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return "reading body: " + err.Error()
|
||||
}
|
||||
var jsonErrReply gcpErrorData
|
||||
err = json.Unmarshal(b, &jsonErrReply)
|
||||
_ = body.Close()
|
||||
if err != nil {
|
||||
return "failed JSON decoding body: " + err.Error() + ": " + utils.ToSingleLine(string(b))
|
||||
}
|
||||
return jsonErrReply.String()
|
||||
}
|
||||
33
internal/provider/providers/gcp/oauth2.go
Normal file
33
internal/provider/providers/gcp/oauth2.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package gcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
func createOauth2Client(ctx context.Context, client *http.Client, credentialsJSON []byte) (
|
||||
oauth2Client *http.Client, err error) {
|
||||
scopes := []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/cloud-platform.read-only",
|
||||
"https://www.googleapis.com/auth/ndev.clouddns.readonly",
|
||||
"https://www.googleapis.com/auth/ndev.clouddns.readwrite",
|
||||
}
|
||||
credentials, err := google.CredentialsFromJSON(ctx, credentialsJSON, scopes...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating Google credentials: %w", err)
|
||||
}
|
||||
oauth2Client = &http.Client{
|
||||
Timeout: client.Timeout,
|
||||
Transport: &oauth2.Transport{
|
||||
Base: client.Transport,
|
||||
Source: oauth2.ReuseTokenSource(nil, credentials.TokenSource),
|
||||
},
|
||||
}
|
||||
|
||||
return oauth2Client, nil
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func New(data json.RawMessage, domain, host string,
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Provider) isValid() error {
|
||||
func (p *Provider) isValid() (err error) {
|
||||
if p.project == "" {
|
||||
return fmt.Errorf("%w", ddnserrors.ErrGCPProjectNotSet)
|
||||
}
|
||||
@@ -66,6 +66,14 @@ func (p *Provider) isValid() error {
|
||||
if len(p.credentials) == 0 {
|
||||
return fmt.Errorf("%w", ddnserrors.ErrCredentialsNotSet)
|
||||
}
|
||||
var creds struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
err = json.Unmarshal(p.credentials, &creds)
|
||||
if err != nil || creds.Type == "" {
|
||||
return fmt.Errorf("%w: 'type' JSON field value missing",
|
||||
ddnserrors.ErrCredentialsNotValid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -9,9 +9,6 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
ddnserrors "github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
clouddns "google.golang.org/api/dns/v1"
|
||||
"google.golang.org/api/googleapi"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
@@ -20,17 +17,14 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
|
||||
ddnsService, err := clouddns.NewService(ctx,
|
||||
option.WithCredentialsJSON(p.credentials),
|
||||
option.WithHTTPClient(client))
|
||||
client, err = createOauth2Client(ctx, client, p.credentials)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating GCP DDNS service: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("creating OAuth2 client: %w", err)
|
||||
}
|
||||
rrSetsService := clouddns.NewResourceRecordSetsService(ddnsService)
|
||||
|
||||
fqdn := fmt.Sprintf("%s.%s.", p.host, p.domain)
|
||||
|
||||
recordResourceSet, err := p.getResourceRecordSet(rrSetsService, fqdn, recordType)
|
||||
recordResourceSet, err := p.getRRSet(ctx, client, fqdn, recordType)
|
||||
rrSetFound := true
|
||||
if err != nil {
|
||||
if errors.Is(err, ddnserrors.ErrRecordResourceSetNotFound) {
|
||||
@@ -48,56 +42,17 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
|
||||
if !rrSetFound {
|
||||
err = p.createRecord(rrSetsService, fqdn, recordType, ip)
|
||||
err = p.createRRSet(ctx, client, fqdn, recordType, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating record: %w", err)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
err = p.updateRecord(rrSetsService, fqdn, recordType, ip)
|
||||
err = p.patchRRSet(ctx, client, fqdn, recordType, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("updating record: %w", err)
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (p *Provider) getResourceRecordSet(rrSetsService *clouddns.ResourceRecordSetsService,
|
||||
fqdn, recordType string) (resourceRecordSet *clouddns.ResourceRecordSet, err error) {
|
||||
call := rrSetsService.Get(p.project, p.zone, fqdn, recordType)
|
||||
resourceRecordSet, err = call.Do()
|
||||
if err != nil {
|
||||
googleAPIError := new(googleapi.Error)
|
||||
if errors.As(err, &googleAPIError) && googleAPIError.Code == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("%w: %w", ddnserrors.ErrRecordResourceSetNotFound, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resourceRecordSet, nil
|
||||
}
|
||||
|
||||
func (p *Provider) createRecord(rrSetsService *clouddns.ResourceRecordSetsService,
|
||||
fqdn, recordType string, ip netip.Addr) (err error) {
|
||||
rrSet := &clouddns.ResourceRecordSet{
|
||||
Name: fqdn,
|
||||
Rrdatas: []string{ip.String()},
|
||||
Type: recordType,
|
||||
}
|
||||
rrSetCall := rrSetsService.Create(p.project, p.zone, rrSet)
|
||||
_, err = rrSetCall.Do()
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Provider) updateRecord(rrSetsService *clouddns.ResourceRecordSetsService,
|
||||
fqdn, recordType string, ip netip.Addr) (err error) {
|
||||
rrSet := &clouddns.ResourceRecordSet{
|
||||
Name: fqdn,
|
||||
Rrdatas: []string{ip.String()},
|
||||
Type: recordType,
|
||||
}
|
||||
rrSetCall := rrSetsService.Patch(p.project, p.zone, fqdn, recordType, rrSet)
|
||||
_, err = rrSetCall.Do()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -160,5 +160,15 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
if jsonErr != nil || parsedJSON.Message == "" {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", err, utils.ToSingleLine(string(b)))
|
||||
}
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", err, parsedJSON.Message)
|
||||
|
||||
err = fmt.Errorf("%w: %s", err, parsedJSON.Message)
|
||||
|
||||
if response.StatusCode == http.StatusForbidden &&
|
||||
parsedJSON.Message == "Authenticated user is not allowed access" {
|
||||
err = fmt.Errorf("%w - "+
|
||||
"See https://github.com/qdm12/ddns-updater/issues/707#issuecomment-2089632215",
|
||||
err)
|
||||
}
|
||||
|
||||
return netip.Addr{}, err
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (p *Provider) createRecord(ctx context.Context, client *http.Client, ip net
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
ZoneIdentifier string `json:"zone_id"`
|
||||
TTL uint `json:"ttl"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Type: recordType,
|
||||
Name: p.host,
|
||||
|
||||
@@ -22,7 +22,7 @@ type Provider struct {
|
||||
ipv6Suffix netip.Prefix
|
||||
token string
|
||||
zoneIdentifier string
|
||||
ttl uint
|
||||
ttl uint32
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, host string,
|
||||
@@ -31,7 +31,7 @@ func New(data json.RawMessage, domain, host string,
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
ZoneIdentifier string `json:"zone_identifier"`
|
||||
TTL uint `json:"ttl"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{}
|
||||
err = json.Unmarshal(data, &extraSettings)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,7 +24,7 @@ func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dns.hetzner.com",
|
||||
Path: fmt.Sprintf("/api/v1/records/%s", recordID),
|
||||
Path: "/api/v1/records/" + recordID,
|
||||
}
|
||||
|
||||
requestData := struct {
|
||||
@@ -32,7 +32,7 @@ func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
ZoneIdentifier string `json:"zone_id"`
|
||||
TTL uint `json:"ttl"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}{
|
||||
Type: recordType,
|
||||
Name: p.host,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user