001/*
002 * (C) Copyright 2010-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Robert Browning - initial implementation
018 *     Nuxeo - code review and integration
019 */
020package org.nuxeo.ecm.directory.ldap.dns;
021
022import java.time.Duration;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.Properties;
029
030import javax.naming.NamingException;
031import javax.naming.directory.Attribute;
032import javax.naming.directory.Attributes;
033import javax.naming.directory.DirContext;
034import javax.naming.directory.InitialDirContext;
035
036import org.apache.commons.logging.Log;
037import org.apache.commons.logging.LogFactory;
038import org.nuxeo.runtime.api.Framework;
039
040/**
041 * Utility class to perform DNS lookups for services.
042 */
043public class DNSServiceResolverImpl implements DNSServiceResolver {
044
045    public static final Log log = LogFactory.getLog(DNSServiceResolverImpl.class);
046
047    protected static DNSServiceResolver instance;
048
049    protected static final String SRV_RECORD = "SRV";
050
051    /**
052     * Create a cache to hold the at most 100 recent DNS lookups for a period of 10 minutes.
053     */
054    protected Map<String, List<DNSServiceEntry>> cache = new HashMap<>();
055
056    protected long lastCacheUpdate = System.currentTimeMillis();
057
058    protected final long maxDelay;
059
060    protected final DirContext context;
061
062    public static synchronized DNSServiceResolver getInstance() {
063        if (instance == null) {
064            instance = new DNSServiceResolverImpl();
065        }
066        return instance;
067    }
068
069    protected DNSServiceResolverImpl() {
070        /*
071         * The expiry of the cache in minutes
072         */
073        int cacheExpiry = 10;
074        try {
075            cacheExpiry = Integer.parseInt(Framework.getProperty(DNS_CACHE_EXPIRY, "10"));
076        } catch (NumberFormatException e) {
077            log.warn("invalid value for property: " + DNS_CACHE_EXPIRY
078                    + ", falling back to default value of 10 minutes");
079        }
080        maxDelay = Duration.ofMinutes(cacheExpiry).toMillis();
081
082        Properties env = new Properties();
083        env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
084        try {
085            context = new InitialDirContext(env);
086        } catch (NamingException e) {
087            throw new RuntimeException(e);
088        }
089    }
090
091    /**
092     * Returns the host name and port that a server providing the specified service can be reached at. A DNS lookup for
093     * a SRV record in the form "_service.example.com" is attempted.
094     * <p>
095     * As an example, a lookup for "example.com" for the service _gc._tcp may return "dc01.example.com:3268".
096     *
097     * @param service the service.
098     * @param domain the domain.
099     * @return a List of DNSServiceEntrys, which encompasses the hostname and port that the server can be reached at for
100     *         the specified domain.
101     * @throws NamingException if the DNS server is unreachable
102     */
103    protected List<DNSServiceEntry> resolveDnsServiceRecord(final String service, final String domain)
104            throws NamingException {
105        List<DNSServiceEntry> addresses = new ArrayList<>();
106
107        if (context == null) {
108            return addresses;
109        }
110
111        final String key = service + "." + domain;
112        /*
113         * Return item from cache if it exists.
114         */
115        if (System.currentTimeMillis() - lastCacheUpdate > maxDelay) {
116            cache.clear();
117        }
118        if (cache.containsKey(key)) {
119            List<DNSServiceEntry> cachedAddresses = cache.get(key);
120            if (cachedAddresses != null) {
121                return cachedAddresses;
122            }
123        }
124
125        Attributes dnsLookup = context.getAttributes(service + "." + domain, new String[] { SRV_RECORD });
126
127        Attribute attribute = dnsLookup.get(SRV_RECORD);
128        for (int i = 0; i < attribute.size(); i++) {
129            /*
130             * Get the current resource record
131             */
132            String entry = (String) attribute.get(i);
133
134            String[] records = entry.split(" ");
135            String host = records[records.length - 1];
136            int port = Integer.parseInt(records[records.length - 2]);
137            int weight = Integer.parseInt(records[records.length - 3]);
138            int priority = Integer.parseInt(records[records.length - 4]);
139
140            /*
141             * possible to get TTL?
142             */
143
144            /*
145             * Host entries in DNS should end with a "."
146             */
147            if (host.endsWith(".")) {
148                host = host.substring(0, host.length() - 1);
149            }
150
151            addresses.add(new DNSServiceEntry(host, port, priority, weight));
152        }
153
154        /*
155         * Sort the addresses by DNS priority and weight settings
156         */
157        Collections.sort(addresses);
158
159        /*
160         * Add item to cache.
161         */
162        if (cache.size() > 100) {
163            cache.clear();
164        }
165        cache.put(key, addresses);
166        lastCacheUpdate = System.currentTimeMillis();
167        return addresses;
168    }
169
170    @Override
171    public List<DNSServiceEntry> resolveLDAPDomainServers(final String domain) throws NamingException {
172        return resolveDnsServiceRecord(LDAP_SERVICE_PREFIX, domain);
173    }
174
175    @Override
176    public List<DNSServiceEntry> resolveLDAPDomainServers(final String domain, final String prefix)
177            throws NamingException {
178        return resolveDnsServiceRecord(prefix, domain);
179    }
180
181}