Skip to content

Commit fcf8b79

Browse files
authored
fix: allow connections via IP address as host (#641)
* fix: allow connections via IP address as host Currently, we're passing in a root CA for all requests as SSL option. However, when passing in an IP address, this will always fail. As we are starting to transition storage over to pgbouncer, internal connections require connecting via an IPv6 address. To support this, we need to not include the root CA options, in case we're connecting against an IP. * unify * decode uri component
1 parent c6f5771 commit fcf8b79

File tree

8 files changed

+103
-24
lines changed

8 files changed

+103
-24
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ jobs:
1919
runs-on: ${{ matrix.platform }}
2020

2121
steps:
22-
- uses: actions/checkout@v2
23-
- uses: actions/cache@v1
22+
- uses: actions/checkout@v4
23+
- uses: actions/cache@v4
2424
with:
2525
path: ~/.npm
2626
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
2727
restore-keys: |
2828
${{ runner.os }}-node-
2929
- name: Set up Node
30-
uses: actions/setup-node@v1
30+
uses: actions/setup-node@v4
3131
with:
3232
node-version: ${{ matrix.node }}
3333

.github/workflows/cli.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
git config --global user.email '${{ env.github_user_email }}'
2525
2626
- name: Checkout remote repository
27-
uses: actions/checkout@v2
27+
uses: actions/checkout@v4
2828
with:
2929
repository: '${{ env.cli_repo_owner }}/${{ env.cli_repo }}'
3030
token: ${{ secrets.CLI_PR_USER_ACCESS_TOKEN }}

.github/workflows/docs.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ jobs:
1818
runs-on: ${{ matrix.platform }}
1919

2020
steps:
21-
- uses: actions/checkout@v2
22-
- uses: actions/cache@v1
21+
- uses: actions/checkout@v4
22+
- uses: actions/cache@v4
2323
with:
2424
path: ~/.npm
2525
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
2626
restore-keys: |
2727
${{ runner.os }}-node-
2828
- name: Set up Node
29-
uses: actions/setup-node@v1
29+
uses: actions/setup-node@v4
3030
with:
3131
node-version: ${{ matrix.node }}
3232

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ jobs:
1313
published: ${{ steps.semantic.outputs.new_release_published }}
1414
version: ${{ steps.semantic.outputs.new_release_version }}
1515
steps:
16-
- uses: actions/checkout@v3
16+
- uses: actions/checkout@v4
1717

1818
- name: Set up Node
19-
uses: actions/setup-node@v1
19+
uses: actions/setup-node@v4
2020
with:
2121
node-version: 14 # https://github.com/cycjimmy/semantic-release-action/issues/159
2222

src/internal/database/connection.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DbActiveConnection, DbActivePool } from '../monitoring/metrics'
88
import KnexTimeoutError = knex.KnexTimeoutError
99
import { logger, logSchema } from '../monitoring'
1010
import { ERRORS } from '@internal/errors'
11+
import { getSslSettings } from './util'
1112

1213
// https://github.com/knex/knex/issues/387#issuecomment-51554522
1314
pg.types.setTypeParser(20, 'text', parseInt)
@@ -95,6 +96,8 @@ export class TenantConnection {
9596

9697
const isExternalPool = Boolean(options.isExternalPool)
9798

99+
const sslSettings = getSslSettings({ connectionString, databaseSSLRootCert })
100+
98101
knexPool = knex({
99102
client: 'pg',
100103
version: dbPostgresVersion,
@@ -113,7 +116,7 @@ export class TenantConnection {
113116
connection: {
114117
connectionString: connectionString,
115118
connectionTimeoutMillis: databaseConnectionTimeout,
116-
...this.sslSettings(),
119+
ssl: sslSettings ? { ...sslSettings } : undefined,
117120
},
118121
acquireConnectionTimeout: databaseConnectionTimeout,
119122
})
@@ -143,13 +146,6 @@ export class TenantConnection {
143146
return new this(knexPool, options)
144147
}
145148

146-
protected static sslSettings() {
147-
if (databaseSSLRootCert) {
148-
return { ssl: { ca: databaseSSLRootCert } }
149-
}
150-
return {}
151-
}
152-
153149
async dispose() {
154150
if (this.options.isExternalPool) {
155151
await this.pool.destroy()

src/internal/database/migrations/migrate.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ProgressiveMigrations } from './progressive'
1313
import { RunMigrationsOnTenants, ResetMigrationsOnTenant } from '@storage/events'
1414
import { ERRORS } from '@internal/errors'
1515
import { DBMigration } from './types'
16+
import { getSslSettings } from '../util'
1617

1718
const {
1819
multitenantDatabaseUrl,
@@ -312,16 +313,10 @@ export async function runMigrationsOnTenant(
312313
tenantId?: string,
313314
waitForLock = true
314315
): Promise<void> {
315-
let ssl: ClientConfig['ssl'] | undefined = undefined
316-
317-
if (databaseSSLRootCert) {
318-
ssl = { ca: databaseSSLRootCert }
319-
}
320-
321316
await connectAndMigrate({
322317
databaseUrl,
323318
migrationsDirectory: './migrations/tenant',
324-
ssl,
319+
ssl: getSslSettings({ connectionString: databaseUrl, databaseSSLRootCert }),
325320
shouldCreateStorageSchema: true,
326321
tenantId,
327322
waitForLock,

src/internal/database/util.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { logger } from '@internal/monitoring'
2+
3+
export function getSslSettings({
4+
connectionString,
5+
databaseSSLRootCert,
6+
}: {
7+
connectionString: string
8+
databaseSSLRootCert: string | undefined
9+
}): { ca: string } | undefined {
10+
if (!databaseSSLRootCert) return undefined
11+
12+
try {
13+
// When connecting through PGBouncer, we connect through an IPv6 address rather than a hostname
14+
// When passing in the root CA for SSL, this will always fail, so we need to skip passing in the SSL root cert
15+
// in case the hostname is an IP address
16+
const url = new URL(connectionString)
17+
if (url.hostname && isIpAddress(url.hostname)) {
18+
return undefined
19+
}
20+
} catch (err) {
21+
// ignore to ensure this never breaks the connection in case of an invalid URL
22+
logger.warn(err, 'Failed to parse connection string')
23+
}
24+
25+
return { ca: databaseSSLRootCert }
26+
}
27+
28+
export function isIpAddress(ip: string) {
29+
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/
30+
const ipv6Pattern = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/
31+
32+
// IP might be URL-encoded and won't match the regex unless we decode first
33+
const decodedIp = decodeURIComponent(ip)
34+
return ipv4Pattern.test(decodedIp) || ipv6Pattern.test(decodedIp)
35+
}

src/test/database/util.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { getSslSettings, isIpAddress } from '@internal/database/util'
2+
3+
describe('database utils', () => {
4+
test.each([
5+
['', false],
6+
['test', false],
7+
['db.foobar.supabase.red', false],
8+
['5.5.5.a', false],
9+
['5.5.5.5', true],
10+
['121.212.187.123', true],
11+
['121.212.187.5', true],
12+
['2001:db8:3333:4444:5555:6666:7777:8888', true],
13+
['2001:db8:3333:4444:CCCC:DDDD:EEEE:FFFF', true],
14+
['2406%3Ada18%3A4fd%3A9b09%3A2c76%3A5d38%3Ade30%3A7904', true],
15+
])('is %s ip address, expected %s', (text: string, expected: boolean) => {
16+
expect(isIpAddress(text)).toBe(expected)
17+
})
18+
19+
describe('getSslSettings', () => {
20+
test('should return no SSL settings if database root CA not set', () => {
21+
expect(
22+
getSslSettings({ connectionString: 'foobar', databaseSSLRootCert: undefined })
23+
).toBeUndefined()
24+
})
25+
26+
test('should return no SSL settings if hostname is an IP address', () => {
27+
expect(
28+
getSslSettings({
29+
connectionString: 'postgres://foo:[email protected]:5432/postgres',
30+
databaseSSLRootCert: '<cert>',
31+
})
32+
).toBeUndefined()
33+
})
34+
35+
test('should return SSL settings if hostname is not an IP address', () => {
36+
expect(
37+
getSslSettings({
38+
connectionString: 'postgres://foo:[email protected]:5432/postgres',
39+
databaseSSLRootCert: '<cert>',
40+
})
41+
).toStrictEqual({ ca: '<cert>' })
42+
})
43+
44+
test('should return SSL settings if hostname is not parseable', () => {
45+
expect(
46+
getSslSettings({
47+
connectionString: 'postgres://foo:[email protected]."red:5432/postgres',
48+
databaseSSLRootCert: '<cert>',
49+
})
50+
).toStrictEqual({ ca: '<cert>' })
51+
})
52+
})
53+
})

0 commit comments

Comments
 (0)