Erster Commit
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2021, Oracle and/or its affiliates.
|
||||
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
class AuthNextFactor {
|
||||
constructor(opts) {
|
||||
this.pluginName = opts.pluginName;
|
||||
this.pluginData = opts.pluginData;
|
||||
}
|
||||
|
||||
toPacket(encoding) {
|
||||
const length = 6 + this.pluginName.length + this.pluginData.length;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(0x02);
|
||||
packet.writeNullTerminatedString(this.pluginName, encoding);
|
||||
packet.writeBuffer(this.pluginData);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static fromPacket(packet, encoding) {
|
||||
packet.readInt8(); // marker
|
||||
const name = packet.readNullTerminatedString(encoding);
|
||||
const data = packet.readBuffer();
|
||||
return new AuthNextFactor({
|
||||
pluginName: name,
|
||||
pluginData: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthNextFactor;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
class AuthSwitchRequest {
|
||||
constructor(opts) {
|
||||
this.pluginName = opts.pluginName;
|
||||
this.pluginData = opts.pluginData;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const length = 6 + this.pluginName.length + this.pluginData.length;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(0xfe);
|
||||
// TODO: use server encoding
|
||||
packet.writeNullTerminatedString(this.pluginName, 'cesu8');
|
||||
packet.writeBuffer(this.pluginData);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static fromPacket(packet) {
|
||||
packet.readInt8(); // marker
|
||||
// assert marker == 0xfe?
|
||||
// TODO: use server encoding
|
||||
const name = packet.readNullTerminatedString('cesu8');
|
||||
const data = packet.readBuffer();
|
||||
return new AuthSwitchRequest({
|
||||
pluginName: name,
|
||||
pluginData: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthSwitchRequest;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
class AuthSwitchRequestMoreData {
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const length = 5 + this.data.length;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(0x01);
|
||||
packet.writeBuffer(this.data);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static fromPacket(packet) {
|
||||
packet.readInt8(); // marker
|
||||
const data = packet.readBuffer();
|
||||
return new AuthSwitchRequestMoreData(data);
|
||||
}
|
||||
|
||||
static verifyMarker(packet) {
|
||||
return packet.peekByte() === 0x01;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthSwitchRequestMoreData;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
class AuthSwitchResponse {
|
||||
constructor(data) {
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
data = Buffer.from(data);
|
||||
}
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const length = 4 + this.data.length;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeBuffer(this.data);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static fromPacket(packet) {
|
||||
const data = packet.readBuffer();
|
||||
return new AuthSwitchResponse(data);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthSwitchResponse;
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
'use strict';
|
||||
|
||||
const Types = require('../constants/types');
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
const binaryReader = new Array(256);
|
||||
|
||||
class BinaryRow {
|
||||
constructor(columns) {
|
||||
this.columns = columns || [];
|
||||
}
|
||||
|
||||
static toPacket(columns, encoding) {
|
||||
// throw new Error('Not implemented');
|
||||
const sequenceId = 0; // TODO remove, this is calculated now in connection
|
||||
let length = 0;
|
||||
columns.forEach((val) => {
|
||||
if (val === null || typeof val === 'undefined') {
|
||||
++length;
|
||||
return;
|
||||
}
|
||||
length += Packet.lengthCodedStringLength(val.toString(10), encoding);
|
||||
});
|
||||
|
||||
length = length + 2;
|
||||
|
||||
const buffer = Buffer.allocUnsafe(length + 4);
|
||||
const packet = new Packet(sequenceId, buffer, 0, length + 4);
|
||||
packet.offset = 4;
|
||||
|
||||
packet.writeInt8(0);
|
||||
|
||||
let bitmap = 0;
|
||||
let bitValue = 1;
|
||||
columns.forEach((parameter) => {
|
||||
if (parameter.type === Types.NULL) {
|
||||
bitmap += bitValue;
|
||||
}
|
||||
bitValue *= 2;
|
||||
if (bitValue === 256) {
|
||||
packet.writeInt8(bitmap);
|
||||
bitmap = 0;
|
||||
bitValue = 1;
|
||||
}
|
||||
});
|
||||
if (bitValue !== 1) {
|
||||
packet.writeInt8(bitmap);
|
||||
}
|
||||
|
||||
columns.forEach((val) => {
|
||||
if (val === null) {
|
||||
packet.writeNull();
|
||||
return;
|
||||
}
|
||||
if (typeof val === 'undefined') {
|
||||
packet.writeInt8(0);
|
||||
return;
|
||||
}
|
||||
packet.writeLengthCodedString(val.toString(10), encoding);
|
||||
});
|
||||
return packet;
|
||||
}
|
||||
|
||||
// TODO: complete list of types...
|
||||
static fromPacket(fields, packet) {
|
||||
const columns = new Array(fields.length);
|
||||
packet.readInt8(); // TODO check it's 0
|
||||
const nullBitmapLength = Math.floor((fields.length + 7 + 2) / 8);
|
||||
// TODO: read and interpret null bitmap
|
||||
packet.skip(nullBitmapLength);
|
||||
for (let i = 0; i < columns.length; ++i) {
|
||||
columns[i] = binaryReader[fields[i].columnType].apply(packet);
|
||||
}
|
||||
return new BinaryRow(columns);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: replace with constants.MYSQL_TYPE_*
|
||||
binaryReader[Types.DECIMAL] = Packet.prototype.readLengthCodedString;
|
||||
binaryReader[1] = Packet.prototype.readInt8; // tiny
|
||||
binaryReader[2] = Packet.prototype.readInt16; // short
|
||||
binaryReader[3] = Packet.prototype.readInt32; // long
|
||||
binaryReader[4] = Packet.prototype.readFloat; // float
|
||||
binaryReader[5] = Packet.prototype.readDouble; // double
|
||||
binaryReader[6] = Packet.prototype.assertInvalid; // null, should be skipped via null bitmap
|
||||
binaryReader[7] = Packet.prototype.readTimestamp; // timestamp, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_TIMESTAMP
|
||||
binaryReader[8] = Packet.prototype.readInt64; // long long
|
||||
binaryReader[9] = Packet.prototype.readInt32; // int24
|
||||
binaryReader[10] = Packet.prototype.readTimestamp; // date
|
||||
binaryReader[11] = Packet.prototype.readTime; // time, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_TIME
|
||||
binaryReader[12] = Packet.prototype.readDateTime; // datetime, http://dev.mysql.com/doc/internals/en/prepared-statements.html#packet-ProtocolBinary::MYSQL_TYPE_DATETIME
|
||||
binaryReader[13] = Packet.prototype.readInt16; // year
|
||||
binaryReader[Types.VAR_STRING] = Packet.prototype.readLengthCodedString; // var string
|
||||
|
||||
module.exports = BinaryRow;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/com-binlog-dump.html#packet-COM_BINLOG_DUMP
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
const CommandCodes = require('../constants/commands');
|
||||
|
||||
// TODO: add flag to constants
|
||||
// 0x01 - BINLOG_DUMP_NON_BLOCK
|
||||
// send EOF instead of blocking
|
||||
class BinlogDump {
|
||||
constructor(opts) {
|
||||
this.binlogPos = opts.binlogPos || 0;
|
||||
this.serverId = opts.serverId || 0;
|
||||
this.flags = opts.flags || 0;
|
||||
this.filename = opts.filename || '';
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const length = 15 + Buffer.byteLength(this.filename, 'utf8'); // TODO: should be ascii?
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCodes.BINLOG_DUMP);
|
||||
packet.writeInt32(this.binlogPos);
|
||||
packet.writeInt16(this.flags);
|
||||
packet.writeInt32(this.serverId);
|
||||
packet.writeString(this.filename);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BinlogDump;
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/query-event.html
|
||||
|
||||
const keys = {
|
||||
FLAGS2: 0,
|
||||
SQL_MODE: 1,
|
||||
CATALOG: 2,
|
||||
AUTO_INCREMENT: 3,
|
||||
CHARSET: 4,
|
||||
TIME_ZONE: 5,
|
||||
CATALOG_NZ: 6,
|
||||
LC_TIME_NAMES: 7,
|
||||
CHARSET_DATABASE: 8,
|
||||
TABLE_MAP_FOR_UPDATE: 9,
|
||||
MASTER_DATA_WRITTEN: 10,
|
||||
INVOKERS: 11,
|
||||
UPDATED_DB_NAMES: 12,
|
||||
MICROSECONDS: 3,
|
||||
};
|
||||
|
||||
module.exports = function parseStatusVars(buffer) {
|
||||
const result = {};
|
||||
let offset = 0;
|
||||
let key, length, prevOffset;
|
||||
while (offset < buffer.length) {
|
||||
key = buffer[offset++];
|
||||
switch (key) {
|
||||
case keys.FLAGS2:
|
||||
result.flags = buffer.readUInt32LE(offset);
|
||||
offset += 4;
|
||||
break;
|
||||
case keys.SQL_MODE:
|
||||
// value is 8 bytes, but all documented flags are in first 4 bytes
|
||||
result.sqlMode = buffer.readUInt32LE(offset);
|
||||
offset += 8;
|
||||
break;
|
||||
case keys.CATALOG:
|
||||
length = buffer[offset++];
|
||||
result.catalog = buffer.toString('utf8', offset, offset + length);
|
||||
offset += length + 1; // null byte after string
|
||||
break;
|
||||
case keys.CHARSET:
|
||||
result.clientCharset = buffer.readUInt16LE(offset);
|
||||
result.connectionCollation = buffer.readUInt16LE(offset + 2);
|
||||
result.serverCharset = buffer.readUInt16LE(offset + 4);
|
||||
offset += 6;
|
||||
break;
|
||||
case keys.TIME_ZONE:
|
||||
length = buffer[offset++];
|
||||
result.timeZone = buffer.toString('utf8', offset, offset + length);
|
||||
offset += length; // no null byte
|
||||
break;
|
||||
case keys.CATALOG_NZ:
|
||||
length = buffer[offset++];
|
||||
result.catalogNz = buffer.toString('utf8', offset, offset + length);
|
||||
offset += length; // no null byte
|
||||
break;
|
||||
case keys.LC_TIME_NAMES:
|
||||
result.lcTimeNames = buffer.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
break;
|
||||
case keys.CHARSET_DATABASE:
|
||||
result.schemaCharset = buffer.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
break;
|
||||
case keys.TABLE_MAP_FOR_UPDATE:
|
||||
result.mapForUpdate1 = buffer.readUInt32LE(offset);
|
||||
result.mapForUpdate2 = buffer.readUInt32LE(offset + 4);
|
||||
offset += 8;
|
||||
break;
|
||||
case keys.MASTER_DATA_WRITTEN:
|
||||
result.masterDataWritten = buffer.readUInt32LE(offset);
|
||||
offset += 4;
|
||||
break;
|
||||
case keys.INVOKERS:
|
||||
length = buffer[offset++];
|
||||
result.invokerUsername = buffer.toString(
|
||||
'utf8',
|
||||
offset,
|
||||
offset + length
|
||||
);
|
||||
offset += length;
|
||||
length = buffer[offset++];
|
||||
result.invokerHostname = buffer.toString(
|
||||
'utf8',
|
||||
offset,
|
||||
offset + length
|
||||
);
|
||||
offset += length;
|
||||
break;
|
||||
case keys.UPDATED_DB_NAMES:
|
||||
length = buffer[offset++];
|
||||
// length - number of null-terminated strings
|
||||
result.updatedDBs = []; // we'll store them as array here
|
||||
for (; length; --length) {
|
||||
prevOffset = offset;
|
||||
// fast forward to null terminating byte
|
||||
while (buffer[offset++] && offset < buffer.length) {
|
||||
// empty body, everything inside while condition
|
||||
}
|
||||
result.updatedDBs.push(
|
||||
buffer.toString('utf8', prevOffset, offset - 1)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case keys.MICROSECONDS:
|
||||
result.microseconds =
|
||||
// REVIEW: INVALID UNKNOWN VARIABLE!
|
||||
buffer.readInt16LE(offset) + (buffer[offset + 2] << 16);
|
||||
offset += 3;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
'use strict';
|
||||
|
||||
const CommandCode = require('../constants/commands.js');
|
||||
const ClientConstants = require('../constants/client.js');
|
||||
const Packet = require('../packets/packet.js');
|
||||
const auth41 = require('../auth_41.js');
|
||||
const CharsetToEncoding = require('../constants/charset_encodings.js');
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/com-change-user.html#packet-COM_CHANGE_USER
|
||||
class ChangeUser {
|
||||
constructor(opts) {
|
||||
this.flags = opts.flags;
|
||||
this.user = opts.user || '';
|
||||
this.database = opts.database || '';
|
||||
this.password = opts.password || '';
|
||||
this.passwordSha1 = opts.passwordSha1;
|
||||
this.authPluginData1 = opts.authPluginData1;
|
||||
this.authPluginData2 = opts.authPluginData2;
|
||||
this.connectAttributes = opts.connectAttributes || {};
|
||||
let authToken;
|
||||
if (this.passwordSha1) {
|
||||
authToken = auth41.calculateTokenFromPasswordSha(
|
||||
this.passwordSha1,
|
||||
this.authPluginData1,
|
||||
this.authPluginData2
|
||||
);
|
||||
} else {
|
||||
authToken = auth41.calculateToken(
|
||||
this.password,
|
||||
this.authPluginData1,
|
||||
this.authPluginData2
|
||||
);
|
||||
}
|
||||
this.authToken = authToken;
|
||||
this.charsetNumber = opts.charsetNumber;
|
||||
}
|
||||
|
||||
// TODO
|
||||
// ChangeUser.fromPacket = function(packet)
|
||||
// };
|
||||
serializeToBuffer(buffer) {
|
||||
const isSet = (flag) => this.flags & ClientConstants[flag];
|
||||
const packet = new Packet(0, buffer, 0, buffer.length);
|
||||
packet.offset = 4;
|
||||
const encoding = CharsetToEncoding[this.charsetNumber];
|
||||
packet.writeInt8(CommandCode.CHANGE_USER);
|
||||
packet.writeNullTerminatedString(this.user, encoding);
|
||||
if (isSet('SECURE_CONNECTION')) {
|
||||
packet.writeInt8(this.authToken.length);
|
||||
packet.writeBuffer(this.authToken);
|
||||
} else {
|
||||
packet.writeBuffer(this.authToken);
|
||||
packet.writeInt8(0);
|
||||
}
|
||||
packet.writeNullTerminatedString(this.database, encoding);
|
||||
packet.writeInt16(this.charsetNumber);
|
||||
if (isSet('PLUGIN_AUTH')) {
|
||||
// TODO: read this from parameters
|
||||
packet.writeNullTerminatedString('mysql_native_password', 'latin1');
|
||||
}
|
||||
if (isSet('CONNECT_ATTRS')) {
|
||||
const connectAttributes = this.connectAttributes;
|
||||
const attrNames = Object.keys(connectAttributes);
|
||||
let keysLength = 0;
|
||||
for (let k = 0; k < attrNames.length; ++k) {
|
||||
keysLength += Packet.lengthCodedStringLength(attrNames[k], encoding);
|
||||
keysLength += Packet.lengthCodedStringLength(
|
||||
connectAttributes[attrNames[k]],
|
||||
encoding
|
||||
);
|
||||
}
|
||||
packet.writeLengthCodedNumber(keysLength);
|
||||
for (let k = 0; k < attrNames.length; ++k) {
|
||||
packet.writeLengthCodedString(attrNames[k], encoding);
|
||||
packet.writeLengthCodedString(
|
||||
connectAttributes[attrNames[k]],
|
||||
encoding
|
||||
);
|
||||
}
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
if (typeof this.user !== 'string') {
|
||||
throw new Error('"user" connection config property must be a string');
|
||||
}
|
||||
if (typeof this.database !== 'string') {
|
||||
throw new Error('"database" connection config property must be a string');
|
||||
}
|
||||
// dry run: calculate resulting packet length
|
||||
const p = this.serializeToBuffer(Packet.MockBuffer());
|
||||
return this.serializeToBuffer(Buffer.allocUnsafe(p.offset));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ChangeUser;
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
const CommandCodes = require('../constants/commands');
|
||||
|
||||
class CloseStatement {
|
||||
constructor(id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
// note: no response sent back
|
||||
toPacket() {
|
||||
const packet = new Packet(0, Buffer.allocUnsafe(9), 0, 9);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCodes.STMT_CLOSE);
|
||||
packet.writeInt32(this.id);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CloseStatement;
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
const StringParser = require('../parsers/string');
|
||||
const CharsetToEncoding = require('../constants/charset_encodings.js');
|
||||
|
||||
const fields = ['catalog', 'schema', 'table', 'orgTable', 'name', 'orgName'];
|
||||
|
||||
// creating JS string is relatively expensive (compared to
|
||||
// reading few bytes from buffer) because all string properties
|
||||
// except for name are unlikely to be used we postpone
|
||||
// string conversion until property access
|
||||
//
|
||||
// TODO: watch for integration benchmarks (one with real network buffer)
|
||||
// there could be bad side effect as keeping reference to a buffer makes it
|
||||
// sit in the memory longer (usually until final .query() callback)
|
||||
// Latest v8 perform much better in regard to bufferer -> string conversion,
|
||||
// at some point of time this optimisation might become unnecessary
|
||||
// see https://github.com/sidorares/node-mysql2/pull/137
|
||||
//
|
||||
class ColumnDefinition {
|
||||
constructor(packet, clientEncoding, mariadbExtendedMetadata) {
|
||||
this._buf = packet.buffer;
|
||||
this._clientEncoding = clientEncoding;
|
||||
this._catalogLength = packet.readLengthCodedNumber();
|
||||
this._catalogStart = packet.offset;
|
||||
packet.offset += this._catalogLength;
|
||||
this._schemaLength = packet.readLengthCodedNumber();
|
||||
this._schemaStart = packet.offset;
|
||||
packet.offset += this._schemaLength;
|
||||
this._tableLength = packet.readLengthCodedNumber();
|
||||
this._tableStart = packet.offset;
|
||||
packet.offset += this._tableLength;
|
||||
this._orgTableLength = packet.readLengthCodedNumber();
|
||||
this._orgTableStart = packet.offset;
|
||||
packet.offset += this._orgTableLength;
|
||||
// name is always used, don't make it lazy
|
||||
const _nameLength = packet.readLengthCodedNumber();
|
||||
const _nameStart = packet.offset;
|
||||
packet.offset += _nameLength;
|
||||
this._orgNameLength = packet.readLengthCodedNumber();
|
||||
this._orgNameStart = packet.offset;
|
||||
packet.offset += this._orgNameLength;
|
||||
// MariaDB-specific column types (UUID, INET4, INET6, VECTOR and the JSON
|
||||
// alias) are sent as standard string/blob types on the wire; the real
|
||||
// type is only visible here, in the extended metadata block negotiated
|
||||
// via the MARIADB_CLIENT_EXTENDED_METADATA capability (parsed out of
|
||||
// line and with prototype defaults to keep this hot constructor small)
|
||||
if (mariadbExtendedMetadata) {
|
||||
this._parseMariadbExtendedMetadata(packet);
|
||||
}
|
||||
packet.skip(1); // length of the following fields (always 0x0c)
|
||||
this.characterSet = packet.readInt16();
|
||||
this.encoding = CharsetToEncoding[this.characterSet];
|
||||
this.name = StringParser.decode(
|
||||
this._buf,
|
||||
this.encoding === 'binary' ? this._clientEncoding : this.encoding,
|
||||
_nameStart,
|
||||
_nameStart + _nameLength
|
||||
);
|
||||
this.columnLength = packet.readInt32();
|
||||
this.columnType = packet.readInt8();
|
||||
this.type = this.columnType;
|
||||
this.flags = packet.readInt16();
|
||||
this.decimals = packet.readInt8();
|
||||
}
|
||||
|
||||
_parseMariadbExtendedMetadata(packet) {
|
||||
// length-encoded block of { int<1> id, string<lenenc> value } pairs
|
||||
const extendedMetadataLength = packet.readLengthCodedNumber() || 0;
|
||||
const extendedMetadataEnd = Math.min(
|
||||
packet.offset + extendedMetadataLength,
|
||||
packet.end
|
||||
);
|
||||
while (packet.offset < extendedMetadataEnd) {
|
||||
const id = packet.readInt8();
|
||||
const value = packet.readLengthCodedString('ascii');
|
||||
if (id === 0) {
|
||||
this.extendedTypeName = value; // e.g. 'uuid', 'inet4', 'inet6', 'point'
|
||||
} else if (id === 1) {
|
||||
this.extendedFormat = value; // e.g. 'json'
|
||||
}
|
||||
}
|
||||
// a malformed entry could read past the block; resynchronise so the
|
||||
// fixed column definition fields that follow parse from the right offset
|
||||
packet.offset = extendedMetadataEnd;
|
||||
}
|
||||
|
||||
inspect() {
|
||||
return {
|
||||
catalog: this.catalog,
|
||||
schema: this.schema,
|
||||
name: this.name,
|
||||
orgName: this.orgName,
|
||||
table: this.table,
|
||||
orgTable: this.orgTable,
|
||||
characterSet: this.characterSet,
|
||||
encoding: this.encoding,
|
||||
columnLength: this.columnLength,
|
||||
type: this.columnType,
|
||||
flags: this.flags,
|
||||
decimals: this.decimals,
|
||||
};
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')](depth, inspectOptions, inspect) {
|
||||
const Types = require('../constants/types.js');
|
||||
const typeNames = [];
|
||||
for (const t in Types) {
|
||||
typeNames[Types[t]] = t;
|
||||
}
|
||||
const fiedFlags = require('../constants/field_flags.js');
|
||||
const flagNames = [];
|
||||
// TODO: respect inspectOptions.showHidden
|
||||
//const inspectFlags = inspectOptions.showHidden ? this.flags : this.flags & ~fiedFlags.PRI_KEY;
|
||||
const inspectFlags = this.flags;
|
||||
for (const f in fiedFlags) {
|
||||
if (inspectFlags & fiedFlags[f]) {
|
||||
if (f === 'PRI_KEY') {
|
||||
flagNames.push('PRIMARY KEY');
|
||||
} else if (f === 'NOT_NULL') {
|
||||
flagNames.push('NOT NULL');
|
||||
} else if (f === 'BINARY') {
|
||||
// ignore flag for now
|
||||
} else if (f === 'MULTIPLE_KEY') {
|
||||
// not sure if that should be part of inspection.
|
||||
// in the schema usually this is part of index definition
|
||||
// example: UNIQUE KEY `my_uniq_id` (`id_box_elements`,`id_router`)
|
||||
// note that only first column has MULTIPLE_KEY flag set in this case
|
||||
// so there is no good way of knowing that this is part of index just
|
||||
// by looking at indifidual field flags
|
||||
} else if (f === 'NO_DEFAULT_VALUE') {
|
||||
// almost the same as NOT_NULL?
|
||||
} else if (f === 'BLOB') {
|
||||
// included in the type
|
||||
} else if (f === 'UNSIGNED') {
|
||||
// this should be first after type
|
||||
} else if (f === 'TIMESTAMP') {
|
||||
// timestamp flag is redundant for inspection - already included in type
|
||||
} else if (f === 'ON_UPDATE_NOW') {
|
||||
flagNames.push('ON UPDATE CURRENT_TIMESTAMP');
|
||||
} else {
|
||||
flagNames.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (depth > 1) {
|
||||
return inspect({
|
||||
...this.inspect(),
|
||||
typeName: typeNames[this.columnType],
|
||||
flags: flagNames,
|
||||
});
|
||||
}
|
||||
|
||||
const isUnsigned = this.flags & fiedFlags.UNSIGNED;
|
||||
|
||||
let typeName = typeNames[this.columnType];
|
||||
if (typeName === 'BLOB') {
|
||||
// TODO: check for non-utf8mb4 encoding
|
||||
if (this.columnLength === 4294967295) {
|
||||
typeName = 'LONGTEXT';
|
||||
} else if (this.columnLength === 67108860) {
|
||||
typeName = 'MEDIUMTEXT';
|
||||
} else if (this.columnLength === 262140) {
|
||||
typeName = 'TEXT';
|
||||
} else if (this.columnLength === 1020) {
|
||||
// 255*4
|
||||
typeName = 'TINYTEXT';
|
||||
} else {
|
||||
typeName = `BLOB(${this.columnLength})`;
|
||||
}
|
||||
} else if (typeName === 'VAR_STRING') {
|
||||
// TODO: check for non-utf8mb4 encoding
|
||||
typeName = `VARCHAR(${Math.ceil(this.columnLength / 4)})`;
|
||||
} else if (typeName === 'TINY') {
|
||||
if (
|
||||
(this.columnLength === 3 && isUnsigned) ||
|
||||
(this.columnLength === 4 && !isUnsigned)
|
||||
) {
|
||||
typeName = 'TINYINT';
|
||||
} else {
|
||||
typeName = `TINYINT(${this.columnLength})`;
|
||||
}
|
||||
} else if (typeName === 'LONGLONG') {
|
||||
if (this.columnLength === 20) {
|
||||
typeName = 'BIGINT';
|
||||
} else {
|
||||
typeName = `BIGINT(${this.columnLength})`;
|
||||
}
|
||||
} else if (typeName === 'SHORT') {
|
||||
if (isUnsigned && this.columnLength === 5) {
|
||||
typeName = 'SMALLINT';
|
||||
} else if (!isUnsigned && this.columnLength === 6) {
|
||||
typeName = 'SMALLINT';
|
||||
} else {
|
||||
typeName = `SMALLINT(${this.columnLength})`;
|
||||
}
|
||||
} else if (typeName === 'LONG') {
|
||||
if (isUnsigned && this.columnLength === 10) {
|
||||
typeName = 'INT';
|
||||
} else if (!isUnsigned && this.columnLength === 11) {
|
||||
typeName = 'INT';
|
||||
} else {
|
||||
typeName = `INT(${this.columnLength})`;
|
||||
}
|
||||
} else if (typeName === 'INT24') {
|
||||
if (isUnsigned && this.columnLength === 8) {
|
||||
typeName = 'MEDIUMINT';
|
||||
} else if (!isUnsigned && this.columnLength === 9) {
|
||||
typeName = 'MEDIUMINT';
|
||||
} else {
|
||||
typeName = `MEDIUMINT(${this.columnLength})`;
|
||||
}
|
||||
} else if (typeName === 'DOUBLE') {
|
||||
// DOUBLE without modifiers is reported as DOUBLE(22, 31)
|
||||
if (this.columnLength === 22 && this.decimals === 31) {
|
||||
typeName = 'DOUBLE';
|
||||
} else {
|
||||
typeName = `DOUBLE(${this.columnLength},${this.decimals})`;
|
||||
}
|
||||
} else if (typeName === 'FLOAT') {
|
||||
// FLOAT without modifiers is reported as FLOAT(12, 31)
|
||||
if (this.columnLength === 12 && this.decimals === 31) {
|
||||
typeName = 'FLOAT';
|
||||
} else {
|
||||
typeName = `FLOAT(${this.columnLength},${this.decimals})`;
|
||||
}
|
||||
} else if (typeName === 'NEWDECIMAL') {
|
||||
if (this.columnLength === 11 && this.decimals === 0) {
|
||||
typeName = 'DECIMAL';
|
||||
} else if (this.decimals === 0) {
|
||||
// not sure why, but DECIMAL(13) is reported as DECIMAL(14, 0)
|
||||
// and DECIMAL(13, 9) is reported as NEWDECIMAL(15, 9)
|
||||
if (isUnsigned) {
|
||||
typeName = `DECIMAL(${this.columnLength})`;
|
||||
} else {
|
||||
typeName = `DECIMAL(${this.columnLength - 1})`;
|
||||
}
|
||||
} else {
|
||||
typeName = `DECIMAL(${this.columnLength - 2},${this.decimals})`;
|
||||
}
|
||||
} else {
|
||||
typeName = `${typeNames[this.columnType]}(${this.columnLength})`;
|
||||
}
|
||||
|
||||
if (isUnsigned) {
|
||||
typeName += ' UNSIGNED';
|
||||
}
|
||||
|
||||
// TODO respect colors option
|
||||
return `\`${this.name}\` ${[typeName, ...flagNames].join(' ')}`;
|
||||
}
|
||||
|
||||
static toPacket(column, sequenceId) {
|
||||
let length = 17; // = 4 padding + 1 + 12 for the rest
|
||||
fields.forEach((field) => {
|
||||
length += Packet.lengthCodedStringLength(
|
||||
column[field],
|
||||
CharsetToEncoding[column.characterSet]
|
||||
);
|
||||
});
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
|
||||
const packet = new Packet(sequenceId, buffer, 0, length);
|
||||
function writeField(name) {
|
||||
packet.writeLengthCodedString(
|
||||
column[name],
|
||||
CharsetToEncoding[column.characterSet]
|
||||
);
|
||||
}
|
||||
packet.offset = 4;
|
||||
fields.forEach(writeField);
|
||||
packet.writeInt8(0x0c);
|
||||
packet.writeInt16(column.characterSet);
|
||||
packet.writeInt32(column.columnLength);
|
||||
packet.writeInt8(column.columnType);
|
||||
packet.writeInt16(column.flags);
|
||||
packet.writeInt8(column.decimals);
|
||||
packet.writeInt16(0); // filler
|
||||
return packet;
|
||||
}
|
||||
|
||||
// node-mysql compatibility: alias "db" to "schema"
|
||||
get db() {
|
||||
return this.schema;
|
||||
}
|
||||
}
|
||||
|
||||
const addString = function (name) {
|
||||
Object.defineProperty(ColumnDefinition.prototype, name, {
|
||||
get: function () {
|
||||
const start = this[`_${name}Start`];
|
||||
const end = start + this[`_${name}Length`];
|
||||
const val = StringParser.decode(
|
||||
this._buf,
|
||||
this.encoding === 'binary' ? this._clientEncoding : this.encoding,
|
||||
start,
|
||||
end
|
||||
);
|
||||
|
||||
Object.defineProperty(this, name, {
|
||||
value: val,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
return val;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
addString('catalog');
|
||||
addString('schema');
|
||||
addString('table');
|
||||
addString('orgTable');
|
||||
addString('orgName');
|
||||
|
||||
// MariaDB extended metadata defaults; instances only carry own values when
|
||||
// the server actually tagged the column
|
||||
ColumnDefinition.prototype.extendedTypeName = undefined;
|
||||
ColumnDefinition.prototype.extendedFormat = undefined;
|
||||
|
||||
module.exports = ColumnDefinition;
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
const Types = require('../constants/types');
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
function isJSON(value) {
|
||||
return (
|
||||
Array.isArray(value) ||
|
||||
value.constructor === Object ||
|
||||
(typeof value.toJSON === 'function' && !Buffer.isBuffer(value))
|
||||
);
|
||||
}
|
||||
|
||||
function toParameter(value, encoding, timezone, jsonAsString) {
|
||||
let type = Types.VAR_STRING;
|
||||
let length;
|
||||
let writer = function (value) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
return Packet.prototype.writeLengthCodedString.call(this, value, encoding);
|
||||
};
|
||||
if (value !== null) {
|
||||
switch (typeof value) {
|
||||
case 'undefined':
|
||||
throw new TypeError('Bind parameters must not contain undefined');
|
||||
|
||||
case 'number':
|
||||
type = Types.DOUBLE;
|
||||
length = 8;
|
||||
writer = Packet.prototype.writeDouble;
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
value = value | 0;
|
||||
type = Types.TINY;
|
||||
length = 1;
|
||||
writer = Packet.prototype.writeInt8;
|
||||
break;
|
||||
|
||||
case 'object':
|
||||
if (Object.prototype.toString.call(value) === '[object Date]') {
|
||||
type = Types.DATETIME;
|
||||
length = 12;
|
||||
writer = function (value) {
|
||||
// eslint-disable-next-line no-invalid-this
|
||||
return Packet.prototype.writeDate.call(this, value, timezone);
|
||||
};
|
||||
} else if (isJSON(value)) {
|
||||
value = JSON.stringify(value);
|
||||
// MariaDB rejects the JSON parameter type with "Incorrect
|
||||
// arguments to mysqld_stmt_execute"; it expects JSON values
|
||||
// as plain strings
|
||||
if (!jsonAsString) {
|
||||
type = Types.JSON;
|
||||
}
|
||||
} else if (Buffer.isBuffer(value)) {
|
||||
// send buffers as BLOB so servers treat the value as binary data
|
||||
// rather than a string in the connection charset (MariaDB converts
|
||||
// string parameters when storing into binary columns such as
|
||||
// VECTOR, corrupting the value)
|
||||
type = Types.BLOB;
|
||||
length = Packet.lengthCodedNumberLength(value.length) + value.length;
|
||||
writer = Packet.prototype.writeLengthCodedBuffer;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
value = value.toString();
|
||||
}
|
||||
} else {
|
||||
value = '';
|
||||
type = Types.NULL;
|
||||
}
|
||||
if (!length) {
|
||||
length = Packet.lengthCodedStringLength(value, encoding);
|
||||
}
|
||||
return { value, type, length, writer };
|
||||
}
|
||||
|
||||
module.exports = { toParameter, isJSON };
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
'use strict';
|
||||
|
||||
const CursorType = require('../constants/cursor');
|
||||
const CommandCodes = require('../constants/commands');
|
||||
const ClientConstants = require('../constants/client');
|
||||
const Types = require('../constants/types');
|
||||
const Packet = require('../packets/packet');
|
||||
const CharsetToEncoding = require('../constants/charset_encodings.js');
|
||||
const { toParameter } = require('./encode_parameter.js');
|
||||
|
||||
class Execute {
|
||||
constructor(
|
||||
id,
|
||||
parameters,
|
||||
charsetNumber,
|
||||
timezone,
|
||||
attributes,
|
||||
clientFlags,
|
||||
jsonAsString
|
||||
) {
|
||||
this.id = id;
|
||||
this.parameters = parameters;
|
||||
this.encoding = CharsetToEncoding[charsetNumber];
|
||||
this.timezone = timezone;
|
||||
this.attributes = attributes;
|
||||
this.clientFlags = clientFlags || 0;
|
||||
this.jsonAsString = jsonAsString || false;
|
||||
}
|
||||
|
||||
static fromPacket(packet, encoding) {
|
||||
const stmtId = packet.readInt32();
|
||||
const flags = packet.readInt8();
|
||||
const iterationCount = packet.readInt32();
|
||||
|
||||
let i = packet.offset;
|
||||
while (i < packet.end - 1) {
|
||||
if (
|
||||
(packet.buffer[i + 1] === Types.VAR_STRING ||
|
||||
packet.buffer[i + 1] === Types.BLOB ||
|
||||
packet.buffer[i + 1] === Types.NULL ||
|
||||
packet.buffer[i + 1] === Types.DOUBLE ||
|
||||
packet.buffer[i + 1] === Types.TINY ||
|
||||
packet.buffer[i + 1] === Types.DATETIME ||
|
||||
packet.buffer[i + 1] === Types.JSON) &&
|
||||
packet.buffer[i] === 1 &&
|
||||
packet.buffer[i + 2] === 0
|
||||
) {
|
||||
break;
|
||||
} else {
|
||||
packet.readInt8();
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
const types = [];
|
||||
|
||||
for (let i = packet.offset + 1; i < packet.end - 1; i++) {
|
||||
if (
|
||||
(packet.buffer[i] === Types.VAR_STRING ||
|
||||
packet.buffer[i] === Types.BLOB ||
|
||||
packet.buffer[i] === Types.NULL ||
|
||||
packet.buffer[i] === Types.DOUBLE ||
|
||||
packet.buffer[i] === Types.TINY ||
|
||||
packet.buffer[i] === Types.DATETIME ||
|
||||
packet.buffer[i] === Types.JSON) &&
|
||||
packet.buffer[i + 1] === 0
|
||||
) {
|
||||
types.push(packet.buffer[i]);
|
||||
packet.skip(2);
|
||||
}
|
||||
}
|
||||
|
||||
packet.skip(1);
|
||||
|
||||
const values = [];
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
if (types[i] === Types.VAR_STRING) {
|
||||
values.push(packet.readLengthCodedString(encoding));
|
||||
} else if (types[i] === Types.BLOB) {
|
||||
values.push(packet.readLengthCodedBuffer());
|
||||
} else if (types[i] === Types.DOUBLE) {
|
||||
values.push(packet.readDouble());
|
||||
} else if (types[i] === Types.TINY) {
|
||||
values.push(packet.readInt8());
|
||||
} else if (types[i] === Types.DATETIME) {
|
||||
values.push(packet.readDateTime());
|
||||
} else if (types[i] === Types.JSON) {
|
||||
values.push(JSON.parse(packet.readLengthCodedString(encoding)));
|
||||
}
|
||||
if (types[i] === Types.NULL) {
|
||||
values.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
return { stmtId, flags, iterationCount, values };
|
||||
}
|
||||
|
||||
_serializeToBuffer(buffer) {
|
||||
const useQueryAttributes =
|
||||
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
|
||||
|
||||
const attrNames =
|
||||
useQueryAttributes && this.attributes ? Object.keys(this.attributes) : [];
|
||||
const numParams = this.parameters ? this.parameters.length : 0;
|
||||
const numAttrs = attrNames.length;
|
||||
const totalParams = numParams + numAttrs;
|
||||
|
||||
const packet = new Packet(0, buffer, 0, buffer.length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCodes.STMT_EXECUTE);
|
||||
packet.writeInt32(this.id);
|
||||
|
||||
let cursorFlags = CursorType.NO_CURSOR;
|
||||
if (useQueryAttributes) {
|
||||
cursorFlags |= CursorType.PARAMETER_COUNT_AVAILABLE;
|
||||
}
|
||||
packet.writeInt8(cursorFlags);
|
||||
packet.writeInt32(1); // iteration-count, always 1
|
||||
|
||||
if (useQueryAttributes) {
|
||||
packet.writeLengthCodedNumber(totalParams);
|
||||
}
|
||||
|
||||
if (totalParams > 0) {
|
||||
const bindParams =
|
||||
numParams > 0
|
||||
? this.parameters.map((v) =>
|
||||
toParameter(v, this.encoding, this.timezone, this.jsonAsString)
|
||||
)
|
||||
: [];
|
||||
const attrParams = attrNames.map((name) =>
|
||||
toParameter(this.attributes[name], this.encoding, this.timezone)
|
||||
);
|
||||
const allParams = bindParams.concat(attrParams);
|
||||
|
||||
// null bitmap
|
||||
let bitmap = 0;
|
||||
let bitValue = 1;
|
||||
allParams.forEach((parameter) => {
|
||||
if (parameter.type === Types.NULL) {
|
||||
bitmap += bitValue;
|
||||
}
|
||||
bitValue *= 2;
|
||||
if (bitValue === 256) {
|
||||
packet.writeInt8(bitmap);
|
||||
bitmap = 0;
|
||||
bitValue = 1;
|
||||
}
|
||||
});
|
||||
if (bitValue !== 1) {
|
||||
packet.writeInt8(bitmap);
|
||||
}
|
||||
|
||||
packet.writeInt8(1); // new-params-bound-flag
|
||||
|
||||
// types (and names for attributes)
|
||||
for (let i = 0; i < allParams.length; i++) {
|
||||
packet.writeInt8(allParams[i].type);
|
||||
packet.writeInt8(0); // unsigned flag
|
||||
if (useQueryAttributes) {
|
||||
const name = i < numParams ? '' : attrNames[i - numParams];
|
||||
packet.writeLengthCodedString(name, this.encoding);
|
||||
}
|
||||
}
|
||||
|
||||
// values
|
||||
allParams.forEach((parameter) => {
|
||||
if (parameter.type !== Types.NULL) {
|
||||
parameter.writer.call(packet, parameter.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return packet;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const p = this._serializeToBuffer(Packet.MockBuffer());
|
||||
return this._serializeToBuffer(Buffer.allocUnsafe(p.offset));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Execute;
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
const ClientConstants = require('../constants/client.js');
|
||||
|
||||
// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
|
||||
|
||||
class Handshake {
|
||||
constructor(args) {
|
||||
this.protocolVersion = args.protocolVersion;
|
||||
this.serverVersion = args.serverVersion;
|
||||
this.capabilityFlags = args.capabilityFlags;
|
||||
this.connectionId = args.connectionId;
|
||||
this.authPluginData1 = args.authPluginData1;
|
||||
this.authPluginData2 = args.authPluginData2;
|
||||
this.characterSet = args.characterSet;
|
||||
this.statusFlags = args.statusFlags;
|
||||
this.authPluginName = args.authPluginName;
|
||||
this.mariadbExtendedCapabilityFlags = args.mariadbExtendedCapabilityFlags;
|
||||
}
|
||||
|
||||
setScrambleData(cb) {
|
||||
require('crypto').randomBytes(20, (err, data) => {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
this.authPluginData1 = data.slice(0, 8);
|
||||
this.authPluginData2 = data.slice(8, 20);
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
toPacket(sequenceId) {
|
||||
const length = 68 + Buffer.byteLength(this.serverVersion, 'utf8');
|
||||
const buffer = Buffer.alloc(length + 4, 0); // zero fill, 10 bytes filler later needs to contain zeros
|
||||
const packet = new Packet(sequenceId, buffer, 0, length + 4);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(this.protocolVersion);
|
||||
packet.writeString(this.serverVersion, 'cesu8');
|
||||
packet.writeInt8(0);
|
||||
packet.writeInt32(this.connectionId);
|
||||
packet.writeBuffer(this.authPluginData1);
|
||||
packet.writeInt8(0);
|
||||
const capabilityFlagsBuffer = Buffer.allocUnsafe(4);
|
||||
capabilityFlagsBuffer.writeUInt32LE(this.capabilityFlags, 0);
|
||||
packet.writeBuffer(capabilityFlagsBuffer.slice(0, 2));
|
||||
packet.writeInt8(this.characterSet);
|
||||
packet.writeInt16(this.statusFlags);
|
||||
packet.writeBuffer(capabilityFlagsBuffer.slice(2, 4));
|
||||
packet.writeInt8(21); // authPluginDataLength
|
||||
packet.skip(10);
|
||||
packet.writeBuffer(this.authPluginData2);
|
||||
packet.writeInt8(0);
|
||||
packet.writeString('mysql_native_password', 'latin1');
|
||||
packet.writeInt8(0);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static fromPacket(packet) {
|
||||
const args = {};
|
||||
args.protocolVersion = packet.readInt8();
|
||||
args.serverVersion = packet.readNullTerminatedString('cesu8');
|
||||
args.connectionId = packet.readInt32();
|
||||
args.authPluginData1 = packet.readBuffer(8);
|
||||
packet.skip(1);
|
||||
const capabilityFlagsBuffer = Buffer.allocUnsafe(4);
|
||||
capabilityFlagsBuffer[0] = packet.readInt8();
|
||||
capabilityFlagsBuffer[1] = packet.readInt8();
|
||||
if (packet.haveMoreData()) {
|
||||
args.characterSet = packet.readInt8();
|
||||
args.statusFlags = packet.readInt16();
|
||||
// upper 2 bytes
|
||||
capabilityFlagsBuffer[2] = packet.readInt8();
|
||||
capabilityFlagsBuffer[3] = packet.readInt8();
|
||||
args.capabilityFlags = capabilityFlagsBuffer.readUInt32LE(0);
|
||||
if (args.capabilityFlags & ClientConstants.PLUGIN_AUTH) {
|
||||
args.authPluginDataLength = packet.readInt8();
|
||||
} else {
|
||||
args.authPluginDataLength = 0;
|
||||
packet.skip(1);
|
||||
}
|
||||
packet.skip(6);
|
||||
if (args.capabilityFlags & ClientConstants.LONG_PASSWORD) {
|
||||
// CLIENT_MYSQL (LONG_PASSWORD) set: a MySQL server, the remaining
|
||||
// 4 reserved bytes are filler
|
||||
packet.skip(4);
|
||||
args.mariadbExtendedCapabilityFlags = 0;
|
||||
} else {
|
||||
// MariaDB servers (10.2+) leave CLIENT_MYSQL unset and use these
|
||||
// 4 bytes to advertise MariaDB-specific capabilities
|
||||
args.mariadbExtendedCapabilityFlags = packet.readInt32();
|
||||
}
|
||||
} else {
|
||||
args.capabilityFlags = capabilityFlagsBuffer.readUInt16LE(0);
|
||||
args.mariadbExtendedCapabilityFlags = 0;
|
||||
}
|
||||
|
||||
const isSecureConnection =
|
||||
args.capabilityFlags & ClientConstants.SECURE_CONNECTION;
|
||||
if (isSecureConnection) {
|
||||
const authPluginDataLength = args.authPluginDataLength;
|
||||
if (authPluginDataLength === 0) {
|
||||
// for Secure Password Authentication
|
||||
args.authPluginDataLength = 20;
|
||||
args.authPluginData2 = packet.readBuffer(12);
|
||||
packet.skip(1);
|
||||
} else {
|
||||
// length > 0
|
||||
// for Custom Auth Plugin (PLUGIN_AUTH)
|
||||
const len = Math.max(13, authPluginDataLength - 8);
|
||||
args.authPluginData2 = packet.readBuffer(len);
|
||||
}
|
||||
}
|
||||
|
||||
if (args.capabilityFlags & ClientConstants.PLUGIN_AUTH) {
|
||||
args.authPluginName = packet.readNullTerminatedString('ascii');
|
||||
}
|
||||
|
||||
return new Handshake(args);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Handshake;
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
'use strict';
|
||||
|
||||
const ClientConstants = require('../constants/client.js');
|
||||
const CharsetToEncoding = require('../constants/charset_encodings.js');
|
||||
const Packet = require('../packets/packet.js');
|
||||
|
||||
const auth41 = require('../auth_41.js');
|
||||
|
||||
class HandshakeResponse {
|
||||
constructor(handshake) {
|
||||
this.user = handshake.user || '';
|
||||
this.database = handshake.database || '';
|
||||
this.password = handshake.password || '';
|
||||
this.passwordSha1 = handshake.passwordSha1;
|
||||
this.authPluginData1 = handshake.authPluginData1;
|
||||
this.authPluginData2 = handshake.authPluginData2;
|
||||
this.compress = handshake.compress;
|
||||
this.clientFlags = handshake.flags;
|
||||
this.mariadbExtendedClientFlags = handshake.mariadbExtendedClientFlags || 0;
|
||||
|
||||
// Accept pre-calculated authToken and authPluginName from caller
|
||||
// This allows the caller to optimize by using the server's preferred auth method
|
||||
if (
|
||||
handshake.authToken !== undefined &&
|
||||
handshake.authPluginName !== undefined
|
||||
) {
|
||||
// Validate types to fail fast with clear errors
|
||||
if (!Buffer.isBuffer(handshake.authToken)) {
|
||||
throw new TypeError(
|
||||
'HandshakeResponse authToken must be a Buffer when provided'
|
||||
);
|
||||
}
|
||||
if (typeof handshake.authPluginName !== 'string') {
|
||||
throw new TypeError(
|
||||
'HandshakeResponse authPluginName must be a string when provided'
|
||||
);
|
||||
}
|
||||
this.authToken = handshake.authToken;
|
||||
this.authPluginName = handshake.authPluginName;
|
||||
} else {
|
||||
// Fallback to legacy behavior: calculate mysql_native_password token
|
||||
// TODO: pre-4.1 auth support
|
||||
let authToken;
|
||||
if (this.passwordSha1) {
|
||||
authToken = auth41.calculateTokenFromPasswordSha(
|
||||
this.passwordSha1,
|
||||
this.authPluginData1,
|
||||
this.authPluginData2
|
||||
);
|
||||
} else {
|
||||
authToken = auth41.calculateToken(
|
||||
this.password,
|
||||
this.authPluginData1,
|
||||
this.authPluginData2
|
||||
);
|
||||
}
|
||||
this.authToken = authToken;
|
||||
this.authPluginName = 'mysql_native_password';
|
||||
}
|
||||
|
||||
this.charsetNumber = handshake.charsetNumber;
|
||||
this.encoding = CharsetToEncoding[handshake.charsetNumber];
|
||||
this.connectAttributes = handshake.connectAttributes;
|
||||
}
|
||||
|
||||
serializeResponse(buffer) {
|
||||
const isSet = (flag) => this.clientFlags & ClientConstants[flag];
|
||||
const packet = new Packet(0, buffer, 0, buffer.length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt32(this.clientFlags);
|
||||
packet.writeInt32(0); // max packet size. todo: move to config
|
||||
packet.writeInt8(this.charsetNumber);
|
||||
// the last 4 of the 23 reserved bytes carry the MariaDB extended client
|
||||
// capabilities (zero when not negotiated, i.e. plain filler)
|
||||
packet.skip(19);
|
||||
packet.writeInt32(this.mariadbExtendedClientFlags);
|
||||
const encoding = this.encoding;
|
||||
packet.writeNullTerminatedString(this.user, encoding);
|
||||
let k;
|
||||
if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
|
||||
packet.writeLengthCodedNumber(this.authToken.length);
|
||||
packet.writeBuffer(this.authToken);
|
||||
} else if (isSet('SECURE_CONNECTION')) {
|
||||
packet.writeInt8(this.authToken.length);
|
||||
packet.writeBuffer(this.authToken);
|
||||
} else {
|
||||
packet.writeBuffer(this.authToken);
|
||||
packet.writeInt8(0);
|
||||
}
|
||||
if (isSet('CONNECT_WITH_DB')) {
|
||||
packet.writeNullTerminatedString(this.database, encoding);
|
||||
}
|
||||
if (isSet('PLUGIN_AUTH')) {
|
||||
// Use the auth plugin name specified by the caller (optimized for server's preference)
|
||||
// or fall back to mysql_native_password for backward compatibility
|
||||
packet.writeNullTerminatedString(
|
||||
this.authPluginName || 'mysql_native_password',
|
||||
'latin1'
|
||||
);
|
||||
}
|
||||
if (isSet('CONNECT_ATTRS')) {
|
||||
const connectAttributes = this.connectAttributes || {};
|
||||
const attrNames = Object.keys(connectAttributes);
|
||||
let keysLength = 0;
|
||||
for (k = 0; k < attrNames.length; ++k) {
|
||||
keysLength += Packet.lengthCodedStringLength(attrNames[k], encoding);
|
||||
keysLength += Packet.lengthCodedStringLength(
|
||||
connectAttributes[attrNames[k]],
|
||||
encoding
|
||||
);
|
||||
}
|
||||
packet.writeLengthCodedNumber(keysLength);
|
||||
for (k = 0; k < attrNames.length; ++k) {
|
||||
packet.writeLengthCodedString(attrNames[k], encoding);
|
||||
packet.writeLengthCodedString(
|
||||
connectAttributes[attrNames[k]],
|
||||
encoding
|
||||
);
|
||||
}
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
if (typeof this.user !== 'string') {
|
||||
throw new Error('"user" connection config property must be a string');
|
||||
}
|
||||
if (typeof this.database !== 'string') {
|
||||
throw new Error('"database" connection config property must be a string');
|
||||
}
|
||||
// dry run: calculate resulting packet length
|
||||
const p = this.serializeResponse(Packet.MockBuffer());
|
||||
return this.serializeResponse(Buffer.alloc(p.offset));
|
||||
}
|
||||
static fromPacket(packet, serverFlags = 0xffffffff) {
|
||||
const args = {};
|
||||
args.clientFlags = packet.readInt32();
|
||||
function isSet(flag) {
|
||||
return args.clientFlags & serverFlags & ClientConstants[flag];
|
||||
}
|
||||
args.maxPacketSize = packet.readInt32();
|
||||
args.charsetNumber = packet.readInt8();
|
||||
const encoding = CharsetToEncoding[args.charsetNumber];
|
||||
args.encoding = encoding;
|
||||
packet.skip(23);
|
||||
args.user = packet.readNullTerminatedString(encoding);
|
||||
let authTokenLength;
|
||||
if (isSet('PLUGIN_AUTH_LENENC_CLIENT_DATA')) {
|
||||
authTokenLength = packet.readLengthCodedNumber(encoding);
|
||||
args.authToken = packet.readBuffer(authTokenLength);
|
||||
} else if (isSet('SECURE_CONNECTION')) {
|
||||
authTokenLength = packet.readInt8();
|
||||
args.authToken = packet.readBuffer(authTokenLength);
|
||||
} else {
|
||||
args.authToken = packet.readNullTerminatedString(encoding);
|
||||
}
|
||||
if (isSet('CONNECT_WITH_DB')) {
|
||||
args.database = packet.readNullTerminatedString(encoding);
|
||||
}
|
||||
if (isSet('PLUGIN_AUTH')) {
|
||||
args.authPluginName = packet.readNullTerminatedString(encoding);
|
||||
}
|
||||
if (isSet('CONNECT_ATTRS')) {
|
||||
const keysLength = packet.readLengthCodedNumber(encoding);
|
||||
const keysEnd = packet.offset + keysLength;
|
||||
const attrs = {};
|
||||
while (packet.offset < keysEnd) {
|
||||
attrs[packet.readLengthCodedString(encoding)] =
|
||||
packet.readLengthCodedString(encoding);
|
||||
}
|
||||
args.connectAttributes = attrs;
|
||||
}
|
||||
return args;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HandshakeResponse;
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// This file was modified by Oracle on June 1, 2021.
|
||||
// A utility method was introduced to generate an Error instance from a
|
||||
// binary server packet.
|
||||
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
|
||||
|
||||
// This file was modified by Oracle on September 21, 2021.
|
||||
// The new AuthNextFactor packet is now available.
|
||||
// Modifications copyright (c) 2021, Oracle and/or its affiliates.
|
||||
|
||||
'use strict';
|
||||
|
||||
const process = require('process');
|
||||
|
||||
const AuthNextFactor = require('./auth_next_factor');
|
||||
const AuthSwitchRequest = require('./auth_switch_request');
|
||||
const AuthSwitchRequestMoreData = require('./auth_switch_request_more_data');
|
||||
const AuthSwitchResponse = require('./auth_switch_response');
|
||||
const BinaryRow = require('./binary_row');
|
||||
const BinlogDump = require('./binlog_dump');
|
||||
const ChangeUser = require('./change_user');
|
||||
const CloseStatement = require('./close_statement');
|
||||
const ColumnDefinition = require('./column_definition');
|
||||
const Execute = require('./execute');
|
||||
const Handshake = require('./handshake');
|
||||
const HandshakeResponse = require('./handshake_response');
|
||||
const PrepareStatement = require('./prepare_statement');
|
||||
const PreparedStatementHeader = require('./prepared_statement_header');
|
||||
const Query = require('./query');
|
||||
const RegisterSlave = require('./register_slave');
|
||||
const ResetConnection = require('./reset_connection');
|
||||
const ResultSetHeader = require('./resultset_header');
|
||||
const SSLRequest = require('./ssl_request');
|
||||
const TextRow = require('./text_row');
|
||||
|
||||
const ctorMap = {
|
||||
AuthNextFactor,
|
||||
AuthSwitchRequest,
|
||||
AuthSwitchRequestMoreData,
|
||||
AuthSwitchResponse,
|
||||
BinaryRow,
|
||||
BinlogDump,
|
||||
ChangeUser,
|
||||
CloseStatement,
|
||||
ColumnDefinition,
|
||||
Execute,
|
||||
Handshake,
|
||||
HandshakeResponse,
|
||||
PrepareStatement,
|
||||
PreparedStatementHeader,
|
||||
Query,
|
||||
RegisterSlave,
|
||||
ResetConnection,
|
||||
ResultSetHeader,
|
||||
SSLRequest,
|
||||
TextRow,
|
||||
};
|
||||
Object.entries(ctorMap).forEach(([name, ctor]) => {
|
||||
module.exports[name] = ctor;
|
||||
// monkey-patch it to include name if debug is on
|
||||
if (process.env.NODE_DEBUG) {
|
||||
if (ctor.prototype.toPacket) {
|
||||
const old = ctor.prototype.toPacket;
|
||||
ctor.prototype.toPacket = function () {
|
||||
const p = old.call(this);
|
||||
p._name = name;
|
||||
return p;
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// simple packets:
|
||||
const Packet = require('./packet');
|
||||
exports.Packet = Packet;
|
||||
|
||||
class OK {
|
||||
static toPacket(args, encoding) {
|
||||
args = args || {};
|
||||
const affectedRows = args.affectedRows || 0;
|
||||
const insertId = args.insertId || 0;
|
||||
const serverStatus = args.serverStatus || 0;
|
||||
const warningCount = args.warningCount || 0;
|
||||
const message = args.message || '';
|
||||
|
||||
let length = 9 + Packet.lengthCodedNumberLength(affectedRows);
|
||||
length += Packet.lengthCodedNumberLength(insertId);
|
||||
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(0);
|
||||
packet.writeLengthCodedNumber(affectedRows);
|
||||
packet.writeLengthCodedNumber(insertId);
|
||||
packet.writeInt16(serverStatus);
|
||||
packet.writeInt16(warningCount);
|
||||
packet.writeString(message, encoding);
|
||||
packet._name = 'OK';
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
exports.OK = OK;
|
||||
|
||||
// warnings, statusFlags
|
||||
class EOF {
|
||||
static toPacket(warnings, statusFlags) {
|
||||
if (typeof warnings === 'undefined') {
|
||||
warnings = 0;
|
||||
}
|
||||
if (typeof statusFlags === 'undefined') {
|
||||
statusFlags = 0;
|
||||
}
|
||||
const packet = new Packet(0, Buffer.allocUnsafe(9), 0, 9);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(0xfe);
|
||||
packet.writeInt16(warnings);
|
||||
packet.writeInt16(statusFlags);
|
||||
packet._name = 'EOF';
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
exports.EOF = EOF;
|
||||
|
||||
class Error {
|
||||
static toPacket(args, encoding) {
|
||||
const length = 13 + Buffer.byteLength(args.message, 'utf8');
|
||||
const packet = new Packet(0, Buffer.allocUnsafe(length), 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(0xff);
|
||||
packet.writeInt16(args.code);
|
||||
// TODO: sql state parameter
|
||||
packet.writeString('#_____', encoding);
|
||||
packet.writeString(args.message, encoding);
|
||||
packet._name = 'Error';
|
||||
return packet;
|
||||
}
|
||||
|
||||
static fromPacket(packet) {
|
||||
packet.readInt8(); // marker
|
||||
const code = packet.readInt16();
|
||||
packet.readString(1, 'ascii'); // sql state marker
|
||||
// The SQL state of the ERR_Packet which is always 5 bytes long.
|
||||
// https://dev.mysql.com/doc/dev/mysql-server/8.0.11/page_protocol_basic_dt_strings.html#sect_protocol_basic_dt_string_fix
|
||||
packet.readString(5, 'ascii'); // sql state (ignore for now)
|
||||
const message = packet.readNullTerminatedString('utf8');
|
||||
const error = new Error();
|
||||
error.message = message;
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
exports.Error = Error;
|
||||
+1023
File diff suppressed because it is too large
Load Diff
+27
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
const CommandCodes = require('../constants/commands');
|
||||
const StringParser = require('../parsers/string.js');
|
||||
const CharsetToEncoding = require('../constants/charset_encodings.js');
|
||||
|
||||
class PrepareStatement {
|
||||
constructor(sql, charsetNumber) {
|
||||
this.query = sql;
|
||||
this.charsetNumber = charsetNumber;
|
||||
this.encoding = CharsetToEncoding[charsetNumber];
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const buf = StringParser.encode(this.query, this.encoding);
|
||||
const length = 5 + buf.length;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCodes.STMT_PREPARE);
|
||||
packet.writeBuffer(buf);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PrepareStatement;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
class PreparedStatementHeader {
|
||||
constructor(packet) {
|
||||
packet.skip(1); // should be 0
|
||||
this.id = packet.readInt32();
|
||||
this.fieldCount = packet.readInt16();
|
||||
this.parameterCount = packet.readInt16();
|
||||
packet.skip(1); // should be 0
|
||||
this.warningCount = packet.readInt16();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: toPacket
|
||||
|
||||
module.exports = PreparedStatementHeader;
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet.js');
|
||||
const CommandCode = require('../constants/commands.js');
|
||||
const StringParser = require('../parsers/string.js');
|
||||
const CharsetToEncoding = require('../constants/charset_encodings.js');
|
||||
const ClientConstants = require('../constants/client.js');
|
||||
const Types = require('../constants/types.js');
|
||||
const { toParameter } = require('./encode_parameter.js');
|
||||
|
||||
class Query {
|
||||
constructor(sql, charsetNumber, attributes, clientFlags) {
|
||||
this.query = sql;
|
||||
this.charsetNumber = charsetNumber;
|
||||
this.encoding = CharsetToEncoding[charsetNumber];
|
||||
this.attributes = attributes;
|
||||
this.clientFlags = clientFlags || 0;
|
||||
}
|
||||
|
||||
serializeToBuffer(buffer) {
|
||||
const useQueryAttributes =
|
||||
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
|
||||
const sqlBuf = StringParser.encode(this.query, this.encoding);
|
||||
const packet = new Packet(0, buffer, 0, buffer.length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCode.QUERY);
|
||||
|
||||
if (useQueryAttributes) {
|
||||
const attrs = this.attributes;
|
||||
const names = attrs ? Object.keys(attrs) : [];
|
||||
const paramCount = names.length;
|
||||
|
||||
packet.writeLengthCodedNumber(paramCount);
|
||||
packet.writeLengthCodedNumber(1); // parameter_set_count, always 1
|
||||
|
||||
if (paramCount > 0) {
|
||||
const parameters = names.map((name) =>
|
||||
toParameter(attrs[name], this.encoding, 'local')
|
||||
);
|
||||
|
||||
// null bitmap
|
||||
let bitmap = 0;
|
||||
let bitValue = 1;
|
||||
parameters.forEach((parameter) => {
|
||||
if (parameter.type === Types.NULL) {
|
||||
bitmap += bitValue;
|
||||
}
|
||||
bitValue *= 2;
|
||||
if (bitValue === 256) {
|
||||
packet.writeInt8(bitmap);
|
||||
bitmap = 0;
|
||||
bitValue = 1;
|
||||
}
|
||||
});
|
||||
if (bitValue !== 1) {
|
||||
packet.writeInt8(bitmap);
|
||||
}
|
||||
|
||||
packet.writeInt8(1); // new_params_bind_flag
|
||||
|
||||
// types and names
|
||||
for (let i = 0; i < paramCount; i++) {
|
||||
packet.writeInt8(parameters[i].type);
|
||||
packet.writeInt8(0); // unsigned flag
|
||||
packet.writeLengthCodedString(names[i], this.encoding);
|
||||
}
|
||||
|
||||
// values
|
||||
parameters.forEach((parameter) => {
|
||||
if (parameter.type !== Types.NULL) {
|
||||
parameter.writer.call(packet, parameter.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
packet.writeBuffer(sqlBuf);
|
||||
return packet;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const useQueryAttributes =
|
||||
this.clientFlags & ClientConstants.CLIENT_QUERY_ATTRIBUTES;
|
||||
|
||||
if (!useQueryAttributes) {
|
||||
const buf = StringParser.encode(this.query, this.encoding);
|
||||
const length = 5 + buf.length;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCode.QUERY);
|
||||
packet.writeBuffer(buf);
|
||||
return packet;
|
||||
}
|
||||
|
||||
// dry run to calculate required buffer length
|
||||
const p = this.serializeToBuffer(Packet.MockBuffer());
|
||||
return this.serializeToBuffer(Buffer.allocUnsafe(p.offset));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Query;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
// http://dev.mysql.com/doc/internals/en/com-register-slave.html
|
||||
// note that documentation is incorrect, for example command code is actually 0x15 but documented as 0x14
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
const CommandCodes = require('../constants/commands');
|
||||
|
||||
class RegisterSlave {
|
||||
constructor(opts) {
|
||||
this.serverId = opts.serverId || 0;
|
||||
this.slaveHostname = opts.slaveHostname || '';
|
||||
this.slaveUser = opts.slaveUser || '';
|
||||
this.slavePassword = opts.slavePassword || '';
|
||||
this.slavePort = opts.slavePort || 0;
|
||||
this.replicationRank = opts.replicationRank || 0;
|
||||
this.masterId = opts.masterId || 0;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const length =
|
||||
15 + // TODO: should be ascii?
|
||||
Buffer.byteLength(this.slaveHostname, 'utf8') +
|
||||
Buffer.byteLength(this.slaveUser, 'utf8') +
|
||||
Buffer.byteLength(this.slavePassword, 'utf8') +
|
||||
3 +
|
||||
4;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCodes.REGISTER_SLAVE);
|
||||
packet.writeInt32(this.serverId);
|
||||
packet.writeInt8(Buffer.byteLength(this.slaveHostname, 'utf8'));
|
||||
packet.writeString(this.slaveHostname);
|
||||
packet.writeInt8(Buffer.byteLength(this.slaveUser, 'utf8'));
|
||||
packet.writeString(this.slaveUser);
|
||||
packet.writeInt8(Buffer.byteLength(this.slavePassword, 'utf8'));
|
||||
packet.writeString(this.slavePassword);
|
||||
packet.writeInt16(this.slavePort);
|
||||
packet.writeInt32(this.replicationRank);
|
||||
packet.writeInt32(this.masterId);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RegisterSlave;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
const CommandCodes = require('../constants/commands');
|
||||
|
||||
class ResetConnection {
|
||||
constructor() {}
|
||||
|
||||
toPacket() {
|
||||
const packet = new Packet(0, Buffer.alloc(5), 0, 5);
|
||||
packet.offset = 4;
|
||||
packet.writeInt8(CommandCodes.RESET_CONNECTION);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResetConnection;
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
// TODO: rename to OK packet
|
||||
// https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
|
||||
|
||||
const Packet = require('./packet.js');
|
||||
const ClientConstants = require('../constants/client.js');
|
||||
const ServerSatusFlags = require('../constants/server_status.js');
|
||||
|
||||
const EncodingToCharset = require('../constants/encoding_charset.js');
|
||||
const sessionInfoTypes = require('../constants/session_track.js');
|
||||
|
||||
class ResultSetHeader {
|
||||
constructor(packet, connection) {
|
||||
const bigNumberStrings = connection.config.bigNumberStrings;
|
||||
const encoding = connection.serverEncoding;
|
||||
const flags = connection._handshakePacket.capabilityFlags;
|
||||
const isSet = function (flag) {
|
||||
return flags & ClientConstants[flag];
|
||||
};
|
||||
if (packet.buffer[packet.offset] !== 0) {
|
||||
this.fieldCount = packet.readLengthCodedNumber();
|
||||
if (this.fieldCount === null) {
|
||||
this.infileName = packet.readString(undefined, encoding);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.fieldCount = packet.readInt8(); // skip OK byte
|
||||
this.affectedRows = packet.readLengthCodedNumber(bigNumberStrings);
|
||||
this.insertId = packet.readLengthCodedNumberSigned(bigNumberStrings);
|
||||
this.info = '';
|
||||
if (isSet('PROTOCOL_41')) {
|
||||
this.serverStatus = packet.readInt16();
|
||||
this.warningStatus = packet.readInt16();
|
||||
} else if (isSet('TRANSACTIONS')) {
|
||||
this.serverStatus = packet.readInt16();
|
||||
}
|
||||
let stateChanges = null;
|
||||
if (isSet('SESSION_TRACK') && packet.offset < packet.end) {
|
||||
this.info = packet.readLengthCodedString(encoding);
|
||||
|
||||
if (this.serverStatus & ServerSatusFlags.SERVER_SESSION_STATE_CHANGED) {
|
||||
// session change info record - see
|
||||
// https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html#cs-sect-packet-ok-sessioninfo
|
||||
let len =
|
||||
packet.offset < packet.end ? packet.readLengthCodedNumber() : 0;
|
||||
const end = packet.offset + len;
|
||||
let type, key, stateEnd;
|
||||
if (len > 0) {
|
||||
stateChanges = {
|
||||
systemVariables: {},
|
||||
schema: null,
|
||||
gtids: [],
|
||||
trackStateChange: null,
|
||||
};
|
||||
}
|
||||
while (packet.offset < end) {
|
||||
type = packet.readInt8();
|
||||
len = packet.readLengthCodedNumber();
|
||||
stateEnd = packet.offset + len;
|
||||
if (type === sessionInfoTypes.SYSTEM_VARIABLES) {
|
||||
key = packet.readLengthCodedString(encoding);
|
||||
const val = packet.readLengthCodedString(encoding);
|
||||
stateChanges.systemVariables[key] = val;
|
||||
if (key === 'character_set_client') {
|
||||
const charsetNumber = EncodingToCharset[val];
|
||||
// TODO - better api for driver users to handle unknown encodings?
|
||||
// maybe custom converter in the config?
|
||||
// For now just ignore character_set_client command if there is
|
||||
// no known mapping from reported encoding to a charset code
|
||||
if (typeof charsetNumber !== 'undefined') {
|
||||
connection.config.charsetNumber = charsetNumber;
|
||||
}
|
||||
}
|
||||
} else if (type === sessionInfoTypes.SCHEMA) {
|
||||
key = packet.readLengthCodedString(encoding);
|
||||
stateChanges.schema = key;
|
||||
} else if (type === sessionInfoTypes.STATE_CHANGE) {
|
||||
stateChanges.trackStateChange =
|
||||
packet.readLengthCodedString(encoding);
|
||||
} else if (type === sessionInfoTypes.STATE_GTIDS) {
|
||||
// TODO: find if the first length coded string means anything. Usually comes as empty
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const _unknownString = packet.readLengthCodedString(encoding);
|
||||
const gtid = packet.readLengthCodedString(encoding);
|
||||
stateChanges.gtids = gtid.split(',');
|
||||
} else {
|
||||
// unsupported session track type. For now just ignore
|
||||
}
|
||||
packet.offset = stateEnd;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.info = packet.readString(undefined, encoding);
|
||||
}
|
||||
if (stateChanges) {
|
||||
this.stateChanges = stateChanges;
|
||||
}
|
||||
const m = this.info.match(/\schanged:\s*(\d+)/i);
|
||||
if (m !== null) {
|
||||
this.changedRows = parseInt(m[1], 10);
|
||||
} else {
|
||||
this.changedRows = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should be consistent instance member, but it's just easier here to have just function
|
||||
static toPacket(fieldCount, insertId) {
|
||||
let length = 4 + Packet.lengthCodedNumberLength(fieldCount);
|
||||
if (typeof insertId !== 'undefined') {
|
||||
length += Packet.lengthCodedNumberLength(insertId);
|
||||
}
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
packet.offset = 4;
|
||||
packet.writeLengthCodedNumber(fieldCount);
|
||||
if (typeof insertId !== 'undefined') {
|
||||
packet.writeLengthCodedNumber(insertId);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResultSetHeader;
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const ClientConstants = require('../constants/client');
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
class SSLRequest {
|
||||
constructor(flags, charset, mariadbExtendedClientFlags) {
|
||||
this.clientFlags = flags | ClientConstants.SSL;
|
||||
this.charset = charset;
|
||||
this.mariadbExtendedClientFlags = mariadbExtendedClientFlags || 0;
|
||||
}
|
||||
|
||||
toPacket() {
|
||||
const length = 36;
|
||||
const buffer = Buffer.allocUnsafe(length);
|
||||
const packet = new Packet(0, buffer, 0, length);
|
||||
buffer.fill(0);
|
||||
packet.offset = 4;
|
||||
packet.writeInt32(this.clientFlags);
|
||||
packet.writeInt32(0); // max packet size. todo: move to config
|
||||
packet.writeInt8(this.charset);
|
||||
// the last 4 of the 23 reserved bytes carry the MariaDB extended client
|
||||
// capabilities (zero when not negotiated, i.e. plain filler)
|
||||
packet.skip(19);
|
||||
packet.writeInt32(this.mariadbExtendedClientFlags);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SSLRequest;
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
|
||||
const Packet = require('../packets/packet');
|
||||
|
||||
class TextRow {
|
||||
constructor(columns) {
|
||||
this.columns = columns || [];
|
||||
}
|
||||
|
||||
static fromPacket(packet) {
|
||||
// packet.reset(); // set offset to starting point?
|
||||
const columns = [];
|
||||
while (packet.haveMoreData()) {
|
||||
columns.push(packet.readLengthCodedString());
|
||||
}
|
||||
return new TextRow(columns);
|
||||
}
|
||||
|
||||
static toPacket(columns, encoding) {
|
||||
const sequenceId = 0; // TODO remove, this is calculated now in connection
|
||||
let length = 0;
|
||||
columns.forEach((val) => {
|
||||
if (val === null || typeof val === 'undefined') {
|
||||
++length;
|
||||
return;
|
||||
}
|
||||
length += Packet.lengthCodedStringLength(val.toString(10), encoding);
|
||||
});
|
||||
const buffer = Buffer.allocUnsafe(length + 4);
|
||||
const packet = new Packet(sequenceId, buffer, 0, length + 4);
|
||||
packet.offset = 4;
|
||||
columns.forEach((val) => {
|
||||
if (val === null) {
|
||||
packet.writeNull();
|
||||
return;
|
||||
}
|
||||
if (typeof val === 'undefined') {
|
||||
packet.writeInt8(0);
|
||||
return;
|
||||
}
|
||||
packet.writeLengthCodedString(val.toString(10), encoding);
|
||||
});
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextRow;
|
||||
Reference in New Issue
Block a user