Compare commits

...

2 Commits

Author SHA1 Message Date
Michael Hoennig
667b3908fd refactor DNS lookup result handling 2024-09-12 07:43:26 +02:00
Michael Hoennig
4f49793817 test for setup of subdomain of non-existing superdomain and improved error message for this case 2024-09-12 07:31:31 +02:00
2 changed files with 66 additions and 33 deletions

View File

@ -2,9 +2,9 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset; import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP;
@ -33,44 +33,18 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
return violations; return violations;
} }
final var domainName = assetEntity.getIdentifier(); final var dnsResult = new Dns(assetEntity.getIdentifier()).fetchRecordsOfType("TXT");
final var dnsResult = new Dns(domainName).fetchRecordsOfType("TXT");
final Supplier<String> getCode = () -> assetEntity.getBookingItem().getDirectValue("verificationCode", String.class);
switch (dnsResult.status()) { switch (dnsResult.status()) {
case Dns.Status.SUCCESS: { case Dns.Status.SUCCESS:
final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get(); violations.addAll(handleDomainNameFound(assetEntity, dnsResult));
final var verificationFound = findTxtRecord(dnsResult, expectedTxtRecordValue)
.or(() -> superDomain(domainName)
.flatMap(superDomainName -> findTxtRecord(
new Dns(superDomainName).fetchRecordsOfType("TXT"),
expectedTxtRecordValue))
);
if (verificationFound.isEmpty()) {
violations.add(
"[DNS] no TXT record '" + expectedTxtRecordValue +
"' found for domain name '" + domainName + "' (nor in its super-domain)");
}
break; break;
}
case Dns.Status.NAME_NOT_FOUND: { case Dns.Status.NAME_NOT_FOUND:
if (isDnsVerificationRequiredForUnregisteredDomain(assetEntity)) { violations.addAll(handleDomainNameNotFoundError(assetEntity, dnsResult));
final var superDomain = superDomain(domainName);
final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + getCode.get();
final var verificationFoundInSuperDomain = superDomain.flatMap(superDomainName -> findTxtRecord(
new Dns(superDomainName).fetchRecordsOfType("TXT"),
expectedTxtRecordValue));
if (verificationFoundInSuperDomain.isEmpty()) {
violations.add(
"[DNS] no TXT record '" + expectedTxtRecordValue +
"' found for domain name '" + superDomain.orElseThrow() + "'");
}
}
// otherwise no DNS verification to be able to setup DNS for domains to register
break; break;
}
case Dns.Status.INVALID_NAME: case Dns.Status.INVALID_NAME:
// should not happen because we validate the domain name at booking item level
violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'"); violations.add("[DNS] invalid domain name '" + assetEntity.getIdentifier() + "'");
break; break;
@ -82,6 +56,10 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
return violations; return violations;
} }
private static String verificationCode(final HsHostingAsset assetEntity) {
return assetEntity.getBookingItem().getDirectValue("verificationCode", String.class);
}
@Override @Override
protected Pattern identifierPattern(final HsHostingAsset assetEntity) { protected Pattern identifierPattern(final HsHostingAsset assetEntity) {
if (assetEntity.getBookingItem() != null) { if (assetEntity.getBookingItem() != null) {
@ -93,6 +71,49 @@ class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE); return Pattern.compile(SUBDOMAIN_NAME_REGEX + "\\." + parentDomainName.replace(".", "\\."), Pattern.CASE_INSENSITIVE);
} }
private static List<String> handleDomainNameFound(final HsHostingAsset assetEntity, final Dns.Result dnsResult) {
final var violations = new ArrayList<String>();
final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + verificationCode(assetEntity);
final var verificationFound = findTxtRecord(dnsResult, expectedTxtRecordValue)
.or(() -> superDomain(assetEntity.getIdentifier())
.flatMap(superDomainName -> findTxtRecord(
new Dns(superDomainName).fetchRecordsOfType("TXT"),
expectedTxtRecordValue))
);
if (verificationFound.isEmpty()) {
violations.add(
"[DNS] no TXT record '" + expectedTxtRecordValue +
"' found for domain name '" + assetEntity.getIdentifier() + "' (nor in its super-domain)");
}
return violations;
}
private static List<String> handleDomainNameNotFoundError(final HsHostingAsset assetEntity, final Dns.Result dnsResult) {
final var violations = new ArrayList<String>();
if (isDnsVerificationRequiredForUnregisteredDomain(assetEntity)) {
final var superDomain = superDomain(assetEntity.getIdentifier());
final var expectedTxtRecordValue = "Hostsharing-domain-setup-verification-code=" + verificationCode(assetEntity);
final var verificationFoundInSuperDomain = superDomain.map(superDomainName ->
{
final Dns.Result superDomainDnsResult = new Dns(superDomainName).fetchRecordsOfType("TXT");
if (superDomainDnsResult.status() != Dns.Status.SUCCESS) {
violations.add("[DNS] lookup failed for domain name '" + superDomainName + "': " + dnsResult.exception());
}
return superDomainDnsResult;
}
)
.flatMap(records -> findTxtRecord(records, expectedTxtRecordValue));
if (verificationFoundInSuperDomain.isEmpty()) {
violations.add(
"[DNS] no TXT record '" + expectedTxtRecordValue +
"' found for domain name '" + superDomain.orElseThrow() + "'");
}
} else {
// otherwise no DNS verification to be able to setup DNS for domains to register
}
return violations;
}
private static boolean isDnsVerificationRequiredForUnregisteredDomain(final HsHostingAsset assetEntity) { private static boolean isDnsVerificationRequiredForUnregisteredDomain(final HsHostingAsset assetEntity) {
return !Dns.isRegistrableDomain(assetEntity.getIdentifier()) return !Dns.isRegistrableDomain(assetEntity.getIdentifier())
&& assetEntity.getParentAsset() == null; && assetEntity.getParentAsset() == null;

View File

@ -358,6 +358,12 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
.isRejectedWithCauseMissingVerificationIn("example.org"); .isRejectedWithCauseMissingVerificationIn("example.org");
} }
@Test
void rejectSetupOfUnregisteredSubdomainOfUnregisteredSuperDomain() {
domainSetupFor("sub.sub.example.org").notRegistered()
.isRejectedWithCauseDomainNameNotFound("sub.example.org");
}
@Test @Test
void acceptSetupOfUnregisteredSubdomainWithParentAssetEvenWithoutDnsVerificationInSuperDomain() { void acceptSetupOfUnregisteredSubdomainWithParentAssetEvenWithoutDnsVerificationInSuperDomain() {
domainSetupWithParentAssetFor("sub.example.org").notRegistered() domainSetupWithParentAssetFor("sub.example.org").notRegistered()
@ -488,6 +494,12 @@ class HsDomainSetupHostingAssetValidatorUnitTest {
); );
} }
void isRejectedWithCauseDomainNameNotFound(final String domainName) {
assertThat(validate()).contains(
"[DNS] lookup failed for domain name '" + domainName + "': javax.naming.NameNotFoundException: domain not registered"
);
}
void isAccepted() { void isAccepted() {
assertThat(validate()).isEmpty(); assertThat(validate()).isEmpty();
} }