package org.limewire.xmpp.client.impl;
import java.io.IOException;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.friend.api.FriendConnectionConfiguration;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.SRVRecord;
import org.xbill.DNS.Type;
/**
* Uses a <code>XMPPConnectionConfiguration</code> to look up the
* XMPP host and port in DNS. Then creates a smack <code>ConnectionConfiguration</code>
* based on the data in the DNS entry.
*/
public class DNSConnectionConfigurationFactory implements ConnectionConfigurationFactory {
private static final Log LOG = LogFactory.getLog(DNSConnectionConfigurationFactory.class);
/**
* The max number of times to attempt looking up
* the XMPP host in DNS.
*/
private static final int MAX_XMPP_HOST_LOOKUPS = 3;
@Override
public boolean hasMore(FriendConnectionConfiguration connectionConfiguration, RequestContext requestContext) {
return requestContext.getNumRequests() < MAX_XMPP_HOST_LOOKUPS;
}
/**
* Converts a LimeWire XMPPConnectionConfiguration into a Smack
* ConnectionConfiguration, trying to obtain the hostname from DNS SRV
* as per RFC 3920 and falling back to the service name and default port
* if the SRV lookup fails. This method blocks during the DNS lookup.
*/
public ConnectionConfiguration getConnectionConfiguration(FriendConnectionConfiguration configuration, RequestContext requestContext) {
return getConnectionConfig(configuration, requestContext);
}
private ConnectionConfiguration getConnectionConfig(FriendConnectionConfiguration configuration, RequestContext requestContext) {
checkHasMore(configuration, requestContext);
HostAndPort hostAndPort = new HostAndPort(configuration.getServiceName(), 5222); // fallback
String serviceName = configuration.getServiceName();
try {
String domain = "_xmpp-client._tcp." + serviceName;
Lookup lookup = new Lookup(domain, Type.SRV);
Record[] answers = lookup.run();
int result = lookup.getResult();
if(result == Lookup.SUCCESSFUL) {
hostAndPort = readHostAndPortFromDNSEntry(hostAndPort, domain, answers);
} else if(result == Lookup.HOST_NOT_FOUND) {
LOG.debugf("dns lookup of {0} failed: host not found", domain);
} else if(result == Lookup.UNRECOVERABLE) {
LOG.debugf("dns lookup of {0} failed: unrecoverable", domain);
} else if(result == Lookup.TYPE_NOT_FOUND) {
LOG.debugf("dns lookup of {0} failed: type not found", domain);
} else if(result == Lookup.TRY_AGAIN) {
LOG.debugf("dns lookup of {0} failed: try again", domain);
requestContext.incrementRequests();
if(hasMore(configuration, requestContext)) {
return getConnectionConfig(configuration, requestContext);
}
}
} catch(IOException iox) {
// Failure looking up the SRV record - use the service name and default port
LOG.debug("Failed to look up SRV record", iox);
}
return new ConnectionConfiguration(hostAndPort.getHost(), hostAndPort.getPort(), serviceName);
}
private static HostAndPort readHostAndPortFromDNSEntry(HostAndPort fallback, String domain, Record[] answers) {
HostAndPort hostAndPort = fallback;
if(answers != null && answers.length > 0) {
// RFC 2782: use the server with the lowest-numbered priority,
// break ties by preferring servers with higher weights
int lowestPriority = Integer.MAX_VALUE;
int highestWeight = Integer.MIN_VALUE;
for(Record rec : answers) {
if(rec instanceof SRVRecord) {
SRVRecord srvRec = (SRVRecord)rec;
int priority = srvRec.getPriority();
int weight = srvRec.getWeight();
if(priority < lowestPriority && weight > highestWeight) {
hostAndPort = new HostAndPort(srvRec.getTarget().toString(), srvRec.getPort());
lowestPriority = priority;
highestWeight = weight;
}
} else {
LOG.debugf("dns lookup of {0} was successful, but contains non-SRV record: {1}: {2}",
domain, rec.getClass().getSimpleName(), rec.toString());
}
}
} else {
LOG.debugf("dns lookup of {0} was successful, but contained no records", domain);
}
return hostAndPort;
}
private static class HostAndPort {
private final String host;
private final int port;
public HostAndPort(String host, int defaultPort) {
int colonIdx = host.indexOf(':');
if(colonIdx == -1) {
this.host = host;
this.port = defaultPort;
} else {
this.host = host.substring(0, colonIdx);
int p = -1;
if (colonIdx < host.length() - 1) {
String portS = host.substring(colonIdx+1);
try {
p = Integer.parseInt(portS);
} catch(NumberFormatException nfe) {
}
}
if(p > 0) {
this.port = p;
} else {
this.port = defaultPort;
}
}
}
public String getHost() {
return host;
}
public int getPort() {
return port;
}
}
private void checkHasMore(FriendConnectionConfiguration connectionConfiguration, RequestContext requestContext) {
if(!hasMore(connectionConfiguration, requestContext)) {
throw new IllegalArgumentException("no more ConnectionConfigurations");
}
}
}