@fireemu/win32-x64 0.7.1 → 0.8.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 +1 -1
- package/bin/fireemu.exe +0 -0
- package/bin/runner-node/blocking-error.mjs +27 -12
- package/bin/runner-node/blocking-response.mjs +81 -20
- package/bin/runner-node/diagnostic-output.mjs +259 -0
- package/bin/runner-node/discovery.mjs +135 -0
- package/bin/runner-node/http-admission.mjs +145 -0
- package/bin/runner-node/http-lifecycle.mjs +44 -0
- package/bin/runner-node/index.mjs +667 -239
- package/bin/runner-node/invocation-error.mjs +29 -0
- package/bin/runner-node/output.mjs +204 -0
- package/bin/runner-node/protocol.mjs +280 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Local receiver limits, not deployment concurrency or Firebase quotas. The
|
|
2
|
+
// byte count is reserved decoded-body capacity, not heap/RSS or user retention.
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
export const HTTP_ADMISSION_LIMITS = Object.freeze({
|
|
6
|
+
requests: 1024,
|
|
7
|
+
bytes: 64 * 1024 * 1024,
|
|
8
|
+
bodyBytes: 32 * 1024 * 1024,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
function positiveLimit(value) {
|
|
12
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function secretMatches(presented, expected) {
|
|
16
|
+
if (typeof presented !== 'string') return false;
|
|
17
|
+
const a = Buffer.from(presented, 'utf8');
|
|
18
|
+
const b = Buffer.from(expected, 'utf8');
|
|
19
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rejection(response, status, message) {
|
|
23
|
+
// Do not read an unauthenticated/rejected body to keep the connection alive.
|
|
24
|
+
// This also prevents a subsequent pipelined request reusing unread bytes.
|
|
25
|
+
if (response.destroyed || response.writableEnded) return;
|
|
26
|
+
response.statusCode = status;
|
|
27
|
+
response.shouldKeepAlive = false;
|
|
28
|
+
response.setHeader('Connection', 'close');
|
|
29
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
30
|
+
response.end(message);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function reservedBodyBytes(headers, limit) {
|
|
34
|
+
const length = headers['content-length'];
|
|
35
|
+
const transfer = headers['transfer-encoding'];
|
|
36
|
+
const encoding = headers['content-encoding'];
|
|
37
|
+
if (length !== undefined && (typeof length !== 'string' || !/^[0-9]+$/.test(length))) {
|
|
38
|
+
throw Object.assign(new Error('invalid content length'), { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
if (length !== undefined && transfer !== undefined) {
|
|
41
|
+
throw Object.assign(new Error('ambiguous content length'), { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
const size = length === undefined ? undefined : Number(length);
|
|
44
|
+
if (size !== undefined && (!Number.isSafeInteger(size) || size > limit)) {
|
|
45
|
+
throw Object.assign(new Error('request body too large'), { status: 413 });
|
|
46
|
+
}
|
|
47
|
+
// Compressed Content-Length is NOT a bound on the decoded parser buffer.
|
|
48
|
+
// Likewise reserve the parser's full limit for an unknown chunked length.
|
|
49
|
+
if ((encoding !== undefined && encoding !== 'identity') || transfer !== undefined) return limit;
|
|
50
|
+
// Node's HTTP/1 parser does not infer an EOF-delimited request body.
|
|
51
|
+
return size ?? 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createHttpAdmission({ secret, isStopping = () => false, limits = HTTP_ADMISSION_LIMITS }) {
|
|
55
|
+
if (typeof secret !== 'string' || typeof isStopping !== 'function' ||
|
|
56
|
+
!positiveLimit(limits.requests) || !positiveLimit(limits.bytes) ||
|
|
57
|
+
!positiveLimit(limits.bodyBytes) || limits.bodyBytes > limits.bytes) {
|
|
58
|
+
throw new TypeError('invalid HTTP admission configuration');
|
|
59
|
+
}
|
|
60
|
+
// Copy configuration: a caller cannot replenish capacity by mutating limits.
|
|
61
|
+
const maximum = Object.freeze({ ...limits });
|
|
62
|
+
const leases = new WeakMap();
|
|
63
|
+
let requests = 0, bytes = 0;
|
|
64
|
+
|
|
65
|
+
function refresh(lease) {
|
|
66
|
+
if (lease.response.destroyed || lease.response.writableFinished) lease.terminal = true;
|
|
67
|
+
if (lease.released || !lease.terminal || lease.held) return;
|
|
68
|
+
lease.released = true;
|
|
69
|
+
requests -= 1;
|
|
70
|
+
bytes -= lease.bytes;
|
|
71
|
+
lease.response.removeListener('finish', lease.settle);
|
|
72
|
+
lease.response.removeListener('close', lease.settle);
|
|
73
|
+
lease.response.removeListener('error', lease.settle);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
// Runs on Node's request/checkContinue event, before ANY Express parser.
|
|
78
|
+
handle(request, response, next, expectContinue = false) {
|
|
79
|
+
if (isStopping()) { rejection(response, 503, 'runner is shutting down'); return; }
|
|
80
|
+
if (!secret) { rejection(response, 500, 'FIREEMU_RUNNER_SECRET is required'); return; }
|
|
81
|
+
if (!secretMatches(request.headers['x-fireemu-runner-secret'], secret)) {
|
|
82
|
+
rejection(response, 403, 'not the fireemu proxy'); return;
|
|
83
|
+
}
|
|
84
|
+
if (leases.has(request)) { rejection(response, 400, 'duplicate request dispatch'); return; }
|
|
85
|
+
let reserved;
|
|
86
|
+
try { reserved = reservedBodyBytes(request.headers, maximum.bodyBytes); }
|
|
87
|
+
catch (error) { rejection(response, error.status, error.message); return; }
|
|
88
|
+
if (requests >= maximum.requests || bytes + reserved > maximum.bytes) {
|
|
89
|
+
rejection(response, 503, 'runner HTTP capacity exhausted'); return;
|
|
90
|
+
}
|
|
91
|
+
requests += 1; bytes += reserved;
|
|
92
|
+
const lease = { response, bytes: reserved, terminal: false, held: false, released: false, begun: false };
|
|
93
|
+
lease.settle = () => { lease.terminal = true; refresh(lease); };
|
|
94
|
+
leases.set(request, lease);
|
|
95
|
+
response.once('finish', lease.settle);
|
|
96
|
+
response.once('close', lease.settle);
|
|
97
|
+
response.once('error', lease.settle);
|
|
98
|
+
refresh(lease);
|
|
99
|
+
if (lease.released) return;
|
|
100
|
+
// Never expose the proxy capability to parsers or user callbacks.
|
|
101
|
+
delete request.headers['x-fireemu-runner-secret'];
|
|
102
|
+
if (Array.isArray(request.rawHeaders)) {
|
|
103
|
+
for (let i = request.rawHeaders.length - 2; i >= 0; i -= 2) {
|
|
104
|
+
if (request.rawHeaders[i].toLowerCase() === 'x-fireemu-runner-secret') request.rawHeaders.splice(i, 2);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
if (expectContinue) response.writeContinue();
|
|
109
|
+
next(request, response);
|
|
110
|
+
} catch {
|
|
111
|
+
if (response.headersSent) response.destroy();
|
|
112
|
+
else rejection(response, 500, 'internal error');
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Express calls verify on its decoded Buffer before JSON/text processing.
|
|
117
|
+
// For compressed/chunked input, unused reserved capacity is returned here.
|
|
118
|
+
verify(request, _response, body) {
|
|
119
|
+
const lease = leases.get(request);
|
|
120
|
+
if (!lease || lease.released || lease.begun || !Buffer.isBuffer(body) ||
|
|
121
|
+
body.length > lease.bytes || body.length > maximum.bodyBytes) {
|
|
122
|
+
throw Object.assign(new Error('unadmitted request body'), { status: 413, statusCode: 413 });
|
|
123
|
+
}
|
|
124
|
+
bytes -= lease.bytes - body.length;
|
|
125
|
+
lease.bytes = body.length;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Pin before environment-queue insertion. Client disconnect must NOT return
|
|
129
|
+
// the slot while that queue or a running user Promise still retains input.
|
|
130
|
+
begin(request) {
|
|
131
|
+
const lease = leases.get(request);
|
|
132
|
+
if (!lease) return null;
|
|
133
|
+
refresh(lease);
|
|
134
|
+
if (lease.released || lease.begun || lease.response.writableEnded) return null;
|
|
135
|
+
lease.begun = true; lease.held = true;
|
|
136
|
+
let completed = false;
|
|
137
|
+
return () => {
|
|
138
|
+
if (completed) return;
|
|
139
|
+
completed = true; lease.held = false; refresh(lease);
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
snapshot() { return Object.freeze({ requests, bytes }); },
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Observe ServerResponse *before* a callback is enqueued or entered. Registering
|
|
2
|
+
// close/finish only after awaiting user code misses already-delivered events.
|
|
3
|
+
// Response termination is not cancellation of the running user Promise, nor an
|
|
4
|
+
// acknowledgement that a remote client received/applied the response.
|
|
5
|
+
export function trackHttpResponse(response) {
|
|
6
|
+
let terminal = false;
|
|
7
|
+
let disposed = false;
|
|
8
|
+
let resolveDone;
|
|
9
|
+
const done = new Promise((resolve) => { resolveDone = resolve; });
|
|
10
|
+
|
|
11
|
+
function removeListeners() {
|
|
12
|
+
response.removeListener('finish', settle);
|
|
13
|
+
response.removeListener('close', settle);
|
|
14
|
+
response.removeListener('error', settle);
|
|
15
|
+
}
|
|
16
|
+
function settle() {
|
|
17
|
+
if (terminal) return;
|
|
18
|
+
terminal = true;
|
|
19
|
+
removeListeners();
|
|
20
|
+
resolveDone();
|
|
21
|
+
}
|
|
22
|
+
function refresh() {
|
|
23
|
+
if (response.destroyed || response.writableFinished) settle();
|
|
24
|
+
}
|
|
25
|
+
response.once('finish', settle);
|
|
26
|
+
response.once('close', settle);
|
|
27
|
+
response.once('error', settle);
|
|
28
|
+
refresh();
|
|
29
|
+
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
canStart() {
|
|
32
|
+
refresh();
|
|
33
|
+
return !disposed && !terminal && !response.writableEnded;
|
|
34
|
+
},
|
|
35
|
+
wait() {
|
|
36
|
+
refresh();
|
|
37
|
+
return done;
|
|
38
|
+
},
|
|
39
|
+
dispose() {
|
|
40
|
+
disposed = true;
|
|
41
|
+
settle();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|