/**
* @module bulb/commands
* @see https://spec.torproject.org/control-spec/commands.html
*/
'use strict';
class ControlCommand {
/**
* Wraps a control command to send over the control port.
* @param {string} commandStr - Command to send.
*/
constructor(str) {
/**
* @property {string} cmd - Command instance was intialized with.
*/
this.cmd = str;
}
/**
* Unwraps the command as a raw string.
* @returns {string}
*/
toString() {
return this.cmd;
}
/**
* Map of control signals.
* @see https://spec.torproject.org/control-spec/commands.html#signal
* @typedef {Object<string, string>} module:bulb/commands~ControlCommand~Signal
*/
static get Signal() {
return {
RELOAD: 'RELOAD',
SHUTDOWN: 'SHUTDOWN',
DUMP: 'DUMP',
DEBUG: 'DEBUG',
HALT: 'HALT',
CLEARDNSCACHE: 'CLEARDNSCACHE',
NEWNYM: 'NEWNYM',
HEARTBEAT: 'HEARTBEAT'
};
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#authenticate
* @param {string} [token=""] - The auth token to send.
* @returns {module:bulb/commands~ControlCommand}
*/
static AUTHENTICATE(token = '') {
return new ControlCommand(`AUTHENTICATE ${token}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#authchallenge
* @param {string} [nonce=""] - Client nonce for challenge.
* @param {string} [type="SAFECOOKIE"] - The type of challenge.
* @returns {module:bulb/commands~ControlCommand}
*/
static AUTHCHALLENGE(nonce = '', type = 'SAFECOOKIE') {
return new ControlCommand(`AUTHCHALLENGE ${type} ${nonce}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#protocolinfo
* @returns {module:bulb/commands~ControlCommand}
*/
static PROTOCOLINFO() {
return new ControlCommand('PROTOCOLINFO');
}
/**
* @typedef {object} module:bulb/commands~ControlCommand~AddOnionOptions
* @property {string} [clientName] - Client auth identifier.
* @property {string} [clientBlob] - Arbitrary auth data.
* @property {string} [keyType="NEW"] - Create a new key or use RSA1024.
* @property {string} [keyBlob="BEST"] - Key type to create or serialized.
* @property {boolean} [discardPrivateKey=false] - Do not return key.
* @property {boolean} [detach=false] - Keep service running after close.
* @property {boolean} [basicAuth=false] - Use client name and blob auth.
* @property {boolean} [nonAnonymous=false] - Non-anononymous mode.
* @property {number} [virtualPort=80] - Virtual port to expose on the hidden service.
*/
/**
* @see https://spec.torproject.org/control-spec/commands.html#add_onion
* @param {string|number[]|number} ports - Ports to map this onion service to.
* @param {module:bulb/commands~ControlCommand~AddOnionOptions} [addOnionOpts] - Configuration options.
* @returns {module:bulb/commands~ControlCommand}
*/
static ADD_ONION(ports, opts = {}) {
const defaultOnionVirtualPort = 80;
function _addOnionPortsStringToCommand(ports, opts, command) {
if (opts.virtualPort) {
command.push(`Port=${opts.virtualPort},${ports}`);
} else {
command.push(`Port=${defaultOnionVirtualPort},${ports}`);
}
}
function _addOnionPortsToCommand(ports, opts, command) {
if (typeof ports === 'string' || typeof ports === 'number') {
_addOnionPortsStringToCommand(ports, opts, command);
return;
}
if (!ports.length) {
command.push(`Port=${defaultOnionVirtualPort}`);
return;
}
for (let port of ports) {
let _portsString;
if (port.virtualPort) {
_portsString = `Port=${port.virtualPort}`;
} else {
_portsString = `Port=${defaultOnionVirtualPort}`;
}
if (port.target) {
_portsString += `,${port.target}`;
}
command.push(_portsString);
}
}
let options = {
clientName: opts.clientName || null,
clientBlob: opts.clientBlob || null,
keyType: opts.keyType || 'NEW',
keyBlob: opts.keyBlob || 'BEST',
discardPrivateKey: opts.discardPrivateKey || false,
detach: opts.detach || false,
basicAuth: opts.basicAuth || false,
nonAnonymous: opts.nonAnonymous || false
};
let command = ['ADD_ONION', `${options.keyType}:${options.keyBlob}`];
let flagMap = [
['discardPrivateKey', 'DiscardPK'],
['detach', 'Detach'],
['basicAuth', 'BasicAuth'],
['nonAnonymous', 'NonAnonymous']
];
let flags = [];
for (let flag of flagMap) {
if (options[flag[0]]) {
flags.push(flag[1]);
}
}
if (flags.length) {
command.push('Flags=' + flags.join(','));
}
_addOnionPortsToCommand(ports, opts, command);
if (options.clientName && options.clientBlob) {
command.push(`ClientAuth=${options.clientName}:${options.clientBlob}`);
}
return new ControlCommand(command.join(' '));
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#del_onion
* @param {string} serviceId - Onion address excluding ".onion".
* @returns {module:bulb/commands~ControlCommand}
*/
static DEL_ONION(serviceId) {
return new ControlCommand(`DEL_ONION ${serviceId}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#setconf
* @param {string} keyword - Configuration key.
* @param {string} value - Configuration value.
* @returns {module:bulb/commands~ControlCommand}
*/
static SETCONF(keyword, value) {
return new ControlCommand(`SETCONF ${keyword}="${value}"`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#resetconf
* @param {string} keyword - Configuration key to reset.
* @returns {module:bulb/commands~ControlCommand}
*/
static RESETCONF(keyword) {
return new ControlCommand(`RESETCONF ${keyword}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#getconf
* @param {string} keyword - Configuration key to get.
* @returns {module:bulb/commands~ControlCommand}
*/
static GETCONF(keyword) {
return new ControlCommand(`GETCONF ${keyword}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#saveconf
* @returns {module:bulb/commands~ControlCommand}
*/
static SAVECONF() {
return new ControlCommand('SAVECONF');
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#signal
* @param {string} signal - {@link module:bulb/commands~ControlCommand~Signal} type to send.
* @returns {module:bulb/commands~ControlCommand}
*/
static SIGNAL(signal) {
return new ControlCommand(`SIGNAL ${signal}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#mapaddress
* @param {string} targetAddr - Address to map from.
* @param {string} replaceAddr - Address to map to.
* @returns {module:bulb/commands~ControlCommand}
*/
static MAPADDRESS(targetAddr, replaceAddr) {
return new ControlCommand(`MAPADDRESS ${targetAddr}=${replaceAddr}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#extendcircuit
* @param {string} circuitId - Circuit ID to extend.
* @param {string} [purpose] - Purpose to set for the circuit.
* @returns {module:bulb/commands~ControlCommand}
*/
static EXTENDCIRCUIT(circuitId, purpose) {
return new ControlCommand(`EXTENDCIRCUIT ${circuitId}` +
(purpose ? ` purpose="${purpose}"` : ''));
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#setcircuitpurpose
* @param {string} circuitId - Circuit ID to set purpose for.
* @param {string} [purpose] - Purpose to set for the circuit.
* @returns {module:bulb/commands~ControlCommand}
*/
static SETCIRCUITPURPOSE(circuitId, purpose) {
return new ControlCommand(`SETCIRCUITPURPOSE ${circuitId} purpose="${purpose}"`);
}
/**
* @typedef {object} module:bulb/commands~ControlCommand~AttachStreamOptions
* @property {string} circuitId - Circuit ID to attach stream to.
* @property {string} [hopNumber] - Hop number in circuit to attach stream to.
*/
/**
* @see https://spec.torproject.org/control-spec/commands.html#attachstream
* @param {string} streamId - Stream ID to attach to circuit.
* @param {module:bulb/commands~ControlCommand~AttachStreamOptions} options
* @returns {module:bulb/commands~ControlCommand}
*/
static ATTACHSTREAM(streamId, options) {
return new ControlCommand(`ATTACHSTREAM ${streamId} ${options.circuitId}` +
(options.hopNumber ? ` HOP=${options.hopNumber}` : ''));
}
/**
* @typedef {object} module:bulb/commands~ControlCommand~PostDescriptorOptions
* @property {string} [options.purpose="general"] - Set descriptor purpose.
* @property {boolean} [options.cache=true] - Cache the descriptor.
*/
/**
* @see https://spec.torproject.org/control-spec/commands.html#postdescriptor
* @param {Object.<string, string>} descriptor - Key value pairs for descriptor to post.
* @param {module:bulb/commands~ControlCommand~PostDescriptorOptions} options
* @returns {module:bulb/commands~ControlCommand}
*/
static POSTDESCRIPTOR(descriptor, options = {}) {
options = {
purpose: options.purpose || 'general',
cache: typeof options.cache === 'undefined'
? true
: options.cache
};
let descStrings = [];
for (let key in descriptor) {
descStrings.push(`${key}=${descriptor[key]}`);
}
return new ControlCommand([
`+POSTDESCRIPTOR purpose=${options.purpose} ` +
`cache=${options.cache ? 'yes' : 'no'}`,
descStrings.join('\r\n'),
'.'
].join('\r\n'));
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#redirectstream
* @param {string} streamId - Stream ID to redirect.
* @param {string} address - Hostname to redirect to.
* @param {number} [port] - Port to redirect to.
* @returns {module:bulb/commands~ControlCommand}
*/
static REDIRECTSTREAM(streamId, address, port = '') {
return new ControlCommand(`REDIRECTSTREAM ${streamId} ${address} ${port}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#closestream
* @param {string} streamId - Stream ID to close.
* @param {number} [reason=1] - Reason code for closing.
* @returns {module:bulb/commands~ControlCommand}
*/
static CLOSESTREAM(streamId, reason = 1) {
return new ControlCommand(`CLOSESTREAM ${streamId} ${reason}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#closecircuit
* @param {string} circuitId - Circuit ID to close.
* @param {object} [options]
* @param {boolean} [options.ifUnused=false] - Only close if circuit is unused.
* @returns {module:bulb/commands~ControlCommand}
*/
static CLOSECIRCUIT(circuitId, options = { ifUnused: false }) {
return new ControlCommand(`CLOSECIRCUIT ${circuitId}` +
(options.ifUnused ? ' IfUnused' : ''));
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#quit
* @returns {module:bulb/commands~ControlCommand}
*/
static QUIT() {
return new ControlCommand('QUIT');
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#resolve
* @param {string} address - Address to resolve.
* @param {boolean} [reverse=false] - Do reverse lookup.
* @returns {module:bulb/commands~ControlCommand}
*/
static RESOLVE(address, reverse) {
return new ControlCommand('RESOLVE ' + (reverse ? 'mode=reverse ' : '') + address);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#loadconf
* @param {string} configText - Sends the torrc configuration to the controller.
* @returns {module:bulb/commands~ControlCommand}
*/
static LOADCONF(configText) {
return new ControlCommand(`+LOADCONF\r\n${configText}\r\n.`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#takeownership
* @returns {module:bulb/commands~ControlCommand}
*/
static TAKEOWNERSHIP() {
return new ControlCommand('TAKEOWNERSHIP');
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#dropguards
* @returns {module:bulb/commands~ControlCommand}
*/
static DROPGUARDS() {
return new ControlCommand('DROPGUARDS');
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#hsfetch
* @param {string} serviceId - Hidden service onion URL without ".onion".
* @param {string} [serverLongName] - Server name to fetch from.
* @returns {string}
*/
static HSFETCH(serviceId, serverLongName) {
return new ControlCommand(`HSFETCH ${serviceId}` +
(serverLongName ? ` SERVER=${serverLongName}` : ''));
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#hspost
* @param {string} descriptor - Raw hidden service descriptor string.
* @param {string} [serverLongName] - Server name to post to.
* @returns {module:bulb/commands~ControlCommand}
*/
static HSPOST(descriptor, serverLongName) {
return new ControlCommand('+HSPOST\r\n' +
(serverLongName ? `SERVER=${serverLongName}\r\n` : '') +
`${descriptor}\r\n.`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#getinfo
* @param {string} keyword - Keyword for info to get.
* @returns {module:bulb/commands~ControlCommand}
*/
static GETINFO(keyword) {
return new ControlCommand(`GETINFO ${keyword}`);
}
/**
* @see https://spec.torproject.org/control-spec/commands.html#setevents
* @param {string[]} events - Names of events that tor should send.
* @returns {module:bulb/commands~ControlCommand}
*/
static SETEVENTS(events) {
return new ControlCommand(`SETEVENTS ${events.join(' ')}`);
}
}
module.exports.ControlCommand = ControlCommand;