001/**
002 *
003 * Copyright 2019 Florian Schmaus
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.dox;
018
019import java.io.IOException;
020import java.util.Map;
021import java.util.WeakHashMap;
022import java.util.logging.Logger;
023
024import org.jivesoftware.smack.Manager;
025import org.jivesoftware.smack.SmackException.NoResponseException;
026import org.jivesoftware.smack.SmackException.NotConnectedException;
027import org.jivesoftware.smack.XMPPConnection;
028import org.jivesoftware.smack.XMPPException.XMPPErrorException;
029import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
030import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
031import org.jivesoftware.smack.packet.IQ;
032import org.jivesoftware.smack.packet.StanzaError;
033import org.jivesoftware.smack.packet.StanzaError.Condition;
034import org.jivesoftware.smack.packet.StanzaError.Type;
035import org.jivesoftware.smack.util.RandomUtil;
036
037import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
038import org.jivesoftware.smackx.dox.element.DnsIq;
039
040import org.jxmpp.jid.Jid;
041import org.minidns.dnsmessage.DnsMessage;
042import org.minidns.dnsmessage.Question;
043
044public final class DnsOverXmppManager extends Manager {
045
046    private static final Logger LOGGER = Logger.getLogger(DnsOverXmppManager.class.getName());
047
048    private static final Map<XMPPConnection, DnsOverXmppManager> INSTANCES = new WeakHashMap<>();
049
050    public static synchronized DnsOverXmppManager getInstanceFor(XMPPConnection connection) {
051        DnsOverXmppManager manager = INSTANCES.get(connection);
052        if (manager == null) {
053            manager = new DnsOverXmppManager(connection);
054            INSTANCES.put(connection, manager);
055        }
056        return manager;
057    }
058
059    private static final String NAMESPACE = DnsIq.NAMESPACE;
060
061    private static DnsOverXmppResolver defaultResolver;
062
063    public static void setDefaultDnsOverXmppResolver(DnsOverXmppResolver resolver) {
064        defaultResolver = resolver;
065    }
066
067    private final ServiceDiscoveryManager serviceDiscoveryManager;
068
069    private DnsOverXmppResolver resolver = defaultResolver;
070
071    private boolean enabled;
072
073    private final AbstractIqRequestHandler dnsIqRequestHandler = new AbstractIqRequestHandler(
074            DnsIq.ELEMENT, DnsIq.NAMESPACE, IQ.Type.get, Mode.async) {
075
076        @Override
077        public IQ handleIQRequest(IQ iqRequest) {
078            DnsOverXmppResolver resolver = DnsOverXmppManager.this.resolver;
079            if (resolver == null) {
080                LOGGER.info("Resolver was null while attempting to handle " + iqRequest);
081                return null;
082            }
083
084            DnsIq dnsIqRequest = (DnsIq) iqRequest;
085            DnsMessage query = dnsIqRequest.getDnsMessage();
086
087            DnsMessage response;
088            try {
089                response = resolver.resolve(query);
090            } catch (IOException exception) {
091                StanzaError errorBuilder = StanzaError.getBuilder()
092                        .setType(Type.CANCEL)
093                        .setCondition(Condition.internal_server_error)
094                        .setDescriptiveEnText("Exception while resolving your DNS query", exception)
095                        .build()
096                        ;
097
098                IQ errorResponse = IQ.createErrorResponse(iqRequest, errorBuilder);
099                return errorResponse;
100            }
101
102            if (query.id != response.id) {
103                // The ID may not match because the resolver returned a cached result.
104                response = response.asBuilder().setId(query.id).build();
105            }
106
107            DnsIq dnsIqResult = new DnsIq(response);
108            dnsIqResult.setType(IQ.Type.result);
109            return dnsIqResult;
110        }
111    };
112
113    private DnsOverXmppManager(XMPPConnection connection) {
114        super(connection);
115        this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
116    }
117
118    public synchronized void setDnsOverXmppResolver(DnsOverXmppResolver resolver) {
119        this.resolver = resolver;
120        if (resolver == null) {
121            disable();
122        }
123    }
124
125    public synchronized void enable() {
126        if (enabled) return;
127
128        if (resolver == null) {
129            throw new IllegalStateException("No DnsOverXmppResolver configured");
130        }
131
132        XMPPConnection connection = connection();
133        if (connection == null) return;
134
135        connection.registerIQRequestHandler(dnsIqRequestHandler);
136        serviceDiscoveryManager.addFeature(NAMESPACE);
137    }
138
139    public synchronized void disable() {
140        if (!enabled) return;
141
142        XMPPConnection connection = connection();
143        if (connection == null) return;
144
145        serviceDiscoveryManager.removeFeature(NAMESPACE);
146        connection.unregisterIQRequestHandler(dnsIqRequestHandler);
147    }
148
149    public boolean isSupported(Jid jid)
150            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
151        return serviceDiscoveryManager.supportsFeature(jid, NAMESPACE);
152    }
153
154    public DnsMessage query(Jid jid, Question question) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
155        DnsMessage queryMessage = DnsMessage.builder()
156                .addQuestion(question)
157                .setId(RandomUtil.nextSecureRandomInt())
158                .setRecursionDesired(true)
159                .build();
160        return query(jid, queryMessage);
161    }
162
163    public DnsMessage query(Jid jid, DnsMessage query)
164            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
165        DnsIq queryIq = new DnsIq(query, jid);
166
167        DnsIq responseIq = connection().sendIqRequestAndWaitForResponse(queryIq);
168
169        return responseIq.getDnsMessage();
170    }
171}