mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-22 09:02:44 -04:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af68f9ba0f | ||
|
|
f7171e4b01 | ||
|
|
0c028f70e9 | ||
|
|
c194681856 | ||
|
|
9c31616b46 | ||
|
|
55668d0310 | ||
|
|
3bdb8ba5ac | ||
|
|
345cc754ff | ||
|
|
9e05c6164d | ||
|
|
ea79ca53ea | ||
|
|
6a3c280f30 | ||
|
|
01e982a4cd | ||
|
|
99d33bbcf9 | ||
|
|
e38351e5a4 |
@@ -43,10 +43,58 @@
|
||||
"deepCompletion": true,
|
||||
"usePlaceholders": false
|
||||
},
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": [
|
||||
"--fast",
|
||||
"--enable",
|
||||
"rowserrcheck",
|
||||
"--enable",
|
||||
"bodyclose",
|
||||
"--enable",
|
||||
"dogsled",
|
||||
"--enable",
|
||||
"dupl",
|
||||
"--enable",
|
||||
"gochecknoglobals",
|
||||
"--enable",
|
||||
"gochecknoinits",
|
||||
"--enable",
|
||||
"gocognit",
|
||||
"--enable",
|
||||
"goconst",
|
||||
"--enable",
|
||||
"gocritic",
|
||||
"--enable",
|
||||
"gocyclo",
|
||||
"--enable",
|
||||
"goimports",
|
||||
"--enable",
|
||||
"golint",
|
||||
"--enable",
|
||||
"gosec",
|
||||
"--enable",
|
||||
"interfacer",
|
||||
"--enable",
|
||||
"maligned",
|
||||
"--enable",
|
||||
"misspell",
|
||||
"--enable",
|
||||
"nakedret",
|
||||
"--enable",
|
||||
"prealloc",
|
||||
"--enable",
|
||||
"scopelint",
|
||||
"--enable",
|
||||
"unconvert",
|
||||
"--enable",
|
||||
"unparam",
|
||||
"--enable",
|
||||
"whitespace"
|
||||
],
|
||||
// Golang on save
|
||||
"go.buildOnSave": "package",
|
||||
"go.lintOnSave": "package",
|
||||
"go.vetOnSave": "package",
|
||||
"go.buildOnSave": "workspace",
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.vetOnSave": "workspace",
|
||||
"editor.formatOnSave": true,
|
||||
"[go]": {
|
||||
"editor.codeActionsOnSave": {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
.devcontainer
|
||||
.git
|
||||
*.exe
|
||||
.github
|
||||
.vscode
|
||||
.travis.yml
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
*.md
|
||||
readme
|
||||
.gitignore
|
||||
.devcontainer
|
||||
.vscode
|
||||
config.json
|
||||
docker-compose.yml
|
||||
LICENSE
|
||||
README.md
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
||||
*.exe
|
||||
updater
|
||||
.vscode
|
||||
.vscode
|
||||
|
||||
49
.golangci.yml
Normal file
49
.golangci.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
linters-settings:
|
||||
maligned:
|
||||
suggest-new: true
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- dogsled
|
||||
- dupl
|
||||
- errcheck
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- goimports
|
||||
- golint
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- maligned
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
|
||||
run:
|
||||
skip-dirs:
|
||||
- .devcontainer
|
||||
- .github
|
||||
|
||||
service:
|
||||
golangci-lint-version: 1.26.x # use the fixed version to not introduce new linters unexpectedly
|
||||
31
Dockerfile
31
Dockerfile
@@ -1,17 +1,25 @@
|
||||
ARG ALPINE_VERSION=3.11
|
||||
ARG GO_VERSION=1.13
|
||||
ARG GO_VERSION=1.14
|
||||
|
||||
FROM alpine:${ALPINE_VERSION} AS alpine
|
||||
RUN apk --update add ca-certificates tzdata
|
||||
|
||||
FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder
|
||||
RUN apk --update add git g++
|
||||
ARG GOLANGCI_LINT_VERSION=v1.26.0
|
||||
RUN apk --update add git
|
||||
ENV CGO_ENABLED=0
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s ${GOLANGCI_LINT_VERSION}
|
||||
WORKDIR /tmp/gobuild
|
||||
COPY .golangci.yml .
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download 2>&1
|
||||
COPY internal/ ./internal/
|
||||
COPY cmd/updater/main.go .
|
||||
RUN CGO_ENABLED=0 go test ./...
|
||||
RUN CGO_ENABLED=1 go build -a -installsuffix cgo -ldflags="-s -w" -o app
|
||||
RUN go test ./...
|
||||
RUN go build -ldflags="-s -w" -o app
|
||||
RUN golangci-lint run --timeout=10m
|
||||
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
FROM scratch
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION
|
||||
@@ -25,13 +33,8 @@ LABEL \
|
||||
org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \
|
||||
org.opencontainers.image.title="ddns-updater" \
|
||||
org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Namecheap, Cloudflare, GoDaddy, DuckDns, Dreamhost, DNSPod and NoIP"
|
||||
RUN apk add --update sqlite ca-certificates && \
|
||||
mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2 && \
|
||||
rm -rf /var/cache/apk/* && \
|
||||
# Creating empty database file in case nothing is mounted
|
||||
mkdir -p /updater/data && \
|
||||
chown -R 1000 /updater && \
|
||||
chmod 700 /updater/data
|
||||
COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo
|
||||
EXPOSE 8000
|
||||
HEALTHCHECK --interval=60s --timeout=5s --start-period=10s --retries=2 CMD ["/updater/app", "healthcheck"]
|
||||
USER 1000
|
||||
@@ -44,6 +47,8 @@ ENV DELAY=10m \
|
||||
NODE_ID=0 \
|
||||
HTTP_TIMEOUT=10s \
|
||||
GOTIFY_URL= \
|
||||
GOTIFY_TOKEN=
|
||||
GOTIFY_TOKEN= \
|
||||
BACKUP_PERIOD=0 \
|
||||
BACKUP_DIRECTORY=/updater/data
|
||||
COPY --from=builder --chown=1000 /tmp/gobuild/app /updater/app
|
||||
COPY --chown=1000 ui/* /updater/ui/
|
||||
|
||||
20
README.md
20
README.md
@@ -2,11 +2,9 @@
|
||||
|
||||
*Light container updating DNS A records periodically for GoDaddy, Namecheap, Cloudflare, Dreamhost, NoIP, DNSPod, Infomaniak, ddnss.de and DuckDNS*
|
||||
|
||||
**SQLite migration support will be removed on 1 April 2020, so be sure to update your image before that**
|
||||
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
|
||||
[](https://travis-ci.org/qdm12/ddns-updater)
|
||||
[](https://github.com/qdm12/ddns-updater/actions?query=workflow%3A%22Buildx+latest%22)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://hub.docker.com/r/qmcgaw/ddns-updater)
|
||||
[](https://microbadger.com/images/qmcgaw/ddns-updater)
|
||||
@@ -24,12 +22,12 @@
|
||||
|
||||

|
||||
|
||||
- Lightweight based on a Go binary and *Alpine 3.11* with Sqlite and Ca-Certificates packages
|
||||
- 12.3MB Docker image based on a Go static binary in a Scratch Docker image with ca-certificates and timezone data
|
||||
- Persistence with a JSON file *updates.json* to store old IP addresses with change times for each record
|
||||
- Docker healthcheck verifying the DNS resolution of your domains
|
||||
- Highly configurable
|
||||
- Sends notifications to your Android phone, see the [**Gotify**](#Gotify) section (it's free, open source and self hosted 🆒)
|
||||
- Compatible with `amd64`, `386`, `arm64` and `arm32v7` (Raspberry Pis) CPU architectures.
|
||||
- Compatible with `amd64`, `386`, `arm64`, `arm32v7` (Raspberry Pis) CPU architectures.
|
||||
|
||||
## Setup
|
||||
|
||||
@@ -94,6 +92,8 @@
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
1. You can update the image with `docker pull qmcgaw/ddns-updater`. Other [Docker image tags are available](https://hub.docker.com/repository/docker/qmcgaw/ddns-updater/tags).
|
||||
|
||||
## Configuration
|
||||
|
||||
Start by having the following content in *config.json*:
|
||||
@@ -148,12 +148,12 @@ Namecheap:
|
||||
|
||||
Cloudflare:
|
||||
|
||||
- `"zone_identifier"`
|
||||
- `"identifier"`
|
||||
- `"zone_identifier"` is the Zone ID of your site
|
||||
- `"identifier"` is the DNS record identifier as returned by the Cloudflare "List DNS Records" API (see below)
|
||||
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
|
||||
- `"ttl"` integer value for record TTL in seconds (specify 1 for automatic)
|
||||
- One of the following:
|
||||
- Email `"email"` and key `"key"`
|
||||
- Email `"email"` and Global API Key `"key"`
|
||||
- User service key `"user_service_key"`
|
||||
- API Token `"token"`, configured with DNS edit permissions for your DNS name's zone.
|
||||
- *Optionally*, `"proxied"` can be `true` or `false` to use the proxy services of Cloudflare
|
||||
@@ -210,6 +210,8 @@ DDNSS.de:
|
||||
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
|
||||
| `GOTIFY_URL` | | (optional) HTTP(s) URL to your Gotify server |
|
||||
| `GOTIFY_TOKEN` | | (optional) Token to access your Gotify server |
|
||||
| `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`.
|
||||
|
||||
### Host firewall
|
||||
|
||||
@@ -376,6 +378,8 @@ To set it up with DDNS updater:
|
||||
|
||||
## TODOs
|
||||
|
||||
- [ ] Update dependencies
|
||||
- [ ] Mockgen instead of mockery
|
||||
- [ ] Other types or records
|
||||
- [ ] icon.ico for webpage
|
||||
- [ ] Record events log
|
||||
|
||||
@@ -1,151 +1,274 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
"github.com/qdm12/golibs/files"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/server"
|
||||
"github.com/qdm12/golibs/signals"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/env"
|
||||
"github.com/qdm12/ddns-updater/internal/handlers"
|
||||
"github.com/qdm12/ddns-updater/internal/healthcheck"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/trigger"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger, err := logging.NewLogger(logging.ConsoleEncoding, logging.InfoLevel, -1)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
paramsReader := params.NewParamsReader(logger)
|
||||
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
} else {
|
||||
logger, err = logging.NewLogger(encoding, level, nodeID)
|
||||
}
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
// Running the program in a separate instance through the Docker
|
||||
// built-in healthcheck, in an ephemeral fashion to query the
|
||||
// long running instance of the program about its status
|
||||
if err := libhealthcheck.Query(); err != nil {
|
||||
logger.Error(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
fmt.Println(splash.Splash(paramsReader))
|
||||
e := env.NewEnv(logger)
|
||||
gotifyURL, err := paramsReader.GetGotifyURL()
|
||||
e.FatalOnError(err)
|
||||
if gotifyURL != nil {
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
e.FatalOnError(err)
|
||||
e.SetGotify(admin.NewGotify(*gotifyURL, gotifyToken, &http.Client{Timeout: time.Second}))
|
||||
}
|
||||
listeningPort, warning, err := paramsReader.GetListeningPort()
|
||||
e.FatalOnError(err)
|
||||
if len(warning) > 0 {
|
||||
logger.Warn(warning)
|
||||
}
|
||||
rootURL, err := paramsReader.GetRootURL()
|
||||
e.FatalOnError(err)
|
||||
defaultPeriod, err := paramsReader.GetDelay(libparams.Default("10m"))
|
||||
e.FatalOnError(err)
|
||||
dir, err := paramsReader.GetExeDir()
|
||||
e.FatalOnError(err)
|
||||
dataDir, err := paramsReader.GetDataDir(dir)
|
||||
e.FatalOnError(err)
|
||||
fileManager := files.NewFileManager()
|
||||
dbSQLiteExists, err := fileManager.FileExists(dataDir + "/updates.db")
|
||||
e.FatalOnError(err)
|
||||
dbJSONExists, err := fileManager.FileExists(dataDir + "/updates.json")
|
||||
e.FatalOnError(err)
|
||||
var persistentDB persistence.Database
|
||||
if dbSQLiteExists && !dbJSONExists {
|
||||
logger.Warn("Migrating from SQLite to JSON based database file")
|
||||
sqlite, err := persistence.NewSQLite(dataDir)
|
||||
e.FatalOnError(err)
|
||||
persistentDB, err = persistence.NewJSON(dataDir)
|
||||
e.FatalOnError(err)
|
||||
err = persistence.Migrate(sqlite, persistentDB, logger)
|
||||
e.FatalOnError(err)
|
||||
logger.Info("Success; you can now safely delete %s", dataDir+"/updates.db")
|
||||
} else {
|
||||
persistentDB, err = persistence.NewJSON(dataDir)
|
||||
e.FatalOnError(err)
|
||||
}
|
||||
go signals.WaitForExit(e.ShutdownFromSignal)
|
||||
settings, warnings, err := paramsReader.GetSettings(dataDir + "/config.json")
|
||||
for _, w := range warnings {
|
||||
e.Warn(w)
|
||||
}
|
||||
if err != nil {
|
||||
e.Fatal(err)
|
||||
}
|
||||
if len(settings) > 1 {
|
||||
logger.Info("Found %d settings to update records", len(settings))
|
||||
} else if len(settings) == 1 {
|
||||
logger.Info("Found single setting to update records")
|
||||
}
|
||||
for _, err := range network.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
e.Warn(err)
|
||||
}
|
||||
var records []models.Record
|
||||
idToPeriod := make(map[int]time.Duration)
|
||||
for id, setting := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", setting.Domain, setting.Host)
|
||||
events, err := persistentDB.GetEvents(setting.Domain, setting.Host)
|
||||
if err != nil {
|
||||
e.FatalOnError(err)
|
||||
}
|
||||
records = append(records, models.NewRecord(setting, events))
|
||||
idToPeriod[id] = defaultPeriod
|
||||
if setting.Delay > 0 {
|
||||
idToPeriod[id] = setting.Delay
|
||||
}
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
e.FatalOnError(err)
|
||||
client := network.NewClient(HTTPTimeout)
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
e.SetDB(db)
|
||||
updater := update.NewUpdater(db, logger, client, e.Notify)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
forceUpdate := trigger.StartUpdates(ctx, updater, idToPeriod, e.CheckError)
|
||||
forceUpdate()
|
||||
productionHandlerFunc := handlers.NewHandler(rootURL, dir, db, logger, forceUpdate, e.CheckError).GetHandlerFunc()
|
||||
healthcheckHandlerFunc := libhealthcheck.GetHandler(func() error {
|
||||
return healthcheck.IsHealthy(db, net.LookupIP, logger)
|
||||
})
|
||||
logger.Info("Web UI listening at address 0.0.0.0:%s with root URL %s", listeningPort, rootURL)
|
||||
e.Notify(1, fmt.Sprintf("Just launched\nIt has %d records to watch", len(records)))
|
||||
serverErrs := server.RunServers(
|
||||
server.Settings{Name: "production", Addr: "0.0.0.0:" + listeningPort, Handler: productionHandlerFunc},
|
||||
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
if len(serverErrs) > 0 {
|
||||
e.Fatal(serverErrs)
|
||||
}
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
libhealthcheck "github.com/qdm12/golibs/healthcheck"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/server"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/backup"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/handlers"
|
||||
"github.com/qdm12/ddns-updater/internal/healthcheck"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/trigger"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
)
|
||||
|
||||
func main() {
|
||||
os.Exit(_main(context.Background(), time.Now))
|
||||
// returns 1 on error
|
||||
// returns 2 on os signal
|
||||
}
|
||||
|
||||
func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
if libhealthcheck.Mode(os.Args) {
|
||||
// Running the program in a separate instance through the Docker
|
||||
// built-in healthcheck, in an ephemeral fashion to query the
|
||||
// long running instance of the program about its status
|
||||
if err := libhealthcheck.Query(); err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
logger, err := setupLogger()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return 1
|
||||
}
|
||||
paramsReader := params.NewReader(logger)
|
||||
|
||||
fmt.Println(splash.Splash(
|
||||
paramsReader.GetVersion(),
|
||||
paramsReader.GetVcsRef(),
|
||||
paramsReader.GetBuildDate()))
|
||||
|
||||
notify, err := setupGotify(paramsReader, logger)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, err := getParams(paramsReader)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
|
||||
persistentDB, err := persistence.NewJSON(dataDir)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
settings, warnings, err := paramsReader.GetSettings(dataDir + "/config.json")
|
||||
for _, w := range warnings {
|
||||
logger.Warn(w)
|
||||
notify(2, w)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
if len(settings) > 1 {
|
||||
logger.Info("Found %d settings to update records", len(settings))
|
||||
} else if len(settings) == 1 {
|
||||
logger.Info("Found single setting to update records")
|
||||
}
|
||||
for _, err := range connectivity.NewConnectivity(5 * time.Second).Checks("google.com") {
|
||||
logger.Warn(err)
|
||||
}
|
||||
records := make([]models.Record, len(settings))
|
||||
idToPeriod := make(map[int]time.Duration)
|
||||
i := 0
|
||||
for id, setting := range settings {
|
||||
logger.Info("Reading history from database: domain %s host %s", setting.Domain, setting.Host)
|
||||
events, err := persistentDB.GetEvents(setting.Domain, setting.Host)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
records[i] = models.NewRecord(setting, events)
|
||||
idToPeriod[id] = defaultPeriod
|
||||
if setting.Delay > 0 {
|
||||
idToPeriod[id] = setting.Delay
|
||||
}
|
||||
i++
|
||||
}
|
||||
HTTPTimeout, err := paramsReader.GetHTTPTimeout()
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
notify(4, err)
|
||||
return 1
|
||||
}
|
||||
client := network.NewClient(HTTPTimeout)
|
||||
defer client.Close()
|
||||
db := data.NewDatabase(records, persistentDB)
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}()
|
||||
updater := update.NewUpdater(db, logger, client, notify)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
checkError := func(err error) {
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
forceUpdate := trigger.StartUpdates(ctx, updater, idToPeriod, checkError)
|
||||
forceUpdate()
|
||||
productionHandlerFunc := handlers.NewHandler(rootURL, dir, db, logger, forceUpdate, checkError).GetHandlerFunc()
|
||||
healthcheckHandlerFunc := libhealthcheck.GetHandler(func() error {
|
||||
return healthcheck.IsHealthy(db, net.LookupIP, logger)
|
||||
})
|
||||
logger.Info("Web UI listening at address 0.0.0.0:%s with root URL %s", listeningPort, rootURL)
|
||||
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:" + listeningPort, Handler: productionHandlerFunc},
|
||||
server.Settings{Name: "healthcheck", Addr: "127.0.0.1:9999", Handler: healthcheckHandlerFunc},
|
||||
)
|
||||
}()
|
||||
|
||||
go backupRunLoop(ctx, backupPeriod, dir, backupDirectory, logger, timeNow)
|
||||
|
||||
osSignals := make(chan os.Signal, 1)
|
||||
signal.Notify(osSignals,
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
os.Interrupt,
|
||||
)
|
||||
select {
|
||||
case errors := <-serverErrors:
|
||||
for _, err := range errors {
|
||||
logger.Error(err)
|
||||
}
|
||||
return 1
|
||||
case signal := <-osSignals:
|
||||
message := fmt.Sprintf("Stopping program: caught OS signal %q", signal)
|
||||
logger.Warn(message)
|
||||
notify(2, message)
|
||||
return 2
|
||||
case <-ctx.Done():
|
||||
message := fmt.Sprintf("Stopping program: %s", ctx.Err())
|
||||
logger.Warn(message)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func setupLogger() (logging.Logger, error) {
|
||||
paramsReader := params.NewReader(nil)
|
||||
encoding, level, nodeID, err := paramsReader.GetLoggerConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logging.NewLogger(encoding, level, nodeID)
|
||||
}
|
||||
|
||||
func setupGotify(paramsReader params.Reader, logger logging.Logger) (notify func(priority int, messageArgs ...interface{}), err error) {
|
||||
gotifyURL, err := paramsReader.GetGotifyURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if gotifyURL == nil {
|
||||
return func(priority int, messageArgs ...interface{}) {}, nil
|
||||
}
|
||||
gotifyToken, err := paramsReader.GetGotifyToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gotify := admin.NewGotify(*gotifyURL, gotifyToken, &http.Client{Timeout: time.Second})
|
||||
return func(priority int, messageArgs ...interface{}) {
|
||||
if err := gotify.Notify("DDNS Updater", priority, messageArgs...); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getParams(paramsReader params.Reader) (
|
||||
dir, dataDir,
|
||||
listeningPort, rootURL string,
|
||||
defaultPeriod time.Duration,
|
||||
backupPeriod time.Duration, backupDirectory string,
|
||||
err error) {
|
||||
dir, err = paramsReader.GetExeDir()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
dataDir, err = paramsReader.GetDataDir(dir)
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
listeningPort, _, err = paramsReader.GetListeningPort()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
rootURL, err = paramsReader.GetRootURL()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
defaultPeriod, err = paramsReader.GetDelay(libparams.Default("10m"))
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
|
||||
backupPeriod, err = paramsReader.GetBackupPeriod()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
backupDirectory, err = paramsReader.GetBackupDirectory()
|
||||
if err != nil {
|
||||
return "", "", "", "", 0, 0, "", err
|
||||
}
|
||||
return dir, dataDir, listeningPort, rootURL, defaultPeriod, backupPeriod, backupDirectory, nil
|
||||
}
|
||||
|
||||
func backupRunLoop(ctx context.Context, backupPeriod time.Duration, exeDir, outputDir string,
|
||||
logger logging.Logger, timeNow func() time.Time) {
|
||||
logger = logger.WithPrefix("backup: ")
|
||||
if backupPeriod == 0 {
|
||||
logger.Info("disabled")
|
||||
return
|
||||
}
|
||||
logger.Info("each %s; writing zip files to directory %s", backupPeriod, outputDir)
|
||||
ziper := backup.NewZiper()
|
||||
timer := time.NewTimer(backupPeriod)
|
||||
for {
|
||||
filepath := fmt.Sprintf("%s/ddns-updater-backup-%d.zip", outputDir, timeNow().UnixNano())
|
||||
if err := ziper.ZipFiles(
|
||||
filepath,
|
||||
fmt.Sprintf("%s/data/updates.json", exeDir),
|
||||
fmt.Sprintf("%s/data/config.json", exeDir)); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
select {
|
||||
case <-timer.C:
|
||||
timer.Reset(backupPeriod)
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,4 +18,6 @@ services:
|
||||
- HTTP_TIMEOUT=10s
|
||||
- GOTIFY_URL=
|
||||
- GOTIFY_TOKEN=
|
||||
- BACKUP_PERIOD=0
|
||||
- BACKUP_DIRECTORY=/updater/data
|
||||
restart: always
|
||||
|
||||
8
go.mod
8
go.mod
@@ -3,9 +3,9 @@ module github.com/qdm12/ddns-updater
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible
|
||||
github.com/mattn/go-sqlite3 v1.10.0
|
||||
github.com/qdm12/golibs v0.0.0-20200224233831-af1ada8e2052
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151
|
||||
github.com/stretchr/testify v1.5.1
|
||||
)
|
||||
|
||||
26
go.sum
26
go.sum
@@ -10,6 +10,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb h1:D4uzjWwKYQ5XnAvUbuvHW93esHg7F8N/OYeBBcJoTr0=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI=
|
||||
@@ -35,6 +37,8 @@ github.com/go-openapi/swag v0.17.0 h1:iqrgMg7Q7SvtbWLlltPrkMs0UBJI6oTSs79JFRUi88
|
||||
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
|
||||
github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq8iS6U=
|
||||
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
@@ -51,10 +55,15 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible h1:+DYU2RgpI6OHG4oQkM5KlqD3Wd3UPEsX8jamTo1Mp6o=
|
||||
github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible h1:gaQFbK2+uSxOR4iGZprJAbpmtqTrHhSdgOyIMD6Oidc=
|
||||
github.com/kyokomi/emoji v2.2.2+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
|
||||
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
|
||||
@@ -67,8 +76,8 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/qdm12/golibs v0.0.0-20200224233831-af1ada8e2052 h1:6KVUzd4oLyHcmmYsXdpE3HR9FUSx3FF8UBTRlJPCKLc=
|
||||
github.com/qdm12/golibs v0.0.0-20200224233831-af1ada8e2052/go.mod h1:YULaFjj6VGmhjak6f35sUWwEleHUmngN5IQ3kdvd6XE=
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151 h1:5q8oyhJqgQyW5v427CDC34SobllqiJCLLfS3Z4EeLCI=
|
||||
github.com/qdm12/golibs v0.0.0-20200430173218-57de728e2151/go.mod h1:pikkTN7g7zRuuAnERwqW1yAFq6pYmxrxpjiwGvb0Ysc=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -77,6 +86,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc=
|
||||
@@ -100,10 +111,15 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwL
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs=
|
||||
@@ -119,3 +135,5 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
67
internal/backup/zip.go
Normal file
67
internal/backup/zip.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Ziper interface {
|
||||
ZipFiles(outputFilepath string, inputFilepaths ...string) error
|
||||
}
|
||||
|
||||
type ziper struct {
|
||||
createFile func(name string) (*os.File, error)
|
||||
openFile func(name string) (*os.File, error)
|
||||
ioCopy func(dst io.Writer, src io.Reader) (written int64, err error)
|
||||
}
|
||||
|
||||
func NewZiper() Ziper {
|
||||
return &ziper{
|
||||
createFile: os.Create,
|
||||
openFile: os.Open,
|
||||
ioCopy: io.Copy,
|
||||
}
|
||||
}
|
||||
|
||||
func (z *ziper) ZipFiles(outputFilepath string, inputFilepaths ...string) error {
|
||||
f, err := z.createFile(outputFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
w := zip.NewWriter(f)
|
||||
defer w.Close()
|
||||
for _, filepath := range inputFilepaths {
|
||||
if err := z.addFile(w, filepath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *ziper) addFile(w *zip.Writer, filepath string) error {
|
||||
f, err := z.openFile(filepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Using FileInfoHeader() above only uses the basename of the file. If we want
|
||||
// to preserve the folder structure we can overwrite this with the full path.
|
||||
// header.Name = filepath
|
||||
header.Method = zip.Deflate
|
||||
ioWriter, err := w.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = z.ioCopy(ioWriter, f)
|
||||
return err
|
||||
}
|
||||
@@ -3,34 +3,34 @@ package constants
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
HTML_FAIL models.HTML = `<font color="red"><b>Failure</b></font>`
|
||||
HTML_SUCCESS models.HTML = `<font color="green"><b>Success</b></font>`
|
||||
HTML_UPTODATE models.HTML = `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
HTML_UPDATING models.HTML = `<font color="orange"><b>Updating</b></font>`
|
||||
HTMLFail models.HTML = `<font color="red"><b>Failure</b></font>`
|
||||
HTMLSuccess models.HTML = `<font color="green"><b>Success</b></font>`
|
||||
HTMLUpdate models.HTML = `<font color="#00CC66"><b>Up to date</b></font>`
|
||||
HTMLUpdating models.HTML = `<font color="orange"><b>Updating</b></font>`
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO have a struct model containing URL, name for each provider
|
||||
HTML_NAMECHEAP models.HTML = "<a href=\"https://namecheap.com\">Namecheap</a>"
|
||||
HTML_GODADDY models.HTML = "<a href=\"https://godaddy.com\">GoDaddy</a>"
|
||||
HTML_DUCKDNS models.HTML = "<a href=\"https://duckdns.org\">DuckDNS</a>"
|
||||
HTML_DREAMHOST models.HTML = "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>"
|
||||
HTML_CLOUDFLARE models.HTML = "<a href=\"https://www.cloudflare.com\">Cloudflare</a>"
|
||||
HTML_NOIP models.HTML = "<a href=\"https://www.noip.com/\">NoIP</a>"
|
||||
HTML_DNSPOD models.HTML = "<a href=\"https://www.dnspod.cn/\">DNSPod</a>"
|
||||
HTML_INFOMANIAK models.HTML = "<a href=\"https://www.infomaniak.com/\">Infomaniak</a>"
|
||||
HTML_DDNSSDE models.HTML = "<a href=\"https://ddnss.de/\">DDNSS.de</a>"
|
||||
HTMLNamecheap models.HTML = "<a href=\"https://namecheap.com\">Namecheap</a>"
|
||||
HTMLGodaddy models.HTML = "<a href=\"https://godaddy.com\">GoDaddy</a>"
|
||||
HTMLDuckDNS models.HTML = "<a href=\"https://duckdns.org\">DuckDNS</a>"
|
||||
HTMLDreamhost models.HTML = "<a href=\"https://www.dreamhost.com/\">Dreamhost</a>"
|
||||
HTMLCloudflare models.HTML = "<a href=\"https://www.cloudflare.com\">Cloudflare</a>"
|
||||
HTMLNoIP models.HTML = "<a href=\"https://www.noip.com/\">NoIP</a>"
|
||||
HTMLDNSPod models.HTML = "<a href=\"https://www.dnspod.cn/\">DNSPod</a>"
|
||||
HTMLInfomaniak models.HTML = "<a href=\"https://www.infomaniak.com/\">Infomaniak</a>"
|
||||
HTMLDdnssde models.HTML = "<a href=\"https://ddnss.de/\">DDNSS.de</a>"
|
||||
)
|
||||
|
||||
const (
|
||||
HTML_GOOGLE models.HTML = "<a href=\"https://google.com/search?q=ip\">Google</a>"
|
||||
HTML_OPENDNS models.HTML = "<a href=\"https://diagnostic.opendns.com/myip\">OpenDNS</a>"
|
||||
HTML_IFCONFIG models.HTML = "<a href=\"https://ifconfig.io\">ifconfig.io</a>"
|
||||
HTML_IPINFO models.HTML = "<a href=\"https://ipinfo.io\">ipinfo.io</a>"
|
||||
HTML_IPIFY models.HTML = "<a href=\"https://api.ipify.org\">api.ipify.org</a>"
|
||||
HTML_IPIFY6 models.HTML = "<a href=\"https://api6.ipify.org\">api6.ipify.org</a>"
|
||||
HTML_DDNSS models.HTML = "<a href=\"https://ddnss.de/meineip.php\">ddnss.de</a>"
|
||||
HTML_DDNSS4 models.HTML = "<a href=\"https://ip4.ddnss.de/meineip.php\">ip4.ddnss.de</a>"
|
||||
HTML_DDNSS6 models.HTML = "<a href=\"https://ip6.ddnss.de/meineip.php\">ip6.ddns.de</a>"
|
||||
HTML_CYCLE models.HTML = "Cycling"
|
||||
HTMLGoogle models.HTML = "<a href=\"https://google.com/search?q=ip\">Google</a>"
|
||||
HTMLOpenDNS models.HTML = "<a href=\"https://diagnostic.opendns.com/myip\">OpenDNS</a>"
|
||||
HTMLIfconfig models.HTML = "<a href=\"https://ifconfig.io\">ifconfig.io</a>"
|
||||
HTMLIpinfo models.HTML = "<a href=\"https://ipinfo.io\">ipinfo.io</a>"
|
||||
HTMLIpify models.HTML = "<a href=\"https://api.ipify.org\">api.ipify.org</a>"
|
||||
HTMLIpify6 models.HTML = "<a href=\"https://api6.ipify.org\">api6.ipify.org</a>"
|
||||
HTMLDdnss models.HTML = "<a href=\"https://ddnss.de/meineip.php\">ddnss.de</a>"
|
||||
HTMLDdnss4 models.HTML = "<a href=\"https://ip4.ddnss.de/meineip.php\">ip4.ddnss.de</a>"
|
||||
HTMLDdnss6 models.HTML = "<a href=\"https://ip6.ddnss.de/meineip.php\">ip6.ddns.de</a>"
|
||||
HTMLCycle models.HTML = "Cycling"
|
||||
)
|
||||
|
||||
@@ -4,13 +4,13 @@ import "regexp"
|
||||
|
||||
const (
|
||||
goDaddyKey = `[A-Za-z0-9]{10,14}\_[A-Za-z0-9]{22}`
|
||||
godaddySecret = `[A-Za-z0-9]{22}`
|
||||
RegexDuckDNSToken = `[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}`
|
||||
namecheapPassword = `[a-f0-9]{32}`
|
||||
godaddySecret = `[A-Za-z0-9]{22}` // #nosec
|
||||
duckDNSToken = `[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}` // #nosec
|
||||
namecheapPassword = `[a-f0-9]{32}` // #nosec
|
||||
dreamhostKey = `[a-zA-Z0-9]{16}`
|
||||
cloudflareKey = `[a-zA-Z0-9]+`
|
||||
cloudflareUserServiceKey = `v1\.0.+`
|
||||
cloudflareToken = `[a-zA-Z0-9_]{40}`
|
||||
cloudflareToken = `[a-zA-Z0-9_]{40}` // #nosec
|
||||
)
|
||||
|
||||
func MatchGodaddyKey(s string) bool {
|
||||
@@ -22,7 +22,7 @@ func MatchGodaddySecret(s string) bool {
|
||||
}
|
||||
|
||||
func MatchDuckDNSToken(s string) bool {
|
||||
return regexp.MustCompile("^" + RegexDuckDNSToken + "$").MatchString(s)
|
||||
return regexp.MustCompile("^" + duckDNSToken + "$").MatchString(s)
|
||||
}
|
||||
|
||||
func MatchNamecheapPassword(s string) bool {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
// Annoucement is a message annoucement
|
||||
Annoucement = "Added support for DDNSS.de"
|
||||
// AnnoucementExpiration is the expiration date of the annoucement in format yyyy-mm-dd
|
||||
AnnoucementExpiration = "2020-03-20"
|
||||
// Announcement is a message announcement
|
||||
Announcement = "Smaller Docker image based on Scratch (12.3MB)"
|
||||
// AnnouncementExpiration is the expiration date of the announcement in format yyyy-mm-dd
|
||||
AnnouncementExpiration = "2020-04-20"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -7,11 +7,11 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_AnnoucementExpiration(t *testing.T) {
|
||||
func Test_AnnouncementExpiration(t *testing.T) {
|
||||
t.Parallel()
|
||||
if len(AnnoucementExpiration) == 0 {
|
||||
if len(AnnouncementExpiration) == 0 {
|
||||
return
|
||||
}
|
||||
_, err := time.Parse("2006-01-02", AnnoucementExpiration)
|
||||
_, err := time.Parse("2006-01-02", AnnouncementExpiration)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
NamecheapURL = "https://dynamicdns.park-your-domain.com/update"
|
||||
GoDaddyURL = "https://api.godaddy.com/v1/domains"
|
||||
DuckDNSURL = "https://www.duckdns.org/update"
|
||||
DreamhostURL = "https://api.dreamhost.com"
|
||||
CloudflareURL = "https://api.cloudflare.com/client/v4"
|
||||
NoIPURL = "https://dynupdate.no-ip.com/nic/update"
|
||||
)
|
||||
132
internal/env/env.go
vendored
132
internal/env/env.go
vendored
@@ -1,132 +0,0 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
|
||||
"github.com/qdm12/golibs/admin"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
type Env interface {
|
||||
SetClient(client network.Client)
|
||||
SetGotify(gotify admin.Gotify)
|
||||
SetDB(db data.Database)
|
||||
Notify(priority int, messageArgs ...interface{})
|
||||
Info(messageArgs ...interface{})
|
||||
Warn(messageArgs ...interface{})
|
||||
CheckError(err error)
|
||||
FatalOnError(err error)
|
||||
ShutdownFromSignal(signal string) (exitCode int)
|
||||
Fatal(messageArgs ...interface{})
|
||||
Shutdown() (exitCode int)
|
||||
}
|
||||
|
||||
func NewEnv(logger logging.Logger) Env {
|
||||
return &env{logger: logger}
|
||||
}
|
||||
|
||||
// env contains objects necessary to the main function.
|
||||
// These are created at start and are needed to the top-level
|
||||
// working management of the program.
|
||||
type env struct {
|
||||
logger logging.Logger
|
||||
client network.Client
|
||||
gotify admin.Gotify
|
||||
db data.Database
|
||||
}
|
||||
|
||||
func (e *env) SetClient(client network.Client) {
|
||||
e.client = client
|
||||
}
|
||||
|
||||
func (e *env) SetGotify(gotify admin.Gotify) {
|
||||
e.gotify = gotify
|
||||
}
|
||||
|
||||
func (e *env) SetDB(db data.Database) {
|
||||
e.db = db
|
||||
}
|
||||
|
||||
// Notify sends a notification to the Gotify server.
|
||||
func (e *env) Notify(priority int, messageArgs ...interface{}) {
|
||||
if e.gotify == nil {
|
||||
return
|
||||
}
|
||||
if err := e.gotify.Notify("DDNS Updater", priority, messageArgs...); err != nil {
|
||||
e.logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Info logs a message and sends a notification to the Gotify server.
|
||||
func (e *env) Info(messageArgs ...interface{}) {
|
||||
e.logger.Info(messageArgs...)
|
||||
e.Notify(1, messageArgs...)
|
||||
}
|
||||
|
||||
// Warn logs a message and sends a notification to the Gotify server.
|
||||
func (e *env) Warn(messageArgs ...interface{}) {
|
||||
e.logger.Warn(messageArgs...)
|
||||
e.Notify(2, messageArgs...)
|
||||
}
|
||||
|
||||
// CheckError logs an error and sends a notification to the Gotify server
|
||||
// if the error is not nil.
|
||||
func (e *env) CheckError(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
s := err.Error()
|
||||
e.logger.Error(s)
|
||||
if len(s) > 100 {
|
||||
s = s[:100] + "..." // trim down message for notification
|
||||
}
|
||||
e.Notify(3, s)
|
||||
}
|
||||
|
||||
// FatalOnError calls Fatal if the error is not nil.
|
||||
func (e *env) FatalOnError(err error) {
|
||||
if err != nil {
|
||||
e.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown cleanly exits the program by closing all connections,
|
||||
// databases and syncing the loggers.
|
||||
func (e *env) Shutdown() (exitCode int) {
|
||||
defer func() {
|
||||
if err := e.logger.Sync(); err != nil {
|
||||
exitCode = 99
|
||||
}
|
||||
}()
|
||||
if e.client != nil {
|
||||
e.client.Close()
|
||||
}
|
||||
if e.db != nil {
|
||||
if err := e.db.Close(); err != nil {
|
||||
e.logger.Error(err)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// ShutdownFromSignal logs a warning, sends a notification to Gotify and shutdowns
|
||||
// the program cleanly when a OS level signal is received. It should be passed as a
|
||||
// callback to a function which would catch such signal.
|
||||
func (e *env) ShutdownFromSignal(signal string) (exitCode int) {
|
||||
e.logger.Warn("Program stopped with signal %q", signal)
|
||||
e.Notify(1, "Caught OS signal %q", signal)
|
||||
return e.Shutdown()
|
||||
}
|
||||
|
||||
// Fatal logs an error, sends a notification to Gotify and shutdowns the program.
|
||||
// It exits the program with an exit code of 1.
|
||||
func (e *env) Fatal(messageArgs ...interface{}) {
|
||||
e.logger.Error(messageArgs...)
|
||||
e.Notify(4, messageArgs...)
|
||||
_ = e.Shutdown()
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ type Handler interface {
|
||||
|
||||
type handler struct {
|
||||
rootURL string
|
||||
UIDir string
|
||||
uiDir string
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
forceUpdate func()
|
||||
@@ -28,11 +28,11 @@ type handler struct {
|
||||
}
|
||||
|
||||
// NewHandler returns a Handler object
|
||||
func NewHandler(rootURL, UIDir string, db data.Database, logger logging.Logger,
|
||||
func NewHandler(rootURL, uiDir string, db data.Database, logger logging.Logger,
|
||||
forceUpdate func(), onError func(err error)) Handler {
|
||||
return &handler{
|
||||
rootURL: rootURL,
|
||||
UIDir: UIDir,
|
||||
uiDir: uiDir,
|
||||
db: db,
|
||||
logger: logger,
|
||||
forceUpdate: forceUpdate,
|
||||
@@ -48,7 +48,7 @@ func (h *handler) GetHandlerFunc() http.HandlerFunc {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.RequestURI == h.rootURL+"/":
|
||||
// TODO: Forms to change existing updates or add some
|
||||
t := template.Must(template.ParseFiles(h.UIDir + "/ui/index.html"))
|
||||
t := template.Must(template.ParseFiles(h.uiDir + "/ui/index.html"))
|
||||
var htmlData models.HTMLData
|
||||
for _, record := range h.db.SelectAll() {
|
||||
row := html.ConvertRecord(record, h.getTime())
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
const NotAvailable = "N/A"
|
||||
row := models.HTMLRow{
|
||||
Domain: convertDomain(record.Settings.BuildDomainName()),
|
||||
Host: models.HTML(record.Settings.Host),
|
||||
@@ -24,7 +25,7 @@ func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
message = fmt.Sprintf("(%s)", message)
|
||||
}
|
||||
if len(record.Status) == 0 {
|
||||
row.Status = "N/A"
|
||||
row.Status = NotAvailable
|
||||
} else {
|
||||
row.Status = models.HTML(fmt.Sprintf("%s %s, %s",
|
||||
convertStatus(record.Status),
|
||||
@@ -35,10 +36,10 @@ func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
if currentIP != nil {
|
||||
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
|
||||
} else {
|
||||
row.CurrentIP = "N/A"
|
||||
row.CurrentIP = NotAvailable
|
||||
}
|
||||
previousIPs := record.History.GetPreviousIPs()
|
||||
row.PreviousIPs = "N/A"
|
||||
row.PreviousIPs = NotAvailable
|
||||
if len(previousIPs) > 0 {
|
||||
var previousIPsStr []string
|
||||
const maxPreviousIPs = 2
|
||||
@@ -57,13 +58,13 @@ func ConvertRecord(record models.Record, now time.Time) models.HTMLRow {
|
||||
func convertStatus(status models.Status) models.HTML {
|
||||
switch status {
|
||||
case constants.SUCCESS:
|
||||
return constants.HTML_SUCCESS
|
||||
return constants.HTMLSuccess
|
||||
case constants.FAIL:
|
||||
return constants.HTML_FAIL
|
||||
return constants.HTMLFail
|
||||
case constants.UPTODATE:
|
||||
return constants.HTML_UPTODATE
|
||||
return constants.HTMLUpdate
|
||||
case constants.UPDATING:
|
||||
return constants.HTML_UPDATING
|
||||
return constants.HTMLUpdating
|
||||
default:
|
||||
return "Unknown status"
|
||||
}
|
||||
@@ -72,23 +73,23 @@ func convertStatus(status models.Status) models.HTML {
|
||||
func convertProvider(provider models.Provider) models.HTML {
|
||||
switch provider {
|
||||
case constants.NAMECHEAP:
|
||||
return constants.HTML_NAMECHEAP
|
||||
return constants.HTMLNamecheap
|
||||
case constants.GODADDY:
|
||||
return constants.HTML_GODADDY
|
||||
return constants.HTMLGodaddy
|
||||
case constants.DUCKDNS:
|
||||
return constants.HTML_DUCKDNS
|
||||
return constants.HTMLDuckDNS
|
||||
case constants.DREAMHOST:
|
||||
return constants.HTML_DREAMHOST
|
||||
return constants.HTMLDreamhost
|
||||
case constants.CLOUDFLARE:
|
||||
return constants.HTML_CLOUDFLARE
|
||||
return constants.HTMLCloudflare
|
||||
case constants.NOIP:
|
||||
return constants.HTML_NOIP
|
||||
return constants.HTMLNoIP
|
||||
case constants.DNSPOD:
|
||||
return constants.HTML_DNSPOD
|
||||
return constants.HTMLDNSPod
|
||||
case constants.INFOMANIAK:
|
||||
return constants.HTML_INFOMANIAK
|
||||
return constants.HTMLInfomaniak
|
||||
case constants.DDNSSDE:
|
||||
return constants.HTML_DDNSSDE
|
||||
return constants.HTMLDdnssde
|
||||
default:
|
||||
s := string(provider)
|
||||
if strings.HasPrefix("https://", s) {
|
||||
@@ -100,31 +101,31 @@ func convertProvider(provider models.Provider) models.HTML {
|
||||
}
|
||||
}
|
||||
|
||||
func convertIPMethod(IPMethod models.IPMethod, provider models.Provider) models.HTML {
|
||||
func convertIPMethod(ipMethod models.IPMethod, provider models.Provider) models.HTML {
|
||||
// TODO map to icons
|
||||
switch IPMethod {
|
||||
switch ipMethod {
|
||||
case constants.PROVIDER:
|
||||
return convertProvider(provider)
|
||||
case constants.OPENDNS:
|
||||
return constants.HTML_OPENDNS
|
||||
return constants.HTMLOpenDNS
|
||||
case constants.IFCONFIG:
|
||||
return constants.HTML_IFCONFIG
|
||||
return constants.HTMLIfconfig
|
||||
case constants.IPINFO:
|
||||
return constants.HTML_IPINFO
|
||||
return constants.HTMLIpinfo
|
||||
case constants.IPIFY:
|
||||
return constants.HTML_IPIFY
|
||||
return constants.HTMLIpify
|
||||
case constants.IPIFY6:
|
||||
return constants.HTML_IPIFY6
|
||||
return constants.HTMLIpify6
|
||||
case constants.DDNSS:
|
||||
return constants.HTML_DDNSS
|
||||
return constants.HTMLDdnss
|
||||
case constants.DDNSS4:
|
||||
return constants.HTML_DDNSS4
|
||||
return constants.HTMLDdnss4
|
||||
case constants.DDNSS6:
|
||||
return constants.HTML_DDNSS6
|
||||
return constants.HTMLDdnss6
|
||||
case constants.CYCLE:
|
||||
return constants.HTML_CYCLE
|
||||
return constants.HTMLCycle
|
||||
default:
|
||||
return models.HTML(string(IPMethod))
|
||||
return models.HTML(string(ipMethod))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
Provider string
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
IPMethod string
|
||||
// Status is the record config status
|
||||
Status string
|
||||
// HTML is for constants HTML strings
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6
|
||||
IPVersion string
|
||||
)
|
||||
package models
|
||||
|
||||
type (
|
||||
// Provider is a possible DNS provider
|
||||
Provider string
|
||||
// IPMethod is a method to obtain your public IP address
|
||||
IPMethod string
|
||||
// Status is the record config status
|
||||
Status string
|
||||
// HTML is for constants HTML strings
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6
|
||||
IPVersion string
|
||||
)
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package models
|
||||
|
||||
// HTMLData is a list of HTML fields to be rendered.
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLData struct {
|
||||
Rows []HTMLRow
|
||||
}
|
||||
|
||||
// HTMLRow contains HTML fields to be rendered
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLRow struct {
|
||||
Domain HTML
|
||||
Host HTML
|
||||
Provider HTML
|
||||
IPMethod HTML
|
||||
Status HTML
|
||||
CurrentIP HTML
|
||||
PreviousIPs HTML
|
||||
}
|
||||
package models
|
||||
|
||||
// HTMLData is a list of HTML fields to be rendered.
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLData struct {
|
||||
Rows []HTMLRow
|
||||
}
|
||||
|
||||
// HTMLRow contains HTML fields to be rendered
|
||||
// It is exported so that the HTML template engine can render it.
|
||||
type HTMLRow struct {
|
||||
Domain HTML
|
||||
Host HTML
|
||||
Provider HTML
|
||||
IPMethod HTML
|
||||
Status HTML
|
||||
CurrentIP HTML
|
||||
PreviousIPs HTML
|
||||
}
|
||||
|
||||
@@ -1,55 +1,57 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Settings contains the elements to update the DNS record
|
||||
type Settings struct {
|
||||
Domain string
|
||||
Host string
|
||||
Provider Provider
|
||||
IPMethod IPMethod
|
||||
IPVersion IPVersion
|
||||
Delay time.Duration
|
||||
NoDNSLookup bool
|
||||
// Provider dependent fields
|
||||
Password string // Namecheap, Infomaniak, DDNSS and NoIP only
|
||||
Key string // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string // GoDaddy only
|
||||
Token string // Cloudflare and DuckDNS only
|
||||
Email string // Cloudflare only
|
||||
UserServiceKey string // Cloudflare only
|
||||
ZoneIdentifier string // Cloudflare only
|
||||
Identifier string // Cloudflare only
|
||||
Proxied bool // Cloudflare only
|
||||
Ttl uint // Cloudflare only
|
||||
Username string // NoIP, Infomaniak, DDNSS only
|
||||
}
|
||||
|
||||
func (settings *Settings) String() string {
|
||||
b, _ := json.Marshal(
|
||||
struct {
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Provider string `json:"provider"`
|
||||
}{
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
string(settings.Provider),
|
||||
},
|
||||
)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// BuildDomainName builds the domain name from the domain and the host of the settings
|
||||
func (settings *Settings) BuildDomainName() string {
|
||||
if settings.Host == "@" {
|
||||
return settings.Domain
|
||||
} else if settings.Host == "*" {
|
||||
return "any." + settings.Domain
|
||||
} else {
|
||||
return settings.Host + "." + settings.Domain
|
||||
}
|
||||
}
|
||||
package models
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Settings contains the elements to update the DNS record
|
||||
// nolint: maligned
|
||||
type Settings struct {
|
||||
Domain string
|
||||
Host string
|
||||
Provider Provider
|
||||
IPMethod IPMethod
|
||||
IPVersion IPVersion
|
||||
Delay time.Duration
|
||||
NoDNSLookup bool
|
||||
// Provider dependent fields
|
||||
Password string // Namecheap, Infomaniak, DDNSS and NoIP only
|
||||
Key string // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string // GoDaddy only
|
||||
Token string // Cloudflare and DuckDNS only
|
||||
Email string // Cloudflare only
|
||||
UserServiceKey string // Cloudflare only
|
||||
ZoneIdentifier string // Cloudflare only
|
||||
Identifier string // Cloudflare only
|
||||
Proxied bool // Cloudflare only
|
||||
TTL uint // Cloudflare only
|
||||
Username string // NoIP, Infomaniak, DDNSS only
|
||||
}
|
||||
|
||||
func (settings *Settings) String() string {
|
||||
b, _ := json.Marshal(
|
||||
struct {
|
||||
Domain string `json:"domain"`
|
||||
Host string `json:"host"`
|
||||
Provider string `json:"provider"`
|
||||
}{
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
string(settings.Provider),
|
||||
},
|
||||
)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// BuildDomainName builds the domain name from the domain and the host of the settings
|
||||
func (settings *Settings) BuildDomainName() string {
|
||||
switch settings.Host {
|
||||
case "@":
|
||||
return settings.Domain
|
||||
case "*":
|
||||
return "any." + settings.Domain
|
||||
default:
|
||||
return settings.Host + "." + settings.Domain
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,27 +13,27 @@ import (
|
||||
)
|
||||
|
||||
// GetPublicIP downloads a webpage and extracts the IP address from it
|
||||
func GetPublicIP(client network.Client, URL string, IPVersion models.IPVersion) (ip net.IP, err error) {
|
||||
content, status, err := client.GetContent(URL)
|
||||
func GetPublicIP(client network.Client, url string, ipVersion models.IPVersion) (ip net.IP, err error) {
|
||||
content, status, err := client.GetContent(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: %s", IPVersion, URL, err)
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: %s", ipVersion, url, err)
|
||||
} else if status != http.StatusOK {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d", IPVersion, URL, status)
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d", ipVersion, url, status)
|
||||
}
|
||||
verifier := verification.NewVerifier()
|
||||
regexSearch := verifier.SearchIPv4
|
||||
if IPVersion == constants.IPv6 {
|
||||
if ipVersion == constants.IPv6 {
|
||||
regexSearch = verifier.SearchIPv6
|
||||
}
|
||||
ips := regexSearch(string(content))
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no public %s address found at %s", IPVersion, URL)
|
||||
return nil, fmt.Errorf("no public %s address found at %s", ipVersion, url)
|
||||
} else if len(ips) > 1 {
|
||||
return nil, fmt.Errorf("multiple public %s addresses found at %s: %s", IPVersion, URL, strings.Join(ips, " "))
|
||||
return nil, fmt.Errorf("multiple public %s addresses found at %s: %s", ipVersion, url, strings.Join(ips, " "))
|
||||
}
|
||||
ip = net.ParseIP(ips[0])
|
||||
if ip == nil { // in case the regex is not restrictive enough
|
||||
return nil, fmt.Errorf("Public IP address %q found at %s is not valid", ips[0], URL)
|
||||
return nil, fmt.Errorf("Public IP address %q found at %s is not valid", ips[0], url)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network/mocks"
|
||||
"github.com/qdm12/golibs/network/mock_network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -63,8 +64,10 @@ func Test_GetPublicIP(t *testing.T) {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := &mocks.Client{}
|
||||
client.On("GetContent", URL).Return(tc.mockContent, tc.mockStatus, tc.mockErr).Once()
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
client := mock_network.NewMockClient(mockCtrl)
|
||||
client.EXPECT().GetContent(URL).Return(tc.mockContent, tc.mockStatus, tc.mockErr).Times(1)
|
||||
ip, err := GetPublicIP(client, URL, tc.IPVersion)
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
@@ -72,9 +75,7 @@ func Test_GetPublicIP(t *testing.T) {
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
fmt.Printf("%#v\n", ip)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
client.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
)
|
||||
|
||||
// BuildHTTPPut is used for GoDaddy and Cloudflare only
|
||||
func BuildHTTPPut(URL string, body interface{}) (request *http.Request, err error) {
|
||||
func BuildHTTPPut(url string, body interface{}) (request *http.Request, err error) {
|
||||
b, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request, err = http.NewRequest(http.MethodPut, URL, bytes.NewBuffer(b))
|
||||
request, err = http.NewRequest(http.MethodPut, url, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -8,131 +8,210 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func (p *params) isConsistent(settings models.Settings) error {
|
||||
// General validity checks
|
||||
func settingsGeneralChecks(settings models.Settings, matchDomain func(s string) bool) error {
|
||||
switch {
|
||||
case !ipMethodIsValid(settings.IPMethod):
|
||||
return fmt.Errorf("IP method %q is not recognized", settings.IPMethod)
|
||||
case settings.IPVersion != constants.IPv4 && settings.IPVersion != constants.IPv6:
|
||||
return fmt.Errorf("IP version %q is not recognized", settings.IPVersion)
|
||||
case !p.verifier.MatchDomain(settings.Domain):
|
||||
case !matchDomain(settings.Domain):
|
||||
return fmt.Errorf("invalid domain name format")
|
||||
case len(settings.Host) == 0:
|
||||
return fmt.Errorf("host cannot be empty")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Checks for each IP versions
|
||||
switch settings.IPVersion {
|
||||
func settingsIPVersionChecks(ipVersion models.IPVersion, ipMethod models.IPMethod, provider models.Provider) error {
|
||||
switch ipVersion {
|
||||
case constants.IPv4:
|
||||
switch settings.IPMethod {
|
||||
switch ipMethod {
|
||||
case constants.IPIFY6, constants.DDNSS6:
|
||||
return fmt.Errorf("IP method %s is only for IPv6 addresses", settings.IPMethod)
|
||||
return fmt.Errorf("IP method %s is only for IPv6 addresses", ipMethod)
|
||||
}
|
||||
case constants.IPv6:
|
||||
switch settings.IPMethod {
|
||||
switch ipMethod {
|
||||
case constants.IPIFY, constants.DDNSS4:
|
||||
return fmt.Errorf("IP method %s is only for IPv4 addresses", settings.IPMethod)
|
||||
return fmt.Errorf("IP method %s is only for IPv4 addresses", ipMethod)
|
||||
}
|
||||
switch settings.Provider {
|
||||
switch provider {
|
||||
case constants.GODADDY, constants.CLOUDFLARE, constants.DNSPOD, constants.DREAMHOST, constants.DUCKDNS, constants.NOIP:
|
||||
return fmt.Errorf("IPv6 support for %s is not supported yet", settings.Provider)
|
||||
return fmt.Errorf("IPv6 support for %s is not supported yet", provider)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check provider ipmethod is available
|
||||
if settings.IPMethod == constants.PROVIDER {
|
||||
switch settings.Provider {
|
||||
func settingsIPMethodChecks(ipMethod models.IPMethod, provider models.Provider) error {
|
||||
if ipMethod == constants.PROVIDER {
|
||||
switch provider {
|
||||
case constants.GODADDY, constants.DREAMHOST, constants.CLOUDFLARE, constants.DNSPOD, constants.DDNSSDE:
|
||||
return fmt.Errorf("unsupported IP update method %q", settings.IPMethod)
|
||||
return fmt.Errorf("unsupported IP update method %q", ipMethod)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsNamecheapChecks(password string) error {
|
||||
if !constants.MatchNamecheapPassword(password) {
|
||||
return fmt.Errorf("invalid password format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsGoDaddyChecks(key, secret string) error {
|
||||
switch {
|
||||
case !constants.MatchGodaddyKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !constants.MatchGodaddySecret(secret):
|
||||
return fmt.Errorf("invalid secret format")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDuckDNSChecks(token, host string) error {
|
||||
switch {
|
||||
case !constants.MatchDuckDNSToken(token):
|
||||
return fmt.Errorf("invalid token format")
|
||||
case host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDreamhostChecks(key, host string) error {
|
||||
switch {
|
||||
case !constants.MatchDreamhostKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsCloudflareChecks(key, email, userServiceKey, token, zoneIdentifier, identifier string, ttl uint, matchEmail func(s string) bool) error {
|
||||
switch {
|
||||
case len(key) > 0: // email and key must be provided
|
||||
switch {
|
||||
case !constants.MatchCloudflareKey(key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !matchEmail(email):
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
case len(userServiceKey) > 0: // only user service key
|
||||
if !constants.MatchCloudflareKey(key) {
|
||||
return fmt.Errorf("invalid user service key format")
|
||||
}
|
||||
default: // API token only
|
||||
if !constants.MatchCloudflareToken(token) {
|
||||
return fmt.Errorf("invalid API token key format")
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(zoneIdentifier) == 0:
|
||||
return fmt.Errorf("zone identifier cannot be empty")
|
||||
case len(identifier) == 0:
|
||||
return fmt.Errorf("identifier cannot be empty")
|
||||
case ttl == 0:
|
||||
return fmt.Errorf("TTL cannot be left to 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsNoIPChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(username) > 50:
|
||||
return fmt.Errorf("username cannot be longer than 50 characters")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDNSPodChecks(token string) error {
|
||||
if len(token) == 0 {
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsInfomaniakChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func settingsDdnssdeChecks(username, password, host string) error {
|
||||
switch {
|
||||
case len(username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *reader) isConsistent(settings models.Settings) error {
|
||||
if err := settingsGeneralChecks(settings, r.verifier.MatchDomain); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingsIPVersionChecks(settings.IPVersion, settings.IPMethod, settings.Provider); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingsIPMethodChecks(settings.IPMethod, settings.Provider); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Checks for each DNS provider
|
||||
switch settings.Provider {
|
||||
case constants.NAMECHEAP:
|
||||
if !constants.MatchNamecheapPassword(settings.Password) {
|
||||
return fmt.Errorf("invalid password format")
|
||||
if err := settingsNamecheapChecks(settings.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.GODADDY:
|
||||
switch {
|
||||
case !constants.MatchGodaddyKey(settings.Key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !constants.MatchGodaddySecret(settings.Secret):
|
||||
return fmt.Errorf("invalid secret format")
|
||||
if err := settingsGoDaddyChecks(settings.Key, settings.Secret); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DUCKDNS:
|
||||
switch {
|
||||
case !constants.MatchDuckDNSToken(settings.Token):
|
||||
return fmt.Errorf("invalid token format")
|
||||
case settings.Host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
if err := settingsDuckDNSChecks(settings.Token, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DREAMHOST:
|
||||
switch {
|
||||
case !constants.MatchDreamhostKey(settings.Key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case settings.Host != "@":
|
||||
return fmt.Errorf(`host can only be "@"`)
|
||||
if err := settingsDreamhostChecks(settings.Key, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.CLOUDFLARE:
|
||||
switch {
|
||||
case len(settings.Key) > 0: // email and key must be provided
|
||||
switch {
|
||||
case !constants.MatchCloudflareKey(settings.Key):
|
||||
return fmt.Errorf("invalid key format")
|
||||
case !p.verifier.MatchEmail(settings.Email):
|
||||
return fmt.Errorf("invalid email format")
|
||||
}
|
||||
case len(settings.UserServiceKey) > 0: // only user service key
|
||||
if !constants.MatchCloudflareKey(settings.Key) {
|
||||
return fmt.Errorf("invalid user service key format")
|
||||
}
|
||||
default: // API token only
|
||||
if !constants.MatchCloudflareToken(settings.Token) {
|
||||
return fmt.Errorf("invalid API token key format")
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case len(settings.ZoneIdentifier) == 0:
|
||||
return fmt.Errorf("zone identifier cannot be empty")
|
||||
case len(settings.Identifier) == 0:
|
||||
return fmt.Errorf("identifier cannot be empty")
|
||||
case settings.Ttl == 0:
|
||||
return fmt.Errorf("TTL cannot be left to 0")
|
||||
if err := settingsCloudflareChecks(settings.Key, settings.Email, settings.UserServiceKey, settings.Token, settings.ZoneIdentifier, settings.Identifier, settings.TTL, r.verifier.MatchEmail); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.NOIP:
|
||||
switch {
|
||||
case len(settings.Username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(settings.Username) > 50:
|
||||
return fmt.Errorf("username cannot be longer than 50 characters")
|
||||
case len(settings.Password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case settings.Host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
if err := settingsNoIPChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DNSPOD:
|
||||
switch {
|
||||
case len(settings.Token) == 0:
|
||||
return fmt.Errorf("token cannot be empty")
|
||||
if err := settingsDNSPodChecks(settings.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.INFOMANIAK:
|
||||
switch {
|
||||
case len(settings.Username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(settings.Password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case settings.Host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
if err := settingsInfomaniakChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
case constants.DDNSSDE:
|
||||
switch {
|
||||
case len(settings.Username) == 0:
|
||||
return fmt.Errorf("username cannot be empty")
|
||||
case len(settings.Password) == 0:
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
case settings.Host == "*":
|
||||
return fmt.Errorf(`host cannot be "*"`)
|
||||
if err := settingsDdnssdeChecks(settings.Username, settings.Password, settings.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("provider %q is not supported", settings.Provider)
|
||||
|
||||
@@ -1,90 +1,91 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
type settingsType struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
IPMethod string `json:"ip_method"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
Delay uint64 `json:"delay"`
|
||||
NoDNSLookup bool `json:"no_dns_lookup"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"` // Namecheap, NoIP only
|
||||
Key string `json:"key"` // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string `json:"secret"` // GoDaddy only
|
||||
Token string `json:"token"` // DuckDNS and Cloudflare only
|
||||
Email string `json:"email"` // Cloudflare only
|
||||
Username string `json:"username"` // NoIP only
|
||||
UserServiceKey string `json:"user_service_key"` // Cloudflare only
|
||||
ZoneIdentifier string `json:"zone_identifier"` // Cloudflare only
|
||||
Identifier string `json:"identifier"` // Cloudflare only
|
||||
Proxied bool `json:"proxied"` // Cloudflare only
|
||||
Ttl uint `json:"ttl"` // Cloudflare only
|
||||
}
|
||||
|
||||
// GetSettings obtain the update settings from config.json
|
||||
func (p *params) GetSettings(filePath string) (settings []models.Settings, warnings []string, err error) {
|
||||
bytes, err := p.readFile(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var config struct {
|
||||
Settings []settingsType `json:"settings"`
|
||||
}
|
||||
if err := json.Unmarshal(bytes, &config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, s := range config.Settings {
|
||||
switch models.Provider(s.Provider) {
|
||||
case constants.DREAMHOST, constants.DUCKDNS:
|
||||
s.Host = "@" // only choice available
|
||||
}
|
||||
ipMethod := models.IPMethod(s.IPMethod)
|
||||
// Retro compatibility
|
||||
if ipMethod == constants.GOOGLE {
|
||||
p.logger.Warn("IP Method %q is no longer valid, please change it. Defaulting it to %s", constants.GOOGLE, constants.CYCLE)
|
||||
ipMethod = constants.CYCLE
|
||||
}
|
||||
ipVersion := models.IPVersion(s.IPVersion)
|
||||
if len(ipVersion) == 0 {
|
||||
ipVersion = constants.IPv4 // default
|
||||
}
|
||||
setting := models.Settings{
|
||||
Provider: models.Provider(s.Provider),
|
||||
Domain: s.Domain,
|
||||
Host: s.Host,
|
||||
IPMethod: ipMethod,
|
||||
IPVersion: ipVersion,
|
||||
Delay: time.Second * time.Duration(s.Delay),
|
||||
NoDNSLookup: s.NoDNSLookup,
|
||||
Password: s.Password,
|
||||
Key: s.Key,
|
||||
Secret: s.Secret,
|
||||
Token: s.Token,
|
||||
Email: s.Email,
|
||||
Username: s.Username,
|
||||
UserServiceKey: s.UserServiceKey,
|
||||
ZoneIdentifier: s.ZoneIdentifier,
|
||||
Identifier: s.Identifier,
|
||||
Proxied: s.Proxied,
|
||||
Ttl: s.Ttl,
|
||||
}
|
||||
if err := p.isConsistent(setting); err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("%s for settings %s", err, setting.String()))
|
||||
continue
|
||||
}
|
||||
settings = append(settings, setting)
|
||||
}
|
||||
if len(settings) == 0 {
|
||||
return nil, warnings, fmt.Errorf("no settings found in config.json")
|
||||
}
|
||||
return settings, warnings, nil
|
||||
}
|
||||
package params
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
// nolint: maligned
|
||||
type settingsType struct {
|
||||
Provider string `json:"provider"`
|
||||
Domain string `json:"domain"`
|
||||
IPMethod string `json:"ip_method"`
|
||||
IPVersion string `json:"ip_version"`
|
||||
Delay uint64 `json:"delay"`
|
||||
NoDNSLookup bool `json:"no_dns_lookup"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"` // Namecheap, NoIP only
|
||||
Key string `json:"key"` // GoDaddy, Dreamhost and Cloudflare only
|
||||
Secret string `json:"secret"` // GoDaddy only
|
||||
Token string `json:"token"` // DuckDNS and Cloudflare only
|
||||
Email string `json:"email"` // Cloudflare only
|
||||
Username string `json:"username"` // NoIP only
|
||||
UserServiceKey string `json:"user_service_key"` // Cloudflare only
|
||||
ZoneIdentifier string `json:"zone_identifier"` // Cloudflare only
|
||||
Identifier string `json:"identifier"` // Cloudflare only
|
||||
Proxied bool `json:"proxied"` // Cloudflare only
|
||||
TTL uint `json:"ttl"` // Cloudflare only
|
||||
}
|
||||
|
||||
// GetSettings obtain the update settings from config.json
|
||||
func (r *reader) GetSettings(filePath string) (settings []models.Settings, warnings []string, err error) {
|
||||
bytes, err := r.readFile(filePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var config struct {
|
||||
Settings []settingsType `json:"settings"`
|
||||
}
|
||||
if err := json.Unmarshal(bytes, &config); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, s := range config.Settings {
|
||||
switch models.Provider(s.Provider) {
|
||||
case constants.DREAMHOST, constants.DUCKDNS:
|
||||
s.Host = "@" // only choice available
|
||||
}
|
||||
ipMethod := models.IPMethod(s.IPMethod)
|
||||
// Retro compatibility
|
||||
if ipMethod == constants.GOOGLE {
|
||||
r.logger.Warn("IP Method %q is no longer valid, please change it. Defaulting it to %s", constants.GOOGLE, constants.CYCLE)
|
||||
ipMethod = constants.CYCLE
|
||||
}
|
||||
ipVersion := models.IPVersion(s.IPVersion)
|
||||
if len(ipVersion) == 0 {
|
||||
ipVersion = constants.IPv4 // default
|
||||
}
|
||||
setting := models.Settings{
|
||||
Provider: models.Provider(s.Provider),
|
||||
Domain: s.Domain,
|
||||
Host: s.Host,
|
||||
IPMethod: ipMethod,
|
||||
IPVersion: ipVersion,
|
||||
Delay: time.Second * time.Duration(s.Delay),
|
||||
NoDNSLookup: s.NoDNSLookup,
|
||||
Password: s.Password,
|
||||
Key: s.Key,
|
||||
Secret: s.Secret,
|
||||
Token: s.Token,
|
||||
Email: s.Email,
|
||||
Username: s.Username,
|
||||
UserServiceKey: s.UserServiceKey,
|
||||
ZoneIdentifier: s.ZoneIdentifier,
|
||||
Identifier: s.Identifier,
|
||||
Proxied: s.Proxied,
|
||||
TTL: s.TTL,
|
||||
}
|
||||
if err := r.isConsistent(setting); err != nil {
|
||||
warnings = append(warnings, fmt.Sprintf("%s for settings %s", err, setting.String()))
|
||||
continue
|
||||
}
|
||||
settings = append(settings, setting)
|
||||
}
|
||||
if len(settings) == 0 {
|
||||
return nil, warnings, fmt.Errorf("no settings found in config.json")
|
||||
}
|
||||
return settings, warnings, nil
|
||||
}
|
||||
|
||||
@@ -1,90 +1,104 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type ParamsReader interface {
|
||||
GetSettings(filePath string) (settings []models.Settings, warnings []string, err error)
|
||||
GetDataDir(currentDir string) (string, error)
|
||||
GetListeningPort() (listeningPort, warning string, err error)
|
||||
GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error)
|
||||
GetGotifyURL(setters ...libparams.GetEnvSetter) (URL *url.URL, err error)
|
||||
GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error)
|
||||
GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error)
|
||||
GetDelay(setters ...libparams.GetEnvSetter) (duration time.Duration, err error)
|
||||
GetExeDir() (dir string, err error)
|
||||
GetHTTPTimeout() (duration time.Duration, err error)
|
||||
|
||||
// Version getters
|
||||
GetVersion() string
|
||||
GetBuildDate() string
|
||||
GetVcsRef() string
|
||||
}
|
||||
|
||||
type params struct {
|
||||
envParams libparams.EnvParams
|
||||
verifier verification.Verifier
|
||||
logger logging.Logger
|
||||
readFile func(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewParamsReader(logger logging.Logger) ParamsReader {
|
||||
return ¶ms{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
verifier: verification.NewVerifier(),
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDataDir obtains the data directory from the environment
|
||||
// variable DATADIR
|
||||
func (p *params) GetDataDir(currentDir string) (string, error) {
|
||||
return p.envParams.GetEnv("DATADIR", libparams.Default(currentDir+"/data"))
|
||||
}
|
||||
|
||||
func (p *params) GetListeningPort() (listeningPort, warning string, err error) {
|
||||
return p.envParams.GetListeningPort()
|
||||
}
|
||||
|
||||
func (p *params) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
|
||||
return p.envParams.GetLoggerConfig()
|
||||
}
|
||||
|
||||
func (p *params) GetGotifyURL(setters ...libparams.GetEnvSetter) (URL *url.URL, err error) {
|
||||
return p.envParams.GetGotifyURL()
|
||||
}
|
||||
|
||||
func (p *params) GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error) {
|
||||
return p.envParams.GetGotifyToken()
|
||||
}
|
||||
|
||||
func (p *params) GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error) {
|
||||
return p.envParams.GetRootURL()
|
||||
}
|
||||
|
||||
func (p *params) GetDelay(setters ...libparams.GetEnvSetter) (period time.Duration, err error) {
|
||||
// Backward compatibility
|
||||
n, err := p.envParams.GetEnvInt("DELAY", libparams.Compulsory()) // TODO change to PERIOD
|
||||
if err == nil { // integer only, treated as seconds
|
||||
p.logger.Warn("The value for the duration period of the updater does not have a time unit, you might want to set it to \"%ds\" instead of \"%d\"", n, n)
|
||||
return time.Duration(n) * time.Second, nil
|
||||
}
|
||||
return p.envParams.GetDuration("DELAY", setters...)
|
||||
}
|
||||
|
||||
func (p *params) GetExeDir() (dir string, err error) {
|
||||
return p.envParams.GetExeDir()
|
||||
}
|
||||
|
||||
func (p *params) GetHTTPTimeout() (duration time.Duration, err error) {
|
||||
return p.envParams.GetHTTPTimeout(libparams.Default("10s"))
|
||||
}
|
||||
package params
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
GetSettings(filePath string) (settings []models.Settings, warnings []string, err error)
|
||||
GetDataDir(currentDir string) (string, error)
|
||||
GetListeningPort() (listeningPort, warning string, err error)
|
||||
GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error)
|
||||
GetGotifyURL(setters ...libparams.GetEnvSetter) (URL *url.URL, err error)
|
||||
GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error)
|
||||
GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error)
|
||||
GetDelay(setters ...libparams.GetEnvSetter) (duration time.Duration, err error)
|
||||
GetExeDir() (dir string, err error)
|
||||
GetHTTPTimeout() (duration time.Duration, err error)
|
||||
GetBackupPeriod() (duration time.Duration, err error)
|
||||
GetBackupDirectory() (directory string, err error)
|
||||
|
||||
// Version getters
|
||||
GetVersion() string
|
||||
GetBuildDate() string
|
||||
GetVcsRef() string
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
envParams libparams.EnvParams
|
||||
verifier verification.Verifier
|
||||
logger logging.Logger
|
||||
readFile func(filename string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewReader(logger logging.Logger) Reader {
|
||||
return &reader{
|
||||
envParams: libparams.NewEnvParams(),
|
||||
verifier: verification.NewVerifier(),
|
||||
logger: logger,
|
||||
readFile: ioutil.ReadFile,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDataDir obtains the data directory from the environment
|
||||
// variable DATADIR
|
||||
func (r *reader) GetDataDir(currentDir string) (string, error) {
|
||||
return r.envParams.GetEnv("DATADIR", libparams.Default(currentDir+"/data"))
|
||||
}
|
||||
|
||||
func (r *reader) GetListeningPort() (listeningPort, warning string, err error) {
|
||||
return r.envParams.GetListeningPort()
|
||||
}
|
||||
|
||||
func (r *reader) GetLoggerConfig() (encoding logging.Encoding, level logging.Level, nodeID int, err error) {
|
||||
return r.envParams.GetLoggerConfig()
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyURL(setters ...libparams.GetEnvSetter) (url *url.URL, err error) {
|
||||
return r.envParams.GetGotifyURL()
|
||||
}
|
||||
|
||||
func (r *reader) GetGotifyToken(setters ...libparams.GetEnvSetter) (token string, err error) {
|
||||
return r.envParams.GetGotifyToken()
|
||||
}
|
||||
|
||||
func (r *reader) GetRootURL(setters ...libparams.GetEnvSetter) (rootURL string, err error) {
|
||||
return r.envParams.GetRootURL()
|
||||
}
|
||||
|
||||
func (r *reader) GetDelay(setters ...libparams.GetEnvSetter) (period time.Duration, err error) {
|
||||
// Backward compatibility
|
||||
n, err := r.envParams.GetEnvInt("DELAY", libparams.Compulsory()) // TODO change to PERIOD
|
||||
if err == nil { // integer only, treated as seconds
|
||||
r.logger.Warn("The value for the duration period of the updater does not have a time unit, you might want to set it to \"%ds\" instead of \"%d\"", n, n)
|
||||
return time.Duration(n) * time.Second, nil
|
||||
}
|
||||
return r.envParams.GetDuration("DELAY", setters...)
|
||||
}
|
||||
|
||||
func (r *reader) GetExeDir() (dir string, err error) {
|
||||
return r.envParams.GetExeDir()
|
||||
}
|
||||
|
||||
func (r *reader) GetHTTPTimeout() (duration time.Duration, err error) {
|
||||
return r.envParams.GetHTTPTimeout(libparams.Default("10s"))
|
||||
}
|
||||
|
||||
func (r *reader) GetBackupPeriod() (duration time.Duration, err error) {
|
||||
s, err := r.envParams.GetEnv("BACKUP_PERIOD", libparams.Default("0"))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.ParseDuration(s)
|
||||
}
|
||||
|
||||
func (r *reader) GetBackupDirectory() (directory string, err error) {
|
||||
return r.envParams.GetEnv("BACKUP_DIRECTORY", libparams.Default("./data"))
|
||||
}
|
||||
|
||||
@@ -4,17 +4,17 @@ import (
|
||||
libparams "github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
func (p *params) GetVersion() string {
|
||||
version, _ := p.envParams.GetEnv("VERSION", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
func (r *reader) GetVersion() string {
|
||||
version, _ := r.envParams.GetEnv("VERSION", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return version
|
||||
}
|
||||
|
||||
func (p *params) GetBuildDate() string {
|
||||
buildDate, _ := p.envParams.GetEnv("BUILD_DATE", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
func (r *reader) GetBuildDate() string {
|
||||
buildDate, _ := r.envParams.GetEnv("BUILD_DATE", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
|
||||
func (p *params) GetVcsRef() string {
|
||||
buildDate, _ := p.envParams.GetEnv("VCS_REF", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
func (r *reader) GetVcsRef() string {
|
||||
buildDate, _ := r.envParams.GetEnv("VCS_REF", libparams.Default("?"), libparams.CaseSensitiveValue())
|
||||
return buildDate
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence/json"
|
||||
"github.com/qdm12/ddns-updater/internal/persistence/sqlite"
|
||||
)
|
||||
|
||||
type Database interface {
|
||||
@@ -17,10 +16,6 @@ type Database interface {
|
||||
Check() error
|
||||
}
|
||||
|
||||
func NewSQLite(dataDir string) (Database, error) {
|
||||
return sqlite.NewDatabase(dataDir)
|
||||
}
|
||||
|
||||
func NewJSON(dataDir string) (Database, error) {
|
||||
return json.NewDatabase(dataDir)
|
||||
}
|
||||
|
||||
@@ -9,22 +9,22 @@ import (
|
||||
"github.com/qdm12/golibs/files"
|
||||
)
|
||||
|
||||
type database struct {
|
||||
type Database struct {
|
||||
data dataModel
|
||||
filepath string
|
||||
fileManager files.FileManager
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func (db *database) Close() error {
|
||||
func (db *Database) Close() error {
|
||||
db.Lock() // ensure a write operation finishes
|
||||
defer db.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewDatabase opens or creates the JSON file database.
|
||||
func NewDatabase(dataDir string) (*database, error) {
|
||||
db := database{
|
||||
func NewDatabase(dataDir string) (*Database, error) {
|
||||
db := Database{
|
||||
filepath: dataDir + "/updates.json",
|
||||
fileManager: files.NewFileManager(),
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func NewDatabase(dataDir string) (*database, error) {
|
||||
return &db, nil
|
||||
}
|
||||
|
||||
func (db *database) Check() error {
|
||||
func (db *Database) Check() error {
|
||||
for _, record := range db.data.Records {
|
||||
switch {
|
||||
case len(record.Domain) == 0:
|
||||
@@ -81,7 +81,7 @@ func (db *database) Check() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *database) write() error {
|
||||
func (db *Database) write() error {
|
||||
data, err := json.MarshalIndent(db.data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// StoreNewIP stores a new IP address for a certain domain and host.
|
||||
func (db *database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error) {
|
||||
func (db *Database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
for i, record := range db.data.Records {
|
||||
@@ -33,22 +33,19 @@ func (db *database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err
|
||||
|
||||
// GetEvents gets all the IP addresses history for a certain domain and host, in the order
|
||||
// from oldest to newest
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
func (db *Database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for _, record := range db.data.Records {
|
||||
if record.Domain == domain && record.Host == host {
|
||||
for _, event := range record.Events {
|
||||
events = append(events, event)
|
||||
}
|
||||
return events, nil
|
||||
return append(events, record.Events...), nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetAllDomainsHosts gets all the domains and hosts from the database
|
||||
func (db *database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
func (db *Database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
db.RLock()
|
||||
defer db.RUnlock()
|
||||
for _, record := range db.data.Records {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package persistence
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
func Migrate(source, destination Database, logger logging.Logger) (err error) {
|
||||
defer func() {
|
||||
closeErr := source.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s, %s", err, closeErr)
|
||||
} else {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
|
||||
type row struct {
|
||||
domain string
|
||||
host string
|
||||
events []models.HistoryEvent
|
||||
}
|
||||
var rows []row
|
||||
|
||||
domainshosts, err := source.GetAllDomainsHosts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("Migrating %d domain-host tuples", len(domainshosts))
|
||||
|
||||
for i := range domainshosts {
|
||||
domain := domainshosts[i].Domain
|
||||
host := domainshosts[i].Host
|
||||
events, err := source.GetEvents(domain, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows = append(rows, row{domain, host, events})
|
||||
}
|
||||
|
||||
for _, r := range rows {
|
||||
for _, event := range r.events {
|
||||
destination.StoreNewIP(r.domain, r.host, event.IP, event.Time)
|
||||
}
|
||||
}
|
||||
return destination.Check()
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type database struct {
|
||||
sqlite *sql.DB
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (db *database) Close() error {
|
||||
return db.sqlite.Close()
|
||||
}
|
||||
|
||||
// NewDatabase opens or creates the database if necessary.
|
||||
func NewDatabase(dataDir string) (*database, error) {
|
||||
sqlite, err := sql.Open("sqlite3", dataDir+"/updates.db")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = sqlite.Exec(
|
||||
`CREATE TABLE IF NOT EXISTS updates_ips (
|
||||
domain TEXT NOT NULL,
|
||||
host TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
t_new DATETIME NOT NULL,
|
||||
t_last DATETIME NOT NULL,
|
||||
current INTEGER DEFAULT 1 NOT NULL,
|
||||
PRIMARY KEY(domain, host, ip, t_new)
|
||||
);`)
|
||||
return &database{sqlite: sqlite}, err
|
||||
}
|
||||
|
||||
func (db *database) Check() error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
/* Access to SQLite is NOT thread safe so we use a mutex */
|
||||
|
||||
// StoreNewIP stores a new IP address for a certain
|
||||
// domain and host.
|
||||
func (db *database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
// Inserts new IP
|
||||
_, err = db.sqlite.Exec(
|
||||
`INSERT INTO updates_ips(domain,host,ip,t_new,t_last)
|
||||
VALUES(?, ?, ?, ?, ?, ?);`,
|
||||
domain,
|
||||
host,
|
||||
ip.String(),
|
||||
t,
|
||||
t, // unneeded but it's hard to modify tables in sqlite
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetEvents gets all the IP addresses history for a certain domain and host, in the order
|
||||
// from oldest to newest
|
||||
func (db *database) GetEvents(domain, host string) (events []models.HistoryEvent, err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
rows, err := db.sqlite.Query(
|
||||
`SELECT ip, t_new
|
||||
FROM updates_ips
|
||||
WHERE domain = ? AND host = ?
|
||||
ORDER BY t_new ASC`,
|
||||
domain,
|
||||
host,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := rows.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s, %s", err, closeErr)
|
||||
} else {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
for rows.Next() {
|
||||
var ip string
|
||||
var t time.Time
|
||||
if err := rows.Scan(&ip, &t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
events = append(events, models.HistoryEvent{
|
||||
IP: net.ParseIP(ip),
|
||||
Time: t,
|
||||
})
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// GetAllDomainsHosts gets all domains and hosts from the database
|
||||
func (db *database) GetAllDomainsHosts() (domainshosts []models.DomainHost, err error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
rows, err := db.sqlite.Query(`SELECT DISTINCT domain, host FROM updates_ips`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
closeErr := rows.Close()
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s, %s", err, closeErr)
|
||||
} else {
|
||||
err = closeErr
|
||||
}
|
||||
}()
|
||||
for rows.Next() {
|
||||
domainHost := models.DomainHost{}
|
||||
if err := rows.Scan(&domainHost.Domain, &domainHost.Host); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domainshosts = append(domainshosts, domainHost)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return domainshosts, nil
|
||||
}
|
||||
|
||||
// SetSuccessTime sets the latest successful update time for a particular domain, host.
|
||||
func (db *database) SetSuccessTime(domain, host string, successTime time.Time) error {
|
||||
return fmt.Errorf("not implemented") // no plan to migrate back to sqlite
|
||||
}
|
||||
@@ -7,19 +7,15 @@ import (
|
||||
|
||||
"github.com/kyokomi/emoji"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/params"
|
||||
)
|
||||
|
||||
// Splash returns the welcome spash message
|
||||
func Splash(paramsReader params.ParamsReader) string {
|
||||
version := paramsReader.GetVersion()
|
||||
vcsRef := paramsReader.GetVcsRef()
|
||||
buildDate := paramsReader.GetBuildDate()
|
||||
func Splash(version, vcsRef, buildDate string) string {
|
||||
lines := title()
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, fmt.Sprintf("Running version %s built on %s (commit %s)", version, buildDate, vcsRef))
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, annoucement()...)
|
||||
lines = append(lines, announcement()...)
|
||||
lines = append(lines, "")
|
||||
lines = append(lines, links()...)
|
||||
return strings.Join(lines, "\n")
|
||||
@@ -36,15 +32,15 @@ func title() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func annoucement() []string {
|
||||
if len(constants.Annoucement) == 0 {
|
||||
func announcement() []string {
|
||||
if len(constants.Announcement) == 0 {
|
||||
return nil
|
||||
}
|
||||
expirationDate, _ := time.Parse("2006-01-02", constants.AnnoucementExpiration) // error covered by a unit test
|
||||
expirationDate, _ := time.Parse("2006-01-02", constants.AnnouncementExpiration) // error covered by a unit test
|
||||
if time.Now().After(expirationDate) {
|
||||
return nil
|
||||
}
|
||||
return []string{emoji.Sprint(":mega: ") + constants.Annoucement}
|
||||
return []string{emoji.Sprint(":mega: ") + constants.Announcement}
|
||||
}
|
||||
|
||||
func links() []string {
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
libnetwork "github.com/qdm12/golibs/network"
|
||||
)
|
||||
@@ -20,22 +20,27 @@ func updateCloudflare(client libnetwork.Client, zoneIdentifier, identifier, host
|
||||
Name string `json:"name"` // DNS record name i.e. example.com
|
||||
Content string `json:"content"` // ip address
|
||||
Proxied bool `json:"proxied"` // whether the record is receiving the performance and security benefits of Cloudflare
|
||||
Ttl uint `json:"ttl"`
|
||||
TTL uint `json:"ttl"`
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.cloudflare.com",
|
||||
Path: fmt.Sprintf("/client/v4/zones/%s/dns_records/%s", zoneIdentifier, identifier),
|
||||
}
|
||||
URL := constants.CloudflareURL + "/zones/" + zoneIdentifier + "/dns_records/" + identifier
|
||||
r, err := network.BuildHTTPPut(
|
||||
URL,
|
||||
u.String(),
|
||||
cloudflarePutBody{
|
||||
Type: "A",
|
||||
Name: host,
|
||||
Content: ip.String(),
|
||||
Proxied: proxied,
|
||||
Ttl: ttl,
|
||||
TTL: ttl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
switch {
|
||||
case len(token) > 0:
|
||||
r.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
@@ -4,30 +4,39 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateDDNSS(client network.Client, domain, host, username, password string, ip net.IP) error {
|
||||
var hostname string
|
||||
if host == "@" {
|
||||
hostname = strings.ToLower(domain)
|
||||
} else {
|
||||
hostname = strings.ToLower(host + "." + domain)
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.ddnss.de",
|
||||
Path: "/upd.php",
|
||||
}
|
||||
url := fmt.Sprintf("http://www.ddnss.de/upd.php?user=%s&pwd=%s&host=%s", username, password, hostname)
|
||||
values := url.Values{}
|
||||
values.Set("user", username)
|
||||
values.Set("pwd", password)
|
||||
fqdn := domain
|
||||
if host != "@" {
|
||||
fqdn = host + "." + domain
|
||||
}
|
||||
values.Set("host", fqdn)
|
||||
if ip != nil {
|
||||
if ip.To4() == nil { // ipv6
|
||||
url += fmt.Sprintf("&ip6=%s", ip)
|
||||
values.Set("ip6", ip.String())
|
||||
} else {
|
||||
url += fmt.Sprintf("&ip=%s", ip)
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -42,7 +51,7 @@ func updateDDNSS(client network.Client, domain, host, username, password string,
|
||||
case strings.Contains(s, "badauth"):
|
||||
return fmt.Errorf("ddnss.de: bad authentication")
|
||||
case strings.Contains(s, "notfqdn"):
|
||||
return fmt.Errorf("ddnss.de: hostname %q does not exist", hostname)
|
||||
return fmt.Errorf("ddnss.de: hostname %q does not exist", fqdn)
|
||||
case strings.Contains(s, "Updated 1 hostname"):
|
||||
return nil
|
||||
default:
|
||||
|
||||
@@ -15,20 +15,26 @@ func updateDNSPod(client network.Client, domain, host, token string, ip net.IP)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("IP address was not given to updater")
|
||||
}
|
||||
body := bytes.NewBufferString(url.Values{
|
||||
"login_token": []string{token},
|
||||
"format": []string{"json"},
|
||||
"domain": []string{domain},
|
||||
"length": []string{"200"},
|
||||
"sub_domain": []string{host},
|
||||
"record_type": []string{"A"},
|
||||
}.Encode())
|
||||
req, err := http.NewRequest(http.MethodPost, "https://dnsapi.cn/Record.List", body)
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dnsapi.cn",
|
||||
Path: "/Record.List",
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("login_token", token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", domain)
|
||||
values.Set("length", "200")
|
||||
values.Set("sub_domain", host)
|
||||
values.Set("record_type", "A")
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
status, content, err := client.DoHTTPRequest(req)
|
||||
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 err
|
||||
} else if status != http.StatusOK {
|
||||
@@ -61,21 +67,24 @@ func updateDNSPod(client network.Client, domain, host, token string, ip net.IP)
|
||||
if len(recordID) == 0 {
|
||||
return fmt.Errorf("record not found")
|
||||
}
|
||||
body = bytes.NewBufferString(url.Values{
|
||||
"login_token": []string{token},
|
||||
"format": []string{"json"},
|
||||
"domain": []string{domain},
|
||||
"record_id": []string{recordID},
|
||||
"value": []string{ip.String()},
|
||||
"record_line": []string{recordLine},
|
||||
"sub_domain": []string{host},
|
||||
}.Encode())
|
||||
req, err = http.NewRequest(http.MethodPost, "https://dnsapi.cn/Record.Ddns", body)
|
||||
|
||||
u.Path = "/Record.Ddns"
|
||||
values = url.Values{}
|
||||
values.Set("login_token", token)
|
||||
values.Set("format", "json")
|
||||
values.Set("domain", domain)
|
||||
values.Set("record_id", recordID)
|
||||
values.Set("value", ip.String())
|
||||
values.Set("record_line", recordLine)
|
||||
values.Set("sub_domain", host)
|
||||
u.RawQuery = values.Encode()
|
||||
r, err = http.NewRequest(http.MethodPost, u.String(), bytes.NewBufferString(values.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
status, content, err = client.DoHTTPRequest(req)
|
||||
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 err
|
||||
} else if status != http.StatusOK {
|
||||
|
||||
@@ -5,34 +5,16 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateDreamhost(client network.Client, domain, key, domainName string, ip net.IP) error {
|
||||
if ip == nil {
|
||||
return fmt.Errorf("IP address was not given to updater")
|
||||
}
|
||||
type dreamhostReponse struct {
|
||||
Result string `json:"result"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
// List records
|
||||
url := constants.DreamhostURL + "/?key=" + key + "&unique_id=" + uuid.New().String() + "&format=json&cmd=dns-list_records"
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhList struct {
|
||||
const success = "success"
|
||||
|
||||
type (
|
||||
dreamHostRecords struct {
|
||||
Result string `json:"result"`
|
||||
Data []struct {
|
||||
Editable string `json:"editable"`
|
||||
@@ -41,13 +23,118 @@ func updateDreamhost(client network.Client, domain, key, domainName string, ip n
|
||||
Value string `json:"value"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &dhList); err != nil {
|
||||
dreamhostReponse struct {
|
||||
Result string `json:"result"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
func makeDreamhostDefaultValues(key string) (values url.Values) { //nolint:unparam
|
||||
values.Set("key", key)
|
||||
values.Set("unique_id", uuid.New().String())
|
||||
values.Set("format", "json")
|
||||
return values
|
||||
}
|
||||
|
||||
func listDreamhostRecords(client network.Client, key string) (records dreamHostRecords, err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-list_records")
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return records, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return records, err
|
||||
} else if status != http.StatusOK {
|
||||
return records, fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
if err := json.Unmarshal(content, &records); err != nil {
|
||||
return records, err
|
||||
} else if records.Result != success {
|
||||
return records, fmt.Errorf(records.Result)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func removeDreamhostRecord(client network.Client, key, domain string, ip net.IP) error { //nolint:dupl
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-remove_record")
|
||||
values.Set("record", domain)
|
||||
values.Set("type", "A")
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != success { // this should not happen
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addDreamhostRecord(client network.Client, key, domain string, ip net.IP) error { //nolint:dupl
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.dreamhost.com",
|
||||
}
|
||||
values := makeDreamhostDefaultValues(key)
|
||||
values.Set("cmd", "dns-add_record")
|
||||
values.Set("record", domain)
|
||||
values.Set("type", "A")
|
||||
values.Set("value", ip.String())
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != success {
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateDreamhost(client network.Client, domain, key, domainName string, ip net.IP) error {
|
||||
if ip == nil {
|
||||
return fmt.Errorf("IP address was not given to updater")
|
||||
}
|
||||
records, err := listDreamhostRecords(client, key)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if dhList.Result != "success" {
|
||||
return fmt.Errorf(dhList.Result)
|
||||
}
|
||||
var oldIP net.IP
|
||||
for _, data := range dhList.Data {
|
||||
for _, data := range records.Data {
|
||||
if data.Type == "A" && data.Record == domainName {
|
||||
if data.Editable == "0" {
|
||||
return fmt.Errorf("record data is not editable")
|
||||
@@ -60,42 +147,9 @@ func updateDreamhost(client network.Client, domain, key, domainName string, ip n
|
||||
}
|
||||
}
|
||||
if oldIP != nil { // Found editable record with a different IP address, so remove it
|
||||
url = constants.DreamhostURL + "?key=" + key + "&unique_id=" + uuid.New().String() + "&format=json&cmd=dns-remove_record&record=" + strings.ToLower(domain) + "&type=A&value=" + oldIP.String()
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
if err := removeDreamhostRecord(client, key, domain, oldIP); err != nil {
|
||||
return err
|
||||
}
|
||||
status, content, err = client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
if err := json.Unmarshal(content, &dhResponse); err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != "success" { // this should not happen
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
}
|
||||
// Create the right record
|
||||
url = constants.DreamhostURL + "?key=" + key + "&unique_id=" + uuid.New().String() + "&format=json&cmd=dns-add_record&record=" + strings.ToLower(domain) + "&type=A&value=" + ip.String()
|
||||
r, err = http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, content, err = client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != http.StatusOK {
|
||||
return fmt.Errorf("HTTP status %d", status)
|
||||
}
|
||||
var dhResponse dreamhostReponse
|
||||
err = json.Unmarshal(content, &dhResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if dhResponse.Result != "success" {
|
||||
return fmt.Errorf("%s - %s", dhResponse.Result, dhResponse.Data)
|
||||
}
|
||||
return nil
|
||||
return addDreamhostRecord(client, key, domain, ip)
|
||||
}
|
||||
|
||||
@@ -4,22 +4,31 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
func updateDuckDNS(client network.Client, domain, token string, ip net.IP) (newIP net.IP, err error) {
|
||||
url := constants.DuckDNSURL + "?domains=" + strings.ToLower(domain) + "&token=" + token + "&verbose=true"
|
||||
if ip != nil {
|
||||
url += "&ip=" + ip.String()
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "www.duckdns.org",
|
||||
Path: "/update",
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
values := url.Values{}
|
||||
values.Set("verbose", "true")
|
||||
values.Set("domains", domain)
|
||||
values.Set("token", token)
|
||||
u.RawQuery = values.Encode()
|
||||
if ip != nil {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -5,9 +5,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
libnetwork "github.com/qdm12/golibs/network"
|
||||
)
|
||||
@@ -19,18 +18,16 @@ func updateGoDaddy(client libnetwork.Client, host, domain, key, secret string, i
|
||||
type goDaddyPutBody struct {
|
||||
Data string `json:"data"` // IP address to update to
|
||||
}
|
||||
URL := constants.GoDaddyURL + "/" + strings.ToLower(domain) + "/records/A/" + strings.ToLower(host)
|
||||
r, err := network.BuildHTTPPut(
|
||||
URL,
|
||||
[]goDaddyPutBody{
|
||||
goDaddyPutBody{
|
||||
ip.String(),
|
||||
},
|
||||
},
|
||||
)
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.godaddy.com",
|
||||
Path: fmt.Sprintf("/v1/domains/%s/records/A/%s", domain, host),
|
||||
}
|
||||
r, err := network.BuildHTTPPut(u.String(), []goDaddyPutBody{{ip.String()}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
r.Header.Set("Authorization", "sso-key "+key+":"+secret)
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,26 +4,33 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateInfomaniak(client network.Client, domain, host, username, password string, ip net.IP) (newIP net.IP, err error) {
|
||||
var hostname string
|
||||
if host == "@" {
|
||||
hostname = strings.ToLower(domain)
|
||||
} else {
|
||||
hostname = strings.ToLower(host + "." + domain)
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "infomaniak.com",
|
||||
Path: "/nic/update",
|
||||
User: url.UserPassword(username, password),
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("hostname", domain)
|
||||
if host != "@" {
|
||||
values.Set("hostname", host+"."+domain)
|
||||
}
|
||||
url := fmt.Sprintf("https://%s:%s@infomaniak.com/nic/update?hostname=%s", username, password, hostname)
|
||||
if ip != nil {
|
||||
url += fmt.Sprintf("&myip=%s", ip)
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -5,21 +5,31 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
)
|
||||
|
||||
func updateNamecheap(client network.Client, host, domain, password string, ip net.IP) (newIP net.IP, err error) {
|
||||
url := constants.NamecheapURL + "?host=" + strings.ToLower(host) + "&domain=" + strings.ToLower(domain) + "&password=" + password
|
||||
if ip != nil {
|
||||
url += "&ip=" + ip.String()
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dynamicdns.park-your-domain.com",
|
||||
Path: "/update",
|
||||
// User: url.UserPassword(username, password),
|
||||
}
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
values := url.Values{}
|
||||
values.Set("host", host)
|
||||
values.Set("domain", domain)
|
||||
values.Set("password", password)
|
||||
if ip != nil {
|
||||
values.Set("ip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Header.Set("User-Agent", "DDNS-Updater quentin.mcgaw@gmail.com")
|
||||
status, content, err := client.DoHTTPRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,20 +4,27 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
func updateNoIP(client network.Client, hostname, username, password string, ip net.IP) (newIP net.IP, err error) {
|
||||
url := constants.NoIPURL + "?hostname=" + strings.ToLower(hostname)
|
||||
if ip != nil {
|
||||
url += "&myip=" + ip.String()
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dynupdate.no-ip.com",
|
||||
Path: "/nic/update",
|
||||
User: url.UserPassword(username, password),
|
||||
}
|
||||
url = strings.Replace(url, "https://", "https://"+username+":"+password+"@", 1)
|
||||
r, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
values := url.Values{}
|
||||
values.Set("hostname", hostname)
|
||||
if ip != nil {
|
||||
values.Set("myip", ip.String())
|
||||
}
|
||||
u.RawQuery = values.Encode()
|
||||
r, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1,214 +1,214 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
libnetwork "github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
)
|
||||
|
||||
type Updater interface {
|
||||
Update(id int) error
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
client libnetwork.Client
|
||||
notify notifyFunc
|
||||
verifier verification.Verifier
|
||||
ipMethods []models.IPMethod
|
||||
counter int
|
||||
counterMutex sync.RWMutex
|
||||
}
|
||||
|
||||
type notifyFunc func(priority int, messageArgs ...interface{})
|
||||
|
||||
func NewUpdater(db data.Database, logger logging.Logger, client libnetwork.Client, notify notifyFunc) Updater {
|
||||
return &updater{
|
||||
db: db,
|
||||
logger: logger,
|
||||
client: client,
|
||||
notify: notify,
|
||||
verifier: verification.NewVerifier(),
|
||||
ipMethods: constants.IPMethodExternalChoices(),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) Update(id int) error {
|
||||
record, err := u.db.Select(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.Time = time.Now()
|
||||
record.Status = constants.UPDATING
|
||||
if err := u.db.Update(id, record); err != nil {
|
||||
return err
|
||||
}
|
||||
status, message, newIP, err := u.update(
|
||||
record.Settings,
|
||||
record.History.GetCurrentIP(),
|
||||
record.History.GetDurationSinceSuccess(time.Now()))
|
||||
record.Status = status
|
||||
record.Message = message
|
||||
if err != nil {
|
||||
if len(record.Message) == 0 {
|
||||
record.Message = err.Error()
|
||||
}
|
||||
if updateErr := u.db.Update(id, record); updateErr != nil {
|
||||
return fmt.Errorf("%s, %s", err, updateErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if newIP != nil {
|
||||
record.History = append(record.History, models.HistoryEvent{
|
||||
IP: newIP,
|
||||
Time: time.Now(),
|
||||
})
|
||||
u.notify(1, fmt.Sprintf("%s %s", record.Settings.BuildDomainName(), message))
|
||||
}
|
||||
return u.db.Update(id, record) // persists some data if needed (i.e new IP)
|
||||
}
|
||||
|
||||
func (u *updater) update(settings models.Settings, currentIP net.IP, durationSinceSuccess string) (status models.Status, message string, newIP net.IP, err error) {
|
||||
// Get the public IP address
|
||||
ip, err := u.getPublicIP(settings.IPMethod, settings.IPVersion) // Note: empty IP means DNS provider provided
|
||||
if err != nil {
|
||||
return constants.FAIL, "", nil, err
|
||||
}
|
||||
if ip != nil && ip.Equal(currentIP) {
|
||||
return constants.UPTODATE, fmt.Sprintf("No IP change for %s", durationSinceSuccess), nil, nil
|
||||
}
|
||||
|
||||
// Update the record
|
||||
switch settings.Provider {
|
||||
case constants.NAMECHEAP:
|
||||
ip, err = updateNamecheap(
|
||||
u.client,
|
||||
settings.Host,
|
||||
settings.Domain,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.GODADDY:
|
||||
err = updateGoDaddy(
|
||||
u.client,
|
||||
settings.Host,
|
||||
settings.Domain,
|
||||
settings.Key,
|
||||
settings.Secret,
|
||||
ip,
|
||||
)
|
||||
case constants.DUCKDNS:
|
||||
ip, err = updateDuckDNS(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Token,
|
||||
ip,
|
||||
)
|
||||
case constants.DREAMHOST:
|
||||
err = updateDreamhost(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Key,
|
||||
settings.BuildDomainName(),
|
||||
ip,
|
||||
)
|
||||
case constants.CLOUDFLARE:
|
||||
err = updateCloudflare(
|
||||
u.client,
|
||||
settings.ZoneIdentifier,
|
||||
settings.Identifier,
|
||||
settings.Host,
|
||||
settings.Email,
|
||||
settings.Key,
|
||||
settings.UserServiceKey,
|
||||
settings.Token,
|
||||
settings.Proxied,
|
||||
settings.Ttl,
|
||||
ip,
|
||||
)
|
||||
case constants.NOIP:
|
||||
ip, err = updateNoIP(
|
||||
u.client,
|
||||
settings.BuildDomainName(),
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.DNSPOD:
|
||||
err = updateDNSPod(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Token,
|
||||
ip,
|
||||
)
|
||||
case constants.INFOMANIAK:
|
||||
ip, err = updateInfomaniak(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.DDNSSDE:
|
||||
err = updateDDNSS(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
default:
|
||||
err = fmt.Errorf("provider %q is not supported", settings.Provider)
|
||||
}
|
||||
if err != nil {
|
||||
return constants.FAIL, "", nil, err
|
||||
}
|
||||
if ip != nil && ip.Equal(currentIP) {
|
||||
return constants.UPTODATE, fmt.Sprintf("No IP change for %s", durationSinceSuccess), nil, nil
|
||||
}
|
||||
return constants.SUCCESS, fmt.Sprintf("changed to %s", ip.String()), ip, nil
|
||||
}
|
||||
|
||||
func (u *updater) incCounter() (value int) {
|
||||
u.counterMutex.Lock()
|
||||
defer u.counterMutex.Unlock()
|
||||
value = u.counter
|
||||
u.counter++
|
||||
return value
|
||||
}
|
||||
|
||||
func (u *updater) getPublicIP(IPMethod models.IPMethod, IPVersion models.IPVersion) (ip net.IP, err error) {
|
||||
var url string
|
||||
switch {
|
||||
case IPMethod == constants.PROVIDER:
|
||||
return nil, nil
|
||||
case strings.HasPrefix(string(IPMethod), "https://"):
|
||||
// Custom URL provided
|
||||
url = string(IPMethod)
|
||||
case IPMethod == constants.CYCLE:
|
||||
i := u.incCounter() % len(u.ipMethods)
|
||||
url = constants.IPMethodMapping()[u.ipMethods[i]]
|
||||
default:
|
||||
var ok bool
|
||||
url, ok = constants.IPMethodMapping()[IPMethod]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("IP method %q not supported", IPMethod)
|
||||
}
|
||||
}
|
||||
return network.GetPublicIP(u.client, url, IPVersion)
|
||||
}
|
||||
package update
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/golibs/logging"
|
||||
libnetwork "github.com/qdm12/golibs/network"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
)
|
||||
|
||||
type Updater interface {
|
||||
Update(id int) error
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
db data.Database
|
||||
logger logging.Logger
|
||||
client libnetwork.Client
|
||||
notify notifyFunc
|
||||
verifier verification.Verifier
|
||||
ipMethods []models.IPMethod
|
||||
counter int
|
||||
counterMutex sync.RWMutex
|
||||
}
|
||||
|
||||
type notifyFunc func(priority int, messageArgs ...interface{})
|
||||
|
||||
func NewUpdater(db data.Database, logger logging.Logger, client libnetwork.Client, notify notifyFunc) Updater {
|
||||
return &updater{
|
||||
db: db,
|
||||
logger: logger,
|
||||
client: client,
|
||||
notify: notify,
|
||||
verifier: verification.NewVerifier(),
|
||||
ipMethods: constants.IPMethodExternalChoices(),
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updater) Update(id int) error {
|
||||
record, err := u.db.Select(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record.Time = time.Now()
|
||||
record.Status = constants.UPDATING
|
||||
if err := u.db.Update(id, record); err != nil {
|
||||
return err
|
||||
}
|
||||
status, message, newIP, err := u.update(
|
||||
record.Settings,
|
||||
record.History.GetCurrentIP(),
|
||||
record.History.GetDurationSinceSuccess(time.Now()))
|
||||
record.Status = status
|
||||
record.Message = message
|
||||
if err != nil {
|
||||
if len(record.Message) == 0 {
|
||||
record.Message = err.Error()
|
||||
}
|
||||
if updateErr := u.db.Update(id, record); updateErr != nil {
|
||||
return fmt.Errorf("%s, %s", err, updateErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if newIP != nil {
|
||||
record.History = append(record.History, models.HistoryEvent{
|
||||
IP: newIP,
|
||||
Time: time.Now(),
|
||||
})
|
||||
u.notify(1, fmt.Sprintf("%s %s", record.Settings.BuildDomainName(), message))
|
||||
}
|
||||
return u.db.Update(id, record) // persists some data if needed (i.e new IP)
|
||||
}
|
||||
|
||||
func (u *updater) update(settings models.Settings, currentIP net.IP, durationSinceSuccess string) (status models.Status, message string, newIP net.IP, err error) {
|
||||
// Get the public IP address
|
||||
ip, err := u.getPublicIP(settings.IPMethod, settings.IPVersion) // Note: empty IP means DNS provider provided
|
||||
if err != nil {
|
||||
return constants.FAIL, "", nil, err
|
||||
}
|
||||
if ip != nil && ip.Equal(currentIP) {
|
||||
return constants.UPTODATE, fmt.Sprintf("No IP change for %s", durationSinceSuccess), nil, nil
|
||||
}
|
||||
|
||||
// Update the record
|
||||
switch settings.Provider {
|
||||
case constants.NAMECHEAP:
|
||||
ip, err = updateNamecheap(
|
||||
u.client,
|
||||
settings.Host,
|
||||
settings.Domain,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.GODADDY:
|
||||
err = updateGoDaddy(
|
||||
u.client,
|
||||
settings.Host,
|
||||
settings.Domain,
|
||||
settings.Key,
|
||||
settings.Secret,
|
||||
ip,
|
||||
)
|
||||
case constants.DUCKDNS:
|
||||
ip, err = updateDuckDNS(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Token,
|
||||
ip,
|
||||
)
|
||||
case constants.DREAMHOST:
|
||||
err = updateDreamhost(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Key,
|
||||
settings.BuildDomainName(),
|
||||
ip,
|
||||
)
|
||||
case constants.CLOUDFLARE:
|
||||
err = updateCloudflare(
|
||||
u.client,
|
||||
settings.ZoneIdentifier,
|
||||
settings.Identifier,
|
||||
settings.Host,
|
||||
settings.Email,
|
||||
settings.Key,
|
||||
settings.UserServiceKey,
|
||||
settings.Token,
|
||||
settings.Proxied,
|
||||
settings.TTL,
|
||||
ip,
|
||||
)
|
||||
case constants.NOIP:
|
||||
ip, err = updateNoIP(
|
||||
u.client,
|
||||
settings.BuildDomainName(),
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.DNSPOD:
|
||||
err = updateDNSPod(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Token,
|
||||
ip,
|
||||
)
|
||||
case constants.INFOMANIAK:
|
||||
ip, err = updateInfomaniak(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
case constants.DDNSSDE:
|
||||
err = updateDDNSS(
|
||||
u.client,
|
||||
settings.Domain,
|
||||
settings.Host,
|
||||
settings.Username,
|
||||
settings.Password,
|
||||
ip,
|
||||
)
|
||||
default:
|
||||
err = fmt.Errorf("provider %q is not supported", settings.Provider)
|
||||
}
|
||||
if err != nil {
|
||||
return constants.FAIL, "", nil, err
|
||||
}
|
||||
if ip != nil && ip.Equal(currentIP) {
|
||||
return constants.UPTODATE, fmt.Sprintf("No IP change for %s", durationSinceSuccess), nil, nil
|
||||
}
|
||||
return constants.SUCCESS, fmt.Sprintf("changed to %s", ip.String()), ip, nil
|
||||
}
|
||||
|
||||
func (u *updater) incCounter() (value int) {
|
||||
u.counterMutex.Lock()
|
||||
defer u.counterMutex.Unlock()
|
||||
value = u.counter
|
||||
u.counter++
|
||||
return value
|
||||
}
|
||||
|
||||
func (u *updater) getPublicIP(ipMethod models.IPMethod, ipVersion models.IPVersion) (ip net.IP, err error) {
|
||||
var url string
|
||||
switch {
|
||||
case ipMethod == constants.PROVIDER:
|
||||
return nil, nil
|
||||
case strings.HasPrefix(string(ipMethod), "https://"):
|
||||
// Custom URL provided
|
||||
url = string(ipMethod)
|
||||
case ipMethod == constants.CYCLE:
|
||||
i := u.incCounter() % len(u.ipMethods)
|
||||
url = constants.IPMethodMapping()[u.ipMethods[i]]
|
||||
default:
|
||||
var ok bool
|
||||
url, ok = constants.IPMethodMapping()[ipMethod]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("IP method %q not supported", ipMethod)
|
||||
}
|
||||
}
|
||||
return network.GetPublicIP(u.client, url, ipVersion)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/network/mocks"
|
||||
"github.com/qdm12/golibs/network/mock_network"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -64,10 +65,11 @@ func Test_getPublicIP(t *testing.T) {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := &mocks.Client{}
|
||||
mockCtrl := gomock.NewController(t)
|
||||
defer mockCtrl.Finish()
|
||||
client := mock_network.NewMockClient(mockCtrl)
|
||||
if len(tc.mockURL) != 0 {
|
||||
client.On("GetContent", tc.mockURL).Return(
|
||||
tc.mockContent, http.StatusOK, nil).Once()
|
||||
client.EXPECT().GetContent(tc.mockURL).Return(tc.mockContent, http.StatusOK, nil).Times(1)
|
||||
}
|
||||
u := &updater{
|
||||
client: client,
|
||||
@@ -81,8 +83,6 @@ func Test_getPublicIP(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
client.AssertExpectations(t)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user