web server service detect sub directory index.html
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user