mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-30 17:38:10 +08:00
fix: secure file routes and dependency management
This commit is contained in:
+5
-7
@@ -98,13 +98,11 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response) => {
|
||||||
try {
|
return res.send({
|
||||||
const configService = Container.get(ConfigService);
|
code: 410,
|
||||||
await configService.getFile(req.params.file, res);
|
message: t('接口已下线,请使用 /configs/detail 接口'),
|
||||||
} catch (e) {
|
});
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+3
-17
@@ -5,7 +5,6 @@ import { Logger } from 'winston';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { t } from '../shared/i18n';
|
import { t } from '../shared/i18n';
|
||||||
import {
|
import {
|
||||||
getFileContentByName,
|
|
||||||
readDirs,
|
readDirs,
|
||||||
removeAnsi,
|
removeAnsi,
|
||||||
rmPath,
|
rmPath,
|
||||||
@@ -89,24 +88,11 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/:file',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
(req: Request, res: Response) => {
|
||||||
try {
|
|
||||||
const logService = Container.get(LogService);
|
|
||||||
const finalPath = logService.checkFilePath(
|
|
||||||
(req.query.path as string) || '',
|
|
||||||
(req.params.file as string) || '',
|
|
||||||
);
|
|
||||||
if (!finalPath || blacklist.includes(req.query.path as string)) {
|
|
||||||
return res.send({
|
return res.send({
|
||||||
code: 403,
|
code: 410,
|
||||||
message: t('暂无权限'),
|
message: t('接口已下线,请使用 /logs/detail 接口'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const content = await getFileContentByName(finalPath);
|
|
||||||
res.send({ code: 200, data: content });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+5
-19
@@ -103,25 +103,11 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
route.get(
|
route.get(
|
||||||
'/:file',
|
'/:file',
|
||||||
celebrate({
|
(req: Request, res: Response) => {
|
||||||
params: Joi.object({
|
return res.send({
|
||||||
file: Joi.string().required(),
|
code: 410,
|
||||||
}),
|
message: t('接口已下线,请使用 /scripts/detail 接口'),
|
||||||
query: Joi.object({
|
});
|
||||||
path: Joi.string().optional().allow(''),
|
|
||||||
}).unknown(true),
|
|
||||||
}),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const scriptService = Container.get(ScriptService);
|
|
||||||
const content = await scriptService.getFile(
|
|
||||||
req.query?.path as string || '',
|
|
||||||
req.params.file,
|
|
||||||
);
|
|
||||||
res.send({ code: 200, data: content });
|
|
||||||
} catch (e) {
|
|
||||||
return next(e);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
+4
-19
@@ -14,6 +14,7 @@ import { AuthInfo } from '../data/system';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { t } from '../shared/i18n';
|
import { t } from '../shared/i18n';
|
||||||
import { AppScope } from '../data/open';
|
import { AppScope } from '../data/open';
|
||||||
|
import protectedPathCase from '../middlewares/protectedPathCase';
|
||||||
|
|
||||||
function resolveTrustProxy(value = process.env.QL_TRUST_PROXY) {
|
function resolveTrustProxy(value = process.env.QL_TRUST_PROXY) {
|
||||||
const setting = value?.trim();
|
const setting = value?.trim();
|
||||||
@@ -36,25 +37,9 @@ export default ({ app }: { app: Application }) => {
|
|||||||
app.set('trust proxy', resolveTrustProxy());
|
app.set('trust proxy', resolveTrustProxy());
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
|
|
||||||
// Security: Path normalization middleware to prevent case variation attacks
|
// Security: Reject case variations under protected API namespaces before
|
||||||
app.use((req, res, next) => {
|
// authentication checks can interpret the request differently from routing.
|
||||||
const originalPath = req.path;
|
app.use(protectedPathCase);
|
||||||
const normalizedPath = originalPath.toLowerCase();
|
|
||||||
|
|
||||||
// Block requests with case variations on protected paths
|
|
||||||
if (
|
|
||||||
originalPath !== normalizedPath &&
|
|
||||||
(normalizedPath.startsWith('/api/') ||
|
|
||||||
normalizedPath.startsWith('/open/'))
|
|
||||||
) {
|
|
||||||
return res.status(400).json({
|
|
||||||
code: 400,
|
|
||||||
message: 'Invalid path format',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rewrite URLs to strip baseUrl prefix if configured
|
// Rewrite URLs to strip baseUrl prefix if configured
|
||||||
// This allows the rest of the app to work without baseUrl awareness
|
// This allows the rest of the app to work without baseUrl awareness
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
|
export default function protectedPathCase(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction,
|
||||||
|
) {
|
||||||
|
const originalPath = req.path;
|
||||||
|
const normalizedPath = originalPath.toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
originalPath !== normalizedPath &&
|
||||||
|
(normalizedPath.startsWith('/api/') ||
|
||||||
|
normalizedPath.startsWith('/open/'))
|
||||||
|
) {
|
||||||
|
return res.status(400).json({
|
||||||
|
code: 400,
|
||||||
|
message: 'Invalid path format',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
getPid,
|
getPid,
|
||||||
killTask,
|
killTask,
|
||||||
promiseExecSuccess,
|
promiseExecSuccess,
|
||||||
|
concurrentRun,
|
||||||
getInstallCommand,
|
getInstallCommand,
|
||||||
getUninstallCommand,
|
getUninstallCommand,
|
||||||
getGetCommand,
|
getGetCommand,
|
||||||
@@ -110,6 +111,11 @@ export default class DependenceService {
|
|||||||
query: any = {},
|
query: any = {},
|
||||||
): Promise<Dependence[]> {
|
): Promise<Dependence[]> {
|
||||||
let condition = query;
|
let condition = query;
|
||||||
|
const dependenceType =
|
||||||
|
type && DependenceTypes[type] !== undefined
|
||||||
|
? DependenceTypes[type]
|
||||||
|
: undefined;
|
||||||
|
await this.refreshInstalledStatuses(dependenceType);
|
||||||
if (type && DependenceTypes[type] !== undefined) {
|
if (type && DependenceTypes[type] !== undefined) {
|
||||||
condition.type = DependenceTypes[type];
|
condition.type = DependenceTypes[type];
|
||||||
}
|
}
|
||||||
@@ -132,6 +138,69 @@ export default class DependenceService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async refreshInstalledStatuses(type?: DependenceTypes) {
|
||||||
|
const cacheDependenceTypes = [
|
||||||
|
DependenceTypes.nodejs,
|
||||||
|
DependenceTypes.python3,
|
||||||
|
];
|
||||||
|
if (type !== undefined && !cacheDependenceTypes.includes(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await DependenceModel.findAll({
|
||||||
|
where: {
|
||||||
|
status: DependenceStatus.installed,
|
||||||
|
type: type === undefined ? { [Op.in]: cacheDependenceTypes } : type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const checks = await concurrentRun(
|
||||||
|
docs.map((doc) => async () => {
|
||||||
|
return (await this.isDependenceInstalled(doc)) ? undefined : doc.id;
|
||||||
|
}),
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
const missingIds = (checks || []).filter(
|
||||||
|
(id): id is number => id !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingIds.length) {
|
||||||
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.installFailed },
|
||||||
|
{ where: { id: missingIds } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isDependenceInstalled(dependency: Dependence) {
|
||||||
|
let depName = dependency.name.trim();
|
||||||
|
const depVersionStr = versionDependenceCommandTypes[dependency.type];
|
||||||
|
let depVersion = '';
|
||||||
|
if (depName.includes(depVersionStr)) {
|
||||||
|
const symbolRegx = new RegExp(
|
||||||
|
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
|
||||||
|
);
|
||||||
|
const [, parsedName, parsedVersion] = depName.match(symbolRegx) || [];
|
||||||
|
if (parsedVersion && parsedName) {
|
||||||
|
depName = parsedName;
|
||||||
|
depVersion = parsedVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const depInfo = (
|
||||||
|
await promiseExecSuccess(getGetCommand(dependency.type, depName))
|
||||||
|
)
|
||||||
|
.replace(/\s{2,}/, ' ')
|
||||||
|
.replace(/\s+$/, '');
|
||||||
|
const nameMatches =
|
||||||
|
(dependency.type === DependenceTypes.nodejs &&
|
||||||
|
depInfo.split(' ')?.[0] === depName) ||
|
||||||
|
dependency.type === DependenceTypes.python3;
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
depInfo && nameMatches && (!depVersion || depInfo.includes(depVersion)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public installDependenceOneByOne(
|
public installDependenceOneByOne(
|
||||||
docs: Dependence[],
|
docs: Dependence[],
|
||||||
isInstall: boolean = true,
|
isInstall: boolean = true,
|
||||||
|
|||||||
+11
-3
@@ -609,10 +609,18 @@ export default class SystemService {
|
|||||||
if (!type || !['node', 'python3'].includes(type)) {
|
if (!type || !['node', 'python3'].includes(type)) {
|
||||||
return { code: 400, message: t('参数错误') };
|
return { code: 400, message: t('参数错误') };
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const finalPath = path.join(config.dependenceCachePath, type);
|
const finalPath = path.join(config.dependenceCachePath, type);
|
||||||
await fs.promises.rm(finalPath, { recursive: true });
|
await fs.promises.rm(finalPath, { recursive: true, force: true });
|
||||||
} catch (error) { }
|
await DependenceModel.update(
|
||||||
|
{ status: DependenceStatus.installFailed },
|
||||||
|
{
|
||||||
|
where: {
|
||||||
|
type:
|
||||||
|
type === 'node' ? DependenceTypes.nodejs : DependenceTypes.python3,
|
||||||
|
status: DependenceStatus.installed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
return { code: 200 };
|
return { code: 200 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -65,7 +65,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sqlite3": "npm:@whyour/sqlite3@1.1.0",
|
"sqlite3": "npm:@whyour/sqlite3@1.1.2",
|
||||||
"@codemirror/state": "6.5.4",
|
"@codemirror/state": "6.5.4",
|
||||||
"@codemirror/view": "6.39.16"
|
"@codemirror/view": "6.39.16"
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
"request-ip": "3.3.0",
|
"request-ip": "3.3.0",
|
||||||
"sequelize": "^6.37.5",
|
"sequelize": "^6.37.5",
|
||||||
"sockjs": "^0.3.24",
|
"sockjs": "^0.3.24",
|
||||||
"sqlite3": "npm:@whyour/sqlite3@1.1.0",
|
"sqlite3": "npm:@whyour/sqlite3@1.1.2",
|
||||||
"toad-scheduler": "^3.0.1",
|
"toad-scheduler": "^3.0.1",
|
||||||
"typedi": "^0.10.0",
|
"typedi": "^0.10.0",
|
||||||
"undici": "^7.9.0",
|
"undici": "^7.9.0",
|
||||||
|
|||||||
Generated
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
lockfileVersion: '6.0'
|
lockfileVersion: '6.0'
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
sqlite3: npm:@whyour/sqlite3@1.1.0
|
sqlite3: npm:@whyour/sqlite3@1.1.2
|
||||||
'@codemirror/state': 6.5.4
|
'@codemirror/state': 6.5.4
|
||||||
'@codemirror/view': 6.39.16
|
'@codemirror/view': 6.39.16
|
||||||
|
|
||||||
@@ -116,13 +116,13 @@ dependencies:
|
|||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
sequelize:
|
sequelize:
|
||||||
specifier: ^6.37.5
|
specifier: ^6.37.5
|
||||||
version: 6.37.5(@whyour/sqlite3@1.1.0)
|
version: 6.37.5(@whyour/sqlite3@1.1.2)
|
||||||
sockjs:
|
sockjs:
|
||||||
specifier: ^0.3.24
|
specifier: ^0.3.24
|
||||||
version: 0.3.24
|
version: 0.3.24
|
||||||
sqlite3:
|
sqlite3:
|
||||||
specifier: npm:@whyour/sqlite3@1.1.0
|
specifier: npm:@whyour/sqlite3@1.1.2
|
||||||
version: /@whyour/sqlite3@1.1.0
|
version: /@whyour/sqlite3@1.1.2
|
||||||
toad-scheduler:
|
toad-scheduler:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
@@ -3190,7 +3190,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-Ngs9jhElXN7efS9WvvCC/p6rXMbihna8eNLVBc421Zf+VcFd+pR4DOcS6yA9V22EKtAy4DEj7LtvEEIm0bb80A==}
|
resolution: {integrity: sha512-Ngs9jhElXN7efS9WvvCC/p6rXMbihna8eNLVBc421Zf+VcFd+pR4DOcS6yA9V22EKtAy4DEj7LtvEEIm0bb80A==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
dependencies:
|
dependencies:
|
||||||
sqlite3: /@whyour/sqlite3@1.1.0
|
sqlite3: /@whyour/sqlite3@1.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -5614,8 +5614,8 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@whyour/sqlite3@1.1.0:
|
/@whyour/sqlite3@1.1.2:
|
||||||
resolution: {integrity: sha512-5g9xsjkv9sUepCvBBKmev6JSKSKjp0ebpW25uSsuV5kurIYrd7AnkbffNMNIswxwsQJSLPewVEX5ROsQfETfnA==}
|
resolution: {integrity: sha512-OY24/OJMqLHIGolo4GCFW4lrJKAoOB+ZqERXb7u7DKZQz7QR3f0GALE99BZWQBSZEVVZ/StVeAb2fCkMfTtuDQ==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
node-gyp:
|
node-gyp:
|
||||||
@@ -13939,7 +13939,7 @@ packages:
|
|||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/sequelize@6.37.5(@whyour/sqlite3@1.1.0):
|
/sequelize@6.37.5(@whyour/sqlite3@1.1.2):
|
||||||
resolution: {integrity: sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==}
|
resolution: {integrity: sha512-10WA4poUb3XWnUROThqL2Apq9C2NhyV1xHPMZuybNMCucDsbbFuKg51jhmyvvAUyUqCiimwTZamc3AHhMoBr2Q==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -13984,7 +13984,7 @@ packages:
|
|||||||
retry-as-promised: 7.0.4
|
retry-as-promised: 7.0.4
|
||||||
semver: 7.6.3
|
semver: 7.6.3
|
||||||
sequelize-pool: 7.1.0
|
sequelize-pool: 7.1.0
|
||||||
sqlite3: /@whyour/sqlite3@1.1.0
|
sqlite3: /@whyour/sqlite3@1.1.2
|
||||||
toposort-class: 1.0.1
|
toposort-class: 1.0.1
|
||||||
uuid: 8.3.2
|
uuid: 8.3.2
|
||||||
validator: 13.12.0
|
validator: 13.12.0
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
test('dependency listing marks cache entries missing from disk for reinstall', async (t) => {
|
||||||
|
const moduleStubs = new Map();
|
||||||
|
const stubModule = (modulePath, exports) => {
|
||||||
|
const resolved = require.resolve(modulePath);
|
||||||
|
moduleStubs.set(resolved, require.cache[resolved]);
|
||||||
|
require.cache[resolved] = {
|
||||||
|
id: resolved,
|
||||||
|
filename: resolved,
|
||||||
|
loaded: true,
|
||||||
|
exports,
|
||||||
|
children: [],
|
||||||
|
paths: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const DependenceStatus = {
|
||||||
|
installing: 0,
|
||||||
|
installed: 1,
|
||||||
|
installFailed: 2,
|
||||||
|
};
|
||||||
|
const DependenceTypes = { nodejs: 0, python3: 1, linux: 2 };
|
||||||
|
const docs = [
|
||||||
|
{ id: 1, name: 'missing-package', type: 0, status: 1 },
|
||||||
|
{ id: 2, name: 'present-package', type: 0, status: 1 },
|
||||||
|
];
|
||||||
|
const updates = [];
|
||||||
|
const DependenceModel = {
|
||||||
|
findAll: async ({ where }) => {
|
||||||
|
if (where.status === DependenceStatus.installed) {
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
return docs.map((doc) => ({
|
||||||
|
...doc,
|
||||||
|
status: updates.some((update) => update.ids.includes(doc.id))
|
||||||
|
? DependenceStatus.installFailed
|
||||||
|
: doc.status,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
update: async ({ status }, { where }) => {
|
||||||
|
updates.push({ status, ids: where.id });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
stubModule('../../back/config', {
|
||||||
|
__esModule: true,
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
stubModule('../../back/data/dependence', {
|
||||||
|
Dependence: class Dependence {},
|
||||||
|
DependenceModel,
|
||||||
|
DependenceStatus,
|
||||||
|
DependenceTypes,
|
||||||
|
versionDependenceCommandTypes: { 0: '@', 1: '==', 2: '=' },
|
||||||
|
});
|
||||||
|
stubModule('../../back/config/util', {
|
||||||
|
concurrentRun: async (tasks) => Promise.all(tasks.map((task) => task())),
|
||||||
|
detectOS: async () => 'Alpine',
|
||||||
|
fileExist: async () => false,
|
||||||
|
getGetCommand: (_type, name) => `check:${name}`,
|
||||||
|
getInstallCommand: () => '',
|
||||||
|
getPid: async () => 0,
|
||||||
|
getUninstallCommand: () => '',
|
||||||
|
killTask: async () => {},
|
||||||
|
promiseExecSuccess: async (command) =>
|
||||||
|
command === 'check:present-package' ? 'present-package 1.0.0\n' : '',
|
||||||
|
});
|
||||||
|
stubModule('../../back/config/const', {
|
||||||
|
LINUX_DEPENDENCE_COMMAND: { Alpine: {} },
|
||||||
|
});
|
||||||
|
stubModule('../../back/shared/pLimit', {
|
||||||
|
__esModule: true,
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
stubModule('../../back/shared/i18n', {
|
||||||
|
t: (message) => message,
|
||||||
|
tf: (message) => message,
|
||||||
|
});
|
||||||
|
stubModule('../../back/services/sock', {
|
||||||
|
__esModule: true,
|
||||||
|
default: class SockService {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const servicePath = require.resolve('../../back/services/dependence');
|
||||||
|
const originalService = require.cache[servicePath];
|
||||||
|
delete require.cache[servicePath];
|
||||||
|
t.after(() => {
|
||||||
|
if (originalService) {
|
||||||
|
require.cache[servicePath] = originalService;
|
||||||
|
} else {
|
||||||
|
delete require.cache[servicePath];
|
||||||
|
}
|
||||||
|
for (const [resolved, original] of moduleStubs) {
|
||||||
|
if (original) {
|
||||||
|
require.cache[resolved] = original;
|
||||||
|
} else {
|
||||||
|
delete require.cache[resolved];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const DependenceService = require('../../back/services/dependence').default;
|
||||||
|
const service = new DependenceService({}, {});
|
||||||
|
const result = await service.dependencies({
|
||||||
|
searchValue: '',
|
||||||
|
type: 'nodejs',
|
||||||
|
status: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(updates, [
|
||||||
|
{ status: DependenceStatus.installFailed, ids: [1] },
|
||||||
|
]);
|
||||||
|
assert.deepEqual(
|
||||||
|
result.map(({ id, status }) => ({ id, status })),
|
||||||
|
[
|
||||||
|
{ id: 1, status: DependenceStatus.installFailed },
|
||||||
|
{ id: 2, status: DependenceStatus.installed },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.dependencies({ searchValue: '', type: 'linux', status: '' });
|
||||||
|
assert.equal(updates.length, 1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
function mockModule(modulePath, exports) {
|
||||||
|
const filename = require.resolve(modulePath);
|
||||||
|
require.cache[filename] = {
|
||||||
|
id: filename,
|
||||||
|
filename,
|
||||||
|
loaded: true,
|
||||||
|
exports,
|
||||||
|
children: [],
|
||||||
|
paths: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mockModule('../../back/config', {
|
||||||
|
__esModule: true,
|
||||||
|
default: {
|
||||||
|
bakPath: '/tmp',
|
||||||
|
blackFileList: [],
|
||||||
|
configPath: '/tmp',
|
||||||
|
logPath: '/tmp',
|
||||||
|
logs: { level: 'info' },
|
||||||
|
rootPath: '/tmp',
|
||||||
|
scriptPath: '/tmp',
|
||||||
|
systemLogPath: '/tmp',
|
||||||
|
writePathList: ['/tmp'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockModule('../../back/shared/i18n', {
|
||||||
|
t: (message) => message,
|
||||||
|
});
|
||||||
|
mockModule('../../back/config/util', {
|
||||||
|
fileExist: async () => false,
|
||||||
|
readDir: async () => [],
|
||||||
|
readDirs: async () => [],
|
||||||
|
removeAnsi: (content) => content,
|
||||||
|
rmPath: async () => {},
|
||||||
|
});
|
||||||
|
mockModule('../../back/shared/utils', {
|
||||||
|
writeFileWithLock: async () => {},
|
||||||
|
});
|
||||||
|
for (const service of ['config', 'script', 'log']) {
|
||||||
|
mockModule(`../../back/services/${service}`, {
|
||||||
|
__esModule: true,
|
||||||
|
default: class {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
mockModule('../../back/data/runningInstance', {
|
||||||
|
InstanceStatus: { running: 'running' },
|
||||||
|
RunningInstanceModel: { findOne: async () => null },
|
||||||
|
});
|
||||||
|
|
||||||
|
const deprecatedRoutes = [
|
||||||
|
['config', '/configs/detail'],
|
||||||
|
['script', '/scripts/detail'],
|
||||||
|
['log', '/logs/detail'],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [moduleName, replacement] of deprecatedRoutes) {
|
||||||
|
test(`${moduleName} filename route points callers to its detail API`, () => {
|
||||||
|
const app = express.Router();
|
||||||
|
require(`../../back/api/${moduleName}`).default(app);
|
||||||
|
const router = app.stack.find((layer) => layer.name === 'router').handle;
|
||||||
|
const deprecatedRoute = router.stack.find(
|
||||||
|
(layer) => layer.route?.path === '/:file',
|
||||||
|
);
|
||||||
|
const handler = deprecatedRoute.route.stack.at(-1).handle;
|
||||||
|
let responseBody;
|
||||||
|
|
||||||
|
handler(
|
||||||
|
{ params: { file: 'Example.js' }, query: {} },
|
||||||
|
{ send: (body) => (responseBody = body) },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(responseBody, {
|
||||||
|
code: 410,
|
||||||
|
message: `接口已下线,请使用 ${replacement} 接口`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
const protectedPathCase = require(
|
||||||
|
'../../back/middlewares/protectedPathCase',
|
||||||
|
).default;
|
||||||
|
|
||||||
|
function runMiddleware(path) {
|
||||||
|
let status;
|
||||||
|
let body;
|
||||||
|
let nextCalled = false;
|
||||||
|
const response = {
|
||||||
|
status(value) {
|
||||||
|
status = value;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
json(value) {
|
||||||
|
body = value;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
protectedPathCase({ path }, response, () => {
|
||||||
|
nextCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status, body, nextCalled };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('rejects case variations under protected API namespaces', () => {
|
||||||
|
for (const path of [
|
||||||
|
'/Api/configs/detail',
|
||||||
|
'/api/Configs/detail',
|
||||||
|
'/OPEN/scripts/detail',
|
||||||
|
'/open/scripts/Detail',
|
||||||
|
]) {
|
||||||
|
assert.deepEqual(runMiddleware(path), {
|
||||||
|
status: 400,
|
||||||
|
body: { code: 400, message: 'Invalid path format' },
|
||||||
|
nextCalled: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows normalized protected paths and unrelated paths', () => {
|
||||||
|
for (const path of [
|
||||||
|
'/api/configs/detail',
|
||||||
|
'/open/scripts/detail',
|
||||||
|
'/OpenApi/status',
|
||||||
|
'/assets/AppBundle.js',
|
||||||
|
]) {
|
||||||
|
assert.deepEqual(runMiddleware(path), {
|
||||||
|
status: undefined,
|
||||||
|
body: undefined,
|
||||||
|
nextCalled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs/promises');
|
||||||
|
const os = require('node:os');
|
||||||
|
const path = require('node:path');
|
||||||
|
const test = require('node:test');
|
||||||
|
|
||||||
|
test('clearing a dependency cache marks installed entries for reinstall', async (t) => {
|
||||||
|
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-dependence-'));
|
||||||
|
const nodeCache = path.join(directory, 'node');
|
||||||
|
await fs.mkdir(nodeCache);
|
||||||
|
await fs.writeFile(path.join(nodeCache, 'package'), 'cached');
|
||||||
|
t.after(() => fs.rm(directory, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const moduleStubs = new Map();
|
||||||
|
const stubModule = (modulePath, exports) => {
|
||||||
|
const resolved = require.resolve(modulePath);
|
||||||
|
moduleStubs.set(resolved, require.cache[resolved]);
|
||||||
|
require.cache[resolved] = {
|
||||||
|
id: resolved,
|
||||||
|
filename: resolved,
|
||||||
|
loaded: true,
|
||||||
|
exports,
|
||||||
|
children: [],
|
||||||
|
paths: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const updates = [];
|
||||||
|
const DependenceStatus = { installed: 1, installFailed: 2 };
|
||||||
|
const DependenceTypes = { nodejs: 0, python3: 1 };
|
||||||
|
|
||||||
|
stubModule('../../back/config', {
|
||||||
|
__esModule: true,
|
||||||
|
default: { dependenceCachePath: directory },
|
||||||
|
});
|
||||||
|
stubModule('../../back/config/const', {
|
||||||
|
NotificationModeStringMap: {},
|
||||||
|
TASK_COMMAND: 'task',
|
||||||
|
});
|
||||||
|
stubModule('../../back/config/util', {
|
||||||
|
getPid: async () => 0,
|
||||||
|
killTask: async () => {},
|
||||||
|
parseContentVersion: () => '',
|
||||||
|
parseVersion: () => '',
|
||||||
|
promiseExec: async () => '',
|
||||||
|
readDirs: async () => [],
|
||||||
|
rmPath: async () => {},
|
||||||
|
setSystemTimezone: async () => true,
|
||||||
|
updateLinuxMirrorFile: async () => {},
|
||||||
|
});
|
||||||
|
stubModule('../../back/data/dependence', {
|
||||||
|
DependenceModel: {
|
||||||
|
update: async (values, options) => updates.push({ values, options }),
|
||||||
|
},
|
||||||
|
DependenceStatus,
|
||||||
|
DependenceTypes,
|
||||||
|
});
|
||||||
|
stubModule('../../back/data/notify', {});
|
||||||
|
stubModule('../../back/data/system', {
|
||||||
|
AuthDataType: {},
|
||||||
|
SystemModel: {},
|
||||||
|
});
|
||||||
|
stubModule('../../back/shared/pLimit', {
|
||||||
|
__esModule: true,
|
||||||
|
default: {},
|
||||||
|
});
|
||||||
|
stubModule('../../back/shared/i18n', {
|
||||||
|
setLang: () => {},
|
||||||
|
t: (message) => message,
|
||||||
|
});
|
||||||
|
stubModule('../../back/services/notify', {
|
||||||
|
__esModule: true,
|
||||||
|
default: class NotificationService {},
|
||||||
|
});
|
||||||
|
stubModule('../../back/services/schedule', {
|
||||||
|
__esModule: true,
|
||||||
|
default: class ScheduleService {},
|
||||||
|
});
|
||||||
|
stubModule('../../back/services/sock', {
|
||||||
|
__esModule: true,
|
||||||
|
default: class SockService {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const servicePath = require.resolve('../../back/services/system');
|
||||||
|
const originalService = require.cache[servicePath];
|
||||||
|
delete require.cache[servicePath];
|
||||||
|
t.after(() => {
|
||||||
|
if (originalService) {
|
||||||
|
require.cache[servicePath] = originalService;
|
||||||
|
} else {
|
||||||
|
delete require.cache[servicePath];
|
||||||
|
}
|
||||||
|
for (const [resolved, original] of moduleStubs) {
|
||||||
|
if (original) {
|
||||||
|
require.cache[resolved] = original;
|
||||||
|
} else {
|
||||||
|
delete require.cache[resolved];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const SystemService = require('../../back/services/system').default;
|
||||||
|
const service = new SystemService({}, {}, {});
|
||||||
|
assert.deepEqual(await service.cleanDependence('node'), { code: 200 });
|
||||||
|
assert.deepEqual(await service.cleanDependence('python3'), { code: 200 });
|
||||||
|
await assert.rejects(fs.stat(nodeCache), { code: 'ENOENT' });
|
||||||
|
assert.deepEqual(updates, [
|
||||||
|
{
|
||||||
|
values: { status: DependenceStatus.installFailed },
|
||||||
|
options: {
|
||||||
|
where: {
|
||||||
|
type: DependenceTypes.nodejs,
|
||||||
|
status: DependenceStatus.installed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
values: { status: DependenceStatus.installFailed },
|
||||||
|
options: {
|
||||||
|
where: {
|
||||||
|
type: DependenceTypes.python3,
|
||||||
|
status: DependenceStatus.installed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user