001/**
002 *
003 * Copyright 2019-2021 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.util.RandomUtil;
035
036import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
037import org.jivesoftware.smackx.dox.element.DnsIq;
038
039import org.jxmpp.jid.Jid;
040import org.minidns.dnsmessage.DnsMessage;
041import org.minidns.dnsmessage.Question;
042
043public final class DnsOverXmppManager extends Manager {
044
045    private static final Logger LOGGER = Logger.getLogger(DnsOverXmppManager.class.getName());
046
047    private static final Map<XMPPConnection, DnsOverXmppManager> INSTANCES = new WeakHashMap<>();
048
049    public static synchronized DnsOverXmppManager getInstanceFor(XMPPConnection connection) {
050        DnsOverXmppManager manager = INSTANCES.get(connection);
051        if (manager == null) {
052            manager = new DnsOverXmppManager(connection);
053            INSTANCES.put(connection, manager);
054        }
055        return manager;
056    }
057
058    private static final String NAMESPACE = DnsIq.NAMESPACE;
059
060    private static DnsOverXmppResolver defaultResolver;
061
062    public static void setDefaultDnsOverXmppResolver(DnsOverXmppResolver resolver) {
063        defaultResolver = resolver;
064    }
065
066    private final ServiceDiscoveryManager serviceDiscoveryManager;
067
068    private DnsOverXmppResolver resolver = defaultResolver;
069
070    private boolean enabled;
071
072    private final AbstractIqRequestHandler dnsIqRequestHandler = new AbstractIqRequestHandler(
073            DnsIq.ELEMENT, DnsIq.NAMESPACE, IQ.Type.get, Mode.async) {
074
075        @Override
076        public IQ handleIQRequest(IQ iqRequest) {
077            DnsOverXmppResolver resolver = DnsOverXmppManager.this.resolver;
078            if (resolver == null) {
079                LOGGER.info("Resolver was null while attempting to handle " + iqRequest);
080                return null;
081            }
082
083            DnsIq dnsIqRequest = (DnsIq) iqRequest;
084            DnsMessage query = dnsIqRequest.getDnsMessage();
085
086            DnsMessage response;
087            try {
088                response = resolver.resolve(query);
089            } catch (IOException exception) {
090                StanzaError errorBuilder = StanzaError.getBuilder()
091                        .setType(StanzaError.Type.CANCEL)
092                        .setCondition(Condition.internal_server_error)
093                        .setDescriptiveEnText("Exception while resolving your DNS query", exception)
094                        .build()
095                        ;
096
097                IQ errorResponse = IQ.createErrorResponse(iqRequest, errorBuilder);
098                return errorResponse;
099            }
100
101            if (query.id != response.id) {
102                // The ID may not match because the resolver returned a cached result.
103                response = response.asBuilder().setId(query.id).build();
104            }
105
106            DnsIq dnsIqResult = new DnsIq(response);
107            dnsIqResult.setType(IQ.Type.result);
108            return dnsIqResult;
109        }
110    };
111
112    private DnsOverXmppManager(XMPPConnection connection) {
113        super(connection);
114        this.serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(connection);
115    }
116
117    public synchronized void setDnsOverXmppResolver(DnsOverXmppResolver resolver) {
118        this.resolver = resolver;
119        if (resolver == null) {
120            disable();
121        }
122    }
123
124    public synchronized void enable() {
125        if (enabled) return;
126
127        if (resolver == null) {
128            throw new IllegalStateException("No DnsOverXmppResolver configured");
129        }
130
131        XMPPConnection connection = connection();
132        if (connection == null) return;
133
134        connection.registerIQRequestHandler(dnsIqRequestHandler);
135        serviceDiscoveryManager.addFeature(NAMESPACE);
136    }
137
138    public synchronized void disable() {
139        if (!enabled) return;
140
141        XMPPConnection connection = connection();
142        if (connection == null) return;
143
144        serviceDiscoveryManager.removeFeature(NAMESPACE);
145        connection.unregisterIQRequestHandler(dnsIqRequestHandler);
146    }
147
148    public boolean isSupported(Jid jid)
149            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
150        return serviceDiscoveryManager.supportsFeature(jid, NAMESPACE);
151    }
152
153    public DnsMessage query(Jid jid, Question question) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
154        DnsMessage queryMessage = DnsMessage.builder()
155                .addQuestion(question)
156                .setId(RandomUtil.nextSecureRandomInt())
157                .setRecursionDesired(true)
158                .build();
159        return query(jid, queryMessage);
160    }
161
162    public DnsMessage query(Jid jid, DnsMessage query)
163            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
164        DnsIq queryIq = new DnsIq(query, jid);
165
166        DnsIq responseIq = connection().sendIqRequestAndWaitForResponse(queryIq);
167
168        return responseIq.getDnsMessage();
169    }
170}