Automerge backed Markdown editor for the Atmosphere
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188import { test } from 'node:test'import assert from 'node:assert/strict'import { generateKeyPairSync } from 'node:crypto'import { mkdtemp, mkdir, readFile, rm, writeFile, symlink } from 'node:fs/promises'import { tmpdir } from 'node:os'import { join } from 'node:path'import { spawn } from 'node:child_process'import { once } from 'node:events'import http from 'node:http'import { accountLoginScope } from '../src/auth/integration-oauth-scopes.mjs'import { retainAssets } from '../src/server/retained-assets.mjs'import { drainRequests } from '../src/server/request-drain.mjs'
const scope = accountLoginScope('atproto repo:ink.reams.vaultAuthority blob:application/vnd.sealight.automerge.amch')
test('portable HTTP backend preserves HTTPS identity, rejects spoofing, retains old assets and restarts its broker', async () => { const dir = await mkdtemp(join(tmpdir(), 'reams-container-server-')) const root = join(dir, 'dist'), archive = join(dir, 'archive') await mkdir(join(root, 'assets'), { recursive: true }) await mkdir(join(root, 'fonts')) await writeFile(join(root, 'index.html'), 'release A') await writeFile(join(root, 'service-worker.js'), '// A') await writeFile(join(root, 'assets', 'old-deadbeef.wasm'), Buffer.from([0, 97, 115, 109])) await writeFile(join(root, 'fonts', 'font.woff2'), 'font bytes') await writeFile(join(root, 'oauth-login-scope.json'), JSON.stringify({ scope })) const identity = { version: 1, clientId: 'https://reams.example.test/oauth-client-metadata.json', redirectUri: 'https://reams.example.test/oauth-callback.html', scope } await writeFile(join(root, 'deployment-config.json'), JSON.stringify(identity)) const keyPath = join(dir, 'key.pem') await writeFile(keyPath, generateKeyPairSync('ec', { namedCurve: 'P-256' }).privateKey.export({ type: 'pkcs8', format: 'pem' })) const port = await freePort() const env = { ...process.env, HOST: '127.0.0.1', PORT: String(port), ATPROTO_DIST_DIR: root, REAMS_BEHIND_HTTPS_PROXY: '1', REAMS_REQUIRE_BUILD_CONFIG: '1', REAMS_ASSET_ARCHIVE_DIR: archive, OAUTH_CLIENT_ID: identity.clientId, OAUTH_REDIRECT_URI: identity.redirectUri, OAUTH_SCOPE: scope, OAUTH_SIGNING_KEY: keyPath, OAUTH_KID: 'test-key', OAUTH_STORE_PATH: join(dir, 'broker.json') } delete env.HTTPS_KEY; delete env.HTTPS_CERT let child try { child = start(env) await ready(child, port) assert.equal((await request(port, '/healthz', 'internal', 'HEAD')).status, 200) assert.equal((await request(port, '/healthz', 'internal', 'POST')).status, 405) const metadata = await request(port, '/oauth-client-metadata.json') assert.equal(metadata.status, 200) assert.equal(JSON.parse(metadata.body).client_id, identity.clientId) const session = await request(port, '/oauth/broker/session') assert.equal(session.status, 200) assert.match(session.headers['set-cookie'][0], /Secure; HttpOnly; SameSite=Strict/) const csrf = JSON.parse(session.body).csrf_token const cookie = session.headers['set-cookie'][0].split(';')[0] for (const route of ['/', '/oauth-client-metadata.json', '/oauth/broker/session', '/api/feedback/board']) { const denied = await request(port, route, 'evil.test', 'GET', { 'x-forwarded-host': 'reams.example.test', 'x-forwarded-proto': 'https', forwarded: 'host=reams.example.test;proto=https' }) assert.equal(denied.status, 404, route) } assert.equal((await request(port, 'https://evil.test/oauth/broker/session')).status, 404) await stop(child) assert.equal(await missing(`${env.OAUTH_STORE_PATH}.lock`), true) await rm(join(root, 'assets', 'old-deadbeef.wasm')) await writeFile(join(root, 'assets', 'new-feedface.js'), 'export const release = "B"') await writeFile(join(root, 'index.html'), 'release B') await writeFile(join(root, 'service-worker.js'), '// B') child = start(env) await ready(child, port) const old = await request(port, '/assets/old-deadbeef.wasm') assert.equal(old.status, 200) assert.equal(old.headers['content-type'], 'application/wasm') assert.equal(old.body, '\0asm') assert.match(old.headers['cache-control'], /immutable/) assert.equal((await request(port, '/fonts/font.woff2')).body, 'font bytes') assert.equal((await request(port, '/')).body, 'release B') const worker = await request(port, '/service-worker.js') assert.equal(worker.body, '// B') assert.equal(worker.headers['cache-control'], 'no-cache') assert.equal((await request(port, '/assets/missing.js')).status, 404) for (const route of ['/assets', '/assets/', '/fonts', '/fonts/', '/assets/missing', '/fonts/missing']) { for (const method of ['GET', 'HEAD']) assert.equal((await request(port, route, undefined, method)).status, 404, `${method} ${route}`) } assert.equal((await request(port, '/some-client-route')).body, 'release B') const reused = await request(port, '/oauth/broker/session', undefined, 'GET', { cookie, 'x-oauth-csrf': csrf }) assert.equal(JSON.parse(reused.body).csrf_token, csrf) await stop(child) child = start({ ...env, REAMS_ASSET_ARCHIVE_DIR: '' }) await ready(child, port) for (const route of ['/assets', '/assets/', '/fonts', '/fonts/', '/assets/missing', '/fonts/missing']) { for (const method of ['GET', 'HEAD']) assert.equal((await request(port, route, undefined, method)).status, 404, `without archive: ${method} ${route}`) } await stop(child) child = start({ ...env, REAMS_SHUTDOWN_TIMEOUT_MS: '100' }) await ready(child, port) const stalled = http.request({ hostname: '127.0.0.1', port, path: '/oauth/broker/par', method: 'POST', headers: { host: 'reams.example.test', origin: 'https://reams.example.test', cookie, 'x-oauth-csrf': csrf, 'content-type': 'application/x-www-form-urlencoded', 'content-length': '999', } }) stalled.on('error', () => {}) stalled.write('scope=') await new Promise(resolve => setTimeout(resolve, 40)) const timeoutExit = exited(child) child.kill('SIGTERM') child.kill('SIGTERM') assert.equal((await timeoutExit).code, 1) assert.match(child.diagnostics, /drain timed out/) stalled.destroy() child = start({ ...env, OAUTH_REDIRECT_URI: 'https://reams.example.test/elsewhere' }) assert.notEqual((await exited(child)).code, 0) assert.match(child.diagnostics, /disagrees with the compiled/) } finally { if (child && child.exitCode === null && child.signalCode === null) { child.kill('SIGKILL'); await exited(child) } await rm(dir, { recursive: true, force: true }) }})
test('request draining waits for asynchronous writes after the HTTP connection has closed', async () => { let finish, written = false const pending = new Promise(resolve => { finish = () => { written = true; resolve() } }) const handlers = new Set([pending]) const server = http.createServer((_req, res) => res.end()) server.listen(0, '127.0.0.1') await once(server, 'listening') const port = server.address().port await request(port, '/') let released = false const drain = drainRequests(server, handlers).then(() => { released = true }) await new Promise(resolve => setTimeout(resolve, 20)) assert.equal(server.listening, false) assert.equal(released, false) finish() await drain assert.equal(written, true) assert.equal(released, true)})
test('asset publication preserves existing bytes and rejects collisions and symlinks', async () => { const dir = await mkdtemp(join(tmpdir(), 'reams-assets-')) const root = join(dir, 'dist'), archive = join(dir, 'archive') await mkdir(join(root, 'assets'), { recursive: true }) try { await writeFile(join(root, 'assets', 'same.js'), 'first') retainAssets(root, archive) retainAssets(root, archive) await writeFile(join(root, 'assets', 'same.js'), 'different') assert.throws(() => retainAssets(root, archive), /collision/) assert.equal(await readFile(join(archive, 'assets', 'same.js'), 'utf8'), 'first') await writeFile(join(root, 'assets', 'same.js'), 'first') await symlink('/missing-target', join(archive, 'assets', 'broken.js')) await writeFile(join(root, 'assets', 'broken.js'), 'new') assert.throws(() => retainAssets(root, archive), /collision/) await rm(join(root, 'assets', 'broken.js')) await symlink(join(dir, 'secret'), join(root, 'assets', 'escape.js')) assert.throws(() => retainAssets(root, archive), /ordinary files/) } finally { await rm(dir, { recursive: true, force: true }) }})
function start(env) { const child = spawn(process.execPath, ['scripts/serve-atproto.mjs'], { env, stdio: ['ignore', 'ignore', 'pipe'] }) child.diagnostics = '' child.stderr.on('data', data => { child.diagnostics += data.toString() }) return child}async function exited(child) { if (child.exitCode !== null || child.signalCode !== null) return { code: child.exitCode, signal: child.signalCode } const [code, signal] = await once(child, 'exit') return { code, signal }}async function stop(child) { const exit = exited(child); child.kill('SIGTERM'); assert.equal((await exit).code, 0) }async function ready(child, port) { for (let attempt = 0; attempt < 100; attempt++) { if (child.exitCode !== null) throw new Error(child.diagnostics) try { if ((await request(port, '/healthz', 'internal')).status === 200) return } catch {} await new Promise(resolve => setTimeout(resolve, 30)) } throw new Error(`Server not ready: ${child.diagnostics}`)}function request(port, route, host = 'reams.example.test', method = 'GET', headers = {}) { return new Promise((resolve, reject) => { const deadline = setTimeout(() => req.destroy(new Error('Test HTTP request timed out')), 5000) const req = http.request({ hostname: '127.0.0.1', port, path: route, method, headers: { host, ...headers } }, async res => { try { const chunks = [] for await (const chunk of res) chunks.push(chunk) clearTimeout(deadline) resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks).toString() }) } catch (error) { clearTimeout(deadline); reject(error) } }) req.on('error', error => { clearTimeout(deadline); reject(error) }); req.end() })}async function freePort() { const server = http.createServer(); server.listen(0, '127.0.0.1'); await once(server, 'listening'); const port = server.address().port; await new Promise(resolve => server.close(resolve)); return port }async function missing(path) { try { await readFile(path); return false } catch (error) { if (error.code === 'ENOENT') return true; throw error } }