Keep large file views from disconnecting (#2482)

* fix(files): keep large downloads connected

Large file reads were emitted as one frame, crossing the physical socket high-water mark and terminating otherwise healthy connections. Stream bounded chunks from one file handle and pace each transfer by its own send completion without globally queueing unrelated traffic.

* fix(files): keep paced relay transfers bounded

Carry send completion through encryption to the physical WebSocket callback, cap growing files at their advertised size, and preserve text classification when a UTF-8 code point crosses the sample boundary.

* fix(files): reject changing transfer snapshots

Abort when a file shrinks below its advertised size, finalize UTF-8 validation for complete samples, and let the physical relay socket remain the sole authority for queued-byte accounting.

* fix(files): validate metadata before transfer

Keep the daemon handshake pending until the ready frame is physically sent, and classify file content with a bounded full-handle scan so streaming preserves the previous binary/text behavior.

* fix(files): detect changing transfer sources

* fix(files): enforce transfer snapshot integrity
This commit is contained in:
Mohamed Boudra
2026-07-27 13:52:57 +02:00
committed by GitHub
parent b97d6d13f3
commit e1b1ca569d
16 changed files with 1044 additions and 158 deletions
+116 -1
View File
@@ -1,5 +1,10 @@
import { describe, it, expect, vi } from "vitest";
import { createClientChannel, createDaemonChannel, Transport } from "./encrypted-channel.js";
import {
createClientChannel,
createDaemonChannel,
EncryptedChannel,
Transport,
} from "./encrypted-channel.js";
import {
deriveSharedKey,
encrypt,
@@ -47,6 +52,64 @@ async function waitForAsyncDelivery(): Promise<void> {
}
describe("EncryptedChannel", () => {
it("rejects the daemon handshake when the ready frame fails to send", async () => {
const daemonKeyPair = generateKeyPair();
const clientKeyPair = generateKeyPair();
const transport: Transport = {
send: () => Promise.reject(new Error("ready send failed")),
close: () => undefined,
onmessage: null,
onclose: null,
onerror: null,
};
const channel = createDaemonChannel(transport, daemonKeyPair);
transport.onmessage?.({
data: JSON.stringify({
type: "e2ee_hello",
key: exportPublicKey(clientKeyPair.publicKey),
}),
isBinary: false,
});
await expect(channel).rejects.toThrow("ready send failed");
});
it("waits for transport send completion", async () => {
let completeSend: (() => void) | undefined;
const transport: Transport = {
send: () =>
new Promise<void>((resolve) => {
completeSend = resolve;
}),
close: () => undefined,
onmessage: null,
onclose: null,
onerror: null,
};
const first = generateKeyPair();
const second = generateKeyPair();
const channel = new EncryptedChannel(
transport,
deriveSharedKey(first.secretKey, second.publicKey),
{},
{ binaryCiphertext: true },
);
channel.setState("open");
let completed = false;
const sending = channel.send(new Uint8Array([1, 2, 3]).buffer).then(() => {
completed = true;
return undefined;
});
await Promise.resolve();
expect(completed).toBe(false);
completeSend?.();
await sending;
expect(completed).toBe(true);
});
it("establishes encrypted channel between daemon and client", async () => {
const [daemonTransport, clientTransport] = createMockTransportPair();
@@ -191,6 +254,58 @@ describe("EncryptedChannel", () => {
}
});
it("reports rejected handshake hello sends", async () => {
const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
const transport: Transport = {
send: () => Promise.reject(new Error("hello send failed")),
close: vi.fn(),
onmessage: null,
onclose: null,
onerror: null,
};
const onerror = vi.fn();
await createClientChannel(transport, daemonPubKeyB64, { onerror });
await Promise.resolve();
expect(onerror).toHaveBeenCalledTimes(1);
expect((onerror.mock.calls[0][0] as Error).message).toBe("hello send failed");
transport.onclose?.(1000, "closed");
});
it("reports rejected sends while flushing the handshake backlog", async () => {
const daemonKeyPair = generateKeyPair();
const daemonPubKeyB64 = exportPublicKey(daemonKeyPair.publicKey);
let sendAttempts = 0;
const transport: Transport = {
send: () => {
sendAttempts += 1;
return sendAttempts === 1 ? undefined : Promise.reject(new Error("backlog send failed"));
},
close: vi.fn(),
onmessage: null,
onclose: null,
onerror: null,
};
const onerror = vi.fn();
const channel = await createClientChannel(transport, daemonPubKeyB64, { onerror });
await channel.send(new ArrayBuffer(8));
transport.onmessage?.({
data: JSON.stringify({
type: "e2ee_ready",
capabilities: { binaryCiphertext: true },
}),
isBinary: false,
});
await waitForAsyncDelivery();
expect(onerror).toHaveBeenCalledTimes(1);
expect((onerror.mock.calls[0][0] as Error).message).toBe("backlog send failed");
expect(transport.close).toHaveBeenCalledWith(1011, "backlog send failed");
});
it("fails handshake on invalid hello", async () => {
const [daemonTransport] = createMockTransportPair();