001/** 002 * 003 * Copyright 2016-2019 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 = connection.getStanzaFactory().buildPresenceStanza() 144 .ofType(Presence.Type.unsubscribed) 145 .to(unfriendJid) 146 .build(); 147 connection.sendStanza(unsubscribed); 148 } 149 }, UNFRIEND_MESSAGE); 150 151 // Stanza listener for XEP-0324 § 3.2.4 "Recommending Friendships". 152 // Also includes business logic for thing-to-thing friendship recommendations, which is not 153 // (yet) part of the XEP. 154 connection.addAsyncStanzaListener(new StanzaListener() { 155 @Override 156 public void processStanza(final Stanza stanza) throws NotConnectedException, InterruptedException { 157 final Message friendMessage = (Message) stanza; 158 final Friend friend = Friend.from(friendMessage); 159 final BareJid friendJid = friend.getFriend(); 160 161 if (isFromProvisioningService(friendMessage, false)) { 162 // We received a recommendation from a provisioning server. 163 // Notify the recommended friend that we will now accept his 164 // friendship requests. 165 final XMPPConnection connection = connection(); 166 Friend friendNotification = new Friend(connection.getUser().asBareJid()); 167 Message notificationMessage = connection.getStanzaFactory().buildMessageStanza() 168 .to(friendJid) 169 .addExtension(friendNotification) 170 .build(); 171 connection.sendStanza(notificationMessage); 172 } else { 173 // Check is the message was send from a thing we previously 174 // tried to become friends with. If this is the case, then 175 // thing is likely telling us that we can become now 176 // friends. 177 BareJid bareFrom = friendMessage.getFrom().asBareJid(); 178 if (!friendshipDeniedCache.containsKey(bareFrom)) { 179 LOGGER.log(Level.WARNING, "Ignoring friendship recommendation " 180 + friendMessage 181 + " because friendship to this JID was not previously denied."); 182 return; 183 } 184 185 // Sanity check: If a thing recommends us itself as friend, 186 // which should be the case once we reach this code, then 187 // the bare 'from' JID should be equals to the JID of the 188 // recommended friend. 189 if (!bareFrom.equals(friendJid)) { 190 LOGGER.log(Level.WARNING, 191 "Ignoring friendship recommendation " + friendMessage 192 + " because it does not recommend itself, but " 193 + friendJid + '.'); 194 return; 195 } 196 197 // Re-try the friendship request. 198 sendFriendshipRequest(friendJid); 199 } 200 } 201 }, FRIEND_MESSAGE); 202 203 connection.registerIQRequestHandler( 204 new AbstractIqRequestHandler(ClearCache.ELEMENT, ClearCache.NAMESPACE, Type.set, Mode.async) { 205 @Override 206 public IQ handleIQRequest(IQ iqRequest) { 207 if (!isFromProvisioningService(iqRequest, true)) { 208 return null; 209 } 210 211 ClearCache clearCache = (ClearCache) iqRequest; 212 213 // Handle <clearCache/> request. 214 Jid from = iqRequest.getFrom(); 215 LruCache<BareJid, Void> cache = negativeFriendshipRequestCache.lookup(from); 216 if (cache != null) { 217 cache.clear(); 218 } 219 220 return new ClearCacheResponse(clearCache); 221 } 222 }); 223 224 roster = Roster.getInstanceFor(connection); 225 roster.addSubscribeListener(new SubscribeListener() { 226 @Override 227 public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) { 228 // First check if the subscription request comes from a known registry and accept the request if so. 229 try { 230 if (IoTDiscoveryManager.getInstanceFor(connection()).isRegistry(from.asBareJid())) { 231 return SubscribeAnswer.Approve; 232 } 233 } 234 catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { 235 LOGGER.log(Level.WARNING, "Could not determine if " + from + " is a registry", e); 236 } 237 238 Jid provisioningServer = null; 239 try { 240 provisioningServer = getConfiguredProvisioningServer(); 241 } 242 catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { 243 LOGGER.log(Level.WARNING, 244 "Could not determine provisioning server. Ignoring friend request from " + from, e); 245 } 246 if (provisioningServer == null) { 247 return null; 248 } 249 250 boolean isFriend; 251 try { 252 isFriend = isFriend(provisioningServer, from.asBareJid()); 253 } 254 catch (NoResponseException | XMPPErrorException | NotConnectedException | InterruptedException e) { 255 LOGGER.log(Level.WARNING, "Could not determine if " + from + " is a friend.", e); 256 return null; 257 } 258 259 if (isFriend) { 260 return SubscribeAnswer.Approve; 261 } 262 else { 263 return SubscribeAnswer.Deny; 264 } 265 } 266 }); 267 268 roster.addPresenceEventListener(new AbstractPresenceEventListener() { 269 @Override 270 public void presenceSubscribed(BareJid address, Presence subscribedPresence) { 271 friendshipRequestedCache.remove(address); 272 for (BecameFriendListener becameFriendListener : becameFriendListeners) { 273 becameFriendListener.becameFriend(address, subscribedPresence); 274 } 275 } 276 @Override 277 public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) { 278 if (friendshipRequestedCache.containsKey(address)) { 279 friendshipDeniedCache.put(address, null); 280 } 281 for (WasUnfriendedListener wasUnfriendedListener : wasUnfriendedListeners) { 282 wasUnfriendedListener.wasUnfriendedListener(address, unsubscribedPresence); 283 } 284 } 285 }); 286 } 287 288 /** 289 * Set the configured provisioning server. Use <code>null</code> as provisioningServer to use 290 * automatic discovery of the provisioning server (the default behavior). 291 * 292 * @param provisioningServer TODO javadoc me please 293 */ 294 public void setConfiguredProvisioningServer(Jid provisioningServer) { 295 this.configuredProvisioningServer = provisioningServer; 296 } 297 298 public Jid getConfiguredProvisioningServer() 299 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 300 if (configuredProvisioningServer == null) { 301 configuredProvisioningServer = findProvisioningServerComponent(); 302 } 303 return configuredProvisioningServer; 304 } 305 306 /** 307 * Try to find a provisioning server component. 308 * 309 * @return the XMPP address of the provisioning server component if one was found. 310 * @throws NoResponseException if there was no response from the remote entity. 311 * @throws XMPPErrorException if there was an XMPP error returned. 312 * @throws NotConnectedException if the XMPP connection is not connected. 313 * @throws InterruptedException if the calling thread was interrupted. 314 * @see <a href="http://xmpp.org/extensions/xep-0324.html#servercomponent">XEP-0324 § 3.1.2 Provisioning Server as a server component</a> 315 */ 316 public DomainBareJid findProvisioningServerComponent() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 317 final XMPPConnection connection = connection(); 318 ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection); 319 List<DiscoverInfo> discoverInfos = sdm.findServicesDiscoverInfo(Constants.IOT_PROVISIONING_NAMESPACE, true, true); 320 if (discoverInfos.isEmpty()) { 321 return null; 322 } 323 Jid jid = discoverInfos.get(0).getFrom(); 324 assert jid.isDomainBareJid(); 325 return jid.asDomainBareJid(); 326 } 327 328 /** 329 * As the given provisioning server is the given JID is a friend. 330 * 331 * @param provisioningServer the provisioning server to ask. 332 * @param friendInQuestion the JID to ask about. 333 * @return <code>true</code> if the JID is a friend, <code>false</code> otherwise. 334 * @throws NoResponseException if there was no response from the remote entity. 335 * @throws XMPPErrorException if there was an XMPP error returned. 336 * @throws NotConnectedException if the XMPP connection is not connected. 337 * @throws InterruptedException if the calling thread was interrupted. 338 */ 339 public boolean isFriend(Jid provisioningServer, BareJid friendInQuestion) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 340 LruCache<BareJid, Void> cache = negativeFriendshipRequestCache.lookup(provisioningServer); 341 if (cache != null && cache.containsKey(friendInQuestion)) { 342 // We hit a cached negative isFriend response for this provisioning server. 343 return false; 344 } 345 346 IoTIsFriend iotIsFriend = new IoTIsFriend(friendInQuestion); 347 iotIsFriend.setTo(provisioningServer); 348 IoTIsFriendResponse response = connection().createStanzaCollectorAndSend(iotIsFriend).nextResultOrThrow(); 349 assert response.getJid().equals(friendInQuestion); 350 boolean isFriend = response.getIsFriendResult(); 351 if (!isFriend) { 352 // Cache the negative is friend response. 353 if (cache == null) { 354 cache = new LruCache<>(1024); 355 negativeFriendshipRequestCache.put(provisioningServer, cache); 356 } 357 cache.put(friendInQuestion, null); 358 } 359 return isFriend; 360 } 361 362 public boolean iAmFriendOf(BareJid otherJid) { 363 return roster.iAmSubscribedTo(otherJid); 364 } 365 366 public void sendFriendshipRequest(BareJid bareJid) throws NotConnectedException, InterruptedException { 367 XMPPConnection connection = connection(); 368 Presence presence = connection.getStanzaFactory().buildPresenceStanza() 369 .ofType(Presence.Type.subscribe) 370 .to(bareJid) 371 .build(); 372 373 friendshipRequestedCache.put(bareJid, null); 374 375 connection().sendStanza(presence); 376 } 377 378 public void sendFriendshipRequestIfRequired(BareJid jid) throws NotConnectedException, InterruptedException { 379 if (iAmFriendOf(jid)) return; 380 381 sendFriendshipRequest(jid); 382 } 383 384 public boolean isMyFriend(Jid friendInQuestion) { 385 return roster.isSubscribedToMyPresence(friendInQuestion); 386 } 387 388 public void unfriend(Jid friend) throws NotConnectedException, InterruptedException { 389 if (isMyFriend(friend)) { 390 XMPPConnection connection = connection(); 391 Presence presence = connection.getStanzaFactory().buildPresenceStanza() 392 .ofType(Presence.Type.unsubscribed) 393 .to(friend) 394 .build(); 395 connection.sendStanza(presence); 396 } 397 } 398 399 public boolean addBecameFriendListener(BecameFriendListener becameFriendListener) { 400 return becameFriendListeners.add(becameFriendListener); 401 } 402 403 public boolean removeBecameFriendListener(BecameFriendListener becameFriendListener) { 404 return becameFriendListeners.remove(becameFriendListener); 405 } 406 407 public boolean addWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { 408 return wasUnfriendedListeners.add(wasUnfriendedListener); 409 } 410 411 public boolean removeWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { 412 return wasUnfriendedListeners.remove(wasUnfriendedListener); 413 } 414 415 private boolean isFromProvisioningService(Stanza stanza, boolean log) { 416 Jid provisioningServer; 417 try { 418 provisioningServer = getConfiguredProvisioningServer(); 419 } 420 catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) { 421 LOGGER.log(Level.WARNING, "Could determine provisioning server", e); 422 return false; 423 } 424 if (provisioningServer == null) { 425 if (log) { 426 LOGGER.warning("Ignoring request '" + stanza 427 + "' because no provisioning server configured."); 428 } 429 return false; 430 } 431 if (!provisioningServer.equals(stanza.getFrom())) { 432 if (log) { 433 LOGGER.warning("Ignoring request '" + stanza 434 + "' because not from provisioning server '" + provisioningServer 435 + "'."); 436 } 437 return false; 438 } 439 return true; 440 } 441}