Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Cloudflare Workers #2289

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 12 additions & 26 deletions lib/auth_41.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -50,37 +36,37 @@ 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
) {
// 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) {
Expand Down
30 changes: 12 additions & 18 deletions lib/auth_plugins/caching_sha2_password.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -15,23 +15,17 @@ 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
Expand All @@ -48,18 +42,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)) {
Expand All @@ -79,7 +73,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;
Expand All @@ -92,7 +86,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.`
Expand Down
6 changes: 3 additions & 3 deletions lib/auth_plugins/mysql_native_password.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions lib/auth_plugins/sha256_password.js
Original file line number Diff line number Diff line change
@@ -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]);
Expand All @@ -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 }) => {
Expand All @@ -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;
Expand All @@ -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.`
Expand Down
6 changes: 5 additions & 1 deletion lib/commands/change_user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 10 additions & 19 deletions lib/commands/client_handshake.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down Expand Up @@ -46,7 +47,7 @@ class ClientHandshake extends Command {
connection.writePacket(sslRequest.toPacket());
}

sendCredentials(connection) {
async sendCredentials(connection) {
if (connection.config.debug) {
// eslint-disable-next-line
console.log(
Expand Down Expand Up @@ -80,22 +81,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
Expand Down Expand Up @@ -146,21 +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);
});
} else {
this.sendCredentials(connection);
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
Expand Down
7 changes: 3 additions & 4 deletions lib/commands/command.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const EventEmitter = require('events').EventEmitter;
const Timers = require('timers');

class Command extends EventEmitter {
constructor() {
Expand Down Expand Up @@ -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) {
Expand All @@ -45,10 +44,10 @@ class Command extends EventEmitter {
this.next = this.next(packet, connection);
if (this.next) {
return false;
}
}
this.emit('end');
return true;

}
}

Expand Down
5 changes: 2 additions & 3 deletions lib/commands/query.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const process = require('process');
const Timers = require('timers');

const Readable = require('stream').Readable;

Expand Down Expand Up @@ -70,7 +69,7 @@
}
// else clear timer
if (this.queryTimeout) {
Timers.clearTimeout(this.queryTimeout);
clearTimeout(this.queryTimeout);
this.queryTimeout = null;
}
if (this.onResult) {
Expand Down Expand Up @@ -301,13 +300,13 @@
_setTimeout() {
if (this.timeout) {
const timeoutHandler = this._handleTimeoutError.bind(this);
this.queryTimeout = Timers.setTimeout(timeoutHandler, this.timeout);

Check failure on line 303 in lib/commands/query.js

View workflow job for this annotation

GitHub Actions / lint-js

'Timers' is not defined
}
}

_handleTimeoutError() {
if (this.queryTimeout) {
Timers.clearTimeout(this.queryTimeout);
clearTimeout(this.queryTimeout);
this.queryTimeout = null;
}

Expand Down
Loading
Loading