1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-11-22 06:12:05 +01:00

Add support for IoT Friend approvals

This adds supports for an experimental protocol flow where a pending
friend request's decission is later on deliverd to the requestor after
the owner made its decission.
This commit is contained in:
Florian Schmaus 2016-10-31 12:24:05 +01:00
parent 5a2326a856
commit 1d3c48e6ce
11 changed files with 407 additions and 56 deletions

View file

@ -0,0 +1,26 @@
/**
*
* 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 org.jivesoftware.smack.packet.Presence;
import org.jxmpp.jid.BareJid;
public interface BecameFriendListener {
void becameFriend(BareJid jid, Presence presence);
}

View file

@ -18,7 +18,9 @@ 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;
@ -41,6 +43,7 @@ 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.RosterEntry;
import org.jivesoftware.smack.roster.SubscribeListener;
@ -50,6 +53,7 @@ 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;
@ -68,6 +72,8 @@ 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));
@ -99,6 +105,13 @@ public final class IoTProvisioningManager extends 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;
@ -129,6 +142,47 @@ public final class IoTProvisioningManager extends Manager {
}
}, UNFRIEND_MESSAGE);
// Stanza listener for XEP-0324 § 3.2.4.
connection.addAsyncStanzaListener(new StanzaListener() {
@Override
public void processPacket(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)) {
// 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 friendNotifiacation = new Friend(connection.getUser().asBareJid());
Message notificationMessage = new Message(friendJid, friendNotifiacation);
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.
Jid from = friendMessage.getFrom();
if (!friendshipDeniedCache.containsKey(from)) {
return;
}
BareJid bareFrom = from.asBareJid();
// 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)) {
return;
}
// Re-try the friendship request.
sendFriendshipRequest(friendJid);
}
}
}, FRIEND_MESSAGE);
connection.registerIQRequestHandler(
new AbstractIqRequestHandler(ClearCache.ELEMENT, ClearCache.NAMESPACE, Type.set, Mode.async) {
@Override
@ -193,6 +247,25 @@ public final class IoTProvisioningManager extends Manager {
}
}
});
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);
}
}
});
}
/**
@ -272,6 +345,9 @@ public final class IoTProvisioningManager extends Manager {
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);
}
@ -295,6 +371,22 @@ public final class IoTProvisioningManager extends Manager {
}
}
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) {
Jid provisioningServer;
try {

View file

@ -0,0 +1,26 @@
/**
*
* 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 org.jivesoftware.smack.packet.Presence;
import org.jxmpp.jid.BareJid;
public interface WasUnfriendedListener {
void wasUnfriendedListener(BareJid jid, Presence presence);
}

View file

@ -0,0 +1,61 @@
/**
*
* 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.element;
import org.jivesoftware.smack.packet.ExtensionElement;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.Objects;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jxmpp.jid.BareJid;
public class Friend implements ExtensionElement {
public static final String ELEMENT = "friend";
public static final String NAMESPACE = Constants.IOT_PROVISIONING_NAMESPACE;
private final BareJid friend;
public Friend(BareJid friend) {
this.friend = Objects.requireNonNull(friend, "Friend must not be null");
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public XmlStringBuilder toXML() {
XmlStringBuilder xml = new XmlStringBuilder(this);
xml.attribute("jid", friend);
xml.closeEmptyElement();
return xml;
}
public BareJid getFriend() {
return friend;
}
public static Friend from(Message message) {
return message.getExtension(ELEMENT, NAMESPACE);
}
}

View file

@ -0,0 +1,34 @@
/**
*
* 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.provider;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.util.ParserUtils;
import org.jivesoftware.smackx.iot.provisioning.element.Friend;
import org.jxmpp.jid.BareJid;
import org.jxmpp.stringprep.XmppStringprepException;
import org.xmlpull.v1.XmlPullParser;
public class FriendProvider extends ExtensionElementProvider<Friend> {
@Override
public Friend parse(XmlPullParser parser, int initialDepth) throws XmppStringprepException {
BareJid jid = ParserUtils.getBareJidAttribute(parser);
return new Friend(jid);
}
}

View file

@ -132,6 +132,11 @@
<namespace>urn:xmpp:iot:provisioning</namespace>
<className>org.jivesoftware.smackx.iot.provisioning.provider.ClearCacheResponseProvider</className>
</iqProvider>
<extensionProvider>
<elementName>friend</elementName>
<namespace>urn:xmpp:iot:provisioning</namespace>
<className>org.jivesoftware.smackx.iot.provisioning.provider.FriendProvider</className>
</extensionProvider>
<extensionProvider>
<elementName>unfriend</elementName>
<namespace>urn:xmpp:iot:provisioning</namespace>

View file

@ -681,6 +681,7 @@ public final class Roster extends Manager {
Objects.requireNonNull(subscribeListener, "SubscribeListener argument must not be null");
if (subscriptionMode != SubscriptionMode.manual) {
previousSubscriptionMode = subscriptionMode;
subscriptionMode = SubscriptionMode.manual;
}
return subscribeListeners.add(subscribeListener);
}
@ -1228,6 +1229,7 @@ public final class Roster extends Manager {
RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry);
if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) {
updatedEntries.add(item.getJid());
oldEntry.updateItem(item);
} else {
// Record the entry as unchanged, so that it doesn't end up as deleted entry
unchangedEntries.add(item.getJid());

View file

@ -28,6 +28,8 @@ import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Presence.Type;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jxmpp.jid.BareJid;
@ -41,7 +43,7 @@ import org.jxmpp.jid.BareJid;
*/
public final class RosterEntry extends Manager {
private final RosterPacket.Item item;
private RosterPacket.Item item;
final private Roster roster;
/**
@ -120,10 +122,9 @@ public final class RosterEntry extends Manager {
* @param type the subscription type.
* @param subscriptionPending TODO
*/
void updateState(String name, RosterPacket.ItemType type, boolean subscriptionPending) {
item.setName(name);
item.setItemType(type);
item.setSubscriptionPending(subscriptionPending);
void updateItem(RosterPacket.Item item) {
assert(item != null);
this.item = item;
}
/**
@ -209,6 +210,18 @@ public final class RosterEntry extends Manager {
}
}
/**
* Cancel the presence subscription the XMPP entity representing this roster entry has with us.
*
* @throws NotConnectedException
* @throws InterruptedException
* @since 4.2
*/
public void cancelSubscription() throws NotConnectedException, InterruptedException {
Presence unsubscribed = new Presence(item.getJid(), Type.unsubscribed);
connection().sendStanza(unsubscribed);
}
public String toString() {
StringBuilder buf = new StringBuilder();
if (getName() != null) {

View file

@ -25,6 +25,7 @@ import java.util.concurrent.locks.ReentrantLock;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.NotLoggedInException;
import org.jivesoftware.smack.XMPPConnection;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
@ -90,4 +91,24 @@ public class RosterUtil {
roster.sendSubscriptionRequest(jid);
}
}
public static void ensureNotSubscribedToEachOther(XMPPConnection connectionOne, XMPPConnection connectionTwo)
throws NotConnectedException, InterruptedException {
final Roster rosterOne = Roster.getInstanceFor(connectionOne);
final BareJid jidOne = connectionOne.getUser().asBareJid();
final Roster rosterTwo = Roster.getInstanceFor(connectionTwo);
final BareJid jidTwo = connectionTwo.getUser().asBareJid();
ensureNotSubscribed(rosterOne, jidTwo);
ensureNotSubscribed(rosterTwo, jidOne);
}
public static void ensureNotSubscribed(Roster roster, BareJid jid)
throws NotConnectedException, InterruptedException {
RosterEntry entry = roster.getEntry(jid);
if (entry != null && entry.canSeeMyPresence()) {
entry.cancelSubscription();
}
}
}

View file

@ -108,6 +108,7 @@ public class RosterPacket extends IQ {
* A roster item, which consists of a JID, their name, the type of subscription, and
* the groups the roster item belongs to.
*/
// TODO Make this class immutable.
public static class Item implements NamedElement {
/**
@ -124,6 +125,7 @@ public class RosterPacket extends IQ {
*/
private boolean subscriptionPending;
// TODO Make immutable.
private String name;
private ItemType itemType = ItemType.none;
private boolean approved;

View file

@ -17,9 +17,11 @@
package org.igniterealtime.smack.smackrepl;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
@ -41,6 +43,7 @@ import org.jivesoftware.smackx.iot.data.element.IoTFieldsExtension;
import org.jivesoftware.smackx.iot.discovery.AbstractThingStateChangeListener;
import org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager;
import org.jivesoftware.smackx.iot.discovery.ThingState;
import org.jivesoftware.smackx.iot.provisioning.BecameFriendListener;
import org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
@ -51,24 +54,23 @@ public class IoT {
// A 10 minute timeout.
private static final long TIMEOUT = 10 * 60 * 1000;
private interface IotScenario {
void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readinThingConnection) throws XMPPException, SmackException, IOException, InterruptedException, TimeoutException, Exception;
}
public static void iotScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString,
String readingThingPassword)
throws TimeoutException, Exception {
String readingThingPassword, IotScenario scenario) throws TimeoutException, Exception {
final EntityBareJid dataThingJid = JidCreate.entityBareFrom(dataThingJidString);
final EntityBareJid readingThingJid = JidCreate.entityBareFrom(readingThingJidString);
final XMPPTCPConnectionConfiguration dataThingConnectionConfiguration = XMPPTCPConnectionConfiguration.builder()
.setUsernameAndPassword(dataThingJid.getLocalpart(), dataThingPassword)
.setXmppDomain(dataThingJid.asDomainBareJid())
.setSecurityMode(SecurityMode.disabled)
.setDebuggerEnabled(true)
.build();
final XMPPTCPConnectionConfiguration readingThingConnectionConfiguration = XMPPTCPConnectionConfiguration.builder()
.setUsernameAndPassword(readingThingJid.getLocalpart(), readingThingPassword)
.setXmppDomain(readingThingJid.asDomainBareJid())
.setSecurityMode(SecurityMode.disabled)
.setDebuggerEnabled(true)
.build();
.setXmppDomain(dataThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled)
.setDebuggerEnabled(true).build();
final XMPPTCPConnectionConfiguration readingThingConnectionConfiguration = XMPPTCPConnectionConfiguration
.builder().setUsernameAndPassword(readingThingJid.getLocalpart(), readingThingPassword)
.setXmppDomain(readingThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled)
.setDebuggerEnabled(true).build();
final XMPPTCPConnection dataThingConnection = new XMPPTCPConnection(dataThingConnectionConfiguration);
final XMPPTCPConnection readingThingConnection = new XMPPTCPConnection(readingThingConnectionConfiguration);
@ -76,19 +78,28 @@ public class IoT {
dataThingConnection.setPacketReplyTimeout(TIMEOUT);
readingThingConnection.setPacketReplyTimeout(TIMEOUT);
dataThingConnection.setUseStreamManagement(false);
readingThingConnection.setUseStreamManagement(false);
try {
iotScenario(dataThingConnection, readingThingConnection);
}
finally {
dataThingConnection.connect().login();
readingThingConnection.connect().login();
scenario.iotScenario(dataThingConnection, readingThingConnection);
} finally {
dataThingConnection.disconnect();
readingThingConnection.disconnect();
}
}
public static void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection)
throws TimeoutException, Exception {
dataThingConnection.connect().login();
readingThingConnection.connect().login();
public static void iotReadOutScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString,
String readingThingPassword)
throws Exception {
iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword, READ_OUT_SCENARIO);
}
public static final IotScenario READ_OUT_SCENARIO = new IotScenario() {
@Override
public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception {
ThingState dataThingState = actAsDataThing(dataThingConnection);
final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint();
@ -119,6 +130,63 @@ public class IoT {
printStatus("DATA READ-OUT SUCCESS: " + field.toXML());
printStatus("IoT SCENARIO FINISHED SUCCESSFULLY");
}
};
public static void iotOwnerApprovesFriendScenario(String dataThingJidString, String dataThingPassword,
String readingThingJidString, String readingThingPassword) throws Exception {
iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword,
OWNER_APPROVES_FRIEND_SCENARIO);
}
public static final IotScenario OWNER_APPROVES_FRIEND_SCENARIO = new IotScenario() {
@Override
public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception {
// First ensure that the two XMPP entities are not already subscribed to each other presences.
RosterUtil.ensureNotSubscribedToEachOther(dataThingConnection, readingThingConnection);
final BareJid dataThingBareJid = dataThingConnection.getUser().asBareJid();
final BareJid readingThingBareJid = readingThingConnection.getUser().asBareJid();
final ThingState dataThingState = actAsDataThing(dataThingConnection);
printStatus("WAITING for 'claimed' notification. Please claim thing now");
final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint();
dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() {
@Override
public void owned(BareJid jid) {
syncPoint.signal();
}
});
// Wait until the thing is owned.
syncPoint.waitForResult(TIMEOUT);
printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner());
// Now, ReadingThing sends a friendship request to data thing, which
// will proxy the request to its provisioning service, which will
// likely return that both a not friends since the owner did not
// authorize the friendship yet.
final SimpleResultSyncPoint friendshipApprovedSyncPoint = new SimpleResultSyncPoint();
final IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection);
final BecameFriendListener becameFriendListener = new BecameFriendListener() {
@Override
public void becameFriend(BareJid jid, Presence presence) {
if (jid.equals(dataThingBareJid)) {
friendshipApprovedSyncPoint.signal();
}
}
};
readingThingProvisioningManager.addBecameFriendListener(becameFriendListener);
try {
readingThingProvisioningManager
.sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid());
friendshipApprovedSyncPoint.waitForResult(TIMEOUT);
} finally {
readingThingProvisioningManager.removeBecameFriendListener(becameFriendListener);
}
printStatus("FRIENDSHIP APPROVED - ReadingThing " + readingThingBareJid + " is now a friend of DataThing " + dataThingBareJid);
}
};
private static ThingState actAsDataThing(XMPPTCPConnection connection) throws XMPPException, SmackException, InterruptedException {
final String key = StringUtils.randomString(12);
@ -126,7 +194,7 @@ public class IoT {
Thing dataThing = Thing.builder()
.setKey(key)
.setSerialNumber(sn)
.setManufacturer("Ignite Realtime")
.setManufacturer("IgniteRealtime")
.setModel("Smack")
.setVersion("0.1")
.setMomentaryReadOutRequestHandler(new ThingMomentaryReadOutRequest() {
@ -153,6 +221,7 @@ public class IoT {
if (args.length != 4) {
throw new IllegalArgumentException();
}
iotScenario(args[0], args[1], args[2], args[3]);
iotOwnerApprovesFriendScenario(args[0], args[1], args[2], args[3]);
}
}