Skip to content
This repository has been archived by the owner on Sep 16, 2024. It is now read-only.

feat(agent): add request/response body intercept #71

Merged
merged 1 commit into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions agent/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"version": "2.0.0-alpha.23",
"dependencies": {
"@ulixee/chrome-113-0": "^5672.127.8",
"@ulixee/js-path": "2.0.0-alpha.23",
"@ulixee/unblocked-agent": "2.0.0-alpha.23"
}
}
10 changes: 10 additions & 0 deletions agent/main/lib/Plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export default class Plugins implements IUnblockedPlugins {
onHttp2SessionConnect: [],
shouldBlockRequest: [],
beforeHttpRequest: [],
beforeHttpRequestBody: [],
beforeHttpResponse: [],
beforeHttpResponseBody: [],
afterHttpResponse: [],
websiteHasFirstPartyInteraction: [],
};
Expand Down Expand Up @@ -228,10 +230,18 @@ export default class Plugins implements IUnblockedPlugins {
await Promise.all(this.hooksByName.beforeHttpRequest.map(fn => fn(resource)));
}

public async beforeHttpRequestBody(resource: IHttpResourceLoadDetails): Promise<void> {
await Promise.all(this.hooksByName.beforeHttpRequestBody.map(fn => fn(resource)));
}

public async beforeHttpResponse(resource: IHttpResourceLoadDetails): Promise<any> {
await Promise.all(this.hooksByName.beforeHttpResponse.map(fn => fn(resource)));
}

public async beforeHttpResponseBody(resource: IHttpResourceLoadDetails): Promise<void> {
await Promise.all(this.hooksByName.beforeHttpResponseBody.map(fn => fn(resource)));
}

public async afterHttpResponse(resource: IHttpResourceLoadDetails): Promise<any> {
await Promise.all(this.hooksByName.afterHttpResponse.map(fn => fn(resource)));
}
Expand Down
2 changes: 1 addition & 1 deletion agent/main/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"@ulixee/js-path": "2.0.0-alpha.23",
"@ulixee/unblocked-agent-mitm": "2.0.0-alpha.23",
"@ulixee/unblocked-specification": "2.0.0-alpha.23",
"devtools-protocol": "^0.0.981744",
"devtools-protocol": "^0.0.1137505",
"nanoid": "^3.3.6",
"tough-cookie": "^4.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions agent/main/test/_pageTestUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function navigateFrame(
}
}

export async function waitForVisible(frame: Frame, selector: string): Promise<INodeVisibility> {
export async function waitForVisible(frame: Frame, selector: string, timeoutMs = 10e3): Promise<INodeVisibility> {
let visibility: INodeVisibility;
await wait(
async () => {
Expand All @@ -66,7 +66,7 @@ export async function waitForVisible(frame: Frame, selector: string): Promise<IN
return true;
}
},
{ loopDelayMs: 100, timeoutMs: 10e3 },
{ loopDelayMs: 100, timeoutMs },
);
return visibility;
}
Expand Down
70 changes: 70 additions & 0 deletions agent/main/test/mitm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import { createPromise } from '@ulixee/commons/lib/utils';
import { LocationStatus } from '@ulixee/unblocked-specification/agent/browser/Location';
import { ITestKoaServer } from '@ulixee/unblocked-agent-testing/helpers';
import Resolvable from '@ulixee/commons/lib/Resolvable';
import IHttpResourceLoadDetails from '@ulixee/unblocked-specification/agent/net/IHttpResourceLoadDetails';
import { Readable } from 'stream';
import { Pool } from '../index';
import { waitForVisible } from './_pageTestUtils';

const mocks = {
MitmRequestContext: {
Expand Down Expand Up @@ -320,3 +323,70 @@ This is the main body
'https://dataliberationfoundation.org/dlfSite.png',
]);
});


test('should be able to intercept http requests and responses', async () => {
const agent = pool.createAgent({ logger: TestLogger.forTest(module) });
agent.hook({
async beforeHttpRequest(request: IHttpResourceLoadDetails): Promise<any> {
if (request.url.pathname === '/intercept-post') {
// NOTE: need to delete the content length (or set to correct value)
delete request.requestHeaders['Content-Length'];
}
},
async beforeHttpRequestBody(request: IHttpResourceLoadDetails): Promise<any> {

if (request.url.pathname === '/intercept-post') {
// drain first
for await (const _ of request.requestPostDataStream) {}
// send body. NOTE: we had to change out the content length before the body step
request.requestPostDataStream = Readable.from(Buffer.from('Intercept request'));
}
},
async beforeHttpResponse(response: IHttpResourceLoadDetails): Promise<any> {
if (response.url.pathname === '/intercept-post') {
response.responseHeaders['Content-Length'] = 'Intercepted'.length.toString();
}
},
async beforeHttpResponseBody(response: IHttpResourceLoadDetails): Promise<any> {
if (response.url.pathname === '/intercept-post') {
for await (const _ of response.responseBodyStream) {
}
response.responseBodyStream = Readable.from(Buffer.from('Intercepted'));
}
}
});
const page = await agent.newPage();
const requestPost = new Resolvable<string>();
koa.post('/intercept-post', async ctx => {
let request = '';
for await (const chunk of ctx.req) {
request += chunk.toString();
}
requestPost.resolve(request);
ctx.body = 'Result';
});
koa.get('/intercept', async ctx => {
ctx.body = `<html>
<body>
<h1>Nothing</h1>
</body>
<script type='text/javascript'>
fetch('/intercept-post', {
method: 'POST',
body: 'Send',
})
.then(x => x.text())
.then(x => {
document.querySelector('h1').textContent = x;
document.body.classList.add('ready');
});
</script>
</html>`;
});
await page.goto(`${koa.baseUrl}/intercept`);
await page.waitForLoad(LocationStatus.AllContentLoaded);
await expect(requestPost).resolves.toBe('Intercept request');
await waitForVisible(page.mainFrame, 'body.ready', 5e3);
await expect(page.evaluate('document.querySelector("h1").textContent')).resolves.toBe('Intercepted');
});
26 changes: 16 additions & 10 deletions agent/mitm/handlers/HttpRequestHandler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as http from 'http';
import { CanceledPromiseError } from '@ulixee/commons/interfaces/IPendingWaitEvent';
import Log, { hasBeenLoggedSymbol } from '@ulixee/commons/lib/Logger';
import * as http from 'http';
import { ClientHttp2Stream, Http2ServerRequest, Http2ServerResponse } from 'http2';
import { CanceledPromiseError } from '@ulixee/commons/interfaces/IPendingWaitEvent';
import IMitmRequestContext from '../interfaces/IMitmRequestContext';
import HeadersHandler from './HeadersHandler';
import ResourceState from '../interfaces/ResourceState';
import HttpResponseCache from '../lib/HttpResponseCache';
import MitmRequestContext from '../lib/MitmRequestContext';
import { parseRawHeaders } from '../lib/Utils';
import BaseHttpHandler from './BaseHttpHandler';
import HttpResponseCache from '../lib/HttpResponseCache';
import ResourceState from '../interfaces/ResourceState';
import HeadersHandler from './HeadersHandler';

const { log } = Log(module);

Expand Down Expand Up @@ -117,7 +117,7 @@ export default class HttpRequestHandler extends BaseHttpHandler {
return;
}

await context.requestSession.willSendResponse(context);
await context.requestSession.willSendHttpResponse(context);

try {
this.writeResponseHead();
Expand All @@ -131,7 +131,7 @@ export default class HttpRequestHandler extends BaseHttpHandler {
return this.onError('ServerToProxyToClient.ReadWriteResponseError', err);
}

await context.requestSession.haveSentResponse(context);
await context.requestSession.didSendHttpResponse(context);

context.setState(ResourceState.End);
this.cleanup();
Expand Down Expand Up @@ -210,11 +210,15 @@ export default class HttpRequestHandler extends BaseHttpHandler {
}
};

this.context.requestPostDataStream = clientToProxyRequest;
await this.context.requestSession.willSendHttpRequestBody(this.context);

const data: Buffer[] = [];
for await (const chunk of clientToProxyRequest) {
for await (const chunk of this.context.requestPostDataStream) {
data.push(chunk);
proxyToServerRequest.write(chunk, onWriteError);
}
delete this.context.requestPostDataStream;

HeadersHandler.sendRequestTrailers(this.context);
await new Promise(resolve => proxyToServerRequest.end(resolve));
Expand Down Expand Up @@ -267,13 +271,15 @@ export default class HttpRequestHandler extends BaseHttpHandler {
context.setState(ResourceState.WriteProxyToClientResponseBody);

context.responseBodySize = 0;

for await (const chunk of serverToProxyResponse) {
context.responseBodyStream = serverToProxyResponse;
await this.context.requestSession.willSendHttpResponseBody(this.context);
for await (const chunk of context.responseBodyStream) {
const buffer = chunk as Buffer;
context.responseBodySize += buffer.length;
const data = context.cacheHandler.onResponseData(buffer);
this.safeWriteToClient(data);
}
delete this.context.responseBodyStream;

if (context.cacheHandler.shouldServeCachedData) {
this.safeWriteToClient(context.cacheHandler.cacheData);
Expand Down
4 changes: 2 additions & 2 deletions agent/mitm/handlers/HttpUpgradeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,14 @@ export default class HttpUpgradeHandler extends BaseHttpHandler {
responseMessage += `${serverResponse.rawHeaders[i]}: ${serverResponse.rawHeaders[i + 1]}\r\n`;
}
this.context.responseBodySize = 0;
await requestSession.willSendResponse(this.context);
await requestSession.willSendHttpResponse(this.context);

this.context.setState(ResourceState.WriteProxyToClientResponseBody);
clientSocket.write(`${responseMessage}\r\n`, error => {
if (error) this.onError('ProxyToClient.UpgradeWriteError', error);
});

await requestSession.haveSentResponse(this.context);
await requestSession.didSendHttpResponse(this.context);

if (!serverSocket.readable || !serverSocket.writable) {
this.context.setState(ResourceState.PrematurelyClosed);
Expand Down
19 changes: 16 additions & 3 deletions agent/mitm/handlers/RequestSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default class RequestSession
hooks: INetworkHooks,
logger: IBoundLog,
public upstreamProxyUrl?: string,
public upstreamProxyUseSystemDns?: boolean
public upstreamProxyUseSystemDns?: boolean,
) {
super();
this.logger = logger.createChild(module);
Expand Down Expand Up @@ -118,7 +118,14 @@ export default class RequestSession
}
}

public async willSendResponse(context: IMitmRequestContext): Promise<void> {
// NOTE: must change names from plugin callbacks or it will loop back here
public async willSendHttpRequestBody(context: IMitmRequestContext): Promise<void> {
for (const hook of this.hooks) {
await hook.beforeHttpRequestBody?.(context);
}
}

public async willSendHttpResponse(context: IMitmRequestContext): Promise<void> {
context.setState(ResourceState.EmulationWillSendResponse);

if (context.resourceType === 'Document' && context.status === 200) {
Expand All @@ -131,7 +138,13 @@ export default class RequestSession
}
}

public async haveSentResponse(context: IMitmRequestContext): Promise<void> {
public async willSendHttpResponseBody(context: IMitmRequestContext): Promise<void> {
for (const hook of this.hooks) {
await hook.beforeHttpResponseBody?.(context);
}
}

public async didSendHttpResponse(context: IMitmRequestContext): Promise<void> {
for (const hook of this.hooks) {
await hook.afterHttpResponse?.(context);
}
Expand Down
1 change: 1 addition & 0 deletions agent/mitm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"devDependencies": {
"@types/node": "^16.18.31",
"@types/ws": "^7.4.7",
"@ulixee/unblocked-agent": "2.0.0-alpha.23",
"@ulixee/unblocked-agent-testing": "2.0.0-alpha.23",
"http-proxy-agent": "^4.0.1",
"proxy": "^1.0.1",
Expand Down
3 changes: 2 additions & 1 deletion agent/testing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@types/koa": "^2.11.3",
"@types/koa__multer": "^2.0.2",
"@types/koa__router": "^8.0.2",
"@types/node": "^16.18.31"
"@types/node": "^16.18.31",
"@ulixee/unblocked-agent-mitm": "2.0.0-alpha.23"
}
}
2 changes: 1 addition & 1 deletion browser-emulator-builder/data
Submodule data updated 1 files
+20 −0 browserEngineOptions.json
3 changes: 3 additions & 0 deletions browser-emulator-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"@double-agent/collect": "2.0.0-alpha.23",
"@double-agent/collect-browser-codecs": "2.0.0-alpha.23",
"@double-agent/collect-browser-dom-environment": "2.0.0-alpha.23",
"@double-agent/collect-browser-fonts": "2.0.0-alpha.23",
"@double-agent/collect-browser-speech": "2.0.0-alpha.23",
"@double-agent/collect-http-basic-headers": "2.0.0-alpha.23",
"@double-agent/collect-http-ua-hints": "2.0.0-alpha.23",
"@double-agent/collect-http2-session": "2.0.0-alpha.23",
"@double-agent/collect-tls-clienthello": "2.0.0-alpha.23",
"@ulixee/commons": "2.0.0-alpha.23",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"main": "index.js",
"private": true,
"license": "MIT",
"scripts": {},
"description": "Collects the browser's DOM environment such as object structure, class inheritance amd key order",
"dependencies": {
"@double-agent/collect": "2.0.0-alpha.23"
Expand Down
1 change: 0 additions & 1 deletion double-agent/collect/plugins/tls-clienthello/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"main": "index.js",
"private": true,
"license": "MIT",
"scripts": {},
"description": "Collects the TLS clienthello handshake when initiating a secure connection",
"dependencies": {
"@double-agent/collect": "2.0.0-alpha.23",
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
"devDependencies": {
"@commitlint/cli": "^17.6.3",
"@commitlint/config-conventional": "^17.6.3",
"@ulixee/repo-tools": "^1.0.26",
"@types/node": "^16.18.31",
"@types/jest": "^29.5.1",
"@types/node": "^16.18.31",
"@ulixee/repo-tools": "^1.0.26",
"concurrently": "^6.2.1",
"cross-env": "^7.0.3",
"husky": "^8.0.3",
Expand Down Expand Up @@ -71,6 +71,5 @@
"*.json": [
"prettier --write"
]
},
"dependencies": {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ export function configureBrowserLaunchArgs(

if (options.showChrome) {
if (options.showDevtools) engine.launchArguments.push('--auto-open-devtools-for-tabs');
} else {
} else { if (process.platform === 'darwin') {
if (process.arch === 'arm64') {
engine.launchArguments.push('--use-gl=any');
}
}
engine.launchArguments.push(
'--hide-scrollbars',
'--mute-audio',
Expand Down
Loading