/*
 * Copyright (c) 2018, 2021, Oracle and/or its affiliates.
 *
 * 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.x;

import java.nio.channels.ClosedChannelException;
import java.util.Arrays;
import java.util.List;

import com.mysql.cj.conf.PropertyDefinitions.AuthMech;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.conf.PropertySet;
import com.mysql.cj.conf.RuntimeProperty;
import com.mysql.cj.exceptions.CJCommunicationsException;
import com.mysql.cj.exceptions.ExceptionInterceptor;
import com.mysql.cj.exceptions.MysqlErrorNumbers;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.protocol.AuthenticationProvider;
import com.mysql.cj.protocol.Protocol;
import com.mysql.cj.util.StringUtils;
import com.mysql.cj.xdevapi.XDevAPIError;

public class XAuthenticationProvider implements AuthenticationProvider<XMessage> {

    XProtocol protocol;
    private AuthMech authMech = null; // Used in test case SecureSessionTest#testAuthMechanisns() to check what type of the authentication was actually used.
    private XMessageBuilder messageBuilder = new XMessageBuilder();

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

    @Override
    public void connect(String userName, String password, String database) {
        changeUser(userName, password, database);
    }

    @Override
    public void changeUser(String userName, String password, String database) {
        boolean overTLS = ((XServerCapabilities) this.protocol.getServerSession().getCapabilities()).getTls();
        RuntimeProperty<AuthMech> authMechProp = this.protocol.getPropertySet().<AuthMech>getEnumProperty(PropertyKey.xdevapiAuth);
        List<AuthMech> tryAuthMech;
        if (overTLS || authMechProp.isExplicitlySet()) {
            tryAuthMech = Arrays.asList(authMechProp.getValue());
        } else {
            tryAuthMech = Arrays.asList(AuthMech.MYSQL41, AuthMech.SHA256_MEMORY);
        }

        XProtocolError capturedAuthErr = null;
        for (AuthMech am : tryAuthMech) {
            this.authMech = am;
            try {
                switch (this.authMech) {
                    case SHA256_MEMORY:
                        this.protocol.send(this.messageBuilder.buildSha256MemoryAuthStart(), 0);
                        byte[] nonce = this.protocol.readAuthenticateContinue();
                        this.protocol.send(this.messageBuilder.buildSha256MemoryAuthContinue(userName, password, nonce, database), 0);
                        break;
                    case MYSQL41:
                        this.protocol.send(this.messageBuilder.buildMysql41AuthStart(), 0);
                        byte[] salt = this.protocol.readAuthenticateContinue();
                        this.protocol.send(this.messageBuilder.buildMysql41AuthContinue(userName, password, salt, database), 0);
                        break;
                    case PLAIN:
                        if (overTLS) {
                            this.protocol.send(this.messageBuilder.buildPlainAuthStart(userName, password, database), 0);
                        } else {
                            throw new XProtocolError("PLAIN authentication is not allowed via unencrypted connection.");
                        }
                        break;
                    case EXTERNAL:
                        this.protocol.send(this.messageBuilder.buildExternalAuthStart(database), 0);
                        break;
                    default:
                        throw new WrongArgumentException("Unknown authentication mechanism '" + this.authMech + "'.");
                }
            } catch (CJCommunicationsException e) {
                if (capturedAuthErr != null && e.getCause() instanceof ClosedChannelException) {
                    // High probability that the server doesn't support authentication sequences. Ignore this exception and throw the previous one.
                    throw capturedAuthErr;
                }
                throw e;
            }

            try {
                this.protocol.readAuthenticateOk();
                // Clear any captured exception and stop trying remaining auth mechanisms.
                capturedAuthErr = null;
                break;
            } catch (XProtocolError e) {
                if (e.getErrorCode() != MysqlErrorNumbers.ER_ACCESS_DENIED_ERROR) {
                    throw e;
                }
                capturedAuthErr = e;
            }
        }

        if (capturedAuthErr != null) {
            if (tryAuthMech.size() == 1) {
                throw capturedAuthErr;
            }
            // More than one authentication mechanism was tried.
            String errMsg = "Authentication failed using " + StringUtils.joinWithSerialComma(tryAuthMech)
                    + ", check username and password or try a secure connection";
            XDevAPIError ex = new XDevAPIError(errMsg, capturedAuthErr);
            ex.setVendorCode(capturedAuthErr.getErrorCode());
            ex.setSQLState(capturedAuthErr.getSQLState());
            throw ex;
        }

        this.protocol.afterHandshake();
    }

}
