From c144ddacb0fd7f5b65e02f227cd149a52dedfb36 Mon Sep 17 00:00:00 2001 From: Shuode Li Date: Thu, 20 Jul 2017 05:46:06 +0000 Subject: [PATCH] Support auth switch in change user flow closes #1776 --- Changes.md | 1 + lib/protocol/Auth.js | 12 +++++ lib/protocol/sequences/ChangeUser.js | 26 +++++++++++ lib/protocol/sequences/Handshake.js | 17 ++++--- .../test-change-user-auth-switch-unknown.js | 40 +++++++++++++++++ .../test-change-user-auth-switch.js | 45 +++++++++++++++++++ 6 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 test/unit/connection/test-change-user-auth-switch-unknown.js create mode 100644 test/unit/connection/test-change-user-auth-switch.js diff --git a/Changes.md b/Changes.md index 2b9673659..36dbccc97 100644 --- a/Changes.md +++ b/Changes.md @@ -19,6 +19,7 @@ you spot any mistakes. * Add Amazon RDS GovCloud SSL certificates #1876 * Add new error codes up to MySQL 5.7.21 * Include connection ID in debug output +* Support auth switch in change user flow #1776 * Support Node.js 9.x * Support Node.js 10.x #2003 #2024 #2026 #2034 * Update Amazon RDS SSL certificates diff --git a/lib/protocol/Auth.js b/lib/protocol/Auth.js index bc649cdac..a1033d1b9 100644 --- a/lib/protocol/Auth.js +++ b/lib/protocol/Auth.js @@ -2,6 +2,18 @@ var Buffer = require('safe-buffer').Buffer; var Crypto = require('crypto'); var Auth = exports; +function auth(name, data, options) { + options = options || {}; + + switch (name) { + case 'mysql_native_password': + return Auth.token(options.password, data.slice(0, 20)); + default: + return undefined; + } +} +Auth.auth = auth; + function sha1(msg) { var hash = Crypto.createHash('sha1'); hash.update(msg, 'binary'); diff --git a/lib/protocol/sequences/ChangeUser.js b/lib/protocol/sequences/ChangeUser.js index 26be6dbbd..e1cc1fbc3 100644 --- a/lib/protocol/sequences/ChangeUser.js +++ b/lib/protocol/sequences/ChangeUser.js @@ -15,6 +15,14 @@ function ChangeUser(options, callback) { this._currentConfig = options.currentConfig; } +ChangeUser.prototype.determinePacket = function determinePacket(firstByte) { + switch (firstByte) { + case 0xfe: return Packets.AuthSwitchRequestPacket; + case 0xff: return Packets.ErrorPacket; + default: return undefined; + } +}; + ChangeUser.prototype.start = function(handshakeInitializationPacket) { var scrambleBuff = handshakeInitializationPacket.scrambleBuff(); scrambleBuff = Auth.token(this._password, scrambleBuff); @@ -34,6 +42,24 @@ ChangeUser.prototype.start = function(handshakeInitializationPacket) { this.emit('packet', packet); }; +ChangeUser.prototype['AuthSwitchRequestPacket'] = function (packet) { + var name = packet.authMethodName; + var data = Auth.auth(name, packet.authMethodData, { + password: this._password + }); + + if (data !== undefined) { + this.emit('packet', new Packets.AuthSwitchResponsePacket({ + data: data + })); + } else { + var err = new Error('MySQL is requesting the ' + name + ' authentication method, which is not supported.'); + err.code = 'UNSUPPORTED_AUTH_METHOD'; + err.fatal = true; + this.end(err); + } +}; + ChangeUser.prototype['ErrorPacket'] = function(packet) { var err = this._packetToError(packet); err.fatal = true; diff --git a/lib/protocol/sequences/Handshake.js b/lib/protocol/sequences/Handshake.js index 2881be844..8fad0fcf3 100644 --- a/lib/protocol/sequences/Handshake.js +++ b/lib/protocol/sequences/Handshake.js @@ -34,20 +34,19 @@ Handshake.prototype.determinePacket = function determinePacket(firstByte, parser }; Handshake.prototype['AuthSwitchRequestPacket'] = function (packet) { - if (packet.authMethodName === 'mysql_native_password') { - var challenge = packet.authMethodData.slice(0, 20); + var name = packet.authMethodName; + var data = Auth.auth(name, packet.authMethodData, { + password: this._config.password + }); + if (data !== undefined) { this.emit('packet', new Packets.AuthSwitchResponsePacket({ - data: Auth.token(this._config.password, challenge) + data: data })); } else { - var err = new Error( - 'MySQL is requesting the ' + packet.authMethodName + ' authentication method, which is not supported.' - ); - - err.code = 'UNSUPPORTED_AUTH_METHOD'; + var err = new Error('MySQL is requesting the ' + name + ' authentication method, which is not supported.'); + err.code = 'UNSUPPORTED_AUTH_METHOD'; err.fatal = true; - this.end(err); } }; diff --git a/test/unit/connection/test-change-user-auth-switch-unknown.js b/test/unit/connection/test-change-user-auth-switch-unknown.js new file mode 100644 index 000000000..dcaa327f1 --- /dev/null +++ b/test/unit/connection/test-change-user-auth-switch-unknown.js @@ -0,0 +1,40 @@ +var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer; +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + user : 'user_1', + password : 'pass_1' +}); + +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function(err) { + assert.ifError(err); + + connection.query('SELECT CURRENT_USER()', function (err, result) { + assert.ifError(err); + assert.strictEqual(result[0]['CURRENT_USER()'], 'user_1@localhost'); + + connection.changeUser({user: 'user_2', password: 'pass_2'}, function (err) { + assert.ok(err); + assert.equal(err.code, 'UNSUPPORTED_AUTH_METHOD'); + assert.equal(err.fatal, true); + assert.ok(/foo_plugin_password/.test(err.message)); + + connection.destroy(); + server.destroy(); + }); + }); +}); + +server.on('connection', function (incomingConnection) { + incomingConnection.on('changeUser', function () { + this.authSwitchRequest({ + authMethodName : 'foo_plugin_password', + authMethodData : Buffer.alloc(0) + }); + }); + + incomingConnection.handshake(); +}); diff --git a/test/unit/connection/test-change-user-auth-switch.js b/test/unit/connection/test-change-user-auth-switch.js new file mode 100644 index 000000000..f3f080549 --- /dev/null +++ b/test/unit/connection/test-change-user-auth-switch.js @@ -0,0 +1,45 @@ +var assert = require('assert'); +var Crypto = require('crypto'); +var common = require('../../common'); +var connection = common.createConnection({ + port : common.fakeServerPort, + user : 'user_1', + password : 'pass_1' +}); + +var random = Crypto.pseudoRandomBytes || Crypto.randomBytes; // Depends on node.js version +var server = common.createFakeServer(); + +server.listen(common.fakeServerPort, function(err) { + assert.ifError(err); + + connection.query('SELECT CURRENT_USER()', function (err, result) { + assert.ifError(err); + assert.strictEqual(result[0]['CURRENT_USER()'], 'user_1@localhost'); + + connection.changeUser({user: 'user_2', password: 'pass_2'}, function (err) { + assert.ifError(err); + connection.destroy(); + server.destroy(); + }); + }); +}); + +server.on('connection', function (incomingConnection) { + random(20, function (err, scramble) { + assert.ifError(err); + + incomingConnection.on('authSwitchResponse', function (packet) { + this._sendAuthResponse(packet.data, common.Auth.token('pass_2', scramble)); + }); + + incomingConnection.on('changeUser', function () { + this.authSwitchRequest({ + authMethodName : 'mysql_native_password', + authMethodData : scramble + }); + }); + + incomingConnection.handshake(); + }); +});