Files
trezor-suite/packages/request-manager/e2e/interceptor.test.ts

329 lines
12 KiB
TypeScript

import http from 'http';
import path from 'path';
import WebSocket from 'ws';
import { TorController, createInterceptor } from '../src';
import { torRunner } from './torRunner';
import { TorIdentities } from '../src/torIdentities';
import { InterceptorOptions } from '../src/types';
const hostIp = '127.0.0.1';
const port = 38835;
const controlPort = 35527;
const processId = process.pid;
// 1 minute before timeout, because Tor might be slow to start.
jest.setTimeout(60000);
// Because tmp/control_auth_cookie is shared by other tests, this test should not run in parallel
// using `--runInBand` option with jest.
const torDataDir = path.join(__dirname, 'tmp');
const ipRegex = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/;
const testGetUrlHttp = 'http://check.torproject.org/';
const testGetUrlHttps = 'https://check.torproject.org/';
const testPostUrlHttps = 'https://httpbin.org/post';
describe('Interceptor', () => {
let torProcess: ReturnType<typeof torRunner> | null;
let torController: TorController;
let torIdentities: TorIdentities;
const torSettings = { running: true, host: hostIp, port };
const interceptorOptions: InterceptorOptions = {
getWhitelistedDomains: () => [
'check.torproject.org',
'httpbin.org',
'tbtc1.trezor.io',
'localhost',
'127.0.0.1',
],
handler: () => {},
getTorSettings: () => torSettings,
};
beforeAll(async () => {
// Callback in createInterceptor should return true in order for the request to use Tor.
torIdentities = createInterceptor(interceptorOptions).torIdentities;
// Starting Tor controller to make sure that Tor is running.
torController = new TorController({
host: hostIp,
port,
controlPort,
torDataDir,
});
const torParams = torController.getTorConfiguration(processId);
// Starting Tor process from binary.
torProcess = torRunner({
torParams,
});
// Waiting for Tor to be ready to accept successful connections.
await torController.waitUntilAlive();
});
afterAll(async () => {
if (torProcess) {
await torProcess.kill();
torProcess = null;
}
});
describe('GET method', () => {
it('HTTP - Each identity has different ip address', async () => {
const identityDefault = await fetch(testGetUrlHttp, {
headers: { 'Proxy-Authorization': 'Basic default' },
});
const identityDefault2 = await fetch(testGetUrlHttp, {
headers: { 'Proxy-Authorization': 'Basic user' },
});
const iPIdentitieA = ((await identityDefault.text()) as any).match(ipRegex)[0];
const iPIdentitieB = ((await identityDefault2.text()) as any).match(ipRegex)[0];
expect(iPIdentitieA).not.toEqual(iPIdentitieB);
});
it('HTTPS - Each identity has different ip address', async () => {
const identityA = await fetch(testGetUrlHttps, {
headers: { 'Proxy-Authorization': 'Basic default' },
});
const identityB = await fetch(testGetUrlHttps, {
headers: { 'Proxy-Authorization': 'Basic user' },
});
// Parsing IP address from html provided by check.torproject.org.
const iPIdentitieA = ((await identityA.text()) as any).match(ipRegex)[0];
const iPIdentitieB = ((await identityB.text()) as any).match(ipRegex)[0];
// Check if identities are different when using different identity.
expect(iPIdentitieA).not.toEqual(iPIdentitieB);
// reset existing circuit using identity with password
const identityB2 = await fetch(testGetUrlHttps, {
headers: { 'Proxy-Authorization': 'Basic user:password' },
});
const iPIdentitieB2 = ((await identityB2.text()) as any).match(ipRegex)[0];
// ip for "user" did change
expect(iPIdentitieB2).not.toEqual(iPIdentitieB);
});
});
describe('POST method', () => {
it('HTTPS - Each identity has different ip address', async () => {
const identityA = await fetch(testPostUrlHttps, {
method: 'POST',
body: JSON.stringify({ test: 'test' }),
headers: { 'Proxy-Authorization': 'Basic default' },
});
const identityB = await fetch(testPostUrlHttps, {
method: 'POST',
body: JSON.stringify({ test: 'test' }),
headers: { 'Proxy-Authorization': 'Basic user' },
});
const iPIdentitieA = ((await identityA.json()) as any).origin;
const iPIdentitieB = ((await identityB.json()) as any).origin;
// Check if identities are different when using different identity.
expect(iPIdentitieA).not.toEqual(iPIdentitieB);
});
});
describe('WebSocket', () => {
const createWebSocket = (options: any = {}) =>
new Promise<void>((resolve, reject) => {
const ws = new WebSocket('wss://tbtc1.trezor.io/websocket', {
headers: {
'User-Agent': 'Trezor Suite',
...options,
},
});
ws.on('open', () => {
ws.close();
resolve();
});
ws.on('error', reject);
});
it('WebSocket - Each connection creates new identity', async () => {
await createWebSocket();
await createWebSocket();
const identities = Object.keys((torIdentities as any).identities).filter(name =>
name.includes('WebSocket'),
);
expect(identities.length).toBe(2);
});
it('WebSocket - Using Proxy-Authorization header', async () => {
await createWebSocket({
'Proxy-Authorization': 'Basic WebSocket-Identity',
});
await createWebSocket({
'Proxy-Authorization': 'Basic WebSocket-Identity',
});
const identities = Object.keys((torIdentities as any).identities).filter(name =>
name.includes('WebSocket-Identity'),
);
expect(identities.length).toBe(1);
});
});
// TODO: Skipping this for now, since I want to get the most critical tests to run in CI.
describe.skip('TorControl', () => {
it('closing circuits', async () => {
await fetch(testGetUrlHttps, {
headers: { 'Proxy-Authorization': 'Basic user-circuit-1' },
});
await fetch(testGetUrlHttps, {
headers: { 'Proxy-Authorization': 'Basic user-circuit-2' },
});
const circuits1 = await torController.controlPort.getCircuits();
// there should be at least 2 circuits
expect(circuits1.length).toBeGreaterThanOrEqual(2);
// there should be circuits with requested username
expect(circuits1.map(c => c.username)).toEqual(
expect.arrayContaining(['user-circuit-1', 'user-circuit-2']),
);
// close specific circuit
await torController.controlPort.closeCircuit('user-circuit-1');
// and validate state afterward
const circuits2 = await torController.controlPort.getCircuits();
expect(circuits2.map(c => c.username)).not.toEqual(
expect.arrayContaining(['user-circuit-1']),
);
expect(circuits2.map(c => c.username)).toEqual(
expect.arrayContaining(['user-circuit-2']),
);
// close remaining circuits
await torController.controlPort.closeActiveCircuits();
// and validate state afterward
const circuits3 = await torController.controlPort.getCircuits();
expect(circuits3.length).toEqual(0);
});
});
describe('Allowed-Headers', () => {
// create simple http server and respond with received headers
const createHttpServer = () =>
new Promise<{ server: http.Server; serverUrl: string; host: string }>(
(resolve, reject) => {
const server = http.createServer((request, response) => {
response.setHeader('Content-Type', 'application/json');
response.write(JSON.stringify(request.headers));
response.end();
});
server.unref();
server.on('error', reject);
server.listen(0, () => {
const addr = server.address() as any; // as net.AddressInfo
resolve({
server,
serverUrl: `http://localhost:${addr.port}`,
host: `localhost:${addr.port}`,
});
});
},
);
let serverInit: Awaited<ReturnType<typeof createHttpServer>>;
beforeAll(async () => {
serverInit = await createHttpServer();
});
afterAll(() => new Promise(resolve => serverInit.server.close(resolve)));
const fetchHeaders = (url: string, options: RequestInit) =>
fetch(url, options).then(r => r.json());
it('POST request headers', async () => {
const { serverUrl, host } = serverInit;
// default headers
await expect(
fetchHeaders(serverUrl, {
method: 'POST',
body: JSON.stringify({ test: 'test' }),
headers: { 'User-Agent': 'TrezorSuite' },
}),
).resolves.toEqual({
host,
accept: '*/*',
'accept-encoding': 'gzip,deflate',
connection: 'keep-alive',
'content-length': '15',
'content-type': 'text/plain;charset=UTF-8',
'user-agent': 'TrezorSuite',
});
// restricted headers
await expect(
fetchHeaders(serverUrl, {
method: 'POST',
body: JSON.stringify({ test: 'test' }),
headers: {
'User-Agent': 'TrezorSuite',
'Allowed-Headers': 'AcCePt-EnCoDiNg;content-type;Content-Length;HOST', // case insensitive
},
}),
).resolves.toEqual({
host,
'accept-encoding': 'gzip,deflate',
'content-length': '15',
'content-type': 'text/plain;charset=UTF-8',
});
});
it('GET request headers', async () => {
const { serverUrl, host } = serverInit;
// default headers
await expect(
fetchHeaders(serverUrl, {
method: 'GET',
headers: { 'User-Agent': 'TrezorSuite' },
}),
).resolves.toEqual({
host,
accept: '*/*',
'accept-encoding': 'gzip,deflate',
connection: 'keep-alive',
'user-agent': 'TrezorSuite',
});
// restricted headers
await expect(
fetchHeaders(serverUrl, {
method: 'GET',
headers: {
'User-Agent': 'TrezorSuite',
'Allowed-Headers': 'Accept-Encoding;Content-Type;Content-Length;Host',
},
}),
).resolves.toEqual({
host,
'accept-encoding': 'gzip,deflate',
});
});
});
it('Block unauthorized requests', async () => {
torSettings.running = false;
await expect(
fetch(testPostUrlHttps, {
method: 'POST',
body: JSON.stringify({ test: 'test' }),
headers: { 'Proxy-Authorization': 'Basic default' },
}),
).rejects.toThrow('Blocked request with Proxy-Authorization');
});
});