Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions packages/pg/lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var Query = require('./query')
var defaults = require('./defaults')
var Connection = require('./connection')
const dns = require('dns')
const { logger } = require('./logger')
const { logger, safeStringify, collectionToJSON, logLazy, redactConnectionString } = require('./logger')
const YB_SERVERS_QUERY = 'SELECT * FROM yb_servers()'
const DEFAULT_FAILED_HOST_TTL_SECONDS = 5

Expand Down Expand Up @@ -58,7 +58,8 @@ class Lock {
const lock = new Lock()
class Client extends EventEmitter {
constructor(config) {
logger.silly("Received connection string " + config)
logLazy('silly', () => "Received connection string: " +
(typeof config === 'string' ? redactConnectionString(config) : safeStringify(config, true)))
super()
this.connectionParameters = new ConnectionParameters(config)
this.user = this.connectionParameters.user
Expand Down Expand Up @@ -159,7 +160,7 @@ class Client extends EventEmitter {
}

getLeastLoadedServer(hostsList) {
logger.silly("getLeastLoadedServer(): hostsList" + [...hostsList])
logLazy('silly', () => "getLeastLoadedServer(): hostsList" + collectionToJSON(hostsList))
if (hostsList.size === 0) {
return this.host
}
Expand All @@ -173,7 +174,7 @@ class Client extends EventEmitter {
} else {
hostServerInfo = new Map(Client.hostServerInfoRR);
}
logger.silly("Potential hosts: " + [...hostServerInfo])
logLazy('silly', () => "Potential hosts: " + collectionToJSON(hostServerInfo))
let minConnectionCount = Number.MAX_VALUE
let leastLoadedHosts = []
for (var i = 1; i <= Client.topologyKeyMap.size; i++) {
Expand Down Expand Up @@ -445,9 +446,9 @@ class Client extends EventEmitter {
}

async iterateHostList(client) {
logger.silly("hostServerInfoPrimary: " + [...Client.hostServerInfoPrimary])
logger.silly("hostServerInfoRR: " + [...Client.hostServerInfoRR])
logger.silly("failedHosts: " + [...Client.failedHosts])
logLazy('silly', () => "hostServerInfoPrimary: " + collectionToJSON(Client.hostServerInfoPrimary))
logLazy('silly', () => "hostServerInfoRR: " + collectionToJSON(Client.hostServerInfoRR))
logLazy('silly', () => "failedHosts: " + collectionToJSON(Client.failedHosts))
let upHostsList = [...Client.hostServerInfoPrimary.keys(), ...Client.hostServerInfoRR.keys()][Symbol.iterator]()
let upHost = upHostsList.next()
let hostIsUp = false
Expand Down Expand Up @@ -596,8 +597,8 @@ class Client extends EventEmitter {
}
}
})
logger.debug("Updated hostServerInfoPrimary to " + [...Client.hostServerInfoPrimary] + " and usePublic to " + Client.usePublic)
logger.debug("Updated hostServerInfoRR to " + [...Client.hostServerInfoRR] + " and usePublic to " + Client.usePublic)
logLazy('debug', () => "Updated hostServerInfoPrimary to " + collectionToJSON(Client.hostServerInfoPrimary) + " and usePublic to " + Client.usePublic)
logLazy('debug', () => "Updated hostServerInfoRR to " + collectionToJSON(Client.hostServerInfoRR) + " and usePublic to " + Client.usePublic)
}

createConnectionMap(data) {
Expand All @@ -621,7 +622,7 @@ class Client extends EventEmitter {
}
}
})
logger.debug("Updated connection map " + [...Client.connectionMap])
logLazy('debug', () => "Updated connection map " + collectionToJSON(Client.connectionMap))
}

createTopologyKeyMap() {
Expand All @@ -646,7 +647,7 @@ class Client extends EventEmitter {
throw new Error('Bad Topology Key found - ' + key)
}
}
logger.debug("Updated topologyKey Map " + [...Client.topologyKeyMap])
logLazy('debug', () => "Updated topologyKey Map " + collectionToJSON(Client.topologyKeyMap))
}

createMetaData(data) {
Expand Down Expand Up @@ -772,8 +773,8 @@ class Client extends EventEmitter {
Client.connectionMap.delete(eachHost)
}
}
logger.debug("Updated connection Map after refresh " + [...Client.connectionMap]);
logger.debug("Updated failed host list after refresh " + [...Client.failedHosts]);
logLazy('debug', () => "Updated connection Map after refresh " + collectionToJSON(Client.connectionMap));
logLazy('debug', () => "Updated failed host list after refresh " + collectionToJSON(Client.failedHosts));
}

updateMetaData(data) {
Expand Down
112 changes: 111 additions & 1 deletion packages/pg/lib/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,117 @@ var setLogLevel = (level) => {
logger.info('Log level set to ' + level);
};

const REDACTED_PLACEHOLDER = '****';

/*
Key names whose values are secrets and must never be written to logs/disk.
Matching is done on a normalized key (lowercased, non-alphanumerics removed)
against these substrings, so casing/separators don't matter: "Password",
"PASSWORD", "db_password", "user-password" and "userPassword" all match.

The list is a superset of the node-postgres config secrets (`password`,
`connectionString` which is handled separately, and the `ssl` fields
`passphrase`/`pfx`/`key`) plus the common credential names used elsewhere
(mirroring what logging libraries like pino/fast-redact redact by default).
Password spellings/typos are covered explicitly rather than with a fuzzy
regex, to avoid redacting unrelated keys.
*/
const REDACTED_KEY_SUBSTRINGS = [
// password family (incl. common misspellings)
'password', 'passwrd', 'passwd', 'psswrd', 'passphrase', 'passcode', 'passkey', 'pwd',
// generic credentials / secrets
'secret', 'token', 'credential', 'apikey', 'accesskey', 'privatekey', 'authorization',
'sessionid', 'cookie'
];

// TLS/SSL config objects (node-postgres passes `ssl` straight to tls.connect)
// carry private-key material under bare keys like `key`/`pfx`/`passphrase`.
// Those bare names are too generic to redact everywhere, so we only redact
// them when the surrounding object looks like a TLS config.
const TLS_SECRET_KEYS = new Set(['key', 'pfx', 'passphrase']);
const looksLikeTlsConfig = (holder) =>
holder !== null && typeof holder === 'object' &&
('cert' in holder || 'ca' in holder || 'pfx' in holder ||
'rejectUnauthorized' in holder || 'passphrase' in holder);

const isSecretKey = (key, holder) => {
if (typeof key !== 'string' || key.length === 0) {
return false;
}
const normalized = key.toLowerCase().replace(/[^a-z0-9]/g, '');
if (REDACTED_KEY_SUBSTRINGS.some((s) => normalized.includes(s))) {
return true;
}
return TLS_SECRET_KEYS.has(normalized) && looksLikeTlsConfig(holder);
};

// Redact the password embedded in a connection string, e.g.
// postgresql://user:secret@host/db -> postgresql://user:****@host/db
const redactConnectionString = (str) =>
str.replace(/(:\/\/[^:/?#@\s]+:)[^@/\s]+@/, '$1' + REDACTED_PLACEHOLDER + '@');

/*
Throw-safe JSON serializer for log messages. Unlike a bare JSON.stringify it:
- never throws (circular refs and BigInt are handled, other errors are caught),
so a log line can never break the code path it is instrumenting; and
- redacts secret fields so they are not persisted to disk via the winston
File transport. A `connectionString` value has only its embedded password
stripped (the rest stays useful for debugging); every other secret field
is fully masked.
*/
const safeStringify = (value, pretty = false) => {
const seen = new WeakSet();
try {
// Uses a non-arrow function so `this` is the object holding `key`,
// which lets us apply context-sensitive rules (e.g. TLS `key`).
return JSON.stringify(value, function (key, val) {
if (key === 'connectionString' && typeof val === 'string') {
return redactConnectionString(val);
}
if (isSecretKey(key, this)) {
return val == null ? val : REDACTED_PLACEHOLDER;
}
if (typeof val === 'bigint') {
return val.toString();
}
if (val !== null && typeof val === 'object') {
if (seen.has(val)) {
return '[Circular]';
}
seen.add(val);
}
return val;
}, pretty ? 2 : undefined);
} catch (err) {
return '[unserializable: ' + err.message + ']';
}
};

// Serialize a Map/Set (or any iterable) consistently and throw-safely.
// Maps are rendered as their [key, value] entries; Sets/arrays as their values.
const collectionToJSON = (collection) => {
const entries = collection instanceof Map
? Array.from(collection.entries())
: Array.from(collection);
return safeStringify(entries);
};

/*
Log a message only when the given level is active, building it lazily via the
supplied callback. This avoids eagerly running (potentially expensive)
serialization on hot paths when the message would be discarded anyway.
*/
const logLazy = (level, buildMessage) => {
if (logger.isLevelEnabled(level)) {
logger.log(level, buildMessage());
}
};

module.exports = {
logger,
setLogLevel
setLogLevel,
safeStringify,
collectionToJSON,
logLazy,
redactConnectionString
};