Compare commits

..

No commits in common. "d8f547d1e78af2ba09b32813d569fb5470758388" and "552454cf6e1eca9f5bed43a3ad6a7ddf337d6678" have entirely different histories.

11 changed files with 40 additions and 276 deletions

View file

@ -1,9 +1,7 @@
# ddnsclient # ddnsclient
This application can be use to update dynamic hostnames. Currently it implements the following DynDNS providers: 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.
- ddnss.de
- duckdns.org
This project uses Quarkus. This project uses Quarkus.

View file

@ -45,7 +45,7 @@ public class Updater {
} }
void run() { void run() {
log.info("Updater running."); log.trace("Updater running.");
if (config.isEmpty()) { if (config.isEmpty()) {
throw new IllegalStateException("Missing configuration"); throw new IllegalStateException("Missing configuration");
@ -58,7 +58,7 @@ public class Updater {
if (updateMap.containsKey(cfg.hostname()) if (updateMap.containsKey(cfg.hostname())
&& Duration.between(updateMap.get(cfg.hostname()), LocalDateTime.now()).toSeconds() < backoff.toSeconds()) { && Duration.between(updateMap.get(cfg.hostname()), LocalDateTime.now()).toSeconds() < backoff.toSeconds()) {
log.info("Back-off period, skipping update."); log.debug("Back-off period, skipping update.");
return; return;
} }
@ -73,7 +73,7 @@ public class Updater {
dynDns.update(cfg.hostname(), publicIps, new DyndnsAuth(null, null, cfg.token())); dynDns.update(cfg.hostname(), publicIps, new DyndnsAuth(null, null, cfg.token()));
updateMap.put(cfg.hostname(), LocalDateTime.now()); updateMap.put(cfg.hostname(), LocalDateTime.now());
} else { } else {
log.infof("Hostname %s is up-to-date.", cfg.hostname()); log.debugf("Hostname is up-to-date.", cfg.hostname());
} }
}); });
} }

View file

@ -6,7 +6,9 @@ import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.text.MessageFormat; import java.text.MessageFormat;
@ -27,29 +29,26 @@ public class Ddnss implements DynDns {
} }
public void update(String hostname, IPs currentIps, DyndnsAuth auth) { public void update(String hostname, IPs currentIps, DyndnsAuth auth) {
log.tracef("Updating ddnss hostname %s", hostname);
String updateUrl = MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}&ip6={3}",
auth.token(), hostname, currentIps.v4(), currentIps.v6());
try (HttpClient http = httpClientFactory.create()) { try (HttpClient http = httpClientFactory.create()) {
String updateUrl = getUpdateUrl(hostname, currentIps, auth.token());
HttpResponse<String> response = http.send(getRequest(updateUrl), HttpResponse.BodyHandlers.ofString()); HttpRequest request = HttpRequest.newBuilder()
.header("User-Agent", "ddns-client")
.GET()
.uri(URI.create(updateUrl))
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
log.errorf("Couldn't update hostname %s." + hostname); log.errorf("Couldn't update hostname %s." + hostname);
} }
log.infof("Hostname %s updated.", hostname); log.info("Hostname updated.");
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
throw new RuntimeException(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().get());
} else if (currentIps.v6().isPresent()) {
return MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip6={2}", token, hostname, currentIps.v6().get());
}
throw new RuntimeException("No ips to update!");
}
} }

View file

@ -1,58 +0,0 @@
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<String> 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());
} else if (currentIps.v6().isPresent()) {
return MessageFormat.format("https://www.duckdns.org/update?domains={0}&token={1}&ipv6={2}", hostname, token, currentIps.v6().get());
}
throw new RuntimeException("No ips to update!");
}
}

View file

@ -3,20 +3,9 @@ package de.rpr.ddnsclient.dyndns;
import de.rpr.ddnsclient.model.DyndnsAuth; import de.rpr.ddnsclient.model.DyndnsAuth;
import de.rpr.ddnsclient.model.IPs; import de.rpr.ddnsclient.model.IPs;
import java.net.URI;
import java.net.http.HttpRequest;
public interface DynDns { public interface DynDns {
String name(); String name();
void update(String hostname, IPs currentIps, DyndnsAuth auth); 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();
}
} }

View file

@ -3,7 +3,6 @@ package de.rpr.ddnsclient.lookup;
import de.rpr.ddnsclient.model.IPs; import de.rpr.ddnsclient.model.IPs;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.xbill.DNS.Record; import org.xbill.DNS.Record;
import org.xbill.DNS.*; import org.xbill.DNS.*;
@ -12,8 +11,6 @@ import java.util.Optional;
@ApplicationScoped @ApplicationScoped
public class DnsJava implements DnsResolver { public class DnsJava implements DnsResolver {
private static final Logger log = Logger.getLogger(DnsJava.class);
private final String resolverAddress; private final String resolverAddress;
public DnsJava(@ConfigProperty(name = "ddnsclient.dns.resolver", defaultValue = "9.9.9.9") String resolverAddress) { public DnsJava(@ConfigProperty(name = "ddnsclient.dns.resolver", defaultValue = "9.9.9.9") String resolverAddress) {
@ -22,24 +19,19 @@ public class DnsJava implements DnsResolver {
@Override @Override
public IPs resolve(String hostname) { public IPs resolve(String hostname) {
Optional<String> v4 = getIp(hostname, RecordType.A); String v4 = getIp(hostname, Type.A).orElseThrow(() -> new RuntimeException("Missing A Record"));
Optional<String> v6 = getIp(hostname, RecordType.AAAA); String v6 = getIp(hostname, Type.AAAA).orElseThrow(() -> new RuntimeException("Missing AAAA Record"));
return new IPs(v4, v6); return new IPs(v4, v6);
} }
private Optional<String> getIp(String hostname, RecordType type) { private Optional<String> getIp(String hostname, int type) {
try { try {
SimpleResolver resolver = new SimpleResolver(resolverAddress); SimpleResolver resolver = new SimpleResolver(resolverAddress);
Lookup lookup = new Lookup(hostname, type.value); Lookup lookup = new Lookup(hostname, type);
lookup.setResolver(resolver); lookup.setResolver(resolver);
Record[] records = lookup.run(); Record[] records = lookup.run();
if (records == null || records.length == 0) {
log.infof("Hostname %s has no record for type %s found", hostname, type.name());
return Optional.empty();
}
for (Record record : records) { for (Record record : records) {
if (record instanceof ARecord) { if (record instanceof ARecord) {
return Optional.of(((ARecord) record).getAddress().getHostAddress()); return Optional.of(((ARecord) record).getAddress().getHostAddress());
@ -53,15 +45,4 @@ public class DnsJava implements DnsResolver {
throw new RuntimeException(e); 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;
}
}
} }

View file

@ -20,7 +20,10 @@ public class PublicIpLookup {
private final CurlProcessFactory curlProcessFactory; private final CurlProcessFactory curlProcessFactory;
private final Optional<String> configuredProvider; private final Optional<String> configuredProvider;
PublicIpLookup(CurlProcessFactory curlProcessFactory, @ConfigProperty(name = "ddnsclient.ip-provider") Optional<String> configuredProvider) { PublicIpLookup(
CurlProcessFactory curlProcessFactory,
@ConfigProperty(name = "ddnsclient.ip-provider") Optional<String> configuredProvider
) {
this.curlProcessFactory = curlProcessFactory; this.curlProcessFactory = curlProcessFactory;
this.configuredProvider = configuredProvider; this.configuredProvider = configuredProvider;
} }
@ -32,7 +35,8 @@ public class PublicIpLookup {
configuredProvider.ifPresent(it -> { configuredProvider.ifPresent(it -> {
log.tracef("Setting ip lookup provider %s", it); log.tracef("Setting ip lookup provider %s", it);
provider = Optional.of(IpLookupProviders.get(it)); provider = Optional.of(IpLookupProviders.get(it));
}); }
);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Unknow provider configured!", e); throw new IllegalStateException("Unknow provider configured!", e);
} }
@ -44,8 +48,8 @@ public class PublicIpLookup {
try { try {
String provider = this.provider.map(it -> it.url).orElseGet(() -> IpLookupProviders.random().url); String provider = this.provider.map(it -> it.url).orElseGet(() -> IpLookupProviders.random().url);
Optional<String> v4 = getIp(provider, CurlProcessFactory.IpClass.V4); String v4 = getIp(provider, CurlProcessFactory.IpClass.V4);
Optional<String> v6 = getIp(provider, CurlProcessFactory.IpClass.V6); String v6 = getIp(provider, CurlProcessFactory.IpClass.V6);
return new IPs(v4, v6); return new IPs(v4, v6);
} catch (Exception e) { } catch (Exception e) {
@ -53,7 +57,7 @@ public class PublicIpLookup {
} }
} }
private Optional<String> getIp(String provider, CurlProcessFactory.IpClass ipClass) { private String getIp(String provider, CurlProcessFactory.IpClass ipClass) {
try { try {
Process curlProcess = curlProcessFactory.create(provider, ipClass).start(); Process curlProcess = curlProcessFactory.create(provider, ipClass).start();
curlProcess.waitFor(); curlProcess.waitFor();
@ -61,10 +65,9 @@ public class PublicIpLookup {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String ip = reader.readLine(); String ip = reader.readLine();
log.tracef("Retrieved ip: %s", ip); log.tracef("Retrieved ip: %s", ip);
return Optional.of(ip); return ip;
} catch (Exception e) { } catch (Exception e) {
log.errorf("Couldn't lookup ip. Provider: %s, ipClass: %s", provider, ipClass.name()); throw new RuntimeException(e);
return Optional.empty();
} }
} }

View file

@ -1,11 +1,6 @@
package de.rpr.ddnsclient.model; package de.rpr.ddnsclient.model;
import java.util.Optional; public record IPs(String v4, String v6) {
public record IPs(Optional<String> v4, Optional<String> v6) {
public IPs(String v4, String v6) {
this(Optional.ofNullable(v4), Optional.ofNullable(v6));
}
} }

View file

@ -58,45 +58,4 @@ class DdnssTest {
.allValues("User-Agent")).containsExactly("ddns-client"); .allValues("User-Agent")).containsExactly("ddns-client");
} }
@Test
void should_send_correct_request_if_only_ipv4_is_present() throws IOException, InterruptedException {
when(httpClientFactory.create()).thenReturn(httpClient);
when(httpClient.send(any(), ArgumentMatchers.<HttpResponse.BodyHandler<String>>any())).thenReturn(httpResponse);
Ddnss dynDns = new Ddnss(httpClientFactory);
dynDns.update(
"example.com",
new IPs("ipv4", null),
new DyndnsAuth(null, null, "token")
);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCaptor.capture(), any());
HttpRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.uri().toString())
.isEqualTo("https://ddnss.de/upd.php?key=token&host=example.com&ip=ipv4");
}
@Test
void should_send_correct_request_if_only_ipv6_is_present() throws IOException, InterruptedException {
when(httpClientFactory.create()).thenReturn(httpClient);
when(httpClient.send(any(), ArgumentMatchers.<HttpResponse.BodyHandler<String>>any())).thenReturn(httpResponse);
Ddnss dynDns = new Ddnss(httpClientFactory);
dynDns.update(
"example.com",
new IPs(null, "ipv6"),
new DyndnsAuth(null, null, "token")
);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCaptor.capture(), any());
HttpRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.uri().toString())
.isEqualTo("https://ddnss.de/upd.php?key=token&host=example.com&ip6=ipv6");
}
} }

View file

@ -1,102 +0,0 @@
package de.rpr.ddnsclient.dyndns;
import de.rpr.ddnsclient.model.DyndnsAuth;
import de.rpr.ddnsclient.model.IPs;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DuckDNSTest {
@Mock
HttpClientFactory httpClientFactory;
@Mock
HttpClient httpClient;
@Mock
HttpResponse<String> httpResponse;
@Test
void should_have_correct_name() {
assertThat(new DuckDNS(httpClientFactory).name()).isEqualTo("duckdns");
}
@Test
void should_send_correct_request_if_ipv4_and_ipv6_are_present() throws IOException, InterruptedException {
when(httpClientFactory.create()).thenReturn(httpClient);
when(httpClient.send(any(), ArgumentMatchers.<HttpResponse.BodyHandler<String>>any())).thenReturn(httpResponse);
DuckDNS dynDns = new DuckDNS(httpClientFactory);
dynDns.update(
"example.com",
new IPs("ipv4", "ipv6"),
new DyndnsAuth(null, null, "token")
);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCaptor.capture(), any());
HttpRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.uri().toString())
.isEqualTo("https://www.duckdns.org/update?domains=example.com&token=token&ip=ipv4&ipv6=ipv6");
assertThat(capturedRequest.headers()
.allValues("User-Agent")).containsExactly("ddns-client");
}
@Test
void should_send_correct_request_if_only_ipv4_is_present() throws IOException, InterruptedException {
when(httpClientFactory.create()).thenReturn(httpClient);
when(httpClient.send(any(), ArgumentMatchers.<HttpResponse.BodyHandler<String>>any())).thenReturn(httpResponse);
DuckDNS dynDns = new DuckDNS(httpClientFactory);
dynDns.update(
"example.com",
new IPs("ipv4", null),
new DyndnsAuth(null, null, "token")
);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCaptor.capture(), any());
HttpRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.uri().toString())
.isEqualTo("https://www.duckdns.org/update?domains=example.com&token=token&ip=ipv4");
}
@Test
void should_send_correct_request_if_only_ipv6_is_present() throws IOException, InterruptedException {
when(httpClientFactory.create()).thenReturn(httpClient);
when(httpClient.send(any(), ArgumentMatchers.<HttpResponse.BodyHandler<String>>any())).thenReturn(httpResponse);
DuckDNS dynDns = new DuckDNS(httpClientFactory);
dynDns.update(
"example.com",
new IPs(null, "ipv6"),
new DyndnsAuth(null, null, "token")
);
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCaptor.capture(), any());
HttpRequest capturedRequest = requestCaptor.getValue();
assertThat(capturedRequest.uri().toString())
.isEqualTo("https://www.duckdns.org/update?domains=example.com&token=token&ipv6=ipv6");
}
}

View file

@ -10,7 +10,7 @@ class DnsJavaTest {
@Test @Test
void test() { void test() {
IPs result = new DnsJava("9.9.9.9").resolve("example.com"); IPs result = new DnsJava("9.9.9.9").resolve("example.com");
assertThat(result.v4()).isPresent(); assertThat(result.v4()).isNotBlank();
assertThat(result.v6()).isPresent(); assertThat(result.v6()).isNotBlank();
} }
} }