1 /* 2 * Copyright (C) 2004-2008 Jive Software, 2021-2022 Ignite Realtime Foundation. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.muc.spi; 18 19 import org.dom4j.DocumentHelper; 20 import org.dom4j.Element; 21 import org.dom4j.QName; 22 import org.jivesoftware.openfire.PacketException; 23 import org.jivesoftware.openfire.RoutingTable; 24 import org.jivesoftware.openfire.XMPPServer; 25 import org.jivesoftware.openfire.XMPPServerListener; 26 import org.jivesoftware.openfire.archive.Archiver; 27 import org.jivesoftware.openfire.auth.UnauthorizedException; 28 import org.jivesoftware.openfire.cluster.ClusterEventListener; 29 import org.jivesoftware.openfire.cluster.ClusterManager; 30 import org.jivesoftware.openfire.cluster.NodeID; 31 import org.jivesoftware.openfire.disco.DiscoInfoProvider; 32 import org.jivesoftware.openfire.disco.DiscoItem; 33 import org.jivesoftware.openfire.disco.DiscoItemsProvider; 34 import org.jivesoftware.openfire.disco.DiscoServerItem; 35 import org.jivesoftware.openfire.disco.IQDiscoInfoHandler; 36 import org.jivesoftware.openfire.disco.ServerItemsProvider; 37 import org.jivesoftware.openfire.group.ConcurrentGroupList; 38 import org.jivesoftware.openfire.group.GroupAwareList; 39 import org.jivesoftware.openfire.group.GroupJID; 40 import org.jivesoftware.openfire.handler.IQHandler; 41 import org.jivesoftware.openfire.handler.IQPingHandler; 42 import org.jivesoftware.openfire.muc.CannotBeInvitedException; 43 import org.jivesoftware.openfire.muc.ConflictException; 44 import org.jivesoftware.openfire.muc.ForbiddenException; 45 import org.jivesoftware.openfire.muc.HistoryRequest; 46 import org.jivesoftware.openfire.muc.HistoryStrategy; 47 import org.jivesoftware.openfire.muc.MUCEventDelegate; 48 import org.jivesoftware.openfire.muc.MUCEventDispatcher; 49 import org.jivesoftware.openfire.muc.MUCRole; 50 import org.jivesoftware.openfire.muc.MUCRoom; 51 import org.jivesoftware.openfire.muc.MultiUserChatService; 52 import org.jivesoftware.openfire.muc.NotAcceptableException; 53 import org.jivesoftware.openfire.muc.NotAllowedException; 54 import org.jivesoftware.openfire.muc.RegistrationRequiredException; 55 import org.jivesoftware.openfire.muc.RoomLockedException; 56 import org.jivesoftware.openfire.muc.ServiceUnavailableException; 57 import org.jivesoftware.openfire.muc.cluster.SyncLocalOccupantsAndSendJoinPresenceTask; 58 import org.jivesoftware.openfire.stanzaid.StanzaIDUtil; 59 import org.jivesoftware.openfire.user.UserAlreadyExistsException; 60 import org.jivesoftware.openfire.user.UserManager; 61 import org.jivesoftware.openfire.user.UserNotFoundException; 62 import org.jivesoftware.util.AutoCloseableReentrantLock; 63 import org.jivesoftware.util.JiveGlobals; 64 import org.jivesoftware.util.JiveProperties; 65 import org.jivesoftware.util.LocaleUtils; 66 import org.jivesoftware.util.NotFoundException; 67 import org.jivesoftware.util.TaskEngine; 68 import org.jivesoftware.util.XMPPDateTimeFormat; 69 import org.jivesoftware.util.cache.Cache; 70 import org.jivesoftware.util.cache.CacheFactory; 71 import org.slf4j.Logger; 72 import org.slf4j.LoggerFactory; 73 import org.xmpp.component.Component; 74 import org.xmpp.component.ComponentManager; 75 import org.xmpp.forms.DataForm; 76 import org.xmpp.forms.DataForm.Type; 77 import org.xmpp.forms.FormField; 78 import org.xmpp.packet.IQ; 79 import org.xmpp.packet.JID; 80 import org.xmpp.packet.Message; 81 import org.xmpp.packet.Packet; 82 import org.xmpp.packet.PacketError; 83 import org.xmpp.packet.Presence; 84 import org.xmpp.resultsetmanagement.ResultSet; 85 86 import javax.annotation.Nonnull; 87 import javax.annotation.Nullable; 88 import java.time.Duration; 89 import java.time.Instant; 90 import java.util.*; 91 import java.util.concurrent.ExecutorService; 92 import java.util.concurrent.Executors; 93 import java.util.concurrent.TimeUnit; 94 import java.util.concurrent.atomic.AtomicInteger; 95 import java.util.concurrent.atomic.AtomicLong; 96 import java.util.concurrent.locks.Lock; 97 import java.util.stream.Collectors; 98 import java.util.stream.Stream; 99 100 /** 101 * Implements the chat server as a cached memory resident chat server. The server is also 102 * responsible for responding Multi-User Chat disco requests as well as removing inactive users from 103 * the rooms after a period of time and to maintain a log of the conversation in the rooms that 104 * require to log their conversations. The conversations log is saved to the database using a 105 * separate process. 106 * <p> 107 * Temporary rooms are held in memory as long as they have occupants. They will be destroyed after 108 * the last occupant left the room. On the other hand, persistent rooms are always present in memory 109 * even after the last occupant left the room. In order to keep memory clean of persistent rooms that 110 * have been forgotten or abandoned this class includes a clean up process. The clean up process 111 * will remove from memory rooms that haven't had occupants for a while. Moreover, forgotten or 112 * abandoned rooms won't be loaded into memory when the Multi-User Chat service starts up.</p> 113 * 114 * @author Gaston Dombiak 115 */ 116 public class MultiUserChatServiceImpl implements Component, MultiUserChatService, 117 ServerItemsProvider, DiscoInfoProvider, DiscoItemsProvider, XMPPServerListener, ClusterEventListener 118 { 119 private static final Logger Log = LoggerFactory.getLogger(MultiUserChatServiceImpl.class); 120 121 /** 122 * The time to elapse between clearing of idle chat users. 123 */ 124 private Duration userIdleTaskInterval; 125 126 /** 127 * The period that a user must be idle before he/she gets kicked from all the rooms. Null to disable the feature. 128 */ 129 private Duration userIdleKick = null; 130 131 /** 132 * The period that a user must be idle before he/she gets pinged from the rooms that they're in, to determine if 133 * they're a 'ghost'. Null to disable the feature. 134 */ 135 private Duration userIdlePing = null; 136 137 /** 138 * Task that kicks and pings idle users from the rooms. 139 */ 140 private UserTimeoutTask userTimeoutTask; 141 142 /** 143 * The maximum amount of logs to be written to the database in one iteration. 144 */ 145 private int logMaxConversationBatchSize; 146 147 /** 148 * The maximum time between database writes of log batches. 149 */ 150 private Duration logMaxBatchInterval; 151 152 /** 153 * Logs are written to the database almost instantly, but are batched together 154 * when a new log entry becomes available within the amount of time defined 155 * in this field - unless the total amount of time since the last write 156 * is larger then #maxbatchinterval. 157 */ 158 private Duration logBatchGracePeriod; 159 160 /** 161 * the chat service's hostname (subdomain) 162 */ 163 private final String chatServiceName; 164 /** 165 * the chat service's description 166 */ 167 private String chatDescription; 168 169 /** 170 * Responsible for maintaining the in-memory collection of MUCRooms for this service. 171 */ 172 private final LocalMUCRoomManager localMUCRoomManager; 173 174 /** 175 * Responsible for maintaining the in-memory collection of MUCUsers for this service. 176 */ 177 private final OccupantManager occupantManager; 178 179 private final HistoryStrategy historyStrategy; 180 181 private RoutingTable routingTable = null; 182 183 /** 184 * The handler of packets with namespace jabber:iq:register for the server. 185 */ 186 private IQMUCRegisterHandler registerHandler = null; 187 188 /** 189 * The handler of search requests ('jabber:iq:search' namespace). 190 */ 191 private IQMUCSearchHandler searchHandler = null; 192 193 /** 194 * The handler of search requests ('https://xmlns.zombofant.net/muclumbus/search/1.0' namespace). 195 */ 196 private IQMuclumbusSearchHandler muclumbusSearchHandler = null; 197 198 /** 199 * The handler of VCard requests. 200 */ 201 private IQMUCvCardHandler mucVCardHandler = null; 202 203 /** 204 * Plugin (etc) provided IQ Handlers for MUC: 205 */ 206 private Map<String,IQHandler> iqHandlers = null; 207 208 /** 209 * The total time all agents took to chat * 210 */ 211 private long totalChatTime; 212 213 /** 214 * Flag that indicates if the service should provide information about locked rooms when 215 * handling service discovery requests. 216 * Note: Setting this flag in false is not compliant with the spec. A user may try to join a 217 * locked room thinking that the room doesn't exist because the user didn't discover it before. 218 */ 219 private boolean allowToDiscoverLockedRooms = true; 220 221 /** 222 * Flag that indicates if the service should provide information about non-public members-only 223 * rooms when handling service discovery requests. 224 */ 225 private boolean allowToDiscoverMembersOnlyRooms = false; 226 227 /** 228 * Returns the permission policy for creating rooms. A true value means that not anyone can 229 * create a room. Users are allowed to create rooms only when 230 * <code>isAllRegisteredUsersAllowedToCreate</code> or <code>getUsersAllowedToCreate</code> 231 * (or both) allow them to. 232 */ 233 private boolean roomCreationRestricted = false; 234 235 /** 236 * Determines if all registered users (as opposed to anonymous users, and users from other 237 * XMPP domains) are allowed to create rooms. 238 */ 239 private boolean allRegisteredUsersAllowedToCreate = false; 240 241 /** 242 * Bare jids of users that are allowed to create MUC rooms. Might also include group jids. 243 */ 244 private GroupAwareList<JID> allowedToCreate = new ConcurrentGroupList<>(); 245 246 /** 247 * Bare jids of users that are system administrators of the MUC service. A sysadmin has the same 248 * permissions as a room owner. Might also contain group jids. 249 */ 250 private GroupAwareList<JID> sysadmins = new ConcurrentGroupList<>(); 251 252 /** 253 * Queue that holds the messages to log for the rooms that need to log their conversations. 254 */ 255 private volatile Archiver<ConversationLogEntry> archiver; 256 257 /** 258 * Max number of hours that a persistent room may be empty before the service removes the 259 * room from memory. Unloaded rooms will exist in the database and may be loaded by a user 260 * request. Default time limit is: 30 days. 261 */ 262 private long emptyLimit = 30 * 24; 263 264 /** 265 * The time to elapse between each rooms cleanup. Default frequency is 60 minutes. 266 */ 267 private static final long CLEANUP_FREQUENCY = 60; 268 269 /** 270 * Total number of received messages in all rooms since the last reset. The counter 271 * is reset each time the Statistic makes a sampling. 272 */ 273 private final AtomicInteger inMessages = new AtomicInteger(0); 274 /** 275 * Total number of broadcasted messages in all rooms since the last reset. The counter 276 * is reset each time the Statistic makes a sampling. 277 */ 278 private final AtomicLong outMessages = new AtomicLong(0); 279 280 /** 281 * Flag that indicates if MUC service is enabled. 282 */ 283 private boolean serviceEnabled = true; 284 285 /** 286 * Flag that indicates if MUC service is hidden from services views. 287 */ 288 private boolean isHidden; 289 290 /** 291 * Delegate responds to events for the MUC service. 292 */ 293 private MUCEventDelegate mucEventDelegate; 294 295 /** 296 * Additional features to be added to the disco response for the service. 297 */ 298 private final List<String> extraDiscoFeatures = new ArrayList<>(); 299 300 /** 301 * Additional identities to be added to the disco response for the service. 302 */ 303 private final List<Element> extraDiscoIdentities = new ArrayList<>(); 304 305 /** 306 * Briefly holds stanza IDs and recipients of IQ ping requests, sent to determine if an occupant is still present in 307 * the room ('ghost user' detection). Maps a stanza ID to the JID that has been pinged. Note that the stanza ID 308 * value should be unique, to not overwrite others. 309 * 310 * The eviction time of entries is short, so that we do not need to bother with manual cache evictions. 311 * 312 * The same cache can be used for all MUC services as there is no service or room-specific data in the cache. 313 */ 314 private static final Cache<String, JID> PINGS_SENT = CacheFactory.createCache("MUC Service Pings Sent"); 315 316 /** 317 * Create a new group chat server. 318 * 319 * @param subdomain 320 * Subdomain portion of the conference services (for example, 321 * conference for conference.example.org) 322 * @param description 323 * Short description of service for disco and such. If 324 * {@code null} or empty, a default value will be used. 325 * @param isHidden 326 * True if this service should be hidden from services views. 327 * @throws IllegalArgumentException 328 * if the provided subdomain is an invalid, according to the JID 329 * domain definition. 330 */ MultiUserChatServiceImpl(final String subdomain, final String description, final Boolean isHidden)331 public MultiUserChatServiceImpl(final String subdomain, final String description, final Boolean isHidden) { 332 // Check subdomain and throw an IllegalArgumentException if its invalid 333 new JID(null,subdomain + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(), null); 334 335 this.chatServiceName = subdomain; 336 if (description != null && description.trim().length() > 0) { 337 this.chatDescription = description; 338 } 339 else { 340 this.chatDescription = LocaleUtils.getLocalizedString("muc.service-name"); 341 } 342 this.isHidden = isHidden; 343 historyStrategy = new HistoryStrategy(null); 344 345 localMUCRoomManager = new LocalMUCRoomManager(this); 346 occupantManager = new OccupantManager(this); 347 } 348 349 @Override 350 @Nonnull getOccupantManager()351 public OccupantManager getOccupantManager() { 352 return occupantManager; 353 } 354 355 @Override addIQHandler(final IQHandler iqHandler)356 public void addIQHandler(final IQHandler iqHandler) { 357 if (this.iqHandlers == null) { 358 this.iqHandlers = new HashMap<>(); 359 } 360 this.iqHandlers.put(iqHandler.getInfo().getNamespace(), iqHandler); 361 } 362 363 @Override removeIQHandler(final IQHandler iqHandler)364 public void removeIQHandler(final IQHandler iqHandler) { 365 if (this.iqHandlers != null) { 366 if (iqHandler == this.iqHandlers.get(iqHandler.getInfo().getNamespace())) { 367 this.iqHandlers.remove(iqHandler.getInfo().getNamespace()); 368 } 369 } 370 } 371 372 @Override getDescription()373 public String getDescription() { 374 return chatDescription; 375 } 376 setDescription(final String desc)377 public void setDescription(final String desc) { 378 this.chatDescription = desc; 379 } 380 381 @Override processPacket(final Packet packet)382 public void processPacket(final Packet packet) { 383 384 Log.trace( "Routing stanza: {}", packet.toXML() ); 385 if (!isServiceEnabled()) { 386 Log.trace( "Service is disabled. Ignoring stanza." ); 387 return; 388 } 389 // The MUC service will receive all the packets whose domain matches the domain of the MUC 390 // service. This means that, for instance, a disco request should be responded by the 391 // service itself instead of relying on the server to handle the request. 392 try { 393 // Check for IQ Ping responses 394 if (isPendingPingResponse(packet)) { 395 Log.debug("Ping response received from occupant '{}', addressed to: '{}'", packet.getFrom(), packet.getTo()); 396 return; 397 } 398 // Check for 'ghost' users (OF-910 / OF-2209 / OF-2369) 399 if (isDeliveryRelatedErrorResponse(packet)) { 400 Log.info("Received a stanza that contained a delivery-related error response from {}. This is indicative of a 'ghost' user. Removing this user from all chat rooms.", packet.getFrom()); 401 removeChatUser(packet.getFrom()); 402 return; 403 } 404 // Check if the packet is a disco request or a packet with namespace iq:register 405 if (packet instanceof IQ) { 406 if (process((IQ)packet)) { 407 Log.trace( "Done processing IQ stanza." ); 408 return; 409 } 410 } 411 412 if ( packet.getTo().getNode() == null ) 413 { 414 Log.trace( "Stanza was addressed at the service itself, which by now should have been handled." ); 415 if ( packet instanceof IQ && ((IQ) packet).isRequest() ) 416 { 417 final IQ reply = IQ.createResultIQ( (IQ) packet ); 418 reply.setChildElement( ((IQ) packet).getChildElement().createCopy() ); 419 reply.setError( PacketError.Condition.feature_not_implemented ); 420 XMPPServer.getInstance().getPacketRouter().route( reply ); 421 } 422 Log.debug( "Ignoring stanza addressed at conference service: {}", packet.toXML() ); 423 } 424 else 425 { 426 Log.trace( "The stanza is a normal packet that should possibly be sent to the room." ); 427 final JID recipient = packet.getTo(); 428 final String roomName = recipient != null ? recipient.getNode() : null; 429 final JID userJid = packet.getFrom(); 430 occupantManager.registerActivity(userJid); 431 Log.trace( "Stanza recipient: {}, room name: {}, sender: {}", recipient, roomName, userJid ); 432 try (final AutoCloseableReentrantLock.AutoCloseableLock ignored = new AutoCloseableReentrantLock(MultiUserChatServiceImpl.class, userJid.toString()).lock()) { 433 if ( !packet.getElement().elements(FMUCHandler.FMUC).isEmpty() ) { 434 Log.trace( "Stanza is a FMUC stanza." ); 435 if (roomName == null) { 436 Log.warn("Unable to process FMUC stanza, as it does not address a room: {}", packet.toXML()); 437 } else { 438 final Lock lock = getChatRoomLock(roomName); 439 lock.lock(); 440 try { 441 final MUCRoom chatRoom = getChatRoom(roomName); 442 if (chatRoom != null) { 443 chatRoom.getFmucHandler().process(packet); 444 // Ensure that other cluster nodes see the changes applied by the method above. 445 syncChatRoom(chatRoom); 446 } else { 447 Log.warn("Unable to process FMUC stanza, as room it's addressed to does not exist: {}", roomName); 448 // FIXME need to send error back in case of IQ request, and FMUC join. Might want to send error back in other cases too. 449 } 450 } finally { 451 lock.unlock(); 452 } 453 } 454 } else { 455 Log.trace( "Stanza is a regular MUC stanza." ); 456 processRegularStanza(packet); 457 } 458 } 459 } 460 } 461 catch (final Exception e) { 462 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 463 } 464 } 465 466 /** 467 * Returns true if the IQ packet was processed. This method should only process disco packets 468 * as well as jabber:iq:register packets sent to the MUC service. 469 * 470 * @param iq the IQ packet to process. 471 * @return true if the IQ packet was processed. 472 */ process(final IQ iq)473 private boolean process(final IQ iq) { 474 final Element childElement = iq.getChildElement(); 475 String namespace = null; 476 // Ignore IQs of type ERROR 477 if (IQ.Type.error == iq.getType()) { 478 return false; 479 } 480 if (iq.getTo().getResource() != null) { 481 // Ignore IQ packets sent to room occupants 482 return false; 483 } 484 if (childElement != null) { 485 namespace = childElement.getNamespaceURI(); 486 } 487 if ("jabber:iq:register".equals(namespace)) { 488 final IQ reply = registerHandler.handleIQ(iq); 489 XMPPServer.getInstance().getPacketRouter().route(reply); 490 } 491 else if ("jabber:iq:search".equals(namespace)) { 492 final IQ reply = searchHandler.handleIQ(iq); 493 XMPPServer.getInstance().getPacketRouter().route(reply); 494 } 495 else if (IQMuclumbusSearchHandler.NAMESPACE.equals(namespace)) { 496 final IQ reply = muclumbusSearchHandler.handleIQ(iq); 497 XMPPServer.getInstance().getPacketRouter().route(reply); 498 } 499 else if (IQMUCvCardHandler.NAMESPACE.equals(namespace)) { 500 final IQ reply = mucVCardHandler.handleIQ(iq); 501 XMPPServer.getInstance().getPacketRouter().route(reply); 502 } 503 else if ("http://jabber.org/protocol/disco#info".equals(namespace)) { 504 // TODO MUC should have an IQDiscoInfoHandler of its own when MUC becomes 505 // a component 506 final IQ reply = XMPPServer.getInstance().getIQDiscoInfoHandler().handleIQ(iq); 507 XMPPServer.getInstance().getPacketRouter().route(reply); 508 } 509 else if ("http://jabber.org/protocol/disco#items".equals(namespace)) { 510 // TODO MUC should have an IQDiscoItemsHandler of its own when MUC becomes 511 // a component 512 final IQ reply = XMPPServer.getInstance().getIQDiscoItemsHandler().handleIQ(iq); 513 XMPPServer.getInstance().getPacketRouter().route(reply); 514 } 515 else if ("urn:xmpp:ping".equals(namespace)) { 516 XMPPServer.getInstance().getPacketRouter().route( IQ.createResultIQ(iq) ); 517 } 518 else if (this.iqHandlers != null) { 519 final IQHandler h = this.iqHandlers.get(namespace); 520 if (h != null) { 521 try { 522 final IQ reply = h.handleIQ(iq); 523 if (reply != null) { 524 XMPPServer.getInstance().getPacketRouter().route(reply); 525 } 526 } catch (final UnauthorizedException e) { 527 final IQ reply = IQ.createResultIQ(iq); 528 reply.setType(IQ.Type.error); 529 reply.setError(PacketError.Condition.service_unavailable); 530 XMPPServer.getInstance().getPacketRouter().route(reply); 531 } 532 return true; 533 } 534 return false; 535 } else { 536 return false; 537 } 538 return true; 539 } 540 541 /** 542 * Generate and send an error packet to indicate that something went wrong. 543 * 544 * @param packet the packet to be responded to with an error. 545 * @param error the reason why the operation failed. 546 * @param message an optional human-readable error message. 547 */ sendErrorPacket( Packet packet, PacketError.Condition error, String message )548 private void sendErrorPacket( Packet packet, PacketError.Condition error, String message ) 549 { 550 if (packet.getError() != null) { 551 Log.debug("Avoid generating an error in response to a stanza that itself has an error (to avoid the chance of entering an endless back-and-forth of exchanging errors). Suppress sending an {} error (with message '{}') in response to: {}", error, message, packet); 552 return; 553 } 554 if ( packet instanceof IQ ) 555 { 556 IQ reply = IQ.createResultIQ((IQ) packet); 557 reply.setChildElement(((IQ) packet).getChildElement().createCopy()); 558 reply.setError(error); 559 if ( message != null ) 560 { 561 reply.getError().setText(message); 562 } 563 XMPPServer.getInstance().getPacketRouter().route(reply); 564 } 565 else 566 { 567 Packet reply = packet.createCopy(); 568 reply.setError(error); 569 if ( message != null ) 570 { 571 reply.getError().setText(message); 572 } 573 reply.setFrom(packet.getTo()); 574 reply.setTo(packet.getFrom()); 575 XMPPServer.getInstance().getPacketRouter().route(reply); 576 } 577 } 578 579 /** 580 * This method does all stanz routing in the chat server for 'regular' MUC stanzas. Packet routing is actually very 581 * simple: 582 * 583 * <ul> 584 * <li>Discover the room the user is talking to</li> 585 * <li>If the room is not registered and this is a presence "available" packet, try to join the room</li> 586 * <li>If the room is registered, and presence "unavailable" leave the room</li> 587 * <li>Otherwise, rewrite the sender address and send to the room.</li> 588 * </ul> 589 * 590 * @param packet The stanza to route 591 */ processRegularStanza( Packet packet )592 public void processRegularStanza( Packet packet ) throws UnauthorizedException, PacketException 593 { 594 // Name of the room that the stanza is addressed to. 595 final String roomName = packet.getTo().getNode(); 596 597 if ( roomName == null ) 598 { 599 // Packets to the groupchat service (as opposed to a specific room on the service). This should not occur 600 // (should be handled by MultiUserChatServiceImpl instead). 601 Log.warn(LocaleUtils.getLocalizedString("muc.error.not-supported") + " " + packet.toString()); 602 if ( packet instanceof IQ && ((IQ) packet).isRequest() ) 603 { 604 sendErrorPacket(packet, PacketError.Condition.feature_not_implemented, "Unable to process stanza."); 605 } 606 return; 607 } 608 609 StanzaIDUtil.ensureUniqueAndStableStanzaID(packet, packet.getTo().asBareJID()); 610 611 final Lock lock = getChatRoomLock(roomName); 612 lock.lock(); 613 try { 614 // Get the room, if one exists. 615 @Nullable MUCRoom room = getChatRoom(roomName); 616 617 // Determine if this user has a pre-existing role in the addressed room. 618 final MUCRole preExistingRole; 619 if (room == null) { 620 preExistingRole = null; 621 } else { 622 preExistingRole = room.getOccupantByFullJID(packet.getFrom()); 623 } 624 Log.debug("Preexisting role for user {} in room {} (that currently {} exist): {}", packet.getFrom(), roomName, room == null ? "does not" : "does", preExistingRole == null ? "(none)" : preExistingRole); 625 626 if ( packet instanceof IQ ) 627 { 628 process((IQ) packet, room, preExistingRole); 629 } 630 else if ( packet instanceof Message ) 631 { 632 process((Message) packet, room, preExistingRole); 633 } 634 else if ( packet instanceof Presence ) 635 { 636 // Return value is non-null while argument is, in case this is a request to create a new room. 637 room = process((Presence) packet, roomName, room, preExistingRole); 638 639 } 640 641 // Ensure that other cluster nodes see any changes that might have been applied. 642 if (room != null) { 643 syncChatRoom(room); 644 } 645 } finally { 646 lock.unlock(); 647 } 648 } 649 650 /** 651 * Processes a Message stanza. 652 * 653 * @param packet The stanza to route 654 * @param room The room that the stanza was addressed to. 655 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 656 */ process( @onnull final Message packet, @Nullable final MUCRoom room, @Nullable final MUCRole preExistingRole )657 private void process( 658 @Nonnull final Message packet, 659 @Nullable final MUCRoom room, 660 @Nullable final MUCRole preExistingRole ) 661 { 662 if (Message.Type.error == packet.getType()) { 663 Log.trace("Ignoring messages of type 'error' sent by '{}' to MUC room '{}'", packet.getFrom(), packet.getTo()); 664 return; 665 } 666 667 if (room == null) { 668 Log.debug("Rejecting message stanza sent by '{}' to room '{}': Room does not exist.", packet.getFrom(), packet.getTo()); 669 sendErrorPacket(packet, PacketError.Condition.recipient_unavailable, "The room that the message was addressed to is not available."); 670 return; 671 } 672 673 if ( preExistingRole == null ) 674 { 675 processNonOccupantMessage(packet, room); 676 } 677 else 678 { 679 processOccupantMessage(packet, room, preExistingRole); 680 } 681 } 682 683 /** 684 * Processes a Message stanza that was sent by a user that's not in the room. 685 * 686 * Only declined invitations (to join a room) are acceptable messages from users that are not in the room. Other 687 * messages are responded to with an error. 688 * 689 * @param packet The stanza to process 690 * @param room The room that the stanza was addressed to. 691 */ processNonOccupantMessage( @onnull final Message packet, @Nonnull final MUCRoom room )692 private void processNonOccupantMessage( 693 @Nonnull final Message packet, 694 @Nonnull final MUCRoom room ) 695 { 696 boolean declinedInvitation = false; 697 Element userInfo = null; 698 if ( Message.Type.normal == packet.getType() ) 699 { 700 // An user that is not an occupant could be declining an invitation 701 userInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc#user"); 702 if ( userInfo != null && userInfo.element("decline") != null ) 703 { 704 // A user has declined an invitation to a room 705 // WARNING: Potential fraud if someone fakes the "from" of the 706 // message with the JID of a member and sends a "decline" 707 declinedInvitation = true; 708 } 709 } 710 711 if ( declinedInvitation ) 712 { 713 Log.debug("Processing room invitation declination sent by '{}' to room '{}'.", packet.getFrom(), room.getName()); 714 final Element info = userInfo.element("decline"); 715 room.sendInvitationRejection( 716 new JID(info.attributeValue("to")), 717 info.elementTextTrim("reason"), 718 packet.getFrom()); 719 } 720 else 721 { 722 Log.debug("Rejecting message stanza sent by '{}' to room '{}': Sender is not an occupant of the room: {}", packet.getFrom(), room.getName(), packet.toXML()); 723 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "You are not in the room."); 724 } 725 } 726 727 /** 728 * Processes a Message stanza that was sent by a user that's in the room. 729 * 730 * @param packet The stanza to process 731 * @param room The room that the stanza was addressed to. 732 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 733 */ processOccupantMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )734 private void processOccupantMessage( 735 @Nonnull final Message packet, 736 @Nonnull final MUCRoom room, 737 @Nonnull final MUCRole preExistingRole ) 738 { 739 // Check and reject conflicting packets with conflicting roles In other words, another user already has this nickname 740 if ( !preExistingRole.getUserAddress().equals(packet.getFrom()) ) 741 { 742 Log.debug("Rejecting conflicting stanza with conflicting roles: {}", packet.toXML()); 743 sendErrorPacket(packet, PacketError.Condition.conflict, "Another user uses this nickname."); 744 return; 745 } 746 747 if (room.getRoomHistory().isSubjectChangeRequest(packet)) 748 { 749 processChangeSubjectMessage(packet, room, preExistingRole); 750 return; 751 } 752 753 // An occupant is trying to send a private message, send public message, invite someone to the room or reject an invitation. 754 final Message.Type type = packet.getType(); 755 String nickname = packet.getTo().getResource(); 756 if ( nickname == null || nickname.trim().length() == 0 ) 757 { 758 nickname = null; 759 } 760 761 // Public message (not addressed to a specific occupant) 762 if ( nickname == null && Message.Type.groupchat == type ) 763 { 764 processPublicMessage(packet, room, preExistingRole); 765 return; 766 } 767 768 // Private message (addressed to a specific occupant) 769 if ( nickname != null && (Message.Type.chat == type || Message.Type.normal == type) ) 770 { 771 processPrivateMessage(packet, room, preExistingRole); 772 return; 773 } 774 775 if ( nickname == null && Message.Type.normal == type ) 776 { 777 // An occupant could be sending an invitation or declining an invitation 778 final Element userInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc#user"); 779 780 if ( userInfo != null && userInfo.element("invite") != null ) 781 { 782 // An occupant is sending invitations 783 processSendingInvitationMessage(packet, room, preExistingRole); 784 return; 785 } 786 787 if ( userInfo != null && userInfo.element("decline") != null ) 788 { 789 // An occupant has declined an invitation 790 processDecliningInvitationMessage(packet, room); 791 return; 792 } 793 } 794 795 Log.debug("Unable to process message: {}", packet.toXML()); 796 sendErrorPacket(packet, PacketError.Condition.bad_request, "Unable to process message."); 797 } 798 799 /** 800 * Process a 'change subject' message sent by an occupant of the room. 801 * 802 * @param packet The stanza to process 803 * @param room The room that the stanza was addressed to. 804 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 805 */ processChangeSubjectMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )806 private void processChangeSubjectMessage( 807 @Nonnull final Message packet, 808 @Nonnull final MUCRoom room, 809 @Nonnull final MUCRole preExistingRole ) 810 { 811 Log.trace("Processing subject change request from occupant '{}' to room '{}'.", packet.getFrom(), room.getName()); 812 try 813 { 814 room.changeSubject(packet, preExistingRole); 815 } 816 catch ( ForbiddenException e ) 817 { 818 Log.debug("Rejecting subject change request from occupant '{}' to room '{}'.", packet.getFrom(), room.getName(), e); 819 sendErrorPacket(packet, PacketError.Condition.forbidden, "You are not allowed to change the subject of this room."); 820 } 821 } 822 823 /** 824 * Process a public message sent by an occupant of the room. 825 * 826 * @param packet The stanza to process 827 * @param room The room that the stanza was addressed to. 828 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 829 */ processPublicMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )830 private void processPublicMessage( 831 @Nonnull final Message packet, 832 @Nonnull final MUCRoom room, 833 @Nonnull final MUCRole preExistingRole ) 834 { 835 Log.trace("Processing public message from occupant '{}' to room '{}'.", packet.getFrom(), room.getName()); 836 try 837 { 838 room.sendPublicMessage(packet, preExistingRole); 839 } 840 catch ( ForbiddenException e ) 841 { 842 Log.debug("Rejecting public message from occupant '{}' to room '{}'. User is not allowed to send message (might not have voice).", packet.getFrom(), room.getName(), e); 843 sendErrorPacket(packet, PacketError.Condition.forbidden, "You are not allowed to send a public message to the room (you might require 'voice')."); 844 } 845 } 846 847 /** 848 * Process a private message sent by an occupant of the room. 849 * 850 * @param packet The stanza to process 851 * @param room The room that the stanza was addressed to. 852 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 853 */ processPrivateMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )854 private void processPrivateMessage( 855 @Nonnull final Message packet, 856 @Nonnull final MUCRoom room, 857 @Nonnull final MUCRole preExistingRole ) 858 { 859 Log.trace("Processing private message from occupant '{}' to room '{}'.", packet.getFrom(), room.getName()); 860 try 861 { 862 room.sendPrivatePacket(packet, preExistingRole); 863 } 864 catch ( ForbiddenException e ) 865 { 866 Log.debug("Rejecting private message from occupant '{}' to room '{}'. User has a role that disallows sending private messages in this room.", packet.getFrom(), room.getName(), e); 867 sendErrorPacket(packet, PacketError.Condition.forbidden, "You are not allowed to send a private messages in the room."); 868 } 869 catch ( NotFoundException e ) 870 { 871 Log.debug("Rejecting private message from occupant '{}' to room '{}'. User addressing a non-existent recipient.", packet.getFrom(), room.getName(), e); 872 sendErrorPacket(packet, PacketError.Condition.recipient_unavailable, "The intended recipient of your private message is not available."); 873 } 874 } 875 876 /** 877 * Process a room-invitation message sent by an occupant of the room. 878 * 879 * @param packet The stanza to process 880 * @param room The room that the stanza was addressed to. 881 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 882 */ processSendingInvitationMessage( @onnull final Message packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )883 private void processSendingInvitationMessage( 884 @Nonnull final Message packet, 885 @Nonnull final MUCRoom room, 886 @Nonnull final MUCRole preExistingRole ) 887 { 888 Log.trace("Processing an invitation message from occupant '{}' to room '{}'.", packet.getFrom(), room.getName()); 889 try 890 { 891 final Element userInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc#user"); 892 893 // Try to keep the list of extensions sent together with the message invitation. These extensions will be sent to the invitees. 894 final List<Element> extensions = new ArrayList<>(packet.getElement().elements()); 895 extensions.remove(userInfo); 896 897 // Send invitations to invitees 898 final Iterator<Element> it = userInfo.elementIterator("invite"); 899 while ( it.hasNext() ) 900 { 901 Element info = it.next(); 902 JID jid = new JID(info.attributeValue("to")); 903 904 // Add the user as a member of the room if the room is members only 905 if (room.isMembersOnly()) 906 { 907 room.addMember(jid, null, preExistingRole); 908 } 909 910 // Send the invitation to the invitee 911 room.sendInvitation(jid, info.elementTextTrim("reason"), preExistingRole, extensions); 912 } 913 } 914 catch ( ForbiddenException e ) 915 { 916 Log.debug("Rejecting invitation message from occupant '{}' in room '{}': Invitations are not allowed, or occupant is not allowed to modify the member list.", packet.getFrom(), room.getName(), e); 917 sendErrorPacket(packet, PacketError.Condition.forbidden, "This room disallows invitations to be sent, or you're not allowed to modify the member list of this room."); 918 } 919 catch ( ConflictException e ) 920 { 921 Log.debug("Rejecting invitation message from occupant '{}' in room '{}'.", packet.getFrom(), room.getName(), e); 922 sendErrorPacket(packet, PacketError.Condition.conflict, "An unexpected exception occurred."); // TODO Is this code reachable? 923 } 924 catch ( CannotBeInvitedException e ) 925 { 926 Log.debug("Rejecting invitation message from occupant '{}' in room '{}': The user being invited does not have access to the room.", packet.getFrom(), room.getName(), e); 927 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "The user being invited does not have access to the room."); 928 } 929 } 930 931 /** 932 * Process a declination of a room-invitation message sent by an occupant of the room. 933 * 934 * @param packet The stanza to process 935 * @param room The room that the stanza was addressed to. 936 */ processDecliningInvitationMessage( @onnull final Message packet, @Nonnull final MUCRoom room)937 private void processDecliningInvitationMessage( 938 @Nonnull final Message packet, 939 @Nonnull final MUCRoom room) 940 { 941 Log.trace("Processing an invite declination message from '{}' to room '{}'.", packet.getFrom(), room.getName()); 942 final Element info = packet.getChildElement("x", "http://jabber.org/protocol/muc#user").element("decline"); 943 room.sendInvitationRejection(new JID(info.attributeValue("to")), 944 info.elementTextTrim("reason"), packet.getFrom()); 945 } 946 947 /** 948 * Processes an IQ stanza. 949 * 950 * @param packet The stanza to route 951 * @param room The room that the stanza was addressed to. 952 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 953 */ process( @onnull final IQ packet, @Nullable final MUCRoom room, @Nullable final MUCRole preExistingRole )954 private void process( 955 @Nonnull final IQ packet, 956 @Nullable final MUCRoom room, 957 @Nullable final MUCRole preExistingRole ) 958 { 959 // Packets to a specific node/group/room 960 if ( preExistingRole == null || room == null) 961 { 962 Log.debug("Ignoring stanza received from a non-occupant of a room (room might not even exist): {}", packet.toXML()); 963 if ( packet.isRequest() ) 964 { 965 // If a non-occupant sends a disco to an address of the form <room@service/nick>, a MUC service MUST 966 // return a <bad-request/> error. http://xmpp.org/extensions/xep-0045.html#disco-occupant 967 sendErrorPacket(packet, PacketError.Condition.bad_request, "You are not an occupant of this room."); 968 } 969 return; 970 } 971 972 if ( packet.isResponse() ) 973 { 974 // Only process IQ result packet if it's a private packet sent to another room occupant 975 if ( packet.getTo().getResource() != null ) 976 { 977 try 978 { 979 // User is sending an IQ result packet to another room occupant 980 room.sendPrivatePacket(packet, preExistingRole); 981 } 982 catch ( NotFoundException | ForbiddenException e ) 983 { 984 // Do nothing. No error will be sent to the sender of the IQ result packet 985 Log.debug("Silently ignoring an IQ response sent to the room as a private message that caused an exception while being processed: {}", packet.toXML(), e); 986 } 987 } 988 else 989 { 990 Log.trace("Silently ignoring an IQ response sent to the room, but not as a private message: {}", packet.toXML()); 991 } 992 } 993 else 994 { 995 // Check and reject conflicting packets with conflicting roles In other words, another user already has this nickname 996 if ( !preExistingRole.getUserAddress().equals(packet.getFrom()) ) 997 { 998 Log.debug("Rejecting conflicting stanza with conflicting roles: {}", packet.toXML()); 999 sendErrorPacket(packet, PacketError.Condition.conflict, "Another user uses this nickname."); 1000 return; 1001 } 1002 1003 try 1004 { 1005 // TODO Analyze if it is correct for these first two blocks to be processed without evaluating if they're addressed to the room or if they're a PM. 1006 Element query = packet.getElement().element("query"); 1007 if ( query != null && "http://jabber.org/protocol/muc#owner".equals(query.getNamespaceURI()) ) 1008 { 1009 room.getIQOwnerHandler().handleIQ(packet, preExistingRole); 1010 } 1011 else if ( query != null && "http://jabber.org/protocol/muc#admin".equals(query.getNamespaceURI()) ) 1012 { 1013 room.getIQAdminHandler().handleIQ(packet, preExistingRole); 1014 } 1015 else 1016 { 1017 final String toNickname = packet.getTo().getResource(); 1018 if ( toNickname != null ) 1019 { 1020 // User is sending to a room occupant. 1021 final boolean selfPingEnabled = JiveGlobals.getBooleanProperty("xmpp.muc.self-ping.enabled", true); 1022 if ( selfPingEnabled && toNickname.equals(preExistingRole.getNickname()) && packet.isRequest() 1023 && packet.getElement().element(QName.get(IQPingHandler.ELEMENT_NAME, IQPingHandler.NAMESPACE)) != null ) 1024 { 1025 Log.trace("User '{}' is sending an IQ 'ping' to itself. See XEP-0410: MUC Self-Ping (Schrödinger's Chat).", packet.getFrom()); 1026 XMPPServer.getInstance().getPacketRouter().route(IQ.createResultIQ(packet)); 1027 } 1028 else 1029 { 1030 Log.trace("User '{}' is sending an IQ stanza to another room occupant (as a PM) with nickname: '{}'.", packet.getFrom(), toNickname); 1031 room.sendPrivatePacket(packet, preExistingRole); 1032 } 1033 } 1034 else 1035 { 1036 Log.debug("An IQ request was addressed to the MUC room '{}' which cannot answer it: {}", room.getName(), packet.toXML()); 1037 sendErrorPacket(packet, PacketError.Condition.bad_request, "IQ request cannot be processed by the MUC room itself."); 1038 } 1039 } 1040 } 1041 catch ( NotAcceptableException e ) 1042 { 1043 Log.debug("Unable to process IQ stanza: room requires a password, but none was supplied.", e); 1044 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "Room requires a password, but none was supplied."); 1045 } 1046 catch ( ForbiddenException e ) 1047 { 1048 Log.debug("Unable to process IQ stanza: sender don't have authorization to perform the request.", e); 1049 sendErrorPacket(packet, PacketError.Condition.forbidden, "You don't have authorization to perform this request."); 1050 } 1051 catch ( NotFoundException e ) 1052 { 1053 Log.debug("Unable to process IQ stanza: the intended recipient is not available.", e); 1054 sendErrorPacket(packet, PacketError.Condition.recipient_unavailable, "The intended recipient is not available."); 1055 } 1056 catch ( ConflictException e ) 1057 { 1058 Log.debug("Unable to process IQ stanza: processing this request would leave the room in an invalid state (eg: without owners).", e); 1059 sendErrorPacket(packet, PacketError.Condition.conflict, "Processing this request would leave the room in an invalid state (eg: without owners)."); 1060 } 1061 catch ( NotAllowedException e ) 1062 { 1063 Log.debug("Unable to process IQ stanza: an owner or administrator cannot be banned from the room.", e); 1064 sendErrorPacket(packet, PacketError.Condition.not_allowed, "An owner or administrator cannot be banned from the room."); 1065 } 1066 catch ( CannotBeInvitedException e ) 1067 { 1068 Log.debug("Unable to process IQ stanza: user being invited as a result of being added to a members-only room still does not have permission.", e); 1069 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "User being invited as a result of being added to a members-only room still does not have permission."); 1070 } 1071 catch ( Exception e ) 1072 { 1073 Log.error("An unexpected exception occurred while processing IQ stanza: {}", packet.toXML(), e); 1074 sendErrorPacket(packet, PacketError.Condition.internal_server_error, "An unexpected exception occurred while processing your request."); 1075 } 1076 } 1077 } 1078 1079 /** 1080 * Process a Presence stanza. 1081 * 1082 * This method might be invoked for a room that does not yet exist (when the presence is a room-creation request). 1083 * This is why this method, unlike the process methods for Message and IQ stanza takes a <em>room name</em> argument 1084 * and returns the room that processed to request. 1085 * 1086 * @param packet The stanza to process. 1087 * @param roomName The name of the room that the stanza was addressed to. 1088 * @param room The room that the stanza was addressed to, if it exists. 1089 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza, if any. 1090 * @return the room that handled the request 1091 */ 1092 @Nullable process( @onnull final Presence packet, @Nonnull final String roomName, @Nullable final MUCRoom room, @Nullable MUCRole preExistingRole )1093 private MUCRoom process( 1094 @Nonnull final Presence packet, 1095 @Nonnull final String roomName, 1096 @Nullable final MUCRoom room, 1097 @Nullable MUCRole preExistingRole ) 1098 { 1099 final Element mucInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc"); // only sent in initial presence 1100 final String nickname = packet.getTo().getResource() == null 1101 || packet.getTo().getResource().trim().isEmpty() ? null 1102 : packet.getTo().getResource().trim(); 1103 1104 if ( preExistingRole == null && Presence.Type.unavailable == packet.getType() ) { 1105 1106 // This is for clustering scenarios where one node could already have cleaned up the clustered cache, 1107 // but the local node still needs to process the 'unavailable' presence of the leaving occupant. 1108 final MUCRoom localRoom = localMUCRoomManager.getLocalRooms().get(roomName); 1109 if (localRoom != null) { 1110 preExistingRole = localRoom.getOccupantByFullJID(packet.getFrom()); 1111 } 1112 1113 if (preExistingRole == null) { 1114 Log.debug("Silently ignoring user '{}' leaving a room that it has no role in '{}' (was the room just destroyed)?", packet.getFrom(), roomName); 1115 return null; 1116 } else { 1117 Log.debug("NOT silently ignoring user {} leaving a room. Sending 'unavailable' presence for room {} because the occupant was still present in the local room cache", packet.getFrom(), roomName); 1118 } 1119 } 1120 if ( preExistingRole == null || (mucInfo != null && preExistingRole.getNickname().equalsIgnoreCase(nickname) ) ) 1121 { 1122 // If we're not already in a room (role == null), we either are joining it or it's not properly addressed and we drop it silently. 1123 // Alternative is that mucInfo is not null, in which case the client thinks it isn't in the room, so we should join anyway. 1124 return processRoomJoinRequest(packet, roomName, room, nickname); 1125 } 1126 else 1127 { 1128 // Check and reject conflicting packets with conflicting roles 1129 // In other words, another user already has this nickname 1130 if ( !preExistingRole.getUserAddress().equals(packet.getFrom()) ) 1131 { 1132 Log.debug("Rejecting conflicting stanza with conflicting roles: {}", packet.toXML()); 1133 sendErrorPacket(packet, PacketError.Condition.conflict, "Another user uses this nickname."); 1134 return room; 1135 } 1136 1137 if (room == null) { 1138 if (Presence.Type.unavailable == packet.getType()) { 1139 Log.debug("Silently ignoring user '{}' leaving a non-existing room '{}' (was the room just destroyed)?", packet.getFrom(), roomName); 1140 } else { 1141 Log.warn("Unable to process presence update from user '{}' to a non-existing room: {}", packet.getFrom(), roomName); 1142 } 1143 return null; 1144 } 1145 try 1146 { 1147 if ( nickname != null && !preExistingRole.getNickname().equalsIgnoreCase(nickname) && Presence.Type.unavailable != packet.getType() ) 1148 { 1149 // Occupant has changed his nickname. Send two presences to each room occupant. 1150 processNickNameChange(packet, room, preExistingRole, nickname); 1151 } 1152 else 1153 { 1154 processPresenceUpdate(packet, room, preExistingRole); 1155 } 1156 } 1157 catch ( Exception e ) 1158 { 1159 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 1160 } 1161 return room; 1162 } 1163 } 1164 1165 /** 1166 * Process a request to join a room. 1167 * 1168 * This method might be invoked for a room that does not yet exist (when the presence is a room-creation request). 1169 * 1170 * @param packet The stanza representing the nickname-change request. 1171 * @param roomName The name of the room that the stanza was addressed to. 1172 * @param room The room that the stanza was addressed to, if it exists. 1173 * @param nickname The requested nickname. 1174 * @return the room that handled the request 1175 */ processRoomJoinRequest( @onnull final Presence packet, @Nonnull final String roomName, @Nullable MUCRoom room, @Nullable String nickname )1176 private MUCRoom processRoomJoinRequest( 1177 @Nonnull final Presence packet, 1178 @Nonnull final String roomName, 1179 @Nullable MUCRoom room, 1180 @Nullable String nickname ) 1181 { 1182 Log.trace("Processing join request from '{}' for room '{}'", packet.getFrom(), roomName); 1183 1184 if ( nickname == null ) 1185 { 1186 Log.debug("Request from '{}' to join room '{}' rejected: request did not specify a nickname", packet.getFrom(), roomName); 1187 1188 // A resource is required in order to join a room http://xmpp.org/extensions/xep-0045.html#enter 1189 // If the user does not specify a room nickname (note the bare JID on the 'from' address in the following example), the service MUST return a <jid-malformed/> error 1190 if ( packet.getType() != Presence.Type.error ) 1191 { 1192 sendErrorPacket(packet, PacketError.Condition.jid_malformed, "A nickname (resource-part) is required in order to join a room."); 1193 } 1194 return null; 1195 } 1196 1197 if ( !packet.isAvailable() ) 1198 { 1199 Log.debug("Request from '{}' to join room '{}' rejected: request unexpectedly provided a presence stanza of type '{}'. Expected none.", packet.getFrom(), roomName, packet.getType()); 1200 if ( packet.getType() != Presence.Type.error ) 1201 { 1202 sendErrorPacket(packet, PacketError.Condition.unexpected_request, "Unexpected stanza type: " + packet.getType()); 1203 } 1204 return null; 1205 } 1206 1207 if (room == null) { 1208 try { 1209 // Create the room 1210 room = getChatRoom(roomName, packet.getFrom()); 1211 } catch (NotAllowedException e) { 1212 Log.debug("Request from '{}' to join room '{}' rejected: user does not have permission to create a new room.", packet.getFrom(), roomName, e); 1213 sendErrorPacket(packet, PacketError.Condition.not_allowed, "You do not have permission to create a new room."); 1214 return null; 1215 } 1216 } 1217 1218 try 1219 { 1220 // User must support MUC in order to create a room 1221 HistoryRequest historyRequest = null; 1222 String password = null; 1223 1224 // Check for password & requested history if client supports MUC 1225 final Element mucInfo = packet.getChildElement("x", "http://jabber.org/protocol/muc"); 1226 if ( mucInfo != null ) 1227 { 1228 password = mucInfo.elementTextTrim("password"); 1229 if ( mucInfo.element("history") != null ) 1230 { 1231 historyRequest = new HistoryRequest(mucInfo); 1232 } 1233 } 1234 1235 // The user joins the room 1236 final MUCRole role = room.joinRoom(nickname, 1237 password, 1238 historyRequest, 1239 packet.getFrom(), 1240 packet.createCopy()); 1241 1242 // If the client that created the room is non-MUC compliant then 1243 // unlock the room thus creating an "instant" room 1244 if ( mucInfo == null && room.isLocked() && !room.isManuallyLocked() ) 1245 { 1246 room.unlock(role); 1247 } 1248 } 1249 catch ( UnauthorizedException e ) 1250 { 1251 Log.debug("Request from '{}' to join room '{}' rejected: user not authorized to create or join the room.", packet.getFrom(), roomName, e); 1252 sendErrorPacket(packet, PacketError.Condition.not_authorized, "You're not authorized to create or join the room."); 1253 } 1254 catch ( ServiceUnavailableException e ) 1255 { 1256 Log.debug("Request from '{}' to join room '{}' rejected: the maximum number of users of the room has been reached.", packet.getFrom(), roomName, e); 1257 sendErrorPacket(packet, PacketError.Condition.service_unavailable, "The maximum number of users of the room has been reached."); 1258 } 1259 catch ( UserAlreadyExistsException | ConflictException e ) 1260 { 1261 Log.debug("Request from '{}' to join room '{}' rejected: the requested nickname '{}' is being used by someone else in the room.", packet.getFrom(), roomName, nickname, e); 1262 sendErrorPacket(packet, PacketError.Condition.conflict, "The nickname that is being used is used by someone else."); 1263 } 1264 catch ( RoomLockedException e ) 1265 { 1266 // If a user attempts to enter a room while it is "locked" (i.e., before the room creator provides an initial configuration and therefore before the room officially exists), the service MUST refuse entry and return an <item-not-found/> error to the user 1267 Log.debug("Request from '{}' to join room '{}' rejected: room is locked.", packet.getFrom(), roomName, e); 1268 sendErrorPacket(packet, PacketError.Condition.item_not_found, "This room is locked (it might not have been configured yet)."); 1269 } 1270 catch ( ForbiddenException e ) 1271 { 1272 Log.debug("Request from '{}' to join room '{}' rejected: user not authorized join the room.", packet.getFrom(), roomName, e); 1273 sendErrorPacket(packet, PacketError.Condition.forbidden, "You're not allowed to join this room."); 1274 } 1275 catch ( RegistrationRequiredException e ) 1276 { 1277 Log.debug("Request from '{}' to join room '{}' rejected: room is member-only, user is not a member.", packet.getFrom(), roomName, e); 1278 sendErrorPacket(packet, PacketError.Condition.registration_required, "This is a member-only room. Membership is required."); 1279 } 1280 catch ( NotAcceptableException e ) 1281 { 1282 Log.debug("Request from '{}' to join room '{}' rejected: user attempts to use nickname '{}' which is different from the reserved nickname.", packet.getFrom(), roomName, nickname, e); 1283 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "You're trying to join with a nickname different than the reserved nickname."); 1284 } 1285 return room; 1286 } 1287 1288 /** 1289 * Process a presence status update for a user. 1290 * 1291 * @param packet The stanza to process 1292 * @param room The room that the stanza was addressed to. 1293 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza. 1294 */ processPresenceUpdate( @onnull final Presence packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole )1295 private void processPresenceUpdate( 1296 @Nonnull final Presence packet, 1297 @Nonnull final MUCRoom room, 1298 @Nonnull final MUCRole preExistingRole ) 1299 { 1300 if ( Presence.Type.unavailable == packet.getType() ) 1301 { 1302 Log.trace("Occupant '{}' of room '{}' is leaving.", preExistingRole.getUserAddress(), room.getName()); 1303 // TODO Consider that different nodes can be creating and processing this presence at the same time (when remote node went down) 1304 preExistingRole.setPresence(packet); 1305 room.leaveRoom(preExistingRole); 1306 } 1307 else 1308 { 1309 Log.trace("Occupant '{}' of room '{}' changed its availability status.", preExistingRole.getUserAddress(), room.getName()); 1310 room.presenceUpdated(preExistingRole, packet); 1311 } 1312 } 1313 1314 /** 1315 * Process a request to change a nickname. 1316 * 1317 * @param packet The stanza representing the nickname-change request. 1318 * @param room The room that the stanza was addressed to. 1319 * @param preExistingRole The role of this user in the addressed room prior to processing of this stanza. 1320 * @param nickname The requested nickname. 1321 */ processNickNameChange( @onnull final Presence packet, @Nonnull final MUCRoom room, @Nonnull final MUCRole preExistingRole, @Nonnull String nickname )1322 private void processNickNameChange( 1323 @Nonnull final Presence packet, 1324 @Nonnull final MUCRoom room, 1325 @Nonnull final MUCRole preExistingRole, 1326 @Nonnull String nickname ) 1327 throws UserNotFoundException 1328 { 1329 Log.trace("Occupant '{}' of room '{}' tries to change its nickname to '{}'.", preExistingRole.getUserAddress(), room.getName(), nickname); 1330 1331 if ( room.getOccupantsByBareJID(packet.getFrom().asBareJID()).isEmpty() ) 1332 { 1333 Log.trace("Nickname change request denied: requestor '{}' is not an occupant of the room.", packet.getFrom().asBareJID()); 1334 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "You are not an occupant of this chatroom."); 1335 return; 1336 } 1337 1338 if ( !room.canChangeNickname() ) 1339 { 1340 Log.trace("Nickname change request denied: Room configuration does not allow nickname changes."); 1341 sendErrorPacket(packet, PacketError.Condition.not_acceptable, "Chatroom does not allow nickname changes."); 1342 return; 1343 } 1344 1345 List<MUCRole> existingOccupants; 1346 try { 1347 existingOccupants = room.getOccupantsByNickname(nickname); 1348 } catch (UserNotFoundException e) { 1349 existingOccupants = Collections.emptyList(); 1350 } 1351 1352 if ( !existingOccupants.isEmpty() && !existingOccupants.stream().allMatch(r->r.getUserAddress().asBareJID().equals(preExistingRole.getUserAddress().asBareJID())) ) 1353 { 1354 Log.trace("Nickname change request denied: the requested nickname '{}' is used by another occupant of the room.", nickname); 1355 sendErrorPacket(packet, PacketError.Condition.conflict, "This nickname is taken."); 1356 return; 1357 } 1358 1359 final JID memberBareJID = room.getMemberForReservedNickname(nickname); 1360 if (memberBareJID != null && !memberBareJID.equals(preExistingRole.getUserAddress().asBareJID())) 1361 { 1362 Log.trace("Nickname change request denied: the requested nickname '{}' is reserved by a member of the room.", nickname); 1363 sendErrorPacket(packet, PacketError.Condition.conflict, "This nickname is taken."); 1364 return; 1365 } 1366 1367 // Send "unavailable" presence for the old nickname 1368 final Presence presence = preExistingRole.getPresence().createCopy(); 1369 // Switch the presence to OFFLINE 1370 presence.setType(Presence.Type.unavailable); 1371 presence.setStatus(null); 1372 // Add the new nickname and status 303 as properties 1373 final Element frag = presence.getChildElement("x", "http://jabber.org/protocol/muc#user"); 1374 frag.element("item").addAttribute("nick", nickname); 1375 frag.addElement("status").addAttribute("code", "303"); 1376 room.send(presence, preExistingRole); 1377 1378 // Send availability presence for the new nickname 1379 final String oldNick = preExistingRole.getNickname(); 1380 room.nicknameChanged(preExistingRole, packet, oldNick, nickname); 1381 } 1382 1383 /** 1384 * Determines if a stanza is a client-generated response to an IQ Ping request sent by this server. 1385 * 1386 * A 'true' result of this method indicates that the client sending the IQ response is currently reachable. 1387 * 1388 * @param stanza The stanza to check 1389 * @return true if the stanza is a response to an IQ Ping request sent by the server, otherwise false. 1390 */ isPendingPingResponse(@onnull final Packet stanza)1391 public boolean isPendingPingResponse(@Nonnull final Packet stanza) { 1392 if (!(stanza instanceof IQ)) { 1393 return false; 1394 } 1395 final IQ iq = (IQ) stanza; 1396 if (iq.isRequest()) { 1397 return false; 1398 } 1399 1400 // Check if this is an error to a ghost-detection ping that we've sent out. Note that clients that are 1401 // connected but do not support XEP-0199 should send back a 'service-unavailable' error per the XEP, but 1402 // some clients are known to send 'feature-not-available'. Treat these as indications that the client is 1403 // still connected. 1404 final Collection<PacketError.Condition> pingErrorsIndicatingClientConnectivity = Arrays.asList( 1405 PacketError.Condition.service_unavailable, 1406 PacketError.Condition.feature_not_implemented 1407 ); 1408 1409 final JID jid = PINGS_SENT.get(iq.getID()); 1410 final boolean result = jid != null && iq.getFrom().equals(jid) 1411 && (iq.getError() == null || pingErrorsIndicatingClientConnectivity.contains(iq.getError().getCondition())); 1412 1413 if (result) { 1414 // If this is _not_ a valid response, 'isDeliveryRelatedErrorResponse' might want to process this. Otherwise, remove. 1415 PINGS_SENT.remove(iq.getID()); 1416 } 1417 return result; 1418 } 1419 1420 /** 1421 * Determines if a stanza is sent by (on behalf of) an entity that MUC room believes to be an occupant 1422 * when they've left: a 'ghost' occupant 1423 * 1424 * For message and presence stanzas, the delivery errors as defined in section 18.1.2 of XEP-0045 are used. 1425 * 1426 * For IQ stanzas, these errors may be due to lack of client support rather than a vanished occupant. Therefore, 1427 * IQ stanzas will only return 'true' when they are an error response to an IQ Ping request sent by this server. 1428 * 1429 * @param stanza The stanza to check 1430 * @return true if the stanza is a delivery related error response (from a 'ghost user'), otherwise false. 1431 * @see <a href="https://xmpp.org/extensions/xep-0045.html#impl-service-ghosts">XEP-0045, section 'ghost users'</a> 1432 */ isDeliveryRelatedErrorResponse(@onnull final Packet stanza)1433 public boolean isDeliveryRelatedErrorResponse(@Nonnull final Packet stanza) 1434 { 1435 if (stanza instanceof IQ) { 1436 final IQ iq = ((IQ)stanza); 1437 if (iq.getType() != IQ.Type.error) { 1438 return false; 1439 } 1440 1441 // Check if this is an error to a ghost-detection ping that we've sent out. Note that clients that are 1442 // connected but do not support XEP-0199 should send back a 'service-unavailable' error per the XEP, but 1443 // some clients are known to send 'feature-not-available'. Treat these as indications that the client is 1444 // still connected. 1445 final Collection<PacketError.Condition> pingErrorsIndicatingClientConnectivity = Arrays.asList( 1446 PacketError.Condition.service_unavailable, 1447 PacketError.Condition.feature_not_implemented 1448 ); 1449 1450 if (stanza.getError() != null && pingErrorsIndicatingClientConnectivity.contains(stanza.getError().getCondition())) { 1451 return false; 1452 } 1453 1454 final JID jid = PINGS_SENT.get(iq.getID()); 1455 return jid != null && iq.getFrom().equals(jid) && stanza.getError() != null; 1456 } 1457 1458 // The remainder of this implementation applies to Message and Presence stanzas. 1459 1460 // Conditions as defined in XEP-0045, section 'ghost users'. 1461 final Collection<PacketError.Condition> deliveryRelatedErrorConditions = Arrays.asList( 1462 PacketError.Condition.gone, 1463 PacketError.Condition.item_not_found, 1464 PacketError.Condition.recipient_unavailable, 1465 PacketError.Condition.redirect, 1466 PacketError.Condition.remote_server_not_found, 1467 PacketError.Condition.remote_server_timeout 1468 ); 1469 1470 final PacketError error = stanza.getError(); 1471 return error != null && deliveryRelatedErrorConditions.contains(error.getCondition()); 1472 } 1473 1474 @Override initialize(final JID jid, final ComponentManager componentManager)1475 public void initialize(final JID jid, final ComponentManager componentManager) { 1476 initialize(XMPPServer.getInstance()); 1477 } 1478 1479 @Override shutdown()1480 public void shutdown() { 1481 enableService( false, false ); 1482 ClusterManager.removeListener(this); 1483 MUCEventDispatcher.removeListener(occupantManager); 1484 } 1485 1486 @Override getServiceDomain()1487 public String getServiceDomain() { 1488 return chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); 1489 } 1490 getAddress()1491 public JID getAddress() { 1492 return new JID(null, getServiceDomain(), null, true); 1493 } 1494 1495 @Override serverStarted()1496 public void serverStarted() 1497 {} 1498 1499 @Override serverStopping()1500 public void serverStopping() 1501 { 1502 // When this is executed, we can be certain that all server modules have not yet shut down. This allows us to 1503 // inform all users. 1504 shutdown(); 1505 } 1506 1507 /** 1508 * Operates on users that have been inactive for a while. Depending on the configuration of Openfire, these uses 1509 * could either be kicked, or be pinged (to determine if they're 'ghost users'). 1510 */ 1511 private class UserTimeoutTask extends TimerTask { 1512 @Override run()1513 public void run() { 1514 checkForTimedOutUsers(); 1515 } 1516 } 1517 1518 /** 1519 * Informs all users local to this cluster node that he or she is being removed from the room because the MUC 1520 * service is being shut down. 1521 * 1522 * The implementation is optimized to run as fast as possible (to prevent prolonging the shutdown). 1523 */ broadcastShutdown()1524 private void broadcastShutdown() 1525 { 1526 Log.debug( "Notifying all local users about the imminent destruction of chat service '{}'", chatServiceName ); 1527 1528 final Set<OccupantManager.Occupant> localOccupants = occupantManager.getLocalOccupants(); 1529 1530 if (localOccupants.isEmpty()) { 1531 return; 1532 } 1533 1534 // A thread pool is used to broadcast concurrently, as well as to limit the execution time of this service. 1535 final ExecutorService service = Executors.newFixedThreadPool( Math.min( localOccupants.size(), 10 ) ); 1536 1537 // Queue all tasks in the executor service. 1538 for ( final OccupantManager.Occupant localOccupant : localOccupants ) 1539 { 1540 service.submit(() -> { 1541 try 1542 { 1543 // Obtaining the room without acquiring a lock. Usage of the room is read-only (the implementation below 1544 // should not modify the room state in a way that the cluster cares about), and more importantly, speed 1545 // is of importance (waiting for every room's lock to be acquired would slow down the shutdown process). 1546 // Lastly, this service is shutting down (likely because the server is shutting down). The trade-off 1547 // between speed and access of room state while not holding a lock seems worth while here. 1548 final MUCRoom room = getChatRoom(localOccupant.getRoomName()); 1549 if (room == null) { 1550 // Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager ? 1551 Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' that does not seem to exist.", localOccupant.getRealJID(), localOccupant.getRoomName(), chatServiceName); 1552 return; 1553 } 1554 final MUCRole role = room.getOccupantByFullJID(localOccupant.getRealJID()); 1555 if (role == null) { 1556 // Mismatch between MUCUser#getRooms() and MUCRoom#occupants ? 1557 Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' but that role does not seem to exist.", localOccupant.getRealJID(), localOccupant.getRoomName(), chatServiceName); 1558 return; 1559 } 1560 1561 // Send a presence stanza of type "unavailable" to the occupant 1562 final Presence presence = room.createPresence( Presence.Type.unavailable ); 1563 presence.setFrom( role.getRoleAddress() ); 1564 1565 // A fragment containing the x-extension. 1566 final Element fragment = presence.addChildElement( "x", "http://jabber.org/protocol/muc#user" ); 1567 final Element item = fragment.addElement( "item" ); 1568 item.addAttribute( "affiliation", "none" ); 1569 item.addAttribute( "role", "none" ); 1570 fragment.addElement( "status" ).addAttribute( "code", "332" ); 1571 1572 // Make sure that the presence change for each user is only sent to that user (and not broadcast in the room)! 1573 // Not needed to create a defensive copy of the stanza. It's not used anywhere else. 1574 role.send( presence ); 1575 1576 // Let all other cluster nodes know! 1577 room.removeOccupantRole(role); 1578 } 1579 catch ( final Exception e ) 1580 { 1581 Log.debug( "Unable to inform {} about the imminent destruction of chat service '{}'", localOccupant.realJID, chatServiceName, e ); 1582 } 1583 }); 1584 } 1585 1586 // Try to shutdown - wait - force shutdown. 1587 service.shutdown(); 1588 try 1589 { 1590 if (service.awaitTermination( JiveGlobals.getIntProperty( "xmpp.muc.await-termination-millis", 500 ), TimeUnit.MILLISECONDS )) { 1591 Log.debug("Successfully notified all local users about the imminent destruction of chat service '{}'", chatServiceName); 1592 } else { 1593 Log.debug("Unable to notify all local users about the imminent destruction of chat service '{}' (timeout)", chatServiceName); 1594 } 1595 } 1596 catch ( final InterruptedException e ) 1597 { 1598 Log.debug( "Interrupted while waiting for all users to be notified of shutdown of chat service '{}'. Shutting down immediately.", chatServiceName ); 1599 } 1600 service.shutdownNow(); 1601 } 1602 1603 /** 1604 * Iterates over the local occupants of MUC rooms (users connected to the local cluster node), to determine if an 1605 * action needs to be taken based on their (lack of) activity. Depending on the configuration of Openfire, inactive 1606 * users (users that are connected, but have not typed anything) are kicked from the room, and/or are explicitly 1607 * asked for a proof of life (connectivity), removing them if this proof is not given. 1608 */ checkForTimedOutUsers()1609 private void checkForTimedOutUsers() 1610 { 1611 for (final OccupantManager.Occupant occupant : occupantManager.getLocalOccupants()) 1612 { 1613 try 1614 { 1615 if (userIdleKick != null && occupant.getLastActive().isBefore(Instant.now().minus(userIdleKick))) 1616 { 1617 // Kick users if 'user_idle' feature is enabled and the user has been idle for too long. 1618 tryRemoveOccupantFromRoom(occupant, JiveGlobals.getProperty("admin.mucRoom.timeoutKickReason", "User was inactive for longer than the allowed maximum duration of " + userIdleKick.toString().substring(2).replaceAll("(\\d[HMS])(?!$)", "$1 ").toLowerCase()) + "." ); 1619 } 1620 else if (userIdlePing != null) 1621 { 1622 // Check if the occupant has been inactive for to long. 1623 final Instant lastActive = occupant.getLastActive(); 1624 final boolean isInactive = lastActive.isBefore(Instant.now().minus(userIdlePing)); 1625 1626 // Ensure that we didn't quite recently send a ping already. 1627 final Instant lastPing = occupant.getLastPingRequest(); 1628 final boolean isRecentlyPinged = lastPing != null && lastPing.isAfter(Instant.now().minus(userIdlePing)); 1629 1630 if (isInactive && !isRecentlyPinged) { 1631 // Ping the user if it hasn't been kicked already, the feature is enabled, and the user has been idle for too long. 1632 pingOccupant(occupant); 1633 } 1634 } 1635 } 1636 catch (final Throwable e) { 1637 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 1638 } 1639 } 1640 } 1641 1642 /** 1643 * Removes an occupant from a room. 1644 * 1645 * When the system is 'busy', the occupant will not be removed to prevent threads blocking. In such cases, a message 1646 * is added to the log files, but the occupant remains in the room. 1647 * 1648 * @param occupant The occupant to be removed 1649 * @param reason A human-readable reason for the removal (to be shared with the occupant as well as other occupants of the room). 1650 */ tryRemoveOccupantFromRoom(@onnull final OccupantManager.Occupant occupant, @Nonnull final String reason)1651 private void tryRemoveOccupantFromRoom(@Nonnull final OccupantManager.Occupant occupant, @Nonnull final String reason) 1652 { 1653 final Lock lock = getChatRoomLock(occupant.getRoomName()); 1654 if (!lock.tryLock()) { // Don't block on locked rooms, as we're processing many of them. We'll get them in the next round. 1655 Log.info("Skip removing as a cluster-wide mutex for the room could not immediately be obtained: {}, should have been removed, because: {}", occupant, reason); 1656 return; 1657 } 1658 try { 1659 final MUCRoom room = getChatRoom(occupant.getRoomName()); 1660 if (room == null) { 1661 // Room was recently removed? Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager? 1662 Log.info("Skip removing {} as the room no longer exists.", occupant); 1663 return; 1664 } 1665 1666 if (!room.hasOccupant(occupant.getRealJID())) { 1667 // Occupant no longer in room? Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager? 1668 Log.debug("Skip removing {} as this occupant no longer is in the room.", occupant); 1669 return; 1670 } 1671 1672 // Kick the user from the room that he/she had previously joined. 1673 Log.debug("Removing/kicking {}: {}", occupant, reason); 1674 room.kickOccupant(occupant.getRealJID(), null, null, reason); 1675 1676 // Ensure that other cluster nodes see any changes that might have been applied. 1677 syncChatRoom(room); 1678 } catch (final NotAllowedException e) { 1679 // Do nothing since we cannot kick owners or admins 1680 Log.debug("Skip removing {}, because it's not allowed (this user likely is an owner of admin of the room).", occupant, e); 1681 } finally { 1682 lock.unlock(); 1683 } 1684 } 1685 1686 /** 1687 * Sends an IQ Ping request to an occupant, and schedules a check that determines if a response to that request 1688 * was received (removing the occupant if it is not). 1689 * 1690 * @param occupant The occupant to ping. 1691 */ pingOccupant(@onnull final OccupantManager.Occupant occupant)1692 private void pingOccupant(@Nonnull final OccupantManager.Occupant occupant) 1693 { 1694 Log.debug("Pinging {} as the occupant is exceeding the idle time limit.", occupant); 1695 final IQ pingRequest = new IQ( IQ.Type.get ); 1696 pingRequest.setChildElement( IQPingHandler.ELEMENT_NAME, IQPingHandler.NAMESPACE ); 1697 pingRequest.setFrom( occupant.getRoomName() + "@" + getServiceDomain() ); 1698 pingRequest.setTo( occupant.getRealJID() ); 1699 pingRequest.setID( UUID.randomUUID().toString() ); // Generate unique ID, to prevent duplicate cache entries. 1700 XMPPServer.getInstance().getPacketRouter().route(pingRequest); 1701 PINGS_SENT.put(pingRequest.getID(), pingRequest.getTo()); 1702 1703 // Schedule a check to see if the ping was answered, kicking the occupant if it wasn't. 1704 // The check should be done _before_ the next ping would be sent (to prevent a backlog of ping requests forming) 1705 long timeoutMs = userIdlePing.dividedBy(4).toMillis(); 1706 final CheckPingResponseTask task = new CheckPingResponseTask(occupant, pingRequest.getID()); 1707 occupant.setPendingPingTask(task); 1708 TaskEngine.getInstance().schedule(task, timeoutMs); 1709 } 1710 1711 /** 1712 * A task that verifies if a response to a certain IQ Ping stanza (identified by stanza ID) was received, kicking 1713 * an occupant of a MUC room if it is not, and if there hasn't been any other activity from the occupant since. 1714 */ 1715 private class CheckPingResponseTask extends TimerTask { 1716 final OccupantManager.Occupant occupant; 1717 final String stanzaID; 1718 final Instant pingRequestSent = Instant.now(); 1719 CheckPingResponseTask(@onnull final OccupantManager.Occupant occupant, @Nonnull final String stanzaID)1720 public CheckPingResponseTask(@Nonnull final OccupantManager.Occupant occupant, @Nonnull final String stanzaID) { 1721 this.occupant = occupant; 1722 this.stanzaID = stanzaID; 1723 } 1724 1725 @Override run()1726 public void run() 1727 { 1728 occupant.setPendingPingTask(null); 1729 1730 Log.trace("Checking if {} has responded to a ping request that we sent earlier (with stanza ID '{}').", occupant, stanzaID); 1731 if (!occupant.getRealJID().equals(PINGS_SENT.remove(stanzaID))) // A null-check should be enough. Check against recipient for extra safety. 1732 { 1733 Log.trace("The ping request that we sent earlier to {} seems to have been answered. No need to remove this occupant.", occupant); 1734 return; 1735 } 1736 1737 final Instant lastActivity = occupantManager.lastActivityOnLocalNode(occupant.getRealJID()); 1738 if (lastActivity == null) { 1739 // If the occupant is in the room, it likely reconnected to another cluster node. Have that node worry about ghost detection. 1740 Log.debug("{} that has been sent a ping request earlier is no longer connected to the local cluster node. No need to remove this occupant.", occupant); 1741 return; 1742 } 1743 1744 if (lastActivity.isAfter(pingRequestSent)) { 1745 Log.debug("{} has not responded to a ping request that we sent earlier, but has had other activity. No need to remove this occupant.", occupant); 1746 return; 1747 } 1748 1749 Log.debug("{} has not responded to a ping request that we sent earlier and didn't have other activity. Occupant should be kicked from the room.", occupant); 1750 tryRemoveOccupantFromRoom(occupant, JiveGlobals.getProperty("admin.mucRoom.noPingResponseKickReason", "User seems to be unreachable (didn't respond to a ping request).") ); 1751 } 1752 } 1753 1754 /** 1755 * Stores Conversations in the database. 1756 */ 1757 private static class ConversationLogEntryArchiver extends Archiver<ConversationLogEntry> 1758 { ConversationLogEntryArchiver( String id, int maxWorkQueueSize, Duration maxPurgeInterval, Duration gracePeriod )1759 ConversationLogEntryArchiver( String id, int maxWorkQueueSize, Duration maxPurgeInterval, Duration gracePeriod ) 1760 { 1761 super( id, maxWorkQueueSize, maxPurgeInterval, gracePeriod ); 1762 } 1763 1764 @Override store( List<ConversationLogEntry> batch )1765 protected void store( List<ConversationLogEntry> batch ) 1766 { 1767 if ( batch.isEmpty() ) 1768 { 1769 return; 1770 } 1771 1772 MUCPersistenceManager.saveConversationLogBatch( batch ); 1773 } 1774 } 1775 1776 /** 1777 * Removes from memory rooms that have been without activity for a period of time. A room is 1778 * considered without activity when no occupants are present in the room for a while. 1779 */ 1780 private class CleanupTask extends TimerTask { 1781 @Override run()1782 public void run() { 1783 if (ClusterManager.isClusteringStarted() && !ClusterManager.isSeniorClusterMember()) { 1784 // Do nothing if we are in a cluster and this JVM is not the senior cluster member 1785 return; 1786 } 1787 try { 1788 Date cleanUpDate = getCleanupDate(); 1789 if (cleanUpDate!=null) 1790 { 1791 totalChatTime += localMUCRoomManager.unloadInactiveRooms(cleanUpDate).toMillis(); 1792 } 1793 } 1794 catch (final Throwable e) { 1795 Log.error(LocaleUtils.getLocalizedString("admin.error"), e); 1796 } 1797 } 1798 } 1799 1800 /** 1801 * Checks if a particular JID is allowed to create rooms. 1802 * 1803 * @param jid The jid for which to check (cannot be null). 1804 * @return true if the JID is allowed to create a room, otherwise false. 1805 */ isAllowedToCreate(final JID jid)1806 private boolean isAllowedToCreate(final JID jid) { 1807 // If room creation is not restricted, everyone is allowed to create a room. 1808 if (!isRoomCreationRestricted()) { 1809 return true; 1810 } 1811 1812 final JID bareJID = jid.asBareJID(); 1813 1814 // System administrators are always allowed to create rooms. 1815 if (sysadmins.includes(bareJID)) { 1816 return true; 1817 } 1818 1819 // If the JID of the user has explicitly been given permission, room creation is allowed. 1820 if (allowedToCreate.includes(bareJID)) { 1821 return true; 1822 } 1823 1824 // Verify the policy that allows all local, registered users to create rooms. 1825 return allRegisteredUsersAllowedToCreate && UserManager.getInstance().isRegisteredUser(bareJID, false); 1826 } 1827 1828 @Override getChatRoomLock(@onnull final String roomName)1829 @Nonnull public Lock getChatRoomLock(@Nonnull final String roomName) { 1830 return localMUCRoomManager.getLock(roomName); 1831 } 1832 1833 @Override syncChatRoom(@onnull final MUCRoom room)1834 public void syncChatRoom(@Nonnull final MUCRoom room) { 1835 localMUCRoomManager.sync(room); 1836 } 1837 1838 @Override 1839 @Nonnull getChatRoom(@onnull final String roomName, @Nonnull final JID userjid)1840 public MUCRoom getChatRoom(@Nonnull final String roomName, @Nonnull final JID userjid) throws NotAllowedException { 1841 MUCRoom room; 1842 boolean loaded = false; 1843 boolean created = false; 1844 final Lock lock = localMUCRoomManager.getLock(roomName); 1845 lock.lock(); 1846 try { 1847 room = localMUCRoomManager.get(roomName); 1848 if (room == null) { 1849 room = new MUCRoom(this, roomName); 1850 // If the room is persistent load the configuration values from the DB 1851 try { 1852 // Try to load the room's configuration from the database (if the room is 1853 // persistent but was added to the DB after the server was started up or the 1854 // room may be an old room that was not present in memory) 1855 MUCPersistenceManager.loadFromDB(room); 1856 loaded = true; 1857 } 1858 catch (final IllegalArgumentException e) { 1859 // Check if room needs to be recreated in case it failed to be created previously 1860 // (or was deleted somehow and is expected to exist by a delegate). 1861 if (mucEventDelegate != null && mucEventDelegate.shouldRecreate(roomName, userjid)) { 1862 if (mucEventDelegate.loadConfig(room)) { 1863 loaded = true; 1864 if (room.isPersistent()) { 1865 MUCPersistenceManager.saveToDB(room); 1866 } 1867 } 1868 else { 1869 // Room does not exist and delegate does not recognize it and does 1870 // not allow room creation 1871 throw new NotAllowedException(); 1872 1873 } 1874 } 1875 else { 1876 // The room does not exist so check for creation permissions 1877 if (!isAllowedToCreate(userjid)) { 1878 throw new NotAllowedException(); 1879 } 1880 room.addFirstOwner(userjid); 1881 created = true; 1882 } 1883 } 1884 localMUCRoomManager.add(room); 1885 } 1886 } finally { 1887 lock.unlock(); 1888 } 1889 if (created) { 1890 // Fire event that a new room has been created 1891 MUCEventDispatcher.roomCreated(room.getRole().getRoleAddress()); 1892 } 1893 if (loaded || created) { 1894 // Initiate FMUC, when enabled. 1895 room.getFmucHandler().applyConfigurationChanges(); 1896 } 1897 return room; 1898 } 1899 1900 @Override getChatRoom(@onnull final String roomName)1901 public MUCRoom getChatRoom(@Nonnull final String roomName) { 1902 boolean loaded = false; 1903 MUCRoom room = localMUCRoomManager.get(roomName); 1904 if (room == null) { 1905 // Check if the room exists in the database and was not present in memory 1906 final Lock lock = localMUCRoomManager.getLock(roomName); 1907 lock.lock(); 1908 try { 1909 room = localMUCRoomManager.get(roomName); 1910 if (room == null) { 1911 room = new MUCRoom(this, roomName); 1912 // If the room is persistent load the configuration values from the DB 1913 try { 1914 // Try to load the room's configuration from the database (if the room is 1915 // persistent but was added to the DB after the server was started up or the 1916 // room may be an old room that was not present in memory) 1917 MUCPersistenceManager.loadFromDB(room); 1918 loaded = true; 1919 localMUCRoomManager.add(room); 1920 } 1921 catch (final IllegalArgumentException e) { 1922 // The room does not exist so do nothing 1923 room = null; 1924 loaded = false; 1925 } 1926 } 1927 } finally { 1928 lock.unlock(); 1929 } 1930 } 1931 if (loaded) { 1932 // Initiate FMUC, when enabled. 1933 room.getFmucHandler().applyConfigurationChanges(); 1934 } 1935 return room; 1936 } 1937 1938 @Override 1939 @Deprecated getChatRooms()1940 public List<MUCRoom> getChatRooms() { 1941 return new ArrayList<>(localMUCRoomManager.getAll()); 1942 } 1943 1944 @Override getActiveChatRooms()1945 public List<MUCRoom> getActiveChatRooms() { 1946 return new ArrayList<>(localMUCRoomManager.getAll()); 1947 } 1948 1949 /** 1950 * Combine names of all rooms in the database (to catch any rooms that aren't currently in memory) with all 1951 * names of rooms currently in memory (to include rooms that are non-persistent / never saved in the database). 1952 * Duplicates will be removed by virtue of using a Set. 1953 * 1954 * @return Names of all rooms that are known to this service. 1955 */ 1956 @Override getAllRoomNames()1957 public Set<String> getAllRoomNames() { 1958 final Set<String> result = new HashSet<>(); 1959 result.addAll( MUCPersistenceManager.loadRoomNamesFromDB(this) ); 1960 result.addAll( localMUCRoomManager.getAll().stream().map(MUCRoom::getName).collect(Collectors.toSet()) ); 1961 1962 return result; 1963 } 1964 1965 // This method operates on MUC rooms without acquiring a cluster lock for them. As the usage is read-only, and the 1966 // method would have to lock _every_ room, the cost of acquiring all locks seem to outweigh the benefit. 1967 @Override getAllRoomSearchInfo()1968 public Collection<MUCRoomSearchInfo> getAllRoomSearchInfo() { 1969 // Base the result for all rooms that are in memory, then complement with rooms in the database that haven't 1970 // been added yet (to catch all non-active rooms); 1971 return getActiveAndInactiveRooms().stream().map(MUCRoomSearchInfo::new).collect(Collectors.toList()); 1972 } 1973 1974 /** 1975 * Returns all rooms serviced by this service. This includes rooms designated as non-active, which are loaded from 1976 * the database if necessary. This method can also be used solely for that 'side' effect of ensuring that all rooms 1977 * are loaded. 1978 * 1979 * @return All rooms serviced by this service. 1980 */ getActiveAndInactiveRooms()1981 public List<MUCRoom> getActiveAndInactiveRooms() { 1982 final List<MUCRoom> result = getActiveChatRooms(); 1983 1984 if (JiveGlobals.getBooleanProperty("xmpp.muc.search.skip-unloaded-rooms", false)) { 1985 return result; 1986 } 1987 1988 final Set<String> loadedNames = result.stream().map(MUCRoom::getName).collect(Collectors.toSet()); 1989 final Collection<String> dbNames = MUCPersistenceManager.loadRoomNamesFromDB(this); 1990 dbNames.removeAll(loadedNames); // what remains needs to be loaded from the database; 1991 1992 for (final String name : dbNames) { 1993 // TODO improve scalability instead of loading every room that wasn't loaded before. 1994 final MUCRoom chatRoom = this.getChatRoom(name); 1995 if (chatRoom != null) { 1996 result.add(chatRoom); 1997 } 1998 } 1999 return result; 2000 } 2001 2002 @Override hasChatRoom(final String roomName)2003 public boolean hasChatRoom(final String roomName) { 2004 return getChatRoom(roomName) != null; 2005 } 2006 2007 @Override removeChatRoom(final String roomName)2008 public void removeChatRoom(final String roomName) { 2009 final Lock lock = localMUCRoomManager.getLock(roomName); 2010 lock.lock(); 2011 try { 2012 final MUCRoom room = localMUCRoomManager.remove(roomName); 2013 if (room != null) { 2014 Log.info("removing chat room:" + roomName + "|" + room.getClass().getName()); 2015 totalChatTime += room.getChatLength(); 2016 } else { 2017 Log.info("No chatroom {} during removal.", roomName); 2018 } 2019 } finally { 2020 lock.unlock(); 2021 } 2022 } 2023 2024 @Override getServiceName()2025 public String getServiceName() { 2026 return chatServiceName; 2027 } 2028 2029 @Override getName()2030 public String getName() { 2031 return getServiceName(); 2032 } 2033 2034 @Override getHistoryStrategy()2035 public HistoryStrategy getHistoryStrategy() { 2036 return historyStrategy; 2037 } 2038 2039 /** 2040 * Removes a user from all chat rooms. 2041 * 2042 * @param userAddress The user's normal jid, not the chat nickname jid. 2043 */ removeChatUser(final JID userAddress)2044 private void removeChatUser(final JID userAddress) 2045 { 2046 final Set<String> roomNames = occupantManager.roomNamesForAddress(userAddress); 2047 2048 for (final String roomName : roomNames) 2049 { 2050 final Lock lock = getChatRoomLock(roomName); 2051 lock.lock(); 2052 try { 2053 final MUCRoom room = getChatRoom(roomName); 2054 if (room == null) { 2055 // Mismatch between MUCUser#getRooms() and MUCRoom#localMUCRoomManager ? 2056 Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' that does not seem to exist.", userAddress, roomName, chatServiceName); 2057 continue; 2058 } 2059 final MUCRole role = room.getOccupantByFullJID(userAddress); 2060 if (role == null) { 2061 // Mismatch between MUCUser#getRooms() and MUCRoom#occupants ? 2062 Log.warn("User '{}' appears to have had a role in room '{}' of service '{}' but that role does not seem to exist.", userAddress, roomName, chatServiceName); 2063 continue; 2064 } 2065 try { 2066 room.leaveRoom(role); 2067 // Ensure that all cluster nodes see the change to the room 2068 syncChatRoom(room); 2069 } catch (final Exception e) { 2070 Log.error(e.getMessage(), e); 2071 } 2072 } finally { 2073 lock.unlock(); 2074 } 2075 } 2076 } 2077 2078 @Override getMUCRoles(final JID user)2079 public Collection<MUCRole> getMUCRoles(final JID user) { 2080 final List<MUCRole> userRoles = new ArrayList<>(); 2081 for (final MUCRoom room : localMUCRoomManager.getAll()) { 2082 final MUCRole role = room.getOccupantByFullJID(user); 2083 if (role != null) { 2084 userRoles.add(role); 2085 } 2086 } 2087 return userRoles; 2088 } 2089 2090 /** 2091 * Returns the limit date after which rooms without activity will be removed from memory. 2092 * 2093 * @return the limit date after which rooms without activity will be removed from memory. 2094 */ getCleanupDate()2095 private Date getCleanupDate() { 2096 if (emptyLimit!=-1) 2097 return new Date(System.currentTimeMillis() - (emptyLimit * 3600000)); 2098 else 2099 return null; 2100 } 2101 2102 @Override setIdleUserTaskInterval(final @Nonnull Duration duration)2103 public void setIdleUserTaskInterval(final @Nonnull Duration duration) { 2104 // Set the new property value 2105 MUCPersistenceManager.setProperty(chatServiceName, "tasks.user.timeout", Long.toString(userIdleTaskInterval.toMillis())); 2106 2107 rescheduleUserTimeoutTask(); 2108 } 2109 rescheduleUserTimeoutTask()2110 private void rescheduleUserTimeoutTask() 2111 { 2112 // Use the 25% of the smallest of 'userIdleKick' or 'userIdlePing', or 5 minutes if both are unset. 2113 Duration recalculated = Stream.of(userIdleKick, userIdlePing) 2114 .filter(Objects::nonNull) 2115 .filter(duration -> duration.compareTo(Duration.ofSeconds(30)) > 0) // Not faster than every so often. 2116 .sorted() 2117 .findFirst() 2118 .orElse(Duration.ofMinutes(5*4)) 2119 .dividedBy(4); 2120 2121 // But if the property is set, use that. 2122 String value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.user.timeout"); 2123 if (value != null) { 2124 try { 2125 recalculated = Duration.ofMillis(Long.parseLong(value)); 2126 } 2127 catch (final NumberFormatException e) { 2128 Log.error("Wrong number format of property tasks.user.timeout for service "+chatServiceName, e); 2129 } 2130 } 2131 2132 if (Objects.equals(recalculated, this.userIdleTaskInterval)) { 2133 return; 2134 } 2135 Log.info("Rescheduling user idle task, recurring every {}", recalculated); 2136 this.userIdleTaskInterval = recalculated; 2137 2138 synchronized (this) { 2139 // Cancel the existing task because the timeout has changed 2140 if (userTimeoutTask != null) { 2141 TaskEngine.getInstance().cancelScheduledTask(userTimeoutTask); 2142 } 2143 2144 // Create a new task and schedule it with the new timeout 2145 userTimeoutTask = new UserTimeoutTask(); 2146 TaskEngine.getInstance().schedule(userTimeoutTask, userIdleTaskInterval.toMillis(), userIdleTaskInterval.toMillis()); 2147 } 2148 } 2149 2150 @Override 2151 @Nonnull getIdleUserTaskInterval()2152 public Duration getIdleUserTaskInterval() { 2153 return this.userIdleTaskInterval; 2154 } 2155 2156 @Override setIdleUserKickThreshold(final @Nullable Duration duration)2157 public void setIdleUserKickThreshold(final @Nullable Duration duration) 2158 { 2159 if (Objects.equals(duration, this.userIdleKick)) { 2160 return; 2161 } 2162 2163 this.userIdleKick = duration; 2164 2165 // Set the new property value 2166 MUCPersistenceManager.setProperty(chatServiceName, "tasks.user.idle", userIdleKick == null ? "-1" : Long.toString(userIdleKick.toMillis())); 2167 2168 rescheduleUserTimeoutTask(); 2169 } 2170 2171 @Override getIdleUserKickThreshold()2172 public Duration getIdleUserKickThreshold() 2173 { 2174 return userIdleKick; 2175 } 2176 2177 @Override setIdleUserPingThreshold(final @Nullable Duration duration)2178 public void setIdleUserPingThreshold(final @Nullable Duration duration) 2179 { 2180 if (Objects.equals(duration, this.userIdlePing)) { 2181 return; 2182 } 2183 2184 this.userIdlePing = duration; 2185 2186 if (userIdlePing != null && PINGS_SENT.getMaxLifetime() < userIdlePing.dividedBy(4).toMillis()) { 2187 Log.warn("The cache that tracks Ping requests ({}) has a maximum entry lifetime ({}) that is shorter than the time that we wait for responses ({}). This will result in (some) occupants not being removed when they fail to respond to Pings. An Openfire admin should adjust the configuration.", PINGS_SENT.getName(), Duration.ofMillis(PINGS_SENT.getMaxLifetime()), userIdlePing.dividedBy(4)); 2188 } 2189 2190 // Set the new property value 2191 MUCPersistenceManager.setProperty(chatServiceName, "tasks.user.ping", userIdlePing == null ? "-1" : Long.toString(userIdlePing.toMillis())); 2192 2193 rescheduleUserTimeoutTask(); 2194 } 2195 2196 @Override 2197 @Nullable getIdleUserPingThreshold()2198 public Duration getIdleUserPingThreshold() { 2199 return userIdlePing; 2200 } 2201 2202 @Override getUsersAllowedToCreate()2203 public Collection<JID> getUsersAllowedToCreate() { 2204 return Collections.unmodifiableCollection(allowedToCreate); 2205 } 2206 2207 @Override getSysadmins()2208 public Collection<JID> getSysadmins() { 2209 return Collections.unmodifiableCollection(sysadmins); 2210 } 2211 2212 @Override isSysadmin(final JID bareJID)2213 public boolean isSysadmin(final JID bareJID) { 2214 return sysadmins.includes(bareJID); 2215 } 2216 2217 @Override addSysadmins(final Collection<JID> userJIDs)2218 public void addSysadmins(final Collection<JID> userJIDs) { 2219 for (final JID userJID : userJIDs) { 2220 addSysadmin(userJID); 2221 } 2222 } 2223 2224 @Override addSysadmin(final JID userJID)2225 public void addSysadmin(final JID userJID) { 2226 final JID bareJID = userJID.asBareJID(); 2227 2228 if (!sysadmins.contains(userJID)) { 2229 sysadmins.add(bareJID); 2230 } 2231 2232 // CopyOnWriteArray does not allow sorting, so do sorting in temp list. 2233 final ArrayList<JID> tempList = new ArrayList<>(sysadmins); 2234 Collections.sort(tempList); 2235 sysadmins = new ConcurrentGroupList<>(tempList); 2236 2237 // Update the config. 2238 final String[] jids = new String[sysadmins.size()]; 2239 for (int i = 0; i < jids.length; i++) { 2240 jids[i] = sysadmins.get(i).toBareJID(); 2241 } 2242 MUCPersistenceManager.setProperty(chatServiceName, "sysadmin.jid", fromArray(jids)); 2243 } 2244 2245 @Override removeSysadmin(final JID userJID)2246 public void removeSysadmin(final JID userJID) { 2247 final JID bareJID = userJID.asBareJID(); 2248 2249 sysadmins.remove(bareJID); 2250 2251 // Update the config. 2252 final String[] jids = new String[sysadmins.size()]; 2253 for (int i = 0; i < jids.length; i++) { 2254 jids[i] = sysadmins.get(i).toBareJID(); 2255 } 2256 MUCPersistenceManager.setProperty(chatServiceName, "sysadmin.jid", fromArray(jids)); 2257 } 2258 2259 /** 2260 * Returns the flag that indicates if the service should provide information about non-public 2261 * members-only rooms when handling service discovery requests. 2262 * 2263 * @return true if the service should provide information about non-public members-only rooms. 2264 */ isAllowToDiscoverMembersOnlyRooms()2265 public boolean isAllowToDiscoverMembersOnlyRooms() { 2266 return allowToDiscoverMembersOnlyRooms; 2267 } 2268 2269 /** 2270 * Sets the flag that indicates if the service should provide information about non-public 2271 * members-only rooms when handling service discovery requests. 2272 * 2273 * @param allowToDiscoverMembersOnlyRooms 2274 * if the service should provide information about 2275 * non-public members-only rooms. 2276 */ setAllowToDiscoverMembersOnlyRooms(final boolean allowToDiscoverMembersOnlyRooms)2277 public void setAllowToDiscoverMembersOnlyRooms(final boolean allowToDiscoverMembersOnlyRooms) { 2278 this.allowToDiscoverMembersOnlyRooms = allowToDiscoverMembersOnlyRooms; 2279 MUCPersistenceManager.setProperty(chatServiceName, "discover.membersOnly", 2280 Boolean.toString(allowToDiscoverMembersOnlyRooms)); 2281 } 2282 2283 /** 2284 * Returns the flag that indicates if the service should provide information about locked rooms 2285 * when handling service discovery requests. 2286 * 2287 * @return true if the service should provide information about locked rooms. 2288 */ isAllowToDiscoverLockedRooms()2289 public boolean isAllowToDiscoverLockedRooms() { 2290 return allowToDiscoverLockedRooms; 2291 } 2292 2293 /** 2294 * Sets the flag that indicates if the service should provide information about locked rooms 2295 * when handling service discovery requests. 2296 * Note: Setting this flag in false is not compliant with the spec. A user may try to join a 2297 * locked room thinking that the room doesn't exist because the user didn't discover it before. 2298 * 2299 * @param allowToDiscoverLockedRooms if the service should provide information about locked 2300 * rooms. 2301 */ setAllowToDiscoverLockedRooms(final boolean allowToDiscoverLockedRooms)2302 public void setAllowToDiscoverLockedRooms(final boolean allowToDiscoverLockedRooms) { 2303 this.allowToDiscoverLockedRooms = allowToDiscoverLockedRooms; 2304 MUCPersistenceManager.setProperty(chatServiceName, "discover.locked", 2305 Boolean.toString(allowToDiscoverLockedRooms)); 2306 } 2307 2308 @Override isRoomCreationRestricted()2309 public boolean isRoomCreationRestricted() { 2310 return roomCreationRestricted; 2311 } 2312 2313 @Override setRoomCreationRestricted(final boolean roomCreationRestricted)2314 public void setRoomCreationRestricted(final boolean roomCreationRestricted) { 2315 this.roomCreationRestricted = roomCreationRestricted; 2316 MUCPersistenceManager.setProperty(chatServiceName, "create.anyone", Boolean.toString(roomCreationRestricted)); 2317 } 2318 2319 @Override isAllRegisteredUsersAllowedToCreate()2320 public boolean isAllRegisteredUsersAllowedToCreate() { 2321 return allRegisteredUsersAllowedToCreate; 2322 } 2323 2324 @Override setAllRegisteredUsersAllowedToCreate( final boolean allow )2325 public void setAllRegisteredUsersAllowedToCreate( final boolean allow ) { 2326 this.allRegisteredUsersAllowedToCreate = allow; 2327 MUCPersistenceManager.setProperty(chatServiceName, "create.all-registered", Boolean.toString(allow)); 2328 } 2329 2330 @Override addUsersAllowedToCreate(final Collection<JID> userJIDs)2331 public void addUsersAllowedToCreate(final Collection<JID> userJIDs) { 2332 boolean listChanged = false; 2333 2334 for(final JID userJID: userJIDs) { 2335 // Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance 2336 // variable there is no need to restart the service 2337 if (!allowedToCreate.contains(userJID)) { 2338 allowedToCreate.add(userJID); 2339 listChanged = true; 2340 } 2341 } 2342 2343 // if nothing was added, there's nothing to update 2344 if(listChanged) { 2345 // CopyOnWriteArray does not allow sorting, so do sorting in temp list. 2346 final List<JID> tempList = new ArrayList<>(allowedToCreate); 2347 Collections.sort(tempList); 2348 allowedToCreate = new ConcurrentGroupList<>(tempList); 2349 // Update the config. 2350 MUCPersistenceManager.setProperty(chatServiceName, "create.jid", fromCollection(allowedToCreate)); 2351 } 2352 } 2353 2354 @Override addUserAllowedToCreate(final JID userJID)2355 public void addUserAllowedToCreate(final JID userJID) { 2356 final List<JID> asList = new ArrayList<>(); 2357 asList.add(userJID); 2358 addUsersAllowedToCreate(asList); 2359 } 2360 2361 @Override removeUsersAllowedToCreate(final Collection<JID> userJIDs)2362 public void removeUsersAllowedToCreate(final Collection<JID> userJIDs) { 2363 boolean listChanged = false; 2364 2365 for(final JID userJID: userJIDs) { 2366 // Update the list of allowed JIDs to create MUC rooms. Since we are updating the instance 2367 // variable there is no need to restart the service 2368 listChanged |= allowedToCreate.remove(userJID); 2369 } 2370 2371 // if none of the JIDs were on the list, there's nothing to update 2372 if(listChanged) { 2373 MUCPersistenceManager.setProperty(chatServiceName, "create.jid", fromCollection(allowedToCreate)); 2374 } 2375 } 2376 2377 @Override removeUserAllowedToCreate(final JID userJID)2378 public void removeUserAllowedToCreate(final JID userJID) { 2379 removeUsersAllowedToCreate(Collections.singleton(userJID)); 2380 } 2381 initialize(final XMPPServer server)2382 public void initialize(final XMPPServer server) { 2383 initializeSettings(); 2384 2385 routingTable = server.getRoutingTable(); 2386 // Configure the handler of iq:register packets 2387 registerHandler = new IQMUCRegisterHandler(this); 2388 // Configure the handlers of search requests 2389 searchHandler = new IQMUCSearchHandler(this); 2390 muclumbusSearchHandler = new IQMuclumbusSearchHandler(this); 2391 mucVCardHandler = new IQMUCvCardHandler(this); 2392 MUCEventDispatcher.addListener(occupantManager); 2393 2394 // Ensure that cluster events are handled in RoutingTableImpl first (due to higher listener sequence here) 2395 ClusterManager.addListener(this, 0); 2396 } 2397 initializeSettings()2398 public void initializeSettings() { 2399 serviceEnabled = JiveProperties.getInstance().getBooleanProperty("xmpp.muc.enabled", true); 2400 serviceEnabled = MUCPersistenceManager.getBooleanProperty(chatServiceName, "enabled", serviceEnabled); 2401 // Trigger the strategy to load itself from the context 2402 historyStrategy.setContext(chatServiceName, "history"); 2403 // Load the list of JIDs that are sysadmins of the MUC service 2404 String property = MUCPersistenceManager.getProperty(chatServiceName, "sysadmin.jid"); 2405 2406 sysadmins.clear(); 2407 if (property != null && property.trim().length() > 0) { 2408 final String[] jids = property.split(","); 2409 for (final String jid : jids) { 2410 if (jid == null || jid.trim().length() == 0) { 2411 continue; 2412 } 2413 try { 2414 // could be a group jid 2415 sysadmins.add(GroupJID.fromString(jid.trim().toLowerCase()).asBareJID()); 2416 } catch (final IllegalArgumentException e) { 2417 Log.warn("The 'sysadmin.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e); 2418 } 2419 } 2420 } 2421 allowToDiscoverLockedRooms = 2422 MUCPersistenceManager.getBooleanProperty(chatServiceName, "discover.locked", true); 2423 allowToDiscoverMembersOnlyRooms = 2424 MUCPersistenceManager.getBooleanProperty(chatServiceName, "discover.membersOnly", true); 2425 roomCreationRestricted = 2426 MUCPersistenceManager.getBooleanProperty(chatServiceName, "create.anyone", false); 2427 allRegisteredUsersAllowedToCreate = 2428 MUCPersistenceManager.getBooleanProperty(chatServiceName, "create.all-registered", false ); 2429 // Load the list of JIDs that are allowed to create a MUC room 2430 property = MUCPersistenceManager.getProperty(chatServiceName, "create.jid"); 2431 allowedToCreate.clear(); 2432 if (property != null && property.trim().length() > 0) { 2433 final String[] jids = property.split(","); 2434 for (final String jid : jids) { 2435 if (jid == null || jid.trim().length() == 0) { 2436 continue; 2437 } 2438 try { 2439 // could be a group jid 2440 allowedToCreate.add(GroupJID.fromString(jid.trim().toLowerCase()).asBareJID()); 2441 } catch (final IllegalArgumentException e) { 2442 Log.warn("The 'create.jid' property contains a value that is not a valid JID. It is ignored. Offending value: '" + jid + "'.", e); 2443 } 2444 } 2445 } 2446 String value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.user.idle"); 2447 userIdleKick = null; 2448 if (value != null) { 2449 try { 2450 final long millis = Long.parseLong(value); 2451 if ( millis < 0 ) { 2452 userIdleKick = null; // feature is disabled. 2453 } else { 2454 userIdleKick = Duration.ofMillis(millis); 2455 } 2456 } 2457 catch (final NumberFormatException e) { 2458 Log.error("Wrong number format of property tasks.user.idle for service "+chatServiceName, e); 2459 } 2460 } 2461 value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.user.ping"); 2462 userIdlePing = Duration.ofMinutes(60); 2463 if (value != null) { 2464 try { 2465 final long millis = Long.parseLong(value); 2466 if ( millis < 0 ) { 2467 userIdlePing = null; // feature is disabled. 2468 } else { 2469 userIdlePing = Duration.ofMillis(millis); 2470 } 2471 } 2472 catch (final NumberFormatException e) { 2473 Log.error("Wrong number format of property tasks.user.ping for service "+chatServiceName, e); 2474 } 2475 } 2476 if (userIdlePing != null && PINGS_SENT.getMaxLifetime() < userIdlePing.dividedBy(4).toMillis()) { 2477 Log.warn("The cache that tracks Ping requests ({}) has a maximum entry lifetime ({}) that is shorter than the time that we wait for responses ({}). This will result in (some) occupants not being removed when they fail to respond to Pings. An Openfire admin should adjust the configuration.", PINGS_SENT.getName(), Duration.ofMillis(PINGS_SENT.getMaxLifetime()), userIdlePing.dividedBy(4)); 2478 } 2479 2480 value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.log.maxbatchsize"); 2481 logMaxConversationBatchSize = 500; 2482 if (value != null) { 2483 try { 2484 logMaxConversationBatchSize = Integer.parseInt(value); 2485 } 2486 catch (final NumberFormatException e) { 2487 Log.error("Wrong number format of property tasks.log.maxbatchsize for service "+chatServiceName, e); 2488 } 2489 } 2490 value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.log.maxbatchinterval"); 2491 logMaxBatchInterval = Duration.ofSeconds( 1 ); 2492 if (value != null) { 2493 try { 2494 logMaxBatchInterval = Duration.ofMillis( Long.parseLong(value) ); 2495 } 2496 catch (final NumberFormatException e) { 2497 Log.error("Wrong number format of property tasks.log.maxbatchinterval for service "+chatServiceName, e); 2498 } 2499 } 2500 value = MUCPersistenceManager.getProperty(chatServiceName, "tasks.log.batchgrace"); 2501 logBatchGracePeriod = Duration.ofMillis( 50 ); 2502 if (value != null) { 2503 try { 2504 logBatchGracePeriod = Duration.ofMillis( Long.parseLong(value) ); 2505 } 2506 catch (final NumberFormatException e) { 2507 Log.error("Wrong number format of property tasks.log.batchgrace for service "+chatServiceName, e); 2508 } 2509 } 2510 value = MUCPersistenceManager.getProperty(chatServiceName, "unload.empty_days"); 2511 emptyLimit = 30 * 24; 2512 if (value != null) { 2513 try { 2514 if (Integer.parseInt(value)>0) 2515 emptyLimit = Integer.parseInt(value) * (long)24; 2516 else 2517 emptyLimit = -1; 2518 } 2519 catch (final NumberFormatException e) { 2520 Log.error("Wrong number format of property unload.empty_days for service "+chatServiceName, e); 2521 } 2522 } 2523 rescheduleUserTimeoutTask(); 2524 } 2525 2526 /** 2527 * Property accessor temporarily retained for backward compatibility. The interface prescribes use of 2528 * {@link #setLogMaxConversationBatchSize(int)} - so please use that instead. 2529 * @param size the number of messages to save to the database on each run of the logging process. 2530 * @deprecated Use {@link #setLogMaxConversationBatchSize(int)} instead. 2531 */ 2532 @Override 2533 @Deprecated setLogConversationBatchSize(int size)2534 public void setLogConversationBatchSize(int size) 2535 { 2536 setLogMaxConversationBatchSize(size); 2537 } 2538 2539 /** 2540 * Property accessor temporarily retained for backward compatibility. The interface prescribes use of 2541 * {@link #getLogMaxConversationBatchSize()} - so please use that instead. 2542 * @return the number of messages to save to the database on each run of the logging process. 2543 * @deprecated Use {@link #getLogMaxConversationBatchSize()} instead. 2544 */ 2545 @Override 2546 @Deprecated getLogConversationBatchSize()2547 public int getLogConversationBatchSize() 2548 { 2549 return getLogMaxConversationBatchSize(); 2550 } 2551 2552 /** 2553 * Sets the maximum number of messages to save to the database on each run of the archiving process. 2554 * Even though the saving of queued conversations takes place in another thread it is not 2555 * recommended specifying a big number. 2556 * 2557 * @param size the maximum number of messages to save to the database on each run of the archiving process. 2558 */ 2559 @Override setLogMaxConversationBatchSize(int size)2560 public void setLogMaxConversationBatchSize(int size) { 2561 if ( this.logMaxConversationBatchSize == size ) { 2562 return; 2563 } 2564 this.logMaxConversationBatchSize = size; 2565 2566 if (archiver != null) { 2567 archiver.setMaxWorkQueueSize(size); 2568 } 2569 MUCPersistenceManager.setProperty( chatServiceName, "tasks.log.maxbatchsize", Integer.toString( size)); 2570 } 2571 2572 /** 2573 * Returns the maximum number of messages to save to the database on each run of the archiving process. 2574 * @return the maximum number of messages to save to the database on each run of the archiving process. 2575 */ 2576 @Override getLogMaxConversationBatchSize()2577 public int getLogMaxConversationBatchSize() { 2578 return logMaxConversationBatchSize; 2579 } 2580 2581 /** 2582 * Sets the maximum time allowed to elapse between writing archive batches to the database. 2583 * @param interval the maximum time allowed to elapse between writing archive batches to the database. 2584 */ 2585 @Override setLogMaxBatchInterval( Duration interval )2586 public void setLogMaxBatchInterval( Duration interval ) 2587 { 2588 if ( this.logMaxBatchInterval.equals( interval ) ) { 2589 return; 2590 } 2591 this.logMaxBatchInterval = interval; 2592 2593 if (archiver != null) { 2594 archiver.setMaxPurgeInterval(interval); 2595 } 2596 MUCPersistenceManager.setProperty(chatServiceName, "tasks.log.maxbatchinterval", Long.toString( interval.toMillis() ) ); 2597 } 2598 2599 /** 2600 * Returns the maximum time allowed to elapse between writing archive entries to the database. 2601 * @return the maximum time allowed to elapse between writing archive entries to the database. 2602 */ 2603 @Override getLogMaxBatchInterval()2604 public Duration getLogMaxBatchInterval() 2605 { 2606 return logMaxBatchInterval; 2607 } 2608 2609 /** 2610 * Sets the maximum time to wait for a next incoming entry before writing the batch to the database. 2611 * @param interval the maximum time to wait for a next incoming entry before writing the batch to the database. 2612 */ 2613 @Override setLogBatchGracePeriod( Duration interval )2614 public void setLogBatchGracePeriod( Duration interval ) 2615 { 2616 if ( this.logBatchGracePeriod.equals( interval ) ) { 2617 return; 2618 } 2619 2620 this.logBatchGracePeriod = interval; 2621 if (archiver != null) { 2622 archiver.setGracePeriod(interval); 2623 } 2624 MUCPersistenceManager.setProperty(chatServiceName, "tasks.log.batchgrace", Long.toString( interval.toMillis() ) ); 2625 } 2626 2627 /** 2628 * Returns the maximum time to wait for a next incoming entry before writing the batch to the database. 2629 * @return the maximum time to wait for a next incoming entry before writing the batch to the database. 2630 */ 2631 @Override getLogBatchGracePeriod()2632 public Duration getLogBatchGracePeriod() 2633 { 2634 return logBatchGracePeriod; 2635 } 2636 2637 /** 2638 * Accessor uses the "double-check idiom" for proper lazy instantiation. 2639 * @return An Archiver instance, never null. 2640 */ 2641 @Override getArchiver()2642 public Archiver<ConversationLogEntry> getArchiver() { 2643 Archiver<ConversationLogEntry> result = this.archiver; 2644 if (result == null) { 2645 synchronized (this) { 2646 result = this.archiver; 2647 if (result == null) { 2648 result = new ConversationLogEntryArchiver("MUC Service " + this.getAddress().toString(), logMaxConversationBatchSize, logMaxBatchInterval, logBatchGracePeriod); 2649 XMPPServer.getInstance().getArchiveManager().add(result); 2650 this.archiver = result; 2651 } 2652 } 2653 } 2654 2655 return result; 2656 } 2657 2658 @Override start()2659 public void start() { 2660 XMPPServer.getInstance().addServerListener( this ); 2661 2662 // Remove unused rooms from memory 2663 long cleanupFreq = JiveGlobals.getLongProperty("xmpp.muc.cleanupFrequency.inMinutes", CLEANUP_FREQUENCY) * 60 * 1000; 2664 TaskEngine.getInstance().schedule(new CleanupTask(), cleanupFreq, cleanupFreq); 2665 2666 // Set us up to answer disco item requests 2667 XMPPServer.getInstance().getIQDiscoItemsHandler().addServerItemsProvider(this); 2668 XMPPServer.getInstance().getIQDiscoInfoHandler().setServerNodeInfoProvider(this.getServiceDomain(), this); 2669 2670 Log.info(LocaleUtils.getLocalizedString("startup.starting.muc", Collections.singletonList(getServiceDomain()))); 2671 2672 final int preloadDays = MUCPersistenceManager.getIntProperty(chatServiceName, "preload.days", 30); 2673 if (preloadDays > 0) { 2674 // Load all the persistent rooms to memory 2675 final Instant cutoff = Instant.now().minus(Duration.ofDays(preloadDays)); 2676 for (final MUCRoom room : MUCPersistenceManager.loadRoomsFromDB(this, Date.from(cutoff))) { 2677 localMUCRoomManager.add(room); 2678 2679 // Start FMUC, if desired. 2680 room.getFmucHandler().applyConfigurationChanges(); 2681 } 2682 } 2683 } 2684 stop()2685 private void stop() { 2686 XMPPServer.getInstance().getIQDiscoItemsHandler().removeServerItemsProvider(this); 2687 XMPPServer.getInstance().getIQDiscoInfoHandler().removeServerNodeInfoProvider(this.getServiceDomain()); 2688 // Remove the route to this service 2689 routingTable.removeComponentRoute(getAddress()); 2690 broadcastShutdown(); 2691 XMPPServer.getInstance().removeServerListener( this ); 2692 if (archiver != null) { 2693 XMPPServer.getInstance().getArchiveManager().remove(archiver); 2694 } 2695 } 2696 2697 @Override enableService(final boolean enabled, final boolean persistent)2698 public void enableService(final boolean enabled, final boolean persistent) { 2699 if (isServiceEnabled() == enabled) { 2700 // Do nothing if the service status has not changed 2701 return; 2702 } 2703 if (!enabled) { 2704 // Stop the service/module 2705 stop(); 2706 } 2707 if (persistent) { 2708 MUCPersistenceManager.setProperty(chatServiceName, "enabled", Boolean.toString(enabled)); 2709 } 2710 serviceEnabled = enabled; 2711 if (enabled) { 2712 // Start the service/module 2713 start(); 2714 } 2715 } 2716 2717 @Override isServiceEnabled()2718 public boolean isServiceEnabled() { 2719 return serviceEnabled; 2720 } 2721 2722 @Override getTotalChatTime()2723 public long getTotalChatTime() { 2724 return totalChatTime; 2725 } 2726 2727 /** 2728 * Returns the number of existing rooms in the server (i.e. persistent or not, 2729 * in memory or not). 2730 * 2731 * @return the number of existing rooms in the server. 2732 */ 2733 @Override getNumberChatRooms()2734 public int getNumberChatRooms() { 2735 int persisted = MUCPersistenceManager.countRooms(this); 2736 final long nonPersisted = localMUCRoomManager.getAll().stream().filter(room -> !room.isPersistent()).count(); 2737 return persisted + (int) nonPersisted; 2738 } 2739 2740 /** 2741 * Returns the total number of occupants in all rooms. 2742 * 2743 * @return the total number of occupants. 2744 */ 2745 @Override getNumberConnectedUsers()2746 public int getNumberConnectedUsers() { 2747 return occupantManager.numberOfUniqueUsers(); 2748 } 2749 2750 /** 2751 * Retuns the total number of users that have joined in all rooms in the server. 2752 * 2753 * @return the number of existing rooms in the server. 2754 */ 2755 @Override getNumberRoomOccupants()2756 public int getNumberRoomOccupants() { 2757 int total = 0; 2758 for (final MUCRoom room : localMUCRoomManager.getAll()) { 2759 total = total + room.getOccupantsCount(); 2760 } 2761 return total; 2762 } 2763 2764 /** 2765 * Returns the total number of incoming messages since last reset. 2766 * 2767 * @param resetAfter True if you want the counter to be reset after results returned. 2768 * @return the number of incoming messages through the service. 2769 */ 2770 @Override getIncomingMessageCount(final boolean resetAfter)2771 public long getIncomingMessageCount(final boolean resetAfter) { 2772 if (resetAfter) { 2773 return inMessages.getAndSet(0); 2774 } 2775 else { 2776 return inMessages.get(); 2777 } 2778 } 2779 2780 /** 2781 * Returns the total number of outgoing messages since last reset. 2782 * 2783 * @param resetAfter True if you want the counter to be reset after results returned. 2784 * @return the number of outgoing messages through the service. 2785 */ 2786 @Override getOutgoingMessageCount(final boolean resetAfter)2787 public long getOutgoingMessageCount(final boolean resetAfter) { 2788 if (resetAfter) { 2789 return outMessages.getAndSet(0); 2790 } 2791 else { 2792 return outMessages.get(); 2793 } 2794 } 2795 2796 @Override logConversation(final MUCRoom room, final Message message, final JID sender)2797 public void logConversation(final MUCRoom room, final Message message, final JID sender) { 2798 // Only log messages that have a subject or body. Otherwise ignore it. 2799 if (message.getSubject() != null || message.getBody() != null) { 2800 getArchiver().archive( new ConversationLogEntry( new Date(), room, message, sender) ); 2801 } 2802 } 2803 2804 @Override messageBroadcastedTo(final int numOccupants)2805 public void messageBroadcastedTo(final int numOccupants) { 2806 // Increment counter of received messages that where broadcasted by one 2807 inMessages.incrementAndGet(); 2808 // Increment counter of outgoing messages with the number of room occupants 2809 // that received the message 2810 outMessages.addAndGet(numOccupants); 2811 } 2812 2813 @Override getItems()2814 public Iterator<DiscoServerItem> getItems() { 2815 // Check if the service is disabled. Info is not available when 2816 // disabled. 2817 if (!isServiceEnabled()) 2818 { 2819 return null; 2820 } 2821 2822 final ArrayList<DiscoServerItem> items = new ArrayList<>(); 2823 final DiscoServerItem item = new DiscoServerItem(new JID( 2824 getServiceDomain()), getDescription(), null, null, this, this); 2825 items.add(item); 2826 return items.iterator(); 2827 } 2828 2829 @Override getIdentities(final String name, final String node, final JID senderJID)2830 public Iterator<Element> getIdentities(final String name, final String node, final JID senderJID) { 2831 final ArrayList<Element> identities = new ArrayList<>(); 2832 if (name == null && node == null) { 2833 // Answer the identity of the MUC service 2834 final Element identity = DocumentHelper.createElement("identity"); 2835 identity.addAttribute("category", "conference"); 2836 identity.addAttribute("name", getDescription()); 2837 identity.addAttribute("type", "text"); 2838 identities.add(identity); 2839 2840 // TODO: Should internationalize Public Chatroom Search, and make it configurable. 2841 final Element searchId = DocumentHelper.createElement("identity"); 2842 searchId.addAttribute("category", "directory"); 2843 searchId.addAttribute("name", "Public Chatroom Search"); 2844 searchId.addAttribute("type", "chatroom"); 2845 identities.add(searchId); 2846 2847 if (!extraDiscoIdentities.isEmpty()) { 2848 identities.addAll(extraDiscoIdentities); 2849 } 2850 } 2851 else if (name != null && node == null) { 2852 // Answer the identity of a given room 2853 final MUCRoom room = getChatRoom(name); 2854 if (room != null) { 2855 final Element identity = DocumentHelper.createElement("identity"); 2856 identity.addAttribute("category", "conference"); 2857 identity.addAttribute("name", room.getNaturalLanguageName()); 2858 identity.addAttribute("type", "text"); 2859 2860 identities.add(identity); 2861 } 2862 } 2863 else if (name != null && "x-roomuser-item".equals(node)) { 2864 // Answer reserved nickname for the sender of the disco request in the requested room 2865 final MUCRoom room = getChatRoom(name); 2866 if (room != null) { 2867 final String reservedNick = room.getReservedNickname(senderJID); 2868 if (reservedNick != null) { 2869 final Element identity = DocumentHelper.createElement("identity"); 2870 identity.addAttribute("category", "conference"); 2871 identity.addAttribute("name", reservedNick); 2872 identity.addAttribute("type", "text"); 2873 2874 identities.add(identity); 2875 } 2876 } 2877 } 2878 return identities.iterator(); 2879 } 2880 2881 @Override getFeatures(final String name, final String node, final JID senderJID)2882 public Iterator<String> getFeatures(final String name, final String node, final JID senderJID) { 2883 final ArrayList<String> features = new ArrayList<>(); 2884 if (name == null && node == null) { 2885 // Answer the features of the MUC service 2886 features.add("http://jabber.org/protocol/muc"); 2887 features.add("http://jabber.org/protocol/disco#info"); 2888 features.add("http://jabber.org/protocol/disco#items"); 2889 if ( IQMuclumbusSearchHandler.PROPERTY_ENABLED.getValue() ) { 2890 features.add( "jabber:iq:search" ); 2891 } 2892 features.add(IQMuclumbusSearchHandler.NAMESPACE); 2893 features.add(ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT); 2894 if (!extraDiscoFeatures.isEmpty()) { 2895 features.addAll(extraDiscoFeatures); 2896 } 2897 } 2898 else if (name != null && node == null) { 2899 // Answer the features of a given room 2900 final MUCRoom room = getChatRoom(name); 2901 if (room != null) { 2902 features.add("http://jabber.org/protocol/muc"); 2903 if (room.isPublicRoom()) { 2904 features.add("muc_public"); 2905 } else { 2906 features.add("muc_hidden"); 2907 } 2908 if (room.isMembersOnly()) { 2909 features.add("muc_membersonly"); 2910 } 2911 else { 2912 features.add("muc_open"); 2913 } 2914 if (room.isModerated()) { 2915 features.add("muc_moderated"); 2916 } 2917 else { 2918 features.add("muc_unmoderated"); 2919 } 2920 if (room.canAnyoneDiscoverJID()) { 2921 features.add("muc_nonanonymous"); 2922 } 2923 else { 2924 features.add("muc_semianonymous"); 2925 } 2926 if (room.isPasswordProtected()) { 2927 features.add("muc_passwordprotected"); 2928 } 2929 else { 2930 features.add("muc_unsecured"); 2931 } 2932 if (room.isPersistent()) { 2933 features.add("muc_persistent"); 2934 } 2935 else { 2936 features.add("muc_temporary"); 2937 } 2938 if (!extraDiscoFeatures.isEmpty()) { 2939 features.addAll(extraDiscoFeatures); 2940 } 2941 if ( JiveGlobals.getBooleanProperty( "xmpp.muc.self-ping.enabled", true ) ) { 2942 features.add( "http://jabber.org/protocol/muc#self-ping-optimization" ); 2943 } 2944 if ( IQMUCvCardHandler.PROPERTY_ENABLED.getValue() ) { 2945 features.add( IQMUCvCardHandler.NAMESPACE ); 2946 } 2947 features.add( "urn:xmpp:sid:0" ); 2948 } 2949 } 2950 return features.iterator(); 2951 } 2952 2953 @Override getExtendedInfo(final String name, final String node, final JID senderJID)2954 public DataForm getExtendedInfo(final String name, final String node, final JID senderJID) { 2955 return IQDiscoInfoHandler.getFirstDataForm(this.getExtendedInfos(name, node, senderJID)); 2956 } 2957 2958 @Override getExtendedInfos(String name, String node, JID senderJID)2959 public Set<DataForm> getExtendedInfos(String name, String node, JID senderJID) { 2960 if (name != null && node == null) { 2961 // Answer the extended info of a given room 2962 final MUCRoom room = getChatRoom(name); 2963 if (room != null) { 2964 final DataForm dataForm = new DataForm(Type.result); 2965 2966 final FormField fieldType = dataForm.addField(); 2967 fieldType.setVariable("FORM_TYPE"); 2968 fieldType.setType(FormField.Type.hidden); 2969 fieldType.addValue("http://jabber.org/protocol/muc#roominfo"); 2970 2971 final FormField fieldDescr = dataForm.addField(); 2972 fieldDescr.setVariable("muc#roominfo_description"); 2973 fieldDescr.setType(FormField.Type.text_single); 2974 fieldDescr.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.desc")); 2975 fieldDescr.addValue(room.getDescription()); 2976 2977 final FormField fieldSubj = dataForm.addField(); 2978 fieldSubj.setVariable("muc#roominfo_subject"); 2979 fieldSubj.setType(FormField.Type.text_single); 2980 fieldSubj.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.subject")); 2981 fieldSubj.addValue(room.getSubject()); 2982 2983 final FormField fieldOcc = dataForm.addField(); 2984 fieldOcc.setVariable("muc#roominfo_occupants"); 2985 fieldOcc.setType(FormField.Type.text_single); 2986 fieldOcc.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.occupants")); 2987 fieldOcc.addValue(Integer.toString(room.getOccupantsCount())); 2988 2989 /*field = new XFormFieldImpl("muc#roominfo_lang"); 2990 field.setType(FormField.Type.text_single); 2991 field.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.language")); 2992 field.addValue(room.getLanguage()); 2993 dataForm.addField(field);*/ 2994 2995 final FormField fieldDate = dataForm.addField(); 2996 fieldDate.setVariable("x-muc#roominfo_creationdate"); 2997 fieldDate.setType(FormField.Type.text_single); 2998 fieldDate.setLabel(LocaleUtils.getLocalizedString("muc.extended.info.creationdate")); 2999 fieldDate.addValue(XMPPDateTimeFormat.format(room.getCreationDate())); 3000 final Set<DataForm> dataForms = new HashSet<>(); 3001 dataForms.add(dataForm); 3002 return dataForms; 3003 } 3004 } 3005 return new HashSet<>(); 3006 } 3007 3008 /** 3009 * Adds an extra Disco feature to the list of features returned for the conference service. 3010 * @param feature Feature to add. 3011 */ 3012 @Override addExtraFeature(final String feature)3013 public void addExtraFeature(final String feature) { 3014 extraDiscoFeatures.add(feature); 3015 } 3016 3017 /** 3018 * Removes an extra Disco feature from the list of features returned for the conference service. 3019 * @param feature Feature to remove. 3020 */ 3021 @Override removeExtraFeature(final String feature)3022 public void removeExtraFeature(final String feature) { 3023 extraDiscoFeatures.remove(feature); 3024 } 3025 3026 /** 3027 * Adds an extra Disco identity to the list of identities returned for the conference service. 3028 * @param category Category for identity. e.g. conference 3029 * @param name Descriptive name for identity. e.g. Public Chatrooms 3030 * @param type Type for identity. e.g. text 3031 */ 3032 @Override addExtraIdentity(final String category, final String name, final String type)3033 public void addExtraIdentity(final String category, final String name, final String type) { 3034 final Element identity = DocumentHelper.createElement("identity"); 3035 identity.addAttribute("category", category); 3036 identity.addAttribute("name", name); 3037 identity.addAttribute("type", type); 3038 extraDiscoIdentities.add(identity); 3039 } 3040 3041 /** 3042 * Removes an extra Disco identity from the list of identities returned for the conference service. 3043 * @param name Name of identity to remove. 3044 */ 3045 @Override removeExtraIdentity(final String name)3046 public void removeExtraIdentity(final String name) { 3047 final Iterator<Element> iter = extraDiscoIdentities.iterator(); 3048 while (iter.hasNext()) { 3049 final Element elem = iter.next(); 3050 if (name.equals(elem.attribute("name").getStringValue())) { 3051 iter.remove(); 3052 break; 3053 } 3054 } 3055 } 3056 3057 /** 3058 * Sets the MUC event delegate handler for this service. 3059 * @param delegate Handler for MUC events. 3060 */ 3061 @Override setMUCDelegate(final MUCEventDelegate delegate)3062 public void setMUCDelegate(final MUCEventDelegate delegate) { 3063 mucEventDelegate = delegate; 3064 } 3065 3066 /** 3067 * Gets the MUC event delegate handler for this service. 3068 * @return Handler for MUC events (delegate) 3069 */ 3070 @Override getMUCDelegate()3071 public MUCEventDelegate getMUCDelegate() { 3072 return mucEventDelegate; 3073 } 3074 3075 @Override hasInfo(final String name, final String node, final JID senderJID)3076 public boolean hasInfo(final String name, final String node, final JID senderJID) { 3077 // Check if the service is disabled. Info is not available when disabled. 3078 if (!isServiceEnabled()) { 3079 return false; 3080 } 3081 if (name == null && node == null) { 3082 // We always have info about the MUC service 3083 return true; 3084 } 3085 else if (name != null && node == null) { 3086 // We only have info if the room exists 3087 return hasChatRoom(name); 3088 } 3089 else if (name != null && "x-roomuser-item".equals(node)) { 3090 // We always have info about reserved names as long as the room exists 3091 return hasChatRoom(name); 3092 } 3093 return false; 3094 } 3095 3096 @Override getItems(final String name, final String node, final JID senderJID)3097 public Iterator<DiscoItem> getItems(final String name, final String node, final JID senderJID) { 3098 // Check if the service is disabled. Info is not available when disabled. 3099 if (!isServiceEnabled()) { 3100 return null; 3101 } 3102 final Set<DiscoItem> answer = new HashSet<>(); 3103 if (name == null && node == null) 3104 { 3105 // Before returning the items, ensure that all rooms are properly loaded in memory 3106 getActiveAndInactiveRooms(); 3107 3108 // Answer all the public rooms as items 3109 for (final MUCRoom room : localMUCRoomManager.getAll()) 3110 { 3111 if (canDiscoverRoom(room, senderJID)) 3112 { 3113 answer.add(new DiscoItem(room.getRole().getRoleAddress(), 3114 room.getNaturalLanguageName(), null, null)); 3115 } 3116 } 3117 } 3118 else if (name != null && node == null) { 3119 // Answer the room occupants as items if that info is publicly available 3120 final MUCRoom room = getChatRoom(name); 3121 if (room != null && canDiscoverRoom(room, senderJID)) { 3122 for (final org.jivesoftware.openfire.muc.MUCRole role : room.getOccupants()) { 3123 // TODO Should we filter occupants that are invisible (presence is not broadcasted)? 3124 answer.add(new DiscoItem(role.getRoleAddress(), null, null, null)); 3125 } 3126 } 3127 } 3128 return answer.iterator(); 3129 } 3130 3131 @Override canDiscoverRoom(final MUCRoom room, final JID entity)3132 public boolean canDiscoverRoom(final MUCRoom room, final JID entity) { 3133 // Check if locked rooms may be discovered 3134 if (!allowToDiscoverLockedRooms && room.isLocked()) { 3135 return false; 3136 } 3137 if (!room.isPublicRoom()) { 3138 if (!allowToDiscoverMembersOnlyRooms && room.isMembersOnly()) { 3139 return false; 3140 } 3141 final MUCRole.Affiliation affiliation = room.getAffiliation(entity.asBareJID()); 3142 return affiliation == MUCRole.Affiliation.owner 3143 || affiliation == MUCRole.Affiliation.admin 3144 || affiliation == MUCRole.Affiliation.member; 3145 } 3146 return true; 3147 } 3148 3149 /** 3150 * Converts an array to a comma-delimited String. 3151 * 3152 * @param array the array. 3153 * @return a comma delimited String of the array values. 3154 */ fromArray(final String [] array)3155 private static String fromArray(final String [] array) { 3156 final StringBuilder buf = new StringBuilder(); 3157 for (int i=0; i<array.length; i++) { 3158 buf.append(array[i]); 3159 if (i != array.length-1) { 3160 buf.append(','); 3161 } 3162 } 3163 return buf.toString(); 3164 } 3165 3166 /** 3167 * Converts a collection to a comma-delimited String. 3168 * 3169 * @param coll the collection. 3170 * @return a comma delimited String of the array values. 3171 */ fromCollection(final Collection<JID> coll)3172 private static String fromCollection(final Collection<JID> coll) { 3173 final StringBuilder buf = new StringBuilder(); 3174 for (final JID elem: coll) { 3175 buf.append(elem.toBareJID()).append(','); 3176 } 3177 final int endPos = buf.length() > 1 ? buf.length() - 1 : 0; 3178 return buf.substring(0, endPos); 3179 } 3180 setHidden(boolean isHidden)3181 public void setHidden(boolean isHidden) { 3182 this.isHidden = isHidden; 3183 } 3184 3185 @Override isHidden()3186 public boolean isHidden() { 3187 return isHidden; 3188 } 3189 3190 3191 @Override joinedCluster()3192 public void joinedCluster() { 3193 final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); 3194 Log.debug("Cluster event: service {} joined a cluster - going to restore {} rooms", fullServiceName, localMUCRoomManager.size()); 3195 3196 // The local node joined a cluster. 3197 3198 // Upon joining a cluster, clustered caches are reset to their clustered equivalent (by the swap from the local 3199 // cache implementation to the clustered cache implementation that's done in the implementation of 3200 // org.jivesoftware.util.cache.CacheFactory.joinedCluster). This means that they now hold data that's 3201 // available on all other cluster nodes. Data that's available on the local node needs to be added again. 3202 final Set<OccupantManager.Occupant> occupantsToSync = localMUCRoomManager.restoreCacheContentAfterJoin(occupantManager); 3203 3204 Log.debug("Occupants to sync: {}", occupantsToSync); 3205 3206 // Let the other nodes know about our local occupants, as they need this information when it might leave the 3207 // cluster (OF-2224) and should tell its local users that new occupants have joined (OF-2233). 3208 if (!occupantsToSync.isEmpty()) { 3209 CacheFactory.doClusterTask(new SyncLocalOccupantsAndSendJoinPresenceTask(this.chatServiceName, occupantsToSync)); 3210 } 3211 // TODO does this work properly when the rooms are not known on the other nodes? 3212 } 3213 3214 @Override joinedCluster(byte[] nodeID)3215 public void joinedCluster(byte[] nodeID) { 3216 3217 final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); 3218 Log.debug("Cluster event: service {} got notified that node {} joined a cluster", fullServiceName, new String(nodeID)); 3219 3220 // Another node joined a cluster that we're already part of. 3221 3222 // Let the new node know about our local occupants, as it needs this information when it might leave the cluster 3223 // again (OF-2224). The data is also needed for the joined node to be able to tell its local users that other 3224 // occupants have now joined a MUC room that they're in (OF-2233). To generate these stanzas, the receiving node 3225 // will probably need additional information. It can obtain this from the clustered cache. Even though we can't 3226 // be sure if that node has fully added _its own_ data to that cache (we don't know if restoreCacheContent() 3227 // has finished execution), we can be sure that the cache already holds data as provided by _us_. 3228 final Set<OccupantManager.Occupant> localOccupants = occupantManager.getLocalOccupants(); 3229 CacheFactory.doClusterTask( new SyncLocalOccupantsAndSendJoinPresenceTask(this.chatServiceName, localOccupants), nodeID ); 3230 } 3231 3232 @Override leftCluster()3233 public void leftCluster() { 3234 final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); 3235 Log.debug("Cluster event: service {} left a cluster - going to restore {} rooms", fullServiceName, localMUCRoomManager.size()); 3236 3237 // The local cluster node left the cluster. 3238 if (XMPPServer.getInstance().isShuttingDown()) { 3239 // Do not put effort in restoring the correct state if we're shutting down anyway. 3240 return; 3241 } 3242 3243 // Get all room occupants that lived on all other nodes, as from the perspective of this node, those nodes are 3244 // now no longer part of the cluster. 3245 final Set<OccupantManager.Occupant> occupantsOnRemovedNodes = occupantManager.leftCluster(); 3246 3247 // Upon leaving a cluster, clustered caches are reset to their local equivalent (by the swap from the clustered 3248 // cache implementation to the default cache implementation that's done in the implementation of 3249 // org.jivesoftware.util.cache.CacheFactory.leftCluster). This means that they now hold no data (as a new cache 3250 // has been created). Data that's available on the local node needs to be added again. 3251 localMUCRoomManager.restoreCacheContentAfterLeave(occupantsOnRemovedNodes); 3252 3253 // Send presence 'leave' for all of these users to the users that remain in the chatroom (on this node) 3254 makeOccupantsOnDisconnectedClusterNodesLeave(occupantsOnRemovedNodes, null); 3255 } 3256 3257 @Override leftCluster(byte[] nodeID)3258 public void leftCluster(byte[] nodeID) 3259 { 3260 // Another node left the cluster. 3261 // 3262 // If the cluster node leaves in an orderly fashion, it might have broadcast 3263 // the necessary events itself. This cannot be depended on, as the cluster node 3264 // might have disconnected unexpectedly (as a result of a crash or network issue). 3265 // 3266 // All chatroom occupants that were connected to the now disconnected node are no longer 'in the room'. The 3267 // remaining occupants should receive 'occupant left' stanzas to reflect this. 3268 3269 final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); 3270 Log.debug("Cluster event: service {} got notified that node {} left a cluster", fullServiceName, new String(nodeID)); 3271 3272 3273 // We can't be certain if the room cache is fully intact at this point, as described in OF-2297. It is not always 3274 // feasible to maintain enough 'local data' to be able to recover from this. Here, we check the cache for 3275 // consistency, and remove occupants from rooms (citing 'technical difficulties') where we find such inconsistency. 3276 final Set<String> lostRoomNames = localMUCRoomManager.detectAndRemoveLostRooms(); 3277 for (final String lostRoomName : lostRoomNames) { 3278 try { 3279 Log.info("Room '{}' was lost from the data structure that's shared in the cluster (the cache). This room is now considered 'gone' for this cluster node. Occupants will be informed.", lostRoomName); 3280 final Set<OccupantManager.Occupant> occupants = occupantManager.occupantsForRoomByNode(lostRoomName, XMPPServer.getInstance().getNodeID()); 3281 final JID roomJID = new JID(lostRoomName, fullServiceName, null); 3282 for (final OccupantManager.Occupant occupant : occupants) { 3283 try { 3284 // Send a presence stanza of type "unavailable" to the occupant 3285 final Presence presence = new Presence(Presence.Type.unavailable); 3286 presence.setTo(occupant.getRealJID()); 3287 assert lostRoomName.equals(occupant.getRoomName()); 3288 presence.setFrom(new JID(lostRoomName, fullServiceName, occupant.getNickname())); 3289 3290 // A fragment containing the x-extension. 3291 final Element fragment = presence.addChildElement("x", "http://jabber.org/protocol/muc#user"); 3292 final Element item = fragment.addElement("item"); 3293 item.addAttribute("affiliation", "none"); 3294 item.addAttribute("role", "none"); 3295 fragment.addElement("status").addAttribute("code", "110"); // Inform user that presence refers to itself. 3296 fragment.addElement("status").addAttribute("code", "333"); // Inform users that a user was removed because of an error reply. 3297 3298 Log.debug("Informing local occupant '{}' on local cluster node that it is being removed from room '{}' because of a (cluster) error.", occupant.getRealJID(), lostRoomName); 3299 XMPPServer.getInstance().getPacketRouter().route(presence); 3300 XMPPServer.getInstance().getPresenceUpdateHandler().removeDirectPresence(occupant.getRealJID(), roomJID); 3301 } catch (Exception e) { 3302 Log.warn("Unable to inform local occupant '{}' on local cluster node that it is being removed from room '{}' because of a (cluster) error.", occupant.getRealJID(), lostRoomName, e); 3303 } 3304 } 3305 // Clean up the locally maintained bookkeeping. 3306 occupantManager.roomDestroyed(roomJID); 3307 removeChatRoom(lostRoomName); 3308 } catch (Exception e) { 3309 Log.warn("Unable to inform occupants on local cluster node that they are being removed from room '{}' because of a (cluster) error.", lostRoomName, e); 3310 } 3311 } 3312 3313 // Now that occupants have been properly ousted from the lost rooms, we can make an effort to restore the rooms 3314 // from the database. Calling getActiveAndInactiveRooms() will do just that. 3315 getActiveAndInactiveRooms(); 3316 3317 // From this point onwards, the remainder of what's in the cache can be considered 'consistent' (as we've dealt with the inconsistencies). 3318 // We now need to inform these occupants that occupants of the same room, that exist on other cluster nodes (which are now unreachable) 3319 // are no longer in the room. 3320 3321 3322 // Get all room occupants that lived on the node that disconnected 3323 final Set<OccupantManager.Occupant> occupantsOnRemovedNode = occupantManager.leftCluster(NodeID.getInstance(nodeID)); 3324 3325 // The content of the room cache can still hold (some) of these occupants. They should be removed from there! 3326 // The state of the rooms in the clustered cache should be modified to remove all but our local occupants. 3327 for (Iterator<OccupantManager.Occupant> i = occupantsOnRemovedNode.iterator(); i.hasNext(); ) { 3328 OccupantManager.Occupant occupant = i.next(); 3329 final Lock lock = this.getChatRoomLock(occupant.getRoomName()); 3330 lock.lock(); 3331 try { 3332 final MUCRoom room = this.getChatRoom(occupant.getRoomName()); 3333 if (room == null) { 3334 continue; 3335 } 3336 final MUCRole occupantRole = room.getOccupantByFullJID(occupant.getRealJID()); 3337 3338 // When leaving a cluster, the routing table will also detect that some clients are no 3339 // longer available. And send presence unavailable stanzas accordingly. Because of that, 3340 // the occupant may already have been removed from the room. This may however not happen 3341 // at all if federated users are in play. Hence the null check here: if the occupant is 3342 // no longer in the room, we don't need to remove it. 3343 if (occupantRole != null) { 3344 room.removeOccupantRole(occupantRole); 3345 this.syncChatRoom(room); 3346 Log.debug("Removed occupant role {} from room {}.", occupantRole, room.getJID()); 3347 } else { 3348 Log.debug("Occupant '{}' already removed, so we don't need to send 'leave' presence in room {}", occupant, room.getJID()); 3349 i.remove(); 3350 } 3351 } finally { 3352 lock.unlock(); 3353 } 3354 } 3355 3356 // Send presence 'leave' for all of these users to the users that remain in the chatroom (on this node) 3357 makeOccupantsOnDisconnectedClusterNodesLeave(occupantsOnRemovedNode, NodeID.getInstance(nodeID)); 3358 } 3359 3360 @Override markedAsSeniorClusterMember()3361 public void markedAsSeniorClusterMember() { 3362 final String fullServiceName = chatServiceName + "." + XMPPServer.getInstance().getServerInfo().getXMPPDomain(); 3363 Log.debug("Cluster event: service {} got notified that it is now the senior cluster member", fullServiceName); 3364 // Do nothing 3365 // TODO: Check if all occupants are still reachable 3366 } 3367 3368 @Override getLocalMUCRoomManager()3369 public LocalMUCRoomManager getLocalMUCRoomManager() { 3370 return localMUCRoomManager; 3371 } 3372 3373 /** 3374 * Used by other nodes telling us about their occupants. 3375 * @param task 3376 */ process(@onnull final SyncLocalOccupantsAndSendJoinPresenceTask task)3377 public void process(@Nonnull final SyncLocalOccupantsAndSendJoinPresenceTask task) { 3378 occupantManager.process(task); 3379 makeOccupantsOnConnectedClusterNodeJoin(task.getOccupants(), task.getOriginator()); 3380 } 3381 3382 /** 3383 * Sends presence 'join' stanzas on behalf of chatroom occupants that are now connected to the cluster (by virtue 3384 * of a cluster node joining the cluster) to other room occupants are local to this cluster node. 3385 * 3386 * Note that this method does not change state (in either the clustered cache, or local equivalents): it only sends stanzas. 3387 * 3388 * @param occupantsOnConnectedNodes The occupants for which to send stanzas. 3389 * @param remoteNodeID The cluster node ID of the server that joined the cluster. 3390 */ makeOccupantsOnConnectedClusterNodeJoin(@ullable final Set<OccupantManager.Occupant> occupantsOnConnectedNodes, @Nonnull NodeID remoteNodeID)3391 private void makeOccupantsOnConnectedClusterNodeJoin(@Nullable final Set<OccupantManager.Occupant> occupantsOnConnectedNodes, @Nonnull NodeID remoteNodeID) 3392 { 3393 Log.debug("Going to send 'join' presence stanzas on the local cluster node for {} occupant(s) of cluster node {} that is now part of our cluster.", occupantsOnConnectedNodes == null || occupantsOnConnectedNodes.isEmpty() ? 0 : occupantsOnConnectedNodes.size(), remoteNodeID); 3394 if (occupantsOnConnectedNodes == null || occupantsOnConnectedNodes.isEmpty()) { 3395 return; 3396 } 3397 3398 if (!MUCRoom.JOIN_PRESENCE_ENABLE.getValue()) { 3399 Log.trace("Skipping, as presences are disabled by configuration."); 3400 return; 3401 } 3402 3403 // Find all occupants that were not already on any other node in the cluster. This intends to prevent sending 'join' 3404 // presences for occupants that are in the same room, using the same nickname, but using a client that is 3405 // connected to a cluster node that already was in the cluster. 3406 final Set<OccupantManager.Occupant> toAdd = occupantsOnConnectedNodes.stream() 3407 .filter(occupant -> !occupantManager.exists(occupant, remoteNodeID)) 3408 .collect(Collectors.toSet()); 3409 Log.trace("After accounting for occupants already in the room by virtue of having another connected device using the same nickname, {} presence stanza(s) are to be sent.", toAdd.size()); 3410 3411 // For each, broadcast a 'join' presence in the room(s). 3412 for (final OccupantManager.Occupant occupant : toAdd) 3413 { 3414 Log.trace("Preparing to send 'join' stanza for occupant '{}' (using nickname '{}') of room '{}'", occupant.getRealJID(), occupant.getNickname(), occupant.getRoomName()); 3415 3416 final MUCRoom chatRoom = getChatRoom(occupant.roomName); 3417 if (chatRoom == null) { 3418 Log.info("User {} seems to be an occupant (using nickname '{}') of a non-existent room named '{}' on newly connected cluster node(s).", occupant.realJID, occupant.nickname, occupant.roomName); 3419 continue; 3420 } 3421 3422 // At this stage, the occupant information must already be available through the clustered cache. 3423 final MUCRole occupantRole = chatRoom.getOccupantByFullJID(occupant.getRealJID()); 3424 if (occupantRole == null) { 3425 Log.warn("A remote cluster node ({}) tells us that user {} is supposed to be an occupant (using nickname '{}') of a room named '{}' but the data in the cluster cache does not indicate that this is true.", remoteNodeID, occupant.realJID, occupant.nickname, occupant.roomName); 3426 continue; 3427 } 3428 3429 if (!chatRoom.canBroadcastPresence(occupantRole.getRole())) { 3430 Log.trace("Skipping join stanza, as room is configured to not broadcast presence for role {}", occupantRole.getRole()); 3431 continue; 3432 } 3433 3434 // To prevent each (remaining) cluster node from broadcasting the same presence to all occupants of all cluster nodes, 3435 // this broadcasts only to occupants on the local node. 3436 final Set<OccupantManager.Occupant> recipients = occupantManager.occupantsForRoomByNode(occupant.roomName, XMPPServer.getInstance().getNodeID()); 3437 for (OccupantManager.Occupant recipient : recipients) { 3438 Log.trace("Preparing stanza for recipient {} (nickname: {})", recipient.getRealJID(), recipient.getNickname()); 3439 try { 3440 // Note that we cannot use org.jivesoftware.openfire.muc.MUCRoom.broadcastPresence() as this would attempt to 3441 // broadcast to the user that is 'joining' to all users. We only want to broadcast locally here. 3442 3443 // We _need_ to go through the MUCRole for sending this stanza, as that has some additional logic (eg: FMUC). 3444 final MUCRole recipientRole = chatRoom.getOccupantByFullJID(recipient.getRealJID()); 3445 if (recipientRole != null) { 3446 recipientRole.send(occupantRole.getPresence()); 3447 } else { 3448 Log.warn("Unable to find MUCRole for recipient '{}' in room {} while broadcasting 'join' presence for occupants on joining cluster node {}.", recipient.getRealJID(), chatRoom.getJID(), remoteNodeID); 3449 XMPPServer.getInstance().getPacketRouter().route(occupantRole.getPresence()); // This is a partial fix that will _probably_ work if FMUC is not used. Better than nothing? (although an argument for failing-fast can be made). 3450 } 3451 } catch (Exception e) { 3452 Log.warn("A problem occurred while notifying local occupant that user '{}' joined room '{}' as a result of a cluster node {} joining the cluster.", occupant.nickname, occupant.roomName, remoteNodeID, e); 3453 } 3454 } 3455 } 3456 Log.debug("Finished sending 'join' presence stanzas on the local cluster node."); 3457 } 3458 3459 /** 3460 * Sends presence 'leave' stanzas on behalf of chatroom occupants that are no longer connected to the cluster to 3461 * room occupants that are local to this cluster node. 3462 * 3463 * Note that this method does not change state (in either the clustered cache, or local equivalents): it only sends stanzas. 3464 * 3465 * @param occupantsOnRemovedNodes The occupants for which to send stanzas 3466 */ makeOccupantsOnDisconnectedClusterNodesLeave(@ullable final Set<OccupantManager.Occupant> occupantsOnRemovedNodes, NodeID nodeID)3467 private void makeOccupantsOnDisconnectedClusterNodesLeave(@Nullable final Set<OccupantManager.Occupant> occupantsOnRemovedNodes, NodeID nodeID) 3468 { 3469 Log.debug("Going to send 'leave' presence stanzas on the local cluster node for {} occupant(s) of one or more cluster nodes that are no longer part of our cluster.", occupantsOnRemovedNodes == null || occupantsOnRemovedNodes.isEmpty() ? 0 : occupantsOnRemovedNodes.size()); 3470 if (occupantsOnRemovedNodes == null || occupantsOnRemovedNodes.isEmpty()) { 3471 return; 3472 } 3473 3474 if (!MUCRoom.JOIN_PRESENCE_ENABLE.getValue()) { 3475 Log.trace("Skipping, as presences are disabled by configuration."); 3476 return; 3477 } 3478 3479 // Find all occupants that are now no longer on any node in the cluster. This intends to prevent sending 'leave' 3480 // presences for occupants that are in the same room, using the same nickname, but using a client that is 3481 // connected to a cluster node that is still in the cluster. 3482 final Set<OccupantManager.Occupant> toRemove = occupantsOnRemovedNodes.stream() 3483 .filter(occupant -> !occupantManager.exists(occupant)) 3484 .collect(Collectors.toSet()); 3485 Log.trace("After accounting for occupants still in the room by virtue of having another connected device using the same nickname, {} presence stanza(s) are to be sent.", toRemove.size()); 3486 3487 // For each, broadcast a 'leave' presence in the room(s). 3488 for(final OccupantManager.Occupant occupant : toRemove) { 3489 Log.trace("Preparing to send 'leave' stanza for occupant '{}' (using nickname '{}') of room '{}'", occupant.getRealJID(), occupant.getNickname(), occupant.getRoomName()); 3490 final MUCRoom chatRoom = getChatRoom(occupant.getRoomName()); 3491 if (chatRoom == null) { 3492 Log.info("User {} seems to be an occupant (using nickname '{}') of a non-existent room named '{}' on disconnected cluster node(s).", occupant.getRealJID(), occupant.getNickname(), occupant.getRoomName()); 3493 continue; 3494 } 3495 3496 // To prevent each (remaining) cluster node from broadcasting the same presence to all occupants of all remaining nodes, 3497 // this broadcasts only to occupants on the local node. 3498 final Set<OccupantManager.Occupant> recipients; 3499 if (nodeID == null) { 3500 recipients = occupantManager.occupantsForRoomByNode(occupant.getRoomName(), XMPPServer.getInstance().getNodeID()); 3501 Log.trace("Intended recipients, count: {} (occupants of the same room, on local node): {}", recipients.size(), recipients.stream().map(OccupantManager.Occupant::getRealJID).map(JID::toString).collect(Collectors.joining( ", " ))); 3502 } else { 3503 recipients = occupantManager.occupantsForRoomExceptForNode(occupant.getRoomName(), nodeID); 3504 Log.trace("Intended recipients, count: {} (occupants of the same room, on all remaining cluster nodes): {}", recipients.size(), recipients.stream().map(OccupantManager.Occupant::getRealJID).map(JID::toString).collect(Collectors.joining( ", " ))); 3505 } 3506 for (OccupantManager.Occupant recipient : recipients) { 3507 try { 3508 Log.trace("Preparing stanza for recipient {} (nickname: {})", recipient.getRealJID(), recipient.getNickname()); 3509 // Note that we cannot use chatRoom.sendLeavePresenceToExistingOccupants(leaveRole) as this would attempt to 3510 // broadcast to the user that is leaving. That user is clearly unreachable in this instance (as it lives on 3511 // a now disconnected cluster node. 3512 final Presence presence = new Presence(Presence.Type.unavailable); 3513 presence.setTo(recipient.getRealJID()); 3514 presence.setFrom(new JID(chatRoom.getJID().getNode(), chatRoom.getJID().getDomain(), occupant.nickname)); 3515 final Element childElement = presence.addChildElement("x", "http://jabber.org/protocol/muc#user"); 3516 final Element item = childElement.addElement("item"); 3517 item.addAttribute("role", "none"); 3518 if (chatRoom.canAnyoneDiscoverJID() || chatRoom.getModerators().stream().anyMatch(m->m.getUserAddress().asBareJID().equals(recipient.getRealJID().asBareJID()))) { 3519 // Send non-anonymous - add JID. 3520 item.addAttribute("jid", occupant.getRealJID().toString()); 3521 } 3522 childElement.addElement( "status" ).addAttribute( "code", "333" ); 3523 3524 // We _need_ to go through the MUCRole for sending this stanza, as that has some additional logic (eg: FMUC). 3525 final MUCRole recipientRole = chatRoom.getOccupantByFullJID(recipient.getRealJID()); 3526 Log.debug("Stanza now being sent: {}", presence.toXML()); 3527 if (recipientRole != null) { 3528 recipientRole.send(presence); 3529 } else { 3530 Log.warn("Unable to find MUCRole for recipient '{}' in room {} while broadcasting 'leave' presence for occupants on disconnected cluster node(s).", recipient.getRealJID(), chatRoom.getJID()); 3531 XMPPServer.getInstance().getPacketRouter().route(presence); // This is a partial fix that will _probably_ work if FMUC is not used. Better than nothing? (although an argument for failing-fast can be made). 3532 } 3533 } catch (Exception e) { 3534 Log.warn("A problem occurred while notifying local occupant that user '{}' left room '{}' as a result of a cluster disconnect.", occupant.nickname, occupant.roomName, e); 3535 } 3536 } 3537 } 3538 Log.debug("Finished sending 'leave' presence stanzas on the local cluster node."); 3539 } 3540 } 3541