Spaces:
Running
Running
// This file was modified by Oracle on June 17, 2021. | |
// Handshake errors are now maked as fatal and the corresponding events are | |
// emitted in the command instance itself. | |
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | |
// This file was modified by Oracle on September 21, 2021. | |
// Handshake workflow now supports additional authentication factors requested | |
// by the server. | |
// Modifications copyright (c) 2021, Oracle and/or its affiliates. | |
; | |
const Command = require('./command.js'); | |
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'); | |
function flagNames(flags) { | |
const res = []; | |
for (const c in ClientConstants) { | |
if (flags & ClientConstants[c]) { | |
res.push(c.replace(/_/g, ' ').toLowerCase()); | |
} | |
} | |
return res; | |
} | |
class ClientHandshake extends Command { | |
constructor(clientFlags) { | |
super(); | |
this.handshake = null; | |
this.clientFlags = clientFlags; | |
this.authenticationFactor = 0; | |
} | |
start() { | |
return ClientHandshake.prototype.handshakeInit; | |
} | |
sendSSLRequest(connection) { | |
const sslRequest = new Packets.SSLRequest( | |
this.clientFlags, | |
connection.config.charsetNumber | |
); | |
connection.writePacket(sslRequest.toPacket()); | |
} | |
sendCredentials(connection) { | |
if (connection.config.debug) { | |
// eslint-disable-next-line | |
console.log( | |
'Sending handshake packet: flags:%d=(%s)', | |
this.clientFlags, | |
flagNames(this.clientFlags).join(', ') | |
); | |
} | |
this.user = connection.config.user; | |
this.password = connection.config.password; | |
// "password1" is an alias to the original "password" value | |
// to make it easier to integrate multi-factor authentication | |
this.password1 = connection.config.password; | |
// "password2" and "password3" are the 2nd and 3rd factor authentication | |
// passwords, which can be undefined depending on the authentication | |
// plugin being used | |
this.password2 = connection.config.password2; | |
this.password3 = connection.config.password3; | |
this.passwordSha1 = connection.config.passwordSha1; | |
this.database = connection.config.database; | |
this.autPluginName = this.handshake.autPluginName; | |
const handshakeResponse = new Packets.HandshakeResponse({ | |
flags: this.clientFlags, | |
user: this.user, | |
database: this.database, | |
password: this.password, | |
passwordSha1: this.passwordSha1, | |
charsetNumber: connection.config.charsetNumber, | |
authPluginData1: this.handshake.authPluginData1, | |
authPluginData2: this.handshake.authPluginData2, | |
compress: connection.config.compress, | |
connectAttributes: connection.config.connectAttributes | |
}); | |
connection.writePacket(handshakeResponse.toPacket()); | |
} | |
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( | |
this.passwordSha1, | |
authPluginData1, | |
authPluginData2 | |
); | |
} else { | |
authToken = auth41.calculateToken( | |
this.password, | |
authPluginData1, | |
authPluginData2 | |
); | |
} | |
return authToken; | |
} | |
handshakeInit(helloPacket, connection) { | |
this.on('error', e => { | |
connection._fatalError = e; | |
connection._protocolError = e; | |
}); | |
this.handshake = Packets.Handshake.fromPacket(helloPacket); | |
if (connection.config.debug) { | |
// eslint-disable-next-line | |
console.log( | |
'Server hello packet: capability flags:%d=(%s)', | |
this.handshake.capabilityFlags, | |
flagNames(this.handshake.capabilityFlags).join(', ') | |
); | |
} | |
connection.serverCapabilityFlags = this.handshake.capabilityFlags; | |
connection.serverEncoding = CharsetToEncoding[this.handshake.characterSet]; | |
connection.connectionId = this.handshake.connectionId; | |
const serverSSLSupport = | |
this.handshake.capabilityFlags & ClientConstants.SSL; | |
// multi factor authentication is enabled with the | |
// "MULTI_FACTOR_AUTHENTICATION" capability and should only be used if it | |
// is supported by the server | |
const multiFactorAuthentication = | |
this.handshake.capabilityFlags & ClientConstants.MULTI_FACTOR_AUTHENTICATION; | |
this.clientFlags = this.clientFlags | multiFactorAuthentication; | |
// use compression only if requested by client and supported by server | |
connection.config.compress = | |
connection.config.compress && | |
this.handshake.capabilityFlags & ClientConstants.COMPRESS; | |
this.clientFlags = this.clientFlags | connection.config.compress; | |
if (connection.config.ssl) { | |
// client requires SSL but server does not support it | |
if (!serverSSLSupport) { | |
const err = new Error('Server does not support secure connection'); | |
err.code = 'HANDSHAKE_NO_SSL_SUPPORT'; | |
err.fatal = true; | |
this.emit('error', err); | |
return false; | |
} | |
// 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); | |
} | |
if (multiFactorAuthentication) { | |
// if the server supports multi-factor authentication, we enable it in | |
// the client | |
this.authenticationFactor = 1; | |
} | |
return ClientHandshake.prototype.handshakeResult; | |
} | |
handshakeResult(packet, connection) { | |
const marker = packet.peekByte(); | |
// packet can be OK_Packet, ERR_Packet, AuthSwitchRequest, AuthNextFactor | |
// or AuthMoreData | |
if (marker === 0xfe || marker === 1 || marker === 0x02) { | |
const authSwitch = require('./auth_switch'); | |
try { | |
if (marker === 1) { | |
authSwitch.authSwitchRequestMoreData(packet, connection, this); | |
} else { | |
// if authenticationFactor === 0, it means the server does not support | |
// the multi-factor authentication capability | |
if (this.authenticationFactor !== 0) { | |
// if we are past the first authentication factor, we should use the | |
// corresponding password (if there is one) | |
connection.config.password = this[`password${this.authenticationFactor}`]; | |
// update the current authentication factor | |
this.authenticationFactor += 1; | |
} | |
// if marker === 0x02, it means it is an AuthNextFactor packet, | |
// which is similar in structure to an AuthSwitchRequest packet, | |
// so, we can use it directly | |
authSwitch.authSwitchRequest(packet, connection, this); | |
} | |
return ClientHandshake.prototype.handshakeResult; | |
} catch (err) { | |
// Authentication errors are fatal | |
err.code = 'AUTH_SWITCH_PLUGIN_ERROR'; | |
err.fatal = true; | |
if (this.onResult) { | |
this.onResult(err); | |
} else { | |
this.emit('error', err); | |
} | |
return null; | |
} | |
} | |
if (marker !== 0) { | |
const err = new Error('Unexpected packet during handshake phase'); | |
// Unknown handshake errors are fatal | |
err.code = 'HANDSHAKE_UNKNOWN_ERROR'; | |
err.fatal = true; | |
if (this.onResult) { | |
this.onResult(err); | |
} else { | |
this.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) { | |
const enableCompression = require('../compressed_protocol.js') | |
.enableCompression; | |
enableCompression(connection); | |
} | |
} | |
if (this.onResult) { | |
this.onResult(null); | |
} | |
return null; | |
} | |
} | |
module.exports = ClientHandshake; | |