Skip to content

Commit

Permalink
proxyless: move the target option from constructor (#2860)
Browse files Browse the repository at this point in the history
* 1

* 2

* 3

* fix servers closing

* fix ssl server creation

* small changes

* fix review issues
  • Loading branch information
miherlosev authored Mar 23, 2023
1 parent 87dd15f commit 3384aca
Show file tree
Hide file tree
Showing 20 changed files with 178 additions and 129 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "testcafe-hammerhead",
"description": "A powerful web-proxy used as a core for the TestCafe testing framework (https://github.com/DevExpress/testcafe).",
"version": "29.0.0",
"version": "30.0.0",
"homepage": "https://github.com/DevExpress/testcafe-hammerhead",
"bugs": {
"url": "https://github.com/DevExpress/testcafe-hammerhead/issues"
Expand Down Expand Up @@ -52,8 +52,8 @@
"@types/parse5": "2.2.34",
"@types/semver": "^6.0.0",
"@types/tough-cookie": "^4.0.1",
"@typescript-eslint/eslint-plugin": "^5.29.0",
"@typescript-eslint/parser": "^5.29.0",
"@typescript-eslint/eslint-plugin": "^5.54.1",
"@typescript-eslint/parser": "^5.54.1",
"babel-eslint": "^10.1.0",
"babel-plugin-add-module-exports": "^1.0.0",
"basic-auth": "1.0.4",
Expand Down
3 changes: 1 addition & 2 deletions src/client/transport/transport-legacy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import TransportBase from './transport-base';
import { ServiceMessage } from '../../typings/proxy';
import MessageSandbox from '../sandbox/event/message';
import Promise from 'pinkie';
import settings from '../settings';
import XhrSandbox from '../sandbox/xhr';
Expand Down Expand Up @@ -127,7 +126,7 @@ export default class TransportLegacy extends TransportBase {
});
}

public start (messageSandbox: MessageSandbox): void {
public start (): void {
// NOTE: There is no special logic here.
}
}
143 changes: 86 additions & 57 deletions src/proxy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
ServiceMessage,
ServerInfo,
ProxyOptions,
RouterOptions,
} from '../typings/proxy';

import http, { ServerOptions } from 'http';
Expand Down Expand Up @@ -65,10 +66,11 @@ const DEFAULT_PROXY_OPTIONS = {

export default class Proxy extends Router {
private readonly openSessions: Map<string, Session> = new Map();
private readonly server1Info: ServerInfo;
private readonly server2Info: ServerInfo;
private readonly server1: http.Server | https.Server;
private readonly server2: http.Server | https.Server;
private server1Info: ServerInfo | null;
private server2Info: ServerInfo | null;
private server1: http.Server | https.Server | null;
private server2: http.Server | https.Server | null;
private proxyOptions: ProxyOptions | null;
private readonly sockets: Set<net.Socket>;

// Max header size for incoming HTTP requests
Expand All @@ -78,43 +80,15 @@ export default class Proxy extends Router {
// https://github.com/nodejs/node/commit/186035243fad247e3955fa0c202987cae99e82db#diff-1d0d420098503156cddb601e523b82e7R59
public static MAX_REQUEST_HEADER_SIZE = 80 * 1024;

constructor (hostname: string, port1: number, port2: number, options?: Partial<ProxyOptions>) {
const prepareOptions = Object.assign({}, DEFAULT_PROXY_OPTIONS, options);
constructor (options: RouterOptions) {
super(options);

super(prepareOptions);

// NOTE: to avoid https://github.com/DevExpress/testcafe/issues/7447
if (typeof dns.setDefaultResultOrder === 'function')
// NOTE: to avoid https://github.com/nodejs/node/issues/40537
dns.setDefaultResultOrder('ipv4first');

const {
ssl,
developmentMode,
cache,
} = prepareOptions;

const protocol = ssl ? 'https:' : 'http:';
const opts = this._getOpts(ssl);
const createServer = this._getCreateServerMethod(ssl);

this.server1Info = createServerInfo(hostname, port1, port2, protocol, cache);
this.server2Info = createServerInfo(hostname, port2, port1, protocol, cache);

this.server1 = createServer(opts, (req: http.IncomingMessage, res: http.ServerResponse) => this._onRequest(req, res, this.server1Info));
this.server2 = createServer(opts, (req: http.IncomingMessage, res: http.ServerResponse) => this._onRequest(req, res, this.server2Info));

this.server1.on('upgrade', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => this._onUpgradeRequest(req, socket, head, this.server1Info));
this.server2.on('upgrade', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => this._onUpgradeRequest(req, socket, head, this.server2Info));

this.server1.listen(port1);
this.server2.listen(port2);

this.sockets = new Set<net.Socket>();

// BUG: GH-89
this._startSocketsCollecting();
this._registerServiceRoutes(developmentMode);
this.server1 = null;
this.server2 = null;
this.server1Info = null;
this.server2Info = null;
this.proxyOptions = null;
this.sockets = new Set<net.Socket>();
}

_getOpts (ssl?: {}): ServerOptions {
Expand Down Expand Up @@ -143,8 +117,8 @@ export default class Proxy extends Router {
socket.on('close', () => this.sockets.delete(socket));
};

this.server1.on('connection', handler);
this.server2.on('connection', handler);
this.server1?.on('connection', handler); // eslint-disable-line no-unused-expressions
this.server2?.on('connection', handler); // eslint-disable-line no-unused-expressions
}

_registerServiceRoutes (developmentMode: boolean): void {
Expand All @@ -168,9 +142,7 @@ export default class Proxy extends Router {
});

this.POST(SERVICE_ROUTES.messaging, (req: http.IncomingMessage, res: http.ServerResponse, serverInfo: ServerInfo) => this._onServiceMessage(req, res, serverInfo));

if (this.options.proxyless)
this.OPTIONS(SERVICE_ROUTES.messaging, (req: http.IncomingMessage, res: http.ServerResponse) => this._onServiceMessagePreflight(req, res));
this.OPTIONS(SERVICE_ROUTES.messaging, (req: http.IncomingMessage, res: http.ServerResponse) => this._onServiceMessagePreflight(req, res));

this.GET(SERVICE_ROUTES.task, (req: http.IncomingMessage, res: http.ServerResponse, serverInfo: ServerInfo) => this._onTaskScriptRequest(req, res, serverInfo, false));
this.GET(SERVICE_ROUTES.iframeTask, (req: http.IncomingMessage, res: http.ServerResponse, serverInfo: ServerInfo) => this._onTaskScriptRequest(req, res, serverInfo, true));
Expand All @@ -189,7 +161,7 @@ export default class Proxy extends Router {

res.setHeader(BUILTIN_HEADERS.setCookie, session.takePendingSyncCookies());

respondWithJSON(res, result, false, this.options.proxyless);
respondWithJSON(res, result, false, this.isProxyless);
}
catch (err) {
logger.serviceMsg.onError(msg, err);
Expand All @@ -205,7 +177,7 @@ export default class Proxy extends Router {
// NOTE: 'Cache-control' header set in the 'Transport' sandbox on the client side.
// Request becomes non-simple (https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests)
// and initiates the CORS preflight request.
res.setHeader('access-control-allow-headers', BUILTIN_HEADERS.cacheControl);
res.setHeader(BUILTIN_HEADERS.accessControlAllowHeaders, BUILTIN_HEADERS.cacheControl);
acceptCrossOrigin(res);
respond204(res);
}
Expand Down Expand Up @@ -241,7 +213,7 @@ export default class Proxy extends Router {
_onRequest (req: http.IncomingMessage, res: http.ServerResponse | net.Socket, serverInfo: ServerInfo): void {
// NOTE: Not a service request, execute the proxy pipeline.
if (!this._route(req, res, serverInfo))
runRequestPipeline(req, res, serverInfo, this.openSessions, this.options.proxyless);
runRequestPipeline(req, res, serverInfo, this.openSessions, this.isProxyless);
}

_onUpgradeRequest (req: http.IncomingMessage, socket: net.Socket, head: Buffer, serverInfo: ServerInfo): void {
Expand All @@ -256,11 +228,52 @@ export default class Proxy extends Router {
handler.content = prepareShadowUIStylesheet(handler.content as string);
}

_prepareDNSRouting (): void {
// NOTE: to avoid https://github.com/DevExpress/testcafe/issues/7447
if (typeof dns.setDefaultResultOrder === 'function')
// NOTE: to avoid https://github.com/nodejs/node/issues/40537
dns.setDefaultResultOrder('ipv4first');
}

// API
start (options: ProxyOptions) {
this.proxyOptions = Object.assign({}, DEFAULT_PROXY_OPTIONS, options);

this._prepareDNSRouting();

const {
hostname,
port1,
port2,
ssl,
developmentMode,
cache,
} = this.proxyOptions;

const protocol = ssl ? 'https:' : 'http:';
const opts = this._getOpts(ssl);
const createServer = this._getCreateServerMethod(ssl);

this.server1Info = createServerInfo(hostname, port1, port2, protocol, !!cache);
this.server2Info = createServerInfo(hostname, port2, port1, protocol, !!cache);

this.server1 = createServer(opts, (req: http.IncomingMessage, res: http.ServerResponse) => this._onRequest(req, res, this.server1Info as ServerInfo));
this.server2 = createServer(opts, (req: http.IncomingMessage, res: http.ServerResponse) => this._onRequest(req, res, this.server2Info as ServerInfo));

this.server1.on('upgrade', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => this._onUpgradeRequest(req, socket, head, this.server1Info as ServerInfo));
this.server2.on('upgrade', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => this._onUpgradeRequest(req, socket, head, this.server2Info as ServerInfo));

this.server1.listen(port1);
this.server2.listen(port2);

// BUG: GH-89
this._startSocketsCollecting();
this._registerServiceRoutes(!!developmentMode);
}
close (): void {
scriptProcessor.jsCache.reset();
this.server1.close();
this.server2.close();
this.server1?.close(); // eslint-disable-line no-unused-expressions
this.server2?.close(); // eslint-disable-line no-unused-expressions
this._closeSockets();
resetKeepAliveConnections();
}
Expand All @@ -273,21 +286,29 @@ export default class Proxy extends Router {
if (externalProxySettings)
session.setExternalProxySettings(externalProxySettings);

if (this.options.disableHttp2)
const {
disableHttp2,
disableCrossDomain,
proxyless,
} = this.proxyOptions as ProxyOptions;

if (disableHttp2)
session.disableHttp2();

if (this.options.disableCrossDomain)
if (disableCrossDomain)
session.disableCrossDomain();

url = urlUtils.prepareUrl(url);

if (this.options.proxyless)
if (proxyless)
return url;

const serverInfo = this.server1Info as ServerInfo;

return urlUtils.getProxyUrl(url, {
proxyHostname: this.server1Info.hostname,
proxyPort: this.server1Info.port.toString(),
proxyProtocol: this.server1Info.protocol,
proxyHostname: serverInfo.hostname,
proxyPort: serverInfo.port.toString(),
proxyProtocol: serverInfo.protocol,
sessionId: session.id,
windowId: session.options.windowId,
});
Expand All @@ -299,7 +320,15 @@ export default class Proxy extends Router {
this.openSessions.delete(session.id);
}

public resolveRelativeServiceUrl (relativeServiceUrl: string, domain = this.server1Info.domain): string {
public resolveRelativeServiceUrl (relativeServiceUrl: string, domain = (this.server1Info as ServerInfo).domain): string {
return new URL(relativeServiceUrl, domain).toString();
}

public switchToProxyless (): void {
(this.proxyOptions as ProxyOptions).proxyless = true;
}

public get isProxyless (): boolean {
return !!(this.proxyOptions as ProxyOptions).proxyless;
}
}
2 changes: 1 addition & 1 deletion src/proxy/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function buildRouteParamsMap (routeMatch, paramNames) {
}

export default abstract class Router {
public readonly options: any;
public readonly options: RouterOptions;
protected readonly routes: Map<string, Route> = new Map();
private readonly routesWithParams: RouteWithParams[] = [];

Expand Down
1 change: 1 addition & 0 deletions src/request-pipeline/builtin-header-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default {

accessControlAllowOrigin: 'access-control-allow-origin',
accessControlAllowCredentials: 'access-control-allow-credentials',
accessControlAllowHeaders: 'access-control-allow-headers',
contentSecurityPolicy: 'content-security-policy',
contentSecurityPolicyReportOnly: 'content-security-policy-report-only',
xContentSecurityPolicy: 'x-content-security-policy',
Expand Down
4 changes: 2 additions & 2 deletions src/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export default abstract class Session extends EventEmitter {
cookie = cookie || '{{{cookie}}}';
iframeTaskScriptTemplate = iframeTaskScriptTemplate || '{{{iframeTaskScriptTemplate}}}';

if (this.proxy?.options.proxyless) {
if (this.proxy?.isProxyless) {
referer = '""';
cookie = '""';
}
Expand All @@ -170,7 +170,7 @@ export default abstract class Session extends EventEmitter {
isRecordMode,

windowId: windowId || '',
proxyless: this.proxy?.options.proxyless || false,
proxyless: this.proxy?.isProxyless || false,

disableCrossDomain: this.isCrossDomainDisabled() || false,
});
Expand Down
17 changes: 10 additions & 7 deletions src/typings/proxy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ interface RequestTimeout {
ajax: number;
}

interface ProxyOptions extends RouterOptions {
ssl: object;
developmentMode: boolean;
cache: boolean;
disableHttp2: boolean;
proxyless: boolean;
disableCrossDomain: boolean;
interface ProxyOptions {
hostname: string;
port1: number;
port2: number;
ssl?: object;
developmentMode?: boolean;
cache?: boolean;
disableHttp2?: boolean;
disableCrossDomain?: boolean;
proxyless?: boolean;
}
4 changes: 2 additions & 2 deletions test/server/auth-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const request = require('request-promise-native');
const headersUtils = require('../../lib/utils/headers');

const {
createProxy,
createAndStartProxy,
createSession,
} = require('./common/utils');

Expand All @@ -16,7 +16,7 @@ describe('Authentication', () => { // eslint-disable-line

beforeEach(() => {
session = createSession();
proxy = createProxy();
proxy = createAndStartProxy();
});

afterEach(() => {
Expand Down
35 changes: 18 additions & 17 deletions test/server/charset-test.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
const fs = require('fs');
const request = require('request-promise-native');
const expect = require('chai').expect;
const express = require('express');
const iconv = require('iconv-lite');
const noop = require('lodash').noop;
const Proxy = require('../../lib/proxy');
const Session = require('../../lib/session');
const Charset = require('../../lib/processing/encoding/charset');
const encodeContent = require('../../lib/processing/encoding').encodeContent;
const decodeContent = require('../../lib/processing/encoding').decodeContent;
const urlUtils = require('../../lib/utils/url');
const processScript = require('../../lib/processing/script').processScript;
const pageProcessor = require('../../lib/processing/resources/page');
const stylesheetProcessor = require('../../lib/processing/resources/stylesheet');
const manifestProcessor = require('../../lib/processing/resources/manifest');
const fs = require('fs');
const request = require('request-promise-native');
const { expect } = require('chai');
const express = require('express');
const iconv = require('iconv-lite');
const { noop } = require('lodash');
const Session = require('../../lib/session');
const Charset = require('../../lib/processing/encoding/charset');
const { encodeContent, decodeContent } = require('../../lib/processing/encoding');
const urlUtils = require('../../lib/utils/url');
const { processScript } = require('../../lib/processing/script');
const pageProcessor = require('../../lib/processing/resources/page');
const stylesheetProcessor = require('../../lib/processing/resources/stylesheet');
const manifestProcessor = require('../../lib/processing/resources/manifest');
const { createAndStartProxy } = require('./common/utils');


function normalizeCode (code) {
return code
Expand Down Expand Up @@ -141,7 +141,8 @@ describe('Content charset', () => {
session.handleAttachment = () => void 0;
session.id = 'sessionId';

proxy = new Proxy('127.0.0.1', 1836, 1837);
proxy = createAndStartProxy();

proxy.openSession('http://127.0.0.1:2000/', session);
});

Expand Down
Loading

0 comments on commit 3384aca

Please sign in to comment.