DodoTransfer/tests/integration/middleware.test.js
2026-11-05 10:28:10 +09:00

171 lines
5.2 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const os = require('os');
const http = require('node:http');
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'psitransfer-int-'));
process.env.PSITRANSFER_UPLOAD_DIR = tmpDir;
// these tests upload without a password
process.env.PSITRANSFER_REQUIRE_BUCKET_PASSWORD = 'false';
const { test, after } = require('node:test');
const assert = require('node:assert');
const tusMeta = require('../../lib/tusboy/tus-metadata');
const app = require('../../lib/endpoints');
const TUS = { 'Tus-Resumable': '1.0.0' };
const UUID = '00000000-0000-4000-8000-000000000001';
function request(port, method, pathname, headers = {}, body = null) {
return new Promise((resolve, reject) => {
const req = http.request(
{
hostname: '127.0.0.1',
port,
method,
path: pathname,
headers,
},
(res) => {
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => {
resolve({
status: res.statusCode,
headers: res.headers,
body: Buffer.concat(chunks).toString(),
});
});
}
);
req.on('error', reject);
if (body != null) req.write(body);
req.end();
});
}
after(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test('store.info error that is not NotFound yields 500 (instanceof guard)', async (t) => {
const sid = 'corruptsid';
const bucketDir = path.join(tmpDir, sid);
const fileBase = path.join(bucketDir, UUID);
fs.mkdirSync(bucketDir, { recursive: true });
fs.writeFileSync(`${fileBase}.json`, '{ not json', 'utf8');
fs.writeFileSync(fileBase, '', 'utf8');
const server = http.createServer(app);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
const fid = `${sid}++${UUID}`;
const res = await request(port, 'PATCH', `/files/${encodeURIComponent(fid)}`, {
...TUS,
'Upload-Offset': '0',
'Content-Type': 'application/offset+octet-stream',
});
assert.strictEqual(res.status, 500);
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(bucketDir, { recursive: true, force: true });
}
});
test('PATCH with encoded traversal in upload id is rejected before Tus handler', async (t) => {
const server = http.createServer(app);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
const pathname =
'/files/foo%2B%2B' + encodeURIComponent('../..') + '%2Fetc%2Fpasswd';
const res = await request(port, 'PATCH', pathname, {
...TUS,
'Upload-Offset': '0',
'Content-Type': 'application/offset+octet-stream',
});
assert.ok(res.status === 400 || res.status === 404);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('POST with malicious meta.sid is rejected', async (t) => {
const server = http.createServer(app);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
const meta = {
name: 'x',
sid: '../../etc',
retention: '3600',
};
const res = await request(port, 'POST', '/files/', {
...TUS,
'Upload-Length': '10',
'Upload-Metadata': tusMeta.encode(meta),
'Content-Type': 'application/offset+octet-stream',
});
assert.strictEqual(res.status, 400);
assert.match(res.body, /bucket|Invalid/i);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('lock PATCH with traversal bucket id is rejected', async (t) => {
const server = http.createServer(app);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
const res = await request(
port,
'PATCH',
'/files/' + encodeURIComponent('..') + '%2F' + encodeURIComponent('..') + '%2Fetc?lock=yes',
TUS
);
assert.strictEqual(res.status, 400);
} finally {
await new Promise((resolve) => server.close(resolve));
}
});
test('PATCH on a running upload of a locked bucket is rejected', async (t) => {
const server = http.createServer(app);
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
const port = server.address().port;
try {
const sid = 'lockedbucket';
const created = await request(port, 'POST', '/files/', {
...TUS,
'Upload-Length': '10',
'Upload-Metadata': tusMeta.encode({ name: 'x', sid, retention: '3600' }),
});
assert.strictEqual(created.status, 201);
const fid = decodeURIComponent(created.headers.location.split('/').pop());
const lock = await request(port, 'PATCH', `/files/${sid}?lock=yes`, TUS);
assert.strictEqual(lock.status, 204);
const res = await request(port, 'PATCH', `/files/${encodeURIComponent(fid)}`, {
...TUS,
'Upload-Offset': '0',
'Content-Type': 'application/offset+octet-stream',
}, '0123456789');
assert.strictEqual(res.status, 400);
assert.match(res.body, /Bucket locked/);
} finally {
await new Promise((resolve) => server.close(resolve));
fs.rmSync(path.join(tmpDir, 'lockedbucket'), { recursive: true, force: true });
}
});