Update Fix

This commit is contained in:
2026-03-15 12:30:40 +01:00
parent 311ba5e7f3
commit 50be8e25f3
176 changed files with 4075 additions and 3013 deletions

View File

@@ -42,7 +42,7 @@ function encrypt(password, scramble, key) {
);
}
module.exports =
const pluginFactory =
(pluginOptions = {}) =>
({ connection }) => {
let state = 0;
@@ -106,3 +106,9 @@ module.exports =
);
};
};
// Export the plugin factory as default
module.exports = pluginFactory;
// Export calculateToken for reuse in initial handshake optimization
module.exports.calculateToken = calculateToken;

View File

@@ -21,7 +21,7 @@ const Timers = require('timers');
const EventEmitter = require('events').EventEmitter;
const Readable = require('stream').Readable;
const Queue = require('denque');
const SqlString = require('sqlstring');
const SqlString = require('sql-escaper');
const { createLRU } = require('lru.min');
const PacketParser = require('../packet_parser.js');
const Packets = require('../packets/index.js');
@@ -835,27 +835,31 @@ class BaseConnection extends EventEmitter {
if (!cb) {
return;
}
if (this._fatalError || this._protocolError) {
return cb(this._fatalError || this._protocolError);
}
if (this._handshakePacket) {
return cb(null, this);
}
let connectCalled = 0;
function callbackOnce(isErrorHandler) {
return function (param) {
if (!connectCalled) {
if (isErrorHandler) {
cb(param);
} else {
cb(null, param);
}
}
connectCalled = 1;
};
}
this.once('error', callbackOnce(true));
this.once('connect', callbackOnce(false));
/* eslint-disable prefer-const */
let onError, onConnect;
onError = (param) => {
this.removeListener('connect', onConnect);
cb(param);
};
onConnect = (param) => {
this.removeListener('error', onError);
cb(null, param);
};
/* eslint-enable prefer-const */
this.once('error', onError);
this.once('connect', onConnect);
}
// ===================================
@@ -922,6 +926,12 @@ class BaseConnection extends EventEmitter {
return this.addCommand(new Commands.ServerHandshake(args));
}
[Symbol.dispose]() {
if (!this._closing) {
this.end();
}
}
// ===============================================================
end(callback) {
if (this.config.isServer) {

55
node_modules/mysql2/lib/base/pool.js generated vendored
View File

@@ -1,11 +1,27 @@
'use strict';
const process = require('process');
const SqlString = require('sqlstring');
const SqlString = require('sql-escaper');
const EventEmitter = require('events').EventEmitter;
const PoolConnection = require('../pool_connection.js');
const Queue = require('denque');
const BaseConnection = require('./connection.js');
const Errors = require('../constants/errors.js');
// Source: https://github.com/go-sql-driver/mysql/blob/76c00e35a8d48f8f70f0e7dffe584692bd3fa612/packets.go#L598-L613
function isReadOnlyError(err) {
if (!err || !err.errno) {
return false;
}
// 1792: ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION
// 1290: ER_OPTION_PREVENTS_STATEMENT (returned by Aurora during failover)
// 1836: ER_READ_ONLY_MODE
return (
err.errno === Errors.ER_OPTION_PREVENTS_STATEMENT ||
err.errno === Errors.ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION ||
err.errno === Errors.ER_READ_ONLY_MODE
);
}
function spliceConnection(queue, connection) {
const len = queue.length;
@@ -92,6 +108,12 @@ class BasePool extends EventEmitter {
}
}
[Symbol.dispose]() {
if (!this._closed) {
this.end();
}
}
end(cb) {
this._closed = true;
clearTimeout(this._removeIdleTimeoutConnectionsTimer);
@@ -146,8 +168,24 @@ class BasePool extends EventEmitter {
return;
}
try {
let queryError = null;
const origOnResult = cmdQuery.onResult;
if (origOnResult) {
cmdQuery.onResult = function (err, rows, fields) {
queryError = err || null;
origOnResult(err, rows, fields);
};
} else {
cmdQuery.once('error', (err) => {
queryError = err;
});
}
conn.query(cmdQuery).once('end', () => {
conn.release();
if (isReadOnlyError(queryError)) {
conn.destroy();
} else {
conn.release();
}
});
} catch (e) {
conn.release();
@@ -169,9 +207,16 @@ class BasePool extends EventEmitter {
return cb(err);
}
try {
conn.execute(sql, values, cb).once('end', () => {
conn.release();
});
conn
.execute(sql, values, (err, rows, fields) => {
if (isReadOnlyError(err)) {
conn.destroy();
}
cb(err, rows, fields);
})
.once('end', () => {
conn.release();
});
} catch (e) {
conn.release();
return cb(e);

View File

@@ -29,10 +29,14 @@ class BasePoolConnection extends BaseConnection {
this._pool.releaseConnection(this);
}
end() {
[Symbol.dispose]() {
this.release();
}
end(callback) {
if (this.config.gracefulEnd) {
this._removeFromPool();
super.end();
super.end(callback);
return;
}
@@ -46,6 +50,9 @@ class BasePoolConnection extends BaseConnection {
this.emit('warn', err);
console.warn(err.message);
this.release();
if (typeof callback === 'function') {
callback();
}
}
destroy() {

View File

@@ -11,12 +11,31 @@ const caching_sha2_password = require('../auth_plugins/caching_sha2_password.js'
const mysql_native_password = require('../auth_plugins/mysql_native_password.js');
const mysql_clear_password = require('../auth_plugins/mysql_clear_password.js');
const standardAuthPlugins = {
// Use Object.create(null) to avoid prototype pollution
// This prevents server-controlled pluginName values like "toString" or "__proto__"
// from resolving to prototype properties
const standardAuthPlugins = Object.assign(Object.create(null), {
sha256_password: sha256_password({}),
caching_sha2_password: caching_sha2_password({}),
mysql_native_password: mysql_native_password({}),
mysql_clear_password: mysql_clear_password({}),
};
});
// Helper function to get auth plugin (custom or standard)
function getAuthPlugin(pluginName, connection) {
const customPlugins = connection.config.authPlugins;
// Check custom plugins with hasOwnProperty for safety
if (
customPlugins &&
Object.prototype.hasOwnProperty.call(customPlugins, pluginName)
) {
return customPlugins[pluginName];
}
// Safe to access standardAuthPlugins directly since it has no prototype
return standardAuthPlugins[pluginName];
}
function warnLegacyAuthSwitch() {
console.warn(
@@ -35,8 +54,6 @@ function authSwitchPluginError(error, command) {
function authSwitchRequest(packet, connection, command) {
const { pluginName, pluginData } =
Packets.AuthSwitchRequest.fromPacket(packet);
let authPlugin =
connection.config.authPlugins && connection.config.authPlugins[pluginName];
// legacy plugin api don't allow to override mysql_native_password
// if pluginName is mysql_native_password it's using standard auth4.1 auth
@@ -54,9 +71,8 @@ function authSwitchRequest(packet, connection, command) {
});
return;
}
if (!authPlugin) {
authPlugin = standardAuthPlugins[pluginName];
}
const authPlugin = getAuthPlugin(pluginName, connection);
if (!authPlugin) {
throw new Error(
`Server requests authentication using unknown plugin ${pluginName}. See ${'TODO: add plugins doco here'} on how to configure or author authentication plugins.`
@@ -108,4 +124,6 @@ function authSwitchRequestMoreData(packet, connection, command) {
module.exports = {
authSwitchRequest,
authSwitchRequestMoreData,
getAuthPlugin,
standardAuthPlugins,
};

View File

@@ -15,6 +15,10 @@ const Packets = require('../packets/index.js');
const ClientConstants = require('../constants/client.js');
const CharsetToEncoding = require('../constants/charset_encodings.js');
const auth41 = require('../auth_41.js');
const { getAuthPlugin } = require('./auth_switch.js');
const {
calculateToken: calculateSha2Token,
} = require('../auth_plugins/caching_sha2_password.js');
function flagNames(flags) {
const res = [];
@@ -67,6 +71,61 @@ class ClientHandshake extends Command {
this.passwordSha1 = connection.config.passwordSha1;
this.database = connection.config.database;
this.authPluginName = this.handshake.authPluginName;
// Optimization: Try to use the server's preferred authentication method
// to avoid an unnecessary auth switch roundtrip
const serverAuthMethod = this.handshake.authPluginName;
const isSecureConnection =
connection.config.ssl || connection.config.socketPath;
// Combine auth plugin data for easier handling
// Note: authPluginData2 can include a trailing NUL byte when PLUGIN_AUTH is set
// We must ensure exactly 20 bytes for the scramble
const authPluginData =
this.handshake.authPluginData1 && this.handshake.authPluginData2
? Buffer.concat([
this.handshake.authPluginData1,
this.handshake.authPluginData2,
]).slice(0, 20)
: Buffer.alloc(20);
// Check if user has custom auth plugin or legacy handler for the server-advertised method
// If so, we must not bypass the auth switch flow with our built-in implementation
const hasCustomAuthPlugin =
connection.config.authPlugins &&
Object.prototype.hasOwnProperty.call(
connection.config.authPlugins,
serverAuthMethod
);
const hasLegacyAuthSwitchHandler =
typeof connection.config.authSwitchHandler === 'function';
// Determine which auth method to use
// Try to use server's preferred method if we can, otherwise fallback to native
const canUseDirectAuth =
!hasCustomAuthPlugin &&
!hasLegacyAuthSwitchHandler &&
this.canUseAuthMethodDirectly(serverAuthMethod, isSecureConnection);
const clientAuthMethod = canUseDirectAuth
? serverAuthMethod
: 'mysql_native_password';
// Calculate the auth token for the chosen method
const authToken = this.calculateAuthToken(
clientAuthMethod,
this.password,
authPluginData
);
if (connection.config.debug) {
console.log(
'Server auth method: %s, Using auth method: %s',
serverAuthMethod,
clientAuthMethod
);
}
const handshakeResponse = new Packets.HandshakeResponse({
flags: this.clientFlags,
user: this.user,
@@ -78,8 +137,17 @@ class ClientHandshake extends Command {
authPluginData2: this.handshake.authPluginData2,
compress: connection.config.compress,
connectAttributes: connection.config.connectAttributes,
authToken: authToken,
authPluginName: clientAuthMethod,
});
connection.writePacket(handshakeResponse.toPacket());
// If we used a non-native auth method in the initial handshake response,
// we need to prepare for potential AuthMoreData packets by creating
// the appropriate auth plugin instance
if (clientAuthMethod !== 'mysql_native_password') {
this.initializeAuthPlugin(clientAuthMethod, authPluginData, connection);
}
}
calculateNativePasswordAuthToken(authPluginData) {
@@ -103,6 +171,82 @@ class ClientHandshake extends Command {
return authToken;
}
calculateSha256Token(password, scramble) {
// Reuse the token calculation from caching_sha2_password plugin
// to avoid code duplication and ensure consistency
return calculateSha2Token(password, scramble);
}
// Helper: Calculate auth token for a specific auth method
calculateAuthToken(authMethod, password, authPluginData) {
switch (authMethod) {
case 'mysql_native_password':
return this.calculateNativePasswordAuthToken(authPluginData);
case 'caching_sha2_password':
return this.calculateSha256Token(password, authPluginData);
case 'sha256_password':
case 'mysql_clear_password':
// These methods send plaintext password over secure connections
return password
? Buffer.from(`${password}\0`, 'utf8')
: Buffer.alloc(0);
default:
// Unknown method - use native password as fallback
return this.calculateNativePasswordAuthToken(authPluginData);
}
}
// Helper: Determine if we can use a specific auth method directly
canUseAuthMethodDirectly(authMethod, isSecureConnection) {
switch (authMethod) {
case 'mysql_native_password':
case 'caching_sha2_password':
// These methods work with or without SSL
return true;
case 'sha256_password':
case 'mysql_clear_password':
// These methods require secure connection for direct use
return isSecureConnection;
default:
// Unknown methods - fallback to native password
return false;
}
}
// Helper: Initialize auth plugin for handling subsequent AuthMoreData packets
initializeAuthPlugin(authMethod, authPluginData, connection) {
const authPlugin = getAuthPlugin(authMethod, connection);
if (!authPlugin) {
return; // Plugin not found, will fallback to auth switch if needed
}
// Initialize the plugin with connection and command context
const pluginHandler = authPlugin({ connection, command: this });
connection._authPlugin = pluginHandler;
// Prime the plugin by calling it with the scramble data
// This advances the plugin's state machine (e.g., to STATE_TOKEN_SENT)
// We don't send the result because we already included it in the handshake response
try {
Promise.resolve(pluginHandler(authPluginData)).catch((err) => {
// Ignore errors during initialization since we already sent the token
if (connection.config.debug) {
console.log('Auth plugin initialization:', err.message);
}
});
} catch (err) {
// Ignore synchronous errors during initialization
if (connection.config.debug) {
console.log('Auth plugin initialization error:', err.message);
}
}
}
handshakeInit(helloPacket, connection) {
this.on('error', (e) => {
connection._fatalError = e;

View File

@@ -6,6 +6,32 @@
const zlib = require('zlib');
const PacketParser = require('./packet_parser.js');
class Queue {
constructor() {
this._queue = [];
this._running = false;
}
push(fn) {
this._queue.push(fn);
if (!this._running) {
this._running = true;
process.nextTick(() => this._next());
}
}
_next() {
const task = this._queue.shift();
if (!task) {
this._running = false;
return;
}
task({
done: () => process.nextTick(() => this._next()),
});
}
}
function handleCompressedPacket(packet) {
// eslint-disable-next-line consistent-this, no-invalid-this
const connection = this;
@@ -117,11 +143,11 @@ function enableCompression(connection) {
connection.writeUncompressed = connection.write;
connection.write = writeCompressed;
const seqqueue = require('seq-queue');
connection.inflateQueue = seqqueue.createQueue();
connection.deflateQueue = seqqueue.createQueue();
connection.inflateQueue = new Queue();
connection.deflateQueue = new Queue();
}
module.exports = {
enableCompression: enableCompression,
Queue: Queue,
};

View File

@@ -277,7 +277,10 @@ class ConnectionConfig {
user: decodeURIComponent(parsedUrl.username),
password: decodeURIComponent(parsedUrl.password),
};
parsedUrl.searchParams.forEach((value, key) => {
for (const [key, value] of parsedUrl.searchParams) {
if (key in options) {
continue;
}
try {
// Try to parse this as a JSON expression first
options[key] = JSON.parse(value);
@@ -285,7 +288,7 @@ class ConnectionConfig {
// Otherwise assume it is a plain string
options[key] = value;
}
});
}
return options;
}
}

View File

@@ -313,4 +313,5 @@ module.exports = [
'utf8',
'utf8',
'utf8',
'utf8',
];

View File

@@ -16,22 +16,47 @@ class HandshakeResponse {
this.authPluginData2 = handshake.authPluginData2;
this.compress = handshake.compress;
this.clientFlags = handshake.flags;
// TODO: pre-4.1 auth support
let authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
this.passwordSha1,
this.authPluginData1,
this.authPluginData2
);
// Accept pre-calculated authToken and authPluginName from caller
// This allows the caller to optimize by using the server's preferred auth method
if (
handshake.authToken !== undefined &&
handshake.authPluginName !== undefined
) {
// Validate types to fail fast with clear errors
if (!Buffer.isBuffer(handshake.authToken)) {
throw new TypeError(
'HandshakeResponse authToken must be a Buffer when provided'
);
}
if (typeof handshake.authPluginName !== 'string') {
throw new TypeError(
'HandshakeResponse authPluginName must be a string when provided'
);
}
this.authToken = handshake.authToken;
this.authPluginName = handshake.authPluginName;
} else {
authToken = auth41.calculateToken(
this.password,
this.authPluginData1,
this.authPluginData2
);
// Fallback to legacy behavior: calculate mysql_native_password token
// TODO: pre-4.1 auth support
let authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(
this.passwordSha1,
this.authPluginData1,
this.authPluginData2
);
} else {
authToken = auth41.calculateToken(
this.password,
this.authPluginData1,
this.authPluginData2
);
}
this.authToken = authToken;
this.authPluginName = 'mysql_native_password';
}
this.authToken = authToken;
this.charsetNumber = handshake.charsetNumber;
this.encoding = CharsetToEncoding[handshake.charsetNumber];
this.connectAttributes = handshake.connectAttributes;
@@ -62,8 +87,12 @@ class HandshakeResponse {
packet.writeNullTerminatedString(this.database, encoding);
}
if (isSet('PLUGIN_AUTH')) {
// TODO: pass from config
packet.writeNullTerminatedString('mysql_native_password', 'latin1');
// Use the auth plugin name specified by the caller (optimized for server's preference)
// or fall back to mysql_native_password for backward compatibility
packet.writeNullTerminatedString(
this.authPluginName || 'mysql_native_password',
'latin1'
);
}
if (isSet('CONNECT_ATTRS')) {
const connectAttributes = this.connectAttributes || {};

View File

@@ -406,8 +406,8 @@ class Packet {
readNullTerminatedString(encoding) {
const start = this.offset;
let end = this.offset;
while (this.buffer[end]) {
end = end + 1; // TODO: handle OOB check
while (end < this.end && this.buffer[end] !== 0x00) {
end = end + 1;
}
this.offset = end + 1;
return StringParser.decode(this.buffer, encoding, start, end);
@@ -516,16 +516,20 @@ class Packet {
return result * sign;
}
// copy-paste from https://github.com/mysqljs/mysql/blob/master/lib/protocol/Parser.js
// adapted from https://github.com/mysqljs/mysql/blob/dc9c152a87ec51a1f647447268917243d2eab1fd/lib/protocol/Parser.js
parseGeometryValue() {
const buffer = this.readLengthCodedBuffer();
let offset = 4;
if (buffer === null || !buffer.length) {
return null;
}
const bufferLength = buffer.length;
function parseGeometry() {
let x, y, i, j, numPoints, line;
let x, y, i, j, numPoints, numRings, num, line;
let result = null;
if (offset + 5 > bufferLength) {
return null;
}
const byteOrder = buffer.readUInt8(offset);
offset += 1;
const wkbType = byteOrder
@@ -534,6 +538,9 @@ class Packet {
offset += 4;
switch (wkbType) {
case 1: // WKBPoint
if (offset + 16 > bufferLength) {
return null;
}
x = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
@@ -545,12 +552,21 @@ class Packet {
result = { x: x, y: y };
break;
case 2: // WKBLineString
if (offset + 4 > bufferLength) {
return null;
}
numPoints = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
if (numPoints > (bufferLength - offset) / 16) {
return null;
}
result = [];
for (i = numPoints; i > 0; i--) {
if (offset + 16 > bufferLength) {
break;
}
x = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
@@ -563,19 +579,30 @@ class Packet {
}
break;
case 3: // WKBPolygon
// eslint-disable-next-line no-case-declarations
const numRings = byteOrder
if (offset + 4 > bufferLength) {
return null;
}
numRings = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
if (numRings > (bufferLength - offset) / 4) {
return null;
}
result = [];
for (i = numRings; i > 0; i--) {
if (offset + 4 > bufferLength) {
break;
}
numPoints = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
line = [];
for (j = numPoints; j > 0; j--) {
if (offset + 16 > bufferLength) {
break;
}
x = byteOrder
? buffer.readDoubleLE(offset)
: buffer.readDoubleBE(offset);
@@ -593,11 +620,16 @@ class Packet {
case 5: // WKBMultiLineString
case 6: // WKBMultiPolygon
case 7: // WKBGeometryCollection
// eslint-disable-next-line no-case-declarations
const num = byteOrder
if (offset + 4 > bufferLength) {
return null;
}
num = byteOrder
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset);
offset += 4;
if (num > (bufferLength - offset) / 9) {
return null;
}
result = [];
for (i = num; i > 0; i--) {
result.push(parseGeometry());
@@ -660,14 +692,27 @@ class Packet {
if (len === null) {
return null;
}
if (len === 0) {
return 0; // TODO: assert? exception?
}
// For numbers with many digits (>17), use built-in parseFloat to avoid
// precision loss from accumulated rounding errors in repeated *10 operations.
// This fixes issues #2928 (MAX_VALUE doubles) and #3690 (DECIMAL(36,18))
// where very large numbers or numbers with many fractional digits lose precision.
// The threshold of 17 is based on IEEE 754 double precision (~15-17 significant digits).
// Testing shows minimal performance impact as most real-world numbers are shorter.
if (len > 17) {
const str = this.buffer.toString('utf8', this.offset, this.offset + len);
this.offset += len;
return Number.parseFloat(str);
}
let result = 0;
const end = this.offset + len;
let factor = 1;
let pastDot = false;
let charCode = 0;
if (len === 0) {
return 0; // TODO: assert? exception?
}
if (this.buffer[this.offset] === minus) {
this.offset++;
factor = -1;
@@ -681,9 +726,13 @@ class Packet {
pastDot = true;
this.offset++;
} else if (charCode === exponent || charCode === exponentCapital) {
this.offset++;
const exponentValue = this.parseInt(end - this.offset);
return (result / factor) * Math.pow(10, exponentValue);
// Scientific notation detected - bail out to parseFloat for exact match.
// Manual calculation with Math.pow(10, exp) cannot match parseFloat()
// exactly for most non-zero exponents due to accumulated rounding errors.
const start = end - len;
const str = this.buffer.toString('utf8', start, end);
this.offset = end;
return Number.parseFloat(str);
} else {
result *= 10;
result += this.buffer[this.offset] - 48;
@@ -831,11 +880,10 @@ class Packet {
if (n === null) {
return this.writeInt8(0xfb);
}
// TODO: check that n is out of int precision
this.writeInt8(0xfe);
this.buffer.writeUInt32LE(n, this.offset);
this.buffer.writeUInt32LE(n >>> 0, this.offset);
this.offset += 4;
this.buffer.writeUInt32LE(n >> 32, this.offset);
this.buffer.writeUInt32LE(Math.floor(n / 0x100000000), this.offset);
this.offset += 4;
return this.offset;
}

View File

@@ -229,6 +229,12 @@ class PoolCluster extends EventEmitter {
namespace.getConnection(cb);
}
[Symbol.dispose]() {
if (!this._closed) {
this.end();
}
}
end(callback) {
const cb =
callback !== undefined

View File

@@ -66,6 +66,12 @@ class PromiseConnection extends EventEmitter {
});
}
async [Symbol.asyncDispose]() {
if (!this.connection._closing) {
await this.end();
}
}
beginTransaction() {
const c = this.connection;
const localErr = new Error();

View File

@@ -85,6 +85,12 @@ class PromisePool extends EventEmitter {
});
});
}
async [Symbol.asyncDispose]() {
if (!this.pool._closed) {
await this.end();
}
}
}
(function (functionsToWrap) {

View File

@@ -14,6 +14,10 @@ class PromisePoolConnection extends PromiseConnection {
arguments
);
}
async [Symbol.asyncDispose]() {
this.release();
}
}
module.exports = PromisePoolConnection;