Compare commits
2 commits
552454cf6e
...
d8f547d1e7
Author | SHA1 | Date | |
---|---|---|---|
d8f547d1e7 | |||
115eeec47d |
11 changed files with 276 additions and 40 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,29 @@ 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<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
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);
|
||||
}
|
||||
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().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!");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
58
src/main/java/de/rpr/ddnsclient/dyndns/DuckDNS.java
Normal file
58
src/main/java/de/rpr/ddnsclient/dyndns/DuckDNS.java
Normal file
|
@ -0,0 +1,58 @@
|
|||
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!");
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> v4 = getIp(hostname, RecordType.A);
|
||||
Optional<String> v6 = getIp(hostname, RecordType.AAAA);
|
||||
return new IPs(v4, v6);
|
||||
}
|
||||
|
||||
private Optional<String> getIp(String hostname, int type) {
|
||||
private Optional<String> 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("Hostname %s has no record for type %s found", hostname, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,7 @@ public class PublicIpLookup {
|
|||
private final CurlProcessFactory curlProcessFactory;
|
||||
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.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<String> v4 = getIp(provider, CurlProcessFactory.IpClass.V4);
|
||||
Optional<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package de.rpr.ddnsclient.model;
|
||||
|
||||
public record IPs(String v4, String v6) {
|
||||
import java.util.Optional;
|
||||
|
||||
public record IPs(Optional<String> v4, Optional<String> v6) {
|
||||
|
||||
public IPs(String v4, String v6) {
|
||||
this(Optional.ofNullable(v4), Optional.ofNullable(v6));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ class DdnssTest {
|
|||
);
|
||||
|
||||
ArgumentCaptor<HttpRequest> requestCaptor = ArgumentCaptor.forClass(HttpRequest.class);
|
||||
verify(httpClient).send(requestCaptor.capture(),any());
|
||||
verify(httpClient).send(requestCaptor.capture(), any());
|
||||
|
||||
HttpRequest capturedRequest = requestCaptor.getValue();
|
||||
|
||||
|
@ -58,4 +58,45 @@ class DdnssTest {
|
|||
.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");
|
||||
}
|
||||
}
|
102
src/test/java/de/rpr/ddnsclient/dyndns/DuckDNSTest.java
Normal file
102
src/test/java/de/rpr/ddnsclient/dyndns/DuckDNSTest.java
Normal file
|
@ -0,0 +1,102 @@
|
|||
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");
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue