Files
openmaic/OpenMAIC/tests/server/public-url-fetch.test.ts
2026-08-16 14:58:47 +08:00

254 lines
8.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => {
const agentInstances: Array<{
options: Record<string, unknown>;
close: ReturnType<typeof vi.fn>;
}> = [];
return {
lookup: vi.fn(),
fetch: vi.fn(),
agentInstances,
};
});
vi.mock('node:dns', () => ({
promises: { lookup: mocks.lookup },
}));
vi.mock('undici', () => ({
Agent: class MockAgent {
readonly close = vi.fn().mockResolvedValue(undefined);
constructor(readonly options: Record<string, unknown>) {
mocks.agentInstances.push(this);
}
},
fetch: mocks.fetch,
}));
const originalAllowLocalNetworks = process.env.ALLOW_LOCAL_NETWORKS;
describe('public URL pinned fetch', () => {
beforeEach(() => {
vi.resetModules();
mocks.lookup.mockReset();
mocks.fetch.mockReset();
mocks.agentInstances.length = 0;
delete process.env.ALLOW_LOCAL_NETWORKS;
});
afterEach(() => {
if (originalAllowLocalNetworks === undefined) {
delete process.env.ALLOW_LOCAL_NETWORKS;
} else {
process.env.ALLOW_LOCAL_NETWORKS = originalAllowLocalNetworks;
}
});
it('accepts only ordinary global-unicast IP literals', async () => {
const { resolvePublicTarget } = await import('@/lib/server/public-url-fetch');
await expect(resolvePublicTarget('https://8.8.8.8/media.png')).resolves.toMatchObject({
hostname: '8.8.8.8',
addresses: [{ address: '8.8.8.8', family: 4 }],
});
await expect(
resolvePublicTarget('https://[2606:4700:4700::1111]/media.png'),
).resolves.toMatchObject({
hostname: '2606:4700:4700::1111',
addresses: [{ address: '2606:4700:4700::1111', family: 6 }],
});
for (const target of [
'http://127.0.0.1/',
'http://100.100.100.200/latest/meta-data/',
'http://198.18.0.1/',
'http://203.0.113.10/',
'http://224.0.0.1/',
'http://[::ffff:127.0.0.1]/',
'http://[2001:db8::1]/',
'http://[2002:0808:0808::]/',
'http://[2001:4860:0:1::5efe:10.0.0.1]/',
]) {
await expect(resolvePublicTarget(target)).rejects.toMatchObject({
code: 'BLOCKED_TARGET',
});
}
expect(mocks.lookup).not.toHaveBeenCalled();
});
it('rejects a hostname if any DNS answer is not global-unicast', async () => {
mocks.lookup.mockResolvedValue([
{ address: '93.184.216.34', family: 4 },
{ address: '100.100.100.200', family: 4 },
]);
const { resolvePublicTarget } = await import('@/lib/server/public-url-fetch');
await expect(resolvePublicTarget('https://mixed.example/media')).rejects.toMatchObject({
code: 'BLOCKED_TARGET',
});
});
it('never honors ALLOW_LOCAL_NETWORKS for the public media fetch policy', async () => {
process.env.ALLOW_LOCAL_NETWORKS = 'true';
mocks.lookup.mockResolvedValue([{ address: '192.168.1.20', family: 4 }]);
const { resolvePublicTarget } = await import('@/lib/server/public-url-fetch');
await expect(resolvePublicTarget('http://10.0.0.1/file')).rejects.toMatchObject({
code: 'BLOCKED_TARGET',
});
await expect(resolvePublicTarget('https://internal.example/file')).rejects.toMatchObject({
code: 'BLOCKED_TARGET',
});
});
it('pins the connector to the first verified DNS result set without a second lookup', async () => {
mocks.lookup
.mockResolvedValueOnce([
{ address: '93.184.216.34', family: 4 },
{ address: '2606:2800:220:1:248:1893:25c8:1946', family: 6 },
])
// A validate-then-fetch implementation would consume this rebinding answer.
.mockResolvedValueOnce([{ address: '127.0.0.1', family: 4 }]);
mocks.fetch.mockResolvedValue(new Response('media', { status: 200 }));
const { fetchPinnedPublicUrl } = await import('@/lib/server/public-url-fetch');
const result = await fetchPinnedPublicUrl('https://rebind.example/media.png');
expect(mocks.lookup).toHaveBeenCalledTimes(1);
expect(mocks.fetch).toHaveBeenCalledOnce();
expect(String(mocks.fetch.mock.calls[0][0])).toBe('https://rebind.example/media.png');
const agentOptions = mocks.agentInstances[0].options as {
connect: {
lookup: (
hostname: string,
options: { all: boolean },
callback: (error: Error | null, addresses: unknown) => void,
) => void;
};
};
const pinnedAddresses = await new Promise<unknown>((resolve, reject) => {
agentOptions.connect.lookup('rebind.example', { all: true }, (error, addresses) => {
if (error) reject(error);
else resolve(addresses);
});
});
expect(pinnedAddresses).toEqual([
{ address: '93.184.216.34', family: 4 },
{ address: '2606:2800:220:1:248:1893:25c8:1946', family: 6 },
]);
expect(mocks.lookup).toHaveBeenCalledTimes(1);
await result.dispose();
expect(mocks.agentInstances[0].close).toHaveBeenCalledOnce();
});
it('fails a pinned lookup if a connector asks for a different hostname', async () => {
mocks.lookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
const { createPinnedLookup, resolvePublicTarget } =
await import('@/lib/server/public-url-fetch');
const target = await resolvePublicTarget('https://safe.example/media');
const pinnedLookup = createPinnedLookup(target);
const error = await new Promise<Error | null>((resolve) => {
pinnedLookup('changed.example', { all: true }, (lookupError) => resolve(lookupError));
});
expect(error).toMatchObject({ code: 'ENOTFOUND' });
});
it('closes the dispatcher and returns a generic typed error when transport fails', async () => {
mocks.lookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
mocks.fetch.mockRejectedValue(new Error('connect ECONNREFUSED 10.0.0.8:443'));
const { fetchPinnedPublicUrl } = await import('@/lib/server/public-url-fetch');
await expect(fetchPinnedPublicUrl('https://safe.example/media')).rejects.toMatchObject({
code: 'UPSTREAM_FAILURE',
message: 'Unable to fetch upstream media',
});
expect(mocks.agentInstances[0].close).toHaveBeenCalledOnce();
});
it('passes trusted authentication headers without allowing Host to be overridden', async () => {
mocks.lookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
mocks.fetch.mockResolvedValue(new Response('ok'));
const { fetchPinnedPublicUrl } = await import('@/lib/server/public-url-fetch');
const result = await fetchPinnedPublicUrl('https://safe.example/models', {
headers: { Authorization: 'Bearer secret' },
});
expect(mocks.fetch).toHaveBeenCalledWith(
new URL('https://safe.example/models'),
expect.objectContaining({
headers: { Authorization: 'Bearer secret' },
redirect: 'manual',
}),
);
await result.dispose();
await expect(
fetchPinnedPublicUrl('https://safe.example/models', {
headers: { Host: 'internal.example' },
}),
).rejects.toMatchObject({ code: 'INVALID_URL' });
await expect(
fetchPinnedPublicUrl('https://safe.example/models', {
headers: { 'Content-Length': '1' },
}),
).rejects.toMatchObject({ code: 'INVALID_URL' });
expect(mocks.fetch).toHaveBeenCalledTimes(1);
});
it('supports a bounded PUT through the same pinned dispatcher', async () => {
mocks.lookup.mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
mocks.fetch.mockResolvedValue(new Response('', { status: 200 }));
const { fetchPinnedPublicUrl } = await import('@/lib/server/public-url-fetch');
const body = Buffer.from('document bytes');
const result = await fetchPinnedPublicUrl('https://upload.example/presigned?signature=secret', {
method: 'PUT',
body,
maxRequestBytes: body.byteLength,
maxResponseBytes: 1024,
headersTimeoutMs: 180_000,
bodyTimeoutMs: 60_000,
});
expect(mocks.lookup).toHaveBeenCalledTimes(1);
expect(mocks.fetch).toHaveBeenCalledWith(
new URL('https://upload.example/presigned?signature=secret'),
expect.objectContaining({
method: 'PUT',
body,
redirect: 'manual',
}),
);
expect(mocks.agentInstances[0].options).toMatchObject({
headersTimeout: 180_000,
bodyTimeout: 60_000,
maxResponseSize: 1024,
});
await result.dispose();
expect(mocks.agentInstances[0].close).toHaveBeenCalledOnce();
});
it('rejects an oversized PUT before DNS or transport work', async () => {
const { fetchPinnedPublicUrl } = await import('@/lib/server/public-url-fetch');
await expect(
fetchPinnedPublicUrl('https://upload.example/presigned', {
method: 'PUT',
body: Buffer.from('too large'),
maxRequestBytes: 1,
}),
).rejects.toMatchObject({ code: 'REQUEST_TOO_LARGE' });
expect(mocks.lookup).not.toHaveBeenCalled();
expect(mocks.fetch).not.toHaveBeenCalled();
expect(mocks.agentInstances).toHaveLength(0);
});
});