mysql2 3.18.2 → 3.18.3-canary.b57c671c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -42,7 +42,7 @@ function encrypt(password, scramble, key) {
|
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
const pluginFactory =
|
|
46
46
|
(pluginOptions = {}) =>
|
|
47
47
|
({ connection }) => {
|
|
48
48
|
let state = 0;
|
|
@@ -106,3 +106,9 @@ module.exports =
|
|
|
106
106
|
);
|
|
107
107
|
};
|
|
108
108
|
};
|
|
109
|
+
|
|
110
|
+
// Export the plugin factory as default
|
|
111
|
+
module.exports = pluginFactory;
|
|
112
|
+
|
|
113
|
+
// Export calculateToken for reuse in initial handshake optimization
|
|
114
|
+
module.exports.calculateToken = calculateToken;
|
|
@@ -11,12 +11,31 @@ const caching_sha2_password = require('../auth_plugins/caching_sha2_password.js'
|
|
|
11
11
|
const mysql_native_password = require('../auth_plugins/mysql_native_password.js');
|
|
12
12
|
const mysql_clear_password = require('../auth_plugins/mysql_clear_password.js');
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Use Object.create(null) to avoid prototype pollution
|
|
15
|
+
// This prevents server-controlled pluginName values like "toString" or "__proto__"
|
|
16
|
+
// from resolving to prototype properties
|
|
17
|
+
const standardAuthPlugins = Object.assign(Object.create(null), {
|
|
15
18
|
sha256_password: sha256_password({}),
|
|
16
19
|
caching_sha2_password: caching_sha2_password({}),
|
|
17
20
|
mysql_native_password: mysql_native_password({}),
|
|
18
21
|
mysql_clear_password: mysql_clear_password({}),
|
|
19
|
-
};
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Helper function to get auth plugin (custom or standard)
|
|
25
|
+
function getAuthPlugin(pluginName, connection) {
|
|
26
|
+
const customPlugins = connection.config.authPlugins;
|
|
27
|
+
|
|
28
|
+
// Check custom plugins with hasOwnProperty for safety
|
|
29
|
+
if (
|
|
30
|
+
customPlugins &&
|
|
31
|
+
Object.prototype.hasOwnProperty.call(customPlugins, pluginName)
|
|
32
|
+
) {
|
|
33
|
+
return customPlugins[pluginName];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Safe to access standardAuthPlugins directly since it has no prototype
|
|
37
|
+
return standardAuthPlugins[pluginName];
|
|
38
|
+
}
|
|
20
39
|
|
|
21
40
|
function warnLegacyAuthSwitch() {
|
|
22
41
|
console.warn(
|
|
@@ -35,8 +54,6 @@ function authSwitchPluginError(error, command) {
|
|
|
35
54
|
function authSwitchRequest(packet, connection, command) {
|
|
36
55
|
const { pluginName, pluginData } =
|
|
37
56
|
Packets.AuthSwitchRequest.fromPacket(packet);
|
|
38
|
-
let authPlugin =
|
|
39
|
-
connection.config.authPlugins && connection.config.authPlugins[pluginName];
|
|
40
57
|
|
|
41
58
|
// legacy plugin api don't allow to override mysql_native_password
|
|
42
59
|
// if pluginName is mysql_native_password it's using standard auth4.1 auth
|
|
@@ -54,9 +71,8 @@ function authSwitchRequest(packet, connection, command) {
|
|
|
54
71
|
});
|
|
55
72
|
return;
|
|
56
73
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
74
|
+
|
|
75
|
+
const authPlugin = getAuthPlugin(pluginName, connection);
|
|
60
76
|
if (!authPlugin) {
|
|
61
77
|
throw new Error(
|
|
62
78
|
`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) {
|
|
|
108
124
|
module.exports = {
|
|
109
125
|
authSwitchRequest,
|
|
110
126
|
authSwitchRequestMoreData,
|
|
127
|
+
getAuthPlugin,
|
|
128
|
+
standardAuthPlugins,
|
|
111
129
|
};
|
|
@@ -15,6 +15,10 @@ const Packets = require('../packets/index.js');
|
|
|
15
15
|
const ClientConstants = require('../constants/client.js');
|
|
16
16
|
const CharsetToEncoding = require('../constants/charset_encodings.js');
|
|
17
17
|
const auth41 = require('../auth_41.js');
|
|
18
|
+
const { getAuthPlugin } = require('./auth_switch.js');
|
|
19
|
+
const {
|
|
20
|
+
calculateToken: calculateSha2Token,
|
|
21
|
+
} = require('../auth_plugins/caching_sha2_password.js');
|
|
18
22
|
|
|
19
23
|
function flagNames(flags) {
|
|
20
24
|
const res = [];
|
|
@@ -67,6 +71,61 @@ class ClientHandshake extends Command {
|
|
|
67
71
|
this.passwordSha1 = connection.config.passwordSha1;
|
|
68
72
|
this.database = connection.config.database;
|
|
69
73
|
this.authPluginName = this.handshake.authPluginName;
|
|
74
|
+
|
|
75
|
+
// Optimization: Try to use the server's preferred authentication method
|
|
76
|
+
// to avoid an unnecessary auth switch roundtrip
|
|
77
|
+
const serverAuthMethod = this.handshake.authPluginName;
|
|
78
|
+
const isSecureConnection =
|
|
79
|
+
connection.config.ssl || connection.config.socketPath;
|
|
80
|
+
|
|
81
|
+
// Combine auth plugin data for easier handling
|
|
82
|
+
// Note: authPluginData2 can include a trailing NUL byte when PLUGIN_AUTH is set
|
|
83
|
+
// We must ensure exactly 20 bytes for the scramble
|
|
84
|
+
const authPluginData =
|
|
85
|
+
this.handshake.authPluginData1 && this.handshake.authPluginData2
|
|
86
|
+
? Buffer.concat([
|
|
87
|
+
this.handshake.authPluginData1,
|
|
88
|
+
this.handshake.authPluginData2,
|
|
89
|
+
]).slice(0, 20)
|
|
90
|
+
: Buffer.alloc(20);
|
|
91
|
+
|
|
92
|
+
// Check if user has custom auth plugin or legacy handler for the server-advertised method
|
|
93
|
+
// If so, we must not bypass the auth switch flow with our built-in implementation
|
|
94
|
+
const hasCustomAuthPlugin =
|
|
95
|
+
connection.config.authPlugins &&
|
|
96
|
+
Object.prototype.hasOwnProperty.call(
|
|
97
|
+
connection.config.authPlugins,
|
|
98
|
+
serverAuthMethod
|
|
99
|
+
);
|
|
100
|
+
const hasLegacyAuthSwitchHandler =
|
|
101
|
+
typeof connection.config.authSwitchHandler === 'function';
|
|
102
|
+
|
|
103
|
+
// Determine which auth method to use
|
|
104
|
+
// Try to use server's preferred method if we can, otherwise fallback to native
|
|
105
|
+
const canUseDirectAuth =
|
|
106
|
+
!hasCustomAuthPlugin &&
|
|
107
|
+
!hasLegacyAuthSwitchHandler &&
|
|
108
|
+
this.canUseAuthMethodDirectly(serverAuthMethod, isSecureConnection);
|
|
109
|
+
|
|
110
|
+
const clientAuthMethod = canUseDirectAuth
|
|
111
|
+
? serverAuthMethod
|
|
112
|
+
: 'mysql_native_password';
|
|
113
|
+
|
|
114
|
+
// Calculate the auth token for the chosen method
|
|
115
|
+
const authToken = this.calculateAuthToken(
|
|
116
|
+
clientAuthMethod,
|
|
117
|
+
this.password,
|
|
118
|
+
authPluginData
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (connection.config.debug) {
|
|
122
|
+
console.log(
|
|
123
|
+
'Server auth method: %s, Using auth method: %s',
|
|
124
|
+
serverAuthMethod,
|
|
125
|
+
clientAuthMethod
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
70
129
|
const handshakeResponse = new Packets.HandshakeResponse({
|
|
71
130
|
flags: this.clientFlags,
|
|
72
131
|
user: this.user,
|
|
@@ -78,8 +137,17 @@ class ClientHandshake extends Command {
|
|
|
78
137
|
authPluginData2: this.handshake.authPluginData2,
|
|
79
138
|
compress: connection.config.compress,
|
|
80
139
|
connectAttributes: connection.config.connectAttributes,
|
|
140
|
+
authToken: authToken,
|
|
141
|
+
authPluginName: clientAuthMethod,
|
|
81
142
|
});
|
|
82
143
|
connection.writePacket(handshakeResponse.toPacket());
|
|
144
|
+
|
|
145
|
+
// If we used a non-native auth method in the initial handshake response,
|
|
146
|
+
// we need to prepare for potential AuthMoreData packets by creating
|
|
147
|
+
// the appropriate auth plugin instance
|
|
148
|
+
if (clientAuthMethod !== 'mysql_native_password') {
|
|
149
|
+
this.initializeAuthPlugin(clientAuthMethod, authPluginData, connection);
|
|
150
|
+
}
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
calculateNativePasswordAuthToken(authPluginData) {
|
|
@@ -103,6 +171,82 @@ class ClientHandshake extends Command {
|
|
|
103
171
|
return authToken;
|
|
104
172
|
}
|
|
105
173
|
|
|
174
|
+
calculateSha256Token(password, scramble) {
|
|
175
|
+
// Reuse the token calculation from caching_sha2_password plugin
|
|
176
|
+
// to avoid code duplication and ensure consistency
|
|
177
|
+
return calculateSha2Token(password, scramble);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Helper: Calculate auth token for a specific auth method
|
|
181
|
+
calculateAuthToken(authMethod, password, authPluginData) {
|
|
182
|
+
switch (authMethod) {
|
|
183
|
+
case 'mysql_native_password':
|
|
184
|
+
return this.calculateNativePasswordAuthToken(authPluginData);
|
|
185
|
+
|
|
186
|
+
case 'caching_sha2_password':
|
|
187
|
+
return this.calculateSha256Token(password, authPluginData);
|
|
188
|
+
|
|
189
|
+
case 'sha256_password':
|
|
190
|
+
case 'mysql_clear_password':
|
|
191
|
+
// These methods send plaintext password over secure connections
|
|
192
|
+
return password
|
|
193
|
+
? Buffer.from(`${password}\0`, 'utf8')
|
|
194
|
+
: Buffer.alloc(0);
|
|
195
|
+
|
|
196
|
+
default:
|
|
197
|
+
// Unknown method - use native password as fallback
|
|
198
|
+
return this.calculateNativePasswordAuthToken(authPluginData);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Helper: Determine if we can use a specific auth method directly
|
|
203
|
+
canUseAuthMethodDirectly(authMethod, isSecureConnection) {
|
|
204
|
+
switch (authMethod) {
|
|
205
|
+
case 'mysql_native_password':
|
|
206
|
+
case 'caching_sha2_password':
|
|
207
|
+
// These methods work with or without SSL
|
|
208
|
+
return true;
|
|
209
|
+
|
|
210
|
+
case 'sha256_password':
|
|
211
|
+
case 'mysql_clear_password':
|
|
212
|
+
// These methods require secure connection for direct use
|
|
213
|
+
return isSecureConnection;
|
|
214
|
+
|
|
215
|
+
default:
|
|
216
|
+
// Unknown methods - fallback to native password
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Helper: Initialize auth plugin for handling subsequent AuthMoreData packets
|
|
222
|
+
initializeAuthPlugin(authMethod, authPluginData, connection) {
|
|
223
|
+
const authPlugin = getAuthPlugin(authMethod, connection);
|
|
224
|
+
if (!authPlugin) {
|
|
225
|
+
return; // Plugin not found, will fallback to auth switch if needed
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Initialize the plugin with connection and command context
|
|
229
|
+
const pluginHandler = authPlugin({ connection, command: this });
|
|
230
|
+
connection._authPlugin = pluginHandler;
|
|
231
|
+
|
|
232
|
+
// Prime the plugin by calling it with the scramble data
|
|
233
|
+
// This advances the plugin's state machine (e.g., to STATE_TOKEN_SENT)
|
|
234
|
+
// We don't send the result because we already included it in the handshake response
|
|
235
|
+
try {
|
|
236
|
+
Promise.resolve(pluginHandler(authPluginData)).catch((err) => {
|
|
237
|
+
// Ignore errors during initialization since we already sent the token
|
|
238
|
+
if (connection.config.debug) {
|
|
239
|
+
console.log('Auth plugin initialization:', err.message);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
// Ignore synchronous errors during initialization
|
|
244
|
+
if (connection.config.debug) {
|
|
245
|
+
console.log('Auth plugin initialization error:', err.message);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
106
250
|
handshakeInit(helloPacket, connection) {
|
|
107
251
|
this.on('error', (e) => {
|
|
108
252
|
connection._fatalError = e;
|
|
@@ -16,22 +16,47 @@ class HandshakeResponse {
|
|
|
16
16
|
this.authPluginData2 = handshake.authPluginData2;
|
|
17
17
|
this.compress = handshake.compress;
|
|
18
18
|
this.clientFlags = handshake.flags;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
|
|
20
|
+
// Accept pre-calculated authToken and authPluginName from caller
|
|
21
|
+
// This allows the caller to optimize by using the server's preferred auth method
|
|
22
|
+
if (
|
|
23
|
+
handshake.authToken !== undefined &&
|
|
24
|
+
handshake.authPluginName !== undefined
|
|
25
|
+
) {
|
|
26
|
+
// Validate types to fail fast with clear errors
|
|
27
|
+
if (!Buffer.isBuffer(handshake.authToken)) {
|
|
28
|
+
throw new TypeError(
|
|
29
|
+
'HandshakeResponse authToken must be a Buffer when provided'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
if (typeof handshake.authPluginName !== 'string') {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
'HandshakeResponse authPluginName must be a string when provided'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
this.authToken = handshake.authToken;
|
|
38
|
+
this.authPluginName = handshake.authPluginName;
|
|
27
39
|
} else {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
// Fallback to legacy behavior: calculate mysql_native_password token
|
|
41
|
+
// TODO: pre-4.1 auth support
|
|
42
|
+
let authToken;
|
|
43
|
+
if (this.passwordSha1) {
|
|
44
|
+
authToken = auth41.calculateTokenFromPasswordSha(
|
|
45
|
+
this.passwordSha1,
|
|
46
|
+
this.authPluginData1,
|
|
47
|
+
this.authPluginData2
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
authToken = auth41.calculateToken(
|
|
51
|
+
this.password,
|
|
52
|
+
this.authPluginData1,
|
|
53
|
+
this.authPluginData2
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
this.authToken = authToken;
|
|
57
|
+
this.authPluginName = 'mysql_native_password';
|
|
33
58
|
}
|
|
34
|
-
|
|
59
|
+
|
|
35
60
|
this.charsetNumber = handshake.charsetNumber;
|
|
36
61
|
this.encoding = CharsetToEncoding[handshake.charsetNumber];
|
|
37
62
|
this.connectAttributes = handshake.connectAttributes;
|
|
@@ -62,8 +87,12 @@ class HandshakeResponse {
|
|
|
62
87
|
packet.writeNullTerminatedString(this.database, encoding);
|
|
63
88
|
}
|
|
64
89
|
if (isSet('PLUGIN_AUTH')) {
|
|
65
|
-
//
|
|
66
|
-
|
|
90
|
+
// Use the auth plugin name specified by the caller (optimized for server's preference)
|
|
91
|
+
// or fall back to mysql_native_password for backward compatibility
|
|
92
|
+
packet.writeNullTerminatedString(
|
|
93
|
+
this.authPluginName || 'mysql_native_password',
|
|
94
|
+
'latin1'
|
|
95
|
+
);
|
|
67
96
|
}
|
|
68
97
|
if (isSet('CONNECT_ATTRS')) {
|
|
69
98
|
const connectAttributes = this.connectAttributes || {};
|
package/lib/packets/packet.js
CHANGED
|
@@ -660,14 +660,27 @@ class Packet {
|
|
|
660
660
|
if (len === null) {
|
|
661
661
|
return null;
|
|
662
662
|
}
|
|
663
|
+
if (len === 0) {
|
|
664
|
+
return 0; // TODO: assert? exception?
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// For numbers with many digits (>17), use built-in parseFloat to avoid
|
|
668
|
+
// precision loss from accumulated rounding errors in repeated *10 operations.
|
|
669
|
+
// This fixes issues #2928 (MAX_VALUE doubles) and #3690 (DECIMAL(36,18))
|
|
670
|
+
// where very large numbers or numbers with many fractional digits lose precision.
|
|
671
|
+
// The threshold of 17 is based on IEEE 754 double precision (~15-17 significant digits).
|
|
672
|
+
// Testing shows minimal performance impact as most real-world numbers are shorter.
|
|
673
|
+
if (len > 17) {
|
|
674
|
+
const str = this.buffer.toString('utf8', this.offset, this.offset + len);
|
|
675
|
+
this.offset += len;
|
|
676
|
+
return Number.parseFloat(str);
|
|
677
|
+
}
|
|
678
|
+
|
|
663
679
|
let result = 0;
|
|
664
680
|
const end = this.offset + len;
|
|
665
681
|
let factor = 1;
|
|
666
682
|
let pastDot = false;
|
|
667
683
|
let charCode = 0;
|
|
668
|
-
if (len === 0) {
|
|
669
|
-
return 0; // TODO: assert? exception?
|
|
670
|
-
}
|
|
671
684
|
if (this.buffer[this.offset] === minus) {
|
|
672
685
|
this.offset++;
|
|
673
686
|
factor = -1;
|
|
@@ -681,9 +694,13 @@ class Packet {
|
|
|
681
694
|
pastDot = true;
|
|
682
695
|
this.offset++;
|
|
683
696
|
} else if (charCode === exponent || charCode === exponentCapital) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
697
|
+
// Scientific notation detected - bail out to parseFloat for exact match.
|
|
698
|
+
// Manual calculation with Math.pow(10, exp) cannot match parseFloat()
|
|
699
|
+
// exactly for most non-zero exponents due to accumulated rounding errors.
|
|
700
|
+
const start = end - len;
|
|
701
|
+
const str = this.buffer.toString('utf8', start, end);
|
|
702
|
+
this.offset = end;
|
|
703
|
+
return Number.parseFloat(str);
|
|
687
704
|
} else {
|
|
688
705
|
result *= 10;
|
|
689
706
|
result += this.buffer[this.offset] - 48;
|
package/package.json
CHANGED