Skip to content

Commit 00b1bc7

Browse files
authored
Merge branch 'develop' into copilot/add-scenario-mode-support
2 parents 2e0775e + 4cb9f57 commit 00b1bc7

File tree

12 files changed

+325
-57
lines changed

12 files changed

+325
-57
lines changed

back/api/env.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { Router, Request, Response, NextFunction } from 'express';
1+
import { Joi, celebrate } from 'celebrate';
2+
import { NextFunction, Request, Response, Router } from 'express';
3+
import fs from 'fs';
4+
import multer from 'multer';
25
import { Container } from 'typedi';
3-
import EnvService from '../services/env';
46
import { Logger } from 'winston';
5-
import { celebrate, Joi } from 'celebrate';
6-
import multer from 'multer';
77
import config from '../config';
8-
import fs from 'fs';
98
import { safeJSONParse } from '../config/util';
9+
import EnvService from '../services/env';
1010
const route = Router();
1111

1212
const storage = multer.diskStorage({
@@ -196,6 +196,40 @@ export default (app: Router) => {
196196
},
197197
);
198198

199+
route.put(
200+
'/pin',
201+
celebrate({
202+
body: Joi.array().items(Joi.number().required()),
203+
}),
204+
async (req: Request, res: Response, next: NextFunction) => {
205+
const logger: Logger = Container.get('logger');
206+
try {
207+
const envService = Container.get(EnvService);
208+
const data = await envService.pin(req.body);
209+
return res.send({ code: 200, data });
210+
} catch (e) {
211+
return next(e);
212+
}
213+
},
214+
);
215+
216+
route.put(
217+
'/unpin',
218+
celebrate({
219+
body: Joi.array().items(Joi.number().required()),
220+
}),
221+
async (req: Request, res: Response, next: NextFunction) => {
222+
const logger: Logger = Container.get('logger');
223+
try {
224+
const envService = Container.get(EnvService);
225+
const data = await envService.unPin(req.body);
226+
return res.send({ code: 200, data });
227+
} catch (e) {
228+
return next(e);
229+
}
230+
},
231+
);
232+
199233
route.post(
200234
'/upload',
201235
upload.single('env'),

back/data/cron.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class Crontab {
2121
extra_schedules?: Array<{ schedule: string }>;
2222
task_before?: string;
2323
task_after?: string;
24+
log_name?: string;
2425

2526
constructor(options: Crontab) {
2627
this.name = options.name;
@@ -45,6 +46,7 @@ export class Crontab {
4546
this.extra_schedules = options.extra_schedules;
4647
this.task_before = options.task_before;
4748
this.task_after = options.task_after;
49+
this.log_name = options.log_name;
4850
}
4951
}
5052

@@ -55,7 +57,7 @@ export enum CrontabStatus {
5557
'disabled',
5658
}
5759

58-
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
60+
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
5961
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
6062
name: {
6163
unique: 'compositeIndex',
@@ -84,4 +86,5 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
8486
extra_schedules: DataTypes.JSON,
8587
task_before: DataTypes.STRING,
8688
task_after: DataTypes.STRING,
89+
log_name: DataTypes.STRING,
8790
});

back/data/env.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { DataTypes, Model } from 'sequelize';
12
import { sequelize } from '.';
2-
import { DataTypes, Model, ModelDefined } from 'sequelize';
33

44
export class Env {
55
value?: string;
@@ -9,6 +9,7 @@ export class Env {
99
position?: number;
1010
name?: string;
1111
remarks?: string;
12+
isPinned?: 1 | 0;
1213

1314
constructor(options: Env) {
1415
this.value = options.value;
@@ -21,6 +22,7 @@ export class Env {
2122
this.position = options.position;
2223
this.name = options.name;
2324
this.remarks = options.remarks || '';
25+
this.isPinned = options.isPinned || 0;
2426
}
2527
}
2628

@@ -42,4 +44,5 @@ export const EnvModel = sequelize.define<EnvInstance>('Env', {
4244
position: DataTypes.NUMBER,
4345
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
4446
remarks: DataTypes.STRING,
47+
isPinned: { type: DataTypes.NUMBER, field: 'is_pinned' },
4548
});

back/loaders/db.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ export default async () => {
6161
await sequelize.query('alter table Crontabs add column task_after TEXT');
6262
} catch (error) {}
6363
try {
64-
await sequelize.query('alter table Scenarios add column workflowGraph JSON');
64+
await sequelize.query(
65+
'alter table Crontabs add column log_name VARCHAR(255)',
66+
);
67+
} catch (error) {}
68+
try {
69+
await sequelize.query('alter table Envs add column is_pinned NUMBER');
6570
} catch (error) {}
6671

6772
Logger.info('✌️ DB loaded');

back/services/cron.ts

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -476,15 +476,61 @@ export default class CronService {
476476
`[panel][开始执行任务] 参数: ${JSON.stringify(params)}`,
477477
);
478478

479-
let { id, command, log_path } = cron;
480-
const uniqPath = await getUniqPath(command, `${id}`);
481-
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
482-
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
483-
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
484-
await fs.mkdir(logDirPath, { recursive: true });
479+
let { id, command, log_path, log_name } = cron;
480+
481+
// Check if log_name is an absolute path
482+
const isAbsolutePath = log_name && log_name.startsWith('/');
483+
484+
let uniqPath: string;
485+
let absolutePath: string;
486+
let logPath: string;
487+
488+
if (isAbsolutePath) {
489+
// Special case: /dev/null is allowed as-is to discard logs
490+
if (log_name === '/dev/null') {
491+
uniqPath = log_name;
492+
absolutePath = log_name;
493+
logPath = log_name;
494+
} else {
495+
// For other absolute paths, ensure they are within the safe log directory
496+
const normalizedLogName = path.normalize(log_name!);
497+
const normalizedLogPath = path.normalize(config.logPath);
498+
499+
if (!normalizedLogName.startsWith(normalizedLogPath)) {
500+
this.logger.error(
501+
`[panel][日志路径安全检查失败] 绝对路径必须在日志目录内: ${log_name}`,
502+
);
503+
// Fallback to auto-generated path for security
504+
const fallbackUniqPath = await getUniqPath(command, `${id}`);
505+
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
506+
const logDirPath = path.resolve(config.logPath, `${fallbackUniqPath}`);
507+
if (log_path?.split('/')?.every((x) => x !== fallbackUniqPath)) {
508+
await fs.mkdir(logDirPath, { recursive: true });
509+
}
510+
logPath = `${fallbackUniqPath}/${logTime}.log`;
511+
absolutePath = path.resolve(config.logPath, `${logPath}`);
512+
uniqPath = fallbackUniqPath;
513+
} else {
514+
// Absolute path is safe, use it
515+
uniqPath = log_name!;
516+
absolutePath = log_name!;
517+
logPath = log_name!;
518+
}
519+
}
520+
} else {
521+
// Sanitize log_name to prevent path traversal for relative paths
522+
const sanitizedLogName = log_name
523+
? log_name.replace(/[\/\\\.]/g, '_').replace(/^_+|_+$/g, '')
524+
: '';
525+
uniqPath = sanitizedLogName || (await getUniqPath(command, `${id}`));
526+
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
527+
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
528+
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
529+
await fs.mkdir(logDirPath, { recursive: true });
530+
}
531+
logPath = `${uniqPath}/${logTime}.log`;
532+
absolutePath = path.resolve(config.logPath, `${logPath}`);
485533
}
486-
const logPath = `${uniqPath}/${logTime}.log`;
487-
const absolutePath = path.resolve(config.logPath, `${logPath}`);
488534
const cp = spawn(
489535
`real_log_path=${logPath} no_delay=true ${this.makeCommand(
490536
cron,

back/services/env.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Service, Inject } from 'typedi';
1+
import groupBy from 'lodash/groupBy';
2+
import { FindOptions, Op } from 'sequelize';
3+
import { Inject, Service } from 'typedi';
24
import winston from 'winston';
35
import config from '../config';
4-
import * as fs from 'fs/promises';
56
import {
67
Env,
78
EnvModel,
@@ -11,8 +12,6 @@ import {
1112
minPosition,
1213
stepPosition,
1314
} from '../data/env';
14-
import groupBy from 'lodash/groupBy';
15-
import { FindOptions, Op } from 'sequelize';
1615
import { writeFileWithLock } from '../shared/utils';
1716

1817
@Service()
@@ -147,6 +146,7 @@ export default class EnvService {
147146
}
148147
try {
149148
const result = await this.find(condition, [
149+
['isPinned', 'DESC'],
150150
['position', 'DESC'],
151151
['createdAt', 'ASC'],
152152
]);
@@ -190,6 +190,14 @@ export default class EnvService {
190190
await this.set_envs();
191191
}
192192

193+
public async pin(ids: number[]) {
194+
await EnvModel.update({ isPinned: 1 }, { where: { id: ids } });
195+
}
196+
197+
public async unPin(ids: number[]) {
198+
await EnvModel.update({ isPinned: 0 }, { where: { id: ids } });
199+
}
200+
193201
public async set_envs() {
194202
const envs = await this.envs('', {
195203
name: { [Op.not]: null },

back/validation/schedule.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Joi } from 'celebrate';
22
import cron_parser from 'cron-parser';
33
import { ScheduleType } from '../interface/schedule';
4+
import path from 'path';
5+
import config from '../config';
46

57
const validateSchedule = (value: string, helpers: any) => {
68
if (
@@ -37,4 +39,43 @@ export const commonCronSchema = {
3739
extra_schedules: Joi.array().optional().allow(null),
3840
task_before: Joi.string().optional().allow('').allow(null),
3941
task_after: Joi.string().optional().allow('').allow(null),
42+
log_name: Joi.string()
43+
.optional()
44+
.allow('')
45+
.allow(null)
46+
.custom((value, helpers) => {
47+
if (!value) return value;
48+
49+
// Check if it's an absolute path
50+
if (value.startsWith('/')) {
51+
// Allow /dev/null as special case
52+
if (value === '/dev/null') {
53+
return value;
54+
}
55+
56+
// For other absolute paths, ensure they are within the safe log directory
57+
const normalizedValue = path.normalize(value);
58+
const normalizedLogPath = path.normalize(config.logPath);
59+
60+
if (!normalizedValue.startsWith(normalizedLogPath)) {
61+
return helpers.error('string.unsafePath');
62+
}
63+
64+
return value;
65+
}
66+
67+
// For relative names, enforce strict pattern
68+
if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
69+
return helpers.error('string.pattern.base');
70+
}
71+
if (value.length > 100) {
72+
return helpers.error('string.max');
73+
}
74+
return value;
75+
})
76+
.messages({
77+
'string.pattern.base': '日志名称只能包含字母、数字、下划线和连字符',
78+
'string.max': '日志名称不能超过100个字符',
79+
'string.unsafePath': '绝对路径必须在日志目录内或使用 /dev/null',
80+
}),
4081
};

shell/check.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ copy_dep() {
2424

2525
pm2_log() {
2626
echo -e "---> pm2日志"
27-
local panelOut="/root/.pm2/logs/panel-out.log"
28-
local panelError="/root/.pm2/logs/panel-error.log"
27+
local panelOut="/root/.pm2/logs/qinglong-out.log"
28+
local panelError="/root/.pm2/logs/qinglong-error.log"
2929
tail -n 300 "$panelOut"
3030
tail -n 300 "$panelError"
3131
}

src/locales/en-US.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,4 +592,13 @@
592592
"否": "No",
593593
"共": "Total",
594594
"项": "items"
595+
"日志名称": "Log Name",
596+
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate",
597+
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports absolute paths like /dev/null",
598+
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持 /dev/null 丢弃日志,其他绝对路径必须在日志目录内": "Custom log folder name to distinguish logs from different tasks. Leave blank to auto-generate. Supports /dev/null to discard logs, other absolute paths must be within log directory",
599+
"请输入自定义日志文件夹名称": "Please enter a custom log folder name",
600+
"请输入自定义日志文件夹名称或绝对路径": "Please enter a custom log folder name or absolute path",
601+
"请输入自定义日志文件夹名称或 /dev/null": "Please enter a custom log folder name or /dev/null",
602+
"日志名称只能包含字母、数字、下划线和连字符": "Log name can only contain letters, numbers, underscores and hyphens",
603+
"日志名称不能超过100个字符": "Log name cannot exceed 100 characters"
595604
}

src/locales/zh-CN.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,4 +592,13 @@
592592
"否": "",
593593
"共": "",
594594
"项": ""
595+
"日志名称": "日志名称",
596+
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成",
597+
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持绝对路径如 /dev/null",
598+
"自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持 /dev/null 丢弃日志,其他绝对路径必须在日志目录内": "自定义日志文件夹名称,用于区分不同任务的日志,留空则自动生成。支持 /dev/null 丢弃日志,其他绝对路径必须在日志目录内",
599+
"请输入自定义日志文件夹名称": "请输入自定义日志文件夹名称",
600+
"请输入自定义日志文件夹名称或绝对路径": "请输入自定义日志文件夹名称或绝对路径",
601+
"请输入自定义日志文件夹名称或 /dev/null": "请输入自定义日志文件夹名称或 /dev/null",
602+
"日志名称只能包含字母、数字、下划线和连字符": "日志名称只能包含字母、数字、下划线和连字符",
603+
"日志名称不能超过100个字符": "日志名称不能超过100个字符"
595604
}

0 commit comments

Comments
 (0)