Package io.fathom.cloud.dns.services

Source Code of io.fathom.cloud.dns.services.DnsServiceImpl

package io.fathom.cloud.dns.services;

import io.fathom.cloud.Clock;
import io.fathom.cloud.CloudException;
import io.fathom.cloud.dns.DnsService;
import io.fathom.cloud.dns.backend.DnsBackend;
import io.fathom.cloud.dns.model.DnsRecordset;
import io.fathom.cloud.dns.model.DnsZone;
import io.fathom.cloud.dns.state.DnsRepository;
import io.fathom.cloud.lifecycle.LifecycleListener;
import io.fathom.cloud.protobuf.DnsModel.BackendData;
import io.fathom.cloud.protobuf.DnsModel.DnsRecordData;
import io.fathom.cloud.protobuf.DnsModel.DnsRecordsetData;
import io.fathom.cloud.protobuf.DnsModel.DnsSuffixData;
import io.fathom.cloud.protobuf.DnsModel.DnsZoneData;
import io.fathom.cloud.server.model.Project;
import io.fathom.cloud.state.DuplicateValueException;
import io.fathom.cloud.state.NamedItemCollection;
import io.fathom.cloud.state.NumberedItemCollection;
import io.fathom.cloud.state.StoreOptions;

import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fathomdb.Configuration;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.common.net.InetAddresses;
import com.google.inject.persist.Transactional;

@Singleton
@Transactional
public class DnsServiceImpl implements DnsService, LifecycleListener {
    private static final Logger log = LoggerFactory.getLogger(DnsServiceImpl.class);

    @Inject
    DnsRepository repository;

    @Inject
    WellKnownTlds wellKnownTlds;

    @Inject
    DnsBackends dnsBackends;

    private final boolean canCreateInTld;

    @Inject
    public DnsServiceImpl(Configuration config) {
        // TODO: Add @Configured?
        this.canCreateInTld = config.lookup("dns.allowCreateInTld", true);
    }

    @Override
    public List<DnsZone> listZones(Project project) throws CloudException {
        List<DnsZone> records = Lists.newArrayList();
        for (DnsZoneData data : repository.getDnsZones(project.getId()).list()) {
            DnsZone record = new DnsZone(data);
            records.add(record);
        }
        return records;
    }

    @Override
    public DnsZone createZone(Project project, DnsZoneSpec spec) throws CloudException, DuplicateValueException {
        String zoneName = spec.name;
        zoneName = zoneName.toLowerCase();

        DnsSuffixData suffixData = findMaximalSuffix(zoneName);
        if (suffixData == null) {
            throw new IllegalArgumentException("Unsupported suffix: " + zoneName);
        }

        if (suffixData.getTld() && !canCreateInTld) {
            throw new IllegalArgumentException("Creation of domains blocked (under TLDs)");
        }

        String backendKey = null;
        String zone;

        // TODO: Is there any point in allowing a shared domain but then
        // blocking creation under it?
        // if (suffixData.getSharedDomain() && !canCreateInShared) {
        // throw new
        // IllegalArgumentException("Creation of domains blocked (under shared domains)");
        // }

        String suffix = suffixData.getKey();
        String prefix = zoneName.substring(0, zoneName.length() - suffix.length());
        prefix = CharMatcher.is('.').trimTrailingFrom(prefix);

        if (prefix.isEmpty()) {
            throw new IllegalArgumentException("Unsupported domain (TLD or marked as shared): " + zoneName);
        } else {
            int dotIndex = prefix.indexOf('.');
            String prefixTail;
            if (dotIndex != -1) {
                prefixTail = prefix.substring(dotIndex + 1);
            } else {
                prefixTail = prefix;
            }

            // If this is a shared domain, try to assign it to the user
            if (suffixData.getSharedDomain()) {
                NamedItemCollection<DnsSuffixData> store = repository.getSharedSubdomains(suffixData.getKey());
                DnsSuffixData allocation = store.find(prefixTail);
                if (allocation == null) {
                    DnsSuffixData.Builder b = DnsSuffixData.newBuilder();
                    b.setKey(prefixTail);
                    b.setOwnerProject(project.getId());

                    allocation = store.create(b);
                } else {
                    if (allocation.getOwnerProject() != project.getId()) {
                        // Already assigned to someone else
                        throw new DuplicateValueException();
                    }
                }
            }

            if (suffixData.hasBackend()) {
                backendKey = suffixData.getBackend();
            }

            zone = prefixTail + "." + suffix;
        }

        if (spec.backend != null) {
            if (backendKey != null && !backendKey.equals(spec.backend)) {
                throw new IllegalArgumentException();
            }
        }

        if (backendKey == null) {
            if (spec.backend != null) {
                backendKey = spec.backend;
            } else {
                // TODO: Cache ?
                for (BackendData backend : repository.getBackends().list()) {
                    if (backend.getDefault()) {
                        backendKey = backend.getKey();
                    }
                }
            }
        }

        DnsBackend backend = getBackend(backendKey);

        String backendId = backend.createZone(project, zoneName, zone, suffixData);

        DnsZoneData.Builder data = DnsZoneData.newBuilder();
        data.setProjectId(project.getId());
        data.setName(zoneName);
        if (backendKey != null) {
            data.setBackend(backendKey);
        }
        if (backendId != null) {
            data.setBackendId(backendId);
        }

        DnsZoneData created = repository.getDnsZones(project.getId()).create(data);

        DnsZone dnsDomain = new DnsZone(created);
        backend.updateDomain(project, dnsDomain);
        return dnsDomain;
    }

    public DnsSuffixData findMaximalSuffix(String domain) throws CloudException {
        NamedItemCollection<DnsSuffixData> store = repository.getDnsSuffixes();

        domain = domain.toLowerCase();

        while (true) {
            if (Strings.isNullOrEmpty(domain)) {
                return null;
            }

            DnsSuffixData data = store.find(domain);
            if (data != null) {
                return data;
            }

            int dotIndex = domain.indexOf('.');
            domain = domain.substring(dotIndex + 1);
        }
    }

    private boolean isTld(String domain) throws CloudException {
        // TODO: Caching

        NamedItemCollection<DnsSuffixData> store = repository.getDnsSuffixes();
        domain = domain.toLowerCase();

        DnsSuffixData data = store.find(domain);
        if (data != null) {
            return data.hasTld() && data.getTld();
        }

        return false;

        // tlds.add("com");
        // tlds.add("net");
        // tlds.add("org");
        // tlds.add("edu");
        // tlds.add("gov");
        // tlds.add("mil");
        // tlds.add("io");
        // tlds.add("co.uk");
        //
        // domain = domain.toLowerCase();
        // if (tlds.contains(domain)) {
        // return true;
        // }
        //
        // boolean found = false;
        // for (String tld : tlds) {
        // if (domain.endsWith("." + tld)) {
        // found = true;
        // break;
        // }
        // }
        //
        // if (!found) {
        // log.warn("Did not recognize TLD for: {}", domain);
        // }
        // return false;
    }

    // public String findHost(String fqdn, String domain) {
    // if (!fqdn.endsWith(domain)) {
    // throw new IllegalArgumentException();
    // }
    //
    // String host = fqdn.substring(0, fqdn.length() - domain.length());
    // // if (host.isEmpty()) {
    // // host = ".";
    // // }
    //
    // if (host.endsWith(".")) {
    // host = host.substring(0, host.length() - 1);
    // }
    // return host;
    // }

    @Override
    public DnsZone findZoneByName(Project project, String zoneName) throws CloudException {
        NumberedItemCollection<DnsZoneData> dnsDomains = repository.getDnsZones(project.getId());
        DnsZoneData data = dnsDomains.findByKey(zoneName);
        if (data == null) {
            return null;
        }
        return new DnsZone(data);
    }

    @Override
    public DnsZone findDomain(Project project, long id) throws CloudException {
        DnsZoneData data = repository.getDnsZones(project.getId()).find(id);
        if (data == null) {
            return null;
        }
        return new DnsZone(data);
    }

    @Override
    public Recordset findRecordset(Project project, Zone zone, long recordsetId) throws CloudException {
        DnsRecordsetData data = repository.getDnsRecordsets(project.getId(), zone.getId()).find(recordsetId);
        if (data == null) {
            return null;
        }
        return new DnsRecordset((DnsZone) zone, data);
    }

    // public DnsRecord findRecord(Project project, DnsDomain domain, String
    // name) throws CloudException {
    // long projectId = project.getId();
    // if (domain.getProjectId() != projectId) {
    // throw new IllegalArgumentException();
    // }
    //
    // DnsRecordData data = repository.getDnsRecords(projectId,
    // domain.getName()).find(name);
    // if (data == null) {
    // return null;
    // }
    // return new DnsRecord(domain, data);
    // }

    public DnsRecordset createRecordset(Project project, DnsZone zone, DnsRecordsetData.Builder data)
            throws CloudException {
        long projectId = project.getId();
        if (zone.getProjectId() != projectId) {
            throw new IllegalArgumentException();
        }

        data.setProjectId(projectId);

        DnsBackend backend = findBackend(zone);

        data.getStateBuilder().setCreatedAt(Clock.getTimestamp());

        DnsRecordsetData created = repository.getDnsRecordsets(projectId, zone.getId()).create(data);
        backend.updateDomain(project, zone);
        return new DnsRecordset(zone, created);
    }

    private DnsBackend findBackend(DnsZone zone) throws CloudException {
        String backendKey = null;
        if (zone.getData().hasBackend()) {
            backendKey = zone.getData().getBackend();
        }
        return getBackend(backendKey);
    }

    private DnsBackend getBackend(String backendKey) throws CloudException {
        if (backendKey == null) {
            return dnsBackends.getGenericBackend();
        }

        BackendData backend = repository.getBackends().find(backendKey);
        if (backend == null) {
            throw new IllegalArgumentException("Cannot find backend");
        }
        return dnsBackends.getBackend(backend);
    }

    String findDomain(String host) throws CloudException {
        int firstDot = host.indexOf('.');
        if (firstDot == -1) {
            log.warn("Suspicious domain; likely won't validate: " + host);
            return host;
        }

        // String prefix = host.substring(0, firstDot);
        String suffix = host.substring(firstDot + 1);
        if (isTld(suffix)) {
            return host;
        }

        return suffix;
    }

    // static class RecordImpl implements DnsService.Record {
    // private final String type;
    // private final String fqdn;
    // private final InetAddress ip;
    //
    // public RecordImpl(String type, String fqdn, InetAddress ip) {
    // this.type = type;
    // this.fqdn = fqdn;
    // this.ip = ip;
    // }
    //
    // }

    // @Override
    // public Record buildAddress(String fqdn, InetAddress ip) {
    // String type = (ip instanceof Inet6Address ? DnsService.TYPE_AAAA :
    // DnsService.TYPE_A);
    // return new RecordImpl(type, fqdn, ip);
    // }

    @Override
    public void setDnsRecordsets(String systemKey, Project project, List<DnsRecordsetSpec> recordsetSpecs)
            throws CloudException {
        List<DnsRecordsetData> recordsets = Lists.newArrayList();

        for (DnsRecordsetSpec recordsetSpec : recordsetSpecs) {
            // DnsRecord recordImpl = (DnsRecord) record;

            String domainName = findDomain(recordsetSpec.fqdn);
            // String name = findHost(recordset.fqdn, domainName);

            DnsZoneSpec zoneSpec = new DnsZoneSpec();
            zoneSpec.name = domainName;

            DnsZone zone = findOrCreateDomain(project, zoneSpec);

            DnsRecordsetData.Builder data = DnsRecordsetData.newBuilder();
            data.setProjectId(project.getId());

            for (DnsRecordSpec record : recordsetSpec.records) {
                // Sanity check
                InetAddress address = InetAddresses.forString(record.address);
                String ip = InetAddresses.toAddrString(address);

                DnsRecordData.Builder recordBuilder = data.addRecordBuilder();
                recordBuilder.setTarget(ip);
            }

            data.setType(recordsetSpec.type);
            data.setFqdn(recordsetSpec.fqdn);
            data.setZoneId(zone.getId());
            data.setSystemKey(systemKey);

            recordsets.add(data.build());
        }

        setDnsRecords(project, recordsets);
    }

    private void setDnsRecords(Project project, List<DnsRecordsetData> recordsets) throws CloudException {
        // This is where a database would be awesome!

        long projectId = project.getId();

        Set<Long> idsInUse = Sets.newHashSet();

        // TODO: This is inefficient
        Multimap<String, DnsRecordsetData> dbState = HashMultimap.create();
        for (DnsZoneData domain : repository.getDnsZones(projectId).list()) {
            for (DnsRecordsetData record : repository.getDnsRecordsets(projectId, domain.getId()).list()) {
                idsInUse.add(record.getId());

                if (!record.hasSystemKey()) {
                    continue;
                }
                dbState.put(record.getSystemKey(), record);
            }
        }

        ImmutableListMultimap<String, DnsRecordsetData> requestedState = Multimaps.index(recordsets,
                new Function<DnsRecordsetData, String>() {
                    @Override
                    public String apply(DnsRecordsetData input) {
                        return input.getSystemKey();
                    }
                });

        Set<Long> dirtyDomains = Sets.newHashSet();

        for (String systemKey : requestedState.keySet()) {
            Set<DnsRecordsetData> requested = Sets.newHashSet(requestedState.get(systemKey));

            Map<DnsRecordsetData, Long> current = Maps.newHashMap();

            for (DnsRecordsetData db : dbState.get(systemKey)) {
                long id = db.getId();

                DnsRecordsetData.Builder b = DnsRecordsetData.newBuilder(db);
                b.clearId();
                b.clearState();
                current.put(b.build(), id);
            }

            SetView<DnsRecordsetData> add = Sets.difference(requested, current.keySet());
            SetView<DnsRecordsetData> remove = Sets.difference(current.keySet(), requested);

            for (DnsRecordsetData a : add) {
                log.debug("Add: {}", a);
            }
            for (DnsRecordsetData r : remove) {
                log.debug("Remove: {}", r);
            }

            for (DnsRecordsetData a : add) {
                DnsRecordsetData.Builder b = DnsRecordsetData.newBuilder(a);
                repository.getDnsRecordsets(projectId, a.getZoneId()).create(b);

                dirtyDomains.add(b.getZoneId());
            }

            for (DnsRecordsetData r : remove) {
                long id = current.get(r);
                repository.getDnsRecordsets(projectId, r.getZoneId()).delete(id);
                dirtyDomains.add(r.getZoneId());
            }
        }

        for (Long domainId : dirtyDomains) {
            DnsZone zone = findDomain(project, domainId);

            DnsBackend backend = findBackend(zone);

            backend.updateDomain(project, zone);
        }
    }

    private DnsZone findOrCreateDomain(Project project, DnsZoneSpec zoneSpec) throws CloudException {
        DnsZone zone = findZoneByName(project, zoneSpec.name);

        if (zone == null) {
            try {
                zone = createZone(project, zoneSpec);
            } catch (DuplicateValueException e) {
                zone = findZoneByName(project, zoneSpec.name);
            }

            if (zone == null) {
                throw new IllegalStateException();
            }
        }

        return zone;
    }

    @Override
    public List<DnsRecordset> listRecordsets(Project project, Zone zone) throws CloudException {
        return listRecordsets(project, zone, new StoreOptions[0]);
    }

    public List<DnsRecordset> listRecordsets(Project project, Zone zone, StoreOptions... options) throws CloudException {
        long projectId = project.getId();
        if (zone.getProjectId() != projectId) {
            throw new IllegalArgumentException();
        }
        List<DnsRecordset> records = Lists.newArrayList();
        for (DnsRecordsetData data : repository.getDnsRecordsets(projectId, zone.getId()).list(options)) {
            DnsRecordset record = new DnsRecordset((DnsZone) zone, data);
            records.add(record);
        }
        return records;
    }

    @Override
    public DnsRecordset createRecordset(Project project, Zone zone, String fqdn, String type, List<String> ips)
            throws CloudException {
        if (ips == null || ips.isEmpty()) {
            throw new IllegalArgumentException();
        }
        if (Strings.isNullOrEmpty(type)) {
            throw new IllegalArgumentException();
        }

        type = type.trim().toUpperCase();
        if (Strings.isNullOrEmpty(type)) {
            throw new IllegalArgumentException();
        }

        DnsRecordsetData.Builder data = DnsRecordsetData.newBuilder();
        data.setZoneId(zone.getId());
        data.setFqdn(fqdn);
        data.setType(type);

        for (String ip : ips) {
            // Sanity check
            InetAddress address = InetAddresses.forString(ip);
            ip = InetAddresses.toAddrString(address);

            DnsRecordData.Builder recordBuilder = data.addRecordBuilder();
            recordBuilder.setTarget(ip);
        }

        data.setProjectId(project.getId());

        return createRecordset(project, (DnsZone) zone, data);
    }

    // private DnsRecordset toRecord(DnsZone zone, DnsRecordsetData data) {
    // if (data == null) {
    // return null;
    // }
    // return new DnsRecordset(zone, data);
    // }

    @Override
    public void start() throws Exception {
        wellKnownTlds.create();
    }

    public List<DnsSuffixData> listSuffix() throws CloudException {
        NamedItemCollection<DnsSuffixData> store = repository.getDnsSuffixes();
        return store.list();
    }

    public DnsSuffixData ensureTld(String tld) throws CloudException, DuplicateValueException {
        NamedItemCollection<DnsSuffixData> store = repository.getDnsSuffixes();

        tld = tld.trim();
        while (tld.startsWith(".")) {
            tld = tld.substring(1);
        }
        tld = tld.toLowerCase();

        DnsSuffixData data = store.find(tld);
        if (data != null) {
            if (!data.getTld()) {
                throw new IllegalArgumentException("Suffix is not marked as TLD: " + tld);
            }
            return data;
        }

        DnsSuffixData.Builder b = DnsSuffixData.newBuilder();
        b.setKey(tld);
        b.setTld(true);
        return store.create(b);
    }

    public DnsSuffixData createShared(Zone zone) throws CloudException, DuplicateValueException {
        DnsZoneData data = ((DnsZone) zone).getData();

        NamedItemCollection<DnsSuffixData> store = repository.getDnsSuffixes();
        String key = zone.getName();
        key = key.toLowerCase();

        DnsSuffixData.Builder b = DnsSuffixData.newBuilder();
        b.setKey(key);
        b.setSharedDomain(true);
        b.setOwnerProject(zone.getProjectId());
        if (data.hasBackend()) {
            b.setBackend(data.getBackend());
        }
        return store.create(b);
    }

    public DnsSuffixData deleteTld(String tld) throws CloudException {
        NamedItemCollection<DnsSuffixData> store = repository.getDnsSuffixes();
        DnsSuffixData ret = store.find(tld);

        store.delete(tld);

        return ret;
    }

    public boolean deleteZone(Project project, DnsZone zone) throws CloudException {
        // TODO: Verify no children
        // TODO: Delete from backend?

        NumberedItemCollection<DnsZoneData> store = repository.getDnsZones(project.getId());
        DnsZoneData found = store.find(zone.getId());
        if (found == null) {
            return false;
        }
        store.delete(zone.getId());
        return true;
    }

    public boolean deleteRecordset(Project project, DnsZone zone, long recordsetId) throws CloudException {
        NumberedItemCollection<DnsRecordsetData> store = repository.getDnsRecordsets(project.getId(), zone.getId());

        DnsRecordsetData found = store.find(recordsetId);
        if (found == null) {
            return false;
        }

        DnsBackend backend = findBackend(zone);

        store.delete(recordsetId);

        backend.updateDomain(project, zone);

        return true;
    }

    public DnsZone findMaximalZone(Project project, String name) throws CloudException {
        NumberedItemCollection<DnsZoneData> store = repository.getDnsZones(project.getId());

        name = name.toLowerCase();

        while (true) {
            if (Strings.isNullOrEmpty(name)) {
                return null;
            }

            DnsZoneData data = store.findByKey(name);
            if (data != null) {
                return new DnsZone(data);
            }

            int dotIndex = name.indexOf('.');
            if (dotIndex == -1) {
                return null;
            }

            name = name.substring(dotIndex + 1);
        }
    }

}
TOP

Related Classes of io.fathom.cloud.dns.services.DnsServiceImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.