diff --git a/server/models/Filesystem.js b/server/models/Filesystem.js deleted file mode 100644 index 63ba3744..00000000 --- a/server/models/Filesystem.js +++ /dev/null @@ -1,45 +0,0 @@ -import fs from 'fs'; -import os from 'os'; -import path from 'path'; - -import fileUtil from '../util/fileUtil'; - -const getDirectoryList = (options, callback) => { - const sourcePath = (options.path || '/').replace(/^~/, os.homedir()); - - const resolvedPath = fileUtil.sanitizePath(sourcePath); - if (!fileUtil.isAllowedPath(resolvedPath)) { - callback(null, fileUtil.accessDeniedError()); - return; - } - - try { - const directories = []; - const files = []; - - fs.readdirSync(resolvedPath).forEach((item) => { - const joinedPath = path.join(resolvedPath, item); - if (fs.existsSync(joinedPath)) { - if (fs.statSync(joinedPath).isDirectory()) { - directories.push(item); - } else { - files.push(item); - } - } - }); - - const hasParent = /^.{0,}:?(\/|\\){1,1}\S{1,}/.test(resolvedPath); - - callback({ - directories, - files, - hasParent, - path: resolvedPath, - separator: path.sep, - }); - } catch (error) { - callback(null, error); - } -}; - -export default {getDirectoryList}; diff --git a/server/models/client.js b/server/models/client.js index 22d6e201..30d162aa 100644 --- a/server/models/client.js +++ b/server/models/client.js @@ -4,10 +4,10 @@ import sanitize from 'sanitize-filename'; import {series} from 'async'; import tar from 'tar-stream'; +import {accessDeniedError, createDirectory, isAllowedPath, sanitizePath} from '../util/fileUtil'; import ClientRequest from './ClientRequest'; import clientResponseUtil from '../util/clientResponseUtil'; import {clientSettingsBiMap} from '../../shared/constants/clientSettingsMap'; -import fileUtil from '../util/fileUtil'; import settings from './settings'; import torrentFilePropsMap from '../../shared/constants/torrentFilePropsMap'; import torrentPeerPropsMap from '../../shared/constants/torrentPeerPropsMap'; @@ -18,13 +18,13 @@ const client = { addFiles(user, services, options, callback) { const {destination: destinationPath, files, isBasePath, start, tags} = options; - const resolvedPath = fileUtil.sanitizePath(destinationPath); - if (!fileUtil.isAllowedPath(resolvedPath)) { - callback(null, fileUtil.accessDeniedError()); + const resolvedPath = sanitizePath(destinationPath); + if (!isAllowedPath(resolvedPath)) { + callback(null, accessDeniedError()); return; } - fileUtil.createDirectory({path: resolvedPath}); + createDirectory({path: resolvedPath}); // Each torrent is sent individually because rTorrent accepts a total // filesize of 524 kilobytes or less. This allows the user to send many @@ -56,12 +56,12 @@ const client = { addUrls(user, services, data, callback) { const {urls, destination, isBasePath, start, tags} = data; const request = new ClientRequest(user, services); - const resolvedPath = fileUtil.sanitizePath(destination); - if (!fileUtil.isAllowedPath(resolvedPath)) { - callback(null, fileUtil.accessDeniedError()); + const resolvedPath = sanitizePath(destination); + if (!isAllowedPath(resolvedPath)) { + callback(null, accessDeniedError()); return; } - fileUtil.createDirectory({path: resolvedPath}); + createDirectory({path: resolvedPath}); request.addURLs({ urls, path: resolvedPath, diff --git a/server/routes/api/index.ts b/server/routes/api/index.ts index cf38517c..347d0af5 100644 --- a/server/routes/api/index.ts +++ b/server/routes/api/index.ts @@ -1,8 +1,6 @@ import express from 'express'; import passport from 'passport'; -import type {Request} from 'express'; - import type {HistorySnapshot} from '@shared/constants/historySnapshotTypes'; import type {NotificationFetchOptions} from '@shared/types/Notification'; @@ -12,7 +10,7 @@ import clientRoutes from './client'; import clientActivityStream from '../../middleware/clientActivityStream'; import eventStream from '../../middleware/eventStream'; import feedMonitorRoutes from './feed-monitor'; -import Filesystem from '../../models/Filesystem'; +import {getDirectoryList} from '../../util/fileUtil'; import settings from '../../models/settings'; import torrentsRoutes from './torrents'; @@ -28,15 +26,22 @@ router.use('/torrents', torrentsRoutes); router.get('/activity-stream', eventStream, clientActivityStream); -router.get('/directory-list', (req, res) => { - Filesystem.getDirectoryList(req.query, ajaxUtil.getResponseFn(res)); +router.get('/directory-list', (req, res) => { + const callback = ajaxUtil.getResponseFn(res); + getDirectoryList(req.query) + .then((data) => { + callback(data); + }) + .catch((error) => { + callback(null, error); + }); }); -router.get('/history', (req: Request, res) => { +router.get('/history', (req, res) => { req.services?.historyService.getHistory(req.query, ajaxUtil.getResponseFn(res)); }); -router.get('/notifications', (req: Request, res) => { +router.get('/notifications', (req, res) => { req.services?.notificationService.getNotifications(req.query, ajaxUtil.getResponseFn(res)); }); diff --git a/server/services/clientGatewayService.ts b/server/services/clientGatewayService.ts index d87f58c0..def8e8ed 100644 --- a/server/services/clientGatewayService.ts +++ b/server/services/clientGatewayService.ts @@ -14,9 +14,9 @@ import type { StopTorrentsOptions, } from '@shared/types/Action'; +import {accessDeniedError, isAllowedPath, sanitizePath} from '../util/fileUtil'; import BaseService from './BaseService'; import fileListMethodCallConfigs from '../constants/fileListMethodCallConfigs'; -import fileUtil from '../util/fileUtil'; import scgiUtil from '../util/scgiUtil'; import type {MethodCallConfigs, MultiMethodCalls} from '../constants/rTorrentMethodCall'; @@ -100,9 +100,9 @@ class ClientGatewayService extends BaseService { * @return {Promise} - Resolves with the processed client response or rejects with the processed client error. */ async moveTorrents({hashes, destination, moveFiles, isBasePath, isCheckHash}: MoveTorrentsOptions) { - const resolvedPath = fileUtil.sanitizePath(destination); - if (!fileUtil.isAllowedPath(resolvedPath)) { - return Promise.reject(fileUtil.accessDeniedError()); + const resolvedPath = sanitizePath(destination); + if (!isAllowedPath(resolvedPath)) { + throw accessDeniedError(); } const hashesToRestart: Array = []; @@ -140,7 +140,7 @@ class ClientGatewayService extends BaseService { return; } - const destinationFilePath = fileUtil.sanitizePath(path.join(resolvedPath, baseFileName)); + const destinationFilePath = sanitizePath(path.join(resolvedPath, baseFileName)); if (sourceBasePath !== destinationFilePath) { try { moveSync(sourceBasePath, destinationFilePath, {overwrite: true}); diff --git a/server/util/fileUtil.ts b/server/util/fileUtil.ts index 8c10ca2d..a1bf93bc 100644 --- a/server/util/fileUtil.ts +++ b/server/util/fileUtil.ts @@ -1,19 +1,10 @@ import fs from 'fs'; +import {homedir} from 'os'; import path from 'path'; import config from '../../config'; -const createDirectory = (options: {path: string}) => { - if (options.path) { - fs.mkdir(options.path, {recursive: true}, (error) => { - if (error) { - console.trace('Error creating directory.', error); - } - }); - } -}; - -const isAllowedPath = (resolvedPath: string) => { +export const isAllowedPath = (resolvedPath: string) => { if (config.allowedPaths == null) { return true; } @@ -25,23 +16,57 @@ const isAllowedPath = (resolvedPath: string) => { }); }; -const sanitizePath = (input: string) => { +export const sanitizePath = (input: string) => { // eslint-disable-next-line no-control-regex const controlRe = /[\x00-\x1f\x80-\x9f]/g; return path.resolve(input).replace(controlRe, ''); }; -const accessDeniedError = () => { +export const accessDeniedError = () => { const error = new Error() as NodeJS.ErrnoException; error.code = 'EACCES'; return error; }; -const fileUtil = { - createDirectory, - isAllowedPath, - sanitizePath, - accessDeniedError, +export const createDirectory = (options: {path: string}) => { + if (options.path) { + fs.mkdir(options.path, {recursive: true}, (error) => { + if (error) { + console.trace('Error creating directory.', error); + } + }); + } }; -export default fileUtil; +export const getDirectoryList = async (inputPath: string) => { + const sourcePath = (inputPath || '/').replace(/^~/, homedir()); + + const resolvedPath = sanitizePath(sourcePath); + if (!isAllowedPath(resolvedPath)) { + throw accessDeniedError(); + } + + const directories: Array = []; + const files: Array = []; + + fs.readdirSync(resolvedPath).forEach((item) => { + const joinedPath = path.join(resolvedPath, item); + if (fs.existsSync(joinedPath)) { + if (fs.statSync(joinedPath).isDirectory()) { + directories.push(item); + } else { + files.push(item); + } + } + }); + + const hasParent = /^.{0,}:?(\/|\\){1,1}\S{1,}/.test(resolvedPath); + + return { + directories, + files, + hasParent, + path: resolvedPath, + separator: path.sep, + }; +};