initial commit

This commit is contained in:
Nicolas Dextraze 2021-01-10 12:43:30 -05:00
commit a5230b9105
18 changed files with 2097 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.idea

21
package-lock.json generated Normal file
View File

@ -0,0 +1,21 @@
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"curve25519-js": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz",
"integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w=="
},
"futoin-hkdf": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.3.2.tgz",
"integrity": "sha512-3EVi3ETTyJg5PSXlxLCaUVVn0pSbDf62L3Gwxne7Uq+d8adOSNWQAad4gg7WToHkcgnCJb3Wlb1P8r4Evj4GPw=="
},
"tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
}
}
}

7
package.json Normal file
View File

@ -0,0 +1,7 @@
{
"dependencies": {
"curve25519-js": "0.0.4",
"futoin-hkdf": "^1.3.2",
"tweetnacl": "^1.0.3"
}
}

148
src/index.js Normal file
View File

@ -0,0 +1,148 @@
const fs = require("fs");
const path = require("path");
const url = require("url");
const http = require("http");
const https = require("https");
const Consensus = require("./tor/Consensus");
const OR = require("./tor/OnionRouter");
const Socket = require("./tor/Socket");
const { get } = require('./utils/http');
const LogLevels = {
off: 0,
error: 1,
warn: 2,
info: 3,
debug: 4
}
let logLevel = LogLevels.error;
function timestamp() {
const d = new Date().toISOString();
return [d.substr(5, 5), d.substr(11, 8)].join(' ')
}
const logger = {
debug(...args) {
if (LogLevels.debug > logLevel) return;
console.debug(timestamp(), "D", ...args);
},
info(...args) {
if (LogLevels.info > logLevel) return;
console.log(timestamp(), "I", ...args);
},
warn(...args) {
if (LogLevels.warn > logLevel) return;
console.warn(timestamp(), "W", ...args);
},
error(...args) {
if (LogLevels.error > logLevel) return;
console.error(timestamp(), "E", ...args);
},
};
(async function main(args) {
let nb_hops = 3;
let arg_url = '';
for (const arg of args) {
if (arg === '-v') {
logLevel = LogLevels.warn;
} else if (arg === '-vv') {
logLevel = LogLevels.info;
} else if (arg === '-vvv') {
logLevel = LogLevels.debug;
} else if (arg.length === 1 && arg[0] >= '0' && arg[0] <= '9') {
nb_hops = parseInt(arg, 10);
} else {
arg_url = arg;
}
}
if (!arg_url) {
console.log('Wrong usage\nUsage: %s [-v|-vv|-vvv] [nb_hops] url');
return;
}
const uri = url.parse(arg_url);
if (!uri.port) {
uri.port = (uri.protocol === 'https:' ? 443 : 80).toString();
}
/** @type Socket */
let socket;
/** @type Circuit */
let circuit;
try {
const app_cache_path = path.join(process.env.HOME, '.cache', 'mini-tor-js');
const cached_consensus_path = path.join(app_cache_path, 'cached-consensus')
fs.mkdirSync(app_cache_path, { recursive: true });
// Load consensus
const consensus = new Consensus(logger);
await consensus.fetch(cached_consensus_path);
consensus.set_allowed_dir_ports(80, 443);
// Connect relays path
const hop_count = () => circuit ? circuit.node_list.length : 0;
const forbidden_routers = [];
while(hop_count() < nb_hops) {
if (hop_count() === 0) { // Entry
// Connect to first OR
const entry_or = consensus.get_random_onion_router_by_criteria({
or_ports: [80, 443],
flags: OR.valid | OR.running | OR.fast,
forbidden_routers
});
socket = new Socket(logger);
await socket.connect(entry_or);
if (!socket.is_connected()) {
logger.error("Could not connect to", entry_or.name);
return;
}
circuit = await socket.create_circuit();
if (!circuit || hop_count() < 1) {
logger.error("Could not create circuit to", entry_or.name);
await socket.close();
} else {
forbidden_routers.push(entry_or.identity_fingerprint.toString('hex'));
logger.info("Connected to", entry_or.name);
}
} else if (hop_count() === nb_hops - 1) { // Exit
const exit_or = consensus.get_random_onion_router_by_criteria({
flags: OR.valid | OR.running | OR.fast | OR.exit,
forbidden_routers
});
const before = hop_count();
await circuit.extend(exit_or, 'ntor');
if (hop_count() === before) {
logger.error("Could not create circuit to", exit_or.name);
} else {
logger.info("Extended to exit", exit_or.name);
}
} else { // Middle(s)
const relay_or = consensus.get_random_onion_router_by_criteria({
flags: OR.valid | OR.running | OR.fast,
forbidden_routers
});
const before = hop_count();
await circuit.extend(relay_or, 'ntor');
if (hop_count() === before) {
logger.error("Could not extend circuit to", relay_or.name);
} else {
forbidden_routers.push(relay_or.identity_fingerprint.toString('hex'));
logger.info("Extended to", relay_or.name);
}
}
}
/** @type TorStream */
const tor_stream = await circuit.create_stream(uri.hostname, uri.port);
if (!tor_stream) {
logger.error("Could not create stream to", uri.href);
return;
}
const stream = tor_stream.get_stream();
const result = await get(uri.protocol, uri.hostname, uri.port, uri.path, stream);
console.log(result);
} catch (err) {
logger.error(err);
} finally {
circuit && await circuit.destroy();
socket && await socket.close();
}
})(process.argv.slice(2));

172
src/tor/Cell.js Normal file
View File

@ -0,0 +1,172 @@
/**
* @property {number} circuit_id
* @property {number} command
* @property {Buffer} payload
* @class
*/
class Cell {
constructor (circuit_id, command, payload) {
this.circuit_id = circuit_id;
this.command = command;
this.payload = payload;
}
is_recognized() {
return this.payload[1] === 0 && this.payload[2] === 0;
}
/**
* @param {number} protocol_version
* @returns {Buffer}
*/
get_bytes(protocol_version) {
let cell_bytes;
if (is_variable_length_cell_command(this.command))
{
cell_bytes = Buffer.alloc(
//
// circuit id.
//
(protocol_version < 4 ? 2 : 4) +
//
// Cell command.
//
1 +
//
// payload size (16 bits).
//
2 +
//
// payload.
//
this.payload.length);
}
else
{
cell_bytes = Buffer.alloc(Cell.size);
}
//
// tor-spec.txt
// 5.1.1.
//
// In link protocol 3 or lower, CircIDs are 2 bytes long;
// in protocol 4 or higher, CircIDs are 4 bytes long.
//
let n = 0;
if (protocol_version < 4)
{
n = cell_bytes.writeUInt16BE(this.circuit_id, n);
//cell_buffer.write<circuit_id_v3_type>(static_cast<circuit_id_v3_type>(_circuit_id));
}
else
{
n = cell_bytes.writeInt32BE(this.circuit_id, n);
}
n = cell_bytes.writeUInt8(this.command, n);
if (is_variable_length_cell_command(this.command))
{
n = cell_bytes.writeUInt16BE(this.payload.length, n);
}
this.payload.copy(cell_bytes, n);
return cell_bytes;
}
}
Cell.size = 514;
Cell.payload_size = 509;
function is_variable_length_cell_command(command) {
return (command === Cell.commands.versions) || (command >= Cell.commands.vpadding);
}
Cell.is_variable_length_cell_command = is_variable_length_cell_command;
Cell.commands = Object.freeze({
//
// Cell commands.
//
padding: 0,
create: 1,
created: 2,
relay: 3,
destroy: 4,
create_fast: 5,
created_fast: 6,
versions: 7,
netinfo: 8,
relay_early: 9,
create2: 10,
created2: 11,
//
// relay commands.
//
relay_begin: 1,
relay_data: 2,
relay_end: 3,
relay_connected: 4,
relay_sendme: 5,
relay_extend: 6,
relay_extended: 7,
relay_truncate: 8,
relay_truncated: 9,
relay_drop: 10,
relay_resolve: 11,
relay_resolved: 12,
relay_begin_dir: 13,
relay_extend2: 14,
relay_extended2: 15,
//
// rendezvous commands.
//
relay_command_establish_intro: 32,
relay_command_establish_rendezvous: 33,
relay_command_introduce1: 34,
relay_command_introduce2: 35,
relay_command_rendezvous1: 36,
relay_command_rendezvous2: 37,
relay_command_intro_established: 38,
relay_command_rendezvous_established: 39,
relay_command_introduce_ack: 40,
//
// variable-length Cell commands.
//
vpadding: 128,
certs: 129,
auth_challenge: 130,
authenticate: 131,
authorize: 132,
});
Cell.commands_lookup = Object.keys(Cell.commands).filter(x => x === 'relay_early' || !x.startsWith('relay_'))
.reduce((a, c) => ({...a, [Cell.commands[c]]: c}), {});
Cell.relay_commands_lookup = Object.keys(Cell.commands).filter(x => x.startsWith('relay_'))
.reduce((a, c) => ({...a, [Cell.commands[c]]: c}), {});
Cell.destroy_reasons = Object.freeze({
none : 0, // no reason given
protocol : 1, // tor protocol violation
internal : 2, // internal error
requested : 3, // a client sent a TRUNCATE command
hibernating : 4, // not currently operating; trying to save bandwidth
resource_limit : 5, // out of memory, sockets, or circuit IDs
connection_failed : 6, // unable to reach relay
onion_router_identity : 7, // connected to relay, but its OR identity was not as expected
onion_router_connection_closed : 8, // the OR connection that was carrying this circuit died
finished : 9, // the circuit has expired for being dirty or old
timeout : 10, // circuit construction took too long
destroyed : 11, // the circuit was destroyed w/o client TRUNCATE
no_such_service : 12, // request for unknown hidden service
});
Cell.destroy_reasons_lookup = Object.keys(Cell.destroy_reasons)
.reduce((a,c) => ({...a, [Cell.destroy_reasons[c]]: c}), {});
module.exports = Cell;

620
src/tor/Circuit.js Normal file
View File

@ -0,0 +1,620 @@
const CircuitNode = require('./CircuitNode');
const Cell = require('./Cell')
const RelayCell = require('./RelayCell')
const Stream = require('./TorStream')
const { parseIp } = require('../utils/net')
const { defer } = require('../utils/time')
const States = Object.freeze({
none:0,
creating:1,
extending:2,
connecting:3,
ready:4,
destroyed:5,
rendezvous_establishing:6,
rendezvous_established:7,
rendezvous_introducing:8,
rendezvous_introduced:9,
rendezvous_completing:10,
rendezvous_completed:11,
});
class Circuit {
static _next_circuit_id = 1;
static _next_stream_id = 1;
/** @type CircuitNode[] */
_node_list = [];
/** @type Socket */
_socket = null;
_circuit_id;
_waits = {};
/** @type CircuitNode */
_extend_node = null;
_stream_map = {};
constructor (socket, logger) {
this._socket = socket;
this._circuit_id = (Circuit._next_circuit_id++) | 0x80000000;
this._logger = logger;
}
get circuit_id() {
return this._circuit_id;
}
get node_list() {
return this._node_list;
}
get state() { return this._state };
set state(state) {
this._state = state;
const waiters = this._waits[state];
if (waiters) {
let d;
while(d = waiters.pop()) {
clearTimeout(d.timeout);
d.resolve(true);
}
}
}
close_streams() {
//
// destroy each stream in this circuit.
//
for(const stream_id in this._stream_map)
{
//
// this call removes the stream from our stream map.
//
this.send_relay_end_cell(this._stream_map[stream_id]);
}
this._node_list = [];
//
// signal destroy.
//
this.state = States.destroyed;
}
destroy() {
if (this.state === States.destroyed)
{
return;
}
this._logger.debug("Circuit.destroy()");
if (this._extend_node)
{
this._extend_node = null;
}
this.close_streams();
this._socket.remove_circuit(this);
}
handle_cell(cell) {
switch(cell.command){
case Cell.commands.created2:
this._on_created2(cell);
break;
case Cell.commands.destroy:
this._on_destroy(cell);
break;
case Cell.commands.relay:
const relay_cell = this._decrypt(cell);
if (!relay_cell.node) {
this._logger.warn(`could not decrypt cell for circuit ${this._circuit_id}. destroying`);
this.destroy();
return;
}
this._logger.debug(
"Circuit.handle_cell() [circuit: %i%s, stream: %i, command: %s, relay_command: %s, payload_size: %i]",
relay_cell.circuit_id & 0x7FFFFFFF,
(relay_cell.circuit_id & 0x80000000 ? " (MSB set)" : ""),
relay_cell.stream_id,
Cell.commands_lookup[relay_cell.command],
Cell.relay_commands_lookup[relay_cell.relay_command],
relay_cell.relay_payload.length);
switch(relay_cell.relay_command) {
case Cell.commands.relay_extended2:
this._on_relay_extended2(relay_cell);
break;
case Cell.commands.relay_truncated:
this._on_relay_truncated_cell(relay_cell);
break;
case Cell.commands.relay_end:
this._on_relay_end_cell(relay_cell);
break;
case Cell.commands.relay_connected:
this._on_relay_connected_cell(relay_cell);
break;
case Cell.commands.relay_extended:
this._on_relay_extended_cell(relay_cell);
break;
case Cell.commands.relay_data:
this._on_relay_data_cell(relay_cell);
break;
case Cell.commands.relay_sendme:
this._on_relay_sendme_cell(relay_cell);
break;
case Cell.commands.relay_command_rendezvous2:
this._on_relay_extended_cell(relay_cell);
this.state = States.rendezvous_completed;
break;
case Cell.commands.relay_command_rendezvous_established:
this.state = States.rendezvous_established;
break;
case Cell.commands.relay_command_introduce_ack:
this.state = States.rendezvous_introduced;
break;
default:
this._logger.warn(
"Circuit._handle_cell() !! unhandled relay cell [ relay_command: %s ]",
Cell.relay_commands_lookup[relay_cell.relay_command]);
break;
}
break;
default:
this._logger.warn(`unhandled cell for circuit ${this._circuit_id}`, cell);
}
}
_on_relay_data_cell(cell) {
this._logger.debug('Circuit._on_relay_data_cell():', cell.relay_payload);
//
// decrement deliver window on circuit node.
//
cell.node.decrement_deliver_window();
if (cell.node.consider_sending_sendme()) {
this.send_relay_sendme_cell(null);
}
const stream = this.get_stream_by_id(cell.stream_id);
if (stream) {
stream.append_to_recv_buffer(cell.relay_payload);
//
// decrement window on stream.
//
stream.decrement_deliver_window();
if (stream.consider_sending_sendme()) {
this.send_relay_sendme_cell(stream);
}
}
}
_on_relay_sendme_cell(cell) {
if (cell.stream_id === 0) {
cell.node.increment_package_window();
} else {
const stream = this.get_stream_by_id(cell.stream_id);
if (stream) {
stream.increment_package_window();
}
}
}
_on_relay_connected_cell(cell) {
const stream = this.get_stream_by_id(cell.stream_id)
if (stream) {
stream.state = Stream.states.ready;
}
this.state = States.ready;
}
_on_relay_truncated_cell(cell) {
//
// tor-spec.txt
// 5.4.
//
// To tear down part of a circuit, the OP may send a RELAY_TRUNCATE cell
// signaling a given OR (Stream ID zero). That OR sends a DESTROY
// cell to the next node in the circuit, and replies to the OP with a
// RELAY_TRUNCATED cell.
//
// [Note: If an OR receives a TRUNCATE cell and it has any RELAY cells
// still queued on the circuit for the next node it will drop them
// without sending them. This is not considered conformant behavior,
// but it probably won't get fixed until a later version of Tor. Thus,
// clients SHOULD NOT send a TRUNCATE cell to a node running any current
// version of Tor if a) they have sent relay cells through that node,
// and b) they aren't sure whether those cells have been sent on yet.]
//
// When an unrecoverable error occurs along one connection in a
// circuit, the nodes on either side of the connection should, if they
// are able, act as follows: the node closer to the OP should send a
// RELAY_TRUNCATED cell towards the OP; the node farther from the OP
// should send a DESTROY cell down the circuit.
//
this._logger.error("circuit::handle_relay_truncated_cell() destroying circuit");
this.destroy();
}
_on_relay_end_cell(cell) {
const stream = this.get_stream_by_id(cell.stream_id);
if (stream) {
this._logger.debug("circuit::handle_relay_end_cell() [stream: %i, reason: %s]", cell.stream_id, Cell.destroy_reasons_lookup[cell.relay_payload[0]]);
stream.state = Stream.states.destroyed;
delete this._stream_map[cell.stream_id];
}
}
_on_destroy(cell) {
const reason = cell.payload.readUInt8(0);
this._logger.info('Circuit._on_destroy:', Cell.destroy_reasons_lookup[reason]);
this.destroy();
}
_on_relay_extended_cell(cell) {
//TODO
}
_on_relay_extended2(cell) {
//
// The payload of an EXTENDED2 cell is the same as the payload of a
// CREATED2 cell
//
//
// finish the handshake.
//
const handshake_data = cell.relay_payload.slice(2);
this._extend_node.compute_shared_secret(handshake_data);
if (this._extend_node.has_valid_crypto_state())
{
this._node_list.push(this._extend_node);
//
// we're ready here.
//
this._extend_node = null;
this.state = States.ready;
}
else
{
this._logger.error(`circuit::handle_extended2_cell() extend node [ ${this._extend_node.onion_router.name} ] has invalid crypto state`);
this.destroy();
}
}
_on_created2(cell) {
//
// A CREATED2 cell contains:
// HLEN (Server Handshake Data Len) [2 bytes]
// HDATA (Server Handshake Data) [HLEN bytes]
//
//
// finish the handshake.
//
const handshake_data = cell.payload.slice(2);
this._extend_node.compute_shared_secret(handshake_data);
if (this._extend_node.has_valid_crypto_state())
{
this._node_list.push(this._extend_node);
//
// we're ready here.
//
this._extend_node = null;
this.state = States.ready;
}
else
{
this._logger.error(`circuit._handle_cell created2 extend node [ ${this._extend_node.onion_router.name} ] has invalid crypto state`);
this.destroy();
}
}
create(first_or, handshake_type) {
switch (handshake_type) {
case 'ntor':
return this._create_ntor(first_or);
default:
throw new Error("Not implemented");
}
}
async _create_ntor(first_or) {
await first_or.fetch_descriptor();
if (!first_or.ntor_onion_key)
{
this._logger.warn(`circuit._create_ntor() [or: ${first_or.name} does not support NTOR handshake]`);
return;
}
this._logger.debug(`circuit._create_ntor() [or: ${first_or.name}, state: creating]`);
this.state = States.creating;
this._extend_node = new CircuitNode(this, first_or, 'normal');
const handshake_bytes = Buffer.alloc(2 + 2 + 84);
handshake_bytes.writeUInt16BE(2, 0); // ntor type
handshake_bytes.writeUInt16BE(84, 2); // ntor onion skin length
const onion_skin_ntor = this._extend_node.create_onion_skin_ntor();
onion_skin_ntor.copy(handshake_bytes, 4);
this._socket.send_cell(new Cell(
this._circuit_id,
Cell.commands.create2,
handshake_bytes
));
if (await this.wait_for_state(States.ready))
{
this._logger.debug(`circuit._create_ntor() [or: ${first_or.name}, state: created]`);
}
else
{
this._logger.error(`circuit._create_ntor() [or: ${first_or.name}, state: destroyed]`);
}
}
async create_stream(host, port) {
//
// tor-spec.txt
// 6.2.
//
// ADDRPORT [nul-terminated string]
// FLAGS[4 bytes]
//
// ADDRPORT is made of ADDRESS | ':' | PORT | [00]
//
const host_port = `${host}:${port}`
const relay_data_bytes = Buffer.alloc(host_port.length + 1);
relay_data_bytes.write(host_port, 0, 'utf8');
//
// send RELAY_BEGIN cell.
//
const stream_id = Circuit._next_stream_id++;
const stream = new Stream(stream_id, this, this._logger);
this._stream_map[stream_id] = stream;
this._logger.debug("circuit::create_stream() [url: %s, stream: %i, status: creating]", host_port, stream_id);
this.state = States.connecting;
this.send_relay_cell(stream_id, Cell.commands.relay_begin, relay_data_bytes);
if (await this.wait_for_state(States.ready))
{
this._logger.debug("circuit::create_stream() [url: %s, stream: %i, status: created]", host_port, stream_id);
return stream;
}
else
{
this._logger.error("circuit::create_stream() [is_ready() == false]");
}
}
extend(or, handshake_type) {
switch (handshake_type) {
case 'ntor':
return this._extend_ntor(or);
default:
throw new Error("Not implemented");
}
}
async _extend_ntor(next_onion_router) {
//
// An EXTEND2 cell's relay payload contains:
// NSPEC (Number of link specifiers) [1 byte]
// NSPEC times:
// LSTYPE (Link specifier type) [1 byte]
// LSLEN (Link specifier length) [1 byte]
// LSPEC (Link specifier) [LSLEN bytes]
// HTYPE (Client Handshake Type) [2 bytes]
// HLEN (Client Handshake Data Len) [2 bytes]
// HDATA (Client Handshake Data) [HLEN bytes]
//
// Link specifiers describe the next node in the circuit and how to
// connect to it. Recognized specifiers are:
// [00] TLS-over-TCP, IPv4 address
// A four-byte IPv4 address plus two-byte ORPort
// [01] TLS-over-TCP, IPv6 address
// A sixteen-byte IPv6 address plus two-byte ORPort
// [02] Legacy identity
// A 20-byte SHA1 identity fingerprint. At most one may be listed.
//
await next_onion_router.fetch_descriptor();
if (!next_onion_router.ntor_onion_key)
{
this._logger.warn(`circuit._extend_ntor() [or: ${next_onion_router.name} does not support NTOR handshake`);
return;
}
const ipv4 = 0;
const ipv6 = 1;
const legacy_id = 2;
this._logger.debug(`circuit::extend_ntor() [or: ${next_onion_router.name}, state: extending]`);
this.state = States.extending;
this._extend_node = new CircuitNode(this, next_onion_router, 'normal');
const onion_skin = this._extend_node.create_onion_skin_ntor();
const relay_payload_bytes = Buffer.alloc(
1 + // NSPEC
1 + 1 + 6 + // NSPEC IPv4 (4 bytes) + port (2 bytes)
1 + 1 + 20 + // NSPEC identity_fingerprint (20 bytes)
2 + // HTYPE
2 + // HLEN
84); // HDATA
let n = relay_payload_bytes.writeUInt8(2, 0); // 2x NSPEC
n = relay_payload_bytes.writeUInt8(ipv4, n);
n = relay_payload_bytes.writeUInt8(6, n);
const ip = parseIp(next_onion_router.ip);
n = relay_payload_bytes.writeInt32BE(ip, n);
n = relay_payload_bytes.writeUInt16BE(next_onion_router.or_port, n);
n = relay_payload_bytes.writeUInt8(legacy_id, n);
n = relay_payload_bytes.writeUInt8(20, n);
n += next_onion_router.identity_fingerprint.copy(relay_payload_bytes, n);
n = relay_payload_bytes.writeUInt16BE(2, n);
n = relay_payload_bytes.writeUInt16BE(84, n);
onion_skin.copy(relay_payload_bytes, n);
this.send_relay_cell(
0,
Cell.commands.relay_extend2,
relay_payload_bytes,
//
// clients MUST only send
// EXTEND cells inside RELAY_EARLY cells
//
Cell.commands.relay_early,
this._extend_node);
if (await this.wait_for_state(States.ready))
{
this._logger.debug(`circuit::extend_ntor() [or: ${next_onion_router.name}, state: extended]`);
}
else
{
this._logger.error(`circuit::extend_ntor() [or: ${next_onion_router.name}, state: destroyed]`);
}
}
/**
* @returns {CircuitNode}
*/
get_final_circuit_node() {
return this._node_list[this._node_list.length - 1];
}
get_stream_by_id(stream_id) {
return this._stream_map[stream_id];
}
send_relay_sendme_cell() {
//TODO
}
send_relay_end_cell(stream) {
this.send_relay_cell(
stream.stream_id,
Cell.commands.relay_end,
Buffer.from([6])); // reason
//
// signal destroy.
//
stream.state = Stream.states.destroyed;
delete this._stream_map[stream.stream_id];
}
send_relay_data_cell(stream, data) {
for (let i = 0; i < data.length; i += Cell.payload_size)
{
const data_size = Math.min(data.length - i, Cell.payload_size);
this.get_final_circuit_node().decrement_package_window();
this.send_relay_cell(
stream.stream_id,
Cell.commands.relay_data,
data.slice(i, i + data_size));
}
}
send_relay_cell(stream_id, relay_command, payload, cell_command = Cell.commands.relay_early, node = null) {
node = node ? node : this.get_final_circuit_node();
if (this.get_stream_by_id(stream_id) == null && stream_id !== 0)
{
this._logger.warn("Circuit.send_relay_cell() attempt to send cell to non-existent stream-id:", stream_id);
return;
}
this._logger.debug(
"Circuit.send_relay_cell() [circuit: %i%s, stream: %i, command: %s, relay_command: %s]",
this._circuit_id & 0x7FFFFFFF,
((this._circuit_id & 0x80000000) ? " (MSB set)" : ""),
stream_id,
Cell.commands_lookup[cell_command],
Cell.relay_commands_lookup[relay_command],
payload);
this._socket.send_cell(this._encrypt(new RelayCell(
this._circuit_id,
cell_command,
node,
relay_command,
stream_id,
payload)));
}
/**
* @param {RelayCell} cell
* @return {Cell}
* @private
*/
_encrypt(cell) {
for (let i = this._node_list.length - 1; i >= 0; i--) {
this._node_list[i].encrypt_forward_cell(cell);
}
return cell;
}
/**
* @param {Cell} cell
* @return {RelayCell}
* @private
*/
_decrypt(cell) {
for (const node of this._node_list) {
if (node.decrypt_backward_cell(cell)) {
return RelayCell.fromCell(node, cell);
}
}
return new RelayCell();
}
wait_for_state(desired_state, timeout = 30000) {
const d = defer();
d.timeout = setTimeout(() => {
this._logger.debug("Circuit.wait_for_state(): timed out");
d.resolve(false);
}, timeout);
(this._waits[desired_state] = this._waits[desired_state] || []).push(d);
return d.promise;
}
}
module.exports = Circuit;

84
src/tor/CircuitNode.js Normal file
View File

@ -0,0 +1,84 @@
const KeyAgreementNtor = require('./KeyAgreementNtor');
const CircuitNodeCryptoState = require('./CircuitNodeCryptoState');
class CircuitNode {
_circuit = null
/** @type OnionRouter */
_onion_router = null;
_type = 'normal';
/** @type KeyAgreementNtor */
_handshake = null;
/** @type CircuitNodeCryptoState */
_crypto_state = null;
get onion_router() {
return this._onion_router;
}
constructor (circuit, or, type) {
this._circuit = circuit;
this._onion_router = or;
this._type = type;
}
create_onion_skin_ntor() {
this._handshake = new KeyAgreementNtor(this._onion_router);
return Buffer.concat([
this._onion_router.identity_fingerprint,
this._onion_router.ntor_onion_key,
this._handshake.public_key
], 20 + 32 + 32);
}
compute_shared_secret(handshake_data) {
const key_material = this._handshake.compute_shared_secret(handshake_data);
if (key_material)
{
//
// struct key_material
// {
// byte_type digest_forward [20];
// byte_type digest_backward[20];
// byte_type cipher_forward [16];
// byte_type cipher_backward[16];
// ^^ sizeof == 40 + 32 == 72 ^^
//
// byte_type rend_nonce [20]; << ignored now
// ^^ sizeof == 72 + 20 == 92 ^^ << (used in establishing of introduction points,
// rend-spec.txt <20> 1.2.)
//
// byte_type __garbage__ []; << ignored
// };
//
this._crypto_state = new CircuitNodeCryptoState(key_material);
}
}
has_valid_crypto_state() {
return this._crypto_state != null;
}
/**
* @param {RelayCell} cell
*/
encrypt_forward_cell(cell) {
return this._crypto_state.encrypt_forward_cell(cell);
}
/**
* @param {Cell} cell
* @returns {boolean}
*/
decrypt_backward_cell(cell) {
return this._crypto_state.decrypt_backward_cell(cell);
}
decrement_deliver_window() {}
consider_sending_sendme() {}
increment_package_window() {}
decrement_package_window() {}
}
module.exports = CircuitNode;

View File

@ -0,0 +1,100 @@
const crypto = require('crypto');
const Cell = require('./Cell')
class CircuitNodeCryptoState {
/** @type Hash */
_forward_digest = null;
/** @type Hash */
_backward_digest = null;
/** @type Cipher */
_backward_cipher = null;
/** @type Cipher */
_forward_cipher = null;
constructor (key_material) {
this._forward_digest = crypto.createHash('sha1');
const df = key_material.slice(0, 20);
this._forward_digest.update(df);
this._backward_digest = crypto.createHash('sha1');
const db = key_material.slice(20, 20 + 20);
this._backward_digest.update(db);
const kf = key_material.slice(20 + 20, 20 + 20 + 16);
this._forward_cipher = crypto.createCipheriv('aes-128-ctr', kf, Buffer.alloc(16));
const kb = key_material.slice(20 + 20 + 16, 20 + 20 + 16 + 16);
this._backward_cipher = crypto.createCipheriv('aes-128-ctr', kb, Buffer.alloc(16));
}
/**
* @param {RelayCell} cell
*/
encrypt_forward_cell(cell) {
const relay_payload_bytes = Buffer.alloc(Cell.payload_size);
if (cell.payload == null)
{
relay_payload_bytes.writeUInt8(cell.relay_command, 0);
relay_payload_bytes.writeUInt16BE(0, 1); // 'recognized'
relay_payload_bytes.writeUInt16BE(cell.stream_id, 3);
relay_payload_bytes.writeInt32BE(0, 5); // digest placeholder
relay_payload_bytes.writeUInt16BE(cell.relay_payload.length, 9);
cell.relay_payload.copy(relay_payload_bytes, 11);
//
// update digest field in the payload
//
this._forward_digest.update(relay_payload_bytes);
const digest = this._forward_digest.copy().digest(); // requires node >= 12.16
digest.copy(relay_payload_bytes, 5, 0, 4);
}
else
{
cell.payload.copy(relay_payload_bytes, 0);
}
//
// encrypt the payload
//
cell.payload = this._forward_cipher.update(relay_payload_bytes);
// console.debug("CircuitNodeCryptoState.encrypt_forward_cell(): %s %s",
// relay_payload_bytes.toString('hex'),
// cell.payload.toString('hex'));
}
/**
* @param {Cell} cell
* @returns {boolean}
*/
decrypt_backward_cell(cell) {
cell.payload = this._backward_cipher.update(cell.payload);
//
// check if this is a cell for us.
//
if (cell.is_recognized())
{
//
// remove the digest from the payload
//
const payload_without_digest = Buffer.alloc(cell.payload.length);
cell.payload.copy(payload_without_digest);
payload_without_digest.writeInt32BE(0, 5);
const backward_digest_clone = this._backward_digest.copy();
backward_digest_clone.update(payload_without_digest);
const digest = backward_digest_clone.digest();
if (digest.compare(cell.payload, 5, 5 + 4, 0, 4) === 0)
{
this._backward_digest.update(payload_without_digest);
return true;
}
}
return false;
}
}
module.exports = CircuitNodeCryptoState;

241
src/tor/Consensus.js Normal file
View File

@ -0,0 +1,241 @@
const fs = require('fs');
const crypto = require('crypto');
const {get} = require('../utils/http');
const OR = require('./OnionRouter');
const OnionRouter = require('./OnionRouter')
function authority_onion_router(name, ip, or_port, dir_port) {
return {
name,
ip,
or_port,
dir_port
}
}
const authorities = [
authority_onion_router("dizum", "194.109.206.212", 443, 80),
authority_onion_router("Serge", "66.111.2.131", 9001, 9030),
authority_onion_router("moria1", "128.31.0.34", 9101, 9131),
//authority_onion_router("tor26", "86.59.21.38", 443, 80},
authority_onion_router("bastet", "204.13.164.118", 443, 80),
authority_onion_router("maatuska", "171.25.193.9", 80, 443),
authority_onion_router("dannenberg", "193.23.244.244", 443, 80),
authority_onion_router("Faravahar", "154.35.175.225", 443, 80),
authority_onion_router("gabelmoo", "131.188.40.189", 443, 80),
authority_onion_router("longclaw", "199.58.81.140", 443, 80),
]
const router_status_flags = [
"Authority",
"BadExit",
"Exit",
"Fast",
"Guard",
"HSDir",
"Named",
"NoEdConsensus",
"Stable",
"Running",
"Unnamed",
"Valid",
"V2Dir"
];
class Consensus {
_allowed_dir_flags = OR.fast | OR.valid | OR.running | OR.v2dir;
_allowed_dir_ports = [];
_max_try_count = 3;
_onion_router_map = {};
_valid_until = 0;
constructor (logger) {
this._logger = logger;
}
async fetch(cached_consensus_path) {
let have_valid_consensus = false;
let force_download = false;
let consensus_content = '';
//
// if no path to the cached consensus file
// was provided, we have to download it.
//
if (!cached_consensus_path || !fs.existsSync(cached_consensus_path))
{
force_download = true;
}
while (!have_valid_consensus)
{
consensus_content = force_download
? await this._download_from_random_router("/tor/status-vote/current/consensus", true)
: fs.readFileSync(cached_consensus_path).toString();
//
// assume newly downloaded consensus as valid.
//
const reject_invalid = !force_download;
this._parse_consensus(consensus_content, reject_invalid);
//
// consider force_download-ed consensus as valid.
//
have_valid_consensus = force_download || (this._valid_until >= Date.now());
//
// if the consensus is invalid, we have to download it anyway.
//
if (!have_valid_consensus)
{
force_download = true;
}
}
this._logger.debug("loaded", Object.keys(this._onion_router_map).length, "routers");
//
// save the consensus content, if the path was provided.
//
if (force_download && cached_consensus_path)
{
fs.writeFileSync(cached_consensus_path, consensus_content);
}
}
set_allowed_dir_ports(...ports) {
this._allowed_dir_ports = [...ports];
}
get_onion_routers_by_criteria(criteria) {
const candidates = [];
for (const key in this._onion_router_map) {
const router = this._onion_router_map[key];
let allowed = true;
if (criteria.dir_ports) {
allowed = allowed && criteria.dir_ports.includes(router.dir_port);
}
if (criteria.or_ports) {
allowed = allowed && criteria.or_ports.includes(router.or_port);
}
if (criteria.flags) {
allowed = allowed && (router.flags & criteria.flags) === criteria.flags;
}
if (criteria.forbidden_routers) {
allowed = allowed && !criteria.forbidden_routers.includes(router.identity_fingerprint.toString('hex'))
}
if (allowed) {
candidates.push(router);
}
}
return candidates;
}
get_random_onion_router_by_criteria(criteria) {
const candidates = this.get_onion_routers_by_criteria(criteria);
if (candidates.length === 0) {
return null;
}
const idx = crypto.randomInt(candidates.length);
return candidates[idx];
}
/**
* @param {Buffer} identity_fingerprint
* @return {Promise<string>}
*/
get_onion_router_descriptor(identity_fingerprint) {
return this._download_from_random_router('/tor/server/fp/' + identity_fingerprint.toString('hex'));
}
/**
* @param {string} path
* @param {boolean} only_authorities
* @return {Promise<string>}
* @private
*/
async _download_from_random_router(path, only_authorities = false) {
let try_count = 0;
let result = '';
do {
result = await this._download_from_random_router_impl(path, only_authorities);
} while(++try_count < this._max_try_count && !result);
return result;
}
/**
* @param {string} path
* @param {boolean} only_authorities
* @return {Promise<string>}
* @private
*/
_download_from_random_router_impl(path, only_authorities) {
let ip;
let port;
//
// if the onion router map is empty,
// we're stuck to authorities anyway.
//
if (only_authorities || Object.keys(this._onion_router_map).length === 0)
{
const random_index = crypto.randomInt(authorities.length);
const authority = authorities[random_index];
ip = authority.ip;
port = authority.dir_port;
}
else
{
const router = this.get_random_onion_router_by_criteria({
dir_ports: this._allowed_dir_ports,
flags: this._allowed_dir_flags
});
ip = router.ip;
port = router.dir_port;
}
this._logger.debug(`consensus::download_from_random_authority() [path: http://${ip}:${port}${path}]`);
return get('http:', ip, port, path);
}
_parse_consensus(content) {
this._onion_router_map = {};
this._valid_until = 0;
const lines = content.split('\n');
let current_router;
for (const line of lines) {
const parts = line.split(' ');
switch (parts[0]) {
case 'valid-until':
this._valid_until = new Date(`${parts[1]}T${parts[2]}Z`).getTime();
break;
case 'r':
const identity_fingerprint = Buffer.from(parts[2], 'base64');
const identity_fingerprint_hex = identity_fingerprint.toString('hex');
const or_port = parseInt(parts[7], 10);
const dir_port = parseInt(parts[8], 10);
current_router = new OnionRouter(this, parts[1], parts[6], or_port, dir_port, identity_fingerprint);
this._onion_router_map[identity_fingerprint_hex] = current_router;
break;
case 's':
let flags = 0;
for (const part of parts) {
const idx = router_status_flags.indexOf(part);
if (idx === -1) continue;
flags |= 1 << idx;
}
current_router.flags = flags;
break;
case 'directory-footer':
return;
}
}
}
}
module.exports = Consensus;

131
src/tor/KeyAgreementNtor.js Normal file
View File

@ -0,0 +1,131 @@
const crypto = require('crypto');
const hkdf = require('futoin-hkdf');
const curve25519 = require('curve25519-js');
const { hmac_sha256 } = require('../utils/crypto')
const const_server = 'Server';
const const_protoid = 'ntor-curve25519-sha256-1';
const const_t_mac = 'ntor-curve25519-sha256-1:mac';
const const_t_verify = 'ntor-curve25519-sha256-1:verify';
const const_m_expand = 'ntor-curve25519-sha256-1:key_expand';
const const_t_key = 'ntor-curve25519-sha256-1:key_extract';
class KeyAgreementNtor {
/** @type Buffer */
_private_key = null;
/** @type Buffer */
_public_key = null;
constructor (or) {
this._onion_router = or;
const kp = curve25519.generateKeyPair(crypto.randomBytes(32));
this._private_key = Buffer.from(kp.private);
this._public_key = Buffer.from(kp.public);
}
get public_key() {
return this._public_key;
}
compute_shared_secret(handshake_data) {
return this._compute_shared_secret(handshake_data.slice(0, 32), handshake_data.slice(32, 32 + 32));
}
_compute_shared_secret(other_public_key, verification_data) {
//
// 5.1.4. The "ntor" handshake
//
// In this section, define:
// H(x,t) as HMAC_SHA256 with message x and key t.
// H_LENGTH = 32.
// ID_LENGTH = 20.
// G_LENGTH = 32
// PROTOID = "ntor-curve25519-sha256-1"
// t_mac = PROTOID | ":mac"
// t_key = PROTOID | ":key_extract"
// t_verify = PROTOID | ":verify"
// MULT(a,b) = the multiplication of the curve25519 point 'a' by the
// scalar 'b'.
// G = The preferred base point for curve25519 ([9])
// KEYGEN() = The curve25519 key generation algorithm, returning
// a private/public keypair.
// m_expand = PROTOID | ":key_expand"
// KEYID(A) = A
//
// To perform the handshake, the client needs to know an identity key
// digest for the server, and an ntor onion key (a curve25519 public
// key) for that server. Call the ntor onion key "B". The client
// generates a temporary keypair:
// x,X = KEYGEN()
// and generates a client-side handshake with contents:
// NODEID Server identity digest [ID_LENGTH bytes]
// KEYID KEYID(B) [H_LENGTH bytes]
// CLIENT_PK X [G_LENGTH bytes]
//
// The server generates a keypair of y,Y = KEYGEN(), and uses its ntor
// private key 'b' to compute:
//
// secret_input = EXP(X,y) | EXP(X,b) | ID | B | X | Y | PROTOID
// KEY_SEED = H(secret_input, t_key)
// verify = H(secret_input, t_verify)
// auth_input = verify | ID | B | Y | X | PROTOID | "Server"
//
// The server's handshake reply is:
// SERVER_PK Y [G_LENGTH bytes]
// AUTH H(auth_input, t_mac) [H_LENGTH bytes]
//
// The client then checks Y is in G^* [see NOTE below], and computes
//
// secret_input = EXP(Y,x) | EXP(B,x) | ID | B | X | Y | PROTOID
// KEY_SEED = H(secret_input, t_key)
// verify = H(secret_input, t_verify)
// auth_input = verify | ID | B | Y | X | PROTOID | "Server"
//
// The client verifies that AUTH == H(auth_input, t_mac).
//
//0d f8 4c b3 97 82 67 a5 b4 56 29 cc 21 c3 6f 0e 9f 83 48 3f 97 d4 45 bf 79 2b 51 7e e5 91 c8 2e
const shared_key1 = Buffer.from(curve25519.sharedKey(this._private_key, other_public_key));
const shared_key2 = Buffer.from(curve25519.sharedKey(this._private_key, this._onion_router.ntor_onion_key));
const secret_input = Buffer.concat([
shared_key1,
shared_key2,
this._onion_router.identity_fingerprint,
this._onion_router.ntor_onion_key,
this._public_key,
other_public_key,
Buffer.from(const_protoid)
], 32 + 32 + 20 + 32 + 32 + 32 + const_protoid.length);
const verify = hmac_sha256(const_t_verify, secret_input);
const auth_input = Buffer.concat([
verify,
this._onion_router.identity_fingerprint,
this._onion_router.ntor_onion_key,
other_public_key,
this._public_key,
Buffer.from(const_protoid),
Buffer.from(const_server)
], 32 + 20 + 32 + 32 + 32 + const_protoid.length + const_server.length);
const computed_verification_data = hmac_sha256(const_t_mac, auth_input);
if (verification_data.equals(computed_verification_data))
{
//
// create key material.
//
return hkdf(secret_input, 92, {
salt: const_t_key,
info: const_m_expand,
hash: 'sha256'
});
}
return null;
}
}
module.exports = KeyAgreementNtor;

64
src/tor/OnionRouter.js Normal file
View File

@ -0,0 +1,64 @@
class OnionRouter {
static none = 0x0000;
static authority = 0x0001;
static bad_exit = 0x0002;
static exit = 0x0004;
static fast = 0x0008;
static guard = 0x0010;
static hsdir = 0x0020;
static named = 0x0040;
static no_ed_consensus = 0x0080;
static stable = 0x0100;
static running = 0x0200;
static unnamed = 0x0400;
static valid = 0x0800;
static v2dir = 0x1000;
/** @type Consensus */
_consensus = null;
_nickname = '';
_ip = '';
_or_port = 0;
_dir_port = 0;
/** @type Buffer */
_identity_fingerprint = null;
_descriptor_fetched = false;
_ntor_onion_key = null;
constructor (consensus, nickname, ip, or_port, dir_port, identity_fingerprint) {
this._consensus = consensus;
this._nickname = nickname;
this._ip = ip;
this._or_port = or_port;
this._dir_port = dir_port;
this._identity_fingerprint = identity_fingerprint;
}
get name() { return this._nickname };
get ip() { return this._ip };
get flags() { return this._flags };
get or_port() { return this._or_port };
get dir_port() { return this._dir_port };
set flags(flags) { this._flags = flags; }
get ntor_onion_key() {
if (!this._descriptor_fetched) throw new Error('forgot to call fetch_descriptor for onion router');
return this._ntor_onion_key;
}
get identity_fingerprint() { return this._identity_fingerprint; }
async fetch_descriptor() {
const descriptor = await this._consensus.get_onion_router_descriptor(this._identity_fingerprint);
// parse
const lines = descriptor.split('\n');
for (const line of lines){
const parts = line.split(' ');
if (parts[0] === 'ntor-onion-key') {
this._ntor_onion_key = Buffer.from(parts[1], 'base64');
break;
}
}
this._descriptor_fetched = true;
}
}
module.exports = OnionRouter;

25
src/tor/RelayCell.js Normal file
View File

@ -0,0 +1,25 @@
const Cell = require('./Cell');
/**
* @extends {Cell}
*/
class RelayCell extends Cell {
constructor (circuit_id, cell_command, node, relay_command, stream_id, payload) {
super(circuit_id, cell_command, null);
this.node = node;
this.relay_command = relay_command;
this.stream_id = stream_id;
this.relay_payload = payload;
}
static fromCell(node, cell) {
const relay_command = cell.payload.readUInt8(0);
const recognized = cell.payload.readUInt16BE(1);
const stream_id = cell.payload.readUInt16BE(3);
const digest = cell.payload.readUInt32BE(5);
const payload_length = cell.payload.readUInt16BE(9);
const payload = cell.payload.slice(11, 11 + payload_length);
return new RelayCell(cell.circuit_id, cell.command, node, relay_command, stream_id, payload);
}
}
module.exports = RelayCell;

280
src/tor/Socket.js Normal file
View File

@ -0,0 +1,280 @@
const net = require('net');
const tls = require('tls');
const Cell = require('./Cell');
const Circuit = require('./Circuit');
const { defer } = require('../utils/time')
const { wait } = require('../utils/time')
const { time } = require('../utils/time')
const { parseIp } = require('../utils/net')
const protocol_version_initial = 3;
const protocol_version_preferred = 4;
const States = {
closed: 0,
connecting: 1,
handshake_in_progress: 2,
ready: 3
}
class Socket {
/** @type TLSSocket */
_socket = null;
_onion_router = null;
_protocol_version = protocol_version_initial;
_circuit_map = {};
_state = States.closed
_waits = {};
// recv
_receive_payload_size = 0;
/** @type Cell */
_cell = null;
constructor (logger) {
this._logger = logger;
}
get state() {
return this._state;
}
set state(state) {
this._state = state;
const waiters = this._waits[state];
if (waiters) {
let d;
while(d = waiters.pop()) {
clearTimeout(d.timeout);
d.resolve(true);
}
}
}
async connect(or) {
if (this.is_connected()) {
this.close();
}
this.state = States.connecting;
this._onion_router = or;
this._logger.debug("Connecting to", or.name, "@", or.ip, or.or_port);
this._socket = new tls.connect({
host: or.ip,
port: or.or_port,
rejectUnauthorized: false
});
await new Promise((resolve, reject) => {
this._socket.once('secureConnect', resolve);
this._socket.once('error', reject);
});
this._socket.on('data', this._recv_cell.bind(this));
if (!this.is_connected()) {
this.state = States.closed;
return;
}
this.state = States.handshake_in_progress;
this._send_versions();
await this.wait_for_state(States.ready);
}
async close() {
this._socket.end();
this._socket.unref();
this.state = States.closed;
}
is_connected() {
return this._socket && !this._socket.connecting;
}
async create_circuit(handshake = 'ntor') {
if (this.state !== States.ready)
{
return null;
}
const new_circuit = new Circuit(this, this._logger);
this._circuit_map[new_circuit.circuit_id] = new_circuit;
await new_circuit.create(this._onion_router, handshake);
//
// should't we close the socket in the case of failure?
//
return new_circuit.node_list.length === 1
? new_circuit
: null;
}
remove_circuit(circuit) {
this._logger.debug("tor_socket::remove_circuit() [circuit: %i]", circuit.circuit_id & 0x7FFFFFFF);
delete this._circuit_map[circuit.circuit_id];
}
async wait_for_state(desired_state, timeout = 30000) {
const d = defer();
d.timeout = setTimeout(() => {
this._logger.debug("Socket.wait_for_state(): timed out");
d.resolve(false);
}, timeout);
(this._waits[desired_state] = this._waits[desired_state] || []).push(d);
return d.promise;
}
_recv_cell(chunk) {
for (let i = 0; i < chunk.length;) {
if (!this._cell) {
let circuit_id;
if (this._protocol_version < 4) {
circuit_id = chunk.readUInt16BE(i);
i += 2;
} else {
circuit_id = chunk.readInt32BE(i);
i += 4;
}
let command = chunk.readUInt8(i);
i++;
let payload_size = Cell.payload_size;
if (Cell.is_variable_length_cell_command(command)) {
payload_size = chunk.readUInt16BE(i);
i += 2;
}
this._cell = new Cell(circuit_id, command, Buffer.alloc(payload_size));
this._receive_payload_left = payload_size;
}
const targetStart = this._cell.payload.length - this._receive_payload_left;
const length = Math.min(chunk.length - i, this._receive_payload_left);
const copied = chunk.copy(this._cell.payload, targetStart, i, i + length);
this._receive_payload_left -= copied;
i += copied;
if (this._receive_payload_left === 0) {
const cell = this._cell;
this._cell = null;
this._handle_cell(cell);
}
}
}
_handle_cell(cell) {
this._logger.debug("Socket._handle_cell(): %i %s", cell.circuit_id & 0x7fffffff, Cell.commands_lookup[cell.command], cell.payload);
if (cell.circuit_id) {
return this._circuit_map[cell.circuit_id].handle_cell(cell);
}
switch(cell.command) {
case Cell.commands.versions:
this._on_recv_versions(cell);
break;
case Cell.commands.certs:
//this._logger.debug("Socket recv certificates", cell);
break;
case Cell.commands.auth_challenge:
//this._logger.debug("Socket recv auth challenge", cell);
break;
case Cell.commands.netinfo:
this._on_recv_net_info(cell);
this._send_net_info();
this.state = States.ready;
break;
default:
this._logger.debug("Cell unhandled");
break;
}
}
send_cell(cell) {
if (!this.is_connected()) return;
this._logger.debug("Socket.send_cell(): %i %s", (cell.circuit_id & 0x7fffffff), Cell.commands_lookup[cell.command], cell.payload);
const bytes = cell.get_bytes(this._protocol_version);
this._socket.write(bytes);
}
_send_versions() {
this._logger.debug("Socket._send_versions()");
this.send_cell(new Cell(0, Cell.commands.versions, Buffer.from([0, 4])));
}
_on_recv_versions(versions_cell) {
this._logger.debug("Socket._recv_versions()");
for (let i = 0; i < versions_cell.payload.length; i += 2)
{
const offered_version = versions_cell.payload.readUInt16BE(i);
if (offered_version === protocol_version_preferred)
{
this._protocol_version = offered_version;
this._logger.debug("switched to protocol v4");
break;
}
}
}
_on_recv_net_info(net_info_cell) {
this._logger.debug("Socket._recv_net_info()");
//const cell = this._recv_cell();
this._localAddress = "127.0.0.1";
if (net_info_cell.payload.readUInt8(4) === 4 && net_info_cell.payload.readUInt8(5) === 4) {
const addr = net_info_cell.payload.readUInt32BE(6);
this._localAddress = [
((addr >> 24) & 0xff),
((addr >> 16) & 0xff),
((addr >> 8) & 0xff),
(addr & 0xff)
].join('.')
this._logger.debug("local addr =", this._localAddress);
}
}
_send_net_info() {
this._logger.debug("Socket._send_net_info()");
const remote = parseIp(this._socket.remoteAddress);
const local = parseIp(this._localAddress);
const epoch = time();
const net_info_bytes = Buffer.alloc(4 + 2 + 4 + 3 + 4);
//
// If version 2 or higher is negotiated, each party sends the other a
// NETINFO Cell. The Cell's payload is:
//
// Timestamp [4 bytes]
// Other OR's address [variable]
// Number of addresses [1 byte]
// This OR's addresses [variable]
//
// Address is:
// Type (1 octet)
// Length (1 octet)
// Value (variable-width)
//
// "Type" is one of:
// 0x00 -- Hostname
// 0x04 -- IPv4 address
// 0x06 -- IPv6 address
// 0xF0 -- Error, transient
// 0xF1 -- Error, nontransient
//
let n = net_info_bytes.writeUInt32BE(epoch, 0);
n = net_info_bytes.writeUInt8(4, n); // type
n = net_info_bytes.writeUInt8(4, n); // length
n = net_info_bytes.writeInt32BE(remote, n);
n = net_info_bytes.writeUInt8(1, n); // number of addresses
n = net_info_bytes.writeUInt8(4, n); // type
n = net_info_bytes.writeUInt8(4, n); // length
net_info_bytes.writeInt32BE(local, n);
this.send_cell(new Cell(
0,
Cell.commands.netinfo,
net_info_bytes));
}
}
module.exports = Socket;

124
src/tor/TorStream.js Normal file
View File

@ -0,0 +1,124 @@
const { Duplex } = require('stream');
const States = Object.freeze({
none: 0,
connecting: 1,
ready: 2,
destroyed: 3,
});
class TorStream extends Duplex {
static states = States;
static window_start = 500;
static window_increment = 50;
static window_max_unflushed = 10;
_stream_id = 0;
/** @type Circuit */
_circuit = null;
_deliver_window = TorStream.window_start;
_package_window = TorStream.window_start;
/** @type Buffer */
_buffer = null;
_state = States.connecting;
_waits = {};
/**
* @param {number} stream_id
* @param {Circuit} circuit
* @param logger
*/
constructor (stream_id, circuit, logger) {
super();
this._stream_id = stream_id;
this._circuit = circuit;
this._logger = logger;
}
get stream_id() { return this._stream_id; }
get state() { return this._state; }
set state(state) {
this._state = state;
if (state === States.destroyed) {
for (const k in this._waits) {
for (const wait of this._waits[k]) {
wait.resolve(false);
}
}
this._waits = {};
}
}
close() {
if (this.state === States.destroyed) {
return;
}
this._circuit.send_relay_end_cell(this);
}
append_to_recv_buffer(data) {
if (this._canRead) {
this._canRead = this.push(data);
} else {
this._buffer = this._buffer
? Buffer.concat([this._buffer, data], this._buffer.length + data.length)
: data;
}
}
decrement_deliver_window() {
this._deliver_window--;
}
consider_sending_sendme() {
if (this._deliver_window > (TorStream.window_start - TorStream.window_increment)) {
this._logger.debug("TorStream.consider_sending_sendme(): false");
return false;
}
//
// we're currently flushing immediatelly upon write,
// therefore there is no need to check unflushed cell count,
// because it's always 0.
//
// if (unflushed_cell_count >= window_max_unflushed)
// {
// return false;
// }
//
this._deliver_window += TorStream.window_increment;
this._logger.debug("TorStream.consider_sending_sendme(): true");
return true;
}
increment_package_window() {
this._deliver_window += TorStream.window_increment;
}
get_stream() {
return this;
}
_write(chunk, encoding, callback) {
let err = null;
try {
this._circuit.send_relay_data_cell(this, chunk);
} catch (error) {
err = error;
} finally {
callback(err);
}
}
_read(size) {
if (this._buffer) {
this._canRead = this.push(this._buffer);
this._buffer = null;
} else {
this._canRead = true;
}
}
}
module.exports = TorStream;

9
src/utils/crypto.js Normal file
View File

@ -0,0 +1,9 @@
const crypto = require('crypto');
/**
* @returns {Buffer}
*/
function hmac_sha256(key, data) {
const h = crypto.createHmac('sha256', key);
return h.update(data).digest();
}
exports.hmac_sha256 = hmac_sha256;

35
src/utils/http.js Normal file
View File

@ -0,0 +1,35 @@
const http = require('http');
const https = require('https');
const tls = require('tls');
/**
* @param {string} protocol
* @param {string} ip
* @param {string} port
* @param {string} path
* @param {object} [stream]
* @return {Promise<string>}
*/
async function get(protocol, ip, port, path, stream) {
let options = {};
if (stream) {
options.createConnection = function(args, cb) {
if (protocol === 'https:') {
return new tls.connect({socket: stream, servername: args.hostname}, () => cb());
}
return stream;
}
}
const proto = protocol === 'https:' ? https : http;
return new Promise((resolve, reject) => {
proto.get(`${protocol}//${ip}:${port}${path}`, options, res => {
let data = '';
res.on('data', chunk => data += chunk.toString());
res.on('end', () => resolve(data));
res.on('error', (err) => reject(err));
});
});
}
exports.get = get;

15
src/utils/net.js Normal file
View File

@ -0,0 +1,15 @@
/**
* @param {string} ip
* @return {number}
*/
function parseIp(ip) {
const parts = ip.split('.').map(x => parseInt(x, 10));
return (
(parts[0] << 24) |
(parts[1] << 16) |
(parts[2] << 8) |
(parts[3])
)
}
exports.parseIp = parseIp;

19
src/utils/time.js Normal file
View File

@ -0,0 +1,19 @@
function time() {
return Math.floor(Date.now()/1000);
}
exports.time = time;
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
exports.wait = wait;
function defer() {
const d = {};
d.promise = new Promise(function(resolve, reject) {
d.reject = reject;
d.resolve = resolve;
});
return d;
}
exports.defer = defer;