// Backup restore and personal export: a good backup is swapped in (the old data kept aside), unsafe archives are refused, and an export holds only your own data without sign-in tokens. const assert = require('assert'), fs = require('fs'), os = require('os'), path = require('path'), http = require('http'), { execFileSync } = require('child_process'); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-backup-')); process.env.DATA_DIR = dir; process.env.NO_RESTART_AFTER_RESTORE = '1'; const users = require('../users'), backup = require('../backup'); const a = users.create({ name: 'Ann', role: 'admin', handle: 'YQ' }), b = users.create({ name: 'Bo', role: 'user', handle: 'ZP' }); fs.writeFileSync(path.join(users.dirOf(a.id), 'tasks.json'), '{"mine":1}'); fs.writeFileSync(path.join(users.dirOf(a.id), 'mail.json'), '{"token":"secret"}'); fs.writeFileSync(path.join(users.dirOf(b.id), 'tasks.json'), '{"bo":1}'); const hub = http.createServer((req, res) => users.runAs(a, () => backup.handle(req, res, 'Ann'))).listen(0, async () => { const base = 'http://127.0.0.1:' + hub.address().port; const mk = (name, fn) => { const w = fs.mkdtempSync(path.join(os.tmpdir(), 'nehub-bk-src-')); fn(w); const f = path.join(os.tmpdir(), name); execFileSync('tar', ['-czf', f, '-C', w, '.']); return fs.readFileSync(f); }; try { // export: only my data, no tokens const ex = Buffer.from(await (await fetch(base + '/api/export')).arrayBuffer()), ef = path.join(os.tmpdir(), 'nehub-ex.tar.gz'); fs.writeFileSync(ef, ex); const list = execFileSync('tar', ['-tzf', ef]).toString(); assert.match(list, /me\/tasks\.json/); assert.ok(!/mail\.json/.test(list), 'sign-in tokens are left out'); assert.ok(!list.includes(b.id), 'nobody else\'s data'); // a good backup is swapped in; the old data is kept const good = mk('good.tar.gz', w => { fs.writeFileSync(path.join(w, 'secret.json'), '{"restored":true}'); fs.mkdirSync(path.join(w, 'users')); }); let r = await fetch(base + '/api/backup/restore', { method: 'POST', body: good }); let j = await r.json(); assert.equal(r.status, 200, JSON.stringify(j)); assert.ok(j.restarting); assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'secret.json'), 'utf8')).restored, true); assert.ok(fs.existsSync(path.join(dir, j.kept, 'users')), 'the old accounts are kept aside'); assert.ok(!fs.existsSync(path.join(dir, '.restore')) && !fs.existsSync(path.join(dir, '.restore.tar.gz')), 'the staging files are cleaned up'); // unsafe or foreign archives are refused and change nothing const evil = mk('evil.tar.gz', w => { fs.writeFileSync(path.join(w, 'secret.json'), '{"evil":true}'); fs.symlinkSync('/etc/passwd', path.join(w, 'users')); }); r = await fetch(base + '/api/backup/restore', { method: 'POST', body: evil }); assert.equal(r.status, 400); assert.match((await r.json()).error, /links/); assert.equal(JSON.parse(fs.readFileSync(path.join(dir, 'secret.json'), 'utf8')).restored, true, 'nothing changed'); const foreign = mk('foreign.tar.gz', w => fs.writeFileSync(path.join(w, 'readme.txt'), 'hi')); r = await fetch(base + '/api/backup/restore', { method: 'POST', body: foreign }); assert.equal(r.status, 400); assert.match((await r.json()).error, /NE Hub backup/); r = await fetch(base + '/api/backup/restore', { method: 'POST', body: Buffer.from('not a tar') }); assert.equal(r.status, 400); console.log('backup test ok'); } catch (e) { console.error(e); process.exitCode = 1; } hub.close(); });