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.iterative;
012
013import java.io.IOException;
014import java.util.LinkedList;
015import java.util.List;
016import java.util.logging.Level;
017
018import org.minidns.AbstractDnsClient;
019import org.minidns.DnsCache;
020import org.minidns.DnsClient;
021import org.minidns.dnsmessage.DnsMessage;
022import org.minidns.dnsmessage.Question;
023import org.minidns.source.DnsDataSource;
024import org.minidns.util.MultipleIoException;
025
026/**
027 * A DNS client using a reliable strategy. First the configured resolver of the
028 * system are used, then, in case there is no answer, a fall back to iterative
029 * resolving is performed.
030 */
031public class ReliableDnsClient extends AbstractDnsClient {
032
033    public enum Mode {
034        /**
035         * Try the recursive servers first and fallback to iterative resolving if it fails. This is the default mode.
036         */
037        recursiveWithIterativeFallback,
038
039        /**
040         * Only try the recursive servers. This makes {@code ReliableDnsClient} behave like a {@link DnsClient}.
041         */
042        recursiveOnly,
043
044        /**
045         * Only use iterative resolving.  This makes {@code ReliableDnsClient} behave like a {@link IterativeDnsClient}.
046         */
047        iterativeOnly,
048    }
049
050    private final IterativeDnsClient recursiveDnsClient;
051    private final DnsClient dnsClient;
052
053    private Mode mode = Mode.recursiveWithIterativeFallback;
054
055    public ReliableDnsClient(DnsCache dnsCache) {
056        super(dnsCache);
057        recursiveDnsClient = new IterativeDnsClient(dnsCache) {
058            @Override
059            protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
060                questionMessage = super.newQuestion(questionMessage);
061                return ReliableDnsClient.this.newQuestion(questionMessage);
062            }
063            @Override
064            protected boolean isResponseCacheable(Question q, DnsMessage dnsMessage) {
065                boolean res = super.isResponseCacheable(q, dnsMessage);
066                return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
067            }
068        };
069        dnsClient = new DnsClient(dnsCache) {
070            @Override
071            protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
072                questionMessage = super.newQuestion(questionMessage);
073                return ReliableDnsClient.this.newQuestion(questionMessage);
074            }
075            @Override
076            protected boolean isResponseCacheable(Question q, DnsMessage dnsMessage) {
077                boolean res = super.isResponseCacheable(q, dnsMessage);
078                return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
079            }
080        };
081    }
082
083    public ReliableDnsClient() {
084        this(DEFAULT_CACHE);
085    }
086
087    @Override
088    protected DnsMessage query(DnsMessage.Builder q) throws IOException {
089        DnsMessage dnsMessage = null;
090        String unacceptableReason = null;
091        List<IOException> ioExceptions = new LinkedList<>();
092
093        if (mode != Mode.iterativeOnly) {
094            // Try a recursive query.
095            try {
096                dnsMessage = dnsClient.query(q);
097                if (dnsMessage != null) {
098                    unacceptableReason = isResponseAcceptable(dnsMessage);
099                    if (unacceptableReason == null) {
100                        return dnsMessage;
101                    }
102                }
103            } catch (IOException ioException) {
104                ioExceptions.add(ioException);
105            }
106        }
107
108        // Abort if we the are in "recursive only" mode.
109        if (mode == Mode.recursiveOnly) return dnsMessage;
110
111        // Eventually log that we fall back to iterative mode.
112        final Level FALLBACK_LOG_LEVEL = Level.FINE;
113        if (LOGGER.isLoggable(FALLBACK_LOG_LEVEL) && mode != Mode.iterativeOnly) {
114            String logString = "Resolution fall back to iterative mode because: ";
115            if (!ioExceptions.isEmpty()) {
116                logString += ioExceptions.get(0);
117            } else if (dnsMessage == null) {
118                logString += " DnsClient did not return a response";
119            } else if (unacceptableReason != null) {
120                logString += unacceptableReason + ". Response:\n" + dnsMessage;
121            } else {
122                throw new AssertionError("This should never been reached");
123            }
124            LOGGER.log(FALLBACK_LOG_LEVEL, logString);
125        }
126
127        try {
128            dnsMessage = recursiveDnsClient.query(q);
129        } catch (IOException ioException) {
130            ioExceptions.add(ioException);
131        }
132
133        if (dnsMessage == null) {
134            MultipleIoException.throwIfRequired(ioExceptions);
135        }
136
137        return dnsMessage;
138    }
139
140    @Override
141    protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
142        return questionMessage;
143    }
144
145    @Override
146    protected boolean isResponseCacheable(Question q, DnsMessage dnsMessage) {
147        return isResponseAcceptable(dnsMessage) == null;
148    }
149
150    /**
151     * Check if the response from the system's nameserver is acceptable. Must return <code>null</code> if the response
152     * is acceptable, or a String describing why it is not acceptable. If the response is not acceptable then
153     * {@link ReliableDnsClient} will fall back to resolve the query iteratively.
154     *
155     * @param response the response we got from the system's nameserver.
156     * @return <code>null</code> if the response is acceptable, or a String if not.
157     */
158    protected String isResponseAcceptable(DnsMessage response) {
159        return null;
160    }
161
162    @Override
163    public void setDataSource(DnsDataSource dataSource) {
164        super.setDataSource(dataSource);
165        recursiveDnsClient.setDataSource(dataSource);
166        dnsClient.setDataSource(dataSource);
167    }
168
169    /**
170     * Set the mode used when resolving queries.
171     *
172     * @param mode the mode to use.
173     */
174    public void setMode(Mode mode) {
175        if (mode == null) {
176            throw new IllegalArgumentException("Mode must not be null.");
177        }
178        this.mode = mode;
179    }
180}