tor-ctrl
Version:
Node.js library for accessing the Tor control port
369 lines (368 loc) • 10.6 kB
JavaScript
// src/index.ts
import { promises } from "fs";
import { connect } from "net";
var TorControl = class {
#connection = null;
#config;
#state = "disconnected";
/**
* Get the current connection state of the TorControl
*/
get state() {
return this.#state;
}
constructor(config = {}) {
this.#config = {
host: "localhost",
port: 9051,
password: void 0,
socketPath: void 0,
cookiePath: void 0,
...config
};
}
/**
* Establish a connection to the TorControl
*/
async connect() {
return new Promise((resolve, reject) => {
const conn = this.#config.socketPath ? connect(this.#config.socketPath) : connect({ port: this.#config.port, host: this.#config.host });
this.#connection = conn;
const onError = (err) => {
const error = err instanceof Error ? err : new Error(`TorControl connection error: ${err}`);
this.#state = "disconnected";
if (!conn.destroyed) {
conn.destroy();
}
this.#connection = null;
reject(error);
};
conn.on("connect", async () => {
try {
if (this.#config.password) {
await this.authenticate(this.#config.password);
} else if (this.#config.cookiePath) {
await this.authenticateCookieFile(this.#config.cookiePath);
}
this.#state = "connected";
conn.removeListener("error", onError);
resolve();
} catch (error) {
onError(error);
}
});
conn.on("error", onError);
conn.on("close", () => {
this.#state = "disconnected";
this.#connection = null;
});
conn.once("data", async (data) => {
const str = data.toString();
if (str.substring(0, 3) !== "250") {
return onError(`TorControl connection error: ${data}`);
}
});
});
}
/**
* Close the connection to the TorControl
*/
async disconnect() {
if (this.#connection) {
try {
await this.quit();
} finally {
this.#connection.end();
this.#connection = null;
this.#state = "disconnected";
}
}
}
/**
* Dispose the connection to the TorControl
*
* This method is called when the `using` statement is used on an instance of the class.
*
* Example:
* ```typescript
* using tor = new TorControl();
* await tor.connect();
* // ...
* ```
*/
[Symbol.dispose]() {
this.disconnect().catch(() => {
});
}
/**
* Send a command to the TorControl
*
* Example:
* ```typescript
* const data = await torControl.sendCommand(['GETINFO', 'version']);
* console.log('GETINFO:', data); // { code: 250, message: 'version=...' }
* ```
*
* @link https://spec.torproject.org/control-spec/commands.html
* @param command
*/
async sendCommand(command) {
if (!this.#connection) {
throw new Error("TorControl not connected");
}
const conn = this.#connection;
if (Array.isArray(command)) {
command = command.join(" ");
}
return new Promise((resolve, reject) => {
const onData = (data) => {
const str = data.toString();
const lines = str.split(/\r?\n/).filter(Boolean);
const result = lines.map((line) => {
const message = line.substring(4);
const code = Number(line.substring(0, 3));
if (code >= 400) {
conn.removeListener("error", onError);
reject(new Error(message));
}
return { code, message };
});
conn.removeListener("error", onError);
resolve(result);
};
const onError = (err) => {
reject(err);
};
conn.once("data", onData);
conn.once("error", onError);
conn.write(`${command}\r
`);
});
}
// ===============================
// Config
// ===============================
/**
* Authenticate
*
* **NOTE:** Authentication is automatically done when the connection is established.
*
* @link https://spec.torproject.org/control-spec/commands.html?highlight=AUTHENTICATE#authenticate
* @param password
*/
async authenticate(password) {
const result = await this.sendCommand(`AUTHENTICATE "${password}"`);
return result[0];
}
/**
* Authenticate using the cookie file
*
* This method reads the specified cookie file, converts its contents to a hexadecimal string,
* and then sends an AUTHENTICATE command to the Tor control port using the cookie.
*
* @link https://spec.torproject.org/control-spec/commands.html?highlight=AUTHENTICATE#authenticate
* @param {string} cookiePath - The path to the cookie file used for authentication.
*/
async authenticateCookieFile(cookiePath) {
const cookieBuffer = await promises.readFile(cookiePath);
const cookie = cookieBuffer.toString("hex").trim();
return this.authenticate(cookie);
}
/**
* Quit
*
* @link https://spec.torproject.org/control-spec/commands.html?highlight=QUIT#quit
*/
async quit() {
const result = await this.sendCommand("QUIT");
return result[0];
}
/**
* Example:
*
* ```typescript
* const socksPort = await torControl.getConfig('SocksPort');
* if (socksPort) {
* console.log('SocksPort:', socksPort); // SocksPort: 9050
* }
* ```
*
* @link https://spec.torproject.org/control-spec/commands.html?highlight=GETCONF#getconf
* @param key
*/
async getConfig(key) {
const result = await this.sendCommand(["GETCONF", key]);
return result[0].message.split("=")[1];
}
/**
* @link https://spec.torproject.org/control-spec/commands.html?highlight=SETCONF#setconf
* @param key
* @param value
*/
async setConfig(key, value) {
const result = await this.sendCommand(["SETCONF", `${key}=${value}`]);
return result[0];
}
/**
* @link https://spec.torproject.org/control-spec/commands.html?highlight=RESETCONF#resetconf
* @param key
*/
async resetConfig(key) {
const result = await this.sendCommand(["RESETCONF", key]);
return result[0];
}
// ===============================
// Signals
// ===============================
async signal(signal) {
return this.sendCommand(["SIGNAL", signal]);
}
async signalReload() {
const result = await this.signal("RELOAD");
return result[0];
}
async signalShutdown() {
const result = await this.signal("SHUTDOWN");
return result[0];
}
async signalDump() {
const result = await this.signal("DUMP");
return result[0];
}
async signalDebug() {
const result = await this.signal("DEBUG");
return result[0];
}
async signalHalt() {
const result = await this.signal("HALT");
return result[0];
}
async signalTerm() {
const result = await this.signal("TERM");
return result[0];
}
async signalNewNym() {
const result = await this.signal("NEWNYM");
return result[0];
}
async signalClearDnsCache() {
const result = await this.signal("CLEARDNSCACHE");
return result[0];
}
async signalUsr1() {
const result = await this.signal("USR1");
return result[0];
}
async signalUsr2() {
const result = await this.signal("USR2");
return result[0];
}
// ===============================
// Misc
// ===============================
/**
* Example:
*
* ```typescript
* const version = await torControl.getInfo('version');
* if (version) {
* console.log('Version:', version); // Version: Tor
* }
* ```
*
* @link https://spec.torproject.org/control-spec/commands.html?highlight=GETINFO#getinfo
* @param key
*/
async getInfo(key) {
const result = await this.sendCommand(["GETINFO", key]);
return result[0];
}
/**
* Example:
*
* ```typescript
* const mar = await torControl.mapAddress('1.2.3.4', 'torproject.org');
* console.log('MAPADDRESS:', mar); // { code: 250, message: '1.2.3.4=torproject.org' }
*
* const gir = await torControl.getInfo('address-mappings/control');
* console.log('GETINFO:', gir); // { code: 250, message: 'address-mappings/control=1.2.3.4 torproject.org NEVER' }
* ```
*
* @link https://spec.torproject.org/control-spec/commands.html?highlight=MAPADDRESS#mapaddress
* @param address
* @param target
*/
async mapAddress(address, target) {
const result = await this.sendCommand(["MAPADDRESS", `${address}=${target}`]);
return result[0];
}
// ===============================
// Circuit
// ===============================
async extendCircuit(circuitId) {
const result = await this.sendCommand(["EXTENDCIRCUIT", circuitId]);
return result[0];
}
/**
* @link https://spec.torproject.org/control-spec/commands.html?highlight=SETCIRCUITPURPOSE#setcircuitpurpose
* @param circuitId
* @param purpose
*/
async setCircuitPurpose(circuitId, purpose) {
const result = await this.sendCommand(["SETCIRCUITPURPOSE", circuitId, purpose]);
return result[0];
}
/**
* @link https://spec.torproject.org/control-spec/commands.html?highlight=SETROUTERPURPOSE#setrouterpurpose
* @param nicknameOrKey
* @param purpose
*/
async setRouterPurpose(nicknameOrKey, purpose) {
const result = await this.sendCommand(["SETROUTERPURPOSE", nicknameOrKey, purpose]);
return result[0];
}
/**
* @link https://spec.torproject.org/control-spec/commands.html?highlight=CLOSESTREAM#closestream
* @param streamId
* @param reason
*/
async setStream(streamId, reason) {
const result = await this.sendCommand(["CLOSESTREAM", streamId, reason]);
return result[0];
}
/**
* @link https://spec.torproject.org/control-spec/commands.html?highlight=CLOSECIRCUIT#closecircuit
* @param circuitId
*/
async closeCircuit(circuitId) {
const result = await this.sendCommand(["CLOSECIRCUIT", circuitId]);
return result[0];
}
/**
* @link https://spec.torproject.org/control-spec/commands.html?highlight=ATTACHSTREAM#attachstream
* @param streamId
* @param circuitId
* @param hop
*/
async attachStream(streamId, circuitId, hop) {
const result = await this.sendCommand([
"ATTACHSTREAM",
streamId,
circuitId,
hop ? String(hop) : ""
]);
return result[0];
}
// ===============================
// Alias
// ===============================
/**
* Alias for `signalNewNym`
*/
async getNewIdentity() {
return this.signalNewNym();
}
};
export {
TorControl
};