Skip to content

Commit

Permalink
feat(*): add support for HTTP(S) inbound and outbound proxies (#516)
Browse files Browse the repository at this point in the history
closes #515
  • Loading branch information
derevnjuk authored Mar 4, 2024
1 parent 9c62d83 commit 7c816b8
Show file tree
Hide file tree
Showing 21 changed files with 624 additions and 116 deletions.
308 changes: 264 additions & 44 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"ci-info": "^3.8.0",
"fast-content-type-parse": "^1.1.0",
"find-up": "^5.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.4",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"ms": "^2.1.3",
Expand All @@ -47,7 +49,7 @@
"socket.io-client": "^4.7.1",
"socket.io-msgpack-parser": "^3.0.2",
"socks": "~2.6.1",
"socks-proxy-agent": "~5.0.0",
"socks-proxy-agent": "~8.0.2",
"tslib": "^2.3.1",
"tsyringe": "~4.6.0",
"win-ca": "^3.5.0",
Expand Down
13 changes: 4 additions & 9 deletions src/Archive/Parsers/NexMock/DefaultHarRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { HarRecorder } from './HarRecorder';
import { Helpers } from '../../../Utils';
import { CaptureHar } from '@neuralegion/capture-har';
import request, { Options, OptionsWithUrl } from 'request';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { inject, injectable } from 'tsyringe';
import { Stream } from 'stream';

Expand All @@ -15,27 +14,23 @@ export interface HarRecorderOptions {

export const HarRecorderOptions: unique symbol = Symbol('HarRecorderOptions');

// TODO: nexmock is not supported anymore.
// We need to remove this class and all its dependencies
@injectable()
export class DefaultHarRecorder implements HarRecorder {
private readonly proxy: CaptureHar;
private readonly pool: number;

constructor(
@inject(HarRecorderOptions)
{
pool = 250,
timeout = 5000,
proxyUrl,
maxRedirects = 20
}: HarRecorderOptions
{ pool = 250, timeout = 5000, maxRedirects = 20 }: HarRecorderOptions
) {
this.pool = pool;
this.proxy = new CaptureHar(
request.defaults({
timeout,
maxRedirects,
rejectUnauthorized: false,
agent: proxyUrl ? new SocksProxyAgent(proxyUrl) : undefined
rejectUnauthorized: false
})
);
}
Expand Down
11 changes: 9 additions & 2 deletions src/Archive/RestArchives.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Archives, Spec, SpecType } from './Archives';
import { ProxyFactory } from '../Utils';
import request, { RequestPromiseAPI } from 'request-promise';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { inject, injectable } from 'tsyringe';
import { ok } from 'assert';

Expand All @@ -24,6 +24,7 @@ export class RestArchives implements Archives {
];

constructor(
@inject(ProxyFactory) private readonly proxyFactory: ProxyFactory,
@inject(RestArchivesOptions)
{
baseUrl,
Expand All @@ -38,7 +39,13 @@ export class RestArchives implements Archives {
timeout,
json: true,
rejectUnauthorized: !insecure,
agent: proxyUrl ? new SocksProxyAgent(proxyUrl) : undefined,
agent: proxyUrl
? this.proxyFactory.createProxyForClient({
proxyUrl,
targetUrl: baseUrl,
rejectUnauthorized: !insecure
})
: undefined,
headers: { authorization: `Api-Key ${apiKey}` }
});
}
Expand Down
8 changes: 4 additions & 4 deletions src/Bus/Brokers/RabbitMQBus.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { ExecutionResult, Handler, HandlerType } from '../Handler';
import { Event, EventType } from '../Event';
import { Bus } from '../Bus';
import { Proxy } from '../Proxy';
import { Backoff, logger } from '../../Utils';
import { Backoff, logger, ProxyFactory } from '../../Utils';
import { Message } from '../Message';
import { ConfirmChannel, connect, Connection, ConsumeMessage } from 'amqplib';
import { DependencyContainer, inject, injectable } from 'tsyringe';
Expand Down Expand Up @@ -53,6 +52,7 @@ export class RabbitMQBus implements Bus {
private _onReconnectionFailure?: (err: Error) => unknown;

constructor(
@inject(ProxyFactory) private readonly proxyFactory: ProxyFactory,
@inject(RabbitMQBusOptions) private readonly options: RabbitMQBusOptions,
@inject('tsyringe') private readonly container: DependencyContainer
) {
Expand Down Expand Up @@ -302,8 +302,8 @@ export class RabbitMQBus implements Bus {
}

private async connect(): Promise<void> {
const proxy: Proxy | undefined = this.options.proxyUrl
? new Proxy(this.options.proxyUrl)
const proxy = this.options.proxyUrl
? this.proxyFactory.createAmqpProxy(this.options.proxyUrl)
: undefined;

// eslint-disable-next-line @typescript-eslint/no-var-requires
Expand Down
1 change: 0 additions & 1 deletion src/Bus/Proxy/index.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/Commands/RunRepeater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,8 @@ export class RunRepeater implements CommandModule {
uri: args.repeaterServer as string,
token: args.token as string,
connectTimeout: 10000,
proxyUrl: (args.proxyExternal ?? args.proxy) as string
proxyUrl: (args.proxyExternal ?? args.proxy) as string,
insecure: args.insecure as boolean
}
}
)
Expand Down
9 changes: 6 additions & 3 deletions src/Config/CliBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,18 @@ export class CliBuilder {
})
.option('proxy', {
requiresArg: true,
describe: 'SOCKS4 or SOCKS5 URL to proxy all traffic'
describe:
'Specify a proxy URL to route all traffic through. This should be an HTTP(S), SOCKS4, or SOCKS5 URL. By default, if you specify SOCKS://<URL>, then SOCKS5h is applied.'
})
.option('proxy-external', {
requiresArg: true,
describe: 'SOCKS4 or SOCKS5 URL to proxy external traffic'
describe:
"Specify a proxy URL to route all outbound traffic through. For more information, see the '--proxy' option."
})
.option('proxy-internal', {
requiresArg: true,
describe: 'SOCKS4 or SOCKS5 URL to proxy internal traffic'
describe:
"Specify a proxy URL to route all inbound traffic through. For more information, see the '--proxy' option."
})
.middleware((args: Arguments) => {
({
Expand Down
4 changes: 4 additions & 0 deletions src/Config/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
RuntimeDetector,
DefaultRuntimeDetector
} from '../Repeater';
import { ProxyFactory, DefaultProxyFactory } from '../Utils';
import { container, Lifecycle } from 'tsyringe';

container
Expand Down Expand Up @@ -231,6 +232,9 @@ container
info: deps.resolve(CliInfo),
configReader: deps.resolve(ConfigReader)
})
})
.register<ProxyFactory>(ProxyFactory, {
useClass: DefaultProxyFactory
});

export default container;
26 changes: 14 additions & 12 deletions src/Repeater/DefaultRepeaterServer.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,32 @@
import { logger } from '../Utils';
import { logger, ProxyFactory } from '../Utils';
import {
DeployCommandOptions,
DeploymentRuntime,
RepeaterServer,
RepeaterServerDeployedEvent,
RepeaterServerReconnectionFailedEvent,
RepeaterServerRequestEvent,
RepeaterServerRequestResponse,
RepeaterServerErrorEvent,
RepeaterServerReconnectionAttemptedEvent,
RepeaterServerNetworkTestEvent,
RepeaterServerNetworkTestResult,
RepeaterServerReconnectionAttemptedEvent,
RepeaterServerReconnectionFailedEvent,
RepeaterServerRequestEvent,
RepeaterServerRequestResponse,
RepeaterServerScriptsUpdatedEvent,
DeployCommandOptions,
DeploymentRuntime,
RepeaterUpgradeAvailableEvent
} from './RepeaterServer';
import { inject, injectable } from 'tsyringe';
import io, { Socket } from 'socket.io-client';
import parser from 'socket.io-msgpack-parser';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { captureException, captureMessage } from '@sentry/node';
import { once } from 'events';
import { parse } from 'url';
import Timer = NodeJS.Timer;

export interface DefaultRepeaterServerOptions {
readonly uri: string;
readonly token: string;
readonly connectTimeout?: number;
readonly proxyUrl?: string;
readonly insecure?: boolean;
}

export const DefaultRepeaterServerOptions: unique symbol = Symbol(
Expand All @@ -43,6 +42,7 @@ export class DefaultRepeaterServer implements RepeaterServer {
private timer?: Timer;

constructor(
@inject(ProxyFactory) private readonly proxyFactory: ProxyFactory,
@inject(DefaultRepeaterServerOptions)
private readonly options: DefaultRepeaterServerOptions
) {}
Expand Down Expand Up @@ -81,10 +81,12 @@ export class DefaultRepeaterServer implements RepeaterServer {
// @ts-expect-error Type is wrong.
// Agent is passed directly to "ws" package, which accepts http.Agent
agent: this.options.proxyUrl
? new SocksProxyAgent({
...parse(this.options.proxyUrl)
? this.proxyFactory.createProxyForClient({
proxyUrl: this.options.proxyUrl,
targetUrl: this.options.uri,
rejectUnauthorized: !this.options.insecure
})
: false,
: undefined,
reconnectionAttempts: this.MAX_RECONNECTION_ATTEMPTS,
auth: {
token: this.options.token,
Expand Down
8 changes: 6 additions & 2 deletions src/RequestExecutor/HttpRequestExecutor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { VirtualScript, VirtualScripts, VirtualScriptType } from '../Scripts';
import { Protocol } from './Protocol';
import { Request, RequestOptions } from './Request';
import { RequestExecutorOptions } from './RequestExecutorOptions';
import { ProxyFactory } from '../Utils';
import nock from 'nock';
import {
anyString,
Expand Down Expand Up @@ -35,6 +36,7 @@ const createRequest = (options?: Partial<RequestOptions>) => {

describe('HttpRequestExecutor', () => {
const virtualScriptsMock = mock<VirtualScripts>();
const proxyFactoryMock = mock<ProxyFactory>();
let spiedExecutorOptions!: RequestExecutorOptions;

let executor!: HttpRequestExecutor;
Expand All @@ -45,14 +47,16 @@ describe('HttpRequestExecutor', () => {

executor = new HttpRequestExecutor(
instance(virtualScriptsMock),
instance(proxyFactoryMock),
executorOptions
);
});

afterEach(() =>
reset<VirtualScripts | RequestExecutorOptions>(
reset<VirtualScripts | RequestExecutorOptions | ProxyFactory>(
virtualScriptsMock,
spiedExecutorOptions
spiedExecutorOptions,
proxyFactoryMock
)
);

Expand Down
18 changes: 9 additions & 9 deletions src/RequestExecutor/HttpRequestExecutor.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { RequestExecutor } from './RequestExecutor';
import { Response } from './Response';
import { Request, RequestOptions } from './Request';
import { logger } from '../Utils';
import { logger, ProxyFactory } from '../Utils';
import { VirtualScripts } from '../Scripts';
import { Protocol } from './Protocol';
import { RequestExecutorOptions } from './RequestExecutorOptions';
import { NormalizeZlibDeflateTransformStream } from '../Utils/NormalizeZlibDeflateTransformStream';
import { SocksProxyAgent } from 'socks-proxy-agent';
import { inject, injectable } from 'tsyringe';
import iconv from 'iconv-lite';
import { safeParse } from 'fast-content-type-parse';
Expand All @@ -32,7 +31,8 @@ type ScriptEntrypoint = (
@injectable()
export class HttpRequestExecutor implements RequestExecutor {
private readonly DEFAULT_SCRIPT_ENTRYPOINT = 'handle';
private readonly proxy?: SocksProxyAgent;
private readonly httpProxyAgent?: http.Agent;
private readonly httpsProxyAgent?: https.Agent;
private readonly httpAgent?: http.Agent;
private readonly httpsAgent?: https.Agent;

Expand All @@ -42,13 +42,13 @@ export class HttpRequestExecutor implements RequestExecutor {

constructor(
@inject(VirtualScripts) private readonly virtualScripts: VirtualScripts,
@inject(ProxyFactory) private readonly proxyFactory: ProxyFactory,
@inject(RequestExecutorOptions)
private readonly options: RequestExecutorOptions
) {
if (this.options.proxyUrl) {
this.proxy = new SocksProxyAgent({
...parseUrl(this.options.proxyUrl)
});
({ https: this.httpsProxyAgent, http: this.httpProxyAgent } =
this.proxyFactory.createProxy({ proxyUrl: this.options.proxyUrl }));
}

if (this.options.reuseConnection) {
Expand Down Expand Up @@ -186,9 +186,9 @@ export class HttpRequestExecutor implements RequestExecutor {
}

private getRequestAgent(options: Request) {
return (
this.proxy ?? (options.secureEndpoint ? this.httpsAgent : this.httpAgent)
);
return options.secureEndpoint
? this.httpsProxyAgent ?? this.httpsAgent
: this.httpProxyAgent ?? this.httpAgent;
}

private async truncateResponse(
Expand Down
Loading

0 comments on commit 7c816b8

Please sign in to comment.