import electron from 'electron'; import { readFile } from 'node:fs/promises'; import { join, normalize, sep, extname } from 'node:path'; const { protocol } = electron; export function registerSchemesAsPrivileged(): void { protocol.registerSchemesAsPrivileged([ { scheme: 'inkling-media', privileges: { secure: true, supportFetchAPI: true, stream: true } } ]); } export function inferMime(filename: string): string { const ext = extname(filename).toLowerCase(); switch (ext) { case '.png': return 'image/png'; case '.jpg': case '.jpeg': return 'image/jpeg'; case '.gif': return 'image/gif'; case '.webp': return 'image/webp'; default: return 'application/octet-stream'; } } export function registerInklingMediaProtocol(profileDir: string): void { const mediaRoot = join(profileDir, 'media'); protocol.handle('inkling-media', async (req) => { // Raw URL 에서 `..` 세그먼트 (URL-encoded 포함) 검출 — `new URL()` 이 normalize 하기 전에 차단. const rawLower = req.url.toLowerCase(); if ( rawLower.includes('/../') || rawLower.endsWith('/..') || rawLower.includes('/%2e%2e/') || rawLower.endsWith('/%2e%2e') ) { return new Response(null, { status: 403 }); } const url = new URL(req.url); // inkling-media://media// → host='media', pathname='//' const relPath = decodeURIComponent(`${url.host}${url.pathname}`); const target = normalize(join(profileDir, relPath)); if (!target.startsWith(mediaRoot + sep) && target !== mediaRoot) { return new Response(null, { status: 403 }); } try { const data = await readFile(target); return new Response(new Uint8Array(data), { headers: { 'content-type': inferMime(target) } }); } catch { return new Response(null, { status: 404 }); } }); }