web server service detect sub directory index.html

This commit is contained in:
Raven Scott
2026-04-22 15:54:32 -04:00
parent faba92f8b1
commit 594c9d2279
2 changed files with 105 additions and 2 deletions
@@ -206,6 +206,25 @@ export function bareOsWwwResolveStaticPath(docrootNorm, pathname) {
return abs
}
/**
* `…/dir/index.html` under docroot when `dirAbs` is a served directory (path traversal guard).
* @param {string} docrootNorm
* @param {string} dirAbs
* @returns {string | null}
*/
function bareOsWwwJoinIndexInDirectory(docrootNorm, dirAbs) {
const d = docrootNorm.replace(/\/+$/, '') || '/'
const base = String(dirAbs || '').replace(/\/+$/, '')
if (!base) return null
const idx = `${base}/index.html`
if (d === '/') {
if (!idx.startsWith('/') || idx.includes('//')) return null
return idx
}
if (!(idx === d || idx.startsWith(`${d}/`))) return null
return idx
}
/**
* @param {string} absPath
*/
@@ -288,7 +307,7 @@ async function handleWwwRequest(req, res, ctx, docrootNorm) {
return
}
const u = new URL(req.url || '/', 'http://127.0.0.1')
const abs = bareOsWwwResolveStaticPath(docrootNorm, u.pathname)
let abs = bareOsWwwResolveStaticPath(docrootNorm, u.pathname)
if (!abs) {
res.statusCode = 400
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
@@ -306,8 +325,43 @@ async function handleWwwRequest(req, res, ctx, docrootNorm) {
return
}
try {
let st = await vfs.stat(abs)
if (st && st.type === 'directory') {
const idx = bareOsWwwJoinIndexInDirectory(docrootNorm, abs)
if (!idx) {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
abs = idx
try {
st = await vfs.stat(abs)
} catch (e) {
const code = /** @type {{ code?: string }} */ (e)?.code
if (code === 'ENOENT') {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
throw e
}
if (!st || st.type === 'directory') {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
}
if (req.method === 'HEAD') {
const st = await vfs.stat(abs)
if (!st || st.type === 'directory') {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
@@ -155,6 +155,55 @@ test('bare-os-www serves default index on ephemeral port', async (t) => {
rmSync(dir, { recursive: true, force: true })
})
test('bare-os-www serves ~/.www/<dir>/index.html for /dir without trailing slash', 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)
const bodyNoSlash = 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(bodyNoSlash), 'SUBDIR_INDEX_BODY')
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)