1 // /Copyright 2003-2005 Arthur van Hoff, Rick Blair
2 // Licensed under Apache License version 2.0
3 // Original license LGPL
4 
5 package javax.jmdns.impl;
6 
7 import java.io.ByteArrayInputStream;
8 import java.io.IOException;
9 import java.net.DatagramPacket;
10 import java.net.InetAddress;
11 import java.util.HashMap;
12 import java.util.Map;
13 import java.util.logging.Level;
14 import java.util.logging.Logger;
15 
16 import javax.jmdns.impl.constants.DNSConstants;
17 import javax.jmdns.impl.constants.DNSLabel;
18 import javax.jmdns.impl.constants.DNSOptionCode;
19 import javax.jmdns.impl.constants.DNSRecordClass;
20 import javax.jmdns.impl.constants.DNSRecordType;
21 import 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  */
28 public 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 
MessageInputStream(byte[] buffer, int length)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          */
MessageInputStream(byte[] buffer, int offset, int length)49         public MessageInputStream(byte[] buffer, int offset, int length) {
50             super(buffer, offset, length);
51             _names = new HashMap<Integer, String>();
52         }
53 
readByte()54         public int readByte() {
55             return this.read();
56         }
57 
readUnsignedShort()58         public int readUnsignedShort() {
59             return (this.read() << 8) | this.read();
60         }
61 
readInt()62         public int readInt() {
63             return (this.readUnsignedShort() << 16) | this.readUnsignedShort();
64         }
65 
readBytes(int len)66         public byte[] readBytes(int len) {
67             byte bytes[] = new byte[len];
68             this.read(bytes, 0, len);
69             return bytes;
70         }
71 
readUTF(int len)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 
peek()110         protected synchronized int peek() {
111             return (pos < count) ? (buf[pos] & 0xff) : -1;
112         }
113 
readName()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 
readNonNameString()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      */
DNSIncoming(DatagramPacket packet)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 
DNSIncoming(int flags, int id, boolean multicast, DatagramPacket packet, long receivedTime)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
clone()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 
readQuestion()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 
readAnswer(InetAddress source)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      */
print(boolean dump)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
toString()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      */
append(DNSIncoming that)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 
elapseSinceArrival()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      */
getSenderUDPPayload()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      */
_hexString(byte[] bytes)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