@google-cloud/cloud-sql-connector
Version:
A JavaScript library for connecting securely to your Cloud SQL instances
240 lines • 10.1 kB
JavaScript
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { GoogleAuth } from 'google-auth-library';
import { sqladmin_v1beta4 } from '@googleapis/sqladmin';
import { instance as gaxios } from 'gaxios';
const { Sqladmin } = sqladmin_v1beta4;
import { parseCert } from './crypto.js';
import { CloudSQLConnectorError } from './errors.js';
import { getNearestExpiration } from './time.js';
import { AuthTypes } from './auth-types.js';
// Default values for the list of http methods to retry in gaxios
// ref: https://github.com/googleapis/gaxios/tree/fdb6a8e542f7782f8d1e487c0ef7d301fa231d18#request-options
const defaultGaxiosHttpMethodsToRetry = [
'GET',
'HEAD',
'PUT',
'OPTIONS',
'DELETE',
];
// https://github.com/googleapis/gaxios is the http request library used by
// google-auth-library and other Cloud SDK libraries, this function will set
// a standard default configuration that will ensure retry works as expected
// for internal google-auth-library requests.
function setupGaxiosConfig() {
gaxios.defaults = {
retryConfig: {
retry: 5,
// Make sure to add POST to the list of default methods to retry
// since it's used in IAM generateAccessToken requests that needs retry
httpMethodsToRetry: ['POST', ...defaultGaxiosHttpMethodsToRetry],
// Should retry on non-http error codes such as ECONNRESET, ETIMEOUT, etc
noResponseRetries: 3,
// Defaults to: [[100, 199], [408, 408], [429, 429], [500, 599]]
statusCodesToRetry: [[500, 599]],
// The amount of time to initially delay the retry, in ms. Defaults to 100ms.
retryDelay: 200,
// The multiplier by which to increase the delay time between the
// completion of failed requests, and the initiation of the subsequent
// retrying request. Defaults to 2.
retryDelayMultiplier: 1.618,
},
};
}
const defaultGaxiosConfig = gaxios.defaults;
// resumes the previous default gaxios config in order to reduce the chance of
// affecting other libraries that might be sharing that same gaxios instance
function cleanGaxiosConfig() {
gaxios.defaults = defaultGaxiosConfig;
}
export class SQLAdminFetcher {
constructor({ loginAuth, sqlAdminAPIEndpoint, universeDomain, userAgent, } = {}) {
let auth;
if (loginAuth instanceof GoogleAuth) {
auth = loginAuth;
}
else {
auth = new GoogleAuth({
authClient: loginAuth, // either an `AuthClient` or undefined
scopes: ['https://www.googleapis.com/auth/sqlservice.admin'],
});
}
this.client = new Sqladmin({
rootUrl: sqlAdminAPIEndpoint,
auth: auth,
userAgentDirectives: [
{
product: 'cloud-sql-nodejs-connector',
version: '1.8.2',
comment: userAgent,
},
],
universeDomain: universeDomain,
});
if (loginAuth instanceof GoogleAuth) {
this.auth = loginAuth;
}
else {
this.auth = new GoogleAuth({
authClient: loginAuth, // either an `AuthClient` or undefined
scopes: ['https://www.googleapis.com/auth/sqlservice.login'],
});
}
}
parseIpAddresses(ipResponse, dnsName, dnsNames, pscEnabled) {
const ipAddresses = {};
if (ipResponse) {
for (const ip of ipResponse) {
if (ip.type === 'PRIMARY' && ip.ipAddress) {
ipAddresses.public = ip.ipAddress;
}
if (ip.type === 'PRIVATE' && ip.ipAddress) {
ipAddresses.private = ip.ipAddress;
}
}
}
// Resolve dnsName into IP address for PSC enabled instances.
// Note that we have to check for PSC enablement because CAS instances
// also set the dnsName field.
// Search the dns_names field for the PSC DNS Name.
if (dnsNames) {
for (const dnm of dnsNames) {
if (dnm.name &&
dnm.connectionType === 'PRIVATE_SERVICE_CONNECT' &&
dnm.dnsScope === 'INSTANCE') {
ipAddresses.psc = dnm.name;
break;
}
}
}
// If the psc dns name was not found, use the legacy dns_name field
if (!ipAddresses.psc && dnsName && pscEnabled) {
ipAddresses.psc = dnsName;
}
if (!ipAddresses.public && !ipAddresses.private && !ipAddresses.psc) {
throw new CloudSQLConnectorError({
message: 'Cannot connect to instance, it has no supported IP addresses',
code: 'ENOSQLADMINIPADDRESS',
});
}
return ipAddresses;
}
async getInstanceMetadata({ projectId, regionId, instanceId, }) {
setupGaxiosConfig();
const res = await this.client.connect.get({
project: projectId,
instance: instanceId,
});
if (!res.data) {
throw new CloudSQLConnectorError({
message: `Failed to find metadata on project id: ${projectId} ` +
`and instance id: ${instanceId}. Ensure network connectivity and ` +
'validate the provided `instanceConnectionName` config value',
code: 'ENOSQLADMIN',
});
}
const ipAddresses = this.parseIpAddresses(res.data.ipAddresses, res.data.dnsName, res.data.dnsNames, res.data.pscEnabled);
const { serverCaCert } = res.data;
if (!serverCaCert || !serverCaCert.cert || !serverCaCert.expirationTime) {
throw new CloudSQLConnectorError({
message: 'Cannot connect to instance, no valid CA certificate found',
code: 'ENOSQLADMINCERT',
});
}
const { region } = res.data;
if (!region) {
throw new CloudSQLConnectorError({
message: 'Cannot connect to instance, no valid region found',
code: 'ENOSQLADMINREGION',
});
}
if (region !== regionId) {
throw new CloudSQLConnectorError({
message: `Provided region was mismatched. Got ${region}, want ${regionId}`,
code: 'EBADSQLADMINREGION',
});
}
cleanGaxiosConfig();
// Find a DNS name to use to validate the certificate from the dns_names field. Any
// name in the list may be used to validate the server TLS certificate.
// Fall back to legacy dns_name field if necessary.
let serverName = null;
if (res.data.dnsNames && res.data.dnsNames.length > 0) {
serverName = res.data.dnsNames[0].name;
}
if (serverName === null) {
serverName = res.data.dnsName;
}
return {
ipAddresses,
serverCaCert: {
cert: serverCaCert.cert,
expirationTime: serverCaCert.expirationTime,
},
serverCaMode: res.data.serverCaMode || '',
dnsName: serverName || '',
};
}
async getEphemeralCertificate({ projectId, instanceId }, publicKey, authType) {
setupGaxiosConfig();
const requestBody = {
public_key: publicKey,
};
let tokenExpiration;
if (authType === AuthTypes.IAM) {
const access_token = await this.auth.getAccessToken();
const client = await this.auth.getClient();
if (access_token) {
tokenExpiration = client.credentials.expiry_date;
requestBody.access_token = access_token;
}
else {
throw new CloudSQLConnectorError({
message: 'Failed to get access token for automatic IAM authentication.',
code: 'ENOACCESSTOKEN',
});
}
}
const res = await this.client.connect.generateEphemeralCert({
project: projectId,
instance: instanceId,
requestBody,
});
if (!res.data) {
throw new CloudSQLConnectorError({
message: `Failed to find metadata on project id: ${projectId} ` +
`and instance id: ${instanceId}. Ensure network connectivity and ` +
'validate the provided `instanceConnectionName` config value',
code: 'ENOSQLADMIN',
});
}
const { ephemeralCert } = res.data;
if (!ephemeralCert || !ephemeralCert.cert) {
throw new CloudSQLConnectorError({
message: 'Cannot connect to instance, failed to retrieve an ephemeral certificate',
code: 'ENOSQLADMINEPH',
});
}
// NOTE: If the SQL Admin generateEphemeralCert API starts returning
// the expirationTime info, this certificate parsing is no longer needed
const { cert, expirationTime } = await parseCert(ephemeralCert.cert);
const nearestExpiration = getNearestExpiration(Date.parse(expirationTime), tokenExpiration);
cleanGaxiosConfig();
return {
cert,
expirationTime: nearestExpiration,
};
}
}
//# sourceMappingURL=sqladmin-fetcher.js.map