Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update packages/pg/lib/connection-parameters.js. Fix SSL issue (#2723) #2994

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 41 additions & 2 deletions packages/pg/lib/connection-parameters.js
@@ -1,6 +1,7 @@
'use strict'

var dns = require('dns')
const _ = require('lodash')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undeclared dependency, and too heavy just to check if an object is empty (should pull in just https://www.npmjs.com/package/lodash.isempty if really necessary).


var defaults = require('./defaults')

Expand All @@ -18,7 +19,7 @@ var val = function (key, config, envVar) {
return config[key] || envVar || defaults[key]
}

var readSSLConfigFromEnvironment = function () {
var readSSLModeFromEnvironment = function () {
switch (process.env.PGSSLMODE) {
case 'disable':
return false
Expand All @@ -33,6 +34,27 @@ var readSSLConfigFromEnvironment = function () {
return defaults.ssl
}

var fillInSSLVariablesFromEnvironment = function(existingSsl) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existingSsl is always {}?

var caFileName = val('sslrootcert', existingSsl)
var crtFileName = val('sslcert', existingSsl)
var keyFileName = val('sslkey', existingSsl)

// Only try to load fs if we expect to read from the disk
const fs = caFileName || crtFileName || keyFileName ? require('fs') : null
var result = {}
if (caFileName) {
result.ca = fs.readFileSync(caFileName).toString()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implicit sync calls that happen on every connection might not be good (one good reason for user code to be responsible for this…)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding sync / async, read this: #2994 (comment)

}
if (crtFileName) {
result.cert = fs.readFileSync(crtFileName).toString()
}
if (keyFileName) {
result.key = fs.readFileSync(keyFileName).toString()
}

return result
}

// Convert arg to a string, surround in single quotes, and escape single quotes and backslashes
var quoteParamValue = function (value) {
return "'" + ('' + value).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"
Expand Down Expand Up @@ -78,7 +100,7 @@ class ConnectionParameters {
this.binary = val('binary', config)
this.options = val('options', config)

this.ssl = typeof config.ssl === 'undefined' ? readSSLConfigFromEnvironment() : config.ssl
this.ssl = typeof config.ssl === 'undefined' ? readSSLModeFromEnvironment() : config.ssl

if (typeof this.ssl === 'string') {
if (this.ssl === 'true') {
Expand All @@ -89,6 +111,23 @@ class ConnectionParameters {
if (this.ssl === 'no-verify') {
this.ssl = { rejectUnauthorized: false }
}

// Fill in possibly missing SSL config parameters from environment variables.
// This lets you mix SSL definitions between connection string, config object and environment variables.
if (this.ssl === true) {
var sslCandidate = fillInSSLVariablesFromEnvironment({})

// Change the type of this.ssl from boolean to object only if relevant environment variables were defined
if (!_.isEmpty(sslCandidate)) {
this.ssl = sslCandidate
}
} else if (typeof this.ssl === 'object') {
var sslCandidate = fillInSSLVariablesFromEnvironment({})
this.ssl = Object.assign(this.ssl, sslCandidate)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially modifies user-provided config.ssl object in place.

Suggested change
this.ssl = Object.assign(this.ssl, sslCandidate)
this.ssl = {...this.ssl, ...sslCandidate}

}

// "hiding" the private key so it doesn't show up in stack traces
// or if the client is console.logged
if (this.ssl && this.ssl.key) {
Object.defineProperty(this.ssl, 'key', {
enumerable: false,
Expand Down
11 changes: 11 additions & 0 deletions packages/pg/test/integration/connection-pool/tls-tests.js
Expand Up @@ -8,6 +8,8 @@ const pg = helper.pg
const suite = new helper.Suite()

if (process.env.PG_CLIENT_CERT_TEST) {

// Test SSL using "options object" constructor
suite.testAsync('client certificate', async () => {
const pool = new pg.Pool({
ssl: {
Expand All @@ -20,4 +22,13 @@ if (process.env.PG_CLIENT_CERT_TEST) {
await pool.query('SELECT 1')
await pool.end()
})

// Test SSL by only defining environment variables without any explicit reference to the cert files in the code
// to be compliant with lib-pg: https://www.postgresql.org/docs/current/libpq-envars.html
suite.testAsync('client certificate', async () => {
const pool = new pg.Pool({})

await pool.query('SELECT 1')
await pool.end()
})
}
Expand Up @@ -3,6 +3,11 @@ var helper = require('../test-helper')
const Suite = require('../../suite')

var assert = require('assert')
const fs = require('fs')

const tmp = require('tmp')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another undeclared dependency. Can the files just be part of the repo instead of written dynamically by test code?

tmp.setGracefulCleanup()

var ConnectionParameters = require('../../../lib/connection-parameters')
var defaults = require('../../../lib').defaults

Expand Down Expand Up @@ -36,6 +41,38 @@ suite.test('ConnectionParameters initialized from environment variables', functi
assert.equal(subject.port, 7890, 'env port')
assert.equal(subject.database, 'allyerbase', 'env database')
assert.equal(subject.password, 'open', 'env password')
assert.equal(subject.ssl, false, 'ssl')
})

suite.test('ConnectionParameters initialized from environment variables - ssl', function () {
createTempTlsFilesAndExecute(function(
certFilePath, keyFilePath, caFilePath,
certFileContents, keyFileContents, caFileContents
) {
clearEnv()
process.env['PGHOST'] = 'local'
process.env['PGUSER'] = 'bmc2'
process.env['PGPORT'] = 7890
process.env['PGDATABASE'] = 'allyerbase'
process.env['PGPASSWORD'] = 'open'

process.env['PGSSLMODE'] = 'verify-full'
process.env['PGSSLCERT'] = certFilePath
process.env['PGSSLKEY'] = keyFilePath
process.env['PGSSLROOTCERT'] = caFilePath

var subject = new ConnectionParameters()
assert.equal(subject.host, 'local', 'env host')
assert.equal(subject.user, 'bmc2', 'env user')
assert.equal(subject.port, 7890, 'env port')
assert.equal(subject.database, 'allyerbase', 'env database')
assert.equal(subject.password, 'open', 'env password')

assert.equal(typeof subject.ssl, 'object', 'env ssl')
assert.equal(subject.ssl.cert, certFileContents, 'env ssl cert')
assert.equal(subject.ssl.key, keyFileContents, 'env ssl key')
assert.equal(subject.ssl.ca, caFileContents, 'env ssl ca')
})
})

suite.test('ConnectionParameters initialized from mix', function () {
Expand All @@ -56,6 +93,77 @@ suite.test('ConnectionParameters initialized from mix', function () {
assert.equal(subject.port, 7890, 'env port')
assert.equal(subject.database, 'zugzug', 'config database')
assert.equal(subject.password, defaults.password, 'defaults password')
assert.equal(subject.ssl, false, 'ssl')
})

suite.test('ConnectionParameters initialized from mix - ssl', function () {
createTempTlsFilesAndExecute(function(
certFilePath, keyFilePath, caFilePath,
certFileContents, keyFileContents, caFileContents
) {
clearEnv()
process.env['PGHOST'] = 'local'
process.env['PGUSER'] = 'bmc2'
process.env['PGPORT'] = 7890
process.env['PGDATABASE'] = 'allyerbase'
process.env['PGPASSWORD'] = 'open'
process.env['PGSSLMODE'] = 'verify-full'
process.env['PGSSLCERT'] = certFilePath
process.env['PGSSLKEY'] = keyFilePath
delete process.env['PGPASSWORD']
delete process.env['PGDATABASE']

var subject = new ConnectionParameters({
// The connection string will mostly override this config. See ConnectionParameters constructor.
user: 'testing',
database: 'zugzug',
ssl: {
ca: caFileContents
},
connectionString: "postgres://user2:pass2@host2:9999/db2"
})
assert.equal(subject.host, 'host2', 'string host')
assert.equal(subject.user, 'user2', 'string user')
assert.equal(subject.port, 9999, 'string port')
assert.equal(subject.database, 'db2', 'string database')
assert.equal(subject.password, 'pass2', 'string password')

assert.equal(typeof subject.ssl, 'object', 'env ssl')
assert.equal(subject.ssl.cert, certFileContents, 'env ssl cert')
assert.equal(subject.ssl.key, keyFileContents, 'env ssl key')
assert.equal(subject.ssl.ca, caFileContents, 'config ssl ca')
})
})

suite.test('ConnectionParameters initialized from config - ssl', function () {
createTempTlsFilesAndExecute(function(
certFilePath, keyFilePath, caFilePath,
certFileContents, keyFileContents, caFileContents
) {
clearEnv()
var subject = new ConnectionParameters({
host: 'local',
user: 'testing',
password: 'open',
port: 7890,
database: 'zugzug',
ssl: {
cert: certFileContents,
key: keyFileContents,
ca: caFileContents
}
})
assert.equal(subject.host, 'local', 'env host')
assert.equal(subject.user, 'testing', 'config user')
assert.equal(subject.port, 7890, 'env port')
assert.equal(subject.database, 'zugzug', 'config database')
assert.equal(subject.password, 'open', 'defaults password')

assert.equal(typeof subject.ssl, 'object', 'config ssl')
assert.equal(subject.ssl.cert, certFileContents, 'config ssl cert')
assert.equal(subject.ssl.key, keyFileContents, 'config ssl key')
assert.equal(subject.ssl.ca, caFileContents, 'config ssl ca')
})
})

suite.test('connection string parsing', function () {
Expand All @@ -67,6 +175,7 @@ suite.test('connection string parsing', function () {
assert.equal(subject.password, 'pw', 'string password')
assert.equal(subject.port, 381, 'string port')
assert.equal(subject.database, 'lala', 'string database')
assert.equal(subject.ssl, false, 'ssl')
})

suite.test('connection string parsing - ssl', function () {
Expand Down Expand Up @@ -104,6 +213,33 @@ suite.test('ssl is false by default', function () {
assert.equal(subject.ssl, false)
})

// Create temp TLS certificate-mock files and run test logic inside this context
function createTempTlsFilesAndExecute(callback) {
tmp.dir(function _tempDirCreated(err, tmpdir) {
if (err) throw err;

const certFilePath = tmpdir + '/client.crt'
const keyFilePath = tmpdir + '/client.key'
const caFilePath = tmpdir + '/ca.crt'

const certFileContents = 'client cert file'
const keyFileContents = 'client key file'
const caFileContents = 'CA cert file'

fs.appendFileSync(certFilePath, certFileContents, function (err) {
if (err) throw err;
})
fs.appendFileSync(keyFilePath, keyFileContents, function (err) {
if (err) throw err;
})
fs.appendFileSync(caFilePath, caFileContents, function (err) {
if (err) throw err;
})

callback(certFilePath, keyFilePath, caFilePath, certFileContents, keyFileContents, caFileContents)
})
}

var testVal = function (mode, expected) {
suite.test('ssl is ' + expected + ' when $PGSSLMODE=' + mode, function () {
clearEnv()
Expand Down