179 lines
6.6 KiB
JavaScript
179 lines
6.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { once } from 'node:events';
|
|
import test from 'node:test';
|
|
import { adminSignature, uploadSignature } from '../src/auth.mjs';
|
|
import { createApp } from '../src/server.mjs';
|
|
|
|
const noUploadStore = { async initialize() {}, async store() { throw new Error('must not upload'); } };
|
|
|
|
test('sync endpoint authenticates a scoped caller and passes only an optional commit', async (t) => {
|
|
const calls = [];
|
|
const service = {
|
|
async initialize() {},
|
|
async deploySync(value) {
|
|
calls.push(value);
|
|
return { changed: false };
|
|
},
|
|
async recordError() {},
|
|
};
|
|
const secret = 's'.repeat(32);
|
|
const { server } = await createApp({
|
|
maxBodyBytes: 4096,
|
|
syncClientSecrets: { core: secret },
|
|
}, { service, uploadStore: noUploadStore });
|
|
server.listen(0, '127.0.0.1');
|
|
await once(server, 'listening');
|
|
t.after(() => server.close());
|
|
const address = server.address();
|
|
const body = Buffer.from(JSON.stringify({ commit: 'a'.repeat(40) }));
|
|
const timestamp = String(Date.now());
|
|
const requestId = 'sync-request-1234';
|
|
const response = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'x-image-client': 'core',
|
|
'x-image-timestamp': timestamp,
|
|
'x-image-request-id': requestId,
|
|
'x-image-signature': adminSignature(secret, timestamp, requestId, body),
|
|
},
|
|
body,
|
|
});
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.deepEqual(calls, [{ requestKey: `core:${requestId}`, expectedCommit: 'a'.repeat(40) }]);
|
|
});
|
|
|
|
test('upload endpoint accepts a short-lived body-bound grant and rejects replay or tampering', async (t) => {
|
|
const service = {
|
|
async initialize() {},
|
|
async recordError() {},
|
|
};
|
|
const calls = [];
|
|
const uploadStore = {
|
|
async initialize() {},
|
|
async store(value) {
|
|
calls.push(value);
|
|
return { duplicate: false, path: `icons/users/${value.client}/${value.filename}` };
|
|
},
|
|
};
|
|
const secret = 'u'.repeat(32);
|
|
const config = {
|
|
maxBodyBytes: 4096,
|
|
maxUploadBytes: 51200,
|
|
maxContentUploadBytes: 1048576,
|
|
syncClientSecrets: { core: 's'.repeat(32) },
|
|
uploadClientSecrets: { core2026: secret },
|
|
publicBases: ['https://sam-image.hided.net', 'https://sam.hided.net/image'],
|
|
};
|
|
const { server } = await createApp(config, { service, uploadStore });
|
|
server.listen(0, '127.0.0.1');
|
|
await once(server, 'listening');
|
|
t.after(() => server.close());
|
|
const address = server.address();
|
|
const pathname = `/v1/uploads/user-icons/core2026/${'a'.repeat(32)}.png`;
|
|
const body = Buffer.from('89504e470d0a1a0a00000000', 'hex');
|
|
const expires = String(Math.floor(Date.now() / 1000) + 60);
|
|
const requestId = 'upload-request-1234';
|
|
const signature = uploadSignature(secret, { expires, requestId, pathname, contentType: 'image/png', body });
|
|
const headers = {
|
|
'content-type': 'image/png',
|
|
'x-image-client': 'core2026',
|
|
'x-image-expires': expires,
|
|
'x-image-request-id': requestId,
|
|
'x-image-signature': signature,
|
|
};
|
|
const accepted = await fetch(`http://127.0.0.1:${address.port}${pathname}`, { method: 'PUT', headers, body });
|
|
assert.equal(accepted.status, 201);
|
|
assert.deepEqual(calls[0], {
|
|
requestKey: `core2026:${requestId}`,
|
|
category: 'user-icons',
|
|
client: 'core2026',
|
|
filename: `${'a'.repeat(32)}.png`,
|
|
body,
|
|
});
|
|
assert.deepEqual((await accepted.json()).urls, [
|
|
`https://sam-image.hided.net/icons/users/core2026/${'a'.repeat(32)}.png`,
|
|
`https://sam.hided.net/image/icons/users/core2026/${'a'.repeat(32)}.png`,
|
|
]);
|
|
|
|
const contentPath = `/v1/uploads/content/core2026/${'b'.repeat(32)}.webp`;
|
|
const contentBody = Buffer.concat([Buffer.from('RIFF'), Buffer.alloc(4), Buffer.from('WEBP'), Buffer.alloc(4)]);
|
|
const contentRequestId = 'content-request-1234';
|
|
const contentSignature = uploadSignature(secret, {
|
|
expires,
|
|
requestId: contentRequestId,
|
|
pathname: contentPath,
|
|
contentType: 'image/webp',
|
|
body: contentBody,
|
|
});
|
|
const contentResponse = await fetch(`http://127.0.0.1:${address.port}${contentPath}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'content-type': 'image/webp',
|
|
'x-image-client': 'core2026',
|
|
'x-image-expires': expires,
|
|
'x-image-request-id': contentRequestId,
|
|
'x-image-signature': contentSignature,
|
|
},
|
|
body: contentBody,
|
|
});
|
|
assert.equal(contentResponse.status, 201);
|
|
assert.equal(calls[1].category, 'content');
|
|
assert.equal(calls[1].filename, `${'b'.repeat(32)}.webp`);
|
|
|
|
const tampered = await fetch(`http://127.0.0.1:${address.port}${pathname}`, {
|
|
method: 'PUT', headers, body: Buffer.from('89504e470d0a1a0affffffff', 'hex'),
|
|
});
|
|
assert.equal(tampered.status, 401);
|
|
const expired = '1000000000';
|
|
const expiredHeaders = {
|
|
...headers,
|
|
'x-image-expires': expired,
|
|
'x-image-signature': uploadSignature(secret, { expires: expired, requestId, pathname, contentType: 'image/png', body }),
|
|
};
|
|
const expiredResponse = await fetch(`http://127.0.0.1:${address.port}${pathname}`, {
|
|
method: 'PUT', headers: expiredHeaders, body,
|
|
});
|
|
assert.equal(expiredResponse.status, 401);
|
|
});
|
|
|
|
test('sync endpoint rejects unknown callers and body fields outside the sync contract', async (t) => {
|
|
const service = {
|
|
async initialize() {},
|
|
async deploySync() { throw new Error('must not deploy'); },
|
|
async recordError() {},
|
|
};
|
|
const secret = 's'.repeat(32);
|
|
const { server } = await createApp(
|
|
{ maxBodyBytes: 4096, syncClientSecrets: { core: secret } },
|
|
{ service, uploadStore: noUploadStore },
|
|
);
|
|
server.listen(0, '127.0.0.1');
|
|
await once(server, 'listening');
|
|
t.after(() => server.close());
|
|
const address = server.address();
|
|
const body = Buffer.from(JSON.stringify({ branch: 'preview' }));
|
|
const timestamp = String(Date.now());
|
|
const requestId = 'sync-request-5678';
|
|
const signedHeaders = {
|
|
'content-type': 'application/json',
|
|
'x-image-timestamp': timestamp,
|
|
'x-image-request-id': requestId,
|
|
'x-image-signature': adminSignature(secret, timestamp, requestId, body),
|
|
};
|
|
|
|
const unknown = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
|
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'unknown' }, body,
|
|
});
|
|
assert.equal(unknown.status, 401);
|
|
const prototypeName = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
|
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'toString' }, body,
|
|
});
|
|
assert.equal(prototypeName.status, 401);
|
|
const extraField = await fetch(`http://127.0.0.1:${address.port}/v1/sync`, {
|
|
method: 'POST', headers: { ...signedHeaders, 'x-image-client': 'core' }, body,
|
|
});
|
|
assert.equal(extraField.status, 400);
|
|
});
|