Skip to content

Commit

Permalink
Merge pull request #331 from sidorares/auth-switch
Browse files Browse the repository at this point in the history
* allow to pass connection handler as createServer parameter

* start using portfinder in tests

* connection.end() for server said of connections pair

* add connectAttributes config parameter

* initial implementation of AuthSwitchRequest/Response/MoreData in handshake

* use PLUGIN_AUTH flag by default only when authSwitchHandler connect parameter is set

* change_user command flow when plugin auth is enabled

* cleanup debug output

* debug

* christmas tree

* fix typos

* bisect

* use portfinder to allocate test server ports

* fix failing tests

* don't crash in debug log if there is unexpected packet

* don't crash in debug log if there is unexpected packet

* don't crash in debug log if there is unexpected packet

* don't crash in debug log if there is unexpected packet

* remove debug

* node 0.10: buffer.fill() does not return ref to buffer

* node 0.10: buffer.fill() does not return ref to buffer

* debug default flags

* debugging change-user test

* debug change-user

* debug change-user

* debug failing test only

* update example

* update example

* typo

* check server version

* handle end of handshake if there is no switch-auth request

* re-enable matrix

* fix lint error

* debug failing test

* set packet length during real serializing

* set mysql server tz offset to 0 in time-related tests

* add auth-switch api to readme
  • Loading branch information
sidorares committed Jun 29, 2016
2 parents 949e708 + cccff5e commit 4652f98
Show file tree
Hide file tree
Showing 32 changed files with 719 additions and 177 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -7,7 +7,7 @@ node_js:
- '0.10'
- '0.12'
- '4.4'
- '5.11'
- '5.12'
- '6.2'


Expand Down
4 changes: 4 additions & 0 deletions Changelog.md
@@ -1,3 +1,7 @@
1.0.0-rc-6 ( 29/06/2016 )
- AuthSwitch support and partial support for
plugin-based authentication #331

1.0.0-rc-5 ( 16/06/2016 )
- Fix incorrect releasing of dead pool connections #326, #325
- Allow pool options to be specified as URL params #327
Expand Down
34 changes: 34 additions & 0 deletions README.md
Expand Up @@ -80,6 +80,40 @@ co(function * () {
```
see examples in [/examples/promise-co-await](/examples/promise-co-await)

### Authentication switch request

During connection phase the server may ask client to switch to a different auth method.
If `authSwitchHandler` connection config option is set it must be a function that receive
switch request data and respond via callback. Note that if `mysql_native_password` method is
requested it will be handled internally according to [Authentication::Native41]( https://dev.mysql.com/doc/internals/en/secure-password-authentication.html#packet-Authentication::Native41) and
`authSwitchHandler` won't be invoked. `authSwitchHandler` MAY be called multiple times if
plugin algorithm requires multiple roundtrips of data exchange between client and server.
First invocation always has `({pluginName, pluginData})` signature, following calls - `({pluginData})`.
The client respond with opaque blob matching requested plugin via `callback(null, data: Buffer)`.

Example: (imaginary `ssh-key-auth` plugin) pseudo code

```js
var conn = mysql.createConnection({
user: 'test_user',
password: 'test',
database: 'test_database',
authSwitchHandler: function(data, cb) {
if (data.pluginName === 'ssh-key-auth') {
getPrivateKey((key) => {
var response = encrypt(key, data.pluginData);
// continue handshake by sending response data
// respond with error to propagate error to connect/changeUser handlers
cb(null, response);
})
}
}
});
```

Initial handshake always performed using `mysql_native_password` plugin. This will be possible to override in
the future versions.

### Named placeholders

You can use named placeholders for parameters by setting `namedPlaceholders` config value or query/execute time option. Named placeholders are converted to unnamed `?` on the client (mysql protocol does not support named parameters). If you reference parameter multiple times under the same name it is sent to server multiple times.
Expand Down
4 changes: 2 additions & 2 deletions examples/promise-co-await/await.js
@@ -1,7 +1,7 @@
var mysql = require('../../promise.js');

async function test() {
const c = await mysql.createConnection({ port: 3306, user: 'mycause_dev', namedPlaceholders: true, password: 'mycause' });
const c = await mysql.createConnection({ port: 3306, user: 'testuser', namedPlaceholders: true, password: 'testpassword' });
console.log('connected!');
const [rows, fields] = await c.query('show databases');
console.log(rows);
Expand All @@ -24,7 +24,7 @@ async function test() {
console.log(end - start);
await c.end();

const p = mysql.createPool({ port: 3306, user: 'mycause_dev', namedPlaceholders: true, password: 'mycause' });
const p = mysql.createPool({ port: 3306, user: 'testuser', namedPlaceholders: true, password: 'testpassword' });
console.log( await p.execute('select sleep(0.5)') );
console.log('after first pool sleep');
var start = +new Date()
Expand Down
9 changes: 6 additions & 3 deletions index.js
Expand Up @@ -21,9 +21,13 @@ exports.createPoolCluster = function (config) {
return new PoolCluster(config);
};

module.exports.createServer = function () {
module.exports.createServer = function (handler) {
var Server = require('./lib/server.js');
return new Server();
var s = new Server();
if (handler) {
s.on('connection', handler);
}
return s;
};

exports.escape = SqlString.escape;
Expand All @@ -43,4 +47,3 @@ exports.__defineGetter__('createPoolClusterPromise', function () {
});

module.exports.Types = require('./lib/constants/types.js');

43 changes: 21 additions & 22 deletions lib/commands/change_user.js
Expand Up @@ -3,44 +3,43 @@ var util = require('util');
var Command = require('./command.js');
var Packets = require('../packets/index.js');
var ClientConstants = require('../constants/client.js');
var ClientHandshake = require('./client_handshake.js');

function ChangeUser (options, callback)
{
this.onResult = callback;
this._user = options.user;
this._password = options.password;
this._database = options.database;
this._passwordSha1 = options.passwordSha1;
this._charsetNumber = options.charsetNumber;
this._currentConfig = options.currentConfig;
this.user = options.user;
this.password = options.password;
this.database = options.database;
this.passwordSha1 = options.passwordSha1;
this.charsetNumber = options.charsetNumber;
this.currentConfig = options.currentConfig;
Command.call(this);
}
util.inherits(ChangeUser, Command);

ChangeUser.prototype.handshakeResult = ClientHandshake.prototype.handshakeResult;
ChangeUser.prototype.calculateNativePasswordAuthToken = ClientHandshake.prototype.calculateNativePasswordAuthToken;

ChangeUser.prototype.start = function (packet, connection) {
var packet = new Packets.ChangeUser({
user : this._user,
database : this._database,
charsetNumber : this._charsetNumber,
password : this._password,
passwordSha1 : this._passwordSha1,
flags : connection.config.clientFlags,
user : this.user,
database : this.database,
charsetNumber : this.charsetNumber,
password : this.password,
passwordSha1 : this.passwordSha1,
authPluginData1 : connection._handshakePacket.authPluginData1,
authPluginData2 : connection._handshakePacket.authPluginData2
});
this._currentConfig.user = this._user;
this._currentConfig.password = this._password;
this._currentConfig.database = this._database;
this._currentConfig.charsetNumber = this._charsetNumber;
this.currentConfig.user = this.user;
this.currentConfig.password = this.password;
this.currentConfig.database = this.database;
this.currentConfig.charsetNumber = this.charsetNumber;
// reset prepared statements cache as all statements become invalid after changeUser
connection._statements = {};
connection.writePacket(packet.toPacket());
return ChangeUser.prototype.changeOk;
return ChangeUser.prototype.handshakeResult;
};

ChangeUser.prototype.changeOk = function (okPacket, connection) {
if (this.onResult) {
this.onResult(null);
}
return null;
};
module.exports = ChangeUser;
77 changes: 70 additions & 7 deletions lib/commands/client_handshake.js
Expand Up @@ -45,11 +45,26 @@ ClientHandshake.prototype.sendCredentials = function (connection) {
charsetNumber : connection.config.charsetNumber,
authPluginData1: this.handshake.authPluginData1,
authPluginData2: this.handshake.authPluginData2,
compress: connection.config.compress
compress: connection.config.compress,
connectAttributes: connection.config.connectAttributes
});
connection.writePacket(handshakeResponse.toPacket());
};

var auth41 = require('../auth_41.js');
ClientHandshake.prototype.calculateNativePasswordAuthToken = function (authPluginData) {
// TODO: dont split into authPluginData1 and authPluginData2, instead join when 1 & 2 received
var authPluginData1 = authPluginData.slice(0, 8);
var authPluginData2 = authPluginData.slice(8, 20);
var authToken;
if (this.passwordSha1) {
authToken = auth41.calculateTokenFromPasswordSha(this.passwordSha1, authPluginData1, authPluginData2);
} else {
authToken = auth41.calculateToken(this.password, authPluginData1, authPluginData2);
}
return authToken;
};

ClientHandshake.prototype.handshakeInit = function (helloPacket, connection) {
var command = this;

Expand Down Expand Up @@ -100,13 +115,61 @@ ClientHandshake.prototype.handshakeInit = function (helloPacket, connection) {
return ClientHandshake.prototype.handshakeResult;
};

ClientHandshake.prototype.handshakeResult = function (okPacket, connection) {
// error is already checked in base class. Done auth.
connection.authorized = true;
if (connection.config.compress) {
var enableCompression = require('../compressed_protocol.js').enableCompression;
enableCompression(connection);
ClientHandshake.prototype.handshakeResult = function (packet, connection) {
var marker = packet.peekByte();
if (marker === 0xfe || marker === 1) {
var asr, asrmd;
var authSwitchHandlerParams = {};
if (marker === 1) {
asrmd = Packets.AuthSwitchRequestMoreData.fromPacket(packet);
authSwitchHandlerParams.pluginData = asrmd.data;
} else {
asr = Packets.AuthSwitchRequest.fromPacket(packet);
authSwitchHandlerParams.pluginName = asr.pluginName;
authSwitchHandlerParams.pluginData = asr.pluginData;
}
if (authSwitchHandlerParams.pluginName == 'mysql_native_password') {
var authToken = this.calculateNativePasswordAuthToken(authSwitchHandlerParams.pluginData);
connection.writePacket(new Packets.AuthSwitchResponse(authToken).toPacket());
} else if (connection.config.authSwitchHandler) {
connection.config.authSwitchHandler(authSwitchHandlerParams, function (err, data) {
if (err) {
connection.emit('error', err);
return;
}
connection.writePacket(new Packets.AuthSwitchResponse(data).toPacket());
});
} else {
connection.emit('error', new Error('Server requires auth switch, but no auth switch handler provided'));
return null;
}
return ClientHandshake.prototype.handshakeResult;
}

if (marker !== 0) {
var err = new Error('Unexpected packet during handshake phase');
if (this.onResult) {
this.onResult(err);
} else {
connection.emit('error', err);
}
return null;
}

// this should be called from ClientHandshake command only
// and skipped when called from ChangeUser command
if (!connection.authorized) {
connection.authorized = true;
if (connection.config.compress) {
var enableCompression = require('../compressed_protocol.js').enableCompression;
enableCompression(connection);
}
}

if (this.onResult) {
this.onResult(null);
}
return null;
};

module.exports = ClientHandshake;
17 changes: 15 additions & 2 deletions lib/connection.js
Expand Up @@ -119,7 +119,7 @@ function Connection (opts)
handshakeCommand.on('end', function () {
connection._handshakePacket = handshakeCommand.handshake;
connection.threadId = handshakeCommand.handshake.connectionId;
connection.emit('connect', handshakeCommand.handshake)
connection.emit('connect', handshakeCommand.handshake);
});
this.addCommand(handshakeCommand);
}
Expand Down Expand Up @@ -249,7 +249,9 @@ Connection.prototype.handlePacket = function (packet) {
if (packet) {
console.log(' raw: ' + packet.buffer.slice(packet.offset, packet.offset + packet.length()).toString('hex'));
console.trace();
console.log(this._internalId + ' ' + this.connectionId + ' ==> ' + this._command._commandName + '#' + this._command.stateName() + '(' + [packet.sequenceId, packet.type(), packet.length()].join(',') + ')');
var commandName = this._command ? this._command._commandName : '(no command)';
var stateName = this._command ? this._command.stateName() : '(no command)';
console.log(this._internalId + ' ' + this.connectionId + ' ==> ' + commandName + '#' + stateName + '(' + [packet.sequenceId, packet.type(), packet.length()].join(',') + ')');
}
}
if (!this._command) {
Expand Down Expand Up @@ -649,6 +651,17 @@ Connection.prototype.serverHandshake = function serverHandshake (args) {
// TODO: domainify
Connection.prototype.end = function (callback) {
var connection = this;

if (this.config.isServer) {
connection._closing = true;
var quitCmd = new EventEmitter();
setImmediate(function () {
connection.stream.end();
quitCmd.emit('end');
});
return quitCmd;
}

// trigger error if more commands enqueued after end command
var quitCmd = this.addCommand(new Commands.Quit(callback));
connection.addCommand = function () {
Expand Down
14 changes: 13 additions & 1 deletion lib/connection_config.js
Expand Up @@ -32,7 +32,6 @@ function ConnectionConfig (options) {
this.trace = options.trace !== false;
this.stringifyObjects = options.stringifyObjects || false;
this.timezone = options.timezone || 'local';
this.flags = options.flags || '';
this.queryFormat = options.queryFormat;
this.pool = options.pool || undefined;
this.ssl = (typeof options.ssl === 'string')
Expand Down Expand Up @@ -64,8 +63,12 @@ function ConnectionConfig (options) {

this.compress = options.compress || false;

this.authSwitchHandler = options.authSwitchHandler;

this.clientFlags = ConnectionConfig.mergeFlags(ConnectionConfig.getDefaultFlags(options),
options.flags || '');

this.connectAttributes = options.connectAttributes;
}

ConnectionConfig.mergeFlags = function (default_flags, user_flags) {
Expand Down Expand Up @@ -107,6 +110,15 @@ ConnectionConfig.getDefaultFlags = function (options) {
defaultFlags.push('MULTI_STATEMENTS');
}

if (options && options.authSwitchHandler) {
defaultFlags.push('PLUGIN_AUTH');
defaultFlags.push('PLUGIN_AUTH_LENENC_CLIENT_DATA');
}

if (options && options.connectAttributes) {
defaultFlags.push('CONNECT_ATTRS');
}

return defaultFlags;
};

Expand Down
37 changes: 37 additions & 0 deletions lib/packets/auth_switch_request.js
@@ -0,0 +1,37 @@
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest

var Packet = require('../packets/packet');

function AuthSwitchRequest (opts)
{
this.pluginName = opts.pluginName;
this.pluginData = opts.pluginData;
}

AuthSwitchRequest.prototype.toPacket = function ()
{
var length = 6 + this.pluginName.length + this.pluginData.length;
var buffer = new Buffer(length);
var packet = new Packet(0, buffer, 0, length);
packet.offset = 4;
packet.writeInt8(0xfe);
packet.writeNullTerminatedString(this.pluginName);
packet.writeBuffer(this.pluginData);
return packet;
};

AuthSwitchRequest.fromPacket = function (packet)
{
var marker = packet.readInt8();
// assert marker == 0xfe?

var name = packet.readNullTerminatedString();
var data = packet.readBuffer();

return new AuthSwitchRequest({
pluginName: name,
pluginData: data
});
};

module.exports = AuthSwitchRequest;

0 comments on commit 4652f98

Please sign in to comment.