90 Commits
v2.6 ... v2.7

Author SHA1 Message Date
Quentin McGaw
f4e1cca222 docs(readme): update v2.7 links to point to v2.7.1 2024-09-17 07:58:22 +00:00
Quentin McGaw
79efca2bf4 fix(publicip/http): remove google provider which no longer works 2024-09-17 07:52:19 +00:00
dependabot[bot]
f17be83112 chore(deps): bump github.com/breml/rootcerts from 0.2.17 to 0.2.18 (#814) 2024-09-17 07:51:39 +00:00
Quentin McGaw
2a9e41c646 fix(ci): ignore duckdns.org for links check 2024-09-17 07:51:06 +00:00
Quentin McGaw
8664aa0e5c fix(config): upgrade qdm12/gosettings from v0.4.1 to v0.4.4-rc1 2024-09-17 07:50:34 +00:00
Quentin McGaw
4c840f4bb6 fix(dondominio): remove unneeded name field 2024-09-17 07:50:17 +00:00
Benjamin Temple
7230fea962 fix(porkbun): fix wildcard behavior (#773) 2024-09-17 07:49:01 +00:00
Benjamin Temple
8aa6aba8b7 fix(porkbun): remove trailing '.' from alias delete request (#775) 2024-09-17 07:48:06 +00:00
Quentin McGaw
1d8690fb14 fix(noip): force useProviderIP to false for IPv6 2024-09-17 07:47:02 +00:00
Quentin McGaw
2922d441d2 docs(readme): add readme and docs/ versioned links 2024-09-17 07:45:46 +00:00
Quentin McGaw
607ca8f0b8 docs(goip): fix documentation for the host field 2024-09-17 07:42:44 +00:00
Quentin McGaw
d37f05766b chore(providers): change ttl type to uint32 2024-06-17 19:01:08 +00:00
Quentin McGaw
ca85596e19 feat(config): CONFIG_FILEPATH option 2024-06-17 19:01:06 +00:00
Quentin McGaw
cf184070b8 docs(readme): better explain container directory and file creation 2024-06-16 15:39:34 +00:00
Quentin McGaw
012a6dddcd fix(namecom): update record using "" when the host is @ 2024-06-16 08:35:04 +00:00
Marcelo HP Ferreira
d3b689d0ef feat(provider): route53 simple routing (#715) 2024-06-15 17:02:22 +02:00
Quentin McGaw
a1a9fc0b62 docs(typo): fix identifiersuffix -> identifier suffix 2024-06-15 10:57:53 +00:00
Quentin McGaw
2f88d44af7 feat(provider): add changeip.com 2024-06-15 10:57:23 +00:00
Quentin McGaw
5d74d11835 docs(readme): fix missing allinkl document link 2024-06-15 10:27:27 +00:00
Quentin McGaw
2f2bef3d6c feat(publicip): add changeip ipv4v6 echo service 2024-06-15 10:24:17 +00:00
Quentin McGaw
3494cfbcbb docs(devcontainer): change 'remote containers extension' to new 'dev containers extension' 2024-06-15 09:55:29 +00:00
Quentin McGaw
b7986dd6b9 chore(ci): pin docker/build-push-action to v5 2024-06-15 09:54:41 +00:00
dependabot[bot]
3644e5cc21 chore(deps): bump goreleaser/goreleaser-action from 5 to 6 (#737) 2024-06-15 11:53:49 +02:00
Ryan Kupka
2f252a3ec1 docs(contributing): change 'remote containers extension' to new 'dev containers extension' (#696) 2024-06-15 11:52:08 +02:00
dependabot[bot]
270014ccda chore(deps): bump github.com/go-chi/chi/v5 from 5.0.11 to 5.0.12 (#654) 2024-06-15 11:49:00 +02:00
Gottfried Mayer
b70a91582c feat(ui): ui improvements and fixes (#687)
- UI theme improved
- Dark/Light themes depending on the user's browser settings
- Mobile UI (vertical table)
- Fix serving static files (favicon did not work before)
- Add readme UI gif and mobile screenshot
2024-06-15 11:48:16 +02:00
Quentin McGaw
5b384cbf18 hotfix(ui): redirect /rooturl to /rooturl/ 2024-06-15 09:38:21 +00:00
dependabot[bot]
ff3b661d66 chore(deps): bump golang.org/x/mod from 0.15.0 to 0.18.0 (#736) 2024-06-15 10:44:25 +02:00
dependabot[bot]
b3134c7770 chore(deps): bump github.com/miekg/dns from 1.1.58 to 1.1.61 (#745) 2024-06-15 10:44:10 +02:00
dependabot[bot]
b9bbbf26d2 chore(deps): bump github.com/breml/rootcerts from 0.2.16 to 0.2.17 (#741) 2024-06-15 10:44:01 +02:00
Quentin McGaw
dfd352b817 docs(readme): reference Qnap setup guide, fix #708 2024-06-15 08:42:15 +00:00
Quentin McGaw
a84a1d664a chore(gcp): drop google sdk dependency
- depend on golang.org/x/oauth2 only
- drop image size from 17MB to 11.5MB
2024-06-15 08:36:10 +00:00
Quentin McGaw
25472f43c3 fix(ui): list IPs in reverse chronological order
- Fix #730
2024-06-14 08:49:37 +00:00
Quentin McGaw
aa4a1f3813 fix(ionos): wildcard handling 2024-06-14 08:25:51 +00:00
Quentin McGaw
76afd8361e feat(server): serve root at /rooturl on top of /rooturl/ 2024-06-14 08:13:51 +00:00
Quentin McGaw
cc995a79c8 feat(config): add SERVER_ENABLED defaulting to yes 2024-06-13 19:51:52 +00:00
Quentin McGaw
776206eec8 feat(health): only run health server when running in Docker 2024-06-13 19:35:11 +00:00
Quentin McGaw
c1bf7a49c1 fix(namecom): detect existing root host domains 2024-06-13 19:15:59 +00:00
Quentin McGaw
987138dfc1 fix(porkbun): do not add * to URL path 2024-06-13 19:01:21 +00:00
Quentin McGaw
85780dc6e1 chore(lint): add multiple linters 2024-06-13 09:23:19 +00:00
Quentin McGaw
130ab008b5 chore(all): migrate to service architecture with github.com/qdm12/goservices (#743) 2024-06-13 11:16:32 +02:00
Quentin McGaw
20792e9460 chore(lint): remove invalid config fields 2024-06-13 09:11:55 +00:00
Quentin McGaw
8e802d45ae chore(lint): upgrade to v1.56.2 2024-06-13 09:10:33 +00:00
Quentin McGaw
2b02ac154e chore(main): split main function into smaller functions 2024-06-13 09:01:38 +00:00
Quentin McGaw
8e09cd6342 change(docker): rename /updater/app -> /updater/ddns-updater
- Clearer `ddns-updater` process name than `app`
- Refers to issue #729
2024-05-23 13:30:03 +00:00
Quentin McGaw
542e89536c fix(noip): useproviderip and no ip returned case 2024-05-06 11:13:07 +00:00
Quentin McGaw
093e8154f3 feat(gcp): validate credentials JSON object has type field 2024-05-05 10:01:00 +00:00
Quentin McGaw
b131c3d90b chore(build): upgrade Go from 1.21 to 1.22 2024-05-03 07:53:18 +00:00
Quentin McGaw
d3541da812 fix(godaddy): link to comment when 403 is encountered 2024-05-02 20:44:12 +00:00
Quentin McGaw
937a249ffa feat(healthchecksio): option HEALTH_HEALTHCHECKSIO_BASE_URL 2024-04-30 15:52:47 +00:00
Quentin McGaw
11575ee82a docs(readme): clarify HEALTH_HEALTHCHECKSIO_UUID description 2024-04-30 12:58:26 +00:00
Quentin McGaw
ae4ab39421 fix(settings): trim spaces from each host value 2024-04-29 14:27:28 +00:00
dependabot[bot]
3e6336fcbc chore(deps): bump DavidAnson/markdownlint-cli2-action from 15 to 16 (#688) 2024-04-29 16:11:36 +02:00
dependabot[bot]
6d4bb626aa chore(deps): bump google.golang.org/api from 0.175.0 to 0.176.1 (#700) 2024-04-29 16:11:10 +02:00
dependabot[bot]
9b142c99dc chore(deps): bump github.com/qdm12/gosettings from 0.4.0-rc9 to 0.4.1 (#683) 2024-04-29 16:11:00 +02:00
Quentin McGaw
7d627c8581 fix(cloudflare): prevent empty key value if email is set 2024-04-29 14:10:22 +00:00
dependabot[bot]
b72711b0de chore(deps): bump google.golang.org/api from 0.114.0 to 0.175.0 (#697) 2024-04-20 06:26:19 +02:00
dependabot[bot]
8f34c766c5 chore(deps): bump google.golang.org/protobuf from 1.30.0 to 1.33.0 (#671) 2024-04-20 06:23:07 +02:00
Quentin McGaw
ae2bcd55c8 fix(custom): keep url values and only set ip 2024-04-01 13:53:07 +00:00
dependabot[bot]
dcc66fb857 chore(deps): bump github.com/stretchr/testify from 1.8.4 to 1.9.0 (#662) 2024-04-01 14:36:41 +02:00
CyberAustin
b31d848d96 chore(params): validate domain strings for providers using it (#638) 2024-04-01 14:35:10 +02:00
Quentin McGaw
99d670f7f9 docs(readme): improve description of RESOLVER_ADDRESS 2024-04-01 12:09:28 +00:00
Gottfried Mayer
a5cc9da33c docs(infomaniak): add missing details and guide (#677)
- Precise credentials are dyndns ones, not infomaniak admin ones
- Add official guide link
- Precise to set IPv4 or IPv6 address depending on which IP version you want to use
2024-03-19 15:14:18 +01:00
Quentin McGaw
877531c3d9 fix(config): allow custom urls for http ip providers 2024-03-04 14:28:58 +00:00
Quentin McGaw
bfdae74925 docs: add contributing document with example provider
- add example provider in code and docs markdown file
- merge contributing guides together
- add contributing section on adding a new provider
2024-02-29 09:25:54 +00:00
Quentin McGaw
c09e01c6c7 docs(readme): fix public ip echo custom url prefix url:https:// instead of https:// 2024-02-29 07:32:18 +00:00
Quentin McGaw
6a6b1a8ebb feat(pkg/publicip/info): add ip2location.io provider 2024-02-13 10:42:07 +00:00
Quentin McGaw
346f4aa7f7 feat(publicip/http): add seeip.org for all ip versions 2024-02-13 10:35:59 +00:00
Quentin McGaw
5e0e3f4702 feat(publicip/http): add multiple providers for all IP versions
- icanhazip
- ident
- nnev
- wtfismyip
2024-02-13 10:33:34 +00:00
Quentin McGaw
d009ef3d3e fix(inwx): allow wildcard hosts 2024-02-12 14:20:11 +00:00
Quentin McGaw
1706443ecb chore(cloudflare): unexport createRecord method 2024-02-10 14:56:01 +00:00
Quentin McGaw
ec4411e12d feat(healthchecks.io): fail and exit codes support
- notify with `/fail` suffix if any update failed
- notify with `/0` on program exit with 0 code
- notify with `/1` on program exit with 1 code
2024-02-09 13:47:29 +00:00
Quentin McGaw
1697697b81 chore(porkbun): add context to top level errors 2024-02-09 10:00:13 +00:00
Quentin McGaw
cb3075ea32 fix(noip): IPv6 query parameter fixed 2024-02-09 08:10:09 +00:00
Quentin McGaw
4499d87e05 feat(version): print version
- When first argument is `version` or `-version` or `--version`
- Print release tag version, or latest-<commithash> otherwise
2024-02-08 20:03:53 +00:00
yorickdowne
e676983dba feat(docker): /updater/data built-in with correct ownership (#634) 2024-02-08 14:07:05 +01:00
dependabot[bot]
5d550980c1 chore(deps): bump github.com/breml/rootcerts from 0.2.15 to 0.2.16 (#631) 2024-02-08 09:47:12 +01:00
dependabot[bot]
983f41743d chore(deps): bump golang.org/x/mod from 0.14.0 to 0.15.0 (#637) 2024-02-08 09:47:03 +01:00
dependabot[bot]
7bbc860225 chore(deps): bump actions/setup-go from 2 to 5 (#630) 2024-02-08 09:46:42 +01:00
dependabot[bot]
9835d5a5d7 chore(deps): bump DavidAnson/markdownlint-cli2-action from 14 to 15 (#629) 2024-02-08 09:46:34 +01:00
Quentin McGaw
463ad6ac93 chore(ovh): add no host case handling 2024-02-08 08:39:07 +00:00
Quentin McGaw
133956f082 feat(publicip): better error messages stating the provider type if unknown 2024-02-04 14:42:24 +00:00
dependabot[bot]
7aa14c8737 chore(deps): bump github.com/breml/rootcerts from 0.2.14 to 0.2.15 (#612) 2024-02-04 14:00:46 +01:00
dependabot[bot]
845770f441 chore(deps): bump peter-evans/dockerhub-description from 3 to 4 (#610) 2024-02-04 14:00:35 +01:00
dependabot[bot]
8f709d7c90 chore(deps): bump github.com/miekg/dns from 1.1.57 to 1.1.58 (#600) 2024-02-04 14:00:28 +01:00
dependabot[bot]
2af4a1bf82 chore(deps): bump actions/checkout from 3 to 4 (#599) 2024-02-04 14:00:16 +01:00
dependabot[bot]
0bb717c55a chore(deps): bump docker/login-action from 2 to 3 (#597) 2024-02-04 14:00:09 +01:00
dependabot[bot]
d07ce88aa3 chore(deps): bump crazy-max/ghaction-github-labeler from 4 to 5 (#596) 2024-02-04 14:00:00 +01:00
dependabot[bot]
860820087f chore(deps): bump docker/setup-qemu-action from 2 to 3 (#584) 2024-02-04 13:59:52 +01:00
Quentin McGaw
e793c4926b docs(readme): update readme for standalone binaries
- Update description and title to be generic and non-specific to Docker
- Describe availability as container image and prebuilt binaries
- Split features specific to the container in their own features subsection
- Merge "Next steps" section in the container setup section
2024-02-04 11:08:47 +00:00
148 changed files with 2843 additions and 948 deletions

View File

@@ -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

View File

@@ -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).

View File

@@ -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

View File

@@ -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",

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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
[![MIT](https://img.shields.io/github/license/qdm12/ddns-updater)](LICENSE)
![Visitors count](https://visitor-badge.laobi.icu/badge?page_id=ddns-updater.readme)
## 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 UI](https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/webui.png)
![Web UI](readme/webui-desktop.gif)
- Web user interface (Mobile)
![Mobile Web UI](readme/webui-mobile.png)
- 11MB Docker image based on a Go static binary in a Scratch Docker image
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
- Docker healthcheck verifying the DNS resolution of your domains
- Highly configurable
- Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/v0.8/services/overview/) using `SHOUTRRR_ADDRESSES`
- Compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures.
- 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)

View File

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

View File

@@ -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

View File

@@ -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
View 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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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
View 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 -->

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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"`

View File

@@ -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
View 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"
}
]
}
```

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -0,0 +1,5 @@
package backup
type Logger interface {
Info(message string)
}

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

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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")
}

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -10,10 +10,6 @@ import (
"time"
)
func IsClientMode(args []string) bool {
return len(args) > 1 && args[1] == "healthcheck"
}
type Client struct {
*http.Client
}

View File

@@ -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

View File

@@ -0,0 +1,8 @@
package health
import "os"
func IsDocker() (ok bool) {
_, err := os.Stat("isdocker")
return err == nil
}

View File

@@ -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,
})
}

View File

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

View File

@@ -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]
}

View File

@@ -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).

View File

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

View File

@@ -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 {

View File

@@ -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": {

View File

@@ -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,

View File

@@ -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")

View File

@@ -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:

View File

@@ -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
}

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

View File

@@ -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),

View File

@@ -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

View File

@@ -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
}

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

View File

@@ -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,

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

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

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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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