diff --git a/package-lock.json b/package-lock.json index 5cb3ea5..f2268b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -145,8 +145,7 @@ "babylon": { "version": "7.0.0-beta.19", "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.19.tgz", - "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==", - "dev": true + "integrity": "sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A==" }, "balanced-match": { "version": "1.0.0", @@ -244,7 +243,6 @@ "version": "0.8.9", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.8.9.tgz", "integrity": "sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is=", - "dev": true, "requires": { "underscore-contrib": "~0.3.0" } @@ -651,8 +649,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "esprima": { "version": "2.7.3", @@ -866,8 +863,7 @@ "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "granax": { "version": "3.1.4", @@ -980,6 +976,34 @@ "which": "^1.2.9" } }, + "jsdoc": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz", + "integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==", + "dev": true, + "requires": { + "babylon": "7.0.0-beta.19", + "bluebird": "~3.5.0", + "catharsis": "~0.8.9", + "escape-string-regexp": "~1.0.5", + "js2xmlparser": "~3.0.0", + "klaw": "~2.0.0", + "marked": "~0.3.6", + "mkdirp": "~0.5.1", + "requizzle": "~0.2.1", + "strip-json-comments": "~2.0.1", + "taffydb": "2.6.2", + "underscore": "~1.8.3" + }, + "dependencies": { + "marked": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", + "dev": true + } + } + }, "marked": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/marked/-/marked-0.5.0.tgz", @@ -1313,7 +1337,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", - "dev": true, "requires": { "xmlcreate": "^1.0.1" } @@ -1329,7 +1352,6 @@ "version": "3.5.5", "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.5.5.tgz", "integrity": "sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg==", - "dev": true, "requires": { "babylon": "7.0.0-beta.19", "bluebird": "~3.5.0", @@ -1387,7 +1409,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/klaw/-/klaw-2.0.0.tgz", "integrity": "sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY=", - "dev": true, "requires": { "graceful-fs": "^4.1.9" } @@ -1535,8 +1556,7 @@ "marked": { "version": "0.3.19", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.19.tgz", - "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==", - "dev": true + "integrity": "sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg==" }, "mem": { "version": "1.1.0", @@ -2166,7 +2186,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.1.tgz", "integrity": "sha1-aUPDUwxNmn5G8c3dUcFY/GcM294=", - "dev": true, "requires": { "underscore": "~1.6.0" }, @@ -2174,8 +2193,7 @@ "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" } } }, @@ -2435,8 +2453,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "supports-color": { "version": "5.4.0", @@ -2450,8 +2467,7 @@ "taffydb": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true + "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=" }, "temp": { "version": "0.8.3", @@ -2512,14 +2528,12 @@ "underscore": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", - "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", - "dev": true + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" }, "underscore-contrib": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/underscore-contrib/-/underscore-contrib-0.3.0.tgz", "integrity": "sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc=", - "dev": true, "requires": { "underscore": "1.6.0" }, @@ -2527,8 +2541,7 @@ "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", - "dev": true + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=" } } }, @@ -2669,8 +2682,7 @@ "xmlcreate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", - "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=", - "dev": true + "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=" }, "xtend": { "version": "4.0.1", diff --git a/package.json b/package.json index 0c2bb04..fa09b0f 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "grunt": "^1.0.3", "grunt-jsdoc": "^2.3.0", "ink-docstrap": "^1.3.2", - "jsdoc": "^3.5.5", "mocha": "^5.2.0", "request": "^2.79.0", "request-promise": "^4.2.2" @@ -34,6 +33,7 @@ "granax": "^3.1.4", "jrpc2": "git+https://github.com/znetstar/jrpc2.git#f1521bd3f2fa73d716e74bf8f746d08d5e03e7d7", "js-weighted-list": "^0.1.1", + "jsdoc": "^3.5.5", "lodash": "^4.17.4", "nanoid": "^1.2.3", "native-dns": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb", diff --git a/src/TorPool.js b/src/TorPool.js index 5143205..776a87d 100644 --- a/src/TorPool.js +++ b/src/TorPool.js @@ -29,7 +29,25 @@ const TorProcess = require('./TorProcess'); Promise.promisifyAll(fs); + + +/** + * Class that represents a pool of Tor processes. + * @extends EventEmitter + */ class TorPool extends EventEmitter { + /** + * Creates an instance of TorPool. + * + * @param {string} tor_path - Path to the Tor executable. + * @param {Object|Function} [default_config] - Default configuration that will be passed to all Tor instances created. Can be a function. See {@link https://bit.ly/2QrmI3o|Tor Documentation} for all possible options + * @param {string} data_directory - Parent directory for the data directory of each proccess. + * @param {string} load_balance_method - Name of the load balance method to use. See {@link TorPool#load_balance_methods}. + * @param {string} [granax_options] - Object containing options that will be passed to granax. + * @param {string} [logger] - A winston logger. If not provided no logging will occur. + * + * @throws If "data_directory" is not provided. + */ constructor(tor_path, default_config, data_directory, load_balance_method, granax_options, logger) { if (!data_directory) throw new Error('Invalid "data_directory"'); @@ -43,11 +61,28 @@ class TorPool extends EventEmitter { this.granax_options = granax_options; } + /** + * Returns a Set containing the names of all of the groups. + * + * @readonly + * @type {Set} + */ get group_names() { return new Set(_.flatten(this.instances.map((instance) => instance.instance_group).filter(Boolean))); } + /** + * Returns an array containing all of the instances in a group. + * + * @param {string} group_name - The group to query. + * @returns {string[]} + * + * @throws If the provided group does not exist + */ instances_by_group(group_name) { + if (!this.group_names.has(group_name)) + throw new Error(`Group "${group_name}" doesn't exist`); + let group = this.groups[group_name]; let arr = []; @@ -58,10 +93,24 @@ class TorPool extends EventEmitter { return arr; } + /** + * Adds an instance to a group. If the group doesn't exist it will be created. + * + * @param {string} group - The group to add the instance to. + * @param {TorProcess} instance - The instance in question. + */ add_instance_to_group(group, instance) { instance.definition.Group = _.union(instance.instance_group, [group]); } + /** + * Adds an instance to a group by the {@link TorProcess#instance_name} property on the instance. If the group doesn't exist it will be created. + * + * @param {string} group - The group to add the instance to. + * @param {string} instance_name - The name of the instance in question. + * + * @throws If an instance with the name provided does not exist + */ add_instance_to_group_by_name(group, instance_name) { let instance = this.instance_by_name(instance_name); @@ -70,6 +119,14 @@ class TorPool extends EventEmitter { return this.add_instance_to_group(group, instance); } + /** + * Adds an instance to a group by the index of the instance in the pool. If the group doesn't exist it will be created. + * + * @param {string} group - The group to add the instance to. + * @param {number} instance_index - The index of the instance in question. + * + * @throws If an instance with the index provided does not exist. + */ add_instance_to_group_at(group, instance_index) { let instance = this.instance_at(instance_index); @@ -78,10 +135,24 @@ class TorPool extends EventEmitter { return this.add_instance_to_group(group, instance); } + /** + * Removes an instance from a group. + * + * @param {string} group - The group to remove the instance from. + * @param {TorProcess} instance - The instance in question. + */ remove_instance_from_group(group, instance) { _.remove(instance.definition.Group, (g) => g === group); } + /** + * Removes an instance from a group by the {@link TorProcess#instance_name} property on the instance. + * + * @param {string} group - The group to remove the instance from. + * @param {string} instance_name - The name of the instance in question. + * + * @throws If an instance with the name provided does not exist. + */ remove_instance_from_group_by_name(group, instance_name) { let instance = this.instance_by_name(instance_name); @@ -90,6 +161,14 @@ class TorPool extends EventEmitter { return this.remove_instance_from_group(group, instance); } + /** + * Removes an instance from a group by the index of the instance in the pool. + * + * @param {string} group - The group to remove the instance from. + * @param {number} instance_index - The index of the instance in question. + * + * @throws If an instance with the index provided does not exist. + */ remove_instance_from_group_at(group, instance_index) { let instance = this.instance_at(instance_index); @@ -98,6 +177,34 @@ class TorPool extends EventEmitter { return this.remove_instance_from_group(group, instance); } + + /** + * Represents a group of instances. Group is a Proxy with an array as its object. The array is generated by calling {@link TorPool#instances_in_group}. + * When called with an index (e.g. `Group[0]`) will return the instance at that index. + * Helper functions are available as properties. + * @typedef {TorProcess[]} Group + * + * @property {Function} add - Adds an instance to the group. + * @property {Function} remove - Removes an instance from the group. + * @property {Function} add_by_name - Adds an instance to the group by the {@link TorProcess#instance_name} property on the instance. + * @property {Function} remove_by_name - Removes an instance from the group by the {@link TorProcess#instance_name} property on the instance. + * @property {Function} remove_at - Removes an instance from the group by the index of the instance in the group. + * @property {number} length - The size of the group of instances + * @property {Function} rotate - Rotates the array of instances + */ + + /** + * Represents a collection of groups as an associative array. GroupCollection is a Proxy with a Set as its object. The Set is {@link TorPool#group_names}. + * If a non-existant group is referenced (e.g. `Groups["doesn't exist"]`) it will be created. So `Groups["doesn't exist"].add(my_instance)` will create the group and add the instance to it. + * @typedef {Group[]} GroupCollection + */ + + /** + * Represents all groups currently in the pool. + * + * @readonly + * @type {GroupCollection} + */ get groups() { let groupHandler = { get: (instances, prop) => { @@ -178,6 +285,15 @@ class TorPool extends EventEmitter { return new Proxy(this.group_names, groupsHandler); } + /** + * The default configuration that will be passed to each instance. Values from "definition.Config" on each instance will override the default config + * + */ + + /** Getter + * + * @type {Object|Function} + */ get default_tor_config() { if (typeof(this._default_tor_config) === 'function') return this._default_tor_config(); @@ -187,10 +303,22 @@ class TorPool extends EventEmitter { return {}; } + /** + * Setter + * + * @param {Object|Function} value + */ set default_tor_config(value) { this._default_tor_config = value; } + /** + * Returns an enumeration of load balance methods as functions + * + * @readonly + * @enum {Function} + * @static + */ static get load_balance_methods() { - return { + return Object.freeze({ round_robin: function (instances) { return instances.rotate(1); }, @@ -206,21 +334,45 @@ class TorPool extends EventEmitter { i._weighted_list = instances._weighted_list; return i; } - }; + }); } + /** + * An array containing all instances in the pool. + * + * @readonly + * @type {TorProcess[]} + */ get instances() { return this._instances.slice(0); } + /** + * An array containing the names of the instances in the pool. + * + * @readonly + * @type {string[]} + */ get instance_names() { return this.instances.map((i) => i.instance_name); } + /** + * Creates an instance then adds it to the pool from the provided definiton. + * Instance will be added (and Promise will resolve) after the instance is fully bootstrapped. + * + * @async + * @param {InstanceDefinition} [instance_definition={}] - Instance definition that will be used to create the instance. + * @returns {Promise} - The instance that was created. + * + * @throws If an instance with the same {@link InstanceDefinition#Name} already exists. + */ async create_instance(instance_definition) { if (!(fs.existsSync(this.data_directory))) await fs.mkdirAsync(this.data_directory); + instance_definition = instance_definition || {}; + if (instance_definition.Name && this.instance_names.indexOf(instance_definition.Name) !== -1) throw new Error(`Instance named ${instance_definition.Name} already exists`); @@ -237,48 +389,107 @@ class TorPool extends EventEmitter { instance.once('error', reject); instance.once('ready', () => { + /** + * Fires when an instance has been created. + * + * @event TorPool#instance_created + * @type {TorProcess} + * @param {TorProcess} instance - The instance that was created. + */ this.emit('instance_created', instance); resolve(instance); }); }); } + /** + * Adds one or more instances to the pool from an array of definitions or single definition. + * @param {InstanceDefinition[]|InstanceDefinition} instance_definitions + * + * @async + * @return {Promise} + * @throws If `instance_definitions` is falsy. + */ async add(instance_definitions) { + if (!instance_definitions) + throw new Error('Invalid "instance_definitions"'); + return await Promise.all([].concat(instance_definitions).map((instance_definition) => this.create_instance(instance_definition))); } + /** + * Creates one or more instances to the pool from either an array of definitions, a single definition or a number. + * If a number is provided it will create n instances with empty definitions (e.g. `TorPool.create(5)` will create 5 instances). + * @param {InstanceDefinition[]|InstanceDefinition|number} instance_definitions + * + * @async + * @return {Promise} + * @throws If `instances` is falsy. + */ async create(instances) { + if (!instances) + throw new Error('Invalid "instances"'); + if (typeof(instances) === 'number') { - instances = Array.from(Array(instances)).map(() => { - return { - Config: {} - }; - }); + instances = Array.from(Array(instances)).map(() => ({})); } return await this.add(instances); } + /** + * Searches for an instance with the matching {@link TorProcess#instance_name} property. + * @param {string} name - Name of the instance to search for + * @returns {TorProcess} - Matching instance + */ instance_by_name(name) { return this._instances.filter((i) => i.instance_name === name)[0]; } + /** + * Returns the instance located at the provided index in the pool. + * Is equivalent to `{@link TorPool#instances}[index]` + * @param {number} index - Index of the instance in the pool + * @returns {TorProcess} - Matching instance + */ instance_at(index) { return this._instances[index]; } - async remove(instances) { + /** + * Removes a number of instances from the pool and kills their Tor processes. + * @param {number} instances - Number of instances to remove + * @param {number} [start_at=0] - Index to start removing from + * @async + * @returns {Promise} - Promise will resolve when the processes are dead + */ + async remove(instances, start_at) { this._instances._weighted_list = void(0); - let instances_to_remove = this._instances.splice(0, instances); + let instances_to_remove = this._instances.splice((start_at || 0), instances); await Promise.all(instances_to_remove.map((instance) => instance.exit())); } + /** + * Removes an instance at the provided index and kills its Tor process. + * @param {number} instance_index - Index of the instance to remove + * @async + * @returns {Promise} - Promise will resolve when the process is dead + */ async remove_at(instance_index) { this._instances._weighted_list = void(0); let instance = this._instances.splice(instance_index, 1)[0]; + if (!instance) + throw new Error(`No instance at "${instance_index}"`); + await instance.exit(); } + /** + * Removes an instance whose {@link TorProcess#instance_name} property matches the provided name and kills its Tor process. + * @param {string} instance_name - Name of the instance to remove + * @async + * @returns {Promise} - Promise will resolve when the process is dead + */ async remove_by_name(instance_name) { let instance = this.instance_by_name(instance_name); if (!instance) @@ -287,33 +498,82 @@ class TorPool extends EventEmitter { await this.remove_at(instance_index); } + /** + * Runs the load balance function ({@link TorPool#load_balance_method}) on the array of instances in the pool and returns the first instance in the array. + * + * @returns {TorProcess} - The first instance in the modified array. + */ next() { this._instances = TorPool.load_balance_methods[this.load_balance_method](this._instances); return this.instances[0]; } + /** + * Rotates the array containing instances in the group provided so that the second element becomes the first element and the first element becomes the last element. + * [1,2,3] -> [2,3,1] + * @todo Load balance methods other than "round_robin" to be used + * @param {string} group - Name of the group + * @returns {TorProcess} - The first element in the modified array + */ + next_by_group(group) { this.groups[group].rotate(1); return this.groups[group][0]; } + + /** + * Kills the Tor processes of all instances in the pool. + * + * @async + * @returns {Promise} - Resolves when all instances have been killed. + */ async exit() { await Promise.all(this._instances.map((instance) => instance.exit())); this._instances = []; } + /** + * Gets new identities for all instances in the pool. + * + * @async + * @returns {Promise} - Resolves when all instances have new identities. + */ async new_identites() { await Promise.all(this.instances.map((instance) => instance.new_identity())); } + /** + * Gets new identities for all instances in a group. + * + * @async + * @param {string} - Name of the group. + * @returns {Promise} - Resolves when all instances in the group have new identities. + */ async new_identites_by_group(group) { await Promise.all(this.instances_by_group(group).map((instance) => instance.new_identity())); } + /** + * Gets a new identity for the instance at the provided index in the pool. + * + * @async + * @param {number} - Index of the instance in the pool. + * @returns {Promise} - Resolves when the instance has a new identity. + */ async new_identity_at(index) { await this.instances[index].new_identity(); } + /** + * Gets a new identity for the instance whose {@link TorProcess.instance_name} matches the provided name. + * + * @async + * @param {string} - Name of the instance. + * @returns {Promise} - Resolves when the instance has a new identity. + * + * @throws When no instance matched the provided name. + */ async new_identity_by_name(name) { let instance = this.instance_by_name(name); @@ -323,7 +583,16 @@ class TorPool extends EventEmitter { await instance.new_identity(); } - + /** + * Get a configuration value from the instance whose {@link TorProcess.instance_name} matches the provided name via the control protocol. + * + * @async + * @param {string} name - Name of the instance. + * @param {string} keyword - Name of the configuration property. + * + * @returns {Promise} - The configuration property's value. + * @throws When no instance matched the provided name. + */ async get_config_by_name(name, keyword) { let instance = this.instance_by_name(name); if (!instance) @@ -332,6 +601,17 @@ class TorPool extends EventEmitter { return await instance.get_config(keyword); } + /** + * Set a configuration value for the instance whose {@link TorProcess.instance_name} matches the provided name via the control protocol. + * + * @async + * @param {string} name - Name of the instance. + * @param {string} keyword - Name of the configuration property. + * @param {*} value - Value to set the configuration property to. + * + * @returns {Promise} + * @throws When no instance matched the provided name. + */ async set_config_by_name(name, keyword, value) { let instance = this.instance_by_name(name); if (!instance) @@ -340,6 +620,16 @@ class TorPool extends EventEmitter { return await instance.set_config(keyword, value); } + /** + * Get a configuration value from the instance at the index in the pool via the control protocol. + * + * @async + * @param {number} index - Index of the instance in the pool. + * @param {string} keyword - Name of the configuration property. + * + * @returns {Promise} - The configuration property's value. + * @throws When no instance exists at the provided index. + */ async get_config_at(index, keyword) { let instance = this.instances[index]; if (!instance) @@ -348,6 +638,17 @@ class TorPool extends EventEmitter { return await instance.get_config(keyword); } + /** + * Set a configuration value for the instance at the index in the pool via the control protocol. + * + * @async + * @param {number} index - Index of the instance in the pool. + * @param {string} keyword - Name of the configuration property. + * @param {*} value - Value to set the configuration property to. + * + * @returns {Promise} + * @throws When no instance exists at the provided index. + */ async set_config_at(index, keyword, value) { let instance = this.instances[index]; if (!instance) @@ -355,22 +656,55 @@ class TorPool extends EventEmitter { return await instance.set_config(keyword, value); } + /** + * Set a configuration value for all instances in the provided group via the control protocol. + * + * @async + * @param {string} group - Name of the group. + * @param {string} keyword - Name of the configuration property. + * @param {*} value - Value to set the configuration property to. + * + * @returns {Promise} + * @throws When the provided group does not exist. + */ async set_config_by_group(group, keyword, value) { return await Promise.all(this.instances_by_group(group).map((instance) => instance.set_config(keyword, value))); } - async signal_by_group(group, signal) { - await Promise.all(this.instances_by_group(group).map((instance) => instance.signal(signal))); - } - + /** + * Set a configuration value for all instances in the pool via the control protocol. + * + * @async + * @param {string} keyword - Name of the configuration property. + * @param {*} value - Value to set the configuration property to. + * + * @returns {Promise} + */ async set_config_all(keyword, value) { return await Promise.all(this.instances.map((instance) => instance.set_config(keyword, value))); } + /** + * Send a signal via the control protocol to all instances in the pool. + * + * @async + * @param {string} signal - The signal to send. + * + * @returns {Promise} + */ async signal_all(signal) { await Promise.all(this.instances.map((instance) => instance.signal(signal))); } + /** + * Send a signal via the control protocol to an instance whose {@link TorProcess#instance_name} property matches the provided name. + * + * @async + * @param {string} name - Name of the instance. + * @param {string} signal - The signal to send. + * + * @returns {Promise} + */ async signal_by_name(name, signal) { let instance = this.instance_by_name(name); if (!instance) @@ -379,6 +713,15 @@ class TorPool extends EventEmitter { await instance.signal(signal); } + /** + * Send a signal via the control protocol to an instance at the provided index in the pool. + * + * @async + * @param {number} index - Index of the instance in the pool. + * @param {string} signal - The signal to send. + * + * @returns {Promise} + */ async signal_at(index, signal) { let instance = this.instances[index]; if (!instance) @@ -386,6 +729,20 @@ class TorPool extends EventEmitter { await instance.signal(signal); } + + /** + * Send a signal via the control protocol to all instances in the provided group. + * + * @async + * @param {string} group - Name of the group. + * @param {string} signal - The signal to send. + * + * @returns {Promise} + * @throws When the provided group does not exist. + */ + async signal_by_group(group, signal) { + await Promise.all(this.instances_by_group(group).map((instance) => instance.signal(signal))); + } }; module.exports = TorPool; \ No newline at end of file diff --git a/src/TorProcess.js b/src/TorProcess.js index 4d4c04d..fb74a23 100644 --- a/src/TorProcess.js +++ b/src/TorProcess.js @@ -16,24 +16,32 @@ const temp = require('temp'); const { TorController } = require('granax'); const nanoid = require("nanoid"); const winston = require('winston'); -winston. Promise.promisifyAll(temp); Promise.promisifyAll(fs); temp.track(); +/** + * @typedef {Object} InstanceDefinition + * @property {string} [Name] - Name of the instance. + * @property {string[]|string} [Group=[]] - Groups the instance belongs to. + * @property {Object} [Config={}] - Configuration that will be passed to Tor. See {@link https://bit.ly/2QrmI3o|Tor Documentation} for all possible options. + * @property {Number} [Weight] - Weight of the instance for "weighted" load balancing. + */ + /** * Class that represents an individual Tor process. + * * @extends EventEmitter */ class TorProcess extends EventEmitter { /** - * Creates a TorProcess Object + * Creates an instance of TorProcess * - * @param {String} tor_path - Path to the Tor executable. - * @param {Object} [definition={}] - Object containing various options for the instance. See {@link https://github.com/znetstar/tor-router/wiki/Configuration|the wiki} for more info - * @param {Object} [granax_options] - Object containing options that will be passed to granax - * @param {Logger} [logger] - A winston logger used for logging. + * @param {string} tor_path - Path to the Tor executable. + * @param {InstanceDefinition} [definition] - Object containing various options for the instance. See {@link InstanceDefinition} for more info. + * @param {Object} [granax_options] - Object containing options that will be passed to granax. + * @param {Logger} [logger] - A winston logger. If not provided no logging will occur. */ constructor(tor_path, definition, granax_options, logger) { super(); @@ -41,11 +49,32 @@ class TorProcess extends EventEmitter { definition = definition || {}; definition.Group = definition.Group ? [].concat(definition.Group) : []; + definition.Config = definition.Config || {}; this._definition = definition; + /** + * Path to the Tor executable. + * + * @type {string} + * @public + */ this.tor_path = tor_path; + + /** + * Object containing options that will be passed to granax. + * + * @type {Object} + * @public + */ this.granax_options = granax_options; + + /** + * The password that will be set for the control protocol. + * + * @type {string} + * @public + */ this.control_password = crypto.randomBytes(128).toString('base64'); this._id = nanoid(12); @@ -53,7 +82,7 @@ class TorProcess extends EventEmitter { } /** - * Kills the Tor process + * Kills the Tor process. * * @async * @returns {Promise} @@ -71,101 +100,101 @@ class TorProcess extends EventEmitter { } /** - * The unique identifier assigned to each instance + * The unique identifier assigned to each instance. * * @readonly - * @returns {String} + * @type {string} */ get id() { return this._id; } /** - * Groups the instance are currently in + * Groups the instance are currently in. * * @readonly - * @returns {String[]} + * @type {string[]} */ get instance_group() { - return (this.definition && this.definition.Group) || []; + return (this.definition && this.definition.Group); } /** - * Either the "Name" property of the definition or the "id" property + * Either the "Name" property of the definition or the {@link TorProcess#id} property. * * @readonly - * @returns {String} + * @type {string} */ get instance_name() { return (this.definition && this.definition.Name) || this.id; } /** - * The definition used to create the instance + * The definition used to create the instance. * * @readonly - * @returns {String} + * @type {string} */ get definition() { return this._definition; } /** - * The configuration passed to Tor. The same value as "definition.Config" + * The configuration passed to Tor. The same value as `definition.Config`. * * @readonly - * @returns {Object} + * @type {Object} */ get tor_config() { return this.definition.Config; } /** - * Port Tor is bound to for DNS traffic + * Port Tor is bound to for DNS traffic. * * @readonly - * @returns {Number} + * @type {number} */ get dns_port() { return this._ports.dns_port; } /** - * Port Tor is bound to for SOCKS5 traffic + * Port Tor is bound to for SOCKS5 traffic. * * @readonly - * @returns {Number} + * @type {number} */ get socks_port() { return this._ports.socks_port; } /** - * Port Tor is bound to for API access + * Port Tor is bound to for API access. * * @readonly - * @returns {Number} + * @type {number} */ get control_port() { return this._ports.control_port; } /** - * Instance of granax connected to the Tor process + * Instance of granax.TorController connected to the Tor process. * * @readonly - * @returns {Object} + * @type {TorController} */ get controller() { return this._controller; } /** - * Property identifiyng whether Tor has started + * Property identifiyng whether Tor has started. * * @readonly - * @returns {Boolean} + * @type {boolean} */ get ready() { return this._ready; } /* Passthrough to granax */ /** - * Requests a new identity via the control interface + * Requests a new identity via the control protocol. * * @async */ @@ -176,12 +205,13 @@ class TorProcess extends EventEmitter { } /** - * Retrieves a configuration value from the instance via the control interface + * Retrieves a configuration value from the instance via the control protocol. * * @async - * @param {String} keyword - The name of the configuration property to retrieve + * @throws Will throw an error if not connected to the control protocol. + * @param {string} keyword - The name of the configuration property to retrieve. * - * @returns {Promise} + * @returns {Promise} */ async get_config(keyword) { if (!this.controller) @@ -191,11 +221,12 @@ class TorProcess extends EventEmitter { } /** - * Sets a configuration value for the instance via the control interface + * Sets a configuration value for the instance via the control protocol. * * @async - * @param {String} keyword - The name of the configuration property to retrieve - * @param value - Value to set the property to + * @throws Will throw an error if not connected to the control protocol. + * @param {string} keyword - The name of the configuration property to retrieve. + * @param {*} value - Value to set the property to. * * @returns {Promise} */ @@ -208,10 +239,11 @@ class TorProcess extends EventEmitter { } /** - * Sends a signal via the control tnterface + * Sends a signal via the control tnterface. * * @async - * @param {String} signal - The signal to send + * @throws Will throw an error if not connected to the control protocol. + * @param {string} signal - The signal to send. * * @returns {Promise} */ @@ -224,11 +256,11 @@ class TorProcess extends EventEmitter { } /** - * Creates the Tor process based on the configuration provided. Promise is resolved when the process has been started + * Creates the Tor process based on the configuration provided. Promise is resolved when the process has been started. * * @async * - * @returns {Promise} - The process that has been created + * @returns {Promise} - The process that has been created. */ async create() { this._ports = {}; @@ -262,11 +294,11 @@ class TorProcess extends EventEmitter { } /** - * An event that fires when the process has closed + * An event that fires when the process has closed. * * @event TorProcess#process_exit - * @type {Number} - * @returns {Number} - The exit code from the process + * @type {number} + * @param {number} code - The exit code from the process. */ this.emit('process_exit', code); })); @@ -297,7 +329,7 @@ class TorProcess extends EventEmitter { this.logger.debug(`[tor-${this.instance_name}]: authenticated with tor instance via the control port`); this.control_port_connected = true; /** - * An event that fires when a connection has been established to the control interface + * An event that fires when a connection has been established to the control protocol. * * @event TorProcess#controller_ready */ @@ -313,7 +345,7 @@ class TorProcess extends EventEmitter { if (text.indexOf('Bootstrapped 100%: Done') !== -1){ this.bootstrapped = true; /** - * An event that fires when the Tor process is fully bootstrapped (and ready for traffic) + * An event that fires when the Tor process is fully bootstrapped (and ready for traffic). * * @event TorProcess#ready */ @@ -323,7 +355,7 @@ class TorProcess extends EventEmitter { if (text.indexOf('Opening Control listener on') !== -1) { this.control_port_listening = true; /** - * An event that fires when the Tor process has started listening for control interface traffic + * An event that fires when the Tor process has started listening for control interface traffic. * * @event TorProcess#control_listen */ @@ -333,7 +365,7 @@ class TorProcess extends EventEmitter { if (text.indexOf('Opening Socks listener on') !== -1) { this.socks_port_listening = true; /** - * An event that fires when the Tor process has started listening for SOCKS5 traffic + * An event that fires when the Tor process has started listening for SOCKS5 traffic. * * @event TorProcess#socks_listen */ @@ -342,7 +374,7 @@ class TorProcess extends EventEmitter { if (text.indexOf('Opening DNS listener on') !== -1) { /** - * An event that fires when the Tor process has started listening for DNS traffic + * An event that fires when the Tor process has started listening for DNS traffic. * * @event TorProcess#dns_listen */ @@ -352,7 +384,7 @@ class TorProcess extends EventEmitter { if (text.indexOf('[err]') !== -1) { /** - * An event that fires the Tor process has written an error to stdout or stderr or an error occured connecting to the control interface + * An event that fires the Tor process has written an error to stdout or stderr or an error occured connecting to the control protocol. * * @event TorProcess#error * @type {Error} diff --git a/src/default_config.js b/src/default_config.js index 3903757..ea68aa3 100644 --- a/src/default_config.js +++ b/src/default_config.js @@ -23,20 +23,19 @@ module.exports = { /* Taken from https://github.com/bookchin/granax/blob/master/index.js */ switch (platform) { case 'win32': - torpath = path.join(BIN_PATH, 'Browser', 'TorBrowser', 'Tor', 'tor.exe'); + return path.join(BIN_PATH, 'Browser', 'TorBrowser', 'Tor', 'tor.exe'); break; case 'darwin': - torpath = path.join(BIN_PATH, '.tbb.app', 'Contents', 'Resources', + return path.join(BIN_PATH, '.tbb.app', 'Contents', 'Resources', 'TorBrowser', 'Tor', 'tor'); break; case 'android': case 'linux': - torpath = path.join(BIN_PATH, 'tor-browser_en-US', 'Browser', 'TorBrowser', 'Tor', 'tor'); + return path.join(BIN_PATH, 'tor-browser_en-US', 'Browser', 'TorBrowser', 'Tor', 'tor'); break; default: - throw new Error(`Unsupported platform "${platform}"`); + return "tor"; } - return torpath; })(), "instances": null, "dns": { diff --git a/src/launch.js b/src/launch.js index 3350240..b6d0621 100644 --- a/src/launch.js +++ b/src/launch.js @@ -12,7 +12,7 @@ const package_json = JSON.parse(fs.readFileSync(`${__dirname}/../package.json`, function extractHost (host) { if (typeof(host) === 'number') - return { hostname: (typeof(default_ports.default_host) === 'string' ? default_ports.default_host : ''), port: host }; + return { hostname: (typeof(default_ports.default_host) === 'string' ? default_ports.default_host : '0.0.0.0'), port: host }; else if (typeof(host) === 'string' && host.indexOf(':') !== -1) return { hostname: host.split(':').shift(), port: Number(host.split(':').pop()) }; else @@ -36,7 +36,7 @@ async function main(nconf, logger) { if (typeof(control_host) === 'boolean') { control_host = extractHost(9077); - nconf.set('controlHost', assembleHost(control_port)); + nconf.set('controlHost', assembleHost(control_host)); } if (typeof(control_host_ws) === 'boolean') { diff --git a/test/TorPool.js b/test/TorPool.js index a880f50..88e328d 100644 --- a/test/TorPool.js +++ b/test/TorPool.js @@ -96,6 +96,18 @@ describe('TorPool', function () { let torPool; before('create tor pool', () => { torPool = torPoolFactory(); }) + it('should throw if the "instance_defintions" field is falsy', async function () { + let fn = () => {}; + try { + await torPool.add(); + } catch (error) { + fn = () => { throw error }; + } + finally { + assert.throws(fn); + } + }); + it('should create instances from several instance definitions', async function () { this.timeout(WAIT_FOR_CREATE*2); await torPool.add(_.cloneDeep(instance_defintions)) @@ -128,13 +140,25 @@ describe('TorPool', function () { }); }); - describe('#create(number_of_instances)', function () { + describe('#create(instances)', function () { let torPool; before('create tor pool', () => { torPool = torPoolFactory(); torPool.default_tor_config = { TestSocks: 1 }; - }) + }); + + it('should throw if the "number_of_instances" field is falsy', async function () { + let fn = () => {}; + try { + await torPool.create(); + } catch (error) { + fn = () => { throw error; } + } + finally { + assert.throws(fn); + } + }); it('should create 2 instances with the default config', async function () { this.timeout(WAIT_FOR_CREATE*2); @@ -169,6 +193,12 @@ describe('TorPool', function () { instances = (await tor_pool.add([ { Name: 'instance-1', Group: [ "bar", "foo" ] }, { Name: 'instance-2', Group: ["foo", "bar"] } ])); }); + it('should throw if the provided group does not exist', function () { + assert.throws(() => { + tor_pool.instances_by_group('baz'); + }); + }); + it('should return both instances', function () { assert.deepEqual(tor_pool.instances_by_group('bar'), instances); });