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