diff --git a/README.md b/README.md index 7f452448e..752cede74 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,8 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog ![Mobile Web UI](readme/webui-mobile.png) +- JSON API endpoint at `/json` for programmatic access to DNS record information + - Send notifications with [**Shoutrrr**](https://containrrr.dev/shoutrrr/v0.8/services/overview/) using `SHOUTRRR_ADDRESSES` - Container (Docker/K8s) specific features: - Lightweight 12MB Docker image based on the Scratch Docker image @@ -266,6 +268,7 @@ Check the documentation for your DNS provider: - [Vultr](docs/vultr.md) - [Zoneedit](docs/zoneedit.md) - [Custom](docs/custom.md) +- [JSON API](docs/json-api.md) Note that: diff --git a/docs/json-api.md b/docs/json-api.md new file mode 100644 index 000000000..8dea88861 --- /dev/null +++ b/docs/json-api.md @@ -0,0 +1,99 @@ +# JSON API Endpoint + +The DDNS Updater provides a JSON API endpoint at `/json` that returns the same information as the web interface but in JSON format. + +## Endpoint + +- **URL**: `/json` +- **Method**: `GET` +- **Content-Type**: `application/json` +- **Cache-Control**: `no-cache` + +## Response Format + +The response is a JSON object with the following structure: + +```json +{ + "records": [ + { + "domain": "example.com", + "owner": "www", + "provider": "cloudflare", + "ip_version": "ipv4", + "status": "success", + "message": "no IP change for 2h", + "current_ip": "192.168.1.1", + "previous_ips": ["192.168.1.2", "192.168.1.3"], + "total_ips_in_history": 3, + "last_update": "2023-01-01T12:00:00Z", + "success_time": "2023-01-01T12:00:00Z", + "duration_since_success": "2h" + } + ], + "time": "2023-01-01T12:00:00Z", + "last_success_time": "2023-01-01T12:00:00Z", + "last_success_ip": "192.168.1.1" +} +``` + +## Field Descriptions + +### Root Object +- `records`: Array of DNS record objects +- `time`: Current server time when the response was generated +- `last_success_time`: Time of the most recent successful update across all records (zero time if no successful updates) +- `last_success_ip`: IP address from the record with the most recent successful update (empty string if no successful updates) + +### Record Object +- `domain`: The domain name (e.g., "example.com") +- `owner`: The subdomain owner (e.g., "www") +- `provider`: The DNS provider name (e.g., "cloudflare", "duckdns") +- `ip_version`: IP version supported ("ipv4", "ipv6", or "ipv4 or ipv6") +- `status`: Current status ("success", "failure", "updating", "up to date", "unset") +- `message`: Additional status information or error message +- `current_ip`: Current IP address (or "N/A" if not available) +- `previous_ips`: Array of previous IP addresses (limited to last 10, empty if none) +- `total_ips_in_history`: Total number of IP addresses available in the history +- `last_update`: Timestamp of the last update attempt +- `success_time`: Timestamp of the last successful update +- `duration_since_success`: Human-readable duration since last success (e.g., "2h", "30m", "1d") + +## Example Usage + +### Using curl +```bash +curl http://localhost:8080/json +``` + +### Using JavaScript +```javascript +fetch('/json') + .then(response => response.json()) + .then(data => { + console.log('Current time:', data.time); + data.records.forEach(record => { + console.log(`${record.domain} (${record.provider}): ${record.status}`); + }); + }); +``` + +### Using Python +```python +import requests + +response = requests.get('http://localhost:8080/json') +data = response.json() + +print(f"Current time: {data['time']}") +for record in data['records']: + print(f"{record['domain']} ({record['provider']}): {record['status']}") +``` + +## Status Values + +- `success`: Last update was successful +- `failure`: Last update failed +- `updating`: Update is currently in progress +- `up to date`: No IP change needed +- `unset`: Status not yet determined diff --git a/internal/models/json.go b/internal/models/json.go new file mode 100644 index 000000000..5382eb8d4 --- /dev/null +++ b/internal/models/json.go @@ -0,0 +1,27 @@ +package models + +import "time" + +// JSONData is the root structure for the JSON API response +type JSONData struct { + Records []JSONRecord `json:"records"` + Time time.Time `json:"time"` + LastSuccessTime time.Time `json:"last_success_time"` + LastSuccessIP string `json:"last_success_ip"` +} + +// JSONRecord contains all the information for a DNS record in JSON format +type JSONRecord struct { + Domain string `json:"domain"` + Owner string `json:"owner"` + Provider string `json:"provider"` + IPVersion string `json:"ip_version"` + Status string `json:"status"` + Message string `json:"message"` + CurrentIP string `json:"current_ip"` + PreviousIPs []string `json:"previous_ips"` + TotalIPsInHistory int `json:"total_ips_in_history"` + LastUpdate time.Time `json:"last_update"` + SuccessTime time.Time `json:"success_time"` + Duration string `json:"duration_since_success"` +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 06e002845..679b9e045 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -67,6 +67,7 @@ import ( type Provider interface { String() string + Name() models.Provider Domain() string Owner() string BuildDomainName() string diff --git a/internal/provider/providers/aliyun/provider.go b/internal/provider/providers/aliyun/provider.go index 9966e5803..e12d8cfae 100644 --- a/internal/provider/providers/aliyun/provider.go +++ b/internal/provider/providers/aliyun/provider.go @@ -74,8 +74,12 @@ func validateSettings(domain, accessKeyID, accessSecret string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Aliyun +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Aliyun, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/allinkl/provider.go b/internal/provider/providers/allinkl/provider.go index 6281eb30e..b14475383 100644 --- a/internal/provider/providers/allinkl/provider.go +++ b/internal/provider/providers/allinkl/provider.go @@ -72,8 +72,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.AllInkl +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.AllInkl, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/changeip/provider.go b/internal/provider/providers/changeip/provider.go index 8997cb180..a576a5ae9 100644 --- a/internal/provider/providers/changeip/provider.go +++ b/internal/provider/providers/changeip/provider.go @@ -69,8 +69,12 @@ func validateSettings(domain, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Changeip +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Changeip, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/cloudflare/provider.go b/internal/provider/providers/cloudflare/provider.go index bf8c6bc0c..d37eddfb9 100644 --- a/internal/provider/providers/cloudflare/provider.go +++ b/internal/provider/providers/cloudflare/provider.go @@ -111,8 +111,12 @@ func validateSettings(domain, email, key, userServiceKey, zoneIdentifier string, return nil } +func (p *Provider) Name() models.Provider { + return constants.Cloudflare +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Cloudflare, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/custom/provider.go b/internal/provider/providers/custom/provider.go index 514ce7dfa..33a9bb7e7 100644 --- a/internal/provider/providers/custom/provider.go +++ b/internal/provider/providers/custom/provider.go @@ -86,8 +86,12 @@ func validateSettings(domain string, url *url.URL, } } +func (p *Provider) Name() models.Provider { + return constants.Custom +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Custom, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dd24/provider.go b/internal/provider/providers/dd24/provider.go index 013d2ce75..2ccaf1253 100644 --- a/internal/provider/providers/dd24/provider.go +++ b/internal/provider/providers/dd24/provider.go @@ -63,8 +63,12 @@ func validateSettings(domain, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Dd24 +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Dd24, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/ddnss/provider.go b/internal/provider/providers/ddnss/provider.go index c9d53be8d..b97ec99dd 100644 --- a/internal/provider/providers/ddnss/provider.go +++ b/internal/provider/providers/ddnss/provider.go @@ -74,8 +74,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DdnssDe +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DdnssDe, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/desec/provider.go b/internal/provider/providers/desec/provider.go index 08ac414ae..16d8321f4 100644 --- a/internal/provider/providers/desec/provider.go +++ b/internal/provider/providers/desec/provider.go @@ -66,8 +66,12 @@ func validateSettings(domain, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DeSEC +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DeSEC, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/digitalocean/provider.go b/internal/provider/providers/digitalocean/provider.go index d1c21abaa..4bb18581d 100644 --- a/internal/provider/providers/digitalocean/provider.go +++ b/internal/provider/providers/digitalocean/provider.go @@ -63,8 +63,12 @@ func validateSettings(domain, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DigitalOcean +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DigitalOcean, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dnsomatic/provider.go b/internal/provider/providers/dnsomatic/provider.go index ea6f85ea7..7be6b320c 100644 --- a/internal/provider/providers/dnsomatic/provider.go +++ b/internal/provider/providers/dnsomatic/provider.go @@ -70,8 +70,12 @@ func validateSettings(domain, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DNSOMatic +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DNSOMatic, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dnspod/provider.go b/internal/provider/providers/dnspod/provider.go index 740eaaf6f..e0b2bae11 100644 --- a/internal/provider/providers/dnspod/provider.go +++ b/internal/provider/providers/dnspod/provider.go @@ -64,8 +64,12 @@ func validateSettings(domain, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DNSPod +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DNSPod, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/domeneshop/provider.go b/internal/provider/providers/domeneshop/provider.go index b67de2f18..18958ab11 100644 --- a/internal/provider/providers/domeneshop/provider.go +++ b/internal/provider/providers/domeneshop/provider.go @@ -71,8 +71,12 @@ func validateSettings(domain, owner, token, secret string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Domeneshop +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Domeneshop, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dondominio/provider.go b/internal/provider/providers/dondominio/provider.go index 50965e25b..d59cefa8b 100644 --- a/internal/provider/providers/dondominio/provider.go +++ b/internal/provider/providers/dondominio/provider.go @@ -76,8 +76,12 @@ func validateSettings(domain, username, key string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DonDominio +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DonDominio, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dreamhost/provider.go b/internal/provider/providers/dreamhost/provider.go index a3cd80b50..be8a0c3b1 100644 --- a/internal/provider/providers/dreamhost/provider.go +++ b/internal/provider/providers/dreamhost/provider.go @@ -70,8 +70,12 @@ func validateSettings(domain, key string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Dreamhost +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Dreamhost, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/duckdns/provider.go b/internal/provider/providers/duckdns/provider.go index ba9e33bd3..9dba9fbb4 100644 --- a/internal/provider/providers/duckdns/provider.go +++ b/internal/provider/providers/duckdns/provider.go @@ -95,8 +95,12 @@ func validateSettings(domain, owner, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DuckDNS +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DuckDNS, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dyn/provider.go b/internal/provider/providers/dyn/provider.go index 59db1511d..edf47388b 100644 --- a/internal/provider/providers/dyn/provider.go +++ b/internal/provider/providers/dyn/provider.go @@ -75,8 +75,12 @@ func validateSettings(domain, username, clientKey string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Dyn +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Dyn, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dynu/provider.go b/internal/provider/providers/dynu/provider.go index 8c1f301c8..a80adf841 100644 --- a/internal/provider/providers/dynu/provider.go +++ b/internal/provider/providers/dynu/provider.go @@ -78,8 +78,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Dynu +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Dynu, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/dynv6/provider.go b/internal/provider/providers/dynv6/provider.go index 63e368d0f..7e5bd906f 100644 --- a/internal/provider/providers/dynv6/provider.go +++ b/internal/provider/providers/dynv6/provider.go @@ -65,8 +65,12 @@ func validateSettings(domain, owner, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.DynV6 +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.DynV6, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/easydns/provider.go b/internal/provider/providers/easydns/provider.go index c948facfc..92bd5b39a 100644 --- a/internal/provider/providers/easydns/provider.go +++ b/internal/provider/providers/easydns/provider.go @@ -69,8 +69,12 @@ func validateSettings(domain, username, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.EasyDNS +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.EasyDNS, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/example/provider.go b/internal/provider/providers/example/provider.go index 0b2a060d0..44ee7d043 100644 --- a/internal/provider/providers/example/provider.go +++ b/internal/provider/providers/example/provider.go @@ -81,10 +81,14 @@ func validateSettings(domain, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Example +} + func (p *Provider) String() string { // TODO update the name of the provider and add it to the // internal/provider/constants package. - return utils.ToString(p.domain, p.owner, constants.Dyn, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/freedns/provider.go b/internal/provider/providers/freedns/provider.go index f5cf4a83a..df41aeedb 100644 --- a/internal/provider/providers/freedns/provider.go +++ b/internal/provider/providers/freedns/provider.go @@ -63,8 +63,12 @@ func validateSettings(domain, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.FreeDNS +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.FreeDNS, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/gandi/provider.go b/internal/provider/providers/gandi/provider.go index f663f6873..6b7ec271b 100644 --- a/internal/provider/providers/gandi/provider.go +++ b/internal/provider/providers/gandi/provider.go @@ -74,8 +74,12 @@ func validateSettings(domain, apiKey, personalAccessToken string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Gandi +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Gandi, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/gcp/provider.go b/internal/provider/providers/gcp/provider.go index 9f5af9c1e..52578efda 100644 --- a/internal/provider/providers/gcp/provider.go +++ b/internal/provider/providers/gcp/provider.go @@ -79,8 +79,12 @@ func validateSettings(domain, project, zone string, credentials json.RawMessage) return nil } +func (p *Provider) Name() models.Provider { + return constants.GCP +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.GCP, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/godaddy/provider.go b/internal/provider/providers/godaddy/provider.go index 8ba04cc4d..bf76a3561 100644 --- a/internal/provider/providers/godaddy/provider.go +++ b/internal/provider/providers/godaddy/provider.go @@ -74,8 +74,12 @@ func validateSettings(domain, key, secret string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.GoDaddy +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.GoDaddy, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/goip/provider.go b/internal/provider/providers/goip/provider.go index 45a9f0e66..d83db3227 100644 --- a/internal/provider/providers/goip/provider.go +++ b/internal/provider/providers/goip/provider.go @@ -95,8 +95,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.GoIP +} + func (p *Provider) String() string { - return utils.ToString(p.owner, p.domain, constants.GoIP, p.ipVersion) + return utils.ToString(p.owner, p.domain, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/he/provider.go b/internal/provider/providers/he/provider.go index affef2534..50d6ee7dd 100644 --- a/internal/provider/providers/he/provider.go +++ b/internal/provider/providers/he/provider.go @@ -64,8 +64,12 @@ func validateSettings(domain, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.HE +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.HE, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/hetzner/provider.go b/internal/provider/providers/hetzner/provider.go index d0ffdcebf..53ac1b976 100644 --- a/internal/provider/providers/hetzner/provider.go +++ b/internal/provider/providers/hetzner/provider.go @@ -75,8 +75,12 @@ func validateSettings(domain, zoneIdentifier, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Hetzner +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Hetzner, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/infomaniak/provider.go b/internal/provider/providers/infomaniak/provider.go index 93b77e337..097e5762b 100644 --- a/internal/provider/providers/infomaniak/provider.go +++ b/internal/provider/providers/infomaniak/provider.go @@ -71,8 +71,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Infomaniak +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Infomaniak, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/inwx/provider.go b/internal/provider/providers/inwx/provider.go index bb2aeb4eb..958f7f2b7 100644 --- a/internal/provider/providers/inwx/provider.go +++ b/internal/provider/providers/inwx/provider.go @@ -69,8 +69,12 @@ func validateSettings(domain, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.INWX +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.INWX, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/ionos/provider.go b/internal/provider/providers/ionos/provider.go index 48e92eadb..163134801 100644 --- a/internal/provider/providers/ionos/provider.go +++ b/internal/provider/providers/ionos/provider.go @@ -59,8 +59,12 @@ func validateSettings(domain, apiKey string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Ionos +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Ionos, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/linode/provider.go b/internal/provider/providers/linode/provider.go index e1a3d2a44..b0f98545e 100644 --- a/internal/provider/providers/linode/provider.go +++ b/internal/provider/providers/linode/provider.go @@ -65,8 +65,12 @@ func validateSettings(domain, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Linode +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Linode, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/loopia/provider.go b/internal/provider/providers/loopia/provider.go index c1dedf607..ef6922bf6 100644 --- a/internal/provider/providers/loopia/provider.go +++ b/internal/provider/providers/loopia/provider.go @@ -73,8 +73,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Loopia +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Dyn, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/luadns/provider.go b/internal/provider/providers/luadns/provider.go index 62376012b..d530fd6ee 100644 --- a/internal/provider/providers/luadns/provider.go +++ b/internal/provider/providers/luadns/provider.go @@ -74,8 +74,12 @@ func validateSettings(domain, email, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.LuaDNS +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.LuaDNS, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/myaddr/provider.go b/internal/provider/providers/myaddr/provider.go index ecbc62d97..7be3e63a5 100644 --- a/internal/provider/providers/myaddr/provider.go +++ b/internal/provider/providers/myaddr/provider.go @@ -59,8 +59,12 @@ func validateSettings(domain, key string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Myaddr +} + func (p *Provider) String() string { - return utils.ToString(p.Domain(), p.Owner(), constants.Myaddr, p.IPVersion()) + return utils.ToString(p.Domain(), p.Owner(), p.Name(), p.IPVersion()) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/namecheap/provider.go b/internal/provider/providers/namecheap/provider.go index 247689943..836a9d523 100644 --- a/internal/provider/providers/namecheap/provider.go +++ b/internal/provider/providers/namecheap/provider.go @@ -63,8 +63,12 @@ func validateSettings(domain, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Namecheap +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Namecheap, ipversion.IP4) + return utils.ToString(p.domain, p.owner, p.Name(), ipversion.IP4) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/namecom/provider.go b/internal/provider/providers/namecom/provider.go index b66d3a987..411528d63 100644 --- a/internal/provider/providers/namecom/provider.go +++ b/internal/provider/providers/namecom/provider.go @@ -75,8 +75,12 @@ func validateSettings(domain, username, token string, ttl *uint32) (err error) { } } +func (p *Provider) Name() models.Provider { + return constants.NameCom +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.NameCom, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/namesilo/provider.go b/internal/provider/providers/namesilo/provider.go index 1b42d41b9..3f1b51d80 100644 --- a/internal/provider/providers/namesilo/provider.go +++ b/internal/provider/providers/namesilo/provider.go @@ -90,8 +90,12 @@ func validateSettings(domain, key string, ttl *uint32) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.NameSilo +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.NameSilo, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/netcup/provider.go b/internal/provider/providers/netcup/provider.go index ef361adc3..92ea6c6c8 100644 --- a/internal/provider/providers/netcup/provider.go +++ b/internal/provider/providers/netcup/provider.go @@ -72,8 +72,12 @@ func validateSettings(domain, customerNumber, apiKey, password string) (err erro return nil } +func (p *Provider) Name() models.Provider { + return constants.Netcup +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Netcup, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/njalla/provider.go b/internal/provider/providers/njalla/provider.go index d802efdba..f970b44f0 100644 --- a/internal/provider/providers/njalla/provider.go +++ b/internal/provider/providers/njalla/provider.go @@ -62,8 +62,12 @@ func validateSettings(domain, key string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Njalla +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Njalla, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/noip/provider.go b/internal/provider/providers/noip/provider.go index 1a3e05603..c6844c4b1 100644 --- a/internal/provider/providers/noip/provider.go +++ b/internal/provider/providers/noip/provider.go @@ -75,8 +75,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.NoIP +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.NoIP, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/nowdns/provider.go b/internal/provider/providers/nowdns/provider.go index b30497526..1cb227ed6 100644 --- a/internal/provider/providers/nowdns/provider.go +++ b/internal/provider/providers/nowdns/provider.go @@ -67,8 +67,12 @@ func validateSettings(domain, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.NowDNS +} + func (p *Provider) String() string { - return utils.ToString(p.domain, "@", constants.NowDNS, p.ipVersion) + return utils.ToString(p.domain, "@", p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/opendns/provider.go b/internal/provider/providers/opendns/provider.go index 99ea84f6f..0fe6039a9 100644 --- a/internal/provider/providers/opendns/provider.go +++ b/internal/provider/providers/opendns/provider.go @@ -71,8 +71,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.OpenDNS +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.OpenDNS, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/ovh/provider.go b/internal/provider/providers/ovh/provider.go index 61ee647bf..de1aa4742 100644 --- a/internal/provider/providers/ovh/provider.go +++ b/internal/provider/providers/ovh/provider.go @@ -109,8 +109,12 @@ func validateSettings(domain, mode, owner, appKey, consumerKey, return nil } +func (p *Provider) Name() models.Provider { + return constants.OVH +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.OVH, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/porkbun/provider.go b/internal/provider/providers/porkbun/provider.go index 45d32534a..365e53a13 100644 --- a/internal/provider/providers/porkbun/provider.go +++ b/internal/provider/providers/porkbun/provider.go @@ -71,8 +71,12 @@ func validateSettings(domain, apiKey, secretAPIKey string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Porkbun +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Porkbun, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/route53/provider.go b/internal/provider/providers/route53/provider.go index 4a8a1beb5..cccccfffc 100644 --- a/internal/provider/providers/route53/provider.go +++ b/internal/provider/providers/route53/provider.go @@ -97,8 +97,12 @@ func validateSettings(domain, accessKey, secretKey, zoneID string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Route53 +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Route53, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/selfhostde/provider.go b/internal/provider/providers/selfhostde/provider.go index 23cc7e2af..ba6853736 100644 --- a/internal/provider/providers/selfhostde/provider.go +++ b/internal/provider/providers/selfhostde/provider.go @@ -71,8 +71,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.SelfhostDe +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.SelfhostDe, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/servercow/provider.go b/internal/provider/providers/servercow/provider.go index e623f97f2..5d8003d2a 100644 --- a/internal/provider/providers/servercow/provider.go +++ b/internal/provider/providers/servercow/provider.go @@ -80,8 +80,12 @@ func validateSettings(domain, owner, username, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Servercow +} + func (p *Provider) String() string { - return utils.ToString("servercow.de", p.owner, constants.Servercow, p.ipVersion) + return utils.ToString("servercow.de", p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/spdyn/provider.go b/internal/provider/providers/spdyn/provider.go index fbd8c556c..905983b9f 100644 --- a/internal/provider/providers/spdyn/provider.go +++ b/internal/provider/providers/spdyn/provider.go @@ -79,8 +79,12 @@ func validateSettings(domain, owner, token, user, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Spdyn +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Spdyn, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/strato/provider.go b/internal/provider/providers/strato/provider.go index eff77af04..74ad2854b 100644 --- a/internal/provider/providers/strato/provider.go +++ b/internal/provider/providers/strato/provider.go @@ -66,8 +66,12 @@ func validateSettings(domain, owner, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Strato +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Strato, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/variomedia/provider.go b/internal/provider/providers/variomedia/provider.go index a57a30375..cfeab84cd 100644 --- a/internal/provider/providers/variomedia/provider.go +++ b/internal/provider/providers/variomedia/provider.go @@ -71,8 +71,12 @@ func validateSettings(domain, owner, email, password string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Variomedia +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Variomedia, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/vultr/provider.go b/internal/provider/providers/vultr/provider.go index e5469f955..0055918ca 100644 --- a/internal/provider/providers/vultr/provider.go +++ b/internal/provider/providers/vultr/provider.go @@ -65,8 +65,12 @@ func validateSettings(domain, apiKey string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Vultr +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Vultr, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/provider/providers/zoneedit/provider.go b/internal/provider/providers/zoneedit/provider.go index b1015eef9..bfafd49f4 100644 --- a/internal/provider/providers/zoneedit/provider.go +++ b/internal/provider/providers/zoneedit/provider.go @@ -70,8 +70,12 @@ func validateSettings(domain, username, token string) (err error) { return nil } +func (p *Provider) Name() models.Provider { + return constants.Zoneedit +} + func (p *Provider) String() string { - return utils.ToString(p.domain, p.owner, constants.Zoneedit, p.ipVersion) + return utils.ToString(p.domain, p.owner, p.Name(), p.ipVersion) } func (p *Provider) Domain() string { diff --git a/internal/records/json.go b/internal/records/json.go new file mode 100644 index 000000000..bdbdce9e8 --- /dev/null +++ b/internal/records/json.go @@ -0,0 +1,56 @@ +package records + +import ( + "time" + + "github.com/qdm12/ddns-updater/internal/constants" + "github.com/qdm12/ddns-updater/internal/models" +) + +func (r *Record) JSON(now time.Time) models.JSONRecord { + const NotAvailable = "N/A" + + // Determine status and message + status := string(r.Status) + message := r.Message + if r.Status == constants.UPTODATE { + message = "no IP change for " + r.History.GetDurationSinceSuccess(now) + } + + // Get current IP + currentIP := r.History.GetCurrentIP() + currentIPStr := NotAvailable + if currentIP.IsValid() { + currentIPStr = currentIP.String() + } + + // Get previous IPs (limit to last 10, similar to HTML view) + previousIPs := r.History.GetPreviousIPs() + var previousIPsStr []string + totalIPsInHistory := len(previousIPs) + + if len(previousIPs) > 0 { + const maxPreviousIPs = 10 + for i, ip := range previousIPs { + if i == maxPreviousIPs { + break + } + previousIPsStr = append(previousIPsStr, ip.String()) + } + } + + return models.JSONRecord{ + Domain: r.Provider.Domain(), + Owner: r.Provider.Owner(), + Provider: string(r.Provider.Name()), + IPVersion: r.Provider.IPVersion().String(), + Status: status, + Message: message, + CurrentIP: currentIPStr, + PreviousIPs: previousIPsStr, + TotalIPsInHistory: totalIPsInHistory, + LastUpdate: r.Time, + SuccessTime: r.History.GetSuccessTime(), + Duration: r.History.GetDurationSinceSuccess(now), + } +} diff --git a/internal/server/handler.go b/internal/server/handler.go index cff3fdacc..a29025fb7 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -56,6 +56,8 @@ func newHandler(ctx context.Context, rootURL string, } router.Get(rootURL+"/", handlers.index) + router.Get(rootURL+"/json", handlers.json) + router.Get(rootURL+"/update", handlers.update) router.Handle(rootURL+"/static/*", http.StripPrefix(rootURL+"/static/", http.FileServerFS(staticFolder))) diff --git a/internal/server/json.go b/internal/server/json.go new file mode 100644 index 000000000..4d3750950 --- /dev/null +++ b/internal/server/json.go @@ -0,0 +1,48 @@ +package server + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/qdm12/ddns-updater/internal/models" +) + +func (h *handlers) json(w http.ResponseWriter, _ *http.Request) { + jsonData := models.JSONData{ + Records: []models.JSONRecord{}, + Time: h.timeNow(), + } + + now := h.timeNow() + var lastSuccessTime time.Time + var lastSuccessIP string + + for _, record := range h.db.SelectAll() { + jsonRecord := record.JSON(now) + jsonData.Records = append(jsonData.Records, jsonRecord) + + // Track the most recent successful update across all records + successTime := record.History.GetSuccessTime() + if !successTime.IsZero() && successTime.After(lastSuccessTime) { + lastSuccessTime = successTime + currentIP := record.History.GetCurrentIP() + if currentIP.IsValid() { + lastSuccessIP = currentIP.String() + } + } + } + + jsonData.LastSuccessTime = lastSuccessTime + jsonData.LastSuccessIP = lastSuccessIP + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-cache") + + err := json.NewEncoder(w).Encode(jsonData) + if err != nil { + httpError(w, http.StatusInternalServerError, "failed encoding JSON: "+err.Error()) + return + } +} + diff --git a/internal/server/json_test.go b/internal/server/json_test.go new file mode 100644 index 000000000..1b25c6fac --- /dev/null +++ b/internal/server/json_test.go @@ -0,0 +1,207 @@ +package server + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/netip" + "testing" + "time" + + "github.com/qdm12/ddns-updater/internal/constants" + "github.com/qdm12/ddns-updater/internal/models" + "github.com/qdm12/ddns-updater/internal/records" + "github.com/qdm12/ddns-updater/pkg/publicip/ipversion" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHandlers_JSON(t *testing.T) { + t.Parallel() + + // Create a mock database with test data + testRecords := []records.Record{ + { + Provider: &mockProvider{}, + History: models.History{ + { + IP: netip.MustParseAddr("192.168.1.1"), + Time: time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC), + }, + { + IP: netip.MustParseAddr("192.168.1.2"), + Time: time.Date(2023, 1, 1, 11, 0, 0, 0, time.UTC), + }, + { + IP: netip.MustParseAddr("192.168.1.3"), + Time: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + }, + Status: constants.SUCCESS, + Message: "test message", + Time: time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + + mockDB := &mockDatabase{ + records: testRecords, + } + + // Create handlers + h := &handlers{ + db: mockDB, + timeNow: func() time.Time { return time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) }, + } + + // Create request + req := httptest.NewRequest(http.MethodGet, "/json", nil) + w := httptest.NewRecorder() + + // Call the handler + h.json(w, req) + + // Check response + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + assert.Equal(t, "no-cache", w.Header().Get("Cache-Control")) + + // Parse response body + var response models.JSONData + err := json.NewDecoder(w.Body).Decode(&response) + require.NoError(t, err) + + // Verify response structure + assert.Len(t, response.Records, 1) + assert.Equal(t, time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), response.Time) + // LastSuccessTime should be from the most recent history entry (History[2] at 12:00:00) + assert.Equal(t, time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), response.LastSuccessTime) + assert.Equal(t, "192.168.1.3", response.LastSuccessIP) + + record := response.Records[0] + assert.Equal(t, "test.example.com", record.Domain) + assert.Equal(t, "test", record.Owner) + assert.Equal(t, "mock", record.Provider) + assert.Equal(t, "ipv4", record.IPVersion) + assert.Equal(t, "success", record.Status) + assert.Equal(t, "test message", record.Message) + assert.Equal(t, "192.168.1.3", record.CurrentIP) + assert.Len(t, record.PreviousIPs, 2) + assert.Equal(t, "192.168.1.2", record.PreviousIPs[0]) // most recent previous + assert.Equal(t, "192.168.1.1", record.PreviousIPs[1]) // oldest previous + assert.Equal(t, 2, record.TotalIPsInHistory) +} + +func TestHandlers_JSON_EmptyRecords(t *testing.T) { + t.Parallel() + + // Create a mock database with no records + mockDB := &mockDatabase{ + records: []records.Record{}, + } + + // Create handlers + h := &handlers{ + db: mockDB, + timeNow: func() time.Time { return time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC) }, + } + + // Create request + req := httptest.NewRequest(http.MethodGet, "/json", nil) + w := httptest.NewRecorder() + + // Call the handler + h.json(w, req) + + // Check response + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + // Get the body string before decoding (since decoding consumes the body) + bodyBytes := w.Body.Bytes() + bodyStr := string(bodyBytes) + + // Verify the raw JSON contains "[]" (with possible whitespace) and not "null" for records + assert.Regexp(t, `"records"\s*:\s*\[\]`, bodyStr) + assert.NotContains(t, bodyStr, `"records":null`) + + // Parse response body + var response models.JSONData + err := json.Unmarshal(bodyBytes, &response) + require.NoError(t, err) + + // Verify response structure - records should be an empty array, not null + assert.NotNil(t, response.Records) + assert.Len(t, response.Records, 0) + assert.Equal(t, time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC), response.Time) + assert.True(t, response.LastSuccessTime.IsZero()) + assert.Empty(t, response.LastSuccessIP) +} + +// Mock database for testing +type mockDatabase struct { + records []records.Record +} + +func (m *mockDatabase) SelectAll() []records.Record { + return m.records +} + +func (m *mockDatabase) Start(ctx context.Context) (<-chan error, error) { + return nil, nil +} + +func (m *mockDatabase) Stop() error { + return nil +} + +// Mock provider for testing +type mockProvider struct{} + +func (m *mockProvider) String() string { + return "[domain: test.example.com | owner: test | provider: mock | ip: ipv4]" +} + +func (m *mockProvider) Name() models.Provider { + return "mock" +} + +func (m *mockProvider) Domain() string { + return "test.example.com" +} + +func (m *mockProvider) Owner() string { + return "test" +} + +func (m *mockProvider) BuildDomainName() string { + return "test.test.example.com" +} + +func (m *mockProvider) HTML() models.HTMLRow { + return models.HTMLRow{ + Domain: "test.example.com", + Owner: "test", + Provider: "mock", + IPVersion: "ipv4", + Status: "", + CurrentIP: "", + PreviousIPs: "", + } +} + +func (m *mockProvider) Proxied() bool { + return false +} + +func (m *mockProvider) IPVersion() ipversion.IPVersion { + return ipversion.IP4 +} + +func (m *mockProvider) IPv6Suffix() netip.Prefix { + return netip.Prefix{} +} + +func (m *mockProvider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) { + return ip, nil +}