http-log-replay 1.0.0 â 1.1.1
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 +56 -5
- package/index.js +22 -12
- package/package.json +60 -55
- package/recorder.js +114 -114
- package/replayer.js +240 -232
- package/server.js +156 -129
- package/traffic-generator.js +204 -149
- package/ui/dist/ui/browser/index.html +1 -1
- package/ui/dist/ui/browser/main-A7C62VO3.js +5 -0
- package/ui/dist/ui/browser/main-4NFTVOCL.js +0 -5
package/README.md
CHANGED
|
@@ -49,16 +49,20 @@ This will launch the **Traffic Mirror Dashboard** at [http://localhost:4200](htt
|
|
|
49
49
|
The Web UI is the best way to get started. It guides you through the entire workflow in three simple tabs.
|
|
50
50
|
|
|
51
51
|
### 1. đ´ Record / Generate
|
|
52
|
-
|
|
52
|
+
|
|
53
|
+
- **Manual Mode**: Start the proxy, point your application/client to it, and select desired HTTP methods (e.g., GET, POST).
|
|
53
54
|
- **Auto-Generate**: Upload an OpenAPI/Swagger JSON file to automatically generate realistic traffic patterns.
|
|
55
|
+
- **YAML Config Mode**: Load a `config.yaml` file to pre-configure Traffic Generation (target, source, exclusions, timeouts, methods).
|
|
54
56
|
|
|
55
57
|
### 2. âśď¸ Replay
|
|
58
|
+
|
|
56
59
|
- Configure your **Primary** (Stable) and **Secondary** (Test) environments.
|
|
57
60
|
- Set **Concurrency** to speed up large suites.
|
|
58
61
|
- Add **Ignore Fields** (e.g., `createdAt`, `traceId`) to filter out noise in the comparison.
|
|
59
62
|
- Click **Replay & Compare**.
|
|
60
63
|
|
|
61
64
|
### 3. đ Report
|
|
65
|
+
|
|
62
66
|
- Instantly view the results.
|
|
63
67
|
- **Green**: Exact match.
|
|
64
68
|
- **Red**: Mismatch (click to expand the JSON diff).
|
|
@@ -70,24 +74,36 @@ The Web UI is the best way to get started. It guides you through the entire work
|
|
|
70
74
|
Perfect for **CI/CD pipelines** or headless environments.
|
|
71
75
|
|
|
72
76
|
### 1. Record Traffic
|
|
77
|
+
|
|
73
78
|
Start a recording proxy on port `3000` forwarding to your real API at `localhost:8080`.
|
|
74
79
|
|
|
75
80
|
```bash
|
|
76
|
-
|
|
81
|
+
# Run from source
|
|
82
|
+
node index.js record --target http://localhost:8080 --port 3000 --out traffic.jsonl
|
|
77
83
|
```
|
|
78
84
|
|
|
79
85
|
### 2. Auto-Generate from Swagger
|
|
80
|
-
|
|
86
|
+
|
|
87
|
+
Generate traffic without manual clicking. You can use command line flags or a YAML configuration file.
|
|
88
|
+
|
|
89
|
+
**Using Configuration File (Recommended):**
|
|
81
90
|
|
|
82
91
|
```bash
|
|
83
|
-
|
|
92
|
+
node index.js generate --config config.example.yaml
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Using Flags:**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
node index.js generate --file ./openapi.json --target http://localhost:3000
|
|
84
99
|
```
|
|
85
100
|
|
|
86
101
|
### 3. Replay & Verify
|
|
102
|
+
|
|
87
103
|
Replay recorded traffic against two environments and generate an HTML report.
|
|
88
104
|
|
|
89
105
|
```bash
|
|
90
|
-
|
|
106
|
+
node index.js replay \
|
|
91
107
|
--log traffic.jsonl \
|
|
92
108
|
--primary http://prod-api.com \
|
|
93
109
|
--secondary http://staging-api.com \
|
|
@@ -102,18 +118,23 @@ npx http-log-replay replay \
|
|
|
102
118
|
We provide optimized Docker images for both the UI and CLI.
|
|
103
119
|
|
|
104
120
|
### Prerequisites
|
|
121
|
+
|
|
105
122
|
- Docker & Docker Compose installed.
|
|
106
123
|
|
|
107
124
|
### Option A: Complete Environment (Recommended)
|
|
125
|
+
|
|
108
126
|
Use the included `docker-compose.yml` to run everything.
|
|
109
127
|
|
|
110
128
|
**Start the GUI:**
|
|
129
|
+
|
|
111
130
|
```bash
|
|
112
131
|
docker-compose up gui
|
|
113
132
|
```
|
|
133
|
+
|
|
114
134
|
> Access at [http://localhost:4200](http://localhost:4200). Data is persisted to your host folder.
|
|
115
135
|
|
|
116
136
|
**Run CLI Commands:**
|
|
137
|
+
|
|
117
138
|
```bash
|
|
118
139
|
docker-compose run cli record --target http://host.docker.internal:8080 ...
|
|
119
140
|
```
|
|
@@ -121,11 +142,13 @@ docker-compose run cli record --target http://host.docker.internal:8080 ...
|
|
|
121
142
|
### Option B: Manual Docker Run
|
|
122
143
|
|
|
123
144
|
**UI Image:**
|
|
145
|
+
|
|
124
146
|
```bash
|
|
125
147
|
docker run -p 4200:4200 -v $(pwd):/app traffic-mirror-gui
|
|
126
148
|
```
|
|
127
149
|
|
|
128
150
|
**CLI Image:**
|
|
151
|
+
|
|
129
152
|
```bash
|
|
130
153
|
docker run -v $(pwd):/app traffic-mirror-cli --help
|
|
131
154
|
```
|
|
@@ -137,6 +160,7 @@ docker run -v $(pwd):/app traffic-mirror-cli --help
|
|
|
137
160
|
We welcome contributions! Here is how to run the project locally for development.
|
|
138
161
|
|
|
139
162
|
### 1. Setup
|
|
163
|
+
|
|
140
164
|
```bash
|
|
141
165
|
git clone https://github.com/your-username/http-log-replay.git
|
|
142
166
|
cd http-log-replay
|
|
@@ -144,7 +168,9 @@ npm install
|
|
|
144
168
|
```
|
|
145
169
|
|
|
146
170
|
### 2. Build the UI
|
|
171
|
+
|
|
147
172
|
The UI is built with Angular. You must build it before running the app.
|
|
173
|
+
|
|
148
174
|
```bash
|
|
149
175
|
cd ui
|
|
150
176
|
npm install
|
|
@@ -153,12 +179,14 @@ cd ..
|
|
|
153
179
|
```
|
|
154
180
|
|
|
155
181
|
### 3. Run Locally (Dev Mode)
|
|
182
|
+
|
|
156
183
|
```bash
|
|
157
184
|
# Start the full app (Frontend + Backend)
|
|
158
185
|
node index.js ui --port 4200
|
|
159
186
|
```
|
|
160
187
|
|
|
161
188
|
### 4. Testing & Code Quality
|
|
189
|
+
|
|
162
190
|
We use **Jest** for testing and **ESLint** for code quality.
|
|
163
191
|
|
|
164
192
|
```bash
|
|
@@ -170,13 +198,35 @@ npm run lint
|
|
|
170
198
|
|
|
171
199
|
# Format Code
|
|
172
200
|
npm run format
|
|
201
|
+
|
|
202
|
+
# Production Start (PM2)
|
|
203
|
+
npm run start:pm2
|
|
173
204
|
```
|
|
174
205
|
|
|
206
|
+
### 5. Contribution Guidelines
|
|
207
|
+
|
|
208
|
+
We use **Semantic Release** to automate version management and package publishing. To ensure this works correctly, please follow the **Conventional Commits** specification for your commit messages.
|
|
209
|
+
|
|
210
|
+
**Commit Message Format:**
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
<type>(<scope>): <subject>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Common Types:**
|
|
217
|
+
|
|
218
|
+
- **`feat`**: A new feature (triggers a **MINOR** release).
|
|
219
|
+
- **`fix`**: A bug fix (triggers a **PATCH** release).
|
|
220
|
+
- **`docs`**: Documentation only changes (triggers a **PATCH** release).
|
|
221
|
+
- **`refactor`**: A code change that neither fixes a bug nor adds a feature (triggers a **PATCH** release).
|
|
222
|
+
- **`perf`**, **`test`**, **`ci`**, **`chore`**: Changes that do not trigger a release (unless configured otherwise).
|
|
223
|
+
|
|
175
224
|
---
|
|
176
225
|
|
|
177
226
|
## đŚ Maintenance
|
|
178
227
|
|
|
179
228
|
### Publishing to NPM
|
|
229
|
+
|
|
180
230
|
1. **Bump Version**: Update `version` in `package.json`.
|
|
181
231
|
2. **Build UI**: The `prepublishOnly` script will automatically build the Angular UI.
|
|
182
232
|
3. **Publish**:
|
|
@@ -185,6 +235,7 @@ npm run format
|
|
|
185
235
|
```
|
|
186
236
|
|
|
187
237
|
### File Structure
|
|
238
|
+
|
|
188
239
|
- `index.js`: CLI entry point.
|
|
189
240
|
- `recorder.js`: Proxy logic.
|
|
190
241
|
- `replayer.js`: Replay & Diff logic.
|
package/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
3
|
const { Command } = require('commander');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
4
6
|
const recorder = require('./recorder');
|
|
5
7
|
const replayAndDiff = require('./replayer');
|
|
6
8
|
const startServer = require('./server');
|
|
@@ -64,26 +66,34 @@ program
|
|
|
64
66
|
|
|
65
67
|
program
|
|
66
68
|
.command('generate')
|
|
67
|
-
.description('Auto-generate traffic from Swagger file')
|
|
68
|
-
.option('-
|
|
69
|
-
.option('-
|
|
70
|
-
.option('-x, --exclude <items>', 'Comma separated list of endpoints to exclude', (val) =>
|
|
69
|
+
.description('Auto-generate traffic from Swagger file using a configuration file')
|
|
70
|
+
.option('-c, --config <path>', 'Path to YAML configuration file', 'config.yaml')
|
|
71
|
+
.option('-m, --methods <items>', 'Comma separated list of HTTP methods', (val) =>
|
|
71
72
|
val.split(',')
|
|
72
|
-
)
|
|
73
|
-
.option('-s, --source <url>', 'Source Server URL', 'http://localhost:1338')
|
|
73
|
+
)
|
|
74
74
|
.action(async (options) => {
|
|
75
75
|
try {
|
|
76
76
|
console.log('đ Starting Traffic Generation...');
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
let config = {};
|
|
79
|
+
if (options.config && fs.existsSync(options.config)) {
|
|
80
|
+
console.log(`Loading configuration from ${options.config}...`);
|
|
81
|
+
const raw = fs.readFileSync(options.config, 'utf8');
|
|
82
|
+
config = yaml.load(raw);
|
|
83
|
+
|
|
84
|
+
// Override methods if provided via CLI
|
|
85
|
+
if (options.methods && options.methods.length > 0) {
|
|
86
|
+
config.methods = options.methods;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
throw new Error(`Configuration file not found at ${options.config}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
79
92
|
await generator.run(
|
|
80
|
-
|
|
81
|
-
options.file,
|
|
82
|
-
options.exclude || [],
|
|
93
|
+
config,
|
|
83
94
|
(log) => {
|
|
84
95
|
console.log(log.message);
|
|
85
|
-
}
|
|
86
|
-
options.source
|
|
96
|
+
}
|
|
87
97
|
);
|
|
88
98
|
|
|
89
99
|
console.log('â
Done.');
|
package/package.json
CHANGED
|
@@ -1,55 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "http-log-replay",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Traffic Mirror is a regression testing tool that records HTTP traffic from a live environment and replays it against two different environments (e.g., Stable vs. Canary) to detect differences in responses.",
|
|
5
|
-
"author": "Xhani Manolis Trungu",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"*.js",
|
|
12
|
-
"ui/dist"
|
|
13
|
-
],
|
|
14
|
-
"scripts": {
|
|
15
|
-
"test": "jest tests/",
|
|
16
|
-
"lint": "eslint .",
|
|
17
|
-
"format": "prettier --write .",
|
|
18
|
-
"start:pm2": "pm2 start ecosystem.config.js",
|
|
19
|
-
"prepublishOnly": "cd ui && npm install && npm run build"
|
|
20
|
-
},
|
|
21
|
-
"keywords": [
|
|
22
|
-
"http-log",
|
|
23
|
-
"http-log-replay",
|
|
24
|
-
"traffic-replay",
|
|
25
|
-
"regression-testing",
|
|
26
|
-
"api-regression",
|
|
27
|
-
"api-testing"
|
|
28
|
-
],
|
|
29
|
-
"license": "ISC",
|
|
30
|
-
"type": "commonjs",
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"axios": "^1.13.2",
|
|
33
|
-
"colors": "^1.4.0",
|
|
34
|
-
"commander": "^14.0.2",
|
|
35
|
-
"cors": "^2.8.5",
|
|
36
|
-
"deep-diff": "^1.0.2",
|
|
37
|
-
"diff": "^8.0.2",
|
|
38
|
-
"express": "^5.2.1",
|
|
39
|
-
"http-proxy": "^1.18.1",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "http-log-replay",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Traffic Mirror is a regression testing tool that records HTTP traffic from a live environment and replays it against two different environments (e.g., Stable vs. Canary) to detect differences in responses.",
|
|
5
|
+
"author": "Xhani Manolis Trungu",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"http-log-replay": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"*.js",
|
|
12
|
+
"ui/dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "jest tests/",
|
|
16
|
+
"lint": "eslint .",
|
|
17
|
+
"format": "prettier --write .",
|
|
18
|
+
"start:pm2": "pm2 start ecosystem.config.js",
|
|
19
|
+
"prepublishOnly": "cd ui && npm install && npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"http-log",
|
|
23
|
+
"http-log-replay",
|
|
24
|
+
"traffic-replay",
|
|
25
|
+
"regression-testing",
|
|
26
|
+
"api-regression",
|
|
27
|
+
"api-testing"
|
|
28
|
+
],
|
|
29
|
+
"license": "ISC",
|
|
30
|
+
"type": "commonjs",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"axios": "^1.13.2",
|
|
33
|
+
"colors": "^1.4.0",
|
|
34
|
+
"commander": "^14.0.2",
|
|
35
|
+
"cors": "^2.8.5",
|
|
36
|
+
"deep-diff": "^1.0.2",
|
|
37
|
+
"diff": "^8.0.2",
|
|
38
|
+
"express": "^5.2.1",
|
|
39
|
+
"http-proxy": "^1.18.1",
|
|
40
|
+
"js-yaml": "^4.1.1",
|
|
41
|
+
"open": "^11.0.0",
|
|
42
|
+
"readline": "^1.3.0",
|
|
43
|
+
"socket.io": "^4.8.1"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
47
|
+
"@semantic-release/git": "^10.0.1",
|
|
48
|
+
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
49
|
+
"eslint": "^9.39.2",
|
|
50
|
+
"eslint-config-prettier": "^10.1.8",
|
|
51
|
+
"jest": "^30.2.0",
|
|
52
|
+
"pm2": "^6.0.14",
|
|
53
|
+
"prettier": "^3.7.4",
|
|
54
|
+
"semantic-release": "^25.0.2"
|
|
55
|
+
},
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/xhani-manolis-trungu/traffic-mirror"
|
|
59
|
+
}
|
|
60
|
+
}
|
package/recorder.js
CHANGED
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
|
-
const http = require('http');
|
|
3
|
-
const httpProxy = require('http-proxy');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
|
|
6
|
-
class Recorder {
|
|
7
|
-
constructor() {
|
|
8
|
-
this.server = null;
|
|
9
|
-
this.proxy = httpProxy.createProxyServer({});
|
|
10
|
-
|
|
11
|
-
// 1. Handle Proxy Errors (Critical for preventing crashes)
|
|
12
|
-
this.proxy.on('error', (err, req, res) => {
|
|
13
|
-
console.error(`â Proxy Error [${req.url}]:`, err.message);
|
|
14
|
-
if (!res.headersSent) {
|
|
15
|
-
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
16
|
-
}
|
|
17
|
-
res.end(JSON.stringify({ error: 'Proxy Request Failed', details: err.message }));
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
start(target, port, outputFile, onLog = () => { }) {
|
|
22
|
-
if (this.server) throw new Error('Recorder already running');
|
|
23
|
-
|
|
24
|
-
console.log(`đ Recording to file: ${outputFile}`);
|
|
25
|
-
const stream = fs.createWriteStream(outputFile, { flags: 'a' });
|
|
26
|
-
|
|
27
|
-
// Handle file write errors
|
|
28
|
-
stream.on('error', (err) => {
|
|
29
|
-
console.error('â File Write Error:', err.message);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
this.server = http.createServer((req, res) => {
|
|
33
|
-
// 2. Capture Request Body safely
|
|
34
|
-
// We use a separate array but we DO NOT attach 'data' listeners directly
|
|
35
|
-
// unless we plan to buffer it for the proxy.
|
|
36
|
-
// For simplicity, let's rely on the response capture primarily.
|
|
37
|
-
|
|
38
|
-
const reqChunks = [];
|
|
39
|
-
// We tap into the stream without consuming it destructively if possible,
|
|
40
|
-
// but the safest way with http-proxy is often just to listen to the proxy events.
|
|
41
|
-
// HOWEVER, for your specific "Request Stealing" fix:
|
|
42
|
-
req.on('data', (chunk) => reqChunks.push(chunk));
|
|
43
|
-
|
|
44
|
-
// --- Response Capture Logic ---
|
|
45
|
-
const originalWrite = res.write;
|
|
46
|
-
const originalEnd = res.end;
|
|
47
|
-
const resChunks = [];
|
|
48
|
-
|
|
49
|
-
res.write = function (chunk, ...args) {
|
|
50
|
-
if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
51
|
-
return originalWrite.apply(res, [chunk, ...args]);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
res.end = function (chunk, ...args) {
|
|
55
|
-
if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
56
|
-
|
|
57
|
-
// WRAP IN TRY/CATCH to prevent silent crashes
|
|
58
|
-
try {
|
|
59
|
-
const reqBody = Buffer.concat(reqChunks).toString();
|
|
60
|
-
const resBody = Buffer.concat(resChunks).toString('utf8');
|
|
61
|
-
|
|
62
|
-
const logData = {
|
|
63
|
-
timestamp: new Date().toISOString(),
|
|
64
|
-
method: req.method,
|
|
65
|
-
url: req.url,
|
|
66
|
-
status: res.statusCode,
|
|
67
|
-
requestBody: reqBody || '',
|
|
68
|
-
responseBody: resBody || '',
|
|
69
|
-
// requestHeaders: req.headers, // Uncomment if needed (verbose)
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const jsonLine = JSON.stringify(logData) + '\n';
|
|
73
|
-
|
|
74
|
-
// 3. Write and Log
|
|
75
|
-
stream.write(jsonLine);
|
|
76
|
-
onLog(logData);
|
|
77
|
-
|
|
78
|
-
// Debug Log to prove it worked
|
|
79
|
-
process.stdout.write('.'); // Prints a dot for every logged request
|
|
80
|
-
} catch (e) {
|
|
81
|
-
console.error('\nâ Error generating log:', e.message);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return originalEnd.apply(res, [chunk, ...args]);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
// 4. Force Plain Text (Disable Gzip)
|
|
88
|
-
delete req.headers['accept-encoding'];
|
|
89
|
-
|
|
90
|
-
// 5. Forward to Target
|
|
91
|
-
// We must set 'buffer' to the request stream if we consumed it,
|
|
92
|
-
// but since we are just tapping 'data' without pausing, http-proxy might miss it.
|
|
93
|
-
// A simple workaround for node streams in this context:
|
|
94
|
-
this.proxy.web(req, res, { target });
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
this.server.listen(port, () => {
|
|
98
|
-
console.log(`\nâşď¸ Recorder listening on port ${port}`);
|
|
99
|
-
console.log(`âĄď¸ Forwarding to ${target}`);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
return `Recorder started`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
stop() {
|
|
106
|
-
if (this.server) {
|
|
107
|
-
this.server.close();
|
|
108
|
-
this.server = null;
|
|
109
|
-
return 'Recorder stopped';
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
module.exports = new Recorder();
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const httpProxy = require('http-proxy');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
class Recorder {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.server = null;
|
|
9
|
+
this.proxy = httpProxy.createProxyServer({});
|
|
10
|
+
|
|
11
|
+
// 1. Handle Proxy Errors (Critical for preventing crashes)
|
|
12
|
+
this.proxy.on('error', (err, req, res) => {
|
|
13
|
+
console.error(`â Proxy Error [${req.url}]:`, err.message);
|
|
14
|
+
if (!res.headersSent) {
|
|
15
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
16
|
+
}
|
|
17
|
+
res.end(JSON.stringify({ error: 'Proxy Request Failed', details: err.message }));
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
start(target, port, outputFile, onLog = () => { }) {
|
|
22
|
+
if (this.server) throw new Error('Recorder already running');
|
|
23
|
+
|
|
24
|
+
console.log(`đ Recording to file: ${outputFile}`);
|
|
25
|
+
const stream = fs.createWriteStream(outputFile, { flags: 'a' });
|
|
26
|
+
|
|
27
|
+
// Handle file write errors
|
|
28
|
+
stream.on('error', (err) => {
|
|
29
|
+
console.error('â File Write Error:', err.message);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
this.server = http.createServer((req, res) => {
|
|
33
|
+
// 2. Capture Request Body safely
|
|
34
|
+
// We use a separate array but we DO NOT attach 'data' listeners directly
|
|
35
|
+
// unless we plan to buffer it for the proxy.
|
|
36
|
+
// For simplicity, let's rely on the response capture primarily.
|
|
37
|
+
|
|
38
|
+
const reqChunks = [];
|
|
39
|
+
// We tap into the stream without consuming it destructively if possible,
|
|
40
|
+
// but the safest way with http-proxy is often just to listen to the proxy events.
|
|
41
|
+
// HOWEVER, for your specific "Request Stealing" fix:
|
|
42
|
+
req.on('data', (chunk) => reqChunks.push(chunk));
|
|
43
|
+
|
|
44
|
+
// --- Response Capture Logic ---
|
|
45
|
+
const originalWrite = res.write;
|
|
46
|
+
const originalEnd = res.end;
|
|
47
|
+
const resChunks = [];
|
|
48
|
+
|
|
49
|
+
res.write = function (chunk, ...args) {
|
|
50
|
+
if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
51
|
+
return originalWrite.apply(res, [chunk, ...args]);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
res.end = function (chunk, ...args) {
|
|
55
|
+
if (chunk) resChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
56
|
+
|
|
57
|
+
// WRAP IN TRY/CATCH to prevent silent crashes
|
|
58
|
+
try {
|
|
59
|
+
const reqBody = Buffer.concat(reqChunks).toString();
|
|
60
|
+
const resBody = Buffer.concat(resChunks).toString('utf8');
|
|
61
|
+
|
|
62
|
+
const logData = {
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
method: req.method,
|
|
65
|
+
url: req.url,
|
|
66
|
+
status: res.statusCode,
|
|
67
|
+
requestBody: reqBody || '',
|
|
68
|
+
responseBody: resBody || '',
|
|
69
|
+
// requestHeaders: req.headers, // Uncomment if needed (verbose)
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const jsonLine = JSON.stringify(logData) + '\n';
|
|
73
|
+
|
|
74
|
+
// 3. Write and Log
|
|
75
|
+
stream.write(jsonLine);
|
|
76
|
+
onLog(logData);
|
|
77
|
+
|
|
78
|
+
// Debug Log to prove it worked
|
|
79
|
+
process.stdout.write('.'); // Prints a dot for every logged request
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error('\nâ Error generating log:', e.message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return originalEnd.apply(res, [chunk, ...args]);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// 4. Force Plain Text (Disable Gzip)
|
|
88
|
+
delete req.headers['accept-encoding'];
|
|
89
|
+
|
|
90
|
+
// 5. Forward to Target
|
|
91
|
+
// We must set 'buffer' to the request stream if we consumed it,
|
|
92
|
+
// but since we are just tapping 'data' without pausing, http-proxy might miss it.
|
|
93
|
+
// A simple workaround for node streams in this context:
|
|
94
|
+
this.proxy.web(req, res, { target });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.server.listen(port, () => {
|
|
98
|
+
console.log(`\nâşď¸ Recorder listening on port ${port}`);
|
|
99
|
+
console.log(`âĄď¸ Forwarding to ${target}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return `Recorder started`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stop() {
|
|
106
|
+
if (this.server) {
|
|
107
|
+
this.server.close();
|
|
108
|
+
this.server = null;
|
|
109
|
+
return 'Recorder stopped';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = new Recorder();
|