Fix installer pack fetch and simplify step 1 UI
The installer pulls each pack JSON from /manifest/<file>.json, but the server was blocking every /manifest/ path. Add a strict whitelist route that only serves /manifest/<a-zA-Z0-9_-name>.json files (no directory listing, no path traversal, no other extensions); keep the catch-all 404 for anything else under /manifest/. Also strip the manifest URL input and "목록 새로고침" button from installer step 1 — packs auto-load on page render, only the selectable list remains. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -67,11 +67,7 @@ function renderStep1() {
|
|||||||
section.className = 'page'
|
section.className = 'page'
|
||||||
section.innerHTML =
|
section.innerHTML =
|
||||||
'<h2>1단계. 설치할 음악퀴즈 선택</h2>' +
|
'<h2>1단계. 설치할 음악퀴즈 선택</h2>' +
|
||||||
'<p>관리 사이트의 manifest.json에서 음악퀴즈 목록을 가져옵니다.</p>' +
|
'<div id="packList" class="cardChoice"><p class="formMessage">목록을 불러오는 중...</p></div>' +
|
||||||
'<div class="fieldset"><label>manifest URL <input id="manifestUrl" type="url" value="' +
|
|
||||||
(state.manifestUrl || 'http://127.0.0.1:3000/manifest.json') + '" /></label>' +
|
|
||||||
'<button class="secondaryBtn" id="reload">목록 새로고침</button></div>' +
|
|
||||||
'<div id="packList" class="cardChoice"></div>' +
|
|
||||||
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
'<div class="actionRow"><span></span><button class="primaryBtn" id="next" disabled>다음</button></div>'
|
||||||
pageHost.appendChild(section)
|
pageHost.appendChild(section)
|
||||||
var listEl = section.querySelector('#packList')
|
var listEl = section.querySelector('#packList')
|
||||||
@@ -80,7 +76,7 @@ function renderStep1() {
|
|||||||
function renderList() {
|
function renderList() {
|
||||||
listEl.innerHTML = ''
|
listEl.innerHTML = ''
|
||||||
if (state.packs.length === 0) {
|
if (state.packs.length === 0) {
|
||||||
listEl.innerHTML = '<p class="formMessage">아직 음악퀴즈가 없습니다. "목록 새로고침"을 눌러 주세요.</p>'
|
listEl.innerHTML = '<p class="formMessage error">등록된 음악퀴즈가 없습니다.</p>'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.packs.forEach(function (pack) {
|
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 () {
|
nextBtn.addEventListener('click', async function () {
|
||||||
if (!state.selectedPackKey) return
|
if (!state.selectedPackKey) return
|
||||||
await installerApi.setSelectedPack(state.selectedPackKey)
|
await installerApi.setSelectedPack(state.selectedPackKey)
|
||||||
@@ -116,7 +100,15 @@ function renderStep1() {
|
|||||||
renderStep2()
|
renderStep2()
|
||||||
})
|
})
|
||||||
|
|
||||||
renderList()
|
;(async function () {
|
||||||
|
try {
|
||||||
|
var packs = await installerApi.loadPacks()
|
||||||
|
state.packs = packs
|
||||||
|
renderList()
|
||||||
|
} catch (err) {
|
||||||
|
listEl.innerHTML = '<p class="formMessage error">목록을 가져오지 못했습니다: ' + err.message + '</p>'
|
||||||
|
}
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStep2() {
|
function renderStep2() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import session from 'express-session'
|
import session from 'express-session'
|
||||||
import path from 'node:path'
|
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 { indexRouter } from './routes/index'
|
||||||
import { opRouter } from './routes/op'
|
import { opRouter } from './routes/op'
|
||||||
|
|
||||||
@@ -28,9 +28,9 @@ app.use(session({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// 외부에서 account.json, /manifest 폴더 등에 절대 접근 불가하도록 가장 먼저 차단한다.
|
// account.json은 외부 노출 절대 금지.
|
||||||
app.use((req, res, next) => {
|
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')
|
res.status(404).send('Not Found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -44,6 +44,29 @@ app.get('/manifest.json', (_req, res) => {
|
|||||||
res.sendFile(manifestRootPath)
|
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('/file', express.static(fileDirPath, { fallthrough: true, index: false }))
|
||||||
|
|
||||||
app.use('/', indexRouter)
|
app.use('/', indexRouter)
|
||||||
|
|||||||
Reference in New Issue
Block a user