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.util.ArrayList;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.Properties;
028
029import javax.naming.NamingException;
030import javax.naming.directory.Attribute;
031import javax.naming.directory.Attributes;
032import javax.naming.directory.DirContext;
033import javax.naming.directory.InitialDirContext;
034
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.nuxeo.runtime.api.Framework;
038
039/**
040 * Utility class to perform DNS lookups for services.
041 */
042public class DNSServiceResolverImpl implements DNSServiceResolver {
043
044    public static final Log log = LogFactory.getLog(DNSServiceResolverImpl.class);
045
046    protected static DNSServiceResolver instance;
047
048    protected static final String SRV_RECORD = "SRV";
049
050    /**
051     * Create a cache to hold the at most 100 recent DNS lookups for a period of 10 minutes.
052     */
053    protected Map<String, List<DNSServiceEntry>> cache = new HashMap<>();
054
055    protected long lastCacheUpdate = System.currentTimeMillis();
056
057    protected final long maxDelay;
058
059    protected static DirContext context;
060
061    public static synchronized DNSServiceResolver getInstance() {
062        if (instance == null) {
063            instance = new DNSServiceResolverImpl();
064        }
065        return instance;
066    }
067
068    protected DNSServiceResolverImpl() {
069        /*
070         * The expiry of the cache in minutes
071         */
072        int cacheExpiry = 10;
073        try {
074            cacheExpiry = Integer.parseInt(Framework.getProperty(DNS_CACHE_EXPIRY, "10"));
075        } catch (NumberFormatException e) {
076            log.warn("invalid value for property: " + DNS_CACHE_EXPIRY
077                    + ", falling back to default value of 10 minutes");
078        }
079        maxDelay = 1000 * 60 * cacheExpiry;
080
081        Properties env = new Properties();
082        env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
083        try {
084            context = new InitialDirContext(env);
085        } catch (NamingException e) {
086            throw new RuntimeException(e);
087        }
088    }
089
090    /**
091     * Returns the host name and port that a server providing the specified service can be reached at. A DNS lookup for
092     * a SRV record in the form "_service.example.com" is attempted.
093     * <p>
094     * As an example, a lookup for "example.com" for the service _gc._tcp may return "dc01.example.com:3268".
095     *
096     * @param service the service.
097     * @param domain the domain.
098     * @return a List of DNSServiceEntrys, which encompasses the hostname and port that the server can be reached at for
099     *         the specified domain.
100     * @throws NamingException if the DNS server is unreachable
101     */
102    protected List<DNSServiceEntry> resolveDnsServiceRecord(final String service, final String domain)
103            throws NamingException {
104        List<DNSServiceEntry> addresses = new ArrayList<>();
105
106        if (context == null) {
107            return addresses;
108        }
109
110        final String key = service + "." + domain;
111        /*
112         * Return item from cache if it exists.
113         */
114        if (System.currentTimeMillis() - lastCacheUpdate > maxDelay) {
115            cache.clear();
116        }
117        if (cache.containsKey(key)) {
118            List<DNSServiceEntry> cachedAddresses = cache.get(key);
119            if (cachedAddresses != null) {
120                return cachedAddresses;
121            }
122        }
123
124        Attributes dnsLookup = context.getAttributes(service + "." + domain, new String[] { SRV_RECORD });
125
126        Attribute attribute = dnsLookup.get(SRV_RECORD);
127        for (int i = 0; i < attribute.size(); i++) {
128            /*
129             * Get the current resource record
130             */
131            String entry = (String) attribute.get(i);
132
133            String[] records = entry.split(" ");
134            String host = records[records.length - 1];
135            int port = Integer.parseInt(records[records.length - 2]);
136            int weight = Integer.parseInt(records[records.length - 3]);
137            int priority = Integer.parseInt(records[records.length - 4]);
138
139            /*
140             * possible to get TTL?
141             */
142
143            /*
144             * Host entries in DNS should end with a "."
145             */
146            if (host.endsWith(".")) {
147                host = host.substring(0, host.length() - 1);
148            }
149
150            addresses.add(new DNSServiceEntry(host, port, priority, weight));
151        }
152
153        /*
154         * Sort the addresses by DNS priority and weight settings
155         */
156        Collections.sort(addresses);
157
158        /*
159         * Add item to cache.
160         */
161        if (cache.size() > 100) {
162            cache.clear();
163        }
164        cache.put(key, addresses);
165        lastCacheUpdate = System.currentTimeMillis();
166        return addresses;
167    }
168
169    @Override
170    public List<DNSServiceEntry> resolveLDAPDomainServers(final String domain) throws NamingException {
171        return resolveDnsServiceRecord(LDAP_SERVICE_PREFIX, domain);
172    }
173
174    @Override
175    public List<DNSServiceEntry> resolveLDAPDomainServers(final String domain, final String prefix)
176            throws NamingException {
177        return resolveDnsServiceRecord(prefix, domain);
178    }
179
180}