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