DNSUtil.java 13.2 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile: DNSUtil.java,v $
 * $Revision: 2867 $
 * $Date: 2005-09-22 03:40:04 -0300 (Thu, 22 Sep 2005) $
 *
6
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
7
 *
8 9 10 11 12 13 14 15 16 17 18
 * 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.
19 20
 */

21
package org.jivesoftware.openfire.net;
22

23
import org.eclipse.jetty.util.MultiMap;
24
import org.jivesoftware.util.JiveGlobals;
25 26
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
27

28
import javax.naming.NameNotFoundException;
29 30
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
31 32
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
33
import javax.naming.directory.InitialDirContext;
34

35
import java.io.Serializable;
36
import java.util.*;
37 38 39 40 41 42 43 44 45 46

/**
 * Utilty class to perform DNS lookups for XMPP services.
 *
 * @author Matt Tucker
 */
public class DNSUtil {

    private static DirContext context;

47
    private static final Logger logger = LoggerFactory.getLogger(DNSUtil.class);
48

49 50 51 52
    /**
     * Internal DNS that allows to specify target IP addresses and ports to use for domains.
     * The internal DNS will be checked up before performing an actual DNS SRV lookup.
     */
53
    private static Map<String, HostAddress> dnsOverride;
54

55 56 57 58 59
    static {
        try {
            Hashtable<String,String> env = new Hashtable<String,String>();
            env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
            context = new InitialDirContext(env);
60

61
            String property = JiveGlobals.getProperty("dnsutil.dnsOverride");
62
            if (property != null) {
63
                dnsOverride = decode(property);
64
            }
65 66
        }
        catch (Exception e) {
67
            logger.error("Can't initialize DNS context!", e);
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
        }
    }

    /**
     * Returns the host name and port that the specified XMPP server can be
     * reached at for server-to-server communication. A DNS lookup for a SRV
     * record in the form "_xmpp-server._tcp.example.com" is attempted, according
     * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form
     * of "_jabber._tcp.example.com" is attempted since servers that implement an
     * older version of the protocol may be listed using that notation. If that
     * lookup fails as well, it's assumed that the XMPP server lives at the
     * host resolved by a DNS lookup at the specified domain on the specified default port.<p>
     *
     * As an example, a lookup for "example.com" may return "im.example.com:5269".
     *
     * @param domain the domain.
     * @param defaultPort default port to return if the DNS look up fails.
     * @return a HostAddress, which encompasses the hostname and port that the XMPP
     *      server can be reached at for the specified domain.
87 88
     * @deprecated replaced with support for multiple srv records, see 
     *      {@link #resolveXMPPDomain(String, int)}
89
     */
90
    @Deprecated
91
    public static HostAddress resolveXMPPServerDomain(String domain, int defaultPort) {
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
        return resolveXMPPDomain(domain, defaultPort).get(0);
    }

    /**
     * Returns a sorted list of host names and ports that the specified XMPP domain
     * can be reached at for server-to-server communication. A DNS lookup for a SRV
     * record in the form "_xmpp-server._tcp.example.com" is attempted, according
     * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form
     * of "_jabber._tcp.example.com" is attempted since servers that implement an
     * older version of the protocol may be listed using that notation. If that
     * lookup fails as well, it's assumed that the XMPP server lives at the
     * host resolved by a DNS lookup at the specified domain on the specified default port.<p>
     *
     * As an example, a lookup for "example.com" may return "im.example.com:5269".
     *
     * @param domain the domain.
     * @param defaultPort default port to return if the DNS look up fails.
     * @return a list of  HostAddresses, which encompasses the hostname and port that the XMPP
     *      server can be reached at for the specified domain.
     */
    public static List<HostAddress> resolveXMPPDomain(String domain, int defaultPort) {
113
        // Check if there is an entry in the internal DNS for the specified domain
114
        List<HostAddress> results = new LinkedList<HostAddress>();
115 116
        if (dnsOverride != null) {
            HostAddress hostAddress = dnsOverride.get(domain);
117
            if (hostAddress != null) {
118 119
                results.add(hostAddress);
                return results;
120 121
            }
        }
122 123

        // Attempt the SRV lookup.
124 125 126
        results.addAll(srvLookup("_xmpp-server._tcp." + domain));
        if (results.isEmpty()) {
            results.addAll(srvLookup("_jabber._tcp." + domain));
127
        }
128 129

        // Use domain and default port as fallback.
130
        if (results.isEmpty()) {
131
            results.add(new HostAddress(domain, defaultPort));
132
        }
133
        return results;
134 135
    }

136 137 138 139 140 141 142 143
    /**
     * Returns the internal DNS that allows to specify target IP addresses and ports
     * to use for domains. The internal DNS will be checked up before performing an
     * actual DNS SRV lookup.
     *
     * @return the internal DNS that allows to specify target IP addresses and ports
     *         to use for domains.
     */
144 145
    public static Map<String, HostAddress> getDnsOverride() {
        return dnsOverride;
146 147 148 149 150 151 152
    }

    /**
     * Sets the internal DNS that allows to specify target IP addresses and ports
     * to use for domains. The internal DNS will be checked up before performing an
     * actual DNS SRV lookup.
     *
153
     * @param dnsOverride the internal DNS that allows to specify target IP addresses and ports
154 155
     *        to use for domains.
     */
156 157 158
    public static void setDnsOverride(Map<String, HostAddress> dnsOverride) {
        DNSUtil.dnsOverride = dnsOverride;
        JiveGlobals.setProperty("dnsutil.dnsOverride", encode(dnsOverride));
159 160 161 162 163 164 165 166 167 168 169
    }

    private static String encode(Map<String, HostAddress> internalDNS) {
        if (internalDNS == null) {
            return "";
        }
        StringBuilder sb = new StringBuilder(100);
        for (String key : internalDNS.keySet()) {
            if (sb.length() > 0) {
                sb.append(",");
            }
170 171 172
            sb.append("{").append(key).append(",");
            sb.append(internalDNS.get(key).getHost()).append(":");
            sb.append(internalDNS.get(key).getPort()).append("}");
173 174 175 176 177 178
        }
        return sb.toString();
    }

    private static Map<String, HostAddress> decode(String encodedValue) {
        Map<String, HostAddress> answer = new HashMap<String, HostAddress>();
179
        StringTokenizer st = new StringTokenizer(encodedValue, "{},:");
180 181 182 183 184 185 186
        while (st.hasMoreElements()) {
            String key = st.nextToken();
            answer.put(key, new HostAddress(st.nextToken(), Integer.parseInt(st.nextToken())));
        }
        return answer;
    }

187
    private static List<? extends HostAddress> srvLookup(String lookup) {
188 189
        if (lookup == null) {
            throw new NullPointerException("DNS lookup can't be null");
190
        }
191 192 193 194
        try {
            Attributes dnsLookup =
                    context.getAttributes(lookup, new String[]{"SRV"});
            Attribute srvRecords = dnsLookup.get("SRV");
guus's avatar
guus committed
195
            if (srvRecords == null) {
196 197
                logger.debug("No SRV record found for domain: " + lookup);
                return new ArrayList<HostAddress>();
guus's avatar
guus committed
198
            }
199
            WeightedHostAddress[] hosts = new WeightedHostAddress[srvRecords.size()];
200 201 202
            for (int i = 0; i < srvRecords.size(); i++) {
                hosts[i] = new WeightedHostAddress(((String)srvRecords.get(i)).split(" "));
            }
203 204

            return prioritize(hosts);
205
        }
206
        catch (NameNotFoundException e) {
207
            logger.debug("No SRV record found for: " + lookup, e);
208 209
        }
        catch (NamingException e) {
210
            logger.error("Can't process DNS lookup!", e);
211
        }
212
        return new ArrayList<HostAddress>();
213 214
    }

215 216 217 218 219
    /**
     * Encapsulates a hostname and port.
     */
    public static class HostAddress {

220 221
        private final String host;
        private final int port;
222 223

        private HostAddress(String host, int port) {
224 225 226 227 228 229 230
            // Host entries in DNS should end with a ".".
            if (host.endsWith(".")) {
                this.host = host.substring(0, host.length()-1);
            }
            else {
                this.host = host;
            }
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
            this.port = port;
        }

        /**
         * Returns the hostname.
         *
         * @return the hostname.
         */
        public String getHost() {
            return host;
        }

        /**
         * Returns the port.
         *
         * @return the port.
         */
        public int getPort() {
            return port;
        }

252
        @Override
253
        public String toString() {
254 255 256
            return host + ":" + port;
        }
    }
257

258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    public static List<WeightedHostAddress> prioritize(WeightedHostAddress[] records) {
        final List<WeightedHostAddress> result = new LinkedList<WeightedHostAddress>();

        // sort by priority (ascending)
        SortedMap<Integer, Set<WeightedHostAddress>> byPriority = new TreeMap<Integer, Set<WeightedHostAddress>>();
        for(final WeightedHostAddress record : records) {
            if (byPriority.containsKey(record.getPriority())) {
                byPriority.get(record.getPriority()).add(record);
            } else {
                final Set<WeightedHostAddress> set = new HashSet<WeightedHostAddress>();
                set.add(record);
                byPriority.put(record.getPriority(), set);
            }
        }

        // now, randomize each priority set by weight.
        for(Map.Entry<Integer, Set<WeightedHostAddress>> weights : byPriority.entrySet()) {

            List<WeightedHostAddress> zeroWeights = new LinkedList<WeightedHostAddress>();

            int totalWeight = 0;
            final Iterator<WeightedHostAddress> i = weights.getValue().iterator();
            while (i.hasNext()) {
                final WeightedHostAddress next = i.next();
                if (next.weight == 0) {
                    // set aside, as these should be considered last according to the RFC.
                    zeroWeights.add(next);
                    i.remove();
                    continue;
                }

                totalWeight += next.getWeight();
            }

            int iterationWeight = totalWeight;
            Iterator<WeightedHostAddress> iter = weights.getValue().iterator();
            while (iter.hasNext()) {
                int needle = new Random().nextInt(iterationWeight);

                while (true) {
                    final WeightedHostAddress record = iter.next();
                    needle -= record.getWeight();
                    if (needle <= 0) {
                        result.add(record);
                        iter.remove();
                        iterationWeight -= record.getWeight();
                        break;
                    }
                }
                iter = weights.getValue().iterator();
            }

            // finally, append the hosts with zero priority (shuffled)
            Collections.shuffle(zeroWeights);
            for(WeightedHostAddress zero : zeroWeights) {
                result.add(zero);
            }
        }

        return result;
    }
319 320 321 322 323
    /**
     * The representation of weighted address.
     */
    public static class WeightedHostAddress extends HostAddress {

324 325
        private final int priority;
        private final int weight;
326 327

        private WeightedHostAddress(String [] srvRecordEntries) {
328
            super(srvRecordEntries[srvRecordEntries.length-1],
329 330 331 332 333
                    Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]));
            weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-3]);
            priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-4]);
        }

334
        WeightedHostAddress(String host, int port, int priority, int weight) {
335 336 337 338 339 340 341
            super(host, port);
            this.priority = priority;
            this.weight = weight;
        }

        /**
         * Returns the priority.
342
         *
343 344 345 346 347 348 349 350
         * @return the priority.
         */
        public int getPriority() {
            return priority;
        }

        /**
         * Returns the weight.
351
         *
352 353 354 355 356 357
         * @return the weight.
         */
        public int getWeight() {
            return weight;
        }
    }
358
}