Skip to content

Commit 57b17b0

Browse files
committed
Update packages/pg/lib/connection-parameters.js. Fix SSL issue (brianc#2723)
1 parent 4dbf1af commit 57b17b0

File tree

3 files changed

+188
-2
lines changed

3 files changed

+188
-2
lines changed

Diff for: packages/pg/lib/connection-parameters.js

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
var dns = require('dns')
4+
const _ = require('lodash')
45

56
var defaults = require('./defaults')
67

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

21-
var readSSLConfigFromEnvironment = function () {
22+
var readSSLModeFromEnvironment = function () {
2223
switch (process.env.PGSSLMODE) {
2324
case 'disable':
2425
return false
@@ -33,6 +34,27 @@ var readSSLConfigFromEnvironment = function () {
3334
return defaults.ssl
3435
}
3536

37+
var fillInSSLVariablesFromEnvironment = function(existingSsl) {
38+
var caFileName = val('sslrootcert', existingSsl)
39+
var crtFileName = val('sslcert', existingSsl)
40+
var keyFileName = val('sslkey', existingSsl)
41+
42+
// Only try to load fs if we expect to read from the disk
43+
const fs = caFileName || crtFileName || keyFileName ? require('fs') : null
44+
var result = {}
45+
if (caFileName) {
46+
result.ca = fs.readFileSync(caFileName).toString()
47+
}
48+
if (crtFileName) {
49+
result.cert = fs.readFileSync(crtFileName).toString()
50+
}
51+
if (keyFileName) {
52+
result.key = fs.readFileSync(keyFileName).toString()
53+
}
54+
55+
return result
56+
}
57+
3658
// Convert arg to a string, surround in single quotes, and escape single quotes and backslashes
3759
var quoteParamValue = function (value) {
3860
return "'" + ('' + value).replace(/\\/g, '\\\\').replace(/'/g, "\\'") + "'"
@@ -78,7 +100,7 @@ class ConnectionParameters {
78100
this.binary = val('binary', config)
79101
this.options = val('options', config)
80102

81-
this.ssl = typeof config.ssl === 'undefined' ? readSSLConfigFromEnvironment() : config.ssl
103+
this.ssl = typeof config.ssl === 'undefined' ? readSSLModeFromEnvironment() : config.ssl
82104

83105
if (typeof this.ssl === 'string') {
84106
if (this.ssl === 'true') {
@@ -89,6 +111,23 @@ class ConnectionParameters {
89111
if (this.ssl === 'no-verify') {
90112
this.ssl = { rejectUnauthorized: false }
91113
}
114+
115+
// Fill in possibly missing SSL config parameters from environment variables.
116+
// This lets you mix SSL definitions between connection string, config object and environment variables.
117+
if (this.ssl === true) {
118+
var sslCandidate = fillInSSLVariablesFromEnvironment({})
119+
120+
// Change the type of this.ssl from boolean to object only if relevant environment variables were defined
121+
if (!_.isEmpty(sslCandidate)) {
122+
this.ssl = sslCandidate
123+
}
124+
} else if (typeof this.ssl === 'object') {
125+
var sslCandidate = fillInSSLVariablesFromEnvironment({})
126+
this.ssl = Object.assign(this.ssl, sslCandidate)
127+
}
128+
129+
// "hiding" the private key so it doesn't show up in stack traces
130+
// or if the client is console.logged
92131
if (this.ssl && this.ssl.key) {
93132
Object.defineProperty(this.ssl, 'key', {
94133
enumerable: false,

Diff for: packages/pg/test/integration/connection-pool/tls-tests.js

+11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ const pg = helper.pg
88
const suite = new helper.Suite()
99

1010
if (process.env.PG_CLIENT_CERT_TEST) {
11+
12+
// Test SSL using "options object" constructor
1113
suite.testAsync('client certificate', async () => {
1214
const pool = new pg.Pool({
1315
ssl: {
@@ -20,4 +22,13 @@ if (process.env.PG_CLIENT_CERT_TEST) {
2022
await pool.query('SELECT 1')
2123
await pool.end()
2224
})
25+
26+
// Test SSL by only defining environment variables without any explicit reference to the cert files in the code
27+
// to be compliant with lib-pg: https://www.postgresql.org/docs/current/libpq-envars.html
28+
suite.testAsync('client certificate', async () => {
29+
const pool = new pg.Pool({})
30+
31+
await pool.query('SELECT 1')
32+
await pool.end()
33+
})
2334
}

Diff for: packages/pg/test/unit/connection-parameters/environment-variable-tests.js

+136
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ var helper = require('../test-helper')
33
const Suite = require('../../suite')
44

55
var assert = require('assert')
6+
const fs = require('fs')
7+
8+
const tmp = require('tmp')
9+
tmp.setGracefulCleanup()
10+
611
var ConnectionParameters = require('../../../lib/connection-parameters')
712
var defaults = require('../../../lib').defaults
813

@@ -36,6 +41,38 @@ suite.test('ConnectionParameters initialized from environment variables', functi
3641
assert.equal(subject.port, 7890, 'env port')
3742
assert.equal(subject.database, 'allyerbase', 'env database')
3843
assert.equal(subject.password, 'open', 'env password')
44+
assert.equal(subject.ssl, false, 'ssl')
45+
})
46+
47+
suite.test('ConnectionParameters initialized from environment variables - ssl', function () {
48+
createTempTlsFilesAndExecute(function(
49+
certFilePath, keyFilePath, caFilePath,
50+
certFileContents, keyFileContents, caFileContents
51+
) {
52+
clearEnv()
53+
process.env['PGHOST'] = 'local'
54+
process.env['PGUSER'] = 'bmc2'
55+
process.env['PGPORT'] = 7890
56+
process.env['PGDATABASE'] = 'allyerbase'
57+
process.env['PGPASSWORD'] = 'open'
58+
59+
process.env['PGSSLMODE'] = 'verify-full'
60+
process.env['PGSSLCERT'] = certFilePath
61+
process.env['PGSSLKEY'] = keyFilePath
62+
process.env['PGSSLROOTCERT'] = caFilePath
63+
64+
var subject = new ConnectionParameters()
65+
assert.equal(subject.host, 'local', 'env host')
66+
assert.equal(subject.user, 'bmc2', 'env user')
67+
assert.equal(subject.port, 7890, 'env port')
68+
assert.equal(subject.database, 'allyerbase', 'env database')
69+
assert.equal(subject.password, 'open', 'env password')
70+
71+
assert.equal(typeof subject.ssl, 'object', 'env ssl')
72+
assert.equal(subject.ssl.cert, certFileContents, 'env ssl cert')
73+
assert.equal(subject.ssl.key, keyFileContents, 'env ssl key')
74+
assert.equal(subject.ssl.ca, caFileContents, 'env ssl ca')
75+
})
3976
})
4077

4178
suite.test('ConnectionParameters initialized from mix', function () {
@@ -56,6 +93,77 @@ suite.test('ConnectionParameters initialized from mix', function () {
5693
assert.equal(subject.port, 7890, 'env port')
5794
assert.equal(subject.database, 'zugzug', 'config database')
5895
assert.equal(subject.password, defaults.password, 'defaults password')
96+
assert.equal(subject.ssl, false, 'ssl')
97+
})
98+
99+
suite.test('ConnectionParameters initialized from mix - ssl', function () {
100+
createTempTlsFilesAndExecute(function(
101+
certFilePath, keyFilePath, caFilePath,
102+
certFileContents, keyFileContents, caFileContents
103+
) {
104+
clearEnv()
105+
process.env['PGHOST'] = 'local'
106+
process.env['PGUSER'] = 'bmc2'
107+
process.env['PGPORT'] = 7890
108+
process.env['PGDATABASE'] = 'allyerbase'
109+
process.env['PGPASSWORD'] = 'open'
110+
process.env['PGSSLMODE'] = 'verify-full'
111+
process.env['PGSSLCERT'] = certFilePath
112+
process.env['PGSSLKEY'] = keyFilePath
113+
delete process.env['PGPASSWORD']
114+
delete process.env['PGDATABASE']
115+
116+
var subject = new ConnectionParameters({
117+
// The connection string will mostly override this config. See ConnectionParameters constructor.
118+
user: 'testing',
119+
database: 'zugzug',
120+
ssl: {
121+
ca: caFileContents
122+
},
123+
connectionString: "postgres://user2:pass2@host2:9999/db2"
124+
})
125+
assert.equal(subject.host, 'host2', 'string host')
126+
assert.equal(subject.user, 'user2', 'string user')
127+
assert.equal(subject.port, 9999, 'string port')
128+
assert.equal(subject.database, 'db2', 'string database')
129+
assert.equal(subject.password, 'pass2', 'string password')
130+
131+
assert.equal(typeof subject.ssl, 'object', 'env ssl')
132+
assert.equal(subject.ssl.cert, certFileContents, 'env ssl cert')
133+
assert.equal(subject.ssl.key, keyFileContents, 'env ssl key')
134+
assert.equal(subject.ssl.ca, caFileContents, 'config ssl ca')
135+
})
136+
})
137+
138+
suite.test('ConnectionParameters initialized from config - ssl', function () {
139+
createTempTlsFilesAndExecute(function(
140+
certFilePath, keyFilePath, caFilePath,
141+
certFileContents, keyFileContents, caFileContents
142+
) {
143+
clearEnv()
144+
var subject = new ConnectionParameters({
145+
host: 'local',
146+
user: 'testing',
147+
password: 'open',
148+
port: 7890,
149+
database: 'zugzug',
150+
ssl: {
151+
cert: certFileContents,
152+
key: keyFileContents,
153+
ca: caFileContents
154+
}
155+
})
156+
assert.equal(subject.host, 'local', 'env host')
157+
assert.equal(subject.user, 'testing', 'config user')
158+
assert.equal(subject.port, 7890, 'env port')
159+
assert.equal(subject.database, 'zugzug', 'config database')
160+
assert.equal(subject.password, 'open', 'defaults password')
161+
162+
assert.equal(typeof subject.ssl, 'object', 'config ssl')
163+
assert.equal(subject.ssl.cert, certFileContents, 'config ssl cert')
164+
assert.equal(subject.ssl.key, keyFileContents, 'config ssl key')
165+
assert.equal(subject.ssl.ca, caFileContents, 'config ssl ca')
166+
})
59167
})
60168

61169
suite.test('connection string parsing', function () {
@@ -67,6 +175,7 @@ suite.test('connection string parsing', function () {
67175
assert.equal(subject.password, 'pw', 'string password')
68176
assert.equal(subject.port, 381, 'string port')
69177
assert.equal(subject.database, 'lala', 'string database')
178+
assert.equal(subject.ssl, false, 'ssl')
70179
})
71180

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

216+
// Create temp TLS certificate-mock files and run test logic inside this context
217+
function createTempTlsFilesAndExecute(callback) {
218+
tmp.dir(function _tempDirCreated(err, tmpdir) {
219+
if (err) throw err;
220+
221+
const certFilePath = tmpdir + '/client.crt'
222+
const keyFilePath = tmpdir + '/client.key'
223+
const caFilePath = tmpdir + '/ca.crt'
224+
225+
const certFileContents = 'client cert file'
226+
const keyFileContents = 'client key file'
227+
const caFileContents = 'CA cert file'
228+
229+
fs.appendFileSync(certFilePath, certFileContents, function (err) {
230+
if (err) throw err;
231+
})
232+
fs.appendFileSync(keyFilePath, keyFileContents, function (err) {
233+
if (err) throw err;
234+
})
235+
fs.appendFileSync(caFilePath, caFileContents, function (err) {
236+
if (err) throw err;
237+
})
238+
239+
callback(certFilePath, keyFilePath, caFilePath, certFileContents, keyFileContents, caFileContents)
240+
})
241+
}
242+
107243
var testVal = function (mode, expected) {
108244
suite.test('ssl is ' + expected + ' when $PGSSLMODE=' + mode, function () {
109245
clearEnv()

0 commit comments

Comments
 (0)