/** * * Copyright 2016-2021 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.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, IQ.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().sendIqRequestAndWaitForResponse(iotIsFriend); 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; } }