Skip to content

Commit

Permalink
feat(request-manager): support external Tor
Browse files Browse the repository at this point in the history
  • Loading branch information
karliatto committed Dec 3, 2024
1 parent d37d36d commit 09ed8cf
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 56 deletions.
97 changes: 41 additions & 56 deletions packages/request-manager/src/controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { EventEmitter } from 'events';
import path from 'path';

import { createTimeoutPromise } from '@trezor/utils';
import { checkFileExists } from '@trezor/node-utils';

import { TorControlPort } from './torControlPort';
Expand All @@ -12,16 +11,17 @@ import {
TOR_CONTROLLER_STATUS,
} from './types';
import { bootstrapParser, BOOTSTRAP_EVENT_PROGRESS } from './events/bootstrap';
import { waitUntil } from './utils';

const WAITING_TIME = 1000;
const MAX_TRIES_WAITING = 200;
const BOOTSTRAP_SLOW_TRESHOLD = 1000 * 5; // 5 seconds.

export class TorController extends EventEmitter {
options: TorConnectionOptions;
controlPort: TorControlPort;
bootstrapSlownessChecker?: NodeJS.Timeout;
status: TorControllerStatus = TOR_CONTROLLER_STATUS.Stopped;
// Configurations
waitingTime = 1000;
maxTriesWaiting = 200;
bootstrapSlowThreshold = 1000 * 5; // 5 seconds.

constructor(options: TorConnectionOptions) {
super();
Expand Down Expand Up @@ -57,14 +57,32 @@ export class TorController extends EventEmitter {
if (this.bootstrapSlownessChecker) {
clearTimeout(this.bootstrapSlownessChecker);
}
// When Bootstrap starts we wait time defined in bootstrapSlowThreshold and if after that time,
// When Bootstrap starts we wait time defined in BOOTSTRAP_SLOW_TRESHOLD and if after that time,
// it has not being finalized, then we send slow event. We know that Bootstrap is going on since
// we received, at least, first Bootstrap events from ControlPort.
this.bootstrapSlownessChecker = setTimeout(() => {
this.emit('bootstrap/event', {
type: 'slow',
});
}, this.bootstrapSlowThreshold);
}, BOOTSTRAP_SLOW_TRESHOLD);
}

private onMessageReceived(message: string) {
const bootstrap: BootstrapEvent[] = bootstrapParser(message);
bootstrap.forEach(event => {
if (event.type !== 'progress') return;
if (event.progress && !this.getIsBootstrapping()) {
// We consider that bootstrap has started when we receive any bootstrap event and
// Tor is not bootstrapping yet.
// If we do not receive any bootstrapping event, we can consider there is something going wrong and
// an error will be thrown when `MAX_TRIES_WAITING` is reached in `waitUntilAlive`.
this.startBootstrap();
}
if (event.progress === BOOTSTRAP_EVENT_PROGRESS.Done) {
this.successfullyBootstrapped();
}
this.emit('bootstrap/event', event);
});
}

public async getTorConfiguration(
Expand Down Expand Up @@ -173,59 +191,26 @@ export class TorController extends EventEmitter {
return config;
}

public onMessageReceived(message: string) {
const bootstrap: BootstrapEvent[] = bootstrapParser(message);
bootstrap.forEach(event => {
if (event.type !== 'progress') return;
if (event.progress && !this.getIsBootstrapping()) {
// We consider that bootstrap has started when we receive any bootstrap event and
// Tor is not bootstrapping yet.
// If we do not receive any bootstrapping event, we can consider there is something going wrong and
// an error will be thrown when `maxTriesWaiting` is reached in `waitUntilAlive`.
this.startBootstrap();
}
if (event.progress === BOOTSTRAP_EVENT_PROGRESS.Done) {
this.successfullyBootstrapped();
}
this.emit('bootstrap/event', event);
});
}

public waitUntilAlive(): Promise<void> {
const errorMessages: string[] = [];
public async waitUntilAlive(): Promise<void> {
this.status = TOR_CONTROLLER_STATUS.Bootstrapping;
const waitUntilResponse = async (triesCount: number): Promise<void> => {
if (this.getIsStopped()) {
// If TOR is starting and we want to cancel it.
return;
}
if (triesCount >= this.maxTriesWaiting) {
throw new Error(
`Timeout waiting for TOR control port: \n${errorMessages.join('\n')}`,
);
}
try {
await waitUntil(
MAX_TRIES_WAITING,
WAITING_TIME,
async () => {
const isConnected = await this.controlPort.connect();
const isAlive = this.controlPort.ping();
if (isConnected && isAlive && this.getIsCircuitEstablished()) {
// It is running so let's not wait anymore.
return;
const isCircuitEstablished = this.getIsCircuitEstablished();
// It is running so let's not wait anymore.
if (isConnected && isAlive && isCircuitEstablished) {
return true;
} else {
return false;
}
} catch (error) {
// Some error here is expected when waiting but
// we do not want to throw until maxTriesWaiting is reach.
// Instead we want to log it to know what causes the error.
if (error && error.message) {
console.warn('request-manager:', error.message);
errorMessages.push(error.message);
}
}
await createTimeoutPromise(this.waitingTime);

return waitUntilResponse(triesCount + 1);
};

return waitUntilResponse(1);
},
() => {
return this.getIsStopped();
},
);
}

public getStatus(): Promise<TorControllerStatus> {
Expand Down
86 changes: 86 additions & 0 deletions packages/request-manager/src/controllerExternal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { EventEmitter } from 'events';
import { waitUntil } from './utils';

import { checkSocks5Proxy } from '@trezor/node-utils';

import { TOR_CONTROLLER_STATUS, TorControllerStatus, TorExternalConnectionOptions } from './types';

const WAITING_TIME = 1_000;
const MAX_TRIES_WAITING = 200;

export class TorControllerExternal extends EventEmitter {
status: TorControllerStatus = TOR_CONTROLLER_STATUS.Stopped;
options: TorExternalConnectionOptions;

constructor(options: TorExternalConnectionOptions) {
super();
this.options = options;
}

private getIsStopped() {
return this.status === TOR_CONTROLLER_STATUS.Stopped;
}

private async getIsExternalTorRunning() {
let isSocks5ProxyPort = false;
try {
isSocks5ProxyPort = await checkSocks5Proxy(this.options.host, this.options.port);
} catch {
// Ignore errors.
}

return isSocks5ProxyPort;
}

private startBootstrap() {
this.status = TOR_CONTROLLER_STATUS.Bootstrapping;
}

private successfullyBootstrapped() {
this.status = TOR_CONTROLLER_STATUS.ExternalTorRunning;
}

public getTorConfiguration() {
return '';
}

public async waitUntilAlive() {
this.startBootstrap();
await waitUntil(
MAX_TRIES_WAITING,
WAITING_TIME,
async () => {
const isRunning = await this.getIsExternalTorRunning();
if (isRunning) {
// this.status = TOR_CONTROLLER_STATUS.ExternalTorRunning;
this.successfullyBootstrapped();
}

return isRunning;
},
() => {
return this.getIsStopped();
},
);
}

public async getStatus() {
const isExternalTorRunning = await this.getIsExternalTorRunning();

return new Promise(resolve => {
if (isExternalTorRunning) {
return resolve(TOR_CONTROLLER_STATUS.ExternalTorRunning);
}

return resolve(TOR_CONTROLLER_STATUS.Stopped);
});
}

public closeActiveCircuits() {
// Do nothing. Not possible in External Tor without ControlPort.
}

public stop() {
this.status = TOR_CONTROLLER_STATUS.Stopped;
}
}
1 change: 1 addition & 0 deletions packages/request-manager/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { TorController } from './controller';
export { TorControllerExternal } from './controllerExternal';
export { createInterceptor } from './interceptor';
export type { InterceptedEvent, BootstrapEvent, TorControllerStatus } from './types';
export { TOR_CONTROLLER_STATUS } from './types';
Expand Down
6 changes: 6 additions & 0 deletions packages/request-manager/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export interface TorConnectionOptions {
snowflakeBinaryPath: string;
}

export interface TorExternalConnectionOptions {
host: string;
port: number;
}

export type TorCommandResponse =
| {
success: true;
Expand Down Expand Up @@ -72,5 +77,6 @@ export const TOR_CONTROLLER_STATUS = {
Bootstrapping: 'Bootstrapping',
Stopped: 'Stopped',
CircuitEstablished: 'CircuitEstablished',
ExternalTorRunning: 'ExternalTorRunning',
} as const;
export type TorControllerStatus = keyof typeof TOR_CONTROLLER_STATUS;
39 changes: 39 additions & 0 deletions packages/request-manager/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { createTimeoutPromise } from '@trezor/utils';

export const waitUntil = (
MAX_TRIES_WAITING: number,
WAITING_TIME: number,
checkToSuccess: () => Promise<boolean>,
getIsStopped: () => boolean,
): Promise<void> => {
const errorMessages: string[] = [];

const waitUntilResponse = async (triesCount: number): Promise<void> => {
if (getIsStopped()) {
// If stopped we do not wait anymore.
return;
}
if (triesCount >= MAX_TRIES_WAITING) {
throw new Error(`Timeout waiting: \n${errorMessages.join('\n')}`);
}
try {
const completed = await checkToSuccess();
if (completed) {
return;
}
} catch (error) {
// Some error here is expected when waiting but
// we do not want to throw until MAX_TRIES_WAITING is reach.
// Instead we want to log it to know what causes the error.
if (error && error.message) {
console.warn('error:', error.message);
errorMessages.push(error.message);
}
}
await createTimeoutPromise(WAITING_TIME);

return waitUntilResponse(triesCount + 1);
};

return waitUntilResponse(1);
};

0 comments on commit 09ed8cf

Please sign in to comment.