///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.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; //import java.util.logging.Level; //import java.util.logging.Logger; /** * Parse an incoming DNS message into its components. * * @version %I%, %G% * @author Arthur van Hoff, Werner Randelshofer, Pierre Frisch, Daniel Bobbert */ public final class DNSIncoming { // private static Logger logger = Logger.getLogger(DNSIncoming.class.getName()); // This is a hack to handle a bug in the BonjourConformanceTest // It is sending out target strings that don't follow the "domain name" // format. public static boolean USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET = true; // Implementation note: This vector should be immutable. // If a client of DNSIncoming changes the contents of this vector, // we get undesired results. To fix this, we have to migrate to // the Collections API of Java 1.2. i.e we replace Vector by List. // final static Vector EMPTY = new Vector(); private DatagramPacket packet; private int off; private int len; private byte data[]; int id; private int flags; private int numQuestions; int numAnswers; private int numAuthorities; private int numAdditionals; private long receivedTime; private List questions; List answers; /** * Parse a message from a datagram packet. */ DNSIncoming(DatagramPacket packet) throws IOException { this.packet = packet; InetAddress source = packet.getAddress(); this.data = packet.getData(); this.len = packet.getLength(); this.off = packet.getOffset(); this.questions = Collections.EMPTY_LIST; this.answers = Collections.EMPTY_LIST; this.receivedTime = System.currentTimeMillis(); try { id = readUnsignedShort(); flags = readUnsignedShort(); numQuestions = readUnsignedShort(); numAnswers = readUnsignedShort(); numAuthorities = readUnsignedShort(); numAdditionals = readUnsignedShort(); // parse questions if (numQuestions > 0) { questions = Collections.synchronizedList(new ArrayList(numQuestions)); for (int i = 0; i < numQuestions; i++) { DNSQuestion question = new DNSQuestion(readName(), readUnsignedShort(), readUnsignedShort()); questions.add(question); } } // parse answers int n = numAnswers + numAuthorities + numAdditionals; if (n > 0) { answers = Collections.synchronizedList(new ArrayList(n)); for (int i = 0; i < n; i++) { String domain = readName(); int type = readUnsignedShort(); int clazz = readUnsignedShort(); int ttl = readInt(); int len = readUnsignedShort(); int end = off + len; DNSRecord rec = null; switch (type) { case DNSConstants.TYPE_A: // IPv4 case DNSConstants.TYPE_AAAA: // IPv6 FIXME [PJYF Oct 14 2004] This has not been tested rec = new DNSRecord.Address(domain, type, clazz, ttl, readBytes(off, len)); break; case DNSConstants.TYPE_CNAME: case DNSConstants.TYPE_PTR: String service = ""; try { service = readName(); } catch (IOException e){ // there was a problem reading the service name e.printStackTrace(); } rec = new DNSRecord.Pointer(domain, type, clazz, ttl, service); break; case DNSConstants.TYPE_TXT: rec = new DNSRecord.Text(domain, type, clazz, ttl, readBytes(off, len)); break; case DNSConstants.TYPE_SRV: int priority = readUnsignedShort(); int weight = readUnsignedShort(); int port = readUnsignedShort(); String target = ""; try { // This is a hack to handle a bug in the BonjourConformanceTest // It is sending out target strings that don't follow the "domain name" // format. if(USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET){ target = readName(); } else { target = readNonNameString(); } } catch (IOException e) { // this can happen if the type of the label // cannot be handled. // down below the offset gets advanced to the end // of the record e.printStackTrace(); } rec = new DNSRecord.Service(domain, type, clazz, ttl, priority, weight, port, target); break; case DNSConstants.TYPE_HINFO: // Maybe we should do something with those break; default : // logger.finer("DNSIncoming() unknown type:" + type); break; } if (rec != null) { rec.setRecordSource(source); // Add a record, if we were able to create one. answers.add(rec); } else { // Addjust the numbers for the skipped record if (answers.size() < numAnswers) { numAnswers--; } else { if (answers.size() < numAnswers + numAuthorities) { numAuthorities--; } else { if (answers.size() < numAnswers + numAuthorities + numAdditionals) { numAdditionals--; } } } } off = end; } } } catch (IOException e) { // logger.log(Level.WARNING, "DNSIncoming() dump " + print(true) + "\n exception ", e); throw e; } } /** * Check if the message is a query. */ boolean isQuery() { return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY; } /** * Check if the message is truncated. */ public boolean isTruncated() { return (flags & DNSConstants.FLAGS_TC) != 0; } /** * Check if the message is a response. */ boolean isResponse() { return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_RESPONSE; } private int get(int off) throws IOException { if ((off < 0) || (off >= len)) { throw new IOException("parser error: offset=" + off); } return data[off] & 0xFF; } private int readUnsignedShort() throws IOException { return (get(off++) << 8) + get(off++); } private int readInt() throws IOException { return (readUnsignedShort() << 16) + readUnsignedShort(); } private byte[] readBytes(int off, int len) throws IOException { byte bytes[] = new byte[len]; System.arraycopy(data, off, bytes, 0, len); return bytes; } private void readUTF(StringBuffer buf, int off, int len) throws IOException { for (int end = off + len; off < end;) { int ch = get(off++); switch (ch >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: // 0xxxxxxx break; case 12: case 13: // 110x xxxx 10xx xxxx ch = ((ch & 0x1F) << 6) | (get(off++) & 0x3F); break; case 14: // 1110 xxxx 10xx xxxx 10xx xxxx ch = ((ch & 0x0f) << 12) | ((get(off++) & 0x3F) << 6) | (get(off++) & 0x3F); break; default: // 10xx xxxx, 1111 xxxx ch = ((ch & 0x3F) << 4) | (get(off++) & 0x0f); break; } buf.append((char) ch); } } private String readNonNameString() throws IOException { StringBuffer buf = new StringBuffer(); int off = this.off; int len = get(off++); readUTF(buf, off, len); return buf.toString(); } private String readName() throws IOException { StringBuffer buf = new StringBuffer(); int off = this.off; int next = -1; int first = off; while (true) { int len = get(off++); if (len == 0) { break; } switch (len & 0xC0) { case 0x00: //buf.append("[" + off + "]"); readUTF(buf, off, len); off += len; buf.append('.'); break; case 0xC0: //buf.append("<" + (off - 1) + ">"); if (next < 0) { next = off + 1; } off = ((len & 0x3F) << 8) | get(off++); if (off >= first) { throw new IOException("bad domain name: possible circular name detected." + " name start: " + first + " bad offset: 0x" + Integer.toHexString(off)); } first = off; break; default: throw new IOException("unsupported dns label type: '" + Integer.toHexString(len & 0xC0) +"' at " + (off-1)); } } this.off = (next >= 0) ? next : off; return buf.toString(); } /** * Debugging. */ String print(boolean dump) { StringBuffer buf = new StringBuffer(); buf.append(toString() + "\n"); for (Iterator iterator = questions.iterator(); iterator.hasNext();) { buf.append(" ques:" + iterator.next() + "\n"); } int count = 0; for (Iterator iterator = answers.iterator(); iterator.hasNext(); count++) { if (count < numAnswers) { buf.append(" answ:"); } else { if (count < numAnswers + numAuthorities) { buf.append(" auth:"); } else { buf.append(" addi:"); } } buf.append(iterator.next() + "\n"); } if (dump) { for (int off = 0, len = packet.getLength(); off < len; off += 32) { int n = Math.min(32, len - off); if (off < 10) { buf.append(' '); } if (off < 100) { buf.append(' '); } buf.append(off); buf.append(':'); for (int i = 0; i < n; i++) { if ((i % 8) == 0) { buf.append(' '); } buf.append(Integer.toHexString((data[off + i] & 0xF0) >> 4)); buf.append(Integer.toHexString((data[off + i] & 0x0F) >> 0)); } buf.append("\n"); buf.append(" "); for (int i = 0; i < n; i++) { if ((i % 8) == 0) { buf.append(' '); } buf.append(' '); int ch = data[off + i] & 0xFF; buf.append(((ch > ' ') && (ch < 127)) ? (char) ch : '.'); } buf.append("\n"); // limit message size if (off + 32 >= 256) { buf.append("....\n"); break; } } } return buf.toString(); } public String toString() { StringBuffer buf = new StringBuffer(); buf.append(isQuery() ? "dns[query," : "dns[response,"); if (packet.getAddress() != null) { buf.append(packet.getAddress().getHostAddress()); } buf.append(':'); buf.append(packet.getPort()); buf.append(",len="); buf.append(packet.getLength()); buf.append(",id=0x"); buf.append(Integer.toHexString(id)); if (flags != 0) { buf.append(",flags=0x"); buf.append(Integer.toHexString(flags)); if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0) { buf.append(":r"); } if ((flags & DNSConstants.FLAGS_AA) != 0) { buf.append(":aa"); } if ((flags & DNSConstants.FLAGS_TC) != 0) { buf.append(":tc"); } } if (numQuestions > 0) { buf.append(",questions="); buf.append(numQuestions); } if (numAnswers > 0) { buf.append(",answers="); buf.append(numAnswers); } if (numAuthorities > 0) { buf.append(",authorities="); buf.append(numAuthorities); } if (numAdditionals > 0) { buf.append(",additionals="); buf.append(numAdditionals); } buf.append("]"); return buf.toString(); } /** * Appends answers to this Incoming. * * @throws IllegalArgumentException If not a query or if Truncated. */ void append(DNSIncoming that) { if (this.isQuery() && this.isTruncated() && that.isQuery()) { if (that.numQuestions > 0) { if (Collections.EMPTY_LIST.equals(this.questions)) this.questions = Collections.synchronizedList(new ArrayList(that.numQuestions)); this.questions.addAll(that.questions); this.numQuestions += that.numQuestions; } if (Collections.EMPTY_LIST.equals(answers)) { answers = Collections.synchronizedList(new ArrayList()); } if (that.numAnswers > 0) { this.answers.addAll(this.numAnswers, that.answers.subList(0, that.numAnswers)); this.numAnswers += that.numAnswers; } if (that.numAuthorities > 0) { this.answers.addAll(this.numAnswers + this.numAuthorities, that.answers.subList(that.numAnswers, that.numAnswers + that.numAuthorities)); this.numAuthorities += that.numAuthorities; } if (that.numAdditionals > 0) { this.answers.addAll(that.answers.subList(that.numAnswers + that.numAuthorities, that.numAnswers + that.numAuthorities + that.numAdditionals)); this.numAdditionals += that.numAdditionals; } } else { throw new IllegalArgumentException(); } } public int elapseSinceArrival() { return (int) (System.currentTimeMillis() - receivedTime); } public List getQuestions() { return questions; } public List getAnswers() { return answers; } }