//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; } }