UNPKG

@mysql/xdevapi

Version:

MySQL Connector/Node.js - A Node.js driver for MySQL using the X Protocol and X DevAPI.

1,251 lines (1,122 loc) 53.5 kB
/* * Copyright (c) 2021, 2022, 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/Node.js, 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 */ 'use strict'; const Client = require('../Protocol/Client'); const Net = require('net'); const TLS = require('tls'); const IntegerType = require('../Protocol/Wrappers/ScalarValues/int64').Type; const authenticationManager = require('../Authentication/AuthenticationManager'); const errors = require('../constants/errors'); const logger = require('../logger'); const multiHost = require('../topology/multi-host'); const pkg = require('../package'); const secureContext = require('../tls/secure-context'); const srv = require('../topology/dns-srv'); const system = require('../system'); const util = require('util'); const warnings = require('../constants/warnings'); const { isValidBoolean, isValidInteger, isValidPlainObject } = require('../validator'); /** * This module specifies the interface of an internal MySQL server connection. * @module Connection */ /** * Convert either all integers to a BigInt or to a string, or alternatively, * convert only unsafe integers to a BigInt or to a string. * @readonly * @name IntegerType * @enum {string} * @example * IntegerType.BIGINT * IntegerType.STRING * IntegerType.UNSAFE_BIGINT * IntegerType.UNSAFE_BIGINT */ /** * MySQL server endpoint details. * @typedef {Object} Endpoint * @prop {string} [host=localhost] - hostname or IP (v4 or v6) of a MySQL server instance * @prop {number} [port=33060] - X Plugin port on the MySQL server instance * @prop {number} [priority] - priority of an endpoint relative to the others (endpoints with higher priority are picked first) * @prop {string} [socket] - relative or absolute path of a local Unix socket file */ /** * Connection TLS-specific properties. * @typedef {Object} TLS * @prop {boolean} [enabled=true] - enables or disables TLS * @prop {string} [ca] - path of a file containing a certificate authority chain used to verify the server certificate * @prop {string} [crl] - path of a file containing a certificate revocation list used, alongside a certificate authority, to verify the server certificate * @prop {string[]} [versions=TLSv1.2, TLSv1.3] - restrict the list of allowed TLS versions (TLSv1.2, TLSv1.3) * @prop {string[]} [ciphershuites] - list of ciphersuites to allow (IANA syntax) */ /** * Connection configuration properties. * @typedef {Object} Properties * @prop {string} [auth] - name of the client-side authentication mechanism to use * @prop {number} [connectTimeout=10000] - maximum ammount of time (ms) to wait for a server connection to be opened * @prop {Object} [connectionAttributes={}] - key-value object containing names and values of session attributes * @prop {module:Connection~Endpoint[]} [endpoints=[]] - list of endpoints to connect to * @prop {string} [host=localhost] - hostname or IP (v4 or v6) of a MySQL server instance * @prop {string} [password] - password for the MySQL account (defaults to '') * @prop {number} [port=33060] - X Plugin port on the MySQL server instance * @prop {boolean} [resolveSrv=false] - enable or disable DNS SRV resolution * @prop {string} [schema] - default database to connect to (defaults to '') * @prop {string} [socket] - relative or absolute path of a local Unix socket file * @prop {boolean} [resolveSrv=false] - use the host to perform a DNS SRV lookup and obtain the list of endpoints * @prop {module:Connection~TLS} [tls] - TLS options * @prop {string} [user] - user of the MySQL account (defaults to '') */ const VALID_OPTIONS = [ 'auth', 'connectTimeout', 'connectionAttributes', 'dbPassword', // deprecated 'dbUser', // deprecated 'endpoints', 'host', 'integerType', 'password', 'port', 'resolveSrv', 'schema', 'socket', 'ssl', // deprecated 'sslOptions', // deprecated 'tls', 'user' ]; /** * @private * @typedef {Object} DefaultSessionAttributes Default set of session * attributes created by the client when connecting to a MySQL server that * supports session attributes. * @prop {string} _pid - client process id * @prop {string} _platform - platform name of the processor architecture * @prop {string} _os - name and version of the operating system * @prop {string} _client_name - name of the client product * @prop {string} _client_version - version of the client product * @prop {string} _client_license - license name of the client product */ const CLIENT_SESSION_ATTRIBUTES = { _pid: system.pid(), _platform: system.platform(), _os: system.brand(), _source_host: system.hostname(), _client_name: pkg.name(), _client_version: pkg.version(), _client_license: pkg.license() }; /** * Stringifies all the values in a given object. * @private * @param {Object} obj * @returns {Object} */ function stringifyValues (obj = {}) { let root = true; return JSON.stringify(obj, (k, v) => { // The replacer should skip the root node to avoid a stack overflow. if (root) { root = false; return v; } // If the value is undefined, in order to keep consistency between // connection strings and configuration objects, we coerce it to // an empty string. If the value is null (only possible with a // configuration object), we also coerce to an empty string in order // to avoid saving 'null'. if (typeof v === 'undefined' || v === null) { return ''; } // If the value is already a string, we should not do anything. if (typeof v === 'string') { return v; } // If the value is neither an object nor an array, we return its // stringified version. if (typeof v !== 'object') { return JSON.stringify(v); } // Otherwise we need to recursively do the same for the values in "v". return stringifyValues(v); }); } /** * @private * @typedef {Object} DeprecatedSSLOptions * @prop {string} [ca] - deprecated connection property for providing a path to a certificate authority file * @prop {string} [crl] - deprecated connection property for providing a path to a certificate revocation list file */ /** * Toggle deprecation warnings for deprecated connection properties. * @private * @param {Object} params * @param {string} [params.dbPassword] - deprecate connection property for the MySQL account password * @param {string} [params.dbUser] - deprecated connection property for the MySQL account user * @param {boolean} [params.ssl] - deprecated connection property for enabling or disabling TLS * @param {DeprecatedSSLOptions} [params.dbUser] - deprecated connection property for additional SSL options * @returns {boolean} */ function deprecate ({ dbPassword, dbUser, ssl, sslOptions }) { const log = logger('connection:options'); // The "dbPassword" property is deprecated. if (typeof dbPassword !== 'undefined') { log.warning('dbPassword', warnings.MESSAGES.WARN_DEPRECATED_DB_PASSWORD, { type: warnings.TYPES.DEPRECATION, code: warnings.CODES.DEPRECATION }); } // The "dbUser" property is deprecated. if (typeof dbUser !== 'undefined') { log.warning('dbUser', warnings.MESSAGES.WARN_DEPRECATED_DB_USER, { type: warnings.TYPES.DEPRECATION, code: warnings.CODES.DEPRECATION }); } // The "ssl" and "sslOptions" properties are deprecated. if (typeof ssl !== 'undefined') { log.warning('ssl', warnings.MESSAGES.WARN_DEPRECATED_SSL_OPTION, { type: warnings.TYPES.DEPRECATION, code: warnings.CODES.DEPRECATION }); } if (typeof sslOptions !== 'undefined') { log.warning('sslOptions', warnings.MESSAGES.WARN_DEPRECATED_SSL_ADDITIONAL_OPTIONS, { type: warnings.TYPES.DEPRECATION, code: warnings.CODES.DEPRECATION }); } return true; } /** * @private * @alias module:Connection * @param {Properties} - connection properties * @returns {module:Connection} */ function Connection ({ auth, connectionAttributes = {}, connectTimeout = 10000, dbPassword, dbUser, endpoints = [], host = 'localhost', integerType = IntegerType.UNSAFE_STRING, password = '', port = 33060, schema, socket, resolveSrv = false, ssl, sslOptions, tls = {}, user = '' } = {}) { // Internal connection state. const state = { // Contains the name of the authentication mechanism that is // effectively negotiated with the server. auth, // Indicates if the connection supports prepared statements (MySQL // 8.0.16 or higher). canPrepareStatements: true, // Contains the list of connection capabilities that were effectively // negotiated with the server. capabilities: {}, // Tracks an internal X Protocol client instance. client: null, // We keep a list of available and unavailable endpoints for // connection failover. endpoints: { // If there is a list of endpoints, we should use it. // If not, it should only include an endpoint with the details // specified by the "host", "port" and/or "socket" properties or // by the first and only element in that list. // When a connection is first opened, all endpoints should be // available. This can change if a socket cannot be created for // a given endpoint. available: endpoints.length ? endpoints : [{ host, port, socket }], unavailable: [] }, // Indicates the connection is being closed. isClosing: false, // Indicates if the connection established with the server // is using TLS. isSecure: false, // Indicates if the connection should be retried to the same endpoint. retry: false, // Indicates the number of milliseconds to wait before retrying a // specific endpoint. retryAfter: 20000, // Contains the connection id assigned by the server. serverId: null, // Contains the list of statements that were prepared and exist in the // scope of the associated server session. statements: [], // Merges any contents of the deprecated "ssl" and "sslOptions" // properties with the content of the "tls" property. // For tls.enable = true, we need to ensure that ssl !== false. // Additional options are merged with precedence for those defined // using the "tls" property, except for "ciphersuites" and "versions" // which we want to override given the validation constraints. tls: Object.assign({}, { enabled: ssl === false ? ssl : true }, sslOptions, tls), // Tracks the list of capabilities that the server does not know. unknownCapabilities: [] }; return { /** * Adds a set of connection capabilities to the existing ones. * @private * @function * @name module:Connection#addCapabilities * @param {Object} capabilities - set of capabilities * @returns {module:Connection} The connection instance */ addCapabilities (capabilities) { state.capabilities = Object.assign({}, state.capabilities, capabilities); return this; }, /** * Checks if the connection setup allows to retry authentication. * @private * @function * @name module:Connection:allowsAuthenticationRetry * @returns {boolean} */ allowsAuthenticationRetry () { // If the application provides its own authentication mechanism, // there is no reason to retry. if (this.hasCustomAuthenticationMechanism()) { return false; } // If the connection is secure, it means the authentication // failed because the credentials did not match, so we should // not allow a retry. if (state.endpoints.available[0].socket || state.tls.enabled) { return false; } // Which leaves us with checking if the server supports or not // the fallback authentication mechanism. return this.allowsAuthenticationWith('SHA256_MEMORY'); }, /** * Checks if a given authentication mechanism can be used with the * connection. * @private * @function * @name module:Connection#allowsAuthenticationWith * @param {string} mechanism - name of the authentication mechanism * @returns {boolean} */ allowsAuthenticationWith (mechanism) { // This is a workaround because on MySQL 5.7.x, calling // Mysqlx.Connection::CapabilitiesGet over a unix socket does not // return `PLAIN` while is in fact supported. const serverSupportedMechanisms = (state.capabilities['authentication.mechanisms'] || []).concat('PLAIN'); return serverSupportedMechanisms.indexOf(mechanism) > -1; }, /** * Negotiates the proper authentication mechanism for the connection user. * @private * @function * @name module:Connection#authenticate * @returns {module:Connection} The connection instance */ authenticate () { // Try the custom authentication mechanism, if it is provided, // PLAIN, if the connection is secure, or MYSQL41, if not. const mechanism = this.getAuth(); if (!this.allowsAuthenticationWith(mechanism)) { const message = util.format(errors.MESSAGES.ER_DEVAPI_AUTH_UNSUPPORTED_SERVER, mechanism); const error = new Error(message); error.info = { code: errors.ER_ACCESS_DENIED_ERROR, msg: message }; return Promise.reject(error); } return this.authenticateWith(mechanism) .catch(err => { // If the setup does not allow to retry the authentication // or if the error is not related to authentication we // should report it. if (!this.allowsAuthenticationRetry() || (err.info && err.info.code !== errors.ER_ACCESS_DENIED_ERROR)) { throw err; } // Otherwise we should perform the fallback strategy and // try with SHA256_MEMORY. return this.authenticateWith('SHA256_MEMORY') .catch(err => { // If it is not an authentication error, we should bubble that // error instead. if (err.info && err.info.code !== errors.ER_ACCESS_DENIED_ERROR) { throw err; } // Otherwise, we improve the existing error message. err.message = err.info.msg = errors.MESSAGES.ER_DEVAPI_AUTH_MORE_INFO; throw err; }); }); }, /** * Authenticates the connection user with a given mechanism. * @private * @function * @name module:Connection#authenticateWith * @param {string} mechanism - name of the authentication mechanism * @returns {Promise<module:Connection>} A Promise that resolves to the * connection instance. */ authenticateWith (mechanism) { const plugin = authenticationManager.getPlugin(mechanism); const theUser = this.getUser(); // We don't expose a getPassword() method because, the connection // is publicly available with session._getConnection() const thePassword = password || dbPassword || ''; return plugin({ password: thePassword, schema, user: theUser }).run(state.client) .then(session => { state.auth = mechanism; state.serverId = session.connectionId; return this; }); }, /** * Retrieves the server-side connection capabilities that have been * effectively negotiated. * @private * @function * @name module:Connection#capabilitiesGet * @returns {Promise} */ capabilitiesGet () { return state.client.capabilitiesGet(); }, /** * Negotiates a set of connection capabilities with the server * (X Plugin). * @private * @function * @name module:Connection#capabilitiesSet * @returns {Promise} */ capabilitiesSet () { // TODO(Rui): any additional capability, such as compression, can // be negotiated in this pipeline. const capabilities = {}; // If TLS is enabled and the connection is not using a local Unix socket, // we need to enable TLS in the X Plugin. if (state.tls.enabled === true && !state.endpoints.available[0].socket) { // The X Protocol capability name is "tls". capabilities.tls = true; } // If connection attributes are to be sent, we need to merge the // default client attributes. Additionally, if the server does // not support session attributes, the connection is re-tried and // we should disable them in the second attempt. if (connectionAttributes !== false && state.unknownCapabilities.indexOf('session_connect_attrs') === -1) { const attributes = Object.assign({}, CLIENT_SESSION_ATTRIBUTES, connectionAttributes); // The X Plugin requires all connection attributes to be // encoded as strings. The client attributes are already // expected to be strings. However, custom application // attributes that are not strings need to be coerced. // If they are not able to be coerced (e.g. null or // undefined), we should propagate the error reported by the // plugin. // The X Protocol capability name is "session_connect_attrs". capabilities.session_connect_attrs = JSON.parse(stringifyValues(attributes)); } return state.client.capabilitiesSet(capabilities) .then(() => { // Must return the client capabilities for further // processing. return capabilities; }) .catch(err => { // When TLS is not enabled in the server, we want // to report back a custom message. if (err.info && err.info.code === errors.ER_X_CAPABILITIES_PREPARE_FAILED) { // By TLS not being enabled, it means that the X // Plugin will report an error stating that the "tls" // capability failed. This is a generic error that can // mean any other capability is invalid, so we need to // ensure we are dealing with TLS. Currently the only // way to retrieve the capability name is to parse the // error message. const failedCapability = err.message.match(/Capability prepare failed for '([^']+)'.*/)[1]; // If the failure is not related to TLS, we should // report the error as is. if (failedCapability !== 'tls') { throw err; } // Otherwise, we want to report a custom message. const message = errors.MESSAGES.ER_DEVAPI_NO_SERVER_TLS; err.message = message; err.info.msg = message; throw err; } // When a new capability is introduced, it will be unknown // to older server versions. In that case, the server will // report an error and will close the connection. However, // since there is no way to ensure the capability is // supported beforehand, we have to try and send it anyway // asking for forgiveness instead of permission. Thus, in // order to make it seamless to the application, we need // to destroy the network socket and re-create the // connection without the unknown capabilities. if (!err.info || err.info.code !== errors.ER_X_CAPABILITY_NOT_FOUND) { throw err; } // By this point, the server has sent back an // ER_X_CAPABILITY_NOT_FOUND error and has closed the // connection. // So, the first thing we need to do is to track the // unknown capability. Currently the only way to retrieve // the capability name is to parse the error message. const unknownCapability = err.message.match(/Capability '([^']+)'.*/)[1]; state.unknownCapabilities.push(unknownCapability); // And we need to ensure that we re-create a connection to // the same endpoint. Since we are now tracking unknown // capabilities, any new connection will not try to // negotiate those. state.retry = true; const socket = state.client.getConnection(); socket.destroy(err); }); }, /** * Closes the X Protocol connection and the underlying connection * socket. * This method is overriden by PoolConnection * @private * @function * @name module:Connection#close * @returns {Promise} */ close () { return this.destroy(); }, /** * Creates a network socket to the most appropriate MySQL server * endpoint. * @private * @function * @name module:Connection#connect * @returns {Promise<module:Connection>} */ connect () { return new Promise((resolve, reject) => { // We want to connect to the first endpoint that is available. const endpoint = state.endpoints.available[0]; // In the case of multi-host connections, we want sane // defaults for each endpoint in the list, in order to // require less verbosity from the user. const nodeSocket = Net.connect({ host: endpoint.host || 'localhost', port: endpoint.port || 33060, path: endpoint.socket }); nodeSocket.setTimeout(connectTimeout); // Actions to perform after a connection is successfully // established (including server session). const postConnect = connection => { // We need to "remove" the timeout because we do not want // the event to be triggered after the connection is // effectively established. // For now, we consider "connectTimeout" to be the maximum // time it can take for a client socket (TLS or not) to // successfully connect to the server. nodeSocket.setTimeout(0); return resolve(connection); }; // Actions to perform when there is an error in the connection // stage. const postDisconnect = err => { // We should cleanup the connection state. this.reset(); // If there isn't an error, it means the socket has been // closed by the server and the client was expecting it. // There's nothing to do, the workflow is just finished // by this point. if (!err) { return resolve(); } // If there is an error, it means the socket has been // closed with an error and we need to report it back // to the application. return reject(err); }; // For now, we consider every error to happen at the // connection stage, to be fatal, and thus, we should stop and // close/destroy the connection. nodeSocket.once('ready', () => { // We can start creating the client instance, which will // hold the given connection. This is important because // we do not need to keep the raw socket available // everywhere but we still want to decouple the TLS logic. state.client = new Client(nodeSocket); // We can now start the process of creating a server-side // X Protocol session. return this.start().then(postConnect).catch(err => nodeSocket.destroy(err)); }); nodeSocket.once('error', err => { // The "close" event will automatically be triggered. state.error = err; }); nodeSocket.once('timeout', () => { const error = new Error(); error.name = 'ETIMEDOUT'; // The error message depends on whether the connection is // multi-host or not. if (!this.hasMultipleEndpoints()) { error.message = util.format(errors.MESSAGES.ER_DEVAPI_CONNECTION_TIMEOUT, connectTimeout); } else { error.message = util.format(errors.MESSAGES.ER_DEVAPI_MULTI_HOST_CONNECTION_TIMEOUT, connectTimeout); } // The connection must be manually closed. // https://nodejs.org/docs/latest/api/net.html#net_event_timeout nodeSocket.destroy(error); }); nodeSocket.on('data', data => { state.client.handleNetworkFragment(data); }); nodeSocket.once('close', hasError => { // When the endpoint becomes unvailable in the middle of // some work load, we should delegate error handling to // the specific worker. if (!hasError && this.isOpen() && this.isActive()) { // We are already past the connection stage. return state.client.handleServerClose(); } // When a fatal error happens in the server, it will close // the connection, but since it is not a network error, // "hasError" will be "false", so we need to account for // that cenario. In that case, we can check if the // connection is already active or not. if (!hasError && this.isOpen()) { // It means the connection was legitimately closed. return postDisconnect(); } // If we cannot retry, the current connection should // become unavailable. One reason to retry the current // connection is if the server is available but refused // the connection with a non-fatal error (such as when // a capability is not known). if (!state.retry) { // When a connection becomes unavailable, it should // include the timestamp of when it was last tried. const unavailable = Object.assign({}, state.endpoints.available.shift(), { unavailableAt: system.time() }); state.endpoints.unavailable.push(unavailable); } // If we are retrying now, we should prevent duplicate // retries. state.retry = false; // By this point, there should be an error available in // "state.error", which is updated for each error event in // the socket (but also includes other kinds of errors). const error = this.getError(); // We want to know if the the timeout has been reached for // all the endpoints, or if there was a different issue. if (!this.hasMoreEndpointsAvailable() && (!this.hasMultipleEndpoints() || error.name === 'ETIMEDOUT')) { return postDisconnect(error); } // If we are in a multi-host setup and there are no more // endpoints available, we want to raise a custom error // as well. if (!this.hasMoreEndpointsAvailable() && this.hasMultipleEndpoints()) { error.name = 'ENOMOREHOSTS'; error.errno = errors.ER_DEVAPI_MULTI_HOST_CONNECTION_FAILED; error.message = errors.MESSAGES.ER_DEVAPI_MULTI_HOST_CONNECTION_FAILED; return postDisconnect(error); } // By this point, the list of available and unavailable // endpoints is already up-to-date and if there are more // endpoints available, we should try to connect to the // next one in the list. this.connect().then(postConnect).catch(postDisconnect); }); }); }, /** * Asks the server to gracefully close the underlying X Protocol * connection. * @private * @function * @name module:Connection#destroy * @returns {Promise} */ destroy () { // This operation should be idempotent because there is a chance // that the connection might already been closed by the server // we should check first. if (!this.isOpen() || state.isClosing) { return Promise.resolve(); } const socket = state.client.getConnection(); // Again, the operation should be idempotent, so, if the socket // reference has already been destroyed, there is nothing to do. if (socket.destroyed) { return Promise.resolve(); } // Eventually, the socket is destroyed and a 'close' event is // emmitted. This is here just to make sure the operation does // not fail if the connection is, for some weird reason, manually // closed again by the application. state.isClosing = true; // The method is shared between this and "PoolConnection". // We cannot re-use "close()" because it is overrided by "PoolConnection". return state.client.connectionClose() .then(() => { // The server closes the socket on their end so we also // need to destroy our reference. // This will trigger a 'close' event whose handler is responsible // for further cleanup. socket.destroy(); // The connection has been safely closed by now. state.isClosing = false; }); }, /** * Enables TLS on the underlying network socket. * @private * @function * @name module:Connection#enableTLS * @returns {Promise} */ enableTLS () { return new Promise(resolve => { const secureContextOptions = Object.assign({}, state.tls); // We already know TLS should be enabled. delete secureContextOptions.enabled; // We need to create a secure socket by providing the existing // socket and the proper security context. const nodeSocket = TLS.connect(Object.assign({}, { socket: state.client.getConnection() }, secureContext.create(secureContextOptions))); nodeSocket.once('secureConnect', () => { state.isSecure = true; // Once a TLS session is established, we need to // update the socket reference. state.client.setConnection(nodeSocket); // Then we are done and can move along. resolve(); }); // We need to re-attach the event listener in order to // be able to handle OpenSSL errors. nodeSocket.once('error', err => { state.error = err; }); // The stream is paused when a secure connection is // established in the socket, so we need to resume the // flow. The handler should be the same. nodeSocket.on('data', data => { state.client.handleNetworkFragment(data); }); }); }, /** * Checks if there is any ongoing workload in the connection. * @private * @function * @name module:Connection#isActive * @returns {boolean} */ isActive () { return !!state.client && state.client.isRunning(); }, /** * Checks of the connection is being closed. * @private * @function module:Connection#isClosing * @returns {boolean} */ isClosing () { return state.isClosing; }, /** * Checks if the connection was created by a pool, which is never the * case for this API. * This method is overriden by {@link module:PoolConnection|PoolConnection}. * @private * @function * @name module:Connection#isFromPool * @returns {boolean} Returns false. */ isFromPool () { return false; }, /** * Checks if the connection is idle, which is never the case for this * API. * This method is overriden by {@link module:PoolConnection|PoolConnection}. * @private * @function * @name module:Connection#isIdle * @returns {boolean} Returns false. */ isIdle () { // Standalone connections never become idle on the client side. return false; }, /** * Checks if a server-side X Protocol connecion has been sucessfuly * established. * @private * @function * @name module:Connection#isOpen * @returns {boolean} */ isOpen () { return state.client !== null && state.serverId !== null; }, /** * Checks if we are in a middle of a connection retry. * @private * @function * @name module:Connection#isReconnecting * @returns {boolean} */ isReconnecting () { return state.retry; }, /** * Checks if the connection is using TLS. * @private * @function * @name module:Connection#isSecure * @returns {boolean} */ isSecure () { return state.isSecure; }, /** * Retrieves the name of the authentication mechanism that was * negotiated with the server. * @private * @function * @name module:Connection#getAuth * @returns {string} */ getAuth () { // If an authentication mechanism is specified, we should try to // use it. if (state.auth) { return state.auth; } // If one is not specified and the connection is not secure, we // should use a mechanism that works with most widespread plugin: // "mysql_native_password". if (!state.endpoints.available[0].socket && !state.tls.enabled) { return 'MYSQL41'; } // If the connection is secure, we should opt for the fastest // alternative which is sending credentials as clear text. return 'PLAIN'; }, /** * Retrieves the underlying X Protocol client instance. * @private * @function * @name module:Connection#getClient * @returns {Client} */ getClient () { return state.client; }, /** * Retrieves any error generated by the connection. * @private * @function * @name module:Connection#getError * @returns {Error} */ getError () { // It does not make sense to have a default error value as part of // the connection state. return state.error || new Error(errors.MESSAGES.ER_DEVAPI_CONNECTION_CLOSED); }, /** * Retrieves the conversion mode selected by the application to handle * downstream integer values. * @private * @function * @name module:Connection#getIntegerType * @returns {IntegerType} */ getIntegerType () { return integerType; }, /** * Retrieves the list of client-side ids associated to server-side * prepared statements created in the scope of the underlying X * Protocol session. * @private * @function * @name module:Connection#getPreparedStatements * @returns {number[]} */ getPreparedStatements () { return state.statements; }, /** * Toggles the a flag to indicate the connection does not support * server-side prepared statements. * @private * @function * @name module:Connection#disablePreparedStatements * @returns {module:Connection} */ disablePreparedStatements () { state.canPrepareStatements = false; return this; }, /** * Removes a deallocated prepared statement for the list of statements * associated to the X Protocol session. * @private * @function * @name module:Connection#removePreparedStatement * @param {number} id - the client-side prepared statement id * @returns {module:Connection} */ removePreparedStatement (id) { state.statements[id - 1] = undefined; return this; }, /** * Retrieves the hostname or IP (v4 or v6) address of the machine * where the MySQL server is hosted. * @private * @function * @name module:Connection#getServerHostname * @returns {string} */ getServerHostname () { const endpoint = state.endpoints.available[0]; // Local socket file paths have precedence over host:port combos // in the core Node.js APIs used to create a network socket. // So, we want to make it clear that a Unix socket is being used // in this case. if (endpoint.socket) { return; } return endpoint.host; }, /** * Retrieves the server-side connection id. * @private * @function * @name module:Connection#getServerId * @returns {number} */ getServerId () { return state.serverId; }, /** * Retrieves the port number where the server is listening for * connections. * @private * @function * @name module:Connection#getServerPort * @returns {number} */ getServerPort () { const endpoint = state.endpoints.available[0]; // Local socket file paths have precedence over host:port combos // in the core Node.js APIs used to create a network socket. // So, we want to make it clear that a Unix socket is being used // in this case. if (endpoint.socket) { return; } return endpoint.port; }, /** * Retrieves the path to the local Unix socket file used for * connecting to the server. * @private * @function * @name module:Connection#getServerSocketPath * @returns {string} */ getServerSocketPath () { return state.endpoints.available[0].socket; }, /** * Retrieves the name of the default schema associated to the * connection. * @private * @function * @name module:Connection#getSchemaName * @returns {string} */ getSchemaName () { return schema; }, /** * Retrieves the list of capabilities that are not known by the server. * @private * @function * @name module:Connection#getUnknownCapabilities * @returns {string[]} */ getUnknownCapabilities () { return state.unknownCapabilities; }, /** * Retrieves the MySQL account user associated to the connection. * @private * @function * @name module:Connection#getUser * @returns {string} */ getUser () { // TODO(Rui): "dbUser" is deprecated. return user || dbUser || ''; }, /** * Checks if the connection is using a custom authentication mechanism * provided by the application. * @private * @function * @name module:Connection#hasCustomAuthenticationMechanism * @returns {boolean} */ hasCustomAuthenticationMechanism () { return !!auth; }, /** * Checks if there are endpoints available. * @private * @function * @name module:Connection#hasMoreEndpointsAvailable * @returns {boolean} */ hasMoreEndpointsAvailable () { // Make sure the list of available endpoints is up-to-date. this.update(); // If there are available endpoints, we can perform a failover. return state.endpoints.available.length > 0; }, /** * Checks if the connection was configured with multiple endpoints. * @private * @function * @name module:Connection#hasMultipleEndpoints * @returns {boolean} */ hasMultipleEndpoints () { return state.endpoints.available.length + state.endpoints.unavailable.length > 1; }, /** * Creates a new connection to a MySQL endpoint. * @private * @function * @name module:Connection#open * @returns {Promise<module:Connection>} */ open () { // Make sure the list of available endpoints is up-to-date. this.update(); // If "resolveSrv" is disabled, it means we already have an // ordered list of endpoints and we can try to connect to the // first one. if (!resolveSrv) { // We sort the list of endpoints (one or more) according to // the set of multi-host rules. state.endpoints.available = multiHost.sort(state.endpoints.available); return this.connect(); } // If "resolveSrv" is enabled, it means we need to retrieve the // ordered list of endpoints from a discovery service potentially // available at that host. return srv.lookup(state.endpoints.available[0].host) .then(endpoints => { // We now have the effective ordered list of endpoints // which we use to update the previous one. state.endpoints.available = srv.sort(endpoints); return this.connect(); }); }, /** * Resets and re-uses the underlying X Protocol connection. * @private * @function * @name module:Connection#override * @returns {Promise<module:Connection>} */ override () { // A connection pool calls this method when it wants to re-use an // existing connection. return state.client.sessionReset() .then(() => { return this; }); }, /** * Resets the internal state of the connection. * @private * @function * @name module:Connection#reset * @returns {Promise<module:Connection>} */ reset () { // The connection capabilities will no longer be up-to-date. state.capabilities = {}; // The client instance contains the work queue, which might not be // empty, and needs to be dereferenced anyway. state.client = null; // The connection is not closing anymore. state.isClosing = false; // An existing server connection id is what tells if a connection // has been successfully established, so we need to deference it // as well, since the connection is closed by this point. state.serverId = null; // Any existing references to prepared statements associated to // the connection should be removed. state.statements = []; // Any unknown capabilities are also no longer valid. state.unknownCapabilities = []; // The connection has closed, so there are no retries left. state.retry = false; return this; }, /** * Updates the underlying X Protocol client instance. * @private * @function * @name module:Connection#setClient * @returns {Promise<module:Connection>} */ setClient (client) { state.client = client; return this; }, /** * Executes the pipeline for creating a server-side X Protocol session. * @private * @function * @name module:Connection#start * @returns {Promise<module:Connection>} */ start () { // Start the pipeline to create a new MySQL server session. return this.capabilitiesSet() .then(capabilities => { // If TLS should be disabled, there is nothing else to do // and we can proceed with the next pipeline stage. if (!capabilities.tls) { return; } // Otherwise, we need to create a secure socket. return this.enableTLS(); }) .then(() => { return this.capabilitiesGet(); }) .then(capabilities => { // We should save the effective list of capabilities // negotiated with the server. retu