// 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.IOException; import java.util.HashMap; import java.util.Map; import javax.jmdns.impl.constants.DNSConstants; import javax.jmdns.impl.constants.DNSRecordClass; /** * An outgoing DNS message. * * @author Arthur van Hoff, Rick Blair, Werner Randelshofer */ public final class DNSOutgoing extends DNSMessage { public static class MessageOutputStream extends ByteArrayOutputStream { private final DNSOutgoing _out; private final int _offset; /** * Creates a new message stream, with a buffer capacity of the specified size, in bytes. * * @param size * the initial size. * @exception IllegalArgumentException * if size is negative. */ MessageOutputStream(int size, DNSOutgoing out) { this(size, out, 0); } MessageOutputStream(int size, DNSOutgoing out, int offset) { super(size); _out = out; _offset = offset; } void writeByte(int value) { this.write(value & 0xFF); } void writeBytes(String str, int off, int len) { for (int i = 0; i < len; i++) { writeByte(str.charAt(off + i)); } } void writeBytes(byte data[]) { if (data != null) { writeBytes(data, 0, data.length); } } void writeBytes(byte data[], int off, int len) { for (int i = 0; i < len; i++) { writeByte(data[off + i]); } } void writeShort(int value) { writeByte(value >> 8); writeByte(value); } void writeInt(int value) { writeShort(value >> 16); writeShort(value); } void writeUTF(String str, int off, int len) { // compute utf length int utflen = 0; for (int i = 0; i < len; i++) { int ch = str.charAt(off + i); if ((ch >= 0x0001) && (ch <= 0x007F)) { utflen += 1; } else { if (ch > 0x07FF) { utflen += 3; } else { utflen += 2; } } } // write utf length writeByte(utflen); // write utf data for (int i = 0; i < len; i++) { int ch = str.charAt(off + i); if ((ch >= 0x0001) && (ch <= 0x007F)) { writeByte(ch); } else { if (ch > 0x07FF) { writeByte(0xE0 | ((ch >> 12) & 0x0F)); writeByte(0x80 | ((ch >> 6) & 0x3F)); writeByte(0x80 | ((ch >> 0) & 0x3F)); } else { writeByte(0xC0 | ((ch >> 6) & 0x1F)); writeByte(0x80 | ((ch >> 0) & 0x3F)); } } } } void writeName(String name) { writeName(name, true); } void writeName(String name, boolean useCompression) { String aName = name; while (true) { int n = aName.indexOf('.'); if (n < 0) { n = aName.length(); } if (n <= 0) { writeByte(0); return; } String label = aName.substring(0, n); if (useCompression && USE_DOMAIN_NAME_COMPRESSION) { Integer offset = _out._names.get(aName); if (offset != null) { int val = offset.intValue(); writeByte((val >> 8) | 0xC0); writeByte(val & 0xFF); return; } _out._names.put(aName, Integer.valueOf(this.size() + _offset)); writeUTF(label, 0, label.length()); } else { writeUTF(label, 0, label.length()); } aName = aName.substring(n); if (aName.startsWith(".")) { aName = aName.substring(1); } } } void writeQuestion(DNSQuestion question) { writeName(question.getName()); writeShort(question.getRecordType().indexValue()); writeShort(question.getRecordClass().indexValue()); } void writeRecord(DNSRecord rec, long now) { writeName(rec.getName()); writeShort(rec.getRecordType().indexValue()); writeShort(rec.getRecordClass().indexValue() | ((rec.isUnique() && _out.isMulticast()) ? DNSRecordClass.CLASS_UNIQUE : 0)); writeInt((now == 0) ? rec.getTTL() : rec.getRemainingTTL(now)); // We need to take into account the 2 size bytes MessageOutputStream record = new MessageOutputStream(512, _out, _offset + this.size() + 2); rec.write(record); byte[] byteArray = record.toByteArray(); writeShort(byteArray.length); write(byteArray, 0, byteArray.length); } } /** * This can be used to turn off domain name compression. This was helpful for tracking problems interacting with other mdns implementations. */ public static boolean USE_DOMAIN_NAME_COMPRESSION = true; Map<String, Integer> _names; private int _maxUDPPayload; private final MessageOutputStream _questionsBytes; private final MessageOutputStream _answersBytes; private final MessageOutputStream _authoritativeAnswersBytes; private final MessageOutputStream _additionalsAnswersBytes; private final static int HEADER_SIZE = 12; /** * Create an outgoing multicast query or response. * * @param flags */ public DNSOutgoing(int flags) { this(flags, true, DNSConstants.MAX_MSG_TYPICAL); } /** * Create an outgoing query or response. * * @param flags * @param multicast */ public DNSOutgoing(int flags, boolean multicast) { this(flags, multicast, DNSConstants.MAX_MSG_TYPICAL); } /** * Create an outgoing query or response. * * @param flags * @param multicast * @param senderUDPPayload * The sender's UDP payload size is the number of bytes of the largest UDP payload that can be reassembled and delivered in the sender's network stack. */ public DNSOutgoing(int flags, boolean multicast, int senderUDPPayload) { super(flags, 0, multicast); _names = new HashMap<String, Integer>(); _maxUDPPayload = (senderUDPPayload > 0 ? senderUDPPayload : DNSConstants.MAX_MSG_TYPICAL); _questionsBytes = new MessageOutputStream(senderUDPPayload, this); _answersBytes = new MessageOutputStream(senderUDPPayload, this); _authoritativeAnswersBytes = new MessageOutputStream(senderUDPPayload, this); _additionalsAnswersBytes = new MessageOutputStream(senderUDPPayload, this); } /** * Return the number of byte available in the message. * * @return available space */ public int availableSpace() { return _maxUDPPayload - HEADER_SIZE - _questionsBytes.size() - _answersBytes.size() - _authoritativeAnswersBytes.size() - _additionalsAnswersBytes.size(); } /** * Add a question to the message. * * @param rec * @exception IOException */ public void addQuestion(DNSQuestion rec) throws IOException { MessageOutputStream record = new MessageOutputStream(512, this); record.writeQuestion(rec); byte[] byteArray = record.toByteArray(); if (byteArray.length < this.availableSpace()) { _questions.add(rec); _questionsBytes.write(byteArray, 0, byteArray.length); } else { throw new IOException("message full"); } } /** * Add an answer if it is not suppressed. * * @param in * @param rec * @exception IOException */ public void addAnswer(DNSIncoming in, DNSRecord rec) throws IOException { if ((in == null) || !rec.suppressedBy(in)) { this.addAnswer(rec, 0); } } /** * Add an answer to the message. * * @param rec * @param now * @exception IOException */ public void addAnswer(DNSRecord rec, long now) throws IOException { if (rec != null) { if ((now == 0) || !rec.isExpired(now)) { MessageOutputStream record = new MessageOutputStream(512, this); record.writeRecord(rec, now); byte[] byteArray = record.toByteArray(); if (byteArray.length < this.availableSpace()) { _answers.add(rec); _answersBytes.write(byteArray, 0, byteArray.length); } else { throw new IOException("message full"); } } } } /** * Add an authoritative answer to the message. * * @param rec * @exception IOException */ public void addAuthorativeAnswer(DNSRecord rec) throws IOException { MessageOutputStream record = new MessageOutputStream(512, this); record.writeRecord(rec, 0); byte[] byteArray = record.toByteArray(); if (byteArray.length < this.availableSpace()) { _authoritativeAnswers.add(rec); _authoritativeAnswersBytes.write(byteArray, 0, byteArray.length); } else { throw new IOException("message full"); } } /** * Add an additional answer to the record. Omit if there is no room. * * @param in * @param rec * @exception IOException */ public void addAdditionalAnswer(DNSIncoming in, DNSRecord rec) throws IOException { MessageOutputStream record = new MessageOutputStream(512, this); record.writeRecord(rec, 0); byte[] byteArray = record.toByteArray(); if (byteArray.length < this.availableSpace()) { _additionals.add(rec); _additionalsAnswersBytes.write(byteArray, 0, byteArray.length); } else { throw new IOException("message full"); } } /** * Builds the final message buffer to be send and returns it. * * @return bytes to send. */ public byte[] data() { long now = System.currentTimeMillis(); // System.currentTimeMillis() _names.clear(); MessageOutputStream message = new MessageOutputStream(_maxUDPPayload, this); message.writeShort(_multicast ? 0 : this.getId()); message.writeShort(this.getFlags()); message.writeShort(this.getNumberOfQuestions()); message.writeShort(this.getNumberOfAnswers()); message.writeShort(this.getNumberOfAuthorities()); message.writeShort(this.getNumberOfAdditionals()); for (DNSQuestion question : _questions) { message.writeQuestion(question); } for (DNSRecord record : _answers) { message.writeRecord(record, now); } for (DNSRecord record : _authoritativeAnswers) { message.writeRecord(record, now); } for (DNSRecord record : _additionals) { message.writeRecord(record, now); } return message.toByteArray(); } @Override public boolean isQuery() { return (this.getFlags() & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY; } /** * Debugging. */ String print(boolean dump) { StringBuilder buf = new StringBuilder(); buf.append(this.print()); if (dump) { buf.append(this.print(this.data())); } return buf.toString(); } @Override public String toString() { StringBuffer buf = new StringBuffer(); buf.append(isQuery() ? "dns[query:" : "dns[response:"); buf.append(" id=0x"); buf.append(Integer.toHexString(this.getId())); if (this.getFlags() != 0) { buf.append(", flags=0x"); buf.append(Integer.toHexString(this.getFlags())); if ((this.getFlags() & DNSConstants.FLAGS_QR_RESPONSE) != 0) { buf.append(":r"); } if ((this.getFlags() & DNSConstants.FLAGS_AA) != 0) { buf.append(":aa"); } if ((this.getFlags() & DNSConstants.FLAGS_TC) != 0) { buf.append(":tc"); } } if (this.getNumberOfQuestions() > 0) { buf.append(", questions="); buf.append(this.getNumberOfQuestions()); } if (this.getNumberOfAnswers() > 0) { buf.append(", answers="); buf.append(this.getNumberOfAnswers()); } if (this.getNumberOfAuthorities() > 0) { buf.append(", authorities="); buf.append(this.getNumberOfAuthorities()); } if (this.getNumberOfAdditionals() > 0) { buf.append(", additionals="); buf.append(this.getNumberOfAdditionals()); } if (this.getNumberOfQuestions() > 0) { buf.append("\nquestions:"); for (DNSQuestion question : _questions) { buf.append("\n\t"); buf.append(question); } } if (this.getNumberOfAnswers() > 0) { buf.append("\nanswers:"); for (DNSRecord record : _answers) { buf.append("\n\t"); buf.append(record); } } if (this.getNumberOfAuthorities() > 0) { buf.append("\nauthorities:"); for (DNSRecord record : _authoritativeAnswers) { buf.append("\n\t"); buf.append(record); } } if (this.getNumberOfAdditionals() > 0) { buf.append("\nadditionals:"); for (DNSRecord record : _additionals) { buf.append("\n\t"); buf.append(record); } } buf.append("\nnames="); buf.append(_names); buf.append("]"); return buf.toString(); } /** * @return the maxUDPPayload */ public int getMaxUDPPayload() { return this._maxUDPPayload; } }