server: allow restriction on file operations by paths

Bug: Flood-UI/flood#588
This commit is contained in:
Jesse Chan
2020-08-26 21:01:24 +08:00
parent 10b75b4f74
commit 3869f90fcc
6 changed files with 83 additions and 22 deletions
+5
View File
@@ -68,6 +68,10 @@ const {argv} = require('yargs')
hidden: true,
type: 'string',
})
.option('allowedpath', {
describe: 'Allowed path for file operations, can be called multiple times',
type: 'string',
})
.option('dbclean', {
default: 1000 * 60 * 60,
describe: 'ADVANCED: Interval between database purge',
@@ -144,6 +148,7 @@ const CONFIG = {
ssl: argv.ssl,
sslKey: argv.sslkey || path.resolve(path.join(argv.rundir, 'key.pem')),
sslCert: argv.sslcert || path.resolve(path.join(argv.rundir, 'fullchain.pem')),
allowedPaths: argv.allowedpath ? [].concat(argv.allowedpath) : null,
};
module.exports = CONFIG;
+3 -1
View File
@@ -67,7 +67,9 @@ const CONFIG = {
// watchMountPoints: [
// "/mnt/disk"
// ]
}
},
// Allowed paths for file operations
// allowedPaths: ['/mnt/download', '/data/download'],
};
// Do not remove the below line.
module.exports = CONFIG;
-11
View File
@@ -67,17 +67,6 @@ class ClientRequest {
}
}
// TODO: Move this to util, doesn't belong here
createDirectory(options) {
if (options.path) {
fs.mkdir(options.path, {recursive: true}, (error) => {
if (error) {
console.trace('Error creating directory.', error);
}
});
}
}
clearRequestQueue() {
this.requests = [];
}
+12 -4
View File
@@ -2,15 +2,23 @@ const fs = require('fs');
const os = require('os');
const path = require('path');
const fileUtil = require('../util/fileUtil');
const getDirectoryList = (options, callback) => {
const sourcePath = (options.path || '/').replace(/^~/, os.homedir());
const resolvedPath = path.resolve(sourcePath);
if (!fileUtil.isAllowedPath(resolvedPath)) {
callback(null, fileUtil.accessDeniedError());
return;
}
try {
const directories = [];
const files = [];
fs.readdirSync(sourcePath).forEach((item) => {
const joinedPath = path.join(sourcePath, item);
fs.readdirSync(resolvedPath).forEach((item) => {
const joinedPath = path.join(resolvedPath, item);
if (fs.existsSync(joinedPath)) {
if (fs.statSync(joinedPath).isDirectory()) {
directories.push(item);
@@ -20,13 +28,13 @@ const getDirectoryList = (options, callback) => {
}
});
const hasParent = /^.{0,}:?(\/|\\){1,1}\S{1,}/.test(sourcePath);
const hasParent = /^.{0,}:?(\/|\\){1,1}\S{1,}/.test(resolvedPath);
callback({
directories,
files,
hasParent,
path: sourcePath,
path: resolvedPath,
separator: path.sep,
});
} catch (error) {
+24 -6
View File
@@ -6,6 +6,7 @@ const tar = require('tar-stream');
const ClientRequest = require('./ClientRequest');
const clientResponseUtil = require('../util/clientResponseUtil');
const clientSettingsMap = require('../../shared/constants/clientSettingsMap');
const fileUtil = require('../util/fileUtil');
const settings = require('./settings');
const torrentFilePropsMap = require('../../shared/constants/torrentFilePropsMap');
const torrentPeerPropsMap = require('../../shared/constants/torrentPeerPropsMap');
@@ -23,7 +24,13 @@ const client = {
tags = tags.split(',');
}
request.createDirectory({path: destinationPath});
const resolvedPath = path.resolve(destinationPath);
if (!fileUtil.isAllowedPath(resolvedPath)) {
callback(null, fileUtil.accessDeniedError());
return;
}
fileUtil.createDirectory({path: resolvedPath});
request.send();
// Each torrent is sent individually because rTorrent accepts a total
@@ -35,7 +42,7 @@ const client = {
const fileRequest = new ClientRequest(user, services);
fileRequest.addFiles({
files: file,
path: destinationPath,
path: resolvedPath,
isBasePath,
start,
tags,
@@ -58,10 +65,15 @@ const client = {
addUrls(user, services, data, callback) {
const {urls, destination, isBasePath, start, tags} = data;
const request = new ClientRequest(user, services);
request.createDirectory({path: destination});
const resolvedPath = path.resolve(destination);
if (!fileUtil.isAllowedPath(resolvedPath)) {
callback(null, fileUtil.accessDeniedError());
return;
}
fileUtil.createDirectory({path: resolvedPath});
request.addURLs({
urls,
path: destination,
path: resolvedPath,
isBasePath,
start,
tags,
@@ -230,6 +242,12 @@ const client = {
const {isBasePath, hashes, filenames, moveFiles, sourcePaths, isCheckHash} = data;
const mainRequest = new ClientRequest(user, services);
const resolvedPath = path.resolve(destinationPath);
if (!fileUtil.isAllowedPath(resolvedPath)) {
callback(null, fileUtil.accessDeniedError());
return;
}
const hashesToRestart = hashes.filter(
(hash) => !services.torrentService.getTorrent(hash).status.includes(torrentStatusMap.stopped),
);
@@ -266,7 +284,7 @@ const client = {
moveTorrentsRequest.moveTorrents({
filenames,
sourcePaths,
destinationPath,
resolvedPath,
});
};
@@ -277,7 +295,7 @@ const client = {
}
mainRequest.stopTorrents({hashes});
mainRequest.setDownloadPath({hashes, path: destinationPath, isBasePath});
mainRequest.setDownloadPath({hashes, path: resolvedPath, isBasePath});
mainRequest.onComplete(afterSetPath);
mainRequest.send();
},
+39
View File
@@ -0,0 +1,39 @@
const fs = require('fs');
const config = require('../../config');
const createDirectory = (options) => {
if (options.path) {
fs.mkdir(options.path, {recursive: true}, (error) => {
if (error) {
console.trace('Error creating directory.', error);
}
});
}
};
const isAllowedPath = (resolvedPath) => {
if (config.allowedPaths == null) {
return true;
}
return config.allowedPaths.some((allowedPath) => {
if (resolvedPath.startsWith(allowedPath)) {
return true;
}
return false;
});
};
const accessDeniedError = () => {
const error = new Error();
error.code = 'EACCES';
return error;
};
const fileUtil = {
createDirectory,
isAllowedPath,
accessDeniedError,
};
module.exports = fileUtil;