mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-24 10:02:20 -04:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe294f52a9 | ||
|
|
62e700c82c | ||
|
|
5d0e8548a1 | ||
|
|
5b52255601 | ||
|
|
04c55028a1 | ||
|
|
e07e8da31c | ||
|
|
af2f3a3257 | ||
|
|
00efca4af4 | ||
|
|
3272612db2 | ||
|
|
5b7968c468 | ||
|
|
7ec39c1256 | ||
|
|
96857f3bae | ||
|
|
57c7d1be2d | ||
|
|
53b6f533a8 | ||
|
|
a82ed93169 | ||
|
|
d07fcc664b | ||
|
|
d013ceb869 | ||
|
|
d3506e9792 | ||
|
|
c0249672bf | ||
|
|
cfeb95872a | ||
|
|
091cf5f855 | ||
|
|
7001add533 | ||
|
|
9ccdbbd2d3 | ||
|
|
f8a3ab63c6 | ||
|
|
14033223d9 | ||
|
|
18161a6064 | ||
|
|
216b8ab1ae | ||
|
|
4af23a756b | ||
|
|
96c84a5a4f | ||
|
|
4564d16c06 | ||
|
|
6e4a56b3cf | ||
|
|
919ab65985 | ||
|
|
e023aae909 | ||
|
|
066bcdd3bf | ||
|
|
0a6c6b9bc7 | ||
|
|
8cdff8e4d3 | ||
|
|
bffc30264f | ||
|
|
4f141c20a0 | ||
|
|
582ce626c8 | ||
|
|
13b29aeba4 | ||
|
|
a5afca15d1 | ||
|
|
25ee692242 | ||
|
|
922146efd3 | ||
|
|
db9959cf59 | ||
|
|
50303aef7b | ||
|
|
137e372102 | ||
|
|
f300c59411 | ||
|
|
c23998bd09 | ||
|
|
af68f9ba0f | ||
|
|
f7171e4b01 | ||
|
|
0c028f70e9 | ||
|
|
c194681856 | ||
|
|
9c31616b46 | ||
|
|
55668d0310 | ||
|
|
3bdb8ba5ac | ||
|
|
345cc754ff | ||
|
|
9e05c6164d | ||
|
|
ea79ca53ea | ||
|
|
6a3c280f30 | ||
|
|
01e982a4cd | ||
|
|
99d33bbcf9 | ||
|
|
e38351e5a4 |
@@ -12,7 +12,7 @@
|
||||
"workspaceFolder": "/workspace",
|
||||
"appPort": 8000,
|
||||
"extensions": [
|
||||
"ms-vscode.go",
|
||||
"golang.go",
|
||||
"IBM.output-colorizer",
|
||||
"eamodio.gitlens",
|
||||
"mhutchie.git-graph",
|
||||
@@ -23,7 +23,6 @@
|
||||
"mohsen1.prettify-json",
|
||||
"quicktype.quicktype",
|
||||
"spikespaz.vscode-smoothtype",
|
||||
"stkb.rewrap",
|
||||
"vscode-icons-team.vscode-icons"
|
||||
],
|
||||
"settings": {
|
||||
@@ -43,10 +42,58 @@
|
||||
"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": "package",
|
||||
"go.lintOnSave": "package",
|
||||
"go.vetOnSave": "package",
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
.devcontainer
|
||||
.git
|
||||
*.exe
|
||||
.github
|
||||
.vscode
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
*.md
|
||||
readme
|
||||
.gitignore
|
||||
.devcontainer
|
||||
.vscode
|
||||
config.json
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
README.md
|
||||
|
||||
63
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
63
.github/ISSUE_TEMPLATE/bug.md
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Report a bug
|
||||
title: 'Bug: ...'
|
||||
labels: ":bug: bug"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent?
|
||||
|
||||
- [ ] 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**
|
||||
|
||||
`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
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file (**remove your credentials!**):
|
||||
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature to add to this project
|
||||
title: 'Feature request: ...'
|
||||
labels: ":bulb: feature request"
|
||||
assignees: qdm12
|
||||
|
||||
---
|
||||
|
||||
1. What's the feature?
|
||||
|
||||
2. Why do you need this feature?
|
||||
|
||||
3. Extra information?
|
||||
63
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
63
.github/ISSUE_TEMPLATE/help.md
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: Help
|
||||
about: Ask for help
|
||||
title: 'Help: ...'
|
||||
labels: ":pray: help wanted"
|
||||
assignees:
|
||||
|
||||
---
|
||||
|
||||
**TLDR**: *Describe your issue in a one liner here*
|
||||
|
||||
1. Is this urgent?
|
||||
|
||||
- [ ] 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**
|
||||
|
||||
`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
|
||||
|
||||
Logs:
|
||||
|
||||
```log
|
||||
|
||||
```
|
||||
|
||||
Configuration file:
|
||||
|
||||
```yml
|
||||
|
||||
```
|
||||
|
||||
Host OS:
|
||||
19
.github/workflows/build.yml
vendored
19
.github/workflows/build.yml
vendored
@@ -2,6 +2,25 @@ name: Docker build
|
||||
on:
|
||||
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
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
47
.github/workflows/buildx-branch.yml
vendored
Normal file
47
.github/workflows/buildx-branch.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
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
|
||||
18
.github/workflows/buildx-latest.yml
vendored
18
.github/workflows/buildx-latest.yml
vendored
@@ -3,18 +3,24 @@ 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/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .github/CODEOWNERS
|
||||
- .github/CONTRIBUTING.md
|
||||
- .github/FUNDING.yml
|
||||
- .github/labels.yml
|
||||
- .vscode
|
||||
- readme
|
||||
- .gitignore
|
||||
- config.json
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -22,8 +28,6 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
|
||||
20
.github/workflows/buildx-release.yml
vendored
20
.github/workflows/buildx-release.yml
vendored
@@ -3,27 +3,31 @@ 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/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .github/CODEOWNERS
|
||||
- .github/CONTRIBUTING.md
|
||||
- .github/FUNDING.yml
|
||||
- .github/labels.yml
|
||||
- .vscode
|
||||
- readme
|
||||
- .gitignore
|
||||
- config.json
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- id: buildx
|
||||
- name: Buildx setup
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
with:
|
||||
version: latest
|
||||
- name: Dockerhub login
|
||||
run: echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login -u qmcgaw --password-stdin 2>&1
|
||||
- name: Run Buildx
|
||||
|
||||
11
.github/workflows/greetings.yml
vendored
11
.github/workflows/greetings.yml
vendored
@@ -1,11 +0,0 @@
|
||||
name: Greetings
|
||||
on: [pull_request, issues]
|
||||
jobs:
|
||||
greeting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: 'Thanks for creating your first issue :+1: Feel free to use [Slack](https://join.slack.com/t/qdm12/shared_invite/enQtODMwMDQyMTAxMjY1LTU1YjE1MTVhNTBmNTViNzJiZmQwZWRmMDhhZjEyNjVhZGM4YmIxOTMxOTYzN2U0N2U2YjQ2MDk3YmYxN2NiNTc) if you just need some quick help or want to chat'
|
||||
pr-message: 'Thank you so much for contributing, that means a lot to me :wink:'
|
||||
16
.github/workflows/misspell.yml
vendored
16
.github/workflows/misspell.yml
vendored
@@ -1,16 +0,0 @@
|
||||
name: Misspells
|
||||
on:
|
||||
pull_request:
|
||||
branches: [master]
|
||||
push:
|
||||
branches: [master]
|
||||
jobs:
|
||||
misspell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: reviewdog/action-misspell@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
locale: "US"
|
||||
level: error
|
||||
59
.github/workflows/security.yml
vendored
59
.github/workflows/security.yml
vendored
@@ -1,59 +0,0 @@
|
||||
name: Security scan of Docker image
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
pull_request:
|
||||
branches: [master]
|
||||
paths-ignore:
|
||||
- .github/workflows/buildx-release.yml
|
||||
- .github/workflows/dockerhub-description.yml
|
||||
- .github/workflows/greetings.yml
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/misspell.yml
|
||||
- .github/workflows/security.yml
|
||||
- .dockerignore
|
||||
- .gitignore
|
||||
- docker-compose.yml
|
||||
- LICENSE
|
||||
- README.md
|
||||
- title.svg
|
||||
schedule:
|
||||
- cron: '0 9 * * *'
|
||||
jobs:
|
||||
security-analysis:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Check for scratch
|
||||
id: scratchCheck
|
||||
run: echo ::set-output name=scratch::$(cat Dockerfile | grep 'FROM scratch')
|
||||
- name: Build image
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
run: docker build -t image .
|
||||
- name: Phonito
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: phonito/phonito-scanner-action@master
|
||||
with:
|
||||
image: image
|
||||
fail-level: LOW
|
||||
phonito-token: ${{ secrets.PHONITO_TOKEN }}
|
||||
- name: Trivy
|
||||
if: steps.scratchCheck.outputs.scratch == ''
|
||||
uses: homoluctus/gitrivy@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
image: image
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
*.exe
|
||||
updater
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
46
.golangci.yml
Normal file
46
.golangci.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
linters-settings:
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- goimports
|
||||
- golint
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- maligned
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- .devcontainer
|
||||
- .github
|
||||
59
Dockerfile
59
Dockerfile
@@ -1,20 +1,31 @@
|
||||
ARG ALPINE_VERSION=3.11
|
||||
ARG GO_VERSION=1.13
|
||||
ARG ALPINE_VERSION=3.12
|
||||
ARG GO_VERSION=1.15
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates tzdata
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
RUN apk --update add git g++
|
||||
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}
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY .golangci.yml .
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download 2>&1
|
||||
RUN go mod download
|
||||
COPY internal/ ./internal/
|
||||
COPY cmd/updater/main.go .
|
||||
RUN CGO_ENABLED=0 go test ./...
|
||||
RUN CGO_ENABLED=1 go build -a -installsuffix cgo -ldflags="-s -w" -o app
|
||||
RUN go test ./...
|
||||
RUN go build -trimpath -ldflags="-s -w" -o app
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
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 \
|
||||
@@ -24,26 +35,36 @@ LABEL \
|
||||
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.title="ddns-updater" \
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Namecheap, Cloudflare, GoDaddy, DuckDns, Dreamhost, DNSPod and NoIP"
|
||||
RUN apk add --update sqlite ca-certificates && \
|
||||
mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
# Creating empty database file in case nothing is mounted
|
||||
mkdir -p /updater/data && \
|
||||
chown -R 1000 /updater && \
|
||||
chmod 700 /updater/data
|
||||
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 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
|
||||
USER 1000
|
||||
ENTRYPOINT ["/updater/app"]
|
||||
ENV DELAY=10m \
|
||||
ROOT_URL=/ \
|
||||
ENV \
|
||||
# Core
|
||||
CONFIG= \
|
||||
PERIOD=5m \
|
||||
IP_METHOD=cycle \
|
||||
IPV4_METHOD=cycle \
|
||||
IPV6_METHOD=cycle \
|
||||
HTTP_TIMEOUT=10s \
|
||||
|
||||
# Web UI
|
||||
LISTENING_PORT=8000 \
|
||||
ROOT_URL=/ \
|
||||
|
||||
# Backup
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data \
|
||||
|
||||
# Other
|
||||
LOG_ENCODING=console \
|
||||
LOG_LEVEL=info \
|
||||
NODE_ID=0 \
|
||||
HTTP_TIMEOUT=10s \
|
||||
NODE_ID=-1 \
|
||||
GOTIFY_URL= \
|
||||
GOTIFY_TOKEN=
|
||||
GOTIFY_TOKEN= \
|
||||
TZ=
|
||||
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
|
||||
COPY --chown=1000 ui/* /updater/ui/
|
||||
|
||||
387
README.md
387
README.md
@@ -1,12 +1,10 @@
|
||||
# Lightweight universal DDNS Updater with Docker and web UI
|
||||
|
||||
*Light container updating DNS A records periodically for GoDaddy, Namecheap, Cloudflare, Dreamhost, NoIP, DNSPod, Infomaniak, ddnss.de and DuckDNS*
|
||||
|
||||
**SQLite migration support will be removed on 1 April 2020, so be sure to update your image before that**
|
||||
*Light container updating DNS A records periodically for Cloudflare, DDNSS.de, DonDominio, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP*
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
|
||||
[](https://travis-ci.org/qdm12/ddns-updater)
|
||||
[](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://microbadger.com/images/qmcgaw/ddns-updater)
|
||||
@@ -19,21 +17,66 @@
|
||||
|
||||
## Features
|
||||
|
||||
- Updates periodically A records for different DNS providers: Namecheap, GoDaddy, Cloudflare, NoIP, Dreamhost, DuckDNS, DNSPod and Infomaniak (ask for more)
|
||||
- Updates periodically A records for different DNS providers: Cloudflare, 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)
|
||||
- Web User interface
|
||||
|
||||

|
||||
|
||||
- Lightweight based on a Go binary and *Alpine 3.11* with Sqlite and Ca-Certificates packages
|
||||
- 14MB Docker image based on a Go static binary in a Scratch Docker image with ca-certificates and timezone data
|
||||
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
|
||||
- Docker healthcheck verifying the DNS resolution of your domains
|
||||
- Highly configurable
|
||||
- Sends notifications to your Android phone, see the [**Gotify**](#Gotify) section (it's free, open source and self hosted 🆒)
|
||||
- Compatible with `amd64`, `386`, `arm64` and `arm32v7` (Raspberry Pis) CPU architectures.
|
||||
- Compatible with `amd64`, `386`, `arm64`, `arm32v7` (Raspberry Pis) CPU architectures.
|
||||
|
||||
## Setup
|
||||
|
||||
1. To setup your domains initially, see the [Domain set up](#domain-set-up) section.
|
||||
The program reads the configuration from a JSON 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
|
||||
|
||||
1. Create a directory of your choice, say *data* with a file named **config.json** inside:
|
||||
|
||||
```sh
|
||||
@@ -49,167 +92,199 @@
|
||||
|
||||
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
|
||||
|
||||
1. Modify the *data/config.json* file similarly to:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "duckduckgo",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
See more information in the [configuration section](#configuration)
|
||||
|
||||
1. Place your JSON configuration in `data/config.json`
|
||||
1. Use the following command:
|
||||
|
||||
```bash
|
||||
```sh
|
||||
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
|
||||
```
|
||||
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
### Next steps
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
|
||||
|
||||
```sh
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
|
||||
## Configuration
|
||||
|
||||
Start by having the following content in *config.json*:
|
||||
Start by having the following content in *config.json*, or in your `CONFIG` environment variable:
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
},
|
||||
{
|
||||
"provider": "",
|
||||
"domain": "",
|
||||
"ip_method": "",
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The following parameters are to be added in *config.json*
|
||||
|
||||
For all record update configuration, you need the following:
|
||||
|
||||
- `"provider"` is the DNS provider and can be `"godaddy"`, `"namecheap"`, `"duckdns"`, `"dreamhost"`, `"cloudflare"`, `"noip"`, `"dnspod"` or `"ddnss"`
|
||||
- `"domain"`
|
||||
- `"ip_method"` is the method to obtain your public IP address and can be:
|
||||
- `"provider"` means the public IP is automatically determined by the DNS provider (**only for DuckDNs, Namecheap, Infomaniak and NoIP**), most reliable.
|
||||
- `"opendns"` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip) (reliable)
|
||||
- `"ifconfig"` using [https://ifconfig.io/ip](https://ifconfig.io/ip) (may be rate limited)
|
||||
- `"ipinfo"` using [https://ipinfo.io/ip](https://ipinfo.io/ip) (may be rate limited)
|
||||
- `"ipify"` using [https://api.ipify.org](https://api.ipify.org) (may be rate limited)
|
||||
- `"ipify6"` using [https://api6.ipify.org](https://api.ipify.org) for IPv6 only (may be rate limited)
|
||||
- `"ddnss"` using [https://ddnss.de/meineip.php](https://ddnss.de/meineip.php)
|
||||
- `"ddnss4"` using [https://ip4.ddnss.de/meineip.php](https://ip4.ddnss.de/meineip.php) for IPv4 only
|
||||
- `"ddnss6"` using [https://ip6.ddnss.de/meineip.php](https://ip6.ddnss.de/meineip.php) for IPv6 only
|
||||
- `"cycle"` to cycle between each external methods, in order to avoid being rate limited
|
||||
- You can also specify an HTTPS URL to obtain your public IP address (i.e. `"ip_method": "https://ipinfo.io/ip"`)
|
||||
The following parameters are to be added:
|
||||
|
||||
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:
|
||||
|
||||
- `"delay"` is the delay in seconds between each update. It defaults to the `DELAY` environment variable value.
|
||||
- `"no_dns_lookup"` can be `true` or `false` and allows, if `true`, to prevent the periodic Docker healthcheck from running a DNS lookup on your domain.
|
||||
- `"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.
|
||||
|
||||
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"`
|
||||
- `"identifier"`
|
||||
- `"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 key `"key"`
|
||||
- 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"`
|
||||
- `"host"` is your host and can be a subdomain or `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
- `"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 `"@"`
|
||||
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records)
|
||||
- `"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",`.
|
||||
|
||||
### Environment variables
|
||||
|
||||
| Environment variable | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `DELAY` | `10m` | Default delay between updates, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `ROOT_URL` | `/` | URL path to append to all paths to the webUI (i.e. `/ddns` for accessing `https://example.com/ddns` through a proxy) |
|
||||
| `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) |
|
||||
| `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) |
|
||||
| `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` | `0` | Node ID (for distributed systems), can be any integer |
|
||||
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
|
||||
| `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 |
|
||||
| `TZ` | | Timezone to have accurate times, i.e. `America/Montreal` |
|
||||
|
||||
#### IP methods
|
||||
|
||||
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.
|
||||
|
||||
- IPv4 or IPv6 (for most cases)
|
||||
- `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)
|
||||
- `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`)
|
||||
|
||||
### Host firewall
|
||||
|
||||
@@ -222,81 +297,7 @@ If you have a host firewall in place, this container needs the following ports:
|
||||
|
||||
## Domain set up
|
||||
|
||||
### Namecheap
|
||||
|
||||
[](https://www.namecheap.com)
|
||||
|
||||
1. Create a Namecheap account and buy a domain name - *example.com* as an example
|
||||
1. Login to Namecheap at [https://www.namecheap.com/myaccount/login.aspx](https://www.namecheap.com/myaccount/login.aspx)
|
||||
|
||||
For **each domain name** you want to add, replace *example.com* in the following link with your domain name and go to [https://ap.www.namecheap.com/Domains/DomainControlPanel/**example.com**/advancedns](https://ap.www.namecheap.com/Domains/DomainControlPanel/example.com/advancedns)
|
||||
|
||||
1. For each host you want to add (if you don't know, create one record with the host set to `*`):
|
||||
1. In the *HOST RECORDS* section, click on *ADD NEW RECORD*
|
||||
|
||||

|
||||
|
||||
1. Select the following settings and create the *A + Dynamic DNS Record*:
|
||||
|
||||

|
||||
|
||||
1. Scroll down and turn on the switch for *DYNAMIC DNS*
|
||||
|
||||

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

|
||||
|
||||
***
|
||||
|
||||
### GoDaddy
|
||||
|
||||
[](https://godaddy.com)
|
||||
|
||||
1. Login to [https://developer.godaddy.com/keys](https://developer.godaddy.com/keys/) with your account credentials.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a Test key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
1. Generate a **Production** key and secret.
|
||||
|
||||
[](https://developer.godaddy.com/keys)
|
||||
|
||||
Obtain the **key** and **secret** of that production key.
|
||||
|
||||
In this example, the key is `dLP4WKz5PdkS_GuUDNigHcLQFpw4CWNwAQ5` and the secret is `GuUFdVFj8nJ1M79RtdwmkZ`.
|
||||
|
||||
***
|
||||
|
||||
### DuckDNS
|
||||
|
||||
[](https://duckdns.org)
|
||||
|
||||
*See [duckdns website](https://duckdns.org)*
|
||||
|
||||
### Cloudflare
|
||||
|
||||
1. Make sure you have `curl` installed
|
||||
1. Obtain your API key from Cloudflare website ([see this](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-API-key-))
|
||||
1. Obtain your zone identifier for your domain name, from the domain's overview page written as *Zone ID*
|
||||
1. Find your **identifier** in the `id` field with
|
||||
|
||||
```sh
|
||||
ZONEID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
EMAIL=example@example.com
|
||||
APIKEY=aaaaaaaaaaaaaaaaaa
|
||||
curl -X GET "https://api.cloudflare.com/client/v4/zones/$ZONEID/dns_records" \
|
||||
-H "X-Auth-Email: $EMAIL" \
|
||||
-H "X-Auth-Key: $APIKEY"
|
||||
```
|
||||
|
||||
You can now fill in the necessary parameters in *config.json*
|
||||
|
||||
Special thanks to @Starttoaster for helping out with the [documentation](https://gist.github.com/Starttoaster/07d568c2a99ad7631dd776688c988326) and testing.
|
||||
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).
|
||||
|
||||
## Gotify
|
||||
|
||||
@@ -319,68 +320,34 @@ To set it up with DDNS updater:
|
||||
## Testing
|
||||
|
||||
- The automated healthcheck verifies all your records are up to date [using DNS lookups](https://github.com/qdm12/ddns-updater/blob/master/internal/healthcheck/healthcheck.go#L15)
|
||||
- You can check manually at:
|
||||
- GoDaddy: [https://dcc.godaddy.com/manage/yourdomain.com/dns](https://dcc.godaddy.com/manage/yourdomain.com/dns) (replace yourdomain.com)
|
||||
- You can also manually check, by:
|
||||
1. Going to your DNS management webpage
|
||||
1. Setting your record to `127.0.0.1`
|
||||
1. Run the container
|
||||
1. Refresh the DNS management webpage and verify the update happened
|
||||
|
||||
[](https://dcc.godaddy.com/manage/)
|
||||
Better testing instructions are written in the [Wiki for GoDaddy](https://github.com/qdm12/ddns-updater/wiki/GoDaddy#testing)
|
||||
|
||||
You might want to try to change the IP address to `127.0.0.1` to see if the update actually occurs.
|
||||
## Development and contributing
|
||||
|
||||
## Development
|
||||
- Contribute with code: see [the Wiki](https://github.com/qdm12/ddns-updater/wiki/Contributing)
|
||||
- [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)
|
||||
|
||||
1. Setup your environment
|
||||
## License
|
||||
|
||||
<details><summary>Using VSCode and Docker (easier)</summary><p>
|
||||
|
||||
1. Install [Docker](https://docs.docker.com/install/)
|
||||
- On Windows, share a drive with Docker Desktop and have the project on that partition
|
||||
- On OSX, share your project directory with Docker Desktop
|
||||
1. With [Visual Studio Code](https://code.visualstudio.com/download), install the [remote containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
||||
1. In Visual Studio Code, press on `F1` and select `Remote-Containers: Open Folder in Container...`
|
||||
1. Your dev environment is ready to go!... and it's running in a container :+1: So you can discard it and update it easily!
|
||||
|
||||
</p></details>
|
||||
|
||||
<details><summary>Locally</summary><p>
|
||||
|
||||
1. Install [Go](https://golang.org/dl/), [Docker](https://www.docker.com/products/docker-desktop) and [Git](https://git-scm.com/downloads)
|
||||
1. Install Go dependencies with
|
||||
|
||||
```sh
|
||||
go mod download
|
||||
```
|
||||
|
||||
1. Install [golangci-lint](https://github.com/golangci/golangci-lint#install)
|
||||
1. You might want to use an editor such as [Visual Studio Code](https://code.visualstudio.com/download) with the [Go extension](https://code.visualstudio.com/docs/languages/go). Working settings are already in [.vscode/settings.json](https://github.com/qdm12/ddns-updater/master/.vscode/settings.json).
|
||||
|
||||
</p></details>
|
||||
|
||||
1. Commands available:
|
||||
|
||||
```sh
|
||||
# Build the binary
|
||||
go build cmd/app/main.go
|
||||
# Test the code
|
||||
go test ./...
|
||||
# Lint the code
|
||||
golangci-lint run
|
||||
# Build the Docker image
|
||||
docker build -t qmcgaw/ddns-updater .
|
||||
```
|
||||
|
||||
1. See [Contributing](https://github.com/qdm12/ddns-updater/master/.github/CONTRIBUTING.md) for more information on how to contribute to this repository.
|
||||
This repository is under an [MIT license](https://github.com/qdm12/ddns-updater/master/license)
|
||||
|
||||
## Used in external projects
|
||||
|
||||
- [Starttoaster/docker-traefik](https://github.com/Starttoaster/docker-traefik#home-networks-extra-credit-dynamic-dns)
|
||||
|
||||
## TODOs
|
||||
## Support
|
||||
|
||||
- [ ] Other types or records
|
||||
- [ ] icon.ico for webpage
|
||||
- [ ] Record events log
|
||||
- [ ] Hot reload of config.json
|
||||
- [ ] Unit tests
|
||||
- [ ] ReactJS frontend
|
||||
- [ ] Live update of website
|
||||
- [ ] Change settings
|
||||
Sponsor me on [Github](https://github.com/sponsors/qdm12) or donate to [paypal.me/qmcgaw](https://www.paypal.me/qmcgaw)
|
||||
|
||||
[](https://github.com/sponsors/qdm12)
|
||||
[](https://www.paypal.me/qmcgaw)
|
||||
|
||||
Many thanks to J. Famiglietti for supporting me financially 🥇👍
|
||||
|
||||
@@ -1,151 +1,285 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
"github.com/qdm12/golibs/files"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/server"
|
||||
"github.com/qdm12/golibs/signals"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/env"
|
||||
"github.com/qdm12/ddns-updater/internal/handlers"
|
||||
"github.com/qdm12/ddns-updater/internal/healthcheck"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/trigger"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
paramsReader := params.NewParamsReader(logger)
|
||||
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
} else {
|
||||
logger, err = logging.NewLogger(encoding, level, nodeID)
|
||||
}
|
||||
if libhealthcheck.Mode(os.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 {
|
||||
logger.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Println(splash.Splash(paramsReader))
|
||||
e := env.NewEnv(logger)
|
||||
gotifyURL, err := paramsReader.GetGotifyURL()
|
||||
e.FatalOnError(err)
|
||||
if gotifyURL != nil {
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
e.FatalOnError(err)
|
||||
e.SetGotify(admin.NewGotify(*gotifyURL, gotifyToken, &http.Client{Timeout: time.Second}))
|
||||
}
|
||||
listeningPort, warning, err := paramsReader.GetListeningPort()
|
||||
e.FatalOnError(err)
|
||||
if len(warning) > 0 {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
rootURL, err := paramsReader.GetRootURL()
|
||||
e.FatalOnError(err)
|
||||
defaultPeriod, err := paramsReader.GetDelay(libparams.Default("10m"))
|
||||
e.FatalOnError(err)
|
||||
dir, err := paramsReader.GetExeDir()
|
||||
e.FatalOnError(err)
|
||||
dataDir, err := paramsReader.GetDataDir(dir)
|
||||
e.FatalOnError(err)
|
||||
fileManager := files.NewFileManager()
|
||||
dbSQLiteExists, err := fileManager.FileExists(dataDir + "/updates.db")
|
||||
e.FatalOnError(err)
|
||||
dbJSONExists, err := fileManager.FileExists(dataDir + "/updates.json")
|
||||
e.FatalOnError(err)
|
||||
var persistentDB persistence.Database
|
||||
if dbSQLiteExists && !dbJSONExists {
|
||||
logger.Warn("Migrating from SQLite to JSON based database file")
|
||||
sqlite, err := persistence.NewSQLite(dataDir)
|
||||
e.FatalOnError(err)
|
||||
persistentDB, err = persistence.NewJSON(dataDir)
|
||||
e.FatalOnError(err)
|
||||
err = persistence.Migrate(sqlite, persistentDB, logger)
|
||||
e.FatalOnError(err)
|
||||
logger.Info("Success; you can now safely delete %s", dataDir+"/updates.db")
|
||||
} else {
|
||||
persistentDB, err = persistence.NewJSON(dataDir)
|
||||
e.FatalOnError(err)
|
||||
}
|
||||
go signals.WaitForExit(e.ShutdownFromSignal)
|
||||
settings, warnings, err := paramsReader.GetSettings(dataDir + "/config.json")
|
||||
for _, w := range warnings {
|
||||
e.Warn(w)
|
||||
}
|
||||
if err != nil {
|
||||
e.Fatal(err)
|
||||
}
|
||||
if len(settings) > 1 {
|
||||
logger.Info("Found %d settings to update records", len(settings))
|
||||
} else if len(settings) == 1 {
|
||||
logger.Info("Found single setting to update records")
|
||||
}
|
||||
for _, err := range network.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
e.Warn(err)
|
||||
}
|
||||
var records []models.Record
|
||||
idToPeriod := make(map[int]time.Duration)
|
||||
for id, setting := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", setting.Domain, setting.Host)
|
||||
events, err := persistentDB.GetEvents(setting.Domain, setting.Host)
|
||||
if err != nil {
|
||||
e.FatalOnError(err)
|
||||
}
|
||||
records = append(records, models.NewRecord(setting, events))
|
||||
idToPeriod[id] = defaultPeriod
|
||||
if setting.Delay > 0 {
|
||||
idToPeriod[id] = setting.Delay
|
||||
}
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
e.FatalOnError(err)
|
||||
client := network.NewClient(HTTPTimeout)
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
e.SetDB(db)
|
||||
updater := update.NewUpdater(db, logger, client, e.Notify)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
forceUpdate := trigger.StartUpdates(ctx, updater, idToPeriod, e.CheckError)
|
||||
forceUpdate()
|
||||
productionHandlerFunc := handlers.NewHandler(rootURL, dir, db, logger, forceUpdate, e.CheckError).GetHandlerFunc()
|
||||
healthcheckHandlerFunc := libhealthcheck.GetHandler(func() error {
|
||||
return healthcheck.IsHealthy(db, net.LookupIP, logger)
|
||||
})
|
||||
logger.Info("Web UI listening at address 0.0.0.0:%s with root URL %s", listeningPort, rootURL)
|
||||
e.Notify(1, fmt.Sprintf("Just launched\nIt has %d records to watch", len(records)))
|
||||
serverErrs := server.RunServers(
|
||||
server.Settings{Name: "production", Addr: "0.0.0.0:" + listeningPort, Handler: productionHandlerFunc},
|
||||
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
if len(serverErrs) > 0 {
|
||||
e.Fatal(serverErrs)
|
||||
}
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/backup"
|
||||
"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/models"
|
||||
"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/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
"github.com/qdm12/golibs/admin"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
"github.com/qdm12/golibs/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(_main(context.Background(), time.Now))
|
||||
// returns 1 on error
|
||||
// returns 2 on os signal
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
p, err := getParams(paramsReader, logger)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewJSON(p.dataDir)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
settings, warnings, err := paramsReader.GetSettings(p.dataDir + "/config.json")
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(2, w)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
if len(settings) > 1 {
|
||||
logger.Info("Found %d settings to update records", len(settings))
|
||||
} else if len(settings) == 1 {
|
||||
logger.Info("Found single setting to update record")
|
||||
}
|
||||
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.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
|
||||
}
|
||||
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()
|
||||
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: "0.0.0.0:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
}()
|
||||
|
||||
go backupRunLoop(ctx, p.backupPeriod, p.dir, p.backupDirectory, logger, timeNow)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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: ")
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
}
|
||||
logger.Info("each %s; writing zip files to directory %s", backupPeriod, outputDir)
|
||||
ziper := backup.NewZiper()
|
||||
timer := time.NewTimer(backupPeriod)
|
||||
for {
|
||||
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
|
||||
if err := ziper.ZipFiles(
|
||||
filepath,
|
||||
fmt.Sprintf("%s/data/updates.json", exeDir),
|
||||
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
timer.Reset(backupPeriod)
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,28 +4,23 @@
|
||||
"provider": "namecheap",
|
||||
"domain": "example.com",
|
||||
"host": "@",
|
||||
"ip_method": "provider",
|
||||
"delay": 86400,
|
||||
"password": "e5322165c1d74692bfa6d807100c0310"
|
||||
},
|
||||
{
|
||||
"provider": "duckdns",
|
||||
"domain": "example.duckdns.org",
|
||||
"ip_method": "provider",
|
||||
"token": "00000000-0000-0000-0000-000000000000"
|
||||
},
|
||||
{
|
||||
"provider": "godaddy",
|
||||
"domain": "example.org",
|
||||
"host": "subdomain",
|
||||
"ip_method": "google",
|
||||
"key": "aaaaaaaaaaaaaaaa",
|
||||
"secret": "aaaaaaaaaaaaaaaa"
|
||||
},
|
||||
{
|
||||
"provider": "dreamhost",
|
||||
"domain": "example.info",
|
||||
"ip_method": "opendns",
|
||||
"key": "aaaaaaaaaaaaaaaa"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,13 +9,25 @@ services:
|
||||
volumes:
|
||||
- ./data:/updater/data
|
||||
environment:
|
||||
- DELAY=300s
|
||||
- ROOT_URL=/
|
||||
- CONFIG=
|
||||
- PERIOD=5m
|
||||
- IP_METHOD=cycle
|
||||
- IPV4_METHOD=cycle
|
||||
- IPV6_METHOD=cycle
|
||||
- HTTP_TIMEOUT=10s
|
||||
|
||||
# Web UI
|
||||
- LISTENING_PORT=8000
|
||||
- ROOT_URL=/
|
||||
|
||||
# Backup
|
||||
- BACKUP_PERIOD=0 # 0 to disable
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
|
||||
# Other
|
||||
- LOG_ENCODING=console
|
||||
- LOG_LEVEL=info
|
||||
- NODE_ID=0
|
||||
- HTTP_TIMEOUT=10s
|
||||
- NODE_ID=-1 # -1 to disable
|
||||
- GOTIFY_URL=
|
||||
- GOTIFY_TOKEN=
|
||||
restart: always
|
||||
|
||||
10
go.mod
10
go.mod
@@ -1,11 +1,11 @@
|
||||
module github.com/qdm12/ddns-updater
|
||||
|
||||
go 1.13
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible
|
||||
github.com/mattn/go-sqlite3 v1.10.0
|
||||
github.com/qdm12/golibs v0.0.0-20200224233831-af1ada8e2052
|
||||
github.com/stretchr/testify v1.4.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
|
||||
)
|
||||
|
||||
32
go.sum
32
go.sum
@@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
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/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/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
@@ -35,6 +37,10 @@ github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi88
|
||||
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/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
@@ -49,12 +55,15 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o=
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/kyokomi/emoji v2.2.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/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
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/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
|
||||
@@ -67,8 +76,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
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-20200224233831-af1ada8e2052 h1:6KVUzd4oLyHcmmYsXdpE3HR9FUSx3FF8UBTRlJPCKLc=
|
||||
github.com/qdm12/golibs v0.0.0-20200224233831-af1ada8e2052/go.mod h1:YULaFjj6VGmhjak6f35sUWwEleHUmngN5IQ3kdvd6XE=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -77,6 +86,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
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=
|
||||
@@ -100,10 +111,15 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
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=
|
||||
@@ -117,5 +133,9 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
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.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=
|
||||
|
||||
67
internal/backup/zip.go
Normal file
67
internal/backup/zip.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Ziper interface {
|
||||
ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
}
|
||||
|
||||
type ziper struct {
|
||||
createFile func(name string) (*os.File, error)
|
||||
openFile func(name string) (*os.File, error)
|
||||
ioCopy func(dst io.Writer, src io.Reader) (written int64, err error)
|
||||
}
|
||||
|
||||
func NewZiper() Ziper {
|
||||
return &ziper{
|
||||
createFile: os.Create,
|
||||
openFile: os.Open,
|
||||
ioCopy: io.Copy,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
f, err := z.createFile(outputFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w := zip.NewWriter(f)
|
||||
defer w.Close()
|
||||
for _, filepath := range inputFilepaths {
|
||||
if err := z.addFile(w, filepath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
f, err := z.openFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Using FileInfoHeader() above only uses the basename of the file. If we want
|
||||
// to preserve the folder structure we can overwrite this with the full path.
|
||||
// header.Name = filepath
|
||||
header.Method = zip.Deflate
|
||||
ioWriter, err := w.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = z.ioCopy(ioWriter, f)
|
||||
return err
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
HTML_FAIL models.HTML = `<font color="red"><b>Failure</b></font>`
|
||||
HTML_SUCCESS models.HTML = `<font color="green"><b>Success</b></font>`
|
||||
HTML_UPTODATE models.HTML = `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
HTML_UPDATING models.HTML = `<font color="orange"><b>Updating</b></font>`
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO have a struct model containing URL, name for each provider
|
||||
HTML_NAMECHEAP models.HTML = "<a href=\"https://namecheap.com\">Namecheap</a>"
|
||||
HTML_GODADDY models.HTML = "<a href=\"https://godaddy.com\">GoDaddy</a>"
|
||||
HTML_DUCKDNS models.HTML = "<a href=\"https://duckdns.org\">DuckDNS</a>"
|
||||
HTML_DREAMHOST models.HTML = "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>"
|
||||
HTML_CLOUDFLARE models.HTML = "<a href=\"https://www.cloudflare.com\">Cloudflare</a>"
|
||||
HTML_NOIP models.HTML = "<a href=\"https://www.noip.com/\">NoIP</a>"
|
||||
HTML_DNSPOD models.HTML = "<a href=\"https://www.dnspod.cn/\">DNSPod</a>"
|
||||
HTML_INFOMANIAK models.HTML = "<a href=\"https://www.infomaniak.com/\">Infomaniak</a>"
|
||||
HTML_DDNSSDE models.HTML = "<a href=\"https://ddnss.de/\">DDNSS.de</a>"
|
||||
)
|
||||
|
||||
const (
|
||||
HTML_GOOGLE models.HTML = "<a href=\"https://google.com/search?q=ip\">Google</a>"
|
||||
HTML_OPENDNS models.HTML = "<a href=\"https://diagnostic.opendns.com/myip\">OpenDNS</a>"
|
||||
HTML_IFCONFIG models.HTML = "<a href=\"https://ifconfig.io\">ifconfig.io</a>"
|
||||
HTML_IPINFO models.HTML = "<a href=\"https://ipinfo.io\">ipinfo.io</a>"
|
||||
HTML_IPIFY models.HTML = "<a href=\"https://api.ipify.org\">api.ipify.org</a>"
|
||||
HTML_IPIFY6 models.HTML = "<a href=\"https://api6.ipify.org\">api6.ipify.org</a>"
|
||||
HTML_DDNSS models.HTML = "<a href=\"https://ddnss.de/meineip.php\">ddnss.de</a>"
|
||||
HTML_DDNSS4 models.HTML = "<a href=\"https://ip4.ddnss.de/meineip.php\">ip4.ddnss.de</a>"
|
||||
HTML_DDNSS6 models.HTML = "<a href=\"https://ip6.ddnss.de/meineip.php\">ip6.ddns.de</a>"
|
||||
HTML_CYCLE models.HTML = "Cycling"
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package constants
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
IPv4 models.IPVersion = "ipv4"
|
||||
IPv6 models.IPVersion = "ipv6"
|
||||
IPv4 models.IPVersion = "ipv4"
|
||||
IPv6 models.IPVersion = "ipv6"
|
||||
IPv4OrIPv6 models.IPVersion = "ipv4 or ipv6"
|
||||
)
|
||||
|
||||
@@ -4,50 +4,74 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
const (
|
||||
PROVIDER models.IPMethod = "provider"
|
||||
OPENDNS models.IPMethod = "opendns"
|
||||
IFCONFIG models.IPMethod = "ifconfig"
|
||||
IPINFO models.IPMethod = "ipinfo"
|
||||
IPIFY models.IPMethod = "ipify"
|
||||
IPIFY6 models.IPMethod = "ipify6"
|
||||
CYCLE models.IPMethod = "cycle"
|
||||
DDNSS models.IPMethod = "ddnss"
|
||||
DDNSS4 models.IPMethod = "ddnss4"
|
||||
DDNSS6 models.IPMethod = "ddnss6"
|
||||
// Retro compatibility only
|
||||
GOOGLE models.IPMethod = "google"
|
||||
)
|
||||
|
||||
func IPMethodMapping() map[models.IPMethod]string {
|
||||
return map[models.IPMethod]string{
|
||||
PROVIDER: string(PROVIDER),
|
||||
CYCLE: string(CYCLE),
|
||||
OPENDNS: "https://diagnostic.opendns.com/myip",
|
||||
IFCONFIG: "https://ifconfig.io/ip",
|
||||
IPINFO: "https://ipinfo.io/ip",
|
||||
IPIFY: "https://api.ipify.org",
|
||||
IPIFY6: "https://api6.ipify.org",
|
||||
DDNSS: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS4: "https://ip4.ddnss.de/meineip.php",
|
||||
DDNSS6: "https://ip6.ddnss.de/meineip.php",
|
||||
func 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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func IPMethodChoices() (choices []models.IPMethod) {
|
||||
for choice := range IPMethodMapping() {
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
return choices
|
||||
}
|
||||
|
||||
func IPMethodExternalChoices() (choices []models.IPMethod) {
|
||||
for _, choice := range IPMethodChoices() {
|
||||
switch choice {
|
||||
case PROVIDER, CYCLE:
|
||||
default:
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
}
|
||||
return choices
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_IPMethodChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "provider", "cycle", "opendns", "ifconfig", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
|
||||
func Test_IPMethodExternalChoices(t *testing.T) {
|
||||
t.Parallel()
|
||||
choices := IPMethodExternalChoices()
|
||||
assert.ElementsMatch(t, []models.IPMethod{"ipinfo", "ipify", "ipify6", "ifconfig", "opendns", "ddnss", "ddnss4", "ddnss6"}, choices)
|
||||
}
|
||||
@@ -4,27 +4,35 @@ import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
// All possible provider values
|
||||
const (
|
||||
GODADDY models.Provider = "godaddy"
|
||||
NAMECHEAP models.Provider = "namecheap"
|
||||
DUCKDNS models.Provider = "duckdns"
|
||||
DREAMHOST models.Provider = "dreamhost"
|
||||
CLOUDFLARE models.Provider = "cloudflare"
|
||||
NOIP models.Provider = "noip"
|
||||
DNSPOD models.Provider = "dnspod"
|
||||
INFOMANIAK models.Provider = "infomaniak"
|
||||
DDNSSDE models.Provider = "ddnss"
|
||||
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{
|
||||
GODADDY,
|
||||
NAMECHEAP,
|
||||
DUCKDNS,
|
||||
DREAMHOST,
|
||||
CLOUDFLARE,
|
||||
NOIP,
|
||||
DNSPOD,
|
||||
INFOMANIAK,
|
||||
DDNSSDE,
|
||||
DONDOMINIO,
|
||||
DNSPOD,
|
||||
DUCKDNS,
|
||||
DYN,
|
||||
DREAMHOST,
|
||||
GODADDY,
|
||||
GOOGLE,
|
||||
HE,
|
||||
INFOMANIAK,
|
||||
NAMECHEAP,
|
||||
NOIP,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "regexp"
|
||||
|
||||
const (
|
||||
goDaddyKey = `[A-Za-z0-9]{10,14}\_[A-Za-z0-9]{22}`
|
||||
godaddySecret = `[A-Za-z0-9]{22}`
|
||||
RegexDuckDNSToken = `[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}`
|
||||
namecheapPassword = `[a-f0-9]{32}`
|
||||
dreamhostKey = `[a-zA-Z0-9]{16}`
|
||||
cloudflareKey = `[a-zA-Z0-9]+`
|
||||
cloudflareUserServiceKey = `v1\.0.+`
|
||||
cloudflareToken = `[a-zA-Z0-9_]{40}`
|
||||
)
|
||||
|
||||
func MatchGodaddyKey(s string) bool {
|
||||
return regexp.MustCompile("^" + goDaddyKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchGodaddySecret(s string) bool {
|
||||
return regexp.MustCompile("^" + godaddySecret + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDuckDNSToken(s string) bool {
|
||||
return regexp.MustCompile("^" + RegexDuckDNSToken + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchNamecheapPassword(s string) bool {
|
||||
return regexp.MustCompile("^" + namecheapPassword + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchDreamhostKey(s string) bool {
|
||||
return regexp.MustCompile("^" + dreamhostKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareUserServiceKey(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareUserServiceKey + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchCloudflareToken(s string) bool {
|
||||
return regexp.MustCompile("^" + cloudflareToken + "$").MatchString(s)
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// Annoucement is a message annoucement
|
||||
Annoucement = "Added support for DDNSS.de"
|
||||
// AnnoucementExpiration is the expiration date of the annoucement in format yyyy-mm-dd
|
||||
AnnoucementExpiration = "2020-03-20"
|
||||
// Announcement is a message announcement
|
||||
Announcement = "Support for he.net"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd
|
||||
AnnouncementExpiration = "2020-10-15"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AnnoucementExpiration(t *testing.T) {
|
||||
func Test_AnnouncementExpiration(t *testing.T) {
|
||||
t.Parallel()
|
||||
if len(AnnoucementExpiration) == 0 {
|
||||
if len(AnnouncementExpiration) == 0 {
|
||||
return
|
||||
}
|
||||
_, err := time.Parse("2006-01-02", AnnoucementExpiration)
|
||||
_, err := time.Parse("2006-01-02", AnnouncementExpiration)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -7,4 +7,5 @@ const (
|
||||
SUCCESS models.Status = "success"
|
||||
UPTODATE models.Status = "up to date"
|
||||
UPDATING models.Status = "updating"
|
||||
UNSET models.Status = "unset"
|
||||
)
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
NamecheapURL = "https://dynamicdns.park-your-domain.com/update"
|
||||
GoDaddyURL = "https://api.godaddy.com/v1/domains"
|
||||
DuckDNSURL = "https://www.duckdns.org/update"
|
||||
DreamhostURL = "https://api.dreamhost.com"
|
||||
CloudflareURL = "https://api.cloudflare.com/client/v4"
|
||||
NoIPURL = "https://dynupdate.no-ip.com/nic/update"
|
||||
)
|
||||
@@ -5,26 +5,27 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
Close() error
|
||||
Insert(record models.Record) (id int)
|
||||
Select(id int) (record models.Record, err error)
|
||||
SelectAll() (records []models.Record)
|
||||
Update(id int, record models.Record) error
|
||||
Insert(record records.Record) (id int)
|
||||
Select(id int) (record records.Record, err error)
|
||||
SelectAll() (records []records.Record)
|
||||
Update(id int, record records.Record) error
|
||||
// From persistence database
|
||||
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
|
||||
}
|
||||
|
||||
type database struct {
|
||||
data []models.Record
|
||||
data []records.Record
|
||||
sync.RWMutex
|
||||
persistentDB persistence.Database
|
||||
}
|
||||
|
||||
// NewDatabase creates a new in memory database
|
||||
func NewDatabase(data []models.Record, persistentDB persistence.Database) Database {
|
||||
func NewDatabase(data []records.Record, persistentDB persistence.Database) Database {
|
||||
return &database{
|
||||
data: data,
|
||||
persistentDB: persistentDB,
|
||||
|
||||
@@ -3,17 +3,17 @@ package data
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) Insert(record models.Record) (id int) {
|
||||
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 models.Record, err error) {
|
||||
func (db *database) Select(id int) (record records.Record, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
if id < 0 {
|
||||
@@ -25,7 +25,7 @@ func (db *database) Select(id int) (record models.Record, err error) {
|
||||
return db.data[id], nil
|
||||
}
|
||||
|
||||
func (db *database) SelectAll() (records []models.Record) {
|
||||
func (db *database) SelectAll() (records []records.Record) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
return db.data
|
||||
|
||||
@@ -4,13 +4,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/records"
|
||||
)
|
||||
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
return db.persistentDB.GetEvents(domain, host)
|
||||
}
|
||||
|
||||
func (db *database) Update(id int, record models.Record) error {
|
||||
func (db *database) Update(id int, record records.Record) error {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
if id < 0 {
|
||||
@@ -25,8 +26,8 @@ func (db *database) Update(id int, record models.Record) error {
|
||||
// new IP address added
|
||||
if newCount > currentCount {
|
||||
if err := db.persistentDB.StoreNewIP(
|
||||
record.Settings.Domain,
|
||||
record.Settings.Host,
|
||||
record.Settings.Domain(),
|
||||
record.Settings.Host(),
|
||||
record.History.GetCurrentIP(),
|
||||
record.History.GetSuccessTime(),
|
||||
); err != nil {
|
||||
|
||||
132
internal/env/env.go
vendored
132
internal/env/env.go
vendored
@@ -1,132 +0,0 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
type Env interface {
|
||||
SetClient(client network.Client)
|
||||
SetGotify(gotify admin.Gotify)
|
||||
SetDB(db data.Database)
|
||||
Notify(priority int, messageArgs ...interface{})
|
||||
Info(messageArgs ...interface{})
|
||||
Warn(messageArgs ...interface{})
|
||||
CheckError(err error)
|
||||
FatalOnError(err error)
|
||||
ShutdownFromSignal(signal string) (exitCode int)
|
||||
Fatal(messageArgs ...interface{})
|
||||
Shutdown() (exitCode int)
|
||||
}
|
||||
|
||||
func NewEnv(logger logging.Logger) Env {
|
||||
return &env{logger: logger}
|
||||
}
|
||||
|
||||
// env contains objects necessary to the main function.
|
||||
// These are created at start and are needed to the top-level
|
||||
// working management of the program.
|
||||
type env struct {
|
||||
logger logging.Logger
|
||||
client network.Client
|
||||
gotify admin.Gotify
|
||||
db data.Database
|
||||
}
|
||||
|
||||
func (e *env) SetClient(client network.Client) {
|
||||
e.client = client
|
||||
}
|
||||
|
||||
func (e *env) SetGotify(gotify admin.Gotify) {
|
||||
e.gotify = gotify
|
||||
}
|
||||
|
||||
func (e *env) SetDB(db data.Database) {
|
||||
e.db = db
|
||||
}
|
||||
|
||||
// Notify sends a notification to the Gotify server.
|
||||
func (e *env) Notify(priority int, messageArgs ...interface{}) {
|
||||
if e.gotify == nil {
|
||||
return
|
||||
}
|
||||
if err := e.gotify.Notify("DDNS Updater", priority, messageArgs...); err != nil {
|
||||
e.logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Info logs a message and sends a notification to the Gotify server.
|
||||
func (e *env) Info(messageArgs ...interface{}) {
|
||||
e.logger.Info(messageArgs...)
|
||||
e.Notify(1, messageArgs...)
|
||||
}
|
||||
|
||||
// Warn logs a message and sends a notification to the Gotify server.
|
||||
func (e *env) Warn(messageArgs ...interface{}) {
|
||||
e.logger.Warn(messageArgs...)
|
||||
e.Notify(2, messageArgs...)
|
||||
}
|
||||
|
||||
// CheckError logs an error and sends a notification to the Gotify server
|
||||
// if the error is not nil.
|
||||
func (e *env) CheckError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
s := err.Error()
|
||||
e.logger.Error(s)
|
||||
if len(s) > 100 {
|
||||
s = s[:100] + "..." // trim down message for notification
|
||||
}
|
||||
e.Notify(3, s)
|
||||
}
|
||||
|
||||
// FatalOnError calls Fatal if the error is not nil.
|
||||
func (e *env) FatalOnError(err error) {
|
||||
if err != nil {
|
||||
e.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown cleanly exits the program by closing all connections,
|
||||
// databases and syncing the loggers.
|
||||
func (e *env) Shutdown() (exitCode int) {
|
||||
defer func() {
|
||||
if err := e.logger.Sync(); err != nil {
|
||||
exitCode = 99
|
||||
}
|
||||
}()
|
||||
if e.client != nil {
|
||||
e.client.Close()
|
||||
}
|
||||
if e.db != nil {
|
||||
if err := e.db.Close(); err != nil {
|
||||
e.logger.Error(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ShutdownFromSignal logs a warning, sends a notification to Gotify and shutdowns
|
||||
// the program cleanly when a OS level signal is received. It should be passed as a
|
||||
// callback to a function which would catch such signal.
|
||||
func (e *env) ShutdownFromSignal(signal string) (exitCode int) {
|
||||
e.logger.Warn("Program stopped with signal %q", signal)
|
||||
e.Notify(1, "Caught OS signal %q", signal)
|
||||
return e.Shutdown()
|
||||
}
|
||||
|
||||
// Fatal logs an error, sends a notification to Gotify and shutdowns the program.
|
||||
// It exits the program with an exit code of 1.
|
||||
func (e *env) Fatal(messageArgs ...interface{}) {
|
||||
e.logger.Error(messageArgs...)
|
||||
e.Notify(4, messageArgs...)
|
||||
_ = e.Shutdown()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -7,61 +7,31 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/html"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
// Handler contains a handler function
|
||||
type Handler interface {
|
||||
GetHandlerFunc() http.HandlerFunc
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
rootURL string
|
||||
UIDir string
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
forceUpdate func()
|
||||
onError func(err error)
|
||||
getTime func() time.Time
|
||||
}
|
||||
|
||||
// NewHandler returns a Handler object
|
||||
func NewHandler(rootURL, UIDir string, db data.Database, logger logging.Logger,
|
||||
forceUpdate func(), onError func(err error)) Handler {
|
||||
return &handler{
|
||||
rootURL: rootURL,
|
||||
UIDir: UIDir,
|
||||
db: db,
|
||||
logger: logger,
|
||||
forceUpdate: forceUpdate,
|
||||
onError: onError,
|
||||
getTime: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// GetHandlerFunc returns a router with all the necessary routes configured
|
||||
func (h *handler) GetHandlerFunc() http.HandlerFunc {
|
||||
// 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) {
|
||||
h.logger.Info("received HTTP request at %s", r.RequestURI)
|
||||
logger.Info("HTTP %s %s", r.Method, r.RequestURI)
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/":
|
||||
// TODO: Forms to change existing updates or add some
|
||||
t := template.Must(template.ParseFiles(h.UIDir + "/ui/index.html"))
|
||||
case r.Method == http.MethodGet && r.RequestURI == rootURL+"/":
|
||||
t := template.Must(template.ParseFiles(uiDir + "/index.html"))
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := html.ConvertRecord(record, h.getTime())
|
||||
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 {
|
||||
h.logger.Warn(err)
|
||||
logger.Warn(err)
|
||||
fmt.Fprint(w, "An error occurred creating this webpage")
|
||||
}
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/update":
|
||||
h.logger.Info("Update started manually")
|
||||
h.forceUpdate()
|
||||
http.Redirect(w, r, h.rootURL, 301)
|
||||
case r.Method == http.MethodGet && r.RequestURI == rootURL+"/update":
|
||||
logger.Info("Update started manually")
|
||||
forceUpdate()
|
||||
http.Redirect(w, r, rootURL, 301)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package healthcheck
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
@@ -13,6 +14,7 @@ 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) {
|
||||
logger.Info("Healthcheck triggered!")
|
||||
defer func() {
|
||||
if err != nil {
|
||||
logger.Warn("unhealthy: %s", err)
|
||||
@@ -22,24 +24,30 @@ func IsHealthy(db data.Database, lookupIP lookupIPFunc, logger logging.Logger) (
|
||||
for _, record := range records {
|
||||
if record.Status == constants.FAIL {
|
||||
return fmt.Errorf("%s", record.String())
|
||||
} else if record.Settings.NoDNSLookup {
|
||||
} else if !record.Settings.DNSLookup() {
|
||||
continue
|
||||
}
|
||||
lookedUpIPs, err := lookupIP(record.Settings.BuildDomainName())
|
||||
hostname := record.Settings.BuildDomainName()
|
||||
lookedUpIPs, err := lookupIP(hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP == nil {
|
||||
return fmt.Errorf("no set IP address found")
|
||||
return fmt.Errorf("no database set IP address found for %s", hostname)
|
||||
}
|
||||
for _, lookedUpIP := range lookedUpIPs {
|
||||
if !lookedUpIP.Equal(currentIP) {
|
||||
return fmt.Errorf(
|
||||
"lookup IP address of %s is %s instead of %s",
|
||||
record.Settings.BuildDomainName(), lookedUpIP, currentIP)
|
||||
found := false
|
||||
lookedUpIPsString := make([]string, len(lookedUpIPs))
|
||||
for i, lookedUpIP := range lookedUpIPs {
|
||||
lookedUpIPsString[i] = lookedUpIP.String()
|
||||
if lookedUpIP.Equal(currentIP) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("lookup IP addresses for %s are %s instead of %s", hostname, strings.Join(lookedUpIPsString, ","), currentIP)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
row := models.HTMLRow{
|
||||
Domain: convertDomain(record.Settings.BuildDomainName()),
|
||||
Host: models.HTML(record.Settings.Host),
|
||||
Provider: convertProvider(record.Settings.Provider),
|
||||
IPMethod: convertIPMethod(record.Settings.IPMethod, record.Settings.Provider),
|
||||
}
|
||||
message := record.Message
|
||||
if record.Status == constants.UPTODATE {
|
||||
message = "no IP change for " + record.History.GetDurationSinceSuccess(now)
|
||||
}
|
||||
if len(message) > 0 {
|
||||
message = fmt.Sprintf("(%s)", message)
|
||||
}
|
||||
if len(record.Status) == 0 {
|
||||
row.Status = "N/A"
|
||||
} else {
|
||||
row.Status = models.HTML(fmt.Sprintf("%s %s, %s",
|
||||
convertStatus(record.Status),
|
||||
message,
|
||||
time.Since(record.Time).Round(time.Second).String()+" ago"))
|
||||
}
|
||||
currentIP := record.History.GetCurrentIP()
|
||||
if currentIP != nil {
|
||||
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
|
||||
} else {
|
||||
row.CurrentIP = "N/A"
|
||||
}
|
||||
previousIPs := record.History.GetPreviousIPs()
|
||||
row.PreviousIPs = "N/A"
|
||||
if len(previousIPs) > 0 {
|
||||
var previousIPsStr []string
|
||||
const maxPreviousIPs = 2
|
||||
for i, previousIP := range previousIPs {
|
||||
if i == maxPreviousIPs {
|
||||
previousIPsStr = append(previousIPsStr, fmt.Sprintf("and %d more", len(previousIPs)-i))
|
||||
break
|
||||
}
|
||||
previousIPsStr = append(previousIPsStr, previousIP.String())
|
||||
}
|
||||
row.PreviousIPs = models.HTML(strings.Join(previousIPsStr, ", "))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func convertStatus(status models.Status) models.HTML {
|
||||
switch status {
|
||||
case constants.SUCCESS:
|
||||
return constants.HTML_SUCCESS
|
||||
case constants.FAIL:
|
||||
return constants.HTML_FAIL
|
||||
case constants.UPTODATE:
|
||||
return constants.HTML_UPTODATE
|
||||
case constants.UPDATING:
|
||||
return constants.HTML_UPDATING
|
||||
default:
|
||||
return "Unknown status"
|
||||
}
|
||||
}
|
||||
|
||||
func convertProvider(provider models.Provider) models.HTML {
|
||||
switch provider {
|
||||
case constants.NAMECHEAP:
|
||||
return constants.HTML_NAMECHEAP
|
||||
case constants.GODADDY:
|
||||
return constants.HTML_GODADDY
|
||||
case constants.DUCKDNS:
|
||||
return constants.HTML_DUCKDNS
|
||||
case constants.DREAMHOST:
|
||||
return constants.HTML_DREAMHOST
|
||||
case constants.CLOUDFLARE:
|
||||
return constants.HTML_CLOUDFLARE
|
||||
case constants.NOIP:
|
||||
return constants.HTML_NOIP
|
||||
case constants.DNSPOD:
|
||||
return constants.HTML_DNSPOD
|
||||
case constants.INFOMANIAK:
|
||||
return constants.HTML_INFOMANIAK
|
||||
case constants.DDNSSDE:
|
||||
return constants.HTML_DDNSSDE
|
||||
default:
|
||||
s := string(provider)
|
||||
if strings.HasPrefix("https://", s) {
|
||||
shorterName := strings.TrimPrefix(s, "https://")
|
||||
shorterName = strings.TrimSuffix(shorterName, "/")
|
||||
return models.HTML(fmt.Sprintf("<a href=\"%s\">%s</a>", s, shorterName))
|
||||
}
|
||||
return models.HTML(string(provider))
|
||||
}
|
||||
}
|
||||
|
||||
func convertIPMethod(IPMethod models.IPMethod, provider models.Provider) models.HTML {
|
||||
// TODO map to icons
|
||||
switch IPMethod {
|
||||
case constants.PROVIDER:
|
||||
return convertProvider(provider)
|
||||
case constants.OPENDNS:
|
||||
return constants.HTML_OPENDNS
|
||||
case constants.IFCONFIG:
|
||||
return constants.HTML_IFCONFIG
|
||||
case constants.IPINFO:
|
||||
return constants.HTML_IPINFO
|
||||
case constants.IPIFY:
|
||||
return constants.HTML_IPIFY
|
||||
case constants.IPIFY6:
|
||||
return constants.HTML_IPIFY6
|
||||
case constants.DDNSS:
|
||||
return constants.HTML_DDNSS
|
||||
case constants.DDNSS4:
|
||||
return constants.HTML_DDNSS4
|
||||
case constants.DDNSS6:
|
||||
return constants.HTML_DDNSS6
|
||||
case constants.CYCLE:
|
||||
return constants.HTML_CYCLE
|
||||
default:
|
||||
return models.HTML(string(IPMethod))
|
||||
}
|
||||
}
|
||||
|
||||
func convertDomain(domain string) models.HTML {
|
||||
return models.HTML("<a href=\"http://" + domain + "\">" + domain + "</a>")
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
Provider string
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
IPMethod string
|
||||
// Status is the record config status
|
||||
Status string
|
||||
// HTML is for constants HTML strings
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6
|
||||
IPVersion string
|
||||
)
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
Provider string
|
||||
// Status is the record config status
|
||||
Status string
|
||||
// HTML is for constants HTML strings
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6
|
||||
IPVersion string
|
||||
)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package models
|
||||
|
||||
// HTMLData is a list of HTML fields to be rendered.
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLData struct {
|
||||
Rows []HTMLRow
|
||||
}
|
||||
|
||||
// HTMLRow contains HTML fields to be rendered
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLRow struct {
|
||||
Domain HTML
|
||||
Host HTML
|
||||
Provider HTML
|
||||
IPMethod HTML
|
||||
Status HTML
|
||||
CurrentIP HTML
|
||||
PreviousIPs HTML
|
||||
}
|
||||
package models
|
||||
|
||||
// HTMLData is a list of HTML fields to be rendered.
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLData struct {
|
||||
Rows []HTMLRow
|
||||
}
|
||||
|
||||
// HTMLRow contains HTML fields to be rendered
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLRow struct {
|
||||
Domain HTML
|
||||
Host HTML
|
||||
Provider HTML
|
||||
IPVersion HTML
|
||||
Status HTML
|
||||
CurrentIP HTML
|
||||
PreviousIPs HTML
|
||||
}
|
||||
|
||||
9
internal/models/ipmethod.go
Normal file
9
internal/models/ipmethod.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package models
|
||||
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
type IPMethod struct {
|
||||
Name string
|
||||
URL string
|
||||
IPv4 bool
|
||||
IPv6 bool
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Settings contains the elements to update the DNS record
|
||||
type Settings struct {
|
||||
Domain string
|
||||
Host string
|
||||
Provider Provider
|
||||
IPMethod IPMethod
|
||||
IPVersion IPVersion
|
||||
Delay time.Duration
|
||||
NoDNSLookup bool
|
||||
// Provider dependent fields
|
||||
Password string // Namecheap, Infomaniak, DDNSS and NoIP only
|
||||
Key string // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string // GoDaddy only
|
||||
Token string // Cloudflare and DuckDNS only
|
||||
Email string // Cloudflare only
|
||||
UserServiceKey string // Cloudflare only
|
||||
ZoneIdentifier string // Cloudflare only
|
||||
Identifier string // Cloudflare only
|
||||
Proxied bool // Cloudflare only
|
||||
Ttl uint // Cloudflare only
|
||||
Username string // NoIP, Infomaniak, DDNSS only
|
||||
}
|
||||
|
||||
func (settings *Settings) String() string {
|
||||
b, _ := json.Marshal(
|
||||
struct {
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Provider string `json:"provider"`
|
||||
}{
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
string(settings.Provider),
|
||||
},
|
||||
)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// BuildDomainName builds the domain name from the domain and the host of the settings
|
||||
func (settings *Settings) BuildDomainName() string {
|
||||
if settings.Host == "@" {
|
||||
return settings.Domain
|
||||
} else if settings.Host == "*" {
|
||||
return "any." + settings.Domain
|
||||
} else {
|
||||
return settings.Host + "." + settings.Domain
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
@@ -13,27 +15,95 @@ import (
|
||||
)
|
||||
|
||||
// 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)
|
||||
func GetPublicIP(client network.Client, url string, ipVersion models.IPVersion) (ip net.IP, err error) {
|
||||
content, status, err := client.GetContent(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: %s", IPVersion, URL, err)
|
||||
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)
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d", ipVersion, url, status)
|
||||
}
|
||||
verifier := verification.NewVerifier()
|
||||
regexSearch := verifier.SearchIPv4
|
||||
if IPVersion == constants.IPv6 {
|
||||
regexSearch = verifier.SearchIPv6
|
||||
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)
|
||||
}
|
||||
ips := regexSearch(string(content))
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no public %s address found at %s", IPVersion, URL)
|
||||
} else if len(ips) > 1 {
|
||||
return nil, fmt.Errorf("multiple public %s addresses found at %s: %s", IPVersion, URL, strings.Join(ips, " "))
|
||||
}
|
||||
ip = net.ParseIP(ips[0])
|
||||
if ip == nil { // in case the regex is not restrictive enough
|
||||
return nil, fmt.Errorf("Public IP address %q found at %s is not valid", ips[0], URL)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"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/mocks"
|
||||
"github.com/qdm12/golibs/network/mock_network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -26,36 +27,47 @@ func Test_GetPublicIP(t *testing.T) {
|
||||
"network error": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockErr: fmt.Errorf("error"),
|
||||
err: fmt.Errorf("cannot get public ipv4 address from https://getmyip.com: 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"),
|
||||
},
|
||||
"no IPs in content": {
|
||||
"ipv4 address": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte(""),
|
||||
mockContent: []byte("55.55.55.55"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("no public ipv4 address found at https://getmyip.com"),
|
||||
ip: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"multiple IPs in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("10.10.10.10 50.50.50.50"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("multiple public ipv4 addresses found at https://getmyip.com: 10.10.10.10 50.50.50.50"),
|
||||
},
|
||||
"single IP in content": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("10.10.10.10"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{10, 10, 10, 10},
|
||||
},
|
||||
"single IPv6 in content": {
|
||||
"ipv6 address": {
|
||||
IPVersion: constants.IPv6,
|
||||
mockContent: []byte("::fe"),
|
||||
mockContent: []byte("ad07:e846:51ac:6cd0:0000:0000:0000:0000"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xfe},
|
||||
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"
|
||||
@@ -63,8 +75,10 @@ func Test_GetPublicIP(t *testing.T) {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := &mocks.Client{}
|
||||
client.On("GetContent", URL).Return(tc.mockContent, tc.mockStatus, tc.mockErr).Once()
|
||||
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)
|
||||
@@ -72,9 +86,70 @@ func Test_GetPublicIP(t *testing.T) {
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
fmt.Printf("%#v\n", ip)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
client.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// BuildHTTPPut is used for GoDaddy and Cloudflare only
|
||||
func BuildHTTPPut(URL string, body interface{}) (request *http.Request, err error) {
|
||||
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))
|
||||
request, err = http.NewRequest(http.MethodPut, url, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func (p *params) isConsistent(settings models.Settings) error {
|
||||
// General validity checks
|
||||
switch {
|
||||
case !ipMethodIsValid(settings.IPMethod):
|
||||
return fmt.Errorf("IP method %q is not recognized", settings.IPMethod)
|
||||
case settings.IPVersion != constants.IPv4 && settings.IPVersion != constants.IPv6:
|
||||
return fmt.Errorf("IP version %q is not recognized", settings.IPVersion)
|
||||
case !p.verifier.MatchDomain(settings.Domain):
|
||||
return fmt.Errorf("invalid domain name format")
|
||||
case len(settings.Host) == 0:
|
||||
return fmt.Errorf("host cannot be empty")
|
||||
}
|
||||
|
||||
// Checks for each IP versions
|
||||
switch settings.IPVersion {
|
||||
case constants.IPv4:
|
||||
switch settings.IPMethod {
|
||||
case constants.IPIFY6, constants.DDNSS6:
|
||||
return fmt.Errorf("IP method %s is only for IPv6 addresses", settings.IPMethod)
|
||||
}
|
||||
case constants.IPv6:
|
||||
switch settings.IPMethod {
|
||||
case constants.IPIFY, constants.DDNSS4:
|
||||
return fmt.Errorf("IP method %s is only for IPv4 addresses", settings.IPMethod)
|
||||
}
|
||||
switch settings.Provider {
|
||||
case constants.GODADDY, constants.CLOUDFLARE, constants.DNSPOD, constants.DREAMHOST, constants.DUCKDNS, constants.NOIP:
|
||||
return fmt.Errorf("IPv6 support for %s is not supported yet", settings.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
// Check provider ipmethod is available
|
||||
if settings.IPMethod == constants.PROVIDER {
|
||||
switch settings.Provider {
|
||||
case constants.GODADDY, constants.DREAMHOST, constants.CLOUDFLARE, constants.DNSPOD, constants.DDNSSDE:
|
||||
return fmt.Errorf("unsupported IP update method %q", settings.IPMethod)
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for each DNS provider
|
||||
switch settings.Provider {
|
||||
case constants.NAMECHEAP:
|
||||
if !constants.MatchNamecheapPassword(settings.Password) {
|
||||
return fmt.Errorf("invalid password format")
|
||||
}
|
||||
case constants.GODADDY:
|
||||
switch {
|
||||
case !constants.MatchGodaddyKey(settings.Key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !constants.MatchGodaddySecret(settings.Secret):
|
||||
return fmt.Errorf("invalid secret format")
|
||||
}
|
||||
case constants.DUCKDNS:
|
||||
switch {
|
||||
case !constants.MatchDuckDNSToken(settings.Token):
|
||||
return fmt.Errorf("invalid token format")
|
||||
case settings.Host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
case constants.DREAMHOST:
|
||||
switch {
|
||||
case !constants.MatchDreamhostKey(settings.Key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case settings.Host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
case constants.CLOUDFLARE:
|
||||
switch {
|
||||
case len(settings.Key) > 0: // email and key must be provided
|
||||
switch {
|
||||
case !constants.MatchCloudflareKey(settings.Key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !p.verifier.MatchEmail(settings.Email):
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
case len(settings.UserServiceKey) > 0: // only user service key
|
||||
if !constants.MatchCloudflareKey(settings.Key) {
|
||||
return fmt.Errorf("invalid user service key format")
|
||||
}
|
||||
default: // API token only
|
||||
if !constants.MatchCloudflareToken(settings.Token) {
|
||||
return fmt.Errorf("invalid API token key format")
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(settings.ZoneIdentifier) == 0:
|
||||
return fmt.Errorf("zone identifier cannot be empty")
|
||||
case len(settings.Identifier) == 0:
|
||||
return fmt.Errorf("identifier cannot be empty")
|
||||
case settings.Ttl == 0:
|
||||
return fmt.Errorf("TTL cannot be left to 0")
|
||||
}
|
||||
case constants.NOIP:
|
||||
switch {
|
||||
case len(settings.Username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(settings.Username) > 50:
|
||||
return fmt.Errorf("username cannot be longer than 50 characters")
|
||||
case len(settings.Password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case settings.Host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
case constants.DNSPOD:
|
||||
switch {
|
||||
case len(settings.Token) == 0:
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
case constants.INFOMANIAK:
|
||||
switch {
|
||||
case len(settings.Username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(settings.Password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case settings.Host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
case constants.DDNSSDE:
|
||||
switch {
|
||||
case len(settings.Username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(settings.Password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case settings.Host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("provider %q is not supported", settings.Provider)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipMethodIsValid(ipMethod models.IPMethod) bool {
|
||||
for _, possibility := range constants.IPMethodChoices() {
|
||||
if ipMethod == possibility {
|
||||
return true
|
||||
}
|
||||
}
|
||||
url, err := url.Parse(string(ipMethod))
|
||||
if err != nil || url == nil || url.Scheme != "https" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_ipMethodIsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
valid bool
|
||||
}{
|
||||
"empty method": {
|
||||
ipMethod: "",
|
||||
valid: false,
|
||||
},
|
||||
"non existing method": {
|
||||
ipMethod: "abc",
|
||||
valid: false,
|
||||
},
|
||||
"existing method": {
|
||||
ipMethod: "opendns",
|
||||
valid: true,
|
||||
},
|
||||
"http url": {
|
||||
ipMethod: "http://ipinfo.io/ip",
|
||||
valid: false,
|
||||
},
|
||||
"https url": {
|
||||
ipMethod: "https://ipinfo.io/ip",
|
||||
valid: true,
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
valid := ipMethodIsValid(tc.ipMethod)
|
||||
assert.Equal(t, tc.valid, valid)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,90 +1,151 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
type settingsType struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
IPMethod string `json:"ip_method"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
Delay uint64 `json:"delay"`
|
||||
NoDNSLookup bool `json:"no_dns_lookup"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"` // Namecheap, NoIP only
|
||||
Key string `json:"key"` // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string `json:"secret"` // GoDaddy only
|
||||
Token string `json:"token"` // DuckDNS and Cloudflare only
|
||||
Email string `json:"email"` // Cloudflare only
|
||||
Username string `json:"username"` // NoIP only
|
||||
UserServiceKey string `json:"user_service_key"` // Cloudflare only
|
||||
ZoneIdentifier string `json:"zone_identifier"` // Cloudflare only
|
||||
Identifier string `json:"identifier"` // Cloudflare only
|
||||
Proxied bool `json:"proxied"` // Cloudflare only
|
||||
Ttl uint `json:"ttl"` // Cloudflare only
|
||||
}
|
||||
|
||||
// GetSettings obtain the update settings from config.json
|
||||
func (p *params) GetSettings(filePath string) (settings []models.Settings, warnings []string, err error) {
|
||||
bytes, err := p.readFile(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var config struct {
|
||||
Settings []settingsType `json:"settings"`
|
||||
}
|
||||
if err := json.Unmarshal(bytes, &config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, s := range config.Settings {
|
||||
switch models.Provider(s.Provider) {
|
||||
case constants.DREAMHOST, constants.DUCKDNS:
|
||||
s.Host = "@" // only choice available
|
||||
}
|
||||
ipMethod := models.IPMethod(s.IPMethod)
|
||||
// Retro compatibility
|
||||
if ipMethod == constants.GOOGLE {
|
||||
p.logger.Warn("IP Method %q is no longer valid, please change it. Defaulting it to %s", constants.GOOGLE, constants.CYCLE)
|
||||
ipMethod = constants.CYCLE
|
||||
}
|
||||
ipVersion := models.IPVersion(s.IPVersion)
|
||||
if len(ipVersion) == 0 {
|
||||
ipVersion = constants.IPv4 // default
|
||||
}
|
||||
setting := models.Settings{
|
||||
Provider: models.Provider(s.Provider),
|
||||
Domain: s.Domain,
|
||||
Host: s.Host,
|
||||
IPMethod: ipMethod,
|
||||
IPVersion: ipVersion,
|
||||
Delay: time.Second * time.Duration(s.Delay),
|
||||
NoDNSLookup: s.NoDNSLookup,
|
||||
Password: s.Password,
|
||||
Key: s.Key,
|
||||
Secret: s.Secret,
|
||||
Token: s.Token,
|
||||
Email: s.Email,
|
||||
Username: s.Username,
|
||||
UserServiceKey: s.UserServiceKey,
|
||||
ZoneIdentifier: s.ZoneIdentifier,
|
||||
Identifier: s.Identifier,
|
||||
Proxied: s.Proxied,
|
||||
Ttl: s.Ttl,
|
||||
}
|
||||
if err := p.isConsistent(setting); err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("%s for settings %s", err, setting.String()))
|
||||
continue
|
||||
}
|
||||
settings = append(settings, setting)
|
||||
}
|
||||
if len(settings) == 0 {
|
||||
return nil, warnings, fmt.Errorf("no settings found in config.json")
|
||||
}
|
||||
return settings, warnings, nil
|
||||
}
|
||||
package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
// 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()
|
||||
if allSettings != nil || warnings != nil || err != nil {
|
||||
return allSettings, warnings, err
|
||||
}
|
||||
return r.getSettingsFromFile(filePath)
|
||||
}
|
||||
|
||||
// getSettingsFromFile obtain the update settings from config.json
|
||||
func (r *reader) getSettingsFromFile(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
bytes, err := r.readFile(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return extractAllSettings(bytes)
|
||||
}
|
||||
|
||||
// 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")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if len(s) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
return extractAllSettings([]byte(s))
|
||||
}
|
||||
|
||||
func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warnings []string, err error) {
|
||||
config := struct {
|
||||
CommonSettings []commonSettings `json:"settings"`
|
||||
}{}
|
||||
rawConfig := struct {
|
||||
Settings []json.RawMessage `json:"settings"`
|
||||
}{}
|
||||
if err := json.Unmarshal(jsonBytes, &config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := json.Unmarshal(jsonBytes, &rawConfig); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
matcher, err := regex.NewMatcher()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for i, common := range config.CommonSettings {
|
||||
newSettings, newWarnings, err := makeSettingsFromObject(common, rawConfig.Settings[i], matcher)
|
||||
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) {
|
||||
provider := models.Provider(common.Provider)
|
||||
if provider == constants.DUCKDNS { // only hosts, no domain
|
||||
if len(common.Domain) > 0 { // retro compatibility
|
||||
if len(common.Host) == 0 {
|
||||
common.Host = strings.TrimSuffix(common.Domain, ".duckdns.org")
|
||||
warnings = append(warnings, fmt.Sprintf("DuckDNS record should have %q specified as host instead of %q as domain", common.Host, common.Domain))
|
||||
} else {
|
||||
warnings = append(warnings, fmt.Sprintf("ignoring domain %q because host %q is specified for DuckDNS record", common.Domain, common.Host))
|
||||
}
|
||||
}
|
||||
}
|
||||
hosts := strings.Split(common.Host, ",")
|
||||
for _, host := range hosts {
|
||||
if len(host) == 0 {
|
||||
return nil, warnings, fmt.Errorf("host cannot be empty")
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
settingsSlice = make([]settings.Settings, len(hosts))
|
||||
for i, host := range hosts {
|
||||
settingsSlice[i], err = settingsConstructor(rawSettings, common.Domain, host, ipVersion, common.NoDNSLookup, matcher)
|
||||
if err != nil {
|
||||
return nil, warnings, err
|
||||
}
|
||||
}
|
||||
return settingsSlice, warnings, nil
|
||||
}
|
||||
|
||||
@@ -1,90 +1,203 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type ParamsReader interface {
|
||||
GetSettings(filePath string) (settings []models.Settings, warnings []string, err error)
|
||||
GetDataDir(currentDir string) (string, error)
|
||||
GetListeningPort() (listeningPort, warning string, err error)
|
||||
GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error)
|
||||
GetGotifyURL(setters ...libparams.GetEnvSetter) (URL *url.URL, err error)
|
||||
GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error)
|
||||
GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error)
|
||||
GetDelay(setters ...libparams.GetEnvSetter) (duration time.Duration, err error)
|
||||
GetExeDir() (dir string, err error)
|
||||
GetHTTPTimeout() (duration time.Duration, err error)
|
||||
|
||||
// Version getters
|
||||
GetVersion() string
|
||||
GetBuildDate() string
|
||||
GetVcsRef() string
|
||||
}
|
||||
|
||||
type params struct {
|
||||
envParams libparams.EnvParams
|
||||
verifier verification.Verifier
|
||||
logger logging.Logger
|
||||
readFile func(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewParamsReader(logger logging.Logger) ParamsReader {
|
||||
return ¶ms{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
verifier: verification.NewVerifier(),
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDataDir obtains the data directory from the environment
|
||||
// variable DATADIR
|
||||
func (p *params) GetDataDir(currentDir string) (string, error) {
|
||||
return p.envParams.GetEnv("DATADIR", libparams.Default(currentDir+"/data"))
|
||||
}
|
||||
|
||||
func (p *params) GetListeningPort() (listeningPort, warning string, err error) {
|
||||
return p.envParams.GetListeningPort()
|
||||
}
|
||||
|
||||
func (p *params) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
|
||||
return p.envParams.GetLoggerConfig()
|
||||
}
|
||||
|
||||
func (p *params) GetGotifyURL(setters ...libparams.GetEnvSetter) (URL *url.URL, err error) {
|
||||
return p.envParams.GetGotifyURL()
|
||||
}
|
||||
|
||||
func (p *params) GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error) {
|
||||
return p.envParams.GetGotifyToken()
|
||||
}
|
||||
|
||||
func (p *params) GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error) {
|
||||
return p.envParams.GetRootURL()
|
||||
}
|
||||
|
||||
func (p *params) GetDelay(setters ...libparams.GetEnvSetter) (period time.Duration, err error) {
|
||||
// Backward compatibility
|
||||
n, err := p.envParams.GetEnvInt("DELAY", libparams.Compulsory()) // TODO change to PERIOD
|
||||
if err == nil { // integer only, treated as seconds
|
||||
p.logger.Warn("The value for the duration period of the updater does not have a time unit, you might want to set it to \"%ds\" instead of \"%d\"", n, n)
|
||||
return time.Duration(n) * time.Second, nil
|
||||
}
|
||||
return p.envParams.GetDuration("DELAY", setters...)
|
||||
}
|
||||
|
||||
func (p *params) GetExeDir() (dir string, err error) {
|
||||
return p.envParams.GetExeDir()
|
||||
}
|
||||
|
||||
func (p *params) GetHTTPTimeout() (duration time.Duration, err error) {
|
||||
return p.envParams.GetHTTPTimeout(libparams.Default("10s"))
|
||||
}
|
||||
package params
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
"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
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
envParams libparams.EnvParams
|
||||
verifier verification.Verifier
|
||||
readFile func(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewReader(logger logging.Logger) Reader {
|
||||
return &reader{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
verifier: verification.NewVerifier(),
|
||||
readFile: ioutil.ReadFile,
|
||||
}
|
||||
}
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ import (
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
func (p *params) GetVersion() string {
|
||||
version, _ := p.envParams.GetEnv("VERSION", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
func (r *reader) GetVersion() string {
|
||||
version, _ := r.envParams.GetEnv("VERSION", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return version
|
||||
}
|
||||
|
||||
func (p *params) GetBuildDate() string {
|
||||
buildDate, _ := p.envParams.GetEnv("BUILD_DATE", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
func (r *reader) GetBuildDate() string {
|
||||
buildDate, _ := r.envParams.GetEnv("BUILD_DATE", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
|
||||
func (p *params) GetVcsRef() string {
|
||||
buildDate, _ := p.envParams.GetEnv("VCS_REF", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
func (r *reader) GetVcsRef() string {
|
||||
buildDate, _ := r.envParams.GetEnv("VCS_REF", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence/json"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence/sqlite"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
@@ -17,10 +16,6 @@ type Database interface {
|
||||
Check() error
|
||||
}
|
||||
|
||||
func NewSQLite(dataDir string) (Database, error) {
|
||||
return sqlite.NewDatabase(dataDir)
|
||||
}
|
||||
|
||||
func NewJSON(dataDir string) (Database, error) {
|
||||
return json.NewDatabase(dataDir)
|
||||
}
|
||||
|
||||
@@ -9,22 +9,22 @@ import (
|
||||
"github.com/qdm12/golibs/files"
|
||||
)
|
||||
|
||||
type database struct {
|
||||
type Database struct {
|
||||
data dataModel
|
||||
filepath string
|
||||
fileManager files.FileManager
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (db *database) Close() error {
|
||||
func (db *Database) Close() error {
|
||||
db.Lock() // ensure a write operation finishes
|
||||
defer db.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewDatabase opens or creates the JSON file database.
|
||||
func NewDatabase(dataDir string) (*database, error) {
|
||||
db := database{
|
||||
func NewDatabase(dataDir string) (*Database, error) {
|
||||
db := Database{
|
||||
filepath: dataDir + "/updates.json",
|
||||
fileManager: files.NewFileManager(),
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func NewDatabase(dataDir string) (*database, error) {
|
||||
return &db, nil
|
||||
}
|
||||
|
||||
func (db *database) Check() error {
|
||||
func (db *Database) Check() error {
|
||||
for _, record := range db.data.Records {
|
||||
switch {
|
||||
case len(record.Domain) == 0:
|
||||
@@ -81,7 +81,7 @@ func (db *database) Check() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *database) write() error {
|
||||
func (db *Database) write() error {
|
||||
data, err := json.MarshalIndent(db.data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// StoreNewIP stores a new IP address for a certain domain and host.
|
||||
func (db *database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error) {
|
||||
func (db *Database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i, record := range db.data.Records {
|
||||
@@ -33,22 +33,19 @@ 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
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for _, record := range db.data.Records {
|
||||
if record.Domain == domain && record.Host == host {
|
||||
for _, event := range record.Events {
|
||||
events = append(events, event)
|
||||
}
|
||||
return events, nil
|
||||
return append(events, record.Events...), nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetAllDomainsHosts gets all the domains and hosts from the database
|
||||
func (db *database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
func (db *Database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for _, record := range db.data.Records {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
func Migrate(source, destination Database, logger logging.Logger) (err error) {
|
||||
defer func() {
|
||||
closeErr := source.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s, %s", err, closeErr)
|
||||
} else {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
type row struct {
|
||||
domain string
|
||||
host string
|
||||
events []models.HistoryEvent
|
||||
}
|
||||
var rows []row
|
||||
|
||||
domainshosts, err := source.GetAllDomainsHosts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("Migrating %d domain-host tuples", len(domainshosts))
|
||||
|
||||
for i := range domainshosts {
|
||||
domain := domainshosts[i].Domain
|
||||
host := domainshosts[i].Host
|
||||
events, err := source.GetEvents(domain, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows = append(rows, row{domain, host, events})
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
for _, event := range r.events {
|
||||
destination.StoreNewIP(r.domain, r.host, event.IP, event.Time)
|
||||
}
|
||||
}
|
||||
return destination.Check()
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type database struct {
|
||||
sqlite *sql.DB
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (db *database) Close() error {
|
||||
return db.sqlite.Close()
|
||||
}
|
||||
|
||||
// NewDatabase opens or creates the database if necessary.
|
||||
func NewDatabase(dataDir string) (*database, error) {
|
||||
sqlite, err := sql.Open("sqlite3", dataDir+"/updates.db")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = sqlite.Exec(
|
||||
`CREATE TABLE IF NOT EXISTS updates_ips (
|
||||
domain TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
t_new DATETIME NOT NULL,
|
||||
t_last DATETIME NOT NULL,
|
||||
current INTEGER DEFAULT 1 NOT NULL,
|
||||
PRIMARY KEY(domain, host, ip, t_new)
|
||||
);`)
|
||||
return &database{sqlite: sqlite}, err
|
||||
}
|
||||
|
||||
func (db *database) Check() error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
/* Access to SQLite is NOT thread safe so we use a mutex */
|
||||
|
||||
// StoreNewIP stores a new IP address for a certain
|
||||
// domain and host.
|
||||
func (db *database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
// Inserts new IP
|
||||
_, err = db.sqlite.Exec(
|
||||
`INSERT INTO updates_ips(domain,host,ip,t_new,t_last)
|
||||
VALUES(?, ?, ?, ?, ?, ?);`,
|
||||
domain,
|
||||
host,
|
||||
ip.String(),
|
||||
t,
|
||||
t, // unneeded but it's hard to modify tables in sqlite
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetEvents gets all the IP addresses history for a certain domain and host, in the order
|
||||
// from oldest to newest
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
rows, err := db.sqlite.Query(
|
||||
`SELECT ip, t_new
|
||||
FROM updates_ips
|
||||
WHERE domain = ? AND host = ?
|
||||
ORDER BY t_new ASC`,
|
||||
domain,
|
||||
host,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := rows.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s, %s", err, closeErr)
|
||||
} else {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
var t time.Time
|
||||
if err := rows.Scan(&ip, &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, models.HistoryEvent{
|
||||
IP: net.ParseIP(ip),
|
||||
Time: t,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetAllDomainsHosts gets all domains and hosts from the database
|
||||
func (db *database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
rows, err := db.sqlite.Query(`SELECT DISTINCT domain, host FROM updates_ips`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := rows.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s, %s", err, closeErr)
|
||||
} else {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
for rows.Next() {
|
||||
domainHost := models.DomainHost{}
|
||||
if err := rows.Scan(&domainHost.Domain, &domainHost.Host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainshosts = append(domainshosts, domainHost)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return domainshosts, nil
|
||||
}
|
||||
|
||||
// SetSuccessTime sets the latest successful update time for a particular domain, host.
|
||||
func (db *database) SetSuccessTime(domain, host string, successTime time.Time) error {
|
||||
return fmt.Errorf("not implemented") // no plan to migrate back to sqlite
|
||||
}
|
||||
68
internal/records/html.go
Normal file
68
internal/records/html.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package records
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func (r *Record) HTML(now time.Time) models.HTMLRow {
|
||||
const NotAvailable = "N/A"
|
||||
row := r.Settings.HTML()
|
||||
message := r.Message
|
||||
if r.Status == constants.UPTODATE {
|
||||
message = "no IP change for " + r.History.GetDurationSinceSuccess(now)
|
||||
}
|
||||
if len(message) > 0 {
|
||||
message = fmt.Sprintf("(%s)", message)
|
||||
}
|
||||
if len(r.Status) == 0 {
|
||||
row.Status = NotAvailable
|
||||
} else {
|
||||
row.Status = models.HTML(fmt.Sprintf("%s %s, %s",
|
||||
convertStatus(r.Status),
|
||||
message,
|
||||
time.Since(r.Time).Round(time.Second).String()+" ago"))
|
||||
}
|
||||
currentIP := r.History.GetCurrentIP()
|
||||
if currentIP != nil {
|
||||
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
|
||||
} else {
|
||||
row.CurrentIP = NotAvailable
|
||||
}
|
||||
previousIPs := r.History.GetPreviousIPs()
|
||||
row.PreviousIPs = NotAvailable
|
||||
if len(previousIPs) > 0 {
|
||||
var previousIPsStr []string
|
||||
const maxPreviousIPs = 2
|
||||
for i, previousIP := range previousIPs {
|
||||
if i == maxPreviousIPs {
|
||||
previousIPsStr = append(previousIPsStr, fmt.Sprintf("and %d more", len(previousIPs)-i))
|
||||
break
|
||||
}
|
||||
previousIPsStr = append(previousIPsStr, previousIP.String())
|
||||
}
|
||||
row.PreviousIPs = models.HTML(strings.Join(previousIPsStr, ", "))
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func convertStatus(status models.Status) models.HTML {
|
||||
switch status {
|
||||
case constants.SUCCESS:
|
||||
return `<font color="green"><b>Success</b></font>`
|
||||
case constants.FAIL:
|
||||
return `<font color="red"><b>Failure</b></font>`
|
||||
case constants.UPTODATE:
|
||||
return `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
case constants.UPDATING:
|
||||
return `<font color="orange"><b>Updating</b></font>`
|
||||
case constants.UNSET:
|
||||
return `<font color="purple"><b>Unset</b></font>`
|
||||
default:
|
||||
return "Unknown status"
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
package models
|
||||
package records
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
)
|
||||
|
||||
// Record contains all the information to update and display a DNS record
|
||||
type Record struct { // internal
|
||||
Settings Settings // fixed
|
||||
History History // past information
|
||||
Status Status
|
||||
Settings settings.Settings // fixed
|
||||
History models.History // past information
|
||||
Status models.Status
|
||||
Message string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// NewRecord returns a new Record with settings and some history
|
||||
func NewRecord(settings Settings, events []HistoryEvent) Record {
|
||||
// New returns a new Record with settings and some history
|
||||
func New(settings settings.Settings, events []models.HistoryEvent) Record {
|
||||
return Record{
|
||||
Settings: settings,
|
||||
History: events,
|
||||
Status: constants.UNSET,
|
||||
}
|
||||
}
|
||||
|
||||
61
internal/regex/regex.go
Normal file
61
internal/regex/regex.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package regex
|
||||
|
||||
import "regexp"
|
||||
|
||||
type Matcher interface {
|
||||
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
|
||||
}
|
||||
|
||||
type matcher struct {
|
||||
goDaddyKey, goDaddySecret, duckDNSToken, namecheapPassword, dreamhostKey, cloudflareKey, cloudflareUserServiceKey *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
|
||||
}
|
||||
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) 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) }
|
||||
func (m *matcher) CloudflareKey(s string) bool { return m.cloudflareKey.MatchString(s) }
|
||||
func (m *matcher) CloudflareUserServiceKey(s string) bool {
|
||||
return m.cloudflareUserServiceKey.MatchString(s)
|
||||
}
|
||||
262
internal/settings/cloudflare.go
Normal file
262
internal/settings/cloudflare.go
Normal file
@@ -0,0 +1,262 @@
|
||||
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
|
||||
}
|
||||
9
internal/settings/constants.go
Normal file
9
internal/settings/constants.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package settings
|
||||
|
||||
const (
|
||||
badauth = "badauth"
|
||||
success = "success"
|
||||
nohost = "nohost"
|
||||
A = "A"
|
||||
AAAA = "AAAA"
|
||||
)
|
||||
144
internal/settings/ddnss.go
Normal file
144
internal/settings/ddnss.go
Normal file
@@ -0,0 +1,144 @@
|
||||
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/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type ddnss struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDdnss(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &ddnss{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *ddnss) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(d.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case d.host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ddnss) String() string {
|
||||
return toString(d.domain, d.host, constants.DDNSSDE, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *ddnss) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *ddnss) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *ddnss) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *ddnss) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *ddnss) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *ddnss) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://ddnss.de/\">DDNSS.de</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ddnss) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.ddnss.de",
|
||||
Path: "/upd.php",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("user", d.username)
|
||||
values.Set("pwd", d.password)
|
||||
fqdn := d.domain
|
||||
if d.host != "@" {
|
||||
fqdn = d.host + "." + d.domain
|
||||
}
|
||||
values.Set("host", fqdn)
|
||||
if !d.useProviderIP {
|
||||
if ip.To4() == nil { // ipv6
|
||||
values.Set("ip6", ip.String())
|
||||
} else {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("received status %d with message: %s", status, s)
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(s, "badysys"):
|
||||
return nil, fmt.Errorf("ddnss.de: invalid system parameter")
|
||||
case strings.Contains(s, badauth):
|
||||
return nil, fmt.Errorf("ddnss.de: bad authentication")
|
||||
case strings.Contains(s, "notfqdn"):
|
||||
return nil, fmt.Errorf("ddnss.de: hostname %q does not exist", fqdn)
|
||||
case strings.Contains(s, "Updated 1 hostname"):
|
||||
return ip, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown response received from ddnss.de: %s", s)
|
||||
}
|
||||
}
|
||||
180
internal/settings/dnspod.go
Normal file
180
internal/settings/dnspod.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
type dnspod struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
token string
|
||||
}
|
||||
|
||||
func NewDNSPod(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &dnspod{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
token: extraSettings.Token,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dnspod) isValid() error {
|
||||
if len(d.token) == 0 {
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dnspod) String() string {
|
||||
return toString(d.domain, d.host, constants.DNSPOD, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *dnspod) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dnspod) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dnspod) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dnspod) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *dnspod) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dnspod) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dnspod.cn/\">DNSPod</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnspod) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dnsapi.cn",
|
||||
Path: "/Record.List",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("login_token", d.token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("length", "200")
|
||||
values.Set("sub_domain", d.host)
|
||||
values.Set("record_type", recordType)
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var recordResp struct {
|
||||
Records []struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Line string `json:"line"`
|
||||
} `json:"records"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &recordResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var recordID, recordLine string
|
||||
for _, record := range recordResp.Records {
|
||||
if record.Type == A && record.Name == d.host {
|
||||
receivedIP := net.ParseIP(record.Value)
|
||||
if ip.Equal(receivedIP) {
|
||||
return ip, nil
|
||||
}
|
||||
recordID = record.ID
|
||||
recordLine = record.Line
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(recordID) == 0 {
|
||||
return nil, fmt.Errorf("record not found")
|
||||
}
|
||||
|
||||
u.Path = "/Record.Ddns"
|
||||
values = url.Values{}
|
||||
values.Set("login_token", d.token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("record_id", recordID)
|
||||
values.Set("value", ip.String())
|
||||
values.Set("record_line", recordLine)
|
||||
values.Set("sub_domain", d.host)
|
||||
u.RawQuery = values.Encode()
|
||||
r, err = http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err = client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var ddnsResp struct {
|
||||
Record struct {
|
||||
ID int64 `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Name string `json:"name"`
|
||||
} `json:"record"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &ddnsResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receivedIP := net.ParseIP(ddnsResp.Record.Value)
|
||||
if !ip.Equal(receivedIP) {
|
||||
return nil, fmt.Errorf("ip not set")
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
155
internal/settings/dondominio.go
Normal file
155
internal/settings/dondominio.go
Normal file
@@ -0,0 +1,155 @@
|
||||
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/regex"
|
||||
netlib "github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type donDominio struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
name string
|
||||
}
|
||||
|
||||
func NewDonDominio(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Name string `json:"name"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(host) == 0 {
|
||||
host = "@" // default
|
||||
}
|
||||
d := &donDominio{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
name: extraSettings.Name,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *donDominio) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(d.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case len(d.name) == 0:
|
||||
return fmt.Errorf("name cannot be empty")
|
||||
case d.host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *donDominio) String() string {
|
||||
return toString(d.domain, d.host, constants.DONDOMINIO, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *donDominio) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *donDominio) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *donDominio) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *donDominio) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *donDominio) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *donDominio) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dondominio.com/\">DonDominio</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *donDominio) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "simple-api.dondominio.net",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("apiuser", d.username)
|
||||
values.Set("apipasswd", d.password)
|
||||
values.Set("domain", d.domain)
|
||||
values.Set("name", d.name)
|
||||
isIPv4 := ip.To4() != nil
|
||||
if isIPv4 {
|
||||
values.Set("ipv4", ip.String())
|
||||
} else {
|
||||
values.Set("ipv6", ip.String())
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(values.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentid.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
ErrorCode int `json:"errorCode"`
|
||||
ErrorCodeMessage string `json:"errorCodeMsg"`
|
||||
ResponseData struct {
|
||||
GlueRecords []struct {
|
||||
IPv4 string `json:"ipv4"`
|
||||
IPv6 string `json:"ipv6"`
|
||||
} `json:"gluerecords"`
|
||||
} `json:"responseData"`
|
||||
}{}
|
||||
if err := json.Unmarshal(content, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !response.Success {
|
||||
return nil, fmt.Errorf("%s (error code %d)", response.ErrorCodeMessage, response.ErrorCode)
|
||||
}
|
||||
ipString := response.ResponseData.GlueRecords[0].IPv4
|
||||
if !isIPv4 {
|
||||
ipString = response.ResponseData.GlueRecords[0].IPv6
|
||||
}
|
||||
newIP = net.ParseIP(ipString)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ipString)
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
241
internal/settings/dreamhost.go
Normal file
241
internal/settings/dreamhost.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
type dreamhost struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
key string
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDreamhost(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(host) == 0 {
|
||||
host = "@" // default
|
||||
}
|
||||
d := &dreamhost{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
key: extraSettings.Key,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) isValid() error {
|
||||
switch {
|
||||
case !d.matcher.DreamhostKey(d.key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case d.host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dreamhost) String() string {
|
||||
return toString(d.domain, d.host, constants.DREAMHOST, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *dreamhost) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dreamhost) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dreamhost) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dreamhost) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *dreamhost) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dreamhost) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dreamhost) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
records, err := listDreamhostRecords(client, d.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var oldIP net.IP
|
||||
for _, data := range records.Data {
|
||||
if data.Type == recordType && data.Record == d.BuildDomainName() {
|
||||
if data.Editable == "0" {
|
||||
return nil, fmt.Errorf("record data is not editable")
|
||||
}
|
||||
oldIP = net.ParseIP(data.Value)
|
||||
if ip.Equal(oldIP) { // success, nothing to change
|
||||
return ip, nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if oldIP != nil { // Found editable record with a different IP address, so remove it
|
||||
if err := removeDreamhostRecord(client, d.key, d.domain, oldIP); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ip, addDreamhostRecord(client, d.key, d.domain, ip)
|
||||
}
|
||||
|
||||
type (
|
||||
dreamHostRecords struct {
|
||||
Result string `json:"result"`
|
||||
Data []struct {
|
||||
Editable string `json:"editable"`
|
||||
Type string `json:"type"`
|
||||
Record string `json:"record"`
|
||||
Value string `json:"value"`
|
||||
} `json:"data"`
|
||||
}
|
||||
dreamhostReponse struct {
|
||||
Result string `json:"result"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
func makeDreamhostDefaultValues(key string) (values url.Values) { //nolint:unparam
|
||||
values.Set("key", key)
|
||||
values.Set("unique_id", uuid.New().String())
|
||||
values.Set("format", "json")
|
||||
return values
|
||||
}
|
||||
|
||||
func listDreamhostRecords(client network.Client, key string) (records dreamHostRecords, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-list_records")
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return records, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return records, err
|
||||
} else if status != http.StatusOK {
|
||||
return records, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
if err := json.Unmarshal(content, &records); err != nil {
|
||||
return records, err
|
||||
} else if records.Result != success {
|
||||
return records, fmt.Errorf(records.Result)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func removeDreamhostRecord(client network.Client, key, domain string, ip net.IP) error { //nolint:dupl
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-remove_record")
|
||||
values.Set("record", domain)
|
||||
values.Set("type", recordType)
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != success { // this should not happen
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addDreamhostRecord(client network.Client, key, domain string, ip net.IP) error { //nolint:dupl
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-add_record")
|
||||
values.Set("record", domain)
|
||||
values.Set("type", recordType)
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != success {
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
144
internal/settings/duckdns.go
Normal file
144
internal/settings/duckdns.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type duckdns struct {
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
token string
|
||||
useProviderIP bool
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDuckdns(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &duckdns{
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
token: extraSettings.Token,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *duckdns) isValid() error {
|
||||
if !d.matcher.DuckDNSToken(d.token) {
|
||||
return fmt.Errorf("invalid token format")
|
||||
}
|
||||
switch d.host {
|
||||
case "@", "*":
|
||||
return fmt.Errorf("host cannot be @ or * and must be a subdomain for DuckDNS")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *duckdns) String() string {
|
||||
return toString("duckdns..org", d.host, constants.DUCKDNS, d.ipVersion)
|
||||
}
|
||||
|
||||
func (d *duckdns) Domain() string {
|
||||
return "duckdns.org"
|
||||
}
|
||||
|
||||
func (d *duckdns) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *duckdns) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *duckdns) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *duckdns) BuildDomainName() string {
|
||||
return buildDomainName(d.host, "duckdns.org")
|
||||
}
|
||||
|
||||
func (d *duckdns) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://duckdns.org\">DuckDNS</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *duckdns) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.duckdns.org",
|
||||
Path: "/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("verbose", "true")
|
||||
values.Set("domains", d.host)
|
||||
values.Set("token", d.token)
|
||||
u.RawQuery = values.Encode()
|
||||
if !d.useProviderIP {
|
||||
if ip.To4() == nil {
|
||||
values.Set("ip6", ip.String())
|
||||
} else {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
s := string(content)
|
||||
switch {
|
||||
case len(s) < 2:
|
||||
return nil, fmt.Errorf("response %q is too short", s)
|
||||
case s[0:2] == "KO":
|
||||
return nil, fmt.Errorf("invalid domain token combination")
|
||||
case s[0:2] == "OK":
|
||||
ips := verification.NewVerifier().SearchIPv4(s)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
|
||||
}
|
||||
if ip != nil && !newIP.Equal(ip) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid response %q", s)
|
||||
}
|
||||
}
|
||||
137
internal/settings/dyn.go
Normal file
137
internal/settings/dyn.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type dyn struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDyn(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d := &dyn{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := d.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *dyn) isValid() error {
|
||||
switch {
|
||||
case len(d.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(d.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case d.host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dyn) String() string {
|
||||
return fmt.Sprintf("[domain: %s | host: %s | provider: Dyn]", d.domain, d.host)
|
||||
}
|
||||
|
||||
func (d *dyn) Domain() string {
|
||||
return d.domain
|
||||
}
|
||||
|
||||
func (d *dyn) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dyn) IPVersion() models.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
func (d *dyn) DNSLookup() bool {
|
||||
return d.dnsLookup
|
||||
}
|
||||
|
||||
func (d *dyn) BuildDomainName() string {
|
||||
return buildDomainName(d.host, d.domain)
|
||||
}
|
||||
|
||||
func (d *dyn) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", d.BuildDomainName(), d.BuildDomainName())),
|
||||
Host: models.HTML(d.Host()),
|
||||
Provider: "<a href=\"https://dyn.com/\">Dyn DNS</a>",
|
||||
IPVersion: models.HTML(d.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dyn) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
User: url.UserPassword(d.username, d.password),
|
||||
Host: "members.dyndns.org",
|
||||
Path: "/v3/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
switch d.host {
|
||||
case "@":
|
||||
values.Set("hostname", d.domain)
|
||||
default:
|
||||
values.Set("hostname", fmt.Sprintf("%s.%s", d.host, d.domain))
|
||||
}
|
||||
if !d.useProviderIP {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
s := string(content)
|
||||
switch {
|
||||
case strings.HasPrefix(s, "notfqdn"):
|
||||
return nil, fmt.Errorf("fully qualified domain name is not valid")
|
||||
case strings.HasPrefix(s, "badrequest"):
|
||||
return nil, fmt.Errorf("bad request")
|
||||
case strings.HasPrefix(s, "good"):
|
||||
return ip, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown response: %s", s)
|
||||
}
|
||||
}
|
||||
127
internal/settings/godaddy.go
Normal file
127
internal/settings/godaddy.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type godaddy struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
key string
|
||||
secret string
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewGodaddy(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
Secret string `json:"secret"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := &godaddy{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
key: extraSettings.Key,
|
||||
secret: extraSettings.Secret,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := g.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *godaddy) isValid() error {
|
||||
switch {
|
||||
case !g.matcher.GodaddyKey(g.key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !g.matcher.GodaddySecret(g.secret):
|
||||
return fmt.Errorf("invalid secret format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *godaddy) String() string {
|
||||
return toString(g.domain, g.host, constants.GODADDY, g.ipVersion)
|
||||
}
|
||||
|
||||
func (g *godaddy) Domain() string {
|
||||
return g.domain
|
||||
}
|
||||
|
||||
func (g *godaddy) Host() string {
|
||||
return g.host
|
||||
}
|
||||
|
||||
func (g *godaddy) IPVersion() models.IPVersion {
|
||||
return g.ipVersion
|
||||
}
|
||||
|
||||
func (g *godaddy) DNSLookup() bool {
|
||||
return g.dnsLookup
|
||||
}
|
||||
|
||||
func (g *godaddy) BuildDomainName() string {
|
||||
return buildDomainName(g.host, g.domain)
|
||||
}
|
||||
|
||||
func (g *godaddy) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", g.BuildDomainName(), g.BuildDomainName())),
|
||||
Host: models.HTML(g.Host()),
|
||||
Provider: "<a href=\"https://godaddy.com\">GoDaddy</a>",
|
||||
IPVersion: models.HTML(g.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *godaddy) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
recordType := A
|
||||
if ip.To4() == nil {
|
||||
recordType = AAAA
|
||||
}
|
||||
type goDaddyPutBody struct {
|
||||
Data string `json:"data"` // IP address to update to
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.godaddy.com",
|
||||
Path: fmt.Sprintf("/v1/domains/%s/records/%s/%s", g.domain, recordType, g.host),
|
||||
}
|
||||
r, err := network.BuildHTTPPut(u.String(), []goDaddyPutBody{{ip.String()}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
r.Header.Set("Authorization", "sso-key "+g.key+":"+g.secret)
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
var parsedJSON struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &parsedJSON); err != nil {
|
||||
return nil, err
|
||||
} else if len(parsedJSON.Message) > 0 {
|
||||
return nil, fmt.Errorf("HTTP status %d - %s", status, parsedJSON.Message)
|
||||
}
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
157
internal/settings/google.go
Normal file
157
internal/settings/google.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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/regex"
|
||||
netlib "github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type google struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewGoogle(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g := &google{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := g.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *google) isValid() error {
|
||||
switch {
|
||||
case len(g.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(g.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *google) String() string {
|
||||
return toString(g.domain, g.host, constants.GOOGLE, g.ipVersion)
|
||||
}
|
||||
|
||||
func (g *google) Domain() string {
|
||||
return g.domain
|
||||
}
|
||||
|
||||
func (g *google) Host() string {
|
||||
return g.host
|
||||
}
|
||||
|
||||
func (g *google) DNSLookup() bool {
|
||||
return g.dnsLookup
|
||||
}
|
||||
|
||||
func (g *google) IPVersion() models.IPVersion {
|
||||
return g.ipVersion
|
||||
}
|
||||
|
||||
func (g *google) BuildDomainName() string {
|
||||
return buildDomainName(g.host, g.domain)
|
||||
}
|
||||
|
||||
func (g *google) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", g.BuildDomainName(), g.BuildDomainName())),
|
||||
Host: models.HTML(g.Host()),
|
||||
Provider: "<a href=\"https://domains.google.com/m/registrar\">Google</a>",
|
||||
IPVersion: models.HTML(g.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *google) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "domains.google.com",
|
||||
Path: "/nic/update",
|
||||
User: url.UserPassword(g.username, g.password),
|
||||
}
|
||||
values := url.Values{}
|
||||
fqdn := g.BuildDomainName()
|
||||
values.Set("hostname", fqdn)
|
||||
if !g.useProviderIP {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentig.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
switch s {
|
||||
case "":
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
case nohost:
|
||||
return nil, fmt.Errorf("hostname does not exist")
|
||||
case badauth:
|
||||
return nil, fmt.Errorf("invalid username password combination")
|
||||
case "notfqdn":
|
||||
return nil, fmt.Errorf("hostname %q is not a valid fully qualified domain name", fqdn)
|
||||
case "badagent":
|
||||
return nil, fmt.Errorf("user agent is banned")
|
||||
case "abuse":
|
||||
return nil, fmt.Errorf("username is banned due to abuse")
|
||||
case "911":
|
||||
return nil, fmt.Errorf("Google's internal server error 911")
|
||||
case "conflict A":
|
||||
return nil, fmt.Errorf("custom A record conflicts with the update")
|
||||
case "conflict AAAA":
|
||||
return nil, fmt.Errorf("custom AAAA record conflicts with the update")
|
||||
}
|
||||
if strings.Contains(s, "nochg") || strings.Contains(s, "good") {
|
||||
ipsV4 := verification.NewVerifier().SearchIPv4(s)
|
||||
ipsV6 := verification.NewVerifier().SearchIPv6(s)
|
||||
ips := append(ipsV4, ipsV6...)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
|
||||
}
|
||||
if !g.useProviderIP && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response %q", s)
|
||||
}
|
||||
138
internal/settings/he.go
Normal file
138
internal/settings/he.go
Normal file
@@ -0,0 +1,138 @@
|
||||
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/regex"
|
||||
netlib "github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type he struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewHe(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := &he{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := h.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *he) isValid() error {
|
||||
if len(h.password) == 0 {
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *he) String() string {
|
||||
return toString(h.domain, h.host, constants.HE, h.ipVersion)
|
||||
}
|
||||
|
||||
func (h *he) Domain() string {
|
||||
return h.domain
|
||||
}
|
||||
|
||||
func (h *he) Host() string {
|
||||
return h.host
|
||||
}
|
||||
|
||||
func (h *he) DNSLookup() bool {
|
||||
return h.dnsLookup
|
||||
}
|
||||
|
||||
func (h *he) IPVersion() models.IPVersion {
|
||||
return h.ipVersion
|
||||
}
|
||||
|
||||
func (h *he) BuildDomainName() string {
|
||||
return buildDomainName(h.host, h.domain)
|
||||
}
|
||||
|
||||
func (h *he) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", h.BuildDomainName(), h.BuildDomainName())),
|
||||
Host: models.HTML(h.Host()),
|
||||
Provider: "<a href=\"https://dns.he.net/\">he.net</a>",
|
||||
IPVersion: models.HTML(h.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *he) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
fqdn := h.BuildDomainName()
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dyn.dns.he.net",
|
||||
Path: "/nic/update",
|
||||
User: url.UserPassword(fqdn, h.password),
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", fqdn)
|
||||
if !h.useProviderIP {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentih.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
switch s {
|
||||
case "":
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
case badauth:
|
||||
return nil, fmt.Errorf("invalid username password combination")
|
||||
}
|
||||
if strings.Contains(s, "nochg") || strings.Contains(s, "good") {
|
||||
verifier := verification.NewVerifier()
|
||||
ipsV4 := verifier.SearchIPv4(s)
|
||||
ipsV6 := verifier.SearchIPv6(s)
|
||||
ips := append(ipsV4, ipsV6...)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
|
||||
}
|
||||
if !h.useProviderIP && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response %q", s)
|
||||
}
|
||||
157
internal/settings/infomaniak.go
Normal file
157
internal/settings/infomaniak.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type infomaniak struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewInfomaniak(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := &infomaniak{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := i.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *infomaniak) isValid() error {
|
||||
switch {
|
||||
case len(i.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(i.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case i.host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *infomaniak) String() string {
|
||||
return toString(i.domain, i.host, constants.INFOMANIAK, i.ipVersion)
|
||||
}
|
||||
|
||||
func (i *infomaniak) Domain() string {
|
||||
return i.domain
|
||||
}
|
||||
|
||||
func (i *infomaniak) Host() string {
|
||||
return i.host
|
||||
}
|
||||
|
||||
func (i *infomaniak) IPVersion() models.IPVersion {
|
||||
return i.ipVersion
|
||||
}
|
||||
|
||||
func (i *infomaniak) DNSLookup() bool {
|
||||
return i.dnsLookup
|
||||
}
|
||||
|
||||
func (i *infomaniak) BuildDomainName() string {
|
||||
return buildDomainName(i.host, i.domain)
|
||||
}
|
||||
|
||||
func (i *infomaniak) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", i.BuildDomainName(), i.BuildDomainName())),
|
||||
Host: models.HTML(i.Host()),
|
||||
Provider: "<a href=\"https://www.infomaniak.com/\">Infomaniak</a>",
|
||||
IPVersion: models.HTML(i.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *infomaniak) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "infomaniak.com",
|
||||
Path: "/nic/update",
|
||||
User: url.UserPassword(i.username, i.password),
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", i.domain)
|
||||
if i.host != "@" {
|
||||
values.Set("hostname", i.host+"."+i.domain)
|
||||
}
|
||||
if !i.useProviderIP {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
switch {
|
||||
case strings.HasPrefix(s, "good "):
|
||||
newIP = net.ParseIP(s[5:])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("no received IP in response %q", s)
|
||||
} else if ip != nil && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("received IP %s is not equal to expected IP %s", newIP, ip)
|
||||
}
|
||||
return newIP, nil
|
||||
case strings.HasPrefix(s, "nochg "):
|
||||
newIP = net.ParseIP(s[6:])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("no received IP in response %q", s)
|
||||
} else if ip != nil && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("received IP %s is not equal to expected IP %s", newIP, ip)
|
||||
}
|
||||
return newIP, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("ok status but unknown response %q", s)
|
||||
}
|
||||
case http.StatusBadRequest:
|
||||
switch s {
|
||||
case nohost:
|
||||
return nil, fmt.Errorf("infomaniak.com: host %q does not exist for domain %q", i.host, i.domain)
|
||||
case badauth:
|
||||
return nil, fmt.Errorf("infomaniak.com: bad authentication")
|
||||
default:
|
||||
return nil, fmt.Errorf("infomaniak.com: bad request: %s", s)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("received status %d with message: %s", status, s)
|
||||
}
|
||||
}
|
||||
139
internal/settings/namecheap.go
Normal file
139
internal/settings/namecheap.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type namecheap struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
password string
|
||||
useProviderIP bool
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewNamecheap(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
if ipVersion == constants.IPv6 {
|
||||
return s, fmt.Errorf("IPv6 is not supported by Namecheap API sadly")
|
||||
}
|
||||
extraSettings := struct {
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := &namecheap{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
matcher: matcher,
|
||||
}
|
||||
if err := n.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (n *namecheap) isValid() error {
|
||||
if !n.matcher.NamecheapPassword(n.password) {
|
||||
return fmt.Errorf("invalid password format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *namecheap) String() string {
|
||||
return toString(n.domain, n.host, constants.NAMECHEAP, n.ipVersion)
|
||||
}
|
||||
|
||||
func (n *namecheap) Domain() string {
|
||||
return n.domain
|
||||
}
|
||||
|
||||
func (n *namecheap) Host() string {
|
||||
return n.host
|
||||
}
|
||||
|
||||
func (n *namecheap) IPVersion() models.IPVersion {
|
||||
return n.ipVersion
|
||||
}
|
||||
|
||||
func (n *namecheap) DNSLookup() bool {
|
||||
return n.dnsLookup
|
||||
}
|
||||
|
||||
func (n *namecheap) BuildDomainName() string {
|
||||
return buildDomainName(n.host, n.domain)
|
||||
}
|
||||
|
||||
func (n *namecheap) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", n.BuildDomainName(), n.BuildDomainName())),
|
||||
Host: models.HTML(n.Host()),
|
||||
Provider: "<a href=\"https://namecheap.com\">Namecheap</a>",
|
||||
IPVersion: models.HTML(n.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *namecheap) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dynamicdns.park-your-domain.com",
|
||||
Path: "/update",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("host", n.host)
|
||||
values.Set("domain", n.domain)
|
||||
values.Set("password", n.password)
|
||||
if !n.useProviderIP {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var parsedXML struct {
|
||||
Errors struct {
|
||||
Error string `xml:"Err1"`
|
||||
} `xml:"errors"`
|
||||
IP string `xml:"IP"`
|
||||
}
|
||||
err = xml.Unmarshal(content, &parsedXML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if parsedXML.Errors.Error != "" {
|
||||
return nil, fmt.Errorf(parsedXML.Errors.Error)
|
||||
}
|
||||
newIP = net.ParseIP(parsedXML.IP)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", parsedXML.IP)
|
||||
}
|
||||
if ip != nil && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
158
internal/settings/noip.go
Normal file
158
internal/settings/noip.go
Normal file
@@ -0,0 +1,158 @@
|
||||
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/regex"
|
||||
netlib "github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
//nolint:maligned
|
||||
type noip struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
dnsLookup bool
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewNoip(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
UseProviderIP bool `json:"provider_ip"`
|
||||
}{}
|
||||
if err := json.Unmarshal(data, &extraSettings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
n := &noip{
|
||||
domain: domain,
|
||||
host: host,
|
||||
ipVersion: ipVersion,
|
||||
dnsLookup: !noDNSLookup,
|
||||
username: extraSettings.Username,
|
||||
password: extraSettings.Password,
|
||||
useProviderIP: extraSettings.UseProviderIP,
|
||||
}
|
||||
if err := n.isValid(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (n *noip) isValid() error {
|
||||
switch {
|
||||
case len(n.username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(n.username) > 50:
|
||||
return fmt.Errorf("username cannot be longer than 50 characters")
|
||||
case len(n.password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case n.host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *noip) String() string {
|
||||
return toString(n.domain, n.host, constants.NOIP, n.ipVersion)
|
||||
}
|
||||
|
||||
func (n *noip) Domain() string {
|
||||
return n.domain
|
||||
}
|
||||
|
||||
func (n *noip) Host() string {
|
||||
return n.host
|
||||
}
|
||||
|
||||
func (n *noip) DNSLookup() bool {
|
||||
return n.dnsLookup
|
||||
}
|
||||
|
||||
func (n *noip) IPVersion() models.IPVersion {
|
||||
return n.ipVersion
|
||||
}
|
||||
|
||||
func (n *noip) BuildDomainName() string {
|
||||
return buildDomainName(n.host, n.domain)
|
||||
}
|
||||
|
||||
func (n *noip) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", n.BuildDomainName(), n.BuildDomainName())),
|
||||
Host: models.HTML(n.Host()),
|
||||
Provider: "<a href=\"https://www.noip.com/\">NoIP</a>",
|
||||
IPVersion: models.HTML(n.ipVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *noip) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dynupdate.no-ip.com",
|
||||
Path: "/nic/update",
|
||||
User: url.UserPassword(n.username, n.password),
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", n.BuildDomainName())
|
||||
if !n.useProviderIP {
|
||||
if ip.To4() == nil {
|
||||
values.Set("myipv6", ip.String())
|
||||
} else {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
switch s {
|
||||
case "":
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
case "911":
|
||||
return nil, fmt.Errorf("NoIP's internal server error 911")
|
||||
case "abuse":
|
||||
return nil, fmt.Errorf("username is banned due to abuse")
|
||||
case "!donator":
|
||||
return nil, fmt.Errorf("user has not this extra feature")
|
||||
case "badagent":
|
||||
return nil, fmt.Errorf("user agent is banned")
|
||||
case badauth:
|
||||
return nil, fmt.Errorf("invalid username password combination")
|
||||
case nohost:
|
||||
return nil, fmt.Errorf("hostname does not exist")
|
||||
}
|
||||
if strings.Contains(s, "nochg") || strings.Contains(s, "good") {
|
||||
ips := verification.NewVerifier().SearchIPv4(s)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
|
||||
}
|
||||
if !n.useProviderIP && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response %q", s)
|
||||
}
|
||||
39
internal/settings/settings.go
Normal file
39
internal/settings/settings.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
type Settings interface {
|
||||
String() string
|
||||
Domain() string
|
||||
Host() string
|
||||
BuildDomainName() string
|
||||
HTML() models.HTMLRow
|
||||
DNSLookup() bool
|
||||
IPVersion() models.IPVersion
|
||||
Update(client network.Client, ip net.IP) (newIP net.IP, err error)
|
||||
}
|
||||
|
||||
type Constructor func(data json.RawMessage, domain string, host string, ipVersion models.IPVersion, noDNSLookup bool, matcher regex.Matcher) (s Settings, err error)
|
||||
|
||||
func buildDomainName(host, domain string) string {
|
||||
switch host {
|
||||
case "@":
|
||||
return domain
|
||||
case "*":
|
||||
return "any." + domain
|
||||
default:
|
||||
return host + "." + domain
|
||||
}
|
||||
}
|
||||
|
||||
func toString(domain, host string, provider models.Provider, ipVersion models.IPVersion) string {
|
||||
return fmt.Sprintf("[domain: %s | host: %s | provider: %s | ip: %s]", domain, host, provider, ipVersion)
|
||||
}
|
||||
@@ -7,19 +7,15 @@ import (
|
||||
|
||||
"github.com/kyokomi/emoji"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
)
|
||||
|
||||
// Splash returns the welcome spash message
|
||||
func Splash(paramsReader params.ParamsReader) string {
|
||||
version := paramsReader.GetVersion()
|
||||
vcsRef := paramsReader.GetVcsRef()
|
||||
buildDate := paramsReader.GetBuildDate()
|
||||
func Splash(version, vcsRef, buildDate string) string {
|
||||
lines := title()
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, fmt.Sprintf("Running version %s built on %s (commit %s)", version, buildDate, vcsRef))
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, annoucement()...)
|
||||
lines = append(lines, announcement()...)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, links()...)
|
||||
return strings.Join(lines, "\n")
|
||||
@@ -36,15 +32,15 @@ func title() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func annoucement() []string {
|
||||
if len(constants.Annoucement) == 0 {
|
||||
func announcement() []string {
|
||||
if len(constants.Announcement) == 0 {
|
||||
return nil
|
||||
}
|
||||
expirationDate, _ := time.Parse("2006-01-02", constants.AnnoucementExpiration) // error covered by a unit test
|
||||
expirationDate, _ := time.Parse("2006-01-02", constants.AnnouncementExpiration) // error covered by a unit test
|
||||
if time.Now().After(expirationDate) {
|
||||
return nil
|
||||
}
|
||||
return []string{emoji.Sprint(":mega: ") + constants.Annoucement}
|
||||
return []string{emoji.Sprint(":mega: ") + constants.Announcement}
|
||||
}
|
||||
|
||||
func links() []string {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
package trigger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
)
|
||||
|
||||
// StartUpdates starts periodic updates
|
||||
func StartUpdates(ctx context.Context, updater update.Updater, idPeriodMapping map[int]time.Duration, onError func(err error)) (forceUpdate func()) {
|
||||
errors := make(chan error)
|
||||
triggers := make([]chan struct{}, len(idPeriodMapping))
|
||||
for id, period := range idPeriodMapping {
|
||||
triggers[id] = make(chan struct{})
|
||||
go func(id int, period time.Duration) {
|
||||
ticker := time.NewTicker(period)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-triggers[id]:
|
||||
if err := updater.Update(id); err != nil {
|
||||
errors <- err
|
||||
}
|
||||
case <-ticker.C:
|
||||
if err := updater.Update(id); err != nil {
|
||||
errors <- err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}(id, period)
|
||||
}
|
||||
// collects errors only
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case err := <-errors:
|
||||
onError(err)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() {
|
||||
for i := range triggers {
|
||||
triggers[i] <- struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
libnetwork "github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateCloudflare(client libnetwork.Client, zoneIdentifier, identifier, host, email, key, userServiceKey, token string, proxied bool, ttl uint, ip net.IP) (err error) {
|
||||
if ip == nil {
|
||||
return fmt.Errorf("IP address was not given to updater")
|
||||
}
|
||||
type cloudflarePutBody struct {
|
||||
Type string `json:"type"` // forced to A
|
||||
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"`
|
||||
}
|
||||
URL := constants.CloudflareURL + "/zones/" + zoneIdentifier + "/dns_records/" + identifier
|
||||
r, err := network.BuildHTTPPut(
|
||||
URL,
|
||||
cloudflarePutBody{
|
||||
Type: "A",
|
||||
Name: host,
|
||||
Content: ip.String(),
|
||||
Proxied: proxied,
|
||||
Ttl: ttl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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)
|
||||
default:
|
||||
return fmt.Errorf("email and key are both unset and user service key is not set and no token was provided")
|
||||
}
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status > http.StatusUnsupportedMediaType {
|
||||
return 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 err
|
||||
} else if !parsedJSON.Success {
|
||||
var errStr string
|
||||
for _, e := range parsedJSON.Errors {
|
||||
errStr += fmt.Sprintf("error %d: %s; ", e.Code, e.Message)
|
||||
}
|
||||
return fmt.Errorf(errStr)
|
||||
}
|
||||
newIP := net.ParseIP(parsedJSON.Result.Content)
|
||||
if newIP == nil {
|
||||
return fmt.Errorf("new IP %q is malformed", parsedJSON.Result.Content)
|
||||
} else if !newIP.Equal(ip) {
|
||||
return fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
34
internal/update/cycle.go
Normal file
34
internal/update/cycle.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
type cycler interface {
|
||||
next() models.IPMethod
|
||||
}
|
||||
|
||||
type cyclerImpl struct {
|
||||
sync.Mutex
|
||||
counter int
|
||||
methods []models.IPMethod
|
||||
}
|
||||
|
||||
func newCycler(methods []models.IPMethod) cycler {
|
||||
return &cyclerImpl{
|
||||
methods: methods,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cyclerImpl) next() models.IPMethod {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
method := c.methods[c.counter]
|
||||
c.counter++
|
||||
if c.counter == len(c.methods) {
|
||||
c.counter = 0
|
||||
}
|
||||
return method
|
||||
}
|
||||
64
internal/update/cycle_test.go
Normal file
64
internal/update/cycle_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newCycler(t *testing.T) {
|
||||
t.Parallel()
|
||||
ipMethods := []models.IPMethod{
|
||||
{Name: "a"}, {Name: "b"},
|
||||
}
|
||||
c := newCycler(ipMethods)
|
||||
require.NotNil(t, c)
|
||||
ipMethod := c.next()
|
||||
assert.Equal(t, ipMethod, models.IPMethod{Name: "a"})
|
||||
}
|
||||
|
||||
func Test_next(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &cyclerImpl{
|
||||
methods: []models.IPMethod{
|
||||
{Name: "a"}, {Name: "b"},
|
||||
},
|
||||
}
|
||||
var m models.IPMethod
|
||||
m = c.next()
|
||||
assert.Equal(t, m, models.IPMethod{Name: "a"})
|
||||
m = c.next()
|
||||
assert.Equal(t, m, models.IPMethod{Name: "b"})
|
||||
m = c.next()
|
||||
assert.Equal(t, m, models.IPMethod{Name: "a"})
|
||||
}
|
||||
|
||||
func Test_next_RaceCondition(t *testing.T) {
|
||||
// Run with -race flag
|
||||
t.Parallel()
|
||||
const workers = 5
|
||||
const loopSize = 101
|
||||
c := &cyclerImpl{
|
||||
methods: []models.IPMethod{
|
||||
{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"},
|
||||
},
|
||||
}
|
||||
ready := make(chan struct{})
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
<-ready
|
||||
for i := 0; i < loopSize; i++ {
|
||||
c.next()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
close(ready)
|
||||
wg.Wait()
|
||||
assert.Equal(t, (workers*loopSize)%len(c.methods), c.counter)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateDDNSS(client network.Client, domain, host, username, password string, ip net.IP) error {
|
||||
var hostname string
|
||||
if host == "@" {
|
||||
hostname = strings.ToLower(domain)
|
||||
} else {
|
||||
hostname = strings.ToLower(host + "." + domain)
|
||||
}
|
||||
url := fmt.Sprintf("http://www.ddnss.de/upd.php?user=%s&pwd=%s&host=%s", username, password, hostname)
|
||||
if ip != nil {
|
||||
if ip.To4() == nil { // ipv6
|
||||
url += fmt.Sprintf("&ip6=%s", ip)
|
||||
} else {
|
||||
url += fmt.Sprintf("&ip=%s", ip)
|
||||
}
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s := string(content)
|
||||
if status != http.StatusOK {
|
||||
return fmt.Errorf("received status %d with message: %s", status, s)
|
||||
}
|
||||
switch {
|
||||
case strings.Contains(s, "badysys"):
|
||||
return fmt.Errorf("ddnss.de: invalid system parameter")
|
||||
case strings.Contains(s, "badauth"):
|
||||
return fmt.Errorf("ddnss.de: bad authentication")
|
||||
case strings.Contains(s, "notfqdn"):
|
||||
return fmt.Errorf("ddnss.de: hostname %q does not exist", hostname)
|
||||
case strings.Contains(s, "Updated 1 hostname"):
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown response received from ddnss.de: %s", s)
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateDNSPod(client network.Client, domain, host, token string, ip net.IP) (err error) {
|
||||
if ip == nil {
|
||||
return fmt.Errorf("IP address was not given to updater")
|
||||
}
|
||||
body := bytes.NewBufferString(url.Values{
|
||||
"login_token": []string{token},
|
||||
"format": []string{"json"},
|
||||
"domain": []string{domain},
|
||||
"length": []string{"200"},
|
||||
"sub_domain": []string{host},
|
||||
"record_type": []string{"A"},
|
||||
}.Encode())
|
||||
req, err := http.NewRequest(http.MethodPost, "https://dnsapi.cn/Record.List", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
status, content, err := client.DoHTTPRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var recordResp struct {
|
||||
Records []struct {
|
||||
ID string `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Line string `json:"line"`
|
||||
} `json:"records"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &recordResp); err != nil {
|
||||
return err
|
||||
}
|
||||
var recordID, recordLine string
|
||||
for _, record := range recordResp.Records {
|
||||
if record.Type == "A" && record.Name == host {
|
||||
receivedIP := net.ParseIP(record.Value)
|
||||
if ip.Equal(receivedIP) {
|
||||
return nil
|
||||
}
|
||||
recordID = record.ID
|
||||
recordLine = record.Line
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(recordID) == 0 {
|
||||
return fmt.Errorf("record not found")
|
||||
}
|
||||
body = bytes.NewBufferString(url.Values{
|
||||
"login_token": []string{token},
|
||||
"format": []string{"json"},
|
||||
"domain": []string{domain},
|
||||
"record_id": []string{recordID},
|
||||
"value": []string{ip.String()},
|
||||
"record_line": []string{recordLine},
|
||||
"sub_domain": []string{host},
|
||||
}.Encode())
|
||||
req, err = http.NewRequest(http.MethodPost, "https://dnsapi.cn/Record.Ddns", body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
status, content, err = client.DoHTTPRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var ddnsResp struct {
|
||||
Record struct {
|
||||
ID int64 `json:"id"`
|
||||
Value string `json:"value"`
|
||||
Name string `json:"name"`
|
||||
} `json:"record"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &ddnsResp); err != nil {
|
||||
return err
|
||||
}
|
||||
receivedIP := net.ParseIP(ddnsResp.Record.Value)
|
||||
if !ip.Equal(receivedIP) {
|
||||
return fmt.Errorf("ip not set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateDreamhost(client network.Client, domain, key, domainName string, ip net.IP) error {
|
||||
if ip == nil {
|
||||
return fmt.Errorf("IP address was not given to updater")
|
||||
}
|
||||
type dreamhostReponse struct {
|
||||
Result string `json:"result"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
// List records
|
||||
url := constants.DreamhostURL + "/?key=" + key + "&unique_id=" + uuid.New().String() + "&format=json&cmd=dns-list_records"
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhList struct {
|
||||
Result string `json:"result"`
|
||||
Data []struct {
|
||||
Editable string `json:"editable"`
|
||||
Type string `json:"type"`
|
||||
Record string `json:"record"`
|
||||
Value string `json:"value"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &dhList); err != nil {
|
||||
return err
|
||||
} else if dhList.Result != "success" {
|
||||
return fmt.Errorf(dhList.Result)
|
||||
}
|
||||
var oldIP net.IP
|
||||
for _, data := range dhList.Data {
|
||||
if data.Type == "A" && data.Record == domainName {
|
||||
if data.Editable == "0" {
|
||||
return fmt.Errorf("record data is not editable")
|
||||
}
|
||||
oldIP = net.ParseIP(data.Value)
|
||||
if ip.Equal(oldIP) { // success, nothing to change
|
||||
return nil
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if oldIP != nil { // Found editable record with a different IP address, so remove it
|
||||
url = constants.DreamhostURL + "?key=" + key + "&unique_id=" + uuid.New().String() + "&format=json&cmd=dns-remove_record&record=" + strings.ToLower(domain) + "&type=A&value=" + oldIP.String()
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, content, err = client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != "success" { // this should not happen
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
}
|
||||
// Create the right record
|
||||
url = constants.DreamhostURL + "?key=" + key + "&unique_id=" + uuid.New().String() + "&format=json&cmd=dns-add_record&record=" + strings.ToLower(domain) + "&type=A&value=" + ip.String()
|
||||
r, err = http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, content, err = client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
err = json.Unmarshal(content, &dhResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != "success" {
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
func updateDuckDNS(client network.Client, domain, token string, ip net.IP) (newIP net.IP, err error) {
|
||||
url := constants.DuckDNSURL + "?domains=" + strings.ToLower(domain) + "&token=" + token + "&verbose=true"
|
||||
if ip != nil {
|
||||
url += "&ip=" + ip.String()
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
s := string(content)
|
||||
switch {
|
||||
case len(s) < 2:
|
||||
return nil, fmt.Errorf("response %q is too short", s)
|
||||
case s[0:2] == "KO":
|
||||
return nil, fmt.Errorf("invalid domain token combination")
|
||||
case s[0:2] == "OK":
|
||||
ips := verification.NewVerifier().SearchIPv4(s)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
|
||||
}
|
||||
if ip != nil && !newIP.Equal(ip) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid response %q", s)
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
libnetwork "github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateGoDaddy(client libnetwork.Client, host, domain, key, secret string, ip net.IP) error {
|
||||
if ip == nil {
|
||||
return fmt.Errorf("IP address was not given to updater")
|
||||
}
|
||||
type goDaddyPutBody struct {
|
||||
Data string `json:"data"` // IP address to update to
|
||||
}
|
||||
URL := constants.GoDaddyURL + "/" + strings.ToLower(domain) + "/records/A/" + strings.ToLower(host)
|
||||
r, err := network.BuildHTTPPut(
|
||||
URL,
|
||||
[]goDaddyPutBody{
|
||||
goDaddyPutBody{
|
||||
ip.String(),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("Authorization", "sso-key "+key+":"+secret)
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
var parsedJSON struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &parsedJSON); err != nil {
|
||||
return err
|
||||
} else if len(parsedJSON.Message) > 0 {
|
||||
return fmt.Errorf("HTTP status %d - %s", status, parsedJSON.Message)
|
||||
}
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateInfomaniak(client network.Client, domain, host, username, password string, ip net.IP) (newIP net.IP, err error) {
|
||||
var hostname string
|
||||
if host == "@" {
|
||||
hostname = strings.ToLower(domain)
|
||||
} else {
|
||||
hostname = strings.ToLower(host + "." + domain)
|
||||
}
|
||||
url := fmt.Sprintf("https://%s:%s@infomaniak.com/nic/update?hostname=%s", username, password, hostname)
|
||||
if ip != nil {
|
||||
url += fmt.Sprintf("&myip=%s", ip)
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
switch {
|
||||
case strings.HasPrefix(s, "good "):
|
||||
newIP = net.ParseIP(s[5:])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("no received IP in response %q", s)
|
||||
} else if ip != nil && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("received IP %s is not equal to expected IP %s", newIP, ip)
|
||||
}
|
||||
return newIP, nil
|
||||
case strings.HasPrefix(s, "nochg "):
|
||||
newIP = net.ParseIP(s[6:])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("no received IP in response %q", s)
|
||||
} else if ip != nil && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("received IP %s is not equal to expected IP %s", newIP, ip)
|
||||
}
|
||||
return newIP, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("ok status but unknown response %q", s)
|
||||
}
|
||||
case http.StatusBadRequest:
|
||||
switch s {
|
||||
case "nohost":
|
||||
return nil, fmt.Errorf("infomaniak.com: host %q does not exist for domain %q", host, domain)
|
||||
case "badauth":
|
||||
return nil, fmt.Errorf("infomaniak.com: bad authentication")
|
||||
default:
|
||||
return nil, fmt.Errorf("infomaniak.com: bad request: %s", s)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("received status %d with message: %s", status, s)
|
||||
}
|
||||
}
|
||||
77
internal/update/ip.go
Normal file
77
internal/update/ip.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
libnet "github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
const cycle = "cycle"
|
||||
|
||||
type IPGetter interface {
|
||||
IP() (ip net.IP, err error)
|
||||
IPv4() (ip net.IP, err error)
|
||||
IPv6() (ip net.IP, err error)
|
||||
}
|
||||
|
||||
type ipGetter struct {
|
||||
client libnet.Client
|
||||
ipMethod models.IPMethod
|
||||
ipv4Method models.IPMethod
|
||||
ipv6Method models.IPMethod
|
||||
cyclerIP cycler
|
||||
cyclerIPv4 cycler
|
||||
cyclerIPv6 cycler
|
||||
}
|
||||
|
||||
func NewIPGetter(client libnet.Client, ipMethod, ipv4Method, ipv6Method models.IPMethod) IPGetter {
|
||||
ipMethods := []models.IPMethod{}
|
||||
ipv4Methods := []models.IPMethod{}
|
||||
ipv6Methods := []models.IPMethod{}
|
||||
for _, method := range constants.IPMethods() {
|
||||
switch {
|
||||
case method.IPv4 && method.IPv6:
|
||||
ipMethods = append(ipMethods, method)
|
||||
case method.IPv4:
|
||||
ipv4Methods = append(ipv4Methods, method)
|
||||
case method.IPv6:
|
||||
ipv6Methods = append(ipv6Methods, method)
|
||||
}
|
||||
}
|
||||
return &ipGetter{
|
||||
client: client,
|
||||
ipMethod: ipMethod,
|
||||
ipv4Method: ipv4Method,
|
||||
ipv6Method: ipv6Method,
|
||||
cyclerIP: newCycler(ipMethods),
|
||||
cyclerIPv4: newCycler(ipv4Methods),
|
||||
cyclerIPv6: newCycler(ipv6Methods),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ipGetter) IP() (ip net.IP, err error) {
|
||||
method := i.ipMethod
|
||||
if method.Name == cycle {
|
||||
method = i.cyclerIP.next()
|
||||
}
|
||||
return network.GetPublicIP(i.client, method.URL, constants.IPv4OrIPv6)
|
||||
}
|
||||
|
||||
func (i *ipGetter) IPv4() (ip net.IP, err error) {
|
||||
method := i.ipv4Method
|
||||
if method.Name == cycle {
|
||||
method = i.cyclerIPv4.next()
|
||||
}
|
||||
return network.GetPublicIP(i.client, method.URL, constants.IPv4)
|
||||
}
|
||||
|
||||
func (i *ipGetter) IPv6() (ip net.IP, err error) {
|
||||
method := i.ipv6Method
|
||||
if method.Name == cycle {
|
||||
method = i.cyclerIPv6.next()
|
||||
}
|
||||
return network.GetPublicIP(i.client, method.URL, constants.IPv6)
|
||||
}
|
||||
143
internal/update/ip_test.go
Normal file
143
internal/update/ip_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/network/mock_network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_NewIPGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := network.NewClient(time.Second)
|
||||
ipMethod := models.IPMethod{Name: "ip"}
|
||||
ipv4Method := models.IPMethod{Name: "ipv4"}
|
||||
ipv6Method := models.IPMethod{Name: "ipv6"}
|
||||
ipGetter := NewIPGetter(client, ipMethod, ipv4Method, ipv6Method)
|
||||
assert.NotNil(t, ipGetter)
|
||||
}
|
||||
|
||||
func Test_IP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
mockContent []byte
|
||||
ip net.IP
|
||||
}{
|
||||
"url ipv4": {
|
||||
ipMethod: models.IPMethod{URL: "https://opendns.com/ip"},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
"url ipv6": {
|
||||
ipMethod: models.IPMethod{URL: "https://opendns.com/ip"},
|
||||
mockContent: []byte("blabla ad07:e846:51ac:6cd0:0000:0000:0000:0000 sds"),
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"cycle": {
|
||||
ipMethod: models.IPMethod{Name: cycle},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
}
|
||||
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)
|
||||
url := tc.ipMethod.URL
|
||||
if tc.ipMethod.Name == cycle {
|
||||
url = "https://diagnostic.opendns.com/myip"
|
||||
}
|
||||
client.EXPECT().GetContent(url).Return(tc.mockContent, http.StatusOK, nil).Times(1)
|
||||
ig := NewIPGetter(client, tc.ipMethod, models.IPMethod{}, models.IPMethod{})
|
||||
ip, err := ig.IP()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IPv4(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
mockContent []byte
|
||||
ip net.IP
|
||||
}{
|
||||
"url": {
|
||||
ipMethod: models.IPMethod{URL: "https://opendns.com/ip"},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
"cycle": {
|
||||
ipMethod: models.IPMethod{Name: cycle},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
}
|
||||
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)
|
||||
url := tc.ipMethod.URL
|
||||
if tc.ipMethod.Name == cycle {
|
||||
url = "https://api.ipify.org"
|
||||
}
|
||||
client.EXPECT().GetContent(url).Return(tc.mockContent, http.StatusOK, nil).Times(1)
|
||||
ig := NewIPGetter(client, models.IPMethod{}, tc.ipMethod, models.IPMethod{})
|
||||
ip, err := ig.IPv4()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IPv6(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
mockContent []byte
|
||||
ip net.IP
|
||||
}{
|
||||
"url": {
|
||||
ipMethod: models.IPMethod{URL: "https://ip6.ddnss.de/meineip.php"},
|
||||
mockContent: []byte("blabla ad07:e846:51ac:6cd0:0000:0000:0000:0000 sds"),
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"cycle": {
|
||||
ipMethod: models.IPMethod{Name: cycle},
|
||||
mockContent: []byte("blabla ad07:e846:51ac:6cd0:0000:0000:0000:0000 sds"),
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
}
|
||||
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)
|
||||
url := tc.ipMethod.URL
|
||||
if tc.ipMethod.Name == cycle {
|
||||
url = "https://api6.ipify.org"
|
||||
}
|
||||
client.EXPECT().GetContent(url).Return(tc.mockContent, http.StatusOK, nil).Times(1)
|
||||
ig := NewIPGetter(client, models.IPMethod{}, models.IPMethod{}, tc.ipMethod)
|
||||
ip, err := ig.IPv6()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateNamecheap(client network.Client, host, domain, password string, ip net.IP) (newIP net.IP, err error) {
|
||||
url := constants.NamecheapURL + "?host=" + strings.ToLower(host) + "&domain=" + strings.ToLower(domain) + "&password=" + password
|
||||
if ip != nil {
|
||||
url += "&ip=" + ip.String()
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var parsedXML struct {
|
||||
Errors struct {
|
||||
Error string `xml:"Err1"`
|
||||
} `xml:"errors"`
|
||||
IP string `xml:"IP"`
|
||||
}
|
||||
err = xml.Unmarshal(content, &parsedXML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if parsedXML.Errors.Error != "" {
|
||||
return nil, fmt.Errorf(parsedXML.Errors.Error)
|
||||
}
|
||||
newIP = net.ParseIP(parsedXML.IP)
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", parsedXML.IP)
|
||||
}
|
||||
if ip != nil && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
func updateNoIP(client network.Client, hostname, username, password string, ip net.IP) (newIP net.IP, err error) {
|
||||
url := constants.NoIPURL + "?hostname=" + strings.ToLower(hostname)
|
||||
if ip != nil {
|
||||
url += "&myip=" + ip.String()
|
||||
}
|
||||
url = strings.Replace(url, "https://", "https://"+username+":"+password+"@", 1)
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(content)
|
||||
switch s {
|
||||
case "":
|
||||
return nil, fmt.Errorf("HTTP status %d", status)
|
||||
case "911":
|
||||
return nil, fmt.Errorf("NoIP's internal server error 911")
|
||||
case "abuse":
|
||||
return nil, fmt.Errorf("username is banned due to abuse")
|
||||
case "!donator":
|
||||
return nil, fmt.Errorf("user has not this extra feature")
|
||||
case "badagent":
|
||||
return nil, fmt.Errorf("user agent is banned")
|
||||
case "badauth":
|
||||
return nil, fmt.Errorf("invalid username password combination")
|
||||
case "nohost":
|
||||
return nil, fmt.Errorf("hostname does not exist")
|
||||
}
|
||||
if strings.Contains(s, "nochg") || strings.Contains(s, "good") {
|
||||
ips := verification.NewVerifier().SearchIPv4(s)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no IP address in response")
|
||||
}
|
||||
newIP = net.ParseIP(ips[0])
|
||||
if newIP == nil {
|
||||
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
|
||||
}
|
||||
if ip != nil && !ip.Equal(newIP) {
|
||||
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
|
||||
}
|
||||
return newIP, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid response %q", s)
|
||||
}
|
||||
240
internal/update/run.go
Normal file
240
internal/update/run.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
librecords "github.com/qdm12/ddns-updater/internal/records"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Run(ctx context.Context, period time.Duration) (forceUpdate func())
|
||||
}
|
||||
|
||||
type runner struct {
|
||||
db data.Database
|
||||
updater Updater
|
||||
netLookupIP func(hostname string) ([]net.IP, error)
|
||||
ipGetter IPGetter
|
||||
logger logging.Logger
|
||||
timeNow func() time.Time
|
||||
}
|
||||
|
||||
func NewRunner(db data.Database, updater Updater, ipGetter IPGetter, logger logging.Logger, timeNow func() time.Time) Runner {
|
||||
return &runner{
|
||||
db: db,
|
||||
updater: updater,
|
||||
netLookupIP: net.LookupIP,
|
||||
ipGetter: ipGetter,
|
||||
logger: logger,
|
||||
timeNow: timeNow,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *runner) lookupIPs(hostname string) (ipv4 net.IP, ipv6 net.IP, err error) {
|
||||
ips, err := r.netLookupIP(hostname)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if ip.To4() == nil {
|
||||
ipv6 = ip
|
||||
} else {
|
||||
ipv4 = ip
|
||||
}
|
||||
}
|
||||
return ipv4, ipv6, nil
|
||||
}
|
||||
|
||||
func doIPVersion(records []librecords.Record) (doIP, doIPv4, doIPv6 bool) {
|
||||
for _, record := range records {
|
||||
switch record.Settings.IPVersion() {
|
||||
case constants.IPv4OrIPv6:
|
||||
doIP = true
|
||||
case constants.IPv4:
|
||||
doIPv4 = true
|
||||
case constants.IPv6:
|
||||
doIPv6 = true
|
||||
}
|
||||
if doIP && doIPv4 && doIPv6 {
|
||||
return true, true, true
|
||||
}
|
||||
}
|
||||
return doIP, doIPv4, doIPv6
|
||||
}
|
||||
|
||||
func (r *runner) getNewIPs(doIP, doIPv4, doIPv6 bool) (ip, ipv4, ipv6 net.IP, errors []error) {
|
||||
var err error
|
||||
if doIP {
|
||||
ip, err = r.ipGetter.IP()
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
if doIPv4 {
|
||||
ipv4, err = r.ipGetter.IPv4()
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
if doIPv6 {
|
||||
ipv6, err = r.ipGetter.IPv6()
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
return ip, ipv4, ipv6, errors
|
||||
}
|
||||
|
||||
func (r *runner) getRecordIDsToUpdate(records []librecords.Record, ip, ipv4, ipv6 net.IP) (recordIDs map[int]struct{}) {
|
||||
recordIDs = make(map[int]struct{})
|
||||
for id, record := range records {
|
||||
if shouldUpdate := r.shouldUpdateRecord(record, ip, ipv4, ipv6); shouldUpdate {
|
||||
recordIDs[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
return recordIDs
|
||||
}
|
||||
|
||||
func (r *runner) shouldUpdateRecord(record librecords.Record, ip, ipv4, ipv6 net.IP) (update bool) {
|
||||
hostname := record.Settings.BuildDomainName()
|
||||
ipVersion := record.Settings.IPVersion()
|
||||
if !record.Settings.DNSLookup() {
|
||||
lastIP := record.History.GetCurrentIP() // can be nil
|
||||
return r.shouldUpdateRecordNoLookup(hostname, ipVersion, lastIP, ip, ipv4, ipv6)
|
||||
}
|
||||
return r.shouldUpdateRecordWithLookup(hostname, ipVersion, ip, ipv4, ipv6)
|
||||
}
|
||||
|
||||
func (r *runner) shouldUpdateRecordNoLookup(hostname string, ipVersion models.IPVersion, lastIP, ip, ipv4, ipv6 net.IP) (update bool) {
|
||||
switch ipVersion {
|
||||
case constants.IPv4OrIPv6:
|
||||
if ip != nil && !ip.Equal(lastIP) {
|
||||
r.logger.Info("Last IP address stored for %s is %s and your IP address is %s", hostname, lastIP, ip)
|
||||
return true
|
||||
}
|
||||
case constants.IPv4:
|
||||
if ipv4 != nil && !ipv4.Equal(lastIP) {
|
||||
r.logger.Info("Last IPv4 address stored for %s is %s and your IPv4 address is %s", hostname, lastIP, ip)
|
||||
return true
|
||||
}
|
||||
case constants.IPv6:
|
||||
if ipv6 != nil && !ipv6.Equal(lastIP) {
|
||||
r.logger.Info("Last IPv6 address stored for %s is %s and your IPv6 address is %s", hostname, lastIP, ip)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *runner) shouldUpdateRecordWithLookup(hostname string, ipVersion models.IPVersion, ip, ipv4, ipv6 net.IP) (update bool) {
|
||||
recordIPv4, recordIPv6, err := r.lookupIPs(hostname)
|
||||
if err != nil {
|
||||
r.logger.Warn(err) // update anyway
|
||||
}
|
||||
switch ipVersion {
|
||||
case constants.IPv4OrIPv6:
|
||||
if ip != nil && !ip.Equal(recordIPv4) && !ip.Equal(recordIPv6) {
|
||||
recordIP := recordIPv4
|
||||
if ip.To4() == nil {
|
||||
recordIP = recordIPv6
|
||||
}
|
||||
r.logger.Info("IP address of %s is %s and your IP address is %s", hostname, recordIP, ip)
|
||||
return true
|
||||
}
|
||||
case constants.IPv4:
|
||||
if ipv4 != nil && !ipv4.Equal(recordIPv4) {
|
||||
r.logger.Info("IPv4 address of %s is %s and your IPv4 address is %s", hostname, recordIPv4, ipv4)
|
||||
return true
|
||||
}
|
||||
case constants.IPv6:
|
||||
if ipv6 != nil && !ipv6.Equal(recordIPv6) {
|
||||
r.logger.Info("IPv6 address of %s is %s and your IPv6 address is %s", hostname, recordIPv6, ipv6)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getIPMatchingVersion(ip, ipv4, ipv6 net.IP, ipVersion models.IPVersion) net.IP {
|
||||
switch ipVersion {
|
||||
case constants.IPv4OrIPv6:
|
||||
return ip
|
||||
case constants.IPv4:
|
||||
return ipv4
|
||||
case constants.IPv6:
|
||||
return ipv6
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setInitialUpToDateStatus(db data.Database, id int, updateIP net.IP, now time.Time) error {
|
||||
record, err := db.Select(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.Status = constants.UPTODATE
|
||||
record.Time = now
|
||||
if record.History.GetCurrentIP() == nil {
|
||||
record.History = append(record.History, models.HistoryEvent{
|
||||
IP: updateIP,
|
||||
Time: now,
|
||||
})
|
||||
}
|
||||
return db.Update(id, record)
|
||||
}
|
||||
|
||||
func (r *runner) updateNecessary() {
|
||||
records := r.db.SelectAll()
|
||||
doIP, doIPv4, doIPv6 := doIPVersion(records)
|
||||
ip, ipv4, ipv6, errors := r.getNewIPs(doIP, doIPv4, doIPv6)
|
||||
for _, err := range errors {
|
||||
r.logger.Error(err)
|
||||
}
|
||||
recordIDs := r.getRecordIDsToUpdate(records, ip, ipv4, ipv6)
|
||||
now := r.timeNow()
|
||||
for id, record := range records {
|
||||
_, requireUpdate := recordIDs[id]
|
||||
if requireUpdate || record.Status != constants.UNSET {
|
||||
continue
|
||||
}
|
||||
updateIP := getIPMatchingVersion(ip, ipv4, ipv6, record.Settings.IPVersion())
|
||||
if err := setInitialUpToDateStatus(r.db, id, updateIP, now); err != nil {
|
||||
r.logger.Error(err)
|
||||
}
|
||||
}
|
||||
for id := range recordIDs {
|
||||
record := records[id]
|
||||
updateIP := getIPMatchingVersion(ip, ipv4, ipv6, record.Settings.IPVersion())
|
||||
r.logger.Info("Updating record %s to use %s", record.Settings, updateIP)
|
||||
if err := r.updater.Update(id, updateIP, r.timeNow()); err != nil {
|
||||
r.logger.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *runner) Run(ctx context.Context, period time.Duration) (forceUpdate func()) {
|
||||
timer := time.NewTicker(period)
|
||||
forceChannel := make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
r.updateNecessary()
|
||||
case <-forceChannel:
|
||||
r.updateNecessary()
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return func() {
|
||||
forceChannel <- struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -1,214 +1,62 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
libnetwork "github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
)
|
||||
|
||||
type Updater interface {
|
||||
Update(id int) error
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
client libnetwork.Client
|
||||
notify notifyFunc
|
||||
verifier verification.Verifier
|
||||
ipMethods []models.IPMethod
|
||||
counter int
|
||||
counterMutex sync.RWMutex
|
||||
}
|
||||
|
||||
type notifyFunc func(priority int, messageArgs ...interface{})
|
||||
|
||||
func NewUpdater(db data.Database, logger logging.Logger, client libnetwork.Client, notify notifyFunc) Updater {
|
||||
return &updater{
|
||||
db: db,
|
||||
logger: logger,
|
||||
client: client,
|
||||
notify: notify,
|
||||
verifier: verification.NewVerifier(),
|
||||
ipMethods: constants.IPMethodExternalChoices(),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) Update(id int) error {
|
||||
record, err := u.db.Select(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.Time = time.Now()
|
||||
record.Status = constants.UPDATING
|
||||
if err := u.db.Update(id, record); err != nil {
|
||||
return err
|
||||
}
|
||||
status, message, newIP, err := u.update(
|
||||
record.Settings,
|
||||
record.History.GetCurrentIP(),
|
||||
record.History.GetDurationSinceSuccess(time.Now()))
|
||||
record.Status = status
|
||||
record.Message = message
|
||||
if err != nil {
|
||||
if len(record.Message) == 0 {
|
||||
record.Message = err.Error()
|
||||
}
|
||||
if updateErr := u.db.Update(id, record); updateErr != nil {
|
||||
return fmt.Errorf("%s, %s", err, updateErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if newIP != nil {
|
||||
record.History = append(record.History, models.HistoryEvent{
|
||||
IP: newIP,
|
||||
Time: time.Now(),
|
||||
})
|
||||
u.notify(1, fmt.Sprintf("%s %s", record.Settings.BuildDomainName(), message))
|
||||
}
|
||||
return u.db.Update(id, record) // persists some data if needed (i.e new IP)
|
||||
}
|
||||
|
||||
func (u *updater) update(settings models.Settings, currentIP net.IP, durationSinceSuccess string) (status models.Status, message string, newIP net.IP, err error) {
|
||||
// Get the public IP address
|
||||
ip, err := u.getPublicIP(settings.IPMethod, settings.IPVersion) // Note: empty IP means DNS provider provided
|
||||
if err != nil {
|
||||
return constants.FAIL, "", nil, err
|
||||
}
|
||||
if ip != nil && ip.Equal(currentIP) {
|
||||
return constants.UPTODATE, fmt.Sprintf("No IP change for %s", durationSinceSuccess), nil, nil
|
||||
}
|
||||
|
||||
// Update the record
|
||||
switch settings.Provider {
|
||||
case constants.NAMECHEAP:
|
||||
ip, err = updateNamecheap(
|
||||
u.client,
|
||||
settings.Host,
|
||||
settings.Domain,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.GODADDY:
|
||||
err = updateGoDaddy(
|
||||
u.client,
|
||||
settings.Host,
|
||||
settings.Domain,
|
||||
settings.Key,
|
||||
settings.Secret,
|
||||
ip,
|
||||
)
|
||||
case constants.DUCKDNS:
|
||||
ip, err = updateDuckDNS(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Token,
|
||||
ip,
|
||||
)
|
||||
case constants.DREAMHOST:
|
||||
err = updateDreamhost(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Key,
|
||||
settings.BuildDomainName(),
|
||||
ip,
|
||||
)
|
||||
case constants.CLOUDFLARE:
|
||||
err = updateCloudflare(
|
||||
u.client,
|
||||
settings.ZoneIdentifier,
|
||||
settings.Identifier,
|
||||
settings.Host,
|
||||
settings.Email,
|
||||
settings.Key,
|
||||
settings.UserServiceKey,
|
||||
settings.Token,
|
||||
settings.Proxied,
|
||||
settings.Ttl,
|
||||
ip,
|
||||
)
|
||||
case constants.NOIP:
|
||||
ip, err = updateNoIP(
|
||||
u.client,
|
||||
settings.BuildDomainName(),
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.DNSPOD:
|
||||
err = updateDNSPod(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Token,
|
||||
ip,
|
||||
)
|
||||
case constants.INFOMANIAK:
|
||||
ip, err = updateInfomaniak(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.DDNSSDE:
|
||||
err = updateDDNSS(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
default:
|
||||
err = fmt.Errorf("provider %q is not supported", settings.Provider)
|
||||
}
|
||||
if err != nil {
|
||||
return constants.FAIL, "", nil, err
|
||||
}
|
||||
if ip != nil && ip.Equal(currentIP) {
|
||||
return constants.UPTODATE, fmt.Sprintf("No IP change for %s", durationSinceSuccess), nil, nil
|
||||
}
|
||||
return constants.SUCCESS, fmt.Sprintf("changed to %s", ip.String()), ip, nil
|
||||
}
|
||||
|
||||
func (u *updater) incCounter() (value int) {
|
||||
u.counterMutex.Lock()
|
||||
defer u.counterMutex.Unlock()
|
||||
value = u.counter
|
||||
u.counter++
|
||||
return value
|
||||
}
|
||||
|
||||
func (u *updater) getPublicIP(IPMethod models.IPMethod, IPVersion models.IPVersion) (ip net.IP, err error) {
|
||||
var url string
|
||||
switch {
|
||||
case IPMethod == constants.PROVIDER:
|
||||
return nil, nil
|
||||
case strings.HasPrefix(string(IPMethod), "https://"):
|
||||
// Custom URL provided
|
||||
url = string(IPMethod)
|
||||
case IPMethod == constants.CYCLE:
|
||||
i := u.incCounter() % len(u.ipMethods)
|
||||
url = constants.IPMethodMapping()[u.ipMethods[i]]
|
||||
default:
|
||||
var ok bool
|
||||
url, ok = constants.IPMethodMapping()[IPMethod]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("IP method %q not supported", IPMethod)
|
||||
}
|
||||
}
|
||||
return network.GetPublicIP(u.client, url, IPVersion)
|
||||
}
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
netlib "github.com/qdm12/golibs/network"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
type Updater interface {
|
||||
Update(id int, ip net.IP, now time.Time) (err error)
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
db data.Database
|
||||
client netlib.Client
|
||||
notify notifyFunc
|
||||
}
|
||||
|
||||
type notifyFunc func(priority int, messageArgs ...interface{})
|
||||
|
||||
func NewUpdater(db data.Database, client netlib.Client, notify notifyFunc) Updater {
|
||||
return &updater{
|
||||
db: db,
|
||||
client: client,
|
||||
notify: notify,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) Update(id int, ip net.IP, now time.Time) (err error) {
|
||||
record, err := u.db.Select(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.Time = now
|
||||
record.Status = constants.UPDATING
|
||||
if err := u.db.Update(id, record); err != nil {
|
||||
return err
|
||||
}
|
||||
record.Status = constants.FAIL
|
||||
newIP, err := record.Settings.Update(u.client, ip)
|
||||
if err != nil {
|
||||
record.Message = err.Error()
|
||||
if updateErr := u.db.Update(id, record); updateErr != nil {
|
||||
return fmt.Errorf("%s, %s", err, updateErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
record.Status = constants.SUCCESS
|
||||
record.Message = fmt.Sprintf("changed to %s", ip.String())
|
||||
record.History = append(record.History, models.HistoryEvent{
|
||||
IP: newIP,
|
||||
Time: now,
|
||||
})
|
||||
u.notify(1, fmt.Sprintf("%s %s", record.Settings.BuildDomainName(), record.Message))
|
||||
return u.db.Update(id, record) // persists some data if needed (i.e new IP)
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_incCounter(t *testing.T) {
|
||||
t.Parallel()
|
||||
const initValue = 100
|
||||
u := &updater{
|
||||
counter: initValue,
|
||||
}
|
||||
counter := u.incCounter()
|
||||
assert.Equal(t, initValue, counter)
|
||||
counter = u.incCounter()
|
||||
assert.Equal(t, initValue+1, counter)
|
||||
}
|
||||
|
||||
func Test_getPublicIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
IPMethod models.IPMethod
|
||||
mockURL string
|
||||
mockContent []byte
|
||||
ip net.IP
|
||||
err error
|
||||
}{
|
||||
"bad IP method": {
|
||||
IPMethod: "abc",
|
||||
err: fmt.Errorf("IP method \"abc\" not supported"),
|
||||
},
|
||||
"provider IP method": {
|
||||
IPMethod: constants.PROVIDER,
|
||||
},
|
||||
"OpenDNS IP method": {
|
||||
IPMethod: constants.OPENDNS,
|
||||
mockURL: constants.IPMethodMapping()[constants.OPENDNS],
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
"Custom URL IP method": {
|
||||
IPMethod: models.IPMethod("https://ipinfo.io/ip"),
|
||||
mockURL: "https://ipinfo.io/ip",
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
"Cycle IP method": {
|
||||
IPMethod: constants.CYCLE,
|
||||
mockURL: constants.IPMethodMapping()[constants.OPENDNS],
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := &mocks.Client{}
|
||||
if len(tc.mockURL) != 0 {
|
||||
client.On("GetContent", tc.mockURL).Return(
|
||||
tc.mockContent, http.StatusOK, nil).Once()
|
||||
}
|
||||
u := &updater{
|
||||
client: client,
|
||||
ipMethods: []models.IPMethod{constants.OPENDNS, constants.IPINFO},
|
||||
}
|
||||
ip, err := u.getPublicIP(tc.IPMethod, constants.IPv4)
|
||||
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))
|
||||
client.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
<th>Domain</th>
|
||||
<th>Host</th>
|
||||
<th>Provider</th>
|
||||
<th>IP method</th>
|
||||
<th>IP version</th>
|
||||
<th>Update status</th>
|
||||
<th>Set IP</th>
|
||||
<th>Previous IPs (reverse chronological order)</th>
|
||||
@@ -62,7 +62,7 @@
|
||||
<td>{{.Domain}}</td>
|
||||
<td>{{.Host}}</td>
|
||||
<td>{{.Provider}}</td>
|
||||
<td>{{.IPMethod}}</td>
|
||||
<td>{{.IPVersion}}</td>
|
||||
<td>{{.Status}}</td>
|
||||
<td>{{.CurrentIP}}</td>
|
||||
<td>{{.PreviousIPs}}</td>
|
||||
|
||||
Reference in New Issue
Block a user