mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2024-06-16 00:24:52 +02:00
431 lines
19 KiB
Java
431 lines
19 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.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 = new Presence(Presence.Type.unsubscribed);
|
|
unsubscribed.setTo(unfriendJid);
|
|
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 = new Message(friendJid, friendNotification);
|
|
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
|
|
*/
|
|
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.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 {
|
|
Presence presence = new Presence(Presence.Type.subscribe);
|
|
presence.setTo(bareJid);
|
|
|
|
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)) {
|
|
Presence presence = new Presence(Presence.Type.unsubscribed);
|
|
presence.setTo(friend);
|
|
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;
|
|
}
|
|
}
|