001/* 002 * Copyright 2015-2018 the original author or authors 003 * 004 * This software is licensed under the Apache License, Version 2.0, 005 * the GNU Lesser General Public License version 2 or later ("LGPL") 006 * and the WTFPL. 007 * You may choose either license to govern your use of this software only 008 * upon the condition that you accept all of the terms of either 009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. 010 */ 011package org.minidns.record; 012 013import java.io.ByteArrayOutputStream; 014import java.io.DataInputStream; 015import java.io.DataOutputStream; 016import java.io.IOException; 017import java.util.ArrayList; 018import java.util.Collection; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022 023import org.minidns.dnsmessage.DnsMessage; 024import org.minidns.dnsmessage.Question; 025import org.minidns.dnsname.DnsName; 026 027/** 028 * A generic DNS record. 029 */ 030public final class Record<D extends Data> { 031 032 /** 033 * The resource record type. 034 * 035 * @see <a href= 036 * "http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4"> 037 * IANA DNS Parameters - Resource Record (RR) TYPEs</a> 038 */ 039 public static enum TYPE { 040 UNKNOWN(-1), 041 A(1, A.class), 042 NS(2, NS.class), 043 MD(3), 044 MF(4), 045 CNAME(5, CNAME.class), 046 SOA(6, SOA.class), 047 MB(7), 048 MG(8), 049 MR(9), 050 NULL(10), 051 WKS(11), 052 PTR(12, PTR.class), 053 HINFO(13), 054 MINFO(14), 055 MX(15, MX.class), 056 TXT(16, TXT.class), 057 RP(17), 058 AFSDB(18), 059 X25(19), 060 ISDN(20), 061 RT(21), 062 NSAP(22), 063 NSAP_PTR(23), 064 SIG(24), 065 KEY(25), 066 PX(26), 067 GPOS(27), 068 AAAA(28, AAAA.class), 069 LOC(29), 070 NXT(30), 071 EID(31), 072 NIMLOC(32), 073 SRV(33, SRV.class), 074 ATMA(34), 075 NAPTR(35), 076 KX(36), 077 CERT(37), 078 A6(38), 079 DNAME(39, DNAME.class), 080 SINK(40), 081 OPT(41, OPT.class), 082 APL(42), 083 DS(43, DS.class), 084 SSHFP(44), 085 IPSECKEY(45), 086 RRSIG(46, RRSIG.class), 087 NSEC(47, NSEC.class), 088 DNSKEY(48, DNSKEY.class), 089 DHCID(49), 090 NSEC3(50, NSEC3.class), 091 NSEC3PARAM(51, NSEC3PARAM.class), 092 TLSA(52, TLSA.class), 093 HIP(55), 094 NINFO(56), 095 RKEY(57), 096 TALINK(58), 097 CDS(59), 098 CDNSKEY(60), 099 OPENPGPKEY(61, OPENPGPKEY.class), 100 CSYNC(62), 101 SPF(99), 102 UINFO(100), 103 UID(101), 104 GID(102), 105 UNSPEC(103), 106 NID(104), 107 L32(105), 108 L64(106), 109 LP(107), 110 EUI48(108), 111 EUI64(109), 112 TKEY(249), 113 TSIG(250), 114 IXFR(251), 115 AXFR(252), 116 MAILB(253), 117 MAILA(254), 118 ANY(255), 119 URI(256), 120 CAA(257), 121 TA(32768), 122 DLV(32769, DLV.class), 123 ; 124 125 /** 126 * The value of this DNS record type. 127 */ 128 private final int value; 129 130 private final Class<?> dataClass; 131 132 /** 133 * Internal lookup table to map values to types. 134 */ 135 private final static Map<Integer, TYPE> INVERSE_LUT = new HashMap<>(); 136 137 private final static Map<Class<?>, TYPE> DATA_LUT = new HashMap<>(); 138 139 static { 140 // Initialize the reverse lookup table. 141 for(TYPE t: TYPE.values()) { 142 INVERSE_LUT.put(t.getValue(), t); 143 if (t.dataClass != null) { 144 DATA_LUT.put(t.dataClass, t); 145 } 146 } 147 } 148 149 /** 150 * Create a new record type. 151 * 152 * @param value The binary value of this type. 153 */ 154 private TYPE(int value) { 155 this(value, null); 156 } 157 158 /** 159 * Create a new record type. 160 * 161 * @param <D> The class for this type. 162 * @param dataClass The class for this type. 163 * @param value The binary value of this type. 164 */ 165 private <D extends Data> TYPE(int value, Class<D> dataClass) { 166 this.value = value; 167 this.dataClass = dataClass; 168 } 169 170 /** 171 * Retrieve the binary value of this type. 172 * @return The binary value. 173 */ 174 public int getValue() { 175 return value; 176 } 177 178 /** 179 * Get the {@link Data} class for this type. 180 * 181 * @param <D> The class for this type. 182 * @return the {@link Data} class for this type. 183 */ 184 @SuppressWarnings("unchecked") 185 public <D extends Data> Class<D> getDataClass() { 186 return (Class<D>) dataClass; 187 } 188 189 /** 190 * Retrieve the symbolic type of the binary value. 191 * @param value The binary type value. 192 * @return The symbolic tpye. 193 */ 194 public static TYPE getType(int value) { 195 TYPE type = INVERSE_LUT.get(value); 196 if (type == null) return UNKNOWN; 197 return type; 198 } 199 200 /** 201 * Retrieve the type for a given {@link Data} class. 202 * 203 * @param <D> The class for this type. 204 * @param dataClass the class to lookup the type for. 205 * @return the type for the given data class. 206 */ 207 public static <D extends Data> TYPE getType(Class<D> dataClass) { 208 return DATA_LUT.get(dataClass); 209 } 210 } 211 212 /** 213 * The symbolic class of a DNS record (usually {@link CLASS#IN} for Internet). 214 * 215 * @see <a href="http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-2">IANA Domain Name System (DNS) Parameters - DNS CLASSes</a> 216 */ 217 public static enum CLASS { 218 219 /** 220 * The Internet class. This is the most common class used by todays DNS systems. 221 */ 222 IN(1), 223 224 /** 225 * The Chaos class. 226 */ 227 CH(3), 228 229 /** 230 * The Hesiod class. 231 */ 232 HS(4), 233 NONE(254), 234 ANY(255); 235 236 /** 237 * Internal reverse lookup table to map binary class values to symbolic 238 * names. 239 */ 240 private final static HashMap<Integer, CLASS> INVERSE_LUT = 241 new HashMap<Integer, CLASS>(); 242 243 static { 244 // Initialize the interal reverse lookup table. 245 for(CLASS c: CLASS.values()) { 246 INVERSE_LUT.put(c.getValue(), c); 247 } 248 } 249 250 /** 251 * The binary value of this dns class. 252 */ 253 private final int value; 254 255 /** 256 * Create a new DNS class based on a binary value. 257 * @param value The binary value of this DNS class. 258 */ 259 private CLASS(int value) { 260 this.value = value; 261 } 262 263 /** 264 * Retrieve the binary value of this DNS class. 265 * @return The binary value of this DNS class. 266 */ 267 public int getValue() { 268 return value; 269 } 270 271 /** 272 * Retrieve the symbolic DNS class for a binary class value. 273 * @param value The binary DNS class value. 274 * @return The symbolic class instance. 275 */ 276 public static CLASS getClass(int value) { 277 return INVERSE_LUT.get(value); 278 } 279 280 } 281 282 /** 283 * The generic name of this record. 284 */ 285 public final DnsName name; 286 287 /** 288 * The type (and payload type) of this record. 289 */ 290 public final TYPE type; 291 292 /** 293 * The record class (usually CLASS.IN). 294 */ 295 public final CLASS clazz; 296 297 /** 298 * The value of the class field of a RR. 299 * 300 * According to RFC 2671 (OPT RR) this is not necessarily representable 301 * using clazz field and unicastQuery bit 302 */ 303 public final int clazzValue; 304 305 /** 306 * The ttl of this record. 307 */ 308 public final long ttl; 309 310 /** 311 * The payload object of this record. 312 */ 313 public final D payloadData; 314 315 /** 316 * MDNS defines the highest bit of the class as the unicast query bit. 317 */ 318 public final boolean unicastQuery; 319 320 /** 321 * Parse a given record based on the full message data and the current 322 * stream position. 323 * 324 * @param dis The DataInputStream positioned at the first record byte. 325 * @param data The full message data. 326 * @return the record which was parsed. 327 * @throws IOException In case of malformed replies. 328 */ 329 public static Record<Data> parse(DataInputStream dis, byte[] data) throws IOException { 330 DnsName name = DnsName.parse(dis, data); 331 int typeValue = dis.readUnsignedShort(); 332 TYPE type = TYPE.getType(typeValue); 333 int clazzValue = dis.readUnsignedShort(); 334 CLASS clazz = CLASS.getClass(clazzValue & 0x7fff); 335 boolean unicastQuery = (clazzValue & 0x8000) > 0; 336 long ttl = (((long)dis.readUnsignedShort()) << 16) + 337 dis.readUnsignedShort(); 338 int payloadLength = dis.readUnsignedShort(); 339 Data payloadData; 340 switch (type) { 341 case SOA: 342 payloadData = SOA.parse(dis, data); 343 break; 344 case SRV: 345 payloadData = SRV.parse(dis, data); 346 break; 347 case MX: 348 payloadData = MX.parse(dis, data); 349 break; 350 case AAAA: 351 payloadData = AAAA.parse(dis); 352 break; 353 case A: 354 payloadData = A.parse(dis); 355 break; 356 case NS: 357 payloadData = NS.parse(dis, data); 358 break; 359 case CNAME: 360 payloadData = CNAME.parse(dis, data); 361 break; 362 case DNAME: 363 payloadData = DNAME.parse(dis, data); 364 break; 365 case PTR: 366 payloadData = PTR.parse(dis, data); 367 break; 368 case TXT: 369 payloadData = TXT.parse(dis, payloadLength); 370 break; 371 case OPT: 372 payloadData = OPT.parse(dis, payloadLength); 373 break; 374 case DNSKEY: 375 payloadData = DNSKEY.parse(dis, payloadLength); 376 break; 377 case RRSIG: 378 payloadData = RRSIG.parse(dis, data, payloadLength); 379 break; 380 case DS: 381 payloadData = DS.parse(dis, payloadLength); 382 break; 383 case NSEC: 384 payloadData = NSEC.parse(dis, data, payloadLength); 385 break; 386 case NSEC3: 387 payloadData = NSEC3.parse(dis, payloadLength); 388 break; 389 case NSEC3PARAM: 390 payloadData = NSEC3PARAM.parse(dis); 391 break; 392 case TLSA: 393 payloadData = TLSA.parse(dis, payloadLength); 394 break; 395 case OPENPGPKEY: 396 payloadData = OPENPGPKEY.parse(dis, payloadLength); 397 break; 398 case DLV: 399 payloadData = DLV.parse(dis, payloadLength); 400 break; 401 case UNKNOWN: 402 default: 403 payloadData = UNKNOWN.parse(dis, payloadLength, type); 404 break; 405 } 406 return new Record<>(name, type, clazz, clazzValue, ttl, payloadData, unicastQuery); 407 } 408 409 public Record(DnsName name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) { 410 this(name, type, clazz, clazz.getValue() + (unicastQuery ? 0x8000 : 0), ttl, payloadData, unicastQuery); 411 } 412 413 public Record(String name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) { 414 this(DnsName.from(name), type, clazz, ttl, payloadData, unicastQuery); 415 } 416 417 public Record(String name, TYPE type, int clazzValue, long ttl, D payloadData) { 418 this(DnsName.from(name), type, CLASS.NONE, clazzValue, ttl, payloadData, false); 419 } 420 421 public Record(DnsName name, TYPE type, int clazzValue, long ttl, D payloadData) { 422 this(name, type, CLASS.NONE, clazzValue, ttl, payloadData, false); 423 } 424 425 private Record(DnsName name, TYPE type, CLASS clazz, int clazzValue, long ttl, D payloadData, boolean unicastQuery) { 426 this.name = name; 427 this.type = type; 428 this.clazz = clazz; 429 this.clazzValue = clazzValue; 430 this.ttl = ttl; 431 this.payloadData = payloadData; 432 this.unicastQuery = unicastQuery; 433 } 434 435 public void toOutputStream(DataOutputStream dos) throws IOException { 436 if (payloadData == null) { 437 throw new IllegalStateException("Empty Record has no byte representation"); 438 } 439 440 name.writeToStream(dos); 441 dos.writeShort(type.getValue()); 442 dos.writeShort(clazzValue); 443 dos.writeInt((int) ttl); 444 445 dos.writeShort(payloadData.length()); 446 payloadData.toOutputStream(dos); 447 } 448 449 private byte[] bytes; 450 451 public byte[] toByteArray() { 452 if (bytes == null) { 453 ByteArrayOutputStream baos = new ByteArrayOutputStream(name.size() + 8 + payloadData.length()); 454 DataOutputStream dos = new DataOutputStream(baos); 455 try { 456 toOutputStream(dos); 457 } catch (IOException e) { 458 // Should never happen. 459 throw new AssertionError(e); 460 } 461 bytes = baos.toByteArray(); 462 } 463 return bytes.clone(); 464 } 465 466 /** 467 * Retrieve a textual representation of this resource record. 468 * @return String 469 */ 470 @Override 471 public String toString() { 472 return name.getRawAce() + ".\t" + ttl + '\t' + clazz + '\t' + type + '\t' + payloadData; 473 } 474 475 /** 476 * Check if this record answers a given query. 477 * @param q The query. 478 * @return True if this record is a valid answer. 479 */ 480 public boolean isAnswer(Question q) { 481 return ((q.type == type) || (q.type == TYPE.ANY)) && 482 ((q.clazz == clazz) || (q.clazz == CLASS.ANY)) && 483 (q.name.equals(name)); 484 } 485 486 /** 487 * See if this query/response was a unicast query (highest class bit set). 488 * @return True if it is a unicast query/response record. 489 */ 490 public boolean isUnicastQuery() { 491 return unicastQuery; 492 } 493 494 /** 495 * The payload data, usually a subclass of data (A, AAAA, CNAME, ...). 496 * @return The payload data. 497 */ 498 public D getPayload() { 499 return payloadData; 500 } 501 502 /** 503 * Retrieve the record ttl. 504 * @return The record ttl. 505 */ 506 public long getTtl() { 507 return ttl; 508 } 509 510 /** 511 * Get the question asking for this resource record. This will return <code>null</code> if the record is not retrievable, i.e. 512 * {@link TYPE#OPT}. 513 * 514 * @return the question for this resource record or <code>null</code>. 515 */ 516 public Question getQuestion() { 517 switch (type) { 518 case OPT: 519 // OPT records are not retrievable. 520 return null; 521 case RRSIG: 522 RRSIG rrsig = (RRSIG) payloadData; 523 return new Question(name, rrsig.typeCovered, clazz); 524 default: 525 return new Question(name, type, clazz); 526 } 527 } 528 529 public DnsMessage.Builder getQuestionMessage() { 530 Question question = getQuestion(); 531 if (question == null) { 532 return null; 533 } 534 return question.asMessageBuilder(); 535 } 536 537 private transient Integer hashCodeCache; 538 539 @Override 540 public int hashCode() { 541 if (hashCodeCache == null) { 542 int hashCode = 1; 543 hashCode = 37 * hashCode + name.hashCode(); 544 hashCode = 37 * hashCode + type.hashCode(); 545 hashCode = 37 * hashCode + clazz.hashCode(); 546 hashCode = 37 * hashCode + payloadData.hashCode(); 547 hashCodeCache = hashCode; 548 } 549 return hashCodeCache; 550 } 551 552 @Override 553 public boolean equals(Object other) { 554 if (!(other instanceof Record)) { 555 return false; 556 } 557 if (other == this) { 558 return true; 559 } 560 Record<?> otherRecord = (Record<?>) other; 561 if (!name.equals(otherRecord.name)) return false; 562 if (type != otherRecord.type) return false; 563 if (clazz != otherRecord.clazz) return false; 564 // Note that we do not compare the TTL here, since we consider two Records with everything but the TTL equal to 565 // be equal too. 566 if (!payloadData.equals(otherRecord.payloadData)) return false; 567 568 return true; 569 } 570 571 /** 572 * Return the record if possible as record with the given {@link Data} class. If the record does not hold payload of 573 * the given data class type, then {@code null} will be returned. 574 * 575 * @param dataClass a class of the {@link Data} type. 576 * @param <E> a subtype of {@link Data}. 577 * @return the record with a specialized payload type or {@code null}. 578 */ 579 @SuppressWarnings("unchecked") 580 public <E extends Data> Record<E> ifPossibleAs(Class<E> dataClass) { 581 if (type.dataClass == dataClass) { 582 return (Record<E>) this; 583 } 584 return null; 585 } 586 587 public static <E extends Data> void filter(Collection<Record<E>> result, Class<E> dataClass, 588 Collection<Record<? extends Data>> input) { 589 for (Record<? extends Data> record : input) { 590 Record<E> filteredRecord = record.ifPossibleAs(dataClass); 591 if (filteredRecord == null) 592 continue; 593 594 result.add(filteredRecord); 595 } 596 } 597 598 public static <E extends Data> List<Record<E>> filter(Class<E> dataClass, 599 Collection<Record<? extends Data>> input) { 600 List<Record<E>> result = new ArrayList<>(input.size()); 601 filter(result, dataClass, input); 602 return result; 603 } 604}