http-log-replay 1.0.0
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/README.md +197 -0
- package/ecosystem.config.js +19 -0
- package/index.js +95 -0
- package/package.json +55 -0
- package/recorder.js +114 -0
- package/replayer.js +232 -0
- package/server.js +129 -0
- package/traffic-generator.js +149 -0
- package/ui/dist/ui/3rdpartylicenses.txt +487 -0
- package/ui/dist/ui/browser/favicon.ico +0 -0
- package/ui/dist/ui/browser/index.html +13 -0
- package/ui/dist/ui/browser/main-4NFTVOCL.js +5 -0
- package/ui/dist/ui/browser/styles-5INURTSO.css +0 -0
- package/ui/dist/ui/prerendered-routes.json +3 -0
package/replayer.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const axios = require('axios');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { diffJson } = require('diff');
|
|
6
|
+
require('colors');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
|
|
9
|
+
async function replayAndDiff(logFile, primaryUrl, secondaryUrl, reportFile, ignoreFields = [], injectedHeaders = {}, excludeEndpoints = [], onEvent = () => { }, concurrency = 1) {
|
|
10
|
+
console.log(`\nš Replaying logs from: ${logFile}`);
|
|
11
|
+
console.log(`š
°ļø Primary: ${primaryUrl}`);
|
|
12
|
+
console.log(`š
±ļø Secondary: ${secondaryUrl}`);
|
|
13
|
+
console.log(`š Concurrency: ${concurrency}`);
|
|
14
|
+
|
|
15
|
+
const logs = [];
|
|
16
|
+
const results = [];
|
|
17
|
+
let passed = 0;
|
|
18
|
+
let failed = 0;
|
|
19
|
+
|
|
20
|
+
// 1. Read the JSONL file safely
|
|
21
|
+
try {
|
|
22
|
+
const fileStream = fs.createReadStream(logFile);
|
|
23
|
+
const rl = readline.createInterface({
|
|
24
|
+
input: fileStream,
|
|
25
|
+
crlfDelay: Infinity
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
for await (const line of rl) {
|
|
29
|
+
if (line.trim()) {
|
|
30
|
+
try {
|
|
31
|
+
const entry = JSON.parse(line);
|
|
32
|
+
if (entry.status >= 200 && entry.status < 300) {
|
|
33
|
+
logs.push(entry);
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.warn("ā ļø Skipping malformed log line".yellow);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error(`ā Failed to read log file: ${e.message}`.red);
|
|
42
|
+
onEvent({ type: 'error', message: `Failed to read log file: ${e.message}` });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`š Loaded ${logs.length} valid requests to replay.`.cyan);
|
|
47
|
+
onEvent({ type: 'start', total: logs.length });
|
|
48
|
+
|
|
49
|
+
// 2. Iterate and Replay with Concurrency
|
|
50
|
+
const processLog = async (entry, index) => {
|
|
51
|
+
const endpoint = entry.url;
|
|
52
|
+
|
|
53
|
+
// SKIP LOGIC
|
|
54
|
+
if (excludeEndpoints.includes(endpoint)) {
|
|
55
|
+
console.log(`ā© Skipping excluded endpoint: ${endpoint}`.gray);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`\n[${index + 1}/${logs.length}] ${entry.method} ${endpoint}`.bold);
|
|
60
|
+
|
|
61
|
+
const headers = {
|
|
62
|
+
...injectedHeaders,
|
|
63
|
+
'Content-Type': 'application/json',
|
|
64
|
+
'User-Agent': 'Traffic-Replayer/1.0'
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const config = {
|
|
68
|
+
method: entry.method,
|
|
69
|
+
headers: headers,
|
|
70
|
+
data: entry.requestBody ? JSON.parse(entry.requestBody) : undefined,
|
|
71
|
+
validateStatus: () => true
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const start1 = Date.now();
|
|
76
|
+
const res1 = await axios({ ...config, url: `${primaryUrl}${endpoint}` });
|
|
77
|
+
const time1 = Date.now() - start1;
|
|
78
|
+
|
|
79
|
+
const start2 = Date.now();
|
|
80
|
+
const res2 = await axios({ ...config, url: `${secondaryUrl}${endpoint}` });
|
|
81
|
+
const time2 = Date.now() - start2;
|
|
82
|
+
|
|
83
|
+
const cleanBody1 = clean(res1.data, ignoreFields);
|
|
84
|
+
const cleanBody2 = clean(res2.data, ignoreFields);
|
|
85
|
+
|
|
86
|
+
const differences = diffJson(cleanBody1, cleanBody2);
|
|
87
|
+
const hasDiff = differences.length > 1;
|
|
88
|
+
|
|
89
|
+
const isSuccess = !hasDiff && res1.status === res2.status;
|
|
90
|
+
|
|
91
|
+
if (isSuccess) {
|
|
92
|
+
console.log(`ā
MATCH (${res1.status})`.green);
|
|
93
|
+
passed++;
|
|
94
|
+
} else {
|
|
95
|
+
console.log(`ā MISMATCH`.red);
|
|
96
|
+
failed++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const resultEntry = {
|
|
100
|
+
id: index + 1,
|
|
101
|
+
method: entry.method,
|
|
102
|
+
url: endpoint,
|
|
103
|
+
status1: res1.status,
|
|
104
|
+
status2: res2.status,
|
|
105
|
+
time1,
|
|
106
|
+
time2,
|
|
107
|
+
diff: hasDiff ? differences : null,
|
|
108
|
+
match: isSuccess
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
results.push(resultEntry);
|
|
112
|
+
onEvent({ type: 'progress', current: index + 1, total: logs.length, result: resultEntry });
|
|
113
|
+
return resultEntry;
|
|
114
|
+
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(`š„ Error replaying ${endpoint}: ${err.message}`.red);
|
|
117
|
+
onEvent({ type: 'error', message: `Error on ${endpoint}: ${err.message}` });
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Queue mechanism: Process logic in chunks based on concurrency
|
|
123
|
+
for (let i = 0; i < logs.length; i += concurrency) {
|
|
124
|
+
const batch = logs.slice(i, i + concurrency);
|
|
125
|
+
// Map batch to promises
|
|
126
|
+
await Promise.all(batch.map((log, idx) => processLog(log, i + idx)));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
generateReport(results, reportFile, primaryUrl, secondaryUrl);
|
|
130
|
+
|
|
131
|
+
console.log(`\nš Replay Complete. passed: ${passed}, failed: ${failed}`.bold);
|
|
132
|
+
|
|
133
|
+
onEvent({ type: 'complete', passed, failed, report: reportFile, results: results });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function clean(obj, ignoreList) {
|
|
137
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
138
|
+
const copy = JSON.parse(JSON.stringify(obj));
|
|
139
|
+
const sanitize = (o) => {
|
|
140
|
+
for (const key in o) {
|
|
141
|
+
if (ignoreList.includes(key)) {
|
|
142
|
+
delete o[key];
|
|
143
|
+
} else if (typeof o[key] === 'object' && o[key] !== null) {
|
|
144
|
+
sanitize(o[key]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
sanitize(copy);
|
|
149
|
+
return copy;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function generateReport(results, filePath, url1, url2) {
|
|
153
|
+
const fs = require('fs');
|
|
154
|
+
|
|
155
|
+
const html = `
|
|
156
|
+
<!DOCTYPE html>
|
|
157
|
+
<html>
|
|
158
|
+
<head>
|
|
159
|
+
<title>Traffic Replay Report</title>
|
|
160
|
+
<style>
|
|
161
|
+
body { font-family: sans-serif; padding: 20px; background: #f4f4f9; }
|
|
162
|
+
h1 { text-align: center; }
|
|
163
|
+
.summary { display: flex; justify-content: center; gap: 20px; margin-bottom: 20px; }
|
|
164
|
+
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
165
|
+
.success { border-left: 5px solid #2ecc71; color: #2ecc71; }
|
|
166
|
+
.failure { border-left: 5px solid #e74c3c; color: #e74c3c; }
|
|
167
|
+
table { width: 100%; border-collapse: collapse; background: white; }
|
|
168
|
+
th, td { padding: 10px; border-bottom: 1px solid #ddd; text-align: left; }
|
|
169
|
+
th { background: #eee; }
|
|
170
|
+
.diff-pre { background: #2d2d2d; color: #ccc; padding: 10px; border-radius: 4px; overflow-x: auto; }
|
|
171
|
+
.diff-added { color: #2ecc71; }
|
|
172
|
+
.diff-removed { color: #e74c3c; }
|
|
173
|
+
tr.fail-row { background-color: #ffe6e6; }
|
|
174
|
+
</style>
|
|
175
|
+
</head>
|
|
176
|
+
<body>
|
|
177
|
+
<h1>š¦ Traffic Replay Report</h1>
|
|
178
|
+
<div class="summary">
|
|
179
|
+
<div class="card success">
|
|
180
|
+
<h2>Passed</h2>
|
|
181
|
+
<p>${results.filter(r => r.match).length}</p>
|
|
182
|
+
</div>
|
|
183
|
+
<div class="card failure">
|
|
184
|
+
<h2>Failed</h2>
|
|
185
|
+
<p>${results.filter(r => !r.match).length}</p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<table>
|
|
190
|
+
<thead>
|
|
191
|
+
<tr>
|
|
192
|
+
<th>Method</th>
|
|
193
|
+
<th>URL</th>
|
|
194
|
+
<th>Env 1 Status</th>
|
|
195
|
+
<th>Env 2 Status</th>
|
|
196
|
+
<th>Time (ms)</th>
|
|
197
|
+
<th>Result</th>
|
|
198
|
+
</tr>
|
|
199
|
+
</thead>
|
|
200
|
+
<tbody>
|
|
201
|
+
${results.map(r => `
|
|
202
|
+
<tr class="${!r.match ? 'fail-row' : ''}">
|
|
203
|
+
<td>${r.method}</td>
|
|
204
|
+
<td>${r.url}</td>
|
|
205
|
+
<td>${r.status1}</td>
|
|
206
|
+
<td>${r.status2}</td>
|
|
207
|
+
<td>${r.time1} / ${r.time2}</td>
|
|
208
|
+
<td>${r.match ? 'ā
Match' : 'ā Mismatch'}</td>
|
|
209
|
+
</tr>
|
|
210
|
+
${!r.match && r.diff ? `
|
|
211
|
+
<tr>
|
|
212
|
+
<td colspan="6">
|
|
213
|
+
<pre class="diff-pre">${r.diff.map(d =>
|
|
214
|
+
d.added ? `<span class="diff-added">+ ${d.value}</span>` :
|
|
215
|
+
d.removed ? `<span class="diff-removed">- ${d.value}</span>` :
|
|
216
|
+
`<span> ${d.value}</span>`
|
|
217
|
+
).join('')}</pre>
|
|
218
|
+
</td>
|
|
219
|
+
</tr>
|
|
220
|
+
` : ''}
|
|
221
|
+
`).join('')}
|
|
222
|
+
</tbody>
|
|
223
|
+
</table>
|
|
224
|
+
</body>
|
|
225
|
+
</html>
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
fs.writeFileSync(filePath, html);
|
|
229
|
+
console.log(`š Report saved to ${filePath}`.green);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = replayAndDiff;
|
package/server.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
// ... existing imports
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const { Server } = require('socket.io');
|
|
6
|
+
const cors = require('cors');
|
|
7
|
+
const recorder = require('./recorder');
|
|
8
|
+
const replayer = require('./replayer');
|
|
9
|
+
const generator = require('./traffic-generator');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
|
|
13
|
+
function startServer(port = 4200) {
|
|
14
|
+
const app = express();
|
|
15
|
+
const server = http.createServer(app);
|
|
16
|
+
const io = new Server(server, { cors: { origin: '*' } });
|
|
17
|
+
|
|
18
|
+
app.use(cors());
|
|
19
|
+
app.use(express.json());
|
|
20
|
+
|
|
21
|
+
// --- Recorder API ---
|
|
22
|
+
app.post('/api/record/start', (req, res) => {
|
|
23
|
+
const { target, port, file } = req.body;
|
|
24
|
+
try {
|
|
25
|
+
const msg = recorder.start(target, port, file, (log) => {
|
|
26
|
+
io.emit('record-log', log);
|
|
27
|
+
});
|
|
28
|
+
res.json({ status: 'ok', message: msg });
|
|
29
|
+
} catch (e) {
|
|
30
|
+
res.status(400).json({ error: e.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.post('/api/record/stop', (req, res) => {
|
|
35
|
+
recorder.stop();
|
|
36
|
+
res.json({ status: 'ok', message: 'Stopped' });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// --- Replayer API (FIXED) ---
|
|
40
|
+
app.post('/api/replay', (req, res) => {
|
|
41
|
+
// Added 'exclude' to destructuring
|
|
42
|
+
const { file, env1, env2, ignore, auth, exclude } = req.body;
|
|
43
|
+
|
|
44
|
+
const injectedHeaders = {};
|
|
45
|
+
if (auth && auth.trim() !== '') {
|
|
46
|
+
injectedHeaders['Authorization'] = auth;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fixed Argument Order:
|
|
50
|
+
// 1. file, 2. env1, 3. env2, 4. reportPath, 5. ignore, 6. headers, 7. EXCLUDE, 8. callback
|
|
51
|
+
replayer(
|
|
52
|
+
file,
|
|
53
|
+
env1,
|
|
54
|
+
env2,
|
|
55
|
+
'last-report.html',
|
|
56
|
+
ignore || [],
|
|
57
|
+
injectedHeaders,
|
|
58
|
+
exclude || [], // <--- Pass exclude array here
|
|
59
|
+
(event) => {
|
|
60
|
+
io.emit('replay-event', event);
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
res.json({ status: 'ok', message: 'Replay started' });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// --- Generator API ---
|
|
68
|
+
app.post('/api/generate', async (req, res) => {
|
|
69
|
+
// 1. Get arguments (Added 'target' and 'port' so we can start the recorder)
|
|
70
|
+
const { proxyUrl, swaggerFile, exclude, target, port } = req.body;
|
|
71
|
+
|
|
72
|
+
const targetProxy = proxyUrl || 'http://localhost:3000';
|
|
73
|
+
const targetFile = swaggerFile || './full_documentation.json';
|
|
74
|
+
const targetExclude = Array.isArray(exclude) ? exclude : [];
|
|
75
|
+
const recTarget = target || 'http://localhost:8080';
|
|
76
|
+
const recPort = port || 3000;
|
|
77
|
+
|
|
78
|
+
console.log('š Starting Traffic Generation Workflow...');
|
|
79
|
+
|
|
80
|
+
// --- STEP A: Start the Recorder (The Proxy) ---
|
|
81
|
+
try {
|
|
82
|
+
// We force start the recorder so there is something listening on port 3000
|
|
83
|
+
recorder.start(recTarget, recPort, 'ui-traffic.jsonl', (log) => {
|
|
84
|
+
io.emit('record-log', log);
|
|
85
|
+
});
|
|
86
|
+
console.log(`šļø Auto-started Recorder on port ${recPort} -> ${recTarget}`);
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.log('ā ļø Recorder might already be running, proceeding...');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
res.json({ status: 'started', message: 'Traffic generation started' });
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// --- STEP B: Run the Generator ---
|
|
95
|
+
// Wait a moment for the server to bind the port
|
|
96
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
97
|
+
|
|
98
|
+
await generator.run(targetProxy, targetFile, targetExclude, (log) => {
|
|
99
|
+
// If the log is an object, emit it; if string, just log console
|
|
100
|
+
if (typeof log === 'object' && log.message) {
|
|
101
|
+
// specific generator status updates
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
console.log('š Generation finished. Stopping Recorder...');
|
|
106
|
+
|
|
107
|
+
// --- STEP C: Stop the Recorder ---
|
|
108
|
+
recorder.stop();
|
|
109
|
+
io.emit('record-stopped');
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error('Generator Error:', e);
|
|
112
|
+
recorder.stop(); // Ensure we stop even if generator crashes
|
|
113
|
+
io.emit('record-stopped');
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ... Serve Angular logic (keep existing) ...
|
|
118
|
+
const frontendPath = path.join(__dirname, 'ui/dist/ui/browser');
|
|
119
|
+
if (fs.existsSync(frontendPath)) {
|
|
120
|
+
app.use(express.static(frontendPath));
|
|
121
|
+
app.get('/', (req, res) => res.sendFile(path.join(frontendPath, 'index.html')));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
server.listen(port, () => {
|
|
125
|
+
console.log(`\nš TrafficMirror running at http://localhost:${port}`);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = startServer;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
class TrafficGenerator {
|
|
7
|
+
/**
|
|
8
|
+
* Fixed signature to match index.js:
|
|
9
|
+
* 1. proxyUrl (from options.target)
|
|
10
|
+
* 2. swaggerPath (from options.file)
|
|
11
|
+
* 3. exclude (from options.exclude)
|
|
12
|
+
* 4. onProgress (the callback)
|
|
13
|
+
* 5. sourceUrl (from options.source)
|
|
14
|
+
*/
|
|
15
|
+
async run(proxyUrl, swaggerPath, exclude = [], onProgress = () => { }, sourceUrl) {
|
|
16
|
+
// --- STEP 1: Health Checks ---
|
|
17
|
+
onProgress({ message: `𩺠Starting Pre-flight Health Checks...` });
|
|
18
|
+
|
|
19
|
+
// Check Proxy Health
|
|
20
|
+
onProgress({ message: ` - Checking Proxy: ${proxyUrl}` });
|
|
21
|
+
const isProxyUp = await this.checkConnection(proxyUrl);
|
|
22
|
+
|
|
23
|
+
// Check Source Health (if provided)
|
|
24
|
+
let isSourceUp = true;
|
|
25
|
+
if (sourceUrl) {
|
|
26
|
+
onProgress({ message: ` - Checking Source: ${sourceUrl}` });
|
|
27
|
+
isSourceUp = await this.checkConnection(sourceUrl);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!isProxyUp) {
|
|
31
|
+
const msg = `š ABORTING: Proxy is unreachable at ${proxyUrl}`;
|
|
32
|
+
onProgress({ message: msg });
|
|
33
|
+
throw new Error(msg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (sourceUrl && !isSourceUp) {
|
|
37
|
+
const msg = `š ABORTING: Source Server is unreachable at ${sourceUrl}`;
|
|
38
|
+
onProgress({ message: msg });
|
|
39
|
+
throw new Error(msg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onProgress({ message: `ā
Systems Nominal. Proxy and Source are UP.` });
|
|
43
|
+
// -----------------------------
|
|
44
|
+
|
|
45
|
+
onProgress({ message: `š Reading Swagger file: ${swaggerPath}` });
|
|
46
|
+
|
|
47
|
+
let doc;
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(swaggerPath)) {
|
|
50
|
+
throw new Error(`Swagger file not found at ${swaggerPath}`);
|
|
51
|
+
}
|
|
52
|
+
const raw = fs.readFileSync(swaggerPath);
|
|
53
|
+
doc = JSON.parse(raw);
|
|
54
|
+
} catch (e) {
|
|
55
|
+
throw new Error(`Failed to load Swagger: ${e.message}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate Swagger structure
|
|
59
|
+
if (!doc.paths) {
|
|
60
|
+
throw new Error("Invalid Swagger file: 'paths' property missing.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const paths = Object.keys(doc.paths);
|
|
64
|
+
const getEndpoints = paths.filter((p) => doc.paths[p].get);
|
|
65
|
+
|
|
66
|
+
onProgress({
|
|
67
|
+
message: `š Found ${getEndpoints.length} GET endpoints. Applying filters...`,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const rawExclude = Array.isArray(exclude) ? exclude : [exclude];
|
|
71
|
+
const cleanExclude = rawExclude.map((e) => (e ? e.trim() : '')).filter((e) => e !== '');
|
|
72
|
+
|
|
73
|
+
const validEndpoints = getEndpoints.filter((endpoint) => {
|
|
74
|
+
if (cleanExclude.includes(endpoint)) return false;
|
|
75
|
+
const isExcluded = cleanExclude.some((excludedItem) => {
|
|
76
|
+
return excludedItem.endsWith(endpoint) || endpoint.endsWith(excludedItem);
|
|
77
|
+
});
|
|
78
|
+
if (isExcluded) return false;
|
|
79
|
+
return true;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
onProgress({
|
|
83
|
+
message: `ā” Generating traffic for ${validEndpoints.length
|
|
84
|
+
} endpoints (Skipped ${getEndpoints.length - validEndpoints.length})`,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Ensure we send traffic to the PROXY
|
|
88
|
+
const cleanProxyUrl = proxyUrl.replace(/\/$/, '');
|
|
89
|
+
let count = 0;
|
|
90
|
+
|
|
91
|
+
for (const endpoint of validEndpoints) {
|
|
92
|
+
// Skip parameterized endpoints like /users/{id} for now as we can't guess IDs
|
|
93
|
+
if (endpoint.includes('{')) {
|
|
94
|
+
onProgress({ message: `ā ļø Skipping parameterized path: ${endpoint}` });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const url = `${cleanProxyUrl}/api${endpoint}`;
|
|
99
|
+
console.log(`[Generator] Requesting: ${url}`);
|
|
100
|
+
count++;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await this.sendRequest(url);
|
|
104
|
+
onProgress({
|
|
105
|
+
message: `[${count}/${validEndpoints.length}] š HIT: ${endpoint}`,
|
|
106
|
+
});
|
|
107
|
+
// Small delay to prevent overwhelming the server
|
|
108
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
109
|
+
} catch (err) {
|
|
110
|
+
onProgress({ message: `ā FAIL: ${endpoint} - ${err.message}` });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onProgress({ message: 'ā
Traffic generation complete!' });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
checkConnection(url) {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
if (!url) resolve(true);
|
|
121
|
+
|
|
122
|
+
const client = url.startsWith('https') ? https : http;
|
|
123
|
+
const req = client.get(url, (res) => {
|
|
124
|
+
res.resume();
|
|
125
|
+
resolve(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
req.on('error', () => resolve(false));
|
|
129
|
+
|
|
130
|
+
req.setTimeout(5000, () => {
|
|
131
|
+
req.destroy();
|
|
132
|
+
resolve(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
sendRequest(url) {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const client = url.startsWith('https') ? https : http;
|
|
140
|
+
const req = client.get(url, { headers: { 'User-Agent': 'Traffic-Mirror-Bot' } }, (res) => {
|
|
141
|
+
res.resume();
|
|
142
|
+
resolve();
|
|
143
|
+
});
|
|
144
|
+
req.on('error', (e) => reject(e));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = new TrafficGenerator();
|