All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager Maven / Gradle / Ivy

Go to download

Smack experimental extensions. Classes and methods for XEPs that are in status 'experimental' or that should otherwise carefully considered for deployment. The API may change even between patch versions.

There is a newer version: 4.5.0-beta5
Show newest version
/**
 *
 * Copyright 2016-2019 Florian Schmaus
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jivesoftware.smackx.iot.provisioning;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.StanzaListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.StanzaExtensionFilter;
import org.jivesoftware.smack.filter.StanzaFilter;
import org.jivesoftware.smack.filter.StanzaTypeFilter;
import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.roster.AbstractPresenceEventListener;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.SubscribeListener;

import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.iot.IoTManager;
import org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager;
import org.jivesoftware.smackx.iot.provisioning.element.ClearCache;
import org.jivesoftware.smackx.iot.provisioning.element.ClearCacheResponse;
import org.jivesoftware.smackx.iot.provisioning.element.Constants;
import org.jivesoftware.smackx.iot.provisioning.element.Friend;
import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriend;
import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriendResponse;
import org.jivesoftware.smackx.iot.provisioning.element.Unfriend;

import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.DomainBareJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.util.cache.LruCache;

/**
 * A manager for XEP-0324: Internet of Things - Provisioning.
 *
 * @author Florian Schmaus {@literal }
 * @see XEP-0324: Internet of Things - Provisioning
 */
public final class IoTProvisioningManager extends Manager {

    private static final Logger LOGGER = Logger.getLogger(IoTProvisioningManager.class.getName());

    private static final StanzaFilter FRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE,
            new StanzaExtensionFilter(Friend.ELEMENT, Friend.NAMESPACE));
    private static final StanzaFilter UNFRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE,
                    new StanzaExtensionFilter(Unfriend.ELEMENT, Unfriend.NAMESPACE));

    private static final Map INSTANCES = new WeakHashMap<>();

    // Ensure a IoTProvisioningManager exists for every connection.
    static {
        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
            @Override
            public void connectionCreated(XMPPConnection connection) {
                if (!IoTManager.isAutoEnableActive()) return;
                getInstanceFor(connection);
            }
        });
    }

    /**
     * Get the manger instance responsible for the given connection.
     *
     * @param connection the XMPP connection.
     * @return a manager instance.
     */
    public static synchronized IoTProvisioningManager getInstanceFor(XMPPConnection connection) {
        IoTProvisioningManager manager = INSTANCES.get(connection);
        if (manager == null) {
            manager = new IoTProvisioningManager(connection);
            INSTANCES.put(connection, manager);
        }
        return manager;
    }

    private final Roster roster;
    private final LruCache> negativeFriendshipRequestCache = new LruCache<>(8);
    private final LruCache friendshipDeniedCache = new LruCache<>(16);

    private final LruCache friendshipRequestedCache = new LruCache<>(16);

    private final Set becameFriendListeners = new CopyOnWriteArraySet<>();

    private final Set wasUnfriendedListeners = new CopyOnWriteArraySet<>();

    private Jid configuredProvisioningServer;

    private IoTProvisioningManager(XMPPConnection connection) {
        super(connection);

        // Stanza listener for XEP-0324 § 3.2.3.
        connection.addAsyncStanzaListener(new StanzaListener() {
            @Override
            public void processStanza(Stanza stanza) throws NotConnectedException, InterruptedException {
                if (!isFromProvisioningService(stanza, true)) {
                    return;
                }

                Message message = (Message) stanza;
                Unfriend unfriend = Unfriend.from(message);
                BareJid unfriendJid = unfriend.getJid();
                final XMPPConnection connection = connection();
                Roster roster = Roster.getInstanceFor(connection);
                if (!roster.isSubscribedToMyPresence(unfriendJid)) {
                    LOGGER.warning("Ignoring  request '" + stanza + "' because " + unfriendJid
                                    + " is already not subscribed to our presence.");
                    return;
                }
                Presence unsubscribed = connection.getStanzaFactory().buildPresenceStanza()
                        .ofType(Presence.Type.unsubscribed)
                        .to(unfriendJid)
                        .build();
                connection.sendStanza(unsubscribed);
            }
        }, UNFRIEND_MESSAGE);

        // Stanza listener for XEP-0324 § 3.2.4 "Recommending Friendships".
        // Also includes business logic for thing-to-thing friendship recommendations, which is not
        // (yet) part of the XEP.
        connection.addAsyncStanzaListener(new StanzaListener() {
            @Override
            public void processStanza(final Stanza stanza) throws NotConnectedException, InterruptedException {
                final Message friendMessage = (Message) stanza;
                final Friend friend = Friend.from(friendMessage);
                final BareJid friendJid = friend.getFriend();

                if (isFromProvisioningService(friendMessage, false)) {
                    // We received a recommendation from a provisioning server.
                    // Notify the recommended friend that we will now accept his
                    // friendship requests.
                    final XMPPConnection connection = connection();
                    Friend friendNotification = new Friend(connection.getUser().asBareJid());
                    Message notificationMessage = connection.getStanzaFactory().buildMessageStanza()
                            .to(friendJid)
                            .addExtension(friendNotification)
                            .build();
                    connection.sendStanza(notificationMessage);
                } else {
                    // Check is the message was send from a thing we previously
                    // tried to become friends with. If this is the case, then
                    // thing is likely telling us that we can become now
                    // friends.
                    BareJid bareFrom = friendMessage.getFrom().asBareJid();
                    if (!friendshipDeniedCache.containsKey(bareFrom)) {
                        LOGGER.log(Level.WARNING, "Ignoring friendship recommendation "
                                        + friendMessage
                                        + " because friendship to this JID was not previously denied.");
                        return;
                    }

                    // Sanity check: If a thing recommends us itself as friend,
                    // which should be the case once we reach this code, then
                    // the bare 'from' JID should be equals to the JID of the
                    // recommended friend.
                    if (!bareFrom.equals(friendJid)) {
                        LOGGER.log(Level.WARNING,
                                        "Ignoring friendship recommendation " + friendMessage
                                                        + " because it does not recommend itself, but "
                                                        + friendJid + '.');
                        return;
                    }

                    // Re-try the friendship request.
                    sendFriendshipRequest(friendJid);
                }
            }
        }, FRIEND_MESSAGE);

        connection.registerIQRequestHandler(
                        new AbstractIqRequestHandler(ClearCache.ELEMENT, ClearCache.NAMESPACE, Type.set, Mode.async) {
                            @Override
                            public IQ handleIQRequest(IQ iqRequest) {
                                if (!isFromProvisioningService(iqRequest, true)) {
                                    return null;
                                }

                                ClearCache clearCache = (ClearCache) iqRequest;

                                // Handle  request.
                                Jid from = iqRequest.getFrom();
                                LruCache cache = negativeFriendshipRequestCache.lookup(from);
                                if (cache != null) {
                                    cache.clear();
                                }

                                return new ClearCacheResponse(clearCache);
                            }
                        });

        roster = Roster.getInstanceFor(connection);
        roster.addSubscribeListener(new SubscribeListener() {
            @Override
            public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) {
                // First check if the subscription request comes from a known registry and accept the request if so.
                try {
                    if (IoTDiscoveryManager.getInstanceFor(connection()).isRegistry(from.asBareJid())) {
                        return SubscribeAnswer.Approve;
                    }
                }
                catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) {
                    LOGGER.log(Level.WARNING, "Could not determine if " + from + " is a registry", e);
                }

                Jid provisioningServer = null;
                try {
                    provisioningServer = getConfiguredProvisioningServer();
                }
                catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) {
                    LOGGER.log(Level.WARNING,
                                    "Could not determine provisioning server. Ignoring friend request from " + from, e);
                }
                if (provisioningServer == null) {
                    return null;
                }

                boolean isFriend;
                try {
                    isFriend = isFriend(provisioningServer, from.asBareJid());
                }
                catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) {
                    LOGGER.log(Level.WARNING, "Could not determine if " + from + " is a friend.", e);
                    return null;
                }

                if (isFriend) {
                    return SubscribeAnswer.Approve;
                }
                else {
                    return SubscribeAnswer.Deny;
                }
            }
        });

        roster.addPresenceEventListener(new AbstractPresenceEventListener() {
            @Override
            public void presenceSubscribed(BareJid address, Presence subscribedPresence) {
                friendshipRequestedCache.remove(address);
                for (BecameFriendListener becameFriendListener : becameFriendListeners) {
                    becameFriendListener.becameFriend(address, subscribedPresence);
                }
            }
            @Override
            public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) {
                if (friendshipRequestedCache.containsKey(address)) {
                    friendshipDeniedCache.put(address, null);
                }
                for (WasUnfriendedListener wasUnfriendedListener : wasUnfriendedListeners) {
                    wasUnfriendedListener.wasUnfriendedListener(address, unsubscribedPresence);
                }
            }
        });
    }

    /**
     * Set the configured provisioning server. Use null as provisioningServer to use
     * automatic discovery of the provisioning server (the default behavior).
     *
     * @param provisioningServer TODO javadoc me please
     */
    public void setConfiguredProvisioningServer(Jid provisioningServer) {
        this.configuredProvisioningServer = provisioningServer;
    }

    public Jid getConfiguredProvisioningServer()
                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        if (configuredProvisioningServer == null) {
            configuredProvisioningServer = findProvisioningServerComponent();
        }
        return configuredProvisioningServer;
    }

    /**
     * Try to find a provisioning server component.
     *
     * @return the XMPP address of the provisioning server component if one was found.
     * @throws NoResponseException if there was no response from the remote entity.
     * @throws XMPPErrorException if there was an XMPP error returned.
     * @throws NotConnectedException if the XMPP connection is not connected.
     * @throws InterruptedException if the calling thread was interrupted.
     * @see XEP-0324 § 3.1.2 Provisioning Server as a server component
     */
    public DomainBareJid findProvisioningServerComponent() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        final XMPPConnection connection = connection();
        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
        List discoverInfos = sdm.findServicesDiscoverInfo(Constants.IOT_PROVISIONING_NAMESPACE, true, true);
        if (discoverInfos.isEmpty()) {
            return null;
        }
        Jid jid = discoverInfos.get(0).getFrom();
        assert jid.isDomainBareJid();
        return jid.asDomainBareJid();
    }

    /**
     * As the given provisioning server is the given JID is a friend.
     *
     * @param provisioningServer the provisioning server to ask.
     * @param friendInQuestion the JID to ask about.
     * @return true if the JID is a friend, false otherwise.
     * @throws NoResponseException if there was no response from the remote entity.
     * @throws XMPPErrorException if there was an XMPP error returned.
     * @throws NotConnectedException if the XMPP connection is not connected.
     * @throws InterruptedException if the calling thread was interrupted.
     */
    public boolean isFriend(Jid provisioningServer, BareJid friendInQuestion) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        LruCache cache = negativeFriendshipRequestCache.lookup(provisioningServer);
        if (cache != null && cache.containsKey(friendInQuestion)) {
            // We hit a cached negative isFriend response for this provisioning server.
            return false;
        }

        IoTIsFriend iotIsFriend = new IoTIsFriend(friendInQuestion);
        iotIsFriend.setTo(provisioningServer);
        IoTIsFriendResponse response = connection().createStanzaCollectorAndSend(iotIsFriend).nextResultOrThrow();
        assert response.getJid().equals(friendInQuestion);
        boolean isFriend = response.getIsFriendResult();
        if (!isFriend) {
            // Cache the negative is friend response.
            if (cache == null) {
                cache = new LruCache<>(1024);
                negativeFriendshipRequestCache.put(provisioningServer, cache);
            }
            cache.put(friendInQuestion, null);
        }
        return isFriend;
    }

    public boolean iAmFriendOf(BareJid otherJid) {
        return roster.iAmSubscribedTo(otherJid);
    }

    public void sendFriendshipRequest(BareJid bareJid) throws NotConnectedException, InterruptedException {
        XMPPConnection connection = connection();
        Presence presence = connection.getStanzaFactory().buildPresenceStanza()
            .ofType(Presence.Type.subscribe)
            .to(bareJid)
            .build();

        friendshipRequestedCache.put(bareJid, null);

        connection().sendStanza(presence);
    }

    public void sendFriendshipRequestIfRequired(BareJid jid) throws NotConnectedException, InterruptedException {
        if (iAmFriendOf(jid)) return;

        sendFriendshipRequest(jid);
    }

    public boolean isMyFriend(Jid friendInQuestion) {
        return roster.isSubscribedToMyPresence(friendInQuestion);
    }

    public void unfriend(Jid friend) throws NotConnectedException, InterruptedException {
        if (isMyFriend(friend)) {
            XMPPConnection connection = connection();
            Presence presence = connection.getStanzaFactory().buildPresenceStanza()
                    .ofType(Presence.Type.unsubscribed)
                    .to(friend)
                    .build();
            connection.sendStanza(presence);
        }
    }

    public boolean addBecameFriendListener(BecameFriendListener becameFriendListener) {
        return becameFriendListeners.add(becameFriendListener);
    }

    public boolean removeBecameFriendListener(BecameFriendListener becameFriendListener) {
        return becameFriendListeners.remove(becameFriendListener);
    }

    public boolean addWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) {
        return wasUnfriendedListeners.add(wasUnfriendedListener);
    }

    public boolean removeWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) {
        return wasUnfriendedListeners.remove(wasUnfriendedListener);
    }

    private boolean isFromProvisioningService(Stanza stanza, boolean log) {
        Jid provisioningServer;
        try {
            provisioningServer = getConfiguredProvisioningServer();
        }
        catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
            LOGGER.log(Level.WARNING, "Could determine provisioning server", e);
            return false;
        }
        if (provisioningServer == null) {
            if (log) {
                LOGGER.warning("Ignoring request '" + stanza
                                + "' because no provisioning server configured.");
            }
            return false;
        }
        if (!provisioningServer.equals(stanza.getFrom())) {
            if (log) {
                LOGGER.warning("Ignoring  request '" + stanza
                                + "' because not from provisioning server '" + provisioningServer
                                + "'.");
            }
            return false;
        }
        return true;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy