From 115eeec47d8ff73020228ac4448177c1a3a1088e Mon Sep 17 00:00:00 2001 From: Ryan Harg Date: Fri, 29 Nov 2024 13:54:55 +0100 Subject: [PATCH] Implement updating of DuckDNS --- README.md | 6 +- src/main/java/de/rpr/ddnsclient/Updater.java | 6 +- .../java/de/rpr/ddnsclient/dyndns/Ddnss.java | 27 +++++---- .../de/rpr/ddnsclient/dyndns/DuckDNS.java | 56 +++++++++++++++++++ .../java/de/rpr/ddnsclient/dyndns/DynDns.java | 11 ++++ .../de/rpr/ddnsclient/lookup/DnsJava.java | 27 +++++++-- .../rpr/ddnsclient/lookup/PublicIpLookup.java | 23 ++++---- .../java/de/rpr/ddnsclient/model/IPs.java | 7 ++- .../de/rpr/ddnsclient/lookup/DnsJavaTest.java | 4 +- 9 files changed, 128 insertions(+), 39 deletions(-) create mode 100644 src/main/java/de/rpr/ddnsclient/dyndns/DuckDNS.java diff --git a/README.md b/README.md index e2bac74..6ada074 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # ddnsclient -This application can be use to update dynamic hostnames. Currently it implements only the ddnss.de provider, but it -is extensible and more providers can be added easily. +This application can be use to update dynamic hostnames. Currently it implements the following DynDNS providers: + +- ddnss.de +- duckdns.org This project uses Quarkus. diff --git a/src/main/java/de/rpr/ddnsclient/Updater.java b/src/main/java/de/rpr/ddnsclient/Updater.java index d681cab..fcc4bce 100644 --- a/src/main/java/de/rpr/ddnsclient/Updater.java +++ b/src/main/java/de/rpr/ddnsclient/Updater.java @@ -45,7 +45,7 @@ public class Updater { } void run() { - log.trace("Updater running."); + log.info("Updater running."); if (config.isEmpty()) { throw new IllegalStateException("Missing configuration"); @@ -58,7 +58,7 @@ public class Updater { if (updateMap.containsKey(cfg.hostname()) && Duration.between(updateMap.get(cfg.hostname()), LocalDateTime.now()).toSeconds() < backoff.toSeconds()) { - log.debug("Back-off period, skipping update."); + log.info("Back-off period, skipping update."); return; } @@ -73,7 +73,7 @@ public class Updater { dynDns.update(cfg.hostname(), publicIps, new DyndnsAuth(null, null, cfg.token())); updateMap.put(cfg.hostname(), LocalDateTime.now()); } else { - log.debugf("Hostname is up-to-date.", cfg.hostname()); + log.infof("Hostname %s is up-to-date.", cfg.hostname()); } }); } diff --git a/src/main/java/de/rpr/ddnsclient/dyndns/Ddnss.java b/src/main/java/de/rpr/ddnsclient/dyndns/Ddnss.java index 1483500..2c41687 100644 --- a/src/main/java/de/rpr/ddnsclient/dyndns/Ddnss.java +++ b/src/main/java/de/rpr/ddnsclient/dyndns/Ddnss.java @@ -6,9 +6,7 @@ import jakarta.enterprise.context.ApplicationScoped; import org.jboss.logging.Logger; import java.io.IOException; -import java.net.URI; import java.net.http.HttpClient; -import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.text.MessageFormat; @@ -29,26 +27,27 @@ public class Ddnss implements DynDns { } public void update(String hostname, IPs currentIps, DyndnsAuth auth) { - - String updateUrl = MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}&ip6={3}", - auth.token(), hostname, currentIps.v4(), currentIps.v6()); + log.tracef("Updating ddnss hostname %s", hostname); try (HttpClient http = httpClientFactory.create()) { - - HttpRequest request = HttpRequest.newBuilder() - .header("User-Agent", "ddns-client") - .GET() - .uri(URI.create(updateUrl)) - .build(); - - HttpResponse response = http.send(request, HttpResponse.BodyHandlers.ofString()); + String updateUrl = getUpdateUrl(hostname, currentIps, auth.token()); + HttpResponse response = http.send(getRequest(updateUrl), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { log.errorf("Couldn't update hostname %s." + hostname); } - log.info("Hostname updated."); + log.infof("Hostname %s updated.", hostname); } catch (IOException | InterruptedException e) { throw new RuntimeException(e); } } + private String getUpdateUrl(String hostname, IPs currentIps, String token) { + if (currentIps.v4().isPresent() && currentIps.v6().isPresent()) { + return MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}&ip6={3}", token, hostname, currentIps.v4().get(), currentIps.v6().get()); + } else if (currentIps.v4().isPresent()) { + return MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}", token, hostname, currentIps.v4()); + } + throw new RuntimeException("No ips to update!"); + } + } diff --git a/src/main/java/de/rpr/ddnsclient/dyndns/DuckDNS.java b/src/main/java/de/rpr/ddnsclient/dyndns/DuckDNS.java new file mode 100644 index 0000000..875e565 --- /dev/null +++ b/src/main/java/de/rpr/ddnsclient/dyndns/DuckDNS.java @@ -0,0 +1,56 @@ +package de.rpr.ddnsclient.dyndns; + +import de.rpr.ddnsclient.model.DyndnsAuth; +import de.rpr.ddnsclient.model.IPs; +import jakarta.enterprise.context.ApplicationScoped; +import org.jboss.logging.Logger; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; +import java.text.MessageFormat; + +@ApplicationScoped +public class DuckDNS implements DynDns { + + private static final Logger log = Logger.getLogger(DuckDNS.class); + + private final HttpClientFactory httpClientFactory; + + public DuckDNS(HttpClientFactory httpClientFactory) { + this.httpClientFactory = httpClientFactory; + } + + @Override + public String name() { + return "duckdns"; + } + + @Override + public void update(String hostname, IPs currentIps, DyndnsAuth auth) { + log.tracef("Updating duckdns hostname %s", hostname); + + try (HttpClient http = httpClientFactory.create()) { + String updateUrl = getUpdateUrl(hostname, currentIps, auth.token()); + HttpResponse response = http.send(getRequest(updateUrl), HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + log.errorf("Couldn't update hostname %s." + hostname); + } else if (!response.body().equals("OK")) { + throw new RuntimeException("Couldn't update hostname!"); + } + log.infof("Hostname %s updated.", hostname); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + private String getUpdateUrl(String hostname, IPs currentIps, String token) { + if (currentIps.v4().isPresent() && currentIps.v6().isPresent()) { + return MessageFormat.format("https://www.duckdns.org/update?domains={0}&token={1}&ip={2}&ipv6={3}", hostname, token, currentIps.v4().get(), currentIps.v6().get()); + } else if (currentIps.v4().isPresent()) { + return MessageFormat.format("https://www.duckdns.org/update?domains={0}&token={1}&ip={2}", hostname, token, currentIps.v4().get()); + } + throw new RuntimeException("No ips to update!"); + } + +} diff --git a/src/main/java/de/rpr/ddnsclient/dyndns/DynDns.java b/src/main/java/de/rpr/ddnsclient/dyndns/DynDns.java index 3323b16..74e67e5 100644 --- a/src/main/java/de/rpr/ddnsclient/dyndns/DynDns.java +++ b/src/main/java/de/rpr/ddnsclient/dyndns/DynDns.java @@ -3,9 +3,20 @@ package de.rpr.ddnsclient.dyndns; import de.rpr.ddnsclient.model.DyndnsAuth; import de.rpr.ddnsclient.model.IPs; +import java.net.URI; +import java.net.http.HttpRequest; + public interface DynDns { String name(); void update(String hostname, IPs currentIps, DyndnsAuth auth); + + default HttpRequest getRequest(String updateUrl) { + return HttpRequest.newBuilder() + .header("User-Agent", "ddns-client") + .GET() + .uri(URI.create(updateUrl)) + .build(); + } } diff --git a/src/main/java/de/rpr/ddnsclient/lookup/DnsJava.java b/src/main/java/de/rpr/ddnsclient/lookup/DnsJava.java index a20ae51..1f1e19c 100644 --- a/src/main/java/de/rpr/ddnsclient/lookup/DnsJava.java +++ b/src/main/java/de/rpr/ddnsclient/lookup/DnsJava.java @@ -3,6 +3,7 @@ package de.rpr.ddnsclient.lookup; import de.rpr.ddnsclient.model.IPs; import jakarta.enterprise.context.ApplicationScoped; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.jboss.logging.Logger; import org.xbill.DNS.Record; import org.xbill.DNS.*; @@ -11,6 +12,8 @@ import java.util.Optional; @ApplicationScoped public class DnsJava implements DnsResolver { + private static final Logger log = Logger.getLogger(DnsJava.class); + private final String resolverAddress; public DnsJava(@ConfigProperty(name = "ddnsclient.dns.resolver", defaultValue = "9.9.9.9") String resolverAddress) { @@ -19,19 +22,24 @@ public class DnsJava implements DnsResolver { @Override public IPs resolve(String hostname) { - String v4 = getIp(hostname, Type.A).orElseThrow(() -> new RuntimeException("Missing A Record")); - String v6 = getIp(hostname, Type.AAAA).orElseThrow(() -> new RuntimeException("Missing AAAA Record")); + Optional v4 = getIp(hostname, RecordType.A); + Optional v6 = getIp(hostname, RecordType.AAAA); return new IPs(v4, v6); } - private Optional getIp(String hostname, int type) { + private Optional getIp(String hostname, RecordType type) { try { SimpleResolver resolver = new SimpleResolver(resolverAddress); - Lookup lookup = new Lookup(hostname, type); + Lookup lookup = new Lookup(hostname, type.value); lookup.setResolver(resolver); Record[] records = lookup.run(); + if (records == null || records.length == 0) { + log.infof("No record for type %s found", type.name()); + return Optional.empty(); + } + for (Record record : records) { if (record instanceof ARecord) { return Optional.of(((ARecord) record).getAddress().getHostAddress()); @@ -45,4 +53,15 @@ public class DnsJava implements DnsResolver { throw new RuntimeException(e); } } + + private enum RecordType { + A(1), + AAAA(28); + + private final int value; // org.bill.DNS.Type.AAAA + + RecordType(int value) { + this.value = value; + } + } } diff --git a/src/main/java/de/rpr/ddnsclient/lookup/PublicIpLookup.java b/src/main/java/de/rpr/ddnsclient/lookup/PublicIpLookup.java index cd3f23b..a87a5bf 100644 --- a/src/main/java/de/rpr/ddnsclient/lookup/PublicIpLookup.java +++ b/src/main/java/de/rpr/ddnsclient/lookup/PublicIpLookup.java @@ -20,10 +20,7 @@ public class PublicIpLookup { private final CurlProcessFactory curlProcessFactory; private final Optional configuredProvider; - PublicIpLookup( - CurlProcessFactory curlProcessFactory, - @ConfigProperty(name = "ddnsclient.ip-provider") Optional configuredProvider - ) { + PublicIpLookup(CurlProcessFactory curlProcessFactory, @ConfigProperty(name = "ddnsclient.ip-provider") Optional configuredProvider) { this.curlProcessFactory = curlProcessFactory; this.configuredProvider = configuredProvider; } @@ -33,10 +30,9 @@ public class PublicIpLookup { void onStart(@Observes StartupEvent event) { try { configuredProvider.ifPresent(it -> { - log.tracef("Setting ip lookup provider %s", it); - provider = Optional.of(IpLookupProviders.get(it)); - } - ); + log.tracef("Setting ip lookup provider %s", it); + provider = Optional.of(IpLookupProviders.get(it)); + }); } catch (Exception e) { throw new IllegalStateException("Unknow provider configured!", e); } @@ -48,8 +44,8 @@ public class PublicIpLookup { try { String provider = this.provider.map(it -> it.url).orElseGet(() -> IpLookupProviders.random().url); - String v4 = getIp(provider, CurlProcessFactory.IpClass.V4); - String v6 = getIp(provider, CurlProcessFactory.IpClass.V6); + Optional v4 = getIp(provider, CurlProcessFactory.IpClass.V4); + Optional v6 = getIp(provider, CurlProcessFactory.IpClass.V6); return new IPs(v4, v6); } catch (Exception e) { @@ -57,7 +53,7 @@ public class PublicIpLookup { } } - private String getIp(String provider, CurlProcessFactory.IpClass ipClass) { + private Optional getIp(String provider, CurlProcessFactory.IpClass ipClass) { try { Process curlProcess = curlProcessFactory.create(provider, ipClass).start(); curlProcess.waitFor(); @@ -65,9 +61,10 @@ public class PublicIpLookup { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); String ip = reader.readLine(); log.tracef("Retrieved ip: %s", ip); - return ip; + return Optional.of(ip); } catch (Exception e) { - throw new RuntimeException(e); + log.errorf("Couldn't lookup ip. Provider: %s, ipClass: %s", provider, ipClass.name()); + return Optional.empty(); } } diff --git a/src/main/java/de/rpr/ddnsclient/model/IPs.java b/src/main/java/de/rpr/ddnsclient/model/IPs.java index 6ec79af..004eee3 100644 --- a/src/main/java/de/rpr/ddnsclient/model/IPs.java +++ b/src/main/java/de/rpr/ddnsclient/model/IPs.java @@ -1,6 +1,11 @@ package de.rpr.ddnsclient.model; -public record IPs(String v4, String v6) { +import java.util.Optional; +public record IPs(Optional v4, Optional v6) { + + public IPs(String v4, String v6) { + this(Optional.ofNullable(v4), Optional.ofNullable(v6)); + } } diff --git a/src/test/java/de/rpr/ddnsclient/lookup/DnsJavaTest.java b/src/test/java/de/rpr/ddnsclient/lookup/DnsJavaTest.java index 22e67a2..d910057 100644 --- a/src/test/java/de/rpr/ddnsclient/lookup/DnsJavaTest.java +++ b/src/test/java/de/rpr/ddnsclient/lookup/DnsJavaTest.java @@ -10,7 +10,7 @@ class DnsJavaTest { @Test void test() { IPs result = new DnsJava("9.9.9.9").resolve("example.com"); - assertThat(result.v4()).isNotBlank(); - assertThat(result.v6()).isNotBlank(); + assertThat(result.v4()).isPresent(); + assertThat(result.v6()).isPresent(); } } \ No newline at end of file