Connection.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. var Crypto = require('crypto');
  2. var Events = require('events');
  3. var Net = require('net');
  4. var tls = require('tls');
  5. var ConnectionConfig = require('./ConnectionConfig');
  6. var Protocol = require('./protocol/Protocol');
  7. var SqlString = require('./protocol/SqlString');
  8. var Query = require('./protocol/sequences/Query');
  9. var Util = require('util');
  10. module.exports = Connection;
  11. Util.inherits(Connection, Events.EventEmitter);
  12. function Connection(options) {
  13. Events.EventEmitter.call(this);
  14. this.config = options.config;
  15. this._socket = options.socket;
  16. this._protocol = new Protocol({config: this.config, connection: this});
  17. this._connectCalled = false;
  18. this.state = 'disconnected';
  19. this.threadId = null;
  20. }
  21. Connection.createQuery = function createQuery(sql, values, callback) {
  22. if (sql instanceof Query) {
  23. return sql;
  24. }
  25. var cb = callback;
  26. var options = {};
  27. if (typeof sql === 'function') {
  28. cb = sql;
  29. } else if (typeof sql === 'object') {
  30. options = Object.create(sql);
  31. if (typeof values === 'function') {
  32. cb = values;
  33. } else if (values !== undefined) {
  34. Object.defineProperty(options, 'values', { value: values });
  35. }
  36. } else {
  37. options.sql = sql;
  38. if (typeof values === 'function') {
  39. cb = values;
  40. } else if (values !== undefined) {
  41. options.values = values;
  42. }
  43. }
  44. if (cb !== undefined) {
  45. cb = wrapCallbackInDomain(null, cb);
  46. if (cb === undefined) {
  47. throw new TypeError('argument callback must be a function when provided');
  48. }
  49. }
  50. return new Query(options, cb);
  51. };
  52. Connection.prototype.connect = function connect(options, callback) {
  53. if (!callback && typeof options === 'function') {
  54. callback = options;
  55. options = {};
  56. }
  57. if (!this._connectCalled) {
  58. this._connectCalled = true;
  59. // Connect either via a UNIX domain socket or a TCP socket.
  60. this._socket = (this.config.socketPath)
  61. ? Net.createConnection(this.config.socketPath)
  62. : Net.createConnection(this.config.port, this.config.host);
  63. // Connect socket to connection domain
  64. if (Events.usingDomains) {
  65. this._socket.domain = this.domain;
  66. }
  67. var connection = this;
  68. this._protocol.on('data', function(data) {
  69. connection._socket.write(data);
  70. });
  71. this._socket.on('data', wrapToDomain(connection, function (data) {
  72. connection._protocol.write(data);
  73. }));
  74. this._protocol.on('end', function() {
  75. connection._socket.end();
  76. });
  77. this._socket.on('end', wrapToDomain(connection, function () {
  78. connection._protocol.end();
  79. }));
  80. this._socket.on('error', this._handleNetworkError.bind(this));
  81. this._socket.on('connect', this._handleProtocolConnect.bind(this));
  82. this._protocol.on('handshake', this._handleProtocolHandshake.bind(this));
  83. this._protocol.on('initialize', this._handleProtocolInitialize.bind(this));
  84. this._protocol.on('unhandledError', this._handleProtocolError.bind(this));
  85. this._protocol.on('drain', this._handleProtocolDrain.bind(this));
  86. this._protocol.on('end', this._handleProtocolEnd.bind(this));
  87. this._protocol.on('enqueue', this._handleProtocolEnqueue.bind(this));
  88. if (this.config.connectTimeout) {
  89. var handleConnectTimeout = this._handleConnectTimeout.bind(this);
  90. this._socket.setTimeout(this.config.connectTimeout, handleConnectTimeout);
  91. this._socket.once('connect', function() {
  92. this.setTimeout(0, handleConnectTimeout);
  93. });
  94. }
  95. }
  96. this._protocol.handshake(options, wrapCallbackInDomain(this, callback));
  97. };
  98. Connection.prototype.changeUser = function changeUser(options, callback) {
  99. if (!callback && typeof options === 'function') {
  100. callback = options;
  101. options = {};
  102. }
  103. this._implyConnect();
  104. var charsetNumber = (options.charset)
  105. ? ConnectionConfig.getCharsetNumber(options.charset)
  106. : this.config.charsetNumber;
  107. return this._protocol.changeUser({
  108. user : options.user || this.config.user,
  109. password : options.password || this.config.password,
  110. database : options.database || this.config.database,
  111. timeout : options.timeout,
  112. charsetNumber : charsetNumber,
  113. currentConfig : this.config
  114. }, wrapCallbackInDomain(this, callback));
  115. };
  116. Connection.prototype.beginTransaction = function beginTransaction(options, callback) {
  117. if (!callback && typeof options === 'function') {
  118. callback = options;
  119. options = {};
  120. }
  121. options = options || {};
  122. options.sql = 'START TRANSACTION';
  123. options.values = null;
  124. return this.query(options, callback);
  125. };
  126. Connection.prototype.commit = function commit(options, callback) {
  127. if (!callback && typeof options === 'function') {
  128. callback = options;
  129. options = {};
  130. }
  131. options = options || {};
  132. options.sql = 'COMMIT';
  133. options.values = null;
  134. return this.query(options, callback);
  135. };
  136. Connection.prototype.rollback = function rollback(options, callback) {
  137. if (!callback && typeof options === 'function') {
  138. callback = options;
  139. options = {};
  140. }
  141. options = options || {};
  142. options.sql = 'ROLLBACK';
  143. options.values = null;
  144. return this.query(options, callback);
  145. };
  146. Connection.prototype.query = function query(sql, values, cb) {
  147. var query = Connection.createQuery(sql, values, cb);
  148. query._connection = this;
  149. if (!(typeof sql === 'object' && 'typeCast' in sql)) {
  150. query.typeCast = this.config.typeCast;
  151. }
  152. if (query.sql) {
  153. query.sql = this.format(query.sql, query.values);
  154. }
  155. if (query._callback) {
  156. query._callback = wrapCallbackInDomain(this, query._callback);
  157. }
  158. this._implyConnect();
  159. return this._protocol._enqueue(query);
  160. };
  161. Connection.prototype.ping = function ping(options, callback) {
  162. if (!callback && typeof options === 'function') {
  163. callback = options;
  164. options = {};
  165. }
  166. this._implyConnect();
  167. this._protocol.ping(options, wrapCallbackInDomain(this, callback));
  168. };
  169. Connection.prototype.statistics = function statistics(options, callback) {
  170. if (!callback && typeof options === 'function') {
  171. callback = options;
  172. options = {};
  173. }
  174. this._implyConnect();
  175. this._protocol.stats(options, wrapCallbackInDomain(this, callback));
  176. };
  177. Connection.prototype.end = function end(options, callback) {
  178. var cb = callback;
  179. var opts = options;
  180. if (!callback && typeof options === 'function') {
  181. cb = options;
  182. opts = null;
  183. }
  184. // create custom options reference
  185. opts = Object.create(opts || null);
  186. if (opts.timeout === undefined) {
  187. // default timeout of 30 seconds
  188. opts.timeout = 30000;
  189. }
  190. this._implyConnect();
  191. this._protocol.quit(opts, wrapCallbackInDomain(this, cb));
  192. };
  193. Connection.prototype.destroy = function() {
  194. this.state = 'disconnected';
  195. this._implyConnect();
  196. this._socket.destroy();
  197. this._protocol.destroy();
  198. };
  199. Connection.prototype.pause = function() {
  200. this._socket.pause();
  201. this._protocol.pause();
  202. };
  203. Connection.prototype.resume = function() {
  204. this._socket.resume();
  205. this._protocol.resume();
  206. };
  207. Connection.prototype.escape = function(value) {
  208. return SqlString.escape(value, false, this.config.timezone);
  209. };
  210. Connection.prototype.escapeId = function escapeId(value) {
  211. return SqlString.escapeId(value, false);
  212. };
  213. Connection.prototype.format = function(sql, values) {
  214. if (typeof this.config.queryFormat === 'function') {
  215. return this.config.queryFormat.call(this, sql, values, this.config.timezone);
  216. }
  217. return SqlString.format(sql, values, this.config.stringifyObjects, this.config.timezone);
  218. };
  219. if (tls.TLSSocket) {
  220. // 0.11+ environment
  221. Connection.prototype._startTLS = function _startTLS(onSecure) {
  222. var connection = this;
  223. createSecureContext(this.config, function (err, secureContext) {
  224. if (err) {
  225. onSecure(err);
  226. return;
  227. }
  228. // "unpipe"
  229. connection._socket.removeAllListeners('data');
  230. connection._protocol.removeAllListeners('data');
  231. // socket <-> encrypted
  232. var rejectUnauthorized = connection.config.ssl.rejectUnauthorized;
  233. var secureEstablished = false;
  234. var secureSocket = new tls.TLSSocket(connection._socket, {
  235. rejectUnauthorized : rejectUnauthorized,
  236. requestCert : true,
  237. secureContext : secureContext,
  238. isServer : false
  239. });
  240. // error handler for secure socket
  241. secureSocket.on('_tlsError', function(err) {
  242. if (secureEstablished) {
  243. connection._handleNetworkError(err);
  244. } else {
  245. onSecure(err);
  246. }
  247. });
  248. // cleartext <-> protocol
  249. secureSocket.pipe(connection._protocol);
  250. connection._protocol.on('data', function(data) {
  251. secureSocket.write(data);
  252. });
  253. secureSocket.on('secure', function() {
  254. secureEstablished = true;
  255. onSecure(rejectUnauthorized ? this.ssl.verifyError() : null);
  256. });
  257. // start TLS communications
  258. secureSocket._start();
  259. });
  260. };
  261. } else {
  262. // pre-0.11 environment
  263. Connection.prototype._startTLS = function _startTLS(onSecure) {
  264. // before TLS:
  265. // _socket <-> _protocol
  266. // after:
  267. // _socket <-> securePair.encrypted <-> securePair.cleartext <-> _protocol
  268. var connection = this;
  269. var credentials = Crypto.createCredentials({
  270. ca : this.config.ssl.ca,
  271. cert : this.config.ssl.cert,
  272. ciphers : this.config.ssl.ciphers,
  273. key : this.config.ssl.key,
  274. passphrase : this.config.ssl.passphrase
  275. });
  276. var rejectUnauthorized = this.config.ssl.rejectUnauthorized;
  277. var secureEstablished = false;
  278. var securePair = tls.createSecurePair(credentials, false, true, rejectUnauthorized);
  279. // error handler for secure pair
  280. securePair.on('error', function(err) {
  281. if (secureEstablished) {
  282. connection._handleNetworkError(err);
  283. } else {
  284. onSecure(err);
  285. }
  286. });
  287. // "unpipe"
  288. this._socket.removeAllListeners('data');
  289. this._protocol.removeAllListeners('data');
  290. // socket <-> encrypted
  291. securePair.encrypted.pipe(this._socket);
  292. this._socket.on('data', function(data) {
  293. securePair.encrypted.write(data);
  294. });
  295. // cleartext <-> protocol
  296. securePair.cleartext.pipe(this._protocol);
  297. this._protocol.on('data', function(data) {
  298. securePair.cleartext.write(data);
  299. });
  300. // secure established
  301. securePair.on('secure', function() {
  302. secureEstablished = true;
  303. if (!rejectUnauthorized) {
  304. onSecure();
  305. return;
  306. }
  307. var verifyError = this.ssl.verifyError();
  308. var err = verifyError;
  309. // node.js 0.6 support
  310. if (typeof err === 'string') {
  311. err = new Error(verifyError);
  312. err.code = verifyError;
  313. }
  314. onSecure(err);
  315. });
  316. // node.js 0.8 bug
  317. securePair._cycle = securePair.cycle;
  318. securePair.cycle = function cycle() {
  319. if (this.ssl && this.ssl.error) {
  320. this.error();
  321. }
  322. return this._cycle.apply(this, arguments);
  323. };
  324. };
  325. }
  326. Connection.prototype._handleConnectTimeout = function() {
  327. if (this._socket) {
  328. this._socket.setTimeout(0);
  329. this._socket.destroy();
  330. }
  331. var err = new Error('connect ETIMEDOUT');
  332. err.errorno = 'ETIMEDOUT';
  333. err.code = 'ETIMEDOUT';
  334. err.syscall = 'connect';
  335. this._handleNetworkError(err);
  336. };
  337. Connection.prototype._handleNetworkError = function(err) {
  338. this._protocol.handleNetworkError(err);
  339. };
  340. Connection.prototype._handleProtocolError = function(err) {
  341. this.state = 'protocol_error';
  342. this.emit('error', err);
  343. };
  344. Connection.prototype._handleProtocolDrain = function() {
  345. this.emit('drain');
  346. };
  347. Connection.prototype._handleProtocolConnect = function() {
  348. this.state = 'connected';
  349. this.emit('connect');
  350. };
  351. Connection.prototype._handleProtocolHandshake = function _handleProtocolHandshake() {
  352. this.state = 'authenticated';
  353. };
  354. Connection.prototype._handleProtocolInitialize = function _handleProtocolInitialize(packet) {
  355. this.threadId = packet.threadId;
  356. };
  357. Connection.prototype._handleProtocolEnd = function(err) {
  358. this.state = 'disconnected';
  359. this.emit('end', err);
  360. };
  361. Connection.prototype._handleProtocolEnqueue = function _handleProtocolEnqueue(sequence) {
  362. this.emit('enqueue', sequence);
  363. };
  364. Connection.prototype._implyConnect = function() {
  365. if (!this._connectCalled) {
  366. this.connect();
  367. }
  368. };
  369. function createSecureContext (config, cb) {
  370. var context = null;
  371. var error = null;
  372. try {
  373. context = tls.createSecureContext({
  374. ca : config.ssl.ca,
  375. cert : config.ssl.cert,
  376. ciphers : config.ssl.ciphers,
  377. key : config.ssl.key,
  378. passphrase : config.ssl.passphrase
  379. });
  380. } catch (err) {
  381. error = err;
  382. }
  383. cb(error, context);
  384. }
  385. function unwrapFromDomain(fn) {
  386. return function () {
  387. var domains = [];
  388. var ret;
  389. while (process.domain) {
  390. domains.shift(process.domain);
  391. process.domain.exit();
  392. }
  393. try {
  394. ret = fn.apply(this, arguments);
  395. } finally {
  396. for (var i = 0; i < domains.length; i++) {
  397. domains[i].enter();
  398. }
  399. }
  400. return ret;
  401. };
  402. }
  403. function wrapCallbackInDomain(ee, fn) {
  404. if (typeof fn !== 'function') {
  405. return undefined;
  406. }
  407. if (fn.domain) {
  408. return fn;
  409. }
  410. var domain = process.domain;
  411. if (domain) {
  412. return domain.bind(fn);
  413. } else if (ee) {
  414. return unwrapFromDomain(wrapToDomain(ee, fn));
  415. } else {
  416. return fn;
  417. }
  418. }
  419. function wrapToDomain(ee, fn) {
  420. return function () {
  421. if (Events.usingDomains && ee.domain) {
  422. ee.domain.enter();
  423. fn.apply(this, arguments);
  424. ee.domain.exit();
  425. } else {
  426. fn.apply(this, arguments);
  427. }
  428. };
  429. }