/**
 * $RCSfile$
 * $Revision: 3187 $
 * $Date: 2005-12-11 13:34:34 -0300 (Sun, 11 Dec 2005) $
 *
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.openfire.session;

import java.net.UnknownHostException;
import java.util.*;

import org.jivesoftware.openfire.Connection;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.auth.AuthToken;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.openfire.cluster.ClusterManager;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.privacy.PrivacyList;
import org.jivesoftware.openfire.privacy.PrivacyListManager;
import org.jivesoftware.openfire.spi.ConnectionConfiguration;
import org.jivesoftware.openfire.streammanagement.StreamManager;
import org.jivesoftware.openfire.user.PresenceEventDispatcher;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmpp.packet.JID;
import org.xmpp.packet.Packet;
import org.xmpp.packet.Presence;
import org.xmpp.packet.StreamError;

/**
 * Represents a session between the server and a client.
 *
 * @author Gaston Dombiak
 */
public class LocalClientSession extends LocalSession implements ClientSession {

	private static final Logger Log = LoggerFactory.getLogger(LocalClientSession.class);

    private static final String ETHERX_NAMESPACE = "http://etherx.jabber.org/streams";
    private static final String FLASH_NAMESPACE = "http://www.jabber.com/streams/flash";

    /**
     * Keep the list of IP address that are allowed to connect to the server.
     *
     * If the list is  empty then anyone is allowed to connect to the server, unless the IP is on the blacklist (which
     * always takes precedence over the whitelist).
     *
     * Note: the values in this list can be hostnames, IP addresses or IP ranges (with wildcards).
     */
    private static Set<String> allowedIPs = new HashSet<>();
    private static Set<String> allowedAnonymIPs = new HashSet<>();

    /**
     * Similar to {@link #allowedIPs}, but used for blacklisting rather than whitelisting.
     */
    private static Set<String> blockedIPs = new HashSet<>();

    private boolean messageCarbonsEnabled;

    /**
     * The authentication token for this session.
     */
    protected AuthToken authToken;

    /**
     * Flag indicating if this session has been initialized yet (upon first available transition).
     */
    private boolean initialized;

    /**
     * Flag that indicates if the session was available ever.
     */
    private boolean wasAvailable = false;

    /**
     * Flag indicating if the user requested to not receive offline messages when sending
     * an available presence. The user may send a disco request with node
     * "http://jabber.org/protocol/offline" so that no offline messages are sent to the
     * user when he becomes online. If the user is connected from many resources then
     * if one of the sessions stopped the flooding then no session should flood the user.
     */
    private boolean offlineFloodStopped = false;

    private Presence presence = null;

    private int conflictCount = 0;

    /**
     * Privacy list that overrides the default privacy list. This list affects only this
     * session and only for the duration of the session.
     */
    private String activeList;
    /**
     * Default privacy list used for the session's user. This list is processed if there
     * is no active list set for the session.
     */
    private String defaultList;

    static {
        // Fill out the allowedIPs with the system property
        String allowed = JiveGlobals.getProperty(ConnectionSettings.Client.LOGIN_ALLOWED, "");
        StringTokenizer tokens = new StringTokenizer(allowed, ", ");
        while (tokens.hasMoreTokens()) {
            String address = tokens.nextToken().trim();
            allowedIPs.add( address );
        }
        String allowedAnonym = JiveGlobals.getProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED, "");
        tokens = new StringTokenizer(allowedAnonym, ", ");
        while (tokens.hasMoreTokens()) {
            String address = tokens.nextToken().trim();
            allowedAnonymIPs.add(address);
        }
        String blocked = JiveGlobals.getProperty(ConnectionSettings.Client.LOGIN_BLOCKED, "");
        tokens = new StringTokenizer(blocked, ", ");
        while (tokens.hasMoreTokens()) {
            String address = tokens.nextToken().trim();
            blockedIPs.add( address );
        }
    }

    /**
     * Returns the list of IP address that are allowed to connect to the server. If the list is
     * empty then anyone is allowed to connect to the server except for anonymous users that are
     * subject to {@link #getAllowedAnonymIPs()}. This list is used for both anonymous and
     * non-anonymous users.
     *
     * Note that the blacklist in {@link #getBlacklistedIPs()} should take precedence!
     *
     * @return the list of IP address that are allowed to connect to the server.
     * @deprecated Use #getWhitelistedIPs instead.
     */
    @Deprecated
    public static Map<String, String> getAllowedIPs()
    {
        final Map<String, String> result = new HashMap<>();
        for ( String item : allowedIPs )
        {
            result.put( item, null );
        }
        return result;
    }

    /**
     * Returns the list of IP address that are allowed to connect to the server. If the list is empty then anyone is
     * allowed to connect to the server except for anonymous users that are subject to
     * {@link #getWhitelistedAnonymousIPs()}. This list is used for both anonymous and non-anonymous users.
     *
     * Note that the blacklist in {@link #getBlacklistedIPs()} should take precedence!
     *
     * @return the collection of IP address that are allowed to connect to the server. Never null, possibly empty.
     */
    public static Set<String> getWhitelistedIPs() { return allowedIPs; }

    /**
     * Returns the list of IP address that are allowed to connect to the server for anonymous
     * users. If the list is empty then anonymous will be only restricted by {@link #getAllowedIPs()}.
     *
     * Note that the blacklist in {@link #getBlacklistedIPs()} should take precedence!
     *
     * @return the list of IP address that are allowed to connect to the server.
     * @deprecated Use #getWhitelistedAnonymousIPs instead.
     */
    public static Map<String, String> getAllowedAnonymIPs()
    {
        final Map<String, String> result = new HashMap<>();
        for ( String item : allowedAnonymIPs )
        {
            result.put( item, null );
        }
        return result;
    }

    /**
     * Returns the list of IP address that are allowed to connect to the server for anonymous users. If the list is
     * empty then anonymous will be only restricted by {@link #getWhitelistedIPs()}.
     *
     * Note that the blacklist in {@link #getBlacklistedIPs()} should take precedence!
     *
     * @return the collection of IP address that are allowed to connect to the server. Never null, possibly empty.
     */
    public static Set<String> getWhitelistedAnonymousIPs() {
        return allowedAnonymIPs;
    }

    /**
     * Returns the list of IP address that are disallowed to connect to the server. If the list is empty then anyone is
     * allowed to connect to the server, subject to whitelisting. This list is used for both anonymous and
     * non-anonymous users.
     *
     * @return the collection of IP address that are not allowed to connect to the server. Never null, possibly empty.
     */
    public static Set<String> getBlacklistedIPs() { return blockedIPs; }

    /**
     * Returns a newly created session between the server and a client. The session will
     * be created and returned only if correct name/prefix (i.e. 'stream' or 'flash')
     * and namespace were provided by the client.
     *
     * @param serverName the name of the server where the session is connecting to.
     * @param xpp the parser that is reading the provided XML through the connection.
     * @param connection the connection with the client.
     * @return a newly created session between the server and a client.
     * @throws org.xmlpull.v1.XmlPullParserException if an error occurs while parsing incoming data.
     */
    public static LocalClientSession createSession(String serverName, XmlPullParser xpp, Connection connection)
            throws XmlPullParserException {
        boolean isFlashClient = xpp.getPrefix().equals("flash");
        connection.setFlashClient(isFlashClient);

        // Conduct error checking, the opening tag should be 'stream'
        // in the 'etherx' namespace
        if (!xpp.getName().equals("stream") && !isFlashClient) {
            throw new XmlPullParserException(
                    LocaleUtils.getLocalizedString("admin.error.bad-stream"));
        }

        if (!xpp.getNamespace(xpp.getPrefix()).equals(ETHERX_NAMESPACE) &&
                !(isFlashClient && xpp.getNamespace(xpp.getPrefix()).equals(FLASH_NAMESPACE)))
        {
            throw new XmlPullParserException(LocaleUtils.getLocalizedString(
                    "admin.error.bad-namespace"));
        }

        if (!isAllowed(connection))
        {
            // Client cannot connect from this IP address so end the stream and TCP connection.
            String hostAddress = "Unknown";
            try {
                hostAddress = connection.getHostAddress();
            } catch (UnknownHostException e) {
                // Do nothing
            }

            Log.debug("LocalClientSession: Closed connection to client attempting to connect from: " + hostAddress);
            // Include the not-authorized error in the response
            StreamError error = new StreamError(StreamError.Condition.not_authorized);
            connection.deliverRawText(error.toXML());
            // Close the underlying connection
            connection.close();
            return null;
        }

        // Default language is English ("en").
        Locale language = Locale.forLanguageTag("en");
        // Default to a version of "0.0". Clients written before the XMPP 1.0 spec may
        // not report a version in which case "0.0" should be assumed (per rfc3920
        // section 4.4.1).
        int majorVersion = 0;
        int minorVersion = 0;
        for (int i = 0; i < xpp.getAttributeCount(); i++) {
            if ("lang".equals(xpp.getAttributeName(i))) {
                language = Locale.forLanguageTag(xpp.getAttributeValue(i));
            }
            if ("version".equals(xpp.getAttributeName(i))) {
                try {
                    int[] version = decodeVersion(xpp.getAttributeValue(i));
                    majorVersion = version[0];
                    minorVersion = version[1];
                }
                catch (Exception e) {
                    Log.error(e.getMessage(), e);
                }
            }
        }

        // If the client supports a greater major version than the server,
        // set the version to the highest one the server supports.
        if (majorVersion > MAJOR_VERSION) {
            majorVersion = MAJOR_VERSION;
            minorVersion = MINOR_VERSION;
        }
        else if (majorVersion == MAJOR_VERSION) {
            // If the client supports a greater minor version than the
            // server, set the version to the highest one that the server
            // supports.
            if (minorVersion > MINOR_VERSION) {
                minorVersion = MINOR_VERSION;
            }
        }

        connection.setXMPPVersion(majorVersion, minorVersion);
        final ConnectionConfiguration connectionConfiguration = connection.getConfiguration();

        // Indicate the TLS policy to use for this connection
        if (!connection.isSecure()) {
            boolean hasCertificates = false;
            try {
                hasCertificates = connectionConfiguration.getIdentityStore().getAllCertificates().size() > 0;
            }
            catch (Exception e) {
                Log.error(e.getMessage(), e);
            }
            Connection.TLSPolicy tlsPolicy = connectionConfiguration.getTlsPolicy();
            if (Connection.TLSPolicy.required == tlsPolicy && !hasCertificates) {
                Log.error("Client session rejected. TLS is required but no certificates " +
                        "were created.");
                return null;
            }
            // Set default TLS policy
            connection.setTlsPolicy(hasCertificates ? tlsPolicy : Connection.TLSPolicy.disabled);
        } else {
            // Set default TLS policy
            connection.setTlsPolicy(Connection.TLSPolicy.disabled);
        }

        // Indicate the compression policy to use for this connection
        connection.setCompressionPolicy( connectionConfiguration.getCompressionPolicy() );

        // Create a ClientSession for this user.
        LocalClientSession session = SessionManager.getInstance().createClientSession(connection, language);

        // Build the start packet response
        StringBuilder sb = new StringBuilder(200);
        sb.append("<?xml version='1.0' encoding='");
        sb.append(CHARSET);
        sb.append("'?>");
        if (isFlashClient) {
            sb.append("<flash:stream xmlns:flash=\"http://www.jabber.com/streams/flash\" ");
        }
        else {
            sb.append("<stream:stream ");
        }
        sb.append("xmlns:stream=\"http://etherx.jabber.org/streams\" xmlns=\"jabber:client\" from=\"");
        sb.append(serverName);
        sb.append("\" id=\"");
        sb.append(session.getStreamID().toString());
        sb.append("\" xml:lang=\"");
        sb.append(language);
        // Don't include version info if the version is 0.0.
        if (majorVersion != 0) {
            sb.append("\" version=\"");
            sb.append(majorVersion).append('.').append(minorVersion);
        }
        sb.append("\">");
        connection.deliverRawText(sb.toString());

        // If this is a "Jabber" connection, the session is now initialized and we can
        // return to allow normal packet parsing.
        if (majorVersion == 0) {
            return session;
        }
        // Otherwise, this is at least XMPP 1.0 so we need to announce stream features.

        sb = new StringBuilder(490);
        sb.append("<stream:features>");
        if (connection.getTlsPolicy() != Connection.TLSPolicy.disabled) {
            sb.append("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\">");
            if (connection.getTlsPolicy() == Connection.TLSPolicy.required) {
                sb.append("<required/>");
            }
            sb.append("</starttls>");
        }
        // Include available SASL Mechanisms
        sb.append(SASLAuthentication.getSASLMechanisms(session));
        // Include Stream features
        String specificFeatures = session.getAvailableStreamFeatures();
        if (specificFeatures != null) {
            sb.append(specificFeatures);
        }
        sb.append("</stream:features>");

        connection.deliverRawText(sb.toString());
        return session;
    }

    public static boolean isAllowed( Connection connection )
    {
        try
        {
            final String hostAddress = connection.getHostAddress();
            final byte[] address = connection.getAddress();

            // Blacklist takes precedence over whitelist.
            if ( blockedIPs.contains( hostAddress ) || isAddressInRange( address, blockedIPs ) ) {
                return false;
            }

            // When there's a whitelist (not empty), you must be on it to be allowed.
            return allowedIPs.isEmpty() || allowedIPs.contains( hostAddress ) || isAddressInRange( address, allowedIPs );
        }
        catch ( UnknownHostException e )
        {
            return false;
        }
    }

    public static boolean isAllowedAnonymous( Connection connection )
    {
        try
        {
            final String hostAddress = connection.getHostAddress();
            final byte[] address = connection.getAddress();

            // Blacklist takes precedence over whitelist.
            if ( blockedIPs.contains( hostAddress ) || isAddressInRange( address, blockedIPs ) ) {
                return false;
            }

            // When there's a whitelist (not empty), you must be on it to be allowed.
            return allowedAnonymIPs.isEmpty() || allowedAnonymIPs.contains( hostAddress ) || isAddressInRange( address, allowedAnonymIPs );
        }
        catch ( UnknownHostException e )
        {
            return false;
        }
    }

    // TODO Add IPv6 support
    public static boolean isAddressInRange( byte[] address, Set<String> ranges ) {
        final String range0 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." + (address[2] & 0xff) + "." + (address[3] & 0xff);
        final String range1 = (address[0] & 0xff) + "." + (address[1] & 0xff) + "." + (address[2] & 0xff) + ".*";
        final String range2 = (address[0] & 0xff) + "." + (address[1] & 0xff) + ".*.*";
        final String range3 = (address[0] & 0xff) + ".*.*.*";
        return ranges.contains(range0) || ranges.contains(range1) || ranges.contains(range2) || ranges.contains(range3);
    }

    /**
     * Sets the list of IP address that are allowed to connect to the server. If the list is
     * empty then anyone is allowed to connect to the server except for anonymous users that are
     * subject to {@link #getAllowedAnonymIPs()}. This list is used for both anonymous and
     * non-anonymous users.
     *
     * @param allowed the list of IP address that are allowed to connect to the server.
     * @deprecated Use setWhitelistedIPs instead.
     */
    @Deprecated
    public static void setAllowedIPs(Map<String, String> allowed) {
        setWhitelistedIPs( allowed.keySet() );
    }

    /**
     * Sets the list of IP address that are allowed to connect to the server. If the list is empty then anyone not on
     * {@link #getBlacklistedIPs()} is  allowed to connect to the server except for anonymous users that are subject to
     * {@link #getWhitelistedAnonymousIPs()}. This list is used for both anonymous and non-anonymous users.
     *
     * Note that blacklisting takes precedence over whitelisting: if an address is matched by both, access is denied.
     *
     * @param allowed the list of IP address that are allowed to connect to the server. Can be empty, but not null.
     */
    public static void setWhitelistedIPs(Set<String> allowed) {
        if (allowed == null) {
            throw new NullPointerException();
        }
        allowedIPs = allowed;
        if (allowedIPs.isEmpty()) {
            JiveGlobals.deleteProperty(ConnectionSettings.Client.LOGIN_ALLOWED);
        }
        else {
            // Iterate through the elements in the map.
            StringBuilder buf = new StringBuilder();
            Iterator<String> iter = allowedIPs.iterator();
            if (iter.hasNext()) {
                buf.append(iter.next());
            }
            while (iter.hasNext()) {
                buf.append(", ").append(iter.next());
            }
            JiveGlobals.setProperty(ConnectionSettings.Client.LOGIN_ALLOWED, buf.toString());
        }
    }

    /**
     * Sets the list of IP address that are allowed to connect to the server for anonymous
     * users. If the list is empty then anonymous will be only restricted by {@link #getAllowedIPs()}.
     *
     * @param allowed the list of IP address that are allowed to connect to the server.
     * @deprecated use #setWhitelistedAnonymousIPs instead.
     */
    @Deprecated
    public static void setAllowedAnonymIPs(Map<String, String> allowed) {
        setWhitelistedAnonymousIPs( allowed.keySet() );
    }

    /**
     * Sets the list of IP address that are allowed to connect to the server for anonymous users. If the list is empty
     * then anonymous will be only restricted by {@link #getBlacklistedIPs()} and {@link #getWhitelistedIPs()}.
     *
     * @param allowed the list of IP address that are allowed to connect to the server. Can be empty, but not null.
     */
    public static void setWhitelistedAnonymousIPs(Set<String> allowed) {
        if (allowed == null) {
            throw new NullPointerException();
        }
        allowedAnonymIPs = allowed;
        if (allowedAnonymIPs.isEmpty()) {
            JiveGlobals.deleteProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED);
        }
        else {
            // Iterate through the elements in the map.
            StringBuilder buf = new StringBuilder();
            Iterator<String> iter = allowedAnonymIPs.iterator();
            if (iter.hasNext()) {
                buf.append(iter.next());
            }
            while (iter.hasNext()) {
                buf.append(", ").append(iter.next());
            }
            JiveGlobals.setProperty(ConnectionSettings.Client.LOGIN_ANONYM_ALLOWED, buf.toString());
        }
    }

    /**
     * Sets the list of IP address that are not allowed to connect to the server. This list is used for both anonymous
     * and non-anonymous users, and always takes precedence over a whitelist.
     *
     * @param blocked the list of IP address that are not allowed to connect to the server. Can be empty, but not null.
     */
    public static void setBlacklistedIPs(Set<String> blocked) {
        if (blocked == null) {
            throw new NullPointerException();
        }
        blockedIPs = blocked;
        if (blockedIPs.isEmpty()) {
            JiveGlobals.deleteProperty(ConnectionSettings.Client.LOGIN_BLOCKED);
        }
        else {
            // Iterate through the elements in the map.
            StringBuilder buf = new StringBuilder();
            Iterator<String> iter = blocked.iterator();
            if (iter.hasNext()) {
                buf.append(iter.next());
            }
            while (iter.hasNext()) {
                buf.append(", ").append(iter.next());
            }
            JiveGlobals.setProperty(ConnectionSettings.Client.LOGIN_BLOCKED, buf.toString());
        }
    }

    /**
     * Returns the Privacy list that overrides the default privacy list. This list affects
     * only this session and only for the duration of the session.
     *
     * @return the Privacy list that overrides the default privacy list.
     */
    @Override
    public PrivacyList getActiveList() {
        if (activeList != null) {
            try {
                return PrivacyListManager.getInstance().getPrivacyList(getUsername(), activeList);
            } catch (UserNotFoundException e) {
                Log.error(e.getMessage(), e);
            }
        }
        return null;
    }

    /**
     * Sets the Privacy list that overrides the default privacy list. This list affects
     * only this session and only for the duration of the session.
     *
     * @param activeList the Privacy list that overrides the default privacy list.
     */
    @Override
    public void setActiveList(PrivacyList activeList) {
        this.activeList = activeList != null ? activeList.getName() : null;
        if (ClusterManager.isClusteringStarted()) {
            // Track information about the session and share it with other cluster nodes
            Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
            cache.put(getAddress().toString(), new ClientSessionInfo(this));
        }
    }

    /**
     * Returns the default Privacy list used for the session's user. This list is
     * processed if there is no active list set for the session.
     *
     * @return the default Privacy list used for the session's user.
     */
    @Override
    public PrivacyList getDefaultList() {
        if (defaultList != null) {
            try {
                return PrivacyListManager.getInstance().getPrivacyList(getUsername(), defaultList);
            } catch (UserNotFoundException e) {
                Log.error(e.getMessage(), e);
            }
        }
        return null;
    }

    /**
     * Sets the default Privacy list used for the session's user. This list is
     * processed if there is no active list set for the session.
     *
     * @param defaultList the default Privacy list used for the session's user.
     */
    @Override
    public void setDefaultList(PrivacyList defaultList) {
        // Do nothing if nothing has changed
        if ((this.defaultList == null && defaultList == null) ||
                (defaultList != null && defaultList.getName().equals(this.defaultList))) {
            return;
        }
        this.defaultList = defaultList != null ? defaultList.getName() : null;
        if (ClusterManager.isClusteringStarted()) {
            // Track information about the session and share it with other cluster nodes
            Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
            cache.put(getAddress().toString(), new ClientSessionInfo(this));
        }
    }

    /**
     * Creates a session with an underlying connection and permission protection.
     *
     * @param serverName name of the server.
     * @param connection The connection we are proxying.
     * @param streamID unique identifier of this session.
     */
    public LocalClientSession(String serverName, Connection connection, StreamID streamID, Locale language) {
        super(serverName, connection, streamID, language);
        // Set an unavailable initial presence
        presence = new Presence();
        presence.setType(Presence.Type.unavailable);
    }

    /**
     * Returns the username associated with this session. Use this information
     * with the user manager to obtain the user based on username.
     *
     * @return the username associated with this session
     * @throws org.jivesoftware.openfire.user.UserNotFoundException if a user is not associated with a session
     *      (the session has not authenticated yet)
     */
    @Override
    public String getUsername() throws UserNotFoundException {
        if (authToken == null) {
            throw new UserNotFoundException();
        }
        return getAddress().getNode();
    }

    /**
     * Sets the new Authorization Token for this session. The session is not yet considered fully
     * authenticated (i.e. active) since a resource has not been binded at this point. This
     * message will be sent after SASL authentication was successful but yet resource binding
     * is required.
     *
     * @param auth the authentication token obtained from SASL authentication.
     */
    public void setAuthToken(AuthToken auth) {
        authToken = auth;
    }

    /**
     * Initialize the session with a valid authentication token and
     * resource name. This automatically upgrades the session's
     * status to authenticated and enables many features that are not
     * available until authenticated (obtaining managers for example).
     *
     * @param auth the authentication token obtained from the AuthFactory.
     * @param resource the resource this session authenticated under.
     */
    public void setAuthToken(AuthToken auth, String resource) {
        setAddress(new JID(auth.getUsername(), getServerName(), resource));
        authToken = auth;
        setStatus(Session.STATUS_AUTHENTICATED);

        // Set default privacy list for this session
        setDefaultList(PrivacyListManager.getInstance().getDefaultPrivacyList(auth.getUsername()));
        // Add session to the session manager. The session will be added to the routing table as well
        sessionManager.addSession(this);
    }

    /**
     * Initialize the session as an anonymous login. This automatically upgrades the session's
     * status to authenticated and enables many features that are not available until
     * authenticated (obtaining managers for example).<p>
     */
    public void setAnonymousAuth() {
        // Anonymous users have a full JID. Use the random resource as the JID's node
        String resource = getAddress().getResource();
        setAddress(new JID(resource, getServerName(), resource, true));
        setStatus(Session.STATUS_AUTHENTICATED);
        if (authToken == null) {
            authToken = new AuthToken(resource, true);
        }
        // Add session to the session manager. The session will be added to the routing table as well
        sessionManager.addSession(this);
    }

    /**
     * Returns the authentication token associated with this session.
     *
     * @return the authentication token associated with this session (can be null).
     */
    public AuthToken getAuthToken() {
        return authToken;
    }

    @Override
    public boolean isAnonymousUser() {
        return authToken == null || authToken.isAnonymous();
    }

    /**
     * Flag indicating if this session has been initialized once coming
     * online. Session initialization occurs after the session receives
     * the first "available" presence update from the client. Initialization
     * actions include pushing offline messages, presence subscription requests,
     * and presence statuses to the client. Initialization occurs only once
     * following the first available presence transition.
     *
     * @return True if the session has already been initializsed
     */
    @Override
    public boolean isInitialized() {
        return initialized;
    }

    /**
     * Sets the initialization state of the session.
     *
     * @param isInit True if the session has been initialized
     * @see #isInitialized
     */
    @Override
    public void setInitialized(boolean isInit) {
        initialized = isInit;
    }

    /**
     * Returns true if the session was available ever.
     *
     * @return true if the session was available ever.
     */
    public boolean wasAvailable() {
        return wasAvailable;
    }

    /**
     * Returns true if the offline messages of the user should be sent to the user when
     * the user becomes online. If the user sent a disco request with node
     * "http://jabber.org/protocol/offline" before the available presence then do not
     * flood the user with the offline messages. If the user is connected from many resources
     * then if one of the sessions stopped the flooding then no session should flood the user.
     *
     * @return true if the offline messages of the user should be sent to the user when the user
     *         becomes online.
     * @see <a href="http://www.xmpp.org/extensions/xep-0160.html">XEP-0160: Best Practices for Handling Offline Messages</a>
     */
    @Override
    public boolean canFloodOfflineMessages() {
        // XEP-0160: When the recipient next sends non-negative available presence to the server, the server delivers the message to the resource that has sent that presence.
        if(offlineFloodStopped || presence.getPriority() < 0) {
            return false;
        }
        String username = getAddress().getNode();
        for (ClientSession session : sessionManager.getSessions(username)) {
            if (session.isOfflineFloodStopped()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if the user requested to not receive offline messages when sending
     * an available presence. The user may send a disco request with node
     * "http://jabber.org/protocol/offline" so that no offline messages are sent to the
     * user when he becomes online. If the user is connected from many resources then
     * if one of the sessions stopped the flooding then no session should flood the user.
     *
     * @return true if the user requested to not receive offline messages when sending
     *         an available presence.
     */
    @Override
    public boolean isOfflineFloodStopped() {
        return offlineFloodStopped;
    }

    /**
     * Sets if the user requested to not receive offline messages when sending
     * an available presence. The user may send a disco request with node
     * "http://jabber.org/protocol/offline" so that no offline messages are sent to the
     * user when he becomes online. If the user is connected from many resources then
     * if one of the sessions stopped the flooding then no session should flood the user.
     *
     * @param offlineFloodStopped if the user requested to not receive offline messages when
     *        sending an available presence.
     */
    public void setOfflineFloodStopped(boolean offlineFloodStopped) {
        this.offlineFloodStopped = offlineFloodStopped;
        if (ClusterManager.isClusteringStarted()) {
            // Track information about the session and share it with other cluster nodes
            Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
            cache.put(getAddress().toString(), new ClientSessionInfo(this));
        }
    }

    /**
     * Obtain the presence of this session.
     *
     * @return The presence of this session or null if not authenticated
     */
    @Override
    public Presence getPresence() {
        return presence;
    }

    /**
     * Set the presence of this session
     *
     * @param presence The presence for the session
     */
    @Override
    public void setPresence(Presence presence) {
        Presence oldPresence = this.presence;
        this.presence = presence;
        if (oldPresence.isAvailable() && !this.presence.isAvailable()) {
            // The client is no longer available
            sessionManager.sessionUnavailable(this);
            // Mark that the session is no longer initialized. This means that if the user sends
            // an available presence again the session will be initialized again thus receiving
            // offline messages and offline presence subscription requests
            setInitialized(false);
            // Notify listeners that the session is no longer available
            PresenceEventDispatcher.unavailableSession(this, presence);
        }
        else if (!oldPresence.isAvailable() && this.presence.isAvailable()) {
            // The client is available
            sessionManager.sessionAvailable(this, presence);
            wasAvailable = true;
            // Notify listeners that the session is now available
            PresenceEventDispatcher.availableSession(this, presence);
        }
        else if (this.presence.isAvailable() && oldPresence.getPriority() != this.presence.getPriority())
        {
            // The client has changed the priority of his presence
            sessionManager.changePriority(this, oldPresence.getPriority());
            // Notify listeners that the priority of the session/resource has changed
            PresenceEventDispatcher.presenceChanged(this, presence);
        }
        else if (this.presence.isAvailable()) {
            // Notify listeners that the show or status value of the presence has changed
            PresenceEventDispatcher.presenceChanged(this, presence);
        }
        if (ClusterManager.isClusteringStarted()) {
            // Track information about the session and share it with other cluster nodes
            Cache<String,ClientSessionInfo> cache = SessionManager.getInstance().getSessionInfoCache();
            cache.put(getAddress().toString(), new ClientSessionInfo(this));
        }
    }

    @Override
	public String getAvailableStreamFeatures() {
        // Offer authenticate and registration only if TLS was not required or if required
        // then the connection is already secured
        if (conn.getTlsPolicy() == Connection.TLSPolicy.required && !conn.isSecure()) {
            return null;
        }

        StringBuilder sb = new StringBuilder(200);

        // Include Stream Compression Mechanism
        if (conn.getCompressionPolicy() != Connection.CompressionPolicy.disabled &&
                !conn.isCompressed()) {
            sb.append(
                    "<compression xmlns=\"http://jabber.org/features/compress\"><method>zlib</method></compression>");
        }

        if (getAuthToken() == null) {
            // Advertise that the server supports Non-SASL Authentication
            sb.append("<auth xmlns=\"http://jabber.org/features/iq-auth\"/>");
            // Advertise that the server supports In-Band Registration
            if (XMPPServer.getInstance().getIQRegisterHandler().isInbandRegEnabled()) {
                sb.append("<register xmlns=\"http://jabber.org/features/iq-register\"/>");
            }
        }
        else {
            // If the session has been authenticated then offer resource binding,
            // and session establishment
            sb.append("<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\"/>");
            sb.append("<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"><optional/></session>");

            // Offer XEP-0198 stream management capabilities if enabled.
            if(JiveGlobals.getBooleanProperty("stream.management.active", true)) {
            	sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V2));
            	sb.append(String.format("<sm xmlns='%s'/>", StreamManager.NAMESPACE_V3));
            }
        }
        return sb.toString();
    }

    /**
     * Increments the conflict by one.
     */
    @Override
    public int incrementConflictCount() {
        conflictCount++;
        return conflictCount;
    }

    @Override
    public boolean isMessageCarbonsEnabled() {
        return messageCarbonsEnabled;
    }

    @Override
    public void setMessageCarbonsEnabled(boolean enabled) {
        messageCarbonsEnabled = enabled;
    }

    /**
     * Returns true if the specified packet must not be blocked based on the active or default
     * privacy list rules. The active list will be tried first. If none was found then the
     * default list is going to be used. If no default list was defined for this user then
     * allow the packet to flow.
     *
     * @param packet the packet to analyze if it must be blocked.
     * @return true if the specified packet must be blocked.
     */
    @Override
	public boolean canProcess(Packet packet) {

        PrivacyList list = getActiveList();
        if (list != null) {
            // If a privacy list is active then make sure that the packet is not blocked
            return !list.shouldBlockPacket(packet);
        }
        else {
            list = getDefaultList();
            // There is no active list so check if there exists a default list and make
            // sure that the packet is not blocked
            return list == null || !list.shouldBlockPacket(packet);
        }
    }

    @Override
	public void deliver(Packet packet) throws UnauthorizedException {

        conn.deliver(packet);
        streamManager.sentStanza(packet);
    }

    @Override
	public String toString() {
        return super.toString() + " presence: " + presence;
    }
}
