diff --git a/apps/code/src/server/index.tsx b/apps/code/src/server/index.tsx index cd303d1..f7fb8ae 100644 --- a/apps/code/src/server/index.tsx +++ b/apps/code/src/server/index.tsx @@ -3,11 +3,17 @@ import { define, stylesToCSS } from '@because/forge' import { baseStyles, ToolScript, theme } from '@because/toes/tools' import { readdir, stat } from 'fs/promises' import { readFileSync } from 'fs' -import { join, extname, basename } from 'path' +import { join, resolve, extname, basename } from 'path' import type { Child } from 'hono/jsx' const APPS_DIR = process.env.APPS_DIR! +const safePath = (base: string, ...segments: string[]) => { + const full = resolve(base, ...segments) + if (!full.startsWith(base + '/') && full !== base) return null + return full +} + const app = new Hype({ prettyHTML: false }) const Container = define('Container', { @@ -257,21 +263,15 @@ const fileMemoryScript = ` var params = new URLSearchParams(window.location.search); var app = params.get('app'); var file = params.get('file'); - var version = params.get('version') || 'current'; if (!app) return; - var key = 'code-app:' + app + ':' + version + ':file'; + var key = 'code-app:' + app + ':file'; if (params.has('file')) { - // Explicit file param (even empty) - save it if (file) localStorage.setItem(key, file); else localStorage.removeItem(key); } else { - // No file param - restore saved location var saved = localStorage.getItem(key); if (saved) { - var url = '/?app=' + encodeURIComponent(app); - if (version !== 'current') url += '&version=' + encodeURIComponent(version); - url += '&file=' + encodeURIComponent(saved); - window.location.replace(url); + window.location.replace('/?app=' + encodeURIComponent(app) + '&file=' + encodeURIComponent(saved)); } } })(); @@ -327,14 +327,14 @@ app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { app.get('/raw', async c => { const appName = c.req.query('app') - const version = c.req.query('version') || 'current' const filePath = c.req.query('file') if (!appName || !filePath) { return c.text('Missing app or file parameter', 400) } - const fullPath = join(APPS_DIR, appName, version, filePath) + const fullPath = safePath(APPS_DIR, appName, filePath) + if (!fullPath) return c.text('Invalid path', 400) const file = Bun.file(fullPath) if (!await file.exists()) { @@ -346,14 +346,14 @@ app.get('/raw', async c => { app.post('/save', async c => { const appName = c.req.query('app') - const version = c.req.query('version') || 'current' const filePath = c.req.query('file') if (!appName || !filePath) { return c.text('Missing app or file parameter', 400) } - const fullPath = join(APPS_DIR, appName, version, filePath) + const fullPath = safePath(APPS_DIR, appName, filePath) + if (!fullPath) return c.text('Invalid path', 400) const content = await c.req.text() try { @@ -385,10 +385,9 @@ async function listFiles(appPath: string, subPath: string = '') { interface BreadcrumbProps { appName: string filePath: string - versionParam: string } -function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) { +function PathBreadcrumb({ appName, filePath }: BreadcrumbProps) { const parts = filePath ? filePath.split('/').filter(Boolean) : [] const crumbs: { name: string; path: string }[] = [] @@ -401,7 +400,7 @@ function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) { return ( {crumbs.length > 0 ? ( - {appName} + {appName} ) : ( {appName} )} @@ -411,7 +410,7 @@ function PathBreadcrumb({ appName, filePath, versionParam }: BreadcrumbProps) { {i === crumbs.length - 1 ? ( {crumb.name} ) : ( - {crumb.name} + {crumb.name} )} ))} @@ -479,7 +478,6 @@ function getPrismLanguage(filename: string): string { app.get('/', async c => { const appName = c.req.query('app') - const version = c.req.query('version') || 'current' const filePath = c.req.query('file') || '' if (!appName) { @@ -490,19 +488,34 @@ app.get('/', async c => { ) } - const appPath = join(APPS_DIR, appName, version) + const appPath = safePath(APPS_DIR, appName) + if (!appPath) { + return c.html( + + Invalid app name + + ) + } try { await stat(appPath) } catch { return c.html( - App "{appName}" (version: {version}) not found + App "{appName}" not found + + ) + } + + const fullPath = safePath(appPath, filePath) + if (!fullPath) { + return c.html( + + Invalid file path ) } - const fullPath = join(appPath, filePath) let fileStats try { @@ -515,18 +528,16 @@ app.get('/', async c => { ) } - const versionParam = version !== 'current' ? `&version=${version}` : '' - if (fileStats.isFile()) { const filename = basename(fullPath) const fileType = getFileType(filename) - const rawUrl = `/raw?app=${appName}${versionParam}&file=${filePath}` + const rawUrl = `/raw?app=${appName}&file=${filePath}` const downloadUrl = `${rawUrl}&download=1` if (fileType === 'image') { return c.html( - + {filename} @@ -543,7 +554,7 @@ app.get('/', async c => { if (fileType === 'audio') { return c.html( - + {filename} @@ -560,7 +571,7 @@ app.get('/', async c => { if (fileType === 'video') { return c.html( - + {filename} @@ -615,7 +626,7 @@ saveBtn.onclick = async () => { status.textContent = 'Saving...'; try { - const res = await fetch('/save?app=${appName}${versionParam}&file=${filePath}', { + const res = await fetch('/save?app=${appName}&file=${filePath}', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: jar.toString() @@ -641,14 +652,14 @@ document.addEventListener('keydown', (e) => { ` return c.html( - + {filename}
Save - Done + Done
{content}
@@ -660,11 +671,11 @@ document.addEventListener('keydown', (e) => { return c.html( - + {filename} - Edit + Edit
{content}
@@ -676,11 +687,11 @@ document.addEventListener('keydown', (e) => { return c.html( - + {files.map(file => ( - + {file.isDirectory ? : } {file.name}