UNPKG

snowflake-sdk

Version:
348 lines 12.7 kB
"use strict"; const http = require('http'); const path = require('path'); const fs = require('fs'); const SimpleCache = require('simple-lru-cache'); const Errors = require('../errors'); const ErrorCodes = Errors.codes; const Util = require('../util'); const CertUtil = require('./cert_util'); const GlobalConfig = require('../global_config'); const { getDefaultCacheDir } = require('../disk_cache'); const Logger = require('../logger'); const status = { NOT_START: 'not_start', STARTED: 'started', FINISHED: 'finish', }; // validate input const sizeLimit = GlobalConfig.getOcspResponseCacheSizeLimit(); // ocsp cache max age in second let maxAgeSec = GlobalConfig.getOcspResponseCacheMaxAge(); Errors.assertInternal(Util.number.isPositiveInteger(sizeLimit)); Errors.assertInternal(Util.number.isPositiveInteger(maxAgeSec)); function getOcspCacheDir() { return process.env.SF_OCSP_RESPONSE_CACHE_DIR || getDefaultCacheDir(); } exports.getOcspCacheDir = getOcspCacheDir; const cacheDir = getOcspCacheDir(); try { fs.mkdirSync(cacheDir, { recursive: true }); } catch (e) { Logger.getInstance().debug('Failed to create OCSP cache directory %s, err: %s', cacheDir, e); } const cacheFileName = path.join(cacheDir, 'ocsp_response_cache.json'); // create a cache to store the responses, dynamically changes in size let cache; // Cache updated time, in seconds, initialized as current time. // Will be updated when load from local cache file or refresh by downloading function deleteCache() { try { cache.reset(); fs.unlinkSync(cacheFileName); } catch (e) { Logger.getInstance().debug('Failed to delete OCSP cache file: %s, err: %s', cacheFileName, e); } } exports.deleteCache = deleteCache; /** * Cache for storing OCSP responses. This covers both client and server caches. * * @constructor */ function OcspResponseCache() { let downloadStatus = status.NOT_START; let cacheUpdated = false; let cacheInitialized = false; let proxyAgent = null; /** * Reads OCSP cache file. */ // Cache update time in second let cacheUpdateTimeSec = Date.now() / 1000; let OCSP_URL = process.env.SF_OCSP_RESPONSE_CACHE_SERVER_URL; if (!OCSP_URL) { OCSP_URL = 'http://ocsp.snowflakecomputing.com/ocsp_response_cache.json'; } try { Logger.getInstance().debug('Reading OCSP cache file. %s', cacheFileName); const contents = fs.readFileSync(cacheFileName, 'utf-8'); const jsonCacheFromFile = JSON.parse(contents); updateCache(jsonCacheFromFile); cacheInitialized = true; } catch (e) { Logger.getInstance().debug('Failed to read OCSP cache file: %s, err: %s', cacheFileName, e); } /** * set proxy agent for ocsp validation * * @param agent */ this.setAgent = function setAgent(agent) { proxyAgent = agent; }; /** * Initializes the cache * * @param cert * @param response */ this.initCache = function initCache(cert, response) { cache = new SimpleCache({ maxSize: 1 }); this.set(cert, response); }; /** * Is OCSP Cache initialized? * @returns {boolean} */ this.isInitialized = function () { return cacheInitialized; }; /** * Is OCSP Cache download finished? * @returns {boolean} */ this.isDownloadFinished = function () { return downloadStatus === status.FINISHED; }; /** * Forces download status to finish */ this.forceDownloadToFinish = function () { downloadStatus = status.FINISHED; }; /** * Is local OCSP Cache expired? * @returns {boolean} */ this.IsCacheExpired = function () { if (!cacheInitialized) { return false; } // Update maxAge in case it could be changed through environment variable maxAgeSec = GlobalConfig.getOcspResponseCacheMaxAge(); // Current time in seconds const currentTimeSec = Date.now() / 1000; if (currentTimeSec - cacheUpdateTimeSec > maxAgeSec) { Logger.getInstance().debug('OCSP local cache validity is out of range. currentTime: %s, timestamp: %s, maxAge: %s', currentTimeSec, cacheUpdateTimeSec, maxAgeSec); return true; } return false; }; /** * Resets OCSP Cache status */ this.resetCacheStatus = function () { downloadStatus = status.NOT_START; if (cacheUpdated) { Logger.getInstance().debug(cacheFileName); // current time in second const currentTimeSec = Date.now() / 1000; const cacheOutput = {}; cache.forEach(function (v, k) { const certIdInBase64 = CertUtil.decodeKey(k); const ocspResponseInBase64 = v.toString('BASE64'); cacheOutput[certIdInBase64] = [currentTimeSec, ocspResponseInBase64]; }); const writeContent = JSON.stringify(cacheOutput); Logger.getInstance().debug('Writing OCSP cache file. %s', cacheFileName); try { fs.writeFileSync(cacheFileName, writeContent, 'utf-8'); } catch (e) { Logger.getInstance().debug('Failed to update OCSP cache file: %s, err: %s', cacheFileName, e); } cacheUpdated = false; } }; /** * Adds an entry to the cache. * * @param cert * @param response */ this.set = function set(cert, response) { try { const certId = CertUtil.buildCertId(cert); cache.set(certId, response); cacheUpdated = true; } catch (e) { Logger.getInstance().debug('Failed to add certificate to OCSP cache file. err: %s', e); } }; /** * Returns an entry from the cache. * * @param cert * @returns {*} */ this.get = function get(cert) { try { const certId = CertUtil.buildCertId(cert); return cache.get(certId); } catch (e) { Logger.getInstance().debug('Failed to get certificate from OCSP cache. err: %s', e); return null; } }; /** * Downloads OCSP cache from the Snowflake OCSP cache server. * @param cb callback */ this.downloadCache = function (cb) { if (downloadStatus === status.STARTED) { // reschedule calling cb return false; } else if (downloadStatus === status.FINISHED) { // call cb immediately cb(null, false); return true; } downloadStatus = status.STARTED; function checkOCSPResponse(err, cacheContent) { if (downloadStatus === status.FINISHED) { return; } downloadStatus = status.FINISHED; Logger.getInstance().debug('Finish OCSP Cache Server: %s', OCSP_URL); if (err) { Logger.getInstance().debug('Failed to download OCSP cache file. %s. Ignored', err); return cb(err, false); } try { const jsonParsed = JSON.parse(cacheContent); updateCache(jsonParsed); cacheUpdated = true; return cb(null, false); } catch (e) { cb(e, false); } } function onResponse(response) { if (response.statusCode < 200 || response.statusCode >= 400) { return checkOCSPResponse(new Error('Failed to obtain OCSP response: ' + response.statusCode), null); } let rawData = ''; // A chunk of data has been received. response.on('data', function (chunk) { rawData += chunk; }); // The whole response has been received. Print out the result. response.on('end', function () { checkOCSPResponse(null, rawData); }); } const parsedUrl = new URL(OCSP_URL); const timeout = process.env.SF_OCSP_TEST_OCSP_RESPONSE_CACHE_SERVER_TIMEOUT || 5000; const options = { hostname: parsedUrl.hostname, port: parsedUrl.port || undefined, path: parsedUrl.pathname + parsedUrl.search, protocol: parsedUrl.protocol, timeout: Number(timeout), method: 'GET', agent: proxyAgent, }; const httpRequest = http.request(options, onResponse); httpRequest.on('error', function (e) { downloadStatus = status.FINISHED; if (cb) { cb(e, false); } cb = null; }); httpRequest.on('timeout', function () { downloadStatus = status.FINISHED; httpRequest.abort(); Logger.getInstance().debug('Timeout OCSP responder: %s, %ss', OCSP_URL, options.timeout); if (cb) { cb(Errors.createOCSPError(ErrorCodes.ERR_OCSP_CACHE_SERVER_TIMEOUT), false); } cb = null; }); httpRequest.end(); Logger.getInstance().trace('Contact OCSP Cache Server: %s', OCSP_URL); return true; }; /** * Validate cache entry * @param certIdBase64 cache key * @param ocspResponseBase64 cache value * @returns {Object} */ function validateCacheEntry(certIdBase64, ocspResponseBase64) { let err; if (ocspResponseBase64.length !== 2) { Logger.getInstance().debug("OCSP cache value doesn't consist of two elements. Ignored."); err = Errors.createOCSPError(ErrorCodes.ERR_OCSP_NOT_TWO_ELEMENTS); } const cacheEntryWriteTime = ocspResponseBase64[0]; const currentTimeSec = Date.now() / 1000; if (currentTimeSec - cacheEntryWriteTime > maxAgeSec) { Logger.getInstance().debug('OCSP cache validity is out of range. currentTime: %s, timestamp: %s, maxAge: %s', currentTimeSec, cacheEntryWriteTime, maxAgeSec); err = Errors.createOCSPError(ErrorCodes.ERR_OCSP_CACHE_EXPIRED); } try { const k = CertUtil.encodeKey(certIdBase64); if (err) { return { err: err, key: k }; } const cacheEntryOcspResponse = ocspResponseBase64[1]; const rawOCSPResponse = Buffer.from(cacheEntryOcspResponse, 'base64'); const status = CertUtil.verifyOCSPResponse(null, rawOCSPResponse); if (!status.err) { return { err: null, key: k, value: rawOCSPResponse }; } return { err: status.err }; } catch (e) { Logger.getInstance().debug('Failed to parse OCSP response. %s. Ignored.', e); return { err: Errors.createOCSPError(ErrorCodes.ERR_OCSP_FAILED_PARSE_RESPONSE) }; } } function updateCache(jsonObject) { // Get the size of cache const cacheSize = Object.keys(jsonObject).length; // Create cache using response cache size if it doesn't exceed the upper limit cache = new SimpleCache({ maxSize: cacheSize < sizeLimit ? cacheSize : sizeLimit }); // Add new entries setCacheEntries(jsonObject); // set cache update time cacheInitialized = true; } function setCacheEntries(jsonObject) { let cacheUpdateTime = Date.now() / 1000; for (const entry in jsonObject) { if (Object.prototype.hasOwnProperty.call(jsonObject, entry)) { const newUpdateTime = validateAndSetEntry(jsonObject, entry, cacheUpdateTime); if (newUpdateTime) { cacheUpdateTime = newUpdateTime; } } } cacheUpdateTimeSec = cacheUpdateTime; } function validateAndSetEntry(jsonObject, entry, cacheUpdateTime) { const status = validateCacheEntry(entry, jsonObject[entry]); if (!status.err) { // Add new entry or update existing one cache.set(status.key, status.value); // change cache update time if needed if (jsonObject[entry][0] < cacheUpdateTime) { return jsonObject[entry][0]; } } else { Logger.getInstance().trace('Error when validating OCSP cache entry %s, %s', entry, status.err.toString()); } } } exports.OcspResponseCache = OcspResponseCache; //# sourceMappingURL=ocsp_response_cache.js.map