'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-features-')); process.env.PSITRANSFER_UPLOAD_DIR = tmpDir; process.env.PSITRANSFER_UPLOAD_PASS = 'up-secret'; process.env.PSITRANSFER_ADMIN_PASS = 'ad-secret'; process.env.PSITRANSFER_ACCESS_LOG = ''; const { test, before, 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 PNG = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.alloc(64)]); const ADMIN = { 'x-passwd': 'ad-secret' }; let server; let port; let token; function request(method, pathname, headers = {}, body = null) { return new Promise((resolve, reject) => { const req = http.request( { hostname: '127.0.0.1', port, method, path: pathname, headers, agent: false }, (res) => { const chunks = []; res.on('data', (c) => chunks.push(c)); res.on('end', () => { const buf = Buffer.concat(chunks); let json; try { json = JSON.parse(buf.toString()); } catch (e) { /* not json */ } resolve({ status: res.statusCode, headers: res.headers, body: buf, text: buf.toString(), json }); }); } ); req.on('error', reject); if (body != null) req.write(body); req.end(); }); } const bearer = () => ({ Authorization: `Bearer ${ token }` }); const jsonBody = (obj) => [{ 'Content-Type': 'application/json' }, JSON.stringify(obj)]; /** Upload one file via Tus, returns { fid, key, created } or the failed creation response */ async function upload({ sid, name = 'file.txt', content = 'hello', retention = '3600', password, internalBypass, headers = bearer() }) { const meta = { name, sid, retention }; if (password) meta.password = password; if (internalBypass) meta.internalBypass = internalBypass; const created = await request('POST', '/files/', { ...TUS, ...headers, 'Upload-Length': String(Buffer.byteLength(content)), 'Upload-Metadata': tusMeta.encode(meta), }); if (created.status !== 201) return { created }; const fid = decodeURIComponent(created.headers.location.split('/').pop()); const patched = await request('PATCH', `/files/${ encodeURIComponent(fid) }`, { ...TUS, ...headers, 'Upload-Offset': '0', 'Content-Type': 'application/offset+octet-stream', }, content); assert.strictEqual(patched.status, 204); return { fid, key: fid.split('++')[1], created }; } const tick = () => new Promise(resolve => setTimeout(resolve, 100)); before(async () => { server = http.createServer(app); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); port = server.address().port; const login = await request('POST', '/login', ...jsonBody({ password: 'up-secret' })); assert.strictEqual(login.status, 200); token = login.json.token; }); after(async () => { server.closeAllConnections(); await new Promise((resolve) => server.close(resolve)); fs.rmSync(tmpDir, { recursive: true, force: true }); }); // ---------------------------------------------------------------- login test('login token: wrong password, right password, token reuse', async () => { const wrong = await request('POST', '/login', ...jsonBody({ password: 'nope' })); assert.strictEqual(wrong.status, 403); assert.ok(token && token.includes('.')); assert.strictEqual((await request('GET', '/config.json')).status, 401); assert.strictEqual((await request('GET', '/config.json', { Authorization: 'Bearer garbage' })).status, 403); const withToken = await request('GET', '/config.json', bearer()); assert.strictEqual(withToken.status, 200); assert.ok(withToken.json.session.token, 'the token is renewed on use'); assert.ok(withToken.json.session.exp > Date.now() + 6 * 24 * 3600 * 1000, 'valid for about 7 days'); assert.deepStrictEqual(withToken.json.quickRetentions, ['one-time', '86400', '259200', '432000', '604800']); assert.strictEqual(withToken.json.authRequired, true); }); test('legacy x-passwd header still works for scripts', async () => { const res = await request('GET', '/config.json', { 'x-passwd': 'up-secret' }); assert.strictEqual(res.status, 200); const up = await upload({ sid: 'legacyheader', headers: { 'x-passwd': 'up-secret' }, password: 'pw' }); assert.ok(up.fid); }); test('uploading needs a login', async () => { const meta = tusMeta.encode({ name: 'x', sid: 'nologin', retention: '3600', password: 'p' }); const res = await request('POST', '/files/', { ...TUS, 'Upload-Length': '1', 'Upload-Metadata': meta }); assert.strictEqual(res.status, 401); }); // ------------------------------------------------------- password rules test('a password is required except for one-time downloads', async () => { const noPw = await upload({ sid: 'needpw', retention: '3600' }); assert.strictEqual(noPw.created.status, 400); assert.match(noPw.created.json.message, /password/i); const oneTime = await upload({ sid: 'needpw1', retention: 'one-time' }); assert.ok(oneTime.fid, 'one-time may go without password'); const withPw = await upload({ sid: 'needpw2', retention: '3600', password: 'pw' }); assert.ok(withPw.fid); }); test('metadata the server owns can not be forged by the client', async () => { const up = await upload({ sid: 'forgery', password: 'pw' }); const meta = tusMeta.encode({ name: 'a.txt', sid: 'forgery', retention: '3600', password: 'pw', thumb: 'text/html', lastDownload: '1', bucketLocked: 'true' }); const res = await request('POST', '/files/', { ...TUS, ...bearer(), 'Upload-Length': '1', 'Upload-Metadata': meta }); assert.strictEqual(res.status, 201); const list = await request('GET', '/forgery.json', { 'x-download-pass': 'pw' }); const forged = list.json.items.find(i => i.key !== up.key); // unfinished uploads are not listed assert.strictEqual(forged, undefined); }); // -------------------------------------------------- listing and bypass test('download list: password, wrong password, no password leak', async () => { const { key } = await upload({ sid: 'plainlist', password: 'secret1', name: 'doc.txt' }); assert.strictEqual((await request('GET', '/plainlist.json')).status, 401); assert.strictEqual((await request('GET', '/plainlist.json', { 'x-download-pass': 'bad' })).status, 401); // a login alone does not skip a password unless the uploader allowed it assert.strictEqual((await request('GET', '/plainlist.json', bearer())).status, 401); const ok = await request('GET', '/plainlist.json', { 'x-download-pass': 'secret1' }); assert.strictEqual(ok.status, 200); assert.strictEqual(ok.json.bypassed, false); const item = ok.json.items[0]; assert.strictEqual(item.key, key); assert.strictEqual(item.metadata.hasPassword, true); assert.strictEqual(item.metadata.password, undefined, 'the hash is not sent'); assert.strictEqual(ok.json.session, null, 'no login, no token'); assert.ok(ok.json.expiresAt > Date.now()); assert.strictEqual(item.expiresAt, ok.json.expiresAt); }); test('logged in users skip the password when the uploader allowed it', async () => { await upload({ sid: 'bypassed', password: 'secret2', internalBypass: 'true' }); assert.strictEqual((await request('GET', '/bypassed.json')).status, 401); assert.strictEqual((await request('GET', '/bypassed.json', { Authorization: 'Bearer garbage' })).status, 401); const viaLogin = await request('GET', '/bypassed.json', bearer()); assert.strictEqual(viaLogin.status, 200); assert.strictEqual(viaLogin.json.bypassed, true); assert.ok(viaLogin.json.session.token, 'using the download page renews the login'); // everybody else still needs the password const viaPass = await request('GET', '/bypassed.json', { 'x-download-pass': 'secret2' }); assert.strictEqual(viaPass.status, 200); assert.strictEqual(viaPass.json.bypassed, false); }); test('the bypass flag is ignored without a password', async () => { const { fid } = await upload({ sid: 'bypassnopw', retention: 'one-time', internalBypass: 'true' }); const info = JSON.parse(fs.readFileSync(path.join(tmpDir, ...fid.split('++')) + '.json', 'utf8')); assert.strictEqual(info.metadata.internalBypass, undefined); assert.strictEqual(info.metadata.password, undefined); }); test('archive of a bucket needs the token from the list', async () => { await upload({ sid: 'arch1', password: 'pw', name: 'a.txt', content: 'AAA' }); await upload({ sid: 'arch1', password: 'pw', name: 'b.txt', content: 'BBB' }); const list = await request('GET', '/arch1.json', { 'x-download-pass': 'pw' }); const res = await request('GET', `/files/arch1++${ list.json.archiveToken }.zip`); assert.strictEqual(res.status, 200); assert.strictEqual(res.headers['content-type'], 'application/zip'); assert.strictEqual(res.body.subarray(0, 2).toString(), 'PK'); const bad = await request('GET', '/files/arch1++' + '0'.repeat(32) + '.zip'); assert.strictEqual(bad.status, 404); }); // -------------------------------------------------------------- preview test('inline preview: type from the file name, not a download, no one-time', async () => { const { fid, key } = await upload({ sid: 'preview1', password: 'pw', name: 'note.txt', content: 'hi' }); const res = await request('GET', `/files/${ fid }?inline=1`); assert.strictEqual(res.status, 200); assert.strictEqual(res.headers['content-type'], 'text/plain; charset=utf-8'); assert.strictEqual(res.headers['content-disposition'], 'inline'); assert.strictEqual(res.headers['x-content-type-options'], 'nosniff'); assert.match(res.headers['content-security-policy'], /sandbox/); await tick(); let list = await request('GET', '/preview1.json', { 'x-download-pass': 'pw' }); assert.strictEqual(list.json.items[0].metadata.lastDownload, undefined, 'a preview is not a download'); assert.strictEqual(list.json.items[0].previewType, 'text'); const dl = await request('GET', `/files/${ fid }`); assert.strictEqual(dl.status, 200); assert.match(dl.headers['content-disposition'], /^attachment; filename="note.txt"/); await tick(); list = await request('GET', '/preview1.json', { 'x-download-pass': 'pw' }); assert.ok(list.json.items.find(i => i.key === key).metadata.lastDownload, 'a download is counted'); }); test('inline preview: html and unknown types are refused, pdf has no sandbox', async () => { const html = await upload({ sid: 'preview2', password: 'pw', name: 'page.html', content: '' }); assert.strictEqual((await request('GET', `/files/${ html.fid }?inline=1`)).status, 404); const bin = await upload({ sid: 'preview2', password: 'pw', name: 'data.bin' }); assert.strictEqual((await request('GET', `/files/${ bin.fid }?inline=1`)).status, 404); const pdf = await upload({ sid: 'preview2', password: 'pw', name: 'doc.pdf', content: '%PDF-1.4' }); const res = await request('GET', `/files/${ pdf.fid }?inline=1`); assert.strictEqual(res.status, 200); assert.strictEqual(res.headers['content-type'], 'application/pdf'); assert.strictEqual(res.headers['content-security-policy'], undefined); }); test('inline preview never uses up a one-time file', async () => { const { fid } = await upload({ sid: 'preview3', name: 'once.txt', retention: 'one-time', content: 'secret' }); assert.strictEqual((await request('GET', `/files/${ fid }?inline=1`)).status, 404); const dl = await request('GET', `/files/${ fid }`); assert.strictEqual(dl.status, 200); assert.strictEqual(dl.text, 'secret'); }); // ------------------------------------------------------------ thumbnails test('thumbnails: upload, serve, delete with the file', async () => { const { fid, key } = await upload({ sid: 'thumbs1', password: 'pw', name: 'pic.png', content: 'x' }); assert.strictEqual((await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, {}, PNG)).status, 401); const bad = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), Buffer.from('not an image')); assert.strictEqual(bad.status, 400); const ok = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, { ...bearer(), 'Content-Type': 'image/png' }, PNG); assert.strictEqual(ok.status, 204); const list = await request('GET', '/thumbs1.json', { 'x-download-pass': 'pw' }); const item = list.json.items[0]; assert.match(item.thumbUrl, /files\/thumbs1\+\+.+\.thumb$/); const img = await request('GET', item.thumbUrl.replace(/^\//, '/')); assert.strictEqual(img.status, 200); assert.strictEqual(img.headers['content-type'], 'image/png'); assert.strictEqual(img.headers['x-content-type-options'], 'nosniff'); assert.ok(img.body.equals(PNG)); const thumbFile = path.join(tmpDir, 'thumbs1', key) + '.thumb'; assert.ok(fs.existsSync(thumbFile)); // too large const big = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), Buffer.concat([PNG, Buffer.alloc(70 * 1024)])); assert.strictEqual(big.status, 413); // locked buckets take no more thumbnails assert.strictEqual((await request('PATCH', '/files/thumbs1?lock=yes', { ...TUS, ...bearer() })).status, 204); const locked = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), PNG); assert.strictEqual(locked.status, 400); // deleting the file deletes the thumbnail const del = await request('DELETE', `/admin/buckets/thumbs1/files/${ key }`, ADMIN); assert.strictEqual(del.status, 200); assert.ok(!fs.existsSync(thumbFile)); assert.strictEqual((await request('GET', item.thumbUrl)).status, 404); }); test('one-time files show no thumbnail', async () => { const { fid } = await upload({ sid: 'thumbs2', name: 'pic.png', retention: 'one-time' }); const put = await request('PUT', `/files/${ encodeURIComponent(fid) }.thumb`, bearer(), PNG); assert.strictEqual(put.status, 204); const list = await request('GET', '/thumbs2.json'); assert.strictEqual(list.json.items[0].thumbUrl, null); assert.strictEqual((await request('GET', `/files/${ fid }.thumb`)).status, 404); assert.strictEqual((await request('GET', `/files/${ fid }`)).status, 200); }); // ----------------------------------------------------------------- admin test('admin area needs the admin password', async () => { assert.strictEqual((await request('GET', '/admin/data.json')).status, 401); assert.strictEqual((await request('GET', '/admin/data.json', { 'x-passwd': 'up-secret' })).status, 403); assert.strictEqual((await request('GET', '/admin/data.json', bearer())).status, 403, 'a login token is no admin'); const ok = await request('GET', '/admin/data.json', ADMIN); assert.strictEqual(ok.status, 200); assert.ok(Array.isArray(ok.json.buckets)); assert.strictEqual((await request('GET', '/admin')).status, 200); }); test('admin sees everything and removes single files', async () => { const a = await upload({ sid: 'admin1', password: 'pw', name: 'a.txt', content: 'AAA' }); const b = await upload({ sid: 'admin1', password: 'pw', name: 'b.txt', content: 'BBB' }); const data = await request('GET', '/admin/data.json', ADMIN); const bucket = data.json.buckets.find(x => x.sid === 'admin1'); assert.strictEqual(bucket.files.length, 2); assert.strictEqual(bucket.files[0].hasPassword, true); assert.ok(bucket.files[0].url, 'urls of password buckets are not hidden'); assert.strictEqual(bucket.files[0].password, undefined); assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ a.key }`)).status, 401); assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/not-a-uuid`, ADMIN)).status, 400); assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ a.key }`, ADMIN)).status, 200); assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ a.key }`, ADMIN)).status, 404); // recipients see the remaining file only const list = await request('GET', '/admin1.json', { 'x-download-pass': 'pw' }); assert.deepStrictEqual(list.json.items.map(i => i.key), [b.key]); assert.strictEqual((await request('GET', `/files/${ a.fid }`)).status, 404); // the last file takes the bucket with it assert.strictEqual((await request('DELETE', `/admin/buckets/admin1/files/${ b.key }`, ADMIN)).status, 200); assert.strictEqual((await request('GET', '/admin1.json')).status, 404); }); test('admin removes a whole bucket', async () => { await upload({ sid: 'admin2', password: 'pw', name: 'a.txt' }); await upload({ sid: 'admin2', password: 'pw', name: 'b.txt' }); const res = await request('DELETE', '/admin/buckets/admin2', ADMIN); assert.strictEqual(res.status, 200); assert.strictEqual(res.json.removed, 2); assert.strictEqual((await request('GET', '/admin2.json')).status, 404); assert.strictEqual((await request('DELETE', '/admin/buckets/admin2', ADMIN)).status, 404); assert.strictEqual(fs.existsSync(path.join(tmpDir, 'admin2')), false); }); test('admin download uses a short lived link and keeps one-time files', async () => { const { key } = await upload({ sid: 'admin3', name: 'once.txt', retention: 'one-time', content: 'only once' }); const noAuth = await request('POST', '/admin/download-link', ...jsonBody({ sid: 'admin3', key })); assert.strictEqual(noAuth.status, 401); const link = await request('POST', '/admin/download-link', { ...ADMIN, ...jsonBody({ sid: 'admin3', key })[0] }, jsonBody({ sid: 'admin3', key })[1]); assert.strictEqual(link.status, 200); const res = await request('GET', link.json.url); assert.strictEqual(res.status, 200); assert.strictEqual(res.text, 'only once'); assert.match(res.headers['content-disposition'], /^attachment; filename="once.txt"/); await tick(); // twice is fine and the recipient can still get the file once assert.strictEqual((await request('GET', link.json.url)).status, 200); assert.strictEqual((await request('GET', `/files/admin3++${ key }`)).status, 200); await tick(); assert.strictEqual((await request('GET', `/files/admin3++${ key }`)).status, 404); assert.strictEqual((await request('GET', '/admin/dl?t=garbage')).status, 403); // a login token can not be used as download link assert.strictEqual((await request('GET', `/admin/dl?t=${ encodeURIComponent(token) }`)).status, 403); }); test('admin archive download of a bucket', async () => { await upload({ sid: 'admin4', password: 'pw', name: 'a.txt', content: 'AAA' }); const link = await request('POST', '/admin/download-link', { ...ADMIN, 'Content-Type': 'application/json' }, JSON.stringify({ sid: 'admin4', format: 'tar.gz' })); assert.strictEqual(link.status, 200); const res = await request('GET', link.json.url); assert.strictEqual(res.status, 200); assert.strictEqual(res.body[0], 0x1f); assert.strictEqual(res.body[1], 0x8b); }); // -------------------------------------------------------------- settings test('admin settings change the brand for everybody', async () => { const before = await request('GET', '/brand.json'); assert.strictEqual(before.json.name, 'Dodo Transfer'); assert.strictEqual((await request('PUT', '/admin/settings', { 'Content-Type': 'application/json' }, '{"name":"x"}')).status, 401); const bad = await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' }, '{"name":""}'); assert.strictEqual(bad.status, 400); const ok = await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' }, JSON.stringify({ name: 'Local Transfer', owner: 'Dev', from: 2025 })); assert.strictEqual(ok.status, 200); const brand = await request('GET', '/brand.json'); assert.strictEqual(brand.json.name, 'Local Transfer'); assert.match(brand.json.footer, /^© 2025(–\d{4})? Dev$/); // the shell page carries the new name const page = await request('GET', '/'); assert.match(page.text, /Local Transfer<\/title>/); assert.ok(!page.text.includes('Dodo Transfer')); // names are escaped await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' }, JSON.stringify({ name: '</script><b>x' })); const evil = await request('GET', '/'); assert.ok(!evil.text.includes('</script><b>')); await request('PUT', '/admin/settings', { ...ADMIN, 'Content-Type': 'application/json' }, JSON.stringify({ name: 'Dodo Transfer', owner: '아진산업(주) 전장선행개발팀', from: 2026 })); }); test('admin logo upload, serving and reset', async () => { const defaultLogo = await request('GET', '/assets/brand-logo'); assert.strictEqual(defaultLogo.status, 200); assert.strictEqual(defaultLogo.headers['content-type'], 'image/svg+xml'); const text = await request('POST', '/admin/settings/logo', ADMIN, Buffer.from('not an image')); assert.strictEqual(text.status, 400); assert.strictEqual((await request('POST', '/admin/settings/logo', {}, PNG)).status, 401); const big = await request('POST', '/admin/settings/logo', ADMIN, Buffer.concat([PNG, Buffer.alloc(1024 * 1024)])); assert.strictEqual(big.status, 413); const svg = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect width="1" height="1"/></svg>'); const ok = await request('POST', '/admin/settings/logo', ADMIN, svg); assert.strictEqual(ok.status, 200); assert.strictEqual(ok.json.brand.customLogo, true); const served = await request('GET', ok.json.brand.logoUrl); assert.strictEqual(served.headers['content-type'], 'image/svg+xml'); assert.match(served.headers['content-security-policy'], /sandbox/); assert.ok(served.body.equals(svg)); const reset = await request('DELETE', '/admin/settings/logo', ADMIN); assert.strictEqual(reset.json.brand.customLogo, false); assert.strictEqual((await request('GET', '/assets/brand-logo')).headers['content-type'], 'image/svg+xml'); }); // ---------------------------------------------------------------- misc test('expiry is the earlier of retention and maxAge', async () => { const db = require('../../lib/db'); const config = require('../../config'); const probe = new db(tmpDir, null); const created = Date.now(); const file = (retention) => ({ metadata: { createdAt: String(created), retention } }); assert.strictEqual(probe.expiresAt(file('3600')), created + 3600_000); assert.strictEqual(probe.expiresAt(file('one-time')), created + config.maxAge * 1000); assert.strictEqual(probe.expiresAt(file(String(config.maxAge * 2))), created + config.maxAge * 1000); }); test('the old misspelled lock flag is still honored', async () => { const { fid } = await upload({ sid: 'oldlock', password: 'pw' }); const jsonFile = path.join(tmpDir, ...fid.split('++')) + '.json'; const info = JSON.parse(fs.readFileSync(jsonFile, 'utf8')); info.metadata.buckedLocked = true; fs.writeFileSync(jsonFile, JSON.stringify(info)); const db = require('../../lib/db'); const fresh = new db(tmpDir, new (require('../../lib/store'))(tmpDir)); fresh.init(); assert.strictEqual(fresh.isLocked('oldlock'), true); }); test('hidden folders in the data dir are no buckets', async () => { fs.mkdirSync(path.join(tmpDir, '.settings'), { recursive: true }); fs.writeFileSync(path.join(tmpDir, '.settings', 'settings.json'), '{}'); const db = require('../../lib/db'); const fresh = new db(tmpDir, new (require('../../lib/store'))(tmpDir)); fresh.init(); assert.strictEqual(fresh.get('.settings'), undefined); }); test('health check answers without a login', async () => { const res = await request('GET', '/healthz'); assert.strictEqual(res.status, 200); assert.strictEqual(res.text, 'ok'); }); test('error pages are branded and escaped', async () => { const res = await request('GET', '/does-not-exist'); assert.strictEqual(res.status, 404); assert.match(res.text, /<title>Dodo Transfer<\/title>/); });