AbstractDnsNodeRepository.java
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.bremersee.dccon.repository;
import static org.bremersee.dccon.model.UnknownFilter.ALL;
import static org.bremersee.dccon.model.UnknownFilter.NO_UNKNOWN;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.Getter;
import org.bremersee.data.ldaptive.LdaptiveTemplate;
import org.bremersee.dccon.config.DomainControllerProperties;
import org.bremersee.dccon.model.DhcpLease;
import org.bremersee.dccon.model.DnsNode;
import org.bremersee.dccon.model.DnsPair;
import org.bremersee.dccon.model.DnsRecord;
import org.bremersee.dccon.model.DnsZone;
import org.bremersee.dccon.model.UnknownFilter;
import org.springframework.util.StringUtils;
/**
* The abstract dns node repository.
*
* @author Christian Bremer
*/
public abstract class AbstractDnsNodeRepository extends AbstractRepository
implements DnsNodeRepository {
@Getter(AccessLevel.PACKAGE)
private final DhcpRepository dhcpRepository;
@Getter(AccessLevel.PACKAGE)
private final DnsZoneRepository dnsZoneRepository;
@Getter(AccessLevel.PACKAGE)
private final List<Pattern> excludedNodeNamePatterns;
@Getter(AccessLevel.PACKAGE)
private final Pattern patternIp4;
/**
* Instantiates a new abstract repository.
*
* @param properties the properties
* @param ldapTemplate the ldap template
* @param dhcpRepository the dhcp repository
* @param dnsZoneRepository the dns zone repository
*/
AbstractDnsNodeRepository(
final DomainControllerProperties properties,
final LdaptiveTemplate ldapTemplate,
final DhcpRepository dhcpRepository,
final DnsZoneRepository dnsZoneRepository) {
super(properties, ldapTemplate);
this.dhcpRepository = dhcpRepository;
this.dnsZoneRepository = dnsZoneRepository;
this.excludedNodeNamePatterns = properties.getExcludedNodeRegexList().stream()
.map(Pattern::compile)
.collect(Collectors.toList());
this.patternIp4 = Pattern.compile(properties.getIp4Regex());
}
/**
* Is non excluded dns node boolean.
*
* @param dnsNode the dns node
* @return the boolean
*/
boolean isNonExcludedDnsNode(final DnsNode dnsNode) {
return dnsNode != null && !isExcludedDnsNode(dnsNode);
}
/**
* Is non excluded dns node boolean.
*
* @param dnsNodeName the dns node name
* @return the boolean
*/
boolean isNonExcludedDnsNode(final String dnsNodeName) {
return dnsNodeName != null && !isExcludedDnsNode(dnsNodeName);
}
/**
* Is excluded dns node boolean.
*
* @param dnsNode the dns node
* @return the boolean
*/
boolean isExcludedDnsNode(final DnsNode dnsNode) {
return dnsNode != null && isExcludedDnsNode(dnsNode.getName());
}
/**
* Is excluded dns node boolean.
*
* @param dnsNodeName the dns node name
* @return the boolean
*/
boolean isExcludedDnsNode(final String dnsNodeName) {
return dnsNodeName != null && excludedNodeNamePatterns.stream()
.anyMatch(pattern -> pattern.matcher(dnsNodeName).matches());
}
@Override
public List<DnsNode> findByIps(final Set<String> ips, final UnknownFilter unknownFilter) {
final List<DnsNode> nodes = new ArrayList<>();
for (DnsZone zone : dnsZoneRepository.findNonDnsReverseZones().collect(Collectors.toList())) {
for (DnsNode node : findAll(zone.getName(), unknownFilter, null)
.collect(Collectors.toList())) {
for (DnsRecord record : node.getRecords()) {
if (DnsRecordType.A.is(record.getRecordType())
&& ips.contains(record.getRecordValue())) {
nodes.add(node);
}
}
}
}
return nodes;
}
@Override
public Optional<DnsNode> findByHostName(
final String hostName,
final UnknownFilter unknownFilter) {
final List<String> zoneNames = dnsZoneRepository.findNonDnsReverseZones()
.map(DnsZone::getName)
.sorted((o1, o2) -> {
int n1 = StringUtils.countOccurrencesOf(o1, ".");
int n2 = StringUtils.countOccurrencesOf(o2, ".");
return n2 - n1;
})
.collect(Collectors.toList());
for (String zoneName : zoneNames) {
final String suffix = "." + zoneName;
if (hostName.endsWith(suffix)) {
final String name = hostName.substring(0, hostName.length() - suffix.length());
return findOne(zoneName, name, unknownFilter);
}
}
return findOne(getProperties().getDefaultZone(), hostName, unknownFilter);
}
/**
* Is query result boolean.
*
* @param dnsNode the dns node
* @param query the query
* @return the boolean
*/
boolean isQueryResult(final DnsNode dnsNode, final String query) {
return query != null && query.length() > 2 && dnsNode != null
&& (contains(dnsNode.getName(), query)
|| isQueryResult(dnsNode.getRecords(), query));
}
/**
* Is query result boolean.
*
* @param dnsRecords the dns records
* @param query the query
* @return the boolean
*/
boolean isQueryResult(final Collection<DnsRecord> dnsRecords, final String query) {
if (dnsRecords != null) {
for (DnsRecord dnsRecord : dnsRecords) {
if (isQueryResult(dnsRecord, query)) {
return true;
}
}
}
return false;
}
/**
* Is query result boolean.
*
* @param dnsRecord the dns record
* @param query the query
* @return the boolean
*/
boolean isQueryResult(final DnsRecord dnsRecord, final String query) {
return query != null && query.length() > 2 && dnsRecord != null
&& (contains(dnsRecord.getRecordValue(), query)
|| contains(dnsRecord.getCorrelatedRecordValue(), query)
|| isQueryResult(dnsRecord.getDhcpLease(), query));
}
/**
* Is query result boolean.
*
* @param dhcpLease the dhcp lease
* @param query the query
* @return the boolean
*/
boolean isQueryResult(final DhcpLease dhcpLease, final String query) {
return query != null && query.length() > 2 && dhcpLease != null
&& (contains(dhcpLease.getHostname(), query)
|| contains(dhcpLease.getIp(), query)
|| contains(dhcpLease.getMac(), query)
|| contains(dhcpLease.getManufacturer(), query));
}
/**
* Find one optional.
*
* @param zoneName the zone name
* @param nodeName the node name
* @param unknownFilter the unknown filter
* @param withCorrelationValues the with correlation values
* @param withDhcpLeases the with dhcp leases
* @return the optional
*/
abstract Optional<DnsNode> findOne(
String zoneName,
String nodeName,
UnknownFilter unknownFilter,
boolean withCorrelationValues,
boolean withDhcpLeases);
@Override
public Optional<DnsNode> findOne(
final String zoneName,
final String nodeName,
final UnknownFilter unknownFilter) {
return findOne(zoneName, nodeName, unknownFilter, true, true);
}
/**
* Insert dhcp leases dns node.
*
* @param zoneName the zone name
* @param dnsNode the dns node
* @return the dns node
*/
DnsNode insertDhcpLeases(
final String zoneName,
final DnsNode dnsNode) {
final boolean isReverseZone = dnsZoneRepository.isDnsReverseZone(zoneName);
final Map<String, DhcpLease> leaseMap = isReverseZone
? dhcpRepository.findActiveByHostName() : dhcpRepository.findActiveByIp();
for (final DnsRecord record : dnsNode.getRecords()) {
if (DnsRecordType.A.is(record.getRecordType())) {
record.setDhcpLease(leaseMap.get(record.getRecordValue()));
} else if (DnsRecordType.PTR.is(record.getRecordType())
&& record.getRecordValue().endsWith("." + getProperties().getDefaultZone())) {
final String hostName = record.getRecordValue().substring(
0,
record.getRecordValue().length() - ("." + getProperties().getDefaultZone()).length())
.toLowerCase();
record.setDhcpLease(leaseMap.get(hostName));
}
}
return dnsNode;
}
/**
* Insert correlation values dns node.
*
* @param zoneName the zone name
* @param dnsNode the dns node
* @return the dns node
*/
DnsNode insertCorrelationValues(
final String zoneName,
final DnsNode dnsNode) {
dnsNode.setRecords(dnsNode.getRecords().stream()
.map(dnsRecord -> insertCorrelationValue(zoneName, dnsRecord))
.collect(Collectors.toSet()));
return dnsNode;
}
/**
* Insert correlation value dns record.
*
* @param zoneName the zone name
* @param dnsRecord the dns record
* @return the dns record
*/
DnsRecord insertCorrelationValue(
final String zoneName,
final DnsRecord dnsRecord) {
return findCorrelatedDnsNode(zoneName, dnsRecord)
.filter(dnsPair -> Boolean.TRUE.equals(dnsPair.getNodeExists()))
.flatMap(dnsPair -> dnsPair.getNode().getRecords().stream()
.filter(correlatedRecord -> DnsRecordType.areCorrelated(dnsRecord, correlatedRecord))
.findAny())
.map(correlatedRecord -> {
dnsRecord.setCorrelatedRecordValue(correlatedRecord.getRecordValue());
return dnsRecord;
})
.orElse(dnsRecord);
}
@Override
public Optional<DnsPair> findCorrelatedDnsNode(
final String zoneName,
final DnsRecord record) {
if (DnsRecordType.A.is(record.getRecordType())) {
final String ip4 = record.getRecordValue();
return findDnsZoneByIp4(ip4)
.flatMap(reverseZone -> buildDnsPairByIp4(ip4, reverseZone));
} else if (DnsRecordType.PTR.is(record.getRecordType())) {
final String fqdn = record.getRecordValue();
return findDnsZoneByFqdn(fqdn)
.flatMap(zone -> buildDnsPairByFqdn(fqdn, zone));
}
return Optional.empty();
}
/**
* Build dns pair by ip 4 optional.
*
* @param ip4 the ip 4
* @param reverseZone the reverse zone
* @return the optional
*/
Optional<DnsPair> buildDnsPairByIp4(
final String ip4,
final DnsZone reverseZone) {
return getDnsNodeNameByIp4(ip4, reverseZone.getName())
.map(nodeName -> findOne(reverseZone.getName(), nodeName, NO_UNKNOWN, false, false)
.map(dnsNode -> DnsPair.builder()
.zoneName(reverseZone.getName())
.node(dnsNode)
.nodeExists(true)
.build())
.orElseGet(() -> DnsPair.builder()
.zoneName(reverseZone.getName())
.node(DnsNode.builder()
.name(nodeName)
.build())
.nodeExists(false)
.build()));
}
/**
* Build dns pair by fqdn optional.
*
* @param fqdn the fqdn
* @param dnsZone the dns zone
* @return the optional
*/
Optional<DnsPair> buildDnsPairByFqdn(final String fqdn, final DnsZone dnsZone) {
return getDnsNodeNameByFqdn(fqdn, dnsZone.getName())
.map(nodeName -> findOne(dnsZone.getName(), nodeName, NO_UNKNOWN, false, false)
.map(dnsNode -> DnsPair.builder()
.zoneName(dnsZone.getName())
.node(dnsNode)
.nodeExists(true)
.build())
.orElseGet(() -> DnsPair.builder()
.zoneName(dnsZone.getName())
.node(DnsNode.builder()
.name(nodeName)
.build())
.nodeExists(false)
.build()));
}
@Override
public boolean delete(final String zoneName, final String nodeName) {
return findOne(zoneName, nodeName, ALL, false, false)
.map(node -> delete(zoneName, node))
.orElse(false);
}
@Override
public void deleteAll(final String zoneName, final Collection<String> nodeNames) {
if (nodeNames != null && !nodeNames.isEmpty()) {
for (String nodeName : new LinkedHashSet<>(nodeNames)) {
findOne(zoneName, nodeName, ALL, false, false)
.ifPresent(dnsNode -> delete(zoneName, dnsNode));
}
}
}
/**
* Handle ptr records.
*
* @param zoneName the zone name
* @param nodeName the node name
* @param newRecords the new records
* @param deletedRecords the deleted records
*/
void handlePtrRecords(
final String zoneName,
final String nodeName,
final Set<DnsRecord> newRecords,
final Set<DnsRecord> deletedRecords) {
if (dnsZoneRepository.isDnsReverseZone(zoneName)) {
return;
}
for (final DnsRecord record : deletedRecords) {
findCorrelatedDnsNode(zoneName, record).ifPresent(pair -> {
final Set<DnsRecord> records = new LinkedHashSet<>(pair.getNode().getRecords());
records.remove(DnsRecord.builder()
.recordType(DnsRecordType.PTR.name())
.recordValue(nodeName + "." + zoneName)
.build());
final DnsNode node = pair.getNode().toBuilder()
.records(records)
.build();
save(pair.getZoneName(), node);
});
}
for (final DnsRecord record : newRecords) {
findCorrelatedDnsNode(zoneName, record).ifPresent(pair -> {
final DnsRecord newRecord = DnsRecord.builder()
.recordType(DnsRecordType.PTR.name())
.recordValue(nodeName + "." + zoneName)
.build();
if (!pair.getNode().getRecords().contains(newRecord)) {
final Set<DnsRecord> records = new LinkedHashSet<>(pair.getNode().getRecords());
records.add(newRecord);
final DnsNode node = pair.getNode().toBuilder()
.records(records)
.build();
save(pair.getZoneName(), node);
}
});
}
}
/**
* Find dns zone by an IPv4.
*
* @param ip the IPv4
* @return the dns zone or {@code empty}
*/
Optional<DnsZone> findDnsZoneByIp4(final String ip) {
return dnsZoneRepository.findDnsReverseZones()
.filter(dnsZone -> ip4MatchesDnsZone(ip, dnsZone.getName()))
.findFirst();
}
/**
* Find dns zone by fqdn optional.
*
* @param fqdn the fqdn
* @return the optional
*/
Optional<DnsZone> findDnsZoneByFqdn(final String fqdn) {
final Map<String, DnsZone> zoneMap = dnsZoneRepository.findNonDnsReverseZones()
.collect(Collectors.toMap(dnsNode -> dnsNode.getName().toLowerCase(), dnsNode -> dnsNode));
String tmp = fqdn;
int i;
while ((i = tmp.indexOf('.')) > -1) {
final String zoneName = fqdn.substring(i + 1);
final DnsZone dnsZone = zoneMap.get(zoneName.toLowerCase());
if (dnsZone != null) {
return Optional.of(dnsZone);
}
tmp = zoneName;
}
return Optional.empty();
}
/**
* Checks whether the given IPv4 (e. g. {@code 192.168.1.123}) matches the given dns zone name (e.
* g. {@code 1.168.192.in-addr.arpa}).
*
* @param ip the IPv4 (e. g. {@code 192.168.1.123})
* @param zoneName the dns reverse zone name (e. g. {@code 1.168.192.in-addr.arpa})
* @return {@code true} if the ip matches the dns reverse zone, otherwise {@code false}
*/
boolean ip4MatchesDnsZone(final String ip, final String zoneName) {
if (ip == null || zoneName == null || !patternIp4.matcher(ip).matches()) {
return false;
}
return Optional.ofNullable(splitIp4(ip, zoneName))
.map(ipParts -> ip.equals(ipParts[0] + "." + ipParts[1]))
.orElse(false);
}
/**
* Returns the dns reverse node name.
*
* @param ip the IPv4 (e. g. {@code 192.168.1.123}
* @param zoneName the dns reverse zone name (e. g. {@code 1.168.192.in-addr.arpa}
* @return the dns reverse node name (e. g. {@code 123}
*/
Optional<String> getDnsNodeNameByIp4(final String ip, final String zoneName) {
return Optional.ofNullable(splitIp4(ip, zoneName)).map(parts -> parts[1]);
}
/**
* Returns the dns node name.
*
* @param fqdn the full qualified domain name (e. g. {@code pluto.eixe.bremersee.org})
* @param zoneName the dns zone name (e. g. {@code eixe.bremersee.org})
* @return the dns node name (e. g. {@code pluto})
*/
Optional<String> getDnsNodeNameByFqdn(final String fqdn, final String zoneName) {
if (fqdn == null || zoneName == null) {
return Optional.empty();
}
if (fqdn.toLowerCase().endsWith("." + zoneName.toLowerCase())) {
return Optional.of(fqdn.substring(0, fqdn.length() - ("." + zoneName).length()));
}
return Optional.empty();
}
/**
* Split Ipv4 into parts, e. g. {@code 192.168.1} from the dns reverse zone name and into the node
* name {@code 123}.
*
* @param ip the IPv4 (e. g. {@code 192.168.1.123}
* @param zoneName the dns reverse zone name (e. g. {@code 1.168.192.in-addr.arpa}
* @return the Ipv4 parts or {@code null} if the ip doesn't belong to the dns reverse zone
*/
String[] splitIp4(final String ip, final String zoneName) {
if (ip == null || zoneName == null
|| zoneName.length() <= getProperties().getReverseZoneSuffixIp4().length()
|| !patternIp4.matcher(ip).matches()) {
return null;
}
final String ipPart = zoneName.substring(
0,
zoneName.length() - getProperties().getReverseZoneSuffixIp4().length());
final String[] ipParts = ipPart.split(Pattern.quote("."));
final StringBuilder ipBuilder = new StringBuilder();
for (int i = ipParts.length - 1; i >= 0; i--) {
ipBuilder.append(ipParts[i]);
if (i > 0) {
ipBuilder.append('.');
}
}
final String ipPrefix = ipBuilder.toString();
final String ipPostfix = ip.substring(ipPrefix.length() + 1);
// ipPrefix is something like 192.168.1
// ipPostfix is something like 123
// or
// ipPrefix is something like 192.168
// ipPostfix is something like 1.123 // TODO is this correct or do it have to be 123.1 ?
if (!ip.equals(ipPrefix + "." + ipPostfix)) {
return null;
}
return new String[]{ipPrefix, ipPostfix};
}
}