2024-07-04 03:29:44 -07:00
|
|
|
import { createServer, type AddressInfo } from 'node:net';
|
2023-04-03 08:18:01 -07:00
|
|
|
import pgPromise from 'pg-promise';
|
2024-04-09 08:41:51 -07:00
|
|
|
import type {
|
2024-07-04 03:29:44 -07:00
|
|
|
IExecuteFunctions,
|
|
|
|
ICredentialTestFunctions,
|
|
|
|
ILoadOptionsFunctions,
|
|
|
|
ITriggerFunctions,
|
|
|
|
} from 'n8n-workflow';
|
|
|
|
|
|
|
|
import { formatPrivateKey } from '@utils/utilities';
|
|
|
|
import type {
|
|
|
|
ConnectionsData,
|
|
|
|
PgpConnectionParameters,
|
2024-04-09 08:41:51 -07:00
|
|
|
PostgresNodeCredentials,
|
|
|
|
PostgresNodeOptions,
|
|
|
|
} from '../helpers/interfaces';
|
2024-07-04 03:29:44 -07:00
|
|
|
import { LOCALHOST } from '@utils/constants';
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
const getPostgresConfig = (
|
|
|
|
credentials: PostgresNodeCredentials,
|
|
|
|
options: PostgresNodeOptions = {},
|
|
|
|
) => {
|
|
|
|
const dbConfig: PgpConnectionParameters = {
|
|
|
|
host: credentials.host,
|
|
|
|
port: credentials.port,
|
|
|
|
database: credentials.database,
|
|
|
|
user: credentials.user,
|
|
|
|
password: credentials.password,
|
|
|
|
keepAlive: true,
|
|
|
|
};
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
if (options.connectionTimeout) {
|
|
|
|
dbConfig.connectionTimeoutMillis = options.connectionTimeout * 1000;
|
|
|
|
}
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
if (options.delayClosingIdleConnection) {
|
|
|
|
dbConfig.keepAliveInitialDelayMillis = options.delayClosingIdleConnection * 1000;
|
2023-04-03 08:18:01 -07:00
|
|
|
}
|
2024-07-04 03:29:44 -07:00
|
|
|
|
|
|
|
if (credentials.allowUnauthorizedCerts === true) {
|
|
|
|
dbConfig.ssl = {
|
|
|
|
rejectUnauthorized: false,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
dbConfig.ssl = !['disable', undefined].includes(credentials.ssl as string | undefined);
|
|
|
|
// @ts-ignore these typings need to be updated
|
|
|
|
dbConfig.sslmode = credentials.ssl || 'disable';
|
|
|
|
}
|
|
|
|
|
|
|
|
return dbConfig;
|
|
|
|
};
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2023-05-19 06:42:24 -07:00
|
|
|
export async function configurePostgres(
|
2024-07-04 03:29:44 -07:00
|
|
|
this: IExecuteFunctions | ICredentialTestFunctions | ILoadOptionsFunctions | ITriggerFunctions,
|
2024-04-09 08:41:51 -07:00
|
|
|
credentials: PostgresNodeCredentials,
|
|
|
|
options: PostgresNodeOptions = {},
|
2024-07-04 03:29:44 -07:00
|
|
|
): Promise<ConnectionsData> {
|
2023-05-19 06:42:24 -07:00
|
|
|
const pgp = pgPromise({
|
|
|
|
// prevent spam in console "WARNING: Creating a duplicate database object for the same connection."
|
2024-07-04 03:29:44 -07:00
|
|
|
// duplicate connections created when auto loading parameters, they are closed immediately after, but several could be open at the same time
|
2023-05-19 06:42:24 -07:00
|
|
|
noWarnings: true,
|
|
|
|
});
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2023-05-04 08:25:54 -07:00
|
|
|
if (typeof options.nodeVersion === 'number' && options.nodeVersion >= 2.1) {
|
|
|
|
// Always return dates as ISO strings
|
|
|
|
[pgp.pg.types.builtins.TIMESTAMP, pgp.pg.types.builtins.TIMESTAMPTZ].forEach((type) => {
|
|
|
|
pgp.pg.types.setTypeParser(type, (value: string) => {
|
|
|
|
return new Date(value).toISOString();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-04-03 08:18:01 -07:00
|
|
|
if (options.largeNumbersOutput === 'numbers') {
|
|
|
|
pgp.pg.types.setTypeParser(20, (value: string) => {
|
|
|
|
return parseInt(value, 10);
|
|
|
|
});
|
|
|
|
pgp.pg.types.setTypeParser(1700, (value: string) => {
|
|
|
|
return parseFloat(value);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
const dbConfig = getPostgresConfig(credentials, options);
|
2023-04-03 08:18:01 -07:00
|
|
|
|
|
|
|
if (!credentials.sshTunnel) {
|
|
|
|
const db = pgp(dbConfig);
|
|
|
|
return { db, pgp };
|
|
|
|
} else {
|
2024-07-04 03:29:44 -07:00
|
|
|
if (credentials.sshAuthenticateWith === 'privateKey' && credentials.privateKey) {
|
|
|
|
credentials.privateKey = formatPrivateKey(credentials.privateKey);
|
|
|
|
}
|
|
|
|
const sshClient = await this.helpers.getSSHClient(credentials);
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
// Create a TCP proxy listening on a random available port
|
|
|
|
const proxy = createServer();
|
|
|
|
const proxyPort = await new Promise<number>((resolve) => {
|
|
|
|
proxy.listen(0, LOCALHOST, () => {
|
|
|
|
resolve((proxy.address() as AddressInfo).port);
|
|
|
|
});
|
|
|
|
});
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
const close = () => {
|
|
|
|
proxy.close();
|
|
|
|
sshClient.off('end', close);
|
|
|
|
sshClient.off('error', close);
|
|
|
|
};
|
|
|
|
sshClient.on('end', close);
|
|
|
|
sshClient.on('error', close);
|
2023-04-03 08:18:01 -07:00
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
|
|
proxy.on('error', (err) => reject(err));
|
|
|
|
proxy.on('connection', (localSocket) => {
|
2023-04-03 08:18:01 -07:00
|
|
|
sshClient.forwardOut(
|
2024-07-04 03:29:44 -07:00
|
|
|
LOCALHOST,
|
|
|
|
localSocket.remotePort!,
|
2024-04-09 08:41:51 -07:00
|
|
|
credentials.host,
|
|
|
|
credentials.port,
|
2024-07-04 03:29:44 -07:00
|
|
|
(err, clientChannel) => {
|
|
|
|
if (err) {
|
|
|
|
proxy.close();
|
|
|
|
localSocket.destroy();
|
|
|
|
} else {
|
|
|
|
localSocket.pipe(clientChannel);
|
|
|
|
clientChannel.pipe(localSocket);
|
|
|
|
}
|
2023-04-03 08:18:01 -07:00
|
|
|
},
|
|
|
|
);
|
|
|
|
});
|
2024-07-04 03:29:44 -07:00
|
|
|
resolve();
|
2023-04-03 08:18:01 -07:00
|
|
|
}).catch((err) => {
|
2024-07-04 03:29:44 -07:00
|
|
|
proxy.close();
|
2023-04-03 08:18:01 -07:00
|
|
|
|
|
|
|
let message = err.message;
|
|
|
|
let description = err.description;
|
|
|
|
|
|
|
|
if (err.message.includes('ECONNREFUSED')) {
|
|
|
|
message = 'Connection refused';
|
|
|
|
try {
|
|
|
|
description = err.message.split('ECONNREFUSED ')[1].trim();
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (err.message.includes('ENOTFOUND')) {
|
|
|
|
message = 'Host not found';
|
|
|
|
try {
|
|
|
|
description = err.message.split('ENOTFOUND ')[1].trim();
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (err.message.includes('ETIMEDOUT')) {
|
|
|
|
message = 'Connection timed out';
|
|
|
|
try {
|
|
|
|
description = err.message.split('ETIMEDOUT ')[1].trim();
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
err.message = message;
|
|
|
|
err.description = description;
|
|
|
|
throw err;
|
|
|
|
});
|
|
|
|
|
2024-07-04 03:29:44 -07:00
|
|
|
const db = pgp({
|
|
|
|
...dbConfig,
|
|
|
|
port: proxyPort,
|
|
|
|
host: LOCALHOST,
|
|
|
|
});
|
|
|
|
return { db, pgp };
|
2023-04-03 08:18:01 -07:00
|
|
|
}
|
|
|
|
}
|