Pithos runs on the user's computer, so the pithos.sia website could only describe it. Two ways in now: - Open in browser (Theseus, desktop) opens the current page in the default browser with a one-time sign-in link (60 s, single use), unlike the host's reusable ?t= link. - The website links to http://127.0.0.1:47621/open?path=/<user>/<drive>. Any page or local program could follow that link, so the host first asks the user in its own window (a native dialog on the desktop, Theseus's approval prompt with the vault unlocked). Only loopback Host names are accepted, one question at a time, and the website learns nothing either way. Sessions started this way are marked external, so the browser does not get host-only controls such as Back to sidebar.
80 lines
3.8 KiB
JavaScript
80 lines
3.8 KiB
JavaScript
// "Open my Pithos" from the pithos.sia website: nobody is signed in until the
|
|
// host says yes, and the sign-in link it hands out works once.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import http from 'node:http';
|
|
import { createPithos, handoffRoute } from '../core/server.js';
|
|
|
|
const get = (url, headers = {}) => new Promise((resolve, reject) => {
|
|
http.get(url, { headers }, (res) => { let body = ''; res.on('data', (d) => { body += d; }); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body })); }).on('error', reject);
|
|
});
|
|
|
|
test('website paths become app routes, and odd ones go to Overview', () => {
|
|
assert.equal(handoffRoute('/'), '#/overview');
|
|
assert.equal(handoffRoute('/backup-agent/nightly-backups/2026'), '#/u/backup-agent/nightly-backups/2026');
|
|
assert.equal(handoffRoute('/settings'), '#/settings');
|
|
assert.equal(handoffRoute('/drives'), '#/buckets');
|
|
assert.equal(handoffRoute('/a/<script>'), '#/overview');
|
|
assert.equal(handoffRoute('/a/b"onload'), '#/overview');
|
|
});
|
|
|
|
test('the handoff asks first, signs in once, and marks the browser as external', async (t) => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pithos-handoff-'));
|
|
fs.writeFileSync(path.join(dir, 's3d.yml'), `apiAddress: 127.0.0.1:18400\nadminAddress: 127.0.0.1:18401\ndirectory: ${dir.replace(/\\/g, '/')}/data\n`);
|
|
const port = 47700 + Math.floor(Math.random() * 200);
|
|
let answer = false;
|
|
const asked = [];
|
|
const opened = [];
|
|
const p = await createPithos({
|
|
configFile: path.join(dir, 's3d.yml'), binary: path.join(dir, 'no-s3d'), hostName: 'desktop', handoffPort: port,
|
|
confirmBrowserOpen: async (q) => { asked.push(q.path); return answer; },
|
|
openBrowser: (url) => opened.push(url),
|
|
});
|
|
t.after(async () => { await p.close(); fs.rmSync(dir, { recursive: true, force: true }); });
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
|
|
// a hostname that merely resolves to 127.0.0.1 (DNS rebinding) is refused before asking
|
|
const rebind = await get(`http://127.0.0.1:${port}/open?path=/`, { host: `evil.example:${port}` });
|
|
assert.equal(rebind.status, 403);
|
|
assert.equal(asked.length, 0);
|
|
|
|
// declined: no session, nothing to follow
|
|
const no = await get(`http://127.0.0.1:${port}/open?path=/u1/d1`);
|
|
assert.equal(no.status, 403);
|
|
assert.deepEqual(asked, ['/u1/d1']);
|
|
|
|
// allowed: a one-time link at the right page
|
|
answer = true;
|
|
const yes = await get(`http://127.0.0.1:${port}/open?path=/u1/d1`);
|
|
assert.equal(yes.status, 302);
|
|
const link = new URL(yes.headers.location);
|
|
assert.equal(link.origin, new URL(p.url).origin);
|
|
assert.equal(link.hash, '#/u/u1/d1');
|
|
assert.ok(link.searchParams.get('s'));
|
|
|
|
const first = await get(link.href.replace(link.hash, ''));
|
|
assert.equal(first.status, 302);
|
|
const cookie = String(first.headers['set-cookie'] || '').split(';')[0];
|
|
assert.match(cookie, /^pithos=/);
|
|
const session = JSON.parse((await get(`${p.url}api/session`, { cookie })).body);
|
|
assert.equal(session.authenticated, true);
|
|
assert.equal(session.external, true);
|
|
|
|
// the same link again signs nobody in
|
|
const again = await get(link.href.replace(link.hash, ''));
|
|
assert.equal(again.headers['set-cookie'], undefined);
|
|
|
|
// "Open in browser" from the app hands the browser the same kind of link
|
|
const own = await p.internalSession();
|
|
const res = await new Promise((resolve, reject) => {
|
|
const req = http.request(`${p.url}api/open-in-browser`, { method: 'POST', headers: { cookie: own, 'x-pithos': '1', 'content-type': 'application/json' } }, (r) => { r.resume(); r.on('end', () => resolve(r.statusCode)); });
|
|
req.on('error', reject);
|
|
req.end(JSON.stringify({ route: '#/u/u1/d1' }));
|
|
});
|
|
assert.equal(res, 200);
|
|
assert.equal(opened.length, 1);
|
|
assert.match(opened[0], /\?s=[\w-]+#\/u\/u1\/d1$/);
|
|
});
|