IoTProvisioningManager.java
/**
*
* 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 <flo@geekplace.eu>}
* @see <a href="http://xmpp.org/extensions/xep-0324.html">XEP-0324: Internet of Things - Provisioning</a>
*/
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<XMPPConnection, IoTProvisioningManager> 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<Jid, LruCache<BareJid, Void>> negativeFriendshipRequestCache = new LruCache<>(8);
private final LruCache<BareJid, Void> friendshipDeniedCache = new LruCache<>(16);
private final LruCache<BareJid, Void> friendshipRequestedCache = new LruCache<>(16);
private final Set<BecameFriendListener> becameFriendListeners = new CopyOnWriteArraySet<>();
private final Set<WasUnfriendedListener> 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 <unfriend/> 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 <clearCache/> request.
Jid from = iqRequest.getFrom();
LruCache<BareJid, Void> 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 <code>null</code> 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 <a href="http://xmpp.org/extensions/xep-0324.html#servercomponent">XEP-0324 § 3.1.2 Provisioning Server as a server component</a>
*/
public DomainBareJid findProvisioningServerComponent() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
final XMPPConnection connection = connection();
ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
List<DiscoverInfo> 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 <code>true</code> if the JID is a friend, <code>false</code> 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<BareJid, Void> 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;
}
}