All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mariadb.jdbc.internal.protocol.AbstractConnectProtocol Maven / Gradle / Ivy

There is a newer version: 0.40.13
Show newest version
/*
 *
 * MariaDB Client for Java
 *
 * Copyright (c) 2012-2014 Monty Program Ab.
 * Copyright (c) 2015-2017 MariaDB Ab.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to Monty Program Ab [email protected].
 *
 * This particular MariaDB Client for Java file is work
 * derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to
 * the following copyright and notice provisions:
 *
 * Copyright (c) 2009-2011, Marcus Eriksson
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this list
 * of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this
 * list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.
 *
 * Neither the name of the driver nor the names of its contributors may not be
 * used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS  AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 *
 */

package org.mariadb.jdbc.internal.protocol;

import org.mariadb.jdbc.HostAddress;
import org.mariadb.jdbc.MariaDbConnection;
import org.mariadb.jdbc.UrlParser;
import org.mariadb.jdbc.internal.MariaDbServerCapabilities;
import org.mariadb.jdbc.internal.com.read.Buffer;
import org.mariadb.jdbc.internal.com.read.ErrorPacket;
import org.mariadb.jdbc.internal.com.read.OkPacket;
import org.mariadb.jdbc.internal.com.read.ReadInitialHandShakePacket;
import org.mariadb.jdbc.internal.com.read.dao.Results;
import org.mariadb.jdbc.internal.com.read.resultset.SelectResultSet;
import org.mariadb.jdbc.internal.com.send.*;
import org.mariadb.jdbc.internal.failover.FailoverProxy;
import org.mariadb.jdbc.internal.io.LruTraceCache;
import org.mariadb.jdbc.internal.io.input.DecompressPacketInputStream;
import org.mariadb.jdbc.internal.io.input.PacketInputStream;
import org.mariadb.jdbc.internal.io.input.StandardPacketInputStream;
import org.mariadb.jdbc.internal.io.output.CompressPacketOutputStream;
import org.mariadb.jdbc.internal.io.output.PacketOutputStream;
import org.mariadb.jdbc.internal.io.output.StandardPacketOutputStream;
import org.mariadb.jdbc.internal.logging.Logger;
import org.mariadb.jdbc.internal.logging.LoggerFactory;
import org.mariadb.jdbc.internal.protocol.authentication.AuthenticationProviderHolder;
import org.mariadb.jdbc.internal.protocol.authentication.DefaultAuthenticationProvider;
import org.mariadb.jdbc.internal.protocol.tls.HostnameVerifierImpl;
import org.mariadb.jdbc.internal.protocol.tls.MariaDbX509KeyManager;
import org.mariadb.jdbc.internal.protocol.tls.MariaDbX509TrustManager;
import org.mariadb.jdbc.internal.util.Options;
import org.mariadb.jdbc.internal.util.ServerPrepareStatementCache;
import org.mariadb.jdbc.internal.util.Utils;
import org.mariadb.jdbc.internal.util.constant.HaMode;
import org.mariadb.jdbc.internal.util.constant.ParameterConstant;
import org.mariadb.jdbc.internal.util.constant.ServerStatus;
import org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper;
import org.mariadb.jdbc.internal.util.pool.GlobalStateInfo;

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.nio.charset.Charset;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLNonTransientConnectionException;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

import static org.mariadb.jdbc.internal.com.Packet.*;

public abstract class AbstractConnectProtocol implements Protocol {
    private static final byte[] SESSION_QUERY = ("SELECT @@max_allowed_packet,"
            + "@@system_time_zone,"
            + "@@time_zone,"
            + "@@auto_increment_increment").getBytes(Buffer.UTF_8);
    private static final byte[] IS_MASTER_QUERY = "show global variables like 'innodb_read_only'".getBytes(Buffer.UTF_8);
    private static final Logger logger = LoggerFactory.getLogger(AbstractConnectProtocol.class);
    protected final ReentrantLock lock;
    protected final UrlParser urlParser;
    protected final Options options;
    private final String username;
    private final String password;
    public boolean hasWarnings = false;
    public Results activeStreamingResult = null;
    public short serverStatus;
    protected int autoIncrementIncrement;
    protected Socket socket;
    protected PacketOutputStream writer;
    protected boolean readOnly = false;
    protected PacketInputStream reader;
    private HostAddress currentHost;
    protected FailoverProxy proxy;
    protected volatile boolean connected = false;
    protected boolean explicitClosed = false;
    protected String database;
    protected long serverThreadId;
    protected ServerPrepareStatementCache serverPrepareStatementCache;
    protected boolean eofDeprecated = false;
    protected long serverCapabilities;
    private boolean hostFailed;
    private String serverVersion;
    private boolean serverMariaDb;
    private int majorVersion;
    private int minorVersion;
    private int patchVersion;
    private TimeZone timeZone;
    private final LruTraceCache traceCache = new LruTraceCache();
    private final GlobalStateInfo globalInfo;

    /**
     * Get a protocol instance.
     *
     * @param urlParser     connection URL information
     * @param globalInfo    server global variables information
     * @param lock          the lock for thread synchronisation
     */

    public AbstractConnectProtocol(final UrlParser urlParser, final GlobalStateInfo globalInfo, final ReentrantLock lock) {
        urlParser.auroraPipelineQuirks();
        this.lock = lock;
        this.urlParser = urlParser;
        this.options = urlParser.getOptions();
        this.database = (urlParser.getDatabase() == null ? "" : urlParser.getDatabase());
        this.username = (urlParser.getUsername() == null ? "" : urlParser.getUsername());
        this.password = (urlParser.getPassword() == null ? "" : urlParser.getPassword());
        this.globalInfo = globalInfo;
        if (options.cachePrepStmts && options.useServerPrepStmts) {
            serverPrepareStatementCache = ServerPrepareStatementCache.newInstance(options.prepStmtCacheSize, this);
        }
    }

    private static void closeSocket(PacketInputStream packetInputStream, PacketOutputStream packetOutputStream, Socket socket) {
        try {
            try {
                socket.shutdownOutput();
                socket.setSoTimeout(3);
                InputStream is = socket.getInputStream();
                //noinspection StatementWithEmptyBody
                while (is.read() != -1) {
                    //read byte
                }
            } catch (Throwable t) {
                //eat exception
            }
            packetOutputStream.close();
            packetInputStream.close();
        } catch (IOException e) {
            //eat
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                //socket closed, if any error, so not throwing error
            }
        }
    }

    /**
     * Closes socket and stream readers/writers Attempts graceful shutdown.
     */
    public void close() {
        if (lock != null) lock.lock();
        if (this.connected) {
            this.connected = false;
            try {
            /* If a streaming result set is open, close it.*/
                skip();
            } catch (Exception e) {
            /* eat exception */
            }

            SendClosePacket.send(writer);
            closeSocket(reader, writer, socket);
            cleanMemory();
        }
        if (lock != null) lock.unlock();
    }

    /**
     * Force closes socket and stream readers/writers.
     */
    public void abort() {
        this.explicitClosed = true;

        boolean lockStatus = false;
        if (lock != null) lockStatus = lock.tryLock();
        this.connected = false;

        abortActiveStream();

        if (!lockStatus) {
            //lock not available : query is running
            // force end by executing an KILL connection
            forceAbort();
            try {
                socket.setSoLinger(true, 0);
            } catch (IOException ioException) {
                //eat
            }
        } else {
            SendClosePacket.send(writer);
        }

        closeSocket(reader, writer, socket);
        cleanMemory();
        if (lockStatus) lock.unlock();
    }

    private void forceAbort() {
        MasterProtocol copiedProtocol = null;
        try {
            copiedProtocol = new MasterProtocol(urlParser, new GlobalStateInfo(), new ReentrantLock());
            copiedProtocol.setHostAddress(getHostAddress());
            copiedProtocol.connect();
            //no lock, because there is already a query running that possessed the lock.
            copiedProtocol.executeQuery("KILL " + serverThreadId);
        } catch (SQLException sqle) {
            //eat
        } finally {
            if (copiedProtocol != null) copiedProtocol.close();
        }
    }

    private void abortActiveStream() {
        try {
            /* If a streaming result set is open, abort it.*/
            if (activeStreamingResult != null) {
                activeStreamingResult.abort();
                activeStreamingResult = null;
            }
        } catch (Exception e) {
            /* eat exception */
        }
    }

    /**
     * Skip packets not read that are not needed.
     * Packets are read according to needs.
     * If some data have not been read before next execution, skip it.
     * 

* Lock must be set before using this method * * @throws SQLException exception */ public void skip() throws SQLException { if (activeStreamingResult != null) { activeStreamingResult.loadFully(true, this); activeStreamingResult = null; } } private void cleanMemory() { if (options.cachePrepStmts && options.useServerPrepStmts) serverPrepareStatementCache.clear(); if (options.enablePacketDebug) traceCache.clearMemory(); } public void setServerStatus(short serverStatus) { this.serverStatus = serverStatus; } public void removeHasMoreResults() { if (hasMoreResults()) this.serverStatus = (short) (serverStatus ^ ServerStatus.MORE_RESULTS_EXISTS); } private SSLSocketFactory getSslSocketFactory() throws SQLException { if (!options.trustServerCertificate && options.serverSslCert == null && options.trustStore == null && options.keyStore == null) { return (SSLSocketFactory) SSLSocketFactory.getDefault(); } TrustManager[] trustManager = null; KeyManager[] keyManager = null; if (options.trustServerCertificate || options.serverSslCert != null || options.trustStore != null) { trustManager = new X509TrustManager[]{new MariaDbX509TrustManager(options)}; } if (options.keyStore != null) { keyManager = new KeyManager[]{loadClientCerts(options.keyStore, options.keyStorePassword, options.keyPassword)}; } else { String keyStore = System.getProperty("javax.net.ssl.keyStore"); String keyStorePassword = System.getProperty("javax.net.ssl.keyStorePassword"); if (keyStore != null) { try { keyManager = new KeyManager[]{loadClientCerts(keyStore, keyStorePassword, keyStorePassword)}; } catch (SQLException queryException) { keyManager = null; logger.error("Error loading keymanager from system properties", queryException); } } } try { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManager, trustManager, null); return sslContext.getSocketFactory(); } catch (KeyManagementException keyManagementEx) { throw ExceptionMapper.connException("Could not initialize SSL context", keyManagementEx); } catch (NoSuchAlgorithmException noSuchAlgorithmEx) { throw ExceptionMapper.connException("SSLContext TLS Algorithm not unknown", noSuchAlgorithmEx); } } private KeyManager loadClientCerts(String keyStoreUrl, String keyStorePassword, String keyPassword) throws SQLException { InputStream inStream = null; try { char[] keyStorePasswordChars = keyStorePassword == null ? null : keyStorePassword.toCharArray(); try { inStream = new URL(keyStoreUrl).openStream(); } catch (IOException ioexception) { inStream = new FileInputStream(keyStoreUrl); } KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(inStream, keyStorePasswordChars); char[] keyStoreChars = (keyPassword == null) ? keyStorePasswordChars : keyPassword.toCharArray(); return new MariaDbX509KeyManager(ks, keyStoreChars); } catch (GeneralSecurityException generalSecurityEx) { throw ExceptionMapper.connException("Failed to create keyStore instance", generalSecurityEx); } catch (FileNotFoundException fileNotFoundEx) { throw ExceptionMapper.connException("Failed to find keyStore file. Option keyStore=" + keyStoreUrl, fileNotFoundEx); } catch (IOException ioEx) { throw ExceptionMapper.connException("Failed to read keyStore file. Option keyStore=" + keyStoreUrl, ioEx); } finally { try { if (inStream != null) inStream.close(); } catch (IOException ioEx) { //ignore error } } } /** * InitializeSocketOption. */ private void initializeSocketOption() { try { socket.setTcpNoDelay(options.tcpNoDelay); if (options.tcpKeepAlive) { socket.setKeepAlive(true); } if (options.tcpRcvBuf != null) { socket.setReceiveBufferSize(options.tcpRcvBuf); } if (options.tcpSndBuf != null) { socket.setSendBufferSize(options.tcpSndBuf); } if (options.tcpAbortiveClose) { socket.setSoLinger(true, 0); } } catch (Exception e) { logger.debug("Failed to set socket option", e); } } /** * Connect to currentHost. * * @throws SQLException exception */ public void connect() throws SQLException { if (!isClosed()) close(); try { connect((currentHost != null) ? currentHost.host : null, (currentHost != null) ? currentHost.port : 3306); } catch (IOException ioException) { throw ExceptionMapper.connException( "Could not connect to " + currentHost + ". " + ioException.getMessage() + getTraces(), ioException); } } /** * Connect the client and perform handshake. * * @param host host * @param port port * @throws SQLException handshake error, e.g wrong user or password * @throws IOException connection error (host/port not available) */ private void connect(String host, int port) throws SQLException, IOException { try { socket = Utils.createSocket(urlParser, host); if (options.socketTimeout != null) socket.setSoTimeout(options.socketTimeout); initializeSocketOption(); // Bind the socket to a particular interface if the connection property // localSocketAddress has been defined. if (options.localSocketAddress != null) { InetSocketAddress localAddress = new InetSocketAddress(options.localSocketAddress, 0); socket.bind(localAddress); } if (!socket.isConnected()) { InetSocketAddress sockAddr = urlParser.getOptions().pipe == null ? new InetSocketAddress(host, port) : null; if (options.connectTimeout != 0) { socket.connect(sockAddr, options.connectTimeout); } else { socket.connect(sockAddr); } } handleConnectionPhases(host); connected = true; if (options.useCompression) { writer = new CompressPacketOutputStream(writer.getOutputStream(), options.maxQuerySizeToLog); reader = new DecompressPacketInputStream(((StandardPacketInputStream) reader).getBufferedInputStream(), options.maxQuerySizeToLog); if (options.enablePacketDebug) { writer.setTraceCache(traceCache); reader.setTraceCache(traceCache); } } boolean mustLoadAdditionalInfo = true; if (globalInfo != null) { if (globalInfo.isAutocommit() == options.autocommit) mustLoadAdditionalInfo = false; } if (mustLoadAdditionalInfo) { Map serverData = new TreeMap(); if (options.usePipelineAuth && !options.createDatabaseIfNotExist) { try { sendPipelineAdditionalData(); readPipelineAdditionalData(serverData); } catch (SQLException sqle) { if ("08".equals(sqle.getSQLState())) throw sqle; //in case pipeline is not supported //(proxy flush socket after reading first packet) additionalData(serverData); } } else additionalData(serverData); writer.setMaxAllowedPacket(Integer.parseInt(serverData.get("max_allowed_packet"))); autoIncrementIncrement = Integer.parseInt(serverData.get("auto_increment_increment")); loadCalendar(serverData.get("time_zone"), serverData.get("system_time_zone")); } else { writer.setMaxAllowedPacket((int) globalInfo.getMaxAllowedPacket()); autoIncrementIncrement = globalInfo.getAutoIncrementIncrement(); loadCalendar(globalInfo.getTimeZone(), globalInfo.getSystemTimeZone()); } reader.setServerThreadId(this.serverThreadId, isMasterConnection()); writer.setServerThreadId(this.serverThreadId, isMasterConnection()); activeStreamingResult = null; hostFailed = false; } catch (IOException ioException) { ensureClosingSocketOnException(); throw ioException; } catch (SQLException sqlException) { ensureClosingSocketOnException(); throw sqlException; } } /** * Send all additional needed values. * Command are send one after the other, assuming that command are less than 65k * (minimum hosts TCP/IP buffer size) * * @throws IOException if socket exception occur */ private void sendPipelineAdditionalData() throws IOException { sendSessionInfos(); sendRequestSessionVariables(); //for aurora, check that connection is master sendPipelineCheckMaster(); } private void sendSessionInfos() throws IOException { // In JDBC, connection must start in autocommit mode // [CONJ-269] we cannot rely on serverStatus & ServerStatus.AUTOCOMMIT before this command to avoid this command. // if autocommit=0 is set on server configuration, DB always send Autocommit on serverStatus flag // after setting autocommit, we can rely on serverStatus value StringBuilder sessionOption = new StringBuilder("autocommit=").append(options.autocommit ? "1" : "0"); if ((serverCapabilities & MariaDbServerCapabilities.CLIENT_SESSION_TRACK) != 0) { sessionOption.append(", session_track_schema=1"); if (options.rewriteBatchedStatements) { sessionOption.append(", session_track_system_variables='auto_increment_increment' "); } } if (options.jdbcCompliantTruncation) { sessionOption.append(", sql_mode = concat(@@sql_mode,',STRICT_TRANS_TABLES')"); } if (options.sessionVariables != null && !options.sessionVariables.isEmpty()) { sessionOption.append(",").append(Utils.parseSessionVariables(options.sessionVariables)); } writer.startPacket(0); writer.write(COM_QUERY); writer.write("set " + sessionOption.toString()); writer.flush(); } private void sendRequestSessionVariables() throws IOException { writer.startPacket(0); writer.write(COM_QUERY); writer.write(SESSION_QUERY); writer.flush(); } private void readRequestSessionVariables(Map serverData) throws SQLException { Results results = new Results(); getResult(results); results.commandEnd(); ResultSet resultSet = results.getResultSet(); if (resultSet != null) { resultSet.next(); serverData.put("max_allowed_packet", resultSet.getString(1)); serverData.put("system_time_zone", resultSet.getString(2)); serverData.put("time_zone", resultSet.getString(3)); serverData.put("auto_increment_increment", resultSet.getString(4)); } else { throw new SQLException("Error reading SessionVariables results. Socket is connected ? " + socket.isConnected()); } } private void sendCreateDatabaseIfNotExist(String quotedDb) throws IOException { writer.startPacket(0); writer.write(COM_QUERY); writer.write("CREATE DATABASE IF NOT EXISTS " + quotedDb); writer.flush(); } private void sendUseDatabaseIfNotExist(String quotedDb) throws IOException { writer.startPacket(0); writer.write(COM_QUERY); writer.write("USE " + quotedDb); writer.flush(); } private void readPipelineAdditionalData(Map serverData) throws SQLException { SQLException resultingException = null; //read set session OKPacket try { getResult(new Results()); } catch (SQLException sqlException) { //must read all results, will be thrown only when all results are read. resultingException = sqlException; } boolean canTrySessionWithShow = false; try { readRequestSessionVariables(serverData); } catch (SQLException sqlException) { if (resultingException == null) { resultingException = ExceptionMapper.connException("could not load system variables", sqlException); canTrySessionWithShow = true; } } try { readPipelineCheckMaster(); } catch (SQLException sqlException) { canTrySessionWithShow = false; if (resultingException == null) { throw ExceptionMapper.connException("could not identified if server is master", sqlException); } } if (canTrySessionWithShow) { //fallback in case of galera non primary nodes that permit only show / set command, //not SELECT when not part of quorum requestSessionDataWithShow(serverData); } if (resultingException != null) throw resultingException; connected = true; } private void requestSessionDataWithShow(Map serverData) throws SQLException { try { Results results = new Results(); executeQuery(true, results, "SHOW VARIABLES WHERE Variable_name in (" + "'max_allowed_packet'," + "'system_time_zone'," + "'time_zone'," + "'auto_increment_increment')"); results.commandEnd(); ResultSet resultSet = results.getResultSet(); if (resultSet != null) { while (resultSet.next()) { if (logger.isDebugEnabled()) { logger.debug("server data {} = {}", resultSet.getString(1), resultSet.getString(2)); } serverData.put(resultSet.getString(1), resultSet.getString(2)); } if (serverData.size() < 4) { throw ExceptionMapper.connException("could not load system variables. socket connected: " + socket.isConnected()); } } } catch (SQLException sqlException) { throw ExceptionMapper.connException("could not load system variables", sqlException); } } private void additionalData(Map serverData) throws IOException, SQLException { sendSessionInfos(); getResult(new Results()); try { sendRequestSessionVariables(); readRequestSessionVariables(serverData); } catch (SQLException sqlException) { requestSessionDataWithShow(serverData); } //for aurora, check that connection is master sendPipelineCheckMaster(); readPipelineCheckMaster(); if (options.createDatabaseIfNotExist && !database.isEmpty()) { // Try to create the database if it does not exist String quotedDb = MariaDbConnection.quoteIdentifier(this.database); sendCreateDatabaseIfNotExist(quotedDb); getResult(new Results()); sendUseDatabaseIfNotExist(quotedDb); getResult(new Results()); } } private void ensureClosingSocketOnException() { if (socket != null) { try { socket.close(); } catch (IOException ioe) { //eat exception } } } /** * Is the connection closed. * * @return true if the connection is closed */ public boolean isClosed() { return !this.connected; } private void handleConnectionPhases(String host) throws SQLException { try { reader = new StandardPacketInputStream(socket.getInputStream(), options.maxQuerySizeToLog); writer = new StandardPacketOutputStream(socket.getOutputStream(), options.maxQuerySizeToLog); if (options.enablePacketDebug) { writer.setTraceCache(traceCache); reader.setTraceCache(traceCache); } final ReadInitialHandShakePacket greetingPacket = new ReadInitialHandShakePacket(reader); this.serverThreadId = greetingPacket.getServerThreadId(); reader.setServerThreadId(this.serverThreadId, null); writer.setServerThreadId(this.serverThreadId, null); this.serverVersion = greetingPacket.getServerVersion(); this.serverMariaDb = greetingPacket.isServerMariaDb(); this.serverCapabilities = greetingPacket.getServerCapabilities(); byte exchangeCharset = decideLanguage(greetingPacket.getServerLanguage() & 0xFF); parseVersion(); long clientCapabilities = initializeClientCapabilities(serverCapabilities); byte packetSeq = 1; if (options.useSsl && (greetingPacket.getServerCapabilities() & MariaDbServerCapabilities.SSL) != 0) { clientCapabilities |= MariaDbServerCapabilities.SSL; SendSslConnectionRequestPacket amcap = new SendSslConnectionRequestPacket(clientCapabilities, exchangeCharset); amcap.send(writer); SSLSocketFactory sslSocketFactory = getSslSocketFactory(); SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); enabledSslProtocolSuites(sslSocket); enabledSslCipherSuites(sslSocket); sslSocket.setUseClientMode(true); sslSocket.startHandshake(); // perform hostname verification // (rfc2818 indicate that if "client has external information as to the expected identity of the server, // the hostname check MAY be omitted") if (!options.disableSslHostnameVerification && !options.trustServerCertificate) { HostnameVerifierImpl hostnameVerifier = new HostnameVerifierImpl(); SSLSession session = sslSocket.getSession(); if (!hostnameVerifier.verify(host, session, serverThreadId)) { //Use proprietary verify method in order to have an exception with a better description of error. try { Certificate[] certs = session.getPeerCertificates(); X509Certificate cert = (X509Certificate) certs[0]; hostnameVerifier.verify(host, cert, serverThreadId); } catch (SSLException ex) { throw new SQLNonTransientConnectionException("SSL hostname verification failed : " + ex.getMessage() + "\nThis verification can be disabled using the option \"disableSslHostnameVerification\" " + "but won't prevent man-in-the-middle attacks anymore", "08006"); } } } socket = sslSocket; writer = new StandardPacketOutputStream(socket.getOutputStream(), options.maxQuerySizeToLog); reader = new StandardPacketInputStream(socket.getInputStream(), options.maxQuerySizeToLog); if (options.enablePacketDebug) { writer.setTraceCache(traceCache); reader.setTraceCache(traceCache); } packetSeq++; } else if (options.useSsl) { throw new SQLException("Trying to connect with ssl, but ssl not enabled in the server"); } authentication(exchangeCharset, clientCapabilities, packetSeq, greetingPacket); } catch (IOException ioException) { if (reader != null) { try { reader.close(); } catch (IOException ee) { //eat exception } } if (currentHost == null) { throw ExceptionMapper.connException( "Could not connect to socket : " + ioException.getMessage(), ioException); } throw ExceptionMapper.connException( "Could not connect to " + currentHost.host + ":" + currentHost.port + " : " + ioException.getMessage(), ioException); } } private void authentication(byte exchangeCharset, long clientCapabilities, byte packetSeq, ReadInitialHandShakePacket greetingPacket) throws SQLException, IOException { //send handshake response SendHandshakeResponsePacket.send(writer, this.username, this.password, this.currentHost, database, clientCapabilities, serverCapabilities, exchangeCharset, packetSeq, options, greetingPacket); Buffer buffer = reader.getPacket(false); if ((buffer.getByteAt(0) & 0xFF) == 0xFE) { writer.permitTrace(false); InterfaceAuthSwitchSendResponsePacket interfaceSendPacket; if ((serverCapabilities & MariaDbServerCapabilities.PLUGIN_AUTH) != 0) { buffer.readByte(); byte[] authData; String plugin; if (buffer.remaining() > 0) { //AuthSwitchRequest packet. plugin = buffer.readStringNullEnd(Charset.forName("ASCII")); authData = buffer.readRawBytes(buffer.remaining()); } else { //OldAuthSwitchRequest plugin = DefaultAuthenticationProvider.MYSQL_OLD_PASSWORD; authData = Utils.copyWithLength(greetingPacket.getSeed(), 8); } //Authentication according to plugin. //see AuthenticationProviderHolder for implement other plugin interfaceSendPacket = AuthenticationProviderHolder.getAuthenticationProvider() .processAuthPlugin(reader, plugin, password, authData, reader.getLastPacketSeq() + 1, options.passwordCharacterEncoding); } else { interfaceSendPacket = new SendOldPasswordAuthPacket(this.password, Utils.copyWithLength(greetingPacket.getSeed(), 8), reader.getLastPacketSeq() + 1, options.passwordCharacterEncoding); } interfaceSendPacket.send(writer); interfaceSendPacket.handleResultPacket(reader); } else { if (buffer.getByteAt(0) == ERROR) { ErrorPacket errorPacket = new ErrorPacket(buffer); if (password != null && !password.isEmpty() && errorPacket.getErrorNumber() == 1045 && "28000".equals(errorPacket.getSqlState())) { //Access denied throw new SQLException(errorPacket.getMessage() + "\nCurrent charset is " + Charset.defaultCharset().displayName() + ". If password has been set using other charset, consider " + "using option 'passwordCharacterEncoding'", errorPacket.getSqlState(), errorPacket.getErrorNumber()); } throw new SQLException(errorPacket.getMessage(), errorPacket.getSqlState(), errorPacket.getErrorNumber()); } serverStatus = new OkPacket(buffer).getServerStatus(); } writer.permitTrace(true); } private long initializeClientCapabilities(long serverCapabilities) { long capabilities = MariaDbServerCapabilities.IGNORE_SPACE | MariaDbServerCapabilities.CLIENT_PROTOCOL_41 | MariaDbServerCapabilities.TRANSACTIONS | MariaDbServerCapabilities.SECURE_CONNECTION | MariaDbServerCapabilities.LOCAL_FILES | MariaDbServerCapabilities.MULTI_RESULTS | MariaDbServerCapabilities.PS_MULTI_RESULTS | MariaDbServerCapabilities.FOUND_ROWS | MariaDbServerCapabilities.PLUGIN_AUTH | MariaDbServerCapabilities.CONNECT_ATTRS | MariaDbServerCapabilities.PLUGIN_AUTH_LENENC_CLIENT_DATA | MariaDbServerCapabilities.CLIENT_SESSION_TRACK; if (options.allowMultiQueries || (options.rewriteBatchedStatements)) { capabilities |= MariaDbServerCapabilities.MULTI_STATEMENTS; } if ((serverCapabilities & MariaDbServerCapabilities.CLIENT_DEPRECATE_EOF) != 0) { capabilities |= MariaDbServerCapabilities.CLIENT_DEPRECATE_EOF; eofDeprecated = true; } if (options.useCompression) { if ((serverCapabilities & MariaDbServerCapabilities.COMPRESS) == 0) { //ensure that server has compress capacity - MaxScale doesn't options.useCompression = false; } else { capabilities |= MariaDbServerCapabilities.COMPRESS; } } if (options.interactiveClient) { capabilities |= MariaDbServerCapabilities.CLIENT_INTERACTIVE; } // If a database is given, but createDatabaseIfNotExist is not defined or is false, // then just try to connect to the given database if (!database.isEmpty() && !options.createDatabaseIfNotExist) { capabilities |= MariaDbServerCapabilities.CONNECT_WITH_DB; } return capabilities; } private void loadCalendar(final String srvTimeZone, final String srvSystemTimeZone) throws SQLException { if (options.useLegacyDatetimeCode) { //legacy use client timezone timeZone = Calendar.getInstance().getTimeZone(); } else { //use server time zone String tz = options.serverTimezone; if (tz == null) { tz = srvTimeZone; if ("SYSTEM".equals(tz)) { tz = srvSystemTimeZone; } } //handle custom timezone id if (tz != null && tz.length() >= 2 && (tz.startsWith("+") || tz.startsWith("-")) && Character.isDigit(tz.charAt(1))) { tz = "GMT" + tz; } try { timeZone = Utils.getTimeZone(tz); } catch (SQLException e) { if (options.serverTimezone != null) { throw new SQLException("The server time_zone '" + tz + "' defined in the 'serverTimezone' parameter cannot be parsed " + "by java TimeZone implementation. See java.util.TimeZone#getAvailableIDs() for available TimeZone, depending on your " + "JRE implementation.", "01S00"); } else { throw new SQLException("The server time_zone '" + tz + "' cannot be parsed. The server time zone must defined in the " + "jdbc url string with the 'serverTimezone' parameter (or server time zone must be defined explicitly with " + "sessionVariables=time_zone='Canada/Atlantic' for example). See " + "java.util.TimeZone#getAvailableIDs() for available TimeZone, depending on your JRE implementation.", "01S00"); } } } } /** * Check that current connection is a master connection (not read-only) * * @return true if master * @throws SQLException if requesting infos for server fail. */ public boolean checkIfMaster() throws SQLException { return isMasterConnection(); } /** * Default collation used for string exchanges with server. * (always use utf8) * * @param serverLanguage server default collation * @return collation byte */ private byte decideLanguage(int serverLanguage) { //force UTF8mb4 if possible, UTF8 if not. if (serverLanguage == 45 //utf8mb4_general_ci || serverLanguage == 46 //utf8mb4_bin || (serverLanguage >= 224 && serverLanguage <= 247)) { return (byte) serverLanguage; } else if (serverLanguage == 33 //utf8_general_ci || serverLanguage == 83 //utf8_bin || serverLanguage == 223 //utf8_general_mysql500_ci || (serverLanguage >= 192 && serverLanguage <= 215)) { return (byte) serverLanguage; } return (byte) 224; //UTF8MB4_UNICODE_CI; } /** * Check that next read packet is a End-of-file packet. * * @throws SQLException if not a End-of-file packet * @throws IOException if connection error occur */ public void readEofPacket() throws SQLException, IOException { Buffer buffer = reader.getPacket(true); switch (buffer.getByteAt(0)) { case EOF: buffer.skipByte(); this.hasWarnings = buffer.readShort() > 0; this.serverStatus = buffer.readShort(); break; case ERROR: ErrorPacket ep = new ErrorPacket(buffer); throw new SQLException("Could not connect: " + ep.getMessage(), ep.getSqlState(), ep.getErrorNumber()); default: throw new SQLException("Unexpected packet type " + buffer.getByteAt(0) + " instead of EOF"); } } /** * Check that next read packet is a End-of-file packet. * * @throws SQLException if not a End-of-file packet * @throws IOException if connection error occur */ public void skipEofPacket() throws SQLException, IOException { Buffer buffer = reader.getPacket(true); switch (buffer.getByteAt(0)) { case EOF: break; case ERROR: ErrorPacket ep = new ErrorPacket(buffer); throw new SQLException("Could not connect: " + ep.getMessage(), ep.getSqlState(), ep.getErrorNumber()); default: throw new SQLException("Unexpected packet type " + buffer.getByteAt(0) + " instead of EOF"); } } public void setHostFailedWithoutProxy() { hostFailed = true; close(); } public UrlParser getUrlParser() { return urlParser; } /** * Indicate if current protocol is a master protocol. * * @return is master flag */ public boolean isMasterConnection() { return currentHost == null || ParameterConstant.TYPE_MASTER.equals(currentHost.type); } /** * Send query to identify if server is master. * * @throws IOException in case of socket error. */ private void sendPipelineCheckMaster() throws IOException { if (urlParser.getHaMode() == HaMode.AURORA) { writer.startPacket(0); writer.write(COM_QUERY); writer.write(IS_MASTER_QUERY); writer.flush(); } } public void readPipelineCheckMaster() throws SQLException { //nothing if not aurora } public boolean mustBeMasterConnection() { return true; } public boolean noBackslashEscapes() { return ((serverStatus & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); } /** * Connect without proxy. (use basic failover implementation) * * @throws SQLException exception */ public void connectWithoutProxy() throws SQLException { if (!isClosed()) close(); List addrs = urlParser.getHostAddresses(); LinkedList hosts = new LinkedList(addrs); if (urlParser.getHaMode().equals(HaMode.LOADBALANCE)) { Collections.shuffle(hosts); } //CONJ-293 : handle name-pipe without host if (hosts.isEmpty() && options.pipe != null) { try { connect(null, 0); return; } catch (IOException ioException) { throw ExceptionMapper.connException( "Could not connect to named pipe '" + options.pipe + "' : " + ioException.getMessage() + getTraces(), ioException); } } // There could be several addresses given in the URL spec, try all of them, and throw exception if all hosts // fail. while (!hosts.isEmpty()) { currentHost = hosts.poll(); try { connect(currentHost.host, currentHost.port); return; } catch (SQLException e) { if (hosts.isEmpty()) { throw ExceptionMapper.getException(e, null, null, false); } } catch (IOException ioException) { if (hosts.isEmpty()) { throw ExceptionMapper.connException( "Could not connect to " + HostAddress.toString(addrs) + " : " + ioException.getMessage() + getTraces(), ioException); } } } } public boolean shouldReconnectWithoutProxy() { return (((serverStatus & ServerStatus.IN_TRANSACTION) == 0) && hostFailed && urlParser.getOptions().autoReconnect); } public String getServerVersion() { return serverVersion; } public boolean getReadonly() { return readOnly; } public void setReadonly(final boolean readOnly) { this.readOnly = readOnly; } public HostAddress getHostAddress() { return currentHost; } public void setHostAddress(HostAddress host) { this.currentHost = host; this.readOnly = ParameterConstant.TYPE_SLAVE.equals(this.currentHost.type); } public String getHost() { return (currentHost == null) ? null : currentHost.host; } public FailoverProxy getProxy() { return proxy; } public void setProxy(FailoverProxy proxy) { this.proxy = proxy; } public int getPort() { return (currentHost == null) ? 3306 : currentHost.port; } public String getDatabase() { return database; } public String getUsername() { return username; } private void parseVersion() { int length = serverVersion.length(); char car; int offset = 0; int type = 0; int val = 0; for (;offset < length; offset++) { car = serverVersion.charAt(offset); if (car < '0' || car > '9') { switch (type) { case 0: majorVersion = val; break; case 1: minorVersion = val; break; case 2: patchVersion = val; return; default: break; } type++; val = 0; } else { val = val * 10 + car - 48; } } //serverVersion finished by number like "5.5.57", assign patchVersion if (type == 2) patchVersion = val; } public int getMajorServerVersion() { return majorVersion; } public int getMinorServerVersion() { return minorVersion; } /** * Return possible protocols : values of option enabledSslProtocolSuites is set, or default to "TLSv1,TLSv1.1". * MariaDB versions ≥ 10.0.15 and ≥ 5.5.41 supports TLSv1.2 if compiled with openSSL (default). * MySQL community versions ≥ 5.7.10 is compile with yaSSL, so max TLS is TLSv1.1. * * @param sslSocket current sslSocket * @throws SQLException if protocol isn't a supported protocol */ private void enabledSslProtocolSuites(SSLSocket sslSocket) throws SQLException { if (options.enabledSslProtocolSuites == null) { sslSocket.setEnabledProtocols(new String[]{"TLSv1", "TLSv1.1"}); } else { List possibleProtocols = Arrays.asList(sslSocket.getSupportedProtocols()); String[] protocols = options.enabledSslProtocolSuites.split("[,;\\s]+"); for (String protocol : protocols) { if (!possibleProtocols.contains(protocol)) { throw new SQLException("Unsupported SSL protocol '" + protocol + "'. Supported protocols : " + possibleProtocols.toString().replace("[", "").replace("]", "")); } } sslSocket.setEnabledProtocols(protocols); } } /** * Set ssl socket cipher according to options. * * @param sslSocket current ssl socket * @throws SQLException if a cipher isn't known */ private void enabledSslCipherSuites(SSLSocket sslSocket) throws SQLException { if (options.enabledSslCipherSuites != null) { List possibleCiphers = Arrays.asList(sslSocket.getSupportedCipherSuites()); String[] ciphers = options.enabledSslCipherSuites.split("[,;\\s]+"); for (String cipher : ciphers) { if (!possibleCiphers.contains(cipher)) { throw new SQLException("Unsupported SSL cipher '" + cipher + "'. Supported ciphers : " + possibleCiphers.toString().replace("[", "").replace("]", "")); } } sslSocket.setEnabledCipherSuites(ciphers); } } /** * Utility method to check if database version is greater than parameters. * * @param major major version * @param minor minor version * @param patch patch version * @return true if version is greater than parameters */ public boolean versionGreaterOrEqual(int major, int minor, int patch) { if (this.majorVersion > major) { return true; } if (this.majorVersion < major) { return false; } /* * Major versions are equal, compare minor versions */ if (this.minorVersion > minor) { return true; } if (this.minorVersion < minor) { return false; } //Minor versions are equal, compare patch version. return this.patchVersion >= patch; } public boolean getPinGlobalTxToPhysicalConnection() { return this.options.pinGlobalTxToPhysicalConnection; } /** * Has warnings. * * @return true if as warnings. */ public boolean hasWarnings() { lock.lock(); try { return hasWarnings; } finally { lock.unlock(); } } /** * Is connected. * * @return true if connected */ public boolean isConnected() { lock.lock(); try { return connected; } finally { lock.unlock(); } } public long getServerThreadId() { return serverThreadId; } public boolean isExplicitClosed() { return explicitClosed; } public TimeZone getTimeZone() { return timeZone; } public Options getOptions() { return options; } public void setHasWarnings(boolean hasWarnings) { this.hasWarnings = hasWarnings; } public Results getActiveStreamingResult() { return activeStreamingResult; } public void setActiveStreamingResult(Results activeStreamingResult) { this.activeStreamingResult = activeStreamingResult; } /** * Remove exception result and since totally fetched, set fetch size to 0. */ public void removeActiveStreamingResult() { if (this.activeStreamingResult != null) { this.activeStreamingResult.removeFetchSize(); this.activeStreamingResult = null; } } @Override public ReentrantLock getLock() { return lock; } @Override public boolean hasMoreResults() { return (serverStatus & ServerStatus.MORE_RESULTS_EXISTS) != 0; } public ServerPrepareStatementCache prepareStatementCache() { return serverPrepareStatementCache; } public abstract void executeQuery(final String sql) throws SQLException; /** * Change Socket TcpNoDelay option. * * @param setTcpNoDelay value to set. */ public void changeSocketTcpNoDelay(boolean setTcpNoDelay) { try { socket.setTcpNoDelay(setTcpNoDelay); } catch (SocketException socketException) { //eat exception } } public void changeSocketSoTimeout(int setSoTimeout) throws SocketException { socket.setSoTimeout(setSoTimeout); } public boolean isServerMariaDb() { return serverMariaDb; } public PacketInputStream getReader() { return reader; } public PacketOutputStream getWriter() { return writer; } public boolean isEofDeprecated() { return eofDeprecated; } public boolean sessionStateAware() { return (serverCapabilities & MariaDbServerCapabilities.CLIENT_SESSION_TRACK) != 0; } public String getTraces() { if (options.enablePacketDebug) return traceCache.printStack(); return ""; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy