1// Copyright 2003-2005 Arthur van Hoff, Rick Blair
2// Licensed under Apache License version 2.0
3// Original license LGPL
4
5package javax.jmdns.impl;
6
7import java.io.ByteArrayOutputStream;
8import java.io.IOException;
9import java.util.HashMap;
10import java.util.Map;
11
12import javax.jmdns.impl.constants.DNSConstants;
13import javax.jmdns.impl.constants.DNSRecordClass;
14
15/**
16 * An outgoing DNS message.
17 *
18 * @author Arthur van Hoff, Rick Blair, Werner Randelshofer
19 */
20public final class DNSOutgoing extends DNSMessage {
21
22    public static class MessageOutputStream extends ByteArrayOutputStream {
23        private final DNSOutgoing _out;
24
25        private final int         _offset;
26
27        /**
28         * Creates a new message stream, with a buffer capacity of the specified size, in bytes.
29         *
30         * @param size
31         *            the initial size.
32         * @exception IllegalArgumentException
33         *                if size is negative.
34         */
35        MessageOutputStream(int size, DNSOutgoing out) {
36            this(size, out, 0);
37        }
38
39        MessageOutputStream(int size, DNSOutgoing out, int offset) {
40            super(size);
41            _out = out;
42            _offset = offset;
43        }
44
45        void writeByte(int value) {
46            this.write(value & 0xFF);
47        }
48
49        void writeBytes(String str, int off, int len) {
50            for (int i = 0; i < len; i++) {
51                writeByte(str.charAt(off + i));
52            }
53        }
54
55        void writeBytes(byte data[]) {
56            if (data != null) {
57                writeBytes(data, 0, data.length);
58            }
59        }
60
61        void writeBytes(byte data[], int off, int len) {
62            for (int i = 0; i < len; i++) {
63                writeByte(data[off + i]);
64            }
65        }
66
67        void writeShort(int value) {
68            writeByte(value >> 8);
69            writeByte(value);
70        }
71
72        void writeInt(int value) {
73            writeShort(value >> 16);
74            writeShort(value);
75        }
76
77        void writeUTF(String str, int off, int len) {
78            // compute utf length
79            int utflen = 0;
80            for (int i = 0; i < len; i++) {
81                int ch = str.charAt(off + i);
82                if ((ch >= 0x0001) && (ch <= 0x007F)) {
83                    utflen += 1;
84                } else {
85                    if (ch > 0x07FF) {
86                        utflen += 3;
87                    } else {
88                        utflen += 2;
89                    }
90                }
91            }
92            // write utf length
93            writeByte(utflen);
94            // write utf data
95            for (int i = 0; i < len; i++) {
96                int ch = str.charAt(off + i);
97                if ((ch >= 0x0001) && (ch <= 0x007F)) {
98                    writeByte(ch);
99                } else {
100                    if (ch > 0x07FF) {
101                        writeByte(0xE0 | ((ch >> 12) & 0x0F));
102                        writeByte(0x80 | ((ch >> 6) & 0x3F));
103                        writeByte(0x80 | ((ch >> 0) & 0x3F));
104                    } else {
105                        writeByte(0xC0 | ((ch >> 6) & 0x1F));
106                        writeByte(0x80 | ((ch >> 0) & 0x3F));
107                    }
108                }
109            }
110        }
111
112        void writeName(String name) {
113            writeName(name, true);
114        }
115
116        void writeName(String name, boolean useCompression) {
117            String aName = name;
118            while (true) {
119                int n = aName.indexOf('.');
120                if (n < 0) {
121                    n = aName.length();
122                }
123                if (n <= 0) {
124                    writeByte(0);
125                    return;
126                }
127                String label = aName.substring(0, n);
128                if (useCompression && USE_DOMAIN_NAME_COMPRESSION) {
129                    Integer offset = _out._names.get(aName);
130                    if (offset != null) {
131                        int val = offset.intValue();
132                        writeByte((val >> 8) | 0xC0);
133                        writeByte(val & 0xFF);
134                        return;
135                    }
136                    _out._names.put(aName, Integer.valueOf(this.size() + _offset));
137                    writeUTF(label, 0, label.length());
138                } else {
139                    writeUTF(label, 0, label.length());
140                }
141                aName = aName.substring(n);
142                if (aName.startsWith(".")) {
143                    aName = aName.substring(1);
144                }
145            }
146        }
147
148        void writeQuestion(DNSQuestion question) {
149            writeName(question.getName());
150            writeShort(question.getRecordType().indexValue());
151            writeShort(question.getRecordClass().indexValue());
152        }
153
154        void writeRecord(DNSRecord rec, long now) {
155            writeName(rec.getName());
156            writeShort(rec.getRecordType().indexValue());
157            writeShort(rec.getRecordClass().indexValue() | ((rec.isUnique() && _out.isMulticast()) ? DNSRecordClass.CLASS_UNIQUE : 0));
158            writeInt((now == 0) ? rec.getTTL() : rec.getRemainingTTL(now));
159
160            // We need to take into account the 2 size bytes
161            MessageOutputStream record = new MessageOutputStream(512, _out, _offset + this.size() + 2);
162            rec.write(record);
163            byte[] byteArray = record.toByteArray();
164
165            writeShort(byteArray.length);
166            write(byteArray, 0, byteArray.length);
167        }
168
169    }
170
171    /**
172     * This can be used to turn off domain name compression. This was helpful for tracking problems interacting with other mdns implementations.
173     */
174    public static boolean             USE_DOMAIN_NAME_COMPRESSION = true;
175
176    Map<String, Integer>              _names;
177
178    private int                       _maxUDPPayload;
179
180    private final MessageOutputStream _questionsBytes;
181
182    private final MessageOutputStream _answersBytes;
183
184    private final MessageOutputStream _authoritativeAnswersBytes;
185
186    private final MessageOutputStream _additionalsAnswersBytes;
187
188    private final static int          HEADER_SIZE                 = 12;
189
190    /**
191     * Create an outgoing multicast query or response.
192     *
193     * @param flags
194     */
195    public DNSOutgoing(int flags) {
196        this(flags, true, DNSConstants.MAX_MSG_TYPICAL);
197    }
198
199    /**
200     * Create an outgoing query or response.
201     *
202     * @param flags
203     * @param multicast
204     */
205    public DNSOutgoing(int flags, boolean multicast) {
206        this(flags, multicast, DNSConstants.MAX_MSG_TYPICAL);
207    }
208
209    /**
210     * Create an outgoing query or response.
211     *
212     * @param flags
213     * @param multicast
214     * @param senderUDPPayload
215     *            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.
216     */
217    public DNSOutgoing(int flags, boolean multicast, int senderUDPPayload) {
218        super(flags, 0, multicast);
219        _names = new HashMap<String, Integer>();
220        _maxUDPPayload = (senderUDPPayload > 0 ? senderUDPPayload : DNSConstants.MAX_MSG_TYPICAL);
221        _questionsBytes = new MessageOutputStream(senderUDPPayload, this);
222        _answersBytes = new MessageOutputStream(senderUDPPayload, this);
223        _authoritativeAnswersBytes = new MessageOutputStream(senderUDPPayload, this);
224        _additionalsAnswersBytes = new MessageOutputStream(senderUDPPayload, this);
225    }
226
227    /**
228     * Return the number of byte available in the message.
229     *
230     * @return available space
231     */
232    public int availableSpace() {
233        return _maxUDPPayload - HEADER_SIZE - _questionsBytes.size() - _answersBytes.size() - _authoritativeAnswersBytes.size() - _additionalsAnswersBytes.size();
234    }
235
236    /**
237     * Add a question to the message.
238     *
239     * @param rec
240     * @exception IOException
241     */
242    public void addQuestion(DNSQuestion rec) throws IOException {
243        MessageOutputStream record = new MessageOutputStream(512, this);
244        record.writeQuestion(rec);
245        byte[] byteArray = record.toByteArray();
246        if (byteArray.length < this.availableSpace()) {
247            _questions.add(rec);
248            _questionsBytes.write(byteArray, 0, byteArray.length);
249        } else {
250            throw new IOException("message full");
251        }
252    }
253
254    /**
255     * Add an answer if it is not suppressed.
256     *
257     * @param in
258     * @param rec
259     * @exception IOException
260     */
261    public void addAnswer(DNSIncoming in, DNSRecord rec) throws IOException {
262        if ((in == null) || !rec.suppressedBy(in)) {
263            this.addAnswer(rec, 0);
264        }
265    }
266
267    /**
268     * Add an answer to the message.
269     *
270     * @param rec
271     * @param now
272     * @exception IOException
273     */
274    public void addAnswer(DNSRecord rec, long now) throws IOException {
275        if (rec != null) {
276            if ((now == 0) || !rec.isExpired(now)) {
277                MessageOutputStream record = new MessageOutputStream(512, this);
278                record.writeRecord(rec, now);
279                byte[] byteArray = record.toByteArray();
280                if (byteArray.length < this.availableSpace()) {
281                    _answers.add(rec);
282                    _answersBytes.write(byteArray, 0, byteArray.length);
283                } else {
284                    throw new IOException("message full");
285                }
286            }
287        }
288    }
289
290    /**
291     * Add an authoritative answer to the message.
292     *
293     * @param rec
294     * @exception IOException
295     */
296    public void addAuthorativeAnswer(DNSRecord rec) throws IOException {
297        MessageOutputStream record = new MessageOutputStream(512, this);
298        record.writeRecord(rec, 0);
299        byte[] byteArray = record.toByteArray();
300        if (byteArray.length < this.availableSpace()) {
301            _authoritativeAnswers.add(rec);
302            _authoritativeAnswersBytes.write(byteArray, 0, byteArray.length);
303        } else {
304            throw new IOException("message full");
305        }
306    }
307
308    /**
309     * Add an additional answer to the record. Omit if there is no room.
310     *
311     * @param in
312     * @param rec
313     * @exception IOException
314     */
315    public void addAdditionalAnswer(DNSIncoming in, DNSRecord rec) throws IOException {
316        MessageOutputStream record = new MessageOutputStream(512, this);
317        record.writeRecord(rec, 0);
318        byte[] byteArray = record.toByteArray();
319        if (byteArray.length < this.availableSpace()) {
320            _additionals.add(rec);
321            _additionalsAnswersBytes.write(byteArray, 0, byteArray.length);
322        } else {
323            throw new IOException("message full");
324        }
325    }
326
327    /**
328     * Builds the final message buffer to be send and returns it.
329     *
330     * @return bytes to send.
331     */
332    public byte[] data() {
333        long now = System.currentTimeMillis(); // System.currentTimeMillis()
334        _names.clear();
335
336        MessageOutputStream message = new MessageOutputStream(_maxUDPPayload, this);
337        message.writeShort(_multicast ? 0 : this.getId());
338        message.writeShort(this.getFlags());
339        message.writeShort(this.getNumberOfQuestions());
340        message.writeShort(this.getNumberOfAnswers());
341        message.writeShort(this.getNumberOfAuthorities());
342        message.writeShort(this.getNumberOfAdditionals());
343        for (DNSQuestion question : _questions) {
344            message.writeQuestion(question);
345        }
346        for (DNSRecord record : _answers) {
347            message.writeRecord(record, now);
348        }
349        for (DNSRecord record : _authoritativeAnswers) {
350            message.writeRecord(record, now);
351        }
352        for (DNSRecord record : _additionals) {
353            message.writeRecord(record, now);
354        }
355        return message.toByteArray();
356    }
357
358    @Override
359    public boolean isQuery() {
360        return (this.getFlags() & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY;
361    }
362
363    /**
364     * Debugging.
365     */
366    String print(boolean dump) {
367        StringBuilder buf = new StringBuilder();
368        buf.append(this.print());
369        if (dump) {
370            buf.append(this.print(this.data()));
371        }
372        return buf.toString();
373    }
374
375    @Override
376    public String toString() {
377        StringBuffer buf = new StringBuffer();
378        buf.append(isQuery() ? "dns[query:" : "dns[response:");
379        buf.append(" id=0x");
380        buf.append(Integer.toHexString(this.getId()));
381        if (this.getFlags() != 0) {
382            buf.append(", flags=0x");
383            buf.append(Integer.toHexString(this.getFlags()));
384            if ((this.getFlags() & DNSConstants.FLAGS_QR_RESPONSE) != 0) {
385                buf.append(":r");
386            }
387            if ((this.getFlags() & DNSConstants.FLAGS_AA) != 0) {
388                buf.append(":aa");
389            }
390            if ((this.getFlags() & DNSConstants.FLAGS_TC) != 0) {
391                buf.append(":tc");
392            }
393        }
394        if (this.getNumberOfQuestions() > 0) {
395            buf.append(", questions=");
396            buf.append(this.getNumberOfQuestions());
397        }
398        if (this.getNumberOfAnswers() > 0) {
399            buf.append(", answers=");
400            buf.append(this.getNumberOfAnswers());
401        }
402        if (this.getNumberOfAuthorities() > 0) {
403            buf.append(", authorities=");
404            buf.append(this.getNumberOfAuthorities());
405        }
406        if (this.getNumberOfAdditionals() > 0) {
407            buf.append(", additionals=");
408            buf.append(this.getNumberOfAdditionals());
409        }
410        if (this.getNumberOfQuestions() > 0) {
411            buf.append("\nquestions:");
412            for (DNSQuestion question : _questions) {
413                buf.append("\n\t");
414                buf.append(question);
415            }
416        }
417        if (this.getNumberOfAnswers() > 0) {
418            buf.append("\nanswers:");
419            for (DNSRecord record : _answers) {
420                buf.append("\n\t");
421                buf.append(record);
422            }
423        }
424        if (this.getNumberOfAuthorities() > 0) {
425            buf.append("\nauthorities:");
426            for (DNSRecord record : _authoritativeAnswers) {
427                buf.append("\n\t");
428                buf.append(record);
429            }
430        }
431        if (this.getNumberOfAdditionals() > 0) {
432            buf.append("\nadditionals:");
433            for (DNSRecord record : _additionals) {
434                buf.append("\n\t");
435                buf.append(record);
436            }
437        }
438        buf.append("\nnames=");
439        buf.append(_names);
440        buf.append("]");
441        return buf.toString();
442    }
443
444    /**
445     * @return the maxUDPPayload
446     */
447    public int getMaxUDPPayload() {
448        return this._maxUDPPayload;
449    }
450
451}
452