n8n-nodes-nvk-browser 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nodes/PageInteraction/BrowserHttpRequest/BrowserHttpRequest.description.d.ts +2 -0
- package/dist/nodes/PageInteraction/BrowserHttpRequest/BrowserHttpRequest.description.js +274 -0
- package/dist/nodes/PageInteraction/BrowserHttpRequest/BrowserHttpRequest.node.d.ts +5 -0
- package/dist/nodes/PageInteraction/BrowserHttpRequest/BrowserHttpRequest.node.js +244 -0
- package/dist/nodes/PageInteraction/GetNetworkResponse/GetNetworkResponse.description.d.ts +2 -0
- package/dist/nodes/PageInteraction/GetNetworkResponse/GetNetworkResponse.description.js +125 -0
- package/dist/nodes/PageInteraction/GetNetworkResponse/GetNetworkResponse.node.d.ts +5 -0
- package/dist/nodes/PageInteraction/GetNetworkResponse/GetNetworkResponse.node.js +265 -0
- package/package.json +4 -2
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.browserHttpRequestFields = void 0;
|
|
4
|
+
exports.browserHttpRequestFields = [
|
|
5
|
+
{
|
|
6
|
+
displayName: 'Profile ID',
|
|
7
|
+
name: 'profileId',
|
|
8
|
+
type: 'string',
|
|
9
|
+
required: true,
|
|
10
|
+
default: '',
|
|
11
|
+
description: 'ID of the running profile to use for cookies and headers',
|
|
12
|
+
displayOptions: {
|
|
13
|
+
show: {
|
|
14
|
+
resource: ['page'],
|
|
15
|
+
operation: ['browserHttpRequest'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
displayName: 'Method',
|
|
21
|
+
name: 'method',
|
|
22
|
+
type: 'options',
|
|
23
|
+
options: [
|
|
24
|
+
{
|
|
25
|
+
name: 'GET',
|
|
26
|
+
value: 'GET',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'POST',
|
|
30
|
+
value: 'POST',
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
default: 'GET',
|
|
34
|
+
description: 'HTTP method to use',
|
|
35
|
+
displayOptions: {
|
|
36
|
+
show: {
|
|
37
|
+
resource: ['page'],
|
|
38
|
+
operation: ['browserHttpRequest'],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
displayName: 'URL',
|
|
44
|
+
name: 'url',
|
|
45
|
+
type: 'string',
|
|
46
|
+
required: true,
|
|
47
|
+
default: '',
|
|
48
|
+
description: 'The URL to make the request to',
|
|
49
|
+
displayOptions: {
|
|
50
|
+
show: {
|
|
51
|
+
resource: ['page'],
|
|
52
|
+
operation: ['browserHttpRequest'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
displayName: 'Send Body',
|
|
58
|
+
name: 'sendBody',
|
|
59
|
+
type: 'boolean',
|
|
60
|
+
default: false,
|
|
61
|
+
description: 'Whether to send a body in the request',
|
|
62
|
+
displayOptions: {
|
|
63
|
+
show: {
|
|
64
|
+
resource: ['page'],
|
|
65
|
+
operation: ['browserHttpRequest'],
|
|
66
|
+
method: ['POST'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
displayName: 'Body Content Type',
|
|
72
|
+
name: 'bodyContentType',
|
|
73
|
+
type: 'options',
|
|
74
|
+
options: [
|
|
75
|
+
{
|
|
76
|
+
name: 'JSON',
|
|
77
|
+
value: 'json',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Raw',
|
|
81
|
+
value: 'raw',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'Form Data',
|
|
85
|
+
value: 'formData',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
default: 'json',
|
|
89
|
+
description: 'Content type of the body',
|
|
90
|
+
displayOptions: {
|
|
91
|
+
show: {
|
|
92
|
+
resource: ['page'],
|
|
93
|
+
operation: ['browserHttpRequest'],
|
|
94
|
+
method: ['POST'],
|
|
95
|
+
sendBody: [true],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
displayName: 'JSON Body',
|
|
101
|
+
name: 'jsonBody',
|
|
102
|
+
type: 'string',
|
|
103
|
+
typeOptions: {
|
|
104
|
+
rows: 4,
|
|
105
|
+
},
|
|
106
|
+
default: '',
|
|
107
|
+
description: 'JSON body to send',
|
|
108
|
+
displayOptions: {
|
|
109
|
+
show: {
|
|
110
|
+
resource: ['page'],
|
|
111
|
+
operation: ['browserHttpRequest'],
|
|
112
|
+
method: ['POST'],
|
|
113
|
+
sendBody: [true],
|
|
114
|
+
bodyContentType: ['json'],
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
displayName: 'Raw Body',
|
|
120
|
+
name: 'rawBody',
|
|
121
|
+
type: 'string',
|
|
122
|
+
typeOptions: {
|
|
123
|
+
rows: 4,
|
|
124
|
+
},
|
|
125
|
+
default: '',
|
|
126
|
+
description: 'Raw body to send',
|
|
127
|
+
displayOptions: {
|
|
128
|
+
show: {
|
|
129
|
+
resource: ['page'],
|
|
130
|
+
operation: ['browserHttpRequest'],
|
|
131
|
+
method: ['POST'],
|
|
132
|
+
sendBody: [true],
|
|
133
|
+
bodyContentType: ['raw'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
displayName: 'Form Data',
|
|
139
|
+
name: 'formData',
|
|
140
|
+
type: 'fixedCollection',
|
|
141
|
+
typeOptions: {
|
|
142
|
+
multipleValues: true,
|
|
143
|
+
},
|
|
144
|
+
default: {},
|
|
145
|
+
description: 'Form data to send',
|
|
146
|
+
displayOptions: {
|
|
147
|
+
show: {
|
|
148
|
+
resource: ['page'],
|
|
149
|
+
operation: ['browserHttpRequest'],
|
|
150
|
+
method: ['POST'],
|
|
151
|
+
sendBody: [true],
|
|
152
|
+
bodyContentType: ['formData'],
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
options: [
|
|
156
|
+
{
|
|
157
|
+
displayName: 'Parameter',
|
|
158
|
+
name: 'parameter',
|
|
159
|
+
values: [
|
|
160
|
+
{
|
|
161
|
+
displayName: 'Name',
|
|
162
|
+
name: 'name',
|
|
163
|
+
type: 'string',
|
|
164
|
+
default: '',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
displayName: 'Value',
|
|
168
|
+
name: 'value',
|
|
169
|
+
type: 'string',
|
|
170
|
+
default: '',
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
displayName: 'Query Parameters',
|
|
178
|
+
name: 'queryParameters',
|
|
179
|
+
type: 'fixedCollection',
|
|
180
|
+
typeOptions: {
|
|
181
|
+
multipleValues: true,
|
|
182
|
+
},
|
|
183
|
+
default: {},
|
|
184
|
+
description: 'Query parameters to send',
|
|
185
|
+
displayOptions: {
|
|
186
|
+
show: {
|
|
187
|
+
resource: ['page'],
|
|
188
|
+
operation: ['browserHttpRequest'],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
options: [
|
|
192
|
+
{
|
|
193
|
+
displayName: 'Parameter',
|
|
194
|
+
name: 'parameter',
|
|
195
|
+
values: [
|
|
196
|
+
{
|
|
197
|
+
displayName: 'Name',
|
|
198
|
+
name: 'name',
|
|
199
|
+
type: 'string',
|
|
200
|
+
default: '',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
displayName: 'Value',
|
|
204
|
+
name: 'value',
|
|
205
|
+
type: 'string',
|
|
206
|
+
default: '',
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
displayName: 'Additional Headers',
|
|
214
|
+
name: 'additionalHeaders',
|
|
215
|
+
type: 'fixedCollection',
|
|
216
|
+
typeOptions: {
|
|
217
|
+
multipleValues: true,
|
|
218
|
+
},
|
|
219
|
+
default: {},
|
|
220
|
+
description: 'Additional headers to send (will be merged with browser headers)',
|
|
221
|
+
displayOptions: {
|
|
222
|
+
show: {
|
|
223
|
+
resource: ['page'],
|
|
224
|
+
operation: ['browserHttpRequest'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
options: [
|
|
228
|
+
{
|
|
229
|
+
displayName: 'Header',
|
|
230
|
+
name: 'header',
|
|
231
|
+
values: [
|
|
232
|
+
{
|
|
233
|
+
displayName: 'Name',
|
|
234
|
+
name: 'name',
|
|
235
|
+
type: 'string',
|
|
236
|
+
default: '',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
displayName: 'Value',
|
|
240
|
+
name: 'value',
|
|
241
|
+
type: 'string',
|
|
242
|
+
default: '',
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
displayName: 'Tab Index',
|
|
250
|
+
name: 'tabIndex',
|
|
251
|
+
type: 'number',
|
|
252
|
+
default: 0,
|
|
253
|
+
description: 'Index of the tab to get cookies and headers from (0 = first tab)',
|
|
254
|
+
displayOptions: {
|
|
255
|
+
show: {
|
|
256
|
+
resource: ['page'],
|
|
257
|
+
operation: ['browserHttpRequest'],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
displayName: 'Auto Start Profile',
|
|
263
|
+
name: 'autoStart',
|
|
264
|
+
type: 'boolean',
|
|
265
|
+
default: false,
|
|
266
|
+
description: 'Automatically start the profile if it is not running',
|
|
267
|
+
displayOptions: {
|
|
268
|
+
show: {
|
|
269
|
+
resource: ['page'],
|
|
270
|
+
operation: ['browserHttpRequest'],
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class BrowserHttpRequest implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.BrowserHttpRequest = void 0;
|
|
27
|
+
const BrowserManager_1 = require("../../../utils/BrowserManager");
|
|
28
|
+
const BrowserHttpRequest_description_1 = require("./BrowserHttpRequest.description");
|
|
29
|
+
const path = __importStar(require("path"));
|
|
30
|
+
class BrowserHttpRequest {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.description = {
|
|
33
|
+
displayName: 'Browser HTTP Request',
|
|
34
|
+
name: 'browserHttpRequest',
|
|
35
|
+
icon: 'file:http.svg',
|
|
36
|
+
group: ['transform'],
|
|
37
|
+
version: 1,
|
|
38
|
+
description: 'Make HTTP requests using browser profile cookies and headers to bypass Cloudflare',
|
|
39
|
+
defaults: {
|
|
40
|
+
name: 'Browser HTTP Request',
|
|
41
|
+
},
|
|
42
|
+
inputs: ['main'],
|
|
43
|
+
outputs: ['main'],
|
|
44
|
+
properties: [
|
|
45
|
+
{
|
|
46
|
+
displayName: 'Resource',
|
|
47
|
+
name: 'resource',
|
|
48
|
+
type: 'options',
|
|
49
|
+
noDataExpression: true,
|
|
50
|
+
options: [
|
|
51
|
+
{
|
|
52
|
+
name: 'Page',
|
|
53
|
+
value: 'page',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
default: 'page',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
displayName: 'Operation',
|
|
60
|
+
name: 'operation',
|
|
61
|
+
type: 'options',
|
|
62
|
+
noDataExpression: true,
|
|
63
|
+
options: [
|
|
64
|
+
{
|
|
65
|
+
name: 'Browser HTTP Request',
|
|
66
|
+
value: 'browserHttpRequest',
|
|
67
|
+
description: 'Make HTTP request with browser cookies and headers',
|
|
68
|
+
action: 'Make HTTP request',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
default: 'browserHttpRequest',
|
|
72
|
+
},
|
|
73
|
+
...BrowserHttpRequest_description_1.browserHttpRequestFields,
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async execute() {
|
|
78
|
+
const items = this.getInputData();
|
|
79
|
+
const returnData = [];
|
|
80
|
+
const workspacePath = process.cwd();
|
|
81
|
+
const profilesDir = process.env.NVK_PROFILES_DIR || path.join(workspacePath, 'profiles');
|
|
82
|
+
const browserPath = process.env.NVK_BROWSER_PATH || path.join(workspacePath, 'browser-142', 'chrome.exe');
|
|
83
|
+
const resolvedBrowserPath = path.isAbsolute(browserPath)
|
|
84
|
+
? browserPath
|
|
85
|
+
: path.resolve(workspacePath, browserPath);
|
|
86
|
+
const resolvedProfilesDir = path.isAbsolute(profilesDir)
|
|
87
|
+
? profilesDir
|
|
88
|
+
: path.resolve(workspacePath, profilesDir);
|
|
89
|
+
const browserManager = BrowserManager_1.BrowserManager.getInstance(resolvedBrowserPath, resolvedProfilesDir);
|
|
90
|
+
for (let i = 0; i < items.length; i++) {
|
|
91
|
+
try {
|
|
92
|
+
const profileId = this.getNodeParameter('profileId', i);
|
|
93
|
+
const method = this.getNodeParameter('method', i) || 'GET';
|
|
94
|
+
const url = this.getNodeParameter('url', i);
|
|
95
|
+
const tabIndex = this.getNodeParameter('tabIndex', i) || 0;
|
|
96
|
+
const autoStart = this.getNodeParameter('autoStart', i) || false;
|
|
97
|
+
let instance = browserManager.getInstance(profileId);
|
|
98
|
+
// Auto start profile nếu chưa chạy và option được bật
|
|
99
|
+
if (!instance && autoStart) {
|
|
100
|
+
instance = await browserManager.startProfileIfNotRunning(profileId);
|
|
101
|
+
}
|
|
102
|
+
if (!instance) {
|
|
103
|
+
throw new Error(`Profile ${profileId} is not running. Enable "Auto Start Profile" to start it automatically.`);
|
|
104
|
+
}
|
|
105
|
+
const page = await browserManager.getPage(profileId, tabIndex);
|
|
106
|
+
if (!page) {
|
|
107
|
+
throw new Error(`Could not get page for profile ${profileId}`);
|
|
108
|
+
}
|
|
109
|
+
// Get cookies from browser
|
|
110
|
+
const cookies = await page.cookies();
|
|
111
|
+
const cookieString = cookies.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
|
|
112
|
+
// Get user agent from browser
|
|
113
|
+
const userAgent = await page.evaluate(() => navigator.userAgent);
|
|
114
|
+
// Get additional headers from browser context
|
|
115
|
+
const browserHeaders = {
|
|
116
|
+
'User-Agent': userAgent,
|
|
117
|
+
'Accept': '*/*',
|
|
118
|
+
'Accept-Language': 'en-US,en;q=0.9',
|
|
119
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
120
|
+
'Connection': 'keep-alive',
|
|
121
|
+
'Sec-Fetch-Dest': 'empty',
|
|
122
|
+
'Sec-Fetch-Mode': 'cors',
|
|
123
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
124
|
+
'Cookie': cookieString,
|
|
125
|
+
};
|
|
126
|
+
// Get additional headers from node parameters
|
|
127
|
+
const additionalHeadersParam = this.getNodeParameter('additionalHeaders', i);
|
|
128
|
+
if (additionalHeadersParam?.header) {
|
|
129
|
+
additionalHeadersParam.header.forEach((header) => {
|
|
130
|
+
if (header.name && header.value) {
|
|
131
|
+
browserHeaders[header.name] = header.value;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Build URL with query parameters
|
|
136
|
+
const queryParametersParam = this.getNodeParameter('queryParameters', i);
|
|
137
|
+
let finalUrl = url;
|
|
138
|
+
if (queryParametersParam?.parameter && queryParametersParam.parameter.length > 0) {
|
|
139
|
+
const urlObj = new URL(url);
|
|
140
|
+
queryParametersParam.parameter.forEach((param) => {
|
|
141
|
+
if (param.name && param.value) {
|
|
142
|
+
urlObj.searchParams.append(param.name, param.value);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
finalUrl = urlObj.toString();
|
|
146
|
+
}
|
|
147
|
+
// Prepare request body for POST
|
|
148
|
+
let body;
|
|
149
|
+
let contentType;
|
|
150
|
+
if (method === 'POST') {
|
|
151
|
+
const sendBody = this.getNodeParameter('sendBody', i);
|
|
152
|
+
if (sendBody) {
|
|
153
|
+
const bodyContentType = this.getNodeParameter('bodyContentType', i);
|
|
154
|
+
if (bodyContentType === 'json') {
|
|
155
|
+
const jsonBody = this.getNodeParameter('jsonBody', i);
|
|
156
|
+
body = jsonBody;
|
|
157
|
+
contentType = 'application/json';
|
|
158
|
+
}
|
|
159
|
+
else if (bodyContentType === 'raw') {
|
|
160
|
+
const rawBody = this.getNodeParameter('rawBody', i);
|
|
161
|
+
body = rawBody;
|
|
162
|
+
contentType = 'text/plain';
|
|
163
|
+
}
|
|
164
|
+
else if (bodyContentType === 'formData') {
|
|
165
|
+
const formDataParam = this.getNodeParameter('formData', i);
|
|
166
|
+
if (formDataParam?.parameter) {
|
|
167
|
+
const formData = new URLSearchParams();
|
|
168
|
+
formDataParam.parameter.forEach((param) => {
|
|
169
|
+
if (param.name && param.value) {
|
|
170
|
+
formData.append(param.name, param.value);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
body = formData.toString();
|
|
174
|
+
contentType = 'application/x-www-form-urlencoded';
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Set Content-Type header if body is provided
|
|
180
|
+
if (contentType) {
|
|
181
|
+
browserHeaders['Content-Type'] = contentType;
|
|
182
|
+
}
|
|
183
|
+
// Make HTTP request using page.evaluate to use browser's fetch with all cookies and headers
|
|
184
|
+
const response = await page.evaluate(async (requestData) => {
|
|
185
|
+
const fetchOptions = {
|
|
186
|
+
method: requestData.method,
|
|
187
|
+
headers: requestData.headers,
|
|
188
|
+
};
|
|
189
|
+
if (requestData.body) {
|
|
190
|
+
fetchOptions.body = requestData.body;
|
|
191
|
+
}
|
|
192
|
+
const response = await fetch(requestData.url, fetchOptions);
|
|
193
|
+
// Get response headers
|
|
194
|
+
const responseHeaders = {};
|
|
195
|
+
response.headers.forEach((value, key) => {
|
|
196
|
+
responseHeaders[key] = value;
|
|
197
|
+
});
|
|
198
|
+
// Get response body
|
|
199
|
+
let responseBody;
|
|
200
|
+
const contentType = response.headers.get('content-type') || '';
|
|
201
|
+
if (contentType.includes('application/json')) {
|
|
202
|
+
responseBody = await response.json();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
responseBody = await response.text();
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
status: response.status,
|
|
209
|
+
statusText: response.statusText,
|
|
210
|
+
headers: responseHeaders,
|
|
211
|
+
body: responseBody,
|
|
212
|
+
url: response.url,
|
|
213
|
+
};
|
|
214
|
+
}, {
|
|
215
|
+
url: finalUrl,
|
|
216
|
+
method,
|
|
217
|
+
headers: browserHeaders,
|
|
218
|
+
body,
|
|
219
|
+
});
|
|
220
|
+
returnData.push({
|
|
221
|
+
json: {
|
|
222
|
+
success: true,
|
|
223
|
+
statusCode: response.status,
|
|
224
|
+
statusMessage: response.statusText,
|
|
225
|
+
headers: response.headers,
|
|
226
|
+
body: response.body,
|
|
227
|
+
url: response.url,
|
|
228
|
+
message: 'Request completed successfully',
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
returnData.push({
|
|
234
|
+
json: {
|
|
235
|
+
success: false,
|
|
236
|
+
error: error instanceof Error ? error.message : String(error),
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return [returnData];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
exports.BrowserHttpRequest = BrowserHttpRequest;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getNetworkResponseFields = void 0;
|
|
4
|
+
exports.getNetworkResponseFields = [
|
|
5
|
+
{
|
|
6
|
+
displayName: 'Profile ID',
|
|
7
|
+
name: 'profileId',
|
|
8
|
+
type: 'string',
|
|
9
|
+
required: true,
|
|
10
|
+
default: '',
|
|
11
|
+
description: 'ID of the running profile',
|
|
12
|
+
displayOptions: {
|
|
13
|
+
show: {
|
|
14
|
+
resource: ['page'],
|
|
15
|
+
operation: ['getNetworkResponse'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
displayName: 'Request Name or URL',
|
|
21
|
+
name: 'requestFilter',
|
|
22
|
+
type: 'string',
|
|
23
|
+
required: true,
|
|
24
|
+
default: '',
|
|
25
|
+
description: 'Name of the request (e.g., "create") or URL pattern to match. Can be partial match.',
|
|
26
|
+
displayOptions: {
|
|
27
|
+
show: {
|
|
28
|
+
resource: ['page'],
|
|
29
|
+
operation: ['getNetworkResponse'],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
displayName: 'Match Type',
|
|
35
|
+
name: 'matchType',
|
|
36
|
+
type: 'options',
|
|
37
|
+
options: [
|
|
38
|
+
{
|
|
39
|
+
name: 'Contains',
|
|
40
|
+
value: 'contains',
|
|
41
|
+
description: 'Request name or URL contains the filter text',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'Exact',
|
|
45
|
+
value: 'exact',
|
|
46
|
+
description: 'Request name or URL exactly matches the filter text',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Starts With',
|
|
50
|
+
value: 'startsWith',
|
|
51
|
+
description: 'Request name or URL starts with the filter text',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'Ends With',
|
|
55
|
+
value: 'endsWith',
|
|
56
|
+
description: 'Request name or URL ends with the filter text',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'Regex',
|
|
60
|
+
value: 'regex',
|
|
61
|
+
description: 'Request name or URL matches the regex pattern',
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
default: 'contains',
|
|
65
|
+
description: 'How to match the request',
|
|
66
|
+
displayOptions: {
|
|
67
|
+
show: {
|
|
68
|
+
resource: ['page'],
|
|
69
|
+
operation: ['getNetworkResponse'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
displayName: 'Timeout (Milliseconds)',
|
|
75
|
+
name: 'timeout',
|
|
76
|
+
type: 'number',
|
|
77
|
+
default: 30000,
|
|
78
|
+
description: 'Maximum time to wait for the request to appear (in milliseconds)',
|
|
79
|
+
displayOptions: {
|
|
80
|
+
show: {
|
|
81
|
+
resource: ['page'],
|
|
82
|
+
operation: ['getNetworkResponse'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
displayName: 'Wait For Request',
|
|
88
|
+
name: 'waitForRequest',
|
|
89
|
+
type: 'boolean',
|
|
90
|
+
default: true,
|
|
91
|
+
description: 'Wait for the request to appear if it has not been made yet',
|
|
92
|
+
displayOptions: {
|
|
93
|
+
show: {
|
|
94
|
+
resource: ['page'],
|
|
95
|
+
operation: ['getNetworkResponse'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
displayName: 'Tab Index',
|
|
101
|
+
name: 'tabIndex',
|
|
102
|
+
type: 'number',
|
|
103
|
+
default: 0,
|
|
104
|
+
description: 'Index of the tab to monitor (0 = first tab)',
|
|
105
|
+
displayOptions: {
|
|
106
|
+
show: {
|
|
107
|
+
resource: ['page'],
|
|
108
|
+
operation: ['getNetworkResponse'],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
displayName: 'Auto Start Profile',
|
|
114
|
+
name: 'autoStart',
|
|
115
|
+
type: 'boolean',
|
|
116
|
+
default: false,
|
|
117
|
+
description: 'Automatically start the profile if it is not running',
|
|
118
|
+
displayOptions: {
|
|
119
|
+
show: {
|
|
120
|
+
resource: ['page'],
|
|
121
|
+
operation: ['getNetworkResponse'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class GetNetworkResponse implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.GetNetworkResponse = void 0;
|
|
27
|
+
const BrowserManager_1 = require("../../../utils/BrowserManager");
|
|
28
|
+
const GetNetworkResponse_description_1 = require("./GetNetworkResponse.description");
|
|
29
|
+
const path = __importStar(require("path"));
|
|
30
|
+
class GetNetworkResponse {
|
|
31
|
+
constructor() {
|
|
32
|
+
this.description = {
|
|
33
|
+
displayName: 'Get Network Response',
|
|
34
|
+
name: 'getNetworkResponse',
|
|
35
|
+
icon: 'file:network.svg',
|
|
36
|
+
group: ['transform'],
|
|
37
|
+
version: 1,
|
|
38
|
+
description: 'Capture and return the response of a network request by name or URL',
|
|
39
|
+
defaults: {
|
|
40
|
+
name: 'Get Network Response',
|
|
41
|
+
},
|
|
42
|
+
inputs: ['main'],
|
|
43
|
+
outputs: ['main'],
|
|
44
|
+
properties: [
|
|
45
|
+
{
|
|
46
|
+
displayName: 'Resource',
|
|
47
|
+
name: 'resource',
|
|
48
|
+
type: 'options',
|
|
49
|
+
noDataExpression: true,
|
|
50
|
+
options: [
|
|
51
|
+
{
|
|
52
|
+
name: 'Page',
|
|
53
|
+
value: 'page',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
default: 'page',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
displayName: 'Operation',
|
|
60
|
+
name: 'operation',
|
|
61
|
+
type: 'options',
|
|
62
|
+
noDataExpression: true,
|
|
63
|
+
options: [
|
|
64
|
+
{
|
|
65
|
+
name: 'Get Network Response',
|
|
66
|
+
value: 'getNetworkResponse',
|
|
67
|
+
description: 'Capture network request response',
|
|
68
|
+
action: 'Get network response',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
default: 'getNetworkResponse',
|
|
72
|
+
},
|
|
73
|
+
...GetNetworkResponse_description_1.getNetworkResponseFields,
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async execute() {
|
|
78
|
+
const items = this.getInputData();
|
|
79
|
+
const returnData = [];
|
|
80
|
+
const workspacePath = process.cwd();
|
|
81
|
+
const profilesDir = process.env.NVK_PROFILES_DIR || path.join(workspacePath, 'profiles');
|
|
82
|
+
const browserPath = process.env.NVK_BROWSER_PATH || path.join(workspacePath, 'browser-142', 'chrome.exe');
|
|
83
|
+
const resolvedBrowserPath = path.isAbsolute(browserPath)
|
|
84
|
+
? browserPath
|
|
85
|
+
: path.resolve(workspacePath, browserPath);
|
|
86
|
+
const resolvedProfilesDir = path.isAbsolute(profilesDir)
|
|
87
|
+
? profilesDir
|
|
88
|
+
: path.resolve(workspacePath, profilesDir);
|
|
89
|
+
const browserManager = BrowserManager_1.BrowserManager.getInstance(resolvedBrowserPath, resolvedProfilesDir);
|
|
90
|
+
for (let i = 0; i < items.length; i++) {
|
|
91
|
+
try {
|
|
92
|
+
const profileId = this.getNodeParameter('profileId', i);
|
|
93
|
+
const requestFilter = this.getNodeParameter('requestFilter', i);
|
|
94
|
+
const matchType = this.getNodeParameter('matchType', i) || 'contains';
|
|
95
|
+
const timeout = this.getNodeParameter('timeout', i) || 30000;
|
|
96
|
+
const waitForRequest = this.getNodeParameter('waitForRequest', i) !== false;
|
|
97
|
+
const tabIndex = this.getNodeParameter('tabIndex', i) || 0;
|
|
98
|
+
const autoStart = this.getNodeParameter('autoStart', i) || false;
|
|
99
|
+
let instance = browserManager.getInstance(profileId);
|
|
100
|
+
// Auto start profile nếu chưa chạy và option được bật
|
|
101
|
+
if (!instance && autoStart) {
|
|
102
|
+
instance = await browserManager.startProfileIfNotRunning(profileId);
|
|
103
|
+
}
|
|
104
|
+
if (!instance) {
|
|
105
|
+
throw new Error(`Profile ${profileId} is not running. Enable "Auto Start Profile" to start it automatically.`);
|
|
106
|
+
}
|
|
107
|
+
const page = await browserManager.getPage(profileId, tabIndex);
|
|
108
|
+
if (!page) {
|
|
109
|
+
throw new Error(`Could not get page for profile ${profileId}`);
|
|
110
|
+
}
|
|
111
|
+
// Enable network domain in CDP
|
|
112
|
+
const client = await page.target().createCDPSession();
|
|
113
|
+
await client.send('Network.enable');
|
|
114
|
+
// Function to match request
|
|
115
|
+
const matchesRequest = (requestUrl, requestName) => {
|
|
116
|
+
const searchText = requestFilter.toLowerCase();
|
|
117
|
+
const url = requestUrl.toLowerCase();
|
|
118
|
+
const name = requestName.toLowerCase();
|
|
119
|
+
switch (matchType) {
|
|
120
|
+
case 'exact':
|
|
121
|
+
return url === searchText || name === searchText;
|
|
122
|
+
case 'startsWith':
|
|
123
|
+
return url.startsWith(searchText) || name.startsWith(searchText);
|
|
124
|
+
case 'endsWith':
|
|
125
|
+
return url.endsWith(searchText) || name.endsWith(searchText);
|
|
126
|
+
case 'regex':
|
|
127
|
+
try {
|
|
128
|
+
const regex = new RegExp(requestFilter, 'i');
|
|
129
|
+
return regex.test(url) || regex.test(name);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new Error(`Invalid regex pattern: ${requestFilter}`);
|
|
133
|
+
}
|
|
134
|
+
case 'contains':
|
|
135
|
+
default:
|
|
136
|
+
return url.includes(searchText) || name.includes(searchText);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
// Store matched requests
|
|
140
|
+
const matchedRequests = new Map();
|
|
141
|
+
// Promise to capture the response
|
|
142
|
+
let capturedResponse = null;
|
|
143
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
144
|
+
const timeoutId = setTimeout(() => {
|
|
145
|
+
client.off('Network.responseReceived', responseHandler);
|
|
146
|
+
client.off('Network.loadingFinished', loadingHandler);
|
|
147
|
+
reject(new Error(`Timeout: Request matching "${requestFilter}" not found within ${timeout}ms`));
|
|
148
|
+
}, timeout);
|
|
149
|
+
const responseHandler = (event) => {
|
|
150
|
+
try {
|
|
151
|
+
const responseUrl = event.response.url || '';
|
|
152
|
+
const responseName = responseUrl.split('/').pop()?.split('?')[0] || '';
|
|
153
|
+
if (matchesRequest(responseUrl, responseName)) {
|
|
154
|
+
// Store response info, will get body when loading finishes
|
|
155
|
+
matchedRequests.set(event.requestId, {
|
|
156
|
+
url: responseUrl,
|
|
157
|
+
status: event.response.status,
|
|
158
|
+
statusText: event.response.statusText,
|
|
159
|
+
headers: event.response.headers,
|
|
160
|
+
mimeType: event.response.mimeType,
|
|
161
|
+
requestId: event.requestId,
|
|
162
|
+
timestamp: Date.now(),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// Ignore errors in handler
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const loadingHandler = async (event) => {
|
|
171
|
+
if (matchedRequests.has(event.requestId)) {
|
|
172
|
+
try {
|
|
173
|
+
const responseInfo = matchedRequests.get(event.requestId);
|
|
174
|
+
// Get response body
|
|
175
|
+
const responseBody = await client.send('Network.getResponseBody', {
|
|
176
|
+
requestId: event.requestId,
|
|
177
|
+
});
|
|
178
|
+
// Try to parse as JSON if possible
|
|
179
|
+
let parsedBody = responseBody.body;
|
|
180
|
+
try {
|
|
181
|
+
if (responseInfo.mimeType?.includes('application/json')) {
|
|
182
|
+
parsedBody = JSON.parse(responseBody.body);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Not JSON or parse failed, keep as string
|
|
187
|
+
}
|
|
188
|
+
capturedResponse = {
|
|
189
|
+
...responseInfo,
|
|
190
|
+
body: parsedBody,
|
|
191
|
+
bodyText: responseBody.body,
|
|
192
|
+
};
|
|
193
|
+
clearTimeout(timeoutId);
|
|
194
|
+
client.off('Network.responseReceived', responseHandler);
|
|
195
|
+
client.off('Network.loadingFinished', loadingHandler);
|
|
196
|
+
resolve();
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// Response body might not be available, try to resolve with what we have
|
|
200
|
+
if (matchedRequests.has(event.requestId)) {
|
|
201
|
+
capturedResponse = matchedRequests.get(event.requestId);
|
|
202
|
+
clearTimeout(timeoutId);
|
|
203
|
+
client.off('Network.responseReceived', responseHandler);
|
|
204
|
+
client.off('Network.loadingFinished', loadingHandler);
|
|
205
|
+
resolve();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
client.on('Network.responseReceived', responseHandler);
|
|
211
|
+
client.on('Network.loadingFinished', loadingHandler);
|
|
212
|
+
});
|
|
213
|
+
if (waitForRequest) {
|
|
214
|
+
// Wait for the request to appear
|
|
215
|
+
await responsePromise;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
// Don't wait, just check if request already exists
|
|
219
|
+
// This is a simplified approach - in a real scenario, you might want to
|
|
220
|
+
// check existing network logs or use a different strategy
|
|
221
|
+
try {
|
|
222
|
+
await Promise.race([
|
|
223
|
+
responsePromise,
|
|
224
|
+
new Promise((resolve) => setTimeout(resolve, 1000)), // Quick check
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Request not found, that's okay if waitForRequest is false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
await client.detach();
|
|
232
|
+
if (capturedResponse) {
|
|
233
|
+
returnData.push({
|
|
234
|
+
json: {
|
|
235
|
+
success: true,
|
|
236
|
+
response: capturedResponse,
|
|
237
|
+
message: 'Network response captured successfully',
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
else if (waitForRequest) {
|
|
242
|
+
throw new Error(`Request matching "${requestFilter}" not found`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
returnData.push({
|
|
246
|
+
json: {
|
|
247
|
+
success: false,
|
|
248
|
+
message: `Request matching "${requestFilter}" not found (waitForRequest is disabled)`,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
returnData.push({
|
|
255
|
+
json: {
|
|
256
|
+
success: false,
|
|
257
|
+
error: error instanceof Error ? error.message : String(error),
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return [returnData];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
exports.GetNetworkResponse = GetNetworkResponse;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-nvk-browser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "n8n nodes for managing Chrome browser profiles and page interactions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"dist/nodes/ProfileManagement/StartProfile/StartProfile.node.js",
|
|
40
40
|
"dist/nodes/ProfileManagement/StopProfile/StopProfile.node.js",
|
|
41
41
|
"dist/nodes/PageInteraction/MoveAndClick/MoveAndClick.node.js",
|
|
42
|
-
"dist/nodes/PageInteraction/RunJavaScript/RunJavaScript.node.js"
|
|
42
|
+
"dist/nodes/PageInteraction/RunJavaScript/RunJavaScript.node.js",
|
|
43
|
+
"dist/nodes/PageInteraction/GetNetworkResponse/GetNetworkResponse.node.js",
|
|
44
|
+
"dist/nodes/PageInteraction/BrowserHttpRequest/BrowserHttpRequest.node.js"
|
|
43
45
|
]
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|