Skip to content

Commit e052e7b

Browse files
committed
add handshake fatal error handling
Errors that occur during the handshake stage should be fatal and should cause the connection to be closed. Currently, although unexpected errors such as ones created by unexpected packets sent by the server, are being handled, they are not considered fatal and do not result in closing the connection. On the other hand, errors reported by client-side authentication plugins are being ignored and are not reported to the application. Since errors reported by server-side authentication plugins are considered fatal and cause the connection to be closed, the fact that the same does not happen for errors generated on the client-side leads to some inconsistencies and forces custom authentication plugin implementations to forcibly close the connection on their own. This patch ensures that both this kind of errors are considered fatal and cause the connection to be closed.
1 parent e76d196 commit e052e7b

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)