Skip to content

Commit 5dcf1bf

Browse files
authored
Merge pull request #1352 from ruiquelhas/master
Add proper handshake fatal error handling
2 parents e76d196 + e052e7b commit 5dcf1bf

File tree

4 files changed

+198
-5
lines changed

4 files changed

+198
-5
lines changed

lib/commands/client_handshake.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
// This file was modified by Oracle on June 17, 2021.
2+
// Handshake errors are now maked as fatal and the corresponding events are
3+
// emitted in the command instance itself.
4+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
5+
16
'use strict';
27

38
const Command = require('./command.js');
@@ -151,20 +156,28 @@ class ClientHandshake extends Command {
151156
}
152157
return ClientHandshake.prototype.handshakeResult;
153158
} catch (err) {
159+
// Authentication errors are fatal
160+
err.code = 'AUTH_SWITCH_PLUGIN_ERROR';
161+
err.fatal = true;
162+
154163
if (this.onResult) {
155164
this.onResult(err);
156165
} else {
157-
connection.emit('error', err);
166+
this.emit('error', err);
158167
}
159168
return null;
160169
}
161170
}
162171
if (marker !== 0) {
163172
const err = new Error('Unexpected packet during handshake phase');
173+
// Unknown handshake errors are fatal
174+
err.code = 'HANDSHAKE_UNKNOWN_ERROR';
175+
err.fatal = true;
176+
164177
if (this.onResult) {
165178
this.onResult(err);
166179
} else {
167-
connection.emit('error', err);
180+
this.emit('error', err);
168181
}
169182
return null;
170183
}

lib/connection.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
// the MySQL server when the connection is closed unexpectedly.
44
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
55

6+
// This file was modified by Oracle on June 17, 2021.
7+
// The changes involve logic to ensure the socket connection is closed when
8+
// there is a fatal error.
9+
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
10+
611
'use strict';
712

813
const Net = require('net');
@@ -105,9 +110,10 @@ class Connection extends EventEmitter {
105110
if (!this.config.isServer) {
106111
handshakeCommand = new Commands.ClientHandshake(this.config.clientFlags);
107112
handshakeCommand.on('end', () => {
108-
// this happens when handshake finishes early and first packet is error
109-
// and not server hello ( for example, 'Too many connactions' error)
110-
if (!handshakeCommand.handshake) {
113+
// this happens when handshake finishes early either because there was
114+
// some fatal error or the server sent an error packet instead of
115+
// an hello packet (for example, 'Too many connactions' error)
116+
if (!handshakeCommand.handshake || this._fatalError || this._protocolError) {
111117
return;
112118
}
113119
this._handshakePacket = handshakeCommand.handshake;
@@ -229,6 +235,10 @@ class Connection extends EventEmitter {
229235
if (bubbleErrorToConnection || this._pool) {
230236
this.emit('error', err);
231237
}
238+
// close connection after emitting the event in case of a fatal error
239+
if (err.fatal) {
240+
this.close();
241+
}
232242
}
233243

234244
write(buffer) {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright (c) 2021, Oracle and/or its affiliates.
2+
3+
'use strict';
4+
5+
const mysql = require('../../index.js');
6+
const Command = require('../../lib/commands/command.js');
7+
const Packets = require('../../lib/packets/index.js');
8+
9+
const assert = require('assert');
10+
11+
class TestAuthSwitchPluginError extends Command {
12+
constructor(args) {
13+
super();
14+
this.args = args;
15+
}
16+
17+
start(_, connection) {
18+
const serverHelloPacket = new Packets.Handshake({
19+
// "required" properties
20+
protocolVersion: 10,
21+
serverVersion: 'node.js rocks'
22+
});
23+
this.serverHello = serverHelloPacket;
24+
serverHelloPacket.setScrambleData(() => {
25+
connection.writePacket(serverHelloPacket.toPacket(0));
26+
});
27+
return TestAuthSwitchPluginError.prototype.sendAuthSwitchRequest;
28+
}
29+
30+
sendAuthSwitchRequest(_, connection) {
31+
const asr = new Packets.AuthSwitchRequest(this.args);
32+
connection.writePacket(asr.toPacket());
33+
return TestAuthSwitchPluginError.prototype.finish;
34+
}
35+
36+
finish(_, connection) {
37+
connection.end();
38+
return TestAuthSwitchPluginError.prototype.finish;
39+
}
40+
}
41+
42+
const server = mysql.createServer(conn => {
43+
conn.addCommand(
44+
new TestAuthSwitchPluginError({
45+
pluginName: 'auth_test_plugin',
46+
pluginData: Buffer.allocUnsafe(0)
47+
})
48+
);
49+
});
50+
51+
let error;
52+
let uncaughtExceptions = 0;
53+
54+
const portfinder = require('portfinder');
55+
portfinder.getPort((_, port) => {
56+
server.listen(port);
57+
const conn = mysql.createConnection({
58+
port: port,
59+
authPlugins: {
60+
auth_test_plugin: () => {
61+
throw new Error('boom');
62+
}
63+
}
64+
});
65+
66+
conn.on('error', err => {
67+
error = err;
68+
69+
conn.end();
70+
server.close();
71+
});
72+
});
73+
74+
process.on('uncaughtException', err => {
75+
// The plugin reports a fatal error
76+
assert.equal(error.code, 'AUTH_SWITCH_PLUGIN_ERROR');
77+
assert.equal(error.message, 'boom');
78+
assert.equal(error.fatal, true);
79+
// The server must close the connection
80+
assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST');
81+
82+
uncaughtExceptions += 1;
83+
});
84+
85+
process.on('exit', () => {
86+
assert.equal(uncaughtExceptions, 1);
87+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) 2021, Oracle and/or its affiliates.
2+
3+
'use strict';
4+
5+
const mysql = require('../../index.js');
6+
const Command = require('../../lib/commands/command.js');
7+
const Packet = require('../../lib/packets/packet.js');
8+
const Packets = require('../../lib/packets/index.js');
9+
10+
const assert = require('assert');
11+
12+
class TestUnknownHandshakePacket extends Command {
13+
constructor(args) {
14+
super();
15+
this.args = args;
16+
}
17+
18+
start(_, connection) {
19+
const serverHelloPacket = new Packets.Handshake({
20+
// "required" properties
21+
protocolVersion: 10,
22+
serverVersion: 'node.js rocks'
23+
});
24+
this.serverHello = serverHelloPacket;
25+
serverHelloPacket.setScrambleData(() => {
26+
connection.writePacket(serverHelloPacket.toPacket(0));
27+
});
28+
return TestUnknownHandshakePacket.prototype.writeUnexpectedPacket;
29+
}
30+
31+
writeUnexpectedPacket(_, connection) {
32+
const length = 6 + this.args.length;
33+
const buffer = Buffer.allocUnsafe(length);
34+
const up = new Packet(0, buffer, 0, length);
35+
up.offset = 4;
36+
up.writeInt8(0xfd);
37+
up.writeBuffer(this.args);
38+
connection.writePacket(up);
39+
return TestUnknownHandshakePacket.prototype.finish;
40+
}
41+
42+
finish(_, connection) {
43+
connection.end();
44+
return TestUnknownHandshakePacket.prototype.finish;
45+
}
46+
}
47+
48+
const server = mysql.createServer(conn => {
49+
conn.addCommand(new TestUnknownHandshakePacket(Buffer.alloc(0)));
50+
});
51+
52+
let error;
53+
let uncaughtExceptions = 0;
54+
55+
const portfinder = require('portfinder');
56+
portfinder.getPort((_, port) => {
57+
server.listen(port);
58+
const conn = mysql.createConnection({
59+
port: port
60+
});
61+
62+
conn.on('error', err => {
63+
error = err;
64+
65+
conn.end();
66+
server.close();
67+
});
68+
});
69+
70+
process.on('uncaughtException', err => {
71+
// The plugin reports a fatal error
72+
assert.equal(error.code, 'HANDSHAKE_UNKNOWN_ERROR');
73+
assert.equal(error.message, 'Unexpected packet during handshake phase');
74+
assert.equal(error.fatal, true);
75+
// The server must close the connection
76+
assert.equal(err.code, 'PROTOCOL_CONNECTION_LOST');
77+
78+
uncaughtExceptions += 1;
79+
});
80+
81+
process.on('exit', () => {
82+
assert.equal(uncaughtExceptions, 1);
83+
});

0 commit comments

Comments
 (0)