refactor(routes): Phase 3.4 - standardize browse.js
This commit is contained in:
@@ -4,8 +4,18 @@ const fsp = require('fs').promises;
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { exists, isAccessible } = require('../fs-helpers');
|
const { exists, isAccessible } = require('../fs-helpers');
|
||||||
const { paginate, parsePaginationParams } = require('../pagination');
|
const { paginate, parsePaginationParams } = require('../pagination');
|
||||||
|
const { ValidationError } = require('../errors');
|
||||||
|
|
||||||
module.exports = function(ctx) {
|
/**
|
||||||
|
* Browse route factory
|
||||||
|
* @param {Object} deps - Explicit dependencies
|
||||||
|
* @param {Function} deps.asyncHandler - Async route handler wrapper
|
||||||
|
* @param {Function} deps.validateSecurePath - Path traversal validator
|
||||||
|
* @param {Object} deps.auditLogger - Audit logger
|
||||||
|
* @param {Object} deps.docker - Docker client
|
||||||
|
* @returns {express.Router}
|
||||||
|
*/
|
||||||
|
module.exports = function({ asyncHandler, validateSecurePath, auditLogger, docker }) {
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Parse browse roots from environment
|
// Parse browse roots from environment
|
||||||
@@ -20,7 +30,7 @@ module.exports = function(ctx) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get available browse roots
|
// Get available browse roots
|
||||||
router.get('/browse/roots', ctx.asyncHandler(async (req, res) => {
|
router.get('/browse/roots', asyncHandler(async (req, res) => {
|
||||||
const allRoots = BROWSE_ROOTS.map(r => ({
|
const allRoots = BROWSE_ROOTS.map(r => ({
|
||||||
name: r.hostPath,
|
name: r.hostPath,
|
||||||
path: r.hostPath,
|
path: r.hostPath,
|
||||||
@@ -38,7 +48,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'browse-roots'));
|
}, 'browse-roots'));
|
||||||
|
|
||||||
// Browse directory contents
|
// Browse directory contents
|
||||||
router.get('/browse/directories', ctx.asyncHandler(async (req, res) => {
|
router.get('/browse/directories', asyncHandler(async (req, res) => {
|
||||||
const requestedPath = req.query.path || '';
|
const requestedPath = req.query.path || '';
|
||||||
|
|
||||||
if (!requestedPath) {
|
if (!requestedPath) {
|
||||||
@@ -62,7 +72,7 @@ module.exports = function(ctx) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!matchingRoot) {
|
if (!matchingRoot) {
|
||||||
return ctx.errorResponse(res, 400, 'Path not in browseable roots', {
|
throw new ValidationError('Path not in browseable roots', {
|
||||||
availableRoots: BROWSE_ROOTS.map(r => r.hostPath)
|
availableRoots: BROWSE_ROOTS.map(r => r.hostPath)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -73,10 +83,10 @@ module.exports = function(ctx) {
|
|||||||
const allowedRoots = BROWSE_ROOTS.map(r => r.containerPath);
|
const allowedRoots = BROWSE_ROOTS.map(r => r.containerPath);
|
||||||
let resolvedPath;
|
let resolvedPath;
|
||||||
try {
|
try {
|
||||||
resolvedPath = await ctx.validateSecurePath(containerFullPath, allowedRoots, ctx.auditLogger);
|
resolvedPath = await validateSecurePath(containerFullPath, allowedRoots, auditLogger);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.constructor.name === 'ValidationError') {
|
if (error.constructor.name === 'ValidationError') {
|
||||||
ctx.auditLogger.logSecurityEvent('path_traversal_attempt', {
|
auditLogger.logSecurityEvent('path_traversal_attempt', {
|
||||||
requestedPath, containerFullPath, allowedRoots,
|
requestedPath, containerFullPath, allowedRoots,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
@@ -124,7 +134,7 @@ module.exports = function(ctx) {
|
|||||||
}, 'browse-dir'));
|
}, 'browse-dir'));
|
||||||
|
|
||||||
// Detect media mounts from existing media server containers
|
// Detect media mounts from existing media server containers
|
||||||
router.get('/media/detected-mounts', ctx.asyncHandler(async (req, res) => {
|
router.get('/media/detected-mounts', asyncHandler(async (req, res) => {
|
||||||
const mediaServerPatterns = [
|
const mediaServerPatterns = [
|
||||||
'plex', 'jellyfin', 'emby', 'kodi', 'navidrome', 'airsonic',
|
'plex', 'jellyfin', 'emby', 'kodi', 'navidrome', 'airsonic',
|
||||||
'subsonic', 'funkwhale', 'beets', 'lidarr', 'sonarr', 'radarr',
|
'subsonic', 'funkwhale', 'beets', 'lidarr', 'sonarr', 'radarr',
|
||||||
@@ -136,7 +146,7 @@ module.exports = function(ctx) {
|
|||||||
'/tmp', '/var', '/etc', '/opt', '/root', '/home', '/.', '/caddyfile'
|
'/tmp', '/var', '/etc', '/opt', '/root', '/home', '/.', '/caddyfile'
|
||||||
];
|
];
|
||||||
|
|
||||||
const containers = await ctx.docker.client.listContainers({ all: false });
|
const containers = await docker.client.listContainers({ all: false });
|
||||||
const detectedMounts = [];
|
const detectedMounts = [];
|
||||||
const seenPaths = new Set();
|
const seenPaths = new Set();
|
||||||
|
|
||||||
@@ -145,7 +155,7 @@ module.exports = function(ctx) {
|
|||||||
const isMediaServer = mediaServerPatterns.some(p => imageName.includes(p));
|
const isMediaServer = mediaServerPatterns.some(p => imageName.includes(p));
|
||||||
if (!isMediaServer) continue;
|
if (!isMediaServer) continue;
|
||||||
|
|
||||||
const container = ctx.docker.client.getContainer(containerInfo.Id);
|
const container = docker.client.getContainer(containerInfo.Id);
|
||||||
const details = await container.inspect();
|
const details = await container.inspect();
|
||||||
const binds = details.HostConfig?.Binds || [];
|
const binds = details.HostConfig?.Binds || [];
|
||||||
|
|
||||||
|
|||||||
@@ -371,7 +371,12 @@ async function createApp() {
|
|||||||
asyncHandler: ctx.asyncHandler
|
asyncHandler: ctx.asyncHandler
|
||||||
}));
|
}));
|
||||||
apiRouter.use('/ca', caRoutes(ctx));
|
apiRouter.use('/ca', caRoutes(ctx));
|
||||||
apiRouter.use(browseRoutes(ctx));
|
apiRouter.use(browseRoutes({
|
||||||
|
asyncHandler: ctx.asyncHandler,
|
||||||
|
validateSecurePath: ctx.validateSecurePath,
|
||||||
|
auditLogger: ctx.auditLogger,
|
||||||
|
docker: ctx.docker
|
||||||
|
}));
|
||||||
apiRouter.use(errorLogsRoutes({
|
apiRouter.use(errorLogsRoutes({
|
||||||
ERROR_LOG_FILE: ctx.ERROR_LOG_FILE,
|
ERROR_LOG_FILE: ctx.ERROR_LOG_FILE,
|
||||||
auditLogger: ctx.auditLogger,
|
auditLogger: ctx.auditLogger,
|
||||||
|
|||||||
Reference in New Issue
Block a user