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*
[](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

@@ -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