diff --git a/back/api/script.ts b/back/api/script.ts index 8b1faaad..9be941d6 100644 --- a/back/api/script.ts +++ b/back/api/script.ts @@ -12,6 +12,11 @@ import multer from 'multer'; import { writeFileWithLock } from '../shared/utils'; const route = Router(); +function isPathAllowed(targetPath: string): boolean { + const resolved = path.resolve(targetPath); + return config.writePathList.some((x) => resolved.startsWith(x)); +} + const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, config.scriptPath); @@ -161,24 +166,32 @@ export default (app: Router) => { } if (req.file) { - await fs.rename(req.file.path, join(path, filename)); + const uploadPath = join(path, filename); + if (!isPathAllowed(uploadPath)) { + return res.send({ code: 403, message: t('暂无权限') }); + } + await fs.rename(req.file.path, uploadPath); return res.send({ code: 200 }); } if (directory) { - await fs.mkdir(join(path, directory), { recursive: true }); + const dirPath = join(path, directory); + if (!isPathAllowed(dirPath)) { + return res.send({ code: 403, message: t('暂无权限') }); + } + await fs.mkdir(dirPath, { recursive: true }); return res.send({ code: 200 }); } if (!originFilename) { originFilename = filename; } - const originFilePath = join( - path, - `${originFilename.replace(/\//g, '')}`, - ); + const originFilePath = join(path, originFilename); + const filePath = join(path, filename); + if (!isPathAllowed(filePath) || !isPathAllowed(originFilePath)) { + return res.send({ code: 403, message: t('暂无权限') }); + } await fs.mkdir(path, { recursive: true }); - const filePath = join(path, `${filename.replace(/\//g, '')}`); const fileExists = await fileExist(filePath); if (fileExists) { await fs.copyFile( @@ -317,6 +330,9 @@ export default (app: Router) => { } const { name, ext } = parse(filename); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); + if (!isPathAllowed(filePath)) { + return res.send({ code: 403, message: t('暂无权限') }); + } await writeFileWithLock(filePath, content || ''); const scriptService = Container.get(ScriptService); @@ -345,6 +361,9 @@ export default (app: Router) => { } const { name, ext } = parse(filename); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); + if (!isPathAllowed(filePath)) { + return res.send({ code: 403, message: t('暂无权限') }); + } const logPath = join(config.logPath, path, `${name}.swap`); const scriptService = Container.get(ScriptService); @@ -380,6 +399,9 @@ export default (app: Router) => { } const filePath = join(config.scriptPath, path, filename); const newPath = join(config.scriptPath, path, newFilename); + if (!isPathAllowed(filePath) || !isPathAllowed(newPath)) { + return res.send({ code: 403, message: t('暂无权限') }); + } await fs.rename(filePath, newPath); res.send({ code: 200 }); } catch (e) { diff --git a/back/config/const.ts b/back/config/const.ts index 59350a98..8bfeff96 100644 --- a/back/config/const.ts +++ b/back/config/const.ts @@ -1,3 +1,5 @@ +import { maybeSudo } from './container'; + export const LOG_END_SYMBOL = '     '; export const TASK_COMMAND = 'task'; @@ -60,17 +62,17 @@ export const LINUX_DEPENDENCE_COMMAND: Record< } > = { Debian: { - install: 'sudo apt-get install -y', - uninstall: 'sudo apt-get remove -y', - info: 'sudo dpkg-query -s', + install: maybeSudo('apt-get install -y'), + uninstall: maybeSudo('apt-get remove -y'), + info: maybeSudo('dpkg-query -s'), check(info: string) { return info.includes('install ok installed'); }, }, Ubuntu: { - install: 'sudo apt-get install -y', - uninstall: 'sudo apt-get remove -y', - info: 'sudo dpkg-query -s', + install: maybeSudo('apt-get install -y'), + uninstall: maybeSudo('apt-get remove -y'), + info: maybeSudo('dpkg-query -s'), check(info: string) { return info.includes('install ok installed'); }, diff --git a/back/config/container.ts b/back/config/container.ts new file mode 100644 index 00000000..57539684 --- /dev/null +++ b/back/config/container.ts @@ -0,0 +1,7 @@ +export function isInContainer(): boolean { + return process.env.QL_CONTAINER === 'true'; +} + +export function maybeSudo(cmd: string): string { + return isInContainer() ? `sudo ${cmd}` : cmd; +} diff --git a/back/config/util.ts b/back/config/util.ts index 5f24dced..4a654bb7 100644 --- a/back/config/util.ts +++ b/back/config/util.ts @@ -11,6 +11,7 @@ import { writeFileWithLock } from '../shared/utils'; import { DependenceTypes } from '../data/dependence'; import { FormData } from 'undici'; import os from 'os'; +import { maybeSudo, isInContainer } from './container'; export * from './share'; @@ -557,8 +558,8 @@ export async function setSystemTimezone(timezone: string): Promise { throw new Error('Invalid timezone'); } - await promiseExec(`sudo ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`); - await promiseExec(`echo "${timezone}" | sudo tee /etc/timezone`); + await promiseExec(maybeSudo(`ln -sf /usr/share/zoneinfo/${timezone} /etc/localtime`)); + await promiseExec(`echo "${timezone}" | ${maybeSudo('tee /etc/timezone')}`); return true; } catch (error) { @@ -584,7 +585,7 @@ except: ''')"`, [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' ? `apk info -es ${name}` - : `sudo dpkg-query -s ${name}`, + : maybeSudo(`dpkg-query -s ${name}`), }; return baseCommands[type]; @@ -597,7 +598,7 @@ export function getInstallCommand(type: DependenceTypes, name: string): string { 'pip3 install --disable-pip-version-check --root-user-action=ignore', [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' ? 'apk add --no-check-certificate' - : 'sudo apt-get install -y', + : maybeSudo('apt-get install -y'), }; let command = baseCommands[type]; @@ -619,7 +620,7 @@ export function getUninstallCommand( 'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y', [DependenceTypes.linux]: getOsTypeSync() === 'Alpine' ? 'apk del' - : 'sudo apt-get remove -y', + : maybeSudo('apt-get remove -y'), }; return `${baseCommands[type]} ${name.trim()}`; @@ -732,23 +733,24 @@ async function _updateLinuxMirror( osType: string, mirrorDomainWithScheme: string, ): Promise { + const S = isInContainer() ? 'sudo ' : ''; let filePath: string, currentDomainWithScheme: string | null; switch (osType) { case 'Debian': filePath = '/etc/apt/sources.list.d/debian.sources'; currentDomainWithScheme = await getCurrentMirrorDomain(filePath); if (currentDomainWithScheme) { - return `sudo sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://deb.debian.org'}|g' ${filePath} || (sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | sudo tee ${filePath}) && sudo apt-get update`; + return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://deb.debian.org'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath}) && ${S}apt-get update`; } else { - return `sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | sudo tee ${filePath} && sudo apt-get update`; + return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://deb.debian.org'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates\\nComponents: main\\nSigned-By: /usr/share/keyrings/debian-archive-keyring.gpg" | ${S}tee ${filePath} && ${S}apt-get update`; } case 'Ubuntu': filePath = '/etc/apt/sources.list.d/ubuntu.sources'; currentDomainWithScheme = await getCurrentMirrorDomain(filePath); if (currentDomainWithScheme) { - return `sudo sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}|g' ${filePath} || (sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | sudo tee ${filePath}) && sudo apt-get update`; + return `${S}sed -i 's|${currentDomainWithScheme}|${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}|g' ${filePath} || (${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath}) && ${S}apt-get update`; } else { - return `sudo mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | sudo tee ${filePath} && sudo apt-get update`; + return `${S}mkdir -p /etc/apt/sources.list.d && echo -e "Types: deb\\nURIs: ${mirrorDomainWithScheme || 'http://archive.ubuntu.com'}\\nSuites: \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2) \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-updates \\$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2)-backports\\nComponents: main restricted universe multiverse\\nSigned-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg" | ${S}tee ${filePath} && ${S}apt-get update`; } case 'Alpine': filePath = '/etc/apk/repositories'; diff --git a/back/services/cron.ts b/back/services/cron.ts index 5a4bd774..6ecc9088 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -31,6 +31,7 @@ import { writeFileWithLock } from '../shared/utils'; import { t } from '../shared/i18n'; import { ScheduleType } from '../interface/schedule'; import { logStreamManager } from '../shared/logStreamManager'; +import { isEmpty } from 'lodash'; @Service() export default class CronService { @@ -401,7 +402,7 @@ export default class CronService { } private formatFilterQuery(query: any, filterQuery: any) { - if (filterQuery) { + if (!isEmpty(filterQuery)) { if (!query[Op.and]) { query[Op.and] = []; } diff --git a/docker/Dockerfile b/docker/Dockerfile index b874649f..f9de1f79 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -17,6 +17,7 @@ ARG PYTHON_SHORT_VERSION=3.11 ENV QL_DIR=/ql \ QL_BRANCH=${QL_BRANCH} \ + QL_CONTAINER=true \ LANG=C.UTF-8 \ SHELL=/bin/bash \ PS1="\u@\h:\w \$ " diff --git a/docker/Dockerfile.310 b/docker/Dockerfile.310 index 6312448b..30e6a332 100644 --- a/docker/Dockerfile.310 +++ b/docker/Dockerfile.310 @@ -17,6 +17,7 @@ ARG PYTHON_SHORT_VERSION=3.10 ENV QL_DIR=/ql \ QL_BRANCH=${QL_BRANCH} \ + QL_CONTAINER=true \ LANG=C.UTF-8 \ SHELL=/bin/bash \ PS1="\u@\h:\w \$ " diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index e4d791f3..ba3e455b 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -22,6 +22,7 @@ ARG PYTHON_SHORT_VERSION=3.11 ENV QL_DIR=/ql \ QL_BRANCH=${QL_BRANCH} \ + QL_CONTAINER=true \ LANG=C.UTF-8 \ SHELL=/bin/bash \ PS1="\u@\h:\w \$ " diff --git a/docker/Dockerfile.debian310 b/docker/Dockerfile.debian310 index bc110ed7..afa41e19 100644 --- a/docker/Dockerfile.debian310 +++ b/docker/Dockerfile.debian310 @@ -22,6 +22,7 @@ ARG PYTHON_SHORT_VERSION=3.10 ENV QL_DIR=/ql \ QL_BRANCH=${QL_BRANCH} \ + QL_CONTAINER=true \ LANG=C.UTF-8 \ SHELL=/bin/bash \ PS1="\u@\h:\w \$ " diff --git a/src/pages/crontab/index.tsx b/src/pages/crontab/index.tsx index 830e370a..2c933492 100644 --- a/src/pages/crontab/index.tsx +++ b/src/pages/crontab/index.tsx @@ -367,7 +367,7 @@ const Crontab = () => { const getCrons = async (silent?: boolean) => { if (!silent) setLoading(true); - const { page = 1, size = 10, sorter, filters = '{}' } = pageConf; + const { page = 1, size = 20, sorter, filters = {} } = pageConf; let url = `${config.apiPrefix }crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify( filters,