![JAR search and dependency download from the Maven repository](/logo.png)
org.jivesoftware.openfire.net.DNSUtil Maven / Gradle / Ivy
/*
* Copyright (C) 2004-2008 Jive Software. All rights reserved.
*
* 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.
*/
package org.jivesoftware.openfire.net;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.*;
/**
* Utility class to perform DNS lookups for XMPP services.
*
* @author Matt Tucker
*/
public class DNSUtil {
private static DirContext context;
private static final Logger logger = LoggerFactory.getLogger(DNSUtil.class);
/**
* 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.
*/
private static Map dnsOverride;
static {
try {
Hashtable env = new Hashtable<>();
env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
context = new InitialDirContext(env);
String property = JiveGlobals.getProperty("dnsutil.dnsOverride");
if (property != null) {
dnsOverride = decode(property);
}
}
catch (Exception e) {
logger.error("Can't initialize DNS context!", e);
}
}
/**
* 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.
*
* 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 resolveXMPPDomain(String domain, int defaultPort) {
// Check if there is an entry in the internal DNS for the specified domain
List results = new LinkedList<>();
if (dnsOverride != null) {
HostAddress hostAddress = dnsOverride.get(domain);
if (hostAddress == null) {
hostAddress = dnsOverride.get("*");
}
if (hostAddress != null) {
results.add(hostAddress);
return results;
}
}
// Attempt the SRV lookup.
results.addAll(srvLookup("xmpp-server", "tcp", domain ) );
if (results.isEmpty()) {
results.addAll(srvLookup( "jabber", "tcp", domain ) );
}
// Use domain and default port as fallback.
if (results.isEmpty()) {
results.add(new HostAddress(domain, defaultPort));
}
return results;
}
/**
* 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.
*/
public static Map getDnsOverride() {
return dnsOverride;
}
/**
* 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.
*
* @param dnsOverride the internal DNS that allows to specify target IP addresses and ports
* to use for domains.
*/
public static void setDnsOverride(Map dnsOverride) {
DNSUtil.dnsOverride = dnsOverride;
JiveGlobals.setProperty("dnsutil.dnsOverride", encode(dnsOverride));
}
private static String encode(Map internalDNS) {
if (internalDNS == null) {
return "";
}
StringBuilder sb = new StringBuilder(100);
for (String key : internalDNS.keySet()) {
if (sb.length() > 0) {
sb.append(',');
}
sb.append('{').append(key).append(',');
sb.append(internalDNS.get(key).getHost()).append(':');
sb.append(internalDNS.get(key).getPort()).append('}');
}
return sb.toString();
}
private static Map decode(String encodedValue) {
Map answer = new HashMap<>();
StringTokenizer st = new StringTokenizer(encodedValue, "{},:");
while (st.hasMoreElements()) {
String key = st.nextToken();
answer.put(key, new HostAddress(st.nextToken(), Integer.parseInt(st.nextToken())));
}
return answer;
}
/**
* Performs a DNS SRV lookup. Does not take into account any DNS overrides configured in this class.
*
* The results returned by this method are ordered by priority (ascending), and order of equal priority entries is
* randomized by weight, as defined in the DNS SRV specification.
*
* @param service the symbolic name of the desired service (cannot be null).
* @param proto the transport protocol of the desired service; this is usually either TCP or UDP (cannot be null).
* @param name the domain name for which this record is valid (cannot be null).
* @return An ordered of results (possibly empty, never null).
*/
public static List srvLookup(String service, String proto, String name) {
if (service == null || proto == null || name == null) {
throw new NullPointerException("DNS lookup can't be null");
}
if ( !service.startsWith( "_" ) )
{
service = "_" + service;
}
if ( !service.endsWith( "." ) )
{
service = service + ".";
}
if ( !proto.startsWith( "_" ) )
{
proto = "_" + proto;
}
if ( !proto.endsWith( "." ) )
{
proto = proto+ ".";
}
if ( !name.endsWith( "." ) ) {
name = name + ".";
}
// _service._proto.name.
final String lookup = (service + proto + name).toLowerCase();
try {
Attributes dnsLookup =
context.getAttributes(lookup, new String[]{"SRV"});
Attribute srvRecords = dnsLookup.get("SRV");
if (srvRecords == null) {
logger.debug("No SRV record found for domain: " + lookup);
return Collections.emptyList();
}
WeightedHostAddress[] hosts = new WeightedHostAddress[srvRecords.size()];
for (int i = 0; i < srvRecords.size(); i++) {
hosts[i] = new WeightedHostAddress(((String)srvRecords.get(i)).split(" "));
}
return prioritize(hosts);
}
catch (NameNotFoundException e) {
logger.debug("No SRV record found for: " + lookup, e);
}
catch (NamingException e) {
logger.error("Can't process DNS lookup!", e);
}
return Collections.emptyList();
}
/**
* Checks if the provided DNS pattern matches the provided name. For example, this method will:
* return true for name: xmpp.example.org, pattern: *.example.org
* return false for name: xmpp.example.org, pattern: example.org
*
* This method is not case sensitive.
*
* @param name The name to check against a pattern (cannot be null or empty).
* @param pattern the pattern (cannot be null or empty).
* @return true when the name is covered by the pattern, otherwise false.
*/
public static boolean isNameCoveredByPattern( String name, String pattern )
{
if ( name == null || name.isEmpty() || pattern == null || pattern.isEmpty() )
{
throw new IllegalArgumentException( "Arguments cannot be null or empty." );
}
final String needle = name.toLowerCase();
final String hayStack = pattern.toLowerCase();
if ( needle.equals( hayStack )) {
return true;
}
if ( hayStack.startsWith( "*." ) ) {
return needle.endsWith( hayStack.substring( 2 ) );
}
return false;
}
/**
* Encapsulates a hostname and port.
*/
public static class HostAddress {
private final String host;
private final int port;
private HostAddress(String host, int port) {
// Host entries in DNS should end with a ".".
if (host.endsWith(".")) {
this.host = host.substring(0, host.length()-1);
}
else {
this.host = host;
}
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;
}
@Override
public String toString() {
return host + ":" + port;
}
}
public static List prioritize(WeightedHostAddress[] records) {
final List result = new LinkedList<>();
// sort by priority (ascending)
SortedMap> byPriority = new TreeMap<>();
for(final WeightedHostAddress record : records) {
if (byPriority.containsKey(record.getPriority())) {
byPriority.get(record.getPriority()).add(record);
} else {
final Set set = new HashSet<>();
set.add(record);
byPriority.put(record.getPriority(), set);
}
}
// now, randomize each priority set by weight.
for(Map.Entry> weights : byPriority.entrySet()) {
List zeroWeights = new LinkedList<>();
int totalWeight = 0;
final Iterator 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 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;
}
/**
* The representation of weighted address.
*/
public static class WeightedHostAddress extends HostAddress {
private final int priority;
private final int weight;
private WeightedHostAddress(String [] srvRecordEntries) {
super(srvRecordEntries[srvRecordEntries.length-1],
Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]));
weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-3]);
priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-4]);
}
WeightedHostAddress(String host, int port, int priority, int weight) {
super(host, port);
this.priority = priority;
this.weight = weight;
}
/**
* Returns the priority.
*
* @return the priority.
*/
public int getPriority() {
return priority;
}
/**
* Returns the weight.
*
* @return the weight.
*/
public int getWeight() {
return weight;
}
}
}