snowflake-sdk
Version:
Node.js driver for Snowflake
414 lines (367 loc) • 12.2 kB
JavaScript
/*
* Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved.
*/
const http = require('http');
const url = require('url');
const util = require('util');
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 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));
const cacheDir = GlobalConfig.mkdirCacheDir();
const cacheFileName = path.join(cacheDir, 'ocsp_response_cache.json');
// create a cache to store the responses, dynamically changes in size
let cache;
// JSON object with previous cache's responses
let prevCacheObj;
// Cache updated time, in seconds, initialized as current time.
// Will be updated when load from local cache file or refresh by downloading
let cacheUpdateTimeSec = Date.now() / 1000;
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.
*/
// Current time in second
const currentTimeSec = Date.now() / 1000;
cacheUpdateTimeSec = currentTimeSec;
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');
prevCacheObj = JSON.parse(contents);
cache = new SimpleCache({ maxSize: Object.keys(prevCacheObj).length });
setCacheEntries(prevCacheObj);
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 uri = url.parse(OCSP_URL);
const timeout = process.env.SF_OCSP_TEST_OCSP_RESPONSE_CACHE_SERVER_TIMEOUT || 5000;
const options = util._extend({
timeout: Number(timeout),
method: 'GET',
agent: proxyAgent,
}, uri);
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);
}
if ((currentTimeSec - ocspResponseBase64[0]) > maxAgeSec) {
Logger.getInstance().debug(
'OCSP cache validity is out of range. currentTime: %s, timestamp: %s, maxAge: %s',
currentTimeSec, ocspResponseBase64[0], maxAgeSec);
err = Errors.createOCSPError(ErrorCodes.ERR_OCSP_CACHE_EXPIRED);
}
try {
const k = CertUtil.encodeKey(certIdBase64);
if (err) {
return { err: err, key: k };
}
const rawOCSPResponse = Buffer.from(ocspResponseBase64[1], '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 retrieved from the cache server
const responseCacheSize = Object.keys(jsonObject).length;
// Check if there are previous entries to append
if (prevCacheObj) {
// Count overlap between previous cache and response cache
// And delete entry if expired
let cacheOverlapCount = 0;
for (const entry in jsonObject) {
if (entryExists(prevCacheObj, entry)) {
cacheOverlapCount++;
updateOrDeleteEntry(prevCacheObj, entry);
}
}
// Count entries from previous cache
const prevCacheSize = Object.keys(prevCacheObj).length;
// New cache size = previous cache size + response cache size - overlap between the two caches
const newCacheSize = prevCacheSize + responseCacheSize - cacheOverlapCount;
// Create cache using new cache size if it doesn't exceed the upper limit
cache = new SimpleCache({ maxSize: newCacheSize < sizeLimit ? newCacheSize : sizeLimit });
// Add new entries
setCacheEntries(jsonObject);
// Append older unique entries to cache
setCacheEntries(prevCacheObj);
} else {
// Create cache using response cache size if it doesn't exceed the upper limit
cache = new SimpleCache({ maxSize: responseCacheSize < sizeLimit ? responseCacheSize : sizeLimit });
// Add new entries
setCacheEntries(jsonObject);
}
// set cache update time
cacheUpdateTimeSec = Date.now() / 1000;
cacheInitialized = true;
}
function setCacheEntries(jsonObject) {
for (const entry in jsonObject) {
if (Object.prototype.hasOwnProperty.call(jsonObject, entry)) {
updateOrDeleteEntry(jsonObject, entry);
}
}
}
function updateOrDeleteEntry(jsonObject, entry) {
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] < cacheUpdateTimeSec) {
cacheUpdateTimeSec = jsonObject[entry][0];
}
} else if (status.err.code === ErrorCodes.ERR_OCSP_CACHE_EXPIRED) {
// If timestamp expired, delete entry
cache.del(status.key);
}
}
function entryExists(jsonObject, entry) {
for (const otherEntry in jsonObject) {
if (entry === otherEntry) {
return true;
}
}
return false;
}
}
exports.OcspResponseCache = OcspResponseCache;