Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Open
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
1,295 changes: 1,065 additions & 230 deletions 1,295 package-lock.json

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions 20 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,20 +63,20 @@
"@sequelize/mssql": "^7.0.0-alpha.29",
"@sequelize/mysql": "^7.0.0-alpha.29",
"@sequelize/postgres": "^7.0.0-alpha.29",
"@types/node": "^24.12.4",
"@types/node": "^24.13.3",
"@types/pg": "^8.20.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-prettier": "^5.5.6",
"gts": "^5.3.1",
"knex": "^3.2.10",
"mssql": "^12.5.4",
"mysql2": "^3.22.3",
"nock": "^14.0.15",
"pg": "^8.21.0",
"knex": "^3.3.0",
"mssql": "^12.7.0",
"mysql2": "^3.23.1",
"nock": "^14.0.16",
"pg": "^8.22.0",
"prisma": "^5.22.0",
"tap": "^21.7.4",
"tedious": "^20.0.0",
"typeorm": "^1.0.0",
"typeorm": "^1.1.0",
"typescript": "^5.9.3"
},
"engines": {
Expand All @@ -88,8 +88,8 @@
},
"dependencies": {
"@googleapis/sqladmin": "^37.0.0",
"gaxios": "^7.1.4",
"google-auth-library": "^10.6.2",
"gaxios": "^7.3.0",
"google-auth-library": "^10.9.1",
"p-throttle": "^8.1.0"
}
}
7 changes: 5 additions & 2 deletions 7 src/cloud-sql-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ interface Fetcher {
publicKey: string,
authType: AuthTypes
): Promise<SslCert>;
resolveConnectSettings(region: string, dnsName: string): Promise<string>;
}

interface CloudSQLInstanceOptions {
Expand All @@ -72,7 +73,8 @@ export class CloudSQLInstance {
): Promise<CloudSQLInstance> {
const instanceInfo = await resolveInstanceName(
options.instanceConnectionName,
options.domainName
options.domainName,
options.sqlAdminFetcher
);
const instance = new CloudSQLInstance({
options: options,
Expand Down Expand Up @@ -383,7 +385,8 @@ export class CloudSQLInstance {

const newInfo = await resolveInstanceName(
undefined,
this.instanceInfo.domainName
this.instanceInfo.domainName,
this.sqlAdminFetcher
);
if (!isSameInstance(this.instanceInfo, newInfo)) {
Comment thread
kgala2 marked this conversation as resolved.
// Domain name changed. Close and remove, then create a new map entry.
Expand Down
23 changes: 19 additions & 4 deletions 23 src/dns-lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import dns from 'node:dns';
import {CloudSQLConnectorError} from './errors';

export async function resolveTxtRecord(name: string): Promise<string> {
export async function resolveTxtRecord(name: string): Promise<string[]> {
return new Promise((resolve, reject) => {
dns.resolveTxt(name, (err, addresses) => {
if (err) {
Expand All @@ -41,10 +41,9 @@ export async function resolveTxtRecord(name: string): Promise<string> {

// Each result may be split into multiple strings. Join the strings.
const joinedAddresses = addresses.map(strs => strs.join(''));
// Sort the results alphabetically for consistency,
// Sort the results alphabetically for consistency.
joinedAddresses.sort((a, b) => a.localeCompare(b));
// Return the first result.
resolve(joinedAddresses[0]);
resolve(joinedAddresses);
});
});
}
Expand All @@ -60,3 +59,19 @@ export async function resolveARecord(name: string): Promise<string[]> {
});
});
}

export async function resolveCnameRecord(name: string): Promise<string> {
return new Promise((resolve, reject) => {
dns.resolveCname(name, (err, addresses) => {
if (err) {
reject(err);
return;
}
if (!addresses || addresses.length === 0) {
reject(new Error('No CNAME records found for ' + name));
return;
}
resolve(addresses[0]);
});
});
}
185 changes: 168 additions & 17 deletions 185 src/parse-instance-connection-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,108 @@

import {InstanceConnectionInfo} from './instance-connection-info';
import {CloudSQLConnectorError} from './errors';
import {resolveTxtRecord} from './dns-lookup';
import {resolveTxtRecord, resolveCnameRecord} from './dns-lookup';

export interface DNSFetcher {
resolveConnectSettings(region: string, dnsName: string): Promise<string>;
}

export function parseInstanceDNSName(dnsName: string): {
instanceLabel: string;
projectLabel: string;
region: string;
suffix: string;
ok: boolean;
} {
let name = dnsName.toLowerCase();
if (name.endsWith('.')) {
name = name.slice(0, -1);
}

const parts = name.split('.');
if (parts.length !== 5) {
return {
instanceLabel: '',
projectLabel: '',
region: '',
suffix: '',
ok: false,
};
}

if (parts[4] !== 'goog') {
return {
instanceLabel: '',
projectLabel: '',
region: '',
suffix: '',
ok: false,
};
}

const suffixType = parts[3];
if (
suffixType !== 'sql' &&
suffixType !== 'sql-psa' &&
suffixType !== 'sql-psc'
) {
return {
instanceLabel: '',
projectLabel: '',
region: '',
suffix: '',
ok: false,
};
}

const instanceLabel = parts[0];
const projectLabel = parts[1];
const region = parts[2];
const suffix = suffixType + '.goog';

if (region === 'global') {
return {
instanceLabel: '',
projectLabel: '',
region: '',
suffix: '',
ok: false,
};
}

if (instanceLabel.length !== 12) {
return {
instanceLabel: '',
projectLabel: '',
region: '',
suffix: '',
ok: false,
};
}

// Validate instanceLabel is hex
if (!/^[0-9a-f]{12}$/.test(instanceLabel)) {
return {
instanceLabel: '',
projectLabel: '',
region: '',
suffix: '',
ok: false,
};
}

if (!region.includes('-')) {
return {
instanceLabel: '',
projectLabel: '',
region: '',
suffix: '',
ok: false,
};
}

return {instanceLabel, projectLabel, region, suffix, ok: true};
}

export function isSameInstance(
a: InstanceConnectionInfo,
Expand All @@ -30,7 +131,8 @@ export function isSameInstance(

export async function resolveInstanceName(
instanceConnectionName?: string,
domainName?: string
domainName?: string,
fetcher?: DNSFetcher
): Promise<InstanceConnectionInfo> {
if (!instanceConnectionName && !domainName) {
throw new CloudSQLConnectorError({
Expand All @@ -44,7 +146,7 @@ export async function resolveInstanceName(
) {
return parseInstanceConnectionName(instanceConnectionName);
} else if (domainName && isValidDomainName(domainName)) {
return await resolveDomainName(domainName);
return await resolveDomainName(domainName, fetcher);
} else {
throw new CloudSQLConnectorError({
message:
Expand Down Expand Up @@ -73,23 +175,72 @@ export function isInstanceConnectionName(name: string): boolean {
}

export async function resolveDomainName(
name: string
name: string,
fetcher?: DNSFetcher
): Promise<InstanceConnectionInfo> {
const icn = await resolveTxtRecord(name);
if (!isInstanceConnectionName(icn)) {
throw new CloudSQLConnectorError({
message:
'Malformed instance connection name returned for domain ' +
name +
' : ' +
icn,
code: 'EBADDOMAINCONNECTIONNAME',
});
let current = name;
let txtErr: Error | undefined;

for (let depth = 0; depth < 10; depth++) {
const dnsInfo = parseInstanceDNSName(current);
if (dnsInfo.ok) {
if (!fetcher) {
throw new CloudSQLConnectorError({
message: 'DNS resolver SQL Admin API client is not initialized',
code: 'EDNSRESOLVERNOTINITIALIZED',
});
}
const resolvedName = await fetcher.resolveConnectSettings(
dnsInfo.region,
current
);
const info = parseInstanceConnectionName(resolvedName);
info.domainName = name;
return info;
}

try {
const records = await resolveTxtRecord(current);
for (const record of records) {
if (isInstanceConnectionName(record)) {
const info = parseInstanceConnectionName(record);
info.domainName = name;
return info;
}
}
txtErr = new CloudSQLConnectorError({
message: `No valid TXT records found for ${current}`,
code: 'ENOPPSCVALIDTXT',
});
} catch (e) {
txtErr = e as Error;
}

try {
let cnameVal = await resolveCnameRecord(current);
if (cnameVal.endsWith('.')) {
cnameVal = cnameVal.slice(0, -1);
}
if (cnameVal === current) {
throw new Error('CNAME record loop detected or record not found');
}
if (!isValidDomainName(cnameVal)) {
throw new Error(`Invalid format for CNAME record: ${cnameVal}`);
}
current = cnameVal;
} catch (cnameErr) {
throw new CloudSQLConnectorError({
message: `No DNS record found for ${name}, lookup of ${current}. Lookup TXT error: ${txtErr?.message} Lookup CNAME error: ${(cnameErr as Error).message}`,
code: 'EDOMAINNAMELOOKUPFAILED',
errors: [txtErr!, cnameErr as Error],
});
}
}

const info = parseInstanceConnectionName(icn);
info.domainName = name;
return info;
throw new CloudSQLConnectorError({
message: `CNAME lookup limit exceeded (max 10) for ${name}`,
code: 'ECNAMELOOPLIMITEXCEEDED',
});
}

export function parseInstanceConnectionName(
Expand Down
44 changes: 44 additions & 0 deletions 44 src/sqladmin-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export interface SQLAdminFetcherOptions {
export class SQLAdminFetcher {
private readonly client: sqladmin_v1beta4.Sqladmin;
private readonly auth: GoogleAuth<AuthClient>;
private readonly adminAuth: GoogleAuth<AuthClient>;

constructor({
loginAuth,
Expand All @@ -105,6 +106,7 @@ export class SQLAdminFetcher {
scopes: ['https://www.googleapis.com/auth/sqlservice.admin'],
});
}
this.adminAuth = auth;

this.client = new Sqladmin({
rootUrl: sqlAdminAPIEndpoint,
Expand Down Expand Up @@ -321,4 +323,46 @@ export class SQLAdminFetcher {
expirationTime: nearestExpiration,
};
}

async resolveConnectSettings(
region: string,
dnsName: string
): Promise<string> {
const client = await this.adminAuth.getClient();
const rootUrl =
this.client.context._options.rootUrl ||
'https://sqladmin.googleapis.com/';

let dnsNameWithDot = dnsName;
if (!dnsNameWithDot.endsWith('.')) {
dnsNameWithDot += '.';
}

const url = `${rootUrl}sql/v1beta4/locations/${region}/dns/${dnsNameWithDot}:resolveConnectSettings`;

setupGaxiosConfig();
try {
const res = await client.request<{connectionName?: string}>({
url,
method: 'GET',
});
if (!res.data || !res.data.connectionName) {
throw new CloudSQLConnectorError({
message: `Failed to resolve connect settings for DNS name: ${dnsName}`,
code: 'ENOSQLADMINRESOLVE',
});
}
return res.data.connectionName;
} catch (e) {
throw new CloudSQLConnectorError({
message:
`Failed to resolve connect settings for DNS name: ${dnsName}. ` +
'Ensure network connectivity and validate the provided DNS name.',
code: 'ENOSQLADMINRESOLVE',
errors: [e as Error],
});
} finally {
cleanGaxiosConfig();
}
}
}
3 changes: 3 additions & 0 deletions 3 test/cloud-sql-instance-dns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ t.test('CloudSQLInstance DNS Lookup', async t => {
'../src/dns-lookup': {
resolveARecord: async (name: string) => resolveARecordMock(name),
resolveTxtRecord: async (name: string) => resolveTXTRecordMock(name),
resolveCnameRecord: async () => {
throw new Error('CNAME not mocked');
},
},
});

Expand Down
Loading
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.