This commit is contained in:
decolua
2026-06-15 18:18:04 +07:00
parent 8ab5af0052
commit b282f05549
66 changed files with 2328 additions and 213 deletions
+21 -5
View File
@@ -9,24 +9,40 @@
*/
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
// Mock DNS so the SSRF guard treats example.com as public.
vi.mock("node:dns/promises", () => ({ lookup: async () => ({ address: "93.184.216.34" }) }));
import { CodexExecutor } from "../../open-sse/executors/codex.js";
import * as proxyFetchModule from "../../open-sse/utils/proxyFetch.js";
const IMAGE_1MB_BYTES = 1024 * 1024;
const REMOTE_URL = "https://example.com/big.jpg";
const DATA_URI = "data:image/png;base64,iVBORw0KGgo=";
// JPEG magic bytes (FF D8 FF) so magic-byte verification passes.
const JPEG_MAGIC = [0xff, 0xd8, 0xff];
function makeImageBuffer(sizeBytes) {
const buf = new Uint8Array(sizeBytes);
for (let i = 0; i < sizeBytes; i++) buf[i] = i & 0xff;
return buf.buffer;
for (let i = 0; i < JPEG_MAGIC.length; i++) buf[i] = JPEG_MAGIC[i];
for (let i = JPEG_MAGIC.length; i < sizeBytes; i++) buf[i] = i & 0xff;
return buf;
}
function mockImageFetch(sizeBytes, mimeType = "image/jpeg") {
// Mock a streaming Response body (getReader) as the hardened fetcher expects.
function mockImageFetch(sizeBytes) {
const bytes = makeImageBuffer(sizeBytes);
return {
ok: true,
headers: { get: (k) => (k === "Content-Type" ? mimeType : null) },
arrayBuffer: async () => makeImageBuffer(sizeBytes),
body: {
getReader() {
let sent = false;
return {
read: async () => sent ? { done: true } : (sent = true, { done: false, value: bytes }),
cancel: async () => {},
};
},
},
};
}
+78
View File
@@ -0,0 +1,78 @@
import { describe, it, expect } from "vitest";
import { detectRequiredCapabilities, reorderByCapabilities } from "../../open-sse/services/combo.js";
describe("detectRequiredCapabilities", () => {
it("text-only -> empty", () => {
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: "hi" }] });
expect(r.size).toBe(0);
});
it("openai image_url -> vision", () => {
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: [
{ type: "image_url", image_url: { url: "x" } },
] }] });
expect(r.has("vision")).toBe(true);
});
it("openai file -> pdf", () => {
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: [
{ type: "file", file: { file_data: "data:application/pdf;base64,x" } },
] }] });
expect(r.has("pdf")).toBe(true);
});
it("gemini inlineData image -> vision", () => {
const r = detectRequiredCapabilities({ contents: [{ role: "user", parts: [
{ inlineData: { mimeType: "image/png", data: "x" } },
] }] });
expect(r.has("vision")).toBe(true);
});
it("antigravity request.contents image -> vision", () => {
const r = detectRequiredCapabilities({ request: { contents: [{ role: "user", parts: [
{ inlineData: { mimeType: "image/jpeg", data: "x" } },
] }] } });
expect(r.has("vision")).toBe(true);
});
it("web_search tool -> search", () => {
const r = detectRequiredCapabilities({ messages: [{ role: "user", content: "q" }], tools: [
{ type: "web_search" },
] });
expect(r.has("search")).toBe(true);
});
it("responses input_image -> vision", () => {
const r = detectRequiredCapabilities({ input: [{ role: "user", content: [
{ type: "input_image", image_url: "x" },
] }] });
expect(r.has("vision")).toBe(true);
});
});
describe("reorderByCapabilities", () => {
it("no required -> unchanged", () => {
const models = ["a/x", "b/y"];
expect(reorderByCapabilities(models, new Set())).toBe(models);
});
it("floats vision-capable model to front, keeps fallback", () => {
// deepseek-chat = no vision; claude-sonnet = vision
const models = ["deepseek/deepseek-chat", "anthropic/claude-sonnet-4.6"];
const out = reorderByCapabilities(models, new Set(["vision"]));
expect(out[0]).toBe("anthropic/claude-sonnet-4.6");
expect(out).toContain("deepseek/deepseek-chat"); // not dropped
expect(out).toHaveLength(2);
});
it("keeps order when no model matches", () => {
const models = ["deepseek/deepseek-chat", "deepseek/deepseek-reasoner"];
const out = reorderByCapabilities(models, new Set(["vision"]));
expect(out).toBe(models);
});
it("single model -> unchanged", () => {
const models = ["a/x"];
expect(reorderByCapabilities(models, new Set(["vision"]))).toBe(models);
});
});
+39
View File
@@ -61,6 +61,26 @@ describe("dashboard guard public LLM API access", () => {
expect(mocks.validateApiKey).not.toHaveBeenCalled();
});
it("rejects remote Host-spoof when real peer IP is non-loopback", async () => {
const response = await proxy(request("/v1/chat/completions", {
host: "localhost",
"x-9r-real-ip": "10.204.111.34",
}));
expect(response.status).toBe(401);
expect(response.body.error).toBe("API key required for remote API access");
});
it("allows loopback peer IP regardless of Host", async () => {
const response = await proxy(request("/v1/chat/completions", {
host: "localhost:20128",
"x-9r-real-ip": "127.0.0.1",
}));
expect(response).toBe(mocks.nextResponse);
expect(mocks.validateApiKey).not.toHaveBeenCalled();
});
it("rejects remote rewritten public LLM API without API key", async () => {
const response = await proxy(request("/api/v1/chat/completions", { host: "router.example.com" }));
@@ -89,6 +109,25 @@ describe("dashboard guard public LLM API access", () => {
expect(response.body.error).toBe("API key required for remote API access");
});
it("rejects remote codex rewrite without API key", async () => {
const response = await proxy(request("/codex/x", { host: "router.example.com" }));
expect(response.status).toBe(401);
expect(response.body.error).toBe("API key required for remote API access");
});
it("allows remote codex rewrite with valid API key", async () => {
mocks.validateApiKey.mockResolvedValue(true);
const response = await proxy(request("/codex/x", {
host: "router.example.com",
authorization: "Bearer sk-valid",
}));
expect(response).toBe(mocks.nextResponse);
expect(mocks.validateApiKey).toHaveBeenCalledWith("sk-valid");
});
it("allows remote public LLM API with valid bearer API key", async () => {
mocks.validateApiKey.mockResolvedValue(true);
+57
View File
@@ -0,0 +1,57 @@
import { describe, it, expect } from "vitest";
import { convertOpenAIContentToParts } from "../../open-sse/translator/formats/gemini.js";
import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.js";
import { VALID_OPENAI_CONTENT_TYPES, OPENAI_BLOCK, CLAUDE_BLOCK } from "../../open-sse/translator/schema/index.js";
const PDF_DATA = "data:application/pdf;base64,JVBERi0xLjE=";
const PNG_DATA = "data:image/png;base64,iVBORw0KGgo=";
describe("file/document block support", () => {
it("schema: file is a valid openai content type", () => {
expect(VALID_OPENAI_CONTENT_TYPES).toContain(OPENAI_BLOCK.FILE);
expect(OPENAI_BLOCK.FILE).toBe("file");
expect(CLAUDE_BLOCK.DOCUMENT).toBe("document");
});
it("gemini: openai file block -> inlineData", () => {
const parts = convertOpenAIContentToParts([
{ type: "text", text: "read this" },
{ type: "file", file: { filename: "d.pdf", file_data: PDF_DATA } },
]);
const inline = parts.find((p) => p.inlineData);
expect(inline).toBeTruthy();
expect(inline.inlineData.mime_type).toBe("application/pdf");
expect(inline.inlineData.data).toBe("JVBERi0xLjE=");
});
it("gemini: ignores non-data-uri file", () => {
const parts = convertOpenAIContentToParts([
{ type: "file", file: { filename: "d.pdf", file_data: "https://x/d.pdf" } },
]);
expect(parts.some((p) => p.inlineData)).toBe(false);
});
it("claude: openai file (pdf) -> document block", () => {
const out = openaiToClaudeRequest("claude-x", {
messages: [{ role: "user", content: [
{ type: "text", text: "read" },
{ type: "file", file: { filename: "d.pdf", file_data: PDF_DATA } },
] }],
}, false);
const blocks = out.messages[0].content;
const doc = blocks.find((b) => b.type === "document");
expect(doc).toBeTruthy();
expect(doc.source.media_type).toBe("application/pdf");
});
it("claude: non-pdf file is dropped (not a document)", () => {
const out = openaiToClaudeRequest("claude-x", {
messages: [{ role: "user", content: [
{ type: "text", text: "read" },
{ type: "file", file: { filename: "i.png", file_data: PNG_DATA } },
] }],
}, false);
const blocks = out.messages[0].content;
expect(blocks.some((b) => b.type === "document")).toBe(false);
});
});
+77
View File
@@ -0,0 +1,77 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock DNS lookup so we control which host resolves to what IP.
const lookupMock = vi.fn();
vi.mock("node:dns/promises", () => ({ lookup: (...a) => lookupMock(...a) }));
import { fetchImageAsBase64 } from "../../open-sse/translator/concerns/image.js";
const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
function mockFetchOnce(bytes, ok = true) {
const body = {
getReader() {
let sent = false;
return {
read: async () => sent ? { done: true } : (sent = true, { done: false, value: new Uint8Array(bytes) }),
cancel: async () => {},
};
},
};
globalThis.fetch = vi.fn(async () => ({ ok, body }));
}
beforeEach(() => {
lookupMock.mockReset();
lookupMock.mockResolvedValue({ address: "93.184.216.34" }); // public by default
});
afterEach(() => { vi.restoreAllMocks(); });
describe("fetchImageAsBase64 hardening", () => {
it("rejects non-http url", async () => {
expect(await fetchImageAsBase64("ftp://x/y.png")).toBeNull();
expect(await fetchImageAsBase64("data:image/png;base64,xx")).toBeNull();
});
it("SSRF: rejects private IP (10.x)", async () => {
lookupMock.mockResolvedValue({ address: "10.0.0.5" });
expect(await fetchImageAsBase64("http://internal.example/x.png")).toBeNull();
});
it("SSRF: rejects cloud metadata 169.254.169.254", async () => {
lookupMock.mockResolvedValue({ address: "169.254.169.254" });
expect(await fetchImageAsBase64("http://metadata/x.png")).toBeNull();
});
it("SSRF: rejects blocked hostname localhost", async () => {
expect(await fetchImageAsBase64("http://localhost/x.png")).toBeNull();
});
it("SSRF: rejects IPv6 loopback", async () => {
lookupMock.mockResolvedValue({ address: "::1" });
expect(await fetchImageAsBase64("http://x/y.png")).toBeNull();
});
it("accepts valid PNG from public host", async () => {
mockFetchOnce(PNG);
const r = await fetchImageAsBase64("https://example.com/a.png");
expect(r).not.toBeNull();
expect(r.mimeType).toBe("image/png");
expect(r.url.startsWith("data:image/png;base64,")).toBe(true);
});
it("rejects disguised non-image payload (magic byte mismatch)", async () => {
mockFetchOnce(Buffer.from("<?php system($_GET[c]); ?>"));
expect(await fetchImageAsBase64("https://example.com/evil.png")).toBeNull();
});
it("rejects payload over size cap", async () => {
mockFetchOnce(Buffer.alloc(1024));
expect(await fetchImageAsBase64("https://example.com/big.png", { maxBytes: 100 })).toBeNull();
});
it("returns null when fetch not ok", async () => {
mockFetchOnce(PNG, false);
expect(await fetchImageAsBase64("https://example.com/404.png")).toBeNull();
});
});
+108
View File
@@ -0,0 +1,108 @@
import { describe, it, expect } from "vitest";
import { stripUnsupportedModalities } from "../../open-sse/translator/concerns/modality.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
const NO_VISION = { vision: false, audioInput: true, pdf: true };
const NO_AUDIO = { vision: true, audioInput: false, pdf: true };
const NO_PDF = { vision: true, audioInput: true, pdf: false };
const ALL = { vision: true, audioInput: true, pdf: true };
describe("stripUnsupportedModalities", () => {
it("fast-exits when model supports all modalities", () => {
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "x" } }] }] };
expect(stripUnsupportedModalities(body, FORMATS.OPENAI, ALL)).toBe(false);
expect(body.messages[0].content).toHaveLength(1);
});
it("openai: strips image when vision:false, leaves placeholder", () => {
const body = { messages: [{ role: "user", content: [
{ type: "text", text: "hi" },
{ type: "image_url", image_url: { url: "data:image/png;base64,xx" } },
] }] };
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_VISION);
const types = body.messages[0].content.map((b) => b.type);
expect(types).toContain("text");
expect(types).not.toContain("image_url");
expect(body.messages[0].content.some((b) => b.type === "text" && /image omitted/.test(b.text))).toBe(true);
});
it("openai: strips input_audio when audioInput:false", () => {
const body = { messages: [{ role: "user", content: [
{ type: "input_audio", input_audio: { data: "x", format: "wav" } },
] }] };
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_AUDIO);
expect(body.messages[0].content.some((b) => b.type === "input_audio")).toBe(false);
expect(body.messages[0].content.some((b) => /audio omitted/.test(b.text || ""))).toBe(true);
});
it("openai: strips file when pdf:false", () => {
const body = { messages: [{ role: "user", content: [
{ type: "file", file: { filename: "d.pdf", file_data: "data:application/pdf;base64,x" } },
] }] };
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_PDF);
expect(body.messages[0].content.some((b) => b.type === "file")).toBe(false);
});
it("openai: keeps image when vision:true", () => {
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "x" } }] }] };
stripUnsupportedModalities(body, FORMATS.OPENAI, NO_AUDIO);
expect(body.messages[0].content.some((b) => b.type === "image_url")).toBe(true);
});
it("claude: strips image + document by capability", () => {
const body = { messages: [{ role: "user", content: [
{ type: "text", text: "hi" },
{ type: "image", source: { type: "base64", media_type: "image/png", data: "x" } },
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: "x" } },
] }] };
stripUnsupportedModalities(body, FORMATS.CLAUDE, { vision: false, audioInput: true, pdf: false });
const types = body.messages[0].content.map((b) => b.type);
expect(types).not.toContain("image");
expect(types).not.toContain("document");
expect(types).toContain("text");
});
it("gemini: strips inlineData image by mime when vision:false", () => {
const body = { contents: [{ role: "user", parts: [
{ text: "hi" },
{ inlineData: { mimeType: "image/png", data: "x" } },
] }] };
stripUnsupportedModalities(body, FORMATS.GEMINI, NO_VISION);
expect(body.contents[0].parts.some((p) => p.inlineData)).toBe(false);
expect(body.contents[0].parts.some((p) => /image omitted/.test(p.text || ""))).toBe(true);
});
it("gemini: keeps inlineData pdf when pdf:true, strips image when vision:false", () => {
const body = { contents: [{ role: "user", parts: [
{ inlineData: { mimeType: "image/png", data: "x" } },
{ inlineData: { mimeType: "application/pdf", data: "y" } },
] }] };
stripUnsupportedModalities(body, FORMATS.GEMINI, NO_VISION);
const mimes = body.contents[0].parts.filter((p) => p.inlineData).map((p) => p.inlineData.mimeType);
expect(mimes).toEqual(["application/pdf"]);
});
it("antigravity: strips inside request.contents", () => {
const body = { request: { contents: [{ role: "user", parts: [
{ inlineData: { mimeType: "image/png", data: "x" } },
] }] } };
stripUnsupportedModalities(body, FORMATS.ANTIGRAVITY, NO_VISION);
expect(body.request.contents[0].parts.some((p) => p.inlineData)).toBe(false);
});
it("responses: strips input_image when vision:false", () => {
const body = { input: [{ role: "user", content: [
{ type: "input_text", text: "hi" },
{ type: "input_image", image_url: "data:image/png;base64,x" },
] }] };
stripUnsupportedModalities(body, FORMATS.OPENAI_RESPONSES, NO_VISION);
expect(body.input[0].content.some((b) => b.type === "input_image")).toBe(false);
expect(body.input[0].content.some((b) => b.type === "input_text" && /image omitted/.test(b.text))).toBe(true);
});
it("handles missing/empty body safely", () => {
expect(stripUnsupportedModalities(null, FORMATS.OPENAI, NO_VISION)).toBe(false);
expect(stripUnsupportedModalities({}, FORMATS.OPENAI, null)).toBe(false);
expect(stripUnsupportedModalities({ messages: [] }, FORMATS.OPENAI, NO_VISION)).toBe(true);
});
});
+58
View File
@@ -0,0 +1,58 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("../../open-sse/translator/concerns/image.js", async (orig) => {
const actual = await orig();
return {
...actual,
fetchImageAsBase64: vi.fn(async () => ({ url: "data:image/png;base64,QUJD", mimeType: "image/png" })),
};
});
import { prefetchRemoteImages } from "../../open-sse/translator/concerns/prefetch.js";
import { fetchImageAsBase64 } from "../../open-sse/translator/concerns/image.js";
import { FORMATS } from "../../open-sse/translator/formats.js";
beforeEach(() => { fetchImageAsBase64.mockClear(); });
afterEach(() => { vi.restoreAllMocks(); });
describe("prefetchRemoteImages", () => {
it("no-op for targets that accept remote URLs (openai)", async () => {
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "https://x/a.png" } }] }] };
const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.OPENAI);
expect(n).toBe(0);
expect(body.messages[0].content[0].image_url.url).toBe("https://x/a.png");
});
it("openai source -> ollama target: converts remote URL to base64", async () => {
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "https://x/a.png" } }] }] };
const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.OLLAMA);
expect(n).toBe(1);
expect(body.messages[0].content[0].image_url.url.startsWith("data:image/png;base64,")).toBe(true);
});
it("skips data URI (already inline)", async () => {
const body = { messages: [{ role: "user", content: [{ type: "image_url", image_url: { url: "data:image/png;base64,xx" } }] }] };
const n = await prefetchRemoteImages(body, FORMATS.OPENAI, FORMATS.OLLAMA);
expect(n).toBe(0);
expect(fetchImageAsBase64).not.toHaveBeenCalled();
});
it("gemini source -> gemini target: fileData URL -> inlineData base64", async () => {
const body = { contents: [{ role: "user", parts: [
{ fileData: { mimeType: "image/png", fileUri: "https://x/a.png" } },
] }] };
const n = await prefetchRemoteImages(body, FORMATS.GEMINI, FORMATS.GEMINI);
expect(n).toBe(1);
expect(body.contents[0].parts[0].inlineData).toBeTruthy();
expect(body.contents[0].parts[0].fileData).toBeUndefined();
});
it("claude source -> kiro target: source.url -> base64", async () => {
const body = { messages: [{ role: "user", content: [
{ type: "image", source: { type: "url", url: "https://x/a.png" } },
] }] };
const n = await prefetchRemoteImages(body, FORMATS.CLAUDE, FORMATS.KIRO);
expect(n).toBe(1);
expect(body.messages[0].content[0].source.type).toBe("base64");
});
});
+3 -3
View File
@@ -95,9 +95,9 @@ describe("RTK filters", () => {
const input = makeFindOutput();
const out = find(input);
expect(out).toContain("55 files in 3 dirs:");
expect(out).toContain("./src/a/ (30):");
expect(out).toContain("./src/b/ (20):");
expect(out).toContain("./ (5):");
expect(out).toContain("./src/a/ (30)");
expect(out).toContain("./src/b/ (20)");
expect(out).toContain("./ (5)");
expect(out.length).toBeLessThan(input.length);
});