UNPKG

@sap-cloud-sdk/connectivity

Version:

SAP Cloud SDK for JavaScript connectivity

362 lines • 16.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultAgentOptions = exports.agentCache = void 0; exports.getAgentConfig = getAgentConfig; exports.urlAndAgent = urlAndAgent; const promises_1 = require("node:fs/promises"); const node_http_1 = __importDefault(require("node:http")); const node_https_1 = __importDefault(require("node:https")); const jks = __importStar(require("jks-js")); const util_1 = require("@sap-cloud-sdk/util"); /* Careful the proxy imports cause circular dependencies if imported from scp directly */ /* eslint-disable import-x/no-internal-modules */ const get_protocol_1 = require("../scp-cf/get-protocol"); const jwt_1 = require("../scp-cf/jwt/jwt"); const cache_1 = require("../scp-cf/cache"); const http_proxy_util_1 = require("../scp-cf/destination/http-proxy-util"); const register_destination_cache_1 = require("../scp-cf/destination/register-destination-cache"); const logger = (0, util_1.createLogger)({ package: 'connectivity', messageContext: 'http-agent' }); /** * Returns a promise of the http or https-agent config depending on the destination URL. * If the destination contains a proxy configuration, the agent will be a proxy-agent. * If not it will be the default http-agent coming from node. * @param destination - Determining which kind of configuration is returned. * @returns A promise of the HTTP or HTTPS agent configuration. */ async function getAgentConfig(destination) { const certificateOptions = { ...getTrustStoreOptions(destination), ...getKeyStoreOptions(destination), ...(await getMtlsOptions(destination)) }; return createAgent(destination, certificateOptions); } /** * @internal * The http agents (proxy and default) use node tls for trust handling. This method creates the options with the 'ca' or 'rejectUnauthorized' option. * https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options * @param destination - Destination object * @returns Options, which can be used later the http client. */ function getTrustStoreOptions(destination) { // http case: no certificate needed if ((0, get_protocol_1.getProtocolOrDefault)(destination) === 'http') { if (destination.isTrustingAllCertificates) { logger.warn('"isTrustingAllCertificates" is not available for HTTP.'); } if (destination.trustStoreCertificate) { logger.warn('"trustStore" is not available for HTTP.'); } return {}; } // https case if (destination.isTrustingAllCertificates && destination.trustStoreCertificate) { logger.warn(`Destination ${destination.name} contains the 'trustAll' and 'trustStoreLocation' property which is a redundant setup.`); } if (destination.isTrustingAllCertificates) { logger.warn('"isTrustingAllCertificates" property in the provided destination is set to "true". This is highly discouraged in production.'); return { rejectUnauthorized: !destination.isTrustingAllCertificates }; } if (destination.trustStoreCertificate) { const decoded = Buffer.from(destination.trustStoreCertificate.content, 'base64').toString('utf8'); return { rejectUnauthorized: true, ca: [decoded] }; } return { rejectUnauthorized: true }; } /** * @internal * The http agent uses node tls for the certificate handling. This method creates the options with the pfx and passphrase or key, cert and passphrase, depending on the format of the certificate. * https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options * @param destination - Destination object. * @returns Options, which can be used later by tls.createSecureContext() e.g. pfx and passphrase or an empty object, if the protocol is not 'https:' or no client information are in the definition. */ function getKeyStoreOptions(destination) { if ( // Only add certificates, when using ClientCertificateAuthentication (https://github.com/SAP/cloud-sdk-js/issues/3544) destination.authentication === 'ClientCertificateAuthentication' && !(mtlsIsEnabled(destination) || destination.mtlsKeyPair) && destination.keyStoreName) { const certificate = selectCertificate(destination); validateFormat(certificate); logger.debug(`Certificate with name "${certificate.name}" selected.`); if (!destination.keyStorePassword) { logger.debug(`Destination '${destination.name}' does not have a keystore password.`); } const certBuffer = Buffer.from(certificate.content, 'base64'); if (getFormat(certificate) === 'jks' || getFormat(certificate) === 'keystore') { const pemKeystore = jks.toPem(certBuffer, destination.keyStorePassword || ''); const aliases = Object.keys(pemKeystore); if (aliases.length === 0) { throw Error('No entries found in JKS keystore'); } const alias = aliases[0]; if (aliases.length > 1) { logger.debug(`JKS keystore contains ${aliases.length} aliases. ` + 'Using the first one. ' + 'If this is not the correct certificate, please use a JKS file with only one entry.'); } const entry = pemKeystore[alias]; if (!entry.cert || !entry.key) { throw Error('Invalid JKS entry: missing cert or key'); } return { cert: Buffer.from(entry.cert, 'utf8'), key: Buffer.from(entry.key, 'utf8') }; } // if the format is pem, the key and certificate needs to be passed separately // it could be required to separate the string into two parts, but this seems to work as well if (getFormat(certificate) === 'pem') { return { cert: certBuffer, key: certBuffer, passphrase: destination.keyStorePassword }; } // pfx is a format that combines key and cert return { pfx: certBuffer, passphrase: destination.keyStorePassword }; } return {}; } /* Reads mTLS client certificates from known environment variables on CloudFoundry. */ async function getMtlsOptions(destination) { if (destination.mtls && !(process.env.CF_INSTANCE_CERT && process.env.CF_INSTANCE_KEY)) { logger.warn(`Destination ${destination.name ? destination.name : ''} has mTLS enabled, but the required Cloud Foundry environment variables (CF_INSTANCE_CERT and CF_INSTANCE_KEY) are not defined. Note that 'inferMtls' only works on Cloud Foundry.`); } if (destination.mtlsKeyPair) { if (mtlsIsEnabled(destination)) { logger.warn(`Destination ${destination.name ? destination.name : ''} has both 'mtlsKeyPair' (used by IAS) and 'mtls' (to use certs from cf) enabled. The 'mtlsKeyPair' will be used.`); } return destination.mtlsKeyPair; } if (mtlsIsEnabled(destination)) { if (register_destination_cache_1.registerDestinationCache.mtls.useMtlsCache) { return register_destination_cache_1.registerDestinationCache.mtls.getMtlsOptions(); } const getCert = (0, promises_1.readFile)(process.env.CF_INSTANCE_CERT, 'utf8'); const getKey = (0, promises_1.readFile)(process.env.CF_INSTANCE_KEY, 'utf8'); const [cert, key] = await Promise.all([getCert, getKey]); return { cert, key }; } return {}; } function mtlsIsEnabled(destination) { return (destination.mtls && process.env.CF_INSTANCE_CERT && process.env.CF_INSTANCE_KEY); } /* The node client supports only these store formats https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions. */ const supportedCertificateFormats = ['p12', 'pfx', 'pem', 'jks', 'keystore']; function isSupportedFormat(format) { return !!format && supportedCertificateFormats.includes(format); } function selectCertificate(destination) { const certificate = destination.certificates?.find((c) => c.name === destination.keyStoreName); if (!certificate) { throw Error(`No certificate with name ${destination.keyStoreName} could be found on the destination!`); } return certificate; } function getFormat(certificate) { return (0, util_1.last)(certificate.name.split('.')); } function validateFormat(certificate) { const format = getFormat(certificate); if (!isSupportedFormat(format)) { throw Error(`The format of the provided certificate '${certificate.name}' is not supported. Supported formats are: ${supportedCertificateFormats.join(', ')}.`); } } /** * Cache for http(s) agents. * Exported for testing purposes only. * @internal */ exports.agentCache = new cache_1.Cache(3600000, // 1 hour 100 // max 100 LRU-cached agents ); /** * Default options for the http(s) agents. * @internal */ exports.defaultAgentOptions = { keepAlive: true, timeout: 5000 }; /** * Builds a secret-free cache key input for the agent cache. * For direct destinations the agent is fully defined by its protocol, TLS options and * optional `agentOptions`. For the on-premise connectivity proxy all requests share the * same proxy origin, so keep-alive sockets would otherwise be reused across Cloud * Connector tunnels. To prevent this, the key is additionally scoped by the location ID, * the proxy host/port and the propagated principal (derived from stable, non-secret JWT * claims). * @param destination - Destination to derive the cache key dimensions from. * @param options - TLS/agent options that define the agent instance. * @returns A plain object to be hashed into the agent cache key. */ function getAgentCacheKeyInput(destination, options) { const protocol = (0, get_protocol_1.getProtocolOrDefault)(destination); const keyInput = { protocol, options, agentOptions: destination.agentOptions }; if (destination.proxyType === 'OnPremise') { keyInput.cloudConnectorLocationId = destination.cloudConnectorLocationId; keyInput.proxyHost = destination.proxyConfiguration?.host; keyInput.proxyPort = destination.proxyConfiguration?.port; const principal = getPrincipalCacheKey(destination); // PrincipalPropagation binds the Cloud Connector tunnel to the propagated // user. Without a stable userId we cannot derive a safe cache key and must // skip caching to avoid reusing a tunnel across principals. if (destination.authentication === 'PrincipalPropagation' && !principal?.userId) { return undefined; } // For all other OnPremise flows the principal is not strictly required, but // we still scope by full available principal information for better cache isolation. if (principal) { keyInput.principal = principal; } } return keyInput; } /** * Derives a stable, non-secret identity scope for OnPremise destinations from * the propagated JWT. The raw token must not be part of the cache key, because * it is a secret and rotates on every refresh. `userId` and `tenantId` are * stable claims that scope the cache entry to a principal. * PrincipalPropagation flows require a `userId`; other flows include the * principal only when a user token is present. * @param destination - Destination carrying the propagated principal JWT. * @returns A stable identity scope, or `undefined` if no usable token is present. */ function getPrincipalCacheKey(destination) { const authHeader = destination.proxyConfiguration?.headers?.['SAP-Connectivity-Authentication']; if (!authHeader) { return undefined; } const encoded = authHeader.replace(/^Bearer /i, ''); try { const decoded = (0, jwt_1.decodeJwt)(encoded); const tenantId = (0, jwt_1.getTenantId)(decoded); const userId = (0, jwt_1.userId)(decoded); if (!tenantId && !userId) { return undefined; } return { userId, tenantId }; } catch { // A malformed token must not break agent creation; fall back to location/host scoping. return undefined; } } async function getAgentCacheKey(destination, options) { const cacheKeyInput = getAgentCacheKeyInput(destination, options); // If the cache key is undefined, avoid caching the agent. if (!cacheKeyInput) { return undefined; } return (0, cache_1.hashCacheKey)(cacheKeyInput); } function createAgentImpl(destination, options) { const protocol = (0, get_protocol_1.getProtocolOrDefault)(destination); logger.debug(`Creating new ${protocol.toUpperCase()} agent for destination ${destination.name || '<unknown>'}`); const optionsWithDefaults = { ...exports.defaultAgentOptions, ...destination.agentOptions, ...options }; return protocol === 'https' ? { httpsAgent: new node_https_1.default.Agent(optionsWithDefaults) } : { httpAgent: new node_http_1.default.Agent(optionsWithDefaults) }; } /** * @internal * Agents are cached for up to one hour, but can be evicted earlier if more than 100 agents are created. * See https://nodejs.org/api/https.html#https_https_createserver_options_requestlistener for details on the possible options */ async function createAgent(destination, options) { const cacheKey = await getAgentCacheKey(destination, options); if (!cacheKey) { logger.info(`Could not derive a cache key for destination ${destination.name || '<unknown>'}. Creating a new agent without caching.`); return createAgentImpl(destination, options); } return exports.agentCache.getOrInsertComputed(cacheKey, () => ({ entry: createAgentImpl(destination, options) })); } /** * Builds part of the request config containing the URL and if needed proxy agents or normal http agents. * Considers the `no_proxy` environment variable together with the `targetUri`. * @internal * @param targetUri - Used as baseURL in request config. * @returns HttpRequestConfig containing baseUrl and http(s) agents. */ async function urlAndAgent(targetUri) { let destination = { url: targetUri, proxyType: 'Internet' }; if ((0, http_proxy_util_1.proxyStrategy)(destination) === 'internet') { destination = (0, http_proxy_util_1.addProxyConfigurationInternet)(destination); } return { baseURL: destination.url, ...(await getAgentConfig(destination)), proxy: (0, http_proxy_util_1.getProxyConfig)(destination) }; } //# sourceMappingURL=http-agent.js.map