Skip to content

Commit 2bb60c0

Browse files
authored
feat: add tls session resumption (#1195)
Signed-off-by: ferhat elmas <elmas.ferhat@gmail.com>
1 parent b66e4c0 commit 2bb60c0

6 files changed

Lines changed: 685 additions & 1 deletion

File tree

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ type StorageConfigType = {
108108
databasePoolURL?: string
109109
databaseMaxConnections: number
110110
databaseFreePoolAfterInactivity: number
111+
databaseTlsSessionResumption: boolean
111112
databasePoolDrainTimeout: number
112113
databaseConnectionTimeout: number
113114
databaseEnableQueryCancellation: boolean
@@ -491,6 +492,8 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
491492
getOptionalConfigFromEnv('DATABASE_FREE_POOL_AFTER_INACTIVITY') || (1000 * 60).toString(),
492493
10
493494
),
495+
databaseTlsSessionResumption:
496+
getOptionalConfigFromEnv('DATABASE_TLS_SESSION_RESUMPTION') === 'true',
494497
databasePoolDrainTimeout: envPositiveInteger(
495498
getOptionalConfigFromEnv('DATABASE_POOL_DRAIN_TIMEOUT'),
496499
30_000

src/internal/database/pg-connection.test.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,24 @@ async function waitForDrainCheck(): Promise<void> {
166166
await new Promise((resolve) => setImmediate(resolve))
167167
}
168168

169+
async function loadPgConnectionModuleWithConfig(configOverrides: Record<string, unknown>) {
170+
vi.resetModules()
171+
172+
const configModule = await import('../../config')
173+
configModule.getConfig({ reload: true })
174+
configModule.mergeConfig({
175+
databaseApplicationName: 'storage-test',
176+
databaseConnectionTimeout: 3000,
177+
databaseFreePoolAfterInactivity: 1000,
178+
databaseMaxConnections: 20,
179+
databaseSSLRootCert: undefined,
180+
databaseTlsSessionResumption: false,
181+
...configOverrides,
182+
} as Parameters<typeof configModule.mergeConfig>[0])
183+
184+
return import('./pg-connection')
185+
}
186+
169187
describe('getPgCancelConnectionTarget', () => {
170188
it('uses direct client host and port for TCP cancel connections', () => {
171189
expect(
@@ -1067,3 +1085,95 @@ describe('PgPoolStrategy', () => {
10671085
}
10681086
})
10691087
})
1088+
1089+
describe('PgPoolStrategy TLS session resumption wiring', () => {
1090+
afterEach(() => {
1091+
vi.resetModules()
1092+
})
1093+
1094+
function createDynamicTestablePgPoolStrategy(PgPoolStrategyCtor: typeof PgPoolStrategy) {
1095+
return class DynamicTestablePgPoolStrategy extends PgPoolStrategyCtor {
1096+
getCurrentPoolForTest(): Pool {
1097+
return this.getPool()
1098+
}
1099+
}
1100+
}
1101+
1102+
it('installs the session getter, slot marker, and custom client when enabled with SSL', async () => {
1103+
const { PgPoolStrategy: DynamicPgPoolStrategy } = await loadPgConnectionModuleWithConfig({
1104+
databaseSSLRootCert: '<cert>',
1105+
databaseTlsSessionResumption: true,
1106+
})
1107+
const { TlsSessionResumptionClient } = await import('./tls-session-resumption')
1108+
const DynamicTestablePgPoolStrategy = createDynamicTestablePgPoolStrategy(DynamicPgPoolStrategy)
1109+
const strategy = new DynamicTestablePgPoolStrategy(
1110+
createPoolStrategySettings({
1111+
dbUrl: 'postgres://postgres:postgres@1.2.3.4:5432/postgres',
1112+
})
1113+
)
1114+
1115+
try {
1116+
const pool = strategy.getCurrentPoolForTest()
1117+
const ssl = pool.options.ssl as object
1118+
1119+
expect(ssl).toBeDefined()
1120+
expect(pool.options.Client).toBe(TlsSessionResumptionClient)
1121+
1122+
const sessionDescriptor = Object.getOwnPropertyDescriptor(ssl, 'session')
1123+
expect(sessionDescriptor?.get).toBeInstanceOf(Function)
1124+
expect(sessionDescriptor?.enumerable).toBe(true)
1125+
expect(sessionDescriptor?.configurable).toBe(true)
1126+
expect(sessionDescriptor?.get?.call(ssl)).toBeUndefined()
1127+
1128+
expect(Object.getOwnPropertySymbols(ssl)).toHaveLength(1)
1129+
const tlsConnectOptions = Object.assign({}, ssl)
1130+
expect(Object.getOwnPropertySymbols(tlsConnectOptions)).toHaveLength(0)
1131+
expect(Object.prototype.hasOwnProperty.call(tlsConnectOptions, 'session')).toBe(true)
1132+
} finally {
1133+
await strategy.destroy()
1134+
}
1135+
})
1136+
1137+
it('leaves SSL options untouched when the feature flag is disabled', async () => {
1138+
const { PgPoolStrategy: DynamicPgPoolStrategy } = await loadPgConnectionModuleWithConfig({
1139+
databaseSSLRootCert: '<cert>',
1140+
databaseTlsSessionResumption: false,
1141+
})
1142+
const DynamicTestablePgPoolStrategy = createDynamicTestablePgPoolStrategy(DynamicPgPoolStrategy)
1143+
const strategy = new DynamicTestablePgPoolStrategy(
1144+
createPoolStrategySettings({
1145+
dbUrl: 'postgres://postgres:postgres@1.2.3.4:5432/postgres',
1146+
})
1147+
)
1148+
1149+
try {
1150+
const pool = strategy.getCurrentPoolForTest()
1151+
const ssl = pool.options.ssl as object
1152+
1153+
expect(ssl).toBeDefined()
1154+
expect(pool.options.Client).toBeUndefined()
1155+
expect(Object.getOwnPropertyDescriptor(ssl, 'session')).toBeUndefined()
1156+
expect(Object.getOwnPropertySymbols(ssl)).toHaveLength(0)
1157+
} finally {
1158+
await strategy.destroy()
1159+
}
1160+
})
1161+
1162+
it('does not install the custom client when SSL settings are absent', async () => {
1163+
const { PgPoolStrategy: DynamicPgPoolStrategy } = await loadPgConnectionModuleWithConfig({
1164+
databaseSSLRootCert: undefined,
1165+
databaseTlsSessionResumption: true,
1166+
})
1167+
const DynamicTestablePgPoolStrategy = createDynamicTestablePgPoolStrategy(DynamicPgPoolStrategy)
1168+
const strategy = new DynamicTestablePgPoolStrategy(createPoolStrategySettings())
1169+
1170+
try {
1171+
const pool = strategy.getCurrentPoolForTest()
1172+
1173+
expect(pool.options.ssl).toBeUndefined()
1174+
expect(pool.options.Client).toBeUndefined()
1175+
} finally {
1176+
await strategy.destroy()
1177+
}
1178+
})
1179+
})

src/internal/database/pg-connection.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import {
1313
TenantConnectionOptions,
1414
} from './pool'
1515
import { getSslSettings } from './ssl'
16+
import {
17+
createTlsSessionSlot,
18+
installTlsSessionResumption,
19+
TlsSessionResumptionClient,
20+
type TlsSessionSlot,
21+
} from './tls-session-resumption'
1622

1723
const {
1824
databaseApplicationName,
@@ -22,6 +28,7 @@ const {
2228
databasePoolDrainTimeout,
2329
databaseSSLRootCert,
2430
databaseStatementTimeout,
31+
databaseTlsSessionResumption,
2532
} = getConfig()
2633

2734
pg.types.setTypeParser(20, 'text', parseInt)
@@ -86,6 +93,7 @@ export type PgCancelConnectionTarget =
8693

8794
export class PgPoolStrategy {
8895
protected pool?: Pool
96+
protected tlsSession?: TlsSessionSlot
8997

9098
constructor(protected readonly options: TenantConnectionOptions) {}
9199

@@ -176,14 +184,21 @@ export class PgPoolStrategy {
176184
databaseSSLRootCert,
177185
})
178186

187+
const ssl = sslSettings ? { ...sslSettings } : undefined
188+
if (databaseTlsSessionResumption && ssl) {
189+
this.tlsSession ??= createTlsSessionSlot()
190+
installTlsSessionResumption(ssl, this.tlsSession)
191+
}
192+
179193
return attachPgPoolErrorHandler(
180194
new Pool({
181195
min: 0,
182196
max: settings.maxConnections,
183197
connectionString: settings.dbUrl,
184198
connectionTimeoutMillis: databaseConnectionTimeout,
185199
idleTimeoutMillis: settings.idleTimeoutMillis,
186-
ssl: sslSettings ? { ...sslSettings } : undefined,
200+
ssl,
201+
Client: databaseTlsSessionResumption && ssl ? TlsSessionResumptionClient : undefined,
187202
application_name: databaseApplicationName,
188203
options: settings.searchPath
189204
? `-c search_path=${settings.searchPath.join(',')}`

0 commit comments

Comments
 (0)