149 Commits

Author SHA1 Message Date
Quentin McGaw (desktop)
70edf86d39 Fix: SHOUTRRR_ADDRESSES case sensitivity 2021-09-12 01:14:22 +00:00
Quentin McGaw (desktop)
fe00994522 Feat: PUBLICIP_DNS_TIMEOUT variable 2021-06-29 20:37:56 +00:00
Quentin McGaw (desktop)
d22dc41903 Maint: pubip: default timeout to 3s for DNS 2021-06-29 20:32:00 +00:00
Quentin McGaw (desktop)
76b1f5f0df Maint: local scoped buildInfo 2021-06-29 20:28:02 +00:00
Quentin McGaw (desktop)
c5de1358dd Doc: add missing architectures to readme 2021-06-29 20:26:06 +00:00
Quentin McGaw (desktop)
4133dbfdc7 Feat: Support Shoutrrr addresses 2021-06-29 20:05:06 +00:00
Quentin McGaw (desktop)
c4f50992c2 Maint: use qdm12/goshutdown 2021-06-29 19:15:55 +00:00
Quentin McGaw (desktop)
f8b67e5261 Maint: context dependent DNS resolutions 2021-06-29 19:13:24 +00:00
Quentin McGaw (desktop)
1b154df8aa Maint: move setupGotify inline in _main function 2021-06-29 18:49:32 +00:00
Quentin McGaw (desktop)
f9edf75540 Maint: more robust main logic
- Encapsulating main() handling OS signals
- _main returns an error instead of an exit code
2021-06-29 18:49:20 +00:00
Quentin McGaw (desktop)
ae748fb80d Maint: use health server port for client query 2021-06-29 18:39:23 +00:00
Quentin McGaw (desktop)
edb6c491bf Maint: use signal.NotifyContext 2021-06-29 18:35:22 +00:00
Quentin McGaw (desktop)
2f196b4886 Maint: pass default parent logger as arg to _main 2021-06-29 18:29:58 +00:00
Quentin McGaw (desktop)
2d7d016650 Maint: pass os.Args as argument to _main 2021-06-29 18:26:28 +00:00
Quentin McGaw (desktop)
1da19b3d6d Maint: pass env as argument to _main 2021-06-29 18:25:44 +00:00
Quentin McGaw (desktop)
0c520eddda Maint: set test stage entrypoint in Dockerfile 2021-06-29 16:25:36 +00:00
Quentin McGaw (desktop)
050825d399 Maint: upgrade xcputranslate to v0.6.0 2021-06-29 16:24:33 +00:00
Quentin McGaw (desktop)
53212df518 Maint: upgrade golangci-lint to v1.41.1 2021-06-29 16:24:05 +00:00
Quentin McGaw (desktop)
6fe4743bc0 Maint: optimize Dockerfile for caching + x-builds
- Pull xcputranslate for build platform only (faster x-builds)
- Install golangci-lint from qmcgaw/binpot (faster)
- Install g++ in base stage (for caching)
- Copy xcputranslate in base stage (for caching)
- Install golangci-lint in base stage (for caching)
- Push ARG TARGETPLATFORM down in build stage (faster x-builds)
- Push versioning ARGs and LABEL down in final stage (for caching)
- Move data directory COPY up in final stage (for caching)
2021-06-29 16:23:21 +00:00
Quentin McGaw (desktop)
4cdc71b45c Maint: server listens on all interfaces 2021-06-29 16:16:07 +00:00
Quentin McGaw (desktop)
36a6275bd2 Maint: config package for environment variables 2021-06-29 16:12:28 +00:00
Quentin McGaw
ee66170a87 Support for domaindiscount24.com (#207) 2021-06-14 08:35:29 -07:00
Quentin McGaw (desktop)
bb62a9d9e1 Fix: wildcard hosts in URL query parameters
- Keep multi-dots wildcard host structure in display strings
- Use another function BuildURLQueryHostname for API calls
- Send the wildcard character in API calls
- Fix issue #214
- Fix behavior for wildcard hosts for:
  - cloudflare
  - ddnss.de
  - digitalocean
  - dnsomatic
  - dreamhost
  - dyn
  - dynv6
  - google
  - informaniak
  - njalla
  - noip
  - opendns
  - ovh
  - selfhost.de
  - spdyn
  - strato
  - variomedia
2021-06-14 00:55:42 +00:00
Quentin McGaw (desktop)
e04c1f83df Maintenance: use time/tzdata instead of Alpine's 2021-06-08 01:43:06 +00:00
Quentin McGaw (desktop)
e19cabc894 Fix: DATADIR defaults to /updater/data 2021-06-08 01:39:29 +00:00
Quentin McGaw (desktop)
682821efd0 Fix: healthcheck query to 127.0.0.1:port 2021-06-07 17:21:36 +00:00
Quentin McGaw (desktop)
276c1c02fd Maintenance: remove github.com/ovh/go-ovh dependency 2021-06-07 16:41:07 +00:00
Quentin McGaw (desktop)
9b8ec3298b Maintenance: simplify file paths logic 2021-06-07 13:51:45 +00:00
Quentin McGaw (desktop)
cc670b3939 Feature: request url and body debug logs 2021-06-07 12:58:17 +00:00
Quentin McGaw (desktop)
0c3d258620 Feature: HEALTH_SERVER_ADDRESS 2021-06-07 12:05:30 +00:00
Quentin McGaw (desktop)
d39ecd6fd3 Fix: IPv6 prefix support to avoid unneeded updates 2021-06-03 14:19:21 +00:00
Quentin McGaw (desktop)
43dd02dbd5 Documentation: add debug log level option 2021-05-27 16:25:39 +00:00
Quentin McGaw
381af0cd90 Feature: allow to run without settings 2021-05-23 22:16:50 +00:00
Quentin McGaw
2bede070de Fix: ROOT_URL behavior when served not at the root 2021-05-23 01:51:17 +00:00
Quentin McGaw
09b810732f Maintenance: use embed for static UI, fix #134 2021-05-23 01:44:33 +00:00
Quentin McGaw
398566850d Fix custom URL for HTTP ip method (fix #203) 2021-05-22 19:42:07 +00:00
Quentin McGaw
201df818d3 Maintenance: rename all provider file to provider.go 2021-05-20 15:29:38 +00:00
Quentin McGaw
b1a3740059 Maintenance: use io instead of ioutil if possible 2021-05-20 15:27:54 +00:00
Quentin McGaw
ae978e007b Maintenance: common receiver struct for all providers 2021-05-20 15:26:05 +00:00
Quentin McGaw
3688208030 Maintenance: split each provider in own package 2021-05-20 15:02:21 +00:00
Quentin McGaw
da4341fbba Maintenance: move settings creation
- From params package to settings package
- Remove constructor type
- Remove regex.Matcher arg if not needed for some providers
2021-05-20 14:47:40 +00:00
Quentin McGaw
1705afeada Maintenance: settings shared code in sub packages 2021-05-20 14:35:52 +00:00
Quentin McGaw
844904aa7b Maintenance: upgrade linting setup
- Update Golangci-lint to v1.40.1
- Add more linters
- Remove rules from .golangci.yml in favor of inline nolint comments
- Fix linting errors
2021-05-19 01:00:42 +00:00
Quentin McGaw
803232e670 Maintenance: upgrade all Go dependencies 2021-05-19 00:41:41 +00:00
Quentin McGaw
2f7ee832b8 Maintenance: go mod tidy 2021-05-19 00:34:56 +00:00
Quentin McGaw
943d1486b3 Maintenance: upgrade devcontainer settings 2021-05-19 00:34:13 +00:00
Quentin McGaw
d727feefc4 Feature: Variomedia support (#208)
- Taken from #174 with added fixes to support IPv6
- Closes #174
2021-05-18 20:28:36 -04:00
Quentin McGaw
aee3022a11 Fix: Dreamhost: create record before removing outdated one (#206) 2021-05-18 19:54:02 -04:00
Quentin McGaw
864a696680 Maintenance: upgrade golibs 2021-04-17 01:20:42 +00:00
Quentin McGaw
34623e1b81 Fix Spdyn authentication 2021-04-10 02:18:43 +00:00
Matthew Hill
c0e57c6e1d Fix: read case sensitive CONFIG variable (#192)
Converting the string to lowercase breaks case sensitive api keys/secrets
Signed-off-by: Matthew Hill <matthewchill7@gmail.com>
2021-04-04 11:14:38 -04:00
Quentin McGaw
1ff629dc17 Feature: provider IP for SPDyn 2021-03-23 23:09:40 +00:00
Quentin McGaw
23f4897365 Fix: ipversion display, fixes #190 2021-03-23 22:54:36 +00:00
Quentin McGaw
e3472476e2 SPDyn support, fixes #182 (#179) 2021-03-23 11:18:02 -04:00
Quentin McGaw
869cf52c7d Njalla support fixes #180 (#181) 2021-03-22 22:15:15 -04:00
Quentin McGaw
8b2e83a69e Feature: HTTP and DNS Public IP fetching options, fixes #136 (#187) 2021-03-22 17:49:58 -04:00
Quentin McGaw
106bcae966 Maintenance: simplify top level publicip package API (#186) 2021-03-21 21:49:47 -04:00
Quentin McGaw
0a89666d1d Feature: public IP package to work over HTTPs and DNS (#158) 2021-03-21 17:59:17 -04:00
Quentin McGaw
3ad9168576 Maintenance: underscore for unused matcher args 2021-03-15 03:26:08 +00:00
Quentin McGaw
1eaa495e66 Maintenance: remove unused Insert database method 2021-03-15 03:10:19 +00:00
Frederic R
5e0cc687ea Fixes #175 documentation (#176) 2021-03-11 08:03:25 -05:00
Quentin McGaw
289536b145 Documentation: architecture section 2021-03-07 01:22:48 +00:00
Quentin McGaw
d5e4936679 Feature: 5-tries DNS resolution per hostname 2021-03-04 14:34:42 +00:00
Quentin McGaw
6e18e921b7 Maintenance: upgrade golangci-lint to 1.37.0 2021-03-02 02:28:10 +00:00
Quentin McGaw
40c92eebf5 Maintenance: Docker build stage uses Alpine 3.13 2021-03-02 02:27:50 +00:00
Quentin McGaw
8adc0556ba Maintenance: upgrade to Go 1.15 2021-03-02 02:27:30 +00:00
Quentin McGaw
e7824014ee Feature: debug logs 2021-03-02 02:21:09 +00:00
Quentin McGaw
6e0d48f7c1 Maintenance: remove no_dns_lookup and replace DNSLookup() method with Proxied() (#160) 2021-03-02 02:07:52 +00:00
Quentin McGaw
a10fb64ffd Maintenance: golibs logger update (#170) 2021-03-01 20:51:22 -05:00
Quentin McGaw
a649f8a4a8 FreeDNS support (#173) 2021-02-20 20:30:16 -05:00
Quentin McGaw
72018451b3 Fix: DuckDNS documentation, fixes #171 2021-02-16 00:28:58 +00:00
Quentin McGaw
2e5b3c7924 Fix: remove Godaddy secret check 2021-02-13 20:26:59 +00:00
Quentin McGaw
c985595969 Fix: godaddy: key regex, fixing #169 2021-02-12 14:37:06 +00:00
Frederic R
caa4840a61 Feature: ddnsgopher favicon (#159) 2021-02-11 08:38:09 -05:00
Quentin McGaw
a86ddd42d1 HTTP server refactor (#164)
* Rework current server
* Update call is blocking
* Run first update without blocking
2021-02-08 21:22:05 -05:00
Frederic R
ce1a447e0a Readme: fix broken links on docker hub (#167) 2021-02-07 19:27:31 -05:00
Frederic R
22c8b587c9 fix typo noticed in issue #165 2021-02-07 18:41:53 +00:00
Quentin McGaw
78c86b0e24 Fix: DNSOMatic fixing #161 2021-01-30 19:58:32 +00:00
Quentin McGaw
fa771cd4b2 CI: use Alpine 3.13 to build Go program 2021-01-29 00:48:04 +00:00
Frederic R
4e94823f69 OVH subdomains (*) support with zone DNS (#154), fixes #153 2021-01-28 19:41:37 -05:00
Frederic R
96521addd5 introduce ddnsgopher logo (#157) 2021-01-28 10:33:53 +00:00
Frederic R
85ddad6da1 Update contributing.md with how to build the program with Go (#151) 2021-01-26 21:57:00 -05:00
Frederic R
d54c334e1c Gandi support (#149)
Authored-by: Frederic R. fredericrous
Co-authored-by: Quentin McGaw <quentin.mcgaw@gmail.com>
2021-01-26 21:54:18 -05:00
Quentin McGaw
49392003e0 CI: rework Github workflows 2021-01-26 02:09:43 +00:00
Quentin McGaw
5a5e0c7375 Fix: Listening port defaults to 8000, fixes #152 2021-01-26 02:04:18 +00:00
Quentin McGaw
180092f47e Linode support (#144) 2021-01-20 18:31:19 -05:00
Quentin McGaw
43e168581c FIX: OVH allow domain parameter, refers to #111 2021-01-20 19:13:45 +00:00
Quentin McGaw
76a40e47c7 CI: Fix BUILD_DATE variable setting 2021-01-19 22:58:01 -05:00
Quentin McGaw
50b1997dbb Change: do not log unhealthy 2021-01-19 06:46:03 +00:00
Quentin McGaw
d75398d71b Maintenance: add response body to error 2021-01-18 00:48:55 +00:00
Quentin McGaw
7191e93932 Maintenance: streamline HTTP headers setting 2021-01-17 21:16:40 +00:00
Quentin McGaw
ec1f7acbde Maintenance: use native Go HTTP client (#145) 2021-01-17 15:48:00 -05:00
Quentin McGaw
a8a8d5793b Maintenance: DNSPod: encode values in body only 2021-01-17 17:37:41 +00:00
Quentin McGaw
dde28ebd1f Maintenance: cloudflare setHeaders receiver 2021-01-17 17:15:01 +00:00
Quentin McGaw
cd5f04acaf CI: Push PR image in PR workflow 2021-01-17 17:06:27 +00:00
Quentin McGaw
5d1809aca6 CI: Fix Docker image tag name in PR workflow 2021-01-17 17:06:04 +00:00
Quentin McGaw
46ae9af6a4 CI: Remove old build workflow 2021-01-17 17:04:56 +00:00
Quentin McGaw
b9a357fc1c Fix: default BUILDPLATFORM for older Docker builds 2021-01-17 17:04:42 +00:00
Quentin McGaw
1e7c909baf Maintenance: additional fixes and changes to errrors 2021-01-17 04:53:59 +00:00
Quentin McGaw
e83857b3db Maintenance: rename params getters 2021-01-16 20:33:13 +00:00
Quentin McGaw
5ab1607f97 Feature: UPDATE_COOLDOWN_PERIOD refers to #140 2021-01-16 20:30:21 +00:00
Quentin McGaw
4bb09e86dd Maintenance: use filepath to join file paths 2021-01-16 20:22:21 +00:00
Quentin McGaw
9460fa969b Maintenance: opendns and ovh receiver letter fixed 2021-01-16 20:15:03 +00:00
Quentin McGaw
157e041b28 Maintenance: use BuildDomainName for url params 2021-01-16 20:12:59 +00:00
Quentin McGaw
b40391bb4e Maintenance: rework errors in settings package 2021-01-16 20:12:31 +00:00
Quentin McGaw
60cf3130e3 Fix: precise 1 hour duration in abuse log 2021-01-16 15:14:53 +00:00
Quentin McGaw
5557c32135 Fix: remove geoblocked ddnss.de ip echo services 2021-01-16 15:12:18 +00:00
Quentin McGaw
46e8647d35 Fix: do not log abuse error twice 2021-01-16 15:10:49 +00:00
Quentin McGaw
eb1f925576 strato: remove user agent string 2021-01-16 15:06:44 +00:00
Quentin McGaw
18b058f188 Maintenance: fix linting errors 2021-01-14 02:01:22 +00:00
Quentin McGaw
be89e798d2 OpenDNS support, fix #121 (#126) 2021-01-13 20:50:53 -05:00
Quentin McGaw
20704eea8c Dynv6 support, fix #89 (#133) 2021-01-13 20:49:01 -05:00
Quentin McGaw
6416c6fed3 OVH support, fix #111 (#127) 2021-01-13 20:48:04 -05:00
Quentin McGaw
a317809ff8 Fix: Strato bad authentication error 2021-01-14 01:43:34 +00:00
Quentin McGaw
7ae9f72038 CI: rework build workflows 2021-01-14 06:16:06 +00:00
Quentin McGaw
5b1bc29ad4 Fix: stop updating when record is in abuse state 2021-01-14 06:07:19 +00:00
Quentin McGaw
e937ee741c Strato: Treat abuse response case 2021-01-13 14:12:49 +00:00
Quentin McGaw
688aebbc4f Fix: Strato: nochg as success, refers to #140 2021-01-12 01:16:41 +00:00
Quentin McGaw
6c2c2cf7cb Maintenance: Update golibs and rework params 2021-01-10 23:45:28 +00:00
Quentin McGaw
1c3e4cdef7 Strato: unknown response print bug fix 2021-01-10 23:17:08 +00:00
Quentin McGaw
d8a7fef6bd LuaDNS support (#137) fix #135 2020-12-30 14:50:41 -05:00
Quentin McGaw
87f59bf498 Remove uuid dependency 2020-12-17 06:05:45 +00:00
Quentin McGaw
818f7471dd Using http.NewRequestWithContext 2020-12-17 06:01:15 +00:00
Quentin McGaw
b35658f32c Remove nolint comments in favor of golangci.yml 2020-12-17 05:58:44 +00:00
Quentin McGaw
48fb1a4b95 Fix noip6 and noip8245_6 links in readme 2020-12-17 05:50:56 +00:00
Quentin McGaw
2e069ccf1d Strato support (#132) 2020-12-16 18:48:19 -05:00
Quentin McGaw
0d38e9385f Remove local vscode directory 2020-12-15 07:48:47 +00:00
Quentin McGaw
b4310ad822 Build information written to Go binary at build 2020-12-15 07:48:27 +00:00
Quentin McGaw
878cf4cc45 Upgrade golangci-lint to 1.33.0 2020-12-15 07:47:44 +00:00
Quentin McGaw
89faafe377 Add .dockerignore to paths for build workflows 2020-12-15 07:47:31 +00:00
Quentin McGaw
bf3f78f9f9 Refactor HTTP servers and upgrade dependencies 2020-12-13 19:09:05 +00:00
Quentin McGaw
6bf82d7be1 Wiki in repository (#128)
* Split provider specific instructions in docs/
* Update links to be relative
* Update Google doc, co-authored by @gauravspatel
2020-12-12 19:38:21 -05:00
Quentin McGaw
77f1681c4c Simplify documentation to ease DNS providers addition 2020-12-13 02:36:43 +00:00
Quentin McGaw
82e3d60db5 Digital ocean support, fix #98 (#110) 2020-12-12 16:20:26 -05:00
Quentin McGaw
c65c8d63bd Selfhost.de support, fix #120 (#122) 2020-12-10 09:19:23 -05:00
Quentin McGaw
0f1ddfb9b0 Upgrade golangci-lint to v1.32.2 2020-12-10 14:11:16 +00:00
Quentin McGaw
1d466cdc83 Workflow trigger paths adjustments 2020-12-10 19:04:18 +00:00
Quentin McGaw
0a6ef7ffbf Small simplification in Runner code 2020-12-10 09:51:34 +00:00
Quentin McGaw
e7ae5ac4cc Fix #104 2020-12-07 04:27:34 +00:00
Quentin McGaw
701ae125bf Upgrade dependencies, refers to #119 2020-11-25 14:27:04 +00:00
Quentin McGaw
b775798b65 Better error log, refers to 119 2020-11-22 02:28:54 +00:00
Quentin McGaw
166b0c7095 Fix #115 2020-11-11 22:28:20 +00:00
Quentin McGaw
3240bb7d26 Fix #113 2020-10-31 14:40:52 +00:00
Quentin McGaw
3047c83ee9 Add linters and fix lint issues 2020-10-25 16:47:27 +00:00
Quentin McGaw
3b29a33849 Update golibs, fixing #112
- All network requests are run with a context for faster shutdowns
- No node id variable for logging
- Stability improvements
2020-10-25 16:31:26 +00:00
Quentin McGaw
860bc02e2e VSCode development container changes
- SSH bind mount as read write
- More Golang linters
2020-10-13 01:10:22 +00:00
Quentin McGaw
cd2d3c46cc DNS-O-Matic support (#97), fixes #88 2020-10-13 01:02:53 +00:00
Quentin McGaw
e630dd9889 Fix up readme instructions, fixes #106 2020-10-04 14:33:05 +00:00
Quentin McGaw
b2d96787b8 Add empty data directory in Dockerfile, fixes #107 2020-10-04 14:32:45 +00:00
200 changed files with 11630 additions and 4336 deletions

View File

@@ -0,0 +1,5 @@
.dockerignore
devcontainer.json
docker-compose.yml
Dockerfile
README.md

1
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1 @@
FROM qmcgaw/godevcontainer

69
.devcontainer/README.md Normal file
View File

@@ -0,0 +1,69 @@
# Development container
Development container that can be used with VSCode.
It works on Linux, Windows and OSX.
## Requirements
- [VS code](https://code.visualstudio.com/download) installed
- [VS code remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed
- [Docker](https://www.docker.com/products/docker-desktop) installed and running
- If you don't use Linux or WSL 2, share your home directory `~/` and the directory of your project with Docker Desktop
- [Docker Compose](https://docs.docker.com/compose/install/) installed
- Ensure your host has the following and that they are accessible by Docker:
- `~/.ssh` directory
- `~/.gitconfig` file (can be empty)
## Setup
1. Open the command palette in Visual Studio Code (CTRL+SHIFT+P).
1. Select `Remote-Containers: Open Folder in Container...` and choose the project directory.
1. For Docker running on Windows HyperV, if you want to use SSH keys, bind mount them at `/tmp/.ssh` by changing the `volumes` section in the [docker-compose.yml](docker-compose.yml).
## Customization
### Customize the image
You can make changes to the [Dockerfile](Dockerfile) and then rebuild the image. For example, your Dockerfile could be:
```Dockerfile
FROM qmcgaw/godevcontainer
USER root
RUN apk add curl
USER vscode
```
Note that you may need to use `USER root` to build as root, and then change back to `USER vscode`.
To rebuild the image, either:
- With VSCode through the command palette, select `Remote-Containers: Rebuild and reopen in container`
- With a terminal, go to this directory and `docker-compose build`
### Customize VS code settings
You can customize **settings** and **extensions** in the [devcontainer.json](devcontainer.json) definition file.
### Entrypoint script
You can bind mount a shell script to `/home/vscode/.welcome.sh` to replace the [current welcome script](shell/.welcome.sh).
### Publish a port
To access a port from your host to your development container, publish a port in [docker-compose.yml](docker-compose.yml).
### Run other services
1. Modify [docker-compose.yml](docker-compose.yml) to launch other services at the same time as this development container, such as a test database:
```yml
database:
image: postgres
restart: always
environment:
POSTGRES_PASSWORD: password
```
1. In [devcontainer.json](devcontainer.json), change the line `"runServices": ["vscode"],` to `"runServices": ["vscode", "database"],`.
1. In the VS code command palette, rebuild the container.

View File

@@ -1,116 +1,79 @@
{
"name": "ddns-dev",
"dockerComposeFile": [
"docker-compose.yml"
],
"service": "vscode",
"runServices": [
"vscode"
],
"shutdownAction": "stopCompose",
"postCreateCommand": "go mod download",
"workspaceFolder": "/workspace",
"appPort": 8000,
"extensions": [
"golang.go",
"IBM.output-colorizer",
"eamodio.gitlens",
"mhutchie.git-graph",
"davidanson.vscode-markdownlint",
"shardulm94.trailing-spaces",
"alefragnani.Bookmarks",
"Gruntfuggly.todo-tree",
"mohsen1.prettify-json",
"quicktype.quicktype",
"spikespaz.vscode-smoothtype",
"vscode-icons-team.vscode-icons"
],
"settings": {
// General settings
"files.eol": "\n",
// Docker
"remote.extensionKind": {
"ms-azuretools.vscode-docker": "workspace"
},
// Golang general settings
"go.useLanguageServer": true,
"go.autocompleteUnimportedPackages": true,
"go.gotoSymbol.includeImports": true,
"go.gotoSymbol.includeGoroot": true,
"gopls": {
"completeUnimported": true,
"deepCompletion": true,
"usePlaceholders": false
},
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast",
"--enable",
"rowserrcheck",
"--enable",
"bodyclose",
"--enable",
"dogsled",
"--enable",
"dupl",
"--enable",
"gochecknoglobals",
"--enable",
"gochecknoinits",
"--enable",
"gocognit",
"--enable",
"goconst",
"--enable",
"gocritic",
"--enable",
"gocyclo",
"--enable",
"goimports",
"--enable",
"golint",
"--enable",
"gosec",
"--enable",
"interfacer",
"--enable",
"maligned",
"--enable",
"misspell",
"--enable",
"nakedret",
"--enable",
"prealloc",
"--enable",
"scopelint",
"--enable",
"unconvert",
"--enable",
"unparam",
"--enable",
"whitespace"
],
// Golang on save
"go.buildOnSave": "workspace",
"go.lintOnSave": "workspace",
"go.vetOnSave": "workspace",
"editor.formatOnSave": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
// Golang testing
"go.toolsEnvVars": {
"GOFLAGS": "-tags=integration"
},
"gopls.env": {
"GOFLAGS": "-tags=integration"
},
"go.testEnvVars": {},
"go.testFlags": [
"-v"
],
"go.testTimeout": "600s"
}
{
"name": "ddns-dev",
"dockerComposeFile": ["docker-compose.yml"],
"service": "vscode",
"runServices": ["vscode"],
"shutdownAction": "stopCompose",
"postCreateCommand": "source ~/.windows.sh && go mod download && go mod tidy",
"workspaceFolder": "/workspace",
// "overrideCommand": "",
"extensions": [
"golang.go",
"eamodio.gitlens", // IDE Git information
"davidanson.vscode-markdownlint",
"ms-azuretools.vscode-docker", // Docker integration and linting
"shardulm94.trailing-spaces", // Show trailing spaces
"Gruntfuggly.todo-tree", // Highlights TODO comments
"bierner.emojisense", // Emoji sense for markdown
"stkb.rewrap", // rewrap comments after n characters on one line
"vscode-icons-team.vscode-icons", // Better file extension icons
"github.vscode-pull-request-github", // Github interaction
"redhat.vscode-yaml", // Kubernetes, Drone syntax highlighting
"bajdzis.vscode-database", // Supports connections to mysql or postgres, over SSL, socked
"IBM.output-colorizer", // Colorize your output/test logs
// "mohsen1.prettify-json", // Prettify JSON data
// "zxh404.vscode-proto3", // Supports Proto syntax
// "jrebocho.vscode-random", // Generates random values
// "alefragnani.Bookmarks", // Manage bookmarks
// "quicktype.quicktype", // Paste JSON as code
// "spikespaz.vscode-smoothtype", // smooth cursor animation
],
"settings": {
"files.eol": "\n",
"remote.extensionKind": {
"ms-azuretools.vscode-docker": "workspace"
},
"editor.codeActionsOnSaveTimeout": 3000,
"go.useLanguageServer": true,
"[go]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
// Optional: Disable snippets, as they conflict with completion ranking.
"editor.snippetSuggestions": "none"
},
"[go.mod]": {
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
},
},
"gopls": {
"usePlaceholders": false,
"staticcheck": true
},
"go.autocompleteUnimportedPackages": true,
"go.gotoSymbol.includeImports": true,
"go.gotoSymbol.includeGoroot": true,
"go.lintTool": "golangci-lint",
"go.buildOnSave": "workspace",
"go.lintOnSave": "workspace",
"go.vetOnSave": "workspace",
"editor.formatOnSave": true,
"go.toolsEnvVars": {
"GOFLAGS": "-tags=",
"CGO_ENABLED": 1 // for the race detector
},
"gopls.env": {
"GOFLAGS": "-tags="
},
"go.testEnvVars": {
"": "",
},
"go.testFlags": ["-v", "-race"],
"go.testTimeout": "10s",
"go.coverOnSingleTest": true,
"go.coverOnSingleTestFile": true,
}
}

View File

@@ -2,14 +2,31 @@ version: "3.7"
services:
vscode:
image: qmcgaw/godevcontainer
build: .
image: godevcontainer
volumes:
- ../:/workspace
- ~/.ssh:/home/vscode/.ssh:ro
- ~/.ssh:/root/.ssh:ro
# Docker
- ~/.docker:/root/.docker:z
# Docker socket to access Docker server
- /var/run/docker.sock:/var/run/docker.sock
# SSH directory for Linux, OSX and WSL
- ~/.ssh:/root/.ssh:z
# For Windows without WSL, a copy will be made
# from /tmp/.ssh to ~/.ssh to fix permissions
# - ~/.ssh:/tmp/.ssh:ro
# Shell history persistence
- ~/.zsh_history:/root/.zsh_history:z
# Git config
- ~/.gitconfig:/root/.gitconfig:z
# Kubernetes
- ~/.kube:/root/.kube:z
environment:
- TZ=
cap_add:
# For debugging with dlv
- SYS_PTRACE
security_opt:
# For debugging with dlv
- seccomp:unconfined
entrypoint: zsh -c "while sleep 1000; do :; done"

View File

@@ -2,9 +2,11 @@
.git
.github
.vscode
docs
readme
.gitignore
config.json
docker-compose.yml
LICENSE
README.md
ui/favicon.svg

View File

@@ -7,46 +7,26 @@ assignees: qdm12
---
<!--
YOU CAN CHAT THERE EVENTUALLY:
https://github.com/qdm12/ddns-updater/discussions
-->
**TLDR**: *Describe your issue in a one liner here*
1. Is this urgent?
1. Is this urgent: Yes/No
2. DNS provider(s) you use: Answer here
3. Program version:
- [ ] Yes
- [x] No
2. What DNS service provider(s) are you using?
- [x] Cloudflare
- [ ] DDNSS.de
- [ ] DonDominio
- [ ] DNSPod
- [ ] Dreamhost
- [ ] DuckDNS
- [ ] DynDNS
- [ ] GoDaddy
- [ ] Google
- [ ] He.net
- [ ] Infomaniak
- [ ] Namecheap
- [ ] NoIP
3. What's the version of the program?
**See the line at the top of your logs**
<!-- See the line at the top of your logs -->
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
4. What are you using to run the container?
- [ ] Docker run
- [x] Docker Compose
- [ ] Kubernetes
- [ ] Docker stack
- [ ] Docker swarm
- [ ] Podman
- [ ] Other:
5. Extra information
4. What are you using to run the container: docker-compose
5. Extra information (optional)
Logs:

View File

@@ -9,6 +9,12 @@ assignees: qdm12
1. What's the feature?
2. Why do you need this feature?
2. Extra information?
3. Extra information?
<!--
YOU CAN CHAT THERE EVENTUALLY:
https://github.com/qdm12/ddns-updater/discussions
-->

View File

@@ -7,46 +7,26 @@ assignees:
---
<!--
HAVE A CHAT FIRST!
https://github.com/qdm12/ddns-updater/discussions
-->
**TLDR**: *Describe your issue in a one liner here*
1. Is this urgent?
1. Is this urgent: Yes/No
2. DNS provider(s) you use: Answer here
3. Program version:
- [ ] Yes
- [x] No
2. What DNS service provider(s) are you using?
- [x] Cloudflare
- [ ] DDNSS.de
- [ ] DonDominio
- [ ] DNSPod
- [ ] Dreamhost
- [ ] DuckDNS
- [ ] DynDNS
- [ ] GoDaddy
- [ ] Google
- [ ] He.net
- [ ] Infomaniak
- [ ] Namecheap
- [ ] NoIP
3. What's the version of the program?
**See the line at the top of your logs**
<!-- See the line at the top of your logs -->
`Running version latest built on 2020-03-13T01:30:06Z (commit d0f678c)`
4. What are you using to run the container?
- [ ] Docker run
- [x] Docker Compose
- [ ] Kubernetes
- [ ] Docker stack
- [ ] Docker swarm
- [ ] Podman
- [ ] Other:
5. Extra information
4. What are you using to run the container: docker-compose
5. Extra information (optional)
Logs:
@@ -54,9 +34,9 @@ Logs:
```
Configuration file:
Configuration file (**remove your credentials!**):
```yml
```json
```

View File

@@ -1,31 +1,101 @@
name: Docker build
name: CI
on:
push:
paths:
- .github/workflows/build.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
pull_request:
branches: [master]
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/build-branch.yml
- .github/workflows/buildx-release.yml
- .github/workflows/buildx-latest.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- readme
- .gitignore
- config.json
- docker-compose.yml
- LICENSE
- README.md
paths:
- .github/workflows/build.yml
- cmd/**
- internal/**
- pkg/**
- .dockerignore
- .golangci.yml
- Dockerfile
- go.mod
- go.sum
jobs:
build:
verify:
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: "1"
steps:
- uses: actions/checkout@v2
- name: Linting
run: docker build --target lint .
- name: Go mod tidy check
run: docker build --target tidy .
- name: Build test image
run: docker build --target test -t test-container .
- name: Run tests in test container
run: |
touch coverage.txt
docker run --rm \
-v "$(pwd)/coverage.txt:/tmp/gobuild/coverage.txt" \
test-container
# We run this here to use the caching of the previous steps
- name: Build final image
run: docker build .
publish:
needs: [verify]
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Build image
run: docker build .
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: qmcgaw
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Set variables
id: vars
run: |
BRANCH=${GITHUB_REF#refs/heads/}
TAG=${GITHUB_REF#refs/tags/}
echo ::set-output name=commit::$(git rev-parse --short HEAD)
echo ::set-output name=build_date::$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ "$TAG" != "$GITHUB_REF" ]; then
echo ::set-output name=version::$TAG
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
elif [ "$BRANCH" = "master" ]; then
echo ::set-output name=version::latest
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7,linux/s390x,linux/ppc64le,linux/riscv64
else
echo ::set-output name=version::$BRANCH
echo ::set-output name=platforms::linux/amd64,linux/386,linux/arm64,linux/arm/v6,linux/arm/v7
fi
- name: Build and push final image
uses: docker/build-push-action@v2
with:
platforms: ${{ steps.vars.outputs.platforms }}
build-args: |
BUILD_DATE=${{ steps.vars.outputs.build_date }}
COMMIT=${{ steps.vars.outputs.commit }}
VERSION=${{ steps.vars.outputs.version }}
tags: qmcgaw/ddns-updater:${{ steps.vars.outputs.version }}
push: true
- if: github.event.ref == 'refs/heads/master'
name: Microbadger hook
run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s=
continue-on-error: true

View File

@@ -1,47 +0,0 @@
name: Buildx branch
on:
push:
branches:
- '*'
- '*/*'
- '!master'
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/build.yml
- .github/workflows/buildx-release.yml
- .github/workflows/buildx-latest.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- readme
- .gitignore
- config.json
- docker-compose.yml
- LICENSE
- README.md
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
run: |
docker buildx build \
--progress plain \
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=${GITHUB_REF##*/} \
-t qmcgaw/ddns-updater:${GITHUB_REF##*/} \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0

View File

@@ -1,44 +0,0 @@
name: Buildx latest
on:
push:
branches: [master]
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/build.yml
- .github/workflows/buildx-release.yml
- .github/workflows/buildx-branch.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- readme
- .gitignore
- config.json
- docker-compose.yml
- LICENSE
- README.md
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
run: |
docker buildx build \
--progress plain \
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=latest \
-t qmcgaw/ddns-updater:latest \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0

View File

@@ -1,44 +0,0 @@
name: Buildx release
on:
release:
types: [published]
paths-ignore:
- .devcontainer
- .github/ISSUE_TEMPLATE
- .github/workflows/build.yml
- .github/workflows/buildx-branch.yml
- .github/workflows/buildx-latest.yml
- .github/workflows/dockerhub-description.yml
- .github/workflows/labels.yml
- .github/CODEOWNERS
- .github/CONTRIBUTING.md
- .github/FUNDING.yml
- .github/labels.yml
- .vscode
- readme
- .gitignore
- config.json
- docker-compose.yml
- LICENSE
- README.md
jobs:
buildx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Buildx setup
uses: crazy-max/ghaction-docker-buildx@v1
- name: Dockerhub login
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
- name: Run Buildx
run: |
docker buildx build \
--progress plain \
--platform=linux/amd64,linux/386,linux/arm64,linux/arm/v7 \
--build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` \
--build-arg VCS_REF=`git rev-parse --short HEAD` \
--build-arg VERSION=${GITHUB_REF##*/} \
-t qmcgaw/ddns-updater:${GITHUB_REF##*/} \
--push \
.
- run: curl -X POST https://hooks.microbadger.com/images/qmcgaw/ddns-updater/t2fcZxog8ce_kJYJ61JjkYwHF5s= || exit 0

View File

@@ -1,10 +1,10 @@
name: labels
on:
push:
branches: ["master"]
branches: [master]
paths:
- '.github/labels.yml'
- '.github/workflows/labels.yml'
- .github/labels.yml
- .github/workflows/labels.yml
jobs:
labeler:
runs-on: ubuntu-latest

3
.gitignore vendored
View File

@@ -1,3 +0,0 @@
*.exe
updater
.vscode

View File

@@ -4,40 +4,68 @@ linters-settings:
misspell:
locale: US
issues:
exclude-rules:
- path: cmd/updater/main.go
text: "mnd: Magic number: 4, in <argument> detected"
linters:
- gomnd
- path: cmd/updater/main.go
text: "mnd: Magic number: 2, in <argument> detected"
linters:
- gomnd
linters:
disable-all: true
enable:
- asciicheck
- bodyclose
- deadcode
- dogsled
- dupl
- errcheck
- exhaustive
- exportloopref
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- goheader
- goimports
- golint
- gomnd
- goprintffuncname
- gosec
# - goerr113 # TODO
- gosimple
- govet
- importas
- ineffassign
- interfacer
- maligned
- lll
- misspell
- nakedret
- nestif
- nilerr
- noctx
- nolintlint
- prealloc
- predeclared
- rowserrcheck
- scopelint
- exportloopref
- sqlclosecheck
- staticcheck
- structcheck
- thelper
- tparallel
- typecheck
- unconvert
- unparam
- unused
- varcheck
- wastedassign
- whitespace
run:

88
.vscode/settings.json vendored
View File

@@ -1,88 +0,0 @@
{
// General settings
"files.eol": "\n",
// Docker
"remote.extensionKind": {
"ms-azuretools.vscode-docker": "workspace"
},
// Golang general settings
"go.useLanguageServer": true,
"go.autocompleteUnimportedPackages": true,
"go.gotoSymbol.includeImports": true,
"go.gotoSymbol.includeGoroot": true,
"gopls": {
"completeUnimported": true,
"deepCompletion": true,
"usePlaceholders": false
},
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast",
"--enable",
"rowserrcheck",
"--enable",
"bodyclose",
"--enable",
"dogsled",
"--enable",
"dupl",
"--enable",
"gochecknoglobals",
"--enable",
"gochecknoinits",
"--enable",
"gocognit",
"--enable",
"goconst",
"--enable",
"gocritic",
"--enable",
"gocyclo",
"--enable",
"goimports",
"--enable",
"golint",
"--enable",
"gosec",
"--enable",
"interfacer",
"--enable",
"maligned",
"--enable",
"misspell",
"--enable",
"nakedret",
"--enable",
"prealloc",
"--enable",
"scopelint",
"--enable",
"unconvert",
"--enable",
"unparam",
"--enable",
"whitespace"
],
// Golang on save
"go.buildOnSave": "workspace",
"go.lintOnSave": "workspace",
"go.vetOnSave": "workspace",
"editor.formatOnSave": true,
"[go]": {
"editor.codeActionsOnSave": {
"source.organizeImports": true
}
},
// Golang testing
"go.toolsEnvVars": {
"GOFLAGS": "-tags="
},
"gopls.env": {
"GOFLAGS": "-tags="
},
"go.testEnvVars": {},
"go.testFlags": [
"-v"
],
"go.testTimeout": "600s"
}

View File

@@ -1,43 +1,67 @@
ARG ALPINE_VERSION=3.12
ARG GO_VERSION=1.15
ARG BUILDPLATFORM=linux/amd64
ARG ALPINE_VERSION=3.13
ARG GO_VERSION=1.16
ARG XCPUTRANSLATE_VERSION=v0.6.0
ARG GOLANGCI_LINT_VERSION=v1.41.1
FROM alpine:${ALPINE_VERSION} AS alpine
RUN apk --update add ca-certificates tzdata
FROM --platform=${BUILDPLATFORM} qmcgaw/xcputranslate:${XCPUTRANSLATE_VERSION} AS xcputranslate
FROM --platform=${BUILDPLATFORM} qmcgaw/binpot:golangci-lint-${GOLANGCI_LINT_VERSION} AS golangci-lint
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
ARG GOLANGCI_LINT_VERSION=v1.31.0
RUN apk --update add git
ENV CGO_ENABLED=0
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS alpine
RUN apk --update add ca-certificates
RUN mkdir /tmp/data && \
chown 1000 /tmp/data && \
chmod 700 /tmp/data
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
WORKDIR /tmp/gobuild
COPY .golangci.yml .
ENV CGO_ENABLED=0
RUN apk --update add git g++
COPY --from=xcputranslate /xcputranslate /usr/local/bin/xcputranslate
COPY --from=golangci-lint /bin /go/bin/golangci-lint
# Copy repository code and install Go dependencies
COPY go.mod go.sum ./
RUN go mod download
COPY pkg/ ./pkg/
COPY cmd/ ./cmd/
COPY internal/ ./internal/
COPY cmd/updater/main.go .
RUN go test ./...
RUN go build -trimpath -ldflags="-s -w" -o app
FROM --platform=$BUILDPLATFORM base AS test
# Note on the go race detector:
# - we set CGO_ENABLED=1 to have it enabled
# - we installed g++ to support the race detector
ENV CGO_ENABLED=1
ENTRYPOINT go test -race -coverpkg=./... -coverprofile=coverage.txt -covermode=atomic ./...
FROM --platform=$BUILDPLATFORM base AS lint
COPY .golangci.yml ./
RUN golangci-lint run --timeout=10m
FROM --platform=$BUILDPLATFORM base AS tidy
RUN git init && \
git config user.email ci@localhost && \
git config user.name ci && \
git add -A && git commit -m ci && \
sed -i '/\/\/ indirect/d' go.mod && \
go mod tidy && \
git diff --exit-code -- go.mod
FROM --platform=$BUILDPLATFORM base AS build
ARG VERSION=unknown
ARG BUILD_DATE="an unknown date"
ARG COMMIT=unknown
ARG TARGETPLATFORM
RUN GOARCH="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field arch)" \
GOARM="$(xcputranslate translate -targetplatform ${TARGETPLATFORM} -field arm)" \
go build -trimpath -ldflags="-s -w \
-X 'main.version=$VERSION' \
-X 'main.buildDate=$BUILD_DATE' \
-X 'main.commit=$COMMIT' \
" -o app cmd/updater/main.go
FROM scratch
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
ENV VERSION=$VERSION \
BUILD_DATE=$BUILD_DATE \
VCS_REF=$VCS_REF
LABEL \
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.title="ddns-updater" \
org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP"
COPY --from=alpine --chown=1000 /tmp/data /updater/data/
COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo
EXPOSE 8000
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
USER 1000
@@ -46,10 +70,15 @@ ENV \
# Core
CONFIG= \
PERIOD=5m \
IP_METHOD=cycle \
IPV4_METHOD=cycle \
IPV6_METHOD=cycle \
UPDATE_COOLDOWN_PERIOD=5m \
PUBLICIP_FETCHERS=all \
PUBLICIP_HTTP_PROVIDERS=all \
PUBLICIPV4_HTTP_PROVIDERS=all \
PUBLICIPV6_HTTP_PROVIDERS=all \
PUBLICIP_DNS_PROVIDERS=all \
PUBLICIP_DNS_TIMEOUT=3s \
HTTP_TIMEOUT=10s \
DATADIR=/updater/data \
# Web UI
LISTENING_PORT=8000 \
@@ -60,11 +89,21 @@ ENV \
BACKUP_DIRECTORY=/updater/data \
# Other
LOG_ENCODING=console \
LOG_LEVEL=info \
NODE_ID=-1 \
GOTIFY_URL= \
GOTIFY_TOKEN= \
LOG_CALLER=hidden \
SHOUTRRR_ADDRESSES= \
TZ=
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
COPY --chown=1000 ui/* /updater/ui/
ARG VERSION=unknown
ARG BUILD_DATE="an unknown date"
ARG COMMIT=unknown
LABEL \
org.opencontainers.image.authors="quentin.mcgaw@gmail.com" \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.revision=$COMMIT \
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.title="ddns-updater" \
org.opencontainers.image.description="Universal DNS updater with WebUI"
COPY --from=build --chown=1000 /tmp/gobuild/app /updater/app

357
README.md
View File

@@ -1,8 +1,8 @@
# Lightweight universal DDNS Updater with Docker and web UI
*Light container updating DNS A records periodically for Cloudflare, DDNSS.de, DonDominio, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP*
*Light container updating DNS A and/or AAAA records periodically for multiple DNS providers*
[![DDNS Updater by Quentin McGaw](https://github.com/qdm12/ddns-updater/raw/master/readme/title.png)](https://hub.docker.com/r/qmcgaw/ddns-updater)
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg?sanitize=true">
[![Build status](https://github.com/qdm12/ddns-updater/workflows/Buildx%20latest/badge.svg)](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
[![Docker Pulls](https://img.shields.io/docker/pulls/qmcgaw/ddns-updater.svg)](https://hub.docker.com/r/qmcgaw/ddns-updater)
@@ -17,7 +17,35 @@
## Features
- Updates periodically A records for different DNS providers: Cloudflare, DDNSS.de, DonDominio, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP ([create an issue](https://github.com/qdm12/ddns-updater/issues/new/choose) for more)
- Updates periodically A records for different DNS providers:
- Cloudflare
- DD24
- DDNSS.de
- DigitalOcean
- DonDominio
- DNSOMatic
- DNSPod
- Dreamhost
- DuckDNS
- DynDNS
- FreeDNS
- Gandi
- GoDaddy
- Google
- He.net
- Infomaniak
- Linode
- LuaDNS
- Namecheap
- NoIP
- Njalla
- OpenDNS
- OVH
- Selfhost.de
- Spdyn
- Strato.de
- Variomedia.de
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
- Web User interface
![Web UI](https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/webui.png)
@@ -26,56 +54,12 @@
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
- Docker healthcheck verifying the DNS resolution of your domains
- Highly configurable
- Sends notifications to your Android phone, see the [**Gotify**](#Gotify) section (it's free, open source and self hosted 🆒)
- Compatible with `amd64`, `386`, `arm64`, `arm32v7` (Raspberry Pis) CPU architectures.
- Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/services/overview/) using `SHOUTRRR_ADDRESSES`
- Compatible with `amd64`, `386`, `arm64`, `armv7`, `armv6`, `s390x`, `ppc64le`, `riscv64` CPU architectures.
## Setup
The program reads the configuration from a JSON configuration file.
1. First, create a JSON configuration starting from, for example:
```json
{
"settings": [
{
"provider": "namecheap",
"domain": "example.com",
"host": "@",
"password": "e5322165c1d74692bfa6d807100c0310"
},
{
"provider": "duckdns",
"domain": "example.duckdns.org",
"token": "00000000-0000-0000-0000-000000000000"
},
{
"provider": "godaddy",
"domain": "example.org",
"host": "subdomain",
"key": "aaaaaaaaaaaaaaaa",
"secret": "aaaaaaaaaaaaaaaa"
}
]
}
```
1. You can find more information in the [configuration section](#configuration) to customize it.
1. You can either use a bind mounted file or put all your JSON in a single line with the `CONFIG` environment variable, see the two subsections below for each
### Using the CONFIG variable
1. Remove all 'new lines' in order to put your entire JSON in a single line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`)
1. Set the `CONFIG` environment variable to your single line configuration
1. Use the following command:
```sh
docker run -d -p 8000:8000/tcp -e CONFIG='{"settings": [{"provider": "namecheap", ...}]}' qmcgaw/ddns-updater
```
Note that this CONFIG environment variable takes precedence over the config.json file if it is set.
### Using a file
The program reads the configuration from a JSON object, either from a file or from an environment variable.
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
@@ -92,13 +76,32 @@ Note that this CONFIG environment variable takes precedence over the config.json
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
1. Place your JSON configuration in `data/config.json`
1. Use the following command:
1. Write a JSON configuration in *data/config.json*, for example:
```json
{
"settings": [
{
"provider": "namecheap",
"domain": "example.com",
"host": "@",
"password": "e5322165c1d74692bfa6d807100c0310"
}
]
}
```
You can find more information in the [configuration section](#configuration) to customize it.
1. Run the container with
```sh
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
```
1. ⚠️ If you use IPv6, you might need to set `-e IPV6_PREFIX=/64` (`/64` is your prefix, depending on your ISP)
1. (Optional) You can also set your JSON configuration as a single environment variable line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`), which takes precedence over config.json. Note however that if you don't bind mount the `/updater/data` directory, there won't be a persistent database file `/updater/updates.json` but it will still work.
### Next steps
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
@@ -126,120 +129,41 @@ Start by having the following content in *config.json*, or in your `CONFIG` envi
}
```
The following parameters are to be added:
For each setting, you need to fill in parameters.
Check the documentation for your DNS provider:
For all record update configuration, you have to specify the DNS provider with `"provider"` which can be `"cloudflare"`, `"ddnss"`, `"dondominio"`, `"dnspod"`, `"dreamhost"`, `"duckdns"`, `"dyn"`, `"godaddy"`, `"google"`, `"he"`, `"infomaniak"`, `"namecheap"` or `"noip"`.
You can optionnally add the parameters:
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
- [DD24](https://github.com/qdm12/ddns-updater/blob/master/docs/domaindiscount24.md)
- [DonDominio](https://github.com/qdm12/ddns-updater/blob/master/docs/dondominio.md)
- [DNSOMatic](https://github.com/qdm12/ddns-updater/blob/master/docs/dnsomatic.md)
- [DNSPod](https://github.com/qdm12/ddns-updater/blob/master/docs/dnspod.md)
- [Dreamhost](https://github.com/qdm12/ddns-updater/blob/master/docs/dreamhost.md)
- [DuckDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/duckdns.md)
- [DynDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/dyndns.md)
- [DynV6](https://github.com/qdm12/ddns-updater/blob/master/docs/dynv6.md)
- [FreeDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/freedns.md)
- [Gandi](https://github.com/qdm12/ddns-updater/blob/master/docs/gandi.md)
- [GoDaddy](https://github.com/qdm12/ddns-updater/blob/master/docs/godaddy.md)
- [Google](https://github.com/qdm12/ddns-updater/blob/master/docs/google.md)
- [He.net](https://github.com/qdm12/ddns-updater/blob/master/docs/he.net.md)
- [Infomaniak](https://github.com/qdm12/ddns-updater/blob/master/docs/infomaniak.md)
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
- [Namecheap](https://github.com/qdm12/ddns-updater/blob/master/docs/namecheap.md)
- [NoIP](https://github.com/qdm12/ddns-updater/blob/master/docs/noip.md)
- [Njalla](https://github.com/qdm12/ddns-updater/blob/master/docs/njalla.md)
- [OpenDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/opendns.md)
- [OVH](https://github.com/qdm12/ddns-updater/blob/master/docs/ovh.md)
- [Selfhost.de](https://github.com/qdm12/ddns-updater/blob/master/docs/selfhost.de.md)
- [Spdyn](https://github.com/qdm12/ddns-updater/blob/master/docs/spdyn.md)
- [Strato.de](https://github.com/qdm12/ddns-updater/blob/master/docs/strato.md)
- [Variomedia.de](https://github.com/qdm12/ddns-updater/blob/master/docs/variomedia.md)
- `"no_dns_lookup"` can be `true` or `false` and allows, if `true`, to prevent the program from doing assumptions from DNS lookups returning an IP address not matching your public IP address (in example for proxied records on Cloudflare).
- `"provider_ip"` can be `true` or `false`. It is only available for the providers `ddnss`, `duckdns`, `he`, `infomaniak`, `namecheap`, `noip` and `dyndns`. It allows to let your DNS provider to determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
Note that:
For each DNS provider exist some specific parameters you need to add, as described below:
Namecheap:
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"password"`
Cloudflare:
- `"zone_identifier"` is the Zone ID of your site
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
- One of the following:
- Email `"email"` and Global API Key `"key"`
- User service key `"user_service_key"`
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone.
- *Optionally*, `"proxied"` can be `true` or `false` to use the proxy services of Cloudflare
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
GoDaddy:
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"key"`
- `"secret"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DuckDNS:
- `"domain"` is your fqdn, for example `subdomain.duckdns.org`
- `"token"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
Dreamhost:
- `"domain"`
- `"key"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
NoIP:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DNSPOD:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"token"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
HE.net:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"` (untested)
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
Infomaniak:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"user"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DDNSS.de:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"user"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DYNDNS:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
Google:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DonDominio:
- `"domain"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"name"` is the name server associated with the domain
### Additional notes
- You can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
### Environment variables
@@ -247,75 +171,96 @@ DonDominio:
| --- | --- | --- |
| `CONFIG` | | One line JSON object containing the entire config (takes precendence over config.json file) if specified |
| `PERIOD` | `5m` | Default period of IP address check, following [this format](https://golang.org/pkg/time/#ParseDuration) |
| `IP_METHOD` | `cycle` | Method to obtain the public IP address (ipv4 or ipv6). See the [IP Methods section](#IP-methods) |
| `IPV4_METHOD` | `cycle` | Method to obtain the public IPv4 address only. See the [IP Methods section](#IP-methods) |
| `IPV6_METHOD` | `cycle` | Method to obtain the public IPv6 address only. See the [IP Methods section](#IP-methods) |
| `IPV6_PREFIX` | `/128` | IPv6 prefix used to mask your public IPv6 address and your record IPv6 address. Ranges from `/0` to `/128` depending on your ISP. |
| `PUBLICIP_FETCHERS` | `all` | Comma separated fetcher types to obtain the public IP address from `http` and `dns` |
| `PUBLICIP_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (ipv4 or ipv6). See the [Public IP section](#Public-IP) |
| `PUBLICIPV4_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv4 address only. See the [Public IP section](#Public-IP) |
| `PUBLICIPV6_HTTP_PROVIDERS` | `all` | Comma separated providers to obtain the public IPv6 address only. See the [Public IP section](#Public-IP) |
| `PUBLICIP_DNS_PROVIDERS` | `all` | Comma separated providers to obtain the public IP address (IPv4 and/or IPv6). See the [Public IP section](#Public-IP) |
| `PUBLICIP_DNS_TIMEOUT` | `3s` | Public IP DNS query timeout |
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
| `HEALTH_SERVER_ADDRESS` | `127.0.0.1:9999` | Health server listening address |
| `DATADIR` | `/updater/data` | Directory to read and write data files from internally |
| `BACKUP_PERIOD` | `0` | Set to a period (i.e. `72h15m`) to enable zip backups of data/config.json and data/updates.json in a zip file |
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`.
| `LOG_ENCODING` | `console` | Format of logging, `json` or `console` |
| `LOG_LEVEL` | `info` | Level of logging, `info`, `warning` or `error` |
| `NODE_ID` | `-1` | Node ID (for distributed systems), can be any integer |
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
| `BACKUP_DIRECTORY` | `/updater/data` | Directory to write backup zip files to if `BACKUP_PERIOD` is not `0`. |
| `LOG_LEVEL` | `info` | Level of logging, `debug`, `info`, `warning` or `error` |
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
| `SHOUTRRR_ADDRESSES` | | (optional) Comma separated list of [Shoutrrr addresses](https://containrrr.dev/shoutrrr/services/overview/) (notification services) |
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
#### IP methods
#### Public IP
By default, all ip methods are cycled through between all ip methods available for the specified ip version, if any. This allows you not to be blocked for making too many requests. You can otherwise pick one of the following.
By default, all public IP fetching types are used and cycled (over DNS and over HTTPs).
- IPv4 or IPv6 (for most cases)
On top of that, for each fetching method, all echo services available are cycled on each request.
This allows you not to be blocked for making too many requests.
You can otherwise customize it with the following:
- `PUBLICIP_HTTP_PROVIDERS` gets your public IPv4 or IPv6 address. It can be one or more of the following:
- `opendns` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip)
- `ifconfig` using [https://ifconfig.io/ip](https://ifconfig.io/ip)
- `ipinfo` using [https://ipinfo.io/ip](https://ipinfo.io/ip)
- `ddnss` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
- `PUBLICIPV4_HTTP_PROVIDERS` gets your public IPv4 address only. It can be one or more of the following:
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
- `"ddnss"` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
- `"google"` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
- IPv4 only (useful for updating both ipv4 and ipv6)
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
- `"ddnss4"` using [https://ip4.ddnss.de/meineip.php](https://ip4.ddnss.de/meineip.php)
- `"noip4"` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
- `"noip8245_4"` using [http://ip1.dynupdate.no-ip.com:8245](http://ip1.dynupdate.no-ip.com:8245)
- IPv6 only
- `ipify6` using [https://api6.ipify.org](https://api6.ipify.org)
- `"ddnss6"` using [https://ip6.ddnss.de/meineip.php](https://ip6.ddnss.de/meineip.php)
- `"noip6"` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
- `"noip8245_6"` using [http://ip1.dynupdate.no-ip.com:8245](http://ip1.dynupdate.no-ip.com:8245)
You can also specify an HTTPS URL to obtain your public IP address (i.e. `-e IPV6_METHOD=https://ipinfo.io/ip`)
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
- `PUBLICIPV6_HTTP_PROVIDERS` gets your public IPv6 address only. It can be one or more of the following:
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
- `noip` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
- You can also specify an HTTPS URL such as `https://ipinfo.io/ip`
- `PUBLICIP_DNS_PROVIDERS` gets your public IPv4 address only or IPv6 address only or one of them (see #136). It can be one or more of the following:
- `google`
- `cloudflare`
### Host firewall
If you have a host firewall in place, this container needs the following ports:
- TCP 443 outbound for outbound HTTPS
- TCP 80 outbound if you use a local unsecured HTTP connection to your Gotify server
- UDP 53 outbound for outbound DNS resolution
- TCP 8000 inbound (or other) for the WebUI
## Domain set up
## Architecture
Instructions to setup your domain for this program are available for DuckDNS, Cloudflare, GoDaddy and Namecheap on the [Github Wiki](https://github.com/qdm12/ddns-updater/wiki).
At program start and every period (5 minutes by default):
## Gotify
1. Fetch your public IP address
1. For each record:
1. DNS resolve it to obtain its current IP address(es)
- If the resolution fails, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address is within the resolved IP addresses
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
[![Gotify](https://github.com/qdm12/ddns-updater/blob/master/readme/gotify.png?raw=true)](https://gotify.net)
💡 We do DNS resolution every period so it detects a change made to the record manually, for example on the DNS provider web UI
💡 As DNS resolutions are essentially free and without rate limiting, these are great to avoid getting banned for too many requests.
[**Gotify**](https://gotify.net) is a simple server for sending and receiving messages, and it is **free**, **private** and **open source**
### Special case: Cloudflare
- It has an [Android app](https://play.google.com/store/apps/details?id=com.github.gotify) to receive notifications
- The app does not drain your battery 👍
- The notification server is self hosted, see [how to set it up with Docker](https://gotify.net/docs/install)
- The notifications only go through your own server (ideally through HTTPS though)
For Cloudflare records with the `proxied` option, the following is done.
To set it up with DDNS updater:
At program start and every period (5 minutes by default), for each record:
1. Go to the Web GUI of Gotify
1. Login with the admin credentials
1. Create an app and copy the generated token to the environment variable `GOTIFYTOKEN` (for this container)
1. Set the `GOTIFYURL` variable to the URL of your Gotify server address (i.e. `http://127.0.0.1:8080` or `https://bla.com/gotify`)
1. Fetch your public IP address
1. For each record:
1. Check the last IP address (persisted in `updates.json`) for that record
- If it doesn't exist, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address matches the last IP address you updated the record with
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
This is the only way as doing a DNS resolution on the record will give the IP address of a Cloudflare server instead of your server.
⚠️ This has the disadvantage that if the record is changed manually, the program will not detect it.
We could do an API call to get the record IP address every period, but that would get you banned especially with a low period duration.
## Testing
@@ -326,11 +271,9 @@ To set it up with DDNS updater:
1. Run the container
1. Refresh the DNS management webpage and verify the update happened
Better testing instructions are written in the [Wiki for GoDaddy](https://github.com/qdm12/ddns-updater/wiki/GoDaddy#testing)
## Development and contributing
- Contribute with code: see [the Wiki](https://github.com/qdm12/ddns-updater/wiki/Contributing)
- [Contribute with code](https://github.com/qdm12/ddns-updater/blob/master/docs/contributing.md)
- [Github workflows to know what's building](https://github.com/qdm12/ddns-updater/actions)
- [List of issues and feature requests](https://github.com/qdm12/ddns-updater/issues)
- [Kanban board](https://github.com/qdm12/ddns-updater/projects/1)

View File

@@ -2,263 +2,249 @@ package main
import (
"context"
"net"
"os/signal"
"syscall"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
_ "time/tzdata"
"github.com/containrrr/shoutrrr"
"github.com/qdm12/ddns-updater/internal/backup"
"github.com/qdm12/ddns-updater/internal/config"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/handlers"
"github.com/qdm12/ddns-updater/internal/healthcheck"
"github.com/qdm12/ddns-updater/internal/health"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/params"
jsonparams "github.com/qdm12/ddns-updater/internal/params"
"github.com/qdm12/ddns-updater/internal/persistence"
recordslib "github.com/qdm12/ddns-updater/internal/records"
"github.com/qdm12/ddns-updater/internal/server"
"github.com/qdm12/ddns-updater/internal/splash"
"github.com/qdm12/ddns-updater/internal/update"
"github.com/qdm12/golibs/admin"
libhealthcheck "github.com/qdm12/golibs/healthcheck"
"github.com/qdm12/ddns-updater/pkg/publicip"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/network/connectivity"
"github.com/qdm12/golibs/server"
"github.com/qdm12/golibs/params"
"github.com/qdm12/goshutdown"
)
//nolint:gochecknoglobals
var (
version = "unknown"
commit = "unknown"
buildDate = "an unknown date"
)
func main() {
os.Exit(_main(context.Background(), time.Now))
// returns 1 on error
// returns 2 on os signal
buildInfo := models.BuildInformation{
Version: version,
Commit: commit,
BuildDate: buildDate,
}
env := params.NewEnv()
logger := logging.NewParent(logging.Settings{Writer: os.Stdout})
ctx := context.Background()
ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
ctx, cancel := context.WithCancel(ctx)
errorCh := make(chan error)
go func() {
errorCh <- _main(ctx, env, os.Args, logger, buildInfo, time.Now)
}()
select {
case <-ctx.Done():
stop()
logger.Warn("Caught OS signal, shutting down")
case err := <-errorCh:
stop()
close(errorCh)
if err == nil { // expected exit such as healthcheck
os.Exit(0)
}
logger.Error(err)
cancel()
}
const shutdownGracePeriod = 5 * time.Second
timer := time.NewTimer(shutdownGracePeriod)
select {
case err := <-errorCh:
if !timer.Stop() {
<-timer.C
}
if err != nil {
logger.Error(err)
}
logger.Info("Shutdown successful")
case <-timer.C:
logger.Warn("Shutdown timed out")
}
os.Exit(1)
}
type allParams struct {
period time.Duration
ipMethod models.IPMethod
ipv4Method models.IPMethod
ipv6Method models.IPMethod
dir string
dataDir string
listeningPort string
rootURL string
backupPeriod time.Duration
backupDirectory string
}
func _main(ctx context.Context, timeNow func() time.Time) int {
if libhealthcheck.Mode(os.Args) {
func _main(ctx context.Context, env params.Env, args []string, logger logging.ParentLogger,
buildInfo models.BuildInformation, timeNow func() time.Time) (err error) {
if health.IsClientMode(args) {
// Running the program in a separate instance through the Docker
// built-in healthcheck, in an ephemeral fashion to query the
// long running instance of the program about its status
if err := libhealthcheck.Query(); err != nil {
fmt.Println(err)
return 1
client := health.NewClient()
var healthConfig config.Health
_, err := healthConfig.Get(env)
if err != nil {
return err
}
return 0
}
logger, err := setupLogger()
if err != nil {
fmt.Println(err)
return 1
}
paramsReader := params.NewReader(logger)
fmt.Println(splash.Splash(
paramsReader.GetVersion(),
paramsReader.GetVcsRef(),
paramsReader.GetBuildDate()))
notify, err := setupGotify(paramsReader, logger)
if err != nil {
logger.Error(err)
return 1
if err := client.Query(ctx, healthConfig.Port); err != nil {
return err
}
return nil
}
p, err := getParams(paramsReader, logger)
fmt.Println(splash.Splash(buildInfo))
var config config.Config
warnings, err := config.Get(env)
for _, warning := range warnings {
logger.Warn(warning)
}
if err != nil {
logger.Error(err)
notify(4, err)
return 1
return err
}
persistentDB, err := persistence.NewJSON(p.dataDir)
// Setup logger
loggerSettings := logging.Settings{
Level: config.Logger.Level,
Caller: config.Logger.Caller}
logger = logging.NewParent(loggerSettings)
sender, err := shoutrrr.CreateSender(config.Shoutrrr.Addresses...)
if err != nil {
logger.Error(err)
notify(4, err)
return 1
return err
}
settings, warnings, err := paramsReader.GetSettings(p.dataDir + "/config.json")
notify := func(message string) {
errs := sender.Send(message, &config.Shoutrrr.Params)
for _, err := range errs {
if err != nil {
logger.Error(err.Error())
}
}
}
persistentDB, err := persistence.NewJSON(config.Paths.DataDir)
if err != nil {
notify(err.Error())
return err
}
jsonReader := jsonparams.NewReader(logger)
settings, warnings, err := jsonReader.JSONSettings(config.Paths.JSON, logger)
for _, w := range warnings {
logger.Warn(w)
notify(2, w)
notify(w)
}
if err != nil {
logger.Error(err)
notify(4, err)
return 1
notify(err.Error())
return err
}
if len(settings) > 1 {
logger.Info("Found %d settings to update records", len(settings))
} else if len(settings) == 1 {
L := len(settings)
switch L {
case 0:
logger.Warn("Found no setting to update record")
case 1:
logger.Info("Found single setting to update record")
default:
logger.Info("Found %d settings to update records", len(settings))
}
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
client := &http.Client{Timeout: config.Client.Timeout}
connectivity := connectivity.NewConnectivity(net.DefaultResolver, client)
for _, err := range connectivity.Checks(ctx, "github.com") {
logger.Warn(err)
}
records := make([]recordslib.Record, len(settings))
for i, s := range settings {
logger.Info("Reading history from database: domain %s host %s", s.Domain(), s.Host())
events, err := persistentDB.GetEvents(s.Domain(), s.Host())
if err != nil {
logger.Error(err)
notify(4, err)
return 1
notify(err.Error())
return err
}
records[i] = recordslib.New(s, events)
}
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
if err != nil {
logger.Error(err)
notify(4, err)
return 1
}
client := network.NewClient(HTTPTimeout)
defer client.Close()
defer client.CloseIdleConnections()
db := data.NewDatabase(records, persistentDB)
defer func() {
if err := db.Close(); err != nil {
logger.Error(err)
}
}()
updater := update.NewUpdater(db, client, notify)
ipGetter := update.NewIPGetter(client, p.ipMethod, p.ipv4Method, p.ipv6Method)
runner := update.NewRunner(db, updater, ipGetter, logger, timeNow)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
forceUpdate := runner.Run(ctx, p.period)
forceUpdate()
productionHandlerFunc := handlers.MakeHandler(p.rootURL, p.dir+"/ui", db, logger, forceUpdate, timeNow)
healthcheckHandlerFunc := libhealthcheck.GetHandler(func() error {
return healthcheck.IsHealthy(db, net.LookupIP, logger)
})
logger.Info("Web UI listening at address 0.0.0.0:%s with root URL %q", p.listeningPort, p.rootURL)
notify(1, fmt.Sprintf("Launched with %d records to watch", len(records)))
serverErrors := make(chan []error)
go func() {
serverErrors <- server.RunServers(ctx,
server.Settings{Name: "production", Addr: "0.0.0.0:" + p.listeningPort, Handler: productionHandlerFunc},
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
)
}()
go backupRunLoop(ctx, p.backupPeriod, p.dir, p.backupDirectory, logger, timeNow)
config.PubIP.HTTPSettings.Client = client
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals,
syscall.SIGINT,
syscall.SIGTERM,
os.Interrupt,
)
select {
case errors := <-serverErrors:
for _, err := range errors {
logger.Error(err)
}
return 1
case signal := <-osSignals:
message := fmt.Sprintf("Stopping program: caught OS signal %q", signal)
logger.Warn(message)
notify(2, message)
return 2
case <-ctx.Done():
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
logger.Warn(message)
return 1
ipGetter, err := publicip.NewFetcher(config.PubIP.DNSSettings, config.PubIP.HTTPSettings)
if err != nil {
return err
}
updater := update.NewUpdater(db, client, notify, logger)
runner := update.NewRunner(db, updater, ipGetter, config.Update.Period,
config.IPv6.Mask, config.Update.Cooldown, logger, timeNow)
runnerHandler, runnerCtx, runnerDone := goshutdown.NewGoRoutineHandler(
"runner", goshutdown.GoRoutineSettings{})
go runner.Run(runnerCtx, runnerDone)
// note: errors are logged within the goroutine,
// no need to collect the resulting errors.
go runner.ForceUpdate(ctx)
isHealthy := health.MakeIsHealthy(db, net.LookupIP, logger)
healthServer := health.NewServer(config.Health.ServerAddress,
logger.NewChild(logging.Settings{Prefix: "healthcheck server: "}),
isHealthy)
healthServerHandler, healthServerCtx, healthServerDone := goshutdown.NewGoRoutineHandler(
"health server", goshutdown.GoRoutineSettings{})
go healthServer.Run(healthServerCtx, healthServerDone)
address := ":" + strconv.Itoa(int(config.Server.Port))
serverLogger := logger.NewChild(logging.Settings{Prefix: "http server: "})
server := server.New(ctx, address, config.Server.RootURL, db, serverLogger, runner)
serverHandler, serverCtx, serverDone := goshutdown.NewGoRoutineHandler(
"server", goshutdown.GoRoutineSettings{})
go server.Run(serverCtx, serverDone)
notify("Launched with " + strconv.Itoa(len(records)) + " records to watch")
backupHandler, backupCtx, backupDone := goshutdown.NewGoRoutineHandler(
"backup", goshutdown.GoRoutineSettings{})
go backupRunLoop(backupCtx, backupDone, config.Backup.Period, config.Paths.DataDir, config.Backup.Directory,
logger.NewChild(logging.Settings{Prefix: "backup: "}), timeNow)
shutdownGroup := goshutdown.NewGroupHandler("", goshutdown.GroupSettings{})
shutdownGroup.Add(runnerHandler, healthServerHandler, serverHandler, backupHandler)
<-ctx.Done()
if err := shutdownGroup.Shutdown(context.Background()); err != nil {
notify(err.Error())
return err
}
return nil
}
func setupLogger() (logging.Logger, error) {
paramsReader := params.NewReader(nil)
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
if err != nil {
return nil, err
}
return logging.NewLogger(encoding, level, nodeID)
}
func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func(priority int, messageArgs ...interface{}), err error) {
gotifyURL, err := paramsReader.GetGotifyURL()
if err != nil {
return nil, err
} else if gotifyURL == nil {
return func(priority int, messageArgs ...interface{}) {}, nil
}
gotifyToken, err := paramsReader.GetGotifyToken()
if err != nil {
return nil, err
}
gotify := admin.NewGotify(*gotifyURL, gotifyToken, &http.Client{Timeout: time.Second})
return func(priority int, messageArgs ...interface{}) {
if err := gotify.Notify("DDNS Updater", priority, messageArgs...); err != nil {
logger.Error(err)
}
}, nil
}
func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams, err error) {
var warnings []string
p.period, warnings, err = paramsReader.GetPeriod()
for _, warning := range warnings {
logger.Warn(warning)
}
if err != nil {
return p, err
}
p.ipMethod, err = paramsReader.GetIPMethod()
if err != nil {
return p, err
}
p.ipv4Method, err = paramsReader.GetIPv4Method()
if err != nil {
return p, err
}
p.ipv6Method, err = paramsReader.GetIPv6Method()
if err != nil {
return p, err
}
p.dir, err = paramsReader.GetExeDir()
if err != nil {
return p, err
}
p.dataDir, err = paramsReader.GetDataDir(p.dir)
if err != nil {
return p, err
}
p.listeningPort, _, err = paramsReader.GetListeningPort()
if err != nil {
return p, err
}
p.rootURL, err = paramsReader.GetRootURL()
if err != nil {
return p, err
}
p.backupPeriod, err = paramsReader.GetBackupPeriod()
if err != nil {
return p, err
}
p.backupDirectory, err = paramsReader.GetBackupDirectory()
if err != nil {
return p, err
}
return p, nil
}
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
logger logging.Logger, timeNow func() time.Time) {
logger = logger.WithPrefix("backup: ")
func backupRunLoop(ctx context.Context, done chan<- struct{}, backupPeriod time.Duration,
dataDir, outputDir string, logger logging.Logger, timeNow func() time.Time) {
defer close(done)
if backupPeriod == 0 {
logger.Info("disabled")
return
@@ -267,11 +253,13 @@ func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outp
ziper := backup.NewZiper()
timer := time.NewTimer(backupPeriod)
for {
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
fileName := "ddns-updater-backup-" + strconv.Itoa(int(timeNow().UnixNano())) + ".zip"
zipFilepath := filepath.Join(outputDir, fileName)
if err := ziper.ZipFiles(
filepath,
fmt.Sprintf("%s/data/updates.json", exeDir),
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
zipFilepath,
filepath.Join(dataDir, "updates.json"),
filepath.Join(dataDir, "config.json"),
); err != nil {
logger.Error(err)
}
select {

View File

@@ -11,9 +11,13 @@ services:
environment:
- CONFIG=
- PERIOD=5m
- IP_METHOD=cycle
- IPV4_METHOD=cycle
- IPV6_METHOD=cycle
- UPDATE_COOLDOWN_PERIOD=5m
- PUBLICIP_FETCHERS=all
- PUBLICIP_HTTP_PROVIDERS=all
- PUBLICIPV4_HTTP_PROVIDERS=all
- PUBLICIPV6_HTTP_PROVIDERS=all
- PUBLICIP_DNS_PROVIDERS=all
- PUBLICIP_DNS_TIMEOUT=3s
- HTTP_TIMEOUT=10s
# Web UI
@@ -25,9 +29,7 @@ services:
- BACKUP_DIRECTORY=/updater/data
# Other
- LOG_ENCODING=console
- LOG_LEVEL=info
- NODE_ID=-1 # -1 to disable
- GOTIFY_URL=
- GOTIFY_TOKEN=
- LOG_CALLER=hidden
- SHOUTRRR_ADDRESSES=
restart: always

57
docs/cloudflare.md Normal file
View File

@@ -0,0 +1,57 @@
# Cloudflare
## Configuration
### Example
```json
{
"settings": [
{
"provider": "cloudflare",
"zone_identifier": "some id",
"domain": "domain.com",
"host": "@",
"ttl": 600,
"token": "yourtoken",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"zone_identifier"` is the Zone ID of your site
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
- One of the following:
- Email `"email"` and Global API Key `"key"`
- User service key `"user_service_key"`
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone
### Optional parameters
- `"proxied"` can be set to `true` to use the proxy services of Cloudflare
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), and defaults to `ipv4 or ipv6`
## Domain setup
1. Make sure you have `curl` installed
1. Obtain your API key from Cloudflare website ([see this](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-))
1. Obtain your zone identifier for your domain name, from the domain's overview page written as *Zone ID*
1. Find your **identifier** in the `id` field with
```sh
ZONEID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
EMAIL=example@example.com
APIKEY=aaaaaaaaaaaaaaaaaa
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONEID/dns_records" \
-H "X-Auth-Email: $EMAIL" \
-H "X-Auth-Key: $APIKEY"
```
You can now fill in the necessary parameters in *config.json*
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.

52
docs/contributing.md Normal file
View File

@@ -0,0 +1,52 @@
# Contributing
## Table of content
1. [Setup](#Setup)
1. [Commands available](#Commands-available)
1. [Guidelines](#Guidelines)
## Setup
### Using VSCode and Docker
That should be easier and better than a local setup, although it might use more memory if you're not on Linux.
1. Install [Docker](https://docs.docker.com/install/)
- On Windows, share a drive with Docker Desktop and have the project on that partition
- On OSX, share your project directory with Docker Desktop
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
1. Your dev environment is ready to go!... and it's running in a container :+1:
### Locally
Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads); then:
```sh
go mod download
```
And finally install [golangci-lint](https://github.com/golangci/golangci-lint#install).
You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](../.vscode/settings.json).
## Build and Run
```sh
go build -o app cmd/updater/main.go
./app
```
## Commands available
- Test the code: `go test ./...`
- Lint the code `golangci-lint run`
- Build the Docker image (tests and lint included): `docker build -t qmcgaw/ddns-updater .`
- Run the Docker container: `docker run -it --rm -v /yourpath/data:/updater/data qmcgaw/ddns-updater`
## Guidelines
The Go code is in the Go file [cmd/updater/main.go](](../cmd/updater/main.go) and the [internal directory](](../internal), you might want to start reading the main.go file.
See the [Contributing document](](../.github/CONTRIBUTING.md) for more information on how to contribute to this repository.

29
docs/dd24.md Normal file
View File

@@ -0,0 +1,29 @@
# Domain Discount 24
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dd24",
"domain": "domain.com",
"host": "@",
"password": "password",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"password"` is your password
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`

34
docs/ddnss.de.md Normal file
View File

@@ -0,0 +1,34 @@
# DDNSS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "ddnss",
"provider_ip": true,
"domain": "domain.com",
"host": "@",
"username": "user",
"password": "password",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

31
docs/digitalocean.md Normal file
View File

@@ -0,0 +1,31 @@
# Digital Ocean
## Configuration
### Example
```json
{
"settings": [
{
"provider": "digitalocean",
"domain": "domain.com",
"host": "@",
"token": "yourtoken",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"token"` is your token that you can create [here](https://cloud.digitalocean.com/settings/applications)
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

32
docs/dnsomatic.md Normal file
View File

@@ -0,0 +1,32 @@
# DNS-O-Matic
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dnsomatic",
"domain": "domain.com",
"host": "@",
"token": "yourtoken",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

31
docs/dnspod.md Normal file
View File

@@ -0,0 +1,31 @@
# DNSPod
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dnspod",
"domain": "domain.com",
"host": "@",
"token": "yourtoken",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"token"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

33
docs/dondominio.md Normal file
View File

@@ -0,0 +1,33 @@
# Don Dominio
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dondominio",
"domain": "domain.com",
"name": "something",
"username": "username",
"password": "password",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"name"` is the name server associated with the domain
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

29
docs/dreamhost.md Normal file
View File

@@ -0,0 +1,29 @@
# Dreamhost
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dreamhost",
"domain": "domain.com",
"key": "key",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"key"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

35
docs/duckdns.md Normal file
View File

@@ -0,0 +1,35 @@
# DuckDNS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "duckdns",
"host": "host",
"token": "token",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"host"` is your host, for example `subdomain` for `subdomain.duckdns.org`
- `"token"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup
[![DuckDNS Website](../readme/duckdns.png)](https://duckdns.org)
*See the [duckdns website](https://duckdns.org)*

35
docs/dyndns.md Normal file
View File

@@ -0,0 +1,35 @@
# DynDNS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dyn",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup

33
docs/dynv6.md Normal file
View File

@@ -0,0 +1,33 @@
# DynV6
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dynv6",
"domain": "domain.com",
"host": "@",
"token": "token",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"token"` that you can obtain [here](https://dynv6.com/keys#token)
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup

31
docs/freedns.md Normal file
View File

@@ -0,0 +1,31 @@
# FreeDNS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "freedns",
"domain": "domain.com",
"host": "host",
"token": "token",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host (subdomain)
- `"token"` is the randomized update token you use to update your record
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

37
docs/gandi.md Normal file
View File

@@ -0,0 +1,37 @@
# Gandi
This provider uses Gandi v5 API
## Configuration
### Example
```json
{
"settings": [
{
"provider": "gandi",
"domain": "domain.com",
"host": "@",
"key": "key",
"ttl": 3600,
"ip_version": "ipv4",
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` which can be a subdomain, `@` or a wildcard `*`
- `"key"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"ttl"` default is `3600`
## Domain setup
[Gandi Documentation Website](https://docs.gandi.net/en/domain_names/advanced_users/api.html#gandi-s-api)

61
docs/godaddy.md Normal file
View File

@@ -0,0 +1,61 @@
# GoDaddy
## Configuration
### Example
```json
{
"settings": [
{
"provider": "godaddy",
"domain": "domain.com",
"host": "@",
"key": "key",
"secret": "secret",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"key"`
- `"secret"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup
[![GoDaddy Website](../readme/godaddy.png)](https://godaddy.com)
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
[![GoDaddy Developer Login](../readme/godaddy1.gif)](https://developer.godaddy.com/keys)
1. Generate a Test key and secret.
[![GoDaddy Developer Test Key](../readme/godaddy2.gif)](https://developer.godaddy.com/keys)
1. Generate a **Production** key and secret.
[![GoDaddy Developer Production Key](../readme/godaddy3.gif)](https://developer.godaddy.com/keys)
Obtain the **key** and **secret** of that production key.
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
## Testing
1. Go to [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
[![GoDaddy DNS management](../readme/godaddydnsmanagement.png)](https://dcc.godaddy.com/manage/)
1. Change the IP address to `127.0.0.1`
1. Run the ddns-updater
1. Refresh the Godaddy webpage to check the update occurred.

42
docs/google.md Normal file
View File

@@ -0,0 +1,42 @@
# Google
## Configuration
### Example
```json
{
"settings": [
{
"provider": "godaddy",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup
Thanks to [@gauravspatel](https://github.com/gauravspatel) for #124
1. Enable dynamic DNS in the *synthetic records* section of DNS management.
1. The username and password is generated once you create the dynamic DNS entry.
### Wildcard entries
If you want to create a **wildcard entry**, you have to create a custom **CNAME** record with key `"*"` and value `"@"`.

31
docs/he.net.md Normal file
View File

@@ -0,0 +1,31 @@
# He.net
## Configuration
### Example
```json
{
"settings": [
{
"provider": "he",
"domain": "domain.com",
"host": "@",
"password": "password",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"` (untested)
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

34
docs/infomaniak.md Normal file
View File

@@ -0,0 +1,34 @@
# Infomaniak
## Configuration
### Example
```json
{
"settings": [
{
"provider": "infomaniak",
"domain": "domain.com",
"host": "@",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup

34
docs/linode.md Normal file
View File

@@ -0,0 +1,34 @@
# Linode
## Configuration
### Example
```json
{
"settings": [
{
"provider": "linode",
"domain": "domain.com",
"host": "@",
"token": "token",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"token"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup
1. Create a personal access token with `domains` set, with read and write privileges, ideally that never expires. You can refer to [@AnujRNair's comment](https://github.com/qdm12/ddns-updater/pull/144#discussion_r559292678) and to [Linode's guide](https://www.linode.com/docs/products/tools/cloud-manager/guides/cloud-api-keys).
1. The program will create the A or AAAA record for you if it doesn't exist already.

37
docs/luadns.md Normal file
View File

@@ -0,0 +1,37 @@
# LuaDNS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "luadns",
"domain": "domain.com",
"host": "@",
"email": "email",
"token": "token",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"email"`
- `"token"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup
1. Go to [api.luadns.com/settings](https://api.luadns.com/settings)
1. Enable API access
1. Obtain your API token and replace it in the parameters as the value for `token`

57
docs/namecheap.md Normal file
View File

@@ -0,0 +1,57 @@
# Namecheap
## Configuration
### Example
```json
{
"settings": [
{
"provider": "namecheap",
"domain": "domain.com",
"host": "@",
"password": "password",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"password"`
### Optional parameters
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
Note that Namecheap only supports ipv4 addresses for now.
## Domain setup
[![Namecheap Website](../readme/namecheap.png)](https://www.namecheap.com)
1. Create a Namecheap account and buy a domain name - *example.com* as an example
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap1.png)
1. Select the following settings and create the *A + Dynamic DNS Record*:
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap2.png)
1. Scroll down and turn on the switch for *DYNAMIC DNS*
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap3.png)
1. The Dynamic DNS Password will appear, which is `0e4512a9c45a4fe88313bcc2234bf547` in this example.
![https://ap.www.namecheap.com/Domains/DomainControlPanel/mealracle.com/advancedns](../readme/namecheap4.png)

35
docs/njalla.md Normal file
View File

@@ -0,0 +1,35 @@
# Njalla
## Configuration
### Example
```json
{
"settings": [
{
"provider": "njalla",
"domain": "domain.com",
"host": "@",
"key": "key",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"key"` is the key for your record
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup
See [https://njal.la/docs/ddns](https://njal.la/docs/ddns/)

34
docs/noip.md Normal file
View File

@@ -0,0 +1,34 @@
# NoIP
## Configuration
### Example
```json
{
"settings": [
{
"provider": "noip",
"domain": "domain.com",
"host": "@",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup

35
docs/opendns.md Normal file
View File

@@ -0,0 +1,35 @@
# OpenDNS
## Configuration
### Example
```json
{
"settings": [
{
"provider": "dyn",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup

51
docs/ovh.md Normal file
View File

@@ -0,0 +1,51 @@
# OVH
## Configuration
### Example
```json
{
"settings": [
{
"provider": "ovh",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
#### Using DynHost
- `"username"`
- `"password"`
#### OR Using ZoneDNS
- `"api_endpoint"` default value is `"ovh-eu"`
- `"app_key"` which you can create at [eu.api.ovh.com/createApp](https://eu.api.ovh.com/createApp/)
- `"app_secret"`
- `"consumer_key"`
The ZoneDNS implementation allows you to update any record name including *.yourdomain.tld
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
- `"mode"` select between two modes, OVH's dynamic hosting service (`"dynamic"`) or OVH's API (`"api"`). Default is `"dynamic"`
## Domain setup
- If you use DynHost: [docs.ovh.com/ie/en/domains/hosting_dynhost](https://docs.ovh.com/ie/en/domains/hosting_dynhost/)
- If you use the ZoneDNS API: [docs.ovh.com/gb/en/customer/first-steps-with-ovh-api](https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/)

33
docs/selfhost.de.md Normal file
View File

@@ -0,0 +1,33 @@
# Selfhost.de
## Configuration
### Example
```json
{
"settings": [
{
"provider": "selfhost.de",
"domain": "domain.com",
"host": "@",
"username": "username",
"password": "password",
"ip_version": "ipv4"
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"` is your DynDNS username
- `"password"` is your DynDNS password
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
## Domain setup

41
docs/spdyn.md Normal file
View File

@@ -0,0 +1,41 @@
# Spdyn.de
## Configuration
### Example
```json
{
"settings": [
{
"provider": "spdyn",
"domain": "domain.com",
"host": "@",
"user": "user",
"password": "password",
"token": "token",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
#### Using user and password
- `"user"` is the name of a user who can update this host
- `"password"` is the password of a user who can update this host
#### Using update tokens
- `"token"` is your update token
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (**not IPv6**)automatically when you send an update request, without sending the new IP address detected by the program in the request.

35
docs/strato.md Normal file
View File

@@ -0,0 +1,35 @@
# Strato
## Configuration
### Example
```json
{
"settings": [
{
"provider": "strato",
"domain": "domain.com",
"host": "@",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"password"` is your dyndns password
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup
See [their article](https://www.strato.com/faq/en_us/domain/this-is-how-easy-it-is-to-set-up-dyndns-for-your-domains/)

37
docs/variomedia.md Normal file
View File

@@ -0,0 +1,37 @@
# Variomedia
## Configuration
### Example
```json
{
"settings": [
{
"provider": "variomedia",
"domain": "domain.com",
"host": "@",
"email": "email@domain.com",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```
### Compulsory parameters
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"email"`
- `"password"` is your DNS settings password, not your account password ⚠️
### Optional parameters
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
## Domain setup
See [dyndns.variomedia.de](https://dyndns.variomedia.de/)

13
go.mod
View File

@@ -1,11 +1,14 @@
module github.com/qdm12/ddns-updater
go 1.15
go 1.16
require (
github.com/golang/mock v1.4.4
github.com/google/uuid v1.1.1
github.com/containrrr/shoutrrr v0.4.4
github.com/go-chi/chi v1.5.4
github.com/golang/mock v1.5.0
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a
github.com/stretchr/testify v1.6.1
github.com/miekg/dns v1.1.42
github.com/qdm12/golibs v0.0.0-20210514224620-c025cb0da211
github.com/qdm12/goshutdown v0.1.0
github.com/stretchr/testify v1.7.0
)

392
go.sum
View File

@@ -1,55 +1,148 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzsP1G42dRafH3vf+al2vQIJU0YHX+1Tw87oco=
github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw=
github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0=
github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM=
github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containrrr/shoutrrr v0.4.4 h1:vHZ4E/76pKVY+Jyn/qhBz3X540Bn8NI5ppPHK4PyILY=
github.com/containrrr/shoutrrr v0.4.4/go.mod h1:zqL2BvfC1W4FujrT4b3/ZCLxvD+uoeEpBL7rg9Dqpbg=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.11.0 h1:l4iX0RqNnx/pU7rY2DB/I+znuYY0K3x6Ywac6EIr0PA=
github.com/fatih/color v1.11.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/errors v0.17.2 h1:azEQ8Fnx0jmtFF2fxsnmd6I0x6rsweUF63qqSO1NmKk=
github.com/go-openapi/errors v0.17.2/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonreference v0.17.0 h1:yJW3HCkTHg7NOA+gZ83IPHzUSnUzGXhGmsdiCcMexbA=
github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/loads v0.17.0 h1:H22nMs3GDQk4SwAaFQ+jLNw+0xoFeCueawhZlv8MBYs=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.17.2 h1:/ZK67ikFhQAMFFH/aPu2MaGH7QjP4wHBvHYOVIzDAw0=
github.com/go-openapi/runtime v0.17.2/go.mod h1:QO936ZXeisByFmZEO1IS1Dqhtf4QV1sYYFtIq6Ld86Q=
github.com/go-openapi/spec v0.17.0 h1:XNvrt8FlSVP8T1WuhbAFF6QDhJc0zsoWzX4wXARhhpE=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/strfmt v0.17.0 h1:1isAxYf//QDTnVzbLAMrUK++0k1EjeLJU/gTOR0o3Mc=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi880=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq8iS6U=
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I=
github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jarcoal/httpmock v1.0.4 h1:jp+dy/+nonJE4g4xbVtl9QdrUNbn6/3hDT5R4nDIZnA=
github.com/jarcoal/httpmock v1.0.4/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg=
github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -57,85 +150,234 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.6 h1:11TGpSHY7Esh/i/qnq02Jo5oVrI1Gue8Slbq0ujPZFQ=
github.com/nxadm/tail v1.4.6/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M=
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a h1:IyS72qFm+iXipadmUKXmpJScKXXK2GrD8yYfxXsnIYs=
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/qdm12/golibs v0.0.0-20210514224620-c025cb0da211 h1:tpjavgiEPlyZtsXO1xMzN1JpeDwhCMnn4c9dFVtl0i0=
github.com/qdm12/golibs v0.0.0-20210514224620-c025cb0da211/go.mod h1:Is1wBOULKFH6NFPVBR+ksPWkm8njbKyQkOinLLfFAuE=
github.com/qdm12/goshutdown v0.1.0 h1:lmwnygdXtnr2pa6VqfR/bm8077/BnBef1+7CP96B7Sw=
github.com/qdm12/goshutdown v0.1.0/go.mod h1:/LP3MWLqI+wGH/ijfaUG+RHzBbKXIiVKnrg5vXOCf6Q=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04 h1:cEhElsAv9LUt9ZUUocxzWe05oFLVd+AA2nstydTeI8g=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gosrc.io/xmpp v0.5.1 h1:Rgrm5s2rt+npGggJH3HakQxQXR8ZZz3+QRzakRQqaq4=
gosrc.io/xmpp v0.5.1/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
nhooyr.io/websocket v1.6.5/go.mod h1:F259lAzPRAH0htX2y3ehpJe09ih1aSHN7udWki1defY=
nhooyr.io/websocket v1.8.6 h1:s+C3xAMLwGmlI31Nyn/eAehUlZPwfYZu2JXM621Q5/k=
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=

21
internal/config/backup.go Normal file
View File

@@ -0,0 +1,21 @@
package config
import (
"time"
"github.com/qdm12/golibs/params"
)
type Backup struct {
Period time.Duration
Directory string
}
func (b *Backup) get(env params.Env) (err error) {
b.Period, err = env.Duration("BACKUP_PERIOD", params.Default("0"))
if err != nil {
return err
}
b.Directory, err = env.Path("BACKUP_DIRECTORY", params.Default("./data"))
return err
}

16
internal/config/client.go Normal file
View File

@@ -0,0 +1,16 @@
package config
import (
"time"
"github.com/qdm12/golibs/params"
)
type Client struct {
Timeout time.Duration
}
func (c *Client) get(env params.Env) (err error) {
c.Timeout, err = env.Duration("HTTP_TIMEOUT", params.Default("10s"))
return err
}

72
internal/config/config.go Normal file
View File

@@ -0,0 +1,72 @@
package config
import (
"github.com/qdm12/golibs/params"
)
type Config struct {
Client Client
Update Update
PubIP PubIP
IPv6 IPv6
Server Server
Health Health
Paths Paths
Backup Backup
Logger Logger
Shoutrrr Shoutrrr
}
func (c *Config) Get(env params.Env) (warnings []string, err error) {
if err := c.Client.get(env); err != nil {
return warnings, err
}
warning, err := c.Update.get(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
newWarnings, err := c.PubIP.get(env)
warnings = append(warnings, newWarnings...)
if err != nil {
return warnings, err
}
if err := c.IPv6.get(env); err != nil {
return warnings, err
}
warning, err = c.Server.get(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
warning, err = c.Health.Get(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
if err := c.Paths.get(env); err != nil {
return warnings, err
}
if err := c.Backup.get(env); err != nil {
return warnings, err
}
if err := c.Logger.get(env); err != nil {
return warnings, err
}
newWarnings, err = c.Shoutrrr.get(env)
warnings = append(warnings, newWarnings...)
if err != nil {
return warnings, err
}
return warnings, nil
}

31
internal/config/health.go Normal file
View File

@@ -0,0 +1,31 @@
package config
import (
"net"
"strconv"
"github.com/qdm12/golibs/params"
)
type Health struct {
ServerAddress string
Port uint16 // obtained from ServerAddress
}
func (h *Health) Get(env params.Env) (warning string, err error) {
h.ServerAddress, warning, err = env.ListeningAddress(
"HEALTH_SERVER_ADDRESS", params.Default("127.0.0.1:9999"))
if err != nil {
return warning, err
}
_, portStr, err := net.SplitHostPort(h.ServerAddress)
if err != nil {
return warning, err
}
port, err := strconv.Atoi(portStr)
if err != nil {
return warning, err
}
h.Port = uint16(port)
return warning, nil
}

56
internal/config/ipv6.go Normal file
View File

@@ -0,0 +1,56 @@
package config
import (
"errors"
"fmt"
"net"
"strings"
"github.com/qdm12/golibs/params"
)
type IPv6 struct {
Mask net.IPMask
}
func (i *IPv6) get(env params.Env) (err error) {
maskStr, err := env.Get("IPV6_PREFIX", params.Default("/128"))
if err != nil {
return err
}
i.Mask, err = ipv6DecimalPrefixToMask(maskStr)
return err
}
var ErrParsePrefix = errors.New("cannot parse IP prefix")
func ipv6DecimalPrefixToMask(prefixDecimal string) (ipMask net.IPMask, err error) {
if prefixDecimal == "" {
return nil, fmt.Errorf("%w: empty prefix", ErrParsePrefix)
}
prefixDecimal = strings.TrimPrefix(prefixDecimal, "/")
const bits = 8 * net.IPv6len
ones, consumed, ok := decimalToInteger(prefixDecimal)
if !ok || consumed != len(prefixDecimal) || ones < 0 || ones > bits {
return nil, fmt.Errorf("%w: %s", ErrParsePrefix, prefixDecimal)
}
return net.CIDRMask(ones, bits), nil
}
func decimalToInteger(s string) (ones int, i int, ok bool) {
const big = 0xFFFFFF // Bigger than we need, not too big to worry about overflow
const ten = 10
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
ones = ones*ten + int(s[i]-'0')
if ones >= big {
return big, i, false
}
}
return ones, i, true
}

View File

@@ -0,0 +1,62 @@
package config
import (
"fmt"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_ipv6DecimalPrefixToMask(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
prefixDecimal string
ipMask net.IPMask
err error
}{
"empty": {
err: fmt.Errorf("cannot parse IP prefix: empty prefix"),
},
"malformed": {
prefixDecimal: "malformed",
err: fmt.Errorf("cannot parse IP prefix: malformed"),
},
"with leading slash": {
prefixDecimal: "/78",
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
},
"without leading slash": {
prefixDecimal: "78",
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
},
"full IPv6 mask": {
prefixDecimal: "/128",
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
},
"zero IPv6 mask": {
prefixDecimal: "/0",
ipMask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
}
for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()
ipMask, err := ipv6DecimalPrefixToMask(testCase.prefixDecimal)
if testCase.err != nil {
require.Error(t, err)
assert.Equal(t, testCase.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.Equal(t, testCase.ipMask, ipMask)
})
}
}

21
internal/config/logger.go Normal file
View File

@@ -0,0 +1,21 @@
package config
import (
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
)
type Logger struct {
Caller logging.Caller
Level logging.Level
}
func (l *Logger) get(env params.Env) (err error) {
l.Caller, err = env.LogCaller("LOG_CALLER", params.Default("hidden"))
if err != nil {
return err
}
l.Level, err = env.LogLevel("LOG_LEVEL", params.Default("info"))
return err
}

22
internal/config/paths.go Normal file
View File

@@ -0,0 +1,22 @@
package config
import (
"path/filepath"
"github.com/qdm12/golibs/params"
)
type Paths struct {
DataDir string
JSON string // obtained from DataDir
}
func (p *Paths) get(env params.Env) (err error) {
p.DataDir, err = env.Path("DATADIR", params.Default("./data"))
if err != nil {
return err
}
p.JSON = filepath.Join(p.DataDir, "config.json")
return nil
}

195
internal/config/pubip.go Normal file
View File

@@ -0,0 +1,195 @@
package config
import (
"errors"
"fmt"
"net/url"
"strings"
"github.com/qdm12/ddns-updater/pkg/publicip"
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
"github.com/qdm12/ddns-updater/pkg/publicip/http"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/params"
)
const all = "all"
type PubIP struct {
HTTPSettings publicip.HTTPSettings
DNSSettings publicip.DNSSettings
}
func (p *PubIP) get(env params.Env) (warnings []string, err error) {
if err = p.getFetchers(env); err != nil {
return nil, err
}
httpIPProviders, warning, err := p.getIPHTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
httpIP4Providers, warning, err := p.getIPv4HTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
httpIP6Providers, warning, err := p.getIPv6HTTPProviders(env)
warnings = appendIfNotEmpty(warnings, warning)
if err != nil {
return warnings, err
}
p.HTTPSettings.Options = []http.Option{
http.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
http.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
http.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
}
dnsIPProviders, err := p.getDNSProviders(env)
if err != nil {
return warnings, err
}
dnsTimeout, err := env.Duration("PUBLICIP_DNS_TIMEOUT", params.Default("3s"))
if err != nil {
return warnings, err
}
p.DNSSettings.Options = []dns.Option{
dns.SetTimeout(dnsTimeout),
dns.SetProviders(dnsIPProviders[0], dnsIPProviders[1:]...),
}
return warnings, nil
}
var ErrInvalidFetcher = errors.New("invalid fetcher specified")
func (p *PubIP) getFetchers(env params.Env) (err error) {
s, err := env.Get("PUBLICIP_FETCHERS", params.Default(all))
if err != nil {
return err
}
fields := strings.Split(s, ",")
for i, field := range fields {
switch strings.ToLower(field) {
case all:
p.HTTPSettings.Enabled = true
p.DNSSettings.Enabled = true
case "http":
p.HTTPSettings.Enabled = true
case "dns":
p.DNSSettings.Enabled = true
default:
err = fmt.Errorf(
"%w: %q at position %d of %d",
ErrInvalidFetcher, field, i+1, len(fields))
}
}
return err
}
// getDNSProviders obtains the DNS providers to obtain your public IPv4 and/or IPv6 address.
func (p *PubIP) getDNSProviders(env params.Env) (providers []dns.Provider, err error) {
s, err := env.Get("PUBLICIP_DNS_PROVIDERS", params.Default(all))
if err != nil {
return nil, err
}
availableProviders := dns.ListProviders()
fields := strings.Split(s, ",")
providers = make([]dns.Provider, len(fields))
for i, field := range fields {
if field == all {
return availableProviders, nil
}
providers[i] = dns.Provider(field)
if err := dns.ValidateProvider(providers[i]); err != nil {
return nil, err
}
}
return providers, nil
}
// getHTTPProviders obtains the HTTP providers to obtain your public IPv4 or IPv6 address.
func (p *PubIP) getIPHTTPProviders(env params.Env) (
providers []http.Provider, warning string, err error) {
return httpIPMethod(env, "PUBLICIP_HTTP_PROVIDERS", "IP_METHOD", ipversion.IP4or6)
}
// getIPv4HTTPProviders obtains the HTTP providers to obtain your public IPv4 address.
func (p *PubIP) getIPv4HTTPProviders(env params.Env) (
providers []http.Provider, warning string, err error) {
return httpIPMethod(env, "PUBLICIPV4_HTTP_PROVIDERS", "IPV4_METHOD", ipversion.IP4)
}
// getIPv6HTTPProviders obtains the HTTP providers to obtain your public IPv6 address.
func (p *PubIP) getIPv6HTTPProviders(env params.Env) (
providers []http.Provider, warning string, err error) {
return httpIPMethod(env, "PUBLICIPV6_HTTP_PROVIDERS", "IPV6_METHOD", ipversion.IP6)
}
var (
ErrInvalidPublicIPHTTPProvider = errors.New("invalid public IP HTTP provider")
)
func httpIPMethod(env params.Env, envKey, retroKey string, version ipversion.IPVersion) (
providers []http.Provider, warning string, err error) {
retroKeyOption := params.RetroKeys([]string{retroKey}, func(oldKey, newKey string) {
warning = "You are using an old environment variable " + oldKey +
" please change it to " + newKey
})
s, err := env.Get(envKey, params.Default("cycle"), retroKeyOption)
if err != nil {
return nil, warning, err
}
availableProviders := http.ListProvidersForVersion(version)
choices := make(map[http.Provider]struct{}, len(availableProviders))
for _, provider := range availableProviders {
choices[provider] = struct{}{}
}
fields := strings.Split(s, ",")
for _, field := range fields {
// Retro-compatibility.
switch field {
case "ipify6":
field = "ipify"
case "noip4", "noip6", "noip8245_4", "noip8245_6":
field = "noip"
case "cycle":
field = all
}
if field == all {
return availableProviders, warning, nil
}
// Custom URL check
url, err := url.Parse(field)
if err == nil && url != nil && url.Scheme == "https" {
providers = append(providers, http.CustomProvider(url))
continue
}
provider := http.Provider(field)
if _, ok := choices[provider]; !ok {
return nil, warning, fmt.Errorf("%w: %s", ErrInvalidPublicIPHTTPProvider, provider)
}
providers = append(providers, provider)
}
if len(providers) == 0 {
return nil, warning, fmt.Errorf("%w: for IP version %s", ErrInvalidPublicIPHTTPProvider, version)
}
return providers, warning, nil
}

19
internal/config/server.go Normal file
View File

@@ -0,0 +1,19 @@
package config
import (
"github.com/qdm12/golibs/params"
)
type Server struct {
Port uint16
RootURL string
}
func (s *Server) get(env params.Env) (warning string, err error) {
s.RootURL, err = env.RootURL("ROOT_URL")
if err != nil {
return "", err
}
s.Port, warning, err = env.ListeningPort("LISTENING_PORT", params.Default("8000"))
return warning, err
}

View File

@@ -0,0 +1,69 @@
package config
import (
"net/url"
"path"
"strings"
"github.com/containrrr/shoutrrr"
"github.com/containrrr/shoutrrr/pkg/types"
"github.com/qdm12/golibs/params"
)
type Shoutrrr struct {
Addresses []string
Params types.Params
}
func (s *Shoutrrr) get(env params.Env) (warnings []string, err error) {
s.Addresses, err = env.CSV("SHOUTRRR_ADDRESSES", params.CaseSensitiveValue())
if err != nil {
return warnings, err
}
// Retro-compatibility: GOTIFY_URL and GOTIFY_TOKEN
gotifyURL, err := env.URL("GOTIFY_URL")
if err != nil || gotifyURL != nil {
const warning = "You should use the environment variable SHOUTRRR_ADDRESSES instead of GOTIFY_URL and GOTIFY_TOKEN"
warnings = append(warnings, warning)
}
if err != nil {
return warnings, err
} else if gotifyURL != nil {
gotifyToken, err := env.Get("GOTIFY_TOKEN", params.CaseSensitiveValue(),
params.Compulsory(), params.Unset())
if err != nil {
return warnings, err
}
gotifyShoutrrrAddress := gotifyURLTokenToShoutrrr(gotifyURL, gotifyToken)
s.Addresses = append(s.Addresses, gotifyShoutrrrAddress)
}
if _, err = shoutrrr.CreateSender(s.Addresses...); err != nil {
return warnings, err // validation step
}
str, err := env.Get("SHOUTRRR_PARAMS", params.Default("title=DDNS Updater"), params.CaseSensitiveValue())
if err != nil {
return warnings, err
}
keyValues := strings.Split(str, ",")
s.Params = make(map[string]string, len(keyValues))
for _, keyValue := range keyValues {
fields := strings.Split(keyValue, "=")
key, value := fields[0], fields[1]
s.Params[key] = value
}
return warnings, err
}
func gotifyURLTokenToShoutrrr(url *url.URL, token string) (address string) {
hostAndPath := path.Join(url.Host, url.Path)
address = "gotify://" + hostAndPath + "/" + token
if url.Scheme == "http" {
address += "?DisableTLS=Yes"
}
return address
}

46
internal/config/update.go Normal file
View File

@@ -0,0 +1,46 @@
package config
import (
"strconv"
"time"
"github.com/qdm12/golibs/params"
)
type Update struct {
Period time.Duration
Cooldown time.Duration
}
func (u *Update) get(env params.Env) (warning string, err error) {
warning, err = u.getPeriod(env)
if err != nil {
return warning, err
}
u.Cooldown, err = env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
return warning, err
}
func (u *Update) getPeriod(env params.Env) (warning string, err error) {
// Backward compatibility: DELAY
s, err := env.Get("DELAY", params.Compulsory())
if err == nil {
warning = "the environment variable DELAY should be changed to PERIOD"
// Backward compatibility: integer only, treated as seconds
n, err := strconv.Atoi(s)
if err == nil {
u.Period = time.Duration(n) * time.Second
return warning, nil
}
period, err := time.ParseDuration(s)
if err == nil {
u.Period = period
return warning, nil
}
}
u.Period, err = env.Duration("PERIOD", params.Default("10m"))
return "", err
}

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

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

View File

@@ -1,9 +0,0 @@
package constants
import "github.com/qdm12/ddns-updater/internal/models"
const (
IPv4 models.IPVersion = "ipv4"
IPv6 models.IPVersion = "ipv6"
IPv4OrIPv6 models.IPVersion = "ipv4 or ipv6"
)

View File

@@ -1,77 +0,0 @@
package constants
import (
"github.com/qdm12/ddns-updater/internal/models"
)
func IPMethods() []models.IPMethod {
return []models.IPMethod{
{
Name: "cycle",
},
{
Name: "opendns",
URL: "https://diagnostic.opendns.com/myip",
IPv4: true,
IPv6: true,
},
{
Name: "ifconfig",
URL: "https://ifconfig.io/ip",
IPv4: true,
IPv6: true,
},
{
Name: "ipinfo",
URL: "https://ipinfo.io/ip",
IPv4: true,
IPv6: true,
},
{
Name: "ipify",
URL: "https://api.ipify.org",
IPv4: true,
},
{
Name: "ipify6",
URL: "https://api6.ipify.org",
IPv6: true,
},
{
Name: "ddnss4",
URL: "https://ip4.ddnss.de/meineip.php",
IPv4: true,
},
{
Name: "ddnss6",
URL: "https://ip6.ddnss.de/meineip.php",
IPv6: true,
},
{
Name: "google",
URL: "https://domains.google.com/checkip",
IPv4: true,
IPv6: true,
},
{
Name: "noip4",
URL: "http://ip1.dynupdate.no-ip.com",
IPv4: true,
},
{
Name: "noip6",
URL: "http://ip1.dynupdate6.no-ip.com",
IPv6: true,
},
{
Name: "noip8245_4",
URL: "http://ip1.dynupdate.no-ip.com:8245",
IPv4: true,
},
{
Name: "noip8245_6",
URL: "http://ip1.dynupdate6.no-ip.com:8245",
IPv6: true,
},
}
}

View File

@@ -1,38 +0,0 @@
package constants
import "github.com/qdm12/ddns-updater/internal/models"
// All possible provider values
const (
CLOUDFLARE models.Provider = "cloudflare"
DDNSSDE models.Provider = "ddnss"
DONDOMINIO models.Provider = "dondominio"
DNSPOD models.Provider = "dnspod"
DUCKDNS models.Provider = "duckdns"
DYN models.Provider = "dyn"
DREAMHOST models.Provider = "dreamhost"
GODADDY models.Provider = "godaddy"
GOOGLE models.Provider = "google"
HE models.Provider = "he"
INFOMANIAK models.Provider = "infomaniak"
NAMECHEAP models.Provider = "namecheap"
NOIP models.Provider = "noip"
)
func ProviderChoices() []models.Provider {
return []models.Provider{
CLOUDFLARE,
DDNSSDE,
DONDOMINIO,
DNSPOD,
DUCKDNS,
DYN,
DREAMHOST,
GODADDY,
GOOGLE,
HE,
INFOMANIAK,
NAMECHEAP,
NOIP,
}
}

View File

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

View File

@@ -10,11 +10,10 @@ import (
type Database interface {
Close() error
Insert(record records.Record) (id int)
Select(id int) (record records.Record, err error)
SelectAll() (records []records.Record)
// Using persistence database
Update(id int, record records.Record) error
// From persistence database
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
}
@@ -24,7 +23,7 @@ type database struct {
persistentDB persistence.Database
}
// NewDatabase creates a new in memory database
// NewDatabase creates a new in memory database.
func NewDatabase(data []records.Record, persistentDB persistence.Database) Database {
return &database{
data: data,

View File

@@ -6,13 +6,6 @@ import (
"github.com/qdm12/ddns-updater/internal/records"
)
func (db *database) Insert(record records.Record) (id int) {
db.Lock()
defer db.Unlock()
db.data = append(db.data, record)
return len(db.data) - 1
}
func (db *database) Select(id int) (record records.Record, err error) {
db.RLock()
defer db.RUnlock()

View File

@@ -1,37 +0,0 @@
package handlers
import (
"fmt"
"net/http"
"text/template"
"time"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/golibs/logging"
)
// MakeHandler returns a router with all the necessary routes configured
func MakeHandler(rootURL, uiDir string, db data.Database, logger logging.Logger, forceUpdate func(), timeNow func() time.Time) http.HandlerFunc {
logger = logger.WithPrefix("http server: ")
return func(w http.ResponseWriter, r *http.Request) {
logger.Info("HTTP %s %s", r.Method, r.RequestURI)
switch {
case r.Method == http.MethodGet && r.RequestURI == rootURL+"/":
t := template.Must(template.ParseFiles(uiDir + "/index.html"))
var htmlData models.HTMLData
for _, record := range db.SelectAll() {
row := record.HTML(timeNow())
htmlData.Rows = append(htmlData.Rows, row)
}
if err := t.ExecuteTemplate(w, "index.html", htmlData); err != nil {
logger.Warn(err)
fmt.Fprint(w, "An error occurred creating this webpage")
}
case r.Method == http.MethodGet && r.RequestURI == rootURL+"/update":
logger.Info("Update started manually")
forceUpdate()
http.Redirect(w, r, rootURL, 301)
}
}
}

View File

@@ -1,4 +1,4 @@
package healthcheck
package health
import (
"fmt"
@@ -12,18 +12,19 @@ import (
type lookupIPFunc func(host string) ([]net.IP, error)
// IsHealthy checks all the records were updated successfully and returns an error if not
func IsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) (err error) {
defer func() {
if err != nil {
logger.Warn("unhealthy: %s", err)
}
}()
func MakeIsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) func() error {
return func() (err error) {
return isHealthy(db, lookupIP)
}
}
// isHealthy checks all the records were updated successfully and returns an error if not.
func isHealthy(db data.Database, lookupIP lookupIPFunc) (err error) {
records := db.SelectAll()
for _, record := range records {
if record.Status == constants.FAIL {
return fmt.Errorf("%s", record.String())
} else if !record.Settings.DNSLookup() {
} else if record.Settings.Proxied() {
continue
}
hostname := record.Settings.BuildDomainName()
@@ -45,7 +46,8 @@ func IsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) (
}
}
if !found {
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s", hostname, strings.Join(lookedUpIPsString, ","), currentIP)
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s",
hostname, strings.Join(lookedUpIPsString, ","), currentIP)
}
}
return nil

57
internal/health/client.go Normal file
View File

@@ -0,0 +1,57 @@
package health
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
func IsClientMode(args []string) bool {
return len(args) > 1 && args[1] == "healthcheck"
}
type Client interface {
Query(ctx context.Context, port uint16) error
}
type client struct {
*http.Client
}
func NewClient() Client {
const timeout = 5 * time.Second
return &client{
Client: &http.Client{Timeout: timeout},
}
}
var ErrParseHealthServerAddress = errors.New("cannot parse health server address")
// Query sends an HTTP request to the other instance of
// the program, and to its internal healthcheck server.
func (c *client) Query(ctx context.Context, port uint16) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1:"+strconv.Itoa(int(port)), nil)
if err != nil {
return err
}
resp, err := c.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return nil
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("%s: %s", resp.Status, err)
}
return fmt.Errorf(string(b))
}

View File

@@ -0,0 +1,31 @@
package health
import (
"net/http"
"github.com/qdm12/golibs/logging"
)
func newHandler(logger logging.Logger, healthcheck func() error) http.Handler {
return &handler{
logger: logger,
healthcheck: healthcheck,
}
}
type handler struct {
logger logging.Logger
healthcheck func() error
}
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet || (r.RequestURI != "" && r.RequestURI != "/") {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
if err := h.healthcheck(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}

52
internal/health/server.go Normal file
View File

@@ -0,0 +1,52 @@
package health
import (
"context"
"net/http"
"time"
"github.com/qdm12/golibs/logging"
)
type Server interface {
Run(ctx context.Context, done chan<- struct{})
}
type server struct {
address string
logger logging.Logger
handler http.Handler
}
func NewServer(address string, logger logging.Logger, healthcheck func() error) Server {
handler := newHandler(logger, healthcheck)
return &server{
address: address,
logger: logger,
handler: handler,
}
}
func (s *server) Run(ctx context.Context, done chan<- struct{}) {
defer close(done)
server := http.Server{Addr: s.address, Handler: s.handler}
go func() {
<-ctx.Done()
s.logger.Warn("shutting down (context canceled)")
defer s.logger.Warn("shut down")
const shutdownGraceDuration = 2 * time.Second
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
s.logger.Error("failed shutting down: %s", err)
}
}()
for ctx.Err() == nil {
s.logger.Info("listening on %s", s.address)
err := server.ListenAndServe()
if err != nil && ctx.Err() == nil { // server crashed
s.logger.Error(err)
s.logger.Info("restarting")
}
}
}

View File

@@ -1,12 +1,10 @@
package models
type (
// Provider is a possible DNS provider
// Provider is a possible DNS provider.
Provider string
// Status is the record config status
// Status is the record config status.
Status string
// HTML is for constants HTML strings
// HTML is for constants HTML strings.
HTML string
// IPVersion is ipv4 or ipv6
IPVersion string
)

7
internal/models/build.go Normal file
View File

@@ -0,0 +1,7 @@
package models
type BuildInformation struct {
Version string `json:"version"`
Commit string `json:"commit"`
BuildDate string `json:"buildDate"`
}

View File

@@ -8,7 +8,7 @@ import (
)
// History contains current and previous IP address for a particular record
// with the latest success time
// with the latest success time.
type History []HistoryEvent // current and previous ips
type HistoryEvent struct { // current and previous ips
@@ -23,13 +23,14 @@ func (h History) GetPreviousIPs() []net.IP {
return nil
}
IPs := make([]net.IP, len(h)-1)
for i := len(h) - 2; i >= 0; i-- {
const two = 2
for i := len(h) - two; i >= 0; i-- {
IPs[i] = h[i].IP
}
return IPs
}
// GetCurrentIP returns the current IP address (latest in history)
// GetCurrentIP returns the current IP address (latest in history).
func (h History) GetCurrentIP() net.IP {
if len(h) < 1 {
return nil
@@ -37,7 +38,7 @@ func (h History) GetCurrentIP() net.IP {
return h[len(h)-1].IP
}
// GetSuccessTime returns the latest success update time
// GetSuccessTime returns the latest success update time.
func (h History) GetSuccessTime() time.Time {
if len(h) < 1 {
return time.Time{}
@@ -50,6 +51,7 @@ func (h History) GetDurationSinceSuccess(now time.Time) string {
return "N/A"
}
duration := now.Sub(h[len(h)-1].Time)
const hoursInDay = 24
switch {
case duration < time.Minute:
return fmt.Sprintf("%ds", int(duration.Round(time.Second).Seconds()))
@@ -58,7 +60,7 @@ func (h History) GetDurationSinceSuccess(now time.Time) string {
case duration < 24*time.Hour:
return fmt.Sprintf("%dh", int(duration.Round(time.Hour).Hours()))
default:
return fmt.Sprintf("%dd", int(duration.Round(time.Hour*24).Hours()/24))
return fmt.Sprintf("%dd", int(duration.Round(time.Hour*hoursInDay).Hours()/hoursInDay))
}
}

View File

@@ -1,6 +1,6 @@
package models
// IPMethod is a method to obtain your public IP address
// IPMethod is a method to obtain your public IP address.
type IPMethod struct {
Name string
URL string

View File

@@ -1,109 +0,0 @@
package network
import (
"bytes"
"fmt"
"net"
"net/http"
"sort"
"strings"
"github.com/qdm12/ddns-updater/internal/constants"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
)
// GetPublicIP downloads a webpage and extracts the IP address from it
func GetPublicIP(client network.Client, url string, ipVersion models.IPVersion) (ip net.IP, err error) {
content, status, err := client.GetContent(url)
if err != nil {
return nil, fmt.Errorf("cannot get public %s address: %w", ipVersion, err)
} else if status != http.StatusOK {
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d", ipVersion, url, status)
}
s := string(content)
switch ipVersion {
case constants.IPv4:
return searchIP(constants.IPv4, s)
case constants.IPv6:
return searchIP(constants.IPv6, s)
case constants.IPv4OrIPv6:
var ipv4Err, ipv6Err error
ip, ipv4Err = searchIP(constants.IPv4, s)
if ipv4Err != nil {
ip, ipv6Err = searchIP(constants.IPv6, s)
}
if ipv6Err != nil {
return nil, fmt.Errorf("%s, %s", ipv4Err, ipv6Err)
}
return ip, nil
default:
return nil, fmt.Errorf("ip version %q not supported", ipVersion)
}
}
func searchIP(version models.IPVersion, s string) (ip net.IP, err error) {
verifier := verification.NewVerifier()
var regexSearch func(s string) []string
switch version {
case constants.IPv4:
regexSearch = verifier.SearchIPv4
case constants.IPv6:
regexSearch = verifier.SearchIPv6
default:
return nil, fmt.Errorf("ip version %q is not supported for regex search", version)
}
ips := regexSearch(s)
if ips == nil {
return nil, fmt.Errorf("no public %s address found", version)
}
uniqueIPs := make(map[string]struct{})
for _, ipString := range ips {
uniqueIPs[ipString] = struct{}{}
}
netIPs := []net.IP{}
for ipString := range uniqueIPs {
netIP := net.ParseIP(ipString)
if netIP == nil || netIPIsPrivate(netIP) {
// in case the regex is not restrictive enough
// or the IP address is private
continue
}
netIPs = append(netIPs, netIP)
}
switch len(netIPs) {
case 0:
return nil, fmt.Errorf("no public %s address found", version)
case 1:
return netIPs[0], nil
default:
sort.Slice(netIPs, func(i, j int) bool {
return bytes.Compare(netIPs[i], netIPs[j]) < 0
})
ips = make([]string, len(netIPs))
for i := range netIPs {
ips[i] = netIPs[i].String()
}
return nil, fmt.Errorf("multiple public %s addresses found: %s", version, strings.Join(ips, " "))
}
}
func netIPIsPrivate(netIP net.IP) bool {
for _, privateCIDRBlock := range [8]string{
"127.0.0.1/8", // localhost
"10.0.0.0/8", // 24-bit block
"172.16.0.0/12", // 20-bit block
"192.168.0.0/16", // 16-bit block
"169.254.0.0/16", // link local address
"::1/128", // localhost IPv6
"fc00::/7", // unique local address IPv6
"fe80::/10", // link local address IPv6
} {
_, CIDR, _ := net.ParseCIDR(privateCIDRBlock)
if CIDR.Contains(netIP) {
return true
}
}
return false
}

View File

@@ -1,155 +0,0 @@
package network
import (
"fmt"
"net"
"net/http"
"testing"
"github.com/golang/mock/gomock"
"github.com/qdm12/ddns-updater/internal/constants"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/golibs/network/mock_network"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_GetPublicIP(t *testing.T) {
t.Parallel()
tests := map[string]struct {
IPVersion models.IPVersion
mockContent []byte
mockStatus int
mockErr error
ip net.IP
err error
}{
"network error": {
IPVersion: constants.IPv4,
mockErr: fmt.Errorf("error"),
err: fmt.Errorf("cannot get public ipv4 address: error"),
},
"bad status": {
IPVersion: constants.IPv4,
mockStatus: http.StatusUnauthorized,
err: fmt.Errorf("cannot get public ipv4 address from https://getmyip.com: HTTP status code 401"),
},
"ipv4 address": {
IPVersion: constants.IPv4,
mockContent: []byte("55.55.55.55"),
mockStatus: http.StatusOK,
ip: net.IP{55, 55, 55, 55},
},
"ipv6 address": {
IPVersion: constants.IPv6,
mockContent: []byte("ad07:e846:51ac:6cd0:0000:0000:0000:0000"),
mockStatus: http.StatusOK,
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
},
"ipv4 or ipv6 found ipv4": {
IPVersion: constants.IPv4OrIPv6,
mockContent: []byte("55.55.55.55"),
mockStatus: http.StatusOK,
ip: net.IP{55, 55, 55, 55},
},
"ipv4 or ipv6 found ipv6": {
IPVersion: constants.IPv4OrIPv6,
mockContent: []byte("ad07:e846:51ac:6cd0:0000:0000:0000:0000"),
mockStatus: http.StatusOK,
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
},
"ipv4 or ipv6 not found": {
IPVersion: constants.IPv4OrIPv6,
mockContent: []byte("abc"),
mockStatus: http.StatusOK,
err: fmt.Errorf("no public ipv4 address found, no public ipv6 address found"),
},
"unsupported ip version": {
IPVersion: models.IPVersion("x"),
mockStatus: http.StatusOK,
err: fmt.Errorf("ip version \"x\" not supported"),
},
}
const URL = "https://getmyip.com"
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
client := mock_network.NewMockClient(mockCtrl)
client.EXPECT().GetContent(URL).Return(tc.mockContent, tc.mockStatus, tc.mockErr).Times(1)
ip, err := GetPublicIP(client, URL, tc.IPVersion)
if tc.err != nil {
require.Error(t, err)
assert.Equal(t, tc.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.True(t, tc.ip.Equal(ip))
})
}
}
func Test_searchIP(t *testing.T) {
t.Parallel()
tests := map[string]struct {
IPVersion models.IPVersion
s string
ip net.IP
err error
}{
"unsupported ip version": {
IPVersion: constants.IPv4OrIPv6,
err: fmt.Errorf("ip version \"ipv4 or ipv6\" is not supported for regex search"),
},
"no content": {
IPVersion: constants.IPv4,
err: fmt.Errorf("no public ipv4 address found"),
},
"single ipv4 address": {
IPVersion: constants.IPv4,
s: "abcd 55.55.55.55 abcd",
ip: net.IP{55, 55, 55, 55},
},
"single ipv6 address": {
IPVersion: constants.IPv6,
s: "abcd bd07:e846:51ac:6cd0:0000:0000:0000:0000 abcd",
ip: net.IP{0xbd, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
},
"single private ipv4 address": {
IPVersion: constants.IPv4,
s: "abcd 10.0.0.3 abcd",
err: fmt.Errorf("no public ipv4 address found"),
},
"single private ipv6 address": {
IPVersion: constants.IPv6,
s: "abcd ::1 abcd",
err: fmt.Errorf("no public ipv6 address found"),
},
"2 ipv4 addresses": {
IPVersion: constants.IPv4,
s: "55.55.55.55 56.56.56.56",
err: fmt.Errorf("multiple public ipv4 addresses found: 55.55.55.55 56.56.56.56"),
},
"2 ipv6 addresses": {
IPVersion: constants.IPv6,
s: "bd07:e846:51ac:6cd0:0000:0000:0000:0000 ad07:e846:51ac:6cd0:0000:0000:0000:0000",
err: fmt.Errorf("multiple public ipv6 addresses found: ad07:e846:51ac:6cd0:: bd07:e846:51ac:6cd0::"), //nolint:golint
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
ip, err := searchIP(tc.IPVersion, tc.s)
if tc.err != nil {
require.Error(t, err)
assert.Equal(t, tc.err.Error(), err.Error())
} else {
assert.NoError(t, err)
}
assert.True(t, tc.ip.Equal(ip))
})
}
}

View File

@@ -1,21 +0,0 @@
package network
import (
"bytes"
"encoding/json"
"net/http"
)
// BuildHTTPPut is used for GoDaddy and Cloudflare only
func BuildHTTPPut(url string, body interface{}) (request *http.Request, err error) {
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
request, err = http.NewRequest(http.MethodPut, url, bytes.NewBuffer(b))
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/json")
return request, nil
}

View File

@@ -2,58 +2,69 @@ package params
import (
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"strings"
"github.com/qdm12/ddns-updater/internal/constants"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/regex"
"github.com/qdm12/ddns-updater/internal/settings"
"github.com/qdm12/ddns-updater/internal/settings/constants"
"github.com/qdm12/ddns-updater/internal/settings/log"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/params"
)
// nolint: maligned
type commonSettings struct {
Provider string `json:"provider"`
Domain string `json:"domain"`
Host string `json:"host"`
IPVersion string `json:"ip_version"`
NoDNSLookup bool `json:"no_dns_lookup"`
Provider string `json:"provider"`
Domain string `json:"domain"`
Host string `json:"host"`
IPVersion string `json:"ip_version"`
// Retro values for warnings
IPMethod *string `json:"ip_method,omitempty"`
Delay *uint64 `json:"delay,omitempty"`
}
// GetSettings obtain the update settings from the JSON content, first trying from the environment variable CONFIG
// and then from the file config.json
func (r *reader) GetSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
allSettings, warnings, err = r.getSettingsFromEnv()
// JSONSettings obtain the update settings from the JSON content, first trying from the environment variable CONFIG
// and then from the file config.json.
func (r *reader) JSONSettings(filePath string, logger log.Logger) (
allSettings []settings.Settings, warnings []string, err error) {
allSettings, warnings, err = r.getSettingsFromEnv(logger)
if allSettings != nil || warnings != nil || err != nil {
return allSettings, warnings, err
}
return r.getSettingsFromFile(filePath)
return r.getSettingsFromFile(filePath, logger)
}
// getSettingsFromFile obtain the update settings from config.json
func (r *reader) getSettingsFromFile(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
// getSettingsFromFile obtain the update settings from config.json.
func (r *reader) getSettingsFromFile(filePath string, logger log.Logger) (
allSettings []settings.Settings, warnings []string, err error) {
bytes, err := r.readFile(filePath)
if err != nil {
return nil, nil, err
if !errors.Is(err, os.ErrNotExist) {
return nil, nil, err
}
const mode = fs.FileMode(0600)
return nil, nil, r.writeFile(filePath, []byte(`{}`), mode)
}
return extractAllSettings(bytes)
return extractAllSettings(bytes, logger)
}
// getSettingsFromEnv obtain the update settings from the environment variable CONFIG
func (r *reader) getSettingsFromEnv() (allSettings []settings.Settings, warnings []string, err error) {
s, err := r.envParams.GetEnv("CONFIG")
// getSettingsFromEnv obtain the update settings from the environment variable CONFIG.
func (r *reader) getSettingsFromEnv(logger log.Logger) (allSettings []settings.Settings, warnings []string, err error) {
s, err := r.env.Get("CONFIG", params.CaseSensitiveValue())
if err != nil {
return nil, nil, err
} else if len(s) == 0 {
} else if s == "" {
return nil, nil, nil
}
return extractAllSettings([]byte(s))
return extractAllSettings([]byte(s), logger)
}
func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warnings []string, err error) {
func extractAllSettings(jsonBytes []byte, logger log.Logger) (
allSettings []settings.Settings, warnings []string, err error) {
config := struct {
CommonSettings []commonSettings `json:"settings"`
}{}
@@ -66,83 +77,52 @@ func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warn
if err := json.Unmarshal(jsonBytes, &rawConfig); err != nil {
return nil, nil, err
}
matcher, err := regex.NewMatcher()
if err != nil {
return nil, nil, err
}
matcher := regex.NewMatcher()
for i, common := range config.CommonSettings {
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i], matcher)
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i], matcher, logger)
warnings = append(warnings, newWarnings...)
if err != nil {
return nil, warnings, err
}
allSettings = append(allSettings, newSettings...)
}
if len(allSettings) == 0 {
warnings = append(warnings, "no settings found in JSON data")
}
return allSettings, warnings, nil
}
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage, matcher regex.Matcher) (settingsSlice []settings.Settings, warnings []string, err error) {
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage,
matcher regex.Matcher, logger log.Logger) (
settingsSlice []settings.Settings, warnings []string, err error) {
provider := models.Provider(common.Provider)
if provider == constants.DUCKDNS { // only hosts, no domain
if provider == constants.DuckDNS { // only hosts, no domain
if len(common.Domain) > 0 { // retro compatibility
if len(common.Host) == 0 {
common.Host = strings.TrimSuffix(common.Domain, ".duckdns.org")
warnings = append(warnings, fmt.Sprintf("DuckDNS record should have %q specified as host instead of %q as domain", common.Host, common.Domain))
warnings = append(warnings,
fmt.Sprintf("DuckDNS record should have %q specified as host instead of %q as domain",
common.Host, common.Domain))
} else {
warnings = append(warnings, fmt.Sprintf("ignoring domain %q because host %q is specified for DuckDNS record", common.Domain, common.Host))
warnings = append(warnings,
fmt.Sprintf("ignoring domain %q because host %q is specified for DuckDNS record",
common.Domain, common.Host))
}
}
}
hosts := strings.Split(common.Host, ",")
for _, host := range hosts {
if len(host) == 0 {
return nil, warnings, fmt.Errorf("host cannot be empty")
}
if len(common.IPVersion) == 0 {
common.IPVersion = ipversion.IP4or6.String()
}
ipVersion := models.IPVersion(common.IPVersion)
if len(ipVersion) == 0 {
ipVersion = constants.IPv4OrIPv6 // default
}
if ipVersion != constants.IPv4OrIPv6 && ipVersion != constants.IPv4 && ipVersion != constants.IPv6 {
return nil, warnings, fmt.Errorf("ip version %q is not valid", ipVersion)
}
var settingsConstructor settings.Constructor
switch provider {
case constants.CLOUDFLARE:
settingsConstructor = settings.NewCloudflare
case constants.DDNSSDE:
settingsConstructor = settings.NewDdnss
case constants.DONDOMINIO:
settingsConstructor = settings.NewDonDominio
case constants.DNSPOD:
settingsConstructor = settings.NewDNSPod
case constants.DREAMHOST:
settingsConstructor = settings.NewDreamhost
case constants.DUCKDNS:
settingsConstructor = settings.NewDuckdns
case constants.GODADDY:
settingsConstructor = settings.NewGodaddy
case constants.GOOGLE:
settingsConstructor = settings.NewGoogle
case constants.HE:
settingsConstructor = settings.NewHe
case constants.INFOMANIAK:
settingsConstructor = settings.NewInfomaniak
case constants.NAMECHEAP:
settingsConstructor = settings.NewNamecheap
case constants.NOIP:
settingsConstructor = settings.NewNoip
case constants.DYN:
settingsConstructor = settings.NewDyn
default:
return nil, warnings, fmt.Errorf("provider %q is not supported", provider)
ipVersion, err := ipversion.Parse(common.IPVersion)
if err != nil {
return nil, nil, err
}
settingsSlice = make([]settings.Settings, len(hosts))
for i, host := range hosts {
settingsSlice[i], err = settingsConstructor(rawSettings, common.Domain, host, ipVersion, common.NoDNSLookup, matcher)
settingsSlice[i], err = settings.New(provider, rawSettings, common.Domain,
host, ipVersion, matcher, logger)
if err != nil {
return nil, warnings, err
}

View File

@@ -1,203 +1,30 @@
package params
import (
"fmt"
"io/fs"
"io/ioutil"
"net/url"
"time"
"os"
"github.com/qdm12/ddns-updater/internal/constants"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/settings"
"github.com/qdm12/ddns-updater/internal/settings/log"
"github.com/qdm12/golibs/logging"
"github.com/qdm12/golibs/params"
libparams "github.com/qdm12/golibs/params"
"github.com/qdm12/golibs/verification"
)
const https = "https"
type Reader interface {
// JSON
GetSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error)
// Core
GetPeriod() (period time.Duration, warnings []string, err error)
GetIPMethod() (method models.IPMethod, err error)
GetIPv4Method() (method models.IPMethod, err error)
GetIPv6Method() (method models.IPMethod, err error)
GetHTTPTimeout() (duration time.Duration, err error)
// File paths
GetExeDir() (dir string, err error)
GetDataDir(currentDir string) (string, error)
// Web UI
GetListeningPort() (listeningPort, warning string, err error)
GetRootURL() (rootURL string, err error)
// Backup
GetBackupPeriod() (duration time.Duration, err error)
GetBackupDirectory() (directory string, err error)
// Other
GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error)
GetGotifyURL() (URL *url.URL, err error)
GetGotifyToken() (token string, err error)
// Version getters
GetVersion() string
GetBuildDate() string
GetVcsRef() string
JSONSettings(filePath string, logger log.Logger) (allSettings []settings.Settings, warnings []string, err error)
}
type reader struct {
envParams libparams.EnvParams
verifier verification.Verifier
env params.Env
readFile func(filename string) ([]byte, error)
writeFile func(filename string, data []byte, perm fs.FileMode) (err error)
}
func NewReader(logger logging.Logger) Reader {
return &reader{
envParams: libparams.NewEnvParams(),
verifier: verification.NewVerifier(),
env: params.NewEnv(),
readFile: ioutil.ReadFile,
writeFile: os.WriteFile,
}
}
// GetDataDir obtains the data directory from the environment
// variable DATADIR
func (r *reader) GetDataDir(currentDir string) (string, error) {
return r.envParams.GetEnv("DATADIR", libparams.Default(currentDir+"/data"))
}
func (r *reader) GetListeningPort() (listeningPort, warning string, err error) {
return r.envParams.GetListeningPort()
}
func (r *reader) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
return r.envParams.GetLoggerConfig()
}
func (r *reader) GetGotifyURL() (url *url.URL, err error) {
return r.envParams.GetGotifyURL()
}
func (r *reader) GetGotifyToken() (token string, err error) {
return r.envParams.GetGotifyToken()
}
func (r *reader) GetRootURL() (rootURL string, err error) {
return r.envParams.GetRootURL()
}
func (r *reader) GetPeriod() (period time.Duration, warnings []string, err error) {
// Backward compatibility
n, err := r.envParams.GetEnvInt("DELAY", libparams.Compulsory())
if err == nil { // integer only, treated as seconds
return time.Duration(n) * time.Second,
[]string{
"the environment variable DELAY should be changed to PERIOD",
fmt.Sprintf("the value for the duration period of the updater does not have a time unit, you might want to set it to \"%ds\" instead of \"%d\"", n, n),
}, nil
}
period, err = r.envParams.GetDuration("DELAY", libparams.Compulsory())
if err == nil {
return period,
[]string{
"the environment variable DELAY should be changed to PERIOD",
}, nil
}
period, err = r.envParams.GetDuration("PERIOD", libparams.Default("10m"))
return period, nil, err
}
func (r *reader) GetIPMethod() (method models.IPMethod, err error) {
s, err := r.envParams.GetEnv("IP_METHOD", params.Default("cycle"))
if err != nil {
return method, err
}
for _, choice := range constants.IPMethods() {
if choice.Name == s {
return choice, nil
}
}
url, err := url.Parse(s)
if err != nil || url == nil || url.Scheme != https {
return method, fmt.Errorf("ip method %q is not valid", s)
}
return models.IPMethod{
Name: s,
URL: s,
IPv4: true,
IPv6: true,
}, nil
}
func (r *reader) GetIPv4Method() (method models.IPMethod, err error) {
s, err := r.envParams.GetEnv("IPV4_METHOD", params.Default("cycle"))
if err != nil {
return method, err
}
for _, choice := range constants.IPMethods() {
if choice.Name == s {
if s != "cycle" && !choice.IPv4 {
return method, fmt.Errorf("ip method %s does not support IPv4", s)
}
return choice, nil
}
}
url, err := url.Parse(s)
if err != nil || url == nil || url.Scheme != https {
return method, fmt.Errorf("ipv4 method %q is not valid", s)
}
return models.IPMethod{
Name: s,
URL: s,
IPv4: true,
}, nil
}
func (r *reader) GetIPv6Method() (method models.IPMethod, err error) {
s, err := r.envParams.GetEnv("IPV6_METHOD", params.Default("cycle"))
if err != nil {
return method, err
}
for _, choice := range constants.IPMethods() {
if choice.Name == s {
if s != "cycle" && !choice.IPv6 {
return method, fmt.Errorf("ip method %s does not support IPv6", s)
}
return choice, nil
}
}
url, err := url.Parse(s)
if err != nil || url == nil || url.Scheme != https {
return method, fmt.Errorf("ipv6 method %q is not valid", s)
}
return models.IPMethod{
Name: s,
URL: s,
IPv6: true,
}, nil
}
func (r *reader) GetExeDir() (dir string, err error) {
return r.envParams.GetExeDir()
}
func (r *reader) GetHTTPTimeout() (duration time.Duration, err error) {
return r.envParams.GetHTTPTimeout(libparams.Default("10s"))
}
func (r *reader) GetBackupPeriod() (duration time.Duration, err error) {
s, err := r.envParams.GetEnv("BACKUP_PERIOD", libparams.Default("0"))
if err != nil {
return 0, err
}
return time.ParseDuration(s)
}
func (r *reader) GetBackupDirectory() (directory string, err error) {
return r.envParams.GetEnv("BACKUP_DIRECTORY", libparams.Default("./data"))
}

View File

@@ -1,20 +0,0 @@
package params
import (
libparams "github.com/qdm12/golibs/params"
)
func (r *reader) GetVersion() string {
version, _ := r.envParams.GetEnv("VERSION", libparams.Default("?"), libparams.CaseSensitiveValue())
return version
}
func (r *reader) GetBuildDate() string {
buildDate, _ := r.envParams.GetEnv("BUILD_DATE", libparams.Default("?"), libparams.CaseSensitiveValue())
return buildDate
}
func (r *reader) GetVcsRef() string {
buildDate, _ := r.envParams.GetEnv("VCS_REF", libparams.Default("?"), libparams.CaseSensitiveValue())
return buildDate
}

View File

@@ -51,7 +51,7 @@ func NewDatabase(dataDir string) (*Database, error) {
return nil, err
}
if err := db.Check(); err != nil {
return nil, err
return nil, fmt.Errorf("%s validation error: %w", db.filepath, err)
}
return &db, nil
}
@@ -74,7 +74,7 @@ func (db *Database) Check() error {
case event.IP == nil:
return fmt.Errorf("IP %d of %d is empty for record %s", i+1, len(record.Events), record)
case event.Time.IsZero():
return fmt.Errorf("Time of IP %d of %d is empty for record %s", i+1, len(record.Events), record)
return fmt.Errorf("time of IP %d of %d is empty for record %s", i+1, len(record.Events), record)
}
}
}

View File

@@ -32,7 +32,7 @@ func (db *Database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err
}
// GetEvents gets all the IP addresses history for a certain domain and host, in the order
// from oldest to newest
// from oldest to newest.
func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
db.RLock()
defer db.RUnlock()
@@ -44,7 +44,7 @@ func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent
return nil, nil
}
// GetAllDomainsHosts gets all the domains and hosts from the database
// GetAllDomainsHosts gets all the domains and hosts from the database.
func (db *Database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
db.RLock()
defer db.RUnlock()

View File

@@ -9,16 +9,17 @@ import (
"github.com/qdm12/ddns-updater/internal/settings"
)
// Record contains all the information to update and display a DNS record
// Record contains all the information to update and display a DNS record.
type Record struct { // internal
Settings settings.Settings // fixed
History models.History // past information
Status models.Status
Message string
Time time.Time
LastBan *time.Time // nil means no last ban
}
// New returns a new Record with settings and some history
// New returns a new Record with settings and some history.
func New(settings settings.Settings, events []models.HistoryEvent) Record {
return Record{
Settings: settings,
@@ -32,5 +33,6 @@ func (r *Record) String() string {
if len(r.Message) > 0 {
status += " (" + r.Message + ")"
}
return fmt.Sprintf("%s: %s %s; %s", r.Settings.String(), status, r.Time.Format("2006-01-02 15:04:05 MST"), r.History.String())
return fmt.Sprintf("%s: %s %s; %s",
r.Settings, status, r.Time.Format("2006-01-02 15:04:05 MST"), r.History)
}

View File

@@ -3,55 +3,50 @@ package regex
import "regexp"
type Matcher interface {
GandiKey(s string) bool
GodaddyKey(s string) bool
GodaddySecret(s string) bool
DuckDNSToken(s string) bool
NamecheapPassword(s string) bool
DreamhostKey(s string) bool
CloudflareKey(s string) bool
CloudflareUserServiceKey(s string) bool
DNSOMaticUsername(s string) bool
DNSOMaticPassword(s string) bool
}
type matcher struct {
goDaddyKey, goDaddySecret, duckDNSToken, namecheapPassword, dreamhostKey, cloudflareKey, cloudflareUserServiceKey *regexp.Regexp
goDaddyKey, duckDNSToken, namecheapPassword, dreamhostKey, cloudflareKey,
cloudflareUserServiceKey, dnsOMaticUsername, dnsOMaticPassword, gandiKey *regexp.Regexp
}
//nolint:gocritic
func NewMatcher() (m Matcher, err error) {
matcher := &matcher{}
matcher.goDaddyKey, err = regexp.Compile(`^[A-Za-z0-9]{10,14}\_[A-Za-z0-9]{22}$`)
if err != nil {
return nil, err
var (
gandiKey = regexp.MustCompile(`^[A-Za-z0-9]{24}$`)
goDaddyKey = regexp.MustCompile(`^[A-Za-z0-9]{8,14}\_[A-Za-z0-9]{21,22}$`)
duckDNSToken = regexp.MustCompile(`^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$`)
namecheapPassword = regexp.MustCompile(`^[a-f0-9]{32}$`)
dreamhostKey = regexp.MustCompile(`^[a-zA-Z0-9]{16}$`)
cloudflareKey = regexp.MustCompile(`^[a-zA-Z0-9]+$`)
cloudflareUserServiceKey = regexp.MustCompile(`^v1\.0.+$`)
dnsOMaticUsername = regexp.MustCompile(`^[a-zA-Z0-9._-]{3,25}$`)
dnsOMaticPassword = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{5,19}$`)
)
func NewMatcher() Matcher {
return &matcher{
gandiKey: gandiKey,
goDaddyKey: goDaddyKey,
duckDNSToken: duckDNSToken,
namecheapPassword: namecheapPassword,
dreamhostKey: dreamhostKey,
cloudflareKey: cloudflareKey,
cloudflareUserServiceKey: cloudflareUserServiceKey,
dnsOMaticUsername: dnsOMaticUsername,
dnsOMaticPassword: dnsOMaticPassword,
}
matcher.goDaddySecret, err = regexp.Compile(`^[A-Za-z0-9]{22}$`)
if err != nil {
return nil, err
}
matcher.duckDNSToken, err = regexp.Compile(`^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$`)
if err != nil {
return nil, err
}
matcher.namecheapPassword, err = regexp.Compile(`^[a-f0-9]{32}$`)
if err != nil {
return nil, err
}
matcher.dreamhostKey, err = regexp.Compile(`^[a-zA-Z0-9]{16}$`)
if err != nil {
return nil, err
}
matcher.cloudflareKey, err = regexp.Compile(`^[a-zA-Z0-9]+$`)
if err != nil {
return nil, err
}
matcher.cloudflareUserServiceKey, err = regexp.Compile(`^v1\.0.+$`)
if err != nil {
return nil, err
}
return matcher, nil
}
func (m *matcher) GandiKey(s string) bool { return m.gandiKey.MatchString(s) }
func (m *matcher) GodaddyKey(s string) bool { return m.goDaddyKey.MatchString(s) }
func (m *matcher) GodaddySecret(s string) bool { return m.goDaddySecret.MatchString(s) }
func (m *matcher) DuckDNSToken(s string) bool { return m.duckDNSToken.MatchString(s) }
func (m *matcher) NamecheapPassword(s string) bool { return m.namecheapPassword.MatchString(s) }
func (m *matcher) DreamhostKey(s string) bool { return m.dreamhostKey.MatchString(s) }
@@ -59,3 +54,5 @@ func (m *matcher) CloudflareKey(s string) bool { return m.cloudflareKey.Matc
func (m *matcher) CloudflareUserServiceKey(s string) bool {
return m.cloudflareUserServiceKey.MatchString(s)
}
func (m *matcher) DNSOMaticUsername(s string) bool { return m.dnsOMaticUsername.MatchString(s) }
func (m *matcher) DNSOMaticPassword(s string) bool { return m.dnsOMaticPassword.MatchString(s) }

35
internal/server/error.go Normal file
View File

@@ -0,0 +1,35 @@
package server
import (
"encoding/json"
"net/http"
)
type errJSONWrapper struct {
Error string `json:"error"`
}
func httpError(w http.ResponseWriter, status int, errString string) {
w.WriteHeader(status)
if errString == "" {
errString = http.StatusText(status)
}
body := errJSONWrapper{Error: errString}
_ = json.NewEncoder(w).Encode(body)
}
type errorsJSONWrapper struct {
Errors []string `json:"errors"`
}
func httpErrors(w http.ResponseWriter, status int, errors []error) {
w.WriteHeader(status)
errs := make([]string, len(errors))
for i := range errors {
errs[i] = errors[i].Error()
}
body := errorsJSONWrapper{Errors: errs}
_ = json.NewEncoder(w).Encode(body)
}

View File

@@ -0,0 +1,51 @@
package server
import (
"context"
"embed"
"net/http"
"text/template"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/update"
)
type handlers struct {
ctx context.Context
// Objects
db data.Database
runner update.Runner
indexTemplate *template.Template
// Mockable functions
timeNow func() time.Time
}
//go:embed ui/*
var uiFS embed.FS //nolint:gochecknoglobals
func newHandler(ctx context.Context, rootURL string,
db data.Database, runner update.Runner) http.Handler {
indexTemplate := template.Must(template.ParseFS(uiFS, "ui/index.html"))
handlers := &handlers{
ctx: ctx,
db: db,
indexTemplate: indexTemplate,
// TODO build information
timeNow: time.Now,
runner: runner,
}
router := chi.NewRouter()
router.Use(middleware.Logger)
router.Get(rootURL+"/", handlers.index)
router.Get(rootURL+"/update", handlers.update)
return router
}

18
internal/server/index.go Normal file
View File

@@ -0,0 +1,18 @@
package server
import (
"net/http"
"github.com/qdm12/ddns-updater/internal/models"
)
func (h *handlers) index(w http.ResponseWriter, r *http.Request) {
var htmlData models.HTMLData
for _, record := range h.db.SelectAll() {
row := record.HTML(h.timeNow())
htmlData.Rows = append(htmlData.Rows, row)
}
if err := h.indexTemplate.ExecuteTemplate(w, "index.html", htmlData); err != nil {
httpError(w, http.StatusInternalServerError, "failed generating webpage: "+err.Error())
}
}

55
internal/server/server.go Normal file
View File

@@ -0,0 +1,55 @@
package server
import (
"context"
"net/http"
"time"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/update"
"github.com/qdm12/golibs/logging"
)
type Server interface {
Run(ctx context.Context, done chan<- struct{})
}
type server struct {
address string
logger logging.Logger
handler http.Handler
}
func New(ctx context.Context, address, rootURL string, db data.Database, logger logging.Logger,
runner update.Runner) Server {
handler := newHandler(ctx, rootURL, db, runner)
return &server{
address: address,
logger: logger,
handler: handler,
}
}
func (s *server) Run(ctx context.Context, done chan<- struct{}) {
defer close(done)
server := http.Server{Addr: s.address, Handler: s.handler}
go func() {
<-ctx.Done()
s.logger.Warn("shutting down (context canceled)")
defer s.logger.Warn("shut down")
const shutdownGraceDuration = 2 * time.Second
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGraceDuration)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
s.logger.Error("failed shutting down: %s", err)
}
}()
for ctx.Err() == nil {
s.logger.Info("listening on %s", s.address)
err := server.ListenAndServe()
if err != nil && ctx.Err() == nil { // server crashed
s.logger.Error(err)
s.logger.Info("restarting")
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1 @@
<svg id="レイヤー_1" data-name="レイヤー 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><defs><style>.cls-1{fill:#6edbd1;}.cls-1,.cls-10,.cls-3,.cls-4,.cls-5{stroke:#000;}.cls-1,.cls-10,.cls-3,.cls-4,.cls-5,.cls-7{stroke-linecap:round;}.cls-1,.cls-10,.cls-5,.cls-7{stroke-width:3px;}.cls-1,.cls-10,.cls-2,.cls-3,.cls-4,.cls-5,.cls-6,.cls-7{fill-rule:evenodd;}.cls-3,.cls-4,.cls-5,.cls-6{fill:#fff;}.cls-3{stroke-width:2.91px;}.cls-4{stroke-width:2.82px;}.cls-10,.cls-7{fill:#f6d2a2;}.cls-7{stroke:#231f20;}.cls-8{fill:#febd15;}.cls-9{font-size:130.48px;font-family:Arial-BoldMT, Arial;font-weight:700;}</style></defs><title>ddnsgopher.icon</title><path class="cls-1" d="M27.31,93.21C-21.42,79.51,14.81,17.51,54,43Z"/><path class="cls-1" d="M243.83,38.36c38.57-27.09,73.43,34,28.6,49.42Z"/><path class="cls-2" d="M29.34,76.09c-6.43-3.4-11.14-8-7.21-15.46C25.77,53.75,32.53,54.5,39,57.9Z"/><path class="cls-2" d="M262.27,69.62c6.43-3.4,11.15-8,7.21-15.45-3.64-6.89-10.4-6.14-16.83-2.74Z"/><path class="cls-1" d="M286.18,300.15c.34-23.18-2.14-47.14-3.42-69-2.1-35.74.3-72-3.43-107.23-.21-2,4.06-5.13,3.81-7.14-.22-1.71-4.95-2.29-5.2-4A197.91,197.91,0,0,0,265.7,66.46c-18.2-35.8-49.18-48.33-84.54-52.25-3.11-.34-3.88-4.71-7-4.93-2.08-.15-6.55,3.82-8.65,3.72-6-.28-12-.39-18.12-.39-24.14,1.91-47.5,5.51-67.46,14.08-3.81,1.63-9.76-1.51-13.31.51-3.15,1.8-3.94,8.72-6.86,10.86-14,10.27-25.13,24.63-31.93,44.86-2.76,8.23,1.52,21-5.94,24.76-12,6.15-1.55,12.56-1.87,18.86-1.9,38,6.3,76.48,4.75,114.88-.66,16.19-3.48,40.24-4.54,58.73"/><path class="cls-3" d="M156,78.1c8.76,50.92,92,37.46,80-13.89C225.27,18.15,153,30.9,156,78.1"/><path class="cls-4" d="M56.49,86.34c11.35,44.33,82.33,33,79.65-11.49-3.2-53.25-90.5-43-79.65,11.49"/><ellipse cx="150.96" cy="159.38" rx="25.39" ry="32.86"/><path class="cls-5" d="M166,139.21c0,6.62,1.5,14,.25,21.09-1.69,3.2-5,3.53-7.87,4.83a11.59,11.59,0,0,1-8.86-6.92c-1-8,.38-15.82.64-23.86Z"/><ellipse cx="98.47" cy="77.62" rx="11.99" ry="12.98"/><path class="cls-6" d="M104.62,77.34c0,3.36-2.33,6.08-5.21,6.08s-5.2-2.72-5.2-6.08,2.33-6.08,5.2-6.08S104.62,74,104.62,77.34Z"/><ellipse cx="198.89" cy="74.38" rx="11.79" ry="12.98"/><path class="cls-6" d="M206.22,74.21c0,4.15-2.83,7.51-6.32,7.51s-6.32-3.36-6.32-7.51,2.83-7.5,6.32-7.5S206.22,70.07,206.22,74.21Z"/><path class="cls-5" d="M131.75,138.49c-5.23,12.67,2.91,38,17.1,19.32-1-8,.38-15.82.63-23.86Z"/><path class="cls-7" d="M133.54,114.08c-9.75.83-17.72,12.42-12.65,21.59,6.71,12.14,21.69-1.08,31,.16,10.73.22,19.53,11.35,28.16,2,9.59-10.39-4.13-20.5-14.86-25Z"/><path class="cls-2" d="M132.31,113.57c-.72-16.88,31.47-19,35.27-4.86s-33.69,17.38-35.27,4.86Z"/><polygon id="lighting_bolt" data-name="lighting bolt" class="cls-8" points="10.65 292.5 171.49 238.88 150.71 214.26 295.68 150.26 190.09 219.74 219.64 250.92 10.65 292.5"/><text class="cls-9" transform="translate(15.68 289.59)">DNS</text><path class="cls-10" d="M28.18,222.19s10.24,19.32.26,18.47c-3.07,12.48-15.52,1.79-15.52,1.79s-4.76-5.64-5-12.91C7.61,219.53,19.84,209.64,28.18,222.19Z"/><path class="cls-10" d="M289,218.12s-21.86-.54-16.65,8c-9.79,8.31,5.33,14.68,5.33,14.68s5.22,3.92,13.78-1.3S302.25,223.86,289,218.12Z"/></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -2,6 +2,7 @@
<head>
<title>DDNS Updater</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<style>
table {
font-family: arial, sans-serif;

18
internal/server/update.go Normal file
View File

@@ -0,0 +1,18 @@
package server
import (
"net/http"
)
func (h *handlers) update(w http.ResponseWriter, r *http.Request) {
start := h.timeNow()
errors := h.runner.ForceUpdate(h.ctx)
duration := h.timeNow().Sub(start)
if len(errors) > 0 {
httpErrors(w, http.StatusInternalServerError, errors)
return
}
w.WriteHeader(http.StatusAccepted)
message := "All records updated successfully in " + duration.String()
_, _ = w.Write([]byte(message))
}

View File

@@ -1,262 +0,0 @@
package settings
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"github.com/qdm12/ddns-updater/internal/constants"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/network"
"github.com/qdm12/ddns-updater/internal/regex"
netlib "github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
)
//nolint:maligned
type cloudflare struct {
domain string
host string
ipVersion models.IPVersion
dnsLookup bool
key string
token string
email string
userServiceKey string
zoneIdentifier string
proxied bool
ttl uint
matcher regex.Matcher
}
func NewCloudflare(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
extraSettings := struct {
Key string `json:"key"`
Token string `json:"token"`
Email string `json:"email"`
UserServiceKey string `json:"user_service_key"`
ZoneIdentifier string `json:"zone_identifier"`
Proxied bool `json:"proxied"`
TTL uint `json:"ttl"`
}{}
if err := json.Unmarshal(data, &extraSettings); err != nil {
return nil, err
}
c := &cloudflare{
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
key: extraSettings.Key,
token: extraSettings.Token,
email: extraSettings.Email,
userServiceKey: extraSettings.UserServiceKey,
zoneIdentifier: extraSettings.ZoneIdentifier,
proxied: extraSettings.Proxied,
ttl: extraSettings.TTL,
matcher: matcher,
}
if err := c.isValid(); err != nil {
return nil, err
}
return c, nil
}
func (c *cloudflare) isValid() error {
switch {
case len(c.key) > 0: // email and key must be provided
switch {
case !c.matcher.CloudflareKey(c.key):
return fmt.Errorf("invalid key format")
case !verification.NewVerifier().MatchEmail(c.email):
return fmt.Errorf("invalid email format")
}
case len(c.userServiceKey) > 0: // only user service key
if !c.matcher.CloudflareKey(c.key) {
return fmt.Errorf("invalid user service key format")
}
default: // API token only
}
switch {
case len(c.zoneIdentifier) == 0:
return fmt.Errorf("zone identifier cannot be empty")
case c.ttl == 0:
return fmt.Errorf("TTL cannot be left to 0")
}
return nil
}
func (c *cloudflare) String() string {
return toString(c.domain, c.host, constants.CLOUDFLARE, c.ipVersion)
}
func (c *cloudflare) Domain() string {
return c.domain
}
func (c *cloudflare) Host() string {
return c.host
}
func (c *cloudflare) IPVersion() models.IPVersion {
return c.ipVersion
}
func (c *cloudflare) DNSLookup() bool {
return c.dnsLookup
}
func (c *cloudflare) BuildDomainName() string {
return buildDomainName(c.host, c.domain)
}
func (c *cloudflare) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", c.BuildDomainName(), c.BuildDomainName())),
Host: models.HTML(c.Host()),
Provider: "<a href=\"https://www.cloudflare.com\">Cloudflare</a>",
IPVersion: models.HTML(c.ipVersion),
}
}
func setHeaders(r *http.Request, token, userServiceKey, email, key string) {
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
switch {
case len(token) > 0:
r.Header.Set("Authorization", "Bearer "+token)
case len(userServiceKey) > 0:
r.Header.Set("X-Auth-User-Service-Key", userServiceKey)
case len(email) > 0 && len(key) > 0:
r.Header.Set("X-Auth-Email", email)
r.Header.Set("X-Auth-Key", key)
}
}
// Obtain domain identifier
// See https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records
func (c *cloudflare) getRecordIdentifier(client netlib.Client, newIP net.IP) (identifier string, upToDate bool, err error) {
recordType := A
if newIP.To4() == nil {
recordType = AAAA
}
u := url.URL{
Scheme: "https",
Host: "api.cloudflare.com",
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records", c.zoneIdentifier),
}
values := url.Values{}
values.Set("type", recordType)
values.Set("name", c.BuildDomainName())
values.Set("page", "1")
values.Set("per_page", "1")
u.RawQuery = values.Encode()
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return "", false, err
}
setHeaders(r, c.token, c.userServiceKey, c.email, c.key)
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return "", false, err
} else if status != http.StatusOK {
return "", false, fmt.Errorf("HTTP status %d", status)
}
listRecordsResponse := struct {
Success bool `json:"success"`
Errors []string `json:"errors"`
Result []struct {
ID string `json:"id"`
Content string `json:"content"`
} `json:"result"`
}{}
if err := json.Unmarshal(content, &listRecordsResponse); err != nil {
return "", false, err
}
switch {
case len(listRecordsResponse.Errors) > 0:
return "", false, fmt.Errorf(strings.Join(listRecordsResponse.Errors, ","))
case !listRecordsResponse.Success:
return "", false, fmt.Errorf("request to Cloudflare not successful")
case len(listRecordsResponse.Result) == 0:
return "", false, fmt.Errorf("received no result from Cloudflare")
case len(listRecordsResponse.Result) > 1:
return "", false, fmt.Errorf("received %d results instead of 1 from Cloudflare", len(listRecordsResponse.Result))
case listRecordsResponse.Result[0].Content == newIP.String(): // up to date
return "", true, nil
}
return listRecordsResponse.Result[0].ID, false, nil
}
func (c *cloudflare) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
recordType := A
if ip.To4() == nil {
recordType = AAAA
}
identifier, upToDate, err := c.getRecordIdentifier(client, ip)
if err != nil {
return nil, err
} else if upToDate {
return ip, nil
}
type cloudflarePutBody struct {
Type string `json:"type"` // A or AAAA depending on ip address given
Name string `json:"name"` // DNS record name i.e. example.com
Content string `json:"content"` // ip address
Proxied bool `json:"proxied"` // whether the record is receiving the performance and security benefits of Cloudflare
TTL uint `json:"ttl"`
}
u := url.URL{
Scheme: "https",
Host: "api.cloudflare.com",
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records/%s", c.zoneIdentifier, identifier),
}
r, err := network.BuildHTTPPut(
u.String(),
cloudflarePutBody{
Type: recordType,
Name: c.host,
Content: ip.String(),
Proxied: c.proxied,
TTL: c.ttl,
},
)
if err != nil {
return nil, err
}
setHeaders(r, c.token, c.userServiceKey, c.email, c.key)
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return nil, err
} else if status > http.StatusUnsupportedMediaType {
return nil, fmt.Errorf("HTTP status %d", status)
}
var parsedJSON struct {
Success bool `json:"success"`
Errors []struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"errors"`
Result struct {
Content string `json:"content"`
} `json:"result"`
}
if err := json.Unmarshal(content, &parsedJSON); err != nil {
return nil, err
} else if !parsedJSON.Success {
var errStr string
for _, e := range parsedJSON.Errors {
errStr += fmt.Sprintf("error %d: %s; ", e.Code, e.Message)
}
return nil, fmt.Errorf(errStr)
}
newIP = net.ParseIP(parsedJSON.Result.Content)
if newIP == nil {
return nil, fmt.Errorf("new IP %q is malformed", parsedJSON.Result.Content)
} else if !newIP.Equal(ip) {
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
}
return newIP, nil
}

View File

@@ -1,9 +0,0 @@
package settings
const (
badauth = "badauth"
success = "success"
nohost = "nohost"
A = "A"
AAAA = "AAAA"
)

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