2011-03-31 06:32:47 +00:00
|
|
|
/*
|
|
|
|
* file.js: Simple file storage engine for nconf files
|
|
|
|
*
|
2014-11-26 06:31:48 +00:00
|
|
|
* (C) 2011, Charlie Robbins and the Contributors.
|
2011-03-31 06:32:47 +00:00
|
|
|
*
|
|
|
|
*/
|
2011-06-24 01:06:26 +00:00
|
|
|
|
2017-10-21 19:47:41 +00:00
|
|
|
var fs = require('fs'),
|
2011-08-28 12:50:26 +00:00
|
|
|
path = require('path'),
|
|
|
|
util = require('util'),
|
2019-05-16 03:27:38 +00:00
|
|
|
crypto = require('crypto'),
|
2011-08-28 12:50:26 +00:00
|
|
|
formats = require('../formats'),
|
2017-10-21 20:01:01 +00:00
|
|
|
Memory = require('./memory').Memory;
|
|
|
|
|
|
|
|
var exists = fs.exists || path.exists,
|
2012-03-29 11:13:16 +00:00
|
|
|
existsSync = fs.existsSync || path.existsSync;
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2011-04-02 07:03:16 +00:00
|
|
|
//
|
|
|
|
// ### function File (options)
|
|
|
|
// #### @options {Object} Options for this instance
|
|
|
|
// Constructor function for the File nconf store, a simple abstraction
|
|
|
|
// around the Memory store that can persist configuration to disk.
|
|
|
|
//
|
2011-03-31 06:32:47 +00:00
|
|
|
var File = exports.File = function (options) {
|
2011-09-19 01:37:01 +00:00
|
|
|
if (!options || !options.file) {
|
2017-10-21 20:01:01 +00:00
|
|
|
throw new Error('Missing required option `file`');
|
2011-09-19 01:37:01 +00:00
|
|
|
}
|
2011-03-31 06:32:47 +00:00
|
|
|
|
2011-04-02 08:31:20 +00:00
|
|
|
Memory.call(this, options);
|
2011-03-31 06:32:47 +00:00
|
|
|
|
2015-09-19 03:40:53 +00:00
|
|
|
this.type = 'file';
|
|
|
|
this.file = options.file;
|
|
|
|
this.dir = options.dir || process.cwd();
|
|
|
|
this.format = options.format || formats.json;
|
|
|
|
this.secure = options.secure;
|
|
|
|
this.spacing = options.json_spacing
|
|
|
|
|| options.spacing
|
|
|
|
|| 2;
|
|
|
|
|
|
|
|
if (this.secure) {
|
2016-02-03 00:38:48 +00:00
|
|
|
this.secure = Buffer.isBuffer(this.secure) || typeof this.secure === 'string'
|
|
|
|
? { secret: this.secure.toString() }
|
2015-09-19 03:44:29 +00:00
|
|
|
: this.secure;
|
|
|
|
|
2015-09-19 03:40:53 +00:00
|
|
|
this.secure.alg = this.secure.alg || 'aes-256-ctr';
|
|
|
|
if (this.secure.secretPath) {
|
2015-10-07 19:31:53 +00:00
|
|
|
this.secure.secret = fs.readFileSync(this.secure.secretPath, 'utf8');
|
2015-09-19 03:40:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.secure.secret) {
|
|
|
|
throw new Error('secure.secret option is required');
|
|
|
|
}
|
|
|
|
}
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2011-10-22 06:36:45 +00:00
|
|
|
if (options.search) {
|
|
|
|
this.search(this.dir);
|
|
|
|
}
|
2011-03-31 06:32:47 +00:00
|
|
|
};
|
|
|
|
|
2011-04-02 07:03:16 +00:00
|
|
|
// Inherit from the Memory store
|
2011-03-31 06:32:47 +00:00
|
|
|
util.inherits(File, Memory);
|
|
|
|
|
2011-04-02 07:03:16 +00:00
|
|
|
//
|
2012-04-14 19:28:55 +00:00
|
|
|
// ### function save (value, callback)
|
2011-04-02 07:03:16 +00:00
|
|
|
// #### @value {Object} _Ignored_ Left here for consistency
|
|
|
|
// #### @callback {function} Continuation to respond to when complete.
|
2012-04-14 19:28:55 +00:00
|
|
|
// Saves the current configuration object to disk at `this.file`
|
2011-04-02 07:03:16 +00:00
|
|
|
// using the format specified by `this.format`.
|
|
|
|
//
|
|
|
|
File.prototype.save = function (value, callback) {
|
2017-10-30 14:01:55 +00:00
|
|
|
this.saveToFile(this.file, value, callback);
|
|
|
|
};
|
|
|
|
|
|
|
|
//
|
|
|
|
// ### function saveToFile (path, value, callback)
|
|
|
|
// #### @path {string} The path to the file where we save the configuration to
|
|
|
|
// #### @format {Object} Optional formatter, default behing the one of the store
|
|
|
|
// #### @callback {function} Continuation to respond to when complete.
|
|
|
|
// Saves the current configuration object to disk at `this.file`
|
|
|
|
// using the format specified by `this.format`.
|
|
|
|
//
|
|
|
|
File.prototype.saveToFile = function (path, format, callback) {
|
2011-04-02 07:03:16 +00:00
|
|
|
if (!callback) {
|
2017-10-30 14:01:55 +00:00
|
|
|
callback = format;
|
|
|
|
format = this.format;
|
2011-04-02 07:03:16 +00:00
|
|
|
}
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2017-10-30 14:01:55 +00:00
|
|
|
fs.writeFile(path, this.stringify(format), callback);
|
2011-03-31 06:32:47 +00:00
|
|
|
};
|
|
|
|
|
2011-04-20 05:57:56 +00:00
|
|
|
//
|
2012-04-14 19:28:55 +00:00
|
|
|
// ### function saveSync (value, callback)
|
|
|
|
// Saves the current configuration object to disk at `this.file`
|
2011-04-20 05:57:56 +00:00
|
|
|
// using the format specified by `this.format` synchronously.
|
|
|
|
//
|
2017-10-21 20:01:01 +00:00
|
|
|
File.prototype.saveSync = function () {
|
2015-09-19 07:12:50 +00:00
|
|
|
fs.writeFileSync(this.file, this.stringify());
|
2011-12-25 14:30:48 +00:00
|
|
|
return this.store;
|
2011-04-20 05:57:56 +00:00
|
|
|
};
|
|
|
|
|
2011-04-02 07:03:16 +00:00
|
|
|
//
|
|
|
|
// ### function load (callback)
|
|
|
|
// #### @callback {function} Continuation to respond to when complete.
|
|
|
|
// Responds with an Object representing all keys associated in this instance.
|
|
|
|
//
|
2011-03-31 06:32:47 +00:00
|
|
|
File.prototype.load = function (callback) {
|
|
|
|
var self = this;
|
2011-06-24 01:06:26 +00:00
|
|
|
|
2012-05-15 09:02:46 +00:00
|
|
|
exists(self.file, function (exists) {
|
2011-06-24 01:06:26 +00:00
|
|
|
if (!exists) {
|
2011-11-23 21:40:18 +00:00
|
|
|
return callback(null, {});
|
2011-06-24 01:06:26 +00:00
|
|
|
}
|
2011-11-23 21:40:18 +00:00
|
|
|
|
|
|
|
//
|
|
|
|
// Else, the path exists, read it from disk
|
|
|
|
//
|
|
|
|
fs.readFile(self.file, function (err, data) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2011-11-23 21:40:18 +00:00
|
|
|
try {
|
2015-09-19 03:40:53 +00:00
|
|
|
// Deals with string that include BOM
|
2013-10-26 19:40:12 +00:00
|
|
|
var stringData = data.toString();
|
2015-09-19 03:40:53 +00:00
|
|
|
if (stringData.charAt(0) === '\uFEFF') {
|
|
|
|
stringData = stringData.substr(1);
|
|
|
|
}
|
2013-10-03 08:17:36 +00:00
|
|
|
|
2015-09-19 07:12:50 +00:00
|
|
|
self.store = self.parse(stringData);
|
2011-11-23 21:40:18 +00:00
|
|
|
}
|
|
|
|
catch (ex) {
|
2014-03-09 12:40:10 +00:00
|
|
|
return callback(new Error("Error parsing your configuration file: [" + self.file + ']: ' + ex.message));
|
2011-11-23 21:40:18 +00:00
|
|
|
}
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2011-11-23 21:40:18 +00:00
|
|
|
callback(null, self.store);
|
|
|
|
});
|
2011-03-31 06:32:47 +00:00
|
|
|
});
|
2011-04-20 05:57:56 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
//
|
2011-11-23 21:40:18 +00:00
|
|
|
// ### function loadSync (callback)
|
|
|
|
// Attempts to load the data stored in `this.file` synchronously
|
|
|
|
// and responds appropriately.
|
2011-04-20 05:57:56 +00:00
|
|
|
//
|
2011-06-24 01:06:26 +00:00
|
|
|
File.prototype.loadSync = function () {
|
2015-09-19 07:12:50 +00:00
|
|
|
if (!existsSync(this.file)) {
|
|
|
|
this.store = {};
|
|
|
|
return this.store;
|
2011-04-20 05:57:56 +00:00
|
|
|
}
|
2013-10-03 08:17:36 +00:00
|
|
|
|
2015-09-19 07:12:50 +00:00
|
|
|
//
|
|
|
|
// Else, the path exists, read it from disk
|
|
|
|
//
|
|
|
|
try {
|
|
|
|
// Deals with file that include BOM
|
|
|
|
var fileData = fs.readFileSync(this.file, 'utf8');
|
|
|
|
if (fileData.charAt(0) === '\uFEFF') {
|
|
|
|
fileData = fileData.substr(1);
|
2011-06-24 01:06:26 +00:00
|
|
|
}
|
2015-09-19 07:12:50 +00:00
|
|
|
|
|
|
|
this.store = this.parse(fileData);
|
|
|
|
}
|
|
|
|
catch (ex) {
|
|
|
|
throw new Error("Error parsing your configuration file: [" + this.file + ']: ' + ex.message);
|
2011-04-20 05:57:56 +00:00
|
|
|
}
|
2011-06-24 01:06:26 +00:00
|
|
|
|
2015-09-19 07:12:50 +00:00
|
|
|
return this.store;
|
2011-08-23 00:17:28 +00:00
|
|
|
};
|
|
|
|
|
2015-09-19 03:40:53 +00:00
|
|
|
//
|
2015-09-19 07:12:50 +00:00
|
|
|
// ### function stringify ()
|
2015-09-19 03:40:53 +00:00
|
|
|
// Returns an encrypted version of the contents IIF
|
|
|
|
// `this.secure` is enabled
|
|
|
|
//
|
2017-10-30 14:01:55 +00:00
|
|
|
File.prototype.stringify = function (format) {
|
2017-10-21 20:01:01 +00:00
|
|
|
var data = this.store;
|
2017-10-30 14:01:55 +00:00
|
|
|
if (!format) {
|
|
|
|
format = this.format
|
|
|
|
}
|
2015-09-19 03:40:53 +00:00
|
|
|
|
2015-09-19 07:12:50 +00:00
|
|
|
if (this.secure) {
|
2019-05-16 03:27:38 +00:00
|
|
|
var self = this;
|
|
|
|
data = Object.keys(data).reduce(function (acc, key) {
|
|
|
|
var value = format.stringify(data[key]);
|
|
|
|
var iv = crypto.randomBytes(16);
|
|
|
|
var cipher = crypto.createCipheriv(self.secure.alg, self.secure.secret, iv);
|
|
|
|
var ciphertext = cipher.update(value, 'utf8', 'hex');
|
|
|
|
ciphertext += cipher.final('hex');
|
|
|
|
acc[key] = { alg: self.secure.alg, value: ciphertext, iv: iv.toString('hex') };
|
|
|
|
return acc;
|
|
|
|
}, {});
|
2015-09-19 07:12:50 +00:00
|
|
|
}
|
|
|
|
|
2017-10-30 14:01:55 +00:00
|
|
|
return format.stringify(data, null, this.spacing);
|
2015-09-19 03:40:53 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
//
|
2015-09-19 07:12:50 +00:00
|
|
|
// ### function parse (contents)
|
2015-09-19 03:40:53 +00:00
|
|
|
// Returns a decrypted version of the contents IFF
|
|
|
|
// `this.secure` is enabled.
|
|
|
|
//
|
2015-09-19 07:12:50 +00:00
|
|
|
File.prototype.parse = function (contents) {
|
2017-10-21 20:01:01 +00:00
|
|
|
var parsed = this.format.parse(contents);
|
2015-09-19 03:40:53 +00:00
|
|
|
|
2019-05-16 03:27:38 +00:00
|
|
|
if (this.secure) {
|
|
|
|
var self = this;
|
|
|
|
var outdated = false;
|
|
|
|
parsed = Object.keys(parsed).reduce(function (acc, key) {
|
|
|
|
var value = parsed[key];
|
|
|
|
var decipher = crypto.createDecipher(value.alg, self.secure.secret);
|
|
|
|
if (value.iv) {
|
|
|
|
// For backward compatibility, use createDecipheriv only if there is iv stored in file
|
|
|
|
decipher = crypto.createDecipheriv(value.alg, self.secure.secret, Buffer.from(value.iv, 'hex'));
|
|
|
|
} else {
|
|
|
|
outdated = true;
|
|
|
|
}
|
|
|
|
var plaintext = decipher.update(value.value, 'hex', 'utf8');
|
|
|
|
plaintext += decipher.final('utf8');
|
|
|
|
acc[key] = self.format.parse(plaintext);
|
|
|
|
return acc;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
if (outdated) {
|
|
|
|
// warn user if the file is encrypted without iv
|
|
|
|
console.warn('Your encrypted file is outdated (encrypted without iv). Please re-encrypt your file.');
|
|
|
|
}
|
2015-09-19 07:12:50 +00:00
|
|
|
}
|
|
|
|
|
2019-05-16 03:27:38 +00:00
|
|
|
return parsed;
|
2015-09-19 07:12:50 +00:00
|
|
|
|
2015-09-19 03:40:53 +00:00
|
|
|
};
|
|
|
|
|
2016-02-02 06:21:15 +00:00
|
|
|
|
2011-08-23 00:17:28 +00:00
|
|
|
//
|
2011-08-23 10:38:51 +00:00
|
|
|
// ### function search (base)
|
2011-08-23 00:17:28 +00:00
|
|
|
// #### @base {string} Base directory (or file) to begin searching for the target file.
|
2012-04-14 19:28:55 +00:00
|
|
|
// Attempts to find `this.file` by iteratively searching up the
|
|
|
|
// directory structure
|
2011-08-23 00:17:28 +00:00
|
|
|
//
|
2011-08-23 10:38:51 +00:00
|
|
|
File.prototype.search = function (base) {
|
|
|
|
var looking = true,
|
2011-08-23 00:17:28 +00:00
|
|
|
fullpath,
|
2011-08-23 10:38:51 +00:00
|
|
|
previous,
|
2011-08-23 00:17:28 +00:00
|
|
|
stats;
|
|
|
|
|
2011-08-23 10:38:51 +00:00
|
|
|
base = base || process.cwd();
|
|
|
|
|
2011-08-23 00:17:28 +00:00
|
|
|
if (this.file[0] === '/') {
|
|
|
|
//
|
|
|
|
// If filename for this instance is a fully qualified path
|
|
|
|
// (i.e. it starts with a `'/'`) then check if it exists
|
|
|
|
//
|
|
|
|
try {
|
2011-08-23 10:38:51 +00:00
|
|
|
stats = fs.statSync(fs.realpathSync(this.file));
|
2011-08-23 00:17:28 +00:00
|
|
|
if (stats.isFile()) {
|
|
|
|
fullpath = this.file;
|
|
|
|
looking = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (ex) {
|
|
|
|
//
|
|
|
|
// Ignore errors
|
|
|
|
//
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (looking && base) {
|
|
|
|
//
|
|
|
|
// Attempt to stat the realpath located at `base`
|
|
|
|
// if the directory does not exist then return false.
|
|
|
|
//
|
|
|
|
try {
|
|
|
|
var stat = fs.statSync(fs.realpathSync(base));
|
|
|
|
looking = stat.isDirectory();
|
|
|
|
}
|
|
|
|
catch (ex) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2011-08-23 00:17:28 +00:00
|
|
|
while (looking) {
|
2011-08-23 10:38:51 +00:00
|
|
|
//
|
|
|
|
// Iteratively look up the directory structure from `base`
|
|
|
|
//
|
2011-08-23 00:17:28 +00:00
|
|
|
try {
|
|
|
|
stats = fs.statSync(fs.realpathSync(fullpath = path.join(base, this.file)));
|
|
|
|
looking = stats.isDirectory();
|
|
|
|
}
|
|
|
|
catch (ex) {
|
2011-08-23 10:38:51 +00:00
|
|
|
previous = base;
|
|
|
|
base = path.dirname(base);
|
|
|
|
|
|
|
|
if (previous === base) {
|
|
|
|
//
|
|
|
|
// If we've reached the top of the directory structure then simply use
|
|
|
|
// the default file path.
|
|
|
|
//
|
2011-08-23 00:17:28 +00:00
|
|
|
try {
|
2011-08-23 10:38:51 +00:00
|
|
|
stats = fs.statSync(fs.realpathSync(fullpath = path.join(this.dir, this.file)));
|
|
|
|
if (stats.isDirectory()) {
|
|
|
|
fullpath = undefined;
|
2011-08-23 00:17:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (ex) {
|
|
|
|
//
|
|
|
|
// Ignore errors
|
|
|
|
//
|
|
|
|
}
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2011-08-23 00:17:28 +00:00
|
|
|
looking = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-08-23 10:38:51 +00:00
|
|
|
//
|
|
|
|
// Set the file for this instance to the fullpath
|
|
|
|
// that we have found during the search. In the event that
|
|
|
|
// the search was unsuccessful use the original value for `this.file`.
|
|
|
|
//
|
|
|
|
this.file = fullpath || this.file;
|
2012-04-14 19:28:55 +00:00
|
|
|
|
2011-08-23 00:17:28 +00:00
|
|
|
return fullpath;
|
2011-12-25 14:30:48 +00:00
|
|
|
};
|