Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/core/lib/DetachedTabState.ts
1029 views
1
import INavigation from '@secret-agent/interfaces/INavigation';
2
import IResourceMeta from '@secret-agent/interfaces/IResourceMeta';
3
import { DomActionType } from '@secret-agent/interfaces/IDomChangeEvent';
4
import { IPuppetPage } from '@secret-agent/interfaces/IPuppetPage';
5
import { IDomChangeRecord } from '../models/DomChangesTable';
6
import Session from './Session';
7
import InjectedScripts from './InjectedScripts';
8
import Tab from './Tab';
9
10
export default class DetachedTabState {
11
public get url(): string {
12
return this.initialPageNavigation.finalUrl;
13
}
14
15
public detachedAtCommandId: number;
16
public get domChangeRange(): { indexRange: [number, number]; timestampRange: [number, number] } {
17
if (!this.domChanges?.length) {
18
return { indexRange: [-1, 1], timestampRange: [-1, 1] };
19
}
20
const first = this.domChanges[0];
21
const last = this.domChanges[this.domChanges.length - 1];
22
return {
23
indexRange: [first.eventIndex, last.eventIndex],
24
timestampRange: [first.timestamp, last.timestamp],
25
};
26
}
27
28
private readonly initialPageNavigation: INavigation;
29
private readonly domChanges: IDomChangeRecord[];
30
private readonly resourceLookup: { [method_url: string]: IResourceMeta[] };
31
private session: Session;
32
private doctype = '';
33
34
constructor(
35
session: Session,
36
initialPageNavigation: INavigation,
37
domRecording: IDomChangeRecord[],
38
resourceLookup: { [method_url: string]: IResourceMeta[] },
39
) {
40
this.detachedAtCommandId = session.sessionState.lastCommand.id;
41
this.session = session;
42
this.initialPageNavigation = initialPageNavigation;
43
this.domChanges = this.filterDomChanges(domRecording);
44
this.resourceLookup = resourceLookup;
45
}
46
47
public async restoreDomIntoTab(tab: Tab): Promise<void> {
48
const page = tab.puppetPage;
49
const loader = await page.navigate(this.url);
50
51
tab.navigations.onNavigationRequested(
52
'goto',
53
this.url,
54
this.detachedAtCommandId,
55
loader.loaderId,
56
);
57
await Promise.all([
58
page.mainFrame.waitForLoader(loader.loaderId),
59
page.mainFrame.waitForLifecycleEvent('DOMContentLoaded', loader.loaderId),
60
InjectedScripts.installDetachedScripts(page),
61
]);
62
await InjectedScripts.restoreDom(page, this.domChanges);
63
}
64
65
public mockNetworkRequests: Parameters<
66
IPuppetPage['setNetworkRequestInterceptor']
67
>[0] = async request => {
68
const { url, method } = request.request;
69
if (request.resourceType === 'Document' && url === this.url) {
70
return {
71
requestId: request.requestId,
72
responseCode: 200,
73
responseHeaders: [{ name: 'Content-Type', value: 'text/html; charset=utf-8' }],
74
body: Buffer.from(`${this.doctype}<html><head></head><body></body></html>`).toString(
75
'base64',
76
),
77
};
78
}
79
80
const match = this.resourceLookup[`${method}_${url}`]?.shift();
81
if (!match) return null;
82
83
const { headers, isJavascript } = this.getMockHeaders(match);
84
if (isJavascript || request.resourceType === 'Script') {
85
return {
86
requestId: request.requestId,
87
responseCode: 200,
88
responseHeaders: [{ name: 'Content-Type', value: 'application/javascript' }],
89
body: '',
90
};
91
}
92
93
const body =
94
(await this.session.sessionState.getResourceData(match.id, false))?.toString('base64') ?? '';
95
96
return {
97
requestId: request.requestId,
98
body,
99
responseHeaders: headers,
100
responseCode: match.response.statusCode,
101
};
102
};
103
104
public toJSON(): any {
105
return {
106
domChangeRange: this.domChangeRange,
107
url: this.url,
108
detachedAtCommandId: this.detachedAtCommandId,
109
resources: Object.values(this.resourceLookup).reduce((a, b) => (a += b.length), 0),
110
};
111
}
112
113
private getMockHeaders(
114
resource: IResourceMeta,
115
): { isJavascript: boolean; headers: { name: string; value: string }[] } {
116
const headers: { name: string; value: string }[] = [];
117
let isJavascript = false;
118
119
for (const [key, header] of Object.entries(resource.response.headers)) {
120
const name = key.toLowerCase();
121
122
// only take limited set of headers
123
if (
124
name === 'date' ||
125
name.startsWith('x-') ||
126
name === 'set-cookie' ||
127
name === 'alt-svc' ||
128
name === 'server'
129
) {
130
continue;
131
}
132
133
if (name === 'content-type' && header.includes('javascript')) {
134
isJavascript = true;
135
break;
136
}
137
138
if (Array.isArray(header)) {
139
for (const value of header) {
140
headers.push({ name, value });
141
}
142
} else {
143
headers.push({ name, value: header });
144
}
145
}
146
return { headers, isJavascript };
147
}
148
149
private filterDomChanges(allDomChanges: IDomChangeRecord[]): IDomChangeRecord[] {
150
// find last "newDocument" entry
151
const domChanges: IDomChangeRecord[] = [];
152
for (const entry of allDomChanges) {
153
// 10 is doctype
154
if (entry.action === DomActionType.added && entry.nodeType === 10) {
155
this.doctype = entry.textContent;
156
}
157
if (entry.action === DomActionType.newDocument && this.url === entry.textContent) {
158
// reset list
159
domChanges.length = 0;
160
continue;
161
}
162
domChanges.push(entry);
163
}
164
return domChanges;
165
}
166
}
167
168