@chidchanun/bcp 0.1.26 → 0.1.28
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 +151 -523
- package/docs/README.md +69 -82
- package/docs/production-hardening.md +152 -0
- package/docs/releases/0.1.27.md +350 -0
- package/docs/releases/0.1.28.md +134 -0
- package/docs/s3-storage.md +212 -55
- package/docs/storage-ecosystem.md +434 -0
- package/package.json +2 -1
- package/packages/client/src/server.ts +42 -2
- package/packages/server/src/hardening-proxy.ts +393 -0
- package/packages/server/src/production-hardening.ts +374 -0
- package/packages/server/src/standalone-production-runtime-v7.ts +299 -0
- package/packages/server/src/standalone-production-server.ts +1 -1
- package/packages/server/src/storage-ecosystem.ts +1326 -0
- package/packages/server/src/storage-s3-ecosystem.ts +947 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import * as http from "node:http";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
applyProductionHttpServerHardening,
|
|
5
|
+
closeHttpServerGracefully,
|
|
6
|
+
getProductionHardeningConfig,
|
|
7
|
+
type ProductionHardeningConfig,
|
|
8
|
+
} from "./production-hardening.js";
|
|
9
|
+
|
|
10
|
+
export interface HardeningProxyOptions {
|
|
11
|
+
port: number;
|
|
12
|
+
hostname: string;
|
|
13
|
+
upstreamPort: number;
|
|
14
|
+
upstreamHostname?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createHardeningProxy(
|
|
18
|
+
options: HardeningProxyOptions
|
|
19
|
+
) {
|
|
20
|
+
const upstreamHostname =
|
|
21
|
+
options.upstreamHostname ??
|
|
22
|
+
"127.0.0.1";
|
|
23
|
+
const config =
|
|
24
|
+
getProductionHardeningConfig();
|
|
25
|
+
let server:
|
|
26
|
+
http.Server | null =
|
|
27
|
+
null;
|
|
28
|
+
|
|
29
|
+
async function start(): Promise<void> {
|
|
30
|
+
if (server) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
server =
|
|
35
|
+
http.createServer(
|
|
36
|
+
(
|
|
37
|
+
req,
|
|
38
|
+
res
|
|
39
|
+
) => {
|
|
40
|
+
proxyRequest(
|
|
41
|
+
req,
|
|
42
|
+
res,
|
|
43
|
+
upstreamHostname,
|
|
44
|
+
options.upstreamPort,
|
|
45
|
+
config
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
applyProductionHttpServerHardening(
|
|
51
|
+
server,
|
|
52
|
+
config
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await listen(
|
|
56
|
+
server,
|
|
57
|
+
options.port,
|
|
58
|
+
options.hostname
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function stop(): Promise<void> {
|
|
63
|
+
if (!server) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const current =
|
|
68
|
+
server;
|
|
69
|
+
server =
|
|
70
|
+
null;
|
|
71
|
+
|
|
72
|
+
await closeHttpServerGracefully(
|
|
73
|
+
current,
|
|
74
|
+
{
|
|
75
|
+
timeoutMs:
|
|
76
|
+
config.shutdownTimeoutMs,
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
start,
|
|
83
|
+
stop,
|
|
84
|
+
config,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function proxyRequest(
|
|
89
|
+
req: http.IncomingMessage,
|
|
90
|
+
res: http.ServerResponse,
|
|
91
|
+
upstreamHostname: string,
|
|
92
|
+
upstreamPort: number,
|
|
93
|
+
config: ProductionHardeningConfig
|
|
94
|
+
): void {
|
|
95
|
+
const headers =
|
|
96
|
+
createForwardedHeaders(
|
|
97
|
+
req,
|
|
98
|
+
config.trustProxy
|
|
99
|
+
);
|
|
100
|
+
const upstream =
|
|
101
|
+
http.request({
|
|
102
|
+
hostname:
|
|
103
|
+
upstreamHostname,
|
|
104
|
+
port:
|
|
105
|
+
upstreamPort,
|
|
106
|
+
method:
|
|
107
|
+
req.method ??
|
|
108
|
+
"GET",
|
|
109
|
+
path:
|
|
110
|
+
req.url ??
|
|
111
|
+
"/",
|
|
112
|
+
headers,
|
|
113
|
+
timeout:
|
|
114
|
+
config.requestTimeoutMs,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
upstream.on(
|
|
118
|
+
"timeout",
|
|
119
|
+
() => {
|
|
120
|
+
upstream.destroy(
|
|
121
|
+
new Error(
|
|
122
|
+
"BCP Framework: upstream request timed out."
|
|
123
|
+
)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
upstream.on(
|
|
129
|
+
"response",
|
|
130
|
+
(upstreamResponse) => {
|
|
131
|
+
for (
|
|
132
|
+
const [
|
|
133
|
+
name,
|
|
134
|
+
value,
|
|
135
|
+
]
|
|
136
|
+
of Object.entries(
|
|
137
|
+
upstreamResponse.headers
|
|
138
|
+
)
|
|
139
|
+
) {
|
|
140
|
+
if (
|
|
141
|
+
value !== undefined
|
|
142
|
+
) {
|
|
143
|
+
res.setHeader(
|
|
144
|
+
name,
|
|
145
|
+
value
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
res.statusCode =
|
|
151
|
+
upstreamResponse.statusCode ??
|
|
152
|
+
502;
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
upstreamResponse.statusMessage
|
|
156
|
+
) {
|
|
157
|
+
res.statusMessage =
|
|
158
|
+
upstreamResponse.statusMessage;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
upstreamResponse.pipe(
|
|
162
|
+
res
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
upstream.on(
|
|
168
|
+
"error",
|
|
169
|
+
(error) => {
|
|
170
|
+
if (
|
|
171
|
+
res.headersSent
|
|
172
|
+
) {
|
|
173
|
+
res.destroy(
|
|
174
|
+
error
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
res.writeHead(
|
|
180
|
+
502,
|
|
181
|
+
{
|
|
182
|
+
"Content-Type":
|
|
183
|
+
"text/plain; charset=utf-8",
|
|
184
|
+
"Cache-Control":
|
|
185
|
+
"no-store",
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
res.end(
|
|
189
|
+
"Bad Gateway"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
req.on(
|
|
195
|
+
"aborted",
|
|
196
|
+
() => {
|
|
197
|
+
upstream.destroy();
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
req.on(
|
|
201
|
+
"error",
|
|
202
|
+
(error) => {
|
|
203
|
+
upstream.destroy(
|
|
204
|
+
error
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
req.pipe(
|
|
210
|
+
upstream
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createForwardedHeaders(
|
|
215
|
+
req: http.IncomingMessage,
|
|
216
|
+
trustProxy: boolean
|
|
217
|
+
): http.OutgoingHttpHeaders {
|
|
218
|
+
const headers:
|
|
219
|
+
http.OutgoingHttpHeaders = {
|
|
220
|
+
...req.headers,
|
|
221
|
+
};
|
|
222
|
+
const remoteAddress =
|
|
223
|
+
normalizeRemoteAddress(
|
|
224
|
+
req.socket.remoteAddress
|
|
225
|
+
);
|
|
226
|
+
const host =
|
|
227
|
+
firstHeader(
|
|
228
|
+
req.headers.host
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
if (!trustProxy) {
|
|
232
|
+
delete headers.forwarded;
|
|
233
|
+
delete headers[
|
|
234
|
+
"x-forwarded-for"
|
|
235
|
+
];
|
|
236
|
+
delete headers[
|
|
237
|
+
"x-forwarded-host"
|
|
238
|
+
];
|
|
239
|
+
delete headers[
|
|
240
|
+
"x-forwarded-proto"
|
|
241
|
+
];
|
|
242
|
+
delete headers[
|
|
243
|
+
"x-real-ip"
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (remoteAddress) {
|
|
248
|
+
const existingForwardedFor =
|
|
249
|
+
trustProxy
|
|
250
|
+
? firstHeader(
|
|
251
|
+
req.headers[
|
|
252
|
+
"x-forwarded-for"
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
: undefined;
|
|
256
|
+
|
|
257
|
+
headers[
|
|
258
|
+
"x-forwarded-for"
|
|
259
|
+
] =
|
|
260
|
+
existingForwardedFor
|
|
261
|
+
? `${existingForwardedFor}, ${remoteAddress}`
|
|
262
|
+
: remoteAddress;
|
|
263
|
+
headers[
|
|
264
|
+
"x-real-ip"
|
|
265
|
+
] =
|
|
266
|
+
trustProxy &&
|
|
267
|
+
firstHeader(
|
|
268
|
+
req.headers[
|
|
269
|
+
"x-real-ip"
|
|
270
|
+
]
|
|
271
|
+
)
|
|
272
|
+
? firstHeader(
|
|
273
|
+
req.headers[
|
|
274
|
+
"x-real-ip"
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
: remoteAddress;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (host) {
|
|
281
|
+
headers[
|
|
282
|
+
"x-forwarded-host"
|
|
283
|
+
] =
|
|
284
|
+
trustProxy &&
|
|
285
|
+
firstHeader(
|
|
286
|
+
req.headers[
|
|
287
|
+
"x-forwarded-host"
|
|
288
|
+
]
|
|
289
|
+
)
|
|
290
|
+
? firstHeader(
|
|
291
|
+
req.headers[
|
|
292
|
+
"x-forwarded-host"
|
|
293
|
+
]
|
|
294
|
+
)
|
|
295
|
+
: host;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
headers[
|
|
299
|
+
"x-forwarded-proto"
|
|
300
|
+
] =
|
|
301
|
+
trustProxy &&
|
|
302
|
+
firstHeader(
|
|
303
|
+
req.headers[
|
|
304
|
+
"x-forwarded-proto"
|
|
305
|
+
]
|
|
306
|
+
)
|
|
307
|
+
? firstHeader(
|
|
308
|
+
req.headers[
|
|
309
|
+
"x-forwarded-proto"
|
|
310
|
+
]
|
|
311
|
+
)
|
|
312
|
+
: "http";
|
|
313
|
+
|
|
314
|
+
return headers;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizeRemoteAddress(
|
|
318
|
+
value: string | undefined
|
|
319
|
+
): string | undefined {
|
|
320
|
+
if (!value) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return value.startsWith(
|
|
325
|
+
"::ffff:"
|
|
326
|
+
)
|
|
327
|
+
? value.slice(
|
|
328
|
+
"::ffff:".length
|
|
329
|
+
)
|
|
330
|
+
: value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function firstHeader(
|
|
334
|
+
value:
|
|
335
|
+
string |
|
|
336
|
+
string[] |
|
|
337
|
+
undefined
|
|
338
|
+
): string | undefined {
|
|
339
|
+
if (
|
|
340
|
+
Array.isArray(
|
|
341
|
+
value
|
|
342
|
+
)
|
|
343
|
+
) {
|
|
344
|
+
return value[0];
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return value;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function listen(
|
|
351
|
+
server: http.Server,
|
|
352
|
+
port: number,
|
|
353
|
+
hostname: string
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
return new Promise(
|
|
356
|
+
(
|
|
357
|
+
resolve,
|
|
358
|
+
reject
|
|
359
|
+
) => {
|
|
360
|
+
const onError =
|
|
361
|
+
(error: Error) => {
|
|
362
|
+
server.off(
|
|
363
|
+
"listening",
|
|
364
|
+
onListening
|
|
365
|
+
);
|
|
366
|
+
reject(
|
|
367
|
+
error
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
const onListening =
|
|
371
|
+
() => {
|
|
372
|
+
server.off(
|
|
373
|
+
"error",
|
|
374
|
+
onError
|
|
375
|
+
);
|
|
376
|
+
resolve();
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
server.once(
|
|
380
|
+
"error",
|
|
381
|
+
onError
|
|
382
|
+
);
|
|
383
|
+
server.once(
|
|
384
|
+
"listening",
|
|
385
|
+
onListening
|
|
386
|
+
);
|
|
387
|
+
server.listen(
|
|
388
|
+
port,
|
|
389
|
+
hostname
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
}
|