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.ByteArrayInputStream;
8import java.io.IOException;
9import java.net.DatagramPacket;
10import java.net.InetAddress;
11import java.util.HashMap;
12import java.util.Map;
13import java.util.logging.Level;
14import java.util.logging.Logger;
15
16import javax.jmdns.impl.constants.DNSConstants;
17import javax.jmdns.impl.constants.DNSLabel;
18import javax.jmdns.impl.constants.DNSOptionCode;
19import javax.jmdns.impl.constants.DNSRecordClass;
20import javax.jmdns.impl.constants.DNSRecordType;
21import javax.jmdns.impl.constants.DNSResultCode;
22
23/**
24 * Parse an incoming DNS message into its components.
25 *
26 * @author Arthur van Hoff, Werner Randelshofer, Pierre Frisch, Daniel Bobbert
27 */
28public final class DNSIncoming extends DNSMessage {
29    private static Logger logger                                = Logger.getLogger(DNSIncoming.class.getName());
30
31    // This is a hack to handle a bug in the BonjourConformanceTest
32    // It is sending out target strings that don't follow the "domain name" format.
33    public static boolean USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET = true;
34
35    public static class MessageInputStream extends ByteArrayInputStream {
36        private static Logger      logger1 = Logger.getLogger(MessageInputStream.class.getName());
37
38        final Map<Integer, String> _names;
39
40        public MessageInputStream(byte[] buffer, int length) {
41            this(buffer, 0, length);
42        }
43
44        /**
45         * @param buffer
46         * @param offset
47         * @param length
48         */
49        public MessageInputStream(byte[] buffer, int offset, int length) {
50            super(buffer, offset, length);
51            _names = new HashMap<Integer, String>();
52        }
53
54        public int readByte() {
55            return this.read();
56        }
57
58        public int readUnsignedShort() {
59            return (this.read() << 8) | this.read();
60        }
61
62        public int readInt() {
63            return (this.readUnsignedShort() << 16) | this.readUnsignedShort();
64        }
65
66        public byte[] readBytes(int len) {
67            byte bytes[] = new byte[len];
68            this.read(bytes, 0, len);
69            return bytes;
70        }
71
72        public String readUTF(int len) {
73            StringBuilder buffer = new StringBuilder(len);
74            for (int index = 0; index < len; index++) {
75                int ch = this.read();
76                switch (ch >> 4) {
77                    case 0:
78                    case 1:
79                    case 2:
80                    case 3:
81                    case 4:
82                    case 5:
83                    case 6:
84                    case 7:
85                        // 0xxxxxxx
86                        break;
87                    case 12:
88                    case 13:
89                        // 110x xxxx 10xx xxxx
90                        ch = ((ch & 0x1F) << 6) | (this.read() & 0x3F);
91                        index++;
92                        break;
93                    case 14:
94                        // 1110 xxxx 10xx xxxx 10xx xxxx
95                        ch = ((ch & 0x0f) << 12) | ((this.read() & 0x3F) << 6) | (this.read() & 0x3F);
96                        index++;
97                        index++;
98                        break;
99                    default:
100                        // 10xx xxxx, 1111 xxxx
101                        ch = ((ch & 0x3F) << 4) | (this.read() & 0x0f);
102                        index++;
103                        break;
104                }
105                buffer.append((char) ch);
106            }
107            return buffer.toString();
108        }
109
110        protected synchronized int peek() {
111            return (pos < count) ? (buf[pos] & 0xff) : -1;
112        }
113
114        public String readName() {
115            Map<Integer, StringBuilder> names = new HashMap<Integer, StringBuilder>();
116            StringBuilder buffer = new StringBuilder();
117            boolean finished = false;
118            while (!finished) {
119                int len = this.read();
120                if (len == 0) {
121                    finished = true;
122                    break;
123                }
124                switch (DNSLabel.labelForByte(len)) {
125                    case Standard:
126                        int offset = pos - 1;
127                        String label = this.readUTF(len) + ".";
128                        buffer.append(label);
129                        for (StringBuilder previousLabel : names.values()) {
130                            previousLabel.append(label);
131                        }
132                        names.put(Integer.valueOf(offset), new StringBuilder(label));
133                        break;
134                    case Compressed:
135                        int index = (DNSLabel.labelValue(len) << 8) | this.read();
136                        String compressedLabel = _names.get(Integer.valueOf(index));
137                        if (compressedLabel == null) {
138                            logger1.severe("bad domain name: possible circular name detected. Bad offset: 0x" + Integer.toHexString(index) + " at 0x" + Integer.toHexString(pos - 2));
139                            compressedLabel = "";
140                        }
141                        buffer.append(compressedLabel);
142                        for (StringBuilder previousLabel : names.values()) {
143                            previousLabel.append(compressedLabel);
144                        }
145                        finished = true;
146                        break;
147                    case Extended:
148                        // int extendedLabelClass = DNSLabel.labelValue(len);
149                        logger1.severe("Extended label are not currently supported.");
150                        break;
151                    case Unknown:
152                    default:
153                        logger1.severe("unsupported dns label type: '" + Integer.toHexString(len & 0xC0) + "'");
154                }
155            }
156            for (Integer index : names.keySet()) {
157                _names.put(index, names.get(index).toString());
158            }
159            return buffer.toString();
160        }
161
162        public String readNonNameString() {
163            int len = this.read();
164            return this.readUTF(len);
165        }
166
167    }
168
169    private final DatagramPacket     _packet;
170
171    private final long               _receivedTime;
172
173    private final MessageInputStream _messageInputStream;
174
175    private int                      _senderUDPPayload;
176
177    /**
178     * Parse a message from a datagram packet.
179     *
180     * @param packet
181     * @exception IOException
182     */
183    public DNSIncoming(DatagramPacket packet) throws IOException {
184        super(0, 0, packet.getPort() == DNSConstants.MDNS_PORT);
185        this._packet = packet;
186        InetAddress source = packet.getAddress();
187        this._messageInputStream = new MessageInputStream(packet.getData(), packet.getLength());
188        this._receivedTime = System.currentTimeMillis();
189        this._senderUDPPayload = DNSConstants.MAX_MSG_TYPICAL;
190
191        try {
192            this.setId(_messageInputStream.readUnsignedShort());
193            this.setFlags(_messageInputStream.readUnsignedShort());
194            int numQuestions = _messageInputStream.readUnsignedShort();
195            int numAnswers = _messageInputStream.readUnsignedShort();
196            int numAuthorities = _messageInputStream.readUnsignedShort();
197            int numAdditionals = _messageInputStream.readUnsignedShort();
198
199            // parse questions
200            if (numQuestions > 0) {
201                for (int i = 0; i < numQuestions; i++) {
202                    _questions.add(this.readQuestion());
203                }
204            }
205
206            // parse answers
207            if (numAnswers > 0) {
208                for (int i = 0; i < numAnswers; i++) {
209                    DNSRecord rec = this.readAnswer(source);
210                    if (rec != null) {
211                        // Add a record, if we were able to create one.
212                        _answers.add(rec);
213                    }
214                }
215            }
216
217            if (numAuthorities > 0) {
218                for (int i = 0; i < numAuthorities; i++) {
219                    DNSRecord rec = this.readAnswer(source);
220                    if (rec != null) {
221                        // Add a record, if we were able to create one.
222                        _authoritativeAnswers.add(rec);
223                    }
224                }
225            }
226
227            if (numAdditionals > 0) {
228                for (int i = 0; i < numAdditionals; i++) {
229                    DNSRecord rec = this.readAnswer(source);
230                    if (rec != null) {
231                        // Add a record, if we were able to create one.
232                        _additionals.add(rec);
233                    }
234                }
235            }
236        } catch (Exception e) {
237            logger.log(Level.WARNING, "DNSIncoming() dump " + print(true) + "\n exception ", e);
238            // This ugly but some JVM don't implement the cause on IOException
239            IOException ioe = new IOException("DNSIncoming corrupted message");
240            ioe.initCause(e);
241            throw ioe;
242        }
243    }
244
245    private DNSIncoming(int flags, int id, boolean multicast, DatagramPacket packet, long receivedTime) {
246        super(flags, id, multicast);
247        this._packet = packet;
248        this._messageInputStream = new MessageInputStream(packet.getData(), packet.getLength());
249        this._receivedTime = receivedTime;
250    }
251
252
253    /*
254     * (non-Javadoc)
255     *
256     * @see java.lang.Object#clone()
257     */
258    @Override
259    public DNSIncoming clone() {
260        DNSIncoming in = new DNSIncoming(this.getFlags(), this.getId(), this.isMulticast(), this._packet, this._receivedTime);
261         in._senderUDPPayload = this._senderUDPPayload;
262         in._questions.addAll(this._questions);
263         in._answers.addAll(this._answers);
264         in._authoritativeAnswers.addAll(this._authoritativeAnswers);
265         in._additionals.addAll(this._additionals);
266         return in;
267    }
268
269
270    private DNSQuestion readQuestion() {
271        String domain = _messageInputStream.readName();
272        DNSRecordType type = DNSRecordType.typeForIndex(_messageInputStream.readUnsignedShort());
273        if (type == DNSRecordType.TYPE_IGNORE) {
274            logger.log(Level.SEVERE, "Could not find record type: " + this.print(true));
275        }
276        int recordClassIndex = _messageInputStream.readUnsignedShort();
277        DNSRecordClass recordClass = DNSRecordClass.classForIndex(recordClassIndex);
278        boolean unique = recordClass.isUnique(recordClassIndex);
279        return DNSQuestion.newQuestion(domain, type, recordClass, unique);
280    }
281
282    private DNSRecord readAnswer(InetAddress source) {
283        String domain = _messageInputStream.readName();
284        DNSRecordType type = DNSRecordType.typeForIndex(_messageInputStream.readUnsignedShort());
285        if (type == DNSRecordType.TYPE_IGNORE) {
286            logger.log(Level.SEVERE, "Could not find record type. domain: " + domain + "\n" + this.print(true));
287        }
288        int recordClassIndex = _messageInputStream.readUnsignedShort();
289        DNSRecordClass recordClass = (type == DNSRecordType.TYPE_OPT ? DNSRecordClass.CLASS_UNKNOWN : DNSRecordClass.classForIndex(recordClassIndex));
290        if ((recordClass == DNSRecordClass.CLASS_UNKNOWN) && (type != DNSRecordType.TYPE_OPT)) {
291            logger.log(Level.SEVERE, "Could not find record class. domain: " + domain + " type: " + type + "\n" + this.print(true));
292        }
293        boolean unique = recordClass.isUnique(recordClassIndex);
294        int ttl = _messageInputStream.readInt();
295        int len = _messageInputStream.readUnsignedShort();
296        DNSRecord rec = null;
297
298        switch (type) {
299            case TYPE_A: // IPv4
300                rec = new DNSRecord.IPv4Address(domain, recordClass, unique, ttl, _messageInputStream.readBytes(len));
301                break;
302            case TYPE_AAAA: // IPv6
303                rec = new DNSRecord.IPv6Address(domain, recordClass, unique, ttl, _messageInputStream.readBytes(len));
304                break;
305            case TYPE_CNAME:
306            case TYPE_PTR:
307                String service = "";
308                service = _messageInputStream.readName();
309                if (service.length() > 0) {
310                    rec = new DNSRecord.Pointer(domain, recordClass, unique, ttl, service);
311                } else {
312                    logger.log(Level.WARNING, "PTR record of class: " + recordClass + ", there was a problem reading the service name of the answer for domain:" + domain);
313                }
314                break;
315            case TYPE_TXT:
316                rec = new DNSRecord.Text(domain, recordClass, unique, ttl, _messageInputStream.readBytes(len));
317                break;
318            case TYPE_SRV:
319                int priority = _messageInputStream.readUnsignedShort();
320                int weight = _messageInputStream.readUnsignedShort();
321                int port = _messageInputStream.readUnsignedShort();
322                String target = "";
323                // This is a hack to handle a bug in the BonjourConformanceTest
324                // It is sending out target strings that don't follow the "domain name" format.
325                if (USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET) {
326                    target = _messageInputStream.readName();
327                } else {
328                    // [PJYF Nov 13 2010] Do we still need this? This looks really bad. All label are supposed to start by a length.
329                    target = _messageInputStream.readNonNameString();
330                }
331                rec = new DNSRecord.Service(domain, recordClass, unique, ttl, priority, weight, port, target);
332                break;
333            case TYPE_HINFO:
334                StringBuilder buf = new StringBuilder();
335                buf.append(_messageInputStream.readUTF(len));
336                int index = buf.indexOf(" ");
337                String cpu = (index > 0 ? buf.substring(0, index) : buf.toString()).trim();
338                String os = (index > 0 ? buf.substring(index + 1) : "").trim();
339                rec = new DNSRecord.HostInformation(domain, recordClass, unique, ttl, cpu, os);
340                break;
341            case TYPE_OPT:
342                DNSResultCode extendedResultCode = DNSResultCode.resultCodeForFlags(this.getFlags(), ttl);
343                int version = (ttl & 0x00ff0000) >> 16;
344                if (version == 0) {
345                    _senderUDPPayload = recordClassIndex;
346                    while (_messageInputStream.available() > 0) {
347                        // Read RDData
348                        int optionCodeInt = 0;
349                        DNSOptionCode optionCode = null;
350                        if (_messageInputStream.available() >= 2) {
351                            optionCodeInt = _messageInputStream.readUnsignedShort();
352                            optionCode = DNSOptionCode.resultCodeForFlags(optionCodeInt);
353                        } else {
354                            logger.log(Level.WARNING, "There was a problem reading the OPT record. Ignoring.");
355                            break;
356                        }
357                        int optionLength = 0;
358                        if (_messageInputStream.available() >= 2) {
359                            optionLength = _messageInputStream.readUnsignedShort();
360                        } else {
361                            logger.log(Level.WARNING, "There was a problem reading the OPT record. Ignoring.");
362                            break;
363                        }
364                        byte[] optiondata = new byte[0];
365                        if (_messageInputStream.available() >= optionLength) {
366                            optiondata = _messageInputStream.readBytes(optionLength);
367                        }
368                        //
369                        // We should really do something with those options.
370                        switch (optionCode) {
371                            case Owner:
372                                // Valid length values are 8, 14, 18 and 20
373                                // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
374                                // |Opt|Len|V|S|Primary MAC|Wakeup MAC | Password |
375                                // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
376                                //
377                                int ownerVersion = 0;
378                                int ownerSequence = 0;
379                                byte[] ownerPrimaryMacAddress = null;
380                                byte[] ownerWakeupMacAddress = null;
381                                byte[] ownerPassword = null;
382                                try {
383                                    ownerVersion = optiondata[0];
384                                    ownerSequence = optiondata[1];
385                                    ownerPrimaryMacAddress = new byte[] { optiondata[2], optiondata[3], optiondata[4], optiondata[5], optiondata[6], optiondata[7] };
386                                    ownerWakeupMacAddress = ownerPrimaryMacAddress;
387                                    if (optiondata.length > 8) {
388                                        // We have a wakeupMacAddress.
389                                        ownerWakeupMacAddress = new byte[] { optiondata[8], optiondata[9], optiondata[10], optiondata[11], optiondata[12], optiondata[13] };
390                                    }
391                                    if (optiondata.length == 18) {
392                                        // We have a short password.
393                                        ownerPassword = new byte[] { optiondata[14], optiondata[15], optiondata[16], optiondata[17] };
394                                    }
395                                    if (optiondata.length == 22) {
396                                        // We have a long password.
397                                        ownerPassword = new byte[] { optiondata[14], optiondata[15], optiondata[16], optiondata[17], optiondata[18], optiondata[19], optiondata[20], optiondata[21] };
398                                    }
399                                } catch (Exception exception) {
400                                    logger.warning("Malformed OPT answer. Option code: Owner data: " + this._hexString(optiondata));
401                                }
402                                if (logger.isLoggable(Level.FINE)) {
403                                    logger.fine("Unhandled Owner OPT version: " + ownerVersion + " sequence: " + ownerSequence + " MAC address: " + this._hexString(ownerPrimaryMacAddress)
404                                            + (ownerWakeupMacAddress != ownerPrimaryMacAddress ? " wakeup MAC address: " + this._hexString(ownerWakeupMacAddress) : "") + (ownerPassword != null ? " password: " + this._hexString(ownerPassword) : ""));
405                                }
406                                break;
407                            case LLQ:
408                            case NSID:
409                            case UL:
410                                if (logger.isLoggable(Level.FINE)) {
411                                    logger.log(Level.FINE, "There was an OPT answer. Option code: " + optionCode + " data: " + this._hexString(optiondata));
412                                }
413                                break;
414                            case Unknown:
415                                logger.log(Level.WARNING, "There was an OPT answer. Not currently handled. Option code: " + optionCodeInt + " data: " + this._hexString(optiondata));
416                                break;
417                            default:
418                                // This is to keep the compiler happy.
419                                break;
420                        }
421                    }
422                } else {
423                    logger.log(Level.WARNING, "There was an OPT answer. Wrong version number: " + version + " result code: " + extendedResultCode);
424                }
425                break;
426            default:
427                if (logger.isLoggable(Level.FINER)) {
428                    logger.finer("DNSIncoming() unknown type:" + type);
429                }
430                _messageInputStream.skip(len);
431                break;
432        }
433        if (rec != null) {
434            rec.setRecordSource(source);
435        }
436        return rec;
437    }
438
439    /**
440     * Debugging.
441     */
442    String print(boolean dump) {
443        StringBuilder buf = new StringBuilder();
444        buf.append(this.print());
445        if (dump) {
446            byte[] data = new byte[_packet.getLength()];
447            System.arraycopy(_packet.getData(), 0, data, 0, data.length);
448            buf.append(this.print(data));
449        }
450        return buf.toString();
451    }
452
453    @Override
454    public String toString() {
455        StringBuilder buf = new StringBuilder();
456        buf.append(isQuery() ? "dns[query," : "dns[response,");
457        if (_packet.getAddress() != null) {
458            buf.append(_packet.getAddress().getHostAddress());
459        }
460        buf.append(':');
461        buf.append(_packet.getPort());
462        buf.append(", length=");
463        buf.append(_packet.getLength());
464        buf.append(", id=0x");
465        buf.append(Integer.toHexString(this.getId()));
466        if (this.getFlags() != 0) {
467            buf.append(", flags=0x");
468            buf.append(Integer.toHexString(this.getFlags()));
469            if ((this.getFlags() & DNSConstants.FLAGS_QR_RESPONSE) != 0) {
470                buf.append(":r");
471            }
472            if ((this.getFlags() & DNSConstants.FLAGS_AA) != 0) {
473                buf.append(":aa");
474            }
475            if ((this.getFlags() & DNSConstants.FLAGS_TC) != 0) {
476                buf.append(":tc");
477            }
478        }
479        if (this.getNumberOfQuestions() > 0) {
480            buf.append(", questions=");
481            buf.append(this.getNumberOfQuestions());
482        }
483        if (this.getNumberOfAnswers() > 0) {
484            buf.append(", answers=");
485            buf.append(this.getNumberOfAnswers());
486        }
487        if (this.getNumberOfAuthorities() > 0) {
488            buf.append(", authorities=");
489            buf.append(this.getNumberOfAuthorities());
490        }
491        if (this.getNumberOfAdditionals() > 0) {
492            buf.append(", additionals=");
493            buf.append(this.getNumberOfAdditionals());
494        }
495        if (this.getNumberOfQuestions() > 0) {
496            buf.append("\nquestions:");
497            for (DNSQuestion question : _questions) {
498                buf.append("\n\t");
499                buf.append(question);
500            }
501        }
502        if (this.getNumberOfAnswers() > 0) {
503            buf.append("\nanswers:");
504            for (DNSRecord record : _answers) {
505                buf.append("\n\t");
506                buf.append(record);
507            }
508        }
509        if (this.getNumberOfAuthorities() > 0) {
510            buf.append("\nauthorities:");
511            for (DNSRecord record : _authoritativeAnswers) {
512                buf.append("\n\t");
513                buf.append(record);
514            }
515        }
516        if (this.getNumberOfAdditionals() > 0) {
517            buf.append("\nadditionals:");
518            for (DNSRecord record : _additionals) {
519                buf.append("\n\t");
520                buf.append(record);
521            }
522        }
523        buf.append("]");
524        return buf.toString();
525    }
526
527    /**
528     * Appends answers to this Incoming.
529     *
530     * @exception IllegalArgumentException
531     *                If not a query or if Truncated.
532     */
533    void append(DNSIncoming that) {
534        if (this.isQuery() && this.isTruncated() && that.isQuery()) {
535            this._questions.addAll(that.getQuestions());
536            this._answers.addAll(that.getAnswers());
537            this._authoritativeAnswers.addAll(that.getAuthorities());
538            this._additionals.addAll(that.getAdditionals());
539        } else {
540            throw new IllegalArgumentException();
541        }
542    }
543
544    public int elapseSinceArrival() {
545        return (int) (System.currentTimeMillis() - _receivedTime);
546    }
547
548    /**
549     * This will return the default UDP payload except if an OPT record was found with a different size.
550     *
551     * @return the senderUDPPayload
552     */
553    public int getSenderUDPPayload() {
554        return this._senderUDPPayload;
555    }
556
557    private static final char[] _nibbleToHex = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
558
559    /**
560     * Returns a hex-string for printing
561     *
562     * @param bytes
563     * @return Returns a hex-string which can be used within a SQL expression
564     */
565    private String _hexString(byte[] bytes) {
566
567        StringBuilder result = new StringBuilder(2 * bytes.length);
568
569        for (int i = 0; i < bytes.length; i++) {
570            int b = bytes[i] & 0xFF;
571            result.append(_nibbleToHex[b / 16]);
572            result.append(_nibbleToHex[b % 16]);
573        }
574
575        return result.toString();
576    }
577
578}
579