Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/handlers/RequestSession.ts
1030 views
1
import * as http from 'http';
2
import IResolvablePromise from '@secret-agent/interfaces/IResolvablePromise';
3
import { createPromise } from '@secret-agent/commons/utils';
4
import ResourceType from '@secret-agent/interfaces/ResourceType';
5
import IHttpResourceLoadDetails from '@secret-agent/interfaces/IHttpResourceLoadDetails';
6
import IResourceRequest from '@secret-agent/interfaces/IResourceRequest';
7
import IResourceHeaders from '@secret-agent/interfaces/IResourceHeaders';
8
import * as http2 from 'http2';
9
import IResourceResponse from '@secret-agent/interfaces/IResourceResponse';
10
import * as net from 'net';
11
import { TypedEventEmitter } from '@secret-agent/commons/eventUtils';
12
import Log from '@secret-agent/commons/Logger';
13
import MitmSocket from '@secret-agent/mitm-socket/index';
14
import ICorePlugins from '@secret-agent/interfaces/ICorePlugins';
15
import { URL } from 'url';
16
import MitmRequestAgent from '../lib/MitmRequestAgent';
17
import IMitmRequestContext from '../interfaces/IMitmRequestContext';
18
import { Dns } from '../lib/Dns';
19
import ResourceState from '../interfaces/ResourceState';
20
import BrowserRequestMatcher from '../lib/BrowserRequestMatcher';
21
22
const { log } = Log(module);
23
24
export default class RequestSession extends TypedEventEmitter<IRequestSessionEvents> {
25
public websocketBrowserResourceIds: {
26
[headersHash: string]: IResolvablePromise<string>;
27
} = {};
28
29
public isClosing = false;
30
public blockedResources: {
31
types: ResourceType[];
32
urls: string[];
33
handlerFn?: (
34
request: http.IncomingMessage | http2.Http2ServerRequest,
35
response: http.ServerResponse | http2.Http2ServerResponse,
36
context: IMitmRequestContext,
37
) => boolean;
38
} = {
39
types: [],
40
urls: [],
41
};
42
43
public requestAgent: MitmRequestAgent;
44
public requestedUrls: {
45
url: string;
46
redirectedToUrl: string;
47
redirectChain: string[];
48
responseTime: Date;
49
}[] = [];
50
51
public readonly browserRequestMatcher: BrowserRequestMatcher;
52
53
// use this to bypass the mitm and just return a dummy response (ie for UserProfile setup)
54
public bypassAllWithEmptyResponse: boolean;
55
56
private readonly dns: Dns;
57
58
constructor(
59
readonly sessionId: string,
60
readonly plugins: ICorePlugins,
61
public upstreamProxyUrl?: string,
62
) {
63
super();
64
this.logger = log.createChild(module, {
65
sessionId,
66
});
67
this.requestAgent = new MitmRequestAgent(this);
68
this.dns = new Dns(this);
69
this.browserRequestMatcher = new BrowserRequestMatcher(this);
70
}
71
72
public trackResourceRedirects(resource: IHttpResourceLoadDetails): void {
73
const resourceRedirect = {
74
url: resource.url.href,
75
redirectedToUrl: resource.redirectedToUrl,
76
responseTime: resource.responseTime,
77
redirectChain: [],
78
};
79
this.requestedUrls.push(resourceRedirect);
80
81
const redirect = this.requestedUrls.find(
82
x =>
83
x.redirectedToUrl === resourceRedirect.url &&
84
resource.requestTime.getTime() - x.responseTime.getTime() < 5e3,
85
);
86
resource.isFromRedirect = !!redirect;
87
if (redirect) {
88
const redirectChain = [redirect.url, ...redirect.redirectChain];
89
resource.previousUrl = redirectChain[0];
90
resource.firstRedirectingUrl = redirectChain[redirectChain.length - 1];
91
resourceRedirect.redirectChain = redirectChain;
92
}
93
}
94
95
public async willSendResponse(context: IMitmRequestContext): Promise<void> {
96
context.setState(ResourceState.EmulationWillSendResponse);
97
98
if (context.resourceType === 'Document' && context.status === 200) {
99
this.plugins.websiteHasFirstPartyInteraction(context.url);
100
}
101
102
await this.plugins.beforeHttpResponse(context);
103
}
104
105
public async lookupDns(host: string): Promise<string> {
106
if (this.dns) {
107
try {
108
return await this.dns.lookupIp(host);
109
} catch (error) {
110
log.info('DnsLookup.Error', {
111
sessionId: this.sessionId,
112
error,
113
});
114
// if fails, pass through to returning host untouched
115
}
116
}
117
return Promise.resolve(host);
118
}
119
120
public getProxyCredentials(): string {
121
return `secret-agent:${this.sessionId}`;
122
}
123
124
public close(): void {
125
if (this.isClosing) return;
126
const logid = this.logger.stats('MitmRequestSession.Closing');
127
this.isClosing = true;
128
const errors: Error[] = [];
129
this.browserRequestMatcher.cancelPending();
130
try {
131
this.requestAgent.close();
132
} catch (err) {
133
errors.push(err);
134
}
135
try {
136
this.dns.close();
137
} catch (err) {
138
errors.push(err);
139
}
140
this.logger.stats('MitmRequestSession.Closed', { parentLogId: logid, errors });
141
142
setImmediate(() => this.emit('close'));
143
}
144
145
public shouldBlockRequest(url: string): boolean {
146
if (!this.blockedResources?.urls) {
147
return false;
148
}
149
for (const blockedUrlFragment of this.blockedResources.urls) {
150
if (url.includes(blockedUrlFragment) || url.match(blockedUrlFragment)) {
151
return true;
152
}
153
}
154
return false;
155
}
156
157
// function to override for
158
public blockHandler(ctx: IMitmRequestContext): boolean {
159
if (this.blockedResources?.handlerFn)
160
return this.blockedResources.handlerFn(
161
ctx.clientToProxyRequest,
162
ctx.proxyToClientResponse,
163
ctx,
164
);
165
return false;
166
}
167
168
public recordDocumentUserActivity(documentUrl: string): void {
169
this.plugins.websiteHasFirstPartyInteraction(new URL(documentUrl));
170
}
171
172
/////// Websockets ///////////////////////////////////////////////////////////
173
174
public getWebsocketUpgradeRequestId(headers: IResourceHeaders): Promise<string> {
175
const key = this.getWebsocketHeadersKey(headers);
176
177
this.websocketBrowserResourceIds[key] ??= createPromise<string>(30e3);
178
return this.websocketBrowserResourceIds[key].promise;
179
}
180
181
public registerWebsocketHeaders(
182
tabId: number,
183
message: {
184
browserRequestId: string;
185
headers: IResourceHeaders;
186
},
187
): void {
188
this.browserRequestMatcher.requestIdToTabId.set(message.browserRequestId, tabId);
189
const key = this.getWebsocketHeadersKey(message.headers);
190
191
this.websocketBrowserResourceIds[key] ??= createPromise<string>();
192
this.websocketBrowserResourceIds[key].resolve(message.browserRequestId);
193
}
194
195
private getWebsocketHeadersKey(headers: IResourceHeaders): string {
196
let websocketKey: string;
197
let host: string;
198
for (const key of Object.keys(headers)) {
199
const lowerKey = key.toLowerCase();
200
if (lowerKey === 'sec-websocket-key') websocketKey = headers[key] as string;
201
if (lowerKey === 'host') host = headers[key] as string;
202
}
203
return [host, websocketKey].join(',');
204
}
205
206
public static sendNeedsAuth(socket: net.Socket): void {
207
socket.end(
208
'HTTP/1.1 407 Proxy Authentication Required\r\n' +
209
'Proxy-Authenticate: Basic realm="sa"\r\n\r\n',
210
);
211
}
212
}
213
214
interface IRequestSessionEvents {
215
close: void;
216
response: IRequestSessionResponseEvent;
217
request: IRequestSessionRequestEvent;
218
'http-error': IRequestSessionHttpErrorEvent;
219
'resource-state': IResourceStateChangeEvent;
220
'socket-connect': ISocketEvent;
221
'socket-close': ISocketEvent;
222
}
223
224
export interface ISocketEvent {
225
socket: MitmSocket;
226
}
227
228
export interface IResourceStateChangeEvent {
229
context: IMitmRequestContext;
230
state: ResourceState;
231
}
232
233
export interface IRequestSessionResponseEvent extends IRequestSessionRequestEvent {
234
browserRequestId: string;
235
response: IResourceResponse;
236
wasCached: boolean;
237
dnsResolvedIp?: string;
238
resourceType: ResourceType;
239
responseOriginalHeaders?: IResourceHeaders;
240
body: Buffer;
241
redirectedToUrl?: string;
242
executionMillis: number;
243
browserBlockedReason?: string;
244
browserCanceled?: boolean;
245
}
246
247
export interface IRequestSessionRequestEvent {
248
id: number;
249
request: IResourceRequest;
250
documentUrl: string;
251
serverAlpn: string;
252
protocol: string;
253
socketId: number;
254
isHttp2Push: boolean;
255
didBlockResource: boolean;
256
originalHeaders: IResourceHeaders;
257
localAddress: string;
258
}
259
260
export interface IRequestSessionHttpErrorEvent {
261
request: IRequestSessionResponseEvent;
262
error: Error;
263
}
264
265