Skip to content

Commit

Permalink
fix: update TypeScript configuration and enhance console API for file…
Browse files Browse the repository at this point in the history
… handling
  • Loading branch information
sj817 committed Jan 16, 2025
1 parent 5fda871 commit b5d8d45
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 52 deletions.
128 changes: 79 additions & 49 deletions packages/core/src/server/api/console.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'node:fs'
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { router } from './router'
import { consolePath } from '@/root'
Expand All @@ -7,68 +7,98 @@ import { isLocalRequest } from '@/utils/system/ip'

import type { RequestHandler } from 'express'

const consoleRouter: RequestHandler = async (req, res) => {
const cfg = adapter()
/** 允许的文件类型及其对应的 Content-Type */
const ALLOWED_TYPES = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.mp3': 'audio/mpeg',
'.mp4': 'video/mp4',
'.wav': 'audio/wav',
'.webp': 'image/webp',
'.json': 'application/json',
'.txt': 'text/plain',
'.html': 'text/html',
'.css': 'text/css',
} as const

let name = req.path.split('/').pop()
if (!name) {
res.status(400).json({ error: '无效的请求', message: '文件名不能为空' })
return
}
/** 最大文件大小 (1024MB) */
const MAX_FILE_SIZE = 1024 * 1024 * 1024

name = decodeURIComponent(name)
/** 防止路径穿越 */
if (name.includes('..')) {
res.status(403).json({ error: '禁止访问', message: '非法请求' })
return
}
const consoleRouter: RequestHandler = async (req, res) => {
try {
const cfg = adapter()

const isLocal = await isLocalRequest(req)
if (cfg.console.isLocal) {
if (!isLocal) {
res.status(403).json({ error: '禁止访问', message: '无效的请求' })
let url = decodeURIComponent(req.path)
.replace(/\/+/g, '/') // 处理多个斜杠的情况
.replace(/^\/+|\/+$/g, '') // 移除开头和结尾的斜杠

url = url.split('/').pop() || ''
if (!url) {
res.status(400).json({ error: '无效的请求', message: '文件名不能为空' })
return
}
} else {
if (!cfg.console.token) {
res.status(500).json({ error: '配置错误', message: '缺少 token 配置' })

/** 防止路径穿越 */
if (url.includes('..') || url.includes('~') || !url.match(/^[a-zA-Z0-9-_.]+$/)) {
res.status(403).json({ error: '禁止访问', message: '非法请求' })
return
}

const token = req.query.token
if (!token || token !== cfg.console.token) {
res.status(403).json({ error: '禁止访问', message: '无效的 token' })
const ext = path.extname(url).toLowerCase()
if (!ALLOWED_TYPES[ext as keyof typeof ALLOWED_TYPES]) {
res.status(403).json({ error: '禁止访问', message: '不支持的文件类型' })
return
}
}

const file = path.join(consolePath, name)
if (!fs.existsSync(file)) {
res.status(404).json({ error: '文件不存在', message: '文件不存在' })
return
}
const isLocal = await isLocalRequest(req)
if (cfg.console.isLocal) {
if (!isLocal) {
res.status(403).json({ error: '禁止访问', message: '无效的请求' })
return
}
} else {
if (!cfg.console.token) {
res.status(500).json({ error: '配置错误', message: '缺少 token 配置' })
return
}

const data = fs.readFileSync(file)
if (!data) {
res.status(500).json({ error: '内部错误', message: '读取失败' })
return
}
const token = req.query.token
if (!token || token !== cfg.console.token) {
res.status(403).json({ error: '禁止访问', message: '无效的 token' })
return
}
}

const file = path.join(consolePath, url)

try {
/** 组合路径之后 判断一下文件是否处于 consolePath 目录下 */
if (!file.startsWith(consolePath)) {
res.status(403).json({ error: '禁止访问', message: '非法请求' })
return
}

const stats = await fs.stat(file)
if (stats.size > MAX_FILE_SIZE) {
res.status(413).json({ error: '文件过大', message: '文件大小超过限制' })
return
}
} catch {
res.status(404).json({ error: '文件不存在', message: '文件不存在' })
return
}

const ext = path.extname(name).toLowerCase()
switch (ext) {
case '.png':
res.setHeader('Content-Type', 'image/png')
break
case '.mp3':
res.setHeader('Content-Type', 'audio/mpeg')
break
case '.mp4':
res.setHeader('Content-Type', 'video/mp4')
break
default:
res.setHeader('Content-Type', 'application/octet-stream')
const data = await fs.readFile(file)
res.setHeader('Content-Type', ALLOWED_TYPES[ext as keyof typeof ALLOWED_TYPES])
res.setHeader('Content-Length', data.length)
res.setHeader('X-Content-Type-Options', 'nosniff')
res.send(data)
} catch (error) {
console.error('Console router error:', error)
res.status(500).json({ error: '内部错误', message: '服务器错误' })
}
res.send(data)
}

router.get('/console/*', consoleRouter)
1 change: 1 addition & 0 deletions packages/core/src/utils/fs/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './fs'
export * from './require'
export * from './changelog'
export * from './key'
export * as file from './fs'
8 changes: 5 additions & 3 deletions packages/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
"allowSyntheticDefaultImports": true,
"alwaysStrict": true,
"baseUrl": ".",
"declaration": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"module": "ES2022",
"moduleResolution": "Bundler",
Expand All @@ -20,12 +21,13 @@
"resolveJsonModule": true,
"rootDir": "./src",
"skipLibCheck": false,
"sourceMap": false,
"sourceMap": true,
"strict": true,
"target": "ES2022",
"types": [
"@types/node"
]
],
"declarationDir": "./lib/types"
},
"exclude": [
"@karinjs",
Expand Down

0 comments on commit b5d8d45

Please sign in to comment.