@mark1russell7/rigor-proxy 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +58 -2
- package/build/answer.d.ts +8 -0
- package/build/answer.d.ts.map +1 -0
- package/build/answer.js +25 -0
- package/build/answer.js.map +1 -0
- package/build/applier.d.ts +62 -0
- package/build/applier.d.ts.map +1 -0
- package/build/applier.js +231 -0
- package/build/applier.js.map +1 -0
- package/build/framers.d.ts +48 -0
- package/build/framers.d.ts.map +1 -0
- package/build/framers.js +290 -0
- package/build/framers.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +5 -0
- package/build/proxy.d.ts +64 -0
- package/build/proxy.d.ts.map +1 -0
- package/build/proxy.js +208 -0
- package/build/proxy.js.map +1 -0
- package/package.json +64 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Russell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mark1russell7/rigor-proxy
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This package holds a TCP proxy that puts the faults of a plan into the streams between clients and a server. It records each fault that it applies.
|
|
4
|
+
|
|
5
|
+
A fault plan is data of `@mark1russell7/rigor-chaos`. A seed fixes the plan before the run. Thus a record keeps the plan, and a later run applies the same faults at the same points.
|
|
6
|
+
|
|
7
|
+
## A proxy
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { faultPlan } from "@mark1russell7/rigor-chaos";
|
|
11
|
+
import { startFaultProxy } from "@mark1russell7/rigor-proxy";
|
|
12
|
+
|
|
13
|
+
const plan = faultPlan({ connections: 10, perConnection: 2, kinds: { split: 2, delay: 1, reset: 1 }, within: { frame: 20 } }, "drill");
|
|
14
|
+
const proxy = await startFaultProxy({ target: { port: 8080 }, plan, protocol: "websocket" });
|
|
15
|
+
// The client under test connects to proxy.port.
|
|
16
|
+
await proxy.close();
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- The proxy numbers the connections from 0, in the order that it accepts them. Connection `i` gets the faults of connection `i` of the plan.
|
|
20
|
+
- `records` holds each fault that occurred: its time, its connection, its direction, its kind, and its byte and frame position.
|
|
21
|
+
- `connections` holds the summary of each connection that closed: how it ended, the bytes of each direction, and the faults that did not occur.
|
|
22
|
+
- `onFault` and `onClose` get the same data when it occurs.
|
|
23
|
+
- `clock` sets the clock of the times and the timers. A test gives `fakeClock()` of `@mark1russell7/algebra/clock`, and it moves the time itself.
|
|
24
|
+
|
|
25
|
+
## Frames
|
|
26
|
+
|
|
27
|
+
A fault with a `frame` point counts frames, and `split`, `reorder` and `duplicate` change one frame. The framers of `protocol` set what a frame is.
|
|
28
|
+
|
|
29
|
+
| `protocol` | A frame |
|
|
30
|
+
|---|---|
|
|
31
|
+
| `tcp` | Each read of the socket. |
|
|
32
|
+
| `http` | The head of a request or a response, each read of a body with a length, or each chunk of a chunked body. |
|
|
33
|
+
| `websocket` | The same as `http`. After the `101` response, each WebSocket frame. |
|
|
34
|
+
|
|
35
|
+
- A framer does not change a byte. When it cannot read the stream, it gives each read as one frame.
|
|
36
|
+
- The proxy works on TCP. It cannot read a TLS stream, thus a TLS stream has only `tcp` frames.
|
|
37
|
+
- `framers` replaces the framers of `protocol` with your framers.
|
|
38
|
+
|
|
39
|
+
## An answer
|
|
40
|
+
|
|
41
|
+
An `answer` puts an HTTP/1.1 response from the plan in place of the server. The proxy writes it to the client, and ends the connection with a FIN.
|
|
42
|
+
|
|
43
|
+
| Point | What the client and the server see |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `up`, `{ frame: 0 }` | The server does not get the request. The client gets the answer. |
|
|
46
|
+
| `down`, `{ frame: 0 }` | The server gets the request. The client gets the answer in place of the response of the server. |
|
|
47
|
+
| `down`, a later frame | The client gets a part of the response of the server, then the answer. This stream is not valid HTTP. |
|
|
48
|
+
|
|
49
|
+
- The proxy writes the fields in their order, then `content-length` and `connection: close`. A 204 or a 304 has no `content-length`.
|
|
50
|
+
- After an answer, no byte of the server goes to the client. The summary of the connection says `cut`.
|
|
51
|
+
- `answerBytes` gives the bytes of an answer.
|
|
52
|
+
|
|
53
|
+
## The pure parts
|
|
54
|
+
|
|
55
|
+
The framers (`tcpFramers`, `httpFramers`, `webSocketFramer`), the applier (`applierOf`) and `answerBytes` read no clock and write no socket. A test gives them bytes and times, and it examines the writes that they give back.
|
|
56
|
+
|
|
57
|
+
## Tests
|
|
58
|
+
|
|
59
|
+
The tests with real sockets measure time, thus only CI starts them, in the workflow `proxy-net.yml`. `pnpm test` starts only the tests of the pure parts.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { AnswerFault } from "@mark1russell7/rigor-chaos";
|
|
2
|
+
/**
|
|
3
|
+
* This function gives the bytes of the HTTP/1.1 response of an `answer`. The status line has the reason of Node.js, or
|
|
4
|
+
* no reason for a status that Node.js does not know. The fields go in their order. Then `content-length` and
|
|
5
|
+
* `connection: close` follow. A response with the status 204 or 304 has no `content-length`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function answerBytes(answer: Omit<AnswerFault, "kind">): Uint8Array;
|
|
8
|
+
//# sourceMappingURL=answer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"answer.d.ts","names":[],"sources":["../src/answer.ts"],"mappings":";;;;;;wBAWgB,YAAY,QAAQ,KAAK,uBAAuB"}
|
package/build/answer.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { STATUS_CODES } from "node:http";
|
|
2
|
+
// src/answer.ts
|
|
3
|
+
/** These statuses have no body, thus their response has no `content-length`. */
|
|
4
|
+
const NO_BODY = /* @__PURE__ */ new Set([204, 304]);
|
|
5
|
+
/**
|
|
6
|
+
* This function gives the bytes of the HTTP/1.1 response of an `answer`. The status line has the reason of Node.js, or
|
|
7
|
+
* no reason for a status that Node.js does not know. The fields go in their order. Then `content-length` and
|
|
8
|
+
* `connection: close` follow. A response with the status 204 or 304 has no `content-length`.
|
|
9
|
+
*/
|
|
10
|
+
function answerBytes(answer) {
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
const body = encoder.encode(answer.body ?? "");
|
|
13
|
+
const lines = [`HTTP/1.1 ${answer.status} ${STATUS_CODES[answer.status] ?? ""}`];
|
|
14
|
+
for (const [name, value] of Object.entries(answer.fields ?? {})) lines.push(`${name}: ${value}`);
|
|
15
|
+
if (!NO_BODY.has(answer.status)) lines.push(`content-length: ${body.length}`);
|
|
16
|
+
lines.push("connection: close");
|
|
17
|
+
const head = encoder.encode(`${lines.join("\r\n")}\r\n\r\n`);
|
|
18
|
+
const out = new Uint8Array(head.length + body.length);
|
|
19
|
+
out.set(head);
|
|
20
|
+
out.set(body, head.length);
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
export { answerBytes };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=answer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"answer.js","names":[],"sources":["../src/answer.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport type { AnswerFault } from \"@mark1russell7/rigor-chaos\";\n\n/** These statuses have no body, thus their response has no `content-length`. */\nconst NO_BODY: ReadonlySet<number> = new Set([204, 304]);\n\n/**\n * This function gives the bytes of the HTTP/1.1 response of an `answer`. The status line has the reason of Node.js, or\n * no reason for a status that Node.js does not know. The fields go in their order. Then `content-length` and\n * `connection: close` follow. A response with the status 204 or 304 has no `content-length`.\n */\nexport function answerBytes(answer: Omit<AnswerFault, \"kind\">): Uint8Array {\n const encoder = new TextEncoder();\n const body = encoder.encode(answer.body ?? \"\");\n const lines = [`HTTP/1.1 ${answer.status} ${STATUS_CODES[answer.status] ?? \"\"}`];\n for (const [name, value] of Object.entries(answer.fields ?? {})) lines.push(`${name}: ${value}`);\n if (!NO_BODY.has(answer.status)) lines.push(`content-length: ${body.length}`);\n lines.push(\"connection: close\");\n const head = encoder.encode(`${lines.join(\"\\r\\n\")}\\r\\n\\r\\n`);\n const out = new Uint8Array(head.length + body.length);\n out.set(head);\n out.set(body, head.length);\n return out;\n}\n"],"mappings":";;;AAIA,MAAM,0BAA+B,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC;;;;;;AAOvD,SAAgB,YAAY,QAA+C;CACzE,MAAM,UAAU,IAAI,YAAY;CAChC,MAAM,OAAO,QAAQ,OAAO,OAAO,QAAQ,EAAE;CAC7C,MAAM,QAAQ,CAAC,YAAY,OAAO,OAAO,GAAG,aAAa,OAAO,WAAW,IAAI;CAC/E,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,UAAU,CAAC,CAAC,GAAG,MAAM,KAAK,GAAG,KAAK,IAAI,OAAO;CAC/F,IAAI,CAAC,QAAQ,IAAI,OAAO,MAAM,GAAG,MAAM,KAAK,mBAAmB,KAAK,QAAQ;CAC5E,MAAM,KAAK,mBAAmB;CAC9B,MAAM,OAAO,QAAQ,OAAO,GAAG,MAAM,KAAK,MAAM,EAAE,SAAS;CAC3D,MAAM,MAAM,IAAI,WAAW,KAAK,SAAS,KAAK,MAAM;CACpD,IAAI,IAAI,IAAI;CACZ,IAAI,IAAI,MAAM,KAAK,MAAM;CACzB,OAAO;AACT"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Direction, Fault, FaultPlan } from "@mark1russell7/rigor-chaos";
|
|
2
|
+
/**
|
|
3
|
+
* An applier puts the faults of a plan into the frames of one direction of one connection. It is pure: it reads no
|
|
4
|
+
* clock and writes no socket. Each time is in ms from the start of the connection. The proxy gives it each frame
|
|
5
|
+
* with the time of the read, and it gives back the writes with their times.
|
|
6
|
+
*/
|
|
7
|
+
/** One write of bytes at a time. The time of each write of one direction is the same as, or more than, the time of the write before it. */
|
|
8
|
+
export interface Send {
|
|
9
|
+
readonly bytes: Uint8Array;
|
|
10
|
+
readonly at: number;
|
|
11
|
+
}
|
|
12
|
+
/** A record of one fault that the proxy applied. */
|
|
13
|
+
export interface FaultRecord {
|
|
14
|
+
/** The position of the fault in the list of the plan. */
|
|
15
|
+
readonly index: number;
|
|
16
|
+
readonly connection: number;
|
|
17
|
+
readonly direction: Direction;
|
|
18
|
+
readonly kind: Fault["kind"];
|
|
19
|
+
/** The time of the fault, in ms from the start of the connection. */
|
|
20
|
+
readonly at: number;
|
|
21
|
+
/** The bytes of the direction before the fault. */
|
|
22
|
+
readonly byte: number;
|
|
23
|
+
/** The frames of the direction before the fault. */
|
|
24
|
+
readonly frame: number;
|
|
25
|
+
/** The bytes of the frames that a `split`, a `duplicate` or a `reorder` changed, or the bytes of the response of an `answer`. It is 0 for the other kinds. */
|
|
26
|
+
readonly bytes: number;
|
|
27
|
+
}
|
|
28
|
+
/** The result of one call to an applier. */
|
|
29
|
+
export interface Step {
|
|
30
|
+
readonly sends: readonly Send[];
|
|
31
|
+
readonly records: readonly FaultRecord[];
|
|
32
|
+
/** The direction stops reading until this time. */
|
|
33
|
+
readonly pauseUntil?: number;
|
|
34
|
+
/** The connection closes. A `cut` closes after the writes before it, and a `reset` closes at once. */
|
|
35
|
+
readonly close?: {
|
|
36
|
+
readonly kind: "cut" | "reset";
|
|
37
|
+
readonly at: number;
|
|
38
|
+
};
|
|
39
|
+
/** The response of an `answer`, for the client in both directions. After it, no byte goes to the client. A `cut` follows it. */
|
|
40
|
+
readonly answer?: Send;
|
|
41
|
+
}
|
|
42
|
+
/** An applier of one direction of one connection. */
|
|
43
|
+
export interface Applier {
|
|
44
|
+
/** This method takes one frame that the proxy read at the time `now`. */
|
|
45
|
+
push(frame: Uint8Array, now: number): Step;
|
|
46
|
+
/** This method applies the faults that are due at the time `now`. The proxy uses it at the time `wake`. */
|
|
47
|
+
tick(now: number): Step;
|
|
48
|
+
/** This method sends the frames that a `reorder` holds, at the end of the stream. */
|
|
49
|
+
end(now: number): Step;
|
|
50
|
+
/** The next time that `tick` has work to do, or `undefined`. */
|
|
51
|
+
readonly wake: number | undefined;
|
|
52
|
+
/** The faults of this direction that did not occur, with their positions in the plan. */
|
|
53
|
+
pending(): readonly {
|
|
54
|
+
readonly index: number;
|
|
55
|
+
readonly fault: Fault;
|
|
56
|
+
}[];
|
|
57
|
+
}
|
|
58
|
+
/** This function cuts bytes into `pieces` parts of near the same length. Each part has one byte or more. */
|
|
59
|
+
export declare function splitBytes(bytes: Uint8Array, pieces: number): Uint8Array[];
|
|
60
|
+
/** This function gives the applier of one direction of one connection of a plan. */
|
|
61
|
+
export declare function applierOf(plan: FaultPlan, connection: number, direction: Direction): Applier;
|
|
62
|
+
//# sourceMappingURL=applier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applier.d.ts","names":[],"sources":["../src/applier.ts"],"mappings":";;;;;;;iBAUiB;WACN,OAAO;WACP;;;iBAIM;;WAEN;WACA;WACA,WAAW;WACX,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;;iBAIM;WACN,gBAAgB;WAChB,kBAAkB;;WAElB;;WAEA;aAAmB;aAAgC;;;WAEnD,SAAS;;;iBAIH;;EAEf,KAAK,OAAO,YAAY,cAAc;;EAEtC,KAAK,cAAc;;EAEnB,IAAI,cAAc;;WAET;;EAET;aAA+B;aAAwB,OAAO;;;;wBA8BhD,WAAW,OAAO,YAAY,iBAAiB;;wBAQ/C,UAAU,MAAM,WAAW,oBAAoB,WAAW,YAAY"}
|
package/build/applier.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { answerBytes } from "./answer.js";
|
|
2
|
+
import { triggerOf } from "@mark1russell7/rigor-chaos";
|
|
3
|
+
// src/applier.ts
|
|
4
|
+
/** This function cuts bytes into `pieces` parts of near the same length. Each part has one byte or more. */
|
|
5
|
+
function splitBytes(bytes, pieces) {
|
|
6
|
+
const count = Math.max(1, Math.min(pieces, bytes.length));
|
|
7
|
+
const out = [];
|
|
8
|
+
for (let i = 0; i < count; i += 1) out.push(bytes.subarray(Math.floor(i * bytes.length / count), Math.floor((i + 1) * bytes.length / count)));
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
/** This function gives the applier of one direction of one connection of a plan. */
|
|
12
|
+
function applierOf(plan, connection, direction) {
|
|
13
|
+
const lists = {
|
|
14
|
+
ms: [],
|
|
15
|
+
byte: [],
|
|
16
|
+
frame: []
|
|
17
|
+
};
|
|
18
|
+
plan.faults.forEach((fault, index) => {
|
|
19
|
+
if (fault.connection !== connection || fault.direction !== direction) return;
|
|
20
|
+
const trigger = triggerOf(fault.at);
|
|
21
|
+
if (trigger !== void 0) lists[trigger.measure].push({
|
|
22
|
+
index,
|
|
23
|
+
fault,
|
|
24
|
+
value: trigger.value
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
for (const list of Object.values(lists)) list.sort((a, b) => a.value - b.value);
|
|
28
|
+
const applied = /* @__PURE__ */ new Set();
|
|
29
|
+
let byte = 0;
|
|
30
|
+
let frame = 0;
|
|
31
|
+
let last = 0;
|
|
32
|
+
let closed = false;
|
|
33
|
+
let delay;
|
|
34
|
+
let armed = [];
|
|
35
|
+
let hold;
|
|
36
|
+
let sends = [];
|
|
37
|
+
let records = [];
|
|
38
|
+
let pauseUntil;
|
|
39
|
+
let close;
|
|
40
|
+
let answer;
|
|
41
|
+
const record = (entry, at, bytes, where = {
|
|
42
|
+
byte,
|
|
43
|
+
frame
|
|
44
|
+
}) => {
|
|
45
|
+
applied.add(entry.index);
|
|
46
|
+
records.push({
|
|
47
|
+
index: entry.index,
|
|
48
|
+
connection,
|
|
49
|
+
direction,
|
|
50
|
+
kind: entry.fault.kind,
|
|
51
|
+
at,
|
|
52
|
+
byte: where.byte,
|
|
53
|
+
frame: where.frame,
|
|
54
|
+
bytes
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
const schedule = (unit, now) => {
|
|
58
|
+
const base = Math.max(now, unit.base);
|
|
59
|
+
for (const piece of unit.pieces) {
|
|
60
|
+
last = Math.max(last, base + piece.offset);
|
|
61
|
+
sends.push({
|
|
62
|
+
bytes: piece.bytes,
|
|
63
|
+
at: last
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const release = (now) => {
|
|
68
|
+
if (hold === void 0) return;
|
|
69
|
+
const held = hold;
|
|
70
|
+
hold = void 0;
|
|
71
|
+
const total = held.units.reduce((sum, unit) => sum + unit.pieces.reduce((s, piece) => s + piece.bytes.length, 0), 0);
|
|
72
|
+
record(held.entry, held.at, total, held);
|
|
73
|
+
for (const unit of [...held.units].reverse()) schedule(unit, now);
|
|
74
|
+
};
|
|
75
|
+
/** This function applies one fault at the time `now`. */
|
|
76
|
+
const fire = (entry, now) => {
|
|
77
|
+
const fault = entry.fault;
|
|
78
|
+
switch (fault.kind) {
|
|
79
|
+
case "cut":
|
|
80
|
+
case "reset":
|
|
81
|
+
release(now);
|
|
82
|
+
record(entry, now, 0);
|
|
83
|
+
closed = true;
|
|
84
|
+
close = {
|
|
85
|
+
kind: fault.kind,
|
|
86
|
+
at: fault.kind === "cut" ? Math.max(now, last) : now
|
|
87
|
+
};
|
|
88
|
+
return;
|
|
89
|
+
case "stall":
|
|
90
|
+
record(entry, now, 0);
|
|
91
|
+
pauseUntil = Math.max(pauseUntil ?? 0, now + fault.ms);
|
|
92
|
+
last = Math.max(last, now + fault.ms);
|
|
93
|
+
return;
|
|
94
|
+
case "delay":
|
|
95
|
+
record(entry, now, 0);
|
|
96
|
+
delay = {
|
|
97
|
+
until: now + fault.forMs,
|
|
98
|
+
ms: Math.max(fault.ms, delay !== void 0 && now < delay.until ? delay.ms : 0)
|
|
99
|
+
};
|
|
100
|
+
return;
|
|
101
|
+
case "reorder":
|
|
102
|
+
release(now);
|
|
103
|
+
hold = {
|
|
104
|
+
entry,
|
|
105
|
+
units: [],
|
|
106
|
+
left: fault.count,
|
|
107
|
+
until: now + fault.holdMs,
|
|
108
|
+
at: now,
|
|
109
|
+
byte,
|
|
110
|
+
frame
|
|
111
|
+
};
|
|
112
|
+
return;
|
|
113
|
+
case "answer": {
|
|
114
|
+
release(now);
|
|
115
|
+
const bytes = answerBytes(fault);
|
|
116
|
+
record(entry, now, bytes.length);
|
|
117
|
+
const at = direction === "down" ? Math.max(now, last) : now;
|
|
118
|
+
answer = {
|
|
119
|
+
bytes,
|
|
120
|
+
at
|
|
121
|
+
};
|
|
122
|
+
closed = true;
|
|
123
|
+
close = {
|
|
124
|
+
kind: "cut",
|
|
125
|
+
at
|
|
126
|
+
};
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
default: armed.push(entry);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const fireUntil = (list, value, now) => {
|
|
133
|
+
while (!closed && list.length > 0 && list[0].value <= value) fire(list.shift(), now);
|
|
134
|
+
};
|
|
135
|
+
const fireTimes = (now) => {
|
|
136
|
+
fireUntil(lists.ms, now, now);
|
|
137
|
+
if (!closed && hold !== void 0 && now >= hold.until) release(now);
|
|
138
|
+
};
|
|
139
|
+
/** This function sends one piece of a frame, with the faults that wait for a frame. */
|
|
140
|
+
const emit = (bytes, now) => {
|
|
141
|
+
if (delay !== void 0 && now >= delay.until) delay = void 0;
|
|
142
|
+
let pieces = [{
|
|
143
|
+
bytes,
|
|
144
|
+
offset: 0
|
|
145
|
+
}];
|
|
146
|
+
for (const entry of armed) {
|
|
147
|
+
const fault = entry.fault;
|
|
148
|
+
if (fault.kind === "split") pieces = pieces.flatMap((piece) => splitBytes(piece.bytes, fault.pieces)).map((part, i) => ({
|
|
149
|
+
bytes: part,
|
|
150
|
+
offset: i * fault.gapMs
|
|
151
|
+
}));
|
|
152
|
+
else if (fault.kind === "duplicate") {
|
|
153
|
+
const span = pieces.length === 0 ? 0 : pieces[pieces.length - 1].offset;
|
|
154
|
+
pieces = Array.from({ length: fault.copies + 1 }, (_, copy) => pieces.map((piece) => ({
|
|
155
|
+
bytes: piece.bytes,
|
|
156
|
+
offset: piece.offset + copy * span
|
|
157
|
+
}))).flat();
|
|
158
|
+
}
|
|
159
|
+
record(entry, now, bytes.length);
|
|
160
|
+
}
|
|
161
|
+
armed = [];
|
|
162
|
+
const unit = {
|
|
163
|
+
pieces,
|
|
164
|
+
base: now + (delay?.ms ?? 0)
|
|
165
|
+
};
|
|
166
|
+
if (hold === void 0) {
|
|
167
|
+
schedule(unit, now);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
hold.units.push(unit);
|
|
171
|
+
hold.left -= 1;
|
|
172
|
+
if (hold.left === 0) release(now);
|
|
173
|
+
};
|
|
174
|
+
const step = () => {
|
|
175
|
+
const out = {
|
|
176
|
+
sends,
|
|
177
|
+
records,
|
|
178
|
+
...pauseUntil === void 0 ? {} : { pauseUntil },
|
|
179
|
+
...close === void 0 ? {} : { close },
|
|
180
|
+
...answer === void 0 ? {} : { answer }
|
|
181
|
+
};
|
|
182
|
+
sends = [];
|
|
183
|
+
records = [];
|
|
184
|
+
pauseUntil = void 0;
|
|
185
|
+
close = void 0;
|
|
186
|
+
answer = void 0;
|
|
187
|
+
return out;
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
push(bytes, now) {
|
|
191
|
+
if (closed) return step();
|
|
192
|
+
fireTimes(now);
|
|
193
|
+
fireUntil(lists.frame, frame, now);
|
|
194
|
+
let rest = bytes;
|
|
195
|
+
while (!closed && rest.length > 0) {
|
|
196
|
+
fireUntil(lists.byte, byte, now);
|
|
197
|
+
if (closed) break;
|
|
198
|
+
const next = lists.byte[0]?.value;
|
|
199
|
+
const take = next !== void 0 && next < byte + rest.length ? next - byte : rest.length;
|
|
200
|
+
emit(rest.subarray(0, take), now);
|
|
201
|
+
byte += take;
|
|
202
|
+
rest = rest.subarray(take);
|
|
203
|
+
}
|
|
204
|
+
frame += 1;
|
|
205
|
+
return step();
|
|
206
|
+
},
|
|
207
|
+
tick(now) {
|
|
208
|
+
if (!closed) fireTimes(now);
|
|
209
|
+
return step();
|
|
210
|
+
},
|
|
211
|
+
end(now) {
|
|
212
|
+
if (!closed) release(now);
|
|
213
|
+
closed = true;
|
|
214
|
+
return step();
|
|
215
|
+
},
|
|
216
|
+
get wake() {
|
|
217
|
+
if (closed) return void 0;
|
|
218
|
+
const times = [lists.ms[0]?.value, hold?.until].filter((time) => time !== void 0);
|
|
219
|
+
return times.length === 0 ? void 0 : Math.min(...times);
|
|
220
|
+
},
|
|
221
|
+
pending() {
|
|
222
|
+
return plan.faults.flatMap((fault, index) => fault.connection === connection && fault.direction === direction && !applied.has(index) ? [{
|
|
223
|
+
index,
|
|
224
|
+
fault
|
|
225
|
+
}] : []);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
export { applierOf, splitBytes };
|
|
230
|
+
|
|
231
|
+
//# sourceMappingURL=applier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applier.js","names":[],"sources":["../src/applier.ts"],"sourcesContent":["import { type Direction, type Fault, type FaultPlan, triggerOf } from \"@mark1russell7/rigor-chaos\";\nimport { answerBytes } from \"./answer.ts\";\n\n/**\n * An applier puts the faults of a plan into the frames of one direction of one connection. It is pure: it reads no\n * clock and writes no socket. Each time is in ms from the start of the connection. The proxy gives it each frame\n * with the time of the read, and it gives back the writes with their times.\n */\n\n/** One write of bytes at a time. The time of each write of one direction is the same as, or more than, the time of the write before it. */\nexport interface Send {\n readonly bytes: Uint8Array;\n readonly at: number;\n}\n\n/** A record of one fault that the proxy applied. */\nexport interface FaultRecord {\n /** The position of the fault in the list of the plan. */\n readonly index: number;\n readonly connection: number;\n readonly direction: Direction;\n readonly kind: Fault[\"kind\"];\n /** The time of the fault, in ms from the start of the connection. */\n readonly at: number;\n /** The bytes of the direction before the fault. */\n readonly byte: number;\n /** The frames of the direction before the fault. */\n readonly frame: number;\n /** The bytes of the frames that a `split`, a `duplicate` or a `reorder` changed, or the bytes of the response of an `answer`. It is 0 for the other kinds. */\n readonly bytes: number;\n}\n\n/** The result of one call to an applier. */\nexport interface Step {\n readonly sends: readonly Send[];\n readonly records: readonly FaultRecord[];\n /** The direction stops reading until this time. */\n readonly pauseUntil?: number;\n /** The connection closes. A `cut` closes after the writes before it, and a `reset` closes at once. */\n readonly close?: { readonly kind: \"cut\" | \"reset\"; readonly at: number };\n /** The response of an `answer`, for the client in both directions. After it, no byte goes to the client. A `cut` follows it. */\n readonly answer?: Send;\n}\n\n/** An applier of one direction of one connection. */\nexport interface Applier {\n /** This method takes one frame that the proxy read at the time `now`. */\n push(frame: Uint8Array, now: number): Step;\n /** This method applies the faults that are due at the time `now`. The proxy uses it at the time `wake`. */\n tick(now: number): Step;\n /** This method sends the frames that a `reorder` holds, at the end of the stream. */\n end(now: number): Step;\n /** The next time that `tick` has work to do, or `undefined`. */\n readonly wake: number | undefined;\n /** The faults of this direction that did not occur, with their positions in the plan. */\n pending(): readonly { readonly index: number; readonly fault: Fault }[];\n}\n\ninterface Entry {\n readonly index: number;\n readonly fault: Fault;\n readonly value: number;\n}\n\ninterface Piece {\n readonly bytes: Uint8Array;\n readonly offset: number;\n}\n\ninterface Unit {\n readonly pieces: readonly Piece[];\n readonly base: number;\n}\n\ninterface Hold {\n readonly entry: Entry;\n readonly units: Unit[];\n left: number;\n readonly until: number;\n readonly at: number;\n readonly byte: number;\n readonly frame: number;\n}\n\n/** This function cuts bytes into `pieces` parts of near the same length. Each part has one byte or more. */\nexport function splitBytes(bytes: Uint8Array, pieces: number): Uint8Array[] {\n const count = Math.max(1, Math.min(pieces, bytes.length));\n const out: Uint8Array[] = [];\n for (let i = 0; i < count; i += 1) out.push(bytes.subarray(Math.floor((i * bytes.length) / count), Math.floor(((i + 1) * bytes.length) / count)));\n return out;\n}\n\n/** This function gives the applier of one direction of one connection of a plan. */\nexport function applierOf(plan: FaultPlan, connection: number, direction: Direction): Applier {\n const lists: Record<\"ms\" | \"byte\" | \"frame\", Entry[]> = { ms: [], byte: [], frame: [] };\n plan.faults.forEach((fault, index) => {\n if (fault.connection !== connection || fault.direction !== direction) return;\n const trigger = triggerOf(fault.at);\n if (trigger !== undefined) lists[trigger.measure].push({ index, fault, value: trigger.value });\n });\n for (const list of Object.values(lists)) list.sort((a, b) => a.value - b.value);\n\n const applied = new Set<number>();\n let byte = 0;\n let frame = 0;\n let last = 0;\n let closed = false;\n let delay: { readonly until: number; readonly ms: number } | undefined;\n let armed: Entry[] = [];\n let hold: Hold | undefined;\n\n let sends: Send[] = [];\n let records: FaultRecord[] = [];\n let pauseUntil: number | undefined;\n let close: Step[\"close\"];\n let answer: Send | undefined;\n\n const record = (entry: Entry, at: number, bytes: number, where: { byte: number; frame: number } = { byte, frame }): void => {\n applied.add(entry.index);\n records.push({ index: entry.index, connection, direction, kind: entry.fault.kind, at, byte: where.byte, frame: where.frame, bytes });\n };\n\n const schedule = (unit: Unit, now: number): void => {\n const base = Math.max(now, unit.base);\n for (const piece of unit.pieces) {\n last = Math.max(last, base + piece.offset);\n sends.push({ bytes: piece.bytes, at: last });\n }\n };\n\n const release = (now: number): void => {\n if (hold === undefined) return;\n const held = hold;\n hold = undefined;\n const total = held.units.reduce((sum, unit) => sum + unit.pieces.reduce((s, piece) => s + piece.bytes.length, 0), 0);\n record(held.entry, held.at, total, held);\n for (const unit of [...held.units].reverse()) schedule(unit, now);\n };\n\n /** This function applies one fault at the time `now`. */\n const fire = (entry: Entry, now: number): void => {\n const fault = entry.fault;\n switch (fault.kind) {\n case \"cut\":\n case \"reset\":\n release(now);\n record(entry, now, 0);\n closed = true;\n close = { kind: fault.kind, at: fault.kind === \"cut\" ? Math.max(now, last) : now };\n return;\n case \"stall\":\n record(entry, now, 0);\n pauseUntil = Math.max(pauseUntil ?? 0, now + fault.ms);\n last = Math.max(last, now + fault.ms);\n return;\n case \"delay\":\n record(entry, now, 0);\n delay = { until: now + fault.forMs, ms: Math.max(fault.ms, delay !== undefined && now < delay.until ? delay.ms : 0) };\n return;\n case \"reorder\":\n release(now);\n hold = { entry, units: [], left: fault.count, until: now + fault.holdMs, at: now, byte, frame };\n return;\n case \"answer\": {\n // The frames that a reorder holds came before the point, thus they go out before the answer.\n release(now);\n const bytes = answerBytes(fault);\n record(entry, now, bytes.length);\n const at = direction === \"down\" ? Math.max(now, last) : now;\n answer = { bytes, at };\n closed = true;\n close = { kind: \"cut\", at };\n return;\n }\n default:\n armed.push(entry);\n }\n };\n\n const fireUntil = (list: Entry[], value: number, now: number): void => {\n while (!closed && list.length > 0 && (list[0] as Entry).value <= value) fire(list.shift() as Entry, now);\n };\n\n const fireTimes = (now: number): void => {\n fireUntil(lists.ms, now, now);\n if (!closed && hold !== undefined && now >= hold.until) release(now);\n };\n\n /** This function sends one piece of a frame, with the faults that wait for a frame. */\n const emit = (bytes: Uint8Array, now: number): void => {\n if (delay !== undefined && now >= delay.until) delay = undefined;\n let pieces: Piece[] = [{ bytes, offset: 0 }];\n for (const entry of armed) {\n const fault = entry.fault;\n if (fault.kind === \"split\") {\n const parts = pieces.flatMap((piece) => splitBytes(piece.bytes, fault.pieces));\n pieces = parts.map((part, i) => ({ bytes: part, offset: i * fault.gapMs }));\n } else if (fault.kind === \"duplicate\") {\n const span = pieces.length === 0 ? 0 : (pieces[pieces.length - 1] as Piece).offset;\n pieces = Array.from({ length: fault.copies + 1 }, (_, copy) => pieces.map((piece) => ({ bytes: piece.bytes, offset: piece.offset + copy * span }))).flat();\n }\n record(entry, now, bytes.length);\n }\n armed = [];\n const unit: Unit = { pieces, base: now + (delay?.ms ?? 0) };\n if (hold === undefined) {\n schedule(unit, now);\n return;\n }\n hold.units.push(unit);\n hold.left -= 1;\n if (hold.left === 0) release(now);\n };\n\n const step = (): Step => {\n const out: Step = {\n sends,\n records,\n ...(pauseUntil === undefined ? {} : { pauseUntil }),\n ...(close === undefined ? {} : { close }),\n ...(answer === undefined ? {} : { answer }),\n };\n sends = [];\n records = [];\n pauseUntil = undefined;\n close = undefined;\n answer = undefined;\n return out;\n };\n\n return {\n push(bytes, now) {\n if (closed) return step();\n fireTimes(now);\n fireUntil(lists.frame, frame, now);\n let rest = bytes;\n while (!closed && rest.length > 0) {\n fireUntil(lists.byte, byte, now);\n if (closed) break;\n const next = lists.byte[0]?.value;\n const take = next !== undefined && next < byte + rest.length ? next - byte : rest.length;\n emit(rest.subarray(0, take), now);\n byte += take;\n rest = rest.subarray(take);\n }\n frame += 1;\n return step();\n },\n tick(now) {\n if (!closed) fireTimes(now);\n return step();\n },\n end(now) {\n if (!closed) release(now);\n closed = true;\n return step();\n },\n get wake() {\n if (closed) return undefined;\n const times = [lists.ms[0]?.value, hold?.until].filter((time): time is number => time !== undefined);\n return times.length === 0 ? undefined : Math.min(...times);\n },\n pending() {\n return plan.faults.flatMap((fault, index) => (fault.connection === connection && fault.direction === direction && !applied.has(index) ? [{ index, fault }] : []));\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A framer cuts the bytes of one direction into frames. A fault with a `frame` point counts these frames, and a fault
|
|
3
|
+
* that changes a frame (`split`, `reorder`, `duplicate`) works on one whole frame.
|
|
4
|
+
*
|
|
5
|
+
* A framer does not change, add or remove a byte. When it cannot read the stream, it gives each chunk as one frame for
|
|
6
|
+
* the rest of the stream. Thus a framer can make the frames coarse, but it cannot break the stream.
|
|
7
|
+
*/
|
|
8
|
+
/** A framer of one direction. */
|
|
9
|
+
export interface Framer {
|
|
10
|
+
/** This method takes the bytes of one read, and gives each frame that is whole after this read. */
|
|
11
|
+
push(bytes: Uint8Array): Uint8Array[];
|
|
12
|
+
/** This method gives the bytes that the framer holds at the end of the stream, as one frame, or no frame. */
|
|
13
|
+
end(): Uint8Array[];
|
|
14
|
+
}
|
|
15
|
+
/** The framers of the two directions of one connection. */
|
|
16
|
+
export interface FramerPair {
|
|
17
|
+
readonly up: Framer;
|
|
18
|
+
readonly down: Framer;
|
|
19
|
+
}
|
|
20
|
+
/** This framer gives each read as one frame. It is the framer of a TCP stream. */
|
|
21
|
+
export declare function chunkFramer(): Framer;
|
|
22
|
+
/** This function gives the frames of a TCP connection: each read is one frame. */
|
|
23
|
+
export declare function tcpFramers(): FramerPair;
|
|
24
|
+
/**
|
|
25
|
+
* This function gives the length of the WebSocket frame (RFC 6455, section 5.2) at the start of the bytes. It gives
|
|
26
|
+
* `undefined` when the bytes do not hold the whole header yet, and `null` for a length that is not a safe integer.
|
|
27
|
+
*/
|
|
28
|
+
export declare function webSocketFrameLength(bytes: Uint8Array): number | undefined | null;
|
|
29
|
+
/** This framer gives each WebSocket frame as one frame. */
|
|
30
|
+
export declare function webSocketFramer(): Framer;
|
|
31
|
+
/** The parts of an HTTP/1.1 head that the framer reads. */
|
|
32
|
+
export interface HttpHead {
|
|
33
|
+
/** The method of a request, or `null` for a response. */
|
|
34
|
+
readonly method: string | null;
|
|
35
|
+
/** The status of a response, or `null` for a request. */
|
|
36
|
+
readonly status: number | null;
|
|
37
|
+
/** The fields, with lower-case names. Repeated fields are joined with a comma. */
|
|
38
|
+
readonly fields: ReadonlyMap<string, string>;
|
|
39
|
+
}
|
|
40
|
+
/** This function reads the start line and the fields of an HTTP/1.1 head. It gives `undefined` for a head that it cannot read. */
|
|
41
|
+
export declare function readHttpHead(head: Uint8Array): HttpHead | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* This function gives the frames of an HTTP/1.1 connection. A head is one frame. A body with a length gives each read
|
|
44
|
+
* of the body as one frame. A chunked body gives each chunk as one frame, and the last chunk with its trailer as one
|
|
45
|
+
* frame. After a `101` response to an upgrade to `websocket`, each WebSocket frame is one frame.
|
|
46
|
+
*/
|
|
47
|
+
export declare function httpFramers(): FramerPair;
|
|
48
|
+
//# sourceMappingURL=framers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framers.d.ts","names":[],"sources":["../src/framers.ts"],"mappings":";;;;;;;;iBASiB;;EAEf,KAAK,OAAO,aAAa;;EAEzB,OAAO;;;iBAIQ;WACN,IAAI;WACJ,MAAM;;;wBAID,eAAe;;wBAKf,cAAc;;;;;wBAyEd,qBAAqB,OAAO;;wBAuB5B,mBAAmB;;iBAwClB;;WAEN;;WAEA;;WAEA,QAAQ;;;wBAIH,aAAa,MAAM,aAAa;;;;;;wBAsChC,eAAe"}
|
package/build/framers.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// src/framers.ts
|
|
2
|
+
/** This framer gives each read as one frame. It is the framer of a TCP stream. */
|
|
3
|
+
function chunkFramer() {
|
|
4
|
+
return {
|
|
5
|
+
push: (bytes) => bytes.length === 0 ? [] : [bytes],
|
|
6
|
+
end: () => []
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** This function gives the frames of a TCP connection: each read is one frame. */
|
|
10
|
+
function tcpFramers() {
|
|
11
|
+
return {
|
|
12
|
+
up: chunkFramer(),
|
|
13
|
+
down: chunkFramer()
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** The longest head of an HTTP message that a framer reads. A longer head is not HTTP for the framer. */
|
|
17
|
+
const MAX_HEAD = 65536;
|
|
18
|
+
/** The longest size line of a chunk that a framer reads. */
|
|
19
|
+
const MAX_LINE = 1024;
|
|
20
|
+
/** A queue of bytes. It joins its chunks only for a frame that it gives, thus a large frame costs one copy. */
|
|
21
|
+
var ByteQueue = class {
|
|
22
|
+
/** The chunks from the index `head` hold the bytes. No chunk from `head` is empty. */
|
|
23
|
+
chunks = [];
|
|
24
|
+
head = 0;
|
|
25
|
+
length = 0;
|
|
26
|
+
push(bytes) {
|
|
27
|
+
if (bytes.length === 0) return;
|
|
28
|
+
this.chunks.push(bytes);
|
|
29
|
+
this.length += bytes.length;
|
|
30
|
+
}
|
|
31
|
+
/** This method gives the first `count` bytes, or all the bytes when the queue has fewer. */
|
|
32
|
+
peek(count) {
|
|
33
|
+
const size = Math.min(count, this.length);
|
|
34
|
+
const first = this.chunks[this.head];
|
|
35
|
+
if (first === void 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
36
|
+
if (first.length >= size) return first.subarray(0, size);
|
|
37
|
+
const out = new Uint8Array(size);
|
|
38
|
+
for (let at = 0, i = this.head; at < size; i += 1) {
|
|
39
|
+
const part = this.chunks[i].subarray(0, size - at);
|
|
40
|
+
out.set(part, at);
|
|
41
|
+
at += part.length;
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/** This method removes the first `count` bytes, and gives them. */
|
|
46
|
+
take(count) {
|
|
47
|
+
const out = this.peek(count);
|
|
48
|
+
let left = out.length;
|
|
49
|
+
this.length -= left;
|
|
50
|
+
while (left > 0) {
|
|
51
|
+
const chunk = this.chunks[this.head];
|
|
52
|
+
if (chunk.length <= left) {
|
|
53
|
+
this.head += 1;
|
|
54
|
+
left -= chunk.length;
|
|
55
|
+
} else {
|
|
56
|
+
this.chunks[this.head] = chunk.subarray(left);
|
|
57
|
+
left = 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (this.length === 0) {
|
|
61
|
+
this.chunks = [];
|
|
62
|
+
this.head = 0;
|
|
63
|
+
} else if (this.head > 64 && this.head * 2 > this.chunks.length) {
|
|
64
|
+
this.chunks = this.chunks.slice(this.head);
|
|
65
|
+
this.head = 0;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
takeAll() {
|
|
70
|
+
return this.take(this.length);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* This function gives the length of the WebSocket frame (RFC 6455, section 5.2) at the start of the bytes. It gives
|
|
75
|
+
* `undefined` when the bytes do not hold the whole header yet, and `null` for a length that is not a safe integer.
|
|
76
|
+
*/
|
|
77
|
+
function webSocketFrameLength(bytes) {
|
|
78
|
+
if (bytes.length < 2) return void 0;
|
|
79
|
+
const second = bytes[1];
|
|
80
|
+
const masked = (second & 128) !== 0;
|
|
81
|
+
const short = second & 127;
|
|
82
|
+
let header = 2;
|
|
83
|
+
let payload = short;
|
|
84
|
+
if (short === 126) {
|
|
85
|
+
header = 4;
|
|
86
|
+
if (bytes.length < header) return void 0;
|
|
87
|
+
payload = bytes[2] << 8 | bytes[3];
|
|
88
|
+
} else if (short === 127) {
|
|
89
|
+
header = 10;
|
|
90
|
+
if (bytes.length < header) return void 0;
|
|
91
|
+
const high = (bytes[2] << 24 >>> 0) + (bytes[3] << 16) + (bytes[4] << 8) + bytes[5];
|
|
92
|
+
const low = (bytes[6] << 24 >>> 0) + (bytes[7] << 16) + (bytes[8] << 8) + bytes[9];
|
|
93
|
+
if (high >= 2097152) return null;
|
|
94
|
+
payload = high * 2 ** 32 + low;
|
|
95
|
+
}
|
|
96
|
+
return header + (masked ? 4 : 0) + payload;
|
|
97
|
+
}
|
|
98
|
+
/** This framer gives each WebSocket frame as one frame. */
|
|
99
|
+
function webSocketFramer() {
|
|
100
|
+
const queue = new ByteQueue();
|
|
101
|
+
let raw = false;
|
|
102
|
+
return {
|
|
103
|
+
push(bytes) {
|
|
104
|
+
if (raw) return bytes.length === 0 ? [] : [bytes];
|
|
105
|
+
queue.push(bytes);
|
|
106
|
+
const out = [];
|
|
107
|
+
for (;;) {
|
|
108
|
+
const length = webSocketFrameLength(queue.peek(14));
|
|
109
|
+
if (length === null) {
|
|
110
|
+
raw = true;
|
|
111
|
+
if (queue.length > 0) out.push(queue.takeAll());
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
if (length === void 0 || queue.length < length) return out;
|
|
115
|
+
out.push(queue.take(length));
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
end() {
|
|
119
|
+
return queue.length === 0 ? [] : [queue.takeAll()];
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const CRLF2 = [
|
|
124
|
+
13,
|
|
125
|
+
10,
|
|
126
|
+
13,
|
|
127
|
+
10
|
|
128
|
+
];
|
|
129
|
+
const indexOfHeadEnd = (bytes) => {
|
|
130
|
+
for (let i = 0; i + 3 < bytes.length; i += 1) if (bytes[i] === CRLF2[0] && bytes[i + 1] === CRLF2[1] && bytes[i + 2] === CRLF2[2] && bytes[i + 3] === CRLF2[3]) return i + 4;
|
|
131
|
+
return -1;
|
|
132
|
+
};
|
|
133
|
+
const indexOfLineEnd = (bytes, from) => {
|
|
134
|
+
for (let i = from; i + 1 < bytes.length; i += 1) if (bytes[i] === 13 && bytes[i + 1] === 10) return i + 2;
|
|
135
|
+
return -1;
|
|
136
|
+
};
|
|
137
|
+
/** This function reads the start line and the fields of an HTTP/1.1 head. It gives `undefined` for a head that it cannot read. */
|
|
138
|
+
function readHttpHead(head) {
|
|
139
|
+
const lines = new TextDecoder("latin1").decode(head).split("\r\n");
|
|
140
|
+
const start = lines[0] ?? "";
|
|
141
|
+
const response = /^HTTP\/1\.[01] (\d{3})(?: |$)/.exec(start);
|
|
142
|
+
const request = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+) \S+ HTTP\/1\.[01]$/.exec(start);
|
|
143
|
+
if (response === null && request === null) return void 0;
|
|
144
|
+
const fields = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const line of lines.slice(1)) {
|
|
146
|
+
if (line === "") continue;
|
|
147
|
+
const colon = line.indexOf(":");
|
|
148
|
+
if (colon <= 0) return void 0;
|
|
149
|
+
const name = line.slice(0, colon).trim().toLowerCase();
|
|
150
|
+
const value = line.slice(colon + 1).trim();
|
|
151
|
+
fields.set(name, fields.has(name) ? `${fields.get(name)}, ${value}` : value);
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
method: request === null ? null : request[1],
|
|
155
|
+
status: response === null ? null : Number(response[1]),
|
|
156
|
+
fields
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const hasToken = (value, token) => (value ?? "").split(",").some((part) => part.trim().toLowerCase() === token);
|
|
160
|
+
/**
|
|
161
|
+
* This function gives the frames of an HTTP/1.1 connection. A head is one frame. A body with a length gives each read
|
|
162
|
+
* of the body as one frame. A chunked body gives each chunk as one frame, and the last chunk with its trailer as one
|
|
163
|
+
* frame. After a `101` response to an upgrade to `websocket`, each WebSocket frame is one frame.
|
|
164
|
+
*/
|
|
165
|
+
function httpFramers() {
|
|
166
|
+
const shared = {
|
|
167
|
+
methods: [],
|
|
168
|
+
upgrade: null,
|
|
169
|
+
switched: null
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
up: httpFramer("up", shared),
|
|
173
|
+
down: httpFramer("down", shared)
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function httpFramer(direction, shared) {
|
|
177
|
+
const queue = new ByteQueue();
|
|
178
|
+
let body = { type: "none" };
|
|
179
|
+
let raw = false;
|
|
180
|
+
const giveUp = (out) => {
|
|
181
|
+
raw = true;
|
|
182
|
+
if (queue.length > 0) out.push(queue.takeAll());
|
|
183
|
+
return out;
|
|
184
|
+
};
|
|
185
|
+
const startBody = (head) => {
|
|
186
|
+
if (direction === "up") {
|
|
187
|
+
shared.methods.push(head.method);
|
|
188
|
+
if (hasToken(head.fields.get("connection"), "upgrade") && head.fields.has("upgrade")) shared.upgrade = head.fields.get("upgrade").toLowerCase();
|
|
189
|
+
} else {
|
|
190
|
+
const status = head.status;
|
|
191
|
+
if (status === 101) {
|
|
192
|
+
const protocol = shared.upgrade;
|
|
193
|
+
shared.switched = protocol !== null && hasToken(protocol, "websocket") ? {
|
|
194
|
+
up: webSocketFramer(),
|
|
195
|
+
down: webSocketFramer()
|
|
196
|
+
} : tcpFramers();
|
|
197
|
+
return { type: "none" };
|
|
198
|
+
}
|
|
199
|
+
if (status >= 100 && status < 200) return { type: "none" };
|
|
200
|
+
const method = shared.methods.shift();
|
|
201
|
+
shared.upgrade = null;
|
|
202
|
+
if (method === "HEAD" || status === 204 || status === 304) return { type: "none" };
|
|
203
|
+
}
|
|
204
|
+
if (hasToken(head.fields.get("transfer-encoding"), "chunked")) return { type: "chunked" };
|
|
205
|
+
const length = head.fields.get("content-length");
|
|
206
|
+
if (length !== void 0) {
|
|
207
|
+
if (!/^\d+$/.test(length)) return { type: "close" };
|
|
208
|
+
const left = Number(length);
|
|
209
|
+
return left === 0 ? { type: "none" } : {
|
|
210
|
+
type: "length",
|
|
211
|
+
left
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return direction === "down" ? { type: "close" } : { type: "none" };
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* This function gives the length of the chunk at the start of the queue, with its size line and its CRLF. The last
|
|
218
|
+
* chunk includes its trailer. It gives `undefined` when the chunk is not whole yet, and `null` for bytes that are not a chunk.
|
|
219
|
+
*/
|
|
220
|
+
const chunkOf = () => {
|
|
221
|
+
const start = queue.peek(1026);
|
|
222
|
+
const line = indexOfLineEnd(start, 0);
|
|
223
|
+
if (line < 0) return queue.length > MAX_LINE ? null : void 0;
|
|
224
|
+
const sizeText = new TextDecoder("latin1").decode(start.subarray(0, line - 2)).split(";")[0]?.trim() ?? "";
|
|
225
|
+
if (!/^[0-9A-Fa-f]{1,13}$/.test(sizeText)) return null;
|
|
226
|
+
const size = Number.parseInt(sizeText, 16);
|
|
227
|
+
if (size > 0) return queue.length < line + size + 2 ? void 0 : {
|
|
228
|
+
length: line + size + 2,
|
|
229
|
+
last: false
|
|
230
|
+
};
|
|
231
|
+
const all = queue.peek(line + MAX_HEAD);
|
|
232
|
+
for (let at = line;;) {
|
|
233
|
+
const next = indexOfLineEnd(all, at);
|
|
234
|
+
if (next < 0) return queue.length - line > MAX_HEAD ? null : void 0;
|
|
235
|
+
if (next - at === 2) return {
|
|
236
|
+
length: next,
|
|
237
|
+
last: true
|
|
238
|
+
};
|
|
239
|
+
at = next;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
push(bytes) {
|
|
244
|
+
if (raw) return bytes.length === 0 ? [] : [bytes];
|
|
245
|
+
queue.push(bytes);
|
|
246
|
+
const out = [];
|
|
247
|
+
while (queue.length > 0) {
|
|
248
|
+
if (shared.switched !== null && body.type === "none") {
|
|
249
|
+
out.push(...shared.switched[direction].push(queue.takeAll()));
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
if (body.type === "close") {
|
|
253
|
+
out.push(queue.takeAll());
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
if (body.type === "length") {
|
|
257
|
+
const take = Math.min(body.left, queue.length);
|
|
258
|
+
out.push(queue.take(take));
|
|
259
|
+
body.left -= take;
|
|
260
|
+
if (body.left === 0) body = { type: "none" };
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (body.type === "chunked") {
|
|
264
|
+
const chunk = chunkOf();
|
|
265
|
+
if (chunk === void 0) return out;
|
|
266
|
+
if (chunk === null) return giveUp(out);
|
|
267
|
+
const frame = queue.take(chunk.length);
|
|
268
|
+
out.push(frame);
|
|
269
|
+
if (chunk.last) body = { type: "none" };
|
|
270
|
+
else if (frame[frame.length - 2] !== 13 || frame[frame.length - 1] !== 10) return giveUp(out);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const window = queue.peek(65540);
|
|
274
|
+
const end = indexOfHeadEnd(window);
|
|
275
|
+
if (end < 0) return queue.length > MAX_HEAD ? giveUp(out) : out;
|
|
276
|
+
const head = readHttpHead(window.subarray(0, end));
|
|
277
|
+
if (head === void 0 || (direction === "up" ? head.method === null : head.status === null)) return giveUp(out);
|
|
278
|
+
out.push(queue.take(end));
|
|
279
|
+
body = startBody(head);
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
},
|
|
283
|
+
end() {
|
|
284
|
+
return [...queue.length === 0 ? [] : [queue.takeAll()], ...shared.switched?.[direction].end() ?? []];
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export { chunkFramer, httpFramers, readHttpHead, tcpFramers, webSocketFrameLength, webSocketFramer };
|
|
289
|
+
|
|
290
|
+
//# sourceMappingURL=framers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framers.js","names":[],"sources":["../src/framers.ts"],"sourcesContent":["/**\n * A framer cuts the bytes of one direction into frames. A fault with a `frame` point counts these frames, and a fault\n * that changes a frame (`split`, `reorder`, `duplicate`) works on one whole frame.\n *\n * A framer does not change, add or remove a byte. When it cannot read the stream, it gives each chunk as one frame for\n * the rest of the stream. Thus a framer can make the frames coarse, but it cannot break the stream.\n */\n\n/** A framer of one direction. */\nexport interface Framer {\n /** This method takes the bytes of one read, and gives each frame that is whole after this read. */\n push(bytes: Uint8Array): Uint8Array[];\n /** This method gives the bytes that the framer holds at the end of the stream, as one frame, or no frame. */\n end(): Uint8Array[];\n}\n\n/** The framers of the two directions of one connection. */\nexport interface FramerPair {\n readonly up: Framer;\n readonly down: Framer;\n}\n\n/** This framer gives each read as one frame. It is the framer of a TCP stream. */\nexport function chunkFramer(): Framer {\n return { push: (bytes) => (bytes.length === 0 ? [] : [bytes]), end: () => [] };\n}\n\n/** This function gives the frames of a TCP connection: each read is one frame. */\nexport function tcpFramers(): FramerPair {\n return { up: chunkFramer(), down: chunkFramer() };\n}\n\n/** The longest head of an HTTP message that a framer reads. A longer head is not HTTP for the framer. */\nconst MAX_HEAD = 65_536;\n\n/** The longest size line of a chunk that a framer reads. */\nconst MAX_LINE = 1024;\n\n/** A queue of bytes. It joins its chunks only for a frame that it gives, thus a large frame costs one copy. */\nclass ByteQueue {\n /** The chunks from the index `head` hold the bytes. No chunk from `head` is empty. */\n private chunks: Uint8Array[] = [];\n private head = 0;\n length = 0;\n\n push(bytes: Uint8Array): void {\n if (bytes.length === 0) return;\n this.chunks.push(bytes);\n this.length += bytes.length;\n }\n\n /** This method gives the first `count` bytes, or all the bytes when the queue has fewer. */\n peek(count: number): Uint8Array {\n const size = Math.min(count, this.length);\n const first = this.chunks[this.head];\n if (first === undefined) return new Uint8Array(0);\n if (first.length >= size) return first.subarray(0, size);\n const out = new Uint8Array(size);\n for (let at = 0, i = this.head; at < size; i += 1) {\n const part = (this.chunks[i] as Uint8Array).subarray(0, size - at);\n out.set(part, at);\n at += part.length;\n }\n return out;\n }\n\n /** This method removes the first `count` bytes, and gives them. */\n take(count: number): Uint8Array {\n const out = this.peek(count);\n let left = out.length;\n this.length -= left;\n while (left > 0) {\n const chunk = this.chunks[this.head] as Uint8Array;\n if (chunk.length <= left) {\n this.head += 1;\n left -= chunk.length;\n } else {\n this.chunks[this.head] = chunk.subarray(left);\n left = 0;\n }\n }\n // The index moves, and the array drops its used part only now and then. Thus a take costs no shift of the array.\n if (this.length === 0) {\n this.chunks = [];\n this.head = 0;\n } else if (this.head > 64 && this.head * 2 > this.chunks.length) {\n this.chunks = this.chunks.slice(this.head);\n this.head = 0;\n }\n return out;\n }\n\n takeAll(): Uint8Array {\n return this.take(this.length);\n }\n}\n\n/**\n * This function gives the length of the WebSocket frame (RFC 6455, section 5.2) at the start of the bytes. It gives\n * `undefined` when the bytes do not hold the whole header yet, and `null` for a length that is not a safe integer.\n */\nexport function webSocketFrameLength(bytes: Uint8Array): number | undefined | null {\n if (bytes.length < 2) return undefined;\n const second = bytes[1] as number;\n const masked = (second & 0x80) !== 0;\n const short = second & 0x7f;\n let header = 2;\n let payload = short;\n if (short === 126) {\n header = 4;\n if (bytes.length < header) return undefined;\n payload = ((bytes[2] as number) << 8) | (bytes[3] as number);\n } else if (short === 127) {\n header = 10;\n if (bytes.length < header) return undefined;\n const high = (((bytes[2] as number) << 24) >>> 0) + ((bytes[3] as number) << 16) + ((bytes[4] as number) << 8) + (bytes[5] as number);\n const low = (((bytes[6] as number) << 24) >>> 0) + ((bytes[7] as number) << 16) + ((bytes[8] as number) << 8) + (bytes[9] as number);\n if (high >= 0x200000) return null;\n payload = high * 2 ** 32 + low;\n }\n return header + (masked ? 4 : 0) + payload;\n}\n\n/** This framer gives each WebSocket frame as one frame. */\nexport function webSocketFramer(): Framer {\n const queue = new ByteQueue();\n let raw = false;\n return {\n push(bytes) {\n if (raw) return bytes.length === 0 ? [] : [bytes];\n queue.push(bytes);\n const out: Uint8Array[] = [];\n for (;;) {\n const length = webSocketFrameLength(queue.peek(14));\n if (length === null) {\n raw = true;\n if (queue.length > 0) out.push(queue.takeAll());\n return out;\n }\n if (length === undefined || queue.length < length) return out;\n out.push(queue.take(length));\n }\n },\n end() {\n return queue.length === 0 ? [] : [queue.takeAll()];\n },\n };\n}\n\nconst CRLF2 = [13, 10, 13, 10] as const;\n\nconst indexOfHeadEnd = (bytes: Uint8Array): number => {\n for (let i = 0; i + 3 < bytes.length; i += 1) {\n if (bytes[i] === CRLF2[0] && bytes[i + 1] === CRLF2[1] && bytes[i + 2] === CRLF2[2] && bytes[i + 3] === CRLF2[3]) return i + 4;\n }\n return -1;\n};\n\nconst indexOfLineEnd = (bytes: Uint8Array, from: number): number => {\n for (let i = from; i + 1 < bytes.length; i += 1) if (bytes[i] === 13 && bytes[i + 1] === 10) return i + 2;\n return -1;\n};\n\n/** The parts of an HTTP/1.1 head that the framer reads. */\nexport interface HttpHead {\n /** The method of a request, or `null` for a response. */\n readonly method: string | null;\n /** The status of a response, or `null` for a request. */\n readonly status: number | null;\n /** The fields, with lower-case names. Repeated fields are joined with a comma. */\n readonly fields: ReadonlyMap<string, string>;\n}\n\n/** This function reads the start line and the fields of an HTTP/1.1 head. It gives `undefined` for a head that it cannot read. */\nexport function readHttpHead(head: Uint8Array): HttpHead | undefined {\n const text = new TextDecoder(\"latin1\").decode(head);\n const lines = text.split(\"\\r\\n\");\n const start = lines[0] ?? \"\";\n const response = /^HTTP\\/1\\.[01] (\\d{3})(?: |$)/.exec(start);\n const request = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+) \\S+ HTTP\\/1\\.[01]$/.exec(start);\n if (response === null && request === null) return undefined;\n const fields = new Map<string, string>();\n for (const line of lines.slice(1)) {\n if (line === \"\") continue;\n const colon = line.indexOf(\":\");\n if (colon <= 0) return undefined;\n const name = line.slice(0, colon).trim().toLowerCase();\n const value = line.slice(colon + 1).trim();\n fields.set(name, fields.has(name) ? `${fields.get(name) as string}, ${value}` : value);\n }\n return { method: request === null ? null : (request[1] as string), status: response === null ? null : Number(response[1]), fields };\n}\n\nconst hasToken = (value: string | undefined, token: string): boolean => (value ?? \"\").split(\",\").some((part) => part.trim().toLowerCase() === token);\n\ntype Body = { readonly type: \"none\" } | { readonly type: \"length\"; left: number } | { readonly type: \"chunked\" } | { readonly type: \"close\" };\n\n/** The state that the two HTTP framers of one connection share. */\ninterface Shared {\n /** The methods of the requests that have no response yet, in order. */\n readonly methods: string[];\n /** The protocol of an upgrade that a request asked for, in lower case, or `null`. */\n upgrade: string | null;\n /** The framer of each direction after a `101` response, or `null` before it. */\n switched: FramerPair | null;\n}\n\n/**\n * This function gives the frames of an HTTP/1.1 connection. A head is one frame. A body with a length gives each read\n * of the body as one frame. A chunked body gives each chunk as one frame, and the last chunk with its trailer as one\n * frame. After a `101` response to an upgrade to `websocket`, each WebSocket frame is one frame.\n */\nexport function httpFramers(): FramerPair {\n const shared: Shared = { methods: [], upgrade: null, switched: null };\n return { up: httpFramer(\"up\", shared), down: httpFramer(\"down\", shared) };\n}\n\nfunction httpFramer(direction: \"up\" | \"down\", shared: Shared): Framer {\n const queue = new ByteQueue();\n let body: Body = { type: \"none\" };\n let raw = false;\n\n const giveUp = (out: Uint8Array[]): Uint8Array[] => {\n raw = true;\n if (queue.length > 0) out.push(queue.takeAll());\n return out;\n };\n\n const startBody = (head: HttpHead): Body => {\n if (direction === \"up\") {\n shared.methods.push(head.method as string);\n if (hasToken(head.fields.get(\"connection\"), \"upgrade\") && head.fields.has(\"upgrade\")) shared.upgrade = (head.fields.get(\"upgrade\") as string).toLowerCase();\n } else {\n const status = head.status as number;\n if (status === 101) {\n const protocol = shared.upgrade;\n shared.switched = protocol !== null && hasToken(protocol, \"websocket\") ? { up: webSocketFramer(), down: webSocketFramer() } : tcpFramers();\n return { type: \"none\" };\n }\n if (status >= 100 && status < 200) return { type: \"none\" };\n const method = shared.methods.shift();\n shared.upgrade = null;\n if (method === \"HEAD\" || status === 204 || status === 304) return { type: \"none\" };\n }\n if (hasToken(head.fields.get(\"transfer-encoding\"), \"chunked\")) return { type: \"chunked\" };\n const length = head.fields.get(\"content-length\");\n if (length !== undefined) {\n if (!/^\\d+$/.test(length)) return { type: \"close\" };\n const left = Number(length);\n return left === 0 ? { type: \"none\" } : { type: \"length\", left };\n }\n return direction === \"down\" ? { type: \"close\" } : { type: \"none\" };\n };\n\n /**\n * This function gives the length of the chunk at the start of the queue, with its size line and its CRLF. The last\n * chunk includes its trailer. It gives `undefined` when the chunk is not whole yet, and `null` for bytes that are not a chunk.\n */\n const chunkOf = (): { readonly length: number; readonly last: boolean } | undefined | null => {\n const start = queue.peek(MAX_LINE + 2);\n const line = indexOfLineEnd(start, 0);\n if (line < 0) return queue.length > MAX_LINE ? null : undefined;\n const sizeText = new TextDecoder(\"latin1\").decode(start.subarray(0, line - 2)).split(\";\")[0]?.trim() ?? \"\";\n if (!/^[0-9A-Fa-f]{1,13}$/.test(sizeText)) return null;\n const size = Number.parseInt(sizeText, 16);\n if (size > 0) return queue.length < line + size + 2 ? undefined : { length: line + size + 2, last: false };\n // The last chunk: the trailer fields end with an empty line.\n const all = queue.peek(line + MAX_HEAD);\n for (let at = line; ; ) {\n const next = indexOfLineEnd(all, at);\n if (next < 0) return queue.length - line > MAX_HEAD ? null : undefined;\n if (next - at === 2) return { length: next, last: true };\n at = next;\n }\n };\n\n return {\n push(bytes) {\n if (raw) return bytes.length === 0 ? [] : [bytes];\n queue.push(bytes);\n const out: Uint8Array[] = [];\n while (queue.length > 0) {\n if (shared.switched !== null && body.type === \"none\") {\n out.push(...shared.switched[direction].push(queue.takeAll()));\n return out;\n }\n if (body.type === \"close\") {\n out.push(queue.takeAll());\n return out;\n }\n if (body.type === \"length\") {\n const take = Math.min(body.left, queue.length);\n out.push(queue.take(take));\n body.left -= take;\n if (body.left === 0) body = { type: \"none\" };\n continue;\n }\n if (body.type === \"chunked\") {\n const chunk = chunkOf();\n if (chunk === undefined) return out;\n if (chunk === null) return giveUp(out);\n const frame = queue.take(chunk.length);\n out.push(frame);\n if (chunk.last) body = { type: \"none\" };\n else if (frame[frame.length - 2] !== 13 || frame[frame.length - 1] !== 10) return giveUp(out);\n continue;\n }\n const window = queue.peek(MAX_HEAD + 4);\n const end = indexOfHeadEnd(window);\n if (end < 0) return queue.length > MAX_HEAD ? giveUp(out) : out;\n const head = readHttpHead(window.subarray(0, end));\n if (head === undefined || (direction === \"up\" ? head.method === null : head.status === null)) return giveUp(out);\n out.push(queue.take(end));\n body = startBody(head);\n }\n return out;\n },\n end() {\n const rest = queue.length === 0 ? [] : [queue.takeAll()];\n return [...rest, ...(shared.switched?.[direction].end() ?? [])];\n },\n };\n}\n"],"mappings":"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"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { answerBytes } from "./answer.js";
|
|
2
|
+
import { Applier, FaultRecord, Send, Step, applierOf, splitBytes } from "./applier.js";
|
|
3
|
+
import { Framer, FramerPair, HttpHead, chunkFramer, httpFramers, readHttpHead, tcpFramers, webSocketFrameLength, webSocketFramer } from "./framers.js";
|
|
4
|
+
import { ConnectionEnd, ConnectionSummary, FaultProxy, Protocol, ProxyOptions, startFaultProxy } from "./proxy.js";
|
|
5
|
+
export { type Applier, type ConnectionEnd, type ConnectionSummary, type FaultProxy, type FaultRecord, type Framer, type FramerPair, type HttpHead, type Protocol, type ProxyOptions, type Send, type Step, answerBytes, applierOf, chunkFramer, httpFramers, readHttpHead, splitBytes, startFaultProxy, tcpFramers, webSocketFrameLength, webSocketFramer };
|
package/build/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { answerBytes } from "./answer.js";
|
|
2
|
+
import { applierOf, splitBytes } from "./applier.js";
|
|
3
|
+
import { chunkFramer, httpFramers, readHttpHead, tcpFramers, webSocketFrameLength, webSocketFramer } from "./framers.js";
|
|
4
|
+
import { startFaultProxy } from "./proxy.js";
|
|
5
|
+
export { answerBytes, applierOf, chunkFramer, httpFramers, readHttpHead, splitBytes, startFaultProxy, tcpFramers, webSocketFrameLength, webSocketFramer };
|
package/build/proxy.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FaultRecord } from "./applier.js";
|
|
2
|
+
import { FramerPair } from "./framers.js";
|
|
3
|
+
import { Fault, FaultPlan } from "@mark1russell7/rigor-chaos";
|
|
4
|
+
import { Clock } from "@mark1russell7/algebra/clock";
|
|
5
|
+
/** The protocol of the streams. `http` and `websocket` use the same framers: a WebSocket starts as an HTTP upgrade. */
|
|
6
|
+
export type Protocol = "tcp" | "http" | "websocket";
|
|
7
|
+
/** The options of a fault proxy. */
|
|
8
|
+
export interface ProxyOptions {
|
|
9
|
+
/** The server that the proxy connects each client to. The default host is `127.0.0.1`. */
|
|
10
|
+
readonly target: {
|
|
11
|
+
readonly host?: string;
|
|
12
|
+
readonly port: number;
|
|
13
|
+
};
|
|
14
|
+
readonly plan: FaultPlan;
|
|
15
|
+
/** The protocol sets the framers. The default is `tcp`. */
|
|
16
|
+
readonly protocol?: Protocol;
|
|
17
|
+
/** This function makes the framers of each connection. It replaces the framers of `protocol`. */
|
|
18
|
+
readonly framers?: () => FramerPair;
|
|
19
|
+
/** The address of the proxy. The default is `127.0.0.1`, with a free port. */
|
|
20
|
+
readonly listen?: {
|
|
21
|
+
readonly host?: string;
|
|
22
|
+
readonly port?: number;
|
|
23
|
+
};
|
|
24
|
+
/** The clock of the times and the timers. The default is `platformClock()`. A test can give `fakeClock()`. */
|
|
25
|
+
readonly clock?: Clock;
|
|
26
|
+
/** This function gets each record when the fault occurs. */
|
|
27
|
+
readonly onFault?: (record: FaultRecord) => void;
|
|
28
|
+
/** This function gets the summary of each connection when the connection closes. */
|
|
29
|
+
readonly onClose?: (summary: ConnectionSummary) => void;
|
|
30
|
+
}
|
|
31
|
+
/** How a connection ended. */
|
|
32
|
+
export type ConnectionEnd = "close" | "cut" | "reset" | "error";
|
|
33
|
+
/** The summary of one connection. */
|
|
34
|
+
export interface ConnectionSummary {
|
|
35
|
+
readonly connection: number;
|
|
36
|
+
readonly end: ConnectionEnd;
|
|
37
|
+
/** The bytes that the proxy read in each direction. */
|
|
38
|
+
readonly bytes: {
|
|
39
|
+
readonly up: number;
|
|
40
|
+
readonly down: number;
|
|
41
|
+
};
|
|
42
|
+
/** The faults of the connection that did not occur, with their positions in the plan. */
|
|
43
|
+
readonly missed: readonly {
|
|
44
|
+
readonly index: number;
|
|
45
|
+
readonly fault: Fault;
|
|
46
|
+
}[];
|
|
47
|
+
}
|
|
48
|
+
/** A fault proxy that listens. */
|
|
49
|
+
export interface FaultProxy {
|
|
50
|
+
readonly host: string;
|
|
51
|
+
readonly port: number;
|
|
52
|
+
/** The records of the faults, in the order that they occurred. */
|
|
53
|
+
readonly records: readonly FaultRecord[];
|
|
54
|
+
/** The summaries of the connections that closed. */
|
|
55
|
+
readonly connections: readonly ConnectionSummary[];
|
|
56
|
+
/** This method stops the proxy and destroys each open connection. */
|
|
57
|
+
close(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* This function starts a proxy that puts the faults of a plan into the connections between clients and a target. The
|
|
61
|
+
* proxy numbers the connections from 0, in the order that it accepts them. It throws a `RangeError` for a plan with problems.
|
|
62
|
+
*/
|
|
63
|
+
export declare function startFaultProxy(options: ProxyOptions): Promise<FaultProxy>;
|
|
64
|
+
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","names":[],"sources":["../src/proxy.ts"],"mappings":";;;;;YAOY;;iBAGK;;WAEN;aAAmB;aAAwB;;WAC3C,MAAM;;WAEN,WAAW;;WAEX,gBAAgB;;WAEhB;aAAoB;aAAwB;;;WAE5C,QAAQ;;WAER,WAAW,QAAQ;;WAEnB,WAAW,SAAS;;;YAInB;;iBAGK;WACN;WACA,KAAK;;WAEL;aAAkB;aAAqB;;;WAEvC;aAA4B;aAAwB,OAAO;;;;iBAIrD;WACN;WACA;;WAEA,kBAAkB;;WAElB,sBAAsB;;EAE/B,SAAS;;;;;;wBAuBW,gBAAgB,SAAS,eAAe,QAAQ"}
|
package/build/proxy.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { applierOf } from "./applier.js";
|
|
2
|
+
import { httpFramers, tcpFramers } from "./framers.js";
|
|
3
|
+
import { checkFaultPlan } from "@mark1russell7/rigor-chaos";
|
|
4
|
+
import { connect, createServer } from "node:net";
|
|
5
|
+
import { platformClock } from "@mark1russell7/algebra/clock";
|
|
6
|
+
// src/proxy.ts
|
|
7
|
+
/**
|
|
8
|
+
* This function starts a proxy that puts the faults of a plan into the connections between clients and a target. The
|
|
9
|
+
* proxy numbers the connections from 0, in the order that it accepts them. It throws a `RangeError` for a plan with problems.
|
|
10
|
+
*/
|
|
11
|
+
async function startFaultProxy(options) {
|
|
12
|
+
const problems = checkFaultPlan(options.plan);
|
|
13
|
+
if (problems.length > 0) throw new RangeError(`startFaultProxy: ${problems.join(" ")}`);
|
|
14
|
+
const clock = options.clock ?? platformClock();
|
|
15
|
+
const makeFramers = options.framers ?? (options.protocol === void 0 || options.protocol === "tcp" ? tcpFramers : httpFramers);
|
|
16
|
+
const records = [];
|
|
17
|
+
const connections = [];
|
|
18
|
+
const open = /* @__PURE__ */ new Set();
|
|
19
|
+
let next = 0;
|
|
20
|
+
const server = createServer({ allowHalfOpen: true }, (client) => {
|
|
21
|
+
const connection = next;
|
|
22
|
+
next += 1;
|
|
23
|
+
const start = clock.now();
|
|
24
|
+
const now = () => clock.now() - start;
|
|
25
|
+
const upstream = connect({
|
|
26
|
+
host: options.target.host ?? "127.0.0.1",
|
|
27
|
+
port: options.target.port,
|
|
28
|
+
allowHalfOpen: true,
|
|
29
|
+
noDelay: true
|
|
30
|
+
});
|
|
31
|
+
client.setNoDelay(true);
|
|
32
|
+
open.add(client);
|
|
33
|
+
open.add(upstream);
|
|
34
|
+
const pair = makeFramers();
|
|
35
|
+
const side = (direction, from, to) => ({
|
|
36
|
+
direction,
|
|
37
|
+
from,
|
|
38
|
+
to,
|
|
39
|
+
framer: pair[direction],
|
|
40
|
+
applier: applierOf(options.plan, connection, direction),
|
|
41
|
+
queue: [],
|
|
42
|
+
write: void 0,
|
|
43
|
+
wake: void 0,
|
|
44
|
+
stall: void 0,
|
|
45
|
+
stalled: false,
|
|
46
|
+
draining: false,
|
|
47
|
+
ending: false,
|
|
48
|
+
read: 0
|
|
49
|
+
});
|
|
50
|
+
const sides = [side("up", client, upstream), side("down", upstream, client)];
|
|
51
|
+
let end;
|
|
52
|
+
let closed = 0;
|
|
53
|
+
/** After an answer, no byte of the server goes to the client. */
|
|
54
|
+
let answered = false;
|
|
55
|
+
const cancelTimers = () => {
|
|
56
|
+
for (const s of sides) {
|
|
57
|
+
s.write?.();
|
|
58
|
+
s.wake?.();
|
|
59
|
+
s.stall?.();
|
|
60
|
+
s.write = void 0;
|
|
61
|
+
s.wake = void 0;
|
|
62
|
+
s.stall = void 0;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const resume = (s) => {
|
|
66
|
+
if (end === void 0 && !s.stalled && !s.draining) s.from.resume();
|
|
67
|
+
};
|
|
68
|
+
const pump = (s) => {
|
|
69
|
+
s.write = void 0;
|
|
70
|
+
if (end === "reset" || end === "error") return;
|
|
71
|
+
const t = now();
|
|
72
|
+
while (s.queue.length > 0 && s.queue[0].at <= t) {
|
|
73
|
+
const send = s.queue.shift();
|
|
74
|
+
if (!s.to.write(send.bytes) && !s.draining) {
|
|
75
|
+
s.draining = true;
|
|
76
|
+
s.from.pause();
|
|
77
|
+
s.to.once("drain", () => {
|
|
78
|
+
s.draining = false;
|
|
79
|
+
resume(s);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (s.queue.length > 0) s.write = clock.after(s.queue[0].at - t, () => pump(s));
|
|
84
|
+
else if (s.ending && !s.to.writableEnded) s.to.end(() => end === "cut" ? s.to.destroy() : void 0);
|
|
85
|
+
};
|
|
86
|
+
const apply = (s, step) => {
|
|
87
|
+
for (const record of step.records) {
|
|
88
|
+
records.push(record);
|
|
89
|
+
options.onFault?.(record);
|
|
90
|
+
}
|
|
91
|
+
if (step.sends.length > 0 && !(answered && s.direction === "down")) {
|
|
92
|
+
s.queue.push(...step.sends);
|
|
93
|
+
if (s.write === void 0) pump(s);
|
|
94
|
+
}
|
|
95
|
+
if (step.answer !== void 0 && !answered) {
|
|
96
|
+
answered = true;
|
|
97
|
+
const down = sides[1];
|
|
98
|
+
down.queue.push(step.answer);
|
|
99
|
+
if (down.write === void 0) pump(down);
|
|
100
|
+
}
|
|
101
|
+
if (step.pauseUntil !== void 0) {
|
|
102
|
+
s.stalled = true;
|
|
103
|
+
s.from.pause();
|
|
104
|
+
s.stall?.();
|
|
105
|
+
s.stall = clock.after(step.pauseUntil - now(), () => {
|
|
106
|
+
s.stall = void 0;
|
|
107
|
+
s.stalled = false;
|
|
108
|
+
resume(s);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (step.close !== void 0) shut(step.close.kind);
|
|
112
|
+
rearm(s);
|
|
113
|
+
};
|
|
114
|
+
const rearm = (s) => {
|
|
115
|
+
s.wake?.();
|
|
116
|
+
s.wake = void 0;
|
|
117
|
+
const wake = s.applier.wake;
|
|
118
|
+
if (wake !== void 0 && end === void 0) s.wake = clock.after(Math.max(0, wake - now()), () => apply(s, s.applier.tick(now())));
|
|
119
|
+
};
|
|
120
|
+
const finish = (s) => {
|
|
121
|
+
if (s.ending) return;
|
|
122
|
+
for (const frame of s.framer.end()) apply(s, s.applier.push(frame, now()));
|
|
123
|
+
apply(s, s.applier.end(now()));
|
|
124
|
+
s.ending = true;
|
|
125
|
+
if (s.write === void 0) pump(s);
|
|
126
|
+
};
|
|
127
|
+
const shut = (kind) => {
|
|
128
|
+
if (end !== void 0) return;
|
|
129
|
+
end = kind;
|
|
130
|
+
for (const s of sides) s.from.pause();
|
|
131
|
+
if (kind === "reset") {
|
|
132
|
+
cancelTimers();
|
|
133
|
+
client.resetAndDestroy();
|
|
134
|
+
upstream.resetAndDestroy();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const s of sides) {
|
|
138
|
+
s.wake?.();
|
|
139
|
+
s.stall?.();
|
|
140
|
+
apply(s, s.applier.end(now()));
|
|
141
|
+
s.ending = true;
|
|
142
|
+
if (s.write === void 0) pump(s);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
for (const s of sides) {
|
|
146
|
+
s.from.on("data", (bytes) => {
|
|
147
|
+
if (end !== void 0) return;
|
|
148
|
+
s.read += bytes.length;
|
|
149
|
+
for (const frame of s.framer.push(bytes)) {
|
|
150
|
+
apply(s, s.applier.push(frame, now()));
|
|
151
|
+
if (end !== void 0) return;
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
s.from.on("end", () => {
|
|
155
|
+
if (end === void 0) finish(s);
|
|
156
|
+
});
|
|
157
|
+
s.from.on("error", () => {
|
|
158
|
+
end ??= "error";
|
|
159
|
+
cancelTimers();
|
|
160
|
+
for (const other of sides) other.from.destroy();
|
|
161
|
+
});
|
|
162
|
+
s.from.on("close", () => {
|
|
163
|
+
open.delete(s.from);
|
|
164
|
+
closed += 1;
|
|
165
|
+
if (closed === 1) {
|
|
166
|
+
if (end === void 0) finish(s);
|
|
167
|
+
if (s.to.writableFinished) s.to.destroy();
|
|
168
|
+
else s.to.once("finish", () => s.to.destroy());
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
cancelTimers();
|
|
172
|
+
const summary = {
|
|
173
|
+
connection,
|
|
174
|
+
end: end ?? "close",
|
|
175
|
+
bytes: {
|
|
176
|
+
up: sides[0].read,
|
|
177
|
+
down: sides[1].read
|
|
178
|
+
},
|
|
179
|
+
missed: [...sides[0].applier.pending(), ...sides[1].applier.pending()].sort((a, b) => a.index - b.index)
|
|
180
|
+
};
|
|
181
|
+
connections.push(summary);
|
|
182
|
+
options.onClose?.(summary);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
for (const s of sides) apply(s, s.applier.tick(0));
|
|
186
|
+
});
|
|
187
|
+
await new Promise((resolve, reject) => {
|
|
188
|
+
server.once("error", reject);
|
|
189
|
+
server.listen(options.listen?.port ?? 0, options.listen?.host ?? "127.0.0.1", () => {
|
|
190
|
+
server.off("error", reject);
|
|
191
|
+
resolve();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
const address = server.address();
|
|
195
|
+
return {
|
|
196
|
+
host: address.address,
|
|
197
|
+
port: address.port,
|
|
198
|
+
records,
|
|
199
|
+
connections,
|
|
200
|
+
close: () => new Promise((resolve) => {
|
|
201
|
+
server.close(() => resolve());
|
|
202
|
+
for (const socket of open) socket.destroy();
|
|
203
|
+
})
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export { startFaultProxy };
|
|
207
|
+
|
|
208
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.js","names":[],"sources":["../src/proxy.ts"],"sourcesContent":["import { type AddressInfo, type Socket, connect, createServer } from \"node:net\";\nimport { type Cancel, type Clock, platformClock } from \"@mark1russell7/algebra/clock\";\nimport { type Direction, type Fault, type FaultPlan, checkFaultPlan } from \"@mark1russell7/rigor-chaos\";\nimport { type Applier, type FaultRecord, type Send, type Step, applierOf } from \"./applier.ts\";\nimport { type Framer, type FramerPair, httpFramers, tcpFramers } from \"./framers.ts\";\n\n/** The protocol of the streams. `http` and `websocket` use the same framers: a WebSocket starts as an HTTP upgrade. */\nexport type Protocol = \"tcp\" | \"http\" | \"websocket\";\n\n/** The options of a fault proxy. */\nexport interface ProxyOptions {\n /** The server that the proxy connects each client to. The default host is `127.0.0.1`. */\n readonly target: { readonly host?: string; readonly port: number };\n readonly plan: FaultPlan;\n /** The protocol sets the framers. The default is `tcp`. */\n readonly protocol?: Protocol;\n /** This function makes the framers of each connection. It replaces the framers of `protocol`. */\n readonly framers?: () => FramerPair;\n /** The address of the proxy. The default is `127.0.0.1`, with a free port. */\n readonly listen?: { readonly host?: string; readonly port?: number };\n /** The clock of the times and the timers. The default is `platformClock()`. A test can give `fakeClock()`. */\n readonly clock?: Clock;\n /** This function gets each record when the fault occurs. */\n readonly onFault?: (record: FaultRecord) => void;\n /** This function gets the summary of each connection when the connection closes. */\n readonly onClose?: (summary: ConnectionSummary) => void;\n}\n\n/** How a connection ended. */\nexport type ConnectionEnd = \"close\" | \"cut\" | \"reset\" | \"error\";\n\n/** The summary of one connection. */\nexport interface ConnectionSummary {\n readonly connection: number;\n readonly end: ConnectionEnd;\n /** The bytes that the proxy read in each direction. */\n readonly bytes: { readonly up: number; readonly down: number };\n /** The faults of the connection that did not occur, with their positions in the plan. */\n readonly missed: readonly { readonly index: number; readonly fault: Fault }[];\n}\n\n/** A fault proxy that listens. */\nexport interface FaultProxy {\n readonly host: string;\n readonly port: number;\n /** The records of the faults, in the order that they occurred. */\n readonly records: readonly FaultRecord[];\n /** The summaries of the connections that closed. */\n readonly connections: readonly ConnectionSummary[];\n /** This method stops the proxy and destroys each open connection. */\n close(): Promise<void>;\n}\n\ninterface Side {\n readonly direction: Direction;\n readonly from: Socket;\n readonly to: Socket;\n readonly framer: Framer;\n readonly applier: Applier;\n readonly queue: Send[];\n write: Cancel | undefined;\n wake: Cancel | undefined;\n stall: Cancel | undefined;\n stalled: boolean;\n draining: boolean;\n ending: boolean;\n read: number;\n}\n\n/**\n * This function starts a proxy that puts the faults of a plan into the connections between clients and a target. The\n * proxy numbers the connections from 0, in the order that it accepts them. It throws a `RangeError` for a plan with problems.\n */\nexport async function startFaultProxy(options: ProxyOptions): Promise<FaultProxy> {\n const problems = checkFaultPlan(options.plan);\n if (problems.length > 0) throw new RangeError(`startFaultProxy: ${problems.join(\" \")}`);\n const clock = options.clock ?? platformClock();\n const makeFramers = options.framers ?? (options.protocol === undefined || options.protocol === \"tcp\" ? tcpFramers : httpFramers);\n const records: FaultRecord[] = [];\n const connections: ConnectionSummary[] = [];\n const open = new Set<Socket>();\n let next = 0;\n\n const server = createServer({ allowHalfOpen: true }, (client) => {\n const connection = next;\n next += 1;\n const start = clock.now();\n const now = (): number => clock.now() - start;\n const upstream = connect({ host: options.target.host ?? \"127.0.0.1\", port: options.target.port, allowHalfOpen: true, noDelay: true });\n // Each write goes out at once, thus a split or a gap of the plan stays on the wire.\n client.setNoDelay(true);\n open.add(client);\n open.add(upstream);\n const pair = makeFramers();\n const side = (direction: Direction, from: Socket, to: Socket): Side => ({\n direction,\n from,\n to,\n framer: pair[direction],\n applier: applierOf(options.plan, connection, direction),\n queue: [],\n write: undefined,\n wake: undefined,\n stall: undefined,\n stalled: false,\n draining: false,\n ending: false,\n read: 0,\n });\n const sides = [side(\"up\", client, upstream), side(\"down\", upstream, client)] as const;\n let end: ConnectionEnd | undefined;\n let closed = 0;\n /** After an answer, no byte of the server goes to the client. */\n let answered = false;\n\n const cancelTimers = (): void => {\n for (const s of sides) {\n s.write?.();\n s.wake?.();\n s.stall?.();\n s.write = undefined;\n s.wake = undefined;\n s.stall = undefined;\n }\n };\n\n const resume = (s: Side): void => {\n if (end === undefined && !s.stalled && !s.draining) s.from.resume();\n };\n\n const pump = (s: Side): void => {\n s.write = undefined;\n if (end === \"reset\" || end === \"error\") return;\n const t = now();\n while (s.queue.length > 0 && (s.queue[0] as Send).at <= t) {\n const send = s.queue.shift() as Send;\n if (!s.to.write(send.bytes) && !s.draining) {\n s.draining = true;\n s.from.pause();\n s.to.once(\"drain\", () => {\n s.draining = false;\n resume(s);\n });\n }\n }\n if (s.queue.length > 0) s.write = clock.after((s.queue[0] as Send).at - t, () => pump(s));\n else if (s.ending && !s.to.writableEnded) s.to.end(() => (end === \"cut\" ? s.to.destroy() : undefined));\n };\n\n const apply = (s: Side, step: Step): void => {\n for (const record of step.records) {\n records.push(record);\n options.onFault?.(record);\n }\n if (step.sends.length > 0 && !(answered && s.direction === \"down\")) {\n s.queue.push(...step.sends);\n if (s.write === undefined) pump(s);\n }\n if (step.answer !== undefined && !answered) {\n answered = true;\n const down = sides[1];\n down.queue.push(step.answer);\n if (down.write === undefined) pump(down);\n }\n if (step.pauseUntil !== undefined) {\n s.stalled = true;\n s.from.pause();\n s.stall?.();\n s.stall = clock.after(step.pauseUntil - now(), () => {\n s.stall = undefined;\n s.stalled = false;\n resume(s);\n });\n }\n if (step.close !== undefined) shut(step.close.kind);\n rearm(s);\n };\n\n const rearm = (s: Side): void => {\n s.wake?.();\n s.wake = undefined;\n const wake = s.applier.wake;\n if (wake !== undefined && end === undefined) s.wake = clock.after(Math.max(0, wake - now()), () => apply(s, s.applier.tick(now())));\n };\n\n const finish = (s: Side): void => {\n if (s.ending) return;\n for (const frame of s.framer.end()) apply(s, s.applier.push(frame, now()));\n apply(s, s.applier.end(now()));\n s.ending = true;\n if (s.write === undefined) pump(s);\n };\n\n const shut = (kind: \"cut\" | \"reset\"): void => {\n if (end !== undefined) return;\n end = kind;\n for (const s of sides) s.from.pause();\n if (kind === \"reset\") {\n cancelTimers();\n client.resetAndDestroy();\n upstream.resetAndDestroy();\n return;\n }\n for (const s of sides) {\n s.wake?.();\n s.stall?.();\n apply(s, s.applier.end(now()));\n s.ending = true;\n if (s.write === undefined) pump(s);\n }\n };\n\n for (const s of sides) {\n s.from.on(\"data\", (bytes: Buffer) => {\n if (end !== undefined) return;\n s.read += bytes.length;\n for (const frame of s.framer.push(bytes)) {\n apply(s, s.applier.push(frame, now()));\n if (end !== undefined) return;\n }\n });\n s.from.on(\"end\", () => {\n if (end === undefined) finish(s);\n });\n s.from.on(\"error\", () => {\n end ??= \"error\";\n cancelTimers();\n for (const other of sides) other.from.destroy();\n });\n s.from.on(\"close\", () => {\n open.delete(s.from);\n closed += 1;\n if (closed === 1) {\n // The socket closed fully, thus the other socket gets the bytes that wait for it, and then it closes too.\n if (end === undefined) finish(s);\n if (s.to.writableFinished) s.to.destroy();\n else s.to.once(\"finish\", () => s.to.destroy());\n return;\n }\n cancelTimers();\n const summary: ConnectionSummary = {\n connection,\n end: end ?? \"close\",\n bytes: { up: sides[0].read, down: sides[1].read },\n missed: [...sides[0].applier.pending(), ...sides[1].applier.pending()].sort((a, b) => a.index - b.index),\n };\n connections.push(summary);\n options.onClose?.(summary);\n });\n }\n // A fault at 0 ms occurs before the first byte.\n for (const s of sides) apply(s, s.applier.tick(0));\n });\n\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject);\n server.listen(options.listen?.port ?? 0, options.listen?.host ?? \"127.0.0.1\", () => {\n server.off(\"error\", reject);\n resolve();\n });\n });\n const address = server.address() as AddressInfo;\n return {\n host: address.address,\n port: address.port,\n records,\n connections,\n close: () =>\n new Promise<void>((resolve) => {\n server.close(() => resolve());\n for (const socket of open) socket.destroy();\n }),\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
2
3
|
"name": "@mark1russell7/rigor-proxy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "A TCP proxy that applies a seeded fault plan to TCP, HTTP and WebSocket streams, and records each fault.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"proxy",
|
|
8
|
+
"fault-injection",
|
|
9
|
+
"chaos",
|
|
10
|
+
"tcp",
|
|
11
|
+
"http",
|
|
12
|
+
"websocket",
|
|
13
|
+
"testing",
|
|
14
|
+
"resilience"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/mark1russell7/rigor/tree/main/packages/proxy#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/mark1russell7/rigor/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/mark1russell7/rigor.git",
|
|
23
|
+
"directory": "packages/proxy"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "Mark Russell (https://github.com/mark1russell7)",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"main": "./build/index.js",
|
|
30
|
+
"files": [
|
|
31
|
+
"build"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=22.19"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@mark1russell7/algebra": "^0.8.4",
|
|
41
|
+
"@mark1russell7/rigor-chaos": "^0.2.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@mark1russell7/cue": "^2.0.0",
|
|
45
|
+
"@types/node": "^26.6.5",
|
|
46
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
47
|
+
"tsdown": "^0.23.1",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"vitest": "^5.0.3"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:net": "vitest run --config vitest.net.config.ts",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json"
|
|
57
|
+
},
|
|
58
|
+
"types": "./build/index.d.ts",
|
|
59
|
+
"exports": {
|
|
60
|
+
".": {
|
|
61
|
+
"types": "./build/index.d.ts",
|
|
62
|
+
"import": "./build/index.js",
|
|
63
|
+
"default": "./build/index.js"
|
|
64
|
+
},
|
|
65
|
+
"./package.json": "./package.json"
|
|
66
|
+
}
|
|
6
67
|
}
|