423 lines
13 KiB
JavaScript
423 lines
13 KiB
JavaScript
import test from 'brittle'
|
|
import b4a from 'b4a'
|
|
import http from 'node:http'
|
|
import Hyperdrive from 'hyperdrive'
|
|
import Corestore from 'corestore'
|
|
import { mkdirSync, rmSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { createVfs } from './lib/vfs.js'
|
|
import { createBareOsIpc } from './lib/bare-os-ipc.js'
|
|
import {
|
|
bareOsWwwInitdEnabled,
|
|
bareOsWwwResolveStaticPath,
|
|
BARE_OS_WWW_DEFAULT_INDEX,
|
|
ensureBareOsWwwHomeDefaults,
|
|
maybeRegisterBareOsWwwInitd,
|
|
maybeRestartBareOsWwwAfterIdentity
|
|
} from './lib/bare-os-www-initd.js'
|
|
import { findBareServiceDefinition } from './lib/bare-initd.js'
|
|
import {
|
|
bareHolesailManagedPersistentSeedLooksValid,
|
|
bareHolesailManagedReadState,
|
|
bareHolesailManagedWriteState
|
|
} from './lib/bare-holesail-managed.js'
|
|
import {
|
|
bareOsWwwHolesailConnectionId,
|
|
ensureBareOsWwwHolesailTunnel
|
|
} from './lib/bare-os-www-holesail.js'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
function testCorestoreDir(name) {
|
|
const dir = path.join(
|
|
__dirname,
|
|
'.test-data',
|
|
name + '-' + process.pid + '-' + Math.random().toString(36).slice(2)
|
|
)
|
|
mkdirSync(path.dirname(dir), { recursive: true })
|
|
return dir
|
|
}
|
|
|
|
function testCtx(drive, personal, env = {}) {
|
|
const shellEnv = {
|
|
HOME: '/home/user',
|
|
PATH: '/bin',
|
|
USER: 'user',
|
|
UID: '1000',
|
|
GID: '1000',
|
|
PWD: '/home/user',
|
|
BARE_OS_EXIT_STATUS: '0',
|
|
BARE_OS_SSH_LISTEN_PORT: '0',
|
|
...env
|
|
}
|
|
const bareOsIpc = createBareOsIpc()
|
|
const vfs = createVfs(drive, personal, shellEnv, null, { bareOsIpc })
|
|
return {
|
|
drive,
|
|
personalDrive: personal,
|
|
vfs,
|
|
bareOsIpc,
|
|
env: shellEnv,
|
|
console,
|
|
b4a
|
|
}
|
|
}
|
|
|
|
test('bareOsWwwInitdEnabled respects BARE_OS_WWW_INITD', (t) => {
|
|
t.ok(bareOsWwwInitdEnabled({}))
|
|
t.absent(bareOsWwwInitdEnabled({ BARE_OS_WWW_INITD: '0' }))
|
|
t.absent(bareOsWwwInitdEnabled({ BARE_OS_WWW_INITD: 'false' }))
|
|
})
|
|
|
|
test('bareOsWwwResolveStaticPath', (t) => {
|
|
const d = '/home/user/.www'
|
|
t.is(bareOsWwwResolveStaticPath(d, '/'), `${d}/index.html`)
|
|
t.is(bareOsWwwResolveStaticPath(d, '/index.html'), `${d}/index.html`)
|
|
t.is(bareOsWwwResolveStaticPath(d, '/a/b.css'), `${d}/a/b.css`)
|
|
t.is(bareOsWwwResolveStaticPath(d, '/docs/'), `${d}/docs/index.html`)
|
|
t.absent(bareOsWwwResolveStaticPath(d, '/../x'))
|
|
})
|
|
|
|
test('vfs routes HOME/.www to HDMS www mount when present', async (t) => {
|
|
const dir = testCorestoreDir('vfswww')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pvwww'))
|
|
const wwwDrive = new Hyperdrive(store.namespace('wwwalias'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
await wwwDrive.ready()
|
|
await wwwDrive.put('/hello.txt', b4a.from('from-www-drive'))
|
|
const shellEnv = {
|
|
HOME: '/home/user',
|
|
USER: 'user',
|
|
PATH: '/bin',
|
|
PWD: '/home/user'
|
|
}
|
|
const mntRef = {
|
|
getMounts: () =>
|
|
new Map([['www', { drive: wwwDrive, writable: true }]])
|
|
}
|
|
const vfs = createVfs(sys, personal, shellEnv, mntRef, {
|
|
bareOsIpc: createBareOsIpc()
|
|
})
|
|
const r = vfs.route('/home/user/.www/hello.txt')
|
|
t.is(r.drive, wwwDrive)
|
|
t.is(r.path, '/hello.txt')
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('bare-os-www serves default index on ephemeral port', async (t) => {
|
|
const dir = testCorestoreDir('wwwsrv')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pwww'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
const port = 19000 + Math.floor(Math.random() * 1000)
|
|
const ctx = testCtx(sys, personal, {
|
|
BARE_OS_WWW_PORT: String(port),
|
|
BARE_OS_WWW_HOLESAIL: '0'
|
|
})
|
|
maybeRegisterBareOsWwwInitd(ctx.env)
|
|
const svc = findBareServiceDefinition('bare-os-www')
|
|
t.ok(svc && typeof svc.start === 'function')
|
|
await /** @type {{ start: (ctx: unknown) => Promise<void> }} */ (svc).start(ctx)
|
|
const body = await new Promise((resolve, reject) => {
|
|
http.get(`http://127.0.0.1:${port}/`, (res) => {
|
|
t.is(res.statusCode, 200)
|
|
const chunks = []
|
|
res.on('data', (c) => chunks.push(c))
|
|
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
res.on('error', reject)
|
|
}).on('error', reject)
|
|
})
|
|
t.ok(String(body).includes('Bare OS'))
|
|
t.ok(String(body).includes('~/.www'))
|
|
const headLen = await new Promise((resolve, reject) => {
|
|
const r = http.request(
|
|
{ hostname: '127.0.0.1', port, path: '/', method: 'HEAD' },
|
|
(res) => {
|
|
t.is(res.statusCode, 200)
|
|
const cl = res.headers['content-length']
|
|
t.ok(cl && Number(cl) > 100, 'HEAD Content-Length reflects body size')
|
|
res.resume()
|
|
resolve(Number(cl))
|
|
}
|
|
)
|
|
r.on('error', reject)
|
|
r.end()
|
|
})
|
|
t.ok(headLen > 100)
|
|
await /** @type {{ stop: (ctx: unknown) => Promise<void> }} */ (svc).stop(ctx)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('bare-os-www redirects /dir → /dir/ so relative asset URLs resolve', async (t) => {
|
|
const dir = testCorestoreDir('wwwsubdir')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('psub'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
const port = 18000 + Math.floor(Math.random() * 500)
|
|
const ctx = testCtx(sys, personal, {
|
|
BARE_OS_WWW_PORT: String(port),
|
|
BARE_OS_WWW_HOLESAIL: '0'
|
|
})
|
|
await ctx.vfs.mkdir(ctx.vfs.resolveLogical('~/.www/test'), { recursive: true })
|
|
await ctx.vfs.writeFile(
|
|
ctx.vfs.resolveLogical('~/.www/test/index.html'),
|
|
b4a.from('SUBDIR_INDEX_BODY', 'utf8')
|
|
)
|
|
maybeRegisterBareOsWwwInitd(ctx.env)
|
|
const svc = findBareServiceDefinition('bare-os-www')
|
|
t.ok(svc && typeof svc.start === 'function')
|
|
await /** @type {{ start: (ctx: unknown) => Promise<void> }} */ (svc).start(ctx)
|
|
|
|
await new Promise((resolve, reject) => {
|
|
http.get(`http://127.0.0.1:${port}/test`, (res) => {
|
|
t.is(res.statusCode, 308)
|
|
t.is(res.headers.location, '/test/')
|
|
res.resume()
|
|
res.on('end', resolve)
|
|
res.on('error', reject)
|
|
}).on('error', reject)
|
|
})
|
|
|
|
await new Promise((resolve, reject) => {
|
|
http.get(`http://127.0.0.1:${port}/test?q=1`, (res) => {
|
|
t.is(res.statusCode, 308)
|
|
t.is(res.headers.location, '/test/?q=1')
|
|
res.resume()
|
|
res.on('end', resolve)
|
|
res.on('error', reject)
|
|
}).on('error', reject)
|
|
})
|
|
|
|
const bodySlash = await new Promise((resolve, reject) => {
|
|
http.get(`http://127.0.0.1:${port}/test/`, (res) => {
|
|
t.is(res.statusCode, 200)
|
|
const chunks = []
|
|
res.on('data', (c) => chunks.push(c))
|
|
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
res.on('error', reject)
|
|
}).on('error', reject)
|
|
})
|
|
t.is(String(bodySlash), 'SUBDIR_INDEX_BODY')
|
|
|
|
await /** @type {{ stop: (ctx: unknown) => Promise<void> }} */ (svc).stop(ctx)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('ensureBareOsWwwHolesailTunnel persists managed server entry', async (t) => {
|
|
const dir = testCorestoreDir('wwwhs')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pwwhs'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(sys, personal, {
|
|
USER: 'guest',
|
|
HOME: '/home/guest',
|
|
UID: '65534',
|
|
GID: '65534',
|
|
PWD: '/home/guest'
|
|
})
|
|
const p = 18188
|
|
const id = bareOsWwwHolesailConnectionId(p)
|
|
const empty = { version: 1, connections: {} }
|
|
await bareHolesailManagedWriteState(
|
|
ctx,
|
|
/** @type {Record<string, string | undefined>} */ (ctx.env),
|
|
empty
|
|
)
|
|
await ensureBareOsWwwHolesailTunnel(ctx, ctx.env, p)
|
|
const { state } = await bareHolesailManagedReadState(
|
|
ctx,
|
|
/** @type {Record<string, string | undefined>} */ (ctx.env)
|
|
)
|
|
const row = state.connections[id]
|
|
t.ok(row && /** @type {{ server?: boolean }} */ (row).server === true)
|
|
t.is(/** @type {{ port?: number }} */ (row).port, p)
|
|
t.is(/** @type {{ host?: string }} */ (row).host, '127.0.0.1')
|
|
const seedStr = /** @type {{ seed?: string }} */ (row).seed
|
|
t.ok(seedStr && bareHolesailManagedPersistentSeedLooksValid(seedStr))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('ensureBareOsWwwHolesailTunnel preserves existing hs key when normalizing row', async (t) => {
|
|
const dir = testCorestoreDir('wwwhskey')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pwwhsk'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(sys, personal, {
|
|
USER: 'guest',
|
|
HOME: '/home/guest',
|
|
UID: '65534',
|
|
GID: '65534',
|
|
PWD: '/home/guest'
|
|
})
|
|
const p = 18189
|
|
const id = bareOsWwwHolesailConnectionId(p)
|
|
const keepKey = 'hs://0000keepthiskey000000000000000000000000000000000000000000'
|
|
await bareHolesailManagedWriteState(
|
|
ctx,
|
|
/** @type {Record<string, string | undefined>} */ (ctx.env),
|
|
{
|
|
version: 1,
|
|
connections: {
|
|
[id]: {
|
|
server: true,
|
|
port: 19999,
|
|
host: '0.0.0.0',
|
|
enabled: true,
|
|
key: keepKey
|
|
}
|
|
}
|
|
}
|
|
)
|
|
await ensureBareOsWwwHolesailTunnel(ctx, ctx.env, p)
|
|
const { state } = await bareHolesailManagedReadState(
|
|
ctx,
|
|
/** @type {Record<string, string | undefined>} */ (ctx.env)
|
|
)
|
|
const row = /** @type {{ port?: number, host?: string, key?: string }} */ (
|
|
state.connections[id]
|
|
)
|
|
t.is(row.port, p)
|
|
t.is(row.host, '127.0.0.1')
|
|
t.is(row.key, keepKey)
|
|
const migratedSeed = /** @type {{ seed?: string }} */ (row).seed
|
|
t.ok(migratedSeed && bareHolesailManagedPersistentSeedLooksValid(migratedSeed))
|
|
t.is(
|
|
migratedSeed,
|
|
'keepthiskey000000000000000000000000000000000000000000'.toLowerCase()
|
|
)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('BARE_OS_WWW_DEFAULT_INDEX is non-trivial HTML', (t) => {
|
|
t.ok(BARE_OS_WWW_DEFAULT_INDEX.includes('<!DOCTYPE html>'))
|
|
t.ok(BARE_OS_WWW_DEFAULT_INDEX.includes('Bare OS'))
|
|
})
|
|
|
|
test('ensureBareOsWwwHomeDefaults creates ~/.www/index.html', async (t) => {
|
|
const dir = testCorestoreDir('wwwdefaults')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('pdef'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
const ctx = testCtx(sys, personal)
|
|
await ensureBareOsWwwHomeDefaults(ctx)
|
|
const idx = ctx.vfs.resolveLogical('~/.www/index.html')
|
|
const buf = await ctx.vfs.readFile(idx)
|
|
t.ok(buf && buf.byteLength > 50)
|
|
t.ok(b4a.toString(buf, 'utf8').includes('Bare OS'))
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|
|
|
|
test('maybeRestartBareOsWwwAfterIdentity rebinds HTTP server to new HOME', async (t) => {
|
|
const dir = testCorestoreDir('wwwrebind')
|
|
const store = new Corestore(dir)
|
|
const sys = new Hyperdrive(store)
|
|
const personal = new Hyperdrive(store.namespace('prebind'))
|
|
await sys.ready()
|
|
await personal.ready()
|
|
const port = 19500 + Math.floor(Math.random() * 800)
|
|
const shellEnv = {
|
|
HOME: '/home/alice',
|
|
PATH: '/bin',
|
|
USER: 'alice',
|
|
UID: '1000',
|
|
GID: '1000',
|
|
PWD: '/home/alice',
|
|
BARE_OS_EXIT_STATUS: '0',
|
|
BARE_OS_SSH_LISTEN_PORT: '0',
|
|
BARE_OS_WWW_PORT: String(port),
|
|
BARE_OS_WWW_HOLESAIL: '0'
|
|
}
|
|
const bareOsIpc = createBareOsIpc()
|
|
const vfs = createVfs(sys, personal, shellEnv, null, { bareOsIpc })
|
|
const ctx = {
|
|
drive: sys,
|
|
personalDrive: personal,
|
|
vfs,
|
|
bareOsIpc,
|
|
env: shellEnv,
|
|
console,
|
|
b4a
|
|
}
|
|
await vfs.mkdir(vfs.resolveLogical('~/.www'), { recursive: true })
|
|
await vfs.writeFile(
|
|
vfs.resolveLogical('~/.www/index.html'),
|
|
b4a.from('ALICE', 'utf8')
|
|
)
|
|
shellEnv.HOME = '/home/bob'
|
|
shellEnv.USER = 'bob'
|
|
shellEnv.PWD = '/home/bob'
|
|
await vfs.mkdir(vfs.resolveLogical('~/.www'), { recursive: true })
|
|
await vfs.writeFile(
|
|
vfs.resolveLogical('~/.www/index.html'),
|
|
b4a.from('BOB', 'utf8')
|
|
)
|
|
shellEnv.HOME = '/home/alice'
|
|
shellEnv.USER = 'alice'
|
|
shellEnv.PWD = '/home/alice'
|
|
|
|
maybeRegisterBareOsWwwInitd(ctx.env)
|
|
const svc = findBareServiceDefinition('bare-os-www')
|
|
t.ok(svc && typeof svc.start === 'function')
|
|
await /** @type {{ start: (c: unknown) => Promise<void> }} */ (svc).start(ctx)
|
|
|
|
const fetchText = () =>
|
|
new Promise((resolve, reject) => {
|
|
http.get(`http://127.0.0.1:${port}/`, (res) => {
|
|
const chunks = []
|
|
res.on('data', (c) => chunks.push(c))
|
|
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
res.on('error', reject)
|
|
}).on('error', reject)
|
|
})
|
|
|
|
const fetchTextRetry = async (want, ms = 3000) => {
|
|
const deadline = Date.now() + ms
|
|
let last = /** @type {unknown} */ (null)
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const txt = await fetchText()
|
|
if (txt === want) return txt
|
|
last = txt
|
|
} catch {
|
|
/* port may still be rebound after restart */
|
|
}
|
|
await new Promise((r) => setTimeout(r, 25))
|
|
}
|
|
throw new Error(
|
|
`expected body ${JSON.stringify(want)}, last ${JSON.stringify(last)}`
|
|
)
|
|
}
|
|
|
|
t.is(await fetchText(), 'ALICE')
|
|
|
|
shellEnv.HOME = '/home/bob'
|
|
shellEnv.USER = 'bob'
|
|
shellEnv.PWD = '/home/bob'
|
|
await maybeRestartBareOsWwwAfterIdentity(ctx)
|
|
t.is(await fetchTextRetry('BOB'), 'BOB')
|
|
|
|
await /** @type {{ stop: (c: unknown) => Promise<void> }} */ (svc).stop(ctx)
|
|
await store.close()
|
|
rmSync(dir, { recursive: true, force: true })
|
|
})
|