/*
 * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, version 2.0, as published by the
 * Free Software Foundation.
 *
 * This program is also distributed with certain software (including but not
 * limited to OpenSSL) that is licensed under separate terms, as designated in a
 * particular file or component or in included license documentation. The
 * authors of MySQL hereby grant you an additional permission to link the
 * program and your derivative works with the separately licensed software that
 * they have included with MySQL.
 *
 * Without limiting anything contained in the foregoing, this file, which is
 * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at
 * http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
 */

package com.mysql.cj.protocol.a;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.mysql.cj.Constants;
import com.mysql.cj.Messages;
import com.mysql.cj.conf.PropertyDefinitions.SslMode;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.conf.PropertySet;
import com.mysql.cj.conf.RuntimeProperty;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.ExceptionInterceptor;
import com.mysql.cj.exceptions.UnableToConnectException;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.protocol.AuthenticationPlugin;
import com.mysql.cj.protocol.AuthenticationProvider;
import com.mysql.cj.protocol.Protocol;
import com.mysql.cj.protocol.ServerSession;
import com.mysql.cj.protocol.a.NativeConstants.IntegerDataType;
import com.mysql.cj.protocol.a.NativeConstants.StringLengthDataType;
import com.mysql.cj.protocol.a.NativeConstants.StringSelfDataType;
import com.mysql.cj.protocol.a.authentication.CachingSha2PasswordPlugin;
import com.mysql.cj.protocol.a.authentication.MysqlClearPasswordPlugin;
import com.mysql.cj.protocol.a.authentication.MysqlNativePasswordPlugin;
import com.mysql.cj.protocol.a.authentication.MysqlOldPasswordPlugin;
import com.mysql.cj.protocol.a.authentication.Sha256PasswordPlugin;
import com.mysql.cj.protocol.a.result.OkPacket;
import com.mysql.cj.util.StringUtils;

public class NativeAuthenticationProvider implements AuthenticationProvider<NativePacketPayload> {

    protected static final int AUTH_411_OVERHEAD = 33;
    private static final String NONE = "none";

    protected String seed;
    private boolean useConnectWithDb;

    private ExceptionInterceptor exceptionInterceptor;
    private PropertySet propertySet;

    private Protocol<NativePacketPayload> protocol;

    public NativeAuthenticationProvider() {
    }

    @Override
    public void init(Protocol<NativePacketPayload> prot, PropertySet propSet, ExceptionInterceptor excInterceptor) {
        this.protocol = prot;
        this.propertySet = propSet;
        this.exceptionInterceptor = excInterceptor;
    }

    /**
     * Initialize communications with the MySQL server. Handles logging on, and
     * handling initial connection errors.
     * 
     * @param sessState
     *            The session state object. It's intended to be updated from the handshake
     * @param user
     *            user name
     * @param password
     *            password
     * @param database
     *            database name
     */
    @Override
    public void connect(ServerSession sessState, String user, String password, String database) {
        long clientParam = sessState.getClientParam();

        NativeCapabilities capabilities = (NativeCapabilities) sessState.getCapabilities();

        NativePacketPayload buf = capabilities.getInitialHandshakePacket();

        // read auth-plugin-data-part-1 (string[8])
        this.seed = capabilities.getSeed();

        // read character set (1 byte)
        sessState.setServerDefaultCollationIndex(capabilities.getServerDefaultCollationIndex());
        // read status flags (2 bytes)
        sessState.setStatusFlags(capabilities.getStatusFlags());

        int capabilityFlags = capabilities.getCapabilityFlags();

        if ((capabilityFlags & NativeServerSession.CLIENT_SECURE_CONNECTION) != 0) {
            clientParam |= NativeServerSession.CLIENT_SECURE_CONNECTION;
            String seedPart2;
            StringBuilder newSeed;
            int authPluginDataLength = capabilities.getAuthPluginDataLength();

            // read string[$len] auth-plugin-data-part-2 ($len=MAX(13, length of auth-plugin-data - 8))
            if (authPluginDataLength > 0) {
                // TODO: disabled the following check for further clarification
                //                  if (this.authPluginDataLength < 21) {
                //                      forceClose();
                //                      throw SQLError.createSQLException(Messages.getString("MysqlIO.103"), 
                //                          SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, getExceptionInterceptor());
                //                  }
                seedPart2 = buf.readString(StringLengthDataType.STRING_FIXED, "ASCII", authPluginDataLength - 8);
                newSeed = new StringBuilder(authPluginDataLength);
            } else {
                seedPart2 = buf.readString(StringSelfDataType.STRING_TERM, "ASCII");
                newSeed = new StringBuilder(NativeConstants.SEED_LENGTH);
            }
            newSeed.append(this.seed);
            newSeed.append(seedPart2);
            this.seed = newSeed.toString();
        } else {
            // TODO: better messaging
            throw ExceptionFactory.createException(UnableToConnectException.class, "CLIENT_SECURE_CONNECTION is required", getExceptionInterceptor());

        }

        if (((capabilityFlags & NativeServerSession.CLIENT_COMPRESS) != 0) && this.propertySet.getBooleanProperty(PropertyKey.useCompression).getValue()) {
            clientParam |= NativeServerSession.CLIENT_COMPRESS;
        }

        this.useConnectWithDb = (database != null) && (database.length() > 0)
                && !this.propertySet.getBooleanProperty(PropertyKey.createDatabaseIfNotExist).getValue();

        if (this.useConnectWithDb) {
            clientParam |= NativeServerSession.CLIENT_CONNECT_WITH_DB;
        }

        // Changing defaults for 8.0.3+ server: PNAME_useInformationSchema=true
        RuntimeProperty<Boolean> useInformationSchema = this.propertySet.<Boolean> getProperty(PropertyKey.useInformationSchema);
        if (this.protocol.versionMeetsMinimum(8, 0, 3) && !useInformationSchema.getValue() && !useInformationSchema.isExplicitlySet()) {
            useInformationSchema.setValue(true);
        }

        // check SSL availability
        SslMode sslMode = this.propertySet.<SslMode> getEnumProperty(PropertyKey.sslMode).getValue();
        if (((capabilityFlags & NativeServerSession.CLIENT_SSL) == 0) && sslMode != SslMode.DISABLED && sslMode != SslMode.PREFERRED) {
            throw ExceptionFactory.createException(UnableToConnectException.class, Messages.getString("MysqlIO.15"), getExceptionInterceptor());
        }

        if ((capabilityFlags & NativeServerSession.CLIENT_LONG_FLAG) != 0) {
            clientParam |= NativeServerSession.CLIENT_LONG_FLAG;
            sessState.setHasLongColumnInfo(true);
        }

        // return FOUND rows
        if (!this.propertySet.getBooleanProperty(PropertyKey.useAffectedRows).getValue()) {
            clientParam |= NativeServerSession.CLIENT_FOUND_ROWS;
        }

        if (this.propertySet.getBooleanProperty(PropertyKey.allowLoadLocalInfile).getValue()) {
            clientParam |= NativeServerSession.CLIENT_LOCAL_FILES;
        }

        if (this.propertySet.getBooleanProperty(PropertyKey.interactiveClient).getValue()) {
            clientParam |= NativeServerSession.CLIENT_INTERACTIVE;
        }

        if ((capabilityFlags & NativeServerSession.CLIENT_SESSION_TRACK) != 0) {
            // TODO MYSQLCONNJ-437
            // clientParam |= NativeServerSession.CLIENT_SESSION_TRACK;
        }

        if ((capabilityFlags & NativeServerSession.CLIENT_DEPRECATE_EOF) != 0) {
            clientParam |= NativeServerSession.CLIENT_DEPRECATE_EOF;
        }

        //
        // switch to pluggable authentication if available
        //
        if ((capabilityFlags & NativeServerSession.CLIENT_PLUGIN_AUTH) != 0) {
            sessState.setClientParam(clientParam);
            proceedHandshakeWithPluggableAuthentication(sessState, user, password, database, buf);
        } else {
            // TODO: better messaging
            throw ExceptionFactory.createException(UnableToConnectException.class, "CLIENT_PLUGIN_AUTH is required", getExceptionInterceptor());
        }

    }

    /**
     * Contains instances of authentication plugins which implements {@link AuthenticationPlugin} interface. Key values are mysql
     * protocol plugin names, for example "mysql_native_password" and
     * "mysql_old_password" for built-in plugins.
     */
    private Map<String, AuthenticationPlugin<NativePacketPayload>> authenticationPlugins = null;
    /**
     * Contains names of classes or mechanisms ("mysql_native_password"
     * for example) of authentication plugins which must be disabled.
     */
    private List<String> disabledAuthenticationPlugins = null;
    /**
     * Name of class for default authentication plugin in client
     */
    private String clientDefaultAuthenticationPlugin = null;
    /**
     * Protocol name of default authentication plugin in client
     */
    private String clientDefaultAuthenticationPluginName = null;
    /**
     * Protocol name of default authentication plugin in server
     */
    private String serverDefaultAuthenticationPluginName = null;

    /**
     * Fill the authentication plugins map.
     * First this method fill the map with instances of {@link MysqlNativePasswordPlugin}, {@link MysqlClearPasswordPlugin}, {@link Sha256PasswordPlugin}
     * and {@link MysqlOldPasswordPlugin}. Then it creates instances of plugins listed in "authenticationPlugins" connection property and adds them to the map
     * too.
     * 
     * The key for the map entry is got by {@link AuthenticationPlugin#getProtocolPluginName()} thus it is possible to replace built-in plugin with custom
     * implementation. To do it custom plugin should return value "mysql_native_password", "mysql_old_password", "mysql_clear_password" or "sha256_password"
     * from it's own getProtocolPluginName() method.
     * 
     */
    @SuppressWarnings("unchecked")
    private void loadAuthenticationPlugins() {

        // default plugin
        this.clientDefaultAuthenticationPlugin = this.propertySet.getStringProperty(PropertyKey.defaultAuthenticationPlugin).getValue();
        if (this.clientDefaultAuthenticationPlugin == null || "".equals(this.clientDefaultAuthenticationPlugin.trim())) {
            throw ExceptionFactory.createException(WrongArgumentException.class,
                    Messages.getString("AuthenticationProvider.BadDefaultAuthenticationPlugin", new Object[] { this.clientDefaultAuthenticationPlugin }),
                    getExceptionInterceptor());
        }

        // disabled plugins
        String disabledPlugins = this.propertySet.getStringProperty(PropertyKey.disabledAuthenticationPlugins).getValue();
        if (disabledPlugins != null && !"".equals(disabledPlugins)) {
            this.disabledAuthenticationPlugins = new ArrayList<>();
            List<String> pluginsToDisable = StringUtils.split(disabledPlugins, ",", true);
            Iterator<String> iter = pluginsToDisable.iterator();
            while (iter.hasNext()) {
                this.disabledAuthenticationPlugins.add(iter.next());
            }
        }

        this.authenticationPlugins = new HashMap<>();
        boolean defaultIsFound = false;

        List<AuthenticationPlugin<NativePacketPayload>> pluginsToInit = new LinkedList<>();

        // embedded plugins
        pluginsToInit.add(new MysqlNativePasswordPlugin());
        pluginsToInit.add(new MysqlClearPasswordPlugin());
        pluginsToInit.add(new Sha256PasswordPlugin());
        pluginsToInit.add(new CachingSha2PasswordPlugin());
        pluginsToInit.add(new MysqlOldPasswordPlugin());

        // plugins from authenticationPluginClasses connection parameter
        String authenticationPluginClasses = this.propertySet.getStringProperty(PropertyKey.authenticationPlugins).getValue();
        if (authenticationPluginClasses != null && !"".equals(authenticationPluginClasses)) {
            List<String> pluginsToCreate = StringUtils.split(authenticationPluginClasses, ",", true);
            String className = null;
            try {
                for (int i = 0, s = pluginsToCreate.size(); i < s; i++) {
                    className = pluginsToCreate.get(i);
                    pluginsToInit.add((AuthenticationPlugin<NativePacketPayload>) Class.forName(className).newInstance());
                }
            } catch (Throwable t) {
                throw ExceptionFactory.createException(WrongArgumentException.class,
                        Messages.getString("AuthenticationProvider.BadAuthenticationPlugin", new Object[] { className }), t, this.exceptionInterceptor);
            }
        }

        // initialize plugin instances
        for (AuthenticationPlugin<NativePacketPayload> plugin : pluginsToInit) {
            plugin.init(this.protocol);
            if (addAuthenticationPlugin(plugin)) {
                defaultIsFound = true;
            }
        }

        // check if default plugin is listed
        if (!defaultIsFound) {
            throw ExceptionFactory.createException(WrongArgumentException.class, Messages
                    .getString("AuthenticationProvider.DefaultAuthenticationPluginIsNotListed", new Object[] { this.clientDefaultAuthenticationPlugin }),
                    getExceptionInterceptor());
        }

    }

    /**
     * Add plugin to authentication plugins map if it is not disabled by
     * "disabledAuthenticationPlugins" property, check is it a default plugin.
     * 
     * @param plugin
     *            Instance of AuthenticationPlugin
     * @return True if plugin is default, false if plugin is not default.
     * @throws WrongArgumentException
     *             if plugin is default but disabled.
     */
    private boolean addAuthenticationPlugin(AuthenticationPlugin<NativePacketPayload> plugin) {
        boolean isDefault = false;
        String pluginClassName = plugin.getClass().getName();
        String pluginProtocolName = plugin.getProtocolPluginName();
        boolean disabledByClassName = this.disabledAuthenticationPlugins != null && this.disabledAuthenticationPlugins.contains(pluginClassName);
        boolean disabledByMechanism = this.disabledAuthenticationPlugins != null && this.disabledAuthenticationPlugins.contains(pluginProtocolName);

        if (disabledByClassName || disabledByMechanism) {
            // if disabled then check is it default
            if (this.clientDefaultAuthenticationPlugin.equals(pluginClassName)) {
                throw ExceptionFactory.createException(WrongArgumentException.class,
                        Messages.getString("AuthenticationProvider.BadDisabledAuthenticationPlugin",
                                new Object[] { disabledByClassName ? pluginClassName : pluginProtocolName }),
                        getExceptionInterceptor());
            }
        } else {
            this.authenticationPlugins.put(pluginProtocolName, plugin);
            if (this.clientDefaultAuthenticationPlugin.equals(pluginClassName)) {
                this.clientDefaultAuthenticationPluginName = pluginProtocolName;
                isDefault = true;
            }
        }
        return isDefault;
    }

    /**
     * Get authentication plugin instance from authentication plugins map by
     * pluginName key. If such plugin is found it's {@link AuthenticationPlugin#isReusable()} method
     * is checked, when it's false this method returns a new instance of plugin
     * and the same instance otherwise.
     * 
     * If plugin is not found method returns null, in such case the subsequent behavior
     * of handshake process depends on type of last packet received from server:
     * if it was Auth Challenge Packet then handshake will proceed with default plugin,
     * if it was Auth Method Switch Request Packet then handshake will be interrupted with exception.
     * 
     * @param pluginName
     *            mysql protocol plugin names, for example "mysql_native_password" and "mysql_old_password" for built-in plugins
     * @return null if plugin is not found or authentication plugin instance initialized with current connection properties
     */
    @SuppressWarnings("unchecked")
    private AuthenticationPlugin<NativePacketPayload> getAuthenticationPlugin(String pluginName) {

        AuthenticationPlugin<NativePacketPayload> plugin = this.authenticationPlugins.get(pluginName);

        if (plugin != null && !plugin.isReusable()) {
            try {
                plugin = plugin.getClass().newInstance();
                plugin.init(this.protocol);
            } catch (Throwable t) {
                throw ExceptionFactory.createException(WrongArgumentException.class,
                        Messages.getString("AuthenticationProvider.BadAuthenticationPlugin", new Object[] { plugin.getClass().getName() }), t,
                        getExceptionInterceptor());
            }
        }

        return plugin;
    }

    /**
     * Check if given plugin requires confidentiality, but connection is without SSL
     * 
     * @param plugin
     *            {@link AuthenticationPlugin}
     */
    private void checkConfidentiality(AuthenticationPlugin<?> plugin) {
        if (plugin.requiresConfidentiality() && !this.protocol.getSocketConnection().isSSLEstablished()) {
            throw ExceptionFactory.createException(
                    Messages.getString("AuthenticationProvider.AuthenticationPluginRequiresSSL", new Object[] { plugin.getProtocolPluginName() }),
                    getExceptionInterceptor());
        }
    }

    /**
     * Performs an authentication handshake to authorize connection to a
     * given database as a given MySQL user. This can happen upon initial
     * connection to the server, after receiving Auth Challenge Packet, or
     * at any moment during the connection life-time via a Change User
     * request.
     * 
     * This method is aware of pluggable authentication and will use
     * registered authentication plugins as requested by the server.
     * 
     * @param sessState
     *            The current state of the session
     * @param user
     *            the MySQL user account to log into
     * @param password
     *            authentication data for the user account (depends
     *            on authentication method used - can be empty)
     * @param database
     *            database to connect to (can be empty)
     * @param challenge
     *            the Auth Challenge Packet received from server if
     *            this method is used during the initial connection.
     *            Otherwise null.
     */
    private void proceedHandshakeWithPluggableAuthentication(ServerSession sessState, String user, String password, String database,
            NativePacketPayload challenge) {
        if (this.authenticationPlugins == null) {
            loadAuthenticationPlugins();
        }

        boolean skipPassword = false;
        int passwordLength = 16;
        int userLength = (user != null) ? user.length() : 0;
        int databaseLength = (database != null) ? database.length() : 0;

        int packLength = ((userLength + passwordLength + databaseLength) * 3) + 7 + AUTH_411_OVERHEAD;

        long clientParam = sessState.getClientParam();
        int serverCapabilities = sessState.getCapabilities().getCapabilityFlags();

        AuthenticationPlugin<NativePacketPayload> plugin = null;
        NativePacketPayload fromServer = null;
        ArrayList<NativePacketPayload> toServer = new ArrayList<>();
        boolean done = false;
        NativePacketPayload last_sent = null;

        boolean old_raw_challenge = false;

        int counter = 100;

        while (0 < counter--) {

            if (!done) {

                if (challenge != null) {

                    if (challenge.isOKPacket()) {
                        throw ExceptionFactory.createException(
                                Messages.getString("AuthenticationProvider.UnexpectedAuthenticationApproval", new Object[] { plugin.getProtocolPluginName() }),
                                getExceptionInterceptor());
                    }

                    // read Auth Challenge Packet

                    clientParam |= NativeServerSession.CLIENT_PLUGIN_AUTH | NativeServerSession.CLIENT_LONG_PASSWORD | NativeServerSession.CLIENT_PROTOCOL_41
                            | NativeServerSession.CLIENT_TRANSACTIONS // Need this to get server status values
                            | NativeServerSession.CLIENT_MULTI_RESULTS // We always allow multiple result sets
                            | NativeServerSession.CLIENT_PS_MULTI_RESULTS  // We always allow multiple result sets for SSPS
                            | NativeServerSession.CLIENT_SECURE_CONNECTION; // protocol with pluggable authentication always support this

                    // We allow the user to configure whether or not they want to support multiple queries (by default, this is disabled).
                    if (this.propertySet.getBooleanProperty(PropertyKey.allowMultiQueries).getValue()) {
                        clientParam |= NativeServerSession.CLIENT_MULTI_STATEMENTS;
                    }

                    if (((serverCapabilities & NativeServerSession.CLIENT_CAN_HANDLE_EXPIRED_PASSWORD) != 0)
                            && !this.propertySet.getBooleanProperty(PropertyKey.disconnectOnExpiredPasswords).getValue()) {
                        clientParam |= NativeServerSession.CLIENT_CAN_HANDLE_EXPIRED_PASSWORD;
                    }
                    if (((serverCapabilities & NativeServerSession.CLIENT_CONNECT_ATTRS) != 0)
                            && !NONE.equals(this.propertySet.getStringProperty(PropertyKey.connectionAttributes).getValue())) {
                        clientParam |= NativeServerSession.CLIENT_CONNECT_ATTRS;
                    }
                    if ((serverCapabilities & NativeServerSession.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) != 0) {
                        clientParam |= NativeServerSession.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
                    }

                    sessState.setClientParam(clientParam);

                    if (((serverCapabilities & NativeServerSession.CLIENT_SSL) != 0)
                            && this.propertySet.<SslMode> getEnumProperty(PropertyKey.sslMode).getValue() != SslMode.DISABLED) {
                        negotiateSSLConnection(packLength);
                    }

                    String pluginName = null;
                    if ((serverCapabilities & NativeServerSession.CLIENT_PLUGIN_AUTH) != 0) {
                        // Due to Bug#59453 the auth-plugin-name is missing the terminating NUL-char in versions prior to 5.5.10 and 5.6.2.
                        if (!this.protocol.versionMeetsMinimum(5, 5, 10)
                                || this.protocol.versionMeetsMinimum(5, 6, 0) && !this.protocol.versionMeetsMinimum(5, 6, 2)) {
                            pluginName = challenge.readString(StringLengthDataType.STRING_FIXED, "ASCII",
                                    ((NativeCapabilities) sessState.getCapabilities()).getAuthPluginDataLength());
                        } else {
                            pluginName = challenge.readString(StringSelfDataType.STRING_TERM, "ASCII");
                        }
                    }

                    plugin = getAuthenticationPlugin(pluginName);
                    if (plugin == null) {
                        /*
                         * Use default if there is no plugin for pluginName.
                         */
                        plugin = getAuthenticationPlugin(this.clientDefaultAuthenticationPluginName);
                    } else if (pluginName.equals(Sha256PasswordPlugin.PLUGIN_NAME) && !this.protocol.getSocketConnection().isSSLEstablished()
                            && this.propertySet.getStringProperty(PropertyKey.serverRSAPublicKeyFile).getValue() == null
                            && !this.propertySet.getBooleanProperty(PropertyKey.allowPublicKeyRetrieval).getValue()) {
                        /*
                         * Fall back to default if plugin is 'sha256_password' but required conditions for this to work aren't met. If default is other than
                         * 'sha256_password' this will result in an immediate authentication switch request, allowing for other plugins to authenticate
                         * successfully. If default is 'sha256_password' then the authentication will fail as expected. In both cases user's password won't be
                         * sent to avoid subjecting it to lesser security levels.
                         */
                        plugin = getAuthenticationPlugin(this.clientDefaultAuthenticationPluginName);
                        skipPassword = !this.clientDefaultAuthenticationPluginName.equals(pluginName);
                    }

                    this.serverDefaultAuthenticationPluginName = plugin.getProtocolPluginName();

                    checkConfidentiality(plugin);
                    fromServer = new NativePacketPayload(StringUtils.getBytes(this.seed));
                } else {
                    // no challenge so this is a changeUser call
                    plugin = getAuthenticationPlugin(this.serverDefaultAuthenticationPluginName == null ? this.clientDefaultAuthenticationPluginName
                            : this.serverDefaultAuthenticationPluginName);

                    checkConfidentiality(plugin);

                    // Servers not affected by Bug#70865 expect the Change User Request containing a correct answer
                    // to seed sent by the server during the initial handshake, thus we reuse it here.
                    // Servers affected by Bug#70865 will just ignore it and send the Auth Switch.
                    fromServer = new NativePacketPayload(StringUtils.getBytes(this.seed));
                }

            } else {

                // read packet from server and check if it's an ERROR packet
                challenge = this.protocol.checkErrorMessage();
                old_raw_challenge = false;

                if (plugin == null) {
                    // this shouldn't happen in normal handshake packets exchange,
                    // we do it just to ensure that we don't get NPE in other case
                    plugin = getAuthenticationPlugin(this.serverDefaultAuthenticationPluginName == null ? this.clientDefaultAuthenticationPluginName
                            : this.serverDefaultAuthenticationPluginName);
                }

                if (challenge.isOKPacket()) {
                    // read OK packet
                    OkPacket ok = OkPacket.parse(challenge, null);
                    sessState.setStatusFlags(ok.getStatusFlags(), true);

                    // if OK packet then finish handshake
                    plugin.destroy();
                    break;

                } else if (challenge.isAuthMethodSwitchRequestPacket()) {
                    skipPassword = false;

                    // read Auth Method Switch Request Packet
                    String pluginName;
                    pluginName = challenge.readString(StringSelfDataType.STRING_TERM, "ASCII");

                    // get new plugin
                    if (!plugin.getProtocolPluginName().equals(pluginName)) {
                        plugin.destroy();
                        plugin = getAuthenticationPlugin(pluginName);
                        // if plugin is not found for pluginName throw exception
                        if (plugin == null) {
                            throw ExceptionFactory.createException(WrongArgumentException.class,
                                    Messages.getString("AuthenticationProvider.BadAuthenticationPlugin", new Object[] { pluginName }),
                                    getExceptionInterceptor());
                        }
                    } else {
                        plugin.reset();
                    }

                    checkConfidentiality(plugin);
                    fromServer = new NativePacketPayload(StringUtils.getBytes(challenge.readString(StringSelfDataType.STRING_TERM, "ASCII")));

                } else {
                    // read raw packet
                    if (!this.protocol.versionMeetsMinimum(5, 5, 16)) {
                        old_raw_challenge = true;
                        challenge.setPosition(challenge.getPosition() - 1);
                    }
                    fromServer = new NativePacketPayload(challenge.readBytes(StringSelfDataType.STRING_EOF));
                }

            }

            // call plugin
            plugin.setAuthenticationParameters(user, skipPassword ? null : password);
            done = plugin.nextAuthenticationStep(fromServer, toServer);

            // send response
            if (toServer.size() > 0) {
                if (challenge == null) {
                    String enc = getEncodingForHandshake();

                    // write COM_CHANGE_USER Packet
                    last_sent = new NativePacketPayload(packLength + 1);
                    last_sent.writeInteger(IntegerDataType.INT1, NativeConstants.COM_CHANGE_USER);

                    // User/Password data
                    last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(user, enc));

                    // 'auth-response-len' is limited to one Byte but, in case of success, COM_CHANGE_USER will be followed by an AuthSwitchRequest anyway
                    if (toServer.get(0).getPayloadLength() < 256) {
                        // non-mysql servers may use this information to authenticate without requiring another round-trip
                        last_sent.writeInteger(IntegerDataType.INT1, toServer.get(0).getPayloadLength());
                        last_sent.writeBytes(StringSelfDataType.STRING_EOF, toServer.get(0).getByteBuffer());
                    } else {
                        last_sent.writeInteger(IntegerDataType.INT1, 0);
                    }

                    if (this.useConnectWithDb) {
                        last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(database, enc));
                    } else {
                        /* For empty database */
                        last_sent.writeInteger(IntegerDataType.INT1, 0);
                    }

                    last_sent.writeInteger(IntegerDataType.INT1,
                            AuthenticationProvider.getCharsetForHandshake(enc, sessState.getCapabilities().getServerVersion()));
                    // two (little-endian) bytes for charset in this packet
                    last_sent.writeInteger(IntegerDataType.INT1, 0);

                    // plugin name
                    if ((serverCapabilities & NativeServerSession.CLIENT_PLUGIN_AUTH) != 0) {
                        last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(plugin.getProtocolPluginName(), enc));
                    }

                    // connection attributes
                    if ((clientParam & NativeServerSession.CLIENT_CONNECT_ATTRS) != 0) {
                        appendConnectionAttributes(last_sent, this.propertySet.getStringProperty(PropertyKey.connectionAttributes).getValue(), enc);
                    }

                    this.protocol.send(last_sent, last_sent.getPosition());

                } else if (challenge.isAuthMethodSwitchRequestPacket()) {
                    // write Auth Method Switch Response Packet
                    this.protocol.send(toServer.get(0), toServer.get(0).getPayloadLength());

                } else if (challenge.isAuthMoreData() || old_raw_challenge) {
                    // write raw packet(s)
                    for (NativePacketPayload buffer : toServer) {
                        this.protocol.send(buffer, buffer.getPayloadLength());
                    }

                } else {
                    // write Auth Response Packet
                    String enc = getEncodingForHandshake();

                    last_sent = new NativePacketPayload(packLength);
                    last_sent.writeInteger(IntegerDataType.INT4, clientParam);
                    last_sent.writeInteger(IntegerDataType.INT4, NativeConstants.MAX_PACKET_SIZE);

                    last_sent.writeInteger(IntegerDataType.INT1,
                            AuthenticationProvider.getCharsetForHandshake(enc, sessState.getCapabilities().getServerVersion()));

                    last_sent.writeBytes(StringLengthDataType.STRING_FIXED, new byte[23]);   // Set of bytes reserved for future use.

                    // User/Password data
                    last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(user, enc));

                    if ((serverCapabilities & NativeServerSession.CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) != 0) {
                        // send lenenc-int length of auth-response and string[n] auth-response
                        last_sent.writeBytes(StringSelfDataType.STRING_LENENC, toServer.get(0).readBytes(StringSelfDataType.STRING_EOF));
                    } else {
                        // send 1 byte length of auth-response and string[n] auth-response
                        last_sent.writeInteger(IntegerDataType.INT1, toServer.get(0).getPayloadLength());
                        last_sent.writeBytes(StringSelfDataType.STRING_EOF, toServer.get(0).getByteBuffer());
                    }

                    if (this.useConnectWithDb) {
                        last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(database, enc));
                    }

                    if ((serverCapabilities & NativeServerSession.CLIENT_PLUGIN_AUTH) != 0) {
                        last_sent.writeBytes(StringSelfDataType.STRING_TERM, StringUtils.getBytes(plugin.getProtocolPluginName(), enc));
                    }

                    // connection attributes
                    if (((clientParam & NativeServerSession.CLIENT_CONNECT_ATTRS) != 0)) {
                        appendConnectionAttributes(last_sent, this.propertySet.getStringProperty(PropertyKey.connectionAttributes).getValue(), enc);
                    }

                    this.protocol.send(last_sent, last_sent.getPosition());
                }

            }

        }

        if (counter == 0) {
            throw ExceptionFactory.createException(WrongArgumentException.class,
                    Messages.getString("CommunicationsException.TooManyAuthenticationPluginNegotiations"), getExceptionInterceptor());
        }

        this.protocol.afterHandshake();

        if (!this.useConnectWithDb) {
            this.protocol.changeDatabase(database);
        }

    }

    private Map<String, String> getConnectionAttributesMap(String attStr) {

        Map<String, String> attMap = new HashMap<>();

        if (attStr != null) {
            String[] pairs = attStr.split(",");
            for (String pair : pairs) {
                int keyEnd = pair.indexOf(":");
                if (keyEnd > 0 && (keyEnd + 1) < pair.length()) {
                    attMap.put(pair.substring(0, keyEnd), pair.substring(keyEnd + 1));
                }
            }
        }

        // Leaving disabled until standard values are defined
        // props.setProperty("_os", NonRegisteringDriver.OS);
        // props.setProperty("_platform", NonRegisteringDriver.PLATFORM);
        attMap.put("_client_name", Constants.CJ_NAME);
        attMap.put("_client_version", Constants.CJ_VERSION);
        attMap.put("_runtime_vendor", Constants.JVM_VENDOR);
        attMap.put("_runtime_version", Constants.JVM_VERSION);
        attMap.put("_client_license", Constants.CJ_LICENSE);

        return attMap;
    }

    private void appendConnectionAttributes(NativePacketPayload buf, String attributes, String enc) {

        NativePacketPayload lb = new NativePacketPayload(100);
        Map<String, String> attMap = getConnectionAttributesMap(attributes);

        for (String key : attMap.keySet()) {
            lb.writeBytes(StringSelfDataType.STRING_LENENC, StringUtils.getBytes(key, enc));
            lb.writeBytes(StringSelfDataType.STRING_LENENC, StringUtils.getBytes(attMap.get(key), enc));
        }

        buf.writeInteger(IntegerDataType.INT_LENENC, lb.getPosition());
        buf.writeBytes(StringLengthDataType.STRING_FIXED, lb.getByteBuffer(), 0, lb.getPosition());
    }

    /**
     * Get the Java encoding to be used for the handshake
     * response. Defaults to UTF-8.
     * 
     * @return encoding name
     */
    public String getEncodingForHandshake() {
        String enc = this.propertySet.getStringProperty(PropertyKey.characterEncoding).getValue();
        if (enc == null) {
            enc = "UTF-8";
        }
        return enc;
    }

    public ExceptionInterceptor getExceptionInterceptor() {
        return this.exceptionInterceptor;
    }

    /**
     * Negotiates the SSL communications channel used when connecting
     * to a MySQL server that understands SSL.
     * 
     * @param packLength
     *            packet length
     */
    private void negotiateSSLConnection(int packLength) {
        this.protocol.negotiateSSLConnection(packLength);
    }

    /**
     * Re-authenticates as the given user and password
     * 
     * @param serverSession
     *            current {@link ServerSession}
     * @param userName
     *            user name
     * @param password
     *            password
     * @param database
     *            database name
     */
    @Override
    public void changeUser(ServerSession serverSession, String userName, String password, String database) {
        proceedHandshakeWithPluggableAuthentication(serverSession, userName, password, database, null);
    }

}