nxkit
Version:
This is a collection of tools, independent of any other libraries
312 lines (311 loc) • 11.7 kB
JavaScript
"use strict";
/* ***** BEGIN LICENSE BLOCK *****
* Distributed under the BSD license:
*
* Copyright (c) 2015, xuewen.chu
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of xuewen.chu nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL xuewen.chu BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* ***** END LICENSE BLOCK ***** */
Object.defineProperty(exports, "__esModule", { value: true });
const util_1 = require("../util");
const event_1 = require("../event");
const parser_1 = require("./parser");
const constants_1 = require("./constants");
const charsets_1 = require("./charsets");
const auth = require("./auth");
const outgoing_packet_1 = require("./outgoing_packet");
const buffer_1 = require("buffer");
const net_1 = require("net");
const db_1 = require("../db");
const CONNECT_TIMEOUT = 1e4;
const connect_pool = {};
const require_connect = [];
/**
* <span style="color:#f00">[static]</span>max connect count
* @type {Numbet}
* @static
*/
var MAX_CONNECT_COUNT = 20;
/**
* <b style="color:#f00">[static]</b>max packet size
* @type {Number}
* @static
*/
var MAX_PACKET_SIZE = 0x01000000;
/**
* <b style="color:#f00">[static]</b>default flags
* @type {Number}
* @static
*/
var DEFAULT_FLAGS = constants_1.default.CLIENT_LONG_PASSWORD
| constants_1.default.CLIENT_FOUND_ROWS
| constants_1.default.CLIENT_LONG_FLAG
| constants_1.default.CLIENT_CONNECT_WITH_DB
| constants_1.default.CLIENT_ODBC
| constants_1.default.CLIENT_LOCAL_FILES
| constants_1.default.CLIENT_IGNORE_SPACE
| constants_1.default.CLIENT_PROTOCOL_41
| constants_1.default.CLIENT_INTERACTIVE
| constants_1.default.CLIENT_IGNORE_SIGPIPE
| constants_1.default.CLIENT_TRANSACTIONS
| constants_1.default.CLIENT_RESERVED
| constants_1.default.CLIENT_SECURE_CONNECTION
| constants_1.default.CLIENT_MULTI_STATEMENTS
| constants_1.default.CLIENT_MULTI_RESULTS;
/**
* <b style="color:#f00">[static]</b>charest number
* @type {Number}
* @static
*/
var CHAREST_NUMBER = charsets_1.default.UTF8_UNICODE_CI;
exports.default = {
get MAX_CONNECT_COUNT() { return MAX_CONNECT_COUNT; },
get MAX_PACKET_SIZE() { return MAX_PACKET_SIZE; },
get DEFAULT_FLAGS() { return DEFAULT_FLAGS; },
get CHAREST_NUMBER() { return CHAREST_NUMBER; },
set MAX_CONNECT_COUNT(value) { MAX_CONNECT_COUNT = value; },
set MAX_PACKET_SIZE(value) { MAX_PACKET_SIZE = value; },
set DEFAULT_FLAGS(value) { DEFAULT_FLAGS = value; },
set CHAREST_NUMBER(value) { CHAREST_NUMBER = value; },
};
class Connection {
/**
* @constructor
* @arg opt {Object}
*/
constructor(options) {
this._greeting = null;
this._idle_tomeout = 0;
this._isUse = false;
this._isReady = false;
this.onError = new event_1.EventNoticer('Error', this);
this.onPacket = new event_1.EventNoticer('Packet', this);
this._onReady = new event_1.EventNoticer('_Ready', this);
this.options = Object.assign({}, db_1.defaultOptions, options);
var self = this;
var parser = new parser_1.Parser();
var socket = this._socket = new net_1.Socket();
socket.setNoDelay(true);
socket.setTimeout(36e5, () => /*1h timeout*/ socket.end());
socket.on('data', e => {
try {
parser.write(e);
}
catch (err) {
self.destroy(err);
}
});
socket.on('error', err => self.destroy(err));
socket.on('end', () => self.destroy('mysql server has been socket end'));
socket.on('close', () => self.destroy('mysql server has been socket close'));
parser.onPacket.on(function (e) {
var packet = e.data;
if (packet.type === parser_1.Constants.ERROR_PACKET) {
self.destroy({ message: 'ERROR_PACKET', ...packet.toJSON() });
}
else if (self._isReady) {
self.onPacket.trigger(packet);
}
else if (packet.type == parser_1.Constants.GREETING_PACKET) {
self._sendAuth(packet);
}
else if (packet.type == parser_1.Constants.USE_OLD_PASSWORD_PROTOCOL_PACKET) {
self._sendOldAuth(self._greeting);
}
else { // ok
self._isReady = true;
self._onReady.trigger();
}
});
socket.connect(this.options.port, this.options.host);
}
_write(packet) {
this._socket.write(packet.buffer);
}
_sendAuth(greeting) {
var opt = this.options;
var token = auth.token(opt.password, greeting.data.scrambleBuffer);
var packetSize = (4 + 4 + 1 + 23 +
opt.user.length + 1 +
token.length + 1 +
opt.database.length + 1);
var packet = new outgoing_packet_1.OutgoingPacket(packetSize, greeting.number + 1);
packet.writeNumber(4, DEFAULT_FLAGS);
packet.writeNumber(4, MAX_PACKET_SIZE);
packet.writeNumber(1, CHAREST_NUMBER);
packet.writeFiller(23);
packet.writeNullTerminated(opt.user);
packet.writeLengthCoded(token);
packet.writeNullTerminated(opt.database);
this._write(packet);
// Keep a reference to the greeting packet. We might receive a
// USE_OLD_PASSWORD_PROTOCOL_PACKET as a response, in which case we will need
// the greeting packet again. See sendOldAuth()
this._greeting = greeting;
}
_sendOldAuth(greeting) {
var token = auth.scramble323(greeting.data.scrambleBuffer, this.options.password);
var packetSize = (token.length + 1);
var packet = new outgoing_packet_1.OutgoingPacket(packetSize, greeting.number + 3);
// I could not find any official documentation for this, but from sniffing
// the mysql command line client, I think this is the right way to send the
// scrambled token after receiving the USE_OLD_PASSWORD_PROTOCOL_PACKET.
packet.write(token);
packet.writeFiller(1);
this._write(packet);
}
_clearTimeout() {
if (this._idle_tomeout) {
clearTimeout(this._idle_tomeout);
this._idle_tomeout = 0;
}
}
destroy(reason) {
var self = this;
var socket = self._socket;
if (socket) {
self._socket = null;
if (reason)
self.onError.trigger(Error.new(reason));
self._clearTimeout();
self.onError.off();
self.onPacket.off();
self._onReady.off();
socket.destroy();
connect_pool[self.options.host + ':' + self.options.port].deleteOf(self);
}
}
/**
* write buffer
* @arg {node.Buffer}
*/
write(buffer) {
this._socket.write(buffer);
}
/**
* return connection pool
*/
idle() {
util_1.default.assert(this.onPacket.length === 0, 'Connection.idle(), this.onPacket.length');
util_1.default.assert(this._isUse, 'Connection.idle(), _isUse');
this._isUse = false;
this.onPacket.off();
this.onError.off();
this._onReady.off();
if (!this._socket)
return; // socket destroy
for (var i = 0, l = require_connect.length; i < l; i++) {
var req = require_connect[i];
var args = req.args;
var [opt] = args;
if (opt.host == this.options.host && opt.port === this.options.port &&
opt.user == this.options.user && opt.password == this.options.password) {
require_connect.splice(i, 1);
clearTimeout(req.timeout);
exports.resolve(...args);
return;
}
}
this._idle_tomeout = (() => this.destroy()).setTimeout(CONNECT_TIMEOUT);
}
_changeDB(db, cb) {
if (db != this.options.database) { // change db
// init db, change db
util_1.default.assert(this._isReady);
this.options.database = db;
var packet = new outgoing_packet_1.OutgoingPacket(1 + buffer_1.Buffer.byteLength(db, 'utf-8'));
packet.writeNumber(1, constants_1.default.COM_INIT_DB);
packet.write(db, 'utf-8');
this._write(packet);
this._isReady = false;
}
this._ready(cb);
}
_ready(cb) {
this._use();
if (this._isReady) {
return util_1.default.nextTick(cb, null, this);
}
// wait ready
this._onReady.once(() => {
this.onError.off();
util_1.default.nextTick(cb, null, this);
});
this.onError.once(e => cb(e.data));
}
/**
* start use connect
*/
_use() {
util_1.default.assert(!this._isUse);
this._isUse = true;
this._clearTimeout();
}
/**
* get connect
* @param {Object} opt
* @param {Function} cb
*/
static resolve(opt, cb) {
var key = opt.host + ':' + opt.port;
var pool = connect_pool[key];
if (!pool)
connect_pool[key] = pool = [];
for (var c of pool) {
var options = c.options;
if (!c._isUse && !c._socket) {
if (options.user == opt.user && options.password == opt.password) {
if (options.database == opt.database) {
c._ready(cb);
}
else {
c._changeDB(opt.database, cb);
}
return;
}
}
}
//is max connect
if (pool.length < MAX_CONNECT_COUNT) {
var c = new Connection(opt);
pool.push(c);
c._ready(cb);
}
else {
// queue up
var req = {
timeout: function () {
require_connect.deleteOf(req);
cb(new Error('obtaining a connection from the connection pool timeout'));
}.setTimeout(CONNECT_TIMEOUT),
args: [opt, cb]
};
//append to require connect
require_connect.push(req);
}
}
}
exports.Connection = Connection;
exports.resolve = Connection.resolve;