diff --git a/src/backend/clients/event/types.ts b/src/backend/clients/event/types.ts index e1dcca57f6..f9c65f7ace 100644 --- a/src/backend/clients/event/types.ts +++ b/src/backend/clients/event/types.ts @@ -14,6 +14,8 @@ * the SocketService fans out to user-scoped channels. */ +import { Actor } from '../../core'; + // GUI write events spread an entry plus per-event metadata into `response`. // The exact field set varies by emit site (FSController / LegacyFSController / // WebDAVController each project a slightly different shape) so the envelope @@ -96,8 +98,11 @@ export type EventMap = { }; 'app.from-origin': { origin: string }; 'app.privateAccess.check': { - app: unknown; - actor: unknown; + appUid: string; + userUid: string; + requestHost: string; + requestPath: string; + actor?: Actor; result: { allowed: boolean; reason?: string; diff --git a/src/backend/core/http/middleware/puterSite.test.ts b/src/backend/core/http/middleware/puterSite.test.ts index 6a4bec3b70..c7be4ee027 100644 --- a/src/backend/core/http/middleware/puterSite.test.ts +++ b/src/backend/core/http/middleware/puterSite.test.ts @@ -629,7 +629,7 @@ describe('createPuterSiteMiddleware — file serving', () => { expect(String(out.body)).toContain('Not Found'); }); - it('returns 404 when the resolved URL points at a directory rather than a file', async () => { + it('returns 404 when the resolved URL points at a directory with no index.html', async () => { const owner = await makeUserWithHome(); const homePath = `/${owner.username}`; const homeEntry = await server.stores.fsEntry.getEntryByPath(homePath); @@ -645,8 +645,8 @@ describe('createPuterSiteMiddleware — file serving', () => { await mw( makeReq({ hostname: `${sub}.site.puter.localhost`, - // Documents/ exists from generateDefaultFsentries — a - // directory entry, which the file branch must refuse. + // Documents/ exists from generateDefaultFsentries but has + // no index.html — directory fallback must still 404. path: '/Documents', }), res, @@ -657,6 +657,45 @@ describe('createPuterSiteMiddleware — file serving', () => { expect(out.contentType).toBe('text/html; charset=UTF-8'); }); + it('serves /index.html when the URL resolves to a folder containing one', async () => { + const owner = await makeUserWithHome(); + const homePath = `/${owner.username}`; + const homeEntry = await server.stores.fsEntry.getEntryByPath(homePath); + const sub = `folderidx-${Math.random().toString(36).slice(2, 8)}`; + await server.stores.subdomain.create({ + userId: owner.id, + subdomain: sub, + rootDirId: homeEntry!.id, + }); + const body = Buffer.from('nested doc'); + await writeFile( + owner.id, + `${homePath}/Documents/index.html`, + body, + 'text/html', + ); + + const mw = buildMiddleware(); + const { res, out } = makeRes(); + await mw( + makeReq({ + hostname: `${sub}.site.puter.localhost`, + // No trailing slash; folder exists and contains index.html. + path: '/Documents', + }), + res, + vi.fn(), + ); + // Allow the piped stream to flush. + await new Promise((resolve) => setImmediate(resolve)); + + expect(out.statusCode).toBe(200); + expect(out.headers['Content-Type']).toMatch(/text\/html/); + const piped = out.body as Buffer | undefined; + expect(Buffer.isBuffer(piped)).toBe(true); + expect(piped!.equals(body)).toBe(true); + }); + it("serves the HTML SUBDOMAIN_404 when the subdomain's root_dir_id points to a missing entry", async () => { // Subdomain row references a fsentry id that doesn't exist — // earlier path resolution must catch this with the HTML 404 page. diff --git a/src/backend/core/http/middleware/puterSite.ts b/src/backend/core/http/middleware/puterSite.ts index 7f1712e2f2..457090f8b1 100644 --- a/src/backend/core/http/middleware/puterSite.ts +++ b/src/backend/core/http/middleware/puterSite.ts @@ -436,7 +436,16 @@ export const createPuterSiteMiddleware = ( // Resolve URL path → absolute FS path under the site root. let urlPath = req.path || '/'; if (urlPath.endsWith('/')) urlPath += 'index.html'; - const decoded = decodeURIComponent(urlPath); + let decoded: string; + try { + decoded = decodeURIComponent(urlPath); + } catch { + // Malformed `%xx` escape — treat as a missing path, not a 500. + res.status(404) + .type('text/html; charset=UTF-8') + .send('

404

Not Found

'); + return; + } // pathPosix.normalize strips `..` segments; the join with '/' anchors // it so traversal can't escape the site root. const resolvedUrlPath = pathPosix.normalize( @@ -452,7 +461,14 @@ export const createPuterSiteMiddleware = ( // Subdomain hosting bypasses ACL by design: anything the owner placed // under the registered root_dir is treated as public. Path traversal // is blocked above by `pathPosix.normalize` anchoring at `/`. - const entry = await layers.stores.fsEntry.getEntryByPath(filePath); + let entry = await layers.stores.fsEntry.getEntryByPath(filePath); + if (entry?.isDir) { + // Folder request → fall back to /index.html, the same + // way `/` is rewritten to `/index.html` at the site root above. + entry = await layers.stores.fsEntry.getEntryByPath( + pathPosix.join(filePath, 'index.html'), + ); + } if (!entry || entry.isDir) { res.status(404) .type('text/html; charset=UTF-8')