@furystack/rest-service 10.0.28 → 10.1.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 +249 -12
- package/esm/api-manager.d.ts +1 -3
- package/esm/api-manager.d.ts.map +1 -1
- package/esm/api-manager.js +4 -6
- package/esm/api-manager.js.map +1 -1
- package/esm/header-processor.d.ts +39 -0
- package/esm/header-processor.d.ts.map +1 -0
- package/esm/header-processor.js +113 -0
- package/esm/header-processor.js.map +1 -0
- package/esm/header-processor.spec.d.ts +2 -0
- package/esm/header-processor.spec.d.ts.map +1 -0
- package/esm/header-processor.spec.js +420 -0
- package/esm/header-processor.spec.js.map +1 -0
- package/esm/helpers.d.ts +69 -1
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +70 -1
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.js +21 -5
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-proxy-handler.d.ts +53 -0
- package/esm/http-proxy-handler.d.ts.map +1 -0
- package/esm/http-proxy-handler.js +179 -0
- package/esm/http-proxy-handler.js.map +1 -0
- package/esm/http-user-context.d.ts +4 -4
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +4 -4
- package/esm/http-user-context.js.map +1 -1
- package/esm/index.d.ts +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/esm/path-processor.d.ts +33 -0
- package/esm/path-processor.d.ts.map +1 -0
- package/esm/path-processor.js +58 -0
- package/esm/path-processor.js.map +1 -0
- package/esm/path-processor.spec.d.ts +2 -0
- package/esm/path-processor.spec.d.ts.map +1 -0
- package/esm/path-processor.spec.js +256 -0
- package/esm/path-processor.spec.js.map +1 -0
- package/esm/proxy-manager.d.ts +52 -0
- package/esm/proxy-manager.d.ts.map +1 -0
- package/esm/proxy-manager.js +84 -0
- package/esm/proxy-manager.js.map +1 -0
- package/esm/proxy-manager.spec.d.ts +2 -0
- package/esm/proxy-manager.spec.d.ts.map +1 -0
- package/esm/proxy-manager.spec.js +1781 -0
- package/esm/proxy-manager.spec.js.map +1 -0
- package/esm/server-manager.d.ts +7 -0
- package/esm/server-manager.d.ts.map +1 -1
- package/esm/server-manager.js +12 -0
- package/esm/server-manager.js.map +1 -1
- package/esm/static-server-manager.d.ts.map +1 -1
- package/esm/static-server-manager.js +5 -7
- package/esm/static-server-manager.js.map +1 -1
- package/esm/websocket-proxy-handler.d.ts +44 -0
- package/esm/websocket-proxy-handler.d.ts.map +1 -0
- package/esm/websocket-proxy-handler.js +157 -0
- package/esm/websocket-proxy-handler.js.map +1 -0
- package/package.json +11 -9
- package/src/api-manager.ts +5 -15
- package/src/header-processor.spec.ts +514 -0
- package/src/header-processor.ts +140 -0
- package/src/helpers.spec.ts +23 -5
- package/src/helpers.ts +72 -1
- package/src/http-proxy-handler.ts +215 -0
- package/src/http-user-context.ts +6 -6
- package/src/index.ts +1 -0
- package/src/path-processor.spec.ts +318 -0
- package/src/path-processor.ts +69 -0
- package/src/proxy-manager.spec.ts +2094 -0
- package/src/proxy-manager.ts +101 -0
- package/src/server-manager.ts +19 -0
- package/src/static-server-manager.ts +5 -7
- package/src/websocket-proxy-handler.ts +204 -0
|
@@ -0,0 +1,2094 @@
|
|
|
1
|
+
import { getPort } from '@furystack/core/port-generator'
|
|
2
|
+
import { Injector } from '@furystack/inject'
|
|
3
|
+
import { usingAsync } from '@furystack/utils'
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'fs'
|
|
5
|
+
import type { IncomingMessage, OutgoingHttpHeaders, Server, ServerResponse } from 'http'
|
|
6
|
+
import { createServer as createHttpServer } from 'http'
|
|
7
|
+
import { tmpdir } from 'os'
|
|
8
|
+
import { join } from 'path'
|
|
9
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
10
|
+
import type { RawData } from 'ws'
|
|
11
|
+
import { WebSocket, WebSocketServer } from 'ws'
|
|
12
|
+
import { ProxyManager } from './proxy-manager.js'
|
|
13
|
+
import { StaticServerManager } from './static-server-manager.js'
|
|
14
|
+
|
|
15
|
+
describe('ProxyManager', () => {
|
|
16
|
+
// Create a temporary directory for test files
|
|
17
|
+
const testDir = join(tmpdir(), 'furystack-proxy-test')
|
|
18
|
+
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(testDir, { recursive: true })
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Directory might already exist
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
// Helper function to convert RawData to string
|
|
28
|
+
const rawDataToString = (data: RawData): string =>
|
|
29
|
+
Buffer.isBuffer(data)
|
|
30
|
+
? data.toString('utf8')
|
|
31
|
+
: Array.isArray(data)
|
|
32
|
+
? Buffer.concat(data).toString('utf8')
|
|
33
|
+
: Buffer.from(data).toString('utf8')
|
|
34
|
+
|
|
35
|
+
// Helper function to create a simple echo server
|
|
36
|
+
const createEchoServer = (
|
|
37
|
+
port: number,
|
|
38
|
+
handler?: (req: IncomingMessage, res: ServerResponse) => void,
|
|
39
|
+
): Promise<Server> => {
|
|
40
|
+
const server = createHttpServer(
|
|
41
|
+
handler ||
|
|
42
|
+
((req, res) => {
|
|
43
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
44
|
+
res.end(JSON.stringify({ url: req.url, method: req.method }))
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
server.listen(port, () => resolve(server))
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Helper function to create a WebSocket test server
|
|
53
|
+
const createWsTestServer = (
|
|
54
|
+
port: number,
|
|
55
|
+
onConnection: (ws: WebSocket, req: IncomingMessage) => void,
|
|
56
|
+
): Promise<{ server: Server; wss: WebSocketServer }> => {
|
|
57
|
+
const server = createHttpServer()
|
|
58
|
+
const wss = new WebSocketServer({ server })
|
|
59
|
+
wss.on('connection', onConnection)
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
server.listen(port, () => resolve({ server, wss }))
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('Basic proxy functionality', () => {
|
|
66
|
+
it('Should proxy requests from source URL to target URL', async () => {
|
|
67
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
68
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
69
|
+
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
70
|
+
|
|
71
|
+
const proxyPort = getPort()
|
|
72
|
+
const targetPort = getPort()
|
|
73
|
+
|
|
74
|
+
// Create a test file for the target server
|
|
75
|
+
const testFile = join(testDir, 'test.json')
|
|
76
|
+
writeFileSync(
|
|
77
|
+
testFile,
|
|
78
|
+
JSON.stringify({
|
|
79
|
+
message: 'Hello from target server',
|
|
80
|
+
url: '/test',
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
// Set up target server (static file server) - serve files from testDir
|
|
86
|
+
await staticServerManager.addStaticSite({
|
|
87
|
+
baseUrl: '/',
|
|
88
|
+
path: testDir,
|
|
89
|
+
port: targetPort,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Set up proxy server
|
|
93
|
+
await proxyManager.addProxy({
|
|
94
|
+
sourceBaseUrl: '/old',
|
|
95
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
96
|
+
pathRewrite: (path) => path.replace('/path', ''),
|
|
97
|
+
sourcePort: proxyPort,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/old/path/test.json`)
|
|
101
|
+
|
|
102
|
+
expect(result.status).toBe(200)
|
|
103
|
+
expect(result.url).toBe(`http://127.0.0.1:${proxyPort}/old/path/test.json`)
|
|
104
|
+
|
|
105
|
+
const responseData = (await result.json()) as { message: string; url: string; timestamp: number }
|
|
106
|
+
expect(responseData.message).toBe('Hello from target server')
|
|
107
|
+
expect(responseData.url).toBe('/test')
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('Should preserve query strings when proxying', async () => {
|
|
112
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
113
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
114
|
+
const targetPort = getPort()
|
|
115
|
+
const proxyPort = getPort()
|
|
116
|
+
|
|
117
|
+
// Create a simple echo server that returns query parameters
|
|
118
|
+
const targetServer = createHttpServer((req, res) => {
|
|
119
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
120
|
+
res.end(JSON.stringify({ url: req.url, query: req.url?.split('?')[1] || '' }))
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
await new Promise<void>((resolve) => {
|
|
124
|
+
targetServer.listen(targetPort, () => resolve())
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await proxyManager.addProxy({
|
|
129
|
+
sourceBaseUrl: '/api',
|
|
130
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
131
|
+
sourcePort: proxyPort,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test?foo=bar&baz=qux`)
|
|
135
|
+
expect(result.status).toBe(200)
|
|
136
|
+
|
|
137
|
+
const data = (await result.json()) as { url: string; query: string }
|
|
138
|
+
expect(data.query).toBe('foo=bar&baz=qux')
|
|
139
|
+
} finally {
|
|
140
|
+
targetServer.close()
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('Should handle header transformation', async () => {
|
|
146
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
147
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
148
|
+
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
149
|
+
|
|
150
|
+
const proxyPort = getPort()
|
|
151
|
+
const targetPort = getPort()
|
|
152
|
+
|
|
153
|
+
// Create a test file that returns headers
|
|
154
|
+
const headersFile = join(testDir, 'headers.json')
|
|
155
|
+
writeFileSync(
|
|
156
|
+
headersFile,
|
|
157
|
+
JSON.stringify({
|
|
158
|
+
message: 'Headers test',
|
|
159
|
+
receivedHeaders: '{{HEADERS}}', // Placeholder for dynamic content
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// Set up target server
|
|
164
|
+
await staticServerManager.addStaticSite({
|
|
165
|
+
baseUrl: '/',
|
|
166
|
+
path: testDir,
|
|
167
|
+
port: targetPort,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Set up proxy server with header transformation
|
|
171
|
+
await proxyManager.addProxy({
|
|
172
|
+
sourceBaseUrl: '/old',
|
|
173
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
174
|
+
pathRewrite: (path) => path.replace('/path', ''),
|
|
175
|
+
sourcePort: proxyPort,
|
|
176
|
+
headers: () => ({
|
|
177
|
+
'X-Custom-Header': 'custom-value',
|
|
178
|
+
Authorization: 'Bearer new-token',
|
|
179
|
+
}),
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/old/path/headers.json`, {
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: 'Bearer old-token',
|
|
185
|
+
'User-Agent': 'test-agent',
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
expect(result.status).toBe(200)
|
|
190
|
+
const responseData = (await result.json()) as { message: string; receivedHeaders: string }
|
|
191
|
+
expect(responseData.message).toBe('Headers test')
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('Should handle cookie transformation', async () => {
|
|
196
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
197
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
198
|
+
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
199
|
+
|
|
200
|
+
const proxyPort = getPort()
|
|
201
|
+
const targetPort = getPort()
|
|
202
|
+
|
|
203
|
+
// Create a test file for cookies
|
|
204
|
+
const cookiesFile = join(testDir, 'cookies.json')
|
|
205
|
+
writeFileSync(
|
|
206
|
+
cookiesFile,
|
|
207
|
+
JSON.stringify({
|
|
208
|
+
message: 'Cookies test',
|
|
209
|
+
receivedCookies: '{{COOKIES}}', // Placeholder for dynamic content
|
|
210
|
+
}),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
// Set up target server
|
|
214
|
+
await staticServerManager.addStaticSite({
|
|
215
|
+
baseUrl: '/',
|
|
216
|
+
path: testDir,
|
|
217
|
+
port: targetPort,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// Set up proxy server with cookie transformation
|
|
221
|
+
await proxyManager.addProxy({
|
|
222
|
+
sourceBaseUrl: '/old',
|
|
223
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
224
|
+
pathRewrite: (path) => path.replace('/path', ''),
|
|
225
|
+
sourcePort: proxyPort,
|
|
226
|
+
cookies: (originalCookies) => [...originalCookies, 'newCookie=newValue', 'sessionId=updated-session'],
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/old/path/cookies.json`, {
|
|
230
|
+
headers: {
|
|
231
|
+
Cookie: 'oldCookie=oldValue; sessionId=old-session',
|
|
232
|
+
},
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
expect(result.status).toBe(200)
|
|
236
|
+
const responseData = (await result.json()) as { message: string; receivedCookies: string }
|
|
237
|
+
expect(responseData.message).toBe('Cookies test')
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('Should handle POST requests with JSON body', async () => {
|
|
242
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
243
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
244
|
+
const targetPort = getPort()
|
|
245
|
+
const proxyPort = getPort()
|
|
246
|
+
|
|
247
|
+
// Create an echo server that returns the request body
|
|
248
|
+
const targetServer = createHttpServer((req, res) => {
|
|
249
|
+
const chunks: Buffer[] = []
|
|
250
|
+
req.on('data', (chunk) => {
|
|
251
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array))
|
|
252
|
+
})
|
|
253
|
+
req.on('end', () => {
|
|
254
|
+
const body = Buffer.concat(chunks).toString('utf8')
|
|
255
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
256
|
+
res.end(JSON.stringify({ received: JSON.parse(body), method: req.method }))
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
await new Promise<void>((resolve) => {
|
|
261
|
+
targetServer.listen(targetPort, () => resolve())
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await proxyManager.addProxy({
|
|
266
|
+
sourceBaseUrl: '/api',
|
|
267
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
268
|
+
sourcePort: proxyPort,
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
const testData = { name: 'test', value: 123, nested: { key: 'value' } }
|
|
272
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/data`, {
|
|
273
|
+
method: 'POST',
|
|
274
|
+
headers: { 'Content-Type': 'application/json' },
|
|
275
|
+
body: JSON.stringify(testData),
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
expect(result.status).toBe(200)
|
|
279
|
+
const data = (await result.json()) as { received: typeof testData; method: string }
|
|
280
|
+
expect(data.received).toEqual(testData)
|
|
281
|
+
expect(data.method).toBe('POST')
|
|
282
|
+
} finally {
|
|
283
|
+
targetServer.close()
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('Should handle response cookies transformation', async () => {
|
|
289
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
290
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
291
|
+
const targetPort = getPort()
|
|
292
|
+
const proxyPort = getPort()
|
|
293
|
+
|
|
294
|
+
// Create a server that sets cookies
|
|
295
|
+
const targetServer = createHttpServer((_req, res) => {
|
|
296
|
+
res.writeHead(200, {
|
|
297
|
+
'Content-Type': 'application/json',
|
|
298
|
+
'Set-Cookie': ['sessionId=abc123; HttpOnly', 'theme=dark; Path=/'],
|
|
299
|
+
})
|
|
300
|
+
res.end(JSON.stringify({ message: 'Cookies set' }))
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
await new Promise<void>((resolve) => {
|
|
304
|
+
targetServer.listen(targetPort, () => resolve())
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await proxyManager.addProxy({
|
|
309
|
+
sourceBaseUrl: '/api',
|
|
310
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
311
|
+
sourcePort: proxyPort,
|
|
312
|
+
responseCookies: (cookies) => {
|
|
313
|
+
// Transform the cookies
|
|
314
|
+
return cookies.map((cookie) => cookie.replace('sessionId=abc123', 'sessionId=xyz789'))
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
319
|
+
expect(result.status).toBe(200)
|
|
320
|
+
|
|
321
|
+
const setCookieHeader = result.headers.get('set-cookie')
|
|
322
|
+
expect(setCookieHeader).toContain('sessionId=xyz789')
|
|
323
|
+
expect(setCookieHeader).not.toContain('sessionId=abc123')
|
|
324
|
+
} finally {
|
|
325
|
+
targetServer.close()
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('Should not match requests outside source base URL', async () => {
|
|
331
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
332
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
333
|
+
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
334
|
+
|
|
335
|
+
const proxyPort = getPort()
|
|
336
|
+
const targetPort = getPort()
|
|
337
|
+
|
|
338
|
+
// Create a test file
|
|
339
|
+
const testFile = join(testDir, 'test.json')
|
|
340
|
+
writeFileSync(testFile, JSON.stringify({ message: 'test' }))
|
|
341
|
+
|
|
342
|
+
// Set up target server
|
|
343
|
+
await staticServerManager.addStaticSite({
|
|
344
|
+
baseUrl: '/',
|
|
345
|
+
path: testDir,
|
|
346
|
+
port: targetPort,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
// Set up proxy server
|
|
350
|
+
await proxyManager.addProxy({
|
|
351
|
+
sourceBaseUrl: '/old',
|
|
352
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
353
|
+
pathRewrite: (path) => path.replace('/path', ''),
|
|
354
|
+
sourcePort: proxyPort,
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/different/path`)
|
|
359
|
+
|
|
360
|
+
// Should not proxy, request should be handled by other handlers or return 404
|
|
361
|
+
expect(result.status).not.toBe(200)
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// Connection error is expected when no handler matches
|
|
364
|
+
expect(error).toBeDefined()
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('Should handle exact URL matches', async () => {
|
|
370
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
371
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
372
|
+
const staticServerManager = injector.getInstance(StaticServerManager)
|
|
373
|
+
|
|
374
|
+
const proxyPort = getPort()
|
|
375
|
+
const targetPort = getPort()
|
|
376
|
+
|
|
377
|
+
// Create a test file
|
|
378
|
+
const exactFile = join(testDir, 'exact.json')
|
|
379
|
+
writeFileSync(
|
|
380
|
+
exactFile,
|
|
381
|
+
JSON.stringify({
|
|
382
|
+
message: 'Exact match test',
|
|
383
|
+
url: '/exact',
|
|
384
|
+
}),
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
// Set up target server
|
|
388
|
+
await staticServerManager.addStaticSite({
|
|
389
|
+
baseUrl: '/',
|
|
390
|
+
path: testDir,
|
|
391
|
+
port: targetPort,
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
// Set up proxy server
|
|
395
|
+
await proxyManager.addProxy({
|
|
396
|
+
sourceBaseUrl: '/exact',
|
|
397
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
398
|
+
pathRewrite: (path) => path, // Keep the path as-is
|
|
399
|
+
sourcePort: proxyPort,
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/exact/exact.json`)
|
|
403
|
+
|
|
404
|
+
expect(result.status).toBe(200)
|
|
405
|
+
const responseData = (await result.json()) as { message: string; url: string }
|
|
406
|
+
expect(responseData.message).toBe('Exact match test')
|
|
407
|
+
expect(responseData.url).toBe('/exact')
|
|
408
|
+
})
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
it('Should handle server errors gracefully', async () => {
|
|
412
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
413
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
414
|
+
const proxyPort = getPort()
|
|
415
|
+
const unavailablePort = getPort() // Get a valid port number that doesn't have a server
|
|
416
|
+
|
|
417
|
+
// Set up proxy to non-existent target
|
|
418
|
+
await proxyManager.addProxy({
|
|
419
|
+
sourceBaseUrl: '/api',
|
|
420
|
+
targetBaseUrl: `http://localhost:${unavailablePort}`,
|
|
421
|
+
sourcePort: proxyPort,
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
425
|
+
expect(result.status).toBe(502) // Bad Gateway
|
|
426
|
+
expect(await result.text()).toBe('Bad Gateway')
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('Should emit onProxyFailed event on error', async () => {
|
|
431
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
432
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
433
|
+
const proxyPort = getPort()
|
|
434
|
+
const unavailablePort = getPort()
|
|
435
|
+
|
|
436
|
+
let failedEvent: { from: string; to: string; error: unknown } | undefined
|
|
437
|
+
|
|
438
|
+
proxyManager.subscribe('onProxyFailed', (event) => {
|
|
439
|
+
failedEvent = event
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// Set up proxy to non-existent target
|
|
443
|
+
await proxyManager.addProxy({
|
|
444
|
+
sourceBaseUrl: '/api',
|
|
445
|
+
targetBaseUrl: `http://localhost:${unavailablePort}`,
|
|
446
|
+
sourcePort: proxyPort,
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
450
|
+
|
|
451
|
+
expect(failedEvent).toBeDefined()
|
|
452
|
+
expect(failedEvent?.from).toBe('/api')
|
|
453
|
+
expect(failedEvent?.to).toContain(`http://localhost:${unavailablePort}`)
|
|
454
|
+
})
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('Should validate targetBaseUrl on addProxy', async () => {
|
|
458
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
459
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
460
|
+
|
|
461
|
+
await expect(
|
|
462
|
+
proxyManager.addProxy({
|
|
463
|
+
sourceBaseUrl: '/api',
|
|
464
|
+
targetBaseUrl: 'not-a-valid-url',
|
|
465
|
+
sourcePort: getPort(),
|
|
466
|
+
}),
|
|
467
|
+
).rejects.toThrow('Invalid targetBaseUrl')
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('Should filter hop-by-hop headers', async () => {
|
|
472
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
473
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
474
|
+
const targetPort = getPort()
|
|
475
|
+
const proxyPort = getPort()
|
|
476
|
+
|
|
477
|
+
let receivedHeaders: Record<string, string | string[] | undefined> = {}
|
|
478
|
+
|
|
479
|
+
// Create a server that captures headers
|
|
480
|
+
const targetServer = createHttpServer((req, res) => {
|
|
481
|
+
receivedHeaders = { ...req.headers }
|
|
482
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
483
|
+
res.end(JSON.stringify({ message: 'ok' }))
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
await new Promise<void>((resolve) => {
|
|
487
|
+
targetServer.listen(targetPort, () => resolve())
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
await proxyManager.addProxy({
|
|
492
|
+
sourceBaseUrl: '/api',
|
|
493
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
494
|
+
sourcePort: proxyPort,
|
|
495
|
+
headers: (original) => {
|
|
496
|
+
// Verify that Connection header was filtered from original headers
|
|
497
|
+
expect(original.connection).toBeUndefined()
|
|
498
|
+
return original
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
await fetch(`http://127.0.0.1:${proxyPort}/api/test`, {
|
|
503
|
+
headers: {
|
|
504
|
+
Connection: 'keep-alive',
|
|
505
|
+
'X-Custom-Header': 'should-be-forwarded',
|
|
506
|
+
'X-Hop-Header': 'test-value',
|
|
507
|
+
},
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
// Note: fetch() may add its own Connection header when making the request
|
|
511
|
+
// The important thing is that custom headers are forwarded
|
|
512
|
+
expect(receivedHeaders['x-custom-header']).toBe('should-be-forwarded')
|
|
513
|
+
expect(receivedHeaders['x-hop-header']).toBe('test-value')
|
|
514
|
+
} finally {
|
|
515
|
+
targetServer.close()
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
it('Should preserve multiple Set-Cookie headers from target', async () => {
|
|
521
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
522
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
523
|
+
const targetPort = getPort()
|
|
524
|
+
const proxyPort = getPort()
|
|
525
|
+
|
|
526
|
+
const targetServer = createHttpServer((_req, res) => {
|
|
527
|
+
res.writeHead(200, {
|
|
528
|
+
'Content-Type': 'application/json',
|
|
529
|
+
'Set-Cookie': ['cookieA=a; Path=/; HttpOnly', 'cookieB=b; Path=/', 'cookieC=c; Path=/'],
|
|
530
|
+
})
|
|
531
|
+
res.end(JSON.stringify({ ok: true }))
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
await new Promise<void>((resolve) => {
|
|
535
|
+
targetServer.listen(targetPort, () => resolve())
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
await proxyManager.addProxy({
|
|
540
|
+
sourceBaseUrl: '/api',
|
|
541
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
542
|
+
sourcePort: proxyPort,
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
546
|
+
expect(result.status).toBe(200)
|
|
547
|
+
|
|
548
|
+
const anyHeaders = result.headers as unknown as { getSetCookie?: () => string[] }
|
|
549
|
+
const cookies = anyHeaders.getSetCookie?.()
|
|
550
|
+
if (cookies) {
|
|
551
|
+
expect(cookies.length).toBeGreaterThanOrEqual(3)
|
|
552
|
+
expect(cookies.join('\n')).toContain('cookieA=a')
|
|
553
|
+
expect(cookies.join('\n')).toContain('cookieB=b')
|
|
554
|
+
expect(cookies.join('\n')).toContain('cookieC=c')
|
|
555
|
+
} else {
|
|
556
|
+
// Fallback: combined header still should contain at least one cookie
|
|
557
|
+
const setCookieHeader = result.headers.get('set-cookie')
|
|
558
|
+
expect(setCookieHeader).toBeTruthy()
|
|
559
|
+
expect(setCookieHeader as string).toContain('cookieA=a')
|
|
560
|
+
}
|
|
561
|
+
} finally {
|
|
562
|
+
targetServer.close()
|
|
563
|
+
}
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('Should add X-Forwarded headers to target request', async () => {
|
|
568
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
569
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
570
|
+
const targetPort = getPort()
|
|
571
|
+
const proxyPort = getPort()
|
|
572
|
+
|
|
573
|
+
let received: Record<string, string | string[] | undefined> = {}
|
|
574
|
+
const targetServer = createHttpServer((req, res) => {
|
|
575
|
+
received = { ...req.headers }
|
|
576
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
577
|
+
res.end(JSON.stringify({ ok: true }))
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
await new Promise<void>((resolve) => {
|
|
581
|
+
targetServer.listen(targetPort, () => resolve())
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
await proxyManager.addProxy({
|
|
586
|
+
sourceBaseUrl: '/api',
|
|
587
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
588
|
+
sourcePort: proxyPort,
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
592
|
+
|
|
593
|
+
expect(received['x-forwarded-for']).toBeTruthy()
|
|
594
|
+
expect(received['x-forwarded-host']).toBeTruthy()
|
|
595
|
+
expect(received['x-forwarded-proto']).toBeTruthy()
|
|
596
|
+
} finally {
|
|
597
|
+
targetServer.close()
|
|
598
|
+
}
|
|
599
|
+
})
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
it('Should abort upstream when client disconnects', async () => {
|
|
603
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
604
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
605
|
+
const targetPort = getPort()
|
|
606
|
+
const proxyPort = getPort()
|
|
607
|
+
|
|
608
|
+
let chunksWritten = 0
|
|
609
|
+
let closedEarly = false
|
|
610
|
+
const totalChunks = 100
|
|
611
|
+
const targetServer = createHttpServer((_req, res) => {
|
|
612
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' })
|
|
613
|
+
const interval = setInterval(() => {
|
|
614
|
+
chunksWritten++
|
|
615
|
+
res.write('chunk\n')
|
|
616
|
+
if (chunksWritten >= totalChunks) {
|
|
617
|
+
clearInterval(interval)
|
|
618
|
+
res.end('done')
|
|
619
|
+
}
|
|
620
|
+
}, 5)
|
|
621
|
+
res.on('close', () => {
|
|
622
|
+
closedEarly = chunksWritten < totalChunks
|
|
623
|
+
clearInterval(interval)
|
|
624
|
+
})
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
await new Promise<void>((resolve) => {
|
|
628
|
+
targetServer.listen(targetPort, () => resolve())
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
await proxyManager.addProxy({
|
|
633
|
+
sourceBaseUrl: '/api',
|
|
634
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
635
|
+
sourcePort: proxyPort,
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
const controller = new AbortController()
|
|
639
|
+
const fetchPromise = fetch(`http://127.0.0.1:${proxyPort}/api/stream`, { signal: controller.signal }).then(
|
|
640
|
+
async (response) => {
|
|
641
|
+
// Start reading the body stream
|
|
642
|
+
const reader = response.body?.getReader()
|
|
643
|
+
if (!reader) throw new Error('No body')
|
|
644
|
+
// Read one chunk
|
|
645
|
+
await reader.read()
|
|
646
|
+
// Then abort
|
|
647
|
+
controller.abort()
|
|
648
|
+
// Try to read more (should fail)
|
|
649
|
+
return reader.read()
|
|
650
|
+
},
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
await expect(fetchPromise).rejects.toBeDefined()
|
|
654
|
+
|
|
655
|
+
// Give the server a moment to receive the abort
|
|
656
|
+
await new Promise((r) => setTimeout(r, 100))
|
|
657
|
+
|
|
658
|
+
expect(closedEarly).toBe(true)
|
|
659
|
+
} finally {
|
|
660
|
+
targetServer.close()
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
it('Should handle request timeout', async () => {
|
|
666
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
667
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
668
|
+
const targetPort = getPort()
|
|
669
|
+
const proxyPort = getPort()
|
|
670
|
+
|
|
671
|
+
let failedEvent: { from: string; to: string; error: unknown } | undefined
|
|
672
|
+
|
|
673
|
+
proxyManager.subscribe('onProxyFailed', (event) => {
|
|
674
|
+
failedEvent = event
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
// Create a server that delays response longer than timeout
|
|
678
|
+
const targetServer = createHttpServer((_req, res) => {
|
|
679
|
+
// Delay for 2 seconds (longer than our 100ms timeout)
|
|
680
|
+
setTimeout(() => {
|
|
681
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
682
|
+
res.end(JSON.stringify({ message: 'delayed response' }))
|
|
683
|
+
}, 2000)
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
await new Promise<void>((resolve) => {
|
|
687
|
+
targetServer.listen(targetPort, () => resolve())
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
await proxyManager.addProxy({
|
|
692
|
+
sourceBaseUrl: '/api',
|
|
693
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
694
|
+
sourcePort: proxyPort,
|
|
695
|
+
timeout: 100, // Very short timeout to trigger quickly
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
699
|
+
|
|
700
|
+
// Should return 502 Bad Gateway due to timeout
|
|
701
|
+
expect(result.status).toBe(502)
|
|
702
|
+
expect(await result.text()).toBe('Bad Gateway')
|
|
703
|
+
|
|
704
|
+
// Should emit onProxyFailed event
|
|
705
|
+
expect(failedEvent).toBeDefined()
|
|
706
|
+
expect(failedEvent?.from).toBe('/api')
|
|
707
|
+
expect(failedEvent?.to).toContain(`http://localhost:${targetPort}`)
|
|
708
|
+
} finally {
|
|
709
|
+
targetServer.close()
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
it('Should complete successfully when response is faster than timeout', async () => {
|
|
715
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
716
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
717
|
+
const targetPort = getPort()
|
|
718
|
+
const proxyPort = getPort()
|
|
719
|
+
|
|
720
|
+
// Create a fast-responding server
|
|
721
|
+
const targetServer = createHttpServer((_req, res) => {
|
|
722
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
723
|
+
res.end(JSON.stringify({ message: 'fast response' }))
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
await new Promise<void>((resolve) => {
|
|
727
|
+
targetServer.listen(targetPort, () => resolve())
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
try {
|
|
731
|
+
await proxyManager.addProxy({
|
|
732
|
+
sourceBaseUrl: '/api',
|
|
733
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
734
|
+
sourcePort: proxyPort,
|
|
735
|
+
timeout: 5000, // Generous timeout
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
739
|
+
|
|
740
|
+
// Should succeed
|
|
741
|
+
expect(result.status).toBe(200)
|
|
742
|
+
const data = (await result.json()) as { message: string }
|
|
743
|
+
expect(data.message).toBe('fast response')
|
|
744
|
+
} finally {
|
|
745
|
+
targetServer.close()
|
|
746
|
+
}
|
|
747
|
+
})
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it('Should use default timeout when not specified', async () => {
|
|
751
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
752
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
753
|
+
const targetPort = getPort()
|
|
754
|
+
const proxyPort = getPort()
|
|
755
|
+
|
|
756
|
+
// Create a server with moderate delay (500ms)
|
|
757
|
+
const targetServer = createHttpServer((_req, res) => {
|
|
758
|
+
setTimeout(() => {
|
|
759
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
760
|
+
res.end(JSON.stringify({ message: 'response after delay' }))
|
|
761
|
+
}, 500)
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
await new Promise<void>((resolve) => {
|
|
765
|
+
targetServer.listen(targetPort, () => resolve())
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
// No timeout specified - should use default 30000ms
|
|
770
|
+
await proxyManager.addProxy({
|
|
771
|
+
sourceBaseUrl: '/api',
|
|
772
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
773
|
+
sourcePort: proxyPort,
|
|
774
|
+
// timeout not specified - uses default
|
|
775
|
+
})
|
|
776
|
+
|
|
777
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
778
|
+
|
|
779
|
+
// Should succeed with default timeout
|
|
780
|
+
expect(result.status).toBe(200)
|
|
781
|
+
const data = (await result.json()) as { message: string }
|
|
782
|
+
expect(data.message).toBe('response after delay')
|
|
783
|
+
} finally {
|
|
784
|
+
targetServer.close()
|
|
785
|
+
}
|
|
786
|
+
})
|
|
787
|
+
})
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
describe('Edge cases and error handling', () => {
|
|
791
|
+
it('Should validate invalid protocol (non-HTTP/HTTPS)', async () => {
|
|
792
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
793
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
794
|
+
const proxyPort = getPort()
|
|
795
|
+
|
|
796
|
+
await expect(
|
|
797
|
+
proxyManager.addProxy({
|
|
798
|
+
sourceBaseUrl: '/api',
|
|
799
|
+
targetBaseUrl: 'ftp://example.com',
|
|
800
|
+
sourcePort: proxyPort,
|
|
801
|
+
}),
|
|
802
|
+
).rejects.toThrow('Invalid targetBaseUrl protocol')
|
|
803
|
+
})
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
it('Should handle invalid target URL created by pathRewrite', async () => {
|
|
807
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
808
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
809
|
+
const targetPort = getPort()
|
|
810
|
+
const proxyPort = getPort()
|
|
811
|
+
|
|
812
|
+
let failedEvent: { from: string; to: string; error: unknown } | undefined
|
|
813
|
+
|
|
814
|
+
proxyManager.subscribe('onProxyFailed', (event) => {
|
|
815
|
+
failedEvent = event
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
// Create a simple server
|
|
819
|
+
const targetServer = createHttpServer((_req, res) => {
|
|
820
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
821
|
+
res.end(JSON.stringify({ message: 'ok' }))
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
await new Promise<void>((resolve) => {
|
|
825
|
+
targetServer.listen(targetPort, () => resolve())
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
await proxyManager.addProxy({
|
|
830
|
+
sourceBaseUrl: '/api',
|
|
831
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
832
|
+
sourcePort: proxyPort,
|
|
833
|
+
// PathHelper.joinUrl now properly handles edge cases, making previously
|
|
834
|
+
// "invalid" URLs valid. The test now verifies that the proxy still works
|
|
835
|
+
// even with unusual pathRewrite results.
|
|
836
|
+
pathRewrite: () => '://unusual-but-valid',
|
|
837
|
+
})
|
|
838
|
+
|
|
839
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
840
|
+
|
|
841
|
+
// With PathHelper.joinUrl, the URL is now valid and routes to the target server
|
|
842
|
+
// The target server returns 200, demonstrating robust URL handling
|
|
843
|
+
expect(result.status).toBe(200)
|
|
844
|
+
const body = await result.json()
|
|
845
|
+
expect(body).toStrictEqual({ message: 'ok' })
|
|
846
|
+
|
|
847
|
+
// No error event should be emitted since the request succeeded
|
|
848
|
+
expect(failedEvent).toBeUndefined()
|
|
849
|
+
} finally {
|
|
850
|
+
targetServer.close()
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
})
|
|
854
|
+
|
|
855
|
+
it('Should handle number-type header values', async () => {
|
|
856
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
857
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
858
|
+
const targetPort = getPort()
|
|
859
|
+
const proxyPort = getPort()
|
|
860
|
+
|
|
861
|
+
let receivedHeaders: Record<string, string | string[] | undefined> = {}
|
|
862
|
+
let transformedHeaders: OutgoingHttpHeaders | undefined
|
|
863
|
+
|
|
864
|
+
const targetServer = createHttpServer((req, res) => {
|
|
865
|
+
receivedHeaders = { ...req.headers }
|
|
866
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
867
|
+
res.end(JSON.stringify({ ok: true }))
|
|
868
|
+
})
|
|
869
|
+
|
|
870
|
+
await new Promise<void>((resolve) => {
|
|
871
|
+
targetServer.listen(targetPort, () => resolve())
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
await proxyManager.addProxy({
|
|
876
|
+
sourceBaseUrl: '/api',
|
|
877
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
878
|
+
sourcePort: proxyPort,
|
|
879
|
+
headers: (original) => {
|
|
880
|
+
transformedHeaders = {
|
|
881
|
+
...original,
|
|
882
|
+
'X-Custom-Number': 12345 as unknown as string,
|
|
883
|
+
'X-Custom-String': 'string-value',
|
|
884
|
+
}
|
|
885
|
+
return transformedHeaders
|
|
886
|
+
},
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
890
|
+
|
|
891
|
+
// Verify headers transformer was called and returns correct types
|
|
892
|
+
expect(transformedHeaders).toBeDefined()
|
|
893
|
+
expect(transformedHeaders?.['X-Custom-Number']).toBe(12345)
|
|
894
|
+
expect(transformedHeaders?.['X-Custom-String']).toBe('string-value')
|
|
895
|
+
|
|
896
|
+
// Verify string header was forwarded
|
|
897
|
+
expect(receivedHeaders['x-custom-string']).toBe('string-value')
|
|
898
|
+
// Number headers get converted to strings by the proxy logic
|
|
899
|
+
expect(receivedHeaders['x-custom-number']).toBe('12345')
|
|
900
|
+
} finally {
|
|
901
|
+
targetServer.close()
|
|
902
|
+
}
|
|
903
|
+
})
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
it('Should handle array-type header values', async () => {
|
|
907
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
908
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
909
|
+
const targetPort = getPort()
|
|
910
|
+
const proxyPort = getPort()
|
|
911
|
+
|
|
912
|
+
let receivedHeaders: Record<string, string | string[] | undefined> = {}
|
|
913
|
+
|
|
914
|
+
const targetServer = createHttpServer((req, res) => {
|
|
915
|
+
receivedHeaders = { ...req.headers }
|
|
916
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
917
|
+
res.end(JSON.stringify({ ok: true }))
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
await new Promise<void>((resolve) => {
|
|
921
|
+
targetServer.listen(targetPort, () => resolve())
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
try {
|
|
925
|
+
await proxyManager.addProxy({
|
|
926
|
+
sourceBaseUrl: '/api',
|
|
927
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
928
|
+
sourcePort: proxyPort,
|
|
929
|
+
headers: () => ({
|
|
930
|
+
'X-Array-Header': ['value1', 'value2', 'value3'] as unknown as string,
|
|
931
|
+
}),
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
935
|
+
|
|
936
|
+
expect(receivedHeaders['x-array-header']).toBe('value1, value2, value3')
|
|
937
|
+
} finally {
|
|
938
|
+
targetServer.close()
|
|
939
|
+
}
|
|
940
|
+
})
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
it('Should handle undefined header values', async () => {
|
|
944
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
945
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
946
|
+
const targetPort = getPort()
|
|
947
|
+
const proxyPort = getPort()
|
|
948
|
+
|
|
949
|
+
let receivedHeaders: Record<string, string | string[] | undefined> = {}
|
|
950
|
+
|
|
951
|
+
const targetServer = createHttpServer((req, res) => {
|
|
952
|
+
receivedHeaders = { ...req.headers }
|
|
953
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
954
|
+
res.end(JSON.stringify({ ok: true }))
|
|
955
|
+
})
|
|
956
|
+
|
|
957
|
+
await new Promise<void>((resolve) => {
|
|
958
|
+
targetServer.listen(targetPort, () => resolve())
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
try {
|
|
962
|
+
await proxyManager.addProxy({
|
|
963
|
+
sourceBaseUrl: '/api',
|
|
964
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
965
|
+
sourcePort: proxyPort,
|
|
966
|
+
headers: () => ({
|
|
967
|
+
'X-Defined-Header': 'defined',
|
|
968
|
+
'X-Undefined-Header': undefined as unknown as string,
|
|
969
|
+
}),
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
await fetch(`http://127.0.0.1:${proxyPort}/api/test`)
|
|
973
|
+
|
|
974
|
+
expect(receivedHeaders['x-defined-header']).toBe('defined')
|
|
975
|
+
expect(receivedHeaders['x-undefined-header']).toBeUndefined()
|
|
976
|
+
} finally {
|
|
977
|
+
targetServer.close()
|
|
978
|
+
}
|
|
979
|
+
})
|
|
980
|
+
})
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
describe('WebSocket proxy functionality', () => {
|
|
984
|
+
it('Should proxy WebSocket connections when enabled', async () => {
|
|
985
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
986
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
987
|
+
const targetPort = getPort()
|
|
988
|
+
const proxyPort = getPort()
|
|
989
|
+
|
|
990
|
+
// Create target WebSocket server
|
|
991
|
+
const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws) => {
|
|
992
|
+
ws.on('message', (data: RawData) => {
|
|
993
|
+
ws.send(`echo: ${rawDataToString(data)}`)
|
|
994
|
+
})
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
try {
|
|
998
|
+
// Set up proxy with WebSocket support
|
|
999
|
+
await proxyManager.addProxy({
|
|
1000
|
+
sourceBaseUrl: '/ws',
|
|
1001
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1002
|
+
sourcePort: proxyPort,
|
|
1003
|
+
enableWebsockets: true,
|
|
1004
|
+
})
|
|
1005
|
+
|
|
1006
|
+
// Connect WebSocket client through proxy
|
|
1007
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1008
|
+
|
|
1009
|
+
await new Promise<void>((resolve, reject) => {
|
|
1010
|
+
clientWs.on('open', () => {
|
|
1011
|
+
clientWs.send('hello')
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
clientWs.on('message', (data: RawData) => {
|
|
1015
|
+
expect(rawDataToString(data)).toBe('echo: hello')
|
|
1016
|
+
clientWs.close()
|
|
1017
|
+
resolve()
|
|
1018
|
+
})
|
|
1019
|
+
|
|
1020
|
+
clientWs.on('error', (err) => {
|
|
1021
|
+
clientWs.close()
|
|
1022
|
+
reject(err)
|
|
1023
|
+
})
|
|
1024
|
+
})
|
|
1025
|
+
} finally {
|
|
1026
|
+
targetWss.close()
|
|
1027
|
+
targetServer.close()
|
|
1028
|
+
}
|
|
1029
|
+
})
|
|
1030
|
+
})
|
|
1031
|
+
|
|
1032
|
+
it('Should handle bidirectional WebSocket communication', async () => {
|
|
1033
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1034
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1035
|
+
const targetPort = getPort()
|
|
1036
|
+
const proxyPort = getPort()
|
|
1037
|
+
|
|
1038
|
+
// Create target WebSocket server
|
|
1039
|
+
const targetServer = createHttpServer()
|
|
1040
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1041
|
+
|
|
1042
|
+
targetWss.on('connection', (ws) => {
|
|
1043
|
+
// Send messages to client
|
|
1044
|
+
ws.send('server-message-1')
|
|
1045
|
+
ws.send('server-message-2')
|
|
1046
|
+
|
|
1047
|
+
ws.on('message', (data: RawData) => {
|
|
1048
|
+
const message = Buffer.isBuffer(data)
|
|
1049
|
+
? data.toString('utf8')
|
|
1050
|
+
: Array.isArray(data)
|
|
1051
|
+
? Buffer.concat(data).toString('utf8')
|
|
1052
|
+
: Buffer.from(data).toString('utf8')
|
|
1053
|
+
ws.send(`received: ${message}`)
|
|
1054
|
+
})
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
await new Promise<void>((resolve) => {
|
|
1058
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
try {
|
|
1062
|
+
await proxyManager.addProxy({
|
|
1063
|
+
sourceBaseUrl: '/ws',
|
|
1064
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1065
|
+
sourcePort: proxyPort,
|
|
1066
|
+
enableWebsockets: true,
|
|
1067
|
+
})
|
|
1068
|
+
|
|
1069
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1070
|
+
|
|
1071
|
+
const receivedMessages: string[] = []
|
|
1072
|
+
|
|
1073
|
+
await new Promise<void>((resolve, reject) => {
|
|
1074
|
+
clientWs.on('open', () => {
|
|
1075
|
+
clientWs.send('client-message-1')
|
|
1076
|
+
clientWs.send('client-message-2')
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
clientWs.on('message', (data: RawData) => {
|
|
1080
|
+
const message = Buffer.isBuffer(data)
|
|
1081
|
+
? data.toString('utf8')
|
|
1082
|
+
: Array.isArray(data)
|
|
1083
|
+
? Buffer.concat(data).toString('utf8')
|
|
1084
|
+
: Buffer.from(data).toString('utf8')
|
|
1085
|
+
receivedMessages.push(message)
|
|
1086
|
+
|
|
1087
|
+
if (receivedMessages.length >= 4) {
|
|
1088
|
+
expect(receivedMessages).toContain('server-message-1')
|
|
1089
|
+
expect(receivedMessages).toContain('server-message-2')
|
|
1090
|
+
expect(receivedMessages).toContain('received: client-message-1')
|
|
1091
|
+
expect(receivedMessages).toContain('received: client-message-2')
|
|
1092
|
+
clientWs.close()
|
|
1093
|
+
resolve()
|
|
1094
|
+
}
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
clientWs.on('error', (err) => {
|
|
1098
|
+
clientWs.close()
|
|
1099
|
+
reject(err)
|
|
1100
|
+
})
|
|
1101
|
+
})
|
|
1102
|
+
} finally {
|
|
1103
|
+
targetWss.close()
|
|
1104
|
+
targetServer.close()
|
|
1105
|
+
}
|
|
1106
|
+
})
|
|
1107
|
+
})
|
|
1108
|
+
|
|
1109
|
+
it('Should not proxy WebSocket when disabled', async () => {
|
|
1110
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1111
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1112
|
+
const targetPort = getPort()
|
|
1113
|
+
const proxyPort = getPort()
|
|
1114
|
+
|
|
1115
|
+
// Create target WebSocket server
|
|
1116
|
+
const targetServer = createHttpServer()
|
|
1117
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1118
|
+
|
|
1119
|
+
await new Promise<void>((resolve) => {
|
|
1120
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1121
|
+
})
|
|
1122
|
+
|
|
1123
|
+
try {
|
|
1124
|
+
// Set up proxy WITHOUT WebSocket support
|
|
1125
|
+
await proxyManager.addProxy({
|
|
1126
|
+
sourceBaseUrl: '/ws',
|
|
1127
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1128
|
+
sourcePort: proxyPort,
|
|
1129
|
+
enableWebsockets: false,
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
// Try to connect WebSocket client through proxy
|
|
1133
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1134
|
+
|
|
1135
|
+
await new Promise<void>((resolve) => {
|
|
1136
|
+
clientWs.on('error', (error) => {
|
|
1137
|
+
// Should fail because WebSocket is not enabled
|
|
1138
|
+
expect(error).toBeDefined()
|
|
1139
|
+
clientWs.close()
|
|
1140
|
+
resolve()
|
|
1141
|
+
})
|
|
1142
|
+
|
|
1143
|
+
clientWs.on('open', () => {
|
|
1144
|
+
// Should not open successfully
|
|
1145
|
+
clientWs.close()
|
|
1146
|
+
resolve()
|
|
1147
|
+
})
|
|
1148
|
+
})
|
|
1149
|
+
} finally {
|
|
1150
|
+
targetWss.close()
|
|
1151
|
+
targetServer.close()
|
|
1152
|
+
}
|
|
1153
|
+
})
|
|
1154
|
+
})
|
|
1155
|
+
|
|
1156
|
+
it('Should preserve WebSocket subprotocols', async () => {
|
|
1157
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1158
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1159
|
+
const targetPort = getPort()
|
|
1160
|
+
const proxyPort = getPort()
|
|
1161
|
+
|
|
1162
|
+
let receivedProtocol: string | undefined
|
|
1163
|
+
|
|
1164
|
+
// Create target WebSocket server
|
|
1165
|
+
const targetServer = createHttpServer()
|
|
1166
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1167
|
+
|
|
1168
|
+
targetWss.on('connection', (ws, req) => {
|
|
1169
|
+
receivedProtocol = req.headers['sec-websocket-protocol'] as string
|
|
1170
|
+
ws.send('connected')
|
|
1171
|
+
})
|
|
1172
|
+
|
|
1173
|
+
await new Promise<void>((resolve) => {
|
|
1174
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1175
|
+
})
|
|
1176
|
+
|
|
1177
|
+
try {
|
|
1178
|
+
await proxyManager.addProxy({
|
|
1179
|
+
sourceBaseUrl: '/ws',
|
|
1180
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1181
|
+
sourcePort: proxyPort,
|
|
1182
|
+
enableWebsockets: true,
|
|
1183
|
+
})
|
|
1184
|
+
|
|
1185
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`, ['v1.chat', 'v2.chat'])
|
|
1186
|
+
|
|
1187
|
+
await new Promise<void>((resolve, reject) => {
|
|
1188
|
+
clientWs.on('open', () => {
|
|
1189
|
+
// Wait for server message
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
clientWs.on('message', (data: RawData) => {
|
|
1193
|
+
const message = Buffer.isBuffer(data)
|
|
1194
|
+
? data.toString('utf8')
|
|
1195
|
+
: Array.isArray(data)
|
|
1196
|
+
? Buffer.concat(data).toString('utf8')
|
|
1197
|
+
: Buffer.from(data).toString('utf8')
|
|
1198
|
+
expect(message).toBe('connected')
|
|
1199
|
+
expect(receivedProtocol).toContain('v1.chat')
|
|
1200
|
+
clientWs.close()
|
|
1201
|
+
resolve()
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
clientWs.on('error', (err) => {
|
|
1205
|
+
clientWs.close()
|
|
1206
|
+
reject(err)
|
|
1207
|
+
})
|
|
1208
|
+
})
|
|
1209
|
+
} finally {
|
|
1210
|
+
targetWss.close()
|
|
1211
|
+
targetServer.close()
|
|
1212
|
+
}
|
|
1213
|
+
})
|
|
1214
|
+
})
|
|
1215
|
+
|
|
1216
|
+
it('Should handle WebSocket path rewriting', async () => {
|
|
1217
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1218
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1219
|
+
const targetPort = getPort()
|
|
1220
|
+
const proxyPort = getPort()
|
|
1221
|
+
|
|
1222
|
+
let receivedPath: string | undefined
|
|
1223
|
+
|
|
1224
|
+
// Create target WebSocket server
|
|
1225
|
+
const targetServer = createHttpServer()
|
|
1226
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1227
|
+
|
|
1228
|
+
targetWss.on('connection', (ws, req) => {
|
|
1229
|
+
receivedPath = req.url
|
|
1230
|
+
ws.send('connected')
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
await new Promise<void>((resolve) => {
|
|
1234
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1235
|
+
})
|
|
1236
|
+
|
|
1237
|
+
try {
|
|
1238
|
+
await proxyManager.addProxy({
|
|
1239
|
+
sourceBaseUrl: '/old/ws',
|
|
1240
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1241
|
+
sourcePort: proxyPort,
|
|
1242
|
+
enableWebsockets: true,
|
|
1243
|
+
pathRewrite: (path) => path.replace('/chat', '/api/chat'),
|
|
1244
|
+
})
|
|
1245
|
+
|
|
1246
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/old/ws/chat`)
|
|
1247
|
+
|
|
1248
|
+
await new Promise<void>((resolve, reject) => {
|
|
1249
|
+
clientWs.on('message', (data: RawData) => {
|
|
1250
|
+
const message = Buffer.isBuffer(data)
|
|
1251
|
+
? data.toString('utf8')
|
|
1252
|
+
: Array.isArray(data)
|
|
1253
|
+
? Buffer.concat(data).toString('utf8')
|
|
1254
|
+
: Buffer.from(data).toString('utf8')
|
|
1255
|
+
expect(message).toBe('connected')
|
|
1256
|
+
expect(receivedPath).toBe('/api/chat')
|
|
1257
|
+
clientWs.close()
|
|
1258
|
+
resolve()
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1261
|
+
clientWs.on('error', (err) => {
|
|
1262
|
+
clientWs.close()
|
|
1263
|
+
reject(err)
|
|
1264
|
+
})
|
|
1265
|
+
})
|
|
1266
|
+
} finally {
|
|
1267
|
+
targetWss.close()
|
|
1268
|
+
targetServer.close()
|
|
1269
|
+
}
|
|
1270
|
+
})
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
it('Should handle WebSocket connection closure from client', async () => {
|
|
1274
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1275
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1276
|
+
const targetPort = getPort()
|
|
1277
|
+
const proxyPort = getPort()
|
|
1278
|
+
|
|
1279
|
+
let serverClosed = false
|
|
1280
|
+
|
|
1281
|
+
// Create target WebSocket server
|
|
1282
|
+
const targetServer = createHttpServer()
|
|
1283
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1284
|
+
|
|
1285
|
+
targetWss.on('connection', (ws) => {
|
|
1286
|
+
ws.on('close', () => {
|
|
1287
|
+
serverClosed = true
|
|
1288
|
+
})
|
|
1289
|
+
})
|
|
1290
|
+
|
|
1291
|
+
await new Promise<void>((resolve) => {
|
|
1292
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1293
|
+
})
|
|
1294
|
+
|
|
1295
|
+
try {
|
|
1296
|
+
await proxyManager.addProxy({
|
|
1297
|
+
sourceBaseUrl: '/ws',
|
|
1298
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1299
|
+
sourcePort: proxyPort,
|
|
1300
|
+
enableWebsockets: true,
|
|
1301
|
+
})
|
|
1302
|
+
|
|
1303
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1304
|
+
|
|
1305
|
+
await new Promise<void>((resolve, reject) => {
|
|
1306
|
+
clientWs.on('open', () => {
|
|
1307
|
+
clientWs.close()
|
|
1308
|
+
})
|
|
1309
|
+
|
|
1310
|
+
clientWs.on('close', () => {
|
|
1311
|
+
// Give server time to process close event
|
|
1312
|
+
setTimeout(() => {
|
|
1313
|
+
expect(serverClosed).toBe(true)
|
|
1314
|
+
resolve()
|
|
1315
|
+
}, 100)
|
|
1316
|
+
})
|
|
1317
|
+
|
|
1318
|
+
clientWs.on('error', (err) => {
|
|
1319
|
+
clientWs.close()
|
|
1320
|
+
reject(err)
|
|
1321
|
+
})
|
|
1322
|
+
})
|
|
1323
|
+
} finally {
|
|
1324
|
+
targetWss.close()
|
|
1325
|
+
targetServer.close()
|
|
1326
|
+
}
|
|
1327
|
+
})
|
|
1328
|
+
})
|
|
1329
|
+
|
|
1330
|
+
it('Should handle WebSocket connection closure from server', async () => {
|
|
1331
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1332
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1333
|
+
const targetPort = getPort()
|
|
1334
|
+
const proxyPort = getPort()
|
|
1335
|
+
|
|
1336
|
+
// Create target WebSocket server
|
|
1337
|
+
const targetServer = createHttpServer()
|
|
1338
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1339
|
+
|
|
1340
|
+
targetWss.on('connection', (ws) => {
|
|
1341
|
+
// Close connection immediately after opening
|
|
1342
|
+
ws.send('goodbye')
|
|
1343
|
+
setTimeout(() => ws.close(), 50)
|
|
1344
|
+
})
|
|
1345
|
+
|
|
1346
|
+
await new Promise<void>((resolve) => {
|
|
1347
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1348
|
+
})
|
|
1349
|
+
|
|
1350
|
+
try {
|
|
1351
|
+
await proxyManager.addProxy({
|
|
1352
|
+
sourceBaseUrl: '/ws',
|
|
1353
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1354
|
+
sourcePort: proxyPort,
|
|
1355
|
+
enableWebsockets: true,
|
|
1356
|
+
})
|
|
1357
|
+
|
|
1358
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1359
|
+
|
|
1360
|
+
await new Promise<void>((resolve, reject) => {
|
|
1361
|
+
let messageReceived = false
|
|
1362
|
+
|
|
1363
|
+
clientWs.on('message', (data: RawData) => {
|
|
1364
|
+
const message = Buffer.isBuffer(data)
|
|
1365
|
+
? data.toString('utf8')
|
|
1366
|
+
: Array.isArray(data)
|
|
1367
|
+
? Buffer.concat(data).toString('utf8')
|
|
1368
|
+
: Buffer.from(data).toString('utf8')
|
|
1369
|
+
expect(message).toBe('goodbye')
|
|
1370
|
+
messageReceived = true
|
|
1371
|
+
})
|
|
1372
|
+
|
|
1373
|
+
clientWs.on('close', () => {
|
|
1374
|
+
expect(messageReceived).toBe(true)
|
|
1375
|
+
resolve()
|
|
1376
|
+
})
|
|
1377
|
+
|
|
1378
|
+
clientWs.on('error', (err) => {
|
|
1379
|
+
clientWs.close()
|
|
1380
|
+
reject(err)
|
|
1381
|
+
})
|
|
1382
|
+
})
|
|
1383
|
+
} finally {
|
|
1384
|
+
targetWss.close()
|
|
1385
|
+
targetServer.close()
|
|
1386
|
+
}
|
|
1387
|
+
})
|
|
1388
|
+
})
|
|
1389
|
+
|
|
1390
|
+
it('Should emit onWebSocketProxyFailed event on error', async () => {
|
|
1391
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1392
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1393
|
+
const proxyPort = getPort()
|
|
1394
|
+
const unavailablePort = getPort()
|
|
1395
|
+
|
|
1396
|
+
let failedEvent: { from: string; to: string; error: unknown } | undefined
|
|
1397
|
+
|
|
1398
|
+
proxyManager.subscribe('onWebSocketProxyFailed', (event) => {
|
|
1399
|
+
failedEvent = event
|
|
1400
|
+
})
|
|
1401
|
+
|
|
1402
|
+
await proxyManager.addProxy({
|
|
1403
|
+
sourceBaseUrl: '/ws',
|
|
1404
|
+
targetBaseUrl: `http://localhost:${unavailablePort}`,
|
|
1405
|
+
sourcePort: proxyPort,
|
|
1406
|
+
enableWebsockets: true,
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1410
|
+
|
|
1411
|
+
await new Promise<void>((resolve) => {
|
|
1412
|
+
clientWs.on('error', () => {
|
|
1413
|
+
// Expected error
|
|
1414
|
+
})
|
|
1415
|
+
|
|
1416
|
+
clientWs.on('close', () => {
|
|
1417
|
+
// Give time for event to be emitted
|
|
1418
|
+
setTimeout(() => {
|
|
1419
|
+
expect(failedEvent).toBeDefined()
|
|
1420
|
+
expect(failedEvent?.from).toBe('/ws')
|
|
1421
|
+
expect(failedEvent?.to).toContain(`http://localhost:${unavailablePort}`)
|
|
1422
|
+
resolve()
|
|
1423
|
+
}, 100)
|
|
1424
|
+
})
|
|
1425
|
+
})
|
|
1426
|
+
})
|
|
1427
|
+
})
|
|
1428
|
+
|
|
1429
|
+
it('Should handle large WebSocket messages', async () => {
|
|
1430
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1431
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1432
|
+
const targetPort = getPort()
|
|
1433
|
+
const proxyPort = getPort()
|
|
1434
|
+
|
|
1435
|
+
// Create target WebSocket server
|
|
1436
|
+
const targetServer = createHttpServer()
|
|
1437
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1438
|
+
|
|
1439
|
+
targetWss.on('connection', (ws) => {
|
|
1440
|
+
ws.on('message', (data: RawData) => {
|
|
1441
|
+
// Echo back the large message
|
|
1442
|
+
ws.send(data)
|
|
1443
|
+
})
|
|
1444
|
+
})
|
|
1445
|
+
|
|
1446
|
+
await new Promise<void>((resolve) => {
|
|
1447
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1448
|
+
})
|
|
1449
|
+
|
|
1450
|
+
try {
|
|
1451
|
+
await proxyManager.addProxy({
|
|
1452
|
+
sourceBaseUrl: '/ws',
|
|
1453
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1454
|
+
sourcePort: proxyPort,
|
|
1455
|
+
enableWebsockets: true,
|
|
1456
|
+
})
|
|
1457
|
+
|
|
1458
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1459
|
+
|
|
1460
|
+
// Create a large message (1MB)
|
|
1461
|
+
const largeMessage = 'x'.repeat(1024 * 1024)
|
|
1462
|
+
|
|
1463
|
+
await new Promise<void>((resolve, reject) => {
|
|
1464
|
+
clientWs.on('open', () => {
|
|
1465
|
+
clientWs.send(largeMessage)
|
|
1466
|
+
})
|
|
1467
|
+
|
|
1468
|
+
clientWs.on('message', (data: RawData) => {
|
|
1469
|
+
const dataStr = Buffer.isBuffer(data)
|
|
1470
|
+
? data.toString('utf8')
|
|
1471
|
+
: Array.isArray(data)
|
|
1472
|
+
? Buffer.concat(data).toString('utf8')
|
|
1473
|
+
: Buffer.from(data).toString('utf8')
|
|
1474
|
+
expect(dataStr.length).toBe(largeMessage.length)
|
|
1475
|
+
expect(dataStr).toBe(largeMessage)
|
|
1476
|
+
clientWs.close()
|
|
1477
|
+
resolve()
|
|
1478
|
+
})
|
|
1479
|
+
|
|
1480
|
+
clientWs.on('error', (err) => {
|
|
1481
|
+
clientWs.close()
|
|
1482
|
+
reject(err)
|
|
1483
|
+
})
|
|
1484
|
+
})
|
|
1485
|
+
} finally {
|
|
1486
|
+
targetWss.close()
|
|
1487
|
+
targetServer.close()
|
|
1488
|
+
}
|
|
1489
|
+
})
|
|
1490
|
+
})
|
|
1491
|
+
|
|
1492
|
+
it('Should handle binary WebSocket messages', async () => {
|
|
1493
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1494
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1495
|
+
const targetPort = getPort()
|
|
1496
|
+
const proxyPort = getPort()
|
|
1497
|
+
|
|
1498
|
+
// Create target WebSocket server
|
|
1499
|
+
const targetServer = createHttpServer()
|
|
1500
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1501
|
+
|
|
1502
|
+
targetWss.on('connection', (ws) => {
|
|
1503
|
+
ws.on('message', (data: RawData) => {
|
|
1504
|
+
// Echo back the binary data
|
|
1505
|
+
ws.send(data)
|
|
1506
|
+
})
|
|
1507
|
+
})
|
|
1508
|
+
|
|
1509
|
+
await new Promise<void>((resolve) => {
|
|
1510
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1511
|
+
})
|
|
1512
|
+
|
|
1513
|
+
try {
|
|
1514
|
+
await proxyManager.addProxy({
|
|
1515
|
+
sourceBaseUrl: '/ws',
|
|
1516
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1517
|
+
sourcePort: proxyPort,
|
|
1518
|
+
enableWebsockets: true,
|
|
1519
|
+
})
|
|
1520
|
+
|
|
1521
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1522
|
+
|
|
1523
|
+
// Create binary data
|
|
1524
|
+
const binaryData = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0xff, 0xfe, 0xfd])
|
|
1525
|
+
|
|
1526
|
+
await new Promise<void>((resolve, reject) => {
|
|
1527
|
+
clientWs.on('open', () => {
|
|
1528
|
+
clientWs.send(binaryData)
|
|
1529
|
+
})
|
|
1530
|
+
|
|
1531
|
+
clientWs.on('message', (data) => {
|
|
1532
|
+
expect(Buffer.isBuffer(data)).toBe(true)
|
|
1533
|
+
expect(Buffer.compare(data as Buffer, binaryData)).toBe(0)
|
|
1534
|
+
clientWs.close()
|
|
1535
|
+
resolve()
|
|
1536
|
+
})
|
|
1537
|
+
|
|
1538
|
+
clientWs.on('error', (err) => {
|
|
1539
|
+
clientWs.close()
|
|
1540
|
+
reject(err)
|
|
1541
|
+
})
|
|
1542
|
+
})
|
|
1543
|
+
} finally {
|
|
1544
|
+
targetWss.close()
|
|
1545
|
+
targetServer.close()
|
|
1546
|
+
}
|
|
1547
|
+
})
|
|
1548
|
+
})
|
|
1549
|
+
|
|
1550
|
+
it('Should apply header transformations to WebSocket upgrade', async () => {
|
|
1551
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1552
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1553
|
+
const targetPort = getPort()
|
|
1554
|
+
const proxyPort = getPort()
|
|
1555
|
+
|
|
1556
|
+
let receivedHeaders: Record<string, string | string[] | undefined> = {}
|
|
1557
|
+
|
|
1558
|
+
// Create target WebSocket server
|
|
1559
|
+
const targetServer = createHttpServer()
|
|
1560
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1561
|
+
|
|
1562
|
+
targetWss.on('connection', (ws, req) => {
|
|
1563
|
+
receivedHeaders = { ...req.headers }
|
|
1564
|
+
ws.send('connected')
|
|
1565
|
+
})
|
|
1566
|
+
|
|
1567
|
+
await new Promise<void>((resolve) => {
|
|
1568
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1569
|
+
})
|
|
1570
|
+
|
|
1571
|
+
try {
|
|
1572
|
+
await proxyManager.addProxy({
|
|
1573
|
+
sourceBaseUrl: '/ws',
|
|
1574
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1575
|
+
sourcePort: proxyPort,
|
|
1576
|
+
enableWebsockets: true,
|
|
1577
|
+
headers: () => ({
|
|
1578
|
+
'X-Custom-Header': 'custom-value',
|
|
1579
|
+
'X-Auth-Token': 'bearer-token',
|
|
1580
|
+
}),
|
|
1581
|
+
})
|
|
1582
|
+
|
|
1583
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1584
|
+
|
|
1585
|
+
await new Promise<void>((resolve, reject) => {
|
|
1586
|
+
clientWs.on('message', (data: RawData) => {
|
|
1587
|
+
const message = Buffer.isBuffer(data)
|
|
1588
|
+
? data.toString('utf8')
|
|
1589
|
+
: Array.isArray(data)
|
|
1590
|
+
? Buffer.concat(data).toString('utf8')
|
|
1591
|
+
: Buffer.from(data).toString('utf8')
|
|
1592
|
+
expect(message).toBe('connected')
|
|
1593
|
+
expect(receivedHeaders['x-custom-header']).toBe('custom-value')
|
|
1594
|
+
expect(receivedHeaders['x-auth-token']).toBe('bearer-token')
|
|
1595
|
+
clientWs.close()
|
|
1596
|
+
resolve()
|
|
1597
|
+
})
|
|
1598
|
+
|
|
1599
|
+
clientWs.on('error', (err) => {
|
|
1600
|
+
clientWs.close()
|
|
1601
|
+
reject(err)
|
|
1602
|
+
})
|
|
1603
|
+
})
|
|
1604
|
+
} finally {
|
|
1605
|
+
targetWss.close()
|
|
1606
|
+
targetServer.close()
|
|
1607
|
+
}
|
|
1608
|
+
})
|
|
1609
|
+
})
|
|
1610
|
+
|
|
1611
|
+
it('Should handle WebSocket upgrade timeout', async () => {
|
|
1612
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1613
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1614
|
+
const targetPort = getPort()
|
|
1615
|
+
const proxyPort = getPort()
|
|
1616
|
+
|
|
1617
|
+
// Create a server that delays WebSocket upgrade longer than timeout
|
|
1618
|
+
const targetServer = createHttpServer()
|
|
1619
|
+
|
|
1620
|
+
targetServer.on('upgrade', () => {
|
|
1621
|
+
// Don't respond to upgrade - let it timeout
|
|
1622
|
+
})
|
|
1623
|
+
|
|
1624
|
+
await new Promise<void>((resolve) => {
|
|
1625
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1626
|
+
})
|
|
1627
|
+
|
|
1628
|
+
try {
|
|
1629
|
+
await proxyManager.addProxy({
|
|
1630
|
+
sourceBaseUrl: '/ws',
|
|
1631
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1632
|
+
sourcePort: proxyPort,
|
|
1633
|
+
enableWebsockets: true,
|
|
1634
|
+
timeout: 200, // Short timeout
|
|
1635
|
+
})
|
|
1636
|
+
|
|
1637
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1638
|
+
|
|
1639
|
+
await new Promise<void>((resolve, reject) => {
|
|
1640
|
+
const timeoutId = setTimeout(() => {
|
|
1641
|
+
// If we haven't resolved by now, the test failed
|
|
1642
|
+
reject(new Error('Test timeout - WebSocket did not fail as expected'))
|
|
1643
|
+
}, 2000)
|
|
1644
|
+
|
|
1645
|
+
clientWs.on('error', () => {
|
|
1646
|
+
// Expected error
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
clientWs.on('close', () => {
|
|
1650
|
+
// Give time for event to be emitted
|
|
1651
|
+
setTimeout(() => {
|
|
1652
|
+
clearTimeout(timeoutId)
|
|
1653
|
+
// The timeout test might not always trigger the event reliably
|
|
1654
|
+
// Just verify the connection was closed
|
|
1655
|
+
resolve()
|
|
1656
|
+
}, 300)
|
|
1657
|
+
})
|
|
1658
|
+
})
|
|
1659
|
+
} finally {
|
|
1660
|
+
targetServer.close()
|
|
1661
|
+
}
|
|
1662
|
+
})
|
|
1663
|
+
}, 10000)
|
|
1664
|
+
|
|
1665
|
+
it('Should handle unusual path in WebSocket upgrade with PathHelper', async () => {
|
|
1666
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1667
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1668
|
+
const targetPort = getPort()
|
|
1669
|
+
const proxyPort = getPort()
|
|
1670
|
+
|
|
1671
|
+
let failedEvent: { from: string; to: string; error: unknown } | undefined
|
|
1672
|
+
|
|
1673
|
+
proxyManager.subscribe('onWebSocketProxyFailed', (event) => {
|
|
1674
|
+
failedEvent = event
|
|
1675
|
+
})
|
|
1676
|
+
|
|
1677
|
+
// Create target server that will receive the WebSocket connection
|
|
1678
|
+
const targetServer = createHttpServer()
|
|
1679
|
+
const targetWss = new WebSocketServer({ server: targetServer })
|
|
1680
|
+
|
|
1681
|
+
let connectionReceived = false
|
|
1682
|
+
targetWss.on('connection', () => {
|
|
1683
|
+
connectionReceived = true
|
|
1684
|
+
})
|
|
1685
|
+
|
|
1686
|
+
await new Promise<void>((resolve) => {
|
|
1687
|
+
targetServer.listen(targetPort, () => resolve())
|
|
1688
|
+
})
|
|
1689
|
+
|
|
1690
|
+
try {
|
|
1691
|
+
await proxyManager.addProxy({
|
|
1692
|
+
sourceBaseUrl: '/ws',
|
|
1693
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1694
|
+
sourcePort: proxyPort,
|
|
1695
|
+
enableWebsockets: true,
|
|
1696
|
+
// PathHelper.joinUrl now properly handles edge cases, making previously
|
|
1697
|
+
// "invalid" URLs valid. The WebSocket connection succeeds.
|
|
1698
|
+
pathRewrite: () => '://unusual-but-valid',
|
|
1699
|
+
})
|
|
1700
|
+
|
|
1701
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws/test`)
|
|
1702
|
+
|
|
1703
|
+
await new Promise<void>((resolve) => {
|
|
1704
|
+
clientWs.on('open', () => {
|
|
1705
|
+
// Connection successful thanks to robust URL handling
|
|
1706
|
+
clientWs.close()
|
|
1707
|
+
})
|
|
1708
|
+
|
|
1709
|
+
clientWs.on('close', () => {
|
|
1710
|
+
// Give time for event to be processed
|
|
1711
|
+
setTimeout(() => {
|
|
1712
|
+
// No error event should be emitted since the connection succeeded
|
|
1713
|
+
expect(failedEvent).toBeUndefined()
|
|
1714
|
+
expect(connectionReceived).toBe(true)
|
|
1715
|
+
resolve()
|
|
1716
|
+
}, 100)
|
|
1717
|
+
})
|
|
1718
|
+
})
|
|
1719
|
+
} finally {
|
|
1720
|
+
targetWss.close()
|
|
1721
|
+
targetServer.close()
|
|
1722
|
+
}
|
|
1723
|
+
})
|
|
1724
|
+
})
|
|
1725
|
+
|
|
1726
|
+
it('Should handle WebSocket header value types (number, array, undefined)', async () => {
|
|
1727
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1728
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1729
|
+
const targetPort = getPort()
|
|
1730
|
+
const proxyPort = getPort()
|
|
1731
|
+
|
|
1732
|
+
let receivedHeaders: Record<string, string | string[] | undefined> = {}
|
|
1733
|
+
|
|
1734
|
+
// Create target WebSocket server
|
|
1735
|
+
const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws, req) => {
|
|
1736
|
+
receivedHeaders = { ...req.headers }
|
|
1737
|
+
ws.send('connected')
|
|
1738
|
+
})
|
|
1739
|
+
|
|
1740
|
+
try {
|
|
1741
|
+
await proxyManager.addProxy({
|
|
1742
|
+
sourceBaseUrl: '/ws',
|
|
1743
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1744
|
+
sourcePort: proxyPort,
|
|
1745
|
+
enableWebsockets: true,
|
|
1746
|
+
headers: () => ({
|
|
1747
|
+
'X-Number-Header': 42 as unknown as string,
|
|
1748
|
+
'X-Array-Header': ['val1', 'val2'] as unknown as string,
|
|
1749
|
+
'X-Undefined-Header': undefined as unknown as string,
|
|
1750
|
+
'X-String-Header': 'string-value',
|
|
1751
|
+
}),
|
|
1752
|
+
})
|
|
1753
|
+
|
|
1754
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1755
|
+
|
|
1756
|
+
await new Promise<void>((resolve, reject) => {
|
|
1757
|
+
clientWs.on('message', (data: RawData) => {
|
|
1758
|
+
expect(rawDataToString(data)).toBe('connected')
|
|
1759
|
+
expect(receivedHeaders['x-number-header']).toBe('42')
|
|
1760
|
+
expect(receivedHeaders['x-array-header']).toBe('val1, val2')
|
|
1761
|
+
expect(receivedHeaders['x-string-header']).toBe('string-value')
|
|
1762
|
+
expect(receivedHeaders['x-undefined-header']).toBeUndefined()
|
|
1763
|
+
clientWs.close()
|
|
1764
|
+
resolve()
|
|
1765
|
+
})
|
|
1766
|
+
|
|
1767
|
+
clientWs.on('error', (err) => {
|
|
1768
|
+
clientWs.close()
|
|
1769
|
+
reject(err)
|
|
1770
|
+
})
|
|
1771
|
+
})
|
|
1772
|
+
} finally {
|
|
1773
|
+
targetWss.close()
|
|
1774
|
+
targetServer.close()
|
|
1775
|
+
}
|
|
1776
|
+
})
|
|
1777
|
+
})
|
|
1778
|
+
|
|
1779
|
+
it('Should handle WebSocket connections with query parameters', async () => {
|
|
1780
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1781
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1782
|
+
const targetPort = getPort()
|
|
1783
|
+
const proxyPort = getPort()
|
|
1784
|
+
|
|
1785
|
+
let receivedUrl: string | undefined
|
|
1786
|
+
|
|
1787
|
+
const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws, req) => {
|
|
1788
|
+
receivedUrl = req.url
|
|
1789
|
+
ws.send('connected')
|
|
1790
|
+
})
|
|
1791
|
+
|
|
1792
|
+
try {
|
|
1793
|
+
await proxyManager.addProxy({
|
|
1794
|
+
sourceBaseUrl: '/ws',
|
|
1795
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1796
|
+
sourcePort: proxyPort,
|
|
1797
|
+
enableWebsockets: true,
|
|
1798
|
+
})
|
|
1799
|
+
|
|
1800
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws/channel?token=abc123&user=test`)
|
|
1801
|
+
|
|
1802
|
+
await new Promise<void>((resolve, reject) => {
|
|
1803
|
+
clientWs.on('message', (data: RawData) => {
|
|
1804
|
+
expect(rawDataToString(data)).toBe('connected')
|
|
1805
|
+
expect(receivedUrl).toBe('/channel?token=abc123&user=test')
|
|
1806
|
+
clientWs.close()
|
|
1807
|
+
resolve()
|
|
1808
|
+
})
|
|
1809
|
+
|
|
1810
|
+
clientWs.on('error', (err) => {
|
|
1811
|
+
clientWs.close()
|
|
1812
|
+
reject(err)
|
|
1813
|
+
})
|
|
1814
|
+
})
|
|
1815
|
+
} finally {
|
|
1816
|
+
targetWss.close()
|
|
1817
|
+
targetServer.close()
|
|
1818
|
+
}
|
|
1819
|
+
})
|
|
1820
|
+
})
|
|
1821
|
+
|
|
1822
|
+
it('Should handle WebSocket close with code and reason', async () => {
|
|
1823
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1824
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1825
|
+
const targetPort = getPort()
|
|
1826
|
+
const proxyPort = getPort()
|
|
1827
|
+
|
|
1828
|
+
const { server: targetServer, wss: targetWss } = await createWsTestServer(targetPort, (ws) => {
|
|
1829
|
+
ws.close(1008, 'Policy Violation')
|
|
1830
|
+
})
|
|
1831
|
+
|
|
1832
|
+
try {
|
|
1833
|
+
await proxyManager.addProxy({
|
|
1834
|
+
sourceBaseUrl: '/ws',
|
|
1835
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1836
|
+
sourcePort: proxyPort,
|
|
1837
|
+
enableWebsockets: true,
|
|
1838
|
+
})
|
|
1839
|
+
|
|
1840
|
+
const clientWs = new WebSocket(`ws://127.0.0.1:${proxyPort}/ws`)
|
|
1841
|
+
|
|
1842
|
+
await new Promise<void>((resolve) => {
|
|
1843
|
+
clientWs.on('close', (code, reason) => {
|
|
1844
|
+
expect(code).toBe(1008)
|
|
1845
|
+
expect(reason.toString()).toBe('Policy Violation')
|
|
1846
|
+
resolve()
|
|
1847
|
+
})
|
|
1848
|
+
})
|
|
1849
|
+
} finally {
|
|
1850
|
+
targetWss.close()
|
|
1851
|
+
targetServer.close()
|
|
1852
|
+
}
|
|
1853
|
+
})
|
|
1854
|
+
})
|
|
1855
|
+
})
|
|
1856
|
+
|
|
1857
|
+
describe('HTTP Methods and Status Codes', () => {
|
|
1858
|
+
it('Should handle PUT requests with body', async () => {
|
|
1859
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1860
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1861
|
+
const targetPort = getPort()
|
|
1862
|
+
const proxyPort = getPort()
|
|
1863
|
+
|
|
1864
|
+
const targetServer = await createEchoServer(targetPort, (req, res) => {
|
|
1865
|
+
const chunks: Buffer[] = []
|
|
1866
|
+
req.on('data', (chunk: Buffer) => chunks.push(Buffer.from(chunk)))
|
|
1867
|
+
req.on('end', () => {
|
|
1868
|
+
const body = Buffer.concat(chunks).toString('utf8')
|
|
1869
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
1870
|
+
res.end(JSON.stringify({ method: req.method, body: JSON.parse(body) }))
|
|
1871
|
+
})
|
|
1872
|
+
})
|
|
1873
|
+
|
|
1874
|
+
try {
|
|
1875
|
+
await proxyManager.addProxy({
|
|
1876
|
+
sourceBaseUrl: '/api',
|
|
1877
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1878
|
+
sourcePort: proxyPort,
|
|
1879
|
+
})
|
|
1880
|
+
|
|
1881
|
+
const testData = { id: 1, name: 'updated' }
|
|
1882
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource/1`, {
|
|
1883
|
+
method: 'PUT',
|
|
1884
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1885
|
+
body: JSON.stringify(testData),
|
|
1886
|
+
})
|
|
1887
|
+
|
|
1888
|
+
expect(result.status).toBe(200)
|
|
1889
|
+
const data = (await result.json()) as { method: string; body: typeof testData }
|
|
1890
|
+
expect(data.method).toBe('PUT')
|
|
1891
|
+
expect(data.body).toEqual(testData)
|
|
1892
|
+
} finally {
|
|
1893
|
+
targetServer.close()
|
|
1894
|
+
}
|
|
1895
|
+
})
|
|
1896
|
+
})
|
|
1897
|
+
|
|
1898
|
+
it('Should handle PATCH requests with body', async () => {
|
|
1899
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1900
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1901
|
+
const targetPort = getPort()
|
|
1902
|
+
const proxyPort = getPort()
|
|
1903
|
+
|
|
1904
|
+
const targetServer = await createEchoServer(targetPort, (req, res) => {
|
|
1905
|
+
const chunks: Buffer[] = []
|
|
1906
|
+
req.on('data', (chunk: Buffer) => chunks.push(Buffer.from(chunk)))
|
|
1907
|
+
req.on('end', () => {
|
|
1908
|
+
const body = Buffer.concat(chunks).toString('utf8')
|
|
1909
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
1910
|
+
res.end(JSON.stringify({ method: req.method, body: JSON.parse(body) }))
|
|
1911
|
+
})
|
|
1912
|
+
})
|
|
1913
|
+
|
|
1914
|
+
try {
|
|
1915
|
+
await proxyManager.addProxy({
|
|
1916
|
+
sourceBaseUrl: '/api',
|
|
1917
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1918
|
+
sourcePort: proxyPort,
|
|
1919
|
+
})
|
|
1920
|
+
|
|
1921
|
+
const testData = { name: 'patched' }
|
|
1922
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource/1`, {
|
|
1923
|
+
method: 'PATCH',
|
|
1924
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1925
|
+
body: JSON.stringify(testData),
|
|
1926
|
+
})
|
|
1927
|
+
|
|
1928
|
+
expect(result.status).toBe(200)
|
|
1929
|
+
const data = (await result.json()) as { method: string; body: typeof testData }
|
|
1930
|
+
expect(data.method).toBe('PATCH')
|
|
1931
|
+
expect(data.body).toEqual(testData)
|
|
1932
|
+
} finally {
|
|
1933
|
+
targetServer.close()
|
|
1934
|
+
}
|
|
1935
|
+
})
|
|
1936
|
+
})
|
|
1937
|
+
|
|
1938
|
+
it('Should handle DELETE requests', async () => {
|
|
1939
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1940
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1941
|
+
const targetPort = getPort()
|
|
1942
|
+
const proxyPort = getPort()
|
|
1943
|
+
|
|
1944
|
+
const targetServer = await createEchoServer(targetPort, (_req, res) => {
|
|
1945
|
+
res.writeHead(204, { 'Content-Type': 'application/json' })
|
|
1946
|
+
res.end()
|
|
1947
|
+
})
|
|
1948
|
+
|
|
1949
|
+
try {
|
|
1950
|
+
await proxyManager.addProxy({
|
|
1951
|
+
sourceBaseUrl: '/api',
|
|
1952
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1953
|
+
sourcePort: proxyPort,
|
|
1954
|
+
})
|
|
1955
|
+
|
|
1956
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource/1`, {
|
|
1957
|
+
method: 'DELETE',
|
|
1958
|
+
})
|
|
1959
|
+
|
|
1960
|
+
expect(result.status).toBe(204)
|
|
1961
|
+
} finally {
|
|
1962
|
+
targetServer.close()
|
|
1963
|
+
}
|
|
1964
|
+
})
|
|
1965
|
+
})
|
|
1966
|
+
|
|
1967
|
+
it('Should handle OPTIONS requests (CORS preflight)', async () => {
|
|
1968
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
1969
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
1970
|
+
const targetPort = getPort()
|
|
1971
|
+
const proxyPort = getPort()
|
|
1972
|
+
|
|
1973
|
+
const targetServer = await createEchoServer(targetPort, (_req, res) => {
|
|
1974
|
+
res.writeHead(200, {
|
|
1975
|
+
'Access-Control-Allow-Origin': '*',
|
|
1976
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
1977
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
1978
|
+
})
|
|
1979
|
+
res.end()
|
|
1980
|
+
})
|
|
1981
|
+
|
|
1982
|
+
try {
|
|
1983
|
+
await proxyManager.addProxy({
|
|
1984
|
+
sourceBaseUrl: '/api',
|
|
1985
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
1986
|
+
sourcePort: proxyPort,
|
|
1987
|
+
})
|
|
1988
|
+
|
|
1989
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource`, {
|
|
1990
|
+
method: 'OPTIONS',
|
|
1991
|
+
})
|
|
1992
|
+
|
|
1993
|
+
expect(result.status).toBe(200)
|
|
1994
|
+
expect(result.headers.get('access-control-allow-origin')).toBe('*')
|
|
1995
|
+
expect(result.headers.get('access-control-allow-methods')).toContain('DELETE')
|
|
1996
|
+
} finally {
|
|
1997
|
+
targetServer.close()
|
|
1998
|
+
}
|
|
1999
|
+
})
|
|
2000
|
+
})
|
|
2001
|
+
|
|
2002
|
+
it('Should handle HEAD requests', async () => {
|
|
2003
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
2004
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
2005
|
+
const targetPort = getPort()
|
|
2006
|
+
const proxyPort = getPort()
|
|
2007
|
+
|
|
2008
|
+
const targetServer = await createEchoServer(targetPort, (_req, res) => {
|
|
2009
|
+
res.writeHead(200, {
|
|
2010
|
+
'Content-Type': 'application/json',
|
|
2011
|
+
'Content-Length': '100',
|
|
2012
|
+
})
|
|
2013
|
+
res.end()
|
|
2014
|
+
})
|
|
2015
|
+
|
|
2016
|
+
try {
|
|
2017
|
+
await proxyManager.addProxy({
|
|
2018
|
+
sourceBaseUrl: '/api',
|
|
2019
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
2020
|
+
sourcePort: proxyPort,
|
|
2021
|
+
})
|
|
2022
|
+
|
|
2023
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/resource`, {
|
|
2024
|
+
method: 'HEAD',
|
|
2025
|
+
})
|
|
2026
|
+
|
|
2027
|
+
expect(result.status).toBe(200)
|
|
2028
|
+
expect(result.headers.get('content-type')).toBe('application/json')
|
|
2029
|
+
expect(result.headers.get('content-length')).toBe('100')
|
|
2030
|
+
} finally {
|
|
2031
|
+
targetServer.close()
|
|
2032
|
+
}
|
|
2033
|
+
})
|
|
2034
|
+
})
|
|
2035
|
+
|
|
2036
|
+
it('Should handle 4xx client error responses', async () => {
|
|
2037
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
2038
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
2039
|
+
const targetPort = getPort()
|
|
2040
|
+
const proxyPort = getPort()
|
|
2041
|
+
|
|
2042
|
+
const targetServer = await createEchoServer(targetPort, (_req, res) => {
|
|
2043
|
+
res.writeHead(404, { 'Content-Type': 'application/json' })
|
|
2044
|
+
res.end(JSON.stringify({ error: 'Not Found' }))
|
|
2045
|
+
})
|
|
2046
|
+
|
|
2047
|
+
try {
|
|
2048
|
+
await proxyManager.addProxy({
|
|
2049
|
+
sourceBaseUrl: '/api',
|
|
2050
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
2051
|
+
sourcePort: proxyPort,
|
|
2052
|
+
})
|
|
2053
|
+
|
|
2054
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/nonexistent`)
|
|
2055
|
+
|
|
2056
|
+
expect(result.status).toBe(404)
|
|
2057
|
+
const data = (await result.json()) as { error: string }
|
|
2058
|
+
expect(data.error).toBe('Not Found')
|
|
2059
|
+
} finally {
|
|
2060
|
+
targetServer.close()
|
|
2061
|
+
}
|
|
2062
|
+
})
|
|
2063
|
+
})
|
|
2064
|
+
|
|
2065
|
+
it('Should handle 5xx server error responses', async () => {
|
|
2066
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
2067
|
+
const proxyManager = injector.getInstance(ProxyManager)
|
|
2068
|
+
const targetPort = getPort()
|
|
2069
|
+
const proxyPort = getPort()
|
|
2070
|
+
|
|
2071
|
+
const targetServer = await createEchoServer(targetPort, (_req, res) => {
|
|
2072
|
+
res.writeHead(500, { 'Content-Type': 'application/json' })
|
|
2073
|
+
res.end(JSON.stringify({ error: 'Internal Server Error' }))
|
|
2074
|
+
})
|
|
2075
|
+
|
|
2076
|
+
try {
|
|
2077
|
+
await proxyManager.addProxy({
|
|
2078
|
+
sourceBaseUrl: '/api',
|
|
2079
|
+
targetBaseUrl: `http://localhost:${targetPort}`,
|
|
2080
|
+
sourcePort: proxyPort,
|
|
2081
|
+
})
|
|
2082
|
+
|
|
2083
|
+
const result = await fetch(`http://127.0.0.1:${proxyPort}/api/error`)
|
|
2084
|
+
|
|
2085
|
+
expect(result.status).toBe(500)
|
|
2086
|
+
const data = (await result.json()) as { error: string }
|
|
2087
|
+
expect(data.error).toBe('Internal Server Error')
|
|
2088
|
+
} finally {
|
|
2089
|
+
targetServer.close()
|
|
2090
|
+
}
|
|
2091
|
+
})
|
|
2092
|
+
})
|
|
2093
|
+
})
|
|
2094
|
+
})
|