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&lt;value&gt;" 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}