1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-06-23 20:14:51 +02:00
Smack/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java
Florian Schmaus f1e24e2273 Rework Roster's SubscribeListener
allow multiple of them to be installed, instead of at most one. Fixes
deadlock in LowLevelRosterIntegration test because
IoTProvisioningManager's SubscribeListener would not come up with a
decission.
2016-07-31 14:50:59 +02:00

320 lines
14 KiB
Java

/**
*
* Copyright 2016 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.WeakHashMap;
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.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.SubscribeListener;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
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.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 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() {
public void connectionCreated(XMPPConnection connection) {
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 Jid configuredProvisioningServer;
private IoTProvisioningManager(XMPPConnection connection) {
super(connection);
// Stanza listener for XEP-0324 § 3.2.3.
connection.addAsyncStanzaListener(new StanzaListener() {
@Override
public void processPacket(Stanza stanza) throws NotConnectedException, InterruptedException {
if (!isFromProvisioningService(stanza)) {
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 = new Presence(Presence.Type.unsubscribed);
unsubscribed.setTo(unfriendJid);
connection.sendStanza(unsubscribed);
}
}, UNFRIEND_MESSAGE);
connection.registerIQRequestHandler(
new AbstractIqRequestHandler(ClearCache.ELEMENT, ClearCache.NAMESPACE, Type.set, Mode.async) {
@Override
public IQ handleIQRequest(IQ iqRequest) {
if (!isFromProvisioningService(iqRequest)) {
return null;
}
ClearCache clearCache = (ClearCache) iqRequest;
// Handle <clearCache/> request.
Jid from = iqRequest.getFrom();
LruCache<BareJid, Void> cache = negativeFriendshipRequestCache.get(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 privisioning 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;
}
}
});
}
/**
* Set the configured provisioning server. Use <code>null</code> as provisioningServer to use
* automatic discovery of the provisioning server (the default behavior).
*
* @param provisioningServer
*/
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
* @throws XMPPErrorException
* @throws NotConnectedException
* @throws InterruptedException
* @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
* @throws XMPPErrorException
* @throws NotConnectedException
* @throws InterruptedException
*/
public boolean isFriend(Jid provisioningServer, BareJid friendInQuestion) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
LruCache<BareJid, Void> cache = negativeFriendshipRequestCache.get(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().createPacketCollectorAndSend(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 void sendFriendshipRequest(BareJid bareJid) throws NotConnectedException, InterruptedException {
Presence presence = new Presence(Presence.Type.subscribe);
presence.setTo(bareJid);
connection().sendStanza(presence);
}
public void sendFriendshipRequestIfRequired(BareJid jid) throws NotConnectedException, InterruptedException {
RosterEntry entry = roster.getEntry(jid);
if (entry != null && entry.canSeeHisPresence()) {
return;
}
sendFriendshipRequest(jid);
}
public boolean isBefriended(Jid friendInQuestion) {
return roster.isSubscribedToMyPresence(friendInQuestion);
}
public void unfriend(Jid friend) throws NotConnectedException, InterruptedException {
if (isBefriended(friend)) {
Presence presence = new Presence(Presence.Type.unsubscribed);
presence.setTo(friend);
connection().sendStanza(presence);
}
}
private boolean isFromProvisioningService(Stanza stanza) {
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) {
LOGGER.warning("Ignoring request '" + stanza
+ "' because no provisioning server configured.");
return false;
}
if (!provisioningServer.equals(stanza.getFrom())) {
LOGGER.warning("Ignoring request '" + stanza + "' because not from provising server '"
+ provisioningServer + "'.");
return false;
}
return true;
}
}