import-hosting-domain-assets (#84)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/84
This commit is contained in:
@@ -15,15 +15,16 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
|
||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||
|
||||
class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
// TODO.impl: make package private once we've migrated the legacy data
|
||||
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
// according to RFC 1035 (section 5) and RFC 1034
|
||||
static final String RR_REGEX_NAME = "([a-z0-9\\.-]+|@)\\s+";
|
||||
static final String RR_REGEX_TTL = "(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*";
|
||||
static final String RR_REGEX_IN = "IN\\s+"; // record class IN for Internet
|
||||
static final String RR_RECORD_TYPE = "[A-Z]+\\s+";
|
||||
static final String RR_RECORD_DATA = "[^;].*";
|
||||
static final String RR_COMMENT = "(;.*)*";
|
||||
static final String RR_REGEX_NAME = "(\\*\\.)?([a-zA-Z0-9\\._-]+|@)[ \t]+";
|
||||
static final String RR_REGEX_TTL = "(([1-9][0-9]*[mMhHdDwW]?)+[ \t]+)?";
|
||||
static final String RR_REGEX_IN = "[iI][nN][ \t]+"; // record class IN for Internet
|
||||
static final String RR_RECORD_TYPE = "[a-zA-Z]+[ \t]+";
|
||||
static final String RR_RECORD_DATA = "(([^;]+)|(\".*\")|(\\(.*\\)))[ \t]*";
|
||||
static final String RR_COMMENT = "(;.*)?";
|
||||
|
||||
static final String RR_REGEX_TTL_IN =
|
||||
RR_REGEX_NAME + RR_REGEX_TTL + RR_REGEX_IN + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
||||
@@ -32,26 +33,27 @@ class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator
|
||||
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
||||
public static final String IDENTIFIER_SUFFIX = "|DNS";
|
||||
|
||||
private static List<String> zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated
|
||||
|
||||
HsDomainDnsSetupHostingAssetValidator() {
|
||||
super(
|
||||
DOMAIN_DNS_SETUP,
|
||||
AlarmContact.isOptional(),
|
||||
|
||||
integerProperty("TTL").min(0).withDefault(21600),
|
||||
booleanProperty("auto-SOA-RR").withDefault(true),
|
||||
booleanProperty("auto-SOA").withDefault(true),
|
||||
booleanProperty("auto-NS-RR").withDefault(true),
|
||||
booleanProperty("auto-MX-RR").withDefault(true),
|
||||
booleanProperty("auto-A-RR").withDefault(true),
|
||||
booleanProperty("auto-AAAA-RR").withDefault(true),
|
||||
booleanProperty("auto-MAILSERVICES-RR").withDefault(true),
|
||||
booleanProperty("auto-AUTOCONFIG-RR").withDefault(true), // TODO.spec: does that already exist?
|
||||
booleanProperty("auto-AUTOCONFIG-RR").withDefault(true),
|
||||
booleanProperty("auto-AUTODISCOVER-RR").withDefault(true),
|
||||
booleanProperty("auto-DKIM-RR").withDefault(true),
|
||||
booleanProperty("auto-SPF-RR").withDefault(true),
|
||||
booleanProperty("auto-WILDCARD-MX-RR").withDefault(true),
|
||||
booleanProperty("auto-WILDCARD-A-RR").withDefault(true),
|
||||
booleanProperty("auto-WILDCARD-AAAA-RR").withDefault(true),
|
||||
booleanProperty("auto-WILDCARD-DKIM-RR").withDefault(true), // TODO.spec: check, if that really works
|
||||
booleanProperty("auto-WILDCARD-SPF-RR").withDefault(true),
|
||||
arrayOf(
|
||||
stringProperty("user-RR").matchesRegEx(RR_REGEX_TTL_IN, RR_REGEX_IN_TTL).required()
|
||||
@@ -60,7 +62,7 @@ class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator
|
||||
|
||||
@Override
|
||||
protected Pattern identifierPattern(final HsHostingAsset assetEntity) {
|
||||
return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$");
|
||||
return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,33 +80,105 @@ class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator
|
||||
|
||||
// TODO.spec: define which checks should get raised to error level
|
||||
final var namedCheckZone = new SystemProcess("named-checkzone", fqdn(assetEntity));
|
||||
if (namedCheckZone.execute(toZonefileString(assetEntity)) != 0) {
|
||||
// yes, named-checkzone writes error messages to stdout
|
||||
final var zonefileString = toZonefileString(assetEntity);
|
||||
final var zoneFileErrorResult = zoneFileErrors != null ? zoneFileErrors : result;
|
||||
if (namedCheckZone.execute(zonefileString) != 0) {
|
||||
// yes, named-checkzone writes error messages to stdout, not stderr
|
||||
stream(namedCheckZone.getStdOut().split("\n"))
|
||||
.map(line -> line.replaceAll(" stream-0x[0-9a-f:]+", ""))
|
||||
.forEach(result::add);
|
||||
.map(line -> line.replaceAll(" stream-0x[0-9a-f]+:", "line "))
|
||||
.map(line -> "[" + assetEntity.getIdentifier() + "] " + line)
|
||||
.forEach(zoneFileErrorResult::add);
|
||||
if (!namedCheckZone.getStdErr().isEmpty()) {
|
||||
result.add("unexpected stderr output for " + namedCheckZone.getCommand() + ": " + namedCheckZone.getStdErr());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
String toZonefileString(final HsHostingAsset assetEntity) {
|
||||
// TODO.spec: we need to expand the templates (auto-...) in the same way as in Saltstack
|
||||
// TODO.spec: we need to expand the templates (auto-...) in the same way as in Saltstack, with proper IP-numbers etc.
|
||||
return """
|
||||
$ORIGIN {domain}.
|
||||
$TTL {ttl}
|
||||
$TTL {ttl}
|
||||
|
||||
; these records are just placeholders to create a valid zonefile for the validation
|
||||
@ 1814400 IN SOA {domain}. root.{domain} ( 1999010100 10800 900 604800 86400 )
|
||||
@ IN NS ns
|
||||
|
||||
{userRRs}
|
||||
"""
|
||||
.replace("{domain}", fqdn(assetEntity))
|
||||
.replace("{ttl}", getPropertyValue(assetEntity, "TTL"))
|
||||
.replace("{userRRs}", getPropertyValues(assetEntity, "user-RR") );
|
||||
{auto-SOA}
|
||||
{auto-NS-RR}
|
||||
{auto-MX-RR}
|
||||
{auto-A-RR}
|
||||
{auto-AAAA-RR}
|
||||
{auto-DKIM-RR}
|
||||
{auto-SPF-RR}
|
||||
|
||||
{auto-WILDCARD-MX-RR}
|
||||
{auto-WILDCARD-A-RR}
|
||||
{auto-WILDCARD-AAAA-RR}
|
||||
{auto-WILDCARD-SPF-RR}
|
||||
|
||||
{userRRs}
|
||||
"""
|
||||
.replace("{ttl}", assetEntity.getDirectValue("TTL", Integer.class, 43200).toString())
|
||||
.replace("{auto-SOA}", assetEntity.getDirectValue("auto-SOA", Boolean.class, false).equals(true)
|
||||
? """
|
||||
{domain}. IN SOA h00.hostsharing.net. hostmaster.hostsharing.net. (
|
||||
1303649373 ; serial secs since Jan 1 1970
|
||||
6H ; refresh (>=10000)
|
||||
1H ; retry (>=1800)
|
||||
1W ; expire
|
||||
1H ; minimum
|
||||
)
|
||||
"""
|
||||
: "; no auto-SOA"
|
||||
)
|
||||
.replace("{auto-NS-RR}", assetEntity.getDirectValue("auto-NS-RR", Boolean.class, true)
|
||||
? """
|
||||
{domain}. IN NS dns1.hostsharing.net.
|
||||
{domain}. IN NS dns2.hostsharing.net.
|
||||
{domain}. IN NS dns3.hostsharing.net.
|
||||
"""
|
||||
: "; no auto-NS-RR")
|
||||
.replace("{auto-MX-RR}", assetEntity.getDirectValue("auto-MX-RR", Boolean.class, true)
|
||||
? """
|
||||
{domain}. IN MX 30 mailin1.hostsharing.net.
|
||||
{domain}. IN MX 30 mailin2.hostsharing.net.
|
||||
{domain}. IN MX 30 mailin3.hostsharing.net.
|
||||
"""
|
||||
: "; no auto-MX-RR")
|
||||
.replace("{auto-A-RR}", assetEntity.getDirectValue("auto-A-RR", Boolean.class, true)
|
||||
? "{domain}. IN A 83.223.95.160" // arbitrary IP-number
|
||||
: "; no auto-A-RR")
|
||||
.replace("{auto-AAAA-RR}", assetEntity.getDirectValue("auto-AAA-RR", Boolean.class, true)
|
||||
? "{domain}. IN AAAA 2a01:37:1000::53df:5fa0:0" // arbitrary IP-number
|
||||
: "; no auto-AAAA-RR")
|
||||
.replace("{auto-DKIM-RR}", assetEntity.getDirectValue("auto-DKIM-RR", Boolean.class, true)
|
||||
? "default._domainkey 21600 IN TXT \"v=DKIM1; h=sha256; k=rsa; s=email; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmdM9d15bqe94zbHVcKKpUF875XoCWHKRap/sG3NJZ9xZ/BjfGXmqoEYeFNpX3CB7pOXhH5naq4N+6gTjArTviAiVThHXyebhrxaf1dVS4IUC6raTEyQrWPZUf7ZxXmcCYvOdV4jIQ8GRfxwxqibIJcmMiufXTLIgRUif5uaTgFwIDAQAB\""
|
||||
: "; no auto-DKIM-RR")
|
||||
.replace("{auto-SPF-RR}", assetEntity.getDirectValue("auto-SPF-RR", Boolean.class, true)
|
||||
? "{domain}. IN TXT \"v=spf1 include:spf.hostsharing.net ?all\""
|
||||
: "; no auto-SPF-RR")
|
||||
.replace("{auto-WILDCARD-MX-RR}", assetEntity.getDirectValue("auto-SPF-RR", Boolean.class, true)
|
||||
? """
|
||||
*.{domain}. IN MX 30 mailin1.hostsharing.net.
|
||||
*.{domain}. IN MX 30 mailin1.hostsharing.net.
|
||||
*.{domain}. IN MX 30 mailin1.hostsharing.net.
|
||||
"""
|
||||
: "; no auto-WILDCARD-MX-RR")
|
||||
.replace("{auto-WILDCARD-A-RR}", assetEntity.getDirectValue("auto-WILDCARD-A-RR", Boolean.class, true)
|
||||
? "*.{domain}. IN A 83.223.95.160" // arbitrary IP-number
|
||||
: "; no auto-WILDCARD-A-RR")
|
||||
.replace("{auto-WILDCARD-AAAA-RR}", assetEntity.getDirectValue("auto-WILDCARD-AAAA-RR", Boolean.class, true)
|
||||
? "*.{domain}. IN AAAA 2a01:37:1000::53df:5fa0:0" // arbitrary IP-number
|
||||
: "; no auto-WILDCARD-AAAA-RR")
|
||||
.replace("{auto-WILDCARD-SPF-RR}", assetEntity.getDirectValue("auto-WILDCARD-SPF-RR", Boolean.class, true)
|
||||
? "*.{domain}. IN TXT \"v=spf1 include:spf.hostsharing.net ?all\""
|
||||
: "; no auto-WILDCARD-SPF-RR")
|
||||
.replace("{domain}", fqdn(assetEntity))
|
||||
.replace("{userRRs}", getPropertyValues(assetEntity, "user-RR"));
|
||||
}
|
||||
|
||||
private String fqdn(final HsHostingAsset assetEntity) {
|
||||
return assetEntity.getIdentifier().substring(0, assetEntity.getIdentifier().length()-IDENTIFIER_SUFFIX.length());
|
||||
return assetEntity.getIdentifier().substring(0, assetEntity.getIdentifier().length() - IDENTIFIER_SUFFIX.length());
|
||||
}
|
||||
|
||||
public static void addZonefileErrorsTo(final List<String> zoneFileErrors) {
|
||||
HsDomainDnsSetupHostingAssetValidator.zoneFileErrors = zoneFileErrors;
|
||||
}
|
||||
}
|
||||
|
@@ -13,8 +13,8 @@ import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringPrope
|
||||
class HsDomainHttpSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
public static final String IDENTIFIER_SUFFIX = "|HTTP";
|
||||
public static final String FILESYSTEM_PATH = "^/";
|
||||
public static final String PARTIAL_DOMAIN_NAME_REGEX = "(?!-)[A-Za-z0-9-]{1,63}(?<!-)";
|
||||
public static final String FILESYSTEM_PATH = "^/.*";
|
||||
public static final String SUBDOMAIN_NAME_REGEX = "(\\*|(?!-)[A-Za-z0-9-]{1,63}(?<!-))";
|
||||
|
||||
HsDomainHttpSetupHostingAssetValidator() {
|
||||
super(
|
||||
@@ -37,7 +37,7 @@ class HsDomainHttpSetupHostingAssetValidator extends HostingAssetEntityValidator
|
||||
stringProperty("passenger-python").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/python3").withDefault("/usr/bin/python3"),
|
||||
stringProperty("passenger-ruby").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/ruby").withDefault("/usr/bin/ruby"),
|
||||
arrayOf(
|
||||
stringProperty("subdomains").matchesRegEx(PARTIAL_DOMAIN_NAME_REGEX).required()
|
||||
stringProperty("subdomains").matchesRegEx(SUBDOMAIN_NAME_REGEX).required()
|
||||
).optional());
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMA
|
||||
|
||||
class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||
|
||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}";
|
||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||
|
||||
private final Pattern identifierPattern;
|
||||
|
||||
|
@@ -145,4 +145,8 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public ValidatableProperty<?, ?> getProperty(final String propertyName) {
|
||||
return stream(propertyValidators).filter(pv -> pv.propertyName().equals(propertyName)).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
|
@@ -1,22 +1,27 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
/** This class wraps another (usually persistent) map and
|
||||
* supports applying `PatchMap` as well as a toString method with stable entry order.
|
||||
*/
|
||||
public class PatchableMapWrapper<T> implements Map<String, T> {
|
||||
|
||||
private static final ObjectMapper jsonWriter = new ObjectMapper()
|
||||
.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true)
|
||||
.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
|
||||
private final Map<String, T> delegate;
|
||||
|
||||
private PatchableMapWrapper(final Map<String, T> map) {
|
||||
@@ -53,24 +58,9 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public String toString() {
|
||||
return "{\n"
|
||||
+ (
|
||||
keySet().stream().sorted()
|
||||
.map(k -> " \"" + k + "\": " + formatted(get(k))))
|
||||
.collect(joining(",\n")
|
||||
)
|
||||
+ "\n}\n";
|
||||
}
|
||||
|
||||
private Object formatted(final Object value) {
|
||||
if ( value == null || value instanceof Number || value instanceof Boolean ) {
|
||||
return value;
|
||||
}
|
||||
if ( value.getClass().isArray() ) {
|
||||
return "\"" + Arrays.toString( (Object[]) value) + "\"";
|
||||
}
|
||||
return "\"" + value + "\"";
|
||||
return jsonWriter.writeValueAsString(delegate);
|
||||
}
|
||||
|
||||
// --- below just delegating methods --------------------------------
|
||||
|
@@ -21,6 +21,11 @@ public class SystemProcess {
|
||||
this.processBuilder = new ProcessBuilder(command);
|
||||
}
|
||||
|
||||
|
||||
public String getCommand() {
|
||||
return processBuilder.command().toString();
|
||||
}
|
||||
|
||||
public int execute() throws IOException, InterruptedException {
|
||||
final var process = processBuilder.start();
|
||||
stdOut = fetchOutput(process.getInputStream()); // yeah, twisted ProcessBuilder API
|
||||
|
Reference in New Issue
Block a user