UNPKG

@google-cloud/cloud-sql-connector

Version:

A JavaScript library for connecting securely to your Cloud SQL instances

275 lines 11.5 kB
"use strict"; // 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. Object.defineProperty(exports, "__esModule", { value: true }); exports.CloudSQLInstance = void 0; const ip_addresses_1 = require("./ip-addresses"); const parse_instance_connection_name_1 = require("./parse-instance-connection-name"); const crypto_1 = require("./crypto"); const time_1 = require("./time"); const auth_types_1 = require("./auth-types"); const errors_1 = require("./errors"); class CloudSQLInstance { static async getCloudSQLInstance(options) { const instance = new CloudSQLInstance({ options: options, instanceInfo: await (0, parse_instance_connection_name_1.resolveInstanceName)(options.instanceConnectionName, options.domainName), }); await instance.refresh(); return instance; } constructor({ options, instanceInfo, }) { this.establishedConnection = false; this.scheduledRefreshID = undefined; this.checkDomainID = undefined; this.closed = false; this.sockets = new Set(); this.port = 3307; this.serverCaMode = ''; this.dnsName = ''; this.instanceInfo = instanceInfo; this.authType = options.authType || auth_types_1.AuthTypes.PASSWORD; this.ipType = options.ipType || ip_addresses_1.IpAddressTypes.PUBLIC; this.limitRateInterval = options.limitRateInterval || 30 * 1000; // 30 seconds this.sqlAdminFetcher = options.sqlAdminFetcher; this.failoverPeriod = options.failoverPeriod || 30 * 1000; // 30 seconds } // p-throttle library has to be initialized in an async scope in order to // use dynamic import so that it's also compatible with CommonJS async initializeRateLimiter() { if (this.throttle) { return; } const pThrottle = (await import('p-throttle')).default; this.throttle = pThrottle({ limit: 1, interval: this.limitRateInterval, strict: true, }); } forceRefresh() { // if a refresh is already ongoing, just await for its promise to fulfill // so that a new instance info is available before reconnecting if (this.next) { return new Promise(resolve => { if (this.next) { this.next.finally(resolve); } else { resolve(); } }); } this.cancelRefresh(); this.scheduleRefresh(0); return new Promise(resolve => { // setTimeout() to yield execution to allow other refresh background // task to start. setTimeout(() => { if (this.next) { // If there is a refresh promise in progress, resolve this promise // when the refresh is complete. this.next.finally(resolve); } else { // Else resolve immediately. resolve(); } }, 0); }); } refresh() { var _a; if (this.closed) { this.scheduledRefreshID = undefined; this.next = undefined; return Promise.reject('closed'); } // Lazy instantiation of the checkDomain interval on the first refresh // This avoids issues with test cases that instantiate a CloudSqlInstance. // If failoverPeriod is 0 (or negative) don't check for DNS updates. if (((_a = this === null || this === void 0 ? void 0 : this.instanceInfo) === null || _a === void 0 ? void 0 : _a.domainName) && !this.checkDomainID && this.failoverPeriod > 0) { this.checkDomainID = setInterval(() => { this.checkDomainChanged(); }, this.failoverPeriod); } const currentRefreshId = this.scheduledRefreshID; // Since forceRefresh might be invoked during an ongoing refresh // we keep track of the ongoing promise in order to be able to await // for it in the forceRefresh method. // In case the throttle mechanism is already initialized, we add the // extra wait time `limitRateInterval` in order to limit the rate of // requests to Cloud SQL Admin APIs. this.next = (this.throttle && this.scheduledRefreshID ? this.throttle(this.performRefresh).call(this) : this.performRefresh()) // These needs to be part of the chain of promise referenced in // next in order to avoid race conditions .then((nextValues) => { var _a; // in case the id at the moment of starting this refresh cycle has // changed, that means that it has been canceled if (currentRefreshId !== this.scheduledRefreshID) { return; } // In case the performRefresh method succeeded // then we go ahead and update values this.updateValues(nextValues); const refreshInterval = (0, time_1.getRefreshInterval)( /* c8 ignore next */ String((_a = this.ephemeralCert) === null || _a === void 0 ? void 0 : _a.expirationTime)); this.scheduleRefresh(refreshInterval); // This is the end of the successful refresh chain, so now // we release the reference to the next this.next = undefined; }) .catch((err) => { // In case there's already an active connection we won't throw // refresh errors to the final user, scheduling a new // immediate refresh instead. if (this.establishedConnection) { if (currentRefreshId === this.scheduledRefreshID) { this.scheduleRefresh(0); } } else { throw err; } // This refresh cycle has failed, releases ref to next this.next = undefined; }) // The rate limiter needs to be initialized _after_ assigning a ref // to next in order to avoid race conditions with // the forceRefresh check that ensures a refresh cycle is not ongoing .then(() => { this.initializeRateLimiter(); }); return this.next; } // The performRefresh method will perform all the necessary async steps // in order to get a new set of values for an instance that can then be // used to create new connections to a Cloud SQL instance. It throws in // case any of the internal steps fails. async performRefresh() { if (this.closed) { // The connector may be closed while the rate limiter delayed // a call to performRefresh() so check this.closed before continuing. return Promise.reject('closed'); } const rsaKeys = await (0, crypto_1.generateKeys)(); const metadata = await this.sqlAdminFetcher.getInstanceMetadata(this.instanceInfo); const ephemeralCert = await this.sqlAdminFetcher.getEphemeralCertificate(this.instanceInfo, rsaKeys.publicKey, this.authType); const host = (0, ip_addresses_1.selectIpAddress)(metadata.ipAddresses, this.ipType); const privateKey = rsaKeys.privateKey; const serverCaCert = metadata.serverCaCert; this.serverCaMode = metadata.serverCaMode; this.dnsName = metadata.dnsName; const currentValues = { ephemeralCert: this.ephemeralCert, host: this.host, privateKey: this.privateKey, serverCaCert: this.serverCaCert, }; const nextValues = { ephemeralCert, host, privateKey, serverCaCert, }; // In the rather odd case that the current ephemeral certificate is still // valid while we get an invalid result from the API calls, then preserve // the current metadata. if (this.isValid(currentValues) && !this.isValid(nextValues)) { return currentValues; } return nextValues; } isValid({ ephemeralCert, host, privateKey, serverCaCert, }) { if (!ephemeralCert || !host || !privateKey || !serverCaCert) { return false; } return (0, time_1.isExpirationTimeValid)(ephemeralCert.expirationTime); } updateValues(nextValues) { const { ephemeralCert, host, privateKey, serverCaCert } = nextValues; this.ephemeralCert = ephemeralCert; this.host = host; this.privateKey = privateKey; this.serverCaCert = serverCaCert; } scheduleRefresh(delay) { if (this.closed) { return; } this.scheduledRefreshID = setTimeout(() => this.refresh(), delay); } cancelRefresh() { // If refresh has not yet started, then cancel the setTimeout if (this.scheduledRefreshID) { clearTimeout(this.scheduledRefreshID); } this.scheduledRefreshID = null; } // Mark this instance as having an active connection. This is important to // ensure any possible errors thrown during a future refresh cycle should // not be thrown to the final user. setEstablishedConnection() { this.establishedConnection = true; } // close stops any refresh process in progress and prevents future refresh // connections. close() { this.closed = true; this.cancelRefresh(); if (this.checkDomainID) { clearInterval(this.checkDomainID); this.checkDomainID = null; } for (const socket of this.sockets) { socket.destroy(new errors_1.CloudSQLConnectorError({ code: 'ERRCLOSED', message: 'The connector was closed.', })); } } isClosed() { return this.closed; } async checkDomainChanged() { if (!this.instanceInfo.domainName) { return; } const newInfo = await (0, parse_instance_connection_name_1.resolveInstanceName)(undefined, this.instanceInfo.domainName); if (!(0, parse_instance_connection_name_1.isSameInstance)(this.instanceInfo, newInfo)) { // Domain name changed. Close and remove, then create a new map entry. this.close(); } } addSocket(socket) { if (!this.instanceInfo.domainName) { // This was not connected by domain name. Ignore all sockets. return; } // Add the socket to the list this.sockets.add(socket); // When the socket is closed, remove it. socket.once('closed', () => { this.sockets.delete(socket); }); } } exports.CloudSQLInstance = CloudSQLInstance; //# sourceMappingURL=cloud-sql-instance.js.map