Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/message/custom_event_message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "@Packages/message/common";
import { ReadyWrap } from "@App/pkg/utils/ready-wrap";
import type { ScriptEnvTag } from "@Packages/message/consts";
import { assertStructuredMessageSize } from "./message_size";

// 避免页面载入后改动 Map.prototype 导致消息传递失败
const relatedTargetMapMethods = {
Expand Down Expand Up @@ -133,6 +134,7 @@ export class CustomEventMessage implements Message {

nativeSend(detail: any) {
if (!this.readyWrap.isReady) throw new Error("custom_event_message is not ready.");
assertStructuredMessageSize(detail, "CustomEvent.detail");
pageDispatchCustomEvent(this.sendFlag, detail);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/message/extension_message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import EventEmitter from "eventemitter3";
import type { Message, MessageConnect, MessageSend, RuntimeMessageSender, TMessage, TMessageCommAction } from "./types";
import { uuidv4 } from "@App/pkg/utils/uuid";
import { assertStructuredMessageSize } from "./message_size";

const listenerMgr = new EventEmitter<string, any>(); // 单一管理器

Expand All @@ -9,6 +10,7 @@ export class ExtensionMessage implements Message {

connect(data: TMessage): Promise<MessageConnect> {
return new Promise((resolve) => {
assertStructuredMessageSize(data, "chrome.runtime.connect");
const con = chrome.runtime.connect();
con.postMessage(data);
resolve(new ExtensionMessageConnect(con));
Expand All @@ -18,6 +20,7 @@ export class ExtensionMessage implements Message {
// 发送消息 注意不进行回调的内存泄漏
sendMessage<T = any>(data: TMessage): Promise<T> {
return new Promise((resolve: ((value: T) => void) | null) => {
assertStructuredMessageSize(data, "chrome.runtime.sendMessage");
chrome.runtime.sendMessage(data, (resp: T) => {
const lastError = chrome.runtime.lastError;
if (lastError) {
Expand Down Expand Up @@ -188,6 +191,7 @@ export class ExtensionMessageConnect implements MessageConnect {
// 無法 sendMessage 不应该屏蔽错误
throw new Error("Attempted to sendMessage on a disconnected port.");
}
assertStructuredMessageSize(data, "chrome.runtime.Port.postMessage");
this.con.postMessage(data);
}

Expand Down Expand Up @@ -242,6 +246,7 @@ export class ExtensionContentMessageSend implements MessageSend {

sendMessage<T = any>(data: TMessage): Promise<T> {
return new Promise((resolve) => {
assertStructuredMessageSize(data, "chrome.tabs.sendMessage");
if (!this.options?.documentId && !this.options?.frameId) {
// 发送给指定的tab
chrome.tabs.sendMessage(this.tabId, data, (resp: T) => {
Expand All @@ -267,6 +272,7 @@ export class ExtensionContentMessageSend implements MessageSend {

connect(data: TMessage): Promise<MessageConnect> {
return new Promise((resolve) => {
assertStructuredMessageSize(data, "chrome.tabs.connect");
const con = chrome.tabs.connect(this.tabId, this.options);
con.postMessage(data);
resolve(new ExtensionMessageConnect(con));
Expand Down
21 changes: 15 additions & 6 deletions packages/message/message_queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import EventEmitter from "eventemitter3";
import LoggerCore from "@App/app/logger/core";
import { type TMessage } from "./types";
import type { SystemConfigKey, SystemConfigValueType } from "@App/pkg/config/config";
import { assertStructuredMessageSize, MessageSizeError } from "./message_size";

export type TKeyValue<T extends SystemConfigKey> = {
key: T;
Expand Down Expand Up @@ -79,19 +80,27 @@ export class MessageQueue implements IMessageQueue {
// "Could not establish connection. Receiving end does not exist."——不接住就会变成
// 未处理的 Promise rejection。publish 本身是"广播给任何在监听的人",无人监听应静默忽略。
const messageQueueLogger = LoggerCore.getInstance().logger({ service: "messageQueue" });
chrome.runtime
.sendMessage({
msgQueue: topic,
data: { action: "message", message },
})
.catch((e) => {
const outbound = {
msgQueue: topic,
data: { action: "message", message },
};
try {
assertStructuredMessageSize(outbound, "message-queue.publish");
chrome.runtime.sendMessage(outbound).catch((e) => {
const msg = JSON.stringify(e?.message || e);
if (msg.includes("Could not establish connection. Receiving end does not exist.")) {
messageQueueLogger.debug("No target audience for .publish", { msg });
} else {
messageQueueLogger.error("Unable to execute runtime.sendMessage for .publish", { msg });
}
});
} catch (e) {
if (e instanceof MessageSizeError) {
messageQueueLogger.error("Unable to execute runtime.sendMessage for .publish", { error: e.message });
} else {
throw e;
}
}
this.EE.emit(topic, message);
//@ts-ignore
messageQueueLogger.trace("publish", { topic, message });
Expand Down
77 changes: 77 additions & 0 deletions packages/message/message_size.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, expect, it, vi } from "vitest";
import {
MAX_EXTENSION_MESSAGE_BYTES,
MessageSizeError,
assertMessageSize,
measureMessageBytes,
measureStructuredMessageBytes,
utf8ByteLength,
} from "./message_size";
import { ExtensionMessage } from "./extension_message";

describe("message size accounting", () => {
it("counts UTF-8 bytes after JSON escaping", () => {
const value = { data: '你好"\\😀' };
const serialized = JSON.stringify(value);

expect(utf8ByteLength(serialized)).toBe(new TextEncoder().encode(serialized).byteLength);
expect(measureMessageBytes(value)).toBe(utf8ByteLength(serialized));
expect(measureMessageBytes(value)).toBeGreaterThan(value.data.length);
});

it("measures the complete envelope and accepts exact limits", () => {
const envelope = { messageId: "m1", type: "sendMessage", data: { action: "resource", data: "x" } };
const bytes = measureMessageBytes(envelope);

expect(() => assertMessageSize(envelope, "test-channel", bytes)).not.toThrow();
expect(() => assertMessageSize(envelope, "test-channel", bytes - 1)).toThrow(
new MessageSizeError("test-channel", bytes, bytes - 1).message
);
});

it("includes base64 expansion and reports deterministic over-limit diagnostics", () => {
const envelope = {
msgQueue: "backup",
data: { action: "message", message: { base64: "data:image/png;base64," + "A0==" } },
};
const bytes = measureMessageBytes(envelope);
const error = (() => {
try {
assertMessageSize(envelope, "message-queue", bytes - 1);
return undefined;
} catch (e) {
return e;
}
})();

expect(error).toBeInstanceOf(MessageSizeError);
expect((error as MessageSizeError).message).toBe(
`message-queue message exceeds limit: ${bytes} bytes (limit ${bytes - 1} bytes)`
);
});

it("counts binary payloads on structured-clone channels", () => {
const blob = new Blob([new Uint8Array([1, 2, 3])], { type: "application/octet-stream" });
const envelope = { messageId: "m1", type: "sendMessage", data: { blob } };

expect(measureStructuredMessageBytes(envelope)).toBe(measureMessageBytes(envelope) + blob.size);
});

it("counts binary values nested in Map and Set structured-clone payloads", () => {
const mapBlob = new Blob([new Uint8Array([1, 2])]);
const setBlob = new Blob([new Uint8Array([3, 4, 5])]);
const envelope = { data: new Map([["blob", mapBlob]]), values: new Set([setBlob]) };

expect(measureStructuredMessageBytes(envelope)).toBe(measureMessageBytes(envelope) + mapBlob.size + setBlob.size);
});

it("rejects an oversized extension message before calling chrome.runtime", async () => {
const sendMessage = vi.spyOn(chrome.runtime, "sendMessage");
const message = new ExtensionMessage();

await expect(
message.sendMessage({ action: "large", data: "x".repeat(MAX_EXTENSION_MESSAGE_BYTES) })
).rejects.toBeInstanceOf(MessageSizeError);
expect(sendMessage).not.toHaveBeenCalled();
});
});
102 changes: 102 additions & 0 deletions packages/message/message_size.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/** Chrome's documented per-message runtime limit. */
export const MAX_EXTENSION_MESSAGE_BYTES = 64 * 1024 * 1024;

export class MessageSizeError extends Error {
constructor(
public readonly operation: string,
public readonly actualBytes: number,
public readonly limitBytes: number,
reason?: string
) {
super(
reason
? `${operation} message cannot be serialized: ${reason}`
: `${operation} message exceeds limit: ${actualBytes} bytes (limit ${limitBytes} bytes)`
);
this.name = "MessageSizeError";
}
}

export function utf8ByteLength(value: string): number {
return new TextEncoder().encode(value).byteLength;
}

export function serializeMessage(value: unknown): string {
const serialized = JSON.stringify(value);
if (serialized === undefined) {
throw new TypeError("JSON.stringify returned undefined");
}
return serialized;
}

export function measureMessageBytes(value: unknown): number {
return utf8ByteLength(serializeMessage(value));
}

export function measureStructuredMessageBytes(value: unknown): number {
return measureMessageBytes(value) + measureStructuredBinaryBytes(value);
}

export function assertMessageSize(value: unknown, operation: string, limitBytes = MAX_EXTENSION_MESSAGE_BYTES): number {
let bytes: number;
try {
bytes = measureMessageBytes(value);
} catch (error) {
throw new MessageSizeError(
operation,
Number.NaN,
limitBytes,
error instanceof Error ? error.message : String(error)
);
}
if (bytes > limitBytes) {
throw new MessageSizeError(operation, bytes, limitBytes);
}
return bytes;
}

export function assertStructuredMessageSize(
value: unknown,
operation: string,
limitBytes = MAX_EXTENSION_MESSAGE_BYTES
): number {
let bytes: number;
try {
bytes = measureStructuredMessageBytes(value);
} catch (error) {
throw new MessageSizeError(
operation,
Number.NaN,
limitBytes,
error instanceof Error ? error.message : String(error)
);
}
if (bytes > limitBytes) {
throw new MessageSizeError(operation, bytes, limitBytes);
}
return bytes;
}

function measureStructuredBinaryBytes(value: unknown, seen = new WeakSet<object>()): number {
if (typeof value !== "object" || value === null || seen.has(value)) return 0;
seen.add(value);

if (typeof Blob !== "undefined" && value instanceof Blob) return value.size;
if (value instanceof ArrayBuffer) return value.byteLength;
if (ArrayBuffer.isView(value)) return Object.keys(value).length === 0 ? value.byteLength : 0;

if (value instanceof Map) {
let total = 0;
for (const [key, child] of value) {
total += measureStructuredBinaryBytes(key, seen) + measureStructuredBinaryBytes(child, seen);
}
return total;
}
if (value instanceof Set) {
let total = 0;
for (const child of value) total += measureStructuredBinaryBytes(child, seen);
return total;
}

return Object.values(value).reduce((total, child) => total + measureStructuredBinaryBytes(child, seen), 0);
}
13 changes: 13 additions & 0 deletions packages/message/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { GetSenderType, SenderConnect, SenderRuntime, Server, type IGetSender }
import { CustomEventMessage } from "./custom_event_message";
import type { MessageConnect, RuntimeMessageSender } from "./types";
import { uuidv4 } from "@App/pkg/utils/uuid";
import { MAX_EXTENSION_MESSAGE_BYTES } from "./message_size";

let inboundMessage: CustomEventMessage;
let outboundMessage: CustomEventMessage;
Expand Down Expand Up @@ -459,6 +460,18 @@ describe("Server", () => {
"level1-after",
]);
});

it("过大的响应不会再次触发传输异常,而会返回有界诊断", async () => {
server.on("oversized-response", () => "x".repeat(MAX_EXTENSION_MESSAGE_BYTES));

const response = await client.sendMessage({
action: "api/oversized-response",
data: {},
});

expect(response.code).toBe(-1);
expect(response.message).toContain("message-server/oversized-response message exceeds limit");
});
});

describe("IGetSender 功能测试", () => {
Expand Down
Loading
Loading