/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * 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 net.java.sip.communicator.impl.protocol.jabber;

import net.java.sip.communicator.impl.protocol.jabber.sasl.*;
import net.java.sip.communicator.service.certificate.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import org.jivesoftware.smack.*;

import javax.net.ssl.*;
import java.security.*;

/**
 * Login to Jabber using username & password.
 *
 * @author Stefan Sieber
 */
public class LoginByPasswordStrategy
    implements JabberLoginStrategy
{
    private final AbstractProtocolProviderService protocolProvider;
    private final AccountID accountID;

    private String password;

    /**
     * Disables use of custom digest md5 per account.
     */
    private String DISABLE_CUSTOM_DIGEST_MD5_ACCOUNT_PROP =
        "DISABLE_CUSTOM_DIGEST_MD5";

    /**
     * Disables use of custom digest md5.
     */
    private String DISABLE_CUSTOM_DIGEST_MD5_CONFIG_PROP =
        "net.java.sip.communicator.impl.protocol" +
            ".jabber.DISABLE_CUSTOM_DIGEST_MD5";

    /**
     * SASLAuthentication currently supports only global mechanisms setting
     * so per account property is not of much use, but will keep it for
     * compatibility with old versions.
     * We try to modify it only once or some concurrent modification exceptions
     * can occur.
     */
    private static boolean saslMechanismsInitialized = false;

    /**
     * Used to lock the modifying mechanisms and any login that can occur.
     */
    private static Object modifySASLMechanisms = new Object();

    /**
     * Create a login strategy that logs in using user credentials (username
     * and password)
     * @param protocolProvider  protocol provider service to fire registration
     *                          change events.
     * @param accountID The accountID to use for the login.
     */
    public LoginByPasswordStrategy(
            AbstractProtocolProviderService protocolProvider,
            AccountID accountID)
    {
        this.protocolProvider = protocolProvider;
        this.accountID = accountID;
    }

    /**
     * Loads the account passwords as preparation for the login.
     *
     * @param authority SecurityAuthority to obtain the password
     * @param reasonCode reason why we're preparing for login
     * @return UserCredentials in case they need to be cached for this session
     *         (i.e. password is not persistent)
     */
    public UserCredentials prepareLogin(SecurityAuthority authority,
            int reasonCode)
    {
       return loadPassword(authority, reasonCode);
    }

    /**
     * Determines whether the strategy is ready to perform the login.
     *
     * @return True when the password was sucessfully loaded.
     */
    public boolean loginPreparationSuccessful()
    {
        return password != null;
    }


    /**
     * Performs the login on an XMPP connection using SASL PLAIN.
     *
     * @param connection The connection on which the login is performed.
     * @param userName The username for the login.
     * @param resource The XMPP resource.
     * @return always true.
     * @throws XMPPException
     */
    public boolean login(Connection connection, String userName,
            String resource)
            throws XMPPException
    {
        synchronized(modifySASLMechanisms)
        {
            boolean disableCustomDigestMD5PerAccount
                        = accountID.getAccountPropertyBoolean(
                        DISABLE_CUSTOM_DIGEST_MD5_ACCOUNT_PROP,
                        false);

            if(!saslMechanismsInitialized || disableCustomDigestMD5PerAccount)
            {
                SASLAuthentication.supportSASLMechanism("PLAIN", 0);

                // Insert our sasl mechanism implementation
                // in order to support some incompatible servers
                boolean disableCustomDigestMD5
                    = disableCustomDigestMD5PerAccount
                    || JabberActivator.getConfigurationService().getBoolean(
                        DISABLE_CUSTOM_DIGEST_MD5_CONFIG_PROP, false);

                if(!disableCustomDigestMD5)
                {
                    SASLAuthentication.unregisterSASLMechanism("DIGEST-MD5");
                    SASLAuthentication.registerSASLMechanism("DIGEST-MD5",
                        SASLDigestMD5Mechanism.class);
                    SASLAuthentication.supportSASLMechanism("DIGEST-MD5");
                }

                saslMechanismsInitialized = true;
            }
        }

        synchronized(modifySASLMechanisms)
        {
            connection.login(userName, password, resource);
        }

        return true;
    }

    /*
     * (non-Javadoc)
     *
     * @see net.java.sip.communicator.impl.protocol.jabber.JabberLoginStrategy#
     * isTlsRequired()
     */
    public boolean isTlsRequired()
    {
        // requires TLS by default (i.e. it will not connect to a non-TLS server
        // and will not fallback to cleartext)
        return !accountID.getAccountPropertyBoolean(
                ProtocolProviderFactory.IS_ALLOW_NON_SECURE, false);
    }

    /**
     * Prepares an SSL Context that is customized SSL context.
     *
     * @param cs The certificate service that provides the context.
     * @param trustManager The TrustManager to use within the context.
     * @return An initialized context for the current provider.
     * @throws GeneralSecurityException
     */
    public SSLContext createSslContext(CertificateService cs,
            X509TrustManager trustManager)
            throws GeneralSecurityException
    {
        return cs.getSSLContext(trustManager);
    }

    /**
     * Load the password from the account configuration or ask the user.
     *
     * @param authority SecurityAuthority
     * @param reasonCode the authentication reason code. Indicates the reason of
     *            this authentication.
     * @return The UserCredentials in case they should be cached for this
     *         session (i.e. are not persistent)
     */
    private UserCredentials loadPassword(SecurityAuthority authority,
            int reasonCode)
    {
        UserCredentials cachedCredentials = null;
        //verify whether a password has already been stored for this account
        password = JabberActivator.
                getProtocolProviderFactory().loadPassword(accountID);

        //decode
        if (password == null)
        {
            //create a default credentials object
            UserCredentials credentials = new UserCredentials();
            credentials.setUserName(accountID.getUserID());

            //request a password from the user
            credentials = authority.obtainCredentials(
                    accountID.getDisplayName(),
                    credentials,
                    reasonCode);

            // in case user has canceled the login window
            if(credentials == null)
            {
                protocolProvider.fireRegistrationStateChanged(
                        protocolProvider.getRegistrationState(),
                        RegistrationState.UNREGISTERED,
                        RegistrationStateChangeEvent.REASON_USER_REQUEST,
                        "No credentials provided");
                return null;
            }

            //extract the password the user passed us.
            char[] pass = credentials.getPassword();

            // the user didn't provide us a password (canceled the operation)
            if(pass == null)
            {
                protocolProvider.fireRegistrationStateChanged(
                        protocolProvider.getRegistrationState(),
                        RegistrationState.UNREGISTERED,
                        RegistrationStateChangeEvent.REASON_USER_REQUEST,
                        "No password entered");
                return null;
            }
            password = new String(pass);

            if (credentials.isPasswordPersistent())
            {
                JabberActivator.getProtocolProviderFactory()
                        .storePassword(accountID, password);
            }
            else
                cachedCredentials = credentials;
        }
        return cachedCredentials;
    }
}
