001/** 002 * 003 * Copyright 2016 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.iot.provisioning; 018 019import java.util.List; 020import java.util.Map; 021import java.util.Set; 022import java.util.WeakHashMap; 023import java.util.concurrent.CopyOnWriteArraySet; 024import java.util.logging.Level; 025import java.util.logging.Logger; 026 027import org.jivesoftware.smack.ConnectionCreationListener; 028import org.jivesoftware.smack.Manager; 029import org.jivesoftware.smack.SmackException.NoResponseException; 030import org.jivesoftware.smack.SmackException.NotConnectedException; 031import org.jivesoftware.smack.StanzaListener; 032import org.jivesoftware.smack.XMPPConnection; 033import org.jivesoftware.smack.XMPPConnectionRegistry; 034import org.jivesoftware.smack.XMPPException.XMPPErrorException; 035import org.jivesoftware.smack.filter.AndFilter; 036import org.jivesoftware.smack.filter.StanzaExtensionFilter; 037import org.jivesoftware.smack.filter.StanzaFilter; 038import org.jivesoftware.smack.filter.StanzaTypeFilter; 039import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 040import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; 041import org.jivesoftware.smack.packet.IQ; 042import org.jivesoftware.smack.packet.IQ.Type; 043import org.jivesoftware.smack.packet.Message; 044import org.jivesoftware.smack.packet.Presence; 045import org.jivesoftware.smack.packet.Stanza; 046import org.jivesoftware.smack.roster.AbstractPresenceEventListener; 047import org.jivesoftware.smack.roster.Roster; 048import org.jivesoftware.smack.roster.SubscribeListener; 049 050import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 051import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 052import org.jivesoftware.smackx.iot.IoTManager; 053import org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager; 054import org.jivesoftware.smackx.iot.provisioning.element.ClearCache; 055import org.jivesoftware.smackx.iot.provisioning.element.ClearCacheResponse; 056import org.jivesoftware.smackx.iot.provisioning.element.Constants; 057import org.jivesoftware.smackx.iot.provisioning.element.Friend; 058import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriend; 059import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriendResponse; 060import org.jivesoftware.smackx.iot.provisioning.element.Unfriend; 061 062import org.jxmpp.jid.BareJid; 063import org.jxmpp.jid.DomainBareJid; 064import org.jxmpp.jid.Jid; 065import org.jxmpp.util.cache.LruCache; 066 067/** 068 * A manager for XEP-0324: Internet of Things - Provisioning. 069 * 070 * @author Florian Schmaus {@literal <flo@geekplace.eu>} 071 * @see <a href="http://xmpp.org/extensions/xep-0324.html">XEP-0324: Internet of Things - Provisioning</a> 072 */ 073public final class IoTProvisioningManager extends Manager { 074 075 private static final Logger LOGGER = Logger.getLogger(IoTProvisioningManager.class.getName()); 076 077 private static final StanzaFilter FRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE, 078 new StanzaExtensionFilter(Friend.ELEMENT, Friend.NAMESPACE)); 079 private static final StanzaFilter UNFRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE, 080 new StanzaExtensionFilter(Unfriend.ELEMENT, Unfriend.NAMESPACE)); 081 082 private static final Map<XMPPConnection, IoTProvisioningManager> INSTANCES = new WeakHashMap<>(); 083 084 // Ensure a IoTProvisioningManager exists for every connection. 085 static { 086 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 087 @Override 088 public void connectionCreated(XMPPConnection connection) { 089 if (!IoTManager.isAutoEnableActive()) return; 090 getInstanceFor(connection); 091 } 092 }); 093 } 094 095 /** 096 * Get the manger instance responsible for the given connection. 097 * 098 * @param connection the XMPP connection. 099 * @return a manager instance. 100 */ 101 public static synchronized IoTProvisioningManager getInstanceFor(XMPPConnection connection) { 102 IoTProvisioningManager manager = INSTANCES.get(connection); 103 if (manager == null) { 104 manager = new IoTProvisioningManager(connection); 105 INSTANCES.put(connection, manager); 106 } 107 return manager; 108 } 109 110 private final Roster roster; 111 private final LruCache<Jid, LruCache<BareJid, Void>> negativeFriendshipRequestCache = new LruCache<>(8); 112 private final LruCache<BareJid, Void> friendshipDeniedCache = new LruCache<>(16); 113 114 private final LruCache<BareJid, Void> friendshipRequestedCache = new LruCache<>(16); 115 116 private final Set<BecameFriendListener> becameFriendListeners = new CopyOnWriteArraySet<>(); 117 118 private final Set<WasUnfriendedListener> wasUnfriendedListeners = new CopyOnWriteArraySet<>(); 119 120 private Jid configuredProvisioningServer; 121 122 private IoTProvisioningManager(XMPPConnection connection) { 123 super(connection); 124 125 // Stanza listener for XEP-0324 § 3.2.3. 126 connection.addAsyncStanzaListener(new StanzaListener() { 127 @Override 128 public void processStanza(Stanza stanza) throws NotConnectedException, InterruptedException { 129 if (!isFromProvisioningService(stanza, true)) { 130 return; 131 } 132 133 Message message = (Message) stanza; 134 Unfriend unfriend = Unfriend.from(message); 135 BareJid unfriendJid = unfriend.getJid(); 136 final XMPPConnection connection = connection(); 137 Roster roster = Roster.getInstanceFor(connection); 138 if (!roster.isSubscribedToMyPresence(unfriendJid)) { 139 LOGGER.warning("Ignoring <unfriend/> request '" + stanza + "' because " + unfriendJid 140 + " is already not subscribed to our presence."); 141 return; 142 } 143 Presence unsubscribed = new Presence(Presence.Type.unsubscribed); 144 unsubscribed.setTo(unfriendJid); 145 connection.sendStanza(unsubscribed); 146 } 147 }, UNFRIEND_MESSAGE); 148 149 // Stanza listener for XEP-0324 § 3.2.4 "Recommending Friendships". 150 // Also includes business logic for thing-to-thing friendship recommendations, which is not 151 // (yet) part of the XEP. 152 connection.addAsyncStanzaListener(new StanzaListener() { 153 @Override 154 public void processStanza(final Stanza stanza) throws NotConnectedException, InterruptedException { 155 final Message friendMessage = (Message) stanza; 156 final Friend friend = Friend.from(friendMessage); 157 final BareJid friendJid = friend.getFriend(); 158 159 if (isFromProvisioningService(friendMessage, false)) { 160 // We received a recommendation from a provisioning server. 161 // Notify the recommended friend that we will now accept his 162 // friendship requests. 163 final XMPPConnection connection = connection(); 164 Friend friendNotification = new Friend(connection.getUser().asBareJid()); 165 Message notificationMessage = new Message(friendJid, friendNotification); 166 connection.sendStanza(notificationMessage); 167 } else { 168 // Check is the message was send from a thing we previously 169 // tried to become friends with. If this is the case, then 170 // thing is likely telling us that we can become now 171 // friends. 172 BareJid bareFrom = friendMessage.getFrom().asBareJid(); 173 if (!friendshipDeniedCache.containsKey(bareFrom)) { 174 LOGGER.log(Level.WARNING, "Ignoring friendship recommendation " 175 + friendMessage 176 + " because friendship to this JID was not previously denied."); 177 return; 178 } 179 180 // Sanity check: If a thing recommends us itself as friend, 181 // which should be the case once we reach this code, then 182 // the bare 'from' JID should be equals to the JID of the 183 // recommended friend. 184 if (!bareFrom.equals(friendJid)) { 185 LOGGER.log(Level.WARNING, 186 "Ignoring friendship recommendation " + friendMessage 187 + " because it does not recommend itself, but " 188 + friendJid + '.'); 189 return; 190 } 191 192 // Re-try the friendship request. 193 sendFriendshipRequest(friendJid); 194 } 195 } 196 }, FRIEND_MESSAGE); 197 198 connection.registerIQRequestHandler( 199 new AbstractIqRequestHandler(ClearCache.ELEMENT, ClearCache.NAMESPACE, Type.set, Mode.async) { 200 @Override 201 public IQ handleIQRequest(IQ iqRequest) { 202 if (!isFromProvisioningService(iqRequest, true)) { 203 return null; 204 } 205 206 ClearCache clearCache = (ClearCache) iqRequest; 207 208 // Handle <clearCache/> request. 209 Jid from = iqRequest.getFrom(); 210 LruCache<BareJid, Void> cache = negativeFriendshipRequestCache.lookup(from); 211 if (cache != null) { 212 cache.clear(); 213 } 214 215 return new ClearCacheResponse(clearCache); 216 } 217 }); 218 219 roster = Roster.getInstanceFor(connection); 220 roster.addSubscribeListener(new SubscribeListener() { 221 @Override 222 public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) { 223 // First check if the subscription request comes from a known registry and accept the request if so. 224 try { 225 if (IoTDiscoveryManager.getInstanceFor(connection()).isRegistry(from.asBareJid())) { 226 return SubscribeAnswer.Approve; 227 } 228 } 229 catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { 230 LOGGER.log(Level.WARNING, "Could not determine if " + from + " is a registry", e); 231 } 232 233 Jid provisioningServer = null; 234 try { 235 provisioningServer = getConfiguredProvisioningServer(); 236 } 237 catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { 238 LOGGER.log(Level.WARNING, 239 "Could not determine provisioning server. Ignoring friend request from " + from, e); 240 } 241 if (provisioningServer == null) { 242 return null; 243 } 244 245 boolean isFriend; 246 try { 247 isFriend = isFriend(provisioningServer, from.asBareJid()); 248 } 249 catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { 250 LOGGER.log(Level.WARNING, "Could not determine if " + from + " is a friend.", e); 251 return null; 252 } 253 254 if (isFriend) { 255 return SubscribeAnswer.Approve; 256 } 257 else { 258 return SubscribeAnswer.Deny; 259 } 260 } 261 }); 262 263 roster.addPresenceEventListener(new AbstractPresenceEventListener() { 264 @Override 265 public void presenceSubscribed(BareJid address, Presence subscribedPresence) { 266 friendshipRequestedCache.remove(address); 267 for (BecameFriendListener becameFriendListener : becameFriendListeners) { 268 becameFriendListener.becameFriend(address, subscribedPresence); 269 } 270 } 271 @Override 272 public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) { 273 if (friendshipRequestedCache.containsKey(address)) { 274 friendshipDeniedCache.put(address, null); 275 } 276 for (WasUnfriendedListener wasUnfriendedListener : wasUnfriendedListeners) { 277 wasUnfriendedListener.wasUnfriendedListener(address, unsubscribedPresence); 278 } 279 } 280 }); 281 } 282 283 /** 284 * Set the configured provisioning server. Use <code>null</code> as provisioningServer to use 285 * automatic discovery of the provisioning server (the default behavior). 286 * 287 * @param provisioningServer 288 */ 289 public void setConfiguredProvisioningServer(Jid provisioningServer) { 290 this.configuredProvisioningServer = provisioningServer; 291 } 292 293 public Jid getConfiguredProvisioningServer() 294 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 295 if (configuredProvisioningServer == null) { 296 configuredProvisioningServer = findProvisioningServerComponent(); 297 } 298 return configuredProvisioningServer; 299 } 300 301 /** 302 * Try to find a provisioning server component. 303 * 304 * @return the XMPP address of the provisioning server component if one was found. 305 * @throws NoResponseException 306 * @throws XMPPErrorException 307 * @throws NotConnectedException 308 * @throws InterruptedException 309 * @see <a href="http://xmpp.org/extensions/xep-0324.html#servercomponent">XEP-0324 § 3.1.2 Provisioning Server as a server component</a> 310 */ 311 public DomainBareJid findProvisioningServerComponent() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 312 final XMPPConnection connection = connection(); 313 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); 314 List<DiscoverInfo> discoverInfos = sdm.findServicesDiscoverInfo(Constants.IOT_PROVISIONING_NAMESPACE, true, true); 315 if (discoverInfos.isEmpty()) { 316 return null; 317 } 318 Jid jid = discoverInfos.get(0).getFrom(); 319 assert (jid.isDomainBareJid()); 320 return jid.asDomainBareJid(); 321 } 322 323 /** 324 * As the given provisioning server is the given JID is a friend. 325 * 326 * @param provisioningServer the provisioning server to ask. 327 * @param friendInQuestion the JID to ask about. 328 * @return <code>true</code> if the JID is a friend, <code>false</code> otherwise. 329 * @throws NoResponseException 330 * @throws XMPPErrorException 331 * @throws NotConnectedException 332 * @throws InterruptedException 333 */ 334 public boolean isFriend(Jid provisioningServer, BareJid friendInQuestion) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 335 LruCache<BareJid, Void> cache = negativeFriendshipRequestCache.lookup(provisioningServer); 336 if (cache != null && cache.containsKey(friendInQuestion)) { 337 // We hit a cached negative isFriend response for this provisioning server. 338 return false; 339 } 340 341 IoTIsFriend iotIsFriend = new IoTIsFriend(friendInQuestion); 342 iotIsFriend.setTo(provisioningServer); 343 IoTIsFriendResponse response = connection().createStanzaCollectorAndSend(iotIsFriend).nextResultOrThrow(); 344 assert (response.getJid().equals(friendInQuestion)); 345 boolean isFriend = response.getIsFriendResult(); 346 if (!isFriend) { 347 // Cache the negative is friend response. 348 if (cache == null) { 349 cache = new LruCache<>(1024); 350 negativeFriendshipRequestCache.put(provisioningServer, cache); 351 } 352 cache.put(friendInQuestion, null); 353 } 354 return isFriend; 355 } 356 357 public boolean iAmFriendOf(BareJid otherJid) { 358 return roster.iAmSubscribedTo(otherJid); 359 } 360 361 public void sendFriendshipRequest(BareJid bareJid) throws NotConnectedException, InterruptedException { 362 Presence presence = new Presence(Presence.Type.subscribe); 363 presence.setTo(bareJid); 364 365 friendshipRequestedCache.put(bareJid, null); 366 367 connection().sendStanza(presence); 368 } 369 370 public void sendFriendshipRequestIfRequired(BareJid jid) throws NotConnectedException, InterruptedException { 371 if (iAmFriendOf(jid)) return; 372 373 sendFriendshipRequest(jid); 374 } 375 376 public boolean isMyFriend(Jid friendInQuestion) { 377 return roster.isSubscribedToMyPresence(friendInQuestion); 378 } 379 380 public void unfriend(Jid friend) throws NotConnectedException, InterruptedException { 381 if (isMyFriend(friend)) { 382 Presence presence = new Presence(Presence.Type.unsubscribed); 383 presence.setTo(friend); 384 connection().sendStanza(presence); 385 } 386 } 387 388 public boolean addBecameFriendListener(BecameFriendListener becameFriendListener) { 389 return becameFriendListeners.add(becameFriendListener); 390 } 391 392 public boolean removeBecameFriendListener(BecameFriendListener becameFriendListener) { 393 return becameFriendListeners.remove(becameFriendListener); 394 } 395 396 public boolean addWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { 397 return wasUnfriendedListeners.add(wasUnfriendedListener); 398 } 399 400 public boolean removeWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { 401 return wasUnfriendedListeners.remove(wasUnfriendedListener); 402 } 403 404 private boolean isFromProvisioningService(Stanza stanza, boolean log) { 405 Jid provisioningServer; 406 try { 407 provisioningServer = getConfiguredProvisioningServer(); 408 } 409 catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) { 410 LOGGER.log(Level.WARNING, "Could determine provisioning server", e); 411 return false; 412 } 413 if (provisioningServer == null) { 414 if (log) { 415 LOGGER.warning("Ignoring request '" + stanza 416 + "' because no provisioning server configured."); 417 } 418 return false; 419 } 420 if (!provisioningServer.equals(stanza.getFrom())) { 421 if (log) { 422 LOGGER.warning("Ignoring request '" + stanza 423 + "' because not from provisioning server '" + provisioningServer 424 + "'."); 425 } 426 return false; 427 } 428 return true; 429 } 430}