/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.
*/
/**
* @author Alexei Y. Zakharov
*/
package org.apache.harmony.jndi.provider.dns;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Random;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.ServiceUnavailableException;
import org.apache.harmony.jndi.internal.nls.Messages;
import org.apache.harmony.jndi.provider.dns.SList.Server;
/**
* This class implements the functionality of a simple DNS resolver.<br>
* Following DNS Resource Records are supported:
* <ul>
* <li>A</li>
* <li>NS</li>
* <li>CNAME</li>
* <li>SOA</li>
* <li>PTR</li>
* <li>MX</li>
* <li>TXT</li>
* <li>HINFO</li>
* <li>AAAA (TODO)</li>
* <li>NAPTR (TODO)</li>
* <li>SRV</li>
* </ul>
* Following DNS classes are supported:
* <ul>
* <li>IN</li>
* <li>HS (TODO)</li>
* </ul>
* <br>
* TODO: do we need broadcasting and IP multicasting to obtain initial name
* server address? TODO: network-preference feature for choosing most welcome
* address for multihomed hosts (RFC 1123 point 6.1.3.4) TODO: add general IPv6
* support and support for AAAA resource record
*/
public class Resolver implements Runnable {
private static final int MSG_MAX_BYTES = 512;
/**
* Entry of the resolver thread list.
*/
private static class ThreadListEntry {
Thread thread;
String serverNameToResolve;
int dnsClass;
}
private static final Random rndGen = new Random();
// resolver configuration
private int initialTimeout;
private int timeoutRetries;
private boolean authoritativeAnswerDesired;
private boolean recursionDesired;
// maximum number of active threads
private int threadNumberLimit;
// vector with currently running Resolver threads
private final ArrayList<ThreadListEntry> resolverThreads = new ArrayList<ThreadListEntry>();
// the list of host names that should be resolved
private final ArrayList<ThreadListEntry> hostnamesToResolve = new ArrayList<ThreadListEntry>();
// semaphore that controls access to both lists above
private class ThreadListSemaphore {
}
private final Object threadListSemaphore = new ThreadListSemaphore();
/**
* Constructs a <code>Resolver</code> object with default initial timeout
* (1 second), default timeout retries (4 times), default recursion desired
* switch (true) and default authoritative answer desired
*
* @see #Resolver(int, int)
*/
public Resolver() {
this(ProviderConstants.DEFAULT_INITIAL_TIMEOUT,
ProviderConstants.DEFAULT_TIMEOUT_RETRIES,
ProviderConstants.DEFAULT_MAX_THREADS,
ProviderConstants.DEFAULT_AUTHORITATIVE,
ProviderConstants.DEFAULT_RECURSION);
}
/**
* Constructs a <code>Resolver</code> object with given initial timeout
* and timeout retries. Initially the resolver will try to access DNS
* servers with timeout set to initial timeout. If none of servers answer it
* will double the timeout and perform the second round. The process will
* continue for <code>timeoutRetries</code> rounds. If there is no answer
* still the resolver will give up.
*
* @param initialTimeout
* the initial timeout that is used during the first round (in
* milliseconds)
* @param timeoutRetries
* number of rounds the Resolver should perform before giving up
* @param authoritativeAnswerDesired
* do we want to receive only authoritative answers
* @param recursionDesired
* do we want our outgoing packages to have RD but set
*/
public Resolver(int initialTimeout, int timeoutRetries, int maxThreads,
boolean authoritativeAnswerDesired, boolean recursionDesired) {
this.initialTimeout = initialTimeout;
this.timeoutRetries = timeoutRetries;
this.threadNumberLimit = maxThreads;
this.authoritativeAnswerDesired = authoritativeAnswerDesired;
this.recursionDesired = recursionDesired;
}
/**
* @return Returns the threadNumberLimit.
*/
public int getThreadNumberLimit() {
return threadNumberLimit;
}
/**
* @param threadNumberLimit
* The threadNumberLimit to set.
*/
public void setThreadNumberLimit(int threadNumberLimit) {
this.threadNumberLimit = threadNumberLimit;
}
/**
* @return Returns the authoritativeAnswerDesired.
*/
public boolean isAuthoritativeAnswerDesired() {
return authoritativeAnswerDesired;
}
/**
* @param authoritativeAnswerDesired
* The authoritativeAnswerDesired to set.
*/
public void setAuthoritativeAnswerDesired(boolean authoritativeAnswerDesired) {
this.authoritativeAnswerDesired = authoritativeAnswerDesired;
}
/**
* @return Returns the initialTimeout.
*/
public int getInitialTimeout() {
return initialTimeout;
}
/**
* @param initialTimeout
* The initialTimeout to set.
*/
public void setInitialTimeout(int initialTimeout) {
this.initialTimeout = initialTimeout;
}
/**
* @return Returns the recursionDesired.
*/
public boolean isRecursionDesired() {
return recursionDesired;
}
/**
* @param recursionDesired
* The recursionDesired to set.
*/
public void setRecursionDesired(boolean recursionDesired) {
this.recursionDesired = recursionDesired;
}
/**
* @return Returns the timeoutRetries.
*/
public int getTimeoutRetries() {
return timeoutRetries;
}
/**
* @param timeoutRetries
* The timeoutRetries to set.
*/
public void setTimeoutRetries(int timeoutRetries) {
this.timeoutRetries = timeoutRetries;
}
/**
* Checks available name servers if they have any resource records related
* to given name & type & class combination. Standard DNS lookup algorithm
* is used.
*
* @param name
* well-formed domain name
* @param types
* an array of types; only records that have such types will be
* returned
* @param classes
* @return enumeration with found resource records
* @throws SecurityException
* if the resolver is not allowed to use a network subsystem
* @throws NameNotFoundException
* if authoritative server for desired zone was contacted but
* given name has not been found in that zone
* @throws ServiceUnavailableException
* if no authoritative server for desired name was found or all
* servers are dead or malfunction
* @throws DomainProtocolException
* if some DNS specific error has occurred
*/
public Enumeration<ResourceRecord> lookup(String name, int[] types,
int[] classes) throws SecurityException, NameNotFoundException,
ServiceUnavailableException, DomainProtocolException {
// Algorithm:
// 1. Set workZone to the parent of qName; clear queriedServers.
// 2. Try to get a complete answer for the workZone from the servers
// currently available in SLIST exclude servers from queriedServers.
// 3. update queriedServers with "visited servers" info.
// 4. If the complete answer was received - return it to the user;exit.
// 5. If the delegation was received:
// a) If we already have this server & zone pair in SLIST - skip it.
// b) If we don't have - put it into SLIST
// c) If we haven't received any new delegations - goto step (7)
// d) If some new delegation has been received:
// 1) from delegations: found the zone with the best matching count
// with qName
// 2) if this matching count is bigger than matching count between
// workZone and qName:
// - set workZone to zone with biggest matching count determined
// at step (5.d.1)
// - goto step (2)
// 3) if it doesn't then goto step 2 with the same workZone
// 6. If ALIAS was received ...
// 7. If no answer has been received:
// a) Check if the workZone is the root zone.
// b) If so - give up; return empty result to the user.
// c) If it isn't, set workZone to parent of workZone. Goto step (2).
// SList slist = SList.getInstance();
ResolverCache cache = ResolverCache.getInstance();
Vector<QuestionRecord> questions = new Vector<QuestionRecord>();
Vector<ResourceRecord> answers = new Vector<ResourceRecord>();
if (name == null) {
// jndi.2E=The name is null
throw new NullPointerException(Messages.getString("jndi.2E")); //$NON-NLS-1$
}
if (types == null) {
// jndi.6B=types is null
throw new NullPointerException(Messages.getString("jndi.6B")); //$NON-NLS-1$
}
if (classes == null) {
// jndi.6C=classes is null
throw new NullPointerException(Messages.getString("jndi.6C")); //$NON-NLS-1$
}
for (int element : classes) {
for (int element0 : types) {
QuestionRecord quest = new QuestionRecord(name, element0,
element);
questions.addElement(quest);
}
}
// iterate over question records
for (int i = 0; i < questions.size(); i++) {
QuestionRecord curQuestion = questions.elementAt(i);
String qName = curQuestion.getQName();
Message mesToSend = null;
Message receivedMes = null;
AnalysisReport report = null;
String workZone;
Hashtable<Server, Object> visitedServers = new Hashtable<Server, Object>();
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Current question: " +
// curQuestion.toString());
// }
// look in cache
if (curQuestion.getQType() != ProviderConstants.ANY_QTYPE
&& curQuestion.getQClass() != ProviderConstants.ANY_QCLASS) {
Enumeration<ResourceRecord> recEnum = cache.get(curQuestion);
if (recEnum.hasMoreElements()) {
while (recEnum.hasMoreElements()) {
answers.addElement(recEnum.nextElement());
}
// we don't need to query any servers since the information
// we want has been found in the local cache
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Information was gathered from cache");
// }
continue;
}
}
// query remote DNS servers
// determine work zone
if (qName != null && !qName.equals(".")) { //$NON-NLS-1$
workZone = qName;
// support for SRV-style qNames
while (workZone.startsWith("_")) { //$NON-NLS-1$
workZone = ProviderMgr.getParentName(workZone);
}
} else {
workZone = "."; //$NON-NLS-1$
}
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Lookup: new workZone is " +
// "\"" + workZone + "\"");
// }
// construct request message
try {
mesToSend = createMessageForSending(qName, curQuestion
.getQType(), curQuestion.getQClass());
// if (LogConst.DEBUG) {
// ProviderMgr.logger.finest("Message to send:\n" +
// mesToSend.toString());
// }
} catch (DomainProtocolException e) {
throw e;
}
while (true) {
boolean noIdea = false;
try {
receivedMes = queryServers(mesToSend, workZone,
visitedServers, false);
if (receivedMes != null) {
report = analyzeAnswer(mesToSend, receivedMes);
if (!report.messageWasTruncated) {
// Put all extra records into the cache for
// future use
for (int k = 0; k < report.extraRecords.size(); k++) {
ResourceRecord rec = report.extraRecords
.elementAt(k);
cache.put(rec);
}
} else {
// Truncated message MUST NOT be cached and later
// used in such a way that the fact that they are
// truncated is lost (RFC 1123 point 6.1.3.2).
}
// examine the report
if (report.completeAnswerWasReceived) {
// complete answer
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Lookup: a complete answer was received");
// }
for (int k = 0; k < report.records.size(); k++) {
ResourceRecord rec = report.records
.elementAt(k);
answers.addElement(rec);
// we are sure that the answer section has not
// been truncated so we can put the record
// into the cache
cache.put(rec);
}
// exit the loop
break;
} else if (report.nameError) {
// name error
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Lookup: name error");
// }
// jndi.6D=Name {0} was not found
throw new NameNotFoundException(Messages.getString(
"jndi.6D", name)); //$NON-NLS-1$
} else if (report.aliasInfoWasReceived) {
// alias received
// QuestionRecord newQuestion = new
// QuestionRecord();
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Lookup: an alias was received");
// }
qName = report.newName;
curQuestion.setQName(qName);
// look in cache
if (curQuestion.getQType() != ProviderConstants.ANY_QTYPE
&& curQuestion.getQClass() != ProviderConstants.ANY_QCLASS) {
Enumeration<ResourceRecord> recEnum = cache
.get(curQuestion);
if (recEnum.hasMoreElements()) {
while (recEnum.hasMoreElements()) {
answers.addElement(recEnum
.nextElement());
}
// We don't need to query any more servers
// since the information we want has been
// found in the local cache.
// Let's switch to next question if any.
break;
}
}
if (qName != null && !qName.equals(".")) //$NON-NLS-1$
{
workZone = qName;
} else {
workZone = "."; //$NON-NLS-1$
}
visitedServers = new Hashtable<Server, Object>();
for (int k = 0; k < report.records.size(); k++) {
answers.addElement(report.records.elementAt(k));
}
// construct a new request message
try {
mesToSend = createMessageForSending(qName,
curQuestion.getQType(), curQuestion
.getQClass());
} catch (DomainProtocolException e) {
throw e;
}
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Lookup: new name is " +
// "\"" + qName + "\"");
// ProviderMgr.logger.fine(
// "Lookup: new workZone is " +
// "\"" + workZone + "\"");
// }
} else if (report.delegationArrived) {
// new delegation, probably need to query once again
int k17 = -1;
int matchingCount = ProviderMgr.getMatchingCount(
qName, workZone);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Lookup: delegation arrived");
// }
for (int k = 0; k < report.delegationZones.size(); k++) {
String curZone = report.delegationZones
.elementAt(k);
int tmpMatchingCount = ProviderMgr
.getMatchingCount(qName, curZone);
if (tmpMatchingCount > matchingCount) {
k17 = k;
matchingCount = tmpMatchingCount;
}
}
if (k17 != -1) {
// better delegation was received
workZone = report.delegationZones
.elementAt(k17);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Lookup: better delegation was found");
// }
} else {
// no better delegation
// do nothing, just query the next server of
// the current workZone
}
} else {
noIdea = true;
}
} // end of if report != null block
else {
noIdea = true;
}
if (noIdea) {
// Resolver has no idea how to get info about
// desired host while querying master hosts of the
// current workZone.
// Let's make one step up to the root.
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Lookup: no idea");
// }
if (!workZone.equals(".")) { //$NON-NLS-1$
workZone = ProviderMgr.getParentName(workZone);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Lookup: new work zone is " +
// "\"" + workZone + "\"");
// }
} else {
// give up
break;
// throw new ServiceUnavailableException(
// "Unable to " +
// "contact authoritative server for " +
// qName +
// " and no other results were found");
}
}
} catch (NameNotFoundException e) {
throw e;
} catch (DomainProtocolException e) {
throw e;
}
} // query servers loop
} // questions loop
return answers.elements();
}
/**
* Lists entire DNS zone using zone transfer mechanism.
*
* @param name
* DNS zone name
* @return enumeration with found <code>ResourceRecord</code> objects
* @throws SecurityException
* if the resolver is not allowed to use a network subsystem
* @throws NameNotFoundException
* if authoritative server(s) was not found
* @throws ServiceUnavailableException
* if none of found servers permits zone transfers
* @throws DomainProtocolException
* if some DNS specific error has occured
*/
public Enumeration<ResourceRecord> list(String name) throws NamingException {
final int OUT_BUF_SIZE = 512;
final int IN_BUF_SIZE = 65536;
Vector<ResourceRecord> answerVect = new Vector<ResourceRecord>();
Message mesToSend = null;
Message receivedMes = null;
Enumeration<ResourceRecord> enum1;
// String zoneMasterServer = null;
// Vector authoritativeServerIPs = new Vector();
HashSet<Object> authoritativeServers = new HashSet<Object>();
Iterator<Object> authServersIter;
int qClassArr[] = new int[1];
byte outBuf[] = new byte[OUT_BUF_SIZE];
int outLen;
byte inBuf[] = new byte[IN_BUF_SIZE];
boolean received = false;
boolean completeAnswer = false;
String proto = null;
ResolverCache cache = ResolverCache.getInstance();
// SList slist = SList.getInstance();
if (name == null) {
// jndi.2E=The name is null
throw new NullPointerException(Messages.getString("jndi.2E")); //$NON-NLS-1$
}
// if given name is SRV style name where domain name is prefixed
// with _Proto
if (name.startsWith("_")) { //$NON-NLS-1$
int n = name.indexOf('.');
if (n != -1) {
proto = name.substring(0, n);
if (name.length() > n) {
name = name.substring(n + 1, name.length());
} else {
// nonsense
name = "."; //$NON-NLS-1$
}
} else {
// nonsense
name = "."; //$NON-NLS-1$
}
}
enum1 = lookup(name, new int[] { ProviderConstants.NS_TYPE },
new int[] { ProviderConstants.ANY_QTYPE });
mesToSend = createMessageForSending(name, ProviderConstants.AXFR_QTYPE,
ProviderConstants.ANY_QCLASS);
outLen = mesToSend.writeBytes(outBuf, 0);
// determine the list of zone authoritative servers
while (enum1.hasMoreElements()) {
ResourceRecord rr = enum1.nextElement();
if (rr.getRRType() == ProviderConstants.NS_TYPE) {
authoritativeServers.add(rr.getRData());
// assertion: all authoritative servers should have the same
// DNS class
qClassArr[0] = rr.getRRClass();
} else if (rr.getRRType() == ProviderConstants.SOA_TYPE) {
StringTokenizer st = new StringTokenizer(
(String) rr.getRData(), " "); //$NON-NLS-1$
if (st.hasMoreTokens()) {
authoritativeServers.add(st.nextToken());
qClassArr[0] = rr.getRRClass();
break;
}
}
}
// try to perform a zone transfer
authServersIter = authoritativeServers.iterator();
authServersLoop: while (authServersIter.hasNext()) {
String authServerName = (String) authServersIter.next();
Enumeration<ResourceRecord> addrEnum = lookup(authServerName,
new int[] { ProviderConstants.A_TYPE }, qClassArr);
while (addrEnum.hasMoreElements()) {
ResourceRecord curRR = addrEnum.nextElement();
String ip = (String) curRR.getRData();
try {
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Initiating zone transfer, IP=" + ip);
// }
TransportMgr.sendReceiveTCP(ip,
ProviderConstants.DEFAULT_DNS_PORT, outBuf, outLen,
inBuf, IN_BUF_SIZE, this.initialTimeout
* this.timeoutRetries);
received = true;
} catch (SocketTimeoutException e) {
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Socket timeout");
// }
} catch (DomainProtocolException e) {
// some problem was encountered
// ProviderMgr.logger.log(Level.WARNING,
// "Connection failure", e);
}
if (received) {
receivedMes = new Message();
try {
int rCode;
Message.parseMessage(inBuf, 0, receivedMes);
rCode = receivedMes.getRCode();
switch (rCode) {
case ProviderConstants.NO_ERROR:
// put all received records into Resolver's
// cache
for (int k = 0; k < 3; k++) {
switch (k) {
case 0:
enum1 = receivedMes.getAnswerRRs();
break;
case 1:
enum1 = receivedMes
.getAuthorityRRs();
break;
case 2:
enum1 = receivedMes
.getAdditionalRRs();
break;
}
while (enum1.hasMoreElements()) {
ResourceRecord rr = enum1.nextElement();
cache.put(rr);
if (k == 0) {
answerVect.addElement(rr);
}
}
}
completeAnswer = true;
break;
case ProviderConstants.NAME_ERROR:
// jndi.6D=Name {0} was not found
throw new NameNotFoundException(Messages
.getString("jndi.6D", name)); //$NON-NLS-1$
case ProviderConstants.SERVER_FAILURE:
case ProviderConstants.FORMAT_ERROR:
case ProviderConstants.NOT_IMPLEMENTED:
case ProviderConstants.REFUSED:
default:
}
} catch (DomainProtocolException e) {
// ProviderMgr.logger.log(Level.WARNING,
// "Error while parsing of DNS message", e);
}
} // if received
if (completeAnswer) {
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "list: Complete answer received");
// }
break authServersLoop;
}
} // address loop
} // authoritative servers loop
if (!completeAnswer) {
// found nothing
// jndi.6E=Unable to perform zone transfer
throw new ServiceUnavailableException(Messages.getString("jndi.6E")); //$NON-NLS-1$
}
// SRV _Proto prefix support - filter all records that don't have given
// _Proto field
if (proto != null) {
Vector<ResourceRecord> answerVect2 = new Vector<ResourceRecord>();
for (int i = 0; i < answerVect.size(); i++) {
ResourceRecord rr = answerVect.elementAt(i);
StringTokenizer st = new StringTokenizer(rr.getName(), "."); //$NON-NLS-1$
String token = null;
boolean valid = false;
if (st.hasMoreTokens()) {
token = st.nextToken();
if (token.length() > 0 && token.charAt(0) == '_'
&& st.hasMoreTokens()) {
token = st.nextToken();
if (token.equalsIgnoreCase(proto)) {
valid = true;
}
}
}
if (valid) {
answerVect2.addElement(rr);
}
}
answerVect = answerVect2;
}
return answerVect.elements();
}
/**
* Adds initial DNS server the resolver should start with. Trying underlying
* OS services to determine IP from name.
*
* @param name
* server's name
* @param ip
* server's IP address
* @param port
* port on server
*/
public void addInitialServer(String name, String ip, int port,
String zoneName) {
Server server = new Server(name, ip, port);
SList slist = SList.getInstance();
if (name == null && ip == null) {
// jndi.6F=Both name and IP are null
throw new NullPointerException(Messages.getString("jndi.6F")); //$NON-NLS-1$
}
if (zoneName == null) {
// jndi.70=zoneName is null
throw new NullPointerException(Messages.getString("jndi.70")); //$NON-NLS-1$
}
// if IP is not given and we don't know this server yet
// try to determine IP from underlying OS services
if (ip == null && !slist.hasServer(name)) {
InetAddress addrObj = TransportMgr.getIPByName_OS(name);
if (addrObj != null) {
server.setIP(ProviderMgr.getIpStr(addrObj.getAddress()));
}
}
// add given zone <-> server pair
if (!slist.hasServer(zoneName, server)) {
slist.updateEntry(zoneName, server, SList.UNKNOWN);
}
}
/**
* Query available DNS servers for desired information. This method doesn't
* look into the local cache. Drops all answers that contains "server fail"
* and "not implemented" answer codes and returns the first "good" answer.
*
* @param request
* a DNS message that contains the request record
* @param workZone
* a zone that is closest known (grand-) parent of the desired
* name
* @param visitedServers
* the hash list of servers, that should not be examined; this
* method also appends to this list all server that have been
* visited during execution of this method
* @param tcpOnly
* <code>true</code> if we want to use TCP protocol only;
* otherwise UDP will be tried first
* @return the message received; <code>null</code> if none found
* @throws DomainProtocolException
* some domain protocol related error occured
* @throws SecurityException
* if the resolver doesn't have the permission to use sockets
*/
Message queryServers(Message request, String workZone,
Hashtable<Server, Object> visitedServers, boolean tcpOnly)
throws DomainProtocolException, SecurityException {
QuestionRecord qRecord;
SList slist = SList.getInstance();
Server curServer;
byte[] outBuf = new byte[MSG_MAX_BYTES];
int outBufLen;
byte[] inBuf = new byte[MSG_MAX_BYTES];
Message receivedMes = null;
int idx = 0;
int curTimeout = this.initialTimeout;
boolean received = false;
boolean parsed = false;
boolean correctAnswer = false;
int rCode = -1;
// determine a question
if (!request.getQuestionRecords().hasMoreElements()) {
// jndi.71=no question record
throw new IllegalArgumentException(Messages.getString("jndi.71")); //$NON-NLS-1$
}
qRecord = request.getQuestionRecords().nextElement();
// preparing a domain protocol message
outBufLen = request.writeBytes(outBuf, 0);
// sending message and trying to receive an answer
for (int round = 0; round < this.timeoutRetries; round++) {
Set<Server> queriedServers = new HashSet<Server>();
// start of round
while (true) {
int responseTime = 0;
received = false;
parsed = false;
rCode = -1;
// get next server
curServer = slist.getBestGuess(workZone, visitedServers);
if (curServer == null || queriedServers.contains(curServer)) {
// end of round
break;
}
if (curServer.getIP() == null) {
// if we don't know IP lets start background resolving
// thread
startResolvingThread(curServer.getName(), qRecord
.getQClass());
slist.updateEntry(workZone, curServer,
SList.NETWORK_FAILURE);
queriedServers.add(curServer);
continue;
}
// send the message and receive the answer
try {
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Timeout is set to " +
// curTimeout);
// ProviderMgr.logger.fine("Querying server \"" +
// curServer + "\"");
// }
// timeBeforeSending = System.currentTimeMillis();
if (tcpOnly) {
TransportMgr.sendReceiveTCP(curServer.getIP(),
curServer.getPort(), outBuf, outBufLen, inBuf,
inBuf.length, curTimeout);
} else {
TransportMgr.sendReceiveUDP(curServer.getIP(),
curServer.getPort(), outBuf, outBufLen, inBuf,
inBuf.length, curTimeout);
}
// responseTime = (int) (System.currentTimeMillis() -
// timeBeforeSending);
// ProviderMgr.logger.fine("Answer received in " +
// responseTime + " milliseconds");
received = true;
} catch (SocketTimeoutException e) {
slist.updateEntry(workZone, curServer, SList.TIMEOUT);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Socket timeout");
// }
} catch (DomainProtocolException e) {
// problems with receiving the message
// skipping this server
slist.updateEntry(workZone, curServer,
SList.NETWORK_FAILURE);
// ProviderMgr.logger.log(Level.WARNING,
// "Connection failure", e);
}
// parse the message
if (received) {
try {
boolean answerSectionIsTruncated = false;
receivedMes = new Message();
idx = 0;
idx = Message.parseMessage(inBuf, idx, receivedMes);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.finest("Received message:\n" +
// receivedMes.toString());
// }
parsed = true;
// handle a truncation
if (receivedMes.isTc() && !tcpOnly) {
// The Message is truncated.
// Let's try to establish a TCP connection
// and retransmit the message over that connection.
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Message is truncated");
// ProviderMgr.logger.fine("Trying to establish " +
// "a connection over TCP");
// }
try {
Message receivedMesTcp;
int idx2;
TransportMgr.sendReceiveTCP(curServer.getIP(),
curServer.getPort(), outBuf, outBufLen,
inBuf, inBuf.length, curTimeout);
receivedMesTcp = new Message();
idx2 = Message.parseMessage(inBuf, 0,
receivedMesTcp);
// complete message was received
if (!receivedMesTcp.isTc()) {
receivedMes = receivedMesTcp;
idx = idx2;
}
} catch (Exception e) {
// ProviderMgr.logger.log(Level.WARNING,
// "Receiving a complete message" +
// " over TCP failed", e);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Parsing the message " +
// "previously received over UDP");
// }
}
}
// Is the message still truncated?
// (It is possible in case if TCP connection failed)
if (receivedMes.isTc()) {
// check if the ANSWER section is truncated
// or not
if (!receivedMes.getAuthorityRRs()
.hasMoreElements()
&& !receivedMes.getAdditionalRRs()
.hasMoreElements()) {
answerSectionIsTruncated = true;
}
}
rCode = receivedMes.getRCode();
if (rCode == ProviderConstants.NO_ERROR) {
// correct message has been received
slist
.updateEntry(workZone, curServer,
responseTime);
visitedServers.put(curServer, new Object()); // $NON-LOCK-1$
if (!answerSectionIsTruncated) {
correctAnswer = true;
break;
}
} else if (rCode == ProviderConstants.SERVER_FAILURE) {
// removing server from list
// ProviderMgr.logger.warning("Server failure. " +
// errMsg);
slist.updateEntry(workZone, curServer,
SList.SERVER_FAILURE);
visitedServers.put(curServer, new Object()); // $NON-LOCK-1$
} else if (rCode == ProviderConstants.FORMAT_ERROR) {
// removing server from list
// ProviderMgr.logger.warning("Format error. " +
// errMsg);
slist.updateEntry(workZone, curServer,
SList.SERVER_FAILURE);
visitedServers.put(curServer, new Object()); // $NON-LOCK-1$
} else if (rCode == ProviderConstants.NAME_ERROR) {
// ProviderMgr.logger.warning("Name error. " +
// errMsg);
if (receivedMes.isAA()) {
slist.updateEntry(workZone, curServer,
responseTime);
visitedServers.put(curServer, new Object()); // $NON-LOCK-1$
correctAnswer = true;
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Return name error to user");
// }
break;
}
// This server is not authoritative server for
// this zone. It should not answer with a
// name error. Probably it is misconfigured.
slist.updateEntry(workZone, curServer,
SList.SERVER_FAILURE);
visitedServers.put(curServer, new Object()); // $NON-LOCK-1$
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Not authoritative answer. " +
// "Skip it.");
// }
} else if (rCode == ProviderConstants.NOT_IMPLEMENTED) {
// ProviderMgr.logger.warning("Not implemented. " +
// errMsg);
slist.updateEntry(workZone, curServer,
SList.SERVER_FAILURE);
visitedServers.put(curServer, new Object()); // $NON-LOCK-1$
} else if (rCode == ProviderConstants.REFUSED) {
// ProviderMgr.logger.warning("Refused. " +
// errMsg);
slist.updateEntry(workZone, curServer,
SList.SERVER_FAILURE);
visitedServers.put(curServer, new Object()); // $NON-LOCK-1$
}
} catch (DomainProtocolException e) {
// removing this server from SLIST
slist.dropServer(workZone, curServer);
// ProviderMgr.logger.warning("Unknown error.");
} catch (IndexOutOfBoundsException e) {
// bad message received
slist.dropServer(workZone, curServer);
// ProviderMgr.logger.warning("Bad message received: " +
// " IndexOutOfBoundsException.");
}
}
queriedServers.add(curServer);
}
// end of round
if (received & parsed & correctAnswer) {
// correct answer received
return receivedMes;
}
curTimeout *= 2;
}
// give up - no correct message has been received
return null;
}
/**
* Analyzes the answer message and constructs an analysis report.
*
* @param request
* the request has been send to the server
* @param answer
* the answer has been received
* @param workZone
* the current resolver's work zone
* @return analysis report TODO may be optimized
*/
AnalysisReport analyzeAnswer(Message request, Message answer)
throws DomainProtocolException {
Enumeration<QuestionRecord> questions = request.getQuestionRecords();
Enumeration<ResourceRecord> answerRRs = answer.getAnswerRRs();
Enumeration<ResourceRecord> authorityRRs = answer.getAuthorityRRs();
Enumeration<ResourceRecord> additionalRRs;
QuestionRecord question;
AnalysisReport report = new AnalysisReport();
// Check the ID.
if (request.getId() != answer.getId()) {
// jndi.72=Request and Answer have different ids
throw new DomainProtocolException(Messages.getString("jndi.72")); //$NON-NLS-1$
}
// Determine a question.
if (questions.hasMoreElements()) {
question = questions.nextElement();
} else {
// jndi.73=no question record
throw new IllegalArgumentException(Messages.getString("jndi.73")); //$NON-NLS-1$
}
// If name error occurred - no extra processing needed.
if (answer.getRCode() == ProviderConstants.NAME_ERROR) {
report.nameError = true;
return report;
}
// check truncation, truncated message should not be cached
if (answer.isTc()) {
report.messageWasTruncated = true;
}
// Analyze answer section.
while (answerRRs.hasMoreElements()) {
ResourceRecord curRec = answerRRs.nextElement();
if (question.getQClass() == curRec.getRRClass()
|| question.getQClass() == ProviderConstants.ANY_QCLASS) {
if (question.getQType() == ProviderConstants.ANY_QTYPE
&& ProviderMgr.namesAreEqual(curRec.getName(), question
.getQName())) {
// If we query for ANY record types and the server returns
// some record for the SAME domain name we will collect
// all of such records and treat
// this situation as a complete answer for this query.
// We will not perform any more attempts to obtain more
// records.
report.records.addElement(curRec);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Adding " +
// ProviderConstants.rrTypeNames[
// curRec.getRRType()]);
// }
if (curRec.getRRType() == ProviderConstants.CNAME_TYPE) {
report.aliasInfoWasReceived = true;
report.newName = (String) curRec.getRData();
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Alias \"" +
// report.newName + "\" was received");
// }
} else {
// XXX have we received a complete set of records?
report.completeAnswerWasReceived = true;
}
} else if (question.getQType() == curRec.getRRType()
&& ProviderMgr.namesAreEqual(question.getQName(),
curRec.getName())) {
// This is a situation when we get the record with the
// name and type exactly matching to that we have asked for.
// We will treat this as a complete answer.
report.records.addElement(curRec);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Adding " +
// ProviderConstants.rrTypeNames[
// curRec.getRRType()]);
// }
report.completeAnswerWasReceived = true;
} else if (curRec.getRRType() == ProviderConstants.CNAME_TYPE
&& ProviderMgr.namesAreEqual(curRec.getName(), question
.getQName())) {
// This is the case of an alias. If we received an alias for
// the name we have asked the information for then we need
// to change the desired name to this newly received name.
// Then we will try to find necessary information for
// this new name in the current answer. If we fail then
// we will continue our general lookup algorithm with the
// new name instead of an old one. We will query servers
// from the SLIST with this new name.
// TODO this is not effective
Enumeration<ResourceRecord> answerRRs2 = answer
.getAnswerRRs();
Enumeration<ResourceRecord> additionalRRs2 = answer
.getAdditionalRRs();
report.aliasInfoWasReceived = true;
report.newName = (String) curRec.getRData();
report.extraRecords.addElement(curRec);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Alias \"" + report.newName +
// "\" was received");
// }
// if we find the one of desired records in the
// current answer then we will treat the answer as complete
while (answerRRs2.hasMoreElements()) {
// Try to look for info about newly received name
// in ANSWER section.
ResourceRecord tmpRec = answerRRs2.nextElement();
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine(
// "Look for an answer in ANSWER section");
// }
if (tmpRec.getRRType() == question.getQType()
&& ProviderMgr.namesAreEqual(tmpRec.getName(),
report.newName)) {
// the answer is founded in ANSWER section
report.records.addElement(tmpRec);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Adding " +
// ProviderConstants.rrTypeNames[
// tmpRec.getRRType()]);
// }
report.completeAnswerWasReceived = true;
}
}
while (additionalRRs2.hasMoreElements()) {
// Try to look for info about newly received name
// in ADDITIONAL section.
ResourceRecord tmpRec = additionalRRs2.nextElement();
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Look for an answer in " +
// "ADDITIONAL section");
// }
if (tmpRec.getRRType() == question.getQType()
&& ProviderMgr.namesAreEqual(tmpRec.getName(),
report.newName)) {
// the answer is founded in ADDITIONAL section
report.records.addElement(tmpRec);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Adding " +
// ProviderConstants.rrTypeNames[
// tmpRec.getRRType()]);
// }
report.completeAnswerWasReceived = true;
}
}
// if (report.completeAnswerWasReceived) {
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Complete answer received");
// }
// }
} else {
// We have received some extra records. Let's save it for
// future use.
// we will treat authoritative answer as a complete answer
// and in no case will perform further actions
if (answer.isAA()) {
report.completeAnswerWasReceived = true;
}
report.extraRecords.addElement(curRec);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Adding additional record " +
// ProviderConstants.rrTypeNames[
// curRec.getRRType()]);
// }
}
} else {
// The record from another DNS class arrived. Just ignore it.
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Ignore records from DNS class " +
// curRec.getRRClass());
// }
}
}
// analyze authority section
// 1. Store all info from authority NS records; try to locate NS IPs
// from additional records in case if it is not present in SLIST;
// start new background lookup process if not found in additional
// section
// TODO current implementation isn't effective
while (authorityRRs.hasMoreElements()) {
ResourceRecord curRec = authorityRRs.nextElement();
SList slist = SList.getInstance();
// save record for future use
report.extraRecords.addElement(curRec);
// analyze
if (curRec.getRRType() == ProviderConstants.NS_TYPE) {
String serverName = (String) curRec.getRData();
Server server2 = new Server(serverName, null,
ProviderConstants.DEFAULT_DNS_PORT);
Server server = slist.getServerByServer(curRec.getName(),
server2);
report.delegationArrived = true;
if (server == null) {
// not found in SLIST
slist.updateEntry(curRec.getName(), server2, SList.UNKNOWN);
report.delegationZones.addElement(curRec.getName());
server = server2;
}
if (server != null && server.getIP() == null) {
// try to search additional records to obtain server's IP
additionalRRs = answer.getAdditionalRRs();
while (additionalRRs.hasMoreElements()) {
ResourceRecord addRec = additionalRRs.nextElement();
if (ProviderMgr.namesAreEqual(addRec.getName(),
serverName)
&& addRec.getRRType() == ProviderConstants.A_TYPE) {
server.setIP((String) addRec.getRData());
}
}
if (server.getIP() == null) {
// IP was not found in additional section
// start resolving process in the background
this.startResolvingThread(server.getName(), curRec
.getRRClass());
}
}
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Delegation \"" + server +
// "\" arrived");
// }
} // end of NS type analysis
} // end of authority section analysis
// analyze additional section
additionalRRs = answer.getAdditionalRRs();
while (additionalRRs.hasMoreElements()) {
ResourceRecord addRec = additionalRRs.nextElement();
report.extraRecords.addElement(addRec);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Adding additional record " +
// ProviderConstants.rrTypeNames[addRec.getRRType()]);
// }
}
// Fixing RRSet TTL issue.
// If TTL fields in RRSet are not all the same then we need to set
// all TTLs to lowest found value.
// See RFC 2181 point 5.2
// checking report.records and report.extraRecords
for (int k = 0; k < 2; k++) {
Vector<ResourceRecord> records = null;
HashSet<String> processed = new HashSet<String>();
switch (k) {
case 0:
records = report.records;
break;
case 1:
records = report.extraRecords;
break;
}
for (int i = 0; i < records.size(); i++) {
ResourceRecord rr = records.elementAt(i);
String key = rr.getName() + " " + rr.getRRClass() + " " + //$NON-NLS-1$ //$NON-NLS-2$
rr.getRRType();
long ttl = rr.getTtl();
Vector<ResourceRecord> objToUpdateTTL = new Vector<ResourceRecord>();
if (processed.contains(key)) {
continue;
}
objToUpdateTTL.addElement(rr);
// look forward for records with the same NAME CLASS TYPE
for (int j = i; j < records.size(); j++) {
ResourceRecord rr2 = records.elementAt(j);
String key2 = rr2.getName() + " " + rr2.getRRClass() + " " + //$NON-NLS-1$ //$NON-NLS-2$
rr2.getRRType();
long ttl2 = rr2.getTtl();
if (processed.contains(key2)) {
continue;
}
if (key.equals(key2)) {
if (ttl > ttl2) {
ttl = ttl2;
}
objToUpdateTTL.addElement(rr2);
}
}
// update TTL if necessary
for (int j = 0; j < objToUpdateTTL.size(); j++) {
ResourceRecord rr2 = objToUpdateTTL.elementAt(j);
if (rr2.getTtl() != ttl) {
rr2.setTtl(ttl);
}
}
// don't process such NAME CLASS TYPE combination any more
processed.add(key);
}
} // fixing RRSet TTL issue
return report;
}
/**
* Creates a new <code>Message</code> object and fills some of it's
* standard fields.
*
* @return created <code>Message</code> object
*/
static Message createMessageForSending(String desiredName, int recType,
int recClass) throws DomainProtocolException {
Message mes = new Message();
QuestionRecord qr = new QuestionRecord();
mes.setId(rndGen.nextInt() & 0xffff);
mes.setQR(ProviderConstants.QR_QUERY);
mes.setOpCode(ProviderConstants.QUERY);
mes.setRD(true);
mes.setQDCount(1);
qr.setQName(desiredName);
qr.setQType(recType);
qr.setQClass(recClass);
mes.addQuestionRecord(qr);
return mes;
}
/**
* Starts new resolver thread that will be searching for IP of the given
* hostname.
*
* @param hostname
* hostname to resolve
* @param dnsClass
* DNS class of host
*/
void startResolvingThread(String hostname, int dnsClass) {
Thread newThread;
ThreadListEntry newEntry;
int classes[] = new int[1];
synchronized (threadListSemaphore) {
// check that no currently running thread looks for this hostname
for (int i = 0; i < resolverThreads.size(); i++) {
ThreadListEntry entry = resolverThreads.get(i);
if (ProviderMgr.namesAreEqual(hostname,
entry.serverNameToResolve)
&& entry.dnsClass == dnsClass) {
// this hostname is already under investigation
// exiting
return;
}
}
// check if the hostname is already scheduled for resolving
for (int i = 0; i < hostnamesToResolve.size(); i++) {
ThreadListEntry entry = hostnamesToResolve.get(i);
if (ProviderMgr.namesAreEqual(hostname,
entry.serverNameToResolve)
&& entry.dnsClass == dnsClass) {
// this hostname is already scheduled for resolving
// exiting
return;
}
}
// check that the maximum number of threads is not exceeded
if (resolverThreads.size() >= threadNumberLimit) {
// maximum possible number of threads is reached already
return;
}
classes[0] = dnsClass;
newEntry = new ThreadListEntry();
newEntry.serverNameToResolve = hostname;
newEntry.dnsClass = dnsClass;
hostnamesToResolve.add(newEntry);
// starting new thread that should make further updates by itself
newThread = new Thread(this);
// if (LogConst.DEBUG) {
// ProviderMgr.logger.fine("Starting new resolver thread," +
// " target hostname: " + hostname);
// }
newThread.start();
}
}
/**
* Start background search of the address of next unresolved server hostname
*/
public void run() {
SList slist = SList.getInstance();
Enumeration<ResourceRecord> foundRecords;
ThreadListEntry entryToProcess;
int[] classes = new int[1];
// update lists
synchronized (threadListSemaphore) {
if (hostnamesToResolve.size() > 0) {
entryToProcess = hostnamesToResolve.get(0);
hostnamesToResolve.remove(0);
entryToProcess.thread = Thread.currentThread();
resolverThreads.add(entryToProcess);
} else {
// ProviderMgr.logger.warning(
// "Resolver thread: no host name to resolve");
return;
}
}
// lookup
try {
classes[0] = entryToProcess.dnsClass;
foundRecords = lookup(entryToProcess.serverNameToResolve,
new int[] { ProviderConstants.A_TYPE }, classes);
while (foundRecords != null && foundRecords.hasMoreElements()) {
// we will take all A records and store all of them in SLIST
ResourceRecord rr = foundRecords.nextElement();
if (rr.getRRType() == ProviderConstants.A_TYPE) {
slist.setServerIP(entryToProcess.serverNameToResolve,
(String) rr.getRData());
}
}
} catch (NamingException e) {
// just ignore it
}
// update resolver threads list, remove info about current thread
synchronized (threadListSemaphore) {
for (int i = 0; i < resolverThreads.size(); i++) {
ThreadListEntry entry = resolverThreads.get(i);
if (ProviderMgr.namesAreEqual(
entryToProcess.serverNameToResolve,
entry.serverNameToResolve)
&& entryToProcess.dnsClass == entry.dnsClass) {
resolverThreads.remove(i);
break;
}
}
}
// exiting
}
/**
* Analysis report.
*
* @see Resolver#analyzeAnswer(Message, Message)
*/
static class AnalysisReport {
boolean completeAnswerWasReceived = false;
boolean nameError = false;
boolean delegationArrived = false;
boolean aliasInfoWasReceived = false;
boolean messageWasTruncated = false;
Vector<ResourceRecord> records;
Vector<String> delegationZones;
String newName = null;
Vector<ResourceRecord> extraRecords;
AnalysisReport() {
records = new Vector<ResourceRecord>();
delegationZones = new Vector<String>();
extraRecords = new Vector<ResourceRecord>();
}
}
}