wreq-js 0.2.0 → 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 +110 -182
- package/dist/test/helpers/local-test-server.d.ts +7 -0
- package/dist/test/helpers/local-test-server.d.ts.map +1 -0
- package/dist/test/helpers/local-test-server.js +302 -0
- package/dist/test/helpers/local-test-server.js.map +1 -0
- package/dist/test/http.spec.js +110 -12
- package/dist/test/http.spec.js.map +1 -1
- package/dist/test/run-with-local-server.d.ts +2 -0
- package/dist/test/run-with-local-server.d.ts.map +1 -0
- package/dist/test/run-with-local-server.js +61 -0
- package/dist/test/run-with-local-server.js.map +1 -0
- package/dist/test/websocket.spec.js +13 -16
- package/dist/test/websocket.spec.js.map +1 -1
- package/dist/types.d.ts +271 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -1
- package/dist/wreq-js.d.ts +79 -54
- package/dist/wreq-js.d.ts.map +1 -1
- package/dist/wreq-js.js +559 -63
- package/dist/wreq-js.js.map +1 -1
- package/package.json +2 -2
- package/rust/wreq-js.darwin-arm64.node +0 -0
- package/rust/wreq-js.darwin-x64.node +0 -0
- package/rust/wreq-js.linux-arm64-gnu.node +0 -0
- package/rust/wreq-js.linux-x64-gnu.node +0 -0
- package/rust/wreq-js.win32-x64-msvc.node +0 -0
package/README.md
CHANGED
|
@@ -1,260 +1,188 @@
|
|
|
1
1
|
# wreq-js
|
|
2
2
|
|
|
3
|
-
High-performance
|
|
3
|
+
High-performance HTTP client for Node.JS with real-browser TLS and HTTP/2 fingerprints, powered by Rust.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- ⚡️ A modern, actively maintained alternative to outdated browser-impersonating clients and legacy wrappers.
|
|
6
|
+
- When it comes to web scraping and automation, keeping up with the latest developments is NOT optional. Detection systems like Akamai and Cloudflare change every day using machine learning, old fingerprints are quickly detected.
|
|
7
|
+
- `wreq-js` builds upon the Rust-based [`wreq`](https://github.com/0x676e67/wreq) engine to deliver drop-in Node.js bindings that feel like `fetch()` but behave like a real browser.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
- Native performance (no process spawning)
|
|
10
|
-
- TLS fingerprinting (JA3/JA4) aligned with real browsers
|
|
11
|
-
- HTTP/2 fingerprinting: SETTINGS, PRIORITY, and header ordering
|
|
12
|
-
- Multiple browser profiles (Chrome, Firefox, Safari, Edge, Opera, OkHttp)
|
|
13
|
-
- WebSocket support
|
|
14
|
-
- TypeScript definitions included
|
|
15
|
-
|
|
16
|
-
## How It Works
|
|
17
|
-
|
|
18
|
-
The library provides Node.js bindings over [wreq](https://github.com/0x676e67/wreq), a Rust HTTP client that uses BoringSSL to replicate browser network behavior at the TLS and HTTP/2 layers.
|
|
9
|
+
> This is my maintained fork of [will-work-for-meal/node-wreq](https://github.com/will-work-for-meal/node-wreq), originally named `node-wreq`, with ongoing updates and dependency refreshes for compatibility and speed.
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Traditional HTTP clients (axios, fetch, curl) have differences in:
|
|
23
|
-
- **TLS handshake signatures** — Different cipher suites and extensions
|
|
24
|
-
- **HTTP/2 frame ordering** — Different SETTINGS and PRIORITY patterns
|
|
25
|
-
- **Header ordering** — Different sequence and values
|
|
26
|
-
|
|
27
|
-
This library reproduces browser network behavior with high fidelity.
|
|
28
|
-
|
|
29
|
-
### Browser Profiles and wreq-util
|
|
11
|
+
## Features
|
|
30
12
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
13
|
+
- **Native performance** — no process spawning or browser overhead
|
|
14
|
+
- **Real browser TLS fingerprints** (JA3/JA4)
|
|
15
|
+
- **HTTP/2 impersonation** — replicates SETTINGS, PRIORITY, and header ordering
|
|
16
|
+
- **Multiple browser profiles** — Chrome, Firefox, Safari, Edge, Opera, OkHttp
|
|
17
|
+
- **WebSocket support** with browser fingerprint consistency
|
|
18
|
+
- **Prebuilt native binaries** for macOS, Linux, and Windows
|
|
19
|
+
- **TypeScript-ready** with generated definitions
|
|
34
20
|
|
|
35
21
|
## Installation
|
|
36
22
|
|
|
37
23
|
```bash
|
|
38
|
-
# From GitHub (this fork)
|
|
39
|
-
# Latest master branch
|
|
40
24
|
npm install wreq-js
|
|
25
|
+
# or
|
|
41
26
|
yarn add wreq-js
|
|
42
27
|
pnpm add wreq-js
|
|
43
28
|
bun add wreq-js
|
|
44
|
-
|
|
45
|
-
# From npm registry (original repo as node-wreq)
|
|
46
|
-
npm install node-wreq
|
|
47
|
-
yarn add node-wreq
|
|
48
|
-
pnpm add node-wreq
|
|
49
|
-
bun add node-wreq
|
|
50
29
|
```
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
- macOS (Intel
|
|
54
|
-
- Linux (x64
|
|
31
|
+
Prebuilt binaries are provided for:
|
|
32
|
+
- macOS (Intel & Apple Silicon)
|
|
33
|
+
- Linux (x64 & ARM64)
|
|
55
34
|
- Windows (x64)
|
|
56
35
|
|
|
57
|
-
|
|
36
|
+
> ⚠️ If a prebuilt binary for your platform or commit is unavailable, the package will build from source.
|
|
37
|
+
> Make sure a Rust toolchain and required build dependencies are installed.
|
|
38
|
+
|
|
39
|
+
## Why It Exists
|
|
40
|
+
|
|
41
|
+
HTTP clients like `axios`, `fetch`, `got`, `curl` do not behave like browsers on the network layer.
|
|
42
|
+
They differ in:
|
|
43
|
+
|
|
44
|
+
- **TLS handshake** - unique cipher suite order and extension sets
|
|
45
|
+
- **HTTP/2 frames** - different SETTINGS and PRIORITY sequences
|
|
46
|
+
- **Header ordering** - deterministic but non-browser-compliant
|
|
58
47
|
|
|
59
|
-
|
|
48
|
+
These subtle differences are enough for modern detection systems to identify automation.
|
|
49
|
+
`wreq-js` reproduces browser networking behavior using the `wreq` Rust engine underneath.
|
|
50
|
+
Your job is to write scripts, ours is to make them undetectable, yet effortless.
|
|
60
51
|
|
|
61
|
-
|
|
52
|
+
## Architecture Overview
|
|
53
|
+
|
|
54
|
+
`wreq-js` provides Node.js bindings over [`wreq`](https://github.com/0x676e67/wreq), a Rust HTTP client built on **BoringSSL** to emulate browser TLS and HTTP/2 stacks.
|
|
55
|
+
Browser profiles are defined in the upstream [`wreq-util`](https://github.com/0x676e67/wreq-util) project and automatically synchronized here for faster updates.
|
|
56
|
+
|
|
57
|
+
To query supported profiles:
|
|
62
58
|
|
|
63
59
|
```typescript
|
|
64
|
-
import {
|
|
60
|
+
import { getProfiles } from 'wreq-js';
|
|
61
|
+
console.log(getProfiles());
|
|
62
|
+
// ['chrome_142', 'firefox_139', 'edge_120', 'safari_18', ...]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { fetch } from 'wreq-js';
|
|
65
69
|
|
|
66
|
-
const response = await
|
|
67
|
-
|
|
68
|
-
browser: 'chrome_137',
|
|
70
|
+
const response = await fetch('https://example.com/api', {
|
|
71
|
+
browser: 'chrome_142',
|
|
69
72
|
});
|
|
70
73
|
|
|
71
|
-
console.log(response.
|
|
72
|
-
console.log(response.body); // Response body
|
|
73
|
-
console.log(response.headers); // Response headers
|
|
74
|
-
console.log(response.cookies); // Cookies
|
|
74
|
+
console.log(await response.json());
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
That’s it, you now have full browser impersonation, drop-in compatibility with the `fetch()` API.
|
|
78
|
+
|
|
79
|
+
## Advanced Usage
|
|
80
|
+
|
|
81
|
+
### Custom Headers
|
|
78
82
|
|
|
79
83
|
```typescript
|
|
80
|
-
import {
|
|
84
|
+
import { fetch, Headers } from 'wreq-js';
|
|
81
85
|
|
|
82
|
-
const response = await
|
|
83
|
-
url: 'https://api.example.com/data',
|
|
86
|
+
const response = await fetch('https://api.example.com/data', {
|
|
84
87
|
browser: 'firefox_139',
|
|
85
|
-
headers: {
|
|
86
|
-
|
|
88
|
+
headers: new Headers({
|
|
89
|
+
Authorization: 'Bearer token123',
|
|
87
90
|
'Custom-Header': 'value',
|
|
88
|
-
},
|
|
91
|
+
}),
|
|
89
92
|
});
|
|
90
93
|
```
|
|
91
94
|
|
|
92
95
|
### POST Request
|
|
93
96
|
|
|
94
97
|
```typescript
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'
|
|
99
|
-
JSON.stringify({ foo: 'bar' }),
|
|
100
|
-
|
|
101
|
-
browser: 'chrome_137',
|
|
102
|
-
headers: {
|
|
103
|
-
'Content-Type': 'application/json',
|
|
104
|
-
},
|
|
105
|
-
}
|
|
106
|
-
);
|
|
98
|
+
const res = await fetch('https://api.example.com/submit', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
browser: 'chrome_142',
|
|
101
|
+
headers: { 'Content-Type': 'application/json' },
|
|
102
|
+
body: JSON.stringify({ foo: 'bar' }),
|
|
103
|
+
});
|
|
107
104
|
```
|
|
108
105
|
|
|
109
|
-
|
|
106
|
+
## Session & Cookie Isolation
|
|
107
|
+
|
|
108
|
+
Each `fetch()` call runs in **ephemeral mode** so that TLS caches, cookies, and session data never leak across requests.
|
|
109
|
+
To persist state, use `createSession()` or `withSession()`:
|
|
110
110
|
|
|
111
111
|
```typescript
|
|
112
|
-
import {
|
|
112
|
+
import { createSession, withSession } from 'wreq-js';
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
const session = await createSession({ browser: 'chrome_142' });
|
|
115
|
+
await session.fetch('https://example.com/login', { method: 'POST', body: '...' });
|
|
116
|
+
await session.fetch('https://example.com/dashboard');
|
|
117
|
+
await session.close();
|
|
116
118
|
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
'https://
|
|
120
|
-
|
|
121
|
-
);
|
|
122
|
-
```
|
|
119
|
+
// Auto-disposing helper
|
|
120
|
+
await withSession(async (s) => {
|
|
121
|
+
await s.fetch('https://example.com/a');
|
|
122
|
+
await s.fetch('https://example.com/b');
|
|
123
|
+
});
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
For finer control:
|
|
125
126
|
|
|
126
127
|
```typescript
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
url: 'https://example.com',
|
|
131
|
-
browser: 'chrome_137',
|
|
132
|
-
// proxy: 'http://proxy.example.com:8080',
|
|
133
|
-
// proxy: 'http://username:password@proxy.example.com:8080',
|
|
134
|
-
// proxy: 'socks5://proxy.example.com:1080',
|
|
128
|
+
await fetch('https://example.com', {
|
|
129
|
+
sessionId: 'user-42',
|
|
130
|
+
cookieMode: 'session',
|
|
135
131
|
});
|
|
136
132
|
```
|
|
137
133
|
|
|
138
|
-
|
|
134
|
+
## WebSocket Example
|
|
139
135
|
|
|
140
136
|
```typescript
|
|
141
137
|
import { websocket } from 'wreq-js';
|
|
142
138
|
|
|
143
139
|
const ws = await websocket({
|
|
144
140
|
url: 'wss://echo.websocket.org',
|
|
145
|
-
browser: '
|
|
146
|
-
onMessage: (data) =>
|
|
147
|
-
console.log('Received:', data);
|
|
148
|
-
},
|
|
149
|
-
onClose: () => {
|
|
150
|
-
console.log('Connection closed');
|
|
151
|
-
},
|
|
152
|
-
onError: (error) => {
|
|
153
|
-
console.error('Error:', error);
|
|
154
|
-
},
|
|
141
|
+
browser: 'chrome_142',
|
|
142
|
+
onMessage: (data) => console.log('Received:', data),
|
|
155
143
|
});
|
|
156
144
|
|
|
157
|
-
// Send text message
|
|
158
145
|
await ws.send('Hello!');
|
|
159
|
-
|
|
160
|
-
// Send binary message
|
|
161
|
-
await ws.send(Buffer.from([1, 2, 3]));
|
|
162
|
-
|
|
163
|
-
// Close connection
|
|
164
146
|
await ws.close();
|
|
165
147
|
```
|
|
166
148
|
|
|
167
149
|
## API Reference
|
|
168
150
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
Main function for making HTTP requests with browser impersonation.
|
|
172
|
-
|
|
173
|
-
**Options:**
|
|
174
|
-
<a name="requestoptions"></a>
|
|
151
|
+
The API is aiming to be `fetch`-compatible, with a few `wreq`-specific extensions.
|
|
152
|
+
See inline TypeScript definitions for complete typings.
|
|
175
153
|
|
|
176
154
|
```typescript
|
|
177
|
-
interface
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
155
|
+
interface RequestInit {
|
|
156
|
+
method?: string;
|
|
157
|
+
headers?: HeadersInit;
|
|
158
|
+
body?: BodyInit | null;
|
|
159
|
+
signal?: AbortSignal | null;
|
|
160
|
+
redirect?: 'follow';
|
|
161
|
+
browser?: BrowserProfile;
|
|
162
|
+
proxy?: string;
|
|
163
|
+
timeout?: number;
|
|
164
|
+
cookieMode?: 'session' | 'ephemeral';
|
|
165
|
+
session?: Session;
|
|
166
|
+
sessionId?: string;
|
|
185
167
|
}
|
|
186
168
|
```
|
|
187
169
|
|
|
188
|
-
**Response:**
|
|
189
|
-
<a name="response"></a>
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
interface Response {
|
|
193
|
-
status: number;
|
|
194
|
-
headers: Record<string, string>;
|
|
195
|
-
body: string;
|
|
196
|
-
cookies: Record<string, string>;
|
|
197
|
-
url: string; // Final URL after redirects
|
|
198
|
-
}
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
### `get(url: string, options?): Promise<`[`Response`](#response)`>`
|
|
202
|
-
|
|
203
|
-
### `post(url: string, body?: string, options?): Promise<`[`Response`](#response)`>`
|
|
204
|
-
|
|
205
|
-
### `websocket(options:` [`WebSocketOptions`](#websocketoptions)`): Promise<WebSocket>`
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
**Options:**
|
|
209
|
-
<a name="websocketoptions"></a>
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
interface WebSocketOptions {
|
|
213
|
-
url: string; // Required: WebSocket URL (ws:// or wss://)
|
|
214
|
-
browser?: BrowserProfile; // Default: 'chrome_137'
|
|
215
|
-
headers?: Record<string, string>;
|
|
216
|
-
proxy?: string; // HTTP/HTTPS/SOCKS5 proxy URL
|
|
217
|
-
onMessage: (data: string | Buffer) => void; // Required: Message callback
|
|
218
|
-
onClose?: () => void; // Optional: Close callback
|
|
219
|
-
onError?: (error: string) => void; // Optional: Error callback
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**WebSocket Methods:**
|
|
224
|
-
|
|
225
|
-
```typescript
|
|
226
|
-
class WebSocket {
|
|
227
|
-
send(data: string | Buffer): Promise<void>;
|
|
228
|
-
close(): Promise<void>;
|
|
229
|
-
}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### `getProfiles():` [`BrowserProfile[]`](#browser-profiles)
|
|
233
|
-
|
|
234
|
-
Get list of available browser profiles.
|
|
235
|
-
|
|
236
|
-
```typescript
|
|
237
|
-
import { getProfiles } from 'wreq-js';
|
|
238
|
-
|
|
239
|
-
const profiles = getProfiles();
|
|
240
|
-
|
|
241
|
-
console.log(profiles);
|
|
242
|
-
// ['chrome_100', 'chrome_101', ..., 'chrome_137', 'edge_101', ..., 'safari_18', ...]
|
|
243
|
-
```
|
|
244
|
-
|
|
245
170
|
## Documentation
|
|
246
171
|
|
|
247
|
-
- **[Architecture Guide](docs/ARCHITECTURE.md)**
|
|
248
|
-
- **[Build Instructions](docs/BUILD.md)**
|
|
249
|
-
- **[Publishing Guide](docs/PUBLISHING.md)**
|
|
172
|
+
- **[Architecture Guide](docs/ARCHITECTURE.md)** - How fingerprinting and impersonation work
|
|
173
|
+
- **[Build Instructions](docs/BUILD.md)** - Build from source
|
|
174
|
+
- **[Publishing Guide](docs/PUBLISHING.md)** - Releasing new versions
|
|
250
175
|
|
|
251
176
|
## Contributing
|
|
252
177
|
|
|
253
178
|
Please read the [Contributing Guide](CONTRIBUTING.md).
|
|
254
179
|
|
|
180
|
+
## Origins
|
|
181
|
+
This project began as a fork of [will-work-for-meal/node-wreq](https://github.com/will-work-for-meal/node-wreq) but has since evolved into an independent implementation with extensive rewrites, new APIs, and active maintenance. It is not affiliated with the original project.
|
|
182
|
+
|
|
255
183
|
## Acknowledgments
|
|
256
184
|
|
|
257
|
-
- [wreq](https://github.com/0x676e67/wreq)
|
|
258
|
-
- [wreq-util](https://github.com/0x676e67/wreq-util)
|
|
259
|
-
- [Neon](https://neon-bindings.com/)
|
|
260
|
-
-
|
|
185
|
+
- [wreq](https://github.com/0x676e67/wreq) - Rust HTTP client with browser impersonation
|
|
186
|
+
- [wreq-util](https://github.com/0x676e67/wreq-util) - Source of up-to-date browser profiles
|
|
187
|
+
- [Neon](https://neon-bindings.com/) - Rust ↔ Node.js bindings
|
|
188
|
+
- [will-work-for-meal/node-wreq](https://github.com/will-work-for-meal/node-wreq) - Original Node.js wrapper foundation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-test-server.d.ts","sourceRoot":"","sources":["../../../src/test/helpers/local-test-server.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,eAAe,CAAC,CAuMrE"}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startLocalTestServer = startLocalTestServer;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_http_1 = require("node:http");
|
|
6
|
+
const promises_1 = require("node:timers/promises");
|
|
7
|
+
const WS_MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
8
|
+
async function startLocalTestServer() {
|
|
9
|
+
let baseUrl = "http://127.0.0.1";
|
|
10
|
+
const sockets = new Set();
|
|
11
|
+
const server = (0, node_http_1.createServer)(async (req, res) => {
|
|
12
|
+
try {
|
|
13
|
+
await routeHttpRequest(req, res, baseUrl);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error("Local test server request error:", error);
|
|
17
|
+
if (!res.headersSent) {
|
|
18
|
+
res.statusCode = 500;
|
|
19
|
+
res.setHeader("Content-Type", "application/json");
|
|
20
|
+
}
|
|
21
|
+
res.end(JSON.stringify({ error: "internal server error" }));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
server.on("connection", (socket) => {
|
|
25
|
+
sockets.add(socket);
|
|
26
|
+
socket.on("close", () => sockets.delete(socket));
|
|
27
|
+
});
|
|
28
|
+
server.on("upgrade", (req, socket, head) => {
|
|
29
|
+
handleWebSocketUpgrade(req, socket, head);
|
|
30
|
+
});
|
|
31
|
+
await new Promise((resolve, reject) => {
|
|
32
|
+
const onError = (error) => {
|
|
33
|
+
server.off("listening", onListening);
|
|
34
|
+
reject(error);
|
|
35
|
+
};
|
|
36
|
+
const onListening = () => {
|
|
37
|
+
server.off("error", onError);
|
|
38
|
+
resolve();
|
|
39
|
+
};
|
|
40
|
+
server.once("error", onError);
|
|
41
|
+
server.once("listening", onListening);
|
|
42
|
+
server.listen(0, "127.0.0.1");
|
|
43
|
+
});
|
|
44
|
+
const address = server.address();
|
|
45
|
+
if (!address) {
|
|
46
|
+
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
47
|
+
throw new Error("Unable to determine local test server address");
|
|
48
|
+
}
|
|
49
|
+
baseUrl = `http://127.0.0.1:${address.port}`;
|
|
50
|
+
const wsUrl = `ws://127.0.0.1:${address.port}/ws`;
|
|
51
|
+
const close = async () => {
|
|
52
|
+
for (const socket of sockets) {
|
|
53
|
+
socket.destroy();
|
|
54
|
+
}
|
|
55
|
+
await new Promise((resolve, reject) => {
|
|
56
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
httpBaseUrl: baseUrl,
|
|
61
|
+
wsUrl,
|
|
62
|
+
close,
|
|
63
|
+
};
|
|
64
|
+
async function routeHttpRequest(req, res, resolvedBase) {
|
|
65
|
+
const url = new URL(req.url ?? "/", resolvedBase);
|
|
66
|
+
const path = url.pathname;
|
|
67
|
+
if (path === "/get") {
|
|
68
|
+
return json(res, createEchoPayload(req, url));
|
|
69
|
+
}
|
|
70
|
+
if (path === "/json") {
|
|
71
|
+
return json(res, {
|
|
72
|
+
message: "local test server",
|
|
73
|
+
status: "ok",
|
|
74
|
+
ts: Date.now(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (path === "/user-agent") {
|
|
78
|
+
return json(res, { "user-agent": req.headers["user-agent"] ?? "" });
|
|
79
|
+
}
|
|
80
|
+
if (path === "/cookies") {
|
|
81
|
+
return json(res, { cookies: parseCookies(req.headers.cookie) });
|
|
82
|
+
}
|
|
83
|
+
if (path.startsWith("/cookies/set")) {
|
|
84
|
+
const cookiesToSet = Array.from(url.searchParams.entries()).map(([key, value]) => `${key}=${value}; Path=/`);
|
|
85
|
+
const existingCookies = parseCookies(req.headers.cookie);
|
|
86
|
+
const newCookies = Object.fromEntries(url.searchParams.entries());
|
|
87
|
+
if (cookiesToSet.length > 0) {
|
|
88
|
+
res.setHeader("Set-Cookie", cookiesToSet);
|
|
89
|
+
}
|
|
90
|
+
return json(res, { cookies: { ...existingCookies, ...newCookies } });
|
|
91
|
+
}
|
|
92
|
+
const delayMatch = path.match(/^\/delay\/(\d+)/);
|
|
93
|
+
if (delayMatch) {
|
|
94
|
+
const seconds = Number(delayMatch[1]);
|
|
95
|
+
await (0, promises_1.setTimeout)(seconds * 1000);
|
|
96
|
+
return json(res, { delayed: seconds, ...createEchoPayload(req, url) });
|
|
97
|
+
}
|
|
98
|
+
res.statusCode = 404;
|
|
99
|
+
json(res, { error: "not found", path });
|
|
100
|
+
}
|
|
101
|
+
function createEchoPayload(req, url) {
|
|
102
|
+
const args = Object.fromEntries(url.searchParams.entries());
|
|
103
|
+
return {
|
|
104
|
+
args,
|
|
105
|
+
headers: canonicalizeHeaders(req),
|
|
106
|
+
method: req.method ?? "GET",
|
|
107
|
+
origin: req.socket.remoteAddress ?? "127.0.0.1",
|
|
108
|
+
url: url.toString(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function canonicalizeHeaders(req) {
|
|
112
|
+
const headers = {};
|
|
113
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
114
|
+
if (typeof value === "undefined")
|
|
115
|
+
continue;
|
|
116
|
+
const canonicalName = name
|
|
117
|
+
.split("-")
|
|
118
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
119
|
+
.join("-");
|
|
120
|
+
headers[canonicalName] = Array.isArray(value) ? value.join(", ") : value;
|
|
121
|
+
}
|
|
122
|
+
return headers;
|
|
123
|
+
}
|
|
124
|
+
function parseCookies(cookieHeader) {
|
|
125
|
+
if (!cookieHeader) {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
return cookieHeader.split(";").reduce((acc, cookie) => {
|
|
129
|
+
const [key, ...rest] = cookie.trim().split("=");
|
|
130
|
+
if (!key) {
|
|
131
|
+
return acc;
|
|
132
|
+
}
|
|
133
|
+
acc[key] = rest.join("=");
|
|
134
|
+
return acc;
|
|
135
|
+
}, {});
|
|
136
|
+
}
|
|
137
|
+
function json(res, body) {
|
|
138
|
+
if (!res.hasHeader("Content-Type")) {
|
|
139
|
+
res.setHeader("Content-Type", "application/json");
|
|
140
|
+
}
|
|
141
|
+
res.end(JSON.stringify(body));
|
|
142
|
+
}
|
|
143
|
+
function handleWebSocketUpgrade(req, socket, head) {
|
|
144
|
+
try {
|
|
145
|
+
const url = new URL(req.url ?? "/", baseUrl);
|
|
146
|
+
if (url.pathname !== "/ws") {
|
|
147
|
+
socket.write("HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n");
|
|
148
|
+
socket.destroy();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const secKey = req.headers["sec-websocket-key"];
|
|
152
|
+
if (!secKey || Array.isArray(secKey)) {
|
|
153
|
+
socket.write("HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n");
|
|
154
|
+
socket.destroy();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const acceptKey = (0, node_crypto_1.createHash)("sha1")
|
|
158
|
+
.update(secKey + WS_MAGIC_STRING)
|
|
159
|
+
.digest("base64");
|
|
160
|
+
const responseHeaders = [
|
|
161
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
162
|
+
"Upgrade: websocket",
|
|
163
|
+
"Connection: Upgrade",
|
|
164
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
165
|
+
];
|
|
166
|
+
socket.write(`${responseHeaders.join("\r\n")}\r\n\r\n`);
|
|
167
|
+
if (head.length > 0) {
|
|
168
|
+
socket.unshift(head);
|
|
169
|
+
}
|
|
170
|
+
setupEchoWebSocket(socket);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error("Local test server WebSocket upgrade error:", error);
|
|
174
|
+
socket.destroy();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function setupEchoWebSocket(socket) {
|
|
179
|
+
let buffer = Buffer.alloc(0);
|
|
180
|
+
let closed = false;
|
|
181
|
+
socket.on("data", (chunk) => {
|
|
182
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
183
|
+
parseFrames();
|
|
184
|
+
});
|
|
185
|
+
socket.on("close", () => {
|
|
186
|
+
closed = true;
|
|
187
|
+
});
|
|
188
|
+
socket.on("error", () => {
|
|
189
|
+
socket.destroy();
|
|
190
|
+
});
|
|
191
|
+
function parseFrames() {
|
|
192
|
+
while (buffer.length >= 2) {
|
|
193
|
+
const firstByte = buffer[0];
|
|
194
|
+
const secondByte = buffer[1];
|
|
195
|
+
const opcode = firstByte & 0x0f;
|
|
196
|
+
const isMasked = Boolean(secondByte & 0x80);
|
|
197
|
+
let offset = 2;
|
|
198
|
+
let payloadLength = secondByte & 0x7f;
|
|
199
|
+
if (payloadLength === 126) {
|
|
200
|
+
if (buffer.length < offset + 2)
|
|
201
|
+
return;
|
|
202
|
+
payloadLength = buffer.readUInt16BE(offset);
|
|
203
|
+
offset += 2;
|
|
204
|
+
}
|
|
205
|
+
else if (payloadLength === 127) {
|
|
206
|
+
if (buffer.length < offset + 8)
|
|
207
|
+
return;
|
|
208
|
+
const bigLength = buffer.readBigUInt64BE(offset);
|
|
209
|
+
payloadLength = Number(bigLength);
|
|
210
|
+
offset += 8;
|
|
211
|
+
}
|
|
212
|
+
const maskEnd = offset + (isMasked ? 4 : 0);
|
|
213
|
+
if (buffer.length < maskEnd)
|
|
214
|
+
return;
|
|
215
|
+
const maskingKey = isMasked ? buffer.subarray(offset, maskEnd) : undefined;
|
|
216
|
+
offset = maskEnd;
|
|
217
|
+
const frameEnd = offset + payloadLength;
|
|
218
|
+
if (buffer.length < frameEnd)
|
|
219
|
+
return;
|
|
220
|
+
const payload = buffer.subarray(offset, frameEnd);
|
|
221
|
+
buffer = buffer.subarray(frameEnd);
|
|
222
|
+
const data = isMasked && maskingKey ? unmask(payload, maskingKey) : payload;
|
|
223
|
+
handleFrame(opcode, data);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function handleFrame(opcode, data) {
|
|
227
|
+
if (closed) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
switch (opcode) {
|
|
231
|
+
case 0x1: {
|
|
232
|
+
// Text frame: echo payload back
|
|
233
|
+
sendFrame(0x1, data);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case 0x2: {
|
|
237
|
+
// Binary frame: echo back
|
|
238
|
+
sendFrame(0x2, data);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
case 0x8: {
|
|
242
|
+
// Close frame
|
|
243
|
+
sendFrame(0x8, data);
|
|
244
|
+
closed = true;
|
|
245
|
+
socket.end();
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 0x9: {
|
|
249
|
+
// Ping
|
|
250
|
+
sendFrame(0xa, data);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case 0xa: {
|
|
254
|
+
// Pong - ignore
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
default: {
|
|
258
|
+
// Unsupported opcode: close connection
|
|
259
|
+
sendFrame(0x8, Buffer.alloc(0));
|
|
260
|
+
closed = true;
|
|
261
|
+
socket.end();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function sendFrame(opcode, data) {
|
|
266
|
+
const payloadLength = data.length;
|
|
267
|
+
let headerLength = 2;
|
|
268
|
+
if (payloadLength >= 126 && payloadLength < 65536) {
|
|
269
|
+
headerLength += 2;
|
|
270
|
+
}
|
|
271
|
+
else if (payloadLength >= 65536) {
|
|
272
|
+
headerLength += 8;
|
|
273
|
+
}
|
|
274
|
+
const frame = Buffer.alloc(headerLength + payloadLength);
|
|
275
|
+
frame[0] = 0x80 | (opcode & 0x0f);
|
|
276
|
+
let offset = 2;
|
|
277
|
+
if (payloadLength < 126) {
|
|
278
|
+
frame[1] = payloadLength;
|
|
279
|
+
}
|
|
280
|
+
else if (payloadLength < 65536) {
|
|
281
|
+
frame[1] = 126;
|
|
282
|
+
frame.writeUInt16BE(payloadLength, offset);
|
|
283
|
+
offset += 2;
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
frame[1] = 127;
|
|
287
|
+
frame.writeBigUInt64BE(BigInt(payloadLength), offset);
|
|
288
|
+
offset += 8;
|
|
289
|
+
}
|
|
290
|
+
data.copy(frame, offset);
|
|
291
|
+
socket.write(frame);
|
|
292
|
+
}
|
|
293
|
+
function unmask(payload, maskingKey) {
|
|
294
|
+
const result = Buffer.alloc(payload.length);
|
|
295
|
+
for (let i = 0; i < payload.length; i++) {
|
|
296
|
+
const maskByte = maskingKey[i % 4];
|
|
297
|
+
result[i] = payload[i] ^ maskByte;
|
|
298
|
+
}
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
//# sourceMappingURL=local-test-server.js.map
|