From 44bf3e0546c5f1af2e0f51f9d9655ad6108423cd Mon Sep 17 00:00:00 2001 From: Mini256 Date: Fri, 17 Nov 2023 15:10:08 +0800 Subject: [PATCH 01/17] feat: add static text parser --- lib/commands/command.js | 7 +- lib/commands/query.js | 18 ++-- lib/connection.js | 23 ++--- lib/connection_config.js | 4 +- lib/parsers/static_text_parser.js | 135 ++++++++++++++++++++++++++++++ lib/stream.js | 14 ++++ package.json | 1 + typings/mysql/lib/Connection.d.ts | 2 + 8 files changed, 181 insertions(+), 23 deletions(-) create mode 100644 lib/parsers/static_text_parser.js create mode 100644 lib/stream.js diff --git a/lib/commands/command.js b/lib/commands/command.js index 659086fd14..1e278923d8 100644 --- a/lib/commands/command.js +++ b/lib/commands/command.js @@ -1,7 +1,6 @@ 'use strict'; const EventEmitter = require('events').EventEmitter; -const Timers = require('timers'); class Command extends EventEmitter { constructor() { @@ -29,7 +28,7 @@ class Command extends EventEmitter { const err = packet.asError(connection.clientEncoding); err.sql = this.sql || this.query; if (this.queryTimeout) { - Timers.clearTimeout(this.queryTimeout); + clearTimeout(this.queryTimeout); this.queryTimeout = null; } if (this.onResult) { @@ -45,10 +44,10 @@ class Command extends EventEmitter { this.next = this.next(packet, connection); if (this.next) { return false; - } + } this.emit('end'); return true; - + } } diff --git a/lib/commands/query.js b/lib/commands/query.js index 8d643bdc4b..62485edbb5 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -1,13 +1,13 @@ 'use strict'; const process = require('process'); -const Timers = require('timers'); const Readable = require('stream').Readable; const Command = require('./command.js'); const Packets = require('../packets/index.js'); const getTextParser = require('../parsers/text_parser.js'); +const getStaticTextParser = require('../parsers/static_text_parser.js'); const ServerStatus = require('../constants/server_status.js'); const EmptyPacket = new Packets.Packet(0, Buffer.allocUnsafe(4), 0, 4); @@ -69,7 +69,7 @@ class Query extends Command { } // else clear timer if (this.queryTimeout) { - Timers.clearTimeout(this.queryTimeout); + clearTimeout(this.queryTimeout); this.queryTimeout = null; } if (this.onResult) { @@ -212,7 +212,11 @@ class Query extends Command { if (this._receivedFieldsCount === this._fieldCount) { const fields = this._fields[this._resultIndex]; this.emit('fields', fields); - this._rowParser = new (getTextParser(fields, this.options, connection.config))(fields); + if (this.options.useStaticParser) { + this._rowParser = getStaticTextParser(fields, this.options, connection.config); + } else { + this._rowParser = new (getTextParser(fields, this.options, connection.config))(fields); + } return Query.prototype.fieldsEOF; } return Query.prototype.readField; @@ -227,7 +231,7 @@ class Query extends Command { } /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ - row(packet, _connection) { + row(packet, _connection) { if (packet.isEOF()) { const status = packet.eofStatusFlags(); const moreResults = status & ServerStatus.SERVER_MORE_RESULTS_EXISTS; @@ -290,7 +294,7 @@ class Query extends Command { _setTimeout() { if (this.timeout) { const timeoutHandler = this._handleTimeoutError.bind(this); - this.queryTimeout = Timers.setTimeout( + this.queryTimeout = setTimeout( timeoutHandler, this.timeout ); @@ -299,10 +303,10 @@ class Query extends Command { _handleTimeoutError() { if (this.queryTimeout) { - Timers.clearTimeout(this.queryTimeout); + clearTimeout(this.queryTimeout); this.queryTimeout = null; } - + const err = new Error('Query inactivity timeout'); err.errorno = 'PROTOCOL_SEQUENCE_TIMEOUT'; err.code = 'PROTOCOL_SEQUENCE_TIMEOUT'; diff --git a/lib/connection.js b/lib/connection.js index dcd841cc59..a8c06d964f 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -15,9 +15,7 @@ 'use strict'; -const Net = require('net'); const Tls = require('tls'); -const Timers = require('timers'); const EventEmitter = require('events').EventEmitter; const Readable = require('stream').Readable; const Queue = require('denque'); @@ -29,6 +27,7 @@ const Packets = require('./packets/index.js'); const Commands = require('./commands/index.js'); const ConnectionConfig = require('./connection_config.js'); const CharsetToEncoding = require('./constants/charset_encodings.js'); +const {getStream} = require("./stream"); let _connectionId = 0; @@ -44,10 +43,12 @@ class Connection extends EventEmitter { // TODO: use `/usr/local/mysql/bin/mysql_config --socket` output? as default socketPath // if there is no host/port and no socketPath parameters? if (!opts.config.stream) { + this.stream = getStream(opts.config.ssl); if (opts.config.socketPath) { - this.stream = Net.connect(opts.config.socketPath); + // FIXME + this.stream.connect(opts.config.socketPath); } else { - this.stream = Net.connect( + this.stream.connect( opts.config.port, opts.config.host ); @@ -98,7 +99,7 @@ class Connection extends EventEmitter { }); this.stream.on('data', data => { if (this.connectTimeout) { - Timers.clearTimeout(this.connectTimeout); + clearTimeout(this.connectTimeout); this.connectTimeout = null; } this.packetParser.execute(data); @@ -148,7 +149,7 @@ class Connection extends EventEmitter { this.serverEncoding = 'utf8'; if (this.config.connectTimeout) { const timeoutHandler = this._handleTimeoutError.bind(this); - this.connectTimeout = Timers.setTimeout( + this.connectTimeout = setTimeout( timeoutHandler, this.config.connectTimeout ); @@ -186,7 +187,7 @@ class Connection extends EventEmitter { _handleNetworkError(err) { if (this.connectTimeout) { - Timers.clearTimeout(this.connectTimeout); + clearTimeout(this.connectTimeout); this.connectTimeout = null; } // Do not throw an error when a connection ends with a RST,ACK packet @@ -198,7 +199,7 @@ class Connection extends EventEmitter { _handleTimeoutError() { if (this.connectTimeout) { - Timers.clearTimeout(this.connectTimeout); + clearTimeout(this.connectTimeout); this.connectTimeout = null; } this.stream.destroy && this.stream.destroy(); @@ -213,7 +214,7 @@ class Connection extends EventEmitter { // called on stream error or unexpected termination _notifyError(err) { if (this.connectTimeout) { - Timers.clearTimeout(this.connectTimeout); + clearTimeout(this.connectTimeout); this.connectTimeout = null; } // prevent from emitting 'PROTOCOL_CONNECTION_LOST' after EPIPE or ECONNRESET @@ -408,7 +409,7 @@ class Connection extends EventEmitter { err.code = code || 'PROTOCOL_ERROR'; this.emit('error', err); } - + get fatalError() { return this._fatalError; } @@ -760,7 +761,7 @@ class Connection extends EventEmitter { close() { if (this.connectTimeout) { - Timers.clearTimeout(this.connectTimeout); + clearTimeout(this.connectTimeout); this.connectTimeout = null; } this._closing = true; diff --git a/lib/connection_config.js b/lib/connection_config.js index ec52051625..9add9c8703 100644 --- a/lib/connection_config.js +++ b/lib/connection_config.js @@ -65,7 +65,8 @@ const validOptions = { idleTimeout: 1, Promise: 1, queueLimit: 1, - waitForConnections: 1 + waitForConnections: 1, + useStaticParser: 1 }; class ConnectionConfig { @@ -180,6 +181,7 @@ class ConnectionConfig { }; this.connectAttributes = { ...defaultConnectAttributes, ...(options.connectAttributes || {})}; this.maxPreparedStatements = options.maxPreparedStatements || 16000; + this.useStaticParser = options.useStaticParser || false; } static mergeFlags(default_flags, user_flags) { diff --git a/lib/parsers/static_text_parser.js b/lib/parsers/static_text_parser.js new file mode 100644 index 0000000000..73ffcd26a2 --- /dev/null +++ b/lib/parsers/static_text_parser.js @@ -0,0 +1,135 @@ +'use strict'; + +const Types = require('../constants/types.js'); +const Charsets = require('../constants/charsets.js'); +const helpers = require('../helpers'); + +const typeNames = []; +for (const t in Types) { + typeNames[Types[t]] = t; +} + +function readField({ packet, type, charset, encoding, config, options }) { + const supportBigNumbers = + options.supportBigNumbers || config.supportBigNumbers; + const bigNumberStrings = options.bigNumberStrings || config.bigNumberStrings; + const timezone = options.timezone || config.timezone; + const dateStrings = options.dateStrings || config.dateStrings; + + switch (type) { + case Types.TINY: + case Types.SHORT: + case Types.LONG: + case Types.INT24: + case Types.YEAR: + return packet.parseLengthCodedIntNoBigCheck(); + case Types.LONGLONG: + if (supportBigNumbers && bigNumberStrings) { + return packet.parseLengthCodedIntString(); + } + return packet.parseLengthCodedInt(supportBigNumbers); + case Types.FLOAT: + case Types.DOUBLE: + return packet.parseLengthCodedFloat(); + case Types.NULL: + // case Types.BIT: + // return 'packet.readBuffer(2)[1]'; + case Types.DECIMAL: + case Types.NEWDECIMAL: + if (config.decimalNumbers) { + return packet.parseLengthCodedFloat(); + } + return packet.readLengthCodedString("ascii"); + case Types.DATE: + if (helpers.typeMatch(type, dateStrings, Types)) { + return packet.readLengthCodedString("ascii"); + } + return packet.parseDate(timezone); + case Types.DATETIME: + case Types.TIMESTAMP: + if (helpers.typeMatch(type, dateStrings, Types)) { + return packet.readLengthCodedString('ascii'); + } + return packet.parseDateTime(timezone); + case Types.TIME: + return packet.readLengthCodedString('ascii'); + case Types.GEOMETRY: + return packet.parseGeometryValue(); + case Types.JSON: + // Since for JSON columns mysql always returns charset 63 (BINARY), + // we have to handle it according to JSON specs and use "utf8", + // see https://github.com/sidorares/node-mysql2/issues/409 + return JSON.parse(packet.readLengthCodedString('utf8')); + default: + if (charset === Charsets.BINARY) { + return packet.readLengthCodedBuffer(); + } + return packet.readLengthCodedString(encoding); + } +} + +function createTypecastField(field, packet) { + return { + type: typeNames[field.columnType], + length: field.columnLength, + db: field.schema, + table: field.table, + name: field.name, + string: function(encoding = field.encoding) { + if (field.columnType === Types.JSON && encoding === field.encoding) { + // Since for JSON columns mysql always returns charset 63 (BINARY), + // we have to handle it according to JSON specs and use "utf8", + // see https://github.com/sidorares/node-mysql2/issues/1661 + console.warn(`typeCast: JSON column "${field.name}" is interpreted as BINARY by default, recommended to manually set utf8 encoding: \`field.string("utf8")\``); + } + return packet.readLengthCodedString(encoding); + }, + buffer: function() { + return packet.readLengthCodedBuffer(); + }, + geometry: function() { + return packet.parseGeometryValue(); + } + }; +} + +function getStaticTextParser(fields, options, config) { + return { + next(packet, fields, options) { + const result = options.rowsAsArray ? [] : {}; + for (let i = 0; i < fields.length; i++) { + const field = fields[i]; + const typeCast = options.typeCast ? options.typeCast : config.typeCast; + const next = () => { + return readField({ + packet, + type: field.columnType, + encoding: field.encoding, + charset: field.characterSet, + config, + options + }) + } + let value; + if (typeof typeCast === 'function') { + value = typeCast(createTypecastField(field, packet), next); + } else { + value = next(); + } + if (options.rowsAsArray) { + result.push(value); + } else if (options.nestTables) { + if (!result[field.table]) { + result[field.table] = {}; + } + result[field.table][field.name] = value; + } else { + result[field.name] = value; + } + } + return result; + } + } +} + +module.exports = getStaticTextParser; diff --git a/lib/stream.js b/lib/stream.js new file mode 100644 index 0000000000..3291dcbedc --- /dev/null +++ b/lib/stream.js @@ -0,0 +1,14 @@ +"use strict" + +/** + * Get a socket stream compatible with the current runtime environment. + * @returns {Socket} + */ +module.exports.getStream = function getStream(ssl = false) { + const net = require('net') + if (typeof net.Socket === 'function') { + return net.Socket + } + const { CloudflareSocket } = require('mysql-cloudflare') + return new CloudflareSocket(ssl); +} diff --git a/package.json b/package.json index 8a0f46789d..1005ff74da 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "iconv-lite": "^0.6.3", "long": "^5.2.1", "lru-cache": "^8.0.0", + "mysql-cloudflare": "workspace:^", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index 94527c5d9a..24b543b91f 100644 --- a/typings/mysql/lib/Connection.d.ts +++ b/typings/mysql/lib/Connection.d.ts @@ -297,6 +297,8 @@ export interface ConnectionOptions { authPlugins?: { [key: string]: AuthPlugin; }; + + useStaticParser?: boolean; } declare class Connection extends QueryableBase(ExecutableBase(EventEmitter)) { From 0e082075e7e2257717be8fe5ea5a721f122a3291 Mon Sep 17 00:00:00 2001 From: Mini256 Date: Fri, 17 Nov 2023 17:44:12 +0800 Subject: [PATCH 02/17] using pg-cloudflare --- lib/stream.js | 2 +- package-lock.json | 15 +++++++++++++++ package.json | 4 +++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index 3291dcbedc..31a02bfe91 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -9,6 +9,6 @@ module.exports.getStream = function getStream(ssl = false) { if (typeof net.Socket === 'function') { return net.Socket } - const { CloudflareSocket } = require('mysql-cloudflare') + const { CloudflareSocket } = require('pg-cloudflare') return new CloudflareSocket(ssl); } diff --git a/package-lock.json b/package-lock.json index 48c2019b27..91707ff6e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,9 @@ }, "engines": { "node": ">= 8.0" + }, + "optionalDependencies": { + "pg-cloudflare": "1.x" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2406,6 +2409,12 @@ "node": ">=8" } }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -4850,6 +4859,12 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", diff --git a/package.json b/package.json index 1005ff74da..ad8adeabf0 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,6 @@ "iconv-lite": "^0.6.3", "long": "^5.2.1", "lru-cache": "^8.0.0", - "mysql-cloudflare": "workspace:^", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" @@ -87,5 +86,8 @@ "typescript": "^5.0.2", "urun": "0.0.8", "utest": "0.0.8" + }, + "optionalDependencies": { + "pg-cloudflare": "1.x" } } From a03c024522ee7806c89cf2326baab8c911364b8a Mon Sep 17 00:00:00 2001 From: Mini256 Date: Tue, 21 Nov 2023 15:27:51 +0800 Subject: [PATCH 03/17] polyfill crypto lib --- lib/auth_41.js | 38 +++----- lib/auth_plugins/mysql_native_password.js | 6 +- lib/commands/change_user.js | 6 +- lib/commands/client_handshake.js | 18 ++-- lib/connection.js | 2 +- lib/packets/change_user.js | 20 +++-- lib/packets/handshake_response.js | 26 ++++-- lib/stream.js | 4 +- lib/utils/crypto.js | 9 ++ lib/utils/nodecrypto.js | 69 ++++++++++++++ lib/utils/webcrypto.js | 105 ++++++++++++++++++++++ 11 files changed, 248 insertions(+), 55 deletions(-) create mode 100644 lib/utils/crypto.js create mode 100644 lib/utils/nodecrypto.js create mode 100644 lib/utils/webcrypto.js diff --git a/lib/auth_41.js b/lib/auth_41.js index 15d6d032eb..f6c7726829 100644 --- a/lib/auth_41.js +++ b/lib/auth_41.js @@ -24,21 +24,7 @@ server stores sha1(sha1(password)) ( hash_stag2) */ -const crypto = require('crypto'); - -function sha1(msg, msg1, msg2) { - const hash = crypto.createHash('sha1'); - hash.update(msg); - if (msg1) { - hash.update(msg1); - } - - if (msg2) { - hash.update(msg2); - } - - return hash.digest(); -} +const crypto = require('./utils/crypto'); function xor(a, b) { const result = Buffer.allocUnsafe(a.length); @@ -50,15 +36,15 @@ function xor(a, b) { exports.xor = xor; -function token(password, scramble1, scramble2) { +async function token(password, scramble1, scramble2) { if (!password) { return Buffer.alloc(0); } - const stage1 = sha1(password); - return exports.calculateTokenFromPasswordSha(stage1, scramble1, scramble2); + const stage1 = await crypto.sha1(password); + return await exports.calculateTokenFromPasswordSha(stage1, scramble1, scramble2); } -exports.calculateTokenFromPasswordSha = function( +exports.calculateTokenFromPasswordSha = async function( passwordSha, scramble1, scramble2 @@ -66,21 +52,21 @@ exports.calculateTokenFromPasswordSha = function( // we use AUTH 41 here, and we need only the bytes we just need. const authPluginData1 = scramble1.slice(0, 8); const authPluginData2 = scramble2.slice(0, 12); - const stage2 = sha1(passwordSha); - const stage3 = sha1(authPluginData1, authPluginData2, stage2); + const stage2 = await crypto.sha1(passwordSha); + const stage3 = await crypto.sha1(authPluginData1, authPluginData2, stage2); return xor(stage3, passwordSha); }; exports.calculateToken = token; -exports.verifyToken = function(publicSeed1, publicSeed2, token, doubleSha) { - const hashStage1 = xor(token, sha1(publicSeed1, publicSeed2, doubleSha)); - const candidateHash2 = sha1(hashStage1); +exports.verifyToken = async function(publicSeed1, publicSeed2, token, doubleSha) { + const hashStage1 = xor(token, await crypto.sha1(publicSeed1, publicSeed2, doubleSha)); + const candidateHash2 = await crypto.sha1(hashStage1); return candidateHash2.compare(doubleSha) === 0; }; -exports.doubleSha1 = function(password) { - return sha1(sha1(password)); +exports.doubleSha1 = async function(password) { + return await crypto.sha1(await crypto.sha1(password)); }; function xorRotating(a, seed) { diff --git a/lib/auth_plugins/mysql_native_password.js b/lib/auth_plugins/mysql_native_password.js index 83d2618c00..0fd88e237c 100644 --- a/lib/auth_plugins/mysql_native_password.js +++ b/lib/auth_plugins/mysql_native_password.js @@ -10,18 +10,18 @@ module.exports = pluginOptions => ({ connection, command }) => { command.passwordSha1 || pluginOptions.passwordSha1 || connection.config.passwordSha1; - return data => { + return async data => { const authPluginData1 = data.slice(0, 8); const authPluginData2 = data.slice(8, 20); let authToken; if (passwordSha1) { - authToken = auth41.calculateTokenFromPasswordSha( + authToken = await auth41.calculateTokenFromPasswordSha( passwordSha1, authPluginData1, authPluginData2 ); } else { - authToken = auth41.calculateToken( + authToken = await auth41.calculateToken( password, authPluginData1, authPluginData2 diff --git a/lib/commands/change_user.js b/lib/commands/change_user.js index 0e8715b64c..78d7dd2038 100644 --- a/lib/commands/change_user.js +++ b/lib/commands/change_user.js @@ -46,7 +46,11 @@ class ChangeUser extends Command { connection.clientEncoding = CharsetToEncoding[this.charsetNumber]; // clear prepared statements cache as all statements become invalid after changeUser connection._statements.clear(); - connection.writePacket(newPacket.toPacket()); + newPacket.toPacket().then(packet => { + connection.writePacket(packet); + }).catch(err => { + this.onResult(err); + }); // check if the server supports multi-factor authentication const multiFactorAuthentication = connection.serverCapabilityFlags & ClientConstants.MULTI_FACTOR_AUTHENTICATION; if (multiFactorAuthentication) { diff --git a/lib/commands/client_handshake.js b/lib/commands/client_handshake.js index f6cd56ccdf..e48720dc3a 100644 --- a/lib/commands/client_handshake.js +++ b/lib/commands/client_handshake.js @@ -46,7 +46,7 @@ class ClientHandshake extends Command { connection.writePacket(sslRequest.toPacket()); } - sendCredentials(connection) { + async sendCredentials(connection) { if (connection.config.debug) { // eslint-disable-next-line console.log( @@ -80,22 +80,22 @@ class ClientHandshake extends Command { compress: connection.config.compress, connectAttributes: connection.config.connectAttributes }); - connection.writePacket(handshakeResponse.toPacket()); + connection.writePacket(await handshakeResponse.toPacket()); } - calculateNativePasswordAuthToken(authPluginData) { + async calculateNativePasswordAuthToken(authPluginData) { // TODO: dont split into authPluginData1 and authPluginData2, instead join when 1 & 2 received const authPluginData1 = authPluginData.slice(0, 8); const authPluginData2 = authPluginData.slice(8, 20); let authToken; if (this.passwordSha1) { - authToken = auth41.calculateTokenFromPasswordSha( + authToken = await auth41.calculateTokenFromPasswordSha( this.passwordSha1, authPluginData1, authPluginData2 ); } else { - authToken = auth41.calculateToken( + authToken = await auth41.calculateToken( this.password, authPluginData1, authPluginData2 @@ -156,10 +156,14 @@ class ClientHandshake extends Command { return; } // rest of communication is encrypted - this.sendCredentials(connection); + this.sendCredentials(connection).catch(err => { + this.emit('error', err); + }); }); } else { - this.sendCredentials(connection); + this.sendCredentials(connection).catch(err => { + this.emit('error', err); + }); } if (multiFactorAuthentication) { // if the server supports multi-factor authentication, we enable it in diff --git a/lib/connection.js b/lib/connection.js index a8c06d964f..b340a91571 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -43,7 +43,7 @@ class Connection extends EventEmitter { // TODO: use `/usr/local/mysql/bin/mysql_config --socket` output? as default socketPath // if there is no host/port and no socketPath parameters? if (!opts.config.stream) { - this.stream = getStream(opts.config.ssl); + this.stream = getStream(!!opts.config.ssl); if (opts.config.socketPath) { // FIXME this.stream.connect(opts.config.socketPath); diff --git a/lib/packets/change_user.js b/lib/packets/change_user.js index 3fa3937bdb..6a0ad6949a 100644 --- a/lib/packets/change_user.js +++ b/lib/packets/change_user.js @@ -17,28 +17,33 @@ class ChangeUser { this.authPluginData1 = opts.authPluginData1; this.authPluginData2 = opts.authPluginData2; this.connectAttributes = opts.connectAttrinutes || {}; + this.authTokenReadly = this.initAuthToken(); + this.charsetNumber = opts.charsetNumber; + } + + async initAuthToken() { let authToken; if (this.passwordSha1) { - authToken = auth41.calculateTokenFromPasswordSha( + authToken = await auth41.calculateTokenFromPasswordSha( this.passwordSha1, this.authPluginData1, this.authPluginData2 ); } else { - authToken = auth41.calculateToken( + authToken = await auth41.calculateToken( this.password, this.authPluginData1, this.authPluginData2 ); } this.authToken = authToken; - this.charsetNumber = opts.charsetNumber; } // TODO // ChangeUser.fromPacket = function(packet) // }; - serializeToBuffer(buffer) { + async serializeToBuffer(buffer) { + await this.authTokenReadly; const isSet = flag => this.flags & ClientConstants[flag]; const packet = new Packet(0, buffer, 0, buffer.length); packet.offset = 4; @@ -81,7 +86,8 @@ class ChangeUser { return packet; } - toPacket() { + async toPacket() { + await this.authTokenReadly; if (typeof this.user !== 'string') { throw new Error('"user" connection config property must be a string'); } @@ -89,8 +95,8 @@ class ChangeUser { throw new Error('"database" connection config property must be a string'); } // dry run: calculate resulting packet length - const p = this.serializeToBuffer(Packet.MockBuffer()); - return this.serializeToBuffer(Buffer.allocUnsafe(p.offset)); + const p = await this.serializeToBuffer(Packet.MockBuffer()); + return await this.serializeToBuffer(Buffer.allocUnsafe(p.offset)); } } diff --git a/lib/packets/handshake_response.js b/lib/packets/handshake_response.js index b2dee38c94..3794aa33d1 100644 --- a/lib/packets/handshake_response.js +++ b/lib/packets/handshake_response.js @@ -16,28 +16,34 @@ class HandshakeResponse { this.authPluginData2 = handshake.authPluginData2; this.compress = handshake.compress; this.clientFlags = handshake.flags; + this.authTokenReady = this.initAuthToken(); + this.charsetNumber = handshake.charsetNumber; + this.encoding = CharsetToEncoding[handshake.charsetNumber]; + this.connectAttributes = handshake.connectAttributes; + } + + async initAuthToken() { // TODO: pre-4.1 auth support let authToken; if (this.passwordSha1) { - authToken = auth41.calculateTokenFromPasswordSha( + authToken = await auth41.calculateTokenFromPasswordSha( this.passwordSha1, this.authPluginData1, this.authPluginData2 ); } else { - authToken = auth41.calculateToken( + authToken = await auth41.calculateToken( this.password, this.authPluginData1, this.authPluginData2 ); } this.authToken = authToken; - this.charsetNumber = handshake.charsetNumber; - this.encoding = CharsetToEncoding[handshake.charsetNumber]; - this.connectAttributes = handshake.connectAttributes; } - serializeResponse(buffer) { + async serializeResponse(buffer) { + await this.authTokenReady; + const isSet = flag => this.clientFlags & ClientConstants[flag]; const packet = new Packet(0, buffer, 0, buffer.length); packet.offset = 4; @@ -88,7 +94,8 @@ class HandshakeResponse { return packet; } - toPacket() { + async toPacket() { + await this.authTokenReady; if (typeof this.user !== 'string') { throw new Error('"user" connection config property must be a string'); } @@ -96,9 +103,10 @@ class HandshakeResponse { throw new Error('"database" connection config property must be a string'); } // dry run: calculate resulting packet length - const p = this.serializeResponse(Packet.MockBuffer()); - return this.serializeResponse(Buffer.alloc(p.offset)); + const p = await this.serializeResponse(Packet.MockBuffer()); + return await this.serializeResponse(Buffer.alloc(p.offset)); } + static fromPacket(packet) { const args = {}; args.clientFlags = packet.readInt32(); diff --git a/lib/stream.js b/lib/stream.js index 31a02bfe91..db87ce9bd2 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -7,8 +7,10 @@ module.exports.getStream = function getStream(ssl = false) { const net = require('net') if (typeof net.Socket === 'function') { - return net.Socket + console.log('using node socket'); + return net.Socket() } const { CloudflareSocket } = require('pg-cloudflare') + console.log('using cloudflare socket'); return new CloudflareSocket(ssl); } diff --git a/lib/utils/crypto.js b/lib/utils/crypto.js new file mode 100644 index 0000000000..bca070f92b --- /dev/null +++ b/lib/utils/crypto.js @@ -0,0 +1,9 @@ +'use strict' + +const useLegacyCrypto = parseInt(process.versions && process.versions.node && process.versions.node.split('.')[0]) < 15 +if (useLegacyCrypto) { + // We are on an old version of Node.js that requires legacy crypto utilities. + module.exports = require('./nodecrypto') +} else { + module.exports = require('./webcrypto'); +} diff --git a/lib/utils/nodecrypto.js b/lib/utils/nodecrypto.js new file mode 100644 index 0000000000..d2f1e71c77 --- /dev/null +++ b/lib/utils/nodecrypto.js @@ -0,0 +1,69 @@ +'use strict' +// This file contains crypto utility functions for versions of Node.js < 15.0.0, +// which does not support the WebCrypto.subtle API. + +const nodeCrypto = require('crypto') + +function md5(string) { + return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex') +} + +// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html +function postgresMd5PasswordHash(user, password, salt) { + const inner = md5(password + user); + const outer = md5(Buffer.concat([Buffer.from(inner), salt])); + return `md5${outer}` +} + +function sha256(text) { + return nodeCrypto.createHash('sha256').update(text).digest() +} + +async function sha1(msg,msg1,msg2) { + const hash = nodeCrypto.createHash('sha1'); + hash.update(msg); + if (msg1) { + hash.update(msg1); + } + if (msg2) { + hash.update(msg2); + } + return hash.digest(); +} + +function xorRotating(a, seed) { + const result = Buffer.allocUnsafe(a.length); + const seedLen = seed.length; + + for (let i = 0; i < a.length; i++) { + result[i] = a[i] ^ seed[i % seedLen]; + } + return result; +} + +function encrypt(password, scramble, key) { + const stage1 = xorRotating( + Buffer.from(`${password}\0`, 'utf8'), + scramble + ); + return nodeCrypto.publicEncrypt(key, stage1); +} + +function hmacSha256(key, msg) { + return nodeCrypto.createHmac('sha256', key).update(msg).digest() +} + +function deriveKey(password, salt, iterations) { + return nodeCrypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256') +} + +module.exports = { + postgresMd5PasswordHash, + randomBytes: nodeCrypto.randomBytes, + deriveKey, + sha256, + hmacSha256, + md5, + sha1, + encrypt, +} diff --git a/lib/utils/webcrypto.js b/lib/utils/webcrypto.js new file mode 100644 index 0000000000..7265feec7b --- /dev/null +++ b/lib/utils/webcrypto.js @@ -0,0 +1,105 @@ +'use strict' + +const nodeCrypto = require('crypto') + +/** + * The Web Crypto API - grabbed from the Node.js library or the global + * @type Crypto + */ +// eslint-disable-next-line no-undef +const webCrypto = nodeCrypto.webcrypto || globalThis.crypto +/** + * The SubtleCrypto API for low level crypto operations. + * @type SubtleCrypto + */ +const subtleCrypto = webCrypto.subtle +const textEncoder = new TextEncoder() + +/** + * + * @param {*} length + * @returns + */ +function randomBytes(length) { + return webCrypto.getRandomValues(Buffer.alloc(length)) +} + +async function md5(string) { + try { + return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex') + } catch (e) { + // `createHash()` failed so we are probably not in Node.js, use the WebCrypto API instead. + // Note that the MD5 algorithm on WebCrypto is not available in Node.js. + // This is why we cannot just use WebCrypto in all environments. + const data = typeof string === 'string' ? textEncoder.encode(string) : string + const hash = await subtleCrypto.digest('MD5', data) + return Array.from(new Uint8Array(hash)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + } +} + +// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html +async function postgresMd5PasswordHash(user, password, salt) { + const inner = await md5(password + user); + const outer = await md5(Buffer.concat([Buffer.from(inner), salt])); + return `md5${outer}` +} + +/** + * Create a SHA-256 digest of the given data + * @param {Buffer} data + */ +async function sha256(text) { + return await subtleCrypto.digest('SHA-256', text) +} + +/** + * Sign the message with the given key + * @param {ArrayBuffer} keyBuffer + * @param {string} msg + */ +async function hmacSha256(keyBuffer, msg) { + const key = await subtleCrypto.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) + return await subtleCrypto.sign('HMAC', key, textEncoder.encode(msg)) +} + +/** + * Derive a key from the password and salt + * @param {string} password + * @param {Uint8Array} salt + * @param {number} iterations + */ +async function deriveKey(password, salt, iterations) { + const key = await subtleCrypto.importKey('raw', textEncoder.encode(password), 'PBKDF2', false, ['deriveBits']) + const params = { name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations: iterations } + return await subtleCrypto.deriveBits(params, key, 32 * 8, ['deriveBits']) +} + +function concatenateBuffers(buffer1, buffer2) { + const combined = new Uint8Array(buffer1.length + buffer2.length); + combined.set(new Uint8Array(buffer1), 0); + combined.set(new Uint8Array(buffer2), buffer1.length); + return combined; +} + + +async function sha1(msg,msg1,msg2) { + + let concatenatedData = textEncoder.encode(msg); + concatenatedData = concatenateBuffers(concatenatedData, textEncoder.encode(msg1)); + concatenatedData = concatenateBuffers(concatenatedData, textEncoder.encode(msg2)); + + const arrayBuffer = await subtleCrypto.digest('SHA-1', concatenatedData) + return Buffer.from(arrayBuffer) +} + +module.exports = { + postgresMd5PasswordHash, + randomBytes, + deriveKey, + sha256, + hmacSha256, + md5, + sha1, +} From d644cb8a4babcc6c2d88228ec06988388b632713 Mon Sep 17 00:00:00 2001 From: Mini256 Date: Tue, 21 Nov 2023 15:40:50 +0800 Subject: [PATCH 04/17] fix tests --- lib/stream.js | 2 -- test/integration/connection/test-connect-sha1.js | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index db87ce9bd2..0ad363e06a 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -7,10 +7,8 @@ module.exports.getStream = function getStream(ssl = false) { const net = require('net') if (typeof net.Socket === 'function') { - console.log('using node socket'); return net.Socket() } const { CloudflareSocket } = require('pg-cloudflare') - console.log('using cloudflare socket'); return new CloudflareSocket(ssl); } diff --git a/test/integration/connection/test-connect-sha1.js b/test/integration/connection/test-connect-sha1.js index 4372c6221e..d86b584a49 100644 --- a/test/integration/connection/test-connect-sha1.js +++ b/test/integration/connection/test-connect-sha1.js @@ -4,9 +4,9 @@ const mysql = require('../../../index.js'); const auth = require('../../../lib/auth_41.js'); const assert = require('assert'); -function authenticate(params, cb) { - const doubleSha = auth.doubleSha1('testpassword'); - const isValid = auth.verifyToken( +async function authenticate(params, cb) { + const doubleSha = await auth.doubleSha1('testpassword'); + const isValid = await auth.verifyToken( params.authPluginData1, params.authPluginData2, params.authToken, From 5514defcd626c54bbe7f6a1ba6c301db7246870b Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Tue, 21 Nov 2023 17:04:54 +0800 Subject: [PATCH 05/17] support tls --- lib/commands/client_handshake.js | 23 +++++------------------ lib/connection.js | 2 +- lib/stream.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/commands/client_handshake.js b/lib/commands/client_handshake.js index e48720dc3a..c624ff5478 100644 --- a/lib/commands/client_handshake.js +++ b/lib/commands/client_handshake.js @@ -15,6 +15,7 @@ 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 {secureStream} = require('../stream.js'); function flagNames(flags) { const res = []; @@ -146,25 +147,11 @@ class ClientHandshake extends Command { // send ssl upgrade request and immediately upgrade connection to secure this.clientFlags |= ClientConstants.SSL; this.sendSSLRequest(connection); - connection.startTLS(err => { - // after connection is secure - if (err) { - // SSL negotiation error are fatal - err.code = 'HANDSHAKE_SSL_ERROR'; - err.fatal = true; - this.emit('error', err); - return; - } - // rest of communication is encrypted - this.sendCredentials(connection).catch(err => { - this.emit('error', err); - }); - }); - } else { - this.sendCredentials(connection).catch(err => { - this.emit('error', err); - }); + secureStream(connection) } + this.sendCredentials(connection).catch(err => { + this.emit('error', err); + }); if (multiFactorAuthentication) { // if the server supports multi-factor authentication, we enable it in // the client diff --git a/lib/connection.js b/lib/connection.js index b340a91571..3b9a588648 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -15,7 +15,6 @@ 'use strict'; -const Tls = require('tls'); const EventEmitter = require('events').EventEmitter; const Readable = require('stream').Readable; const Queue = require('denque'); @@ -341,6 +340,7 @@ class Connection extends EventEmitter { // 0.11+ environment startTLS(onSecure) { + const Tls = require('tls'); if (this.config.debug) { // eslint-disable-next-line no-console console.log('Upgrading connection to TLS'); diff --git a/lib/stream.js b/lib/stream.js index 0ad363e06a..f1de4d7271 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -12,3 +12,31 @@ module.exports.getStream = function getStream(ssl = false) { const { CloudflareSocket } = require('pg-cloudflare') return new CloudflareSocket(ssl); } + +/** + * Get a TLS secured socket, compatible with the current environment, + * using the socket and other settings given in `options`. + */ +module.exports.secureStream = function secureStream(connection) { + const Tls = require('tls'); + if (Tls.connect) { + connection.startTLS(err => { + // after connection is secure + if (err) { + // SSL negotiation error are fatal + err.code = 'HANDSHAKE_SSL_ERROR'; + err.fatal = true; + this.emit('error', err); + } + }); + return + } + try { + connection.stream.startTls({}); + }catch (err) { + // SSL negotiation error are fatal + err.code = 'HANDSHAKE_SSL_ERROR'; + err.fatal = true; + this.emit('error', err); + } +} From c4d5458016008dc7c1ee6d15a15bc371285b8bf0 Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Tue, 21 Nov 2023 19:10:07 +0800 Subject: [PATCH 06/17] fix access deny error --- lib/utils/webcrypto.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/utils/webcrypto.js b/lib/utils/webcrypto.js index 7265feec7b..a0c93efd93 100644 --- a/lib/utils/webcrypto.js +++ b/lib/utils/webcrypto.js @@ -85,11 +85,13 @@ function concatenateBuffers(buffer1, buffer2) { async function sha1(msg,msg1,msg2) { - - let concatenatedData = textEncoder.encode(msg); - concatenatedData = concatenateBuffers(concatenatedData, textEncoder.encode(msg1)); - concatenatedData = concatenateBuffers(concatenatedData, textEncoder.encode(msg2)); - + let concatenatedData = typeof msg === 'string' ? textEncoder.encode(msg) : msg; + if (msg1) { + concatenatedData = concatenateBuffers(concatenatedData, typeof msg1 === 'string' ? textEncoder.encode(msg1) : msg1); + } + if (msg2) { + concatenatedData = concatenateBuffers(concatenatedData, typeof msg2 === 'string' ? textEncoder.encode(msg2) : msg2); + } const arrayBuffer = await subtleCrypto.digest('SHA-1', concatenatedData) return Buffer.from(arrayBuffer) } From 590cdef244e9abc40adfeeb972bf69b8b174cb13 Mon Sep 17 00:00:00 2001 From: shi yuhang <52435083+shiyuhang0@users.noreply.github.com> Date: Tue, 21 Nov 2023 19:34:00 +0800 Subject: [PATCH 07/17] Update lib/connection.js --- lib/connection.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/connection.js b/lib/connection.js index 3b9a588648..7903a14e9f 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -42,7 +42,7 @@ class Connection extends EventEmitter { // TODO: use `/usr/local/mysql/bin/mysql_config --socket` output? as default socketPath // if there is no host/port and no socketPath parameters? if (!opts.config.stream) { - this.stream = getStream(!!opts.config.ssl); + this.stream = getStream(opts.config.ssl); if (opts.config.socketPath) { // FIXME this.stream.connect(opts.config.socketPath); From f2fe8d9015ddf56a64166aa11c18fc7ad5aa83d4 Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Tue, 21 Nov 2023 19:39:24 +0800 Subject: [PATCH 08/17] delete useless code & verify servername --- lib/stream.js | 4 ++- lib/utils/nodecrypto.js | 50 +---------------------------- lib/utils/webcrypto.js | 70 +---------------------------------------- 3 files changed, 5 insertions(+), 119 deletions(-) diff --git a/lib/stream.js b/lib/stream.js index f1de4d7271..694ba75e11 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -32,7 +32,9 @@ module.exports.secureStream = function secureStream(connection) { return } try { - connection.stream.startTls({}); + // Configuration of TLS will not work in Cloudflare Workers because startTls doesn't have the corresponding options. + // See https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/#opportunistic-tls-starttls + connection.stream.startTls({expectedServerHostname: connection.config.host}); }catch (err) { // SSL negotiation error are fatal err.code = 'HANDSHAKE_SSL_ERROR'; diff --git a/lib/utils/nodecrypto.js b/lib/utils/nodecrypto.js index d2f1e71c77..fd80ea4bed 100644 --- a/lib/utils/nodecrypto.js +++ b/lib/utils/nodecrypto.js @@ -4,21 +4,6 @@ const nodeCrypto = require('crypto') -function md5(string) { - return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex') -} - -// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html -function postgresMd5PasswordHash(user, password, salt) { - const inner = md5(password + user); - const outer = md5(Buffer.concat([Buffer.from(inner), salt])); - return `md5${outer}` -} - -function sha256(text) { - return nodeCrypto.createHash('sha256').update(text).digest() -} - async function sha1(msg,msg1,msg2) { const hash = nodeCrypto.createHash('sha1'); hash.update(msg); @@ -31,39 +16,6 @@ async function sha1(msg,msg1,msg2) { return hash.digest(); } -function xorRotating(a, seed) { - const result = Buffer.allocUnsafe(a.length); - const seedLen = seed.length; - - for (let i = 0; i < a.length; i++) { - result[i] = a[i] ^ seed[i % seedLen]; - } - return result; -} - -function encrypt(password, scramble, key) { - const stage1 = xorRotating( - Buffer.from(`${password}\0`, 'utf8'), - scramble - ); - return nodeCrypto.publicEncrypt(key, stage1); -} - -function hmacSha256(key, msg) { - return nodeCrypto.createHmac('sha256', key).update(msg).digest() -} - -function deriveKey(password, salt, iterations) { - return nodeCrypto.pbkdf2Sync(password, salt, iterations, 32, 'sha256') -} - module.exports = { - postgresMd5PasswordHash, - randomBytes: nodeCrypto.randomBytes, - deriveKey, - sha256, - hmacSha256, - md5, - sha1, - encrypt, + sha1 } diff --git a/lib/utils/webcrypto.js b/lib/utils/webcrypto.js index a0c93efd93..4dc46dabc8 100644 --- a/lib/utils/webcrypto.js +++ b/lib/utils/webcrypto.js @@ -15,67 +15,6 @@ const webCrypto = nodeCrypto.webcrypto || globalThis.crypto const subtleCrypto = webCrypto.subtle const textEncoder = new TextEncoder() -/** - * - * @param {*} length - * @returns - */ -function randomBytes(length) { - return webCrypto.getRandomValues(Buffer.alloc(length)) -} - -async function md5(string) { - try { - return nodeCrypto.createHash('md5').update(string, 'utf-8').digest('hex') - } catch (e) { - // `createHash()` failed so we are probably not in Node.js, use the WebCrypto API instead. - // Note that the MD5 algorithm on WebCrypto is not available in Node.js. - // This is why we cannot just use WebCrypto in all environments. - const data = typeof string === 'string' ? textEncoder.encode(string) : string - const hash = await subtleCrypto.digest('MD5', data) - return Array.from(new Uint8Array(hash)) - .map(b => b.toString(16).padStart(2, '0')) - .join('') - } -} - -// See AuthenticationMD5Password at https://www.postgresql.org/docs/current/static/protocol-flow.html -async function postgresMd5PasswordHash(user, password, salt) { - const inner = await md5(password + user); - const outer = await md5(Buffer.concat([Buffer.from(inner), salt])); - return `md5${outer}` -} - -/** - * Create a SHA-256 digest of the given data - * @param {Buffer} data - */ -async function sha256(text) { - return await subtleCrypto.digest('SHA-256', text) -} - -/** - * Sign the message with the given key - * @param {ArrayBuffer} keyBuffer - * @param {string} msg - */ -async function hmacSha256(keyBuffer, msg) { - const key = await subtleCrypto.importKey('raw', keyBuffer, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']) - return await subtleCrypto.sign('HMAC', key, textEncoder.encode(msg)) -} - -/** - * Derive a key from the password and salt - * @param {string} password - * @param {Uint8Array} salt - * @param {number} iterations - */ -async function deriveKey(password, salt, iterations) { - const key = await subtleCrypto.importKey('raw', textEncoder.encode(password), 'PBKDF2', false, ['deriveBits']) - const params = { name: 'PBKDF2', hash: 'SHA-256', salt: salt, iterations: iterations } - return await subtleCrypto.deriveBits(params, key, 32 * 8, ['deriveBits']) -} - function concatenateBuffers(buffer1, buffer2) { const combined = new Uint8Array(buffer1.length + buffer2.length); combined.set(new Uint8Array(buffer1), 0); @@ -83,7 +22,6 @@ function concatenateBuffers(buffer1, buffer2) { return combined; } - async function sha1(msg,msg1,msg2) { let concatenatedData = typeof msg === 'string' ? textEncoder.encode(msg) : msg; if (msg1) { @@ -97,11 +35,5 @@ async function sha1(msg,msg1,msg2) { } module.exports = { - postgresMd5PasswordHash, - randomBytes, - deriveKey, - sha256, - hmacSha256, - md5, - sha1, + sha1 } From 758f829c64fae8a97dfa18d8f63f3f65daad84c5 Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Mon, 25 Dec 2023 15:00:05 +0800 Subject: [PATCH 09/17] support cacheing-sha2 --- lib/auth_plugins/caching_sha2_password.js | 32 +++++++---------- lib/auth_plugins/sha256_password.js | 16 ++++----- lib/utils/nodecrypto.js | 14 +++++++- lib/utils/webcrypto.js | 44 ++++++++++++++++++++++- 4 files changed, 77 insertions(+), 29 deletions(-) diff --git a/lib/auth_plugins/caching_sha2_password.js b/lib/auth_plugins/caching_sha2_password.js index 4245f934ea..6c45b1b61d 100644 --- a/lib/auth_plugins/caching_sha2_password.js +++ b/lib/auth_plugins/caching_sha2_password.js @@ -3,7 +3,7 @@ // https://mysqlserverteam.com/mysql-8-0-4-new-default-authentication-plugin-caching_sha2_password/ const PLUGIN_NAME = 'caching_sha2_password'; -const crypto = require('crypto'); +const crypto = require('../utils/crypto'); const { xor, xorRotating } = require('../auth_41'); const REQUEST_SERVER_KEY_PACKET = Buffer.from([2]); @@ -15,28 +15,22 @@ const STATE_TOKEN_SENT = 1; const STATE_WAIT_SERVER_KEY = 2; const STATE_FINAL = -1; -function sha256(msg) { - const hash = crypto.createHash('sha256'); - hash.update(msg); - return hash.digest(); -} - -function calculateToken(password, scramble) { +async function calculateToken(password, scramble) { if (!password) { return Buffer.alloc(0); } - const stage1 = sha256(Buffer.from(password)); - const stage2 = sha256(stage1); - const stage3 = sha256(Buffer.concat([stage2, scramble])); + const stage1 = await crypto.sha256(Buffer.from(password)); + const stage2 = await crypto.sha256(stage1); + const stage3 = await crypto.sha256(Buffer.concat([stage2, scramble])); return xor(stage1, stage3); } -function encrypt(password, scramble, key) { +async function encrypt(password, scramble, key) { const stage1 = xorRotating( Buffer.from(`${password}\0`, 'utf8'), scramble ); - return crypto.publicEncrypt(key, stage1); + return await crypto.publicEncrypt(key, stage1); } module.exports = (pluginOptions = {}) => ({ connection }) => { @@ -45,18 +39,18 @@ module.exports = (pluginOptions = {}) => ({ connection }) => { const password = connection.config.password; - const authWithKey = serverKey => { - const _password = encrypt(password, scramble, serverKey); + const authWithKey = async serverKey => { + const _password = await encrypt(password, scramble, serverKey); state = STATE_FINAL; return _password; }; - return data => { + return async data => { switch (state) { case STATE_INITIAL: scramble = data.slice(0, 20); state = STATE_TOKEN_SENT; - return calculateToken(password, scramble); + return await calculateToken(password, scramble); case STATE_TOKEN_SENT: if (FAST_AUTH_SUCCESS_PACKET.equals(data)) { @@ -76,7 +70,7 @@ module.exports = (pluginOptions = {}) => ({ connection }) => { // if client provides key we can save one extra roundrip on first connection if (pluginOptions.serverPublicKey) { - return authWithKey(pluginOptions.serverPublicKey); + return await authWithKey(pluginOptions.serverPublicKey); } state = STATE_WAIT_SERVER_KEY; @@ -89,7 +83,7 @@ module.exports = (pluginOptions = {}) => ({ connection }) => { if (pluginOptions.onServerPublicKey) { pluginOptions.onServerPublicKey(data); } - return authWithKey(data); + return await authWithKey(data); case STATE_FINAL: throw new Error( `Unexpected data in AuthMoreData packet received by ${PLUGIN_NAME} plugin in STATE_FINAL state.` diff --git a/lib/auth_plugins/sha256_password.js b/lib/auth_plugins/sha256_password.js index 9c104a0715..4980d3b2ba 100644 --- a/lib/auth_plugins/sha256_password.js +++ b/lib/auth_plugins/sha256_password.js @@ -1,7 +1,7 @@ 'use strict'; const PLUGIN_NAME = 'sha256_password'; -const crypto = require('crypto'); +const crypto = require('../utils/crypto'); const { xorRotating } = require('../auth_41'); const REQUEST_SERVER_KEY_PACKET = Buffer.from([1]); @@ -10,12 +10,12 @@ const STATE_INITIAL = 0; const STATE_WAIT_SERVER_KEY = 1; const STATE_FINAL = -1; -function encrypt(password, scramble, key) { +async function encrypt(password, scramble, key) { const stage1 = xorRotating( Buffer.from(`${password}\0`, 'utf8'), scramble ); - return crypto.publicEncrypt(key, stage1); + return await crypto.publicEncrypt(key, stage1); } module.exports = (pluginOptions = {}) => ({ connection }) => { @@ -24,19 +24,19 @@ module.exports = (pluginOptions = {}) => ({ connection }) => { const password = connection.config.password; - const authWithKey = serverKey => { - const _password = encrypt(password, scramble, serverKey); + const authWithKey = async serverKey => { + const _password = await encrypt(password, scramble, serverKey); state = STATE_FINAL; return _password; }; - return data => { + return async data => { switch (state) { case STATE_INITIAL: scramble = data.slice(0, 20); // if client provides key we can save one extra roundrip on first connection if (pluginOptions.serverPublicKey) { - return authWithKey(pluginOptions.serverPublicKey); + return await authWithKey(pluginOptions.serverPublicKey); } state = STATE_WAIT_SERVER_KEY; @@ -46,7 +46,7 @@ module.exports = (pluginOptions = {}) => ({ connection }) => { if (pluginOptions.onServerPublicKey) { pluginOptions.onServerPublicKey(data); } - return authWithKey(data); + return await authWithKey(data); case STATE_FINAL: throw new Error( `Unexpected data in AuthMoreData packet received by ${PLUGIN_NAME} plugin in STATE_FINAL state.` diff --git a/lib/utils/nodecrypto.js b/lib/utils/nodecrypto.js index fd80ea4bed..31be2cfddd 100644 --- a/lib/utils/nodecrypto.js +++ b/lib/utils/nodecrypto.js @@ -16,6 +16,18 @@ async function sha1(msg,msg1,msg2) { return hash.digest(); } +async function sha256(msg) { + const hash = nodeCrypto.createHash('sha256'); + hash.update(msg); + return hash.digest(); +} + +async function publicEncrypt(key,buffer) { + return nodeCrypto.publicEncrypt(key,buffer); +} + module.exports = { - sha1 + sha1, + sha256, + publicEncrypt } diff --git a/lib/utils/webcrypto.js b/lib/utils/webcrypto.js index 4dc46dabc8..3d36aa9fd8 100644 --- a/lib/utils/webcrypto.js +++ b/lib/utils/webcrypto.js @@ -34,6 +34,48 @@ async function sha1(msg,msg1,msg2) { return Buffer.from(arrayBuffer) } +async function sha256(msg) { + const arrayBuffer = await subtleCrypto.digest('SHA-256', typeof msg === 'string' ? textEncoder.encode(msg) : msg) + return Buffer.from(arrayBuffer) +} + +function decodeBase64(b64) { + const binString = atob(b64); + const size = binString.length; + const bytes = new Uint8Array(size); + for (let i = 0; i < size; i++) { + bytes[i] = binString.charCodeAt(i); + } + return bytes; +} + +// need more test +async function publicEncrypt(key, data) { + const pemHeader = "-----BEGIN PUBLIC KEY-----\n"; + const pemFooter = "\n-----END PUBLIC KEY-----"; + key = key.trim(); + key = key.substring(pemHeader.length, key.length - pemFooter.length); + + const importedKey = await subtleCrypto.importKey( + "spki", + decodeBase64(key), + { name: "RSA-OAEP", hash: "SHA-256" }, + false, + ["encrypt"], + ); + + const encryptedData = await subtleCrypto.encrypt( + { + name: "RSA-OAEP", + }, + importedKey, + data, + ); + return Buffer.from(encryptedData) +} + module.exports = { - sha1 + sha1, + sha256, + publicEncrypt } From 0b9245bbb9de43d13cbe11240cbcee15e7ba27ff Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Mon, 25 Dec 2023 16:56:01 +0800 Subject: [PATCH 10/17] fix sha256 access deny --- lib/utils/webcrypto.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/utils/webcrypto.js b/lib/utils/webcrypto.js index 3d36aa9fd8..3d0d68d018 100644 --- a/lib/utils/webcrypto.js +++ b/lib/utils/webcrypto.js @@ -51,6 +51,7 @@ function decodeBase64(b64) { // need more test async function publicEncrypt(key, data) { + key = key.toString() const pemHeader = "-----BEGIN PUBLIC KEY-----\n"; const pemFooter = "\n-----END PUBLIC KEY-----"; key = key.trim(); @@ -59,14 +60,14 @@ async function publicEncrypt(key, data) { const importedKey = await subtleCrypto.importKey( "spki", decodeBase64(key), - { name: "RSA-OAEP", hash: "SHA-256" }, + { name: "RSA-OAEP", hash: "SHA-1" }, false, ["encrypt"], ); const encryptedData = await subtleCrypto.encrypt( { - name: "RSA-OAEP", + name: "RSA-OAEP" }, importedKey, data, From d44c6d7ee4140d4ca7aed136678c4ca4aba5c7bf Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Mon, 25 Dec 2023 17:36:39 +0800 Subject: [PATCH 11/17] fix lint --- lib/parsers/static_text_parser.js | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/parsers/static_text_parser.js b/lib/parsers/static_text_parser.js index 73ffcd26a2..cdaa1c9beb 100644 --- a/lib/parsers/static_text_parser.js +++ b/lib/parsers/static_text_parser.js @@ -32,8 +32,6 @@ function readField({ packet, type, charset, encoding, config, options }) { case Types.DOUBLE: return packet.parseLengthCodedFloat(); case Types.NULL: - // case Types.BIT: - // return 'packet.readBuffer(2)[1]'; case Types.DECIMAL: case Types.NEWDECIMAL: if (config.decimalNumbers) { @@ -100,16 +98,14 @@ function getStaticTextParser(fields, options, config) { for (let i = 0; i < fields.length; i++) { const field = fields[i]; const typeCast = options.typeCast ? options.typeCast : config.typeCast; - const next = () => { - return readField({ - packet, - type: field.columnType, - encoding: field.encoding, - charset: field.characterSet, - config, - options - }) - } + const next = () => readField({ + packet, + type: field.columnType, + encoding: field.encoding, + charset: field.characterSet, + config, + options + }) let value; if (typeof typeCast === 'function') { value = typeCast(createTypecastField(field, packet), next); From cc3458f23b222e49856d5acc14faa6f99f0671ee Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Mon, 25 Dec 2023 19:05:58 +0800 Subject: [PATCH 12/17] add documentation --- documentation/en/Cloudflare-Workers.md | 270 +++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 documentation/en/Cloudflare-Workers.md diff --git a/documentation/en/Cloudflare-Workers.md b/documentation/en/Cloudflare-Workers.md new file mode 100644 index 0000000000..b74cb3da2c --- /dev/null +++ b/documentation/en/Cloudflare-Workers.md @@ -0,0 +1,270 @@ +# Run on Cloudflare Workers + +This document is a step-by-step tutorial on how to use `node-mysql2` on Cloudflare Workers. + +## Before you begin + +Before you try the steps in this article, you need to prepare the following things: + +- A [Cloudflare Workers](https://dash.cloudflare.com/login) account. +- npm is installed. + +## Step 1: Set up project + +[Wrangler](https://developers.cloudflare.com/workers/wrangler/) is the official Cloudflare Worker CLI. You can use it to generate, build, preview, and publish your Workers. + +1. Install Wrangler: + + ``` + npm install wrangler + ``` + +2. To authenticate Wrangler, run wrangler login: + + ``` + wrangler login + ``` + +3. Use Wrangler to create a project: + + ``` + wrangler init mysql2-cloudflare -y + ``` + +## Step 2: Install node-mysql2 + +1. Enter your project directory: + + ``` + cd mysql2-cloudflare + ``` + +2. Install the node-mysql2 with npm: + + ``` + npm install mysql2 + ``` + + This adds the node-mysql2 dependency in `package.json`. + +## Step 3: Use node-mysql2 on Cloudflare Workers + +Develop your code in the `src/index.ts`. Here are some examples: + +1. local mysql example + + ```ts + import { createConnection } from 'mysql2'; + + export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + let result + const connection = createConnection( + { + host: '127.0.0.1', + port: 3306, + user: 'user', + password: 'password', + useStaticParser:true + }); + connection.query( + 'show databases', + function(err, rows, fields) { + if (err) { + throw err + } + result = rows + } + ); + await sleep(2000); + return new Response(JSON.stringify(result)) + }, + }; + + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + ``` + +2. TiDB Serverless example with TLS: + + ```js + import { createConnection } from 'mysql2'; + + export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + let result + const connection = createConnection( + { + host: 'gateway01.ap-southeast-1.prod.aws.tidbcloud.com', + port: 4000, + user: 'your_user', + password: 'your_password', + database: 'test', + ssl: { + minVersion: 'TLSv1.2', + rejectUnauthorized: true + }, + useStaticParser:true + }); + connection.query( + 'show databases', + function(err, rows, fields) { + if (err) { + throw err + } + result = rows + } + ); + await sleep(2000); + return new Response(JSON.stringify(result)) + }, + }; + + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + ``` + +3. PlanetScale example with TLS: + + ```js + import { createConnection } from 'mysql2'; + + export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + let result + const connection = createConnection( + { + host: 'aws.connect.psdb.cloud', + port: 3306, + user: 'your_user', + password: 'your_password', + database: 'test', + ssl: { + minVersion: 'TLSv1.2', + rejectUnauthorized: true + }, + useStaticParser:true + }); + connection.query( + 'show databases', + function(err, rows, fields) { + if (err) { + throw err + } + result = rows + } + ); + await sleep(2000); + return new Response(JSON.stringify(result)) + }, + }; + + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + ``` + +## Step 4: Test locally + +1. Add `node_compat = true` in the `wrangler.toml` to make Cloudflare Workers compatible with node dependencies. + + ``` + vim wrangler.toml + ``` + +2. Upgrade wrangler + + ``` + npm install wrangler@3.22.1 --save-dev + ``` + +2. Run locally + + ``` + wrangler dev + ``` + +## Step 5: Deploy + +You can also deploy it to Cloudflare, run `wrangler deploy`. If you meet any issues, please refer to the [Cloudflare doc](https://developers.cloudflare.com/workers/get-started/guide/#4-deploy-your-project) + +## Develop Guide + +If you want to develop the corresponding feature. Here is a simaple example of testing locally. + +1. enter to node-mysql2 + + ``` + cd node-mysql2 + ``` + +2. vim wrangler.toml + + ``` + name = "mysql2-cloudflare" + main = "src/worker.js" + compatibility_date = "2023-08-21" + node_compat = true + ``` + +3. add worker.js + + ``` + mkdir src + cd src + vim workers.js + ``` + +4. write your test code inside worker.js + + ``` + const { createConnection } = require('../index'); + export default { + async fetch(request, env, ctx) { + let result + const connection = createConnection( + { + host: '127.0.0.1', + port: 3306, + user: 'test', + password: 'password', + useStaticParser:true + }); + connection.query( + 'show databases', + function(err, rows, fields) { + if (err) { + throw err + } + result = rows + } + ); + await sleep(2000); + return new Response(JSON.stringify(result)) + } + }; + + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + ``` + +5. Test locally + + ``` + npx wrangler dev + ``` From 96f630f69d7749ed357815e21582667ef7a45a50 Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Tue, 26 Dec 2023 11:35:43 +0800 Subject: [PATCH 13/17] fix lint --- documentation/en/Cloudflare-Workers.md | 36 ++++++++++++-------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/documentation/en/Cloudflare-Workers.md b/documentation/en/Cloudflare-Workers.md index b74cb3da2c..3b564898fd 100644 --- a/documentation/en/Cloudflare-Workers.md +++ b/documentation/en/Cloudflare-Workers.md @@ -54,8 +54,12 @@ Develop your code in the `src/index.ts`. Here are some examples: 1. local mysql example ```ts + import { createConnection } from 'mysql2'; + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } export default { async fetch( request: Request, @@ -84,17 +88,16 @@ Develop your code in the `src/index.ts`. Here are some examples: return new Response(JSON.stringify(result)) }, }; - - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } ``` 2. TiDB Serverless example with TLS: - ```js + ```ts import { createConnection } from 'mysql2'; + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } export default { async fetch( request: Request, @@ -128,17 +131,17 @@ Develop your code in the `src/index.ts`. Here are some examples: return new Response(JSON.stringify(result)) }, }; - - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } ``` 3. PlanetScale example with TLS: - ```js + ```ts import { createConnection } from 'mysql2'; + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + export default { async fetch( request: Request, @@ -172,10 +175,6 @@ Develop your code in the `src/index.ts`. Here are some examples: return new Response(JSON.stringify(result)) }, }; - - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } ``` ## Step 4: Test locally @@ -231,8 +230,11 @@ If you want to develop the corresponding feature. Here is a simaple example of t 4. write your test code inside worker.js - ``` + ```js const { createConnection } = require('../index'); + async function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } export default { async fetch(request, env, ctx) { let result @@ -257,10 +259,6 @@ If you want to develop the corresponding feature. Here is a simaple example of t return new Response(JSON.stringify(result)) } }; - - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } ``` 5. Test locally From fa767655493824f3d792357a89feb06aef87179a Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Wed, 6 Mar 2024 12:04:21 +0800 Subject: [PATCH 14/17] move doc --- .../cloudflare/Cloudflare-Workers.mdx} | 2 +- website/docs/examples/cloudflare/_category_.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) rename website/docs/{documentation/Cloudflare-Workers.md => examples/cloudflare/Cloudflare-Workers.mdx} (99%) create mode 100644 website/docs/examples/cloudflare/_category_.json diff --git a/website/docs/documentation/Cloudflare-Workers.md b/website/docs/examples/cloudflare/Cloudflare-Workers.mdx similarity index 99% rename from website/docs/documentation/Cloudflare-Workers.md rename to website/docs/examples/cloudflare/Cloudflare-Workers.mdx index 3b564898fd..22f14af75c 100644 --- a/website/docs/documentation/Cloudflare-Workers.md +++ b/website/docs/examples/cloudflare/Cloudflare-Workers.mdx @@ -1,4 +1,4 @@ -# Run on Cloudflare Workers +# Cloudflare Workers This document is a step-by-step tutorial on how to use `node-mysql2` on Cloudflare Workers. diff --git a/website/docs/examples/cloudflare/_category_.json b/website/docs/examples/cloudflare/_category_.json new file mode 100644 index 0000000000..95531d3b07 --- /dev/null +++ b/website/docs/examples/cloudflare/_category_.json @@ -0,0 +1,3 @@ +{ + "label": "Cloudflare" +} From 6da01bf53d520e552c7ae61a47f8d3d9e7fdc1ad Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Wed, 6 Mar 2024 12:17:01 +0800 Subject: [PATCH 15/17] fix lint --- .../cloudflare/Cloudflare-Workers.mdx | 353 +++++++++--------- 1 file changed, 168 insertions(+), 185 deletions(-) diff --git a/website/docs/examples/cloudflare/Cloudflare-Workers.mdx b/website/docs/examples/cloudflare/Cloudflare-Workers.mdx index 22f14af75c..7423783b03 100644 --- a/website/docs/examples/cloudflare/Cloudflare-Workers.mdx +++ b/website/docs/examples/cloudflare/Cloudflare-Workers.mdx @@ -21,29 +21,29 @@ Before you try the steps in this article, you need to prepare the following thin 2. To authenticate Wrangler, run wrangler login: - ``` - wrangler login - ``` + ``` + wrangler login + ``` 3. Use Wrangler to create a project: - ``` - wrangler init mysql2-cloudflare -y - ``` + ``` + wrangler init mysql2-cloudflare -y + ``` ## Step 2: Install node-mysql2 1. Enter your project directory: - ``` - cd mysql2-cloudflare - ``` + ``` + cd mysql2-cloudflare + ``` 2. Install the node-mysql2 with npm: - ``` - npm install mysql2 - ``` + ``` + npm install mysql2 + ``` This adds the node-mysql2 dependency in `package.json`. @@ -53,149 +53,136 @@ Develop your code in the `src/index.ts`. Here are some examples: 1. local mysql example - ```ts - - import { createConnection } from 'mysql2'; - - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - export default { - async fetch( - request: Request, - env: Env, - ctx: ExecutionContext - ): Promise { - let result - const connection = createConnection( - { - host: '127.0.0.1', - port: 3306, - user: 'user', - password: 'password', - useStaticParser:true - }); - connection.query( - 'show databases', - function(err, rows, fields) { - if (err) { - throw err - } - result = rows - } - ); - await sleep(2000); - return new Response(JSON.stringify(result)) - }, - }; - ``` +```ts +import { createConnection } from 'mysql2'; + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + let result; + const connection = createConnection({ + host: '127.0.0.1', + port: 3306, + user: 'user', + password: 'password', + useStaticParser: true, + }); + connection.query('show databases', function (err, rows, fields) { + if (err) { + throw err; + } + result = rows; + }); + await sleep(2000); + return new Response(JSON.stringify(result)); + }, +}; +``` 2. TiDB Serverless example with TLS: - ```ts - import { createConnection } from 'mysql2'; - - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - export default { - async fetch( - request: Request, - env: Env, - ctx: ExecutionContext - ): Promise { - let result - const connection = createConnection( - { - host: 'gateway01.ap-southeast-1.prod.aws.tidbcloud.com', - port: 4000, - user: 'your_user', - password: 'your_password', - database: 'test', - ssl: { - minVersion: 'TLSv1.2', - rejectUnauthorized: true - }, - useStaticParser:true - }); - connection.query( - 'show databases', - function(err, rows, fields) { - if (err) { - throw err - } - result = rows - } - ); - await sleep(2000); - return new Response(JSON.stringify(result)) - }, - }; - ``` +```ts +import { createConnection } from 'mysql2'; + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + let result; + const connection = createConnection({ + host: 'gateway01.ap-southeast-1.prod.aws.tidbcloud.com', + port: 4000, + user: 'your_user', + password: 'your_password', + database: 'test', + ssl: { + minVersion: 'TLSv1.2', + rejectUnauthorized: true, + }, + useStaticParser: true, + }); + connection.query('show databases', function (err, rows, fields) { + if (err) { + throw err; + } + result = rows; + }); + await sleep(2000); + return new Response(JSON.stringify(result)); + }, +}; +``` 3. PlanetScale example with TLS: - ```ts - import { createConnection } from 'mysql2'; - - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - export default { - async fetch( - request: Request, - env: Env, - ctx: ExecutionContext - ): Promise { - let result - const connection = createConnection( - { - host: 'aws.connect.psdb.cloud', - port: 3306, - user: 'your_user', - password: 'your_password', - database: 'test', - ssl: { - minVersion: 'TLSv1.2', - rejectUnauthorized: true - }, - useStaticParser:true - }); - connection.query( - 'show databases', - function(err, rows, fields) { - if (err) { - throw err - } - result = rows - } - ); - await sleep(2000); - return new Response(JSON.stringify(result)) - }, - }; - ``` +```ts +import { createConnection } from 'mysql2'; + +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export default { + async fetch( + request: Request, + env: Env, + ctx: ExecutionContext + ): Promise { + let result; + const connection = createConnection({ + host: 'aws.connect.psdb.cloud', + port: 3306, + user: 'your_user', + password: 'your_password', + database: 'test', + ssl: { + minVersion: 'TLSv1.2', + rejectUnauthorized: true, + }, + useStaticParser: true, + }); + connection.query('show databases', function (err, rows, fields) { + if (err) { + throw err; + } + result = rows; + }); + await sleep(2000); + return new Response(JSON.stringify(result)); + }, +}; +``` ## Step 4: Test locally 1. Add `node_compat = true` in the `wrangler.toml` to make Cloudflare Workers compatible with node dependencies. - ``` - vim wrangler.toml - ``` +``` +vim wrangler.toml +``` 2. Upgrade wrangler - ``` - npm install wrangler@3.22.1 --save-dev - ``` +``` +npm install wrangler@3.22.1 --save-dev +``` 2. Run locally - ``` - wrangler dev - ``` +``` +wrangler dev +``` ## Step 5: Deploy @@ -207,62 +194,58 @@ If you want to develop the corresponding feature. Here is a simaple example of t 1. enter to node-mysql2 - ``` - cd node-mysql2 - ``` +``` +cd node-mysql2 +``` 2. vim wrangler.toml - ``` - name = "mysql2-cloudflare" - main = "src/worker.js" - compatibility_date = "2023-08-21" - node_compat = true - ``` +``` +name = "mysql2-cloudflare" +main = "src/worker.js" +compatibility_date = "2023-08-21" +node_compat = true +``` 3. add worker.js - ``` - mkdir src - cd src - vim workers.js - ``` +``` +mkdir src +cd src +vim workers.js +``` 4. write your test code inside worker.js - ```js - const { createConnection } = require('../index'); - async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - export default { - async fetch(request, env, ctx) { - let result - const connection = createConnection( - { - host: '127.0.0.1', - port: 3306, - user: 'test', - password: 'password', - useStaticParser:true - }); - connection.query( - 'show databases', - function(err, rows, fields) { - if (err) { - throw err - } - result = rows - } - ); - await sleep(2000); - return new Response(JSON.stringify(result)) - } - }; - ``` +```js +const { createConnection } = require('../index'); +async function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} +export default { + async fetch(request, env, ctx) { + let result; + const connection = createConnection({ + host: '127.0.0.1', + port: 3306, + user: 'test', + password: 'password', + useStaticParser: true, + }); + connection.query('show databases', function (err, rows, fields) { + if (err) { + throw err; + } + result = rows; + }); + await sleep(2000); + return new Response(JSON.stringify(result)); + }, +}; +``` 5. Test locally - ``` - npx wrangler dev - ``` +``` +npx wrangler dev +``` From da73b7ae8a05946f271842fdd840f186a5063fa9 Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Mon, 6 May 2024 10:20:39 +0800 Subject: [PATCH 16/17] rename --- website/docs/examples/cloudflare/Cloudflare-Workers.mdx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/examples/cloudflare/Cloudflare-Workers.mdx b/website/docs/examples/cloudflare/Cloudflare-Workers.mdx index 7423783b03..604d56c2a3 100644 --- a/website/docs/examples/cloudflare/Cloudflare-Workers.mdx +++ b/website/docs/examples/cloudflare/Cloudflare-Workers.mdx @@ -71,7 +71,7 @@ export default { port: 3306, user: 'user', password: 'password', - useStaticParser: true, + disableEval: true, }); connection.query('show databases', function (err, rows, fields) { if (err) { @@ -110,7 +110,7 @@ export default { minVersion: 'TLSv1.2', rejectUnauthorized: true, }, - useStaticParser: true, + disableEval: true, }); connection.query('show databases', function (err, rows, fields) { if (err) { @@ -150,7 +150,7 @@ export default { minVersion: 'TLSv1.2', rejectUnauthorized: true, }, - useStaticParser: true, + disableEval: true, }); connection.query('show databases', function (err, rows, fields) { if (err) { @@ -230,7 +230,7 @@ export default { port: 3306, user: 'test', password: 'password', - useStaticParser: true, + disableEval: true, }); connection.query('show databases', function (err, rows, fields) { if (err) { From 9b78fe1c09da60411a912cbec9b166c384b8f22f Mon Sep 17 00:00:00 2001 From: shiyuhang <1136742008@qq.com> Date: Tue, 7 May 2024 16:39:21 +0800 Subject: [PATCH 17/17] rename to disableEval --- lib/commands/query.js | 2 +- lib/connection_config.js | 4 ++-- typings/mysql/lib/Connection.d.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/commands/query.js b/lib/commands/query.js index 971c0ecebc..c2df2d6b5b 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -212,7 +212,7 @@ class Query extends Command { if (this._receivedFieldsCount === this._fieldCount) { const fields = this._fields[this._resultIndex]; this.emit('fields', fields); - if (this.options.useStaticParser) { + if (this.options.disableEval) { this._rowParser = getStaticTextParser(fields, this.options, connection.config); } else { this._rowParser = new (getTextParser(fields, this.options, connection.config))(fields); diff --git a/lib/connection_config.js b/lib/connection_config.js index 269a319eb4..21744c5964 100644 --- a/lib/connection_config.js +++ b/lib/connection_config.js @@ -66,7 +66,7 @@ const validOptions = { Promise: 1, queueLimit: 1, waitForConnections: 1, - useStaticParser: 1 + disableEval: 1 }; class ConnectionConfig { @@ -181,7 +181,7 @@ class ConnectionConfig { }; this.connectAttributes = { ...defaultConnectAttributes, ...(options.connectAttributes || {})}; this.maxPreparedStatements = options.maxPreparedStatements || 16000; - this.useStaticParser = options.useStaticParser || false; + this.disableEval = options.disableEval || false; } static mergeFlags(default_flags, user_flags) { diff --git a/typings/mysql/lib/Connection.d.ts b/typings/mysql/lib/Connection.d.ts index b509851459..cfe626a360 100644 --- a/typings/mysql/lib/Connection.d.ts +++ b/typings/mysql/lib/Connection.d.ts @@ -327,7 +327,7 @@ export interface ConnectionOptions { [key: string]: AuthPlugin; }; - useStaticParser?: boolean; + disableEval?: boolean; } declare class Connection extends QueryableBase(ExecutableBase(EventEmitter)) {