snowflake-sdk
Version:
Node.js driver for Snowflake
348 lines • 12.7 kB
JavaScript
;
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