Initial commit
This commit is contained in:
+227
@@ -0,0 +1,227 @@
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
// Copyright (c) 2015-2024 MariaDB Corporation Ab
|
||||
|
||||
import PluginAuth from './plugin-auth.js';
|
||||
import InitialHandshake from './initial-handshake.js';
|
||||
import ClientCapabilities from '../client-capabilities.js';
|
||||
import * as Capabilities from '../../../const/capabilities.js';
|
||||
import SslRequest from '../ssl-request.js';
|
||||
import * as Errors from '../../../misc/errors.js';
|
||||
import NativePasswordAuth from './native-password-auth.js';
|
||||
import * as Iconv from 'iconv-lite';
|
||||
import Crypto from 'node:crypto';
|
||||
import os from 'node:os';
|
||||
import packageJson from '../../../../package.json' with { type: 'json' };
|
||||
|
||||
const driverVersion = packageJson.version;
|
||||
|
||||
/**
|
||||
* Handshake response
|
||||
*/
|
||||
class Handshake extends PluginAuth {
|
||||
constructor(auth, host, getSocket, multiAuthResolver, reject) {
|
||||
super(null, multiAuthResolver, reject);
|
||||
this.sequenceNo = 0;
|
||||
this.compressSequenceNo = 0;
|
||||
this.auth = auth;
|
||||
this.getSocket = getSocket;
|
||||
this.counter = 0;
|
||||
this.onPacketReceive = this.parseHandshakeInit;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
start(out, opts, info) {}
|
||||
|
||||
parseHandshakeInit(packet, out, opts, info) {
|
||||
if (packet.peek() === 0xff) {
|
||||
//in case that some host is not permit to connect the server
|
||||
const authErr = packet.readError(info);
|
||||
authErr.fatal = true;
|
||||
return this.throwError(authErr, info);
|
||||
}
|
||||
|
||||
let handshake = new InitialHandshake(packet, info);
|
||||
ClientCapabilities(opts, info);
|
||||
this.pluginName = handshake.pluginName;
|
||||
|
||||
if (this.requireSecure() && !this.isSecureConnection(opts)) {
|
||||
return this.throwNewError(
|
||||
`${this.pluginName} authentication requires TLS or a local socket`,
|
||||
true,
|
||||
info,
|
||||
'08S01',
|
||||
Errors.client.ER_CLEAR_PASSWORD_WITHOUT_SSL
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.ssl) {
|
||||
if (info.serverCapabilities & Capabilities.SSL) {
|
||||
info.clientCapabilities |= Capabilities.SSL;
|
||||
SslRequest(this, out, info, opts);
|
||||
this.auth._createSecureContext(info, () => {
|
||||
// mark self-signed error only if was not explicitly forced
|
||||
const secureSocket = this.getSocket();
|
||||
info.selfSignedCertificate = !secureSocket.authorized;
|
||||
info.tlsAuthorizationError = secureSocket.authorizationError;
|
||||
const serverCert = secureSocket.getPeerCertificate(false);
|
||||
info.tlsCert = serverCert;
|
||||
info.tlsFingerprint = serverCert ? serverCert.fingerprint256.replace(/:/gi, '').toLowerCase() : null;
|
||||
|
||||
if (info.requireValidCert && info.selfSignedCertificate && Boolean(opts.password) && !this.isMitmProof()) {
|
||||
return this.throwNewError(
|
||||
`Unsupported authentication plugin ${this.pluginName} with Self signed certificates.
|
||||
Either set 'ssl: { rejectUnauthorized: false }' (trust mode) or provide server certificate to client`,
|
||||
true,
|
||||
info,
|
||||
'08000',
|
||||
Errors.client.ER_SELF_SIGNED_BAD_PLUGIN
|
||||
);
|
||||
}
|
||||
|
||||
Handshake.send.call(this, this, out, opts, handshake.pluginName, info);
|
||||
});
|
||||
} else {
|
||||
return this.throwNewError(
|
||||
'Trying to connect with ssl, but ssl not enabled in the server',
|
||||
true,
|
||||
info,
|
||||
'08S01',
|
||||
Errors.client.ER_SERVER_SSL_DISABLED
|
||||
);
|
||||
}
|
||||
} else {
|
||||
Handshake.send(this, out, opts, handshake.pluginName, info);
|
||||
}
|
||||
this.onPacketReceive = this.auth.handshakeResult.bind(this.auth);
|
||||
}
|
||||
|
||||
isMitmProof() {
|
||||
return this.pluginName !== 'mysql_clear_password';
|
||||
}
|
||||
|
||||
requireSecure() {
|
||||
return this.pluginName === 'mysql_clear_password';
|
||||
}
|
||||
|
||||
hash(conf) {
|
||||
// mysql_native_password hash
|
||||
let hash = Crypto.createHash('sha1');
|
||||
let stage1 = hash.update(conf.password, 'utf8').digest();
|
||||
hash = Crypto.createHash('sha1');
|
||||
return hash.update(stage1).digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Handshake response packet
|
||||
* see https://mariadb.com/kb/en/library/1-connecting-connecting/#handshake-response-packet
|
||||
*
|
||||
* @param cmd current handshake command
|
||||
* @param out output writer
|
||||
* @param opts connection options
|
||||
* @param pluginName plugin name
|
||||
* @param info connection information
|
||||
*/
|
||||
static send(cmd, out, opts, pluginName, info) {
|
||||
out.startPacket(cmd);
|
||||
info.defaultPluginName = pluginName;
|
||||
const pwd = Array.isArray(opts.password) ? opts.password[0] : opts.password;
|
||||
let authToken;
|
||||
let authPlugin;
|
||||
switch (pluginName) {
|
||||
case 'mysql_clear_password':
|
||||
authToken = Buffer.from(pwd);
|
||||
authPlugin = 'mysql_clear_password';
|
||||
break;
|
||||
|
||||
default:
|
||||
authToken = NativePasswordAuth.encryptSha1Password(pwd, info.seed);
|
||||
authPlugin = 'mysql_native_password';
|
||||
break;
|
||||
}
|
||||
out.writeInt32(Number(info.clientCapabilities & BigInt(0xffffffff)));
|
||||
out.writeInt32(1024 * 1024 * 1024); // max packet size
|
||||
|
||||
// if collation and id < 255, set it directly
|
||||
// is not, additional command SET NAMES xx [COLLATE yy] will be issued
|
||||
out.writeInt8(opts.collation && opts.collation.index <= 255 ? opts.collation.index : 224);
|
||||
for (let i = 0; i < 19; i++) {
|
||||
out.writeInt8(0);
|
||||
}
|
||||
|
||||
out.writeInt32(Number(info.clientCapabilities >> 32n));
|
||||
|
||||
//null encoded user
|
||||
out.writeString(opts.user || '');
|
||||
out.writeInt8(0);
|
||||
|
||||
if (info.serverCapabilities & Capabilities.PLUGIN_AUTH_LENENC_CLIENT_DATA) {
|
||||
out.writeLengthCoded(authToken.length);
|
||||
out.writeBuffer(authToken, 0, authToken.length);
|
||||
} else if (info.serverCapabilities & Capabilities.SECURE_CONNECTION) {
|
||||
out.writeInt8(authToken.length);
|
||||
out.writeBuffer(authToken, 0, authToken.length);
|
||||
} else {
|
||||
out.writeBuffer(authToken, 0, authToken.length);
|
||||
out.writeInt8(0);
|
||||
}
|
||||
|
||||
if (info.clientCapabilities & Capabilities.CONNECT_WITH_DB) {
|
||||
out.writeString(opts.database);
|
||||
out.writeInt8(0);
|
||||
info.database = opts.database;
|
||||
}
|
||||
|
||||
if (info.clientCapabilities & Capabilities.PLUGIN_AUTH) {
|
||||
out.writeString(authPlugin);
|
||||
out.writeInt8(0);
|
||||
}
|
||||
|
||||
if (info.clientCapabilities & Capabilities.CONNECT_ATTRS) {
|
||||
out.writeInt8(0xfc);
|
||||
let initPos = out.pos; //save position, assuming connection attributes length will be less than 2 bytes length
|
||||
out.writeInt16(0);
|
||||
const encoding = info.collation ? info.collation.charset : 'utf8';
|
||||
|
||||
Handshake.writeAttribute(out, '_client_name', encoding);
|
||||
Handshake.writeAttribute(out, 'MariaDB connector/Node', encoding);
|
||||
|
||||
Handshake.writeAttribute(out, '_client_version', encoding);
|
||||
Handshake.writeAttribute(out, driverVersion, encoding);
|
||||
|
||||
if (this.host) {
|
||||
Handshake.writeAttribute(out, '_server_host', encoding);
|
||||
Handshake.writeAttribute(out, this.host, encoding);
|
||||
}
|
||||
|
||||
Handshake.writeAttribute(out, '_os', encoding);
|
||||
Handshake.writeAttribute(out, process.platform, encoding);
|
||||
|
||||
Handshake.writeAttribute(out, '_client_host', encoding);
|
||||
Handshake.writeAttribute(out, os.hostname(), encoding);
|
||||
|
||||
Handshake.writeAttribute(out, '_node_version', encoding);
|
||||
Handshake.writeAttribute(out, process.versions.node, encoding);
|
||||
|
||||
if (opts.connectAttributes !== true) {
|
||||
let attrNames = Object.keys(opts.connectAttributes);
|
||||
for (let k = 0; k < attrNames.length; ++k) {
|
||||
Handshake.writeAttribute(out, attrNames[k], encoding);
|
||||
Handshake.writeAttribute(out, opts.connectAttributes[attrNames[k]], encoding);
|
||||
}
|
||||
}
|
||||
|
||||
//write end size
|
||||
out.writeInt16AtPos(initPos);
|
||||
}
|
||||
|
||||
out.flushPacket();
|
||||
}
|
||||
|
||||
static writeAttribute(out, val, encoding) {
|
||||
let param = Buffer.isEncoding(encoding) ? Buffer.from(val, encoding) : Iconv.encode(val, encoding);
|
||||
out.writeLengthCoded(param.length);
|
||||
out.writeBuffer(param, 0, param.length);
|
||||
}
|
||||
}
|
||||
|
||||
export default Handshake;
|
||||
Reference in New Issue
Block a user