refactor(routes): Phase 3.4 - standardize browse.js

This commit is contained in:
Krystie
2026-03-29 20:13:25 -07:00
parent e6e788fdce
commit 8b1492142f
2 changed files with 25 additions and 10 deletions

View File

@@ -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 || [];

View File

@@ -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,