SessionManager.java 75 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13
/**
 * $RCSfile$
 * $Revision: 3170 $
 * $Date: 2005-12-07 14:00:58 -0300 (Wed, 07 Dec 2005) $
 *
 * Copyright (C) 2004 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution.
 */

package org.jivesoftware.wildfire;

14 15 16
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
17 18
import org.jivesoftware.wildfire.audit.AuditStreamIDFactory;
import org.jivesoftware.wildfire.auth.UnauthorizedException;
19 20
import org.jivesoftware.wildfire.component.ComponentSession;
import org.jivesoftware.wildfire.component.InternalComponentManager;
21
import org.jivesoftware.wildfire.container.BasicModule;
22
import org.jivesoftware.wildfire.event.SessionEventDispatcher;
23
import org.jivesoftware.wildfire.handler.PresenceUpdateHandler;
24 25 26
import org.jivesoftware.wildfire.multiplex.ConnectionMultiplexerManager;
import org.jivesoftware.wildfire.multiplex.ConnectionMultiplexerSession;
import org.jivesoftware.wildfire.net.SocketConnection;
27 28 29 30 31 32 33 34 35 36 37
import org.jivesoftware.wildfire.server.IncomingServerSession;
import org.jivesoftware.wildfire.server.OutgoingServerSession;
import org.jivesoftware.wildfire.server.OutgoingSessionPromise;
import org.jivesoftware.wildfire.spi.BasicStreamIDFactory;
import org.jivesoftware.wildfire.user.UserManager;
import org.jivesoftware.wildfire.user.UserNotFoundException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;

38
import java.net.InetAddress;
39 40 41
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
42
import java.util.concurrent.atomic.AtomicInteger;
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61

/**
 * Manages the sessions associated with an account. The information
 * maintained by the Session manager is entirely transient and does
 * not need to be preserved between server restarts.
 *
 * @author Derek DeMoro
 */
public class SessionManager extends BasicModule {

    public static final int NEVER_KICK = -1;

    private PresenceUpdateHandler presenceHandler;
    private PacketRouter router;
    private String serverName;
    private JID serverAddress;
    private UserManager userManager;
    private int conflictLimit;

62 63 64 65 66 67
    /**
     * Counter of user sessions. A session is counted just after it was created and not
     * after the user came available.
     */
    private final AtomicInteger usersSessionsCounter = new AtomicInteger(0);

68 69 70 71
    private ClientSessionListener clientSessionListener = new ClientSessionListener();
    private ComponentSessionListener componentSessionListener = new ComponentSessionListener();
    private IncomingServerSessionListener incomingServerListener = new IncomingServerSessionListener();
    private OutgoingServerSessionListener outgoingServerListener = new OutgoingServerSessionListener();
72
    private ConnectionMultiplexerSessionListener multiplexerSessionListener = new ConnectionMultiplexerSessionListener();
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99

    /**
     * Map that holds sessions that has been created but haven't been authenticated yet. The Map
     * will hold client sessions.
     */
    private Map<String, ClientSession> preAuthenticatedSessions = new ConcurrentHashMap<String, ClientSession>();

    /**
     * Map of priority ordered SessionMap objects with username (toLowerCase) as key. The sessions
     * contained in this Map are client sessions. For each username a SessionMap is kept which
     * tracks the session for each user resource.
     */
    private Map<String, SessionMap> sessions = new ConcurrentHashMap<String, SessionMap>();

    /**
     * Map of anonymous server sessions. They need to be treated separately as they
     * have no associated user, and don't follow the normal routing rules for
     * priority based fall over. The sessions contained in this Map are client sessions.
     */
    private Map<String, ClientSession> anonymousSessions = new ConcurrentHashMap<String, ClientSession>();

    /**
     * The sessions contained in this List are component sessions. For each connected component
     * this Map will keep the component's session.
     */
    private List<ComponentSession> componentsSessions = new CopyOnWriteArrayList<ComponentSession>();

100 101 102 103 104 105 106 107 108 109
    /**
     * Map of connection multiplexer sessions grouped by connection managers. Each connection
     * manager may have many connections to the server (i.e. connection pool). All connections
     * originated from the same connection manager are grouped as a single entry in the map.
     * Once all connections have been closed users that were logged using the connection manager
     * will become unavailable.
     */
    private Map<String, List<ConnectionMultiplexerSession>> connnectionManagerSessions =
            new ConcurrentHashMap<String, List<ConnectionMultiplexerSession>>();

110 111 112 113 114 115 116 117
    /**
     * The sessions contained in this Map are server sessions originated by a remote server. These
     * sessions can only receive packets from the remote server but are not capable of sending
     * packets to the remote server. Sessions will be added to this collecion only after they were
     * authenticated. The key of the Map is the hostname of the remote server. The value is a
     * list of IncomingServerSession that will keep each session created by a remote server to
     * this server.
     */
118 119
    private final Map<String, List<IncomingServerSession>> incomingServerSessions =
            new ConcurrentHashMap<String, List<IncomingServerSession>>();
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

    /**
     * The sessions contained in this Map are server sessions originated from this server to remote
     * servers. These sessions can only send packets to the remote server but are not capable of
     * receiving packets from the remote server. Sessions will be added to this collecion only
     * after they were authenticated. The key of the Map is the hostname of the remote server.
     */
    private Map<String, OutgoingServerSession> outgoingServerSessions = new ConcurrentHashMap<String, OutgoingServerSession>();

    /**
     * <p>Session manager must maintain the routing table as sessions are added and
     * removed.</p>
     */
    private RoutingTable routingTable;

    private StreamIDFactory streamIDFactory;

    /**
     * Timer that will clean up dead or inactive sessions. Currently only outgoing server sessions
     * will be analyzed.
     */
    private Timer timer = new Timer("Sessions cleanup");

    /**
     * Task that closes idle server sessions.
     */
    private ServerCleanupTask serverCleanupTask;

    /**
     * Returns the instance of <CODE>SessionManagerImpl</CODE> being used by the XMPPServer.
     *
     * @return the instance of <CODE>SessionManagerImpl</CODE> being used by the XMPPServer.
     */
    public static SessionManager getInstance() {
        return XMPPServer.getInstance().getSessionManager();
    }

    public SessionManager() {
        super("Session Manager");
        if (JiveGlobals.getBooleanProperty("xmpp.audit.active")) {
            streamIDFactory = new AuditStreamIDFactory();
        }
        else {
            streamIDFactory = new BasicStreamIDFactory();
        }

        String conflictLimitProp = JiveGlobals.getProperty("xmpp.session.conflict-limit");
        if (conflictLimitProp == null) {
            conflictLimit = 0;
            JiveGlobals.setProperty("xmpp.session.conflict-limit", Integer.toString(conflictLimit));
        }
        else {
            try {
                conflictLimit = Integer.parseInt(conflictLimitProp);
            }
            catch (NumberFormatException e) {
                conflictLimit = 0;
                JiveGlobals.setProperty("xmpp.session.conflict-limit", Integer.toString(conflictLimit));
            }
        }
    }

182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
    /**
     * Returns the session originated from the specified address. If the address contains
     * a resource then the exact session is going to be looked for and if none is found then
     * <tt>null</tt> is going to be returned. On the other hand, if the address is just a domain
     * then any session originated from the connection manager is going to be returned and if
     * the connection manager has no sessions then <tt>null</tt> is going to be returned.
     *
     * @param address the address of the connection manager (no resource included) or a specific
     *        session of the connection manager (resource included).
     * @return the session originated from the specified address.
     */
    public ConnectionMultiplexerSession getConnectionMultiplexerSession(JID address) {
        List<ConnectionMultiplexerSession> sessions =
                connnectionManagerSessions.get(address.getDomain());
        if (sessions == null || sessions.isEmpty()) {
            return null;
        }
        if (address.getResource() != null) {
            // Look for the exact session
            for (ConnectionMultiplexerSession session : sessions) {
                if (session.getAddress().equals(address)) {
                    return session;
                }
            }
            return null;
        }
        else {
            // Look for any session of the connection manager
            return sessions.get(0);
        }
    }

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
    /**
     * Returns all sessions originated from connection managers.
     *
     * @return all sessions originated from connection managers.
     */
    public List<ConnectionMultiplexerSession> getConnectionMultiplexerSessions() {
        if (connnectionManagerSessions.isEmpty()) {
            return Collections.emptyList();
        }
        List<ConnectionMultiplexerSession> answer = new ArrayList<ConnectionMultiplexerSession>();
        for (List<ConnectionMultiplexerSession> sessions : connnectionManagerSessions.values()) {
            answer.addAll(sessions);
        }
        return answer;
    }

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
    /**
     * Returns a collection with all the sessions originated from the connection manager
     * whose domain matches the specified domain. If there is no connection manager with
     * the specified domain then an empty list is going to be returned.
     *
     * @param domain the domain of the connection manager.
     * @return a collection with all the sessions originated from the connection manager
     *         whose domain matches the specified domain.
     */
    public List<ConnectionMultiplexerSession> getConnectionMultiplexerSessions(String domain) {
        List<ConnectionMultiplexerSession> sessions = connnectionManagerSessions.get(domain);
        if (sessions == null || sessions.isEmpty()) {
            return Collections.emptyList();
        }
        return new ArrayList<ConnectionMultiplexerSession>(sessions);
    }

247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
    /**
     * Returns the IP address of the connection manager whose domain matches the
     * specified domain.
     *
     * @param domain the domain of the connection manager.
     * @return the IP address of the connection manager.
     */
    public InetAddress getConnectionMultiplexerInetAddress(String domain) {
        List<ConnectionMultiplexerSession> sessions = connnectionManagerSessions.get(domain);
        if (sessions == null || sessions.isEmpty()) {
            return null;
        }
        return sessions.get(0).getConnection().getInetAddress();
    }

262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290
    /**
     * Creates a new <tt>ConnectionMultiplexerSession</tt>.
     *
     * @param conn the connection to create the session from.
     * @param address the JID (may include a resource) of the connection manager's session. 
     * @return a newly created session.
     * @throws UnauthorizedException
     */
    public ConnectionMultiplexerSession createMultiplexerSession(SocketConnection conn, JID address)
            throws UnauthorizedException {
        if (serverName == null) {
            throw new UnauthorizedException("Server not initialized");
        }
        StreamID id = nextStreamID();
        ConnectionMultiplexerSession session = new ConnectionMultiplexerSession(serverName, conn, id);
        conn.init(session);
        // Register to receive close notification on this session so we can
        // figure out when users that were using this connection manager may become unavailable
        conn.registerCloseListener(multiplexerSessionListener, session);

        // Add to connection multiplexer session.
        List<ConnectionMultiplexerSession> sessions =
                connnectionManagerSessions.get(address.getDomain());
        if (sessions == null) {
            synchronized (address.getDomain().intern()) {
                sessions = connnectionManagerSessions.get(address.getDomain());
                if (sessions == null) {
                    sessions = new CopyOnWriteArrayList<ConnectionMultiplexerSession>();
                    connnectionManagerSessions.put(address.getDomain(), sessions);
291 292 293 294
                    // Notify ConnectionMultiplexerManager that a new connection manager
                    // is available
                    ConnectionMultiplexerManager.getInstance()
                            .multiplexerAvailable(address.getDomain());
295 296 297 298 299 300 301
                }
            }
        }
        sessions.add(session);
        return session;
    }

302 303 304 305 306 307
    /**
     * Simple data structure to track sessions for a single user (tracked by resource
     * and priority).
     */
    private class SessionMap {
        private Map<String,ClientSession> resources = new ConcurrentHashMap<String,ClientSession>();
308
        private final LinkedList<String> priorityList = new LinkedList<String>();
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503

        /**
         * Add a session to the manager.
         *
         * @param session
         */
        void addSession(ClientSession session) {
            String resource = session.getAddress().getResource();
            Presence presence = session.getPresence();
            int priority = presence == null ? 0 : presence.getPriority();
            resources.put(resource, session);
            sortSession(resource, priority);
        }

        /**
         * Sorts the session into the list based on priority
         *
         * @param resource The resource corresponding to the session to sort
         * @param priority The priority to use for sorting
         */
        private void sortSession(String resource, int priority) {
            synchronized (priorityList) {
                if (priorityList.size() > 0) {
                    Iterator<String> iter = priorityList.iterator();
                    for (int i = 0; iter.hasNext(); i++) {
                        ClientSession sess = resources.get(iter.next());
                        if (sess != null && sess.getPresence().getPriority() <= priority) {
                            priorityList.add(i, resource);
                            break;
                        }
                    }
                }
                if (!priorityList.contains(resource)) {
                    priorityList.addLast(resource);
                }
            }
        }

        /**
         * Change the priority of a session associated with the sender.
         *
         * @param sender   The sender who's session just changed priority
         * @param priority The new priority for the session
         */
        public void changePriority(JID sender, int priority) {
            String resource = sender.getResource();
            if (resources.containsKey(resource)) {
                synchronized (priorityList) {
                    priorityList.remove(resource);
                    sortSession(resource, priority);
                }
            }
        }

        /**
         * Remove a session from the manager.
         *
         * @param session The session to remove
         */
        void removeSession(Session session) {
            String resource = session.getAddress().getResource();
            resources.remove(resource);
            synchronized (priorityList) {
                priorityList.remove(resource);
            }
        }

        /**
         * Gets the session for the given resource.
         *
         * @param resource The resource describing the particular session
         * @return The session for that resource or null if none found (use getDefaultSession() to obtain default)
         */
        ClientSession getSession(String resource) {
            return resources.get(resource);
        }

        /**
         * Checks to see if a session for the given resource exists.
         *
         * @param resource The resource of the session we're checking
         * @return True if we have a session corresponding to that resource
         */
        boolean hasSession(String resource) {
            return resources.containsKey(resource);
        }

        /**
         * Returns the default session for the user based on presence priority. It's possible to
         * indicate if only available sessions (i.e. with an available presence) should be
         * included in the search.
         *
         * @param filterAvailable flag that indicates if only available sessions should be
         *        considered.
         * @return The default session for the user.
         */
        ClientSession getDefaultSession(boolean filterAvailable) {
            if (priorityList.isEmpty()) {
                return null;
            }

            if (!filterAvailable) {
                return resources.get(priorityList.getFirst());
            }
            else {
                synchronized (priorityList) {
                    for (int i=0; i < priorityList.size(); i++) {
                        ClientSession s = resources.get(priorityList.get(i));
                        if (s != null && s.getPresence().isAvailable()) {
                            return s;
                        }
                    }
                }
                return null;
            }
        }

        /**
         * Determines if this map is empty or not.
         *
         * @return True if the map contains no entries
         */
        boolean isEmpty() {
            return resources.isEmpty();
        }

        /**
         * Broadcast to all resources for the given user
         *
         * @param packet
         */
        private void broadcast(Packet packet) throws UnauthorizedException, PacketException {
            for (Session session : resources.values()) {
                packet.setTo(session.getAddress());
                session.process(packet);
            }
        }

        /**
         * Create an iterator over all sessions for the user.
         * We create a new list to generate the iterator so other threads
         * may safely alter the session map without affecting the iterator.
         *
         * @return An iterator of all sessions
         */
        public Collection<ClientSession> getSessions() {
            return resources.values();
        }

        /**
         * Returns a collection of all the sessions whose presence is available.
         *
         * @return a collection of all the sessions whose presence is available.
         */
        public Collection<ClientSession> getAvailableSessions() {
            LinkedList<ClientSession> list = new LinkedList<ClientSession>();
            for (ClientSession session : resources.values()) {
                if (session.getPresence().isAvailable()) {
                    list.add(session);
                }
            }
            return list;
        }

        /**
         * This specified session has received an available presence so we need to recalculate the
         * order of the sessions so we can have update the default session.
         *
         * @param session the session that received an available presence.
         */
        public void sessionAvailable(ClientSession session) {
            changePriority(session.getAddress(), session.getPresence().getPriority());
        }

        /**
         * This specified session has received an unavailable presence so we need to recalculate the
         * order of the sessions so we can have update the default session.
         *
         * @param session the session that received an unavailable presence.
         */
        public void sessionUnavailable(ClientSession session) {
            changePriority(session.getAddress(), 0);
        }
    }

    /**
     * Returns a randomly created ID to be used in a stream element.
     *
     * @return a randomly created ID to be used in a stream element.
     */
    public StreamID nextStreamID() {
        return streamIDFactory.createStreamID();
    }

    /**
504 505
     * Creates a new <tt>ClientSession</tt>. The new Client session will have a newly created
     * stream ID.
506 507 508 509 510
     *
     * @param conn the connection to create the session from.
     * @return a newly created session.
     * @throws UnauthorizedException
     */
511 512 513 514 515 516 517 518 519 520 521 522 523 524
    public ClientSession createClientSession(Connection conn) throws UnauthorizedException {
        return createClientSession(conn, nextStreamID());
    }

    /**
     * Creates a new <tt>ClientSession</tt> with the specified streamID.
     *
     * @param conn the connection to create the session from.
     * @param id the streamID to use for the new session.
     * @return a newly created session.
     * @throws UnauthorizedException
     */
    public ClientSession createClientSession(Connection conn, StreamID id)
            throws UnauthorizedException {
525 526 527 528 529 530 531 532 533 534 535
        if (serverName == null) {
            throw new UnauthorizedException("Server not initialized");
        }
        ClientSession session = new ClientSession(serverName, conn, id);
        conn.init(session);
        // Register to receive close notification on this session so we can
        // remove  and also send an unavailable presence if it wasn't
        // sent before
        conn.registerCloseListener(clientSessionListener, session);

        // Add to pre-authenticated sessions.
536
        preAuthenticatedSessions.put(session.getAddress().getResource(), session);
537 538
        // Increment the counter of user sessions
        usersSessionsCounter.incrementAndGet();
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
        return session;
    }

    public Session createComponentSession(Connection conn) throws UnauthorizedException {
        if (serverName == null) {
            throw new UnauthorizedException("Server not initialized");
        }
        StreamID id = nextStreamID();
        ComponentSession session = new ComponentSession(serverName, conn, id);
        conn.init(session);
        // Register to receive close notification on this session so we can
        // remove the external component from the list of components
        conn.registerCloseListener(componentSessionListener, session);

        // Add to component session.
        componentsSessions.add(session);
        return session;
    }

    /**
     * Creates a session for a remote server. The session should be created only after the
     * remote server has been authenticated either using "server dialback" or SASL.
     *
     * @param conn the connection to the remote server.
     * @param id the stream ID used in the stream element when authenticating the server.
     * @return the newly created {@link IncomingServerSession}.
     * @throws UnauthorizedException if the local server has not been initialized yet.
     */
    public IncomingServerSession createIncomingServerSession(Connection conn, StreamID id)
            throws UnauthorizedException {
        if (serverName == null) {
            throw new UnauthorizedException("Server not initialized");
        }
        IncomingServerSession session = new IncomingServerSession(serverName, conn, id);
        conn.init(session);
        // Register to receive close notification on this session so we can
        // remove its route from the sessions set
        conn.registerCloseListener(incomingServerListener, session);

        return session;
    }

    /**
     * Notification message that a new OutgoingServerSession has been created. Register a listener
     * that will react when the connection gets closed.
     *
     * @param session the newly created OutgoingServerSession.
     */
    public void outgoingServerSessionCreated(OutgoingServerSession session) {
        // Register to receive close notification on this session so we can
        // remove its route from the sessions set
        session.getConnection().registerCloseListener(outgoingServerListener, session);
    }

    /**
     * Registers that a server session originated by a remote server is hosting a given hostname.
     * Notice that the remote server may be hosting several subdomains as well as virtual hosts so
     * the same IncomingServerSession may be associated with many keys. If the remote server
     * creates many sessions to this server (eg. one for each subdomain) then associate all
     * the sessions with the originating server that created all the sessions.
     *
     * @param hostname the hostname that is being served by the remote server.
     * @param session the incoming server session to the remote server.
     */
    public void registerIncomingServerSession(String hostname, IncomingServerSession session) {
        synchronized (incomingServerSessions) {
            List<IncomingServerSession> sessions = incomingServerSessions.get(hostname);
            if (sessions == null || sessions.isEmpty()) {
                // First session from the remote server to this server so create a
                // new association
                List<IncomingServerSession> value = new CopyOnWriteArrayList<IncomingServerSession>();
                value.add(session);
                incomingServerSessions.put(hostname, value);
            }
            else {
                // Add new session to the existing list of sessions originated by
                // the remote server
                sessions.add(session);
            }
        }
    }

    /**
     * Unregisters the server sessions originated by a remote server with the specified hostname.
     * Notice that the remote server may be hosting several subdomains as well as virtual hosts so
     * the same IncomingServerSession may be associated with many keys. The remote server may have
     * many sessions established with this server (eg. to the server itself and to subdomains
     * hosted by this server).
     *
     * @param hostname the hostname that is being served by the remote server.
     */
    public void unregisterIncomingServerSessions(String hostname) {
        synchronized (incomingServerSessions) {
            incomingServerSessions.remove(hostname);
        }
    }

    /**
     * Unregisters the specified remote server session originiated by the specified remote server.
     *
     * @param hostname the hostname that is being served by the remote server.
     * @param session the session to unregiser.
     */
    private void unregisterIncomingServerSession(String hostname, IncomingServerSession session) {
        synchronized (incomingServerSessions) {
            List<IncomingServerSession> sessions = incomingServerSessions.get(hostname);
            if (sessions != null) {
                sessions.remove(session);
647 648 649 650
                if (sessions.isEmpty()) {
                    // Remove key since there are no more sessions associated
                    incomingServerSessions.remove(hostname);
                }
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
            }
        }
    }

    /**
     * Registers that a server session originated by this server has been established to
     * a remote server named hostname. This session will only be used for sending packets
     * to the remote server and cannot receive packets. The {@link OutgoingServerSession}
     * may have one or more domains, subdomains or virtual hosts authenticated with the
     * remote server.
     *
     * @param hostname the hostname that is being served by the remote server.
     * @param session the outgoing server session to the remote server.
     */
    public void registerOutgoingServerSession(String hostname, OutgoingServerSession session) {
        outgoingServerSessions.put(hostname, session);
    }

    /**
     * Unregisters the server session that was originated by this server to a remote server
     * named hostname. This session was only being used for sending packets
     * to the remote server and not for receiving packets. The {@link OutgoingServerSession}
     * may have one or more domains, subdomains or virtual hosts authenticated with the
     * remote server.
     *
     * @param hostname the hostname that the session was connected with.
     */
    public void unregisterOutgoingServerSession(String hostname) {
        outgoingServerSessions.remove(hostname);
    }

    /**
     * Add a new session to be managed.
     */
685
    public void addSession(ClientSession session) {
686
        String username = session.getAddress().getNode();
687
        SessionMap resources;
688

689
        synchronized (username.intern()) {
690 691 692 693 694 695 696
            resources = sessions.get(username);
            if (resources == null) {
                resources = new SessionMap();
                sessions.put(username, resources);
            }
            resources.addSession(session);
        }
697 698
        // Remove the pre-Authenticated session but remember to use the temporary ID as the key
        preAuthenticatedSessions.remove(session.getStreamID().toString());
699 700 701
        // Fire session created event.
        SessionEventDispatcher
                .dispatchEvent(session, SessionEventDispatcher.EventType.session_created);
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719
    }

    /**
     * Notification message sent when a client sent an available presence for the session. Making
     * the session available means that the session is now eligible for receiving messages from
     * other clients. Sessions whose presence is not available may only receive packets (IQ packets)
     * from the server. Therefore, an unavailable session remains invisible to other clients.
     *
     * @param session the session that receieved an available presence.
     */
    public void sessionAvailable(ClientSession session) {
        if (anonymousSessions.containsValue(session)) {
            // Anonymous session always have resources so we only need to add one route. That is
            // the route to the anonymous session
            routingTable.addRoute(session.getAddress(), session);
        }
        else {
            // A non-anonymous session is now available
720
            Session defaultSession;
721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
            try {
                SessionMap sessionMap = sessions.get(session.getUsername());
                if (sessionMap == null) {
                    Log.warn("No SessionMap found for session" + "\n" + session);
                }
                // Update the order of the sessions based on the new presence of this session
                sessionMap.sessionAvailable(session);
                defaultSession = sessionMap.getDefaultSession(true);
                JID node = new JID(defaultSession.getAddress().getNode(),
                        defaultSession.getAddress().getDomain(), null);
                // Add route to default session (used when no resource is specified)
                routingTable.addRoute(node, defaultSession);
                // Add route to the new session
                routingTable.addRoute(session.getAddress(), session);
                // Broadcast presence between the user's resources
736
                broadcastPresenceOfOtherResource(session);
737 738 739 740 741 742 743 744
            }
            catch (UserNotFoundException e) {
                // Do nothing since the session is anonymous (? - shouldn't happen)
            }
        }
    }

    /**
745
     * Sends the presences of other connected resources to the resource that just connected.
746
     * 
747
     * @param session the newly created session.
748
     */
749
    private void broadcastPresenceOfOtherResource(ClientSession session)
750
            throws UserNotFoundException {
751
        Presence presence;
752 753 754 755 756 757 758 759
        Collection<ClientSession> availableSession;
        SessionMap sessionMap = sessions.get(session.getUsername());
        if (sessionMap != null) {
            availableSession = new ArrayList<ClientSession>(sessionMap.getAvailableSessions());
            for (ClientSession userSession : availableSession) {
                if (userSession != session) {
                    // Send the presence of an existing session to the session that has just changed
                    // the presence
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782
                    presence = userSession.getPresence().createCopy();
                    presence.setTo(session.getAddress());
                    session.process(presence);
                }
            }
        }
    }

    /**
     * Broadcasts presence updates from the originating user's resource to any of the user's
     * existing available resources (if any).
     *
     * @param originatingResource the full JID of the session that sent the presence update.
     */
    public void broadcastPresenceToOtherResources(JID originatingResource, Presence presence) {
        Collection<ClientSession> availableSession;
        SessionMap sessionMap = sessions.get(originatingResource.getNode());
        if (sessionMap != null) {
            availableSession = new ArrayList<ClientSession>(sessionMap.getAvailableSessions());
            for (ClientSession userSession : availableSession) {
                if (userSession.getAddress() != originatingResource) {
                    // Send the presence of the session whose presence has changed to
                    // this other user's session
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846
                    presence.setTo(userSession.getAddress());
                    userSession.process(presence);
                }
            }
        }
    }

    /**
     * Notification message sent when a client sent an unavailable presence for the session. Making
     * the session unavailable means that the session is not eligible for receiving messages from
     * other clients.
     *
     * @param session the session that receieved an unavailable presence.
     */
    public void sessionUnavailable(ClientSession session) {
        if (session.getAddress() != null && routingTable != null &&
                session.getAddress().toBareJID().trim().length() != 0) {
            // Remove route to the removed session (anonymous or not)
            routingTable.removeRoute(session.getAddress());
            try {
                if (session.getUsername() == null) {
                    // Do nothing since this is an anonymous session
                    return;
                }
                SessionMap sessionMap = sessions.get(session.getUsername());
                // If sessionMap is null, which is an irregular case, try to clean up the routes to
                // the user from the routing table
                if (sessionMap == null) {
                    JID userJID = new JID(session.getUsername(), serverName, "");
                    if (routingTable.getRoute(userJID) != null) {
                        // Remove the route for the session's BARE address
                        routingTable.removeRoute(new JID(session.getAddress().getNode(),
                                session.getAddress().getDomain(), ""));
                    }
                }
                // If all the user sessions are gone then remove the route to the default session
                else if (sessionMap.getAvailableSessions().isEmpty()) {
                    // Remove the route for the session's BARE address
                    routingTable.removeRoute(new JID(session.getAddress().getNode(),
                            session.getAddress().getDomain(), ""));
                }
                else {
                    // Update the order of the sessions based on the new presence of this session
                    sessionMap.sessionUnavailable(session);
                    // Update the route for the session's BARE address
                    Session defaultSession = sessionMap.getDefaultSession(true);
                    routingTable.addRoute(new JID(defaultSession.getAddress().getNode(),
                            defaultSession.getAddress().getDomain(), ""),
                            defaultSession);
                }
            }
            catch (UserNotFoundException e) {
                // Do nothing since the session is anonymous
            }
        }
    }

    /**
     * Change the priority of a session, that was already available, associated with the sender.
     *
     * @param sender   The sender who's session just changed priority
     * @param priority The new priority for the session
     */
    public void changePriority(JID sender, int priority) {
847
        if (sender.getNode() == null || !userManager.isRegisteredUser(sender.getNode())) {
848
            // Do nothing if the session belongs to an anonymous user
849 850
            return;
        }
851
        Session defaultSession;
852
        String username = sender.getNode();
853 854 855 856
        SessionMap resources = sessions.get(username);
        if (resources == null) {
            return;
        }
857 858 859 860
        synchronized (username.intern()) {
            resources.changePriority(sender, priority);

            // Get the session with highest priority
861
            defaultSession = resources.getDefaultSession(true);
862
        }
863 864 865 866
        // Update the route to the bareJID with the session with highest priority
        routingTable.addRoute(new JID(defaultSession.getAddress().getNode(),
                defaultSession.getAddress().getDomain(), ""),
                defaultSession);
867 868 869 870 871 872 873 874 875 876 877 878
    }


    /**
     * Retrieve the best route to deliver packets to this session given the recipient jid. If the
     * requested JID does not have a node (i.e. username) then the best route will be looked up
     * in the anonymous sessions list. Otherwise, try to find a root for the exact JID
     * (i.e. including the resource) and if none is found then answer the deafult session if any.
     *
     * @param recipient The recipient ID to deliver packets to
     * @return The XMPPAddress best suited to use for delivery to the recipient
     */
879
    public ClientSession getBestRoute(JID recipient) {
880 881 882 883 884 885 886
        // Return null if the JID belongs to a foreign server
        if (serverName == null || !serverName.equals(recipient.getDomain())) {
             return null;
        }
        ClientSession session = null;
        String resource = recipient.getResource();
        String username = recipient.getNode();
887
        if (resource != null && (username == null || !userManager.isRegisteredUser(username))) {
888 889 890
            session = anonymousSessions.get(resource);
            if (session == null){
                session = getSession(recipient);
891 892 893
            }
        }
        else {
894 895 896 897
            SessionMap sessionMap = sessions.get(username);
            if (sessionMap != null) {
                if (resource == null) {
                    synchronized (username.intern()) {
898 899
                        session = sessionMap.getDefaultSession(false);
                    }
900 901 902 903 904
                }
                else {
                    session = sessionMap.getSession(resource);
                    if (session == null) {
                        synchronized (username.intern()) {
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
                            session = sessionMap.getDefaultSession(false);
                        }
                    }
                }
            }
        }
        // Sanity check - check if the underlying session connection is closed. Remove the session
        // from the list of sessions if the session is closed and proceed to look for another route.
        if (session != null && session.getConnection().isClosed()) {
            removeSession(session);
            return getBestRoute(recipient);
        }
        return session;
    }

920 921 922 923 924
    public boolean isAnonymousRoute(String username) {
        // JID's node and resource are the same for anonymous sessions
        return anonymousSessions.containsKey(username);
    }

925 926 927
    public boolean isActiveRoute(String username, String resource) {
        boolean hasRoute = false;

928 929 930 931
        // Check if there is an anonymous session
        if (resource != null && resource.equals(username) &&
                anonymousSessions.containsKey(resource)) {
            hasRoute = true;
932 933
        }
        else {
934
            // Check if there is a session for a registered user
935
            Session session = null;
936 937 938 939 940 941 942 943
            SessionMap sessionMap = sessions.get(username);
            if (sessionMap != null) {
                if (resource == null) {
                    hasRoute = !sessionMap.isEmpty();
                }
                else {
                    if (sessionMap.hasSession(resource)) {
                        session = sessionMap.getSession(resource);
944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968
                    }
                }
            }
            // Makes sure the session is still active
            // Must occur outside of the lock since validation can cause
            // the socket to close - deadlocking on session removal
            if (session != null && !session.getConnection().isClosed()) {
                hasRoute = session.getConnection().validate();
            }

        }
        return hasRoute;
    }

    /**
     * Returns the session responsible for this JID.
     *
     * @param from the sender of the packet.
     * @return the <code>Session</code> associated with the JID.
     */
    public ClientSession getSession(JID from) {
        // Return null if the JID is null
        if (from == null) {
            return null;
        }
969
        return getSession(from.getNode(), from.getDomain(), from.getResource());
970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990
    }

    /**
     * Returns the session responsible for this JID data. The returned Session may have never sent
     * an available presence (thus not have a route) or could be a Session that hasn't
     * authenticated yet (i.e. preAuthenticatedSessions). 
     *
     * @param username the username of the JID.
     * @param domain the username of the JID.
     * @param resource the username of the JID.
     * @return the <code>Session</code> associated with the JID data.
     */
    public ClientSession getSession(String username, String domain, String resource) {
        // Return null if the JID's data belongs to a foreign server. If the server is
        // shutting down then serverName will be null so answer null too in this case.
        if (serverName == null || !serverName.equals(domain)) {
            return null;
        }

        ClientSession session = null;
        // Initially Check preAuthenticated Sessions
991 992
        if (resource != null) {
            session = preAuthenticatedSessions.get(resource);
993
            if (session != null) {
994 995
                return session;
            }
996 997
        }

998
        if (resource == null || username == null) {
999 1000
            return null;
        }
1001 1002 1003 1004

        SessionMap sessionMap = sessions.get(username);
        if (sessionMap != null) {
            session = sessionMap.getSession(resource);
1005
        }
1006 1007
        else if (!userManager.isRegisteredUser(username)) {
            session = anonymousSessions.get(resource);
1008 1009 1010 1011
        }
        return session;
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
1012 1013 1014 1015 1016 1017
    /**
     * Returns a list that contains all client sessions connected to the server. The list
     * contains sessions of anonymous and non-anonymous users.
     *
     * @return a list that contains all client sessions connected to the server.
     */
1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227
    public Collection<ClientSession> getSessions() {
        List<ClientSession> allSessions = new ArrayList<ClientSession>();
        copyUserSessions(allSessions);
        copyAnonSessions(allSessions);
        return allSessions;
    }


    public Collection<ClientSession> getSessions(SessionResultFilter filter) {
        List<ClientSession> results = new ArrayList<ClientSession>();
        if (filter != null) {
            // Grab all the possible matching sessions by user
            if (filter.getUsername() == null) {
                // No user id filtering
                copyAnonSessions(results);
                copyUserSessions(results);
            }
            else {
                try {
                    copyUserSessions(userManager.getUser(filter.getUsername()).getUsername(),
                            results);
                }
                catch (UserNotFoundException e) {
                    // Ignore.
                }
            }

            Date createMin = filter.getCreationDateRangeMin();
            Date createMax = filter.getCreationDateRangeMax();
            Date activityMin = filter.getLastActivityDateRangeMin();
            Date activityMax = filter.getLastActivityDateRangeMax();

            // Now we have a copy of the references so we can spend some time
            // doing the rest of the filtering without locking out session access
            // so let's iterate and filter each session one by one
            List<ClientSession> filteredResults = new ArrayList<ClientSession>();
            for (ClientSession session : results) {
                // Now filter on creation date if needed
                if (createMin != null || createMax != null) {
                    if (!isBetweenDates(session.getCreationDate(), createMin, createMax)) {
                        session = null;
                    }
                }
                // Now filter on activity date if needed
                if ((activityMin != null || activityMax != null) && session != null) {
                    if (!isBetweenDates(session.getLastActiveDate(), activityMin, activityMax)) {
                        session = null;
                    }
                }
                if (session != null) {
                    if (!isBetweenPacketCount(session.getNumClientPackets(),
                            filter.getClientPacketRangeMin(),
                            filter.getClientPacketRangeMax())) {
                        session = null;
                    }
                }
                if (session != null) {
                    if (!isBetweenPacketCount(session.getNumServerPackets(),
                            filter.getServerPacketRangeMin(),
                            filter.getServerPacketRangeMax())) {
                        session = null;
                    }
                }
                if (session != null) {
                    filteredResults.add(session);
                }
            }

            // Sort list.
            Collections.sort(filteredResults, filter.getSortComparator());

            int maxResults = filter.getNumResults();
            if (maxResults == SessionResultFilter.NO_RESULT_LIMIT) {
                maxResults = filteredResults.size();
            }

            // Now generate the final list. I believe it's faster to to build up a new
            // list than it is to remove items from head and tail of the sorted tree
            List<ClientSession> finalResults = new ArrayList<ClientSession>();
            int startIndex = filter.getStartIndex();
            Iterator<ClientSession> sortedIter = filteredResults.iterator();
            for (int i = 0; sortedIter.hasNext() && finalResults.size() < maxResults; i++) {
                ClientSession result = sortedIter.next();
                if (i >= startIndex) {
                    finalResults.add(result);
                }
            }
            return finalResults;
        }
        return results;
    }

    /**
     * Returns the list of sessions that were originated by a remote server. The list will be
     * ordered chronologically.  IncomingServerSession can only receive packets from the remote
     * server but are not capable of sending packets to the remote server.
     *
     * @param hostname the name of the remote server.
     * @return the sessions that were originated by a remote server.
     */
    public List<IncomingServerSession> getIncomingServerSessions(String hostname) {
        List<IncomingServerSession> sessions = incomingServerSessions.get(hostname);
        if (sessions == null) {
            return Collections.emptyList();
        }
        else {
            return Collections.unmodifiableList(sessions);
        }
    }

    /**
     * Returns a session that was originated from this server to a remote server.
     * OutgoingServerSession an only send packets to the remote server but are not capable of
     * receiving packets from the remote server.
     *
     * @param hostname the name of the remote server.
     * @return a session that was originated from this server to a remote server.
     */
    public OutgoingServerSession getOutgoingServerSession(String hostname) {
        return outgoingServerSessions.get(hostname);
    }

    /**
     * <p>Determines if the given date is before the min date, or after the max date.</p>
     * <p>The check is complicated somewhat by the fact that min can be null indicating
     * no earlier date, and max can be null indicating no upper limit.</p>
     *
     * @param date The date to check
     * @param min  The date must be after min, or any if min is null
     * @param max  The date must be before max, or any if max is null
     * @return True if the date is between min and max
     */
    private boolean isBetweenDates(Date date, Date min, Date max) {
        boolean between = true;
        if (min != null) {
            if (date.before(min)) {
                between = false;
            }
        }
        if (max != null && between) {
            if (date.after(max)) {
                between = false;
            }
        }
        return between;
    }

    /**
     * <p>Determines if the given count is before the min count, or after the max count.</p>
     * <p>The check is complicated somewhat by the fact that min or max
     * can be SessionResultFilter.NO_PACKET_LIMIT indicating no limit.</p>
     *
     * @param count The count to check
     * @param min   The count must be over min, or any if min is SessionResultFilter.NO_PACKET_LIMIT
     * @param max   The count must be under max, or any if max is SessionResultFilter.NO_PACKET_LIMIT
     * @return True if the count is between min and max
     */
    private boolean isBetweenPacketCount(long count, long min, long max) {
        boolean between = true;
        if (min != SessionResultFilter.NO_PACKET_LIMIT) {
            if (count < min) {
                between = false;
            }
        }
        if (max != SessionResultFilter.NO_PACKET_LIMIT && between) {
            if (count > max) {
                between = false;
            }
        }
        return between;
    }

    private void copyAnonSessions(List<ClientSession> sessions) {
        // Add anonymous sessions
        for (ClientSession session : anonymousSessions.values()) {
            sessions.add(session);
        }
    }

    private void copyUserSessions(List<ClientSession> sessions) {
        // Get a copy of the sessions from all users
        for (String username : getSessionUsers()) {
            for (ClientSession session : getSessions(username)) {
                sessions.add(session);
            }
        }
    }

    private void copyUserSessions(String username, List<ClientSession> sessionList) {
        // Get a copy of the sessions from all users
        SessionMap sessionMap = sessions.get(username);
        if (sessionMap != null) {
            for (ClientSession session : sessionMap.getSessions()) {
                sessionList.add(session);
            }
        }
    }

    public Iterator getAnonymousSessions() {
        return Arrays.asList(anonymousSessions.values().toArray()).iterator();
    }

    public Collection<ClientSession> getSessions(String username) {
        List<ClientSession> sessionList = new ArrayList<ClientSession>();
        if (username != null) {
            copyUserSessions(username, sessionList);
        }
        return sessionList;
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
1228 1229 1230 1231 1232 1233
    /**
     * Returns number of client sessions that are connected to the server. Anonymous users
     * are included too.
     *
     * @return number of client sessions that are connected to the server.
     */
1234
    public int getSessionCount() {
1235
        return usersSessionsCounter.get();
1236 1237
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253
    /**
     * Returns number of client sessions that are available. Anonymous users
     * are included too.
     *
     * @return number of client sessions that are available.
     */
    public int getActiveSessionCount() {
        int sessionCount = 0;
        for (ClientSession session : getSessions()) {
            if (session.getPresence().isAvailable()) {
                sessionCount++;
            }
        }
        return sessionCount;
    }

1254 1255 1256 1257 1258
    public int getAnonymousSessionCount() {
        return anonymousSessions.size();
    }

    public int getSessionCount(String username) {
1259
        if (username == null || !userManager.isRegisteredUser(username)) {
1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327
            return 0;
        }
        int sessionCount = 0;
        SessionMap sessionMap = sessions.get(username);
        if (sessionMap != null) {
            sessionCount = sessionMap.resources.size();
        }
        return sessionCount;
    }

    public Collection<String> getSessionUsers() {
        return Collections.unmodifiableCollection(sessions.keySet());
    }

    /**
     * Returns a collection with the established sessions from external components.
     *
     * @return a collection with the established sessions from external components.
     */
    public Collection<ComponentSession> getComponentSessions() {
        return Collections.unmodifiableCollection(componentsSessions);
    }

    /**
     * Returns the session of the component whose domain matches the specified domain.
     *
     * @param domain the domain of the component session to look for.
     * @return the session of the component whose domain matches the specified domain.
     */
    public ComponentSession getComponentSession(String domain) {
        for (ComponentSession session : componentsSessions) {
            if (domain.equals(session.getAddress().getDomain())) {
                return session;
            }
        }
        return null;
    }

    /**
     * Returns a collection with the hostnames of the remote servers that currently have an
     * incoming server connection to this server.
     *
     * @return a collection with the hostnames of the remote servers that currently have an
     *         incoming server connection to this server.
     */
    public Collection<String> getIncomingServers() {
        return Collections.unmodifiableCollection(incomingServerSessions.keySet());
    }

    /**
     * Returns a collection with the hostnames of the remote servers that currently may receive
     * packets sent from this server.
     *
     * @return a collection with the hostnames of the remote servers that currently may receive
     *         packets sent from this server.
     */
    public Collection<String> getOutgoingServers() {
        return Collections.unmodifiableCollection(outgoingServerSessions.keySet());
    }

    /**
     * Broadcasts the given data to all connected sessions. Excellent
     * for server administration messages.
     *
     * @param packet The packet to be broadcast
     */
    public void broadcast(Packet packet) throws UnauthorizedException {
        for (SessionMap sessionMap : sessions.values()) {
1328
            sessionMap.broadcast(packet);
1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360
        }

        for (Session session : anonymousSessions.values()) {
            session.process(packet);
        }
    }

    /**
     * Broadcasts the given data to all connected sessions for a particular
     * user. Excellent for updating all connected resources for users such as
     * roster pushes.
     *
     * @param packet The packet to be broadcast
     */
    public void userBroadcast(String username, Packet packet) throws UnauthorizedException, PacketException {
        SessionMap sessionMap = sessions.get(username);
        if (sessionMap != null) {
            sessionMap.broadcast(packet);
        }
    }

    /**
     * Removes a session.
     *
     * @param session the session.
     */
    public void removeSession(ClientSession session) {
        // Do nothing if session is null or if the server is shutting down. Note: When the server
        // is shutting down the serverName will be null.
        if (session == null || serverName == null) {
            return;
        }
1361
        if (anonymousSessions.remove(session.getAddress().getResource()) != null) {
1362 1363 1364 1365 1366 1367
            // Fire session event.
            SessionEventDispatcher.dispatchEvent(session,
                    SessionEventDispatcher.EventType.anonymous_session_destroyed);
        }
        else {
            // If this is a non-anonymous session then remove the session from the SessionMap
1368
            String username = session.getAddress().getNode();
1369 1370 1371 1372
            if (username != null) {
                SessionMap sessionMap = sessions.get(username);
                if (sessionMap != null) {
                    synchronized (username.intern()) {
1373
                        sessionMap.removeSession(session);
1374 1375 1376
                    }
                    if (sessionMap.isEmpty()) {
                        sessions.remove(username);
1377 1378
                    }
                }
1379 1380 1381 1382 1383
                if (sessionMap != null) {
                    // Fire session event.
                    SessionEventDispatcher.dispatchEvent(session,
                            SessionEventDispatcher.EventType.session_destroyed);
                }
1384 1385
            }
        }
1386 1387
        // Remove the session from the pre-Authenticated sessions list (if present)
        preAuthenticatedSessions.remove(session.getAddress().getResource());
1388 1389
        // If the user is still available then send an unavailable presence
        Presence presence = session.getPresence();
1390
        if (presence.isAvailable()) {
1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401
            Presence offline = new Presence();
            offline.setFrom(session.getAddress());
            offline.setTo(new JID(null, serverName, null));
            offline.setType(Presence.Type.unavailable);
            router.route(offline);
        }
    }

    public void addAnonymousSession(ClientSession session) {
        anonymousSessions.put(session.getAddress().getResource(), session);
        // Remove the session from the pre-Authenticated sessions list
1402
        preAuthenticatedSessions.remove(session.getAddress().getResource());
1403

1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450
        // Fire session event.
        SessionEventDispatcher.dispatchEvent(session,
                SessionEventDispatcher.EventType.anonymous_session_created);
    }

    public int getConflictKickLimit() {
        return conflictLimit;
    }

    /**
     * Returns the temporary keys used by the sessions that has not been authenticated yet. This
     * is an utility method useful for debugging situations.
     *
     * @return the temporary keys used by the sessions that has not been authenticated yet.
     */
    public Collection<String> getPreAuthenticatedKeys() {
        return preAuthenticatedSessions.keySet();
    }

    public void setConflictKickLimit(int limit) {
        conflictLimit = limit;
        JiveGlobals.setProperty("xmpp.session.conflict-limit", Integer.toString(conflictLimit));
    }

    private class ClientSessionListener implements ConnectionCloseListener {
        /**
         * Handle a session that just closed.
         *
         * @param handback The session that just closed
         */
        public void onConnectionClose(Object handback) {
            try {
                ClientSession session = (ClientSession)handback;
                try {
                    if (session.getPresence().isAvailable() || !session.wasAvailable()) {
                        // Send an unavailable presence to the user's subscribers
                        // Note: This gives us a chance to send an unavailable presence to the
                        // entities that the user sent directed presences
                        Presence presence = new Presence();
                        presence.setType(Presence.Type.unavailable);
                        presence.setFrom(session.getAddress());
                        presenceHandler.process(presence);
                    }
                }
                finally {
                    // Remove the session
                    removeSession(session);
1451 1452
                    // Decrement the counter of user sessions
                    usersSessionsCounter.decrementAndGet();
1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470
                }
            }
            catch (Exception e) {
                // Can't do anything about this problem...
                Log.error(LocaleUtils.getLocalizedString("admin.error.close"), e);
            }
        }
    }

    private class ComponentSessionListener implements ConnectionCloseListener {
        /**
         * Handle a session that just closed.
         *
         * @param handback The session that just closed
         */
        public void onConnectionClose(Object handback) {
            ComponentSession session = (ComponentSession)handback;
            try {
1471 1472 1473 1474 1475
                // Unbind registered domains for this external component
                for (String domain : session.getExternalComponent().getSubdomains()) {
                    String subdomain = domain.substring(0, domain.indexOf(serverName) - 1);
                    InternalComponentManager.getInstance().removeComponent(subdomain);
                }
1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519
            }
            catch (Exception e) {
                // Can't do anything about this problem...
                Log.error(LocaleUtils.getLocalizedString("admin.error.close"), e);
            }
            finally {
                // Remove the session
                componentsSessions.remove(session);
            }
        }
    }

    private class IncomingServerSessionListener implements ConnectionCloseListener {
        /**
         * Handle a session that just closed.
         *
         * @param handback The session that just closed
         */
        public void onConnectionClose(Object handback) {
            IncomingServerSession session = (IncomingServerSession)handback;
            // Remove all the hostnames that were registered for this server session
            for (String hostname : session.getValidatedDomains()) {
                unregisterIncomingServerSession(hostname, session);
            }
        }
    }

    private class OutgoingServerSessionListener implements ConnectionCloseListener {
        /**
         * Handle a session that just closed.
         *
         * @param handback The session that just closed
         */
        public void onConnectionClose(Object handback) {
            OutgoingServerSession session = (OutgoingServerSession)handback;
            // Remove all the hostnames that were registered for this server session
            for (String hostname : session.getHostnames()) {
                unregisterOutgoingServerSession(hostname);
                // Remove the route to the session using the hostname
                XMPPServer.getInstance().getRoutingTable().removeRoute(new JID(hostname));
            }
        }
    }

1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535
    private class ConnectionMultiplexerSessionListener implements ConnectionCloseListener {
        /**
         * Handle a session that just closed.
         *
         * @param handback The session that just closed
         */
        public void onConnectionClose(Object handback) {
            ConnectionMultiplexerSession session = (ConnectionMultiplexerSession)handback;
            // Remove all the hostnames that were registered for this server session
            String domain = session.getAddress().getDomain();
            List<ConnectionMultiplexerSession> sessions = connnectionManagerSessions.get(domain);
            sessions.remove(session);
            if (sessions.isEmpty()) {
                connnectionManagerSessions.remove(domain);
                // Terminate ClientSessions originated from this connection manager
                // that are still active since the connection manager has gone down
1536
                ConnectionMultiplexerManager.getInstance().multiplexerUnavailable(domain);
1537 1538 1539 1540
            }
        }
    }

1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602
    public void initialize(XMPPServer server) {
        super.initialize(server);
        presenceHandler = server.getPresenceUpdateHandler();
        router = server.getPacketRouter();
        userManager = server.getUserManager();
        routingTable = server.getRoutingTable();
        serverName = server.getServerInfo().getName();
        serverAddress = new JID(serverName);

        if (JiveGlobals.getBooleanProperty("xmpp.audit.active")) {
            streamIDFactory = new AuditStreamIDFactory();
        }
        else {
            streamIDFactory = new BasicStreamIDFactory();
        }

        String conflictLimitProp = JiveGlobals.getProperty("xmpp.session.conflict-limit");
        if (conflictLimitProp == null) {
            conflictLimit = 0;
            JiveGlobals.setProperty("xmpp.session.conflict-limit", Integer.toString(conflictLimit));
        }
        else {
            try {
                conflictLimit = Integer.parseInt(conflictLimitProp);
            }
            catch (NumberFormatException e) {
                conflictLimit = 0;
                JiveGlobals.setProperty("xmpp.session.conflict-limit", Integer.toString(conflictLimit));
            }
        }
        // Run through the server sessions every 5 minutes after a 5 minutes server
        // startup delay (default values)
        serverCleanupTask = new ServerCleanupTask();
        timer.schedule(serverCleanupTask, getServerSessionTimeout(), getServerSessionTimeout());
    }


    /**
     * Sends a message with a given subject and body to all the active user sessions in the server.
     *
     * @param subject the subject to broadcast.
     * @param body    the body to broadcast.
     */
    public void sendServerMessage(String subject, String body) {
        sendServerMessage(null, subject, body);
    }

    /**
     * Sends a message with a given subject and body to one or more user sessions related to the
     * specified address. If address is null or the address's node is null then the message will be
     * sent to all the user sessions. But if the address includes a node but no resource then
     * the message will be sent to all the user sessions of the requeted user (defined by the node).
     * Finally, if the address is a full JID then the message will be sent to the session associated
     * to the full JID. If no session is found then the message is not sent.
     *
     * @param address the address that defines the sessions that will receive the message.
     * @param subject the subject to broadcast.
     * @param body    the body to broadcast.
     */
    public void sendServerMessage(JID address, String subject, String body) {
        Message packet = createServerMessage(subject, body);
        try {
1603
            if (address == null || address.getNode() == null ||
1604
                    !userManager.isRegisteredUser(address)) {
1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640
                broadcast(packet);
            }
            else if (address.getResource() == null || address.getResource().length() < 1) {
                userBroadcast(address.getNode(), packet);
            }
            else {
                ClientSession session = getSession(address);
                if (session != null) {
                    session.process(packet);
                }
            }
        }
        catch (UnauthorizedException e) {
            // Ignore.
        }
    }

    private Message createServerMessage(String subject, String body) {
        Message message = new Message();
        message.setFrom(serverAddress);
        if (subject != null) {
            message.setSubject(subject);
        }
        message.setBody(body);
        return message;
    }

    public void stop() {
        Log.debug("Stopping server");
        // Stop threads that are sending packets to remote servers
        OutgoingSessionPromise.getInstance().shutdown();
        timer.cancel();
        if (JiveGlobals.getBooleanProperty("shutdownMessage.enabled")) {
            sendServerMessage(null, LocaleUtils.getLocalizedString("admin.shutdown.now"));
        }
        try {
1641 1642 1643 1644 1645 1646 1647 1648
            // Send the close stream header to all connected connections
            Set<Session> sessions = new HashSet<Session>();
            sessions.addAll(getSessions());
            sessions.addAll(getComponentSessions());
            sessions.addAll(outgoingServerSessions.values());
            for (List<IncomingServerSession> incomingSessions : incomingServerSessions.values()) {
                sessions.addAll(incomingSessions);
            }
1649 1650 1651 1652
            for (List<ConnectionMultiplexerSession> multiplexers : connnectionManagerSessions
                    .values()) {
                sessions.addAll(multiplexers);
            }
1653 1654

            for (Session session : sessions) {
1655
                try {
1656 1657
                    // Notify connected client that the server is being shut down
                    session.getConnection().systemShutdown();
1658 1659 1660 1661 1662 1663 1664 1665 1666
                }
                catch (Throwable t) {
                    // Ignore.
                }
            }
        }
        catch (Exception e) {
            // Ignore.
        }
1667
        serverName = null;
1668 1669
    }

1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706
    /**
     * Returns true if remote servers are allowed to have more than one connection to this
     * server. Having more than one connection may improve number of packets that can be
     * transfered per second. This setting only used by the server dialback mehod.<p>
     *
     * It is highly recommended that {@link #getServerSessionTimeout()} is enabled so that
     * dead connections to this server can be easily discarded.
     *
     * @return true if remote servers are allowed to have more than one connection to this
     *         server.
     */
    public boolean isMultipleServerConnectionsAllowed() {
        return JiveGlobals.getBooleanProperty("xmpp.server.session.allowmultiple", true);
    }

    /**
     * Sets if remote servers are allowed to have more than one connection to this
     * server. Having more than one connection may improve number of packets that can be
     * transfered per second. This setting only used by the server dialback mehod.<p>
     *
     * It is highly recommended that {@link #getServerSessionTimeout()} is enabled so that
     * dead connections to this server can be easily discarded.
     *
     * @param allowed true if remote servers are allowed to have more than one connection to this
     *        server.
     */
    public void setMultipleServerConnectionsAllowed(boolean allowed) {
        JiveGlobals.setProperty("xmpp.server.session.allowmultiple", Boolean.toString(allowed));
        if (allowed && JiveGlobals.getIntProperty("xmpp.server.session.idle", 10 * 60 * 1000) <= 0)
        {
            Log.warn("Allowing multiple S2S connections for each domain, without setting a " +
                    "maximum idle timeout for these connections, is unrecommended! Either " +
                    "set xmpp.server.session.allowmultiple to 'false' or change " +
                    "xmpp.server.session.idle to a (large) positive value.");
        }
    }

1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744
    /******************************************************
     * Clean up code
     *****************************************************/
    /**
     * Sets the number of milliseconds to elapse between clearing of idle server sessions.
     *
     * @param timeout the number of milliseconds to elapse between clearings.
     */
    public void setServerSessionTimeout(int timeout) {
        if (getServerSessionTimeout() == timeout) {
            return;
        }
        // Cancel the existing task because the timeout has changed
        if (serverCleanupTask != null) {
            serverCleanupTask.cancel();
        }
        // Create a new task and schedule it with the new timeout
        serverCleanupTask = new ServerCleanupTask();
        timer.schedule(serverCleanupTask, getServerSessionTimeout(), getServerSessionTimeout());
        // Set the new property value
        JiveGlobals.setProperty("xmpp.server.session.timeout", Integer.toString(timeout));
    }

    /**
     * Returns the number of milliseconds to elapse between clearing of idle server sessions.
     *
     * @return the number of milliseconds to elapse between clearing of idle server sessions.
     */
    public int getServerSessionTimeout() {
        return JiveGlobals.getIntProperty("xmpp.server.session.timeout", 5 * 60 * 1000);
    }

    public void setServerSessionIdleTime(int idleTime) {
        if (getServerSessionIdleTime() == idleTime) {
            return;
        }
        // Set the new property value
        JiveGlobals.setProperty("xmpp.server.session.idle", Integer.toString(idleTime));
1745

1746 1747 1748 1749 1750 1751 1752
        if (idleTime <= 0 && isMultipleServerConnectionsAllowed() )
        {
            Log.warn("Allowing multiple S2S connections for each domain, without setting a " +
                "maximum idle timeout for these connections, is unrecommended! Either " +
                "set xmpp.server.session.allowmultiple to 'false' or change " +
                "xmpp.server.session.idle to a (large) positive value.");
        }
1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798
    }

    public int getServerSessionIdleTime() {
        return JiveGlobals.getIntProperty("xmpp.server.session.idle", 10 * 60 * 1000);
    }

    /**
     * Task that closes the idle server sessions.
     */
    private class ServerCleanupTask extends TimerTask {
        /**
         * Close outgoing server sessions that have been idle for a long time.
         */
        public void run() {
            // Do nothing if this feature is disabled
            if (getServerSessionIdleTime() == -1) {
                return;
            }
            final long deadline = System.currentTimeMillis() - getServerSessionIdleTime();
            // Check outgoing server sessions
            for (OutgoingServerSession session : outgoingServerSessions.values()) {
                try {
                    if (session.getLastActiveDate().getTime() < deadline) {
                        session.getConnection().close();
                    }
                }
                catch (Throwable e) {
                    Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
                }
            }
            // Check incoming server sessions
            for (List<IncomingServerSession> sessions : incomingServerSessions.values()) {
                for (IncomingServerSession session : sessions) {
                    try {
                        if (session.getLastActiveDate().getTime() < deadline) {
                            session.getConnection().close();
                        }
                    }
                    catch (Throwable e) {
                        Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
                    }
                }
            }
        }
    }
}