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}