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