blob: c50992b7caa8ccabcb372635f284408d5d1c7ecf [file] [log] [blame]
//Copyright 2003-2005 Arthur van Hoff, Rick Blair
//Licensed under Apache License version 2.0
//Original license LGPL
package javax.jmdns.impl;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Iterator;
//import java.util.logging.Level;
//import java.util.logging.Logger;
/**
* DNS record
*
* @version %I%, %G%
* @author Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch
*/
public abstract class DNSRecord extends DNSEntry
{
// private static Logger logger = Logger.getLogger(DNSRecord.class.getName());
private int ttl;
private long created;
/**
* This source is mainly for debugging purposes, should be the address that
* sent this record.
*/
private InetAddress source;
/**
* Create a DNSRecord with a name, type, clazz, and ttl.
*/
DNSRecord(String name, int type, int clazz, int ttl)
{
super(name, type, clazz);
this.ttl = ttl;
this.created = System.currentTimeMillis();
}
/**
* True if this record is the same as some other record.
*/
public boolean equals(Object other)
{
return (other instanceof DNSRecord) && sameAs((DNSRecord) other);
}
/**
* True if this record is the same as some other record.
*/
boolean sameAs(DNSRecord other)
{
return super.equals(other) && sameValue((DNSRecord) other);
}
/**
* True if this record has the same value as some other record.
*/
abstract boolean sameValue(DNSRecord other);
/**
* True if this record has the same type as some other record.
*/
boolean sameType(DNSRecord other)
{
return type == other.type;
}
/**
* Handles a query represented by this record.
*
* @return Returns true if a conflict with one of the services registered
* with JmDNS or with the hostname occured.
*/
abstract boolean handleQuery(JmDNSImpl dns, long expirationTime);
/**
* Handles a responserepresented by this record.
*
* @return Returns true if a conflict with one of the services registered
* with JmDNS or with the hostname occured.
*/
abstract boolean handleResponse(JmDNSImpl dns);
/**
* Adds this as an answer to the provided outgoing datagram.
*/
abstract DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException;
/**
* True if this record is suppressed by the answers in a message.
*/
boolean suppressedBy(DNSIncoming msg)
{
try
{
for (int i = msg.numAnswers; i-- > 0;)
{
if (suppressedBy((DNSRecord) msg.answers.get(i)))
{
return true;
}
}
return false;
}
catch (ArrayIndexOutOfBoundsException e)
{
// logger.log(Level.WARNING, "suppressedBy() message " + msg + " exception ", e);
// msg.print(true);
return false;
}
}
/**
* True if this record would be supressed by an answer.
* This is the case if this record would not have a
* significantly longer TTL.
*/
boolean suppressedBy(DNSRecord other)
{
if (sameAs(other) && (other.ttl > ttl / 2))
{
return true;
}
return false;
}
/**
* Get the expiration time of this record.
*/
long getExpirationTime(int percent)
{
return created + (percent * ttl * 10L);
}
/**
* Get the remaining TTL for this record.
*/
int getRemainingTTL(long now)
{
return (int) Math.max(0, (getExpirationTime(100) - now) / 1000);
}
/**
* Check if the record is expired.
*/
public boolean isExpired(long now)
{
return getExpirationTime(100) <= now;
}
/**
* Check if the record is stale, ie it has outlived
* more than half of its TTL.
*/
boolean isStale(long now)
{
return getExpirationTime(50) <= now;
}
/**
* Reset the TTL of a record. This avoids having to
* update the entire record in the cache.
*/
void resetTTL(DNSRecord other)
{
created = other.created;
ttl = other.ttl;
}
/**
* Write this record into an outgoing message.
*/
abstract void write(DNSOutgoing out) throws IOException;
/**
* Address record.
*/
static class Address extends DNSRecord
{
// private static Logger logger = Logger.getLogger(Address.class.getName());
InetAddress addr;
Address(String name, int type, int clazz, int ttl, InetAddress addr)
{
super(name, type, clazz, ttl);
this.addr = addr;
}
Address(String name, int type, int clazz, int ttl, byte[] rawAddress)
{
super(name, type, clazz, ttl);
try
{
this.addr = InetAddress.getByAddress(rawAddress);
}
catch (UnknownHostException exception)
{
// logger.log(Level.WARNING, "Address() exception ", exception);
}
}
void write(DNSOutgoing out) throws IOException
{
if (addr != null)
{
byte[] buffer = addr.getAddress();
if (DNSConstants.TYPE_A == type)
{
// If we have a type A records we should answer with a IPv4 address
if (addr instanceof Inet4Address)
{
// All is good
}
else
{
// Get the last four bytes
byte[] tempbuffer = buffer;
buffer = new byte[4];
System.arraycopy(tempbuffer, 12, buffer, 0, 4);
}
}
else
{
// If we have a type AAAA records we should answer with a IPv6 address
if (addr instanceof Inet4Address)
{
byte[] tempbuffer = buffer;
buffer = new byte[16];
for (int i = 0; i < 16; i++)
{
if (i < 11)
{
buffer[i] = tempbuffer[i - 12];
}
else
{
buffer[i] = 0;
}
}
}
}
int length = buffer.length;
out.writeBytes(buffer, 0, length);
}
}
boolean same(DNSRecord other)
{
return ((sameName(other)) && ((sameValue(other))));
}
boolean sameName(DNSRecord other)
{
return name.equalsIgnoreCase(((Address) other).name);
}
boolean sameValue(DNSRecord other)
{
return addr.equals(((Address) other).getAddress());
}
InetAddress getAddress()
{
return addr;
}
/**
* Creates a byte array representation of this record.
* This is needed for tie-break tests according to
* draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2.
*/
private byte[] toByteArray()
{
try
{
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.write(name.getBytes("UTF8"));
dout.writeShort(type);
dout.writeShort(clazz);
//dout.writeInt(len);
byte[] buffer = addr.getAddress();
for (int i = 0; i < buffer.length; i++)
{
dout.writeByte(buffer[i]);
}
dout.close();
return bout.toByteArray();
}
catch (IOException e)
{
throw new InternalError();
}
}
/**
* Does a lexicographic comparison of the byte array representation
* of this record and that record.
* This is needed for tie-break tests according to
* draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2.
*/
private int lexCompare(DNSRecord.Address that)
{
byte[] thisBytes = this.toByteArray();
byte[] thatBytes = that.toByteArray();
for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++)
{
if (thisBytes[i] > thatBytes[i])
{
return 1;
}
else
{
if (thisBytes[i] < thatBytes[i])
{
return -1;
}
}
}
return thisBytes.length - thatBytes.length;
}
/**
* Does the necessary actions, when this as a query.
*/
boolean handleQuery(JmDNSImpl dns, long expirationTime)
{
DNSRecord.Address dnsAddress = dns.getLocalHost().getDNSAddressRecord(this);
if (dnsAddress != null)
{
if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && (!dnsAddress.sameValue(this)))
{
// logger.finer("handleQuery() Conflicting probe detected. dns state " + dns.getState() + " lex compare " + lexCompare(dnsAddress));
// Tie-breaker test
if (dns.getState().isProbing() && lexCompare(dnsAddress) >= 0)
{
// We lost the tie-break. We have to choose a different name.
dns.getLocalHost().incrementHostName();
dns.getCache().clear();
for (Iterator i = dns.getServices().values().iterator(); i.hasNext();)
{
ServiceInfoImpl info = (ServiceInfoImpl) i.next();
info.revertState();
}
}
dns.revertState();
return true;
}
}
return false;
}
/**
* Does the necessary actions, when this as a response.
*/
boolean handleResponse(JmDNSImpl dns)
{
DNSRecord.Address dnsAddress = dns.getLocalHost().getDNSAddressRecord(this);
if (dnsAddress != null)
{
if (dnsAddress.sameType(this) && dnsAddress.sameName(this) && (!dnsAddress.sameValue(this)))
{
// logger.finer("handleResponse() Denial detected");
if (dns.getState().isProbing())
{
dns.getLocalHost().incrementHostName();
dns.getCache().clear();
for (Iterator i = dns.getServices().values().iterator(); i.hasNext();)
{
ServiceInfoImpl info = (ServiceInfoImpl) i.next();
info.revertState();
}
}
dns.revertState();
return true;
}
}
return false;
}
DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException
{
return out;
}
public String toString()
{
return toString(" address '" + (addr != null ? addr.getHostAddress() : "null") + "'");
}
}
/**
* Pointer record.
*/
public static class Pointer extends DNSRecord
{
// private static Logger logger = Logger.getLogger(Pointer.class.getName());
String alias;
public Pointer(String name, int type, int clazz, int ttl, String alias)
{
super(name, type, clazz, ttl);
this.alias = alias;
}
void write(DNSOutgoing out) throws IOException
{
out.writeName(alias);
}
boolean sameValue(DNSRecord other)
{
return alias.equals(((Pointer) other).alias);
}
boolean handleQuery(JmDNSImpl dns, long expirationTime)
{
// Nothing to do (?)
// I think there is no possibility for conflicts for this record type?
return false;
}
boolean handleResponse(JmDNSImpl dns)
{
// Nothing to do (?)
// I think there is no possibility for conflicts for this record type?
return false;
}
String getAlias()
{
return alias;
}
DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException
{
return out;
}
public String toString()
{
return toString(alias);
}
}
public static class Text extends DNSRecord
{
// private static Logger logger = Logger.getLogger(Text.class.getName());
byte text[];
public Text(String name, int type, int clazz, int ttl, byte text[])
{
super(name, type, clazz, ttl);
this.text = text;
}
void write(DNSOutgoing out) throws IOException
{
out.writeBytes(text, 0, text.length);
}
boolean sameValue(DNSRecord other)
{
Text txt = (Text) other;
if (txt.text.length != text.length)
{
return false;
}
for (int i = text.length; i-- > 0;)
{
if (txt.text[i] != text[i])
{
return false;
}
}
return true;
}
boolean handleQuery(JmDNSImpl dns, long expirationTime)
{
// Nothing to do (?)
// I think there is no possibility for conflicts for this record type?
return false;
}
boolean handleResponse(JmDNSImpl dns)
{
// Nothing to do (?)
// Shouldn't we care if we get a conflict at this level?
/*
ServiceInfo info = (ServiceInfo) dns.services.get(name.toLowerCase());
if (info != null) {
if (! Arrays.equals(text,info.text)) {
info.revertState();
return true;
}
}*/
return false;
}
DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException
{
return out;
}
public String toString()
{
return toString((text.length > 10) ? new String(text, 0, 7) + "..." : new String(text));
}
}
/**
* Service record.
*/
public static class Service extends DNSRecord
{
// private static Logger logger = Logger.getLogger(Service.class.getName());
int priority;
int weight;
int port;
String server;
public Service(String name, int type, int clazz, int ttl, int priority, int weight, int port, String server)
{
super(name, type, clazz, ttl);
this.priority = priority;
this.weight = weight;
this.port = port;
this.server = server;
}
void write(DNSOutgoing out) throws IOException
{
out.writeShort(priority);
out.writeShort(weight);
out.writeShort(port);
if(DNSIncoming.USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET){
out.writeName(server, false);
} else {
out.writeUTF(server, 0, server.length());
// add a zero byte to the end just to be safe, this is the strange form
// used by the BonjourConformanceTest
out.writeByte(0);
}
}
private byte[] toByteArray()
{
try
{
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.write(name.getBytes("UTF8"));
dout.writeShort(type);
dout.writeShort(clazz);
//dout.writeInt(len);
dout.writeShort(priority);
dout.writeShort(weight);
dout.writeShort(port);
dout.write(server.getBytes("UTF8"));
dout.close();
return bout.toByteArray();
}
catch (IOException e)
{
throw new InternalError();
}
}
private int lexCompare(DNSRecord.Service that)
{
byte[] thisBytes = this.toByteArray();
byte[] thatBytes = that.toByteArray();
for (int i = 0, n = Math.min(thisBytes.length, thatBytes.length); i < n; i++)
{
if (thisBytes[i] > thatBytes[i])
{
return 1;
}
else
{
if (thisBytes[i] < thatBytes[i])
{
return -1;
}
}
}
return thisBytes.length - thatBytes.length;
}
boolean sameValue(DNSRecord other)
{
Service s = (Service) other;
return (priority == s.priority) && (weight == s.weight) && (port == s.port) && server.equals(s.server);
}
boolean handleQuery(JmDNSImpl dns, long expirationTime)
{
ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(name.toLowerCase());
if (info != null
&& (port != info.port || !server.equalsIgnoreCase(dns.getLocalHost().getName())))
{
// logger.finer("handleQuery() Conflicting probe detected from: " + getRecordSource());
DNSRecord.Service localService = new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV,
DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE,
DNSConstants.DNS_TTL, info.priority,
info.weight, info.port, dns.getLocalHost().getName());
// This block is useful for debugging race conditions when jmdns is respoding to
// itself.
try
{
if(dns.getInterface().equals(getRecordSource())){
// logger.warning("Got conflicting probe from ourselves\n" +
// "incoming: " +this.toString() + "\n" +
// "local : " + localService.toString());
}
}
catch (IOException e)
{
e.printStackTrace();
}
int comparison = lexCompare(localService);
if(comparison == 0){
// the 2 records are identical this probably means we are seeing our own record.
// With mutliple interfaces on a single computer it is possible to see our
// own records come in on different interfaces than the ones they were sent on.
// see section "10. Conflict Resolution" of mdns draft spec.
// logger.finer("handleQuery() Ignoring a identical service query");
return false;
}
// Tie breaker test
if (info.getState().isProbing() && comparison > 0)
{
// We lost the tie break
String oldName = info.getQualifiedName().toLowerCase();
info.setName(dns.incrementName(info.getName()));
dns.getServices().remove(oldName);
dns.getServices().put(info.getQualifiedName().toLowerCase(), info);
// logger.finer("handleQuery() Lost tie break: new unique name chosen:" + info.getName());
// We revert the state to start probing again with the new name
info.revertState();
}
else
{
// We won the tie break, so this conflicting probe should be ignored
// See paragraph 3 of section 9.2 in mdns draft spec
return false;
}
return true;
}
return false;
}
boolean handleResponse(JmDNSImpl dns)
{
ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(name.toLowerCase());
if (info != null
&& (port != info.port || !server.equalsIgnoreCase(dns.getLocalHost().getName())))
{
// logger.finer("handleResponse() Denial detected");
if (info.getState().isProbing())
{
String oldName = info.getQualifiedName().toLowerCase();
info.setName(dns.incrementName(info.getName()));
dns.getServices().remove(oldName);
dns.getServices().put(info.getQualifiedName().toLowerCase(), info);
// logger.finer("handleResponse() New unique name chose:" + info.getName());
}
info.revertState();
return true;
}
return false;
}
DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException
{
ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(name.toLowerCase());
if (info != null)
{
if (this.port == info.port != server.equals(dns.getLocalHost().getName()))
{
return dns.addAnswer(in, addr, port, out,
new DNSRecord.Service(info.getQualifiedName(), DNSConstants.TYPE_SRV,
DNSConstants.CLASS_IN | DNSConstants.CLASS_UNIQUE,
DNSConstants.DNS_TTL, info.priority,
info.weight, info.port, dns.getLocalHost().getName()));
}
}
return out;
}
public String toString()
{
return toString(server + ":" + port);
}
}
public void setRecordSource(InetAddress source)
{
this.source = source;
}
public InetAddress getRecordSource()
{
return source;
}
public String toString(String other)
{
return toString("record", ttl + "/" + getRemainingTTL(System.currentTimeMillis()) + "," + other);
}
public void setTtl(int ttl)
{
this.ttl = ttl;
}
public int getTtl()
{
return ttl;
}
}