From 06aa07329f261d164f67eb6808ad9830886f2db3 Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 9 Nov 2025 21:30:56 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=97=A5=E5=BF=97=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/data/env.ts | 2 +- back/loaders/db.ts | 2 +- back/services/cron.ts | 94 ++++++++++++---------------------- back/validation/schedule.ts | 13 +++-- shell/task.sh | 29 +++++++---- src/pages/crontab/logModal.tsx | 2 +- src/pages/crontab/modal.tsx | 13 ++--- 7 files changed, 64 insertions(+), 91 deletions(-) diff --git a/back/data/env.ts b/back/data/env.ts index d08d2ec7..bd24f22a 100644 --- a/back/data/env.ts +++ b/back/data/env.ts @@ -44,5 +44,5 @@ export const EnvModel = sequelize.define('Env', { position: DataTypes.NUMBER, name: { type: DataTypes.STRING, unique: 'compositeIndex' }, remarks: DataTypes.STRING, - isPinned: { type: DataTypes.NUMBER, field: 'is_pinned' }, + isPinned: DataTypes.NUMBER, }); diff --git a/back/loaders/db.ts b/back/loaders/db.ts index a0af7c6d..f2d0a50d 100644 --- a/back/loaders/db.ts +++ b/back/loaders/db.ts @@ -62,7 +62,7 @@ export default async () => { ); } catch (error) {} try { - await sequelize.query('alter table Envs add column is_pinned NUMBER'); + await sequelize.query('alter table Envs add column isPinned NUMBER'); } catch (error) {} Logger.info('✌️ DB loaded'); diff --git a/back/services/cron.ts b/back/services/cron.ts index cbae3cc0..10e700be 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -27,7 +27,7 @@ import { ScheduleType } from '../interface/schedule'; @Service() export default class CronService { - constructor(@Inject('logger') private logger: winston.Logger) {} + constructor(@Inject('logger') private logger: winston.Logger) { } private isNodeCron(cron: Crontab) { const { schedule, extra_schedules } = cron; @@ -49,9 +49,27 @@ export default class CronService { return this.isOnceSchedule(schedule) || this.isBootSchedule(schedule); } + private async getLogName(cron: Crontab) { + const { log_name, command, id } = cron; + if (log_name === '/dev/null') { + return log_name; + } + let uniqPath = await getUniqPath(command, `${id}`); + if (log_name) { + const normalizedLogName = log_name.startsWith('/') ? log_name : path.join(config.logPath, log_name); + if (normalizedLogName.startsWith(config.logPath)) { + uniqPath = log_name; + } + } + const logDirPath = path.resolve(config.logPath, `${uniqPath}`); + await fs.mkdir(logDirPath, { recursive: true }); + return uniqPath; + } + public async create(payload: Crontab): Promise { const tab = new Crontab(payload); tab.saved = false; + tab.log_name = await this.getLogName(tab); const doc = await this.insert(tab); if (isDemoEnv()) { @@ -82,6 +100,7 @@ export default class CronService { const doc = await this.getDb({ id: payload.id }); const tab = new Crontab({ ...doc, ...payload }); tab.saved = false; + tab.log_name = await this.getLogName(tab); const newDoc = await this.updateDb(tab); if (doc.isDisabled === 1 || isDemoEnv()) { @@ -142,7 +161,7 @@ export default class CronService { let cron; try { cron = await this.getDb({ id }); - } catch (err) {} + } catch (err) { } if (!cron) { continue; } @@ -476,61 +495,14 @@ export default class CronService { `[panel][开始执行任务] 参数: ${JSON.stringify(params)}`, ); - let { id, command, log_path, log_name } = cron; - - // Check if log_name is an absolute path - const isAbsolutePath = log_name && log_name.startsWith('/'); - - let uniqPath: string; - let absolutePath: string; - let logPath: string; - - if (isAbsolutePath) { - // Special case: /dev/null is allowed as-is to discard logs - if (log_name === '/dev/null') { - uniqPath = log_name; - absolutePath = log_name; - logPath = log_name; - } else { - // For other absolute paths, ensure they are within the safe log directory - const normalizedLogName = path.normalize(log_name!); - const normalizedLogPath = path.normalize(config.logPath); - - if (!normalizedLogName.startsWith(normalizedLogPath)) { - this.logger.error( - `[panel][日志路径安全检查失败] 绝对路径必须在日志目录内: ${log_name}`, - ); - // Fallback to auto-generated path for security - const fallbackUniqPath = await getUniqPath(command, `${id}`); - const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); - const logDirPath = path.resolve(config.logPath, `${fallbackUniqPath}`); - if (log_path?.split('/')?.every((x) => x !== fallbackUniqPath)) { - await fs.mkdir(logDirPath, { recursive: true }); - } - logPath = `${fallbackUniqPath}/${logTime}.log`; - absolutePath = path.resolve(config.logPath, `${logPath}`); - uniqPath = fallbackUniqPath; - } else { - // Absolute path is safe, use it - uniqPath = log_name!; - absolutePath = log_name!; - logPath = log_name!; - } - } - } else { - // Sanitize log_name to prevent path traversal for relative paths - const sanitizedLogName = log_name - ? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '') - : ''; - uniqPath = sanitizedLogName || (await getUniqPath(command, `${id}`)); - const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); - const logDirPath = path.resolve(config.logPath, `${uniqPath}`); - if (log_path?.split('/')?.every((x) => x !== uniqPath)) { - await fs.mkdir(logDirPath, { recursive: true }); - } - logPath = `${uniqPath}/${logTime}.log`; - absolutePath = path.resolve(config.logPath, `${logPath}`); - } + let { id, command, log_name } = cron; + + const uniqPath = log_name === '/dev/null' ? (await getUniqPath(command, `${id}`)) : log_name; + const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); + const logDirPath = path.resolve(config.logPath, `${uniqPath}`); + await fs.mkdir(logDirPath, { recursive: true }); + const logPath = `${uniqPath}/${logTime}.log`; + const absolutePath = path.resolve(config.logPath, `${logPath}`); const cp = spawn( `real_log_path=${logPath} no_delay=true ${this.makeCommand( cron, @@ -610,7 +582,9 @@ export default class CronService { if (!doc) { return ''; } - + if (doc.log_name === '/dev/null') { + return '日志设置为忽略'; + } const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const logFileExist = doc.log_path && (await fileExist(absolutePath)); if (logFileExist) { @@ -653,9 +627,7 @@ export default class CronService { if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) { command = `${TASK_PREFIX}${tab.command}`; } - let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${ - tab.id - } `; + let commandVariable = `real_time=${Boolean(realTime)} log_name=${tab.log_name} no_tee=true ID=${tab.id} `; if (tab.task_before) { commandVariable += `task_before='${tab.task_before .replace(/'/g, "'\\''") diff --git a/back/validation/schedule.ts b/back/validation/schedule.ts index 3a429ed1..f396508b 100644 --- a/back/validation/schedule.ts +++ b/back/validation/schedule.ts @@ -45,27 +45,26 @@ export const commonCronSchema = { .allow(null) .custom((value, helpers) => { if (!value) return value; - + // Check if it's an absolute path if (value.startsWith('/')) { // Allow /dev/null as special case if (value === '/dev/null') { return value; } - + // For other absolute paths, ensure they are within the safe log directory const normalizedValue = path.normalize(value); const normalizedLogPath = path.normalize(config.logPath); - + if (!normalizedValue.startsWith(normalizedLogPath)) { return helpers.error('string.unsafePath'); } - + return value; } - - // For relative names, enforce strict pattern - if (!/^[a-zA-Z0-9_-]+$/.test(value)) { + + if (!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(value)) { return helpers.error('string.pattern.base'); } if (value.length > 100) { diff --git a/shell/task.sh b/shell/task.sh index a51eeaef..07c23c5a 100755 --- a/shell/task.sh +++ b/shell/task.sh @@ -46,18 +46,22 @@ handle_log_path() { time=$(date "+$mtime_format") log_time=$(format_log_time "$mtime_format" "$time") - log_dir_tmp="${file_param##*/}" - if [[ $file_param =~ "/" ]]; then - if [[ $file_param == /* ]]; then - log_dir_tmp_path="${file_param:1}" - else - log_dir_tmp_path="${file_param}" + if [[ -z $log_name ]]; then + log_dir_tmp="${file_param##*/}" + if [[ $file_param =~ "/" ]]; then + if [[ $file_param == /* ]]; then + log_dir_tmp_path="${file_param:1}" + else + log_dir_tmp_path="${file_param}" + fi fi + log_dir_tmp_path="${log_dir_tmp_path%/*}" + log_dir_tmp_path="${log_dir_tmp_path##*/}" + [[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}" + log_dir="${log_dir_tmp%.*}${suffix}" + else + log_dir="$log_name" fi - log_dir_tmp_path="${log_dir_tmp_path%/*}" - log_dir_tmp_path="${log_dir_tmp_path##*/}" - [[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}" - log_dir="${log_dir_tmp%.*}${suffix}" log_path="$log_dir/$log_time.log" if [[ ${real_log_path:=} ]]; then @@ -73,6 +77,11 @@ handle_log_path() { if [[ "${real_time:=}" == "true" ]]; then cmd="" fi + + if [[ "${log_dir:=}" == "/dev/null" ]]; then + cmd=">> /dev/null" + log_path="/dev/null" + fi } format_params() { diff --git a/src/pages/crontab/logModal.tsx b/src/pages/crontab/logModal.tsx index 59844305..b23af1bf 100644 --- a/src/pages/crontab/logModal.tsx +++ b/src/pages/crontab/logModal.tsx @@ -55,7 +55,7 @@ const CronLogModal = ({ const log = data as string; setValue(log || intl.get("暂无日志")); const hasNext = Boolean( - log && !logEnded(log) && !log.includes("日志不存在"), + log && !logEnded(log) && !log.includes("日志不存在") && !log.includes("日志设置为忽略"), ); if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) { setTimeout(() => { diff --git a/src/pages/crontab/modal.tsx b/src/pages/crontab/modal.tsx index d010adbe..8951df66 100644 --- a/src/pages/crontab/modal.tsx +++ b/src/pages/crontab/modal.tsx @@ -190,22 +190,15 @@ const CronModal = ({ { validator: (_, value) => { if (!value) return Promise.resolve(); - // Allow /dev/null specifically if (value === '/dev/null') return Promise.resolve(); - // Warn about other absolute paths (server will validate) - if (value.startsWith('/')) { - // We can't validate the exact path on frontend, but inform user - return Promise.resolve(); + if (value.length > 100) { + return Promise.reject(intl.get('日志名称不能超过100个字符')); } - // For relative names, enforce strict pattern - if (!/^[a-zA-Z0-9_-]+$/.test(value)) { + if (!/^(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?:\/)?(?:[\w.-]+\/)*[\w.-]+\/?$/.test(value)) { return Promise.reject( intl.get('日志名称只能包含字母、数字、下划线和连字符'), ); } - if (value.length > 100) { - return Promise.reject(intl.get('日志名称不能超过100个字符')); - } return Promise.resolve(); }, },