diff --git a/installer/renderer.js b/installer/renderer.js index 63dc71f..121c4fc 100644 --- a/installer/renderer.js +++ b/installer/renderer.js @@ -67,11 +67,7 @@ function renderStep1() { section.className = 'page' section.innerHTML = '
관리 사이트의 manifest.json에서 음악퀴즈 목록을 가져옵니다.
' + - '목록을 불러오는 중...
아직 음악퀴즈가 없습니다. "목록 새로고침"을 눌러 주세요.
' + listEl.innerHTML = '등록된 음악퀴즈가 없습니다.
' return } state.packs.forEach(function (pack) { @@ -97,18 +93,6 @@ function renderStep1() { }) } - section.querySelector('#reload').addEventListener('click', async function () { - var manifestUrl = section.querySelector('#manifestUrl').value - state.manifestUrl = manifestUrl - try { - var packs = await installerApi.loadPacks(manifestUrl) - state.packs = packs - renderList() - } catch (err) { - alert('manifest 다운로드 실패: ' + err.message) - } - }) - nextBtn.addEventListener('click', async function () { if (!state.selectedPackKey) return await installerApi.setSelectedPack(state.selectedPackKey) @@ -116,7 +100,15 @@ function renderStep1() { renderStep2() }) - renderList() + ;(async function () { + try { + var packs = await installerApi.loadPacks() + state.packs = packs + renderList() + } catch (err) { + listEl.innerHTML = '목록을 가져오지 못했습니다: ' + err.message + '
' + } + })() } function renderStep2() { diff --git a/src/server/app.ts b/src/server/app.ts index d20e7f7..16a6e1b 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -1,7 +1,7 @@ import express from 'express' import session from 'express-session' import path from 'node:path' -import { manifestRootPath, fileDirPath, viewsDirPath, publicDirPath } from '../shared/paths' +import { manifestRootPath, manifestDirPath, fileDirPath, viewsDirPath, publicDirPath } from '../shared/paths' import { indexRouter } from './routes/index' import { opRouter } from './routes/op' @@ -28,9 +28,9 @@ app.use(session({ } })) -// 외부에서 account.json, /manifest 폴더 등에 절대 접근 불가하도록 가장 먼저 차단한다. +// account.json은 외부 노출 절대 금지. app.use((req, res, next) => { - if (/^\/account\.json/i.test(req.path) || /^\/manifest\//i.test(req.path)) { + if (/^\/account\.json/i.test(req.path)) { res.status(404).send('Not Found') return } @@ -44,6 +44,29 @@ app.get('/manifest.json', (_req, res) => { res.sendFile(manifestRootPath) }) +// 설치기에서 개별 음악퀴즈 JSON을 가져갈 수 있도록 파일 단위로만 허용. +// 디렉토리 리스팅, 다른 확장자, 경로 탈출은 차단. +app.get('/manifest/:fileName', (req, res) => { + const fileName = req.params.fileName + if (!/^[a-zA-Z0-9_\-]+\.json$/.test(fileName)) { + res.status(404).send('Not Found') + return + } + res.sendFile(path.join(manifestDirPath, fileName), (err) => { + if (!err || res.headersSent) return + res.status(404).send('Not Found') + }) +}) + +// 그 외 /manifest/ 하위 모든 요청 차단 (디렉토리 인덱스 포함). +app.use((req, res, next) => { + if (/^\/manifest\//i.test(req.path)) { + res.status(404).send('Not Found') + return + } + next() +}) + app.use('/file', express.static(fileDirPath, { fallthrough: true, index: false })) app.use('/', indexRouter)