/** * Copyright (c) 2010-2017 Brian Carlson (brian.m.carlson@gmail.com) * All rights reserved. * * This source code is licensed under the MIT license found in the * README.md file in the root directory of this source tree. */ var crypto = require('crypto'); var EventEmitter = require('events').EventEmitter; var util = require('util'); var pgPass = require('pgpass'); var TypeOverrides = require('./type-overrides'); var ConnectionParameters = require('./connection-parameters'); var Query = require('./query'); var defaults = require('./defaults'); var Connection = require('./connection'); var Client = function(config) { EventEmitter.call(this); this.connectionParameters = new ConnectionParameters(config); this.user = this.connectionParameters.user; this.database = this.connectionParameters.database; this.port = this.connectionParameters.port; this.host = this.connectionParameters.host; this.password = this.connectionParameters.password; this.replication = this.connectionParameters.replication; var c = config || {}; this._types = new TypeOverrides(c.types); this._ending = false; this._connecting = false; this._connectionError = false; this.connection = c.connection || new Connection({ stream: c.stream, ssl: this.connectionParameters.ssl, keepAlive: c.keepAlive || false }); this.queryQueue = []; this.binary = c.binary || defaults.binary; this.encoding = 'utf8'; this.processID = null; this.secretKey = null; this.ssl = this.connectionParameters.ssl || false; }; util.inherits(Client, EventEmitter); Client.prototype.connect = function(callback) { var self = this; var con = this.connection; this._connecting = true; if(this.host && this.host.indexOf('/') === 0) { con.connect(this.host + '/.s.PGSQL.' + this.port); } else { con.connect(this.port, this.host); } //once connection is established send startup message con.on('connect', function() { if(self.ssl) { con.requestSsl(); } else { con.startup(self.getStartupConf()); } }); con.on('sslconnect', function() { con.startup(self.getStartupConf()); }); function checkPgPass(cb) { return function(msg) { if (null !== self.password) { cb(msg); } else { pgPass(self.connectionParameters, function(pass){ if (undefined !== pass) { self.connectionParameters.password = self.password = pass; } cb(msg); }); } }; } //password request handling con.on('authenticationCleartextPassword', checkPgPass(function() { con.password(self.password); })); //password request handling con.on('authenticationMD5Password', checkPgPass(function(msg) { var inner = Client.md5(self.password + self.user); var outer = Client.md5(Buffer.concat([new Buffer(inner), msg.salt])); var md5password = "md5" + outer; con.password(md5password); })); con.once('backendKeyData', function(msg) { self.processID = msg.processID; self.secretKey = msg.secretKey; }); //hook up query handling events to connection //after the connection initially becomes ready for queries con.once('readyForQuery', function() { self._connecting = false; //delegate rowDescription to active query con.on('rowDescription', function(msg) { self.activeQuery.handleRowDescription(msg); }); //delegate dataRow to active query con.on('dataRow', function(msg) { self.activeQuery.handleDataRow(msg); }); //delegate portalSuspended to active query con.on('portalSuspended', function(msg) { self.activeQuery.handlePortalSuspended(con); }); //deletagate emptyQuery to active query con.on('emptyQuery', function(msg) { self.activeQuery.handleEmptyQuery(con); }); //delegate commandComplete to active query con.on('commandComplete', function(msg) { self.activeQuery.handleCommandComplete(msg, con); }); //if a prepared statement has a name and properly parses //we track that its already been executed so we don't parse //it again on the same client con.on('parseComplete', function(msg) { if(self.activeQuery.name) { con.parsedStatements[self.activeQuery.name] = true; } }); con.on('copyInResponse', function(msg) { self.activeQuery.handleCopyInResponse(self.connection); }); con.on('copyData', function (msg) { self.activeQuery.handleCopyData(msg, self.connection); }); con.on('notification', function(msg) { self.emit('notification', msg); }); //process possible callback argument to Client#connect if (callback) { callback(null, self); //remove callback for proper error handling //after the connect event callback = null; } self.emit('connect'); }); con.on('readyForQuery', function() { var activeQuery = self.activeQuery; self.activeQuery = null; self.readyForQuery = true; self._pulseQueryQueue(); if(activeQuery) { activeQuery.handleReadyForQuery(con); } }); con.on('error', function(error) { if(this.activeQuery) { var activeQuery = self.activeQuery; this.activeQuery = null; return activeQuery.handleError(error, con); } if (this._connecting) { // set a flag indicating we've seen an error during connection // the backend will terminate the connection and we don't want // to throw a second error when the connection is terminated this._connectionError = true; } if(!callback) { return this.emit('error', error); } con.end(); // make sure ECONNRESET errors don't cause error events callback(error); callback = null; }.bind(this)); con.once('end', function() { if (callback) { // haven't received a connection message yet! var err = new Error('Connection terminated'); callback(err); callback = null; return; } if(this.activeQuery) { var disconnectError = new Error('Connection terminated'); this.activeQuery.handleError(disconnectError, con); this.activeQuery = null; } if (!this._ending) { // if the connection is ended without us calling .end() // on this client then we have an unexpected disconnection // treat this as an error unless we've already emitted an error // during connection. if (!this._connectionError) { this.emit('error', new Error('Connection terminated unexpectedly')); } } this.emit('end'); }.bind(this)); con.on('notice', function(msg) { self.emit('notice', msg); }); }; Client.prototype.getStartupConf = function() { var params = this.connectionParameters; var data = { user: params.user, database: params.database }; var appName = params.application_name || params.fallback_application_name; if (appName) { data.application_name = appName; } if (params.replication) { data.replication = '' + params.replication; } return data; }; Client.prototype.cancel = function(client, query) { if(client.activeQuery == query) { var con = this.connection; if(this.host && this.host.indexOf('/') === 0) { con.connect(this.host + '/.s.PGSQL.' + this.port); } else { con.connect(this.port, this.host); } //once connection is established send cancel message con.on('connect', function() { con.cancel(client.processID, client.secretKey); }); } else if(client.queryQueue.indexOf(query) != -1) { client.queryQueue.splice(client.queryQueue.indexOf(query), 1); } }; Client.prototype.setTypeParser = function(oid, format, parseFn) { return this._types.setTypeParser(oid, format, parseFn); }; Client.prototype.getTypeParser = function(oid, format) { return this._types.getTypeParser(oid, format); }; // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c Client.prototype.escapeIdentifier = function(str) { var escaped = '"'; for(var i = 0; i < str.length; i++) { var c = str[i]; if(c === '"') { escaped += c + c; } else { escaped += c; } } escaped += '"'; return escaped; }; // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c Client.prototype.escapeLiteral = function(str) { var hasBackslash = false; var escaped = '\''; for(var i = 0; i < str.length; i++) { var c = str[i]; if(c === '\'') { escaped += c + c; } else if (c === '\\') { escaped += c + c; hasBackslash = true; } else { escaped += c; } } escaped += '\''; if(hasBackslash === true) { escaped = ' E' + escaped; } return escaped; }; Client.prototype._pulseQueryQueue = function() { if(this.readyForQuery===true) { this.activeQuery = this.queryQueue.shift(); if(this.activeQuery) { this.readyForQuery = false; this.hasExecuted = true; this.activeQuery.submit(this.connection); } else if(this.hasExecuted) { this.activeQuery = null; this.emit('drain'); } } }; Client.prototype.copyFrom = function (text) { throw new Error("For PostgreSQL COPY TO/COPY FROM support npm install pg-copy-streams"); }; Client.prototype.copyTo = function (text) { throw new Error("For PostgreSQL COPY TO/COPY FROM support npm install pg-copy-streams"); }; Client.prototype.query = function(config, values, callback) { //can take in strings, config object or query object var query = (typeof config.submit == 'function') ? config : new Query(config, values, callback); if(this.binary && !query.binary) { query.binary = true; } if(query._result) { query._result._getTypeParser = this._types.getTypeParser.bind(this._types); } this.queryQueue.push(query); this._pulseQueryQueue(); return query; }; Client.prototype.end = function(cb) { this._ending = true; this.connection.end(); if (cb) { this.connection.once('end', cb); } }; Client.md5 = function(string) { return crypto.createHash('md5').update(string, 'utf-8').digest('hex'); }; // expose a Query constructor Client.Query = Query; module.exports = Client;