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