Skip to content
12 changes: 11 additions & 1 deletion packages/pg-connection-string/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,22 @@ Query parameters follow a `?` character, including the following special query p
* `host=<host>` - sets `host` property, overriding the URL's host
* `encoding=<encoding>` - sets the `client_encoding` property
* `ssl=1`, `ssl=true`, `ssl=0`, `ssl=false` - sets `ssl` to true or false, accordingly
* `sslmode=<sslmode>`
* `uselibpqcompat=true` - use libpq semantics
* `sslmode=<sslmode>` when `sslcompat` is not set
* `sslmode=disable` - sets `ssl` to false
* `sslmode=no-verify` - sets `ssl` to `{ rejectUnauthorized: false }`
* `sslmode=prefer`, `sslmode=require`, `sslmode=verify-ca`, `sslmode=verify-full` - sets `ssl` to true
* `sslmode=<sslmode>` when `sslcompat=libpq`
* `sslmode=disable` - sets `ssl` to false
* `sslmode=prefer` - sets `ssl` to `{ rejectUnauthorized: false }`
* `sslmode=require` - sets `ssl` to `{ rejectUnauthorized: false }` unless `sslrootcert` is specified, in which case it behaves like `verify-ca`
* `sslmode=verify-ca` - sets `ssl` to `{ checkServerIdentity: no-op }` (verify CA, but not server identity). This verifies the presented certificate against the effective CA specified in sslrootcert.
* `sslmode=verify-full` - sets `ssl` to `{}` (verify CA and server identity)
* `sslcert=<filename>` - reads data from the given file and includes the result as `ssl.cert`
* `sslkey=<filename>` - reads data from the given file and includes the result as `ssl.key`
* `sslrootcert=<filename>` - reads data from the given file and includes the result as `ssl.ca`

A bare relative URL, such as `salesdata`, will indicate a database name while leaving other properties empty.

> [!CAUTION]
> Choosing an sslmode other than verify-full has serious security implications. Please read https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS to understand the trade-offs.
7 changes: 6 additions & 1 deletion packages/pg-connection-string/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { ClientConfig } from 'pg'

export function parse(connectionString: string): ConnectionOptions
export function parse(connectionString: string, options: Options): ConnectionOptions

export interface Options {
// Use libpq semantics when interpreting the connection string
useLibpqCompat?: boolean
}

export interface ConnectionOptions {
host: string | null
Expand Down
66 changes: 52 additions & 14 deletions packages/pg-connection-string/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//MIT License

//parses a connection string
function parse(str) {
function parse(str, options = {}) {
//unix socket
if (str.charAt(0) === '/') {
const config = str.split(' ')
Expand Down Expand Up @@ -87,20 +87,58 @@ function parse(str) {
config.ssl.ca = fs.readFileSync(config.sslrootcert).toString()
}

switch (config.sslmode) {
case 'disable': {
config.ssl = false
break
}
case 'prefer':
case 'require':
case 'verify-ca':
case 'verify-full': {
break
if (options.useLibpqCompat && config.uselibpqcompat) {
throw new Error('Both useLibpqCompat and uselibpqcompat are set. Please use only one of them.')
}

if (config.uselibpqcompat === 'true' || options.useLibpqCompat) {
switch (config.sslmode) {
case 'disable': {
config.ssl = false
break
}
case 'prefer': {
config.ssl.rejectUnauthorized = false
break
}
case 'require': {
if (config.sslrootcert) {
// If a root CA is specified, behavior of `sslmode=require` will be the same as that of `verify-ca`
config.ssl.checkServerIdentity = function () {}
} else {
config.ssl.rejectUnauthorized = false
}
break
}
case 'verify-ca': {
if (!config.ssl.ca) {
throw new Error(
'SECURITY WARNING: Using sslmode=verify-ca requires specifying a CA with sslrootcert. If a public CA is used, verify-ca allows connections to a server that somebody else may have registered with the CA, making you vulnerable to Man-in-the-Middle attacks. Either specify a custom CA certificate with sslrootcert parameter or use sslmode=verify-full for proper security.'
)
}
config.ssl.checkServerIdentity = function () {}
break
}
case 'verify-full': {
break
}
}
case 'no-verify': {
config.ssl.rejectUnauthorized = false
break
} else {
switch (config.sslmode) {
case 'disable': {
config.ssl = false
break
}
case 'prefer':
case 'require':
case 'verify-ca':
case 'verify-full': {
break
}
case 'no-verify': {
config.ssl.rejectUnauthorized = false
break
}
}
}

Expand Down
109 changes: 109 additions & 0 deletions packages/pg-connection-string/test/parse.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'

var chai = require('chai')
var expect = chai.expect
chai.should()

var parse = require('../').parse
Expand Down Expand Up @@ -287,6 +288,114 @@ describe('parse', function () {
})
})

it('configuration parameter sslmode=disable with uselibpqcompat query param', function () {
var connectionString = 'pg:///?sslmode=disable&uselibpqcompat=true'
var subject = parse(connectionString)
subject.ssl.should.eql(false)
})

it('configuration parameter sslmode=prefer with uselibpqcompat query param', function () {
var connectionString = 'pg:///?sslmode=prefer&uselibpqcompat=true'
var subject = parse(connectionString)
subject.ssl.should.eql({
rejectUnauthorized: false,
})
})

it('configuration parameter sslmode=require with uselibpqcompat query param', function () {
var connectionString = 'pg:///?sslmode=require&uselibpqcompat=true'
var subject = parse(connectionString)
subject.ssl.should.eql({
rejectUnauthorized: false,
})
})

it('configuration parameter sslmode=verify-ca with uselibpqcompat query param', function () {
var connectionString = 'pg:///?sslmode=verify-ca&uselibpqcompat=true'
expect(function () {
parse(connectionString)
}).to.throw()
})

it('configuration parameter sslmode=verify-ca and sslrootcert with uselibpqcompat query param', function () {
var connectionString = 'pg:///?sslmode=verify-ca&uselibpqcompat=true&sslrootcert=' + __dirname + '/example.ca'
var subject = parse(connectionString)
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
expect(subject.ssl.checkServerIdentity()).be.undefined
})

it('configuration parameter sslmode=verify-full with uselibpqcompat query param', function () {
var connectionString = 'pg:///?sslmode=verify-full&uselibpqcompat=true'
var subject = parse(connectionString)
subject.ssl.should.eql({})
})

it('configuration parameter ssl=true and sslmode=require still work with sslrootcert=/path/to/ca with uselibpqcompat query param', function () {
var connectionString =
'pg:///?ssl=true&sslrootcert=' + __dirname + '/example.ca&sslmode=require&uselibpqcompat=true'
var subject = parse(connectionString)
subject.ssl.should.have.property('ca', 'example ca\n')
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
expect(subject.ssl.checkServerIdentity()).be.undefined
})

it('configuration parameter sslmode=disable with useLibpqCompat option', function () {
var connectionString = 'pg:///?sslmode=disable'
var subject = parse(connectionString, { useLibpqCompat: true })
subject.ssl.should.eql(false)
})

it('configuration parameter sslmode=prefer with useLibpqCompat option', function () {
var connectionString = 'pg:///?sslmode=prefer'
var subject = parse(connectionString, { useLibpqCompat: true })
subject.ssl.should.eql({
rejectUnauthorized: false,
})
})

it('configuration parameter sslmode=require with useLibpqCompat option', function () {
var connectionString = 'pg:///?sslmode=require'
var subject = parse(connectionString, { useLibpqCompat: true })
subject.ssl.should.eql({
rejectUnauthorized: false,
})
})

it('configuration parameter sslmode=verify-ca with useLibpqCompat option', function () {
var connectionString = 'pg:///?sslmode=verify-ca'
expect(function () {
parse(connectionString, { useLibpqCompat: true })
}).to.throw()
})

it('configuration parameter sslmode=verify-ca and sslrootcert with useLibpqCompat option', function () {
var connectionString = 'pg:///?sslmode=verify-ca&sslrootcert=' + __dirname + '/example.ca'
var subject = parse(connectionString, { useLibpqCompat: true })
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
expect(subject.ssl.checkServerIdentity()).be.undefined
})

it('configuration parameter sslmode=verify-full with useLibpqCompat option', function () {
var connectionString = 'pg:///?sslmode=verify-full'
var subject = parse(connectionString, { useLibpqCompat: true })
subject.ssl.should.eql({})
})

it('configuration parameter ssl=true and sslmode=require still work with sslrootcert=/path/to/ca with useLibpqCompat option', function () {
var connectionString = 'pg:///?ssl=true&sslrootcert=' + __dirname + '/example.ca&sslmode=require'
var subject = parse(connectionString, { useLibpqCompat: true })
subject.ssl.should.have.property('ca', 'example ca\n')
subject.ssl.should.have.property('checkServerIdentity').that.is.a('function')
expect(subject.ssl.checkServerIdentity()).be.undefined
})

it('does not allow sslcompat query parameter and useLibpqCompat option at the same time', function () {
var connectionString = 'pg:///?uselibpqcompat=true'
expect(function () {
parse(connectionString, { useLibpqCompat: true })
}).to.throw()
})

it('allow other params like max, ...', function () {
var subject = parse('pg://myhost/db?max=18&min=4')
subject.max.should.equal('18')
Expand Down