diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 198d3a78..f6a69a1e 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -22,6 +22,7 @@ assignees: qdm12 - [ ] Dreamhost - [ ] DuckDNS - [ ] GoDaddy + - [ ] Google - [ ] Infomaniak - [ ] Namecheap - [ ] NoIP diff --git a/.github/ISSUE_TEMPLATE/help.md b/.github/ISSUE_TEMPLATE/help.md index 13348c3a..bcedc3e9 100644 --- a/.github/ISSUE_TEMPLATE/help.md +++ b/.github/ISSUE_TEMPLATE/help.md @@ -22,6 +22,7 @@ assignees: - [ ] Dreamhost - [ ] DuckDNS - [ ] GoDaddy + - [ ] Google - [ ] Infomaniak - [ ] Namecheap - [ ] NoIP diff --git a/Dockerfile b/Dockerfile index 45ec3669..e536ca6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,7 +32,7 @@ LABEL \ org.opencontainers.image.documentation="https://github.com/qdm12/ddns-updater" \ org.opencontainers.image.source="https://github.com/qdm12/ddns-updater" \ org.opencontainers.image.title="ddns-updater" \ - org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, GoDaddy, Infomaniak, Namecheap and NoIP" + org.opencontainers.image.description="Universal DNS updater with WebUI. Works with Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, GoDaddy, Google, Infomaniak, Namecheap and NoIP" COPY --from=alpine --chown=1000 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=alpine --chown=1000 /usr/share/zoneinfo /usr/share/zoneinfo EXPOSE 8000 diff --git a/README.md b/README.md index 7c7ec743..264da066 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Lightweight universal DDNS Updater with Docker and web UI -*Light container updating DNS A records periodically for Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, GoDaddy, Infomaniak, Namecheap and NoIP* +*Light container updating DNS A records periodically for Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, GoDaddy, Google, Infomaniak, Namecheap and NoIP* [![DDNS Updater by Quentin McGaw](https://github.com/qdm12/ddns-updater/raw/master/readme/title.png)](https://hub.docker.com/r/qmcgaw/ddns-updater) @@ -17,7 +17,7 @@ ## Features -- Updates periodically A records for different DNS providers: Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, GoDaddy, Infomaniak, Namecheap and NoIP ([create an issue](https://github.com/qdm12/ddns-updater/issues/new/choose) for more) +- Updates periodically A records for different DNS providers: Cloudflare, DDNSS.de, DNSPod, Dreamhost, DuckDNS, GoDaddy, Google, Infomaniak, Namecheap and NoIP ([create an issue](https://github.com/qdm12/ddns-updater/issues/new/choose) for more) - Web User interface ![Web UI](https://raw.githubusercontent.com/qdm12/ddns-updater/master/readme/webui.png) @@ -128,7 +128,7 @@ Start by having the following content in *config.json*, or in your `CONFIG` envi The following parameters are to be added: -For all record update configuration, you have to specify the DNS provider with `"provider"` which can be `"cloudflare"`, `"ddnss"`, `"dnspod"`, `"dreamhost"`, `"duckdns"`, `"godaddy"`, `"infomaniak"`, `"namecheap"` or `"noip"`. +For all record update configuration, you have to specify the DNS provider with `"provider"` which can be `"cloudflare"`, `"ddnss"`, `"dnspod"`, `"dreamhost"`, `"duckdns"`, `"godaddy"`, `"google"`, `"infomaniak"`, `"namecheap"` or `"noip"`. You can optionnally add the parameters: - `"no_dns_lookup"` can be `true` or `false` and allows, if `true`, to prevent the periodic Docker healthcheck from running a DNS lookup on your domain. @@ -201,6 +201,14 @@ DDNSS.de: - `"password"` - `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6` +Google: + +- `"domain"` +- `"host"` is your host and can be a subdomain or `"@"` or `"*"` +- `"username"` +- `"password"` +- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6` + ### Additional notes - You can specify multiple hosts for the same domain using a comma separated list. For example with `"host": "@,subdomain1,subdomain2",`. diff --git a/internal/constants/providers.go b/internal/constants/providers.go index f116f89e..521005eb 100644 --- a/internal/constants/providers.go +++ b/internal/constants/providers.go @@ -10,6 +10,7 @@ const ( DUCKDNS models.Provider = "duckdns" DREAMHOST models.Provider = "dreamhost" GODADDY models.Provider = "godaddy" + GOOGLE models.Provider = "google" INFOMANIAK models.Provider = "infomaniak" NAMECHEAP models.Provider = "namecheap" NOIP models.Provider = "noip" @@ -23,6 +24,7 @@ func ProviderChoices() []models.Provider { DUCKDNS, DREAMHOST, GODADDY, + GOOGLE, INFOMANIAK, NAMECHEAP, NOIP, diff --git a/internal/params/json.go b/internal/params/json.go index 7a616121..b9802ae5 100644 --- a/internal/params/json.go +++ b/internal/params/json.go @@ -121,6 +121,8 @@ func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage) settingsConstructor = settings.NewDuckdns case constants.GODADDY: settingsConstructor = settings.NewGodaddy + case constants.GOOGLE: + settingsConstructor = settings.NewGoogle case constants.INFOMANIAK: settingsConstructor = settings.NewInfomaniak case constants.NAMECHEAP: diff --git a/internal/settings/constants.go b/internal/settings/constants.go new file mode 100644 index 00000000..5dd6d920 --- /dev/null +++ b/internal/settings/constants.go @@ -0,0 +1,7 @@ +package settings + +const ( + badauth = "badauth" + success = "success" + nohost = "nohost" +) diff --git a/internal/settings/ddnss.go b/internal/settings/ddnss.go index c73f5486..5ab69e0b 100644 --- a/internal/settings/ddnss.go +++ b/internal/settings/ddnss.go @@ -130,7 +130,7 @@ func (d *ddnss) Update(client network.Client, ip net.IP) (newIP net.IP, err erro switch { case strings.Contains(s, "badysys"): return nil, fmt.Errorf("ddnss.de: invalid system parameter") - case strings.Contains(s, "badauth"): + case strings.Contains(s, badauth): return nil, fmt.Errorf("ddnss.de: bad authentication") case strings.Contains(s, "notfqdn"): return nil, fmt.Errorf("ddnss.de: hostname %q does not exist", fqdn) diff --git a/internal/settings/dreamhost.go b/internal/settings/dreamhost.go index f1dea9e9..2a25a773 100644 --- a/internal/settings/dreamhost.go +++ b/internal/settings/dreamhost.go @@ -87,8 +87,6 @@ func (d *dreamhost) HTML() models.HTMLRow { } } -const success = "success" - func (d *dreamhost) Update(client network.Client, ip net.IP) (newIP net.IP, err error) { if ip == nil { return nil, fmt.Errorf("IP address was not given to updater") diff --git a/internal/settings/google.go b/internal/settings/google.go new file mode 100644 index 00000000..e43e5db1 --- /dev/null +++ b/internal/settings/google.go @@ -0,0 +1,155 @@ +package settings + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "net/url" + "strings" + + "github.com/qdm12/ddns-updater/internal/models" + netlib "github.com/qdm12/golibs/network" + "github.com/qdm12/golibs/verification" +) + +//nolint:maligned +type google struct { + domain string + host string + ipVersion models.IPVersion + dnsLookup bool + username string + password string + useProviderIP bool +} + +func NewGoogle(data json.RawMessage, domain, host string, ipVersion models.IPVersion, noDNSLookup bool) (s Settings, err error) { + extraSettings := struct { + Username string `json:"username"` + Password string `json:"password"` + UseProviderIP bool `json:"provider_ip"` + }{} + if err := json.Unmarshal(data, &extraSettings); err != nil { + return nil, err + } + g := &google{ + domain: domain, + host: host, + ipVersion: ipVersion, + dnsLookup: !noDNSLookup, + username: extraSettings.Username, + password: extraSettings.Password, + useProviderIP: extraSettings.UseProviderIP, + } + if err := g.isValid(); err != nil { + return nil, err + } + return g, nil +} + +func (g *google) isValid() error { + switch { + case len(g.username) == 0: + return fmt.Errorf("username cannot be empty") + case len(g.password) == 0: + return fmt.Errorf("password cannot be empty") + } + return nil +} + +func (g *google) String() string { + return fmt.Sprintf("[domain: %s | host: %s | provider: Google]", g.domain, g.host) +} + +func (g *google) Domain() string { + return g.domain +} + +func (g *google) Host() string { + return g.host +} + +func (g *google) DNSLookup() bool { + return g.dnsLookup +} + +func (g *google) IPVersion() models.IPVersion { + return g.ipVersion +} + +func (g *google) BuildDomainName() string { + return buildDomainName(g.host, g.domain) +} + +func (g *google) HTML() models.HTMLRow { + return models.HTMLRow{ + Domain: models.HTML(fmt.Sprintf("%s", g.BuildDomainName(), g.BuildDomainName())), + Host: models.HTML(g.Host()), + Provider: "Google", + IPVersion: models.HTML(g.ipVersion), + } +} + +func (g *google) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) { + u := url.URL{ + Scheme: "https", + Host: "domains.google.com", + Path: "/nic/update", + User: url.UserPassword(g.username, g.password), + } + values := url.Values{} + fqdn := g.BuildDomainName() + values.Set("hostname", fqdn) + if !g.useProviderIP { + values.Set("myip", ip.String()) + } + u.RawQuery = values.Encode() + r, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, err + } + r.Header.Set("User-Agent", "DDNS-Updater quentig.mcgaw@gmail.com") + status, content, err := client.DoHTTPRequest(r) + if err != nil { + return nil, err + } + s := string(content) + switch s { + case "": + return nil, fmt.Errorf("HTTP status %d", status) + case nohost: + return nil, fmt.Errorf("hostname does not exist") + case badauth: + return nil, fmt.Errorf("invalid username password combination") + case "notfqdn": + return nil, fmt.Errorf("hostname %q is not a valid fully qualified domain name", fqdn) + case "badagent": + return nil, fmt.Errorf("user agent is banned") + case "abuse": + return nil, fmt.Errorf("username is banned due to abuse") + case "911": + return nil, fmt.Errorf("Google's internal server error 911") + case "conflict A": + return nil, fmt.Errorf("custom A record conflicts with the update") + case "conflict AAAA": + return nil, fmt.Errorf("custom AAAA record conflicts with the update") + } + if strings.Contains(s, "nochg") || strings.Contains(s, "good") { + ipsV4 := verification.NewVerifier().SearchIPv4(s) + ipsV6 := verification.NewVerifier().SearchIPv6(s) + ips := append(ipsV4, ipsV6...) + if ips == nil { + return nil, fmt.Errorf("no IP address in response") + } + newIP = net.ParseIP(ips[0]) + if newIP == nil { + return nil, fmt.Errorf("IP address received %q is malformed", ips[0]) + } + if !g.useProviderIP && !ip.Equal(newIP) { + return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String()) + } + return newIP, nil + } + return nil, fmt.Errorf("invalid response %q", s) +} diff --git a/internal/settings/infomaniak.go b/internal/settings/infomaniak.go index 6d442f9d..4cce51cb 100644 --- a/internal/settings/infomaniak.go +++ b/internal/settings/infomaniak.go @@ -142,9 +142,9 @@ func (i *infomaniak) Update(client network.Client, ip net.IP) (newIP net.IP, err } case http.StatusBadRequest: switch s { - case "nohost": + case nohost: return nil, fmt.Errorf("infomaniak.com: host %q does not exist for domain %q", i.host, i.domain) - case "badauth": + case badauth: return nil, fmt.Errorf("infomaniak.com: bad authentication") default: return nil, fmt.Errorf("infomaniak.com: bad request: %s", s) diff --git a/internal/settings/noip.go b/internal/settings/noip.go index 11fe8b51..01be4df1 100644 --- a/internal/settings/noip.go +++ b/internal/settings/noip.go @@ -129,9 +129,9 @@ func (n *noip) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) return nil, fmt.Errorf("user has not this extra feature") case "badagent": return nil, fmt.Errorf("user agent is banned") - case "badauth": + case badauth: return nil, fmt.Errorf("invalid username password combination") - case "nohost": + case nohost: return nil, fmt.Errorf("hostname does not exist") } if strings.Contains(s, "nochg") || strings.Contains(s, "good") { @@ -143,7 +143,7 @@ func (n *noip) Update(client netlib.Client, ip net.IP) (newIP net.IP, err error) if newIP == nil { return nil, fmt.Errorf("IP address received %q is malformed", ips[0]) } - if ip != nil && !ip.Equal(newIP) { + if !n.useProviderIP && !ip.Equal(newIP) { return nil, fmt.Errorf("new IP address %s is not %s", newIP.String(), ip.String()) } return newIP, nil