3 Commits

Author SHA1 Message Date
Quentin McGaw
fe294f52a9 Add healthcheck triggered log line 2020-09-29 22:44:07 +00:00
Quentin McGaw
62e700c82c Alpine image, refers to #100 2020-09-29 22:44:07 +00:00
Quentin McGaw
5d0e8548a1 Healthcheck listens on all interfaces, see #100 2020-09-29 22:44:06 +00:00
145 changed files with 2191 additions and 7302 deletions

View File

@@ -43,6 +43,53 @@
"usePlaceholders": false
},
"go.lintTool": "golangci-lint",
"go.lintFlags": [
"--fast",
"--enable",
"rowserrcheck",
"--enable",
"bodyclose",
"--enable",
"dogsled",
"--enable",
"dupl",
"--enable",
"gochecknoglobals",
"--enable",
"gochecknoinits",
"--enable",
"gocognit",
"--enable",
"goconst",
"--enable",
"gocritic",
"--enable",
"gocyclo",
"--enable",
"goimports",
"--enable",
"golint",
"--enable",
"gosec",
"--enable",
"interfacer",
"--enable",
"maligned",
"--enable",
"misspell",
"--enable",
"nakedret",
"--enable",
"prealloc",
"--enable",
"scopelint",
"--enable",
"unconvert",
"--enable",
"unparam",
"--enable",
"whitespace"
],
// Golang on save
"go.buildOnSave": "workspace",
"go.lintOnSave": "workspace",
@@ -64,6 +111,6 @@
"go.testFlags": [
"-v"
],
"go.testTimeout": "5s"
"go.testTimeout": "600s"
}
}

View File

@@ -5,8 +5,8 @@ services:
image: qmcgaw/godevcontainer
volumes:
- ../:/workspace
- ~/.ssh:/home/vscode/.ssh
- ~/.ssh:/root/.ssh
- ~/.ssh:/home/vscode/.ssh:ro
- ~/.ssh:/root/.ssh:ro
- /var/run/docker.sock:/var/run/docker.sock
cap_add:
- SYS_PTRACE

View File

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

View File

@@ -7,26 +7,46 @@ assignees: qdm12
---
<!--
YOU CAN CHAT THERE EVENTUALLY:
https://github.com/qdm12/ddns-updater/discussions
-->
**TLDR**: *Describe your issue in a one liner here*
1. Is this urgent: Yes/No
2. DNS provider(s) you use: Answer here
3. Program version:
1. Is this urgent?
<!-- See the line at the top of your logs -->
- [ ] 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-compose
5. Extra information (optional)
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:

View File

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

View File

@@ -7,26 +7,46 @@ assignees:
---
<!--
HAVE A CHAT FIRST!
https://github.com/qdm12/ddns-updater/discussions
-->
**TLDR**: *Describe your issue in a one liner here*
1. Is this urgent: Yes/No
2. DNS provider(s) you use: Answer here
3. Program version:
1. Is this urgent?
<!-- See the line at the top of your logs -->
- [ ] 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-compose
5. Extra information (optional)
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:
@@ -34,9 +54,9 @@ Logs:
```
Configuration file (**remove your credentials!**):
Configuration file:
```json
```yml
```

View File

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

47
.github/workflows/buildx-branch.yml vendored Normal file
View 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

44
.github/workflows/buildx-latest.yml vendored Normal file
View File

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

44
.github/workflows/buildx-release.yml vendored Normal file
View File

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

View File

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

3
.gitignore vendored
View File

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

View File

@@ -4,80 +4,33 @@ linters-settings:
misspell:
locale: US
issues:
exclude-rules:
- path: cmd/
text: buildInfo is a global variable
linters:
- gochecknoglobals
- path: cmd/
text: commit is a global variable
linters:
- gochecknoglobals
- path: cmd/
text: buildDate is a global variable
linters:
- gochecknoglobals
- path: cmd/updater/main.go
text: "mnd: Magic number: 4, in <argument> detected"
linters:
- gomnd
- path: cmd/updater/main.go
text: "mnd: Magic number: 2, in <argument> detected"
linters:
- gomnd
- path: internal/regex/regex.go
text: "regexpMust: for const patterns like "
linters:
- gocritic
- path: internal/settings/
linters:
- maligned
- dupl
- path: pkg/publicip/.*_test.go
linters:
- dupl
linters:
disable-all: true
enable:
- asciicheck
- bodyclose
- deadcode
- dogsled
- dupl
- errcheck
- exhaustive
- exportloopref
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- goheader
- goimports
- golint
- gomnd
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- interfacer
- lll
- maligned
- misspell
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
- rowserrcheck
- scopelint
- sqlclosecheck
- staticcheck
- structcheck
- typecheck

88
.vscode/settings.json vendored Normal file
View File

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

View File

@@ -1,73 +1,41 @@
ARG ALPINE_VERSION=3.13
ARG GO_VERSION=1.16
ARG BUILDPLATFORM=linux/amd64
ARG ALPINE_VERSION=3.12
ARG GO_VERSION=1.15
FROM --platform=$BUILDPLATFORM alpine:${ALPINE_VERSION} AS alpine
FROM alpine:${ALPINE_VERSION} AS alpine
RUN apk --update add ca-certificates tzdata
RUN mkdir /tmp/data && \
chown 1000 /tmp/data && \
chmod 700 /tmp/data
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS base
ENV CGO_ENABLED=0
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
ARG GOLANGCI_LINT_VERSION=v1.31.0
RUN apk --update add git
ENV CGO_ENABLED=0
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
WORKDIR /tmp/gobuild
# Copy repository code and install Go dependencies
COPY .golangci.yml .
COPY go.mod go.sum ./
RUN go mod download
COPY pkg/ ./pkg/
COPY cmd/ ./cmd/
COPY internal/ ./internal/
FROM --platform=$BUILDPLATFORM base AS test
ENV CGO_ENABLED=1
# g++ is installed for the -race detector in go test
RUN apk --update add g++
FROM --platform=$BUILDPLATFORM base AS lint
ARG GOLANGCI_LINT_VERSION=v1.37.0
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
sh -s -- -b /usr/local/bin ${GOLANGCI_LINT_VERSION}
COPY .golangci.yml ./
COPY cmd/updater/main.go .
RUN go test ./...
RUN go build -trimpath -ldflags="-s -w" -o app
RUN golangci-lint run --timeout=10m
FROM --platform=$BUILDPLATFORM base AS tidy
RUN git init && \
git config user.email ci@localhost && \
git config user.name ci && \
git add -A && git commit -m ci && \
sed -i '/\/\/ indirect/d' go.mod && \
go mod tidy && \
git diff --exit-code -- go.mod
FROM --platform=$BUILDPLATFORM base AS build
COPY --from=qmcgaw/xcputranslate:v0.4.0 /xcputranslate /usr/local/bin/xcputranslate
ARG TARGETPLATFORM
ARG VERSION=unknown
ARG BUILD_DATE="an unknown date"
ARG COMMIT=unknown
RUN GOARCH="$(xcputranslate -targetplatform ${TARGETPLATFORM} -field arch)" \
GOARM="$(xcputranslate -targetplatform ${TARGETPLATFORM} -field arm)" \
go build -trimpath -ldflags="-s -w \
-X 'main.version=$VERSION' \
-X 'main.buildDate=$BUILD_DATE' \
-X 'main.commit=$COMMIT' \
" -o app cmd/updater/main.go
FROM scratch
ARG VERSION=unknown
ARG BUILD_DATE="an unknown date"
ARG COMMIT=unknown
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.version=$VERSION \
org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.revision=$COMMIT \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.url="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
org.opencontainers.image.title="ddns-updater" \
org.opencontainers.image.description="Universal DNS updater with WebUI"
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
@@ -78,10 +46,9 @@ ENV \
# Core
CONFIG= \
PERIOD=5m \
UPDATE_COOLDOWN_PERIOD=5m \
IP_METHOD=all \
IPV4_METHOD=all \
IPV6_METHOD=all \
IP_METHOD=cycle \
IPV4_METHOD=cycle \
IPV6_METHOD=cycle \
HTTP_TIMEOUT=10s \
# Web UI
@@ -93,11 +60,11 @@ ENV \
BACKUP_DIRECTORY=/updater/data \
# Other
LOG_ENCODING=console \
LOG_LEVEL=info \
LOG_CALLER=hidden \
NODE_ID=-1 \
GOTIFY_URL= \
GOTIFY_TOKEN= \
TZ=
COPY --from=alpine --chown=1000 /tmp/data /updater/data/
COPY --from=build --chown=1000 /tmp/gobuild/app /updater/app
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
COPY --chown=1000 ui/* /updater/ui/

305
README.md
View File

@@ -1,8 +1,8 @@
# Lightweight universal DDNS Updater with Docker and web UI
*Light container updating DNS A and/or AAAA records periodically for multiple DNS providers*
*Light container updating DNS A records periodically for Cloudflare, DDNSS.de, DonDominio, DNSPod, Dreamhost, DuckDNS, DynDNS, GoDaddy, Google, He.net, Infomaniak, Namecheap and NoIP*
<img height="200" alt="DDNS Updater logo" src="https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/ddnsgopher.svg?sanitize=true">
[![DDNS Updater by Quentin McGaw](https://github.com/qdm12/ddns-updater/raw/master/readme/title.png)](https://hub.docker.com/r/qmcgaw/ddns-updater)
[![Build status](https://github.com/qdm12/ddns-updater/workflows/Buildx%20latest/badge.svg)](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
[![Docker Pulls](https://img.shields.io/docker/pulls/qmcgaw/ddns-updater.svg)](https://hub.docker.com/r/qmcgaw/ddns-updater)
@@ -17,31 +17,7 @@
## Features
- Updates periodically A records for different DNS providers:
- Cloudflare
- DDNSS.de
- DigitalOcean
- DonDominio
- DNSOMatic
- DNSPod
- Dreamhost
- DuckDNS
- DynDNS
- FreeDNS
- Gandi
- GoDaddy
- Google
- He.net
- Infomaniak
- Linode
- LuaDNS
- Namecheap
- NoIP
- OpenDNS
- OVH
- Selfhost.de
- Strato.de
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
- 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
![Web UI](https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/webui.png)
@@ -55,7 +31,51 @@
## Setup
The program reads the configuration from a JSON object, either from a file or from an environment variable.
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:
@@ -72,31 +92,13 @@ The program reads the configuration from a JSON object, either from a file or fr
*(You could change the user ID, for example with `1001`, by running the container with `--user=1001`)*
1. Write a JSON configuration in *data/config.json*, for example:
```json
{
"settings": [
{
"provider": "namecheap",
"domain": "example.com",
"host": "@",
"password": "e5322165c1d74692bfa6d807100c0310"
}
]
}
```
You can find more information in the [configuration section](#configuration) to customize it.
1. Run the container with
1. Place your JSON configuration in `data/config.json`
1. Use the following command:
```sh
docker run -d -p 8000:8000/tcp -v "$(pwd)"/data:/updater/data qmcgaw/ddns-updater
```
1. (Optional) You can also set your JSON configuration as a single environment variable line (i.e. `{"settings": [{"provider": "namecheap", ...}]}`), which takes precedence over config.json. Note however that if you don't bind mount the `/updater/data` directory, there won't be a persistent database file `/updater/updates.json` but it will still work.
### Next steps
You can also use [docker-compose.yml](https://github.com/qdm12/ddns-updater/blob/master/docker-compose.yml) with:
@@ -124,37 +126,120 @@ Start by having the following content in *config.json*, or in your `CONFIG` envi
}
```
For each setting, you need to fill in parameters.
Check the documentation for your DNS provider:
The following parameters are to be added:
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
- [DonDominio](https://github.com/qdm12/ddns-updater/blob/master/docs/dondominio.md)
- [DNSOMatic](https://github.com/qdm12/ddns-updater/blob/master/docs/dnsomatic.md)
- [DNSPod](https://github.com/qdm12/ddns-updater/blob/master/docs/dnspod.md)
- [Dreamhost](https://github.com/qdm12/ddns-updater/blob/master/docs/dreamhost.md)
- [DuckDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/duckdns.md)
- [DynDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/dyndns.md)
- [DynV6](https://github.com/qdm12/ddns-updater/blob/master/docs/dynv6.md)
- [FreeDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/freedns.md)
- [Gandi](https://github.com/qdm12/ddns-updater/blob/master/docs/gandi.md)
- [GoDaddy](https://github.com/qdm12/ddns-updater/blob/master/docs/godaddy.md)
- [Google](https://github.com/qdm12/ddns-updater/blob/master/docs/google.md)
- [He.net](https://github.com/qdm12/ddns-updater/blob/master/docs/he.net.md)
- [Infomaniak](https://github.com/qdm12/ddns-updater/blob/master/docs/infomaniak.md)
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
- [Namecheap](https://github.com/qdm12/ddns-updater/blob/master/docs/namecheap.md)
- [NoIP](https://github.com/qdm12/ddns-updater/blob/master/docs/noip.md)
- [OpenDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/opendns.md)
- [OVH](https://github.com/qdm12/ddns-updater/blob/master/docs/ovh.md)
- [Selfhost.de](https://github.com/qdm12/ddns-updater/blob/master/docs/selfhost.de.md)
- [Strato.de](https://github.com/qdm12/ddns-updater/blob/master/docs/strato.md)
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:
Note that:
- `"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.
- you can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
For each DNS provider exist some specific parameters you need to add, as described below:
Namecheap:
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"password"`
Cloudflare:
- `"zone_identifier"` is the Zone ID of your site
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
- One of the following:
- Email `"email"` and Global API Key `"key"`
- User service key `"user_service_key"`
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone.
- *Optionally*, `"proxied"` can be `true` or `false` to use the proxy services of Cloudflare
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
GoDaddy:
- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"key"`
- `"secret"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DuckDNS:
- `"domain"` is your fqdn, for example `subdomain.duckdns.org`
- `"token"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
Dreamhost:
- `"domain"`
- `"key"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
NoIP:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DNSPOD:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"token"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
HE.net:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"` (untested)
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
Infomaniak:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"user"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DDNSS.de:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"user"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DYNDNS:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
Google:
- `"domain"`
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
DonDominio:
- `"domain"`
- `"username"`
- `"password"`
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"name"` is the name server associated with the domain
### Additional notes
- You can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`.
### Environment variables
@@ -162,40 +247,44 @@ Note that:
| --- | --- | --- |
| `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` | `all` | Comma separated methods to obtain the public IP address (ipv4 or ipv6). See the [IP Methods section](#IP-methods) |
| `IPV4_METHOD` | `all` | Comma separated methods to obtain the public IPv4 address only. See the [IP Methods section](#IP-methods) |
| `IPV6_METHOD` | `all` | Comma separated methods to obtain the public IPv6 address only. See the [IP Methods section](#IP-methods) |
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
| `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`. |
| `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` |
| `LOG_CALLER` | `hidden` | Show caller per log line, `hidden` or `short` |
| `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 specified. The program will cycle between each. This allows you not to be blocked for making too many requests. You can otherwise pick one or more of the following, for each ip version:
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)
- `"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)
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
- `"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
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
- `noip` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
- `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 one or more HTTPS URL to obtain your public IP address (i.e. `-e IPV6_METHOD=https://ipinfo.io/ip`).
You can also specify an HTTPS URL to obtain your public IP address (i.e. `-e IPV6_METHOD=https://ipinfo.io/ip`)
### Host firewall
@@ -206,39 +295,9 @@ If you have a host firewall in place, this container needs the following ports:
- UDP 53 outbound for outbound DNS resolution
- TCP 8000 inbound (or other) for the WebUI
## Architecture
## Domain set up
At program start and every period (5 minutes by default):
1. Fetch your public IP address
1. For each record:
1. DNS resolve it to obtain its current IP address(es)
- If the resolution fails, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address is within the resolved IP addresses
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
💡 We do DNS resolution every period so it detects a change made to the record manually, for example on the DNS provider web UI
💡 As DNS resolutions are essentially free and without rate limiting, these are great to avoid getting banned for too many requests.
### Special case: Cloudflare
For Cloudflare records with the `proxied` option, the following is done.
At program start and every period (5 minutes by default), for each record:
1. Fetch your public IP address
1. For each record:
1. Check the last IP address (persisted in `updates.json`) for that record
- If it doesn't exist, update the record with your public IP address by calling the DNS provider API and finish
1. Check if your public IP address matches the last IP address you updated the record with
- Yes: skip the update
- No: update the record with your public IP address by calling the DNS provider API
This is the only way as doing a DNS resolution on the record will give the IP address of a Cloudflare server instead of your server.
⚠️ This has the disadvantage that if the record is changed manually, the program will not detect it.
We could do an API call to get the record IP address every period, but that would get you banned especially with a low period duration.
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
@@ -267,9 +326,11 @@ To set it up with DDNS updater:
1. Run the container
1. Refresh the DNS management webpage and verify the update happened
Better testing instructions are written in the [Wiki for GoDaddy](https://github.com/qdm12/ddns-updater/wiki/GoDaddy#testing)
## Development and contributing
- [Contribute with code](https://github.com/qdm12/ddns-updater/blob/master/docs/contributing.md)
- 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)

View File

@@ -2,82 +2,74 @@ package main
import (
"context"
"fmt"
"net"
"os/signal"
"syscall"
"fmt"
"net/http"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"time"
"github.com/qdm12/ddns-updater/internal/backup"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/health"
"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/server"
"github.com/qdm12/ddns-updater/internal/splash"
"github.com/qdm12/ddns-updater/internal/update"
pubiphttp "github.com/qdm12/ddns-updater/pkg/publicip/http"
"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"
)
var (
buildInfo models.BuildInformation
version = "unknown"
commit = "unknown"
buildDate = "an unknown date"
"github.com/qdm12/golibs/server"
)
func main() {
buildInfo.Version = version
buildInfo.Commit = commit
buildInfo.BuildDate = buildDate
os.Exit(_main(context.Background(), time.Now))
// returns 1 on error
// returns 2 on os signal
}
type allParams struct {
period time.Duration
cooldown time.Duration
httpIPOptions []pubiphttp.Option
ipMethod models.IPMethod
ipv4Method models.IPMethod
ipv6Method models.IPMethod
dir string
dataDir string
listeningPort uint16
listeningPort string
rootURL string
backupPeriod time.Duration
backupDirectory string
}
func _main(ctx context.Context, timeNow func() time.Time) int {
if health.IsClientMode(os.Args) {
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
client := health.NewClient()
if err := client.Query(ctx); err != nil {
if err := libhealthcheck.Query(); err != nil {
fmt.Println(err)
return 1
}
return 0
}
fmt.Println(splash.Splash(buildInfo))
// Setup logger
paramsReader := params.NewReader(logging.New(logging.StdLog)) // use a temporary logger
logLevel, logCaller, err := paramsReader.LoggerConfig()
logger, err := setupLogger()
if err != nil {
fmt.Println(err)
return 1
}
logger := logging.New(logging.StdLog, logging.SetLevel(logLevel), logging.SetCaller(logCaller))
paramsReader = params.NewReader(logger)
paramsReader := params.NewReader(logger)
fmt.Println(splash.Splash(
paramsReader.GetVersion(),
paramsReader.GetVcsRef(),
paramsReader.GetBuildDate()))
notify, err := setupGotify(paramsReader, logger)
if err != nil {
@@ -98,7 +90,7 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
notify(4, err)
return 1
}
settings, warnings, err := paramsReader.JSONSettings(filepath.Join(p.dataDir, "config.json"))
settings, warnings, err := paramsReader.GetSettings(p.dataDir + "/config.json")
for _, w := range warnings {
logger.Warn(w)
notify(2, w)
@@ -113,9 +105,7 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
} else if len(settings) == 1 {
logger.Info("Found single setting to update record")
}
const connectivyCheckTimeout = 5 * time.Second
for _, err := range connectivity.NewConnectivity(connectivyCheckTimeout).
Checks(ctx, "google.com") {
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
logger.Warn(err)
}
records := make([]recordslib.Record, len(settings))
@@ -129,58 +119,42 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
}
records[i] = recordslib.New(s, events)
}
HTTPTimeout, err := paramsReader.HTTPTimeout()
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
if err != nil {
logger.Error(err)
notify(4, err)
return 1
}
client := &http.Client{Timeout: HTTPTimeout}
defer client.CloseIdleConnections()
client := network.NewClient(HTTPTimeout)
defer client.Close()
db := data.NewDatabase(records, persistentDB)
defer func() {
if err := db.Close(); err != nil {
logger.Error(err)
}
}()
wg := &sync.WaitGroup{}
defer wg.Wait()
ipGetter, err := pubiphttp.New(client, p.httpIPOptions...)
if err != nil {
logger.Error(err)
return 1
}
updater := update.NewUpdater(db, client, notify, logger)
runner := update.NewRunner(db, updater, ipGetter, p.cooldown, logger, timeNow)
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()
go runner.Run(ctx, p.period)
// note: errors are logged within the goroutine,
// no need to collect the resulting errors.
go runner.ForceUpdate(ctx)
const healthServerAddr = "127.0.0.1:9999"
isHealthy := health.MakeIsHealthy(db, net.LookupIP, logger)
healthServer := health.NewServer(healthServerAddr,
logger.NewChild(logging.SetPrefix("healthcheck server: ")),
isHealthy)
wg.Add(1)
go healthServer.Run(ctx, wg)
address := fmt.Sprintf("0.0.0.0:%d", p.listeningPort)
uiDir := p.dir + "/ui"
server := server.New(ctx, address, p.rootURL, uiDir, db, logger.NewChild(logging.SetPrefix("http server: ")), runner)
wg.Add(1)
go server.Run(ctx, wg)
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.NewChild(logging.SetPrefix("backup: ")), timeNow)
go backupRunLoop(ctx, p.backupPeriod, p.dir, p.backupDirectory, logger, timeNow)
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals,
@@ -189,11 +163,16 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
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 1
return 2
case <-ctx.Done():
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
logger.Warn(message)
@@ -201,15 +180,23 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
}
}
func setupGotify(paramsReader params.Reader, logger logging.Logger) (
notify func(priority int, messageArgs ...interface{}), err error) {
gotifyURL, err := paramsReader.GotifyURL()
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.GotifyToken()
gotifyToken, err := paramsReader.GetGotifyToken()
if err != nil {
return nil, err
}
@@ -223,57 +210,46 @@ func setupGotify(paramsReader params.Reader, logger logging.Logger) (
func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams, err error) {
var warnings []string
p.period, warnings, err = paramsReader.Period()
p.period, warnings, err = paramsReader.GetPeriod()
for _, warning := range warnings {
logger.Warn(warning)
}
if err != nil {
return p, err
}
p.cooldown, err = paramsReader.CooldownPeriod()
p.ipMethod, err = paramsReader.GetIPMethod()
if err != nil {
return p, err
}
httpIPProviders, err := paramsReader.IPMethod()
p.ipv4Method, err = paramsReader.GetIPv4Method()
if err != nil {
return p, err
}
httpIP4Providers, err := paramsReader.IPv4Method()
p.ipv6Method, err = paramsReader.GetIPv6Method()
if err != nil {
return p, err
}
httpIP6Providers, err := paramsReader.IPv6Method()
p.dir, err = paramsReader.GetExeDir()
if err != nil {
return p, err
}
p.httpIPOptions = []pubiphttp.Option{
pubiphttp.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
pubiphttp.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
pubiphttp.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
}
p.dir, err = paramsReader.ExeDir()
p.dataDir, err = paramsReader.GetDataDir(p.dir)
if err != nil {
return p, err
}
p.dataDir, err = paramsReader.DataDir(p.dir)
p.listeningPort, _, err = paramsReader.GetListeningPort()
if err != nil {
return p, err
}
p.listeningPort, _, err = paramsReader.ListeningPort()
p.rootURL, err = paramsReader.GetRootURL()
if err != nil {
return p, err
}
p.rootURL, err = paramsReader.RootURL()
p.backupPeriod, err = paramsReader.GetBackupPeriod()
if err != nil {
return p, err
}
p.backupPeriod, err = paramsReader.BackupPeriod()
if err != nil {
return p, err
}
p.backupDirectory, err = paramsReader.BackupDirectory()
p.backupDirectory, err = paramsReader.GetBackupDirectory()
if err != nil {
return p, err
}
@@ -282,6 +258,7 @@ func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams,
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

View File

@@ -11,10 +11,9 @@ services:
environment:
- CONFIG=
- PERIOD=5m
- UPDATE_COOLDOWN_PERIOD=5m
- IP_METHOD=all
- IPV4_METHOD=all
- IPV6_METHOD=all
- IP_METHOD=cycle
- IPV4_METHOD=cycle
- IPV6_METHOD=cycle
- HTTP_TIMEOUT=10s
# Web UI
@@ -26,8 +25,9 @@ services:
- BACKUP_DIRECTORY=/updater/data
# Other
- LOG_ENCODING=console
- LOG_LEVEL=info
- LOG_CALLER=hidden
- NODE_ID=-1 # -1 to disable
- GOTIFY_URL=
- GOTIFY_TOKEN=
restart: always

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

10
go.mod
View File

@@ -1,13 +1,11 @@
module github.com/qdm12/ddns-updater
go 1.16
go 1.15
require (
github.com/go-chi/chi v1.5.1
github.com/golang/mock v1.4.3
github.com/golang/mock v1.4.4
github.com/google/uuid v1.1.1
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/miekg/dns v1.1.40
github.com/ovh/go-ovh v1.1.0
github.com/qdm12/golibs v0.0.0-20210215133151-c711ebd3e56a
github.com/qdm12/golibs v0.0.0-20200712151944-a0325873bf5a
github.com/stretchr/testify v1.6.1
)

80
go.sum
View File

@@ -1,3 +1,5 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
@@ -7,14 +9,11 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
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-chi/chi v1.5.1 h1:kfTK3Cxd/dkMu/rKs5ZceWYp+t5CtiE7vmaTv3LjC6w=
github.com/go-chi/chi v1.5.1/go.mod h1:REp24E+25iKvxgeTfHmdUoL5x15kBiDBlnIl5bCwe2k=
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
github.com/go-openapi/analysis v0.17.0 h1:8JV+dzJJiK46XqGLqqLav8ZfEiJECp8jlOFhpiCdZ+0=
github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik=
@@ -40,16 +39,22 @@ github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq
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/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gotify/go-api-client/v2 v2.0.4 h1:0w8skCr8aLBDKaQDg31LKKHUGF7rt7zdRpR+6cqIAlE=
github.com/gotify/go-api-client/v2 v2.0.4/go.mod h1:VKiah/UK20bXsr0JObE1eBVLW44zbBouzjuri9iwjFU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4=
github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
@@ -59,71 +64,78 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
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/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
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=
github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee h1:P6U24L02WMfj9ymZTxl7CxS73JC99x3ukk+DBkgQGQs=
github.com/phayes/permbits v0.0.0-20190612203442-39d7c581d2ee/go.mod h1:3uODdxMgOaPYeWU7RzZLxVtJHZ/x1f/iHkBZuKJDzuY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.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-20210215133151-c711ebd3e56a h1:DxO9jvcQDtWgKSzxL95828kQxO6WCocP9PPpmIqGMRs=
github.com/qdm12/golibs v0.0.0-20210215133151-c711ebd3e56a/go.mod h1:y0qNgur9dTkHK2Bb5tK0UCtYyvEiK08flVIglROmnBg=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
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=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
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-20190924154521-2837fb4f24fe/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
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-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/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=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
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 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

9
internal/constants/ip.go Normal file
View File

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

View File

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

View File

@@ -2,59 +2,37 @@ package constants
import "github.com/qdm12/ddns-updater/internal/models"
// All possible provider values.
// All possible provider values
const (
CLOUDFLARE models.Provider = "cloudflare"
DIGITALOCEAN models.Provider = "digitalocean"
DDNSSDE models.Provider = "ddnss"
DONDOMINIO models.Provider = "dondominio"
DNSOMATIC models.Provider = "dnsomatic"
DNSPOD models.Provider = "dnspod"
DUCKDNS models.Provider = "duckdns"
DYN models.Provider = "dyn"
DYNV6 models.Provider = "dynv6"
DREAMHOST models.Provider = "dreamhost"
FREEDNS models.Provider = "freedns"
GANDI models.Provider = "gandi"
GODADDY models.Provider = "godaddy"
GOOGLE models.Provider = "google"
HE models.Provider = "he"
INFOMANIAK models.Provider = "infomaniak"
LINODE models.Provider = "linode"
LUADNS models.Provider = "luadns"
NAMECHEAP models.Provider = "namecheap"
NOIP models.Provider = "noip"
OPENDNS models.Provider = "opendns"
OVH models.Provider = "ovh"
SELFHOSTDE models.Provider = "selfhost.de"
STRATO models.Provider = "strato"
CLOUDFLARE models.Provider = "cloudflare"
DDNSSDE models.Provider = "ddnss"
DONDOMINIO models.Provider = "dondominio"
DNSPOD models.Provider = "dnspod"
DUCKDNS models.Provider = "duckdns"
DYN models.Provider = "dyn"
DREAMHOST models.Provider = "dreamhost"
GODADDY models.Provider = "godaddy"
GOOGLE models.Provider = "google"
HE models.Provider = "he"
INFOMANIAK models.Provider = "infomaniak"
NAMECHEAP models.Provider = "namecheap"
NOIP models.Provider = "noip"
)
func ProviderChoices() []models.Provider {
return []models.Provider{
CLOUDFLARE,
DIGITALOCEAN,
DDNSSDE,
DONDOMINIO,
DNSOMATIC,
DNSPOD,
DUCKDNS,
DYN,
DYNV6,
DREAMHOST,
FREEDNS,
GANDI,
GODADDY,
GOOGLE,
HE,
INFOMANIAK,
LINODE,
LUADNS,
NAMECHEAP,
NOIP,
OVH,
OPENDNS,
SELFHOSTDE,
STRATO,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,22 +9,23 @@ import (
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/regex"
"github.com/qdm12/ddns-updater/internal/settings"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)
// nolint: maligned
type commonSettings struct {
Provider string `json:"provider"`
Domain string `json:"domain"`
Host string `json:"host"`
IPVersion string `json:"ip_version"`
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"`
}
// JSONSettings obtain the update settings from the JSON content, first trying from the environment variable CONFIG
// and then from the file config.json.
func (r *reader) JSONSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error) {
// 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
@@ -32,7 +33,7 @@ func (r *reader) JSONSettings(filePath string) (allSettings []settings.Settings,
return r.getSettingsFromFile(filePath)
}
// getSettingsFromFile obtain the update settings from config.json.
// 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 {
@@ -41,9 +42,9 @@ func (r *reader) getSettingsFromFile(filePath string) (allSettings []settings.Se
return extractAllSettings(bytes)
}
// getSettingsFromEnv obtain the update settings from the environment variable CONFIG.
// getSettingsFromEnv obtain the update settings from the environment variable CONFIG
func (r *reader) getSettingsFromEnv() (allSettings []settings.Settings, warnings []string, err error) {
s, err := r.env.Get("CONFIG")
s, err := r.envParams.GetEnv("CONFIG")
if err != nil {
return nil, nil, err
} else if len(s) == 0 {
@@ -83,57 +84,45 @@ func extractAllSettings(jsonBytes []byte) (allSettings []settings.Settings, warn
return allSettings, warnings, nil
}
// TODO remove gocyclo.
//nolint:gocyclo
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage, matcher regex.Matcher) (
settingsSlice []settings.Settings, warnings []string, err error) {
func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage, matcher regex.Matcher) (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))
warnings = append(warnings, fmt.Sprintf("DuckDNS record should have %q specified as host instead of %q as domain", common.Host, common.Domain))
} else {
warnings = append(warnings,
fmt.Sprintf("ignoring domain %q because host %q is specified for DuckDNS record",
common.Domain, common.Host))
warnings = append(warnings, fmt.Sprintf("ignoring domain %q because host %q is specified for DuckDNS record", common.Domain, common.Host))
}
}
}
hosts := strings.Split(common.Host, ",")
if len(common.IPVersion) == 0 {
common.IPVersion = ipversion.IP4or6.String()
for _, host := range hosts {
if len(host) == 0 {
return nil, warnings, fmt.Errorf("host cannot be empty")
}
}
ipVersion, err := ipversion.Parse(common.IPVersion)
if err != nil {
return nil, nil, err
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.DIGITALOCEAN:
settingsConstructor = settings.NewDigitalOcean
case constants.DDNSSDE:
settingsConstructor = settings.NewDdnss
case constants.DONDOMINIO:
settingsConstructor = settings.NewDonDominio
case constants.DNSOMATIC:
settingsConstructor = settings.NewDNSOMatic
case constants.DNSPOD:
settingsConstructor = settings.NewDNSPod
case constants.DREAMHOST:
settingsConstructor = settings.NewDreamhost
case constants.DUCKDNS:
settingsConstructor = settings.NewDuckdns
case constants.FREEDNS:
settingsConstructor = settings.NewFreedns
case constants.GANDI:
settingsConstructor = settings.NewGandi
case constants.GODADDY:
settingsConstructor = settings.NewGodaddy
case constants.GOOGLE:
@@ -142,32 +131,18 @@ func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage,
settingsConstructor = settings.NewHe
case constants.INFOMANIAK:
settingsConstructor = settings.NewInfomaniak
case constants.LINODE:
settingsConstructor = settings.NewLinode
case constants.LUADNS:
settingsConstructor = settings.NewLuaDNS
case constants.NAMECHEAP:
settingsConstructor = settings.NewNamecheap
case constants.NOIP:
settingsConstructor = settings.NewNoip
case constants.DYN:
settingsConstructor = settings.NewDyn
case constants.SELFHOSTDE:
settingsConstructor = settings.NewSelfhostde
case constants.STRATO:
settingsConstructor = settings.NewStrato
case constants.OVH:
settingsConstructor = settings.NewOVH
case constants.DYNV6:
settingsConstructor = settings.NewDynV6
case constants.OPENDNS:
settingsConstructor = settings.NewOpendns
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, matcher)
settingsSlice[i], err = settingsConstructor(rawSettings, common.Domain, host, ipVersion, common.NoDNSLookup, matcher)
if err != nil {
return nil, warnings, err
}

View File

@@ -1,215 +1,203 @@
package params
import (
"errors"
"fmt"
"io/ioutil"
"net/url"
"strings"
"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/ddns-updater/pkg/publicip/http"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"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
JSONSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error)
GetSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error)
// Core
Period() (period time.Duration, warnings []string, err error)
IPMethod() (providers []http.Provider, err error)
IPv4Method() (providers []http.Provider, err error)
IPv6Method() (providers []http.Provider, err error)
HTTPTimeout() (duration time.Duration, err error)
CooldownPeriod() (duration time.Duration, err error)
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
ExeDir() (dir string, err error)
DataDir(currentDir string) (string, error)
GetExeDir() (dir string, err error)
GetDataDir(currentDir string) (string, error)
// Web UI
ListeningPort() (listeningPort uint16, warning string, err error)
RootURL() (rootURL string, err error)
GetListeningPort() (listeningPort, warning string, err error)
GetRootURL() (rootURL string, err error)
// Backup
BackupPeriod() (duration time.Duration, err error)
BackupDirectory() (directory string, err error)
GetBackupPeriod() (duration time.Duration, err error)
GetBackupDirectory() (directory string, err error)
// Other
LoggerConfig() (level logging.Level, caller logging.Caller, err error)
GotifyURL() (URL *url.URL, err error)
GotifyToken() (token string, err error)
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 {
env params.Env
os params.OS
readFile func(filename string) ([]byte, error)
envParams libparams.EnvParams
verifier verification.Verifier
readFile func(filename string) ([]byte, error)
}
func NewReader(logger logging.Logger) Reader {
return &reader{
env: params.NewEnv(),
os: params.NewOS(),
readFile: ioutil.ReadFile,
envParams: libparams.NewEnvParams(),
verifier: verification.NewVerifier(),
readFile: ioutil.ReadFile,
}
}
// GetDataDir obtains the data directory from the environment
// variable DATADIR.
func (r *reader) DataDir(currentDir string) (string, error) {
return r.env.Get("DATADIR", params.Default(currentDir+"/data"))
// variable DATADIR
func (r *reader) GetDataDir(currentDir string) (string, error) {
return r.envParams.GetEnv("DATADIR", libparams.Default(currentDir+"/data"))
}
func (r *reader) ListeningPort() (listeningPort uint16, warning string, err error) {
return r.env.ListeningPort("LISTENING_PORT", params.Default("8000"))
func (r *reader) GetListeningPort() (listeningPort, warning string, err error) {
return r.envParams.GetListeningPort()
}
func (r *reader) LoggerConfig() (level logging.Level, caller logging.Caller, err error) {
caller, err = r.env.LogCaller("LOG_CALLER", params.Default("hidden"))
if err != nil {
return level, caller, err
}
level, err = r.env.LogLevel("LOG_LEVEL", params.Default("info"))
if err != nil {
return level, caller, err
}
return level, caller, nil
func (r *reader) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
return r.envParams.GetLoggerConfig()
}
func (r *reader) GotifyURL() (url *url.URL, err error) {
return r.env.URL("GOTIFY_URL")
func (r *reader) GetGotifyURL() (url *url.URL, err error) {
return r.envParams.GetGotifyURL()
}
func (r *reader) GotifyToken() (token string, err error) {
return r.env.Get("GOTIFY_TOKEN",
params.CaseSensitiveValue(),
params.Compulsory(),
params.Unset())
func (r *reader) GetGotifyToken() (token string, err error) {
return r.envParams.GetGotifyToken()
}
func (r *reader) RootURL() (rootURL string, err error) {
return r.env.RootURL("ROOT_URL")
func (r *reader) GetRootURL() (rootURL string, err error) {
return r.envParams.GetRootURL()
}
func (r *reader) Period() (period time.Duration, warnings []string, err error) {
func (r *reader) GetPeriod() (period time.Duration, warnings []string, err error) {
// Backward compatibility
n, err := r.env.Int("DELAY", params.Compulsory())
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), //nolint:lll
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.env.Duration("DELAY", params.Compulsory())
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.env.Duration("PERIOD", params.Default("10m"))
period, err = r.envParams.GetDuration("PERIOD", libparams.Default("10m"))
return period, nil, err
}
var (
ErrIPMethodInvalid = errors.New("ip method is not valid")
ErrIPMethodVersion = errors.New("ip method not valid for IP version")
)
// IPMethod obtains the HTTP method for IP v4 or v6 to obtain your public IP address.
func (r *reader) IPMethod() (providers []http.Provider, err error) {
return r.httpIPMethod("IP_METHOD", ipversion.IP4or6)
}
// IPMethod obtains the HTTP method for IP v4 to obtain your public IP address.
func (r *reader) IPv4Method() (providers []http.Provider, err error) {
return r.httpIPMethod("IPV4_METHOD", ipversion.IP4)
}
// IPMethod obtains the HTTP method for IP v6 to obtain your public IP address.
func (r *reader) IPv6Method() (providers []http.Provider, err error) {
return r.httpIPMethod("IPV6_METHOD", ipversion.IP6)
}
func (r *reader) httpIPMethod(envKey string, version ipversion.IPVersion) (
providers []http.Provider, err error) {
s, err := r.env.Get(envKey, params.Default("cycle"))
func (r *reader) GetIPMethod() (method models.IPMethod, err error) {
s, err := r.envParams.GetEnv("IP_METHOD", params.Default("cycle"))
if err != nil {
return nil, err
return method, err
}
availableProviders := http.ListProvidersForVersion(version)
choices := make(map[http.Provider]struct{}, len(availableProviders))
for _, provider := range availableProviders {
choices[provider] = struct{}{}
for _, choice := range constants.IPMethods() {
if choice.Name == s {
return choice, nil
}
}
fields := strings.Split(s, ",")
for _, field := range fields {
// Retro-compatibility.
switch field {
case "ipify6":
field = "ipify"
case "noip4", "noip6", "noip8245_4", "noip8245_6":
field = "noip"
case "cycle":
field = "all"
}
if field == "all" {
return availableProviders, nil
}
// Custom URL check
url, err := url.Parse(field)
if err == nil && url != nil && url.Scheme == "https" {
providers = append(providers, http.CustomProvider(url))
continue
}
provider := http.Provider(field)
if _, ok := choices[provider]; !ok {
return nil, fmt.Errorf("%w: %s", ErrIPMethodInvalid, provider)
}
providers = append(providers, provider)
url, err := url.Parse(s)
if err != nil || url == nil || url.Scheme != https {
return method, fmt.Errorf("ip method %q is not valid", s)
}
if len(providers) == 0 {
return nil, fmt.Errorf("%w: %s", ErrIPMethodVersion, version)
}
return providers, nil
return models.IPMethod{
Name: s,
URL: s,
IPv4: true,
IPv6: true,
}, nil
}
func (r *reader) ExeDir() (dir string, err error) {
return r.os.ExeDir()
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) HTTPTimeout() (duration time.Duration, err error) {
return r.env.Duration("HTTP_TIMEOUT", params.Default("10s"))
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) BackupPeriod() (duration time.Duration, err error) {
s, err := r.env.Get("BACKUP_PERIOD", params.Default("0"))
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) BackupDirectory() (directory string, err error) {
return r.env.Path("BACKUP_DIRECTORY", params.Default("./data"))
}
func (r *reader) CooldownPeriod() (duration time.Duration, err error) {
return r.env.Duration("UPDATE_COOLDOWN_PERIOD", params.Default("5m"))
func (r *reader) GetBackupDirectory() (directory string, err error) {
return r.envParams.GetEnv("BACKUP_DIRECTORY", libparams.Default("./data"))
}

View File

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

View File

@@ -51,7 +51,7 @@ func NewDatabase(dataDir string) (*Database, error) {
return nil, err
}
if err := db.Check(); err != nil {
return nil, fmt.Errorf("%s validation error: %w", db.filepath, err)
return nil, err
}
return &db, nil
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +0,0 @@
package server
import (
"net/http"
"strings"
"github.com/go-chi/chi"
)
func fileServer(router chi.Router, path string, root http.FileSystem) {
if path != "/" && path[len(path)-1] != '/' {
router.Get(path,
http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP)
path += "/"
}
path += "*"
router.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}

View File

@@ -1,50 +0,0 @@
package server
import (
"context"
"net/http"
"text/template"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/qdm12/ddns-updater/internal/data"
"github.com/qdm12/ddns-updater/internal/update"
)
type handlers struct {
ctx context.Context
// Objects
db data.Database
runner update.Runner
indexTemplate *template.Template
// Mockable functions
timeNow func() time.Time
}
func newHandler(ctx context.Context, rootURL, uiDir string,
db data.Database, runner update.Runner) http.Handler {
indexTemplate := template.Must(template.ParseFiles(uiDir + "/index.html"))
handlers := &handlers{
ctx: ctx,
db: db,
indexTemplate: indexTemplate,
// TODO build information
timeNow: time.Now,
runner: runner,
}
router := chi.NewRouter()
router.Use(middleware.Logger, middleware.CleanPath)
router.Get(rootURL+"/", handlers.index)
router.Get(rootURL+"/update", handlers.update)
// UI file server for other paths
fileServer(router, rootURL+"/", http.Dir(uiDir))
return router
}

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,6 @@
package settings
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
@@ -12,15 +10,18 @@ import (
"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"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
netlib "github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
)
//nolint:maligned
type cloudflare struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
key string
token string
email string
@@ -31,8 +32,7 @@ type cloudflare struct {
matcher regex.Matcher
}
func NewCloudflare(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
matcher regex.Matcher) (s Settings, err error) {
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"`
@@ -49,6 +49,7 @@ func NewCloudflare(data json.RawMessage, domain, host string, ipVersion ipversio
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
key: extraSettings.Key,
token: extraSettings.Token,
email: extraSettings.Email,
@@ -69,21 +70,21 @@ func (c *cloudflare) isValid() error {
case len(c.key) > 0: // email and key must be provided
switch {
case !c.matcher.CloudflareKey(c.key):
return ErrMalformedKey
return fmt.Errorf("invalid key format")
case !verification.NewVerifier().MatchEmail(c.email):
return ErrMalformedEmail
return fmt.Errorf("invalid email format")
}
case len(c.userServiceKey) > 0: // only user service key
if !c.matcher.CloudflareKey(c.key) {
return ErrMalformedUserServiceKey
return fmt.Errorf("invalid user service key format")
}
default: // API token only
}
switch {
case len(c.zoneIdentifier) == 0:
return ErrEmptyZoneIdentifier
return fmt.Errorf("zone identifier cannot be empty")
case c.ttl == 0:
return ErrEmptyTTL
return fmt.Errorf("TTL cannot be left to 0")
}
return nil
}
@@ -100,12 +101,12 @@ func (c *cloudflare) Host() string {
return c.host
}
func (c *cloudflare) IPVersion() ipversion.IPVersion {
func (c *cloudflare) IPVersion() models.IPVersion {
return c.ipVersion
}
func (c *cloudflare) Proxied() bool {
return c.proxied
func (c *cloudflare) DNSLookup() bool {
return c.dnsLookup
}
func (c *cloudflare) BuildDomainName() string {
@@ -121,30 +122,26 @@ func (c *cloudflare) HTML() models.HTMLRow {
}
}
func (c *cloudflare) setHeaders(request *http.Request) {
setUserAgent(request)
setContentType(request, "application/json")
setAccept(request, "application/json")
func setHeaders(r *http.Request, token, userServiceKey, email, key string) {
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
switch {
case len(c.token) > 0:
setAuthBearer(request, c.token)
case len(c.userServiceKey) > 0:
request.Header.Set("X-Auth-User-Service-Key", c.userServiceKey)
case len(c.email) > 0 && len(c.key) > 0:
request.Header.Set("X-Auth-Email", c.email)
request.Header.Set("X-Auth-Key", c.key)
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 ID.
// See https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records.
func (c *cloudflare) getRecordID(ctx context.Context, client *http.Client, newIP net.IP) (
identifier string, upToDate bool, err error) {
// 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",
@@ -156,25 +153,17 @@ func (c *cloudflare) getRecordID(ctx context.Context, client *http.Client, newIP
values.Set("page", "1")
values.Set("per_page", "1")
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return "", false, err
}
c.setHeaders(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", false, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := json.NewDecoder(response.Body)
listRecordsResponse := struct {
Success bool `json:"success"`
Errors []string `json:"errors"`
@@ -183,84 +172,67 @@ func (c *cloudflare) getRecordID(ctx context.Context, client *http.Client, newIP
Content string `json:"content"`
} `json:"result"`
}{}
if err := decoder.Decode(&listRecordsResponse); err != nil {
return "", false, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
if err := json.Unmarshal(content, &listRecordsResponse); err != nil {
return "", false, err
}
switch {
case len(listRecordsResponse.Errors) > 0:
return "", false, fmt.Errorf("%w: %s",
ErrUnsuccessfulResponse, strings.Join(listRecordsResponse.Errors, ","))
return "", false, fmt.Errorf(strings.Join(listRecordsResponse.Errors, ","))
case !listRecordsResponse.Success:
return "", false, ErrUnsuccessfulResponse
return "", false, fmt.Errorf("request to Cloudflare not successful")
case len(listRecordsResponse.Result) == 0:
return "", false, ErrNoResultReceived
return "", false, fmt.Errorf("received no result from Cloudflare")
case len(listRecordsResponse.Result) > 1:
return "", false, fmt.Errorf("%w: %d instead of 1",
ErrNumberOfResultsReceived, len(listRecordsResponse.Result))
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(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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.getRecordID(ctx, client, ip)
identifier, upToDate, err := c.getRecordIdentifier(client, ip)
if err != nil {
return nil, fmt.Errorf("%s: %w", ErrGetRecordID, err)
return nil, err
} else if upToDate {
return ip, nil
}
u := url.URL{
Scheme: "https",
Host: "api.cloudflare.com",
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records/%s", c.zoneIdentifier, identifier),
}
requestData := struct {
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"`
}{
Type: recordType,
Name: c.BuildDomainName(),
Content: ip.String(),
Proxied: c.proxied,
TTL: c.ttl,
}
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
if err := encoder.Encode(requestData); err != nil {
return nil, fmt.Errorf("%w: %s", ErrRequestEncode, err)
u := url.URL{
Scheme: "https",
Host: "api.cloudflare.com",
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records/%s", c.zoneIdentifier, identifier),
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
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
}
c.setHeaders(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode > http.StatusUnsupportedMediaType {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := json.NewDecoder(response.Body)
var parsedJSON struct {
Success bool `json:"success"`
Errors []struct {
@@ -271,23 +243,20 @@ func (c *cloudflare) Update(ctx context.Context, client *http.Client, ip net.IP)
Content string `json:"content"`
} `json:"result"`
}
if err := decoder.Decode(&parsedJSON); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if !parsedJSON.Success {
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("%w: %s", ErrUnsuccessfulResponse, errStr)
return nil, fmt.Errorf(errStr)
}
newIP = net.ParseIP(parsedJSON.Result.Content)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, parsedJSON.Result.Content)
return nil, fmt.Errorf("new IP %q is malformed", parsedJSON.Result.Content)
} else if !newIP.Equal(ip) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
}
return newIP, nil
}

View File

@@ -1,13 +1,9 @@
package settings
const (
badauth = "badauth"
success = "success"
nohost = "nohost"
notfqdn = "notfqdn"
badagent = "badagent"
abuse = "abuse"
nineoneone = "911"
A = "A"
AAAA = "AAAA"
badauth = "badauth"
success = "success"
nohost = "nohost"
A = "A"
AAAA = "AAAA"
)

View File

@@ -1,10 +1,8 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -13,20 +11,21 @@ import (
"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/pkg/publicip/ipversion"
"github.com/qdm12/golibs/network"
)
//nolint:maligned
type ddnss struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
username string
password string
useProviderIP bool
}
func NewDdnss(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
@@ -39,6 +38,7 @@ func NewDdnss(data json.RawMessage, domain, host string, ipVersion ipversion.IPV
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
@@ -52,11 +52,11 @@ func NewDdnss(data json.RawMessage, domain, host string, ipVersion ipversion.IPV
func (d *ddnss) isValid() error {
switch {
case len(d.username) == 0:
return ErrEmptyUsername
return fmt.Errorf("username cannot be empty")
case len(d.password) == 0:
return ErrEmptyPassword
return fmt.Errorf("password cannot be empty")
case d.host == "*":
return ErrHostWildcard
return fmt.Errorf(`host cannot be "*"`)
}
return nil
}
@@ -73,12 +73,12 @@ func (d *ddnss) Host() string {
return d.host
}
func (d *ddnss) IPVersion() ipversion.IPVersion {
func (d *ddnss) IPVersion() models.IPVersion {
return d.ipVersion
}
func (d *ddnss) Proxied() bool {
return false
func (d *ddnss) DNSLookup() bool {
return d.dnsLookup
}
func (d *ddnss) BuildDomainName() string {
@@ -94,7 +94,7 @@ func (d *ddnss) HTML() models.HTMLRow {
}
}
func (d *ddnss) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (d *ddnss) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
u := url.URL{
Scheme: "https",
Host: "www.ddnss.de",
@@ -103,7 +103,11 @@ func (d *ddnss) Update(ctx context.Context, client *http.Client, ip net.IP) (new
values := url.Values{}
values.Set("user", d.username)
values.Set("pwd", d.password)
values.Set("host", d.BuildDomainName())
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())
@@ -112,40 +116,29 @@ func (d *ddnss) Update(ctx context.Context, client *http.Client, ip net.IP) (new
}
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
s := string(content)
if status != http.StatusOK {
return nil, fmt.Errorf("received status %d with message: %s", status, s)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyDataToSingleLine(s))
}
switch {
case strings.Contains(s, "badysys"):
return nil, ErrInvalidSystemParam
return nil, fmt.Errorf("ddnss.de: invalid system parameter")
case strings.Contains(s, badauth):
return nil, ErrAuth
case strings.Contains(s, notfqdn):
return nil, ErrHostnameNotExists
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("%w: %s", ErrUnknownResponse, s)
return nil, fmt.Errorf("unknown response received from ddnss.de: %s", s)
}
}

View File

@@ -1,206 +0,0 @@
package settings
import (
"bytes"
"context"
"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/ddns-updater/pkg/publicip/ipversion"
)
type digitalOcean struct {
domain string
host string
ipVersion ipversion.IPVersion
token string
}
func NewDigitalOcean(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
extraSettings := struct {
Token string `json:"token"`
}{}
if err := json.Unmarshal(data, &extraSettings); err != nil {
return nil, err
}
d := &digitalOcean{
domain: domain,
host: host,
ipVersion: ipVersion,
token: extraSettings.Token,
}
if err := d.isValid(); err != nil {
return nil, err
}
return d, nil
}
func (d *digitalOcean) isValid() error {
if len(d.token) == 0 {
return ErrEmptyToken
}
return nil
}
func (d *digitalOcean) String() string {
return toString(d.domain, d.host, constants.DIGITALOCEAN, d.ipVersion)
}
func (d *digitalOcean) Domain() string {
return d.domain
}
func (d *digitalOcean) Host() string {
return d.host
}
func (d *digitalOcean) IPVersion() ipversion.IPVersion {
return d.ipVersion
}
func (d *digitalOcean) Proxied() bool {
return false
}
func (d *digitalOcean) BuildDomainName() string {
return buildDomainName(d.host, d.domain)
}
func (d *digitalOcean) 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.digitalocean.com/\">DigitalOcean</a>",
IPVersion: models.HTML(d.ipVersion),
}
}
func (d *digitalOcean) setHeaders(request *http.Request) {
setUserAgent(request)
setContentType(request, "application/json")
setAccept(request, "application/json")
setAuthBearer(request, d.token)
}
func (d *digitalOcean) getRecordID(ctx context.Context, recordType string, client *http.Client) (
recordID int, err error) {
values := url.Values{}
values.Set("name", d.BuildDomainName())
values.Set("type", recordType)
u := url.URL{
Scheme: "https",
Host: "api.digitalocean.com",
Path: "/v2/domains/" + d.domain + "/records",
RawQuery: values.Encode(),
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return 0, err
}
d.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return 0, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := json.NewDecoder(response.Body)
var result struct {
DomainRecords []struct {
ID int `json:"id"`
} `json:"domain_records"`
}
if err = decoder.Decode(&result); err != nil {
return 0, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if len(result.DomainRecords) == 0 {
return 0, ErrNotFound
} else if result.DomainRecords[0].ID == 0 {
return 0, ErrDomainIDNotFound
}
return result.DomainRecords[0].ID, nil
}
func (d *digitalOcean) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
recordType := A
if ip.To4() == nil { // IPv6
recordType = AAAA
}
recordID, err := d.getRecordID(ctx, recordType, client)
if err != nil {
return nil, fmt.Errorf("%s: %w", ErrGetRecordID, err)
}
u := url.URL{
Scheme: "https",
Host: "api.digitalocean.com",
Path: fmt.Sprintf("/v2/domains/%s/records/%d", d.domain, recordID),
}
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
requestData := struct {
Type string `json:"type"`
Name string `json:"name"`
Data string `json:"data"`
}{
Type: recordType,
Name: d.host,
Data: ip.String(),
}
if err := encoder.Encode(requestData); err != nil {
return nil, fmt.Errorf("%w: %s", ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
if err != nil {
return nil, err
}
d.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := json.NewDecoder(response.Body)
var responseData struct {
DomainRecord struct {
Data string `json:"data"`
} `json:"domain_record"`
}
if err := decoder.Decode(&responseData); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
newIP = net.ParseIP(responseData.DomainRecord.Data)
if newIP == nil {
return nil, fmt.Errorf("IP address received %q is malformed", responseData.DomainRecord.Data)
} else if !newIP.Equal(ip) {
return nil, fmt.Errorf("updated IP address %s is not the IP address %s sent for update", newIP, ip)
}
return newIP, nil
}

View File

@@ -1,175 +0,0 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"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/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/verification"
)
type dnsomatic struct {
domain string
host string
ipVersion ipversion.IPVersion
username string
password string
useProviderIP bool
matcher regex.Matcher
}
func NewDNSOMatic(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
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 := &dnsomatic{
domain: domain,
host: host,
ipVersion: ipVersion,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
matcher: matcher,
}
if err := d.isValid(); err != nil {
return nil, err
}
return d, nil
}
func (d *dnsomatic) isValid() error {
switch {
case !d.matcher.DNSOMaticUsername(d.username):
return fmt.Errorf("%w: %s", ErrMalformedUsername, d.username)
case !d.matcher.DNSOMaticPassword(d.password):
return ErrMalformedPassword
case len(d.username) == 0:
return ErrEmptyUsername
case len(d.password) == 0:
return ErrEmptyPassword
}
return nil
}
func (d *dnsomatic) String() string {
return toString(d.domain, d.host, constants.DNSOMATIC, d.ipVersion)
}
func (d *dnsomatic) Domain() string {
return d.domain
}
func (d *dnsomatic) Host() string {
return d.host
}
func (d *dnsomatic) IPVersion() ipversion.IPVersion {
return d.ipVersion
}
func (d *dnsomatic) Proxied() bool {
return false
}
func (d *dnsomatic) BuildDomainName() string {
return buildDomainName(d.host, d.domain)
}
func (d *dnsomatic) 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.dnsomatic.com/\">dnsomatic</a>",
IPVersion: models.HTML(d.ipVersion),
}
}
func (d *dnsomatic) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
// Multiple hosts can be updated in one query, see https://www.dnsomatic.com/docs/api
u := url.URL{
Scheme: "https",
Host: "updates.dnsomatic.com",
Path: "/nic/update",
User: url.UserPassword(d.username, d.password),
}
values := url.Values{}
values.Set("hostname", d.BuildDomainName())
if !d.useProviderIP {
values.Set("myip", ip.String())
}
values.Set("wildcard", "NOCHG")
if d.host == "*" {
values.Set("wildcard", "ON")
}
values.Set("mx", "NOCHG")
values.Set("backmx", "NOCHG")
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, s)
}
switch s {
case nohost, notfqdn:
return nil, ErrHostnameNotExists
case badauth:
return nil, ErrAuth
case badagent:
return nil, ErrBannedUserAgent
case abuse:
return nil, ErrAbuse
case "dnserr", nineoneone:
return nil, fmt.Errorf("%w: %s", ErrDNSServerSide, s)
}
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("%w: %s", ErrIPReceivedMalformed, ips[0])
}
if !d.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
}
return newIP, nil
}
return nil, fmt.Errorf("%w: %s", ErrUnknownResponse, s)
}

View File

@@ -2,7 +2,6 @@ package settings
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
@@ -12,18 +11,18 @@ import (
"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/pkg/publicip/ipversion"
"github.com/qdm12/golibs/network"
)
type dnspod struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
token string
}
func NewDNSPod(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
}{}
@@ -34,6 +33,7 @@ func NewDNSPod(data json.RawMessage, domain, host string, ipVersion ipversion.IP
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
token: extraSettings.Token,
}
if err := d.isValid(); err != nil {
@@ -44,7 +44,7 @@ func NewDNSPod(data json.RawMessage, domain, host string, ipVersion ipversion.IP
func (d *dnspod) isValid() error {
if len(d.token) == 0 {
return ErrEmptyToken
return fmt.Errorf("token cannot be empty")
}
return nil
}
@@ -61,12 +61,12 @@ func (d *dnspod) Host() string {
return d.host
}
func (d *dnspod) IPVersion() ipversion.IPVersion {
func (d *dnspod) IPVersion() models.IPVersion {
return d.ipVersion
}
func (d *dnspod) Proxied() bool {
return false
func (d *dnspod) DNSLookup() bool {
return d.dnsLookup
}
func (d *dnspod) BuildDomainName() string {
@@ -82,13 +82,7 @@ func (d *dnspod) HTML() models.HTMLRow {
}
}
func (d *dnspod) setHeaders(request *http.Request) {
setContentType(request, "application/x-www-form-urlencoded")
setAccept(request, "application/json")
setUserAgent(request)
}
func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (d *dnspod) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
recordType := A
if ip.To4() == nil {
recordType = AAAA
@@ -98,7 +92,6 @@ func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (ne
Host: "dnsapi.cn",
Path: "/Record.List",
}
values := url.Values{}
values.Set("login_token", d.token)
values.Set("format", "json")
@@ -106,26 +99,19 @@ func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (ne
values.Set("length", "200")
values.Set("sub_domain", d.host)
values.Set("record_type", recordType)
buffer := bytes.NewBufferString(values.Encode())
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
u.RawQuery = values.Encode()
r, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
if err != nil {
return nil, err
}
d.setHeaders(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := json.NewDecoder(response.Body)
var recordResp struct {
Records []struct {
ID string `json:"id"`
@@ -135,10 +121,9 @@ func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (ne
Line string `json:"line"`
} `json:"records"`
}
if err := decoder.Decode(&recordResp); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
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 {
@@ -152,7 +137,7 @@ func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (ne
}
}
if len(recordID) == 0 {
return nil, ErrNotFound
return nil, fmt.Errorf("record not found")
}
u.Path = "/Record.Ddns"
@@ -164,26 +149,19 @@ func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (ne
values.Set("value", ip.String())
values.Set("record_line", recordLine)
values.Set("sub_domain", d.host)
buffer = bytes.NewBufferString(values.Encode())
request, err = http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
u.RawQuery = values.Encode()
r, err = http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
if err != nil {
return nil, err
}
d.setHeaders(request)
response, err = client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder = json.NewDecoder(response.Body)
var ddnsResp struct {
Record struct {
ID int64 `json:"id"`
@@ -191,16 +169,12 @@ func (d *dnspod) Update(ctx context.Context, client *http.Client, ip net.IP) (ne
Name string `json:"name"`
} `json:"record"`
}
if err := decoder.Decode(&ddnsResp); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
if err := json.Unmarshal(content, &ddnsResp); err != nil {
return nil, err
}
ipStr := ddnsResp.Record.Value
receivedIP := net.ParseIP(ipStr)
if receivedIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ipStr)
} else if !ip.Equal(receivedIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, receivedIP.String())
receivedIP := net.ParseIP(ddnsResp.Record.Value)
if !ip.Equal(receivedIP) {
return nil, fmt.Errorf("ip not set")
}
return ip, nil
}

View File

@@ -1,7 +1,6 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"net"
@@ -12,20 +11,21 @@ import (
"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/pkg/publicip/ipversion"
netlib "github.com/qdm12/golibs/network"
)
//nolint:maligned
type donDominio struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
username string
password string
name string
}
func NewDonDominio(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
@@ -41,6 +41,7 @@ func NewDonDominio(data json.RawMessage, domain, host string, ipVersion ipversio
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
username: extraSettings.Username,
password: extraSettings.Password,
name: extraSettings.Name,
@@ -54,13 +55,13 @@ func NewDonDominio(data json.RawMessage, domain, host string, ipVersion ipversio
func (d *donDominio) isValid() error {
switch {
case len(d.username) == 0:
return ErrEmptyUsername
return fmt.Errorf("username cannot be empty")
case len(d.password) == 0:
return ErrEmptyPassword
return fmt.Errorf("password cannot be empty")
case len(d.name) == 0:
return ErrEmptyName
return fmt.Errorf("name cannot be empty")
case d.host != "@":
return ErrHostOnlyAt
return fmt.Errorf(`host can only be "@"`)
}
return nil
}
@@ -77,12 +78,12 @@ func (d *donDominio) Host() string {
return d.host
}
func (d *donDominio) IPVersion() ipversion.IPVersion {
return d.ipVersion
func (d *donDominio) DNSLookup() bool {
return d.dnsLookup
}
func (d *donDominio) Proxied() bool {
return false
func (d *donDominio) IPVersion() models.IPVersion {
return d.ipVersion
}
func (d *donDominio) BuildDomainName() string {
@@ -98,13 +99,7 @@ func (d *donDominio) HTML() models.HTMLRow {
}
}
func (d *donDominio) setHeaders(request *http.Request) {
setUserAgent(request)
setContentType(request, "application/x-www-form-urlencoded")
setAccept(request, "application/json")
}
func (d *donDominio) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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",
@@ -120,27 +115,18 @@ func (d *donDominio) Update(ctx context.Context, client *http.Client, ip net.IP)
} else {
values.Set("ipv6", ip.String())
}
buffer := strings.NewReader(values.Encode())
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
r, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(values.Encode()))
if err != nil {
return nil, err
}
d.setHeaders(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := json.NewDecoder(response.Body)
var responseData struct {
response := struct {
Success bool `json:"success"`
ErrorCode int `json:"errorCode"`
ErrorCodeMessage string `json:"errorCodeMsg"`
@@ -150,24 +136,20 @@ func (d *donDominio) Update(ctx context.Context, client *http.Client, ip net.IP)
IPv6 string `json:"ipv6"`
} `json:"gluerecords"`
} `json:"responseData"`
}{}
if err := json.Unmarshal(content, &response); err != nil {
return nil, err
}
if err := decoder.Decode(&responseData); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
if !response.Success {
return nil, fmt.Errorf("%s (error code %d)", response.ErrorCodeMessage, response.ErrorCode)
}
if !responseData.Success {
return nil, fmt.Errorf("%w: %s (error code %d)",
ErrUnsuccessfulResponse, responseData.ErrorCodeMessage, responseData.ErrorCode)
}
ipString := responseData.ResponseData.GlueRecords[0].IPv4
ipString := response.ResponseData.GlueRecords[0].IPv4
if !isIPv4 {
ipString = responseData.ResponseData.GlueRecords[0].IPv6
ipString = response.ResponseData.GlueRecords[0].IPv6
}
newIP = net.ParseIP(ipString)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ipString)
} else if !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
return nil, fmt.Errorf("IP address received %q is malformed", ipString)
}
return newIP, nil
}

View File

@@ -1,31 +1,29 @@
package settings
import (
"context"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"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/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/network"
)
type dreamhost struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
key string
matcher regex.Matcher
}
func NewDreamhost(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
matcher regex.Matcher) (s Settings, err error) {
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"`
}{}
@@ -39,6 +37,7 @@ func NewDreamhost(data json.RawMessage, domain, host string, ipVersion ipversion
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
key: extraSettings.Key,
matcher: matcher,
}
@@ -70,12 +69,12 @@ func (d *dreamhost) Host() string {
return d.host
}
func (d *dreamhost) IPVersion() ipversion.IPVersion {
func (d *dreamhost) IPVersion() models.IPVersion {
return d.ipVersion
}
func (d *dreamhost) Proxied() bool {
return false
func (d *dreamhost) DNSLookup() bool {
return d.dnsLookup
}
func (d *dreamhost) BuildDomainName() string {
@@ -91,22 +90,20 @@ func (d *dreamhost) HTML() models.HTMLRow {
}
}
func (d *dreamhost) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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 := d.getRecords(ctx, client)
records, err := listDreamhostRecords(client, d.key)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrListRecords, err)
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, ErrRecordNotEditable
return nil, fmt.Errorf("record data is not editable")
}
oldIP = net.ParseIP(data.Value)
if ip.Equal(oldIP) { // success, nothing to change
@@ -116,15 +113,11 @@ func (d *dreamhost) Update(ctx context.Context, client *http.Client, ip net.IP)
}
}
if oldIP != nil { // Found editable record with a different IP address, so remove it
if err := d.removeRecord(ctx, client, oldIP); err != nil {
return nil, fmt.Errorf("%w: %s", ErrRemoveRecord, err)
if err := removeDreamhostRecord(client, d.key, d.domain, oldIP); err != nil {
return nil, err
}
}
if err := d.createRecord(ctx, client, ip); err != nil {
return nil, fmt.Errorf("%w: %s", ErrCreateRecord, err)
}
return ip, nil
return ip, addDreamhostRecord(client, d.key, d.domain, ip)
}
type (
@@ -143,148 +136,106 @@ type (
}
)
func (d *dreamhost) defaultURLValues() (values url.Values) {
uuid := make([]byte, 16)
_, _ = io.ReadFull(rand.Reader, uuid)
//nolint:gomnd
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
//nolint:gomnd
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
values = url.Values{}
values.Set("key", d.key)
values.Set("unique_id", string(uuid))
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 (d *dreamhost) getRecords(ctx context.Context, client *http.Client) (
records dreamHostRecords, err error) {
func listDreamhostRecords(client network.Client, key string) (records dreamHostRecords, err error) {
u := url.URL{
Scheme: "https",
Host: "api.dreamhost.com",
}
values := d.defaultURLValues()
values := makeDreamhostDefaultValues(key)
values.Set("cmd", "dns-list_records")
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return records, err
}
setUserAgent(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return records, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&records); err != nil {
return records, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if records.Result != success {
return records, fmt.Errorf("%w: %s", ErrUnsuccessfulResponse, records.Result)
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 (d *dreamhost) removeRecord(ctx context.Context, client *http.Client, ip net.IP) error {
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 := d.defaultURLValues()
values := makeDreamhostDefaultValues(key)
values.Set("cmd", "dns-remove_record")
values.Set("record", d.domain)
values.Set("record", domain)
values.Set("type", recordType)
values.Set("value", ip.String())
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return err
}
setUserAgent(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
var dhResponse dreamhostReponse
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&dhResponse); err != nil {
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if dhResponse.Result != success { // this should not happen
return fmt.Errorf("%w: %s - %s",
ErrUnsuccessfulResponse, dhResponse.Result, dhResponse.Data)
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 (d *dreamhost) createRecord(ctx context.Context, client *http.Client, ip net.IP) error {
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 := d.defaultURLValues()
values := makeDreamhostDefaultValues(key)
values.Set("cmd", "dns-add_record")
values.Set("record", d.domain)
values.Set("record", domain)
values.Set("type", recordType)
values.Set("value", ip.String())
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return err
}
setUserAgent(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
var dhResponse dreamhostReponse
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&dhResponse); err != nil {
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if dhResponse.Result != success {
return fmt.Errorf("%w: %s - %s",
ErrUnsuccessfulResponse, dhResponse.Result, dhResponse.Data)
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
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -12,20 +10,21 @@ import (
"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/pkg/publicip/ipversion"
"github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
)
//nolint:maligned
type duckdns struct {
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
token string
useProviderIP bool
matcher regex.Matcher
}
func NewDuckdns(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
matcher regex.Matcher) (s Settings, err error) {
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"`
@@ -36,6 +35,7 @@ func NewDuckdns(data json.RawMessage, domain, host string, ipVersion ipversion.I
d := &duckdns{
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
token: extraSettings.Token,
useProviderIP: extraSettings.UseProviderIP,
matcher: matcher,
@@ -48,17 +48,17 @@ func NewDuckdns(data json.RawMessage, domain, host string, ipVersion ipversion.I
func (d *duckdns) isValid() error {
if !d.matcher.DuckDNSToken(d.token) {
return ErrMalformedToken
return fmt.Errorf("invalid token format")
}
switch d.host {
case "@", "*":
return ErrHostOnlySubdomain
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)
return toString("duckdns..org", d.host, constants.DUCKDNS, d.ipVersion)
}
func (d *duckdns) Domain() string {
@@ -69,12 +69,12 @@ func (d *duckdns) Host() string {
return d.host
}
func (d *duckdns) IPVersion() ipversion.IPVersion {
func (d *duckdns) IPVersion() models.IPVersion {
return d.ipVersion
}
func (d *duckdns) Proxied() bool {
return false
func (d *duckdns) DNSLookup() bool {
return d.dnsLookup
}
func (d *duckdns) BuildDomainName() string {
@@ -90,7 +90,7 @@ func (d *duckdns) HTML() models.HTMLRow {
}
}
func (d *duckdns) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (d *duckdns) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
u := url.URL{
Scheme: "https",
Host: "www.duckdns.org",
@@ -108,50 +108,37 @@ func (d *duckdns) Update(ctx context.Context, client *http.Client, ip net.IP) (n
values.Set("ip", ip.String())
}
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyDataToSingleLine(s))
}
const minChars = 2
s := string(content)
switch {
case len(s) < minChars:
return nil, fmt.Errorf("%w: response %q is too short", ErrUnmarshalResponse, s)
case s[0:minChars] == "KO":
return nil, ErrAuth
case s[0:minChars] == "OK":
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, ErrNoResultReceived
return nil, fmt.Errorf("no IP address in response")
}
newIP = net.ParseIP(ips[0])
if newIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ips[0])
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
}
if ip != nil && !newIP.Equal(ip) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
}
return newIP, nil
default:
return nil, fmt.Errorf("%w: %s", ErrUnknownResponse, s)
return nil, fmt.Errorf("invalid response %q", s)
}
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -12,20 +10,21 @@ import (
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/regex"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/network"
)
//nolint:maligned
type dyn struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
username string
password string
useProviderIP bool
}
func NewDyn(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
@@ -38,6 +37,7 @@ func NewDyn(data json.RawMessage, domain, host string, ipVersion ipversion.IPVer
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
@@ -51,11 +51,11 @@ func NewDyn(data json.RawMessage, domain, host string, ipVersion ipversion.IPVer
func (d *dyn) isValid() error {
switch {
case len(d.username) == 0:
return ErrEmptyUsername
return fmt.Errorf("username cannot be empty")
case len(d.password) == 0:
return ErrEmptyPassword
return fmt.Errorf("password cannot be empty")
case d.host == "*":
return ErrHostWildcard
return fmt.Errorf(`host cannot be "*"`)
}
return nil
}
@@ -72,12 +72,12 @@ func (d *dyn) Host() string {
return d.host
}
func (d *dyn) IPVersion() ipversion.IPVersion {
func (d *dyn) IPVersion() models.IPVersion {
return d.ipVersion
}
func (d *dyn) Proxied() bool {
return false
func (d *dyn) DNSLookup() bool {
return d.dnsLookup
}
func (d *dyn) BuildDomainName() string {
@@ -93,7 +93,7 @@ func (d *dyn) HTML() models.HTMLRow {
}
}
func (d *dyn) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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),
@@ -101,43 +101,37 @@ func (d *dyn) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP
Path: "/v3/update",
}
values := url.Values{}
values.Set("hostname", d.BuildDomainName())
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()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
if status != http.StatusOK {
return nil, fmt.Errorf("HTTP status %d", status)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyDataToSingleLine(s))
}
s := string(content)
switch {
case strings.HasPrefix(s, notfqdn):
return nil, ErrHostnameNotExists
case strings.HasPrefix(s, "notfqdn"):
return nil, fmt.Errorf("fully qualified domain name is not valid")
case strings.HasPrefix(s, "badrequest"):
return nil, ErrBadRequest
return nil, fmt.Errorf("bad request")
case strings.HasPrefix(s, "good"):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", ErrUnknownResponse, s)
return nil, fmt.Errorf("unknown response: %s", s)
}
}

View File

@@ -1,131 +0,0 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/regex"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)
type dynV6 struct {
domain string
host string
ipVersion ipversion.IPVersion
token string
useProviderIP bool
}
func NewDynV6(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ 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 := &dynV6{
domain: domain,
host: host,
ipVersion: ipVersion,
token: extraSettings.Token,
useProviderIP: extraSettings.UseProviderIP,
}
if err := d.isValid(); err != nil {
return nil, err
}
return d, nil
}
func (d *dynV6) isValid() error {
switch {
case len(d.token) == 0:
return ErrEmptyToken
case d.host == "*":
return ErrHostWildcard
}
return nil
}
func (d *dynV6) String() string {
return fmt.Sprintf("[domain: %s | host: %s | provider: DynV6]", d.domain, d.host)
}
func (d *dynV6) Domain() string {
return d.domain
}
func (d *dynV6) Host() string {
return d.host
}
func (d *dynV6) IPVersion() ipversion.IPVersion {
return d.ipVersion
}
func (d *dynV6) Proxied() bool {
return false
}
func (d *dynV6) BuildDomainName() string {
return buildDomainName(d.host, d.domain)
}
func (d *dynV6) 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://dynv6.com/\">DynV6 DNS</a>",
IPVersion: models.HTML(d.ipVersion),
}
}
func (d *dynV6) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
isIPv4 := ip.To4() != nil
host := "dynv6.com"
if isIPv4 {
host = "ipv4." + host
} else {
host = "ipv6." + host
}
u := url.URL{
Scheme: "https",
Host: host,
Path: "/api/update",
}
values := url.Values{}
values.Set("token", d.token)
values.Set("zone", d.BuildDomainName())
if !d.useProviderIP {
if isIPv4 {
values.Set("ipv4", ip.String())
} else {
values.Set("ipv6", ip.String())
}
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
return ip, nil
}
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}

View File

@@ -1,74 +0,0 @@
package settings
import "errors"
var (
ErrIPv6NotSupported = errors.New("IPv6 is not supported by this provider")
)
// Validation errors.
var (
ErrEmptyName = errors.New("empty name")
ErrEmptyPassword = errors.New("empty password")
ErrEmptyKey = errors.New("empty key")
ErrEmptyAppKey = errors.New("empty app key")
ErrEmptyConsumerKey = errors.New("empty consumer key")
ErrEmptySecret = errors.New("empty secret")
ErrEmptyToken = errors.New("empty token")
ErrEmptyTTL = errors.New("TTL is not set")
ErrEmptyUsername = errors.New("empty username")
ErrEmptyZoneIdentifier = errors.New("empty zone identifier")
ErrHostOnlyAt = errors.New(`host can only be "@"`)
ErrHostOnlySubdomain = errors.New("host can only be a subdomain")
ErrHostWildcard = errors.New(`host cannot be a "*"`)
ErrMalformedEmail = errors.New("malformed email address")
ErrMalformedKey = errors.New("malformed key")
ErrMalformedPassword = errors.New("malformed password")
ErrMalformedToken = errors.New("malformed token")
ErrMalformedUsername = errors.New("malformed username")
ErrMalformedUserServiceKey = errors.New("malformed user service key")
)
// Intermediary steps errors.
var (
ErrCreateRecord = errors.New("cannot create record")
ErrGetDomainID = errors.New("cannot get domain ID")
ErrGetRecordID = errors.New("cannot get record ID")
ErrGetRecordInZone = errors.New("cannot get record in zone") // LuaDNS
ErrGetZoneID = errors.New("cannot get zone ID") // LuaDNS
ErrListRecords = errors.New("cannot list records") // Dreamhost
ErrRemoveRecord = errors.New("cannot remove record") // Dreamhost
ErrUpdateRecord = errors.New("cannot update record")
)
// Update errors.
var (
ErrAbuse = errors.New("banned due to abuse")
ErrAccountInactive = errors.New("account is inactive")
ErrAuth = errors.New("bad authentication")
ErrRequestEncode = errors.New("cannot encode request")
ErrBadHTTPStatus = errors.New("bad HTTP status")
ErrBadRequest = errors.New("bad request sent")
ErrBannedUserAgent = errors.New("user agend is banned")
ErrConflictingRecord = errors.New("conflicting record")
ErrDNSServerSide = errors.New("server side DNS error")
ErrDomainDisabled = errors.New("record disabled")
ErrDomainIDNotFound = errors.New("ID not found in domain record")
ErrFeatureUnavailable = errors.New("feature is not available to the user")
ErrHostnameNotExists = errors.New("hostname does not exist")
ErrInvalidSystemParam = errors.New("invalid system parameter")
ErrIPReceivedMalformed = errors.New("malformed IP address received")
ErrIPReceivedMismatch = errors.New("mismatching IP address received")
ErrMalformedIPSent = errors.New("malformed IP address sent")
ErrNoResultReceived = errors.New("no result received")
ErrNotFound = errors.New("not found")
ErrNumberOfResultsReceived = errors.New("wrong number of results received")
ErrPrivateIPSent = errors.New("private IP cannot be routed")
ErrRecordNotEditable = errors.New("record is not editable") // Dreamhost
ErrRecordNotFound = errors.New("record not found")
ErrRequestMarshal = errors.New("cannot marshal request body")
ErrUnknownResponse = errors.New("unknown response received")
ErrUnmarshalResponse = errors.New("cannot unmarshal update response")
ErrUnsuccessfulResponse = errors.New("unsuccessful response")
ErrZoneNotFound = errors.New("zone not found") // LuaDNS
)

View File

@@ -1,139 +0,0 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"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/ddns-updater/pkg/publicip/ipversion"
)
type freedns struct {
domain string
host string
ipVersion ipversion.IPVersion
token string
}
func NewFreedns(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
extraSettings := struct {
Token string `json:"token"`
}{}
if err := json.Unmarshal(data, &extraSettings); err != nil {
return nil, err
}
f := &freedns{
domain: domain,
host: host,
ipVersion: ipVersion,
token: extraSettings.Token,
}
if err := f.isValid(); err != nil {
return nil, err
}
return f, nil
}
func (f *freedns) isValid() error {
if len(f.token) == 0 {
return ErrEmptyToken
}
return nil
}
func (f *freedns) String() string {
return toString(f.domain, f.host, constants.FREEDNS, f.ipVersion)
}
func (f *freedns) Domain() string {
return f.domain
}
func (f *freedns) Host() string {
return f.host
}
func (f *freedns) Proxied() bool {
return false
}
func (f *freedns) IPVersion() ipversion.IPVersion {
return f.ipVersion
}
func (f *freedns) BuildDomainName() string {
return buildDomainName(f.host, f.domain)
}
func (f *freedns) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", f.BuildDomainName(), f.BuildDomainName())),
Host: models.HTML(f.Host()),
Provider: "<a href=\"https://freedns.afraid.org/\">FreeDNS</a>",
IPVersion: models.HTML(f.ipVersion),
}
}
func (f *freedns) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
var hostPrefix string
if ip.To4() == nil {
hostPrefix = "v6."
}
u := url.URL{
Scheme: "https",
Host: hostPrefix + "sync.afraid.org",
Path: "/u/" + f.token + "/",
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", ErrBadHTTPStatus, response.StatusCode, s)
}
if s == "" {
return nil, ErrNoResultReceived
}
// Example: Updated demo.freshdns.com from 50.23.197.94 to 2607:f0d0:1102:d5::2
words := strings.Fields(s)
const expectedWords = 6
if len(words) != expectedWords {
return nil, fmt.Errorf("%w: not enough fields in response: %s", ErrUnmarshalResponse, s)
}
ipString := words[5]
newIP = net.ParseIP(ipString)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, newIP)
}
return newIP, nil
}

View File

@@ -1,147 +0,0 @@
package settings
import (
"bytes"
"context"
"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/ddns-updater/pkg/publicip/ipversion"
)
type gandi struct {
domain string
host string
ttl int
ipVersion ipversion.IPVersion
key string
}
func NewGandi(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
extraSettings := struct {
Key string `json:"key"`
TTL int `json:"ttl"`
}{}
if err := json.Unmarshal(data, &extraSettings); err != nil {
return nil, err
}
g := &gandi{
domain: domain,
host: host,
ipVersion: ipVersion,
key: extraSettings.Key,
ttl: extraSettings.TTL,
}
if err := g.isValid(); err != nil {
return nil, err
}
return g, nil
}
func (g *gandi) isValid() error {
if len(g.key) == 0 {
return ErrEmptyKey
}
return nil
}
func (g *gandi) String() string {
return toString(g.domain, g.host, constants.GANDI, g.ipVersion)
}
func (g *gandi) Domain() string {
return g.domain
}
func (g *gandi) Host() string {
return g.host
}
func (g *gandi) IPVersion() ipversion.IPVersion {
return g.ipVersion
}
func (g *gandi) Proxied() bool {
return false
}
func (g *gandi) BuildDomainName() string {
return buildDomainName(g.host, g.domain)
}
func (g *gandi) 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://www.gandi.net/\">gandi</a>",
IPVersion: models.HTML(g.ipVersion),
}
}
func (g *gandi) setHeaders(request *http.Request) {
setUserAgent(request)
setContentType(request, "application/json")
setAccept(request, "application/json")
request.Header.Set("X-Api-Key", g.key)
}
func (g *gandi) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
recordType := A
var ipStr string
if ip.To4() == nil { // IPv6
recordType = AAAA
ipStr = ip.To16().String()
} else {
ipStr = ip.To4().String()
}
u := url.URL{
Scheme: "https",
Host: "dns.api.gandi.net",
Path: fmt.Sprintf("/api/v5/domains/%s/records/%s/%s", g.domain, g.host, recordType),
}
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
const defaultTTL = 3600
ttl := defaultTTL
if g.ttl != 0 {
ttl = g.ttl
}
requestData := struct {
Values [1]string `json:"rrset_values"`
TTL int `json:"rrset_ttl"`
}{
Values: [1]string{ipStr},
TTL: ttl,
}
if err := encoder.Encode(requestData); err != nil {
return nil, fmt.Errorf("%w: %s", ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
if err != nil {
return nil, err
}
g.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
return ip, nil
}

View File

@@ -1,32 +1,30 @@
package settings
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"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"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
netlib "github.com/qdm12/golibs/network"
)
type godaddy struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
key string
secret string
matcher regex.Matcher
}
func NewGodaddy(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
matcher regex.Matcher) (s Settings, err error) {
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"`
@@ -38,6 +36,7 @@ func NewGodaddy(data json.RawMessage, domain, host string, ipVersion ipversion.I
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
key: extraSettings.Key,
secret: extraSettings.Secret,
matcher: matcher,
@@ -51,9 +50,9 @@ func NewGodaddy(data json.RawMessage, domain, host string, ipVersion ipversion.I
func (g *godaddy) isValid() error {
switch {
case !g.matcher.GodaddyKey(g.key):
return ErrMalformedKey
case len(g.secret) == 0:
return ErrEmptySecret
return fmt.Errorf("invalid key format")
case !g.matcher.GodaddySecret(g.secret):
return fmt.Errorf("invalid secret format")
}
return nil
}
@@ -70,12 +69,12 @@ func (g *godaddy) Host() string {
return g.host
}
func (g *godaddy) IPVersion() ipversion.IPVersion {
func (g *godaddy) IPVersion() models.IPVersion {
return g.ipVersion
}
func (g *godaddy) Proxied() bool {
return false
func (g *godaddy) DNSLookup() bool {
return g.dnsLookup
}
func (g *godaddy) BuildDomainName() string {
@@ -91,14 +90,7 @@ func (g *godaddy) HTML() models.HTMLRow {
}
}
func (g *godaddy) setHeaders(request *http.Request) {
setUserAgent(request)
setAuthSSOKey(request, g.key, g.secret)
setContentType(request, "application/json")
setAccept(request, "application/json")
}
func (g *godaddy) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (g *godaddy) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
recordType := A
if ip.To4() == nil {
recordType = AAAA
@@ -111,44 +103,25 @@ func (g *godaddy) Update(ctx context.Context, client *http.Client, ip net.IP) (n
Host: "api.godaddy.com",
Path: fmt.Sprintf("/v1/domains/%s/records/%s/%s", g.domain, recordType, g.host),
}
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
requestData := []goDaddyPutBody{
{Data: ip.String()},
}
if err := encoder.Encode(requestData); err != nil {
return nil, fmt.Errorf("%w: %s", ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
r, err := network.BuildHTTPPut(u.String(), []goDaddyPutBody{{ip.String()}})
if err != nil {
return nil, err
}
g.setHeaders(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
return ip, nil
}
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
var parsedJSON struct {
Message string `json:"message"`
}
jsonErr := json.Unmarshal(b, &parsedJSON)
if jsonErr != nil || len(parsedJSON.Message) == 0 {
return nil, fmt.Errorf("%w: %s", err, bodyDataToSingleLine(string(b)))
}
return nil, fmt.Errorf("%w: %s", err, parsedJSON.Message)
return ip, nil
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -13,21 +11,22 @@ import (
"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/pkg/publicip/ipversion"
netlib "github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
)
//nolint:maligned
type google struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
username string
password string
useProviderIP bool
}
func NewGoogle(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
@@ -40,6 +39,7 @@ func NewGoogle(data json.RawMessage, domain, host string, ipVersion ipversion.IP
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
@@ -53,9 +53,9 @@ func NewGoogle(data json.RawMessage, domain, host string, ipVersion ipversion.IP
func (g *google) isValid() error {
switch {
case len(g.username) == 0:
return ErrEmptyUsername
return fmt.Errorf("username cannot be empty")
case len(g.password) == 0:
return ErrEmptyPassword
return fmt.Errorf("password cannot be empty")
}
return nil
}
@@ -72,12 +72,12 @@ func (g *google) Host() string {
return g.host
}
func (g *google) IPVersion() ipversion.IPVersion {
return g.ipVersion
func (g *google) DNSLookup() bool {
return g.dnsLookup
}
func (g *google) Proxied() bool {
return false
func (g *google) IPVersion() models.IPVersion {
return g.ipVersion
}
func (g *google) BuildDomainName() string {
@@ -93,7 +93,7 @@ func (g *google) HTML() models.HTMLRow {
}
}
func (g *google) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (g *google) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
u := url.URL{
Scheme: "https",
Host: "domains.google.com",
@@ -107,55 +107,51 @@ func (g *google) Update(ctx context.Context, client *http.Client, ip net.IP) (ne
values.Set("myip", ip.String())
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
r.Header.Set("User-Agent", "DDNS-Updater quentig.mcgaw@gmail.com")
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
s := string(content)
switch s {
case "":
return nil, fmt.Errorf("%w: %d: %s", ErrBadHTTPStatus, response.StatusCode, s)
case nohost, notfqdn:
return nil, ErrHostnameNotExists
return nil, fmt.Errorf("HTTP status %d", status)
case nohost:
return nil, fmt.Errorf("hostname does not exist")
case badauth:
return nil, ErrAuth
case badagent:
return nil, ErrBannedUserAgent
case abuse:
return nil, ErrAbuse
case nineoneone:
return nil, ErrDNSServerSide
case "conflict A", "conflict AAAA":
return nil, ErrConflictingRecord
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 len(ips) == 0 {
return nil, ErrNoResultReceived
if ips == nil {
return nil, fmt.Errorf("no IP address in response")
}
newIP = net.ParseIP(ips[0])
if newIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ips[0])
} else if !g.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
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("%w: %s", ErrUnknownResponse, s)
return nil, fmt.Errorf("invalid response %q", s)
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -13,20 +11,21 @@ import (
"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/pkg/publicip/ipversion"
netlib "github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
)
//nolint:maligned
type he struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
password string
useProviderIP bool
}
func NewHe(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
@@ -38,6 +37,7 @@ func NewHe(data json.RawMessage, domain, host string, ipVersion ipversion.IPVers
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
}
@@ -49,7 +49,7 @@ func NewHe(data json.RawMessage, domain, host string, ipVersion ipversion.IPVers
func (h *he) isValid() error {
if len(h.password) == 0 {
return ErrEmptyPassword
return fmt.Errorf("password cannot be empty")
}
return nil
}
@@ -66,12 +66,12 @@ func (h *he) Host() string {
return h.host
}
func (h *he) IPVersion() ipversion.IPVersion {
return h.ipVersion
func (h *he) DNSLookup() bool {
return h.dnsLookup
}
func (h *he) Proxied() bool {
return false
func (h *he) IPVersion() models.IPVersion {
return h.ipVersion
}
func (h *he) BuildDomainName() string {
@@ -87,7 +87,7 @@ func (h *he) HTML() models.HTMLRow {
}
}
func (h *he) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (h *he) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) {
fqdn := h.BuildDomainName()
u := url.URL{
Scheme: "https",
@@ -101,30 +101,21 @@ func (h *he) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP
values.Set("myip", ip.String())
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
r.Header.Set("User-Agent", "DDNS-Updater quentih.mcgaw@gmail.com")
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
s := string(content)
switch s {
case "":
return nil, fmt.Errorf("%w: %d: %s", ErrBadHTTPStatus, response.StatusCode, s)
return nil, fmt.Errorf("HTTP status %d", status)
case badauth:
return nil, ErrAuth
return nil, fmt.Errorf("invalid username password combination")
}
if strings.Contains(s, "nochg") || strings.Contains(s, "good") {
verifier := verification.NewVerifier()
@@ -132,15 +123,16 @@ func (h *he) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP
ipsV6 := verifier.SearchIPv6(s)
ips := append(ipsV4, ipsV6...)
if ips == nil {
return nil, ErrNoResultReceived
return nil, fmt.Errorf("no IP address in response")
}
newIP = net.ParseIP(ips[0])
if newIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ips[0])
} else if !h.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
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("%w: %s", ErrUnknownResponse, s)
return nil, fmt.Errorf("invalid response %q", s)
}

View File

@@ -1,31 +0,0 @@
package settings
import "net/http"
func setUserAgent(request *http.Request) {
request.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
}
func setContentType(request *http.Request, contentType string) {
request.Header.Set("Content-Type", contentType)
}
func setAccept(request *http.Request, acceptContent string) {
request.Header.Set("Accept", acceptContent)
}
func setAuthBearer(request *http.Request, token string) {
request.Header.Set("Authorization", "Bearer "+token)
}
func setAuthSSOKey(request *http.Request, key, secret string) {
request.Header.Set("Authorization", "sso-key "+key+":"+secret)
}
func setOauth(request *http.Request, value string) {
request.Header.Set("oauth", value)
}
func setXFilter(request *http.Request, value string) {
request.Header.Set("X-Filter", value)
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -13,20 +11,21 @@ import (
"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/pkg/publicip/ipversion"
"github.com/qdm12/golibs/network"
)
//nolint:maligned
type infomaniak struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
username string
password string
useProviderIP bool
}
func NewInfomaniak(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
@@ -39,6 +38,7 @@ func NewInfomaniak(data json.RawMessage, domain, host string, ipVersion ipversio
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
@@ -52,11 +52,11 @@ func NewInfomaniak(data json.RawMessage, domain, host string, ipVersion ipversio
func (i *infomaniak) isValid() error {
switch {
case len(i.username) == 0:
return ErrEmptyUsername
return fmt.Errorf("username cannot be empty")
case len(i.password) == 0:
return ErrEmptyPassword
return fmt.Errorf("password cannot be empty")
case i.host == "*":
return ErrHostWildcard
return fmt.Errorf(`host cannot be "*"`)
}
return nil
}
@@ -73,12 +73,12 @@ func (i *infomaniak) Host() string {
return i.host
}
func (i *infomaniak) IPVersion() ipversion.IPVersion {
func (i *infomaniak) IPVersion() models.IPVersion {
return i.ipVersion
}
func (i *infomaniak) Proxied() bool {
return false
func (i *infomaniak) DNSLookup() bool {
return i.dnsLookup
}
func (i *infomaniak) BuildDomainName() string {
@@ -94,7 +94,7 @@ func (i *infomaniak) HTML() models.HTMLRow {
}
}
func (i *infomaniak) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (i *infomaniak) Update(client network.Client, ip net.IP) (newIP net.IP, err error) {
u := url.URL{
Scheme: "https",
Host: "infomaniak.com",
@@ -110,57 +110,48 @@ func (i *infomaniak) Update(ctx context.Context, client *http.Client, ip net.IP)
values.Set("myip", ip.String())
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
switch response.StatusCode {
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("%w: %s", ErrIPReceivedMalformed, s)
return nil, fmt.Errorf("no received IP in response %q", s)
} else if ip != nil && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, 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("%w: in response %q", ErrNoResultReceived, s)
return nil, fmt.Errorf("no received IP in response %q", s)
} else if ip != nil && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, 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("%w: %s", ErrUnknownResponse, s)
return nil, fmt.Errorf("ok status but unknown response %q", s)
}
case http.StatusBadRequest:
switch s {
case nohost:
return nil, ErrHostnameNotExists
return nil, fmt.Errorf("infomaniak.com: host %q does not exist for domain %q", i.host, i.domain)
case badauth:
return nil, ErrAuth
return nil, fmt.Errorf("infomaniak.com: bad authentication")
default:
return nil, fmt.Errorf("%w: %d: %s", ErrBadHTTPStatus, response.StatusCode, s)
return nil, fmt.Errorf("infomaniak.com: bad request: %s", s)
}
default:
return nil, fmt.Errorf("%w: %d: %s", ErrBadHTTPStatus, response.StatusCode, s)
return nil, fmt.Errorf("received status %d with message: %s", status, s)
}
}

View File

@@ -1,358 +0,0 @@
package settings
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"strconv"
"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/pkg/publicip/ipversion"
)
type linode struct {
domain string
host string
ipVersion ipversion.IPVersion
token string
}
func NewLinode(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
extraSettings := struct {
Token string `json:"token"`
}{}
if err := json.Unmarshal(data, &extraSettings); err != nil {
return nil, err
}
l := &linode{
domain: domain,
host: host,
ipVersion: ipVersion,
token: extraSettings.Token,
}
if err := l.isValid(); err != nil {
return nil, err
}
return l, nil
}
func (l *linode) isValid() error {
if len(l.token) == 0 {
return ErrEmptyToken
}
return nil
}
func (l *linode) String() string {
return toString(l.domain, l.host, constants.LINODE, l.ipVersion)
}
func (l *linode) Domain() string {
return l.domain
}
func (l *linode) Host() string {
return l.host
}
func (l *linode) IPVersion() ipversion.IPVersion {
return l.ipVersion
}
func (l *linode) Proxied() bool {
return false
}
func (l *linode) BuildDomainName() string {
return buildDomainName(l.host, l.domain)
}
func (l *linode) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", l.BuildDomainName(), l.BuildDomainName())),
Host: models.HTML(l.Host()),
Provider: "<a href=\"https://cloud.linode.com/\">Linode</a>",
IPVersion: models.HTML(l.ipVersion),
}
}
// Using https://www.linode.com/docs/api/domains/
func (l *linode) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
domainID, err := l.getDomainID(ctx, client)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrGetDomainID, err)
}
recordType := A
if ip.To4() == nil {
recordType = AAAA
}
recordID, err := l.getRecordID(ctx, client, domainID, recordType)
if errors.Is(err, ErrNotFound) {
err := l.createRecord(ctx, client, domainID, recordType, ip)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrCreateRecord, err)
}
return ip, nil
} else if err != nil {
return nil, fmt.Errorf("%w: %s", ErrGetRecordID, err)
}
if err := l.updateRecord(ctx, client, domainID, recordID, ip); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUpdateRecord, err)
}
return ip, nil
}
type linodeError struct {
Field string `json:"field"`
Reason string `json:"reason"`
}
func (l *linode) setHeaders(request *http.Request) {
setUserAgent(request)
setContentType(request, "application/json")
setAuthBearer(request, l.token)
}
func (l *linode) getDomainID(ctx context.Context, client *http.Client) (domainID int, err error) {
u := url.URL{
Scheme: "https",
Host: "api.linode.com",
Path: "/v4/domains",
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return 0, err
}
l.setHeaders(request)
setOauth(request, "domains:read_only")
setXFilter(request, `{"domain": "`+l.domain+`"}`)
response, err := client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
return 0, fmt.Errorf("%w: %s", err, l.getError(response.Body))
}
decoder := json.NewDecoder(response.Body)
var obj struct {
Data []struct {
ID *int `json:"id,omitempty"`
Type string `json:"type"`
Status string `json:"status"`
} `json:"data"`
}
if err := decoder.Decode(&obj); err != nil {
return 0, err
}
domains := obj.Data
switch len(domains) {
case 0:
return 0, ErrNotFound
case 1:
default:
return 0, fmt.Errorf("%w: %d records instead of 1",
ErrNumberOfResultsReceived, len(domains))
}
if domains[0].Status == "disabled" {
return 0, ErrDomainDisabled
}
if domains[0].ID == nil {
return 0, ErrDomainIDNotFound
}
return *domains[0].ID, nil
}
func (l *linode) getRecordID(ctx context.Context, client *http.Client,
domainID int, recordType string) (recordID int, err error) {
u := url.URL{
Scheme: "https",
Host: "api.linode.com",
Path: "/v4/domains/" + strconv.Itoa(domainID) + "/records",
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return 0, err
}
l.setHeaders(request)
setOauth(request, "domains:read_only")
response, err := client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
return 0, fmt.Errorf("%w: %s", err, l.getError(response.Body))
}
decoder := json.NewDecoder(response.Body)
var obj struct {
Data []struct {
ID int `json:"id"`
Host string `json:"name"`
Type string `json:"type"`
} `json:"data"`
}
if err := decoder.Decode(&obj); err != nil {
return 0, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
for _, domainRecord := range obj.Data {
if domainRecord.Type == recordType && domainRecord.Host == l.host {
return domainRecord.ID, nil
}
}
return 0, ErrNotFound
}
func (l *linode) createRecord(ctx context.Context, client *http.Client,
domainID int, recordType string, ip net.IP) (err error) {
u := url.URL{
Scheme: "https",
Host: "api.linode.com",
Path: "/v4/domains/" + strconv.Itoa(domainID) + "/records",
}
type domainRecord struct {
Type string `json:"type"`
Host string `json:"name"`
IP string `json:"target"`
}
requestData := domainRecord{
Type: recordType,
Host: l.host,
IP: ip.String(),
}
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
if err := encoder.Encode(requestData); err != nil {
return fmt.Errorf("%w: %s", ErrRequestMarshal, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
if err != nil {
return err
}
l.setHeaders(request)
setOauth(request, "domains:read_write")
response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
return fmt.Errorf("%w: %s", err, l.getError(response.Body))
}
var responseData domainRecord
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&responseData); err != nil {
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
newIP := net.ParseIP(responseData.IP)
if newIP == nil {
return fmt.Errorf("%w: %s", ErrIPReceivedMalformed, responseData.IP)
} else if !newIP.Equal(ip) {
return fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
}
return nil
}
func (l *linode) updateRecord(ctx context.Context, client *http.Client,
domainID, recordID int, ip net.IP) (err error) {
u := url.URL{
Scheme: "https",
Host: "api.linode.com",
Path: "/v4/domains/" + strconv.Itoa(domainID) + "/records/" + strconv.Itoa(recordID),
}
data := struct {
IP string `json:"target"`
}{
IP: ip.String(),
}
buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
if err := encoder.Encode(data); err != nil {
return fmt.Errorf("%w: %s", ErrRequestMarshal, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
if err != nil {
return err
}
l.setHeaders(request)
setOauth(request, "domains:read_write")
response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
return fmt.Errorf("%w: %s", err, l.getError(response.Body))
}
data.IP = ""
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(&data); err != nil {
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
newIP := net.ParseIP(data.IP)
if newIP == nil {
return fmt.Errorf("%w: %s", ErrIPReceivedMalformed, data.IP)
} else if !newIP.Equal(ip) {
return fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
}
return nil
}
func (l *linode) getError(body io.Reader) (err error) {
var errorObj linodeError
b, err := ioutil.ReadAll(body)
if err != nil {
return err
}
if err := json.Unmarshal(b, &errorObj); err != nil {
return fmt.Errorf("%s", bodyDataToSingleLine(string(b)))
}
return fmt.Errorf("%s: %s", errorObj.Field, errorObj.Reason)
}

View File

@@ -1,284 +0,0 @@
package settings
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"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/ddns-updater/pkg/publicip/ipversion"
"github.com/qdm12/golibs/verification"
)
type luaDNS struct {
domain string
host string
ipVersion ipversion.IPVersion
email string
token string
}
func NewLuaDNS(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
extraSettings := struct {
Email string `json:"email"`
Token string `json:"token"`
}{}
if err := json.Unmarshal(data, &extraSettings); err != nil {
return nil, err
}
l := &luaDNS{
domain: domain,
host: host,
ipVersion: ipVersion,
email: extraSettings.Email,
token: extraSettings.Token,
}
if err := l.isValid(); err != nil {
return nil, err
}
return l, nil
}
func (l *luaDNS) isValid() error {
switch {
case !verification.NewRegex().MatchEmail(l.email):
return ErrMalformedEmail
case len(l.token) == 0:
return ErrEmptyToken
}
return nil
}
func (l *luaDNS) String() string {
return toString(l.domain, l.host, constants.LUADNS, l.ipVersion)
}
func (l *luaDNS) Domain() string {
return l.domain
}
func (l *luaDNS) Host() string {
return l.host
}
func (l *luaDNS) IPVersion() ipversion.IPVersion {
return l.ipVersion
}
func (l *luaDNS) Proxied() bool {
return false
}
func (l *luaDNS) BuildDomainName() string {
return buildDomainName(l.host, l.domain)
}
func (l *luaDNS) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", l.BuildDomainName(), l.BuildDomainName())),
Host: models.HTML(l.Host()),
Provider: "<a href=\"https://www.luadns.com/\">LuaDNS</a>",
IPVersion: models.HTML(l.ipVersion),
}
}
func (l *luaDNS) setHeaders(request *http.Request) {
setUserAgent(request)
setAccept(request, "application/json")
}
// Using https://www.luadns.com/api.html
func (l *luaDNS) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
zoneID, err := l.getZoneID(ctx, client)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrGetZoneID, err)
}
record, err := l.getRecord(ctx, client, zoneID, ip)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrGetRecordInZone, err)
}
newRecord := record
newRecord.Content = ip.String()
if err := l.updateRecord(ctx, client, zoneID, newRecord); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUpdateRecord, err)
}
return ip, nil
}
type luaDNSRecord struct {
ID int `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL int `json:"ttl"`
}
type luaDNSError struct {
Status string `json:"status"`
Message string `json:"message"`
}
func (l *luaDNS) getZoneID(ctx context.Context, client *http.Client) (zoneID int, err error) {
u := url.URL{
Scheme: "https",
Host: "api.luadns.com",
Path: "/v1/zones",
User: url.UserPassword(l.email, l.token),
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return 0, err
}
request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
response, err := client.Do(request)
if err != nil {
return 0, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return 0, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
var errorObj luaDNSError
if jsonErr := json.Unmarshal(b, &errorObj); jsonErr != nil {
return 0, fmt.Errorf("%w: %s", err, bodyDataToSingleLine(string(b)))
}
return 0, fmt.Errorf("%w: %s: %s", err, errorObj.Status, errorObj.Message)
}
type zone struct {
ID int `json:"id"`
Name string `json:"name"`
}
var zones []zone
if err := json.Unmarshal(b, &zones); err != nil {
return 0, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
for _, zone := range zones {
if zone.Name == l.domain {
return zone.ID, nil
}
}
return 0, ErrZoneNotFound
}
func (l *luaDNS) getRecord(ctx context.Context, client *http.Client, zoneID int, ip net.IP) (
record luaDNSRecord, err error) {
u := url.URL{
Scheme: "https",
Host: "api.luadns.com",
Path: fmt.Sprintf("/v1/zones/%d/records", zoneID),
User: url.UserPassword(l.email, l.token),
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return record, err
}
l.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return record, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return record, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
var errorObj luaDNSError
if jsonErr := json.Unmarshal(b, &errorObj); jsonErr != nil {
return record, fmt.Errorf("%w: %s", err, bodyDataToSingleLine(string(b)))
}
return record, fmt.Errorf("%w: %s: %s",
err, errorObj.Status, errorObj.Message)
}
var records []luaDNSRecord
if err := json.Unmarshal(b, &records); err != nil {
return record, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
recordType := A
if ip.To4() == nil {
recordType = AAAA
}
for _, record := range records {
if record.Type == recordType {
return record, nil
}
}
return record, fmt.Errorf("%w: %s record in zone %d",
ErrRecordNotFound, recordType, zoneID)
}
func (l *luaDNS) updateRecord(ctx context.Context, client *http.Client,
zoneID int, newRecord luaDNSRecord) (err error) {
u := url.URL{
Scheme: "https",
Host: "api.luadns.com",
Path: fmt.Sprintf("/v1/zones/%d/records/%d", zoneID, newRecord.ID),
User: url.UserPassword(l.email, l.token),
}
data, err := json.Marshal(newRecord)
if err != nil {
return err
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), bytes.NewBuffer(data))
if err != nil {
return err
}
l.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if response.StatusCode != http.StatusOK {
err = fmt.Errorf("%w: %d", ErrBadHTTPStatus, response.StatusCode)
var errorObj luaDNSError
if jsonErr := json.Unmarshal(b, &errorObj); jsonErr != nil {
return fmt.Errorf("%w: %s", err, bodyDataToSingleLine(string(b)))
}
return fmt.Errorf("%w: %s: %s",
err, errorObj.Status, errorObj.Message)
}
var updatedRecord luaDNSRecord
if jsonErr := json.Unmarshal(b, &updatedRecord); jsonErr != nil {
return fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if updatedRecord.Content != newRecord.Content {
return fmt.Errorf("%w: %s", ErrIPReceivedMismatch, updatedRecord.Content)
}
return nil
}

View File

@@ -1,7 +1,6 @@
package settings
import (
"context"
"encoding/json"
"encoding/xml"
"fmt"
@@ -12,22 +11,23 @@ import (
"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/pkg/publicip/ipversion"
"github.com/qdm12/golibs/network"
)
//nolint:maligned
type namecheap struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
password string
useProviderIP bool
matcher regex.Matcher
}
func NewNamecheap(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
matcher regex.Matcher) (s Settings, err error) {
if ipVersion == ipversion.IP6 {
return s, ErrIPv6NotSupported
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"`
@@ -40,6 +40,7 @@ func NewNamecheap(data json.RawMessage, domain, host string, ipVersion ipversion
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
matcher: matcher,
@@ -52,7 +53,7 @@ func NewNamecheap(data json.RawMessage, domain, host string, ipVersion ipversion
func (n *namecheap) isValid() error {
if !n.matcher.NamecheapPassword(n.password) {
return ErrMalformedPassword
return fmt.Errorf("invalid password format")
}
return nil
}
@@ -69,12 +70,12 @@ func (n *namecheap) Host() string {
return n.host
}
func (n *namecheap) IPVersion() ipversion.IPVersion {
func (n *namecheap) IPVersion() models.IPVersion {
return n.ipVersion
}
func (n *namecheap) Proxied() bool {
return false
func (n *namecheap) DNSLookup() bool {
return n.dnsLookup
}
func (n *namecheap) BuildDomainName() string {
@@ -90,12 +91,7 @@ func (n *namecheap) HTML() models.HTMLRow {
}
}
func (n *namecheap) setHeaders(request *http.Request) {
setUserAgent(request)
setAccept(request, "application/xml")
}
func (n *namecheap) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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",
@@ -109,44 +105,35 @@ func (n *namecheap) Update(ctx context.Context, client *http.Client, ip net.IP)
values.Set("ip", ip.String())
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
n.setHeaders(request)
response, err := client.Do(request)
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)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
ErrBadHTTPStatus, response.StatusCode, bodyToSingleLine(response.Body))
}
decoder := xml.NewDecoder(response.Body)
var parsedXML struct {
Errors struct {
Error string `xml:"Err1"`
} `xml:"errors"`
IP string `xml:"IP"`
}
if err := decoder.Decode(&parsedXML); err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
if parsedXML.Errors.Error != "" {
return nil, fmt.Errorf("%w: %s", ErrUnsuccessfulResponse, parsedXML.Errors.Error)
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("%w: %s", ErrIPReceivedMalformed, parsedXML.IP)
return nil, fmt.Errorf("IP address received %q is malformed", parsedXML.IP)
}
if ip != nil && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
}
return newIP, nil
}

View File

@@ -1,10 +1,8 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
@@ -13,21 +11,22 @@ import (
"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/pkg/publicip/ipversion"
netlib "github.com/qdm12/golibs/network"
"github.com/qdm12/golibs/verification"
)
//nolint:maligned
type noip struct {
domain string
host string
ipVersion ipversion.IPVersion
ipVersion models.IPVersion
dnsLookup bool
username string
password string
useProviderIP bool
}
func NewNoip(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ regex.Matcher) (s Settings, err error) {
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"`
@@ -40,6 +39,7 @@ func NewNoip(data json.RawMessage, domain, host string, ipVersion ipversion.IPVe
domain: domain,
host: host,
ipVersion: ipVersion,
dnsLookup: !noDNSLookup,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
@@ -51,16 +51,15 @@ func NewNoip(data json.RawMessage, domain, host string, ipVersion ipversion.IPVe
}
func (n *noip) isValid() error {
const maxUsernameLength = 50
switch {
case len(n.username) == 0:
return ErrEmptyUsername
case len(n.username) > maxUsernameLength:
return fmt.Errorf("%w: longer than 50 characters", ErrMalformedUsername)
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 ErrEmptyPassword
return fmt.Errorf("password cannot be empty")
case n.host == "*":
return ErrHostWildcard
return fmt.Errorf(`host cannot be "*"`)
}
return nil
}
@@ -77,12 +76,12 @@ func (n *noip) Host() string {
return n.host
}
func (n *noip) IPVersion() ipversion.IPVersion {
return n.ipVersion
func (n *noip) DNSLookup() bool {
return n.dnsLookup
}
func (n *noip) Proxied() bool {
return false
func (n *noip) IPVersion() models.IPVersion {
return n.ipVersion
}
func (n *noip) BuildDomainName() string {
@@ -98,7 +97,7 @@ func (n *noip) HTML() models.HTMLRow {
}
}
func (n *noip) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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",
@@ -115,58 +114,45 @@ func (n *noip) Update(ctx context.Context, client *http.Client, ip net.IP) (newI
}
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
status, content, err := client.DoHTTPRequest(r)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", ErrBadHTTPStatus, response.StatusCode, s)
}
s := string(content)
switch s {
case "":
return nil, ErrNoResultReceived
case nineoneone:
return nil, ErrDNSServerSide
case abuse:
return nil, ErrAbuse
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, ErrFeatureUnavailable
case badagent:
return nil, ErrBannedUserAgent
return nil, fmt.Errorf("user has not this extra feature")
case "badagent":
return nil, fmt.Errorf("user agent is banned")
case badauth:
return nil, ErrAuth
return nil, fmt.Errorf("invalid username password combination")
case nohost:
return nil, ErrHostnameNotExists
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, ErrNoResultReceived
return nil, fmt.Errorf("no IP address in response")
}
newIP = net.ParseIP(ips[0])
if newIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, ips[0])
return nil, fmt.Errorf("IP address received %q is malformed", ips[0])
}
if !n.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, newIP.String())
return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String())
}
return newIP, nil
}
return nil, ErrUnknownResponse
return nil, fmt.Errorf("invalid response %q", s)
}

View File

@@ -1,143 +0,0 @@
package settings
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"strings"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/regex"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)
type opendns struct {
domain string
host string
ipVersion ipversion.IPVersion
username string
password string
useProviderIP bool
}
func NewOpendns(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
_ 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
}
o := &opendns{
domain: domain,
host: host,
ipVersion: ipVersion,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
}
if err := o.isValid(); err != nil {
return nil, err
}
return o, nil
}
func (o *opendns) isValid() error {
switch {
case len(o.username) == 0:
return ErrEmptyUsername
case len(o.password) == 0:
return ErrEmptyPassword
case o.host == "*":
return ErrHostWildcard
}
return nil
}
func (o *opendns) String() string {
return fmt.Sprintf("[domain: %s | host: %s | provider: Opendns]", o.domain, o.host)
}
func (o *opendns) Domain() string {
return o.domain
}
func (o *opendns) Host() string {
return o.host
}
func (o *opendns) IPVersion() ipversion.IPVersion {
return o.ipVersion
}
func (o *opendns) Proxied() bool {
return false
}
func (o *opendns) BuildDomainName() string {
return buildDomainName(o.host, o.domain)
}
func (o *opendns) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", o.BuildDomainName(), o.BuildDomainName())),
Host: models.HTML(o.Host()),
Provider: "<a href=\"https://opendns.com/\">Opendns DNS</a>",
IPVersion: models.HTML(o.ipVersion),
}
}
func (o *opendns) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
u := url.URL{
Scheme: "https",
User: url.UserPassword(o.username, o.password),
Host: "updates.opendns.com",
Path: "/nic/update",
}
values := url.Values{}
values.Set("hostname", o.BuildDomainName())
if !o.useProviderIP {
values.Set("myip", ip.String())
}
u.RawQuery = values.Encode()
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
setUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", ErrBadHTTPStatus, response.StatusCode, s)
}
if !strings.HasPrefix(s, "good ") {
return nil, fmt.Errorf("%w: %s", ErrUnknownResponse, s)
}
responseIPString := strings.TrimPrefix(s, "good ")
responseIP := net.ParseIP(responseIPString)
if responseIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMalformed, responseIPString)
} else if !newIP.Equal(ip) {
return nil, fmt.Errorf("%w: %s", ErrIPReceivedMismatch, responseIP)
}
return ip, nil
}

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