@mysql/xdevapi
Version:
MySQL Connector/Node.js - A Node.js driver for MySQL using the X Protocol and X DevAPI.
576 lines (470 loc) • 19.4 kB
JavaScript
/*
* Copyright (c) 2016, 2022, Oracle and/or its affiliates.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0, as
* published by the Free Software Foundation.
*
* This program is also distributed with certain software (including
* but not limited to OpenSSL) that is licensed under separate terms,
* as designated in a particular file or component or in included license
* documentation. The authors of MySQL hereby grant you an
* additional permission to link the program and your derivative works
* with the separately licensed software that they have included with
* MySQL.
*
* Without limiting anything contained in the foregoing, this file,
* which is part of MySQL Connector/Node.js, is also subject to the
* Universal FOSS Exception, version 1.0, a copy of which can be found at
* http://oss.oracle.com/licenses/universal-foss-exception.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
'use strict';
const AuthenticationHandler = require('./InboundHandlers/AuthenticationHandler');
const CapabilitiesGetHandler = require('./InboundHandlers/CapabilitiesGetHandler');
const ClientMessages = require('./Stubs/mysqlx_pb').ClientMessages;
const Connection = require('./OutboundHandlers/Connection');
const Crud = require('./OutboundHandlers/Crud');
const Expect = require('./OutboundHandlers/Expect');
const NoticeStub = require('./Stubs/mysqlx_notice_pb');
const OkHandler = require('./InboundHandlers/OkHandler');
const Prepare = require('./OutboundHandlers/Prepare');
const Session = require('./OutboundHandlers/Session');
const Sql = require('./OutboundHandlers/Sql');
const SqlResultHandler = require('./InboundHandlers/SqlResultHandler');
const WorkQueue = require('../WorkQueue');
const condition = require('./Wrappers/Messages/Expect/Condition');
const error = require('./Wrappers/Messages/Error');
const errors = require('../constants/errors');
const log = require('../logger');
const notice = require('./Wrappers/Messages/Notice/Frame');
const REQUIRES_REAUTH = {
NO: 'NO',
UNKNOWN: 'UNKNOWN',
YES: 'YES'
};
/**
* Main Protocol class
* @param stream {stream}
* @constructor
* @private
*/
function Client (stream) {
this._stream = stream;
this._workQueue = new WorkQueue();
this._danglingFragment = null;
this._requiresAuthenticationAfterReset = REQUIRES_REAUTH.UNKNOWN;
}
module.exports = Client;
Client.prototype.getConnection = function () {
return this._stream;
};
Client.prototype.setConnection = function (connection) {
this._stream = connection;
return this;
};
/**
* Encode X protocol message (with header and payload).
* @param {number} messageType - type of the message payload
* @param {Buffer} data - raw payload data
* @returns {Buffer} The full X protocol message buffer.
*/
Client.prototype.encodeMessage = function (messageType, data) {
// The message must accommodate (besides payload size) additionally, 4
// bytes for the header and 1 byte for the message id.
// Since the entire buffer will be written, we should use
// "Buffer.allocUnsafe()" instead of "Buffer.alloc()" because it is faster.
// https://nodejs.org/docs/v14.0.0/api/buffer.html#buffer_buffer_from_buffer_alloc_and_buffer_allocunsafe
const buffer = Buffer.allocUnsafe(data.length + 5);
data.copy(buffer, 5);
// The total payload size should not include the size of the length header field.
buffer.writeUInt32LE(buffer.length - 4, 0);
buffer[4] = messageType;
return buffer;
};
Client.prototype.decodeMessage = function (data) {
// Check if the connection is using the old MySQL wire protocol.
const isWireProtocol = data.readInt32LE(0) + 4 <= data.length && // package length
data[3] === 0 && // package number
data[4] === 10; // protocol version
if (isWireProtocol) {
throw new Error(errors.MESSAGES.ER_CLIENT_NO_X_PROTOCOL);
}
const header = this.decodeMessageHeader(data);
if (data.length < header.packetLength) {
throw new Error(errors.MESSAGES.ER_DEVAPI_INCOMPLETE_PROTOCOL_MESSAGE);
}
const payload = data.slice(5, header.packetLength);
return { id: header.messageId, payload };
};
Client.prototype.decodeMessageHeader = function (data) {
if (data.length < 4 /* header size */ + 1 /* type flag size */) {
throw new Error(errors.MESSAGES.ER_X_CLIENT_UNKNOWN_PROTOCOL_HEADER);
}
return {
// The length reported by the server does not include the length of the header.
packetLength: data.readUInt32LE(0) + 4,
messageId: data[4]
};
};
/**
* Handle an individual message
*
* The passed buffer must only contain the single message
*
* @param {Buffer} message
*/
Client.prototype.handleServerMessage = function (message) {
const decoded = this.decodeMessage(message);
if (decoded.id === notice.MESSAGE_ID) {
// TODO(Rui): Non-global notices are being decoded twice (in the handler as well).
// This will change in an upcoming refactoring plan.
const frame = notice.deserialize(decoded.payload);
if (frame.getScopeId() === NoticeStub.Frame.Scope.GLOBAL) {
log('protocol:inbound:Mysqlx.Notice').info('Frame', frame);
if (frame.getTypeId() === NoticeStub.Frame.Type.WARNING) {
const payload = frame.getPayload();
if (payload.getLevelId() === NoticeStub.Warning.Level.ERROR) {
const error = new Error(errors.MESSAGES[errors[payload.getCode()]]);
this._stream.emit('error', error);
}
}
return;
}
}
this._workQueue.process(decoded);
};
/**
* This is the entry point for everything from the network
*
* Here we split multiple messages from one network packet and and reassemble
* fragmented pieces. Each individual message is passed over to
* handleServerMessage.
*
* @param {Buffer} fragment
*/
Client.prototype.handleNetworkFragment = function (fragment) {
if (this._danglingFragment) {
// The previous fragment contained an incomplete message that requires data from the current fragment.
fragment = Buffer.concat([this._danglingFragment, fragment], this._danglingFragment.length + fragment.length);
}
// X Protocol header length = 4 bytes.
const headerLength = 4;
let offset = 0;
while (offset < fragment.length) {
// Work on the scope of a single message.
const chunk = fragment.slice(offset);
if (chunk.length < headerLength) {
// There is no reason for this to happen, but we can play it safe.
this._danglingFragment = chunk;
return;
}
// The value encoded in the message length segment does not include the size
// of that same segment.
const payloadLength = chunk.readUInt32LE(0) + headerLength;
if (chunk.length < payloadLength) {
// The message is still incomplete.
this._danglingFragment = chunk;
return;
}
this._danglingFragment = null;
this.handleServerMessage(chunk.slice(0, payloadLength));
if (chunk.length === payloadLength) {
// The current chunk matches an entire message.
return;
}
// The current chunk contains data from additional messages.
// The next loop iteration should start from the beginning of the next one.
offset += payloadLength;
}
};
Client.prototype.handleServerClose = function () {
while (this._workQueue.hasMore()) {
this._workQueue.process({ id: error.SERVER_GONE });
}
};
Client.prototype.isRunning = function () {
return this._workQueue.hasMore();
};
/**
* Send a Mysqlx.Connection.CapabilitiesGet message to the server.
* @private
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.capabilitiesGet = function () {
const messageType = ClientMessages.Type.CON_CAPABILITIES_GET;
const protobuf = this.encodeMessage(messageType, Connection.encodeCapabilitiesGet());
const handler = new CapabilitiesGetHandler(this);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Connection.CapabilitiesSet message to the server.
* @private
* @param {Object} properties - connection properties
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.capabilitiesSet = function (properties) {
const messageType = ClientMessages.Type.CON_CAPABILITIES_SET;
const protobuf = this.encodeMessage(messageType, Connection.encodeCapabilitiesSet(properties));
const handler = new OkHandler(this);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Session.AuthenticateStart message to the server.
* @private
* @param {IAuthenticator} authenticator - authenticator instance
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.authenticate = function (authenticator) {
// cache the authentication plugin
this._authenticator = authenticator;
const messageType = ClientMessages.Type.SESS_AUTHENTICATE_START;
const protobuf = this.encodeMessage(messageType, Session.encodeAuthenticateStart(authenticator.getName(), authenticator.getInitialAuthData()));
const handler = new AuthenticationHandler(authenticator, this);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Session.AuthenticateContinue message to the server.
* @private
* @param {Buffer} data - raw connection data
* @param {AuthenticationHandler} handler - active authentication handler
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.authenticateContinue = function (data, handler) {
const messageType = ClientMessages.Type.SESS_AUTHENTICATE_CONTINUE;
const protobuf = this.encodeMessage(messageType, Session.encodeAuthenticateContinue(data));
handler.sendDirect(this._stream, protobuf);
};
/**
* Send a Mysqlx.Crud.Insert message to the server.
* @private
* @param {module:CollectionAdd|module:TableInsert} query - the operation instance
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.crudInsert = function (query) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.CRUD_INSERT, Crud.encodeInsert(query));
} catch (err) {
return Promise.reject(err);
}
const handler = new SqlResultHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Crud.Find message to the server.
* @private
* @param {Object} query - the query data
* @param {Function} resultSetCursor
* @param {Function} metadataCursor
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.crudFind = function (query, resultSetCursor, metadataCursor) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.CRUD_FIND, Crud.encodeFind(query));
} catch (err) {
return Promise.reject(err);
}
const handler = new SqlResultHandler(resultSetCursor, metadataCursor);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Crud.Update message to the server.
* @private
* @param {module:CollectionModify|module:TableUpdate} query - the operation instance
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.crudModify = function (query) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.CRUD_UPDATE, Crud.encodeUpdate(query));
} catch (err) {
return Promise.reject(err);
}
const handler = new SqlResultHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Crud.Delete message to the server.
* @private
* @param {module:CollectionRemove|module:TableDelete} query - the operation instance
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.crudRemove = function (query) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.CRUD_DELETE, Crud.encodeDelete(query));
} catch (err) {
return Promise.reject(err);
}
const handler = new SqlResultHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Sql.StmtExecute message to the server.
* @private
* @param {StmtExecute} query - the operation instance
* @param {Function} resultSetCursor
* @param {Function} metadataCursor
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.sqlStmtExecute = function (query, resultSetCursor, metadataCursor) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.SQL_STMT_EXECUTE, Sql.encodeStmtExecute(query));
} catch (err) {
return Promise.reject(err);
}
const handler = new SqlResultHandler(resultSetCursor, metadataCursor);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Prepare.Prepare message to the server.
* @private
* @param {Query} statement - the statement instance
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.prepare = function (statement) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.PREPARE_PREPARE, Prepare.encodePrepare(statement));
} catch (err) {
return Promise.reject(err);
}
const handler = new OkHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Prepare.Execute message to the server.
* @private
* @param {Query} statement - the statement instance
* @param {Function} resultSetCursor
* @param {Function} metadataCursor
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.prepareExecute = function (statement, resultSetCursor, metadataCursor) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.PREPARE_EXECUTE, Prepare.encodeExecute(statement));
} catch (err) {
return Promise.reject(err);
}
const handler = new SqlResultHandler(resultSetCursor, metadataCursor);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Prepare.Deallocate message to the server.
* @private
* @param {Query} statement - the statement instance
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.deallocate = function (statement) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.PREPARE_DEALLOCATE, Prepare.encodeDeallocate(statement));
} catch (err) {
return Promise.reject(err);
}
const handler = new OkHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Session.Close message to the server.
* @private
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.sessionClose = function () {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.SESS_CLOSE, Session.encodeClose());
} catch (err) {
return Promise.reject(err);
}
const handler = new OkHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
/**
* Send a Mysqlx.Session.Reset message to the server.
* @private
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.sessionReset = function () {
const reset = (options) => {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.SESS_RESET, Session.encodeReset(options));
} catch (err) {
return Promise.reject(err);
}
const handler = new OkHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
if (this._requiresAuthenticationAfterReset === REQUIRES_REAUTH.NO) {
return reset({ keepOpen: true });
}
if (this._requiresAuthenticationAfterReset === REQUIRES_REAUTH.YES) {
return reset({ keepOpen: false }).then(() => this.authenticate(this._authenticator));
}
const expectations = [{
condition: condition.ACTION.EXPECT_OP_SET,
key: condition.TYPE.EXPECT_FIELD_EXIST,
value: '6.1' // checks if SESS_RESET message supports the "keep_open" property
}];
return this.expectOpen(expectations)
.then(() => reset({ keepOpen: true }))
.then(() => this.expectClose())
.then(() => {
this._requiresAuthenticationAfterReset = REQUIRES_REAUTH.NO;
})
.catch(err => {
if (err.info && err.info.code !== errors.ER_X_EXPECT_FIELD_EXISTS_FAILED) {
throw err;
}
this._requiresAuthenticationAfterReset = REQUIRES_REAUTH.YES;
return this.sessionReset();
});
};
/**
* Send a Mysqlx.Connection.Close message to the server.
* @private
* @returns {Promise<Object>} A promise that resolves to an object describing the operation status.
*/
Client.prototype.connectionClose = function () {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.CON_CLOSE, Connection.encodeClose());
} catch (err) {
return Promise.reject(err);
}
const handler = new OkHandler();
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
Client.prototype.expectClose = function () {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.EXPECT_OPEN, Expect.encodeClose());
} catch (err) {
return Promise.reject(err);
}
const handler = new OkHandler(this);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};
Client.prototype.expectOpen = function (expectations) {
let protobuf;
try {
protobuf = this.encodeMessage(ClientMessages.Type.EXPECT_OPEN, Expect.encodeOpen(expectations));
} catch (err) {
return Promise.reject(err);
}
const handler = new OkHandler(this);
return handler.sendMessage(this._workQueue, this._stream, protobuf);
};