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
- module.exports =
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
- const standardAuthPlugins = {
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
- if (!authPlugin) {
58
- authPlugin = standardAuthPlugins[pluginName];
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
- // TODO: pre-4.1 auth support
20
- let authToken;
21
- if (this.passwordSha1) {
22
- authToken = auth41.calculateTokenFromPasswordSha(
23
- this.passwordSha1,
24
- this.authPluginData1,
25
- this.authPluginData2
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
- authToken = auth41.calculateToken(
29
- this.password,
30
- this.authPluginData1,
31
- this.authPluginData2
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
- this.authToken = authToken;
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
- // TODO: pass from config
66
- packet.writeNullTerminatedString('mysql_native_password', 'latin1');
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 || {};
@@ -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
- this.offset++;
685
- const exponentValue = this.parseInt(end - this.offset);
686
- return (result / factor) * Math.pow(10, exponentValue);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mysql2",
3
- "version": "3.18.2",
3
+ "version": "3.18.3-canary.b57c671c",
4
4
  "description": "fast mysql driver. Implements core protocol, prepared statements, ssl and compression in native JS",
5
5
  "main": "index.js",
6
6
  "typings": "typings/mysql/index",