UNPKG

nxkit

Version:

This is a collection of tools, independent of any other libraries

312 lines (311 loc) 11.7 kB
"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;