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 HIP(55), 095 NINFO(56), 096 RKEY(57), 097 TALINK(58), 098 CDS(59), 099 CDNSKEY(60), 100 OPENPGPKEY(61, OPENPGPKEY.class), 101 CSYNC(62), 102 SPF(99), 103 UINFO(100), 104 UID(101), 105 GID(102), 106 UNSPEC(103), 107 NID(104), 108 L32(105), 109 L64(106), 110 LP(107), 111 EUI48(108), 112 EUI64(109), 113 TKEY(249), 114 TSIG(250), 115 IXFR(251), 116 AXFR(252), 117 MAILB(253), 118 MAILA(254), 119 ANY(255), 120 URI(256), 121 CAA(257), 122 TA(32768), 123 DLV(32769, DLV.class), 124 ; 125 126 /** 127 * The value of this DNS record type. 128 */ 129 private final int value; 130 131 private final Class<?> dataClass; 132 133 /** 134 * Internal lookup table to map values to types. 135 */ 136 private static final Map<Integer, TYPE> INVERSE_LUT = new HashMap<>(); 137 138 private static final Map<Class<?>, TYPE> DATA_LUT = new HashMap<>(); 139 140 static { 141 // Initialize the reverse lookup table. 142 for (TYPE t : TYPE.values()) { 143 INVERSE_LUT.put(t.getValue(), t); 144 if (t.dataClass != null) { 145 DATA_LUT.put(t.dataClass, t); 146 } 147 } 148 } 149 150 /** 151 * Create a new record type. 152 * 153 * @param value The binary value of this type. 154 */ 155 TYPE(int value) { 156 this(value, null); 157 } 158 159 /** 160 * Create a new record type. 161 * 162 * @param <D> The class for this type. 163 * @param dataClass The class for this type. 164 * @param value The binary value of this type. 165 */ 166 <D extends Data> TYPE(int value, Class<D> dataClass) { 167 this.value = value; 168 this.dataClass = dataClass; 169 } 170 171 /** 172 * Retrieve the binary value of this type. 173 * @return The binary value. 174 */ 175 public int getValue() { 176 return value; 177 } 178 179 /** 180 * Get the {@link Data} class for this type. 181 * 182 * @param <D> The class for this type. 183 * @return the {@link Data} class for this type. 184 */ 185 @SuppressWarnings("unchecked") 186 public <D extends Data> Class<D> getDataClass() { 187 return (Class<D>) dataClass; 188 } 189 190 /** 191 * Retrieve the symbolic type of the binary value. 192 * @param value The binary type value. 193 * @return The symbolic tpye. 194 */ 195 public static TYPE getType(int value) { 196 TYPE type = INVERSE_LUT.get(value); 197 if (type == null) return UNKNOWN; 198 return type; 199 } 200 201 /** 202 * Retrieve the type for a given {@link Data} class. 203 * 204 * @param <D> The class for this type. 205 * @param dataClass the class to lookup the type for. 206 * @return the type for the given data class. 207 */ 208 public static <D extends Data> TYPE getType(Class<D> dataClass) { 209 return DATA_LUT.get(dataClass); 210 } 211 } 212 213 /** 214 * The symbolic class of a DNS record (usually {@link CLASS#IN} for Internet). 215 * 216 * @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> 217 */ 218 public enum CLASS { 219 220 /** 221 * The Internet class. This is the most common class used by todays DNS systems. 222 */ 223 IN(1), 224 225 /** 226 * The Chaos class. 227 */ 228 CH(3), 229 230 /** 231 * The Hesiod class. 232 */ 233 HS(4), 234 NONE(254), 235 ANY(255); 236 237 /** 238 * Internal reverse lookup table to map binary class values to symbolic 239 * names. 240 */ 241 private static final HashMap<Integer, CLASS> INVERSE_LUT = 242 new HashMap<Integer, CLASS>(); 243 244 static { 245 // Initialize the interal reverse lookup table. 246 for (CLASS c : CLASS.values()) { 247 INVERSE_LUT.put(c.getValue(), c); 248 } 249 } 250 251 /** 252 * The binary value of this dns class. 253 */ 254 private final int value; 255 256 /** 257 * Create a new DNS class based on a binary value. 258 * @param value The binary value of this DNS class. 259 */ 260 CLASS(int value) { 261 this.value = value; 262 } 263 264 /** 265 * Retrieve the binary value of this DNS class. 266 * @return The binary value of this DNS class. 267 */ 268 public int getValue() { 269 return value; 270 } 271 272 /** 273 * Retrieve the symbolic DNS class for a binary class value. 274 * @param value The binary DNS class value. 275 * @return The symbolic class instance. 276 */ 277 public static CLASS getClass(int value) { 278 return INVERSE_LUT.get(value); 279 } 280 281 } 282 283 /** 284 * The generic name of this record. 285 */ 286 public final DnsName name; 287 288 /** 289 * The type (and payload type) of this record. 290 */ 291 public final TYPE type; 292 293 /** 294 * The record class (usually CLASS.IN). 295 */ 296 public final CLASS clazz; 297 298 /** 299 * The value of the class field of a RR. 300 * 301 * According to RFC 2671 (OPT RR) this is not necessarily representable 302 * using clazz field and unicastQuery bit 303 */ 304 public final int clazzValue; 305 306 /** 307 * The ttl of this record. 308 */ 309 public final long ttl; 310 311 /** 312 * The payload object of this record. 313 */ 314 public final D payloadData; 315 316 /** 317 * MDNS defines the highest bit of the class as the unicast query bit. 318 */ 319 public final boolean unicastQuery; 320 321 /** 322 * Parse a given record based on the full message data and the current 323 * stream position. 324 * 325 * @param dis The DataInputStream positioned at the first record byte. 326 * @param data The full message data. 327 * @return the record which was parsed. 328 * @throws IOException In case of malformed replies. 329 */ 330 public static Record<Data> parse(DataInputStream dis, byte[] data) throws IOException { 331 DnsName name = DnsName.parse(dis, data); 332 int typeValue = dis.readUnsignedShort(); 333 TYPE type = TYPE.getType(typeValue); 334 int clazzValue = dis.readUnsignedShort(); 335 CLASS clazz = CLASS.getClass(clazzValue & 0x7fff); 336 boolean unicastQuery = (clazzValue & 0x8000) > 0; 337 long ttl = (((long) dis.readUnsignedShort()) << 16) + 338 dis.readUnsignedShort(); 339 int payloadLength = dis.readUnsignedShort(); 340 Data payloadData; 341 switch (type) { 342 case SOA: 343 payloadData = SOA.parse(dis, data); 344 break; 345 case SRV: 346 payloadData = SRV.parse(dis, data); 347 break; 348 case MX: 349 payloadData = MX.parse(dis, data); 350 break; 351 case AAAA: 352 payloadData = AAAA.parse(dis); 353 break; 354 case A: 355 payloadData = A.parse(dis); 356 break; 357 case NS: 358 payloadData = NS.parse(dis, data); 359 break; 360 case CNAME: 361 payloadData = CNAME.parse(dis, data); 362 break; 363 case DNAME: 364 payloadData = DNAME.parse(dis, data); 365 break; 366 case PTR: 367 payloadData = PTR.parse(dis, data); 368 break; 369 case TXT: 370 payloadData = TXT.parse(dis, payloadLength); 371 break; 372 case OPT: 373 payloadData = OPT.parse(dis, payloadLength); 374 break; 375 case DNSKEY: 376 payloadData = DNSKEY.parse(dis, payloadLength); 377 break; 378 case RRSIG: 379 payloadData = RRSIG.parse(dis, data, payloadLength); 380 break; 381 case DS: 382 payloadData = DS.parse(dis, payloadLength); 383 break; 384 case NSEC: 385 payloadData = NSEC.parse(dis, data, payloadLength); 386 break; 387 case NSEC3: 388 payloadData = NSEC3.parse(dis, payloadLength); 389 break; 390 case NSEC3PARAM: 391 payloadData = NSEC3PARAM.parse(dis); 392 break; 393 case TLSA: 394 payloadData = TLSA.parse(dis, payloadLength); 395 break; 396 case OPENPGPKEY: 397 payloadData = OPENPGPKEY.parse(dis, payloadLength); 398 break; 399 case DLV: 400 payloadData = DLV.parse(dis, payloadLength); 401 break; 402 case UNKNOWN: 403 default: 404 payloadData = UNKNOWN.parse(dis, payloadLength, type); 405 break; 406 } 407 return new Record<>(name, type, clazz, clazzValue, ttl, payloadData, unicastQuery); 408 } 409 410 public Record(DnsName name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) { 411 this(name, type, clazz, clazz.getValue() + (unicastQuery ? 0x8000 : 0), ttl, payloadData, unicastQuery); 412 } 413 414 public Record(String name, TYPE type, CLASS clazz, long ttl, D payloadData, boolean unicastQuery) { 415 this(DnsName.from(name), type, clazz, ttl, payloadData, unicastQuery); 416 } 417 418 public Record(String name, TYPE type, int clazzValue, long ttl, D payloadData) { 419 this(DnsName.from(name), type, CLASS.NONE, clazzValue, ttl, payloadData, false); 420 } 421 422 public Record(DnsName name, TYPE type, int clazzValue, long ttl, D payloadData) { 423 this(name, type, CLASS.NONE, clazzValue, ttl, payloadData, false); 424 } 425 426 private Record(DnsName name, TYPE type, CLASS clazz, int clazzValue, long ttl, D payloadData, boolean unicastQuery) { 427 this.name = name; 428 this.type = type; 429 this.clazz = clazz; 430 this.clazzValue = clazzValue; 431 this.ttl = ttl; 432 this.payloadData = payloadData; 433 this.unicastQuery = unicastQuery; 434 } 435 436 public void toOutputStream(OutputStream outputStream) throws IOException { 437 if (payloadData == null) { 438 throw new IllegalStateException("Empty Record has no byte representation"); 439 } 440 441 DataOutputStream dos = new DataOutputStream(outputStream); 442 443 name.writeToStream(dos); 444 dos.writeShort(type.getValue()); 445 dos.writeShort(clazzValue); 446 dos.writeInt((int) ttl); 447 448 dos.writeShort(payloadData.length()); 449 payloadData.toOutputStream(dos); 450 } 451 452 private transient byte[] bytes; 453 454 public byte[] toByteArray() { 455 if (bytes == null) { 456 int totalSize = name.size() 457 + 10 // 2 byte short type + 2 byte short classValue + 4 byte int ttl + 2 byte short payload length. 458 + payloadData.length(); 459 ByteArrayOutputStream baos = new ByteArrayOutputStream(totalSize); 460 DataOutputStream dos = new DataOutputStream(baos); 461 try { 462 toOutputStream(dos); 463 } catch (IOException e) { 464 // Should never happen. 465 throw new AssertionError(e); 466 } 467 bytes = baos.toByteArray(); 468 } 469 return bytes.clone(); 470 } 471 472 /** 473 * Retrieve a textual representation of this resource record. 474 * @return String 475 */ 476 @Override 477 public String toString() { 478 return name.getRawAce() + ".\t" + ttl + '\t' + clazz + '\t' + type + '\t' + payloadData; 479 } 480 481 /** 482 * Check if this record answers a given query. 483 * @param q The query. 484 * @return True if this record is a valid answer. 485 */ 486 public boolean isAnswer(Question q) { 487 return ((q.type == type) || (q.type == TYPE.ANY)) && 488 ((q.clazz == clazz) || (q.clazz == CLASS.ANY)) && 489 q.name.equals(name); 490 } 491 492 /** 493 * See if this query/response was a unicast query (highest class bit set). 494 * @return True if it is a unicast query/response record. 495 */ 496 public boolean isUnicastQuery() { 497 return unicastQuery; 498 } 499 500 /** 501 * The payload data, usually a subclass of data (A, AAAA, CNAME, ...). 502 * @return The payload data. 503 */ 504 public D getPayload() { 505 return payloadData; 506 } 507 508 /** 509 * Retrieve the record ttl. 510 * @return The record ttl. 511 */ 512 public long getTtl() { 513 return ttl; 514 } 515 516 /** 517 * Get the question asking for this resource record. This will return <code>null</code> if the record is not retrievable, i.e. 518 * {@link TYPE#OPT}. 519 * 520 * @return the question for this resource record or <code>null</code>. 521 */ 522 public Question getQuestion() { 523 switch (type) { 524 case OPT: 525 // OPT records are not retrievable. 526 return null; 527 case RRSIG: 528 RRSIG rrsig = (RRSIG) payloadData; 529 return new Question(name, rrsig.typeCovered, clazz); 530 default: 531 return new Question(name, type, clazz); 532 } 533 } 534 535 public DnsMessage.Builder getQuestionMessage() { 536 Question question = getQuestion(); 537 if (question == null) { 538 return null; 539 } 540 return question.asMessageBuilder(); 541 } 542 543 private transient Integer hashCodeCache; 544 545 @Override 546 public int hashCode() { 547 if (hashCodeCache == null) { 548 int hashCode = 1; 549 hashCode = 37 * hashCode + name.hashCode(); 550 hashCode = 37 * hashCode + type.hashCode(); 551 hashCode = 37 * hashCode + clazz.hashCode(); 552 hashCode = 37 * hashCode + payloadData.hashCode(); 553 hashCodeCache = hashCode; 554 } 555 return hashCodeCache; 556 } 557 558 @Override 559 public boolean equals(Object other) { 560 if (!(other instanceof Record)) { 561 return false; 562 } 563 if (other == this) { 564 return true; 565 } 566 Record<?> otherRecord = (Record<?>) other; 567 if (!name.equals(otherRecord.name)) return false; 568 if (type != otherRecord.type) return false; 569 if (clazz != otherRecord.clazz) return false; 570 // Note that we do not compare the TTL here, since we consider two Records with everything but the TTL equal to 571 // be equal too. 572 if (!payloadData.equals(otherRecord.payloadData)) return false; 573 574 return true; 575 } 576 577 /** 578 * Return the record if possible as record with the given {@link Data} class. If the record does not hold payload of 579 * the given data class type, then {@code null} will be returned. 580 * 581 * @param dataClass a class of the {@link Data} type. 582 * @param <E> a subtype of {@link Data}. 583 * @return the record with a specialized payload type or {@code null}. 584 * @see #as(Class) 585 */ 586 @SuppressWarnings("unchecked") 587 public <E extends Data> Record<E> ifPossibleAs(Class<E> dataClass) { 588 if (type.dataClass == dataClass) { 589 return (Record<E>) this; 590 } 591 return null; 592 } 593 594 /** 595 * Return the record as record with the given {@link Data} class. If the record does not hold payload of 596 * the given data class type, then a {@link IllegalArgumentException} will be thrown. 597 * 598 * @param dataClass a class of the {@link Data} type. 599 * @param <E> a subtype of {@link Data}. 600 * @return the record with a specialized payload type. 601 * @see #ifPossibleAs(Class) 602 */ 603 public <E extends Data> Record<E> as(Class<E> dataClass) { 604 Record<E> eRecord = ifPossibleAs(dataClass); 605 if (eRecord == null) { 606 throw new IllegalArgumentException("The instance " + this + " can not be cast to a Record with" + dataClass); 607 } 608 return eRecord; 609 } 610 611 public static <E extends Data> void filter(Collection<Record<E>> result, Class<E> dataClass, 612 Collection<Record<? extends Data>> input) { 613 for (Record<? extends Data> record : input) { 614 Record<E> filteredRecord = record.ifPossibleAs(dataClass); 615 if (filteredRecord == null) 616 continue; 617 618 result.add(filteredRecord); 619 } 620 } 621 622 public static <E extends Data> List<Record<E>> filter(Class<E> dataClass, 623 Collection<Record<? extends Data>> input) { 624 List<Record<E>> result = new ArrayList<>(input.size()); 625 filter(result, dataClass, input); 626 return result; 627 } 628}