diff --git a/build/eclipse/classpath b/build/eclipse/classpath index ddf552712..67dd4e3b5 100644 --- a/build/eclipse/classpath +++ b/build/eclipse/classpath @@ -28,5 +28,6 @@ + diff --git a/build/merge/org.xbill.dns_2.1.4.jar b/build/merge/org.xbill.dns_2.1.4.jar new file mode 100644 index 000000000..ca8c76c81 Binary files /dev/null and b/build/merge/org.xbill.dns_2.1.4.jar differ diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index 6c4b891e2..1c763e0c2 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -5,15 +5,16 @@ org.jivesoftware.smackx.ServiceDiscoveryManager - org.jivesoftware.smack.PrivacyListManager + org.jivesoftware.smack.PrivacyListManager org.jivesoftware.smackx.XHTMLManager org.jivesoftware.smackx.muc.MultiUserChat org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager - org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager + org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager org.jivesoftware.smackx.filetransfer.FileTransferManager org.jivesoftware.smackx.LastActivityManager - org.jivesoftware.smack.ReconnectionManager - org.jivesoftware.smackx.commands.AdHocCommandManager + org.jivesoftware.smack.ReconnectionManager + org.jivesoftware.smackx.commands.AdHocCommandManager + org.jivesoftware.smack.util.dns.JavaxResolver diff --git a/source/org/jivesoftware/smack/ConnectionConfiguration.java b/source/org/jivesoftware/smack/ConnectionConfiguration.java index 24389ac6f..9eb7bb903 100644 --- a/source/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/source/org/jivesoftware/smack/ConnectionConfiguration.java @@ -22,11 +22,15 @@ package org.jivesoftware.smack; import org.jivesoftware.smack.proxy.ProxyInfo; import org.jivesoftware.smack.util.DNSUtil; +import org.jivesoftware.smack.util.dns.HostAddress; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.security.auth.callback.CallbackHandler; import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; /** * Configuration to use while establishing the connection to the server. It is possible to @@ -48,6 +52,7 @@ public class ConnectionConfiguration implements Cloneable { private String host; private int port; + protected List hostAddresses; private String truststorePath; private String truststoreType; @@ -98,12 +103,11 @@ public class ConnectionConfiguration implements Cloneable { */ public ConnectionConfiguration(String serviceName) { // Perform DNS lookup to get host and port to use - DNSUtil.HostAddress address = DNSUtil.resolveXMPPDomain(serviceName); - init(address.getHost(), address.getPort(), serviceName, - ProxyInfo.forDefaultProxy()); + hostAddresses = DNSUtil.resolveXMPPDomain(serviceName); + init(serviceName, ProxyInfo.forDefaultProxy()); } - - /** + + /** * Creates a new ConnectionConfiguration for the specified service name * with specified proxy. * A DNS SRV lookup will be performed to find out the actual host address @@ -114,8 +118,8 @@ public class ConnectionConfiguration implements Cloneable { */ public ConnectionConfiguration(String serviceName,ProxyInfo proxy) { // Perform DNS lookup to get host and port to use - DNSUtil.HostAddress address = DNSUtil.resolveXMPPDomain(serviceName); - init(address.getHost(), address.getPort(), serviceName, proxy); + hostAddresses = DNSUtil.resolveXMPPDomain(serviceName); + init(serviceName, proxy); } /** @@ -133,7 +137,8 @@ public class ConnectionConfiguration implements Cloneable { * @param serviceName the name of the service provided by an XMPP server. */ public ConnectionConfiguration(String host, int port, String serviceName) { - init(host, port, serviceName, ProxyInfo.forDefaultProxy()); + initHostAddresses(host, port); + init(serviceName, ProxyInfo.forDefaultProxy()); } /** @@ -152,7 +157,8 @@ public class ConnectionConfiguration implements Cloneable { * @param proxy the proxy through which XMPP is to be connected */ public ConnectionConfiguration(String host, int port, String serviceName, ProxyInfo proxy) { - init(host, port, serviceName, proxy); + initHostAddresses(host, port); + init(serviceName, proxy); } /** @@ -163,7 +169,8 @@ public class ConnectionConfiguration implements Cloneable { * @param port the port where the XMPP is listening. */ public ConnectionConfiguration(String host, int port) { - init(host, port, host, ProxyInfo.forDefaultProxy()); + initHostAddresses(host, port); + init(host, ProxyInfo.forDefaultProxy()); } /** @@ -175,12 +182,11 @@ public class ConnectionConfiguration implements Cloneable { * @param proxy the proxy through which XMPP is to be connected */ public ConnectionConfiguration(String host, int port, ProxyInfo proxy) { - init(host, port, host, proxy); + initHostAddresses(host, port); + init(host, proxy); } - private void init(String host, int port, String serviceName, ProxyInfo proxy) { - this.host = host; - this.port = port; + protected void init(String serviceName, ProxyInfo proxy) { this.serviceName = serviceName; this.proxy = proxy; @@ -243,6 +249,11 @@ public class ConnectionConfiguration implements Cloneable { return port; } + public void setUsedHostAddress(HostAddress hostAddress) { + this.host = hostAddress.getFQDN(); + this.port = hostAddress.getPort(); + } + /** * Returns the TLS security mode used when making the connection. By default, * the mode is {@link SecurityMode#enabled}. @@ -674,6 +685,10 @@ public class ConnectionConfiguration implements Cloneable { return this.socketFactory; } + public List getHostAddresses() { + return Collections.unmodifiableList(hostAddresses); + } + /** * An enumeration for TLS security modes that are available when making a connection * to the XMPP server. @@ -742,4 +757,15 @@ public class ConnectionConfiguration implements Cloneable { this.password = password; this.resource = resource; } + + private void initHostAddresses(String host, int port) { + hostAddresses = new ArrayList(1); + HostAddress hostAddress; + try { + hostAddress = new HostAddress(host, port); + } catch (Exception e) { + throw new IllegalStateException(e); + } + hostAddresses.add(hostAddress); + } } diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index 4024bd340..af7b66750 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -26,6 +26,7 @@ import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.util.StringUtils; +import org.jivesoftware.smack.util.dns.HostAddress; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; @@ -51,6 +52,9 @@ import java.security.KeyStore; import java.security.Provider; import java.security.Security; import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; /** * Creates a socket connection to a XMPP server. This is the default connection @@ -546,27 +550,57 @@ public class XMPPConnection extends Connection { } private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException { - String host = config.getHost(); - int port = config.getPort(); - try { - if (config.getSocketFactory() == null) { - this.socket = new Socket(host, port); + XMPPException exception = null; + Iterator it = config.getHostAddresses().iterator(); + List failedAddresses = new LinkedList(); + boolean xmppIOError = false; + while (it.hasNext()) { + exception = null; + HostAddress hostAddress = it.next(); + String host = hostAddress.getFQDN(); + int port = hostAddress.getPort(); + try { + if (config.getSocketFactory() == null) { + this.socket = new Socket(host, port); + } + else { + this.socket = config.getSocketFactory().createSocket(host, port); + } + } catch (UnknownHostException uhe) { + String errorMessage = "Could not connect to " + host + ":" + port + "."; + exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_timeout, + errorMessage), uhe); + } catch (IOException ioe) { + String errorMessage = "XMPPError connecting to " + host + ":" + port + "."; + exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_error, + errorMessage), ioe); + xmppIOError = true; } - else { - this.socket = config.getSocketFactory().createSocket(host, port); + if (exception == null) { + // We found a host to connect to, break here + config.setUsedHostAddress(hostAddress); + break; + } + hostAddress.setException(exception); + failedAddresses.add(hostAddress); + if (!it.hasNext()) { + // There are no more host addresses to try + // throw an exception and report all tried + // HostAddresses in the exception + StringBuilder sb = new StringBuilder(); + for (HostAddress fha : failedAddresses) { + sb.append(fha.getErrorMessage()); + sb.append("; "); + } + XMPPError xmppError; + if (xmppIOError) { + xmppError = new XMPPError(XMPPError.Condition.remote_server_error); + } + else { + xmppError = new XMPPError(XMPPError.Condition.remote_server_timeout); + } + throw new XMPPException(sb.toString(), xmppError); } - } - catch (UnknownHostException uhe) { - String errorMessage = "Could not connect to " + host + ":" + port + "."; - throw new XMPPException(errorMessage, new XMPPError( - XMPPError.Condition.remote_server_timeout, errorMessage), - uhe); - } - catch (IOException ioe) { - String errorMessage = "XMPPError connecting to " + host + ":" - + port + "."; - throw new XMPPException(errorMessage, new XMPPError( - XMPPError.Condition.remote_server_error, errorMessage), ioe); } socketClosed = false; initConnection(); diff --git a/source/org/jivesoftware/smack/util/DNSUtil.java b/source/org/jivesoftware/smack/util/DNSUtil.java index ab3e2e44d..628d8e8f3 100644 --- a/source/org/jivesoftware/smack/util/DNSUtil.java +++ b/source/org/jivesoftware/smack/util/DNSUtil.java @@ -19,18 +19,20 @@ package org.jivesoftware.smack.util; -import java.util.Hashtable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; -import javax.naming.NamingEnumeration; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; - +import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.SRVRecord; /** - * Utilty class to perform DNS lookups for XMPP services. + * Utility class to perform DNS lookups for XMPP services. * * @author Matt Tucker */ @@ -40,23 +42,30 @@ public class DNSUtil { * Create a cache to hold the 100 most recently accessed DNS lookups for a period of * 10 minutes. */ - private static Map cache = new Cache(100, 1000*60*10); + private static Map> cache = new Cache>(100, 1000*60*10); - private static DirContext context; + private static DNSResolver dnsResolver = null; - static { - try { - Hashtable env = new Hashtable(); - env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); - context = new InitialDirContext(env); - } - catch (Exception e) { - // Ignore. - } + /** + * Set the DNS resolver that should be used to perform DNS lookups. + * + * @param resolver + */ + public static void setDNSResolver(DNSResolver resolver) { + dnsResolver = resolver; } /** - * Returns the host name and port that the specified XMPP server can be + * Returns the current DNS resolved used to perform DNS lookups. + * + * @return + */ + public static DNSResolver getDNSResolver() { + return dnsResolver; + } + + /** + * Returns a list of HostAddresses under which the specified XMPP server can be * reached at for client-to-server communication. A DNS lookup for a SRV * record in the form "_xmpp-client._tcp.example.com" is attempted, according * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form @@ -67,81 +76,17 @@ public class DNSUtil { * of 5222.

* * As an example, a lookup for "example.com" may return "im.example.com:5269". - * - * Note on SRV record selection. - * We now check priority and weight, but we still don't do this correctly. - * The missing behavior is this: if we fail to reach a host based on its SRV - * record then we need to select another host from the other SRV records. - * In Smack 3.1.1 we're not going to be able to do the major system redesign to - * correct this. * * @param domain the domain. - * @return a HostAddress, which encompasses the hostname and port that the XMPP - * server can be reached at for the specified domain. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. */ - public static HostAddress resolveXMPPDomain(String domain) { - if (context == null) { - return new HostAddress(domain, 5222); - } - String key = "c" + domain; - // Return item from cache if it exists. - if (cache.containsKey(key)) { - HostAddress address = (HostAddress)cache.get(key); - if (address != null) { - return address; - } - } - String bestHost = domain; - int bestPort = 5222; - int bestPriority = 0; - int bestWeight = 0; - try { - Attributes dnsLookup = context.getAttributes("_xmpp-client._tcp." + domain, new String[]{"SRV"}); - Attribute srvAttribute = dnsLookup.get("SRV"); - NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); - while(srvRecords.hasMore()) { - String srvRecord = srvRecords.next(); - String [] srvRecordEntries = srvRecord.split(" "); - int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); - int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); - int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); - String host = srvRecordEntries[srvRecordEntries.length-1]; - - // Randomize the weight. - weight *= Math.random() * weight; - - if ((bestPriority == 0) || (priority < bestPriority)) { - // Choose a server with the lowest priority. - bestPriority = priority; - bestWeight = weight; - bestHost = host; - bestPort = port; - } else if (priority == bestPriority) { - // When we have like priorities then randomly choose a server based on its weight - // The weights were randomized above. - if (weight > bestWeight) { - bestWeight = weight; - bestHost = host; - bestPort = port; - } - } - } - } - catch (Exception e) { - // Ignore. - } - // Host entries in DNS should end with a ".". - if (bestHost.endsWith(".")) { - bestHost = bestHost.substring(0, bestHost.length()-1); - } - HostAddress address = new HostAddress(bestHost, bestPort); - // Add item to cache. - cache.put(key, address); - return address; + public static List resolveXMPPDomain(String domain) { + return resolveDomain(domain, 'c'); } /** - * Returns the host name and port that the specified XMPP server can be + * Returns a list of HostAddresses under which 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 @@ -154,104 +99,131 @@ public class DNSUtil { * As an example, a lookup for "example.com" may return "im.example.com:5269". * * @param domain the domain. - * @return a HostAddress, which encompasses the hostname and port that the XMPP - * server can be reached at for the specified domain. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. */ - public static HostAddress resolveXMPPServerDomain(String domain) { - if (context == null) { - return new HostAddress(domain, 5269); - } - String key = "s" + domain; + public static List resolveXMPPServerDomain(String domain) { + return resolveDomain(domain, 's'); + } + + private static List resolveDomain(String domain, char keyPrefix) { + // Prefix the key with 's' to distinguish him from the client domain lookups + String key = keyPrefix + domain; // Return item from cache if it exists. if (cache.containsKey(key)) { - HostAddress address = (HostAddress)cache.get(key); - if (address != null) { - return address; + List addresses = cache.get(key); + if (addresses != null) { + return addresses; } } - String host = domain; - int port = 5269; - try { - Attributes dnsLookup = - context.getAttributes("_xmpp-server._tcp." + domain, new String[]{"SRV"}); - String srvRecord = (String)dnsLookup.get("SRV").get(); - String [] srvRecordEntries = srvRecord.split(" "); - port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); - host = srvRecordEntries[srvRecordEntries.length-1]; + + if (dnsResolver == null) + throw new IllegalStateException("No DNS resolver active."); + + List addresses = new ArrayList(); + + // Step one: Do SRV lookups + String srvDomain; + if (keyPrefix == 's') { + srvDomain = "_xmpp-server._tcp." + domain; + } else if (keyPrefix == 'c') { + srvDomain = "_xmpp-client._tcp." + domain; + } else { + srvDomain = domain; } - catch (Exception e) { - // Attempt lookup with older "jabber" name. - try { - Attributes dnsLookup = - context.getAttributes("_jabber._tcp." + domain, new String[]{"SRV"}); - String srvRecord = (String)dnsLookup.get("SRV").get(); - String [] srvRecordEntries = srvRecord.split(" "); - port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length-2]); - host = srvRecordEntries[srvRecordEntries.length-1]; - } - catch (Exception e2) { - // Ignore. - } - } - // Host entries in DNS should end with a ".". - if (host.endsWith(".")) { - host = host.substring(0, host.length()-1); - } - HostAddress address = new HostAddress(host, port); + List srvRecords = dnsResolver.lookupSRVRecords(srvDomain); + List sortedRecords = sortSRVRecords(srvRecords); + if (sortedRecords != null) + addresses.addAll(sortedRecords); + + // Step two: Add the hostname to the end of the list + addresses.add(new HostAddress(domain)); + // Add item to cache. - cache.put(key, address); - return address; + cache.put(key, addresses); + + return addresses; } /** - * Encapsulates a hostname and port. + * Sort a given list of SRVRecords as described in RFC 2782 + * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry + * is calculated by random. The others are ore simply ordered by their priority. + * + * @param records + * @return */ - public static class HostAddress { + protected static List sortSRVRecords(List records) { + // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "." + // (the root domain), abort." + if (records.size() == 1 && records.get(0).getFQDN().equals(".")) + return null; - private String host; - private int port; + // sorting the records improves the performance of the bisection later + Collections.sort(records); - private HostAddress(String host, int port) { - 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; - } - - public String toString() { - return host + ":" + port; - } - - public boolean equals(Object o) { - if (this == o) { - return true; + // create the priority buckets + SortedMap> buckets = new TreeMap>(); + for (SRVRecord r : records) { + Integer priority = r.getPriority(); + List bucket = buckets.get(priority); + // create the list of SRVRecords if it doesn't exist + if (bucket == null) { + bucket = new LinkedList(); + buckets.put(priority, bucket); } - if (!(o instanceof HostAddress)) { - return false; - } - - final HostAddress address = (HostAddress) o; - - if (!host.equals(address.host)) { - return false; - } - return port == address.port; + bucket.add(r); } + + List res = new ArrayList(records.size()); + + for (Integer priority : buckets.keySet()) { + List bucket = buckets.get(priority); + int bucketSize; + while ((bucketSize = bucket.size()) > 0) { + int[] totals = new int[bucket.size()]; + int running_total = 0; + int count = 0; + int zeroWeight = 1; + + for (SRVRecord r : bucket) { + if (r.getWeight() > 0) + zeroWeight = 0; + } + + for (SRVRecord r : bucket) { + running_total += (r.getWeight() + zeroWeight); + totals[count] = running_total; + count++; + } + int selectedPos; + if (running_total == 0) { + // If running total is 0, then all weights in this priority + // group are 0. So we simply select one of the weights randomly + // as the other 'normal' algorithm is unable to handle this case + selectedPos = (int) (Math.random() * bucketSize); + } else { + double rnd = Math.random() * running_total; + selectedPos = bisect(totals, rnd); + } + // add the SRVRecord that was randomly chosen on it's weight + // to the start of the result list + SRVRecord chosenSRVRecord = bucket.remove(selectedPos); + res.add(chosenSRVRecord); + } + } + + return res; + } + + // TODO this is not yet really bisection just a stupid linear search + private static int bisect(int[] array, double value) { + int pos = 0; + for (int element : array) { + if (value < element) + break; + pos++; + } + return pos; } } \ No newline at end of file diff --git a/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java b/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java new file mode 100644 index 000000000..91db73b1a --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/DNSJavaResolver.java @@ -0,0 +1,72 @@ +/** + * Copyright 2013 Florian Schmaus + * + * 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.smack.util.dns; + +import java.util.ArrayList; +import java.util.List; + +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Record; +import org.xbill.DNS.Type; + +public class DNSJavaResolver extends DNSResolver { + + private static DNSJavaResolver instance; + + private DNSJavaResolver() { + + } + + public static DNSResolver getInstance() { + if (instance == null) { + instance = new DNSJavaResolver(); + } + return instance; + } + + @Override + public List lookupSRVRecords(String name) { + List res = new ArrayList(); + + try { + Lookup lookup = new Lookup(name, Type.SRV); + Record recs[] = lookup.run(); + if (recs == null) + return res; + + for (Record record : recs) { + org.xbill.DNS.SRVRecord srvRecord = (org.xbill.DNS.SRVRecord) record; + if (srvRecord != null && srvRecord.getTarget() != null) { + String host = srvRecord.getTarget().toString(); + int port = srvRecord.getPort(); + int priority = srvRecord.getPriority(); + int weight = srvRecord.getWeight(); + + SRVRecord r; + try { + r = new SRVRecord(host, port, priority, weight); + } catch (Exception e) { + continue; + } + res.add(r); + } + } + + } catch (Exception e) { + } + return res; + } +} diff --git a/source/org/jivesoftware/smack/util/dns/DNSResolver.java b/source/org/jivesoftware/smack/util/dns/DNSResolver.java new file mode 100644 index 000000000..2c5dd296c --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/DNSResolver.java @@ -0,0 +1,24 @@ +/** + * Copyright 2013 Florian Schmaus + * + * 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.smack.util.dns; + +import java.util.List; + +public abstract class DNSResolver { + + public abstract List lookupSRVRecords(String name); + +} diff --git a/source/org/jivesoftware/smack/util/dns/HostAddress.java b/source/org/jivesoftware/smack/util/dns/HostAddress.java new file mode 100644 index 000000000..978a6de69 --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/HostAddress.java @@ -0,0 +1,93 @@ +/** + * Copyright 2013 Florian Schmaus + * + * 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.smack.util.dns; + +public class HostAddress { + private String fqdn; + private int port; + private Exception exception; + + /** + * Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222 + * + * @param fqdn + * @throws IllegalArgumentException + */ + public HostAddress(String fqdn) throws IllegalArgumentException { + if (fqdn == null) + throw new IllegalArgumentException("FQDN is null"); + if (fqdn.charAt(fqdn.length() - 1) == '.') { + this.fqdn = fqdn.substring(0, fqdn.length() - 1); + } + else { + this.fqdn = fqdn; + } + // Set port to the default port for XMPP client communication + this.port = 5222; + } + + public HostAddress(String fqdn, int port) throws IllegalArgumentException { + this(fqdn); + if (port < 0 || port > 65535) + throw new IllegalArgumentException( + "DNS SRV records weight must be a 16-bit unsiged integer (i.e. between 0-65535. Port was: " + port); + + this.port = port; + } + + public String getFQDN() { + return fqdn; + } + + public int getPort() { + return port; + } + + public void setException(Exception e) { + this.exception = e; + } + + public String toString() { + return fqdn + ":" + port; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HostAddress)) { + return false; + } + + final HostAddress address = (HostAddress) o; + + if (!fqdn.equals(address.fqdn)) { + return false; + } + return port == address.port; + } + + public String getErrorMessage() { + String error; + if (exception == null) { + error = "No error logged"; + } + else { + error = exception.getMessage(); + } + return toString() + " Exception: " + error; + } +} diff --git a/source/org/jivesoftware/smack/util/dns/JavaxResolver.java b/source/org/jivesoftware/smack/util/dns/JavaxResolver.java new file mode 100644 index 000000000..4ea361fdf --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/JavaxResolver.java @@ -0,0 +1,99 @@ +/** + * Copyright 2013 Florian Schmaus + * + * 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.smack.util.dns; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; + +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +import org.jivesoftware.smack.util.DNSUtil; + +/** + * A DNS resolver (mostly for SRV records), which makes use of the API provided in the javax.* namepsace. + * + * @author Florian Schmaus + * + */ +public class JavaxResolver extends DNSResolver { + + private static JavaxResolver instance; + private static DirContext dirContext; + + static { + try { + Hashtable env = new Hashtable(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + dirContext = new InitialDirContext(env); + } catch (Exception e) { + // Ignore. + } + + // Try to set this DNS resolver as primary one + DNSUtil.setDNSResolver(maybeGetInstance()); + } + + private JavaxResolver() { + + } + + public static DNSResolver maybeGetInstance() { + if (instance == null && isSupported()) { + instance = new JavaxResolver(); + } + return instance; + } + + public static boolean isSupported() { + return dirContext != null; + } + + @Override + public List lookupSRVRecords(String name) { + List res = new ArrayList(); + + try { + Attributes dnsLookup = dirContext.getAttributes(name, new String[]{"SRV"}); + Attribute srvAttribute = dnsLookup.get("SRV"); + @SuppressWarnings("unchecked") + NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); + while (srvRecords.hasMore()) { + String srvRecordString = srvRecords.next(); + String[] srvRecordEntries = srvRecordString.split(" "); + int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); + int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 2]); + int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); + String host = srvRecordEntries[srvRecordEntries.length - 1]; + + SRVRecord srvRecord; + try { + srvRecord = new SRVRecord(host, port, priority, weight); + } catch (Exception e) { + continue; + } + res.add(srvRecord); + } + } catch (Exception e) { + + } + return res; + } +} diff --git a/source/org/jivesoftware/smack/util/dns/SRVRecord.java b/source/org/jivesoftware/smack/util/dns/SRVRecord.java new file mode 100644 index 000000000..87c6e54fc --- /dev/null +++ b/source/org/jivesoftware/smack/util/dns/SRVRecord.java @@ -0,0 +1,77 @@ +/** + * Copyright 2013 Florian Schmaus + * + * 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.smack.util.dns; + +/** + * @see 65535) + throw new IllegalArgumentException( + "DNS SRV records priority must be a 16-bit unsiged integer (i.e. between 0-65535. Priority was: " + + priority); + + this.priority = priority; + this.weight = weight; + + } + + public int getPriority() { + return priority; + } + + public int getWeight() { + return weight; + } + + public int compareTo(SRVRecord other) { + // According to RFC2782, + // "[a] client MUST attempt to contact the target host with the lowest-numbered priority it can reach". + // This means that a SRV record with a higher priority is 'less' then one with a lower. + int res = other.priority - this.priority; + if (res == 0) { + res = this.weight - other.weight; + } + return res; + } + + public String toString() { + return super.toString() + " prio:" + priority + ":w:" + weight; + } +} diff --git a/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java b/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java new file mode 100644 index 000000000..d107f8f0d --- /dev/null +++ b/test-unit/org/jivesoftware/smack/util/DNSUtilTest.java @@ -0,0 +1,147 @@ +package org.jivesoftware.smack.util; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertTrue; +import static junit.framework.Assert.fail; + +import java.util.ArrayList; +import java.util.List; + + +import org.jivesoftware.smack.util.dns.DNSJavaResolver; +import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.JavaxResolver; +import org.jivesoftware.smack.util.dns.SRVRecord; +import org.junit.Test; + +public class DNSUtilTest { + private static final String igniterealtimeDomain = "igniterealtime.org"; + private static final String igniterealtimeXMPPServer = "xmpp." + igniterealtimeDomain; + private static final int igniterealtimeClientPort = 5222; + private static final int igniterealtimeServerPort = 5269; + + @Test + public void xmppClientDomainJavaXTest() { + DNSResolver resolver = JavaxResolver.maybeGetInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppClientDomainTest(); + } + + @Test + public void xmppServerDomainJavaXTest() { + DNSResolver resolver = JavaxResolver.maybeGetInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppServerDomainTest(); + } + + @Test + public void xmppClientDomainDNSJavaTest() { + DNSResolver resolver = DNSJavaResolver.getInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppClientDomainTest(); + } + + @Test + public void xmppServerDomainDNSJavaTest() { + DNSResolver resolver = DNSJavaResolver.getInstance(); + assertNotNull(resolver); + DNSUtil.setDNSResolver(resolver); + xmppServerDomainTest(); + } + + @Test + public void sortSRVlowestPrioFirstTest() { + List sortedRecords = DNSUtil.sortSRVRecords(createSRVRecords()); + assertTrue(sortedRecords.get(0).getFQDN().equals("0.20.foo.bar")); + } + + @Test + public void sortSRVdistributeOverWeights() { + int weight50 = 0; + int weight20one = 0; + int weight20two = 0; + int weight10 = 0; + for (int i = 0; i < 1000; i++) { + List sortedRecords = DNSUtil.sortSRVRecords(createSRVRecords()); + String host = sortedRecords.get(1).getFQDN(); + if (host.equals("5.20.one.foo.bar")) { + weight20one++; + } else if (host.equals("5.20.two.foo.bar")) { + weight20two++; + } else if (host.equals("5.10.foo.bar")) { + weight10++; + } else if (host.equals("5.50.foo.bar")) { + weight50++; + } else { + fail("Wrong host after SRVRecord sorting"); + } + } + assertTrue(weight50 > 400 && weight50 < 600); + assertTrue(weight20one > 100 && weight20one < 300); + assertTrue(weight20two > 100 && weight20two < 300); + assertTrue(weight10 > 0&& weight10 < 200); + } + + @Test + public void sortSRVdistributeZeroWeights() { + int weightZeroOne = 0; + int weightZeroTwo = 0; + for (int i = 0; i < 1000; i++) { + List sortedRecords = DNSUtil.sortSRVRecords(createSRVRecords()); + // Remove the first 5 records with a lower priority + for (int j = 0; j < 5; j++) { + sortedRecords.remove(0); + } + String host = sortedRecords.remove(0).getFQDN(); + if (host.equals("10.0.one.foo.bar")) { + weightZeroOne++; + } else if (host.endsWith("10.0.two.foo.bar")) { + weightZeroTwo++; + } else { + fail("Wrong host after SRVRecord sorting"); + } + } + assertTrue(weightZeroOne > 400 && weightZeroOne < 600); + assertTrue(weightZeroTwo > 400 && weightZeroTwo < 600); + } + + private void xmppClientDomainTest() { + List hostAddresses = DNSUtil.resolveXMPPDomain(igniterealtimeDomain); + HostAddress ha = hostAddresses.get(0); + assertEquals(ha.getFQDN(), igniterealtimeXMPPServer); + assertEquals(ha.getPort(), igniterealtimeClientPort); + } + + private void xmppServerDomainTest() { + List hostAddresses = DNSUtil.resolveXMPPServerDomain(igniterealtimeDomain); + HostAddress ha = hostAddresses.get(0); + assertEquals(ha.getFQDN(), igniterealtimeXMPPServer); + assertEquals(ha.getPort(), igniterealtimeServerPort); + } + + private static List createSRVRecords() { + List records = new ArrayList(); + // We create one record with priority 0 that should also be tried first + // Then 4 records with priority 5 and different weights (50, 20, 20, 10) + // Then 2 records with priority 10 and weight 0 which should be treaded equal + // These records are added in a 'random' way to the list + try { + records.add(new SRVRecord("5.20.one.foo.bar", 42, 5, 20)); // Priority 5, Weight 20 + records.add(new SRVRecord("10.0.one.foo.bar", 42, 10, 0)); // Priority 10, Weight 0 + records.add(new SRVRecord("5.10.foo.bar", 42, 5, 10)); // Priority 5, Weight 10 + records.add(new SRVRecord("10.0.two.foo.bar", 42, 10, 0)); // Priority 10, Weight 0 + records.add(new SRVRecord("5.20.two.foo.bar", 42, 5, 20)); // Priority 5, Weight 20 + records.add(new SRVRecord("0.20.foo.bar", 42, 0, 20)); // Priority 0, Weight 20 + records.add(new SRVRecord("5.50.foo.bar", 42, 5, 50)); // Priority 5, Weight 50 + } catch (IllegalArgumentException e) { + // Ignore + } + assertTrue(records.size() > 0); + return records; + } +}