Pithos: open the app in a browser, also from the pithos.sia website
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.
2026-10-05 01:47:12 +02:00
|
|
|
// "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');
|
2026-10-05 02:04:06 +02:00
|
|
|
assert.equal(handoffRoute('/app'), '#/overview');
|
|
|
|
|
assert.equal(handoffRoute('/app/settings'), '#/settings');
|
Pithos: open the app in a browser, also from the pithos.sia website
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.
2026-10-05 01:47:12 +02:00
|
|
|
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$/);
|
|
|
|
|
});
|