From f48c9f9f88f73db6c9945a0e0210d35673d1fe33 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Tue, 24 Feb 2026 20:13:55 -0800 Subject: [PATCH] Improve HTTP file mappers (cherry picked from commit f30207c3d130c1a37f29e214101c8ec9613d18ee) --- .../Frontend/Mappers/BackupFileMapper.cs | 6 ++++-- .../Frontend/Mappers/BrowserConfig.cs | 11 +++++++++-- .../Frontend/Mappers/CacheBreakerProvider.cs | 6 ++++++ .../Frontend/Mappers/FaviconMapper.cs | 6 ++++-- .../Frontend/Mappers/HtmlMapperBase.cs | 12 ++++++++---- .../Frontend/Mappers/IndexHtmlMapper.cs | 12 +++++++----- .../Frontend/Mappers/LogFileMapper.cs | 6 ++++-- .../Frontend/Mappers/LoginHtmlMapper.cs | 11 +++++++---- .../Frontend/Mappers/ManifestMapper.cs | 8 ++++++-- .../Frontend/Mappers/MediaCoverMapper.cs | 4 +++- .../Frontend/Mappers/RobotsTxtMapper.cs | 6 ++++-- .../Frontend/Mappers/StaticResourceMapper.cs | 6 ++++-- .../Frontend/Mappers/StaticResourceMapperBase.cs | 16 +++++++++++++++- .../Frontend/Mappers/UpdateLogFileMapper.cs | 6 ++++-- .../UrlBaseReplacementResourceMapperBase.cs | 4 ++-- .../Frontend/StaticResourceController.cs | 7 +++++++ 16 files changed, 94 insertions(+), 33 deletions(-) diff --git a/src/Prowlarr.Http/Frontend/Mappers/BackupFileMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/BackupFileMapper.cs index d4157fb46..ad3734f5a 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/BackupFileMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/BackupFileMapper.cs @@ -15,11 +15,13 @@ public BackupFileMapper(IBackupService backupService, IDiskProvider diskProvider _backupService = backupService; } - public override string Map(string resourceUrl) + protected override string FolderPath => _backupService.GetBackupFolder(); + + protected override string MapPath(string resourceUrl) { var path = resourceUrl.Replace("/backup/", "").Replace('/', Path.DirectorySeparatorChar); - return Path.Combine(_backupService.GetBackupFolder(), path); + return Path.Combine(FolderPath, path); } public override bool CanHandle(string resourceUrl) diff --git a/src/Prowlarr.Http/Frontend/Mappers/BrowserConfig.cs b/src/Prowlarr.Http/Frontend/Mappers/BrowserConfig.cs index 0edb50674..714cbfdad 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/BrowserConfig.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/BrowserConfig.cs @@ -8,13 +8,20 @@ namespace Prowlarr.Http.Frontend.Mappers { public class BrowserConfig : UrlBaseReplacementResourceMapperBase { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IConfigFileProvider _configFileProvider; + public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) : base(diskProvider, configFileProvider, logger) { - FilePath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "Content", "browserconfig.xml"); + _appFolderInfo = appFolderInfo; + _configFileProvider = configFileProvider; } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder); + protected override string FilePath => Path.Combine(FolderPath, "Content", "browserconfig.xml"); + + protected override string MapPath(string resourceUrl) { return FilePath; } diff --git a/src/Prowlarr.Http/Frontend/Mappers/CacheBreakerProvider.cs b/src/Prowlarr.Http/Frontend/Mappers/CacheBreakerProvider.cs index d888edd1f..d6eb83d20 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/CacheBreakerProvider.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/CacheBreakerProvider.cs @@ -30,6 +30,12 @@ public string AddCacheBreakerToPath(string resourceUrl) var mapper = _diskMappers.Single(m => m.CanHandle(resourceUrl)); var pathToFile = mapper.Map(resourceUrl); + + if (pathToFile == null) + { + return resourceUrl; + } + var hash = _hashProvider.ComputeMd5(pathToFile).ToBase64(); return resourceUrl + "?h=" + hash.Trim('='); diff --git a/src/Prowlarr.Http/Frontend/Mappers/FaviconMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/FaviconMapper.cs index a0202b4a3..fc15e15d5 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/FaviconMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/FaviconMapper.cs @@ -18,7 +18,9 @@ public FaviconMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, I _configFileProvider = configFileProvider; } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder); + + protected override string MapPath(string resourceUrl) { var fileName = "favicon.ico"; @@ -29,7 +31,7 @@ public override string Map(string resourceUrl) var path = Path.Combine("Content", "Images", "Icons", fileName); - return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); + return Path.Combine(FolderPath, path); } public override bool CanHandle(string resourceUrl) diff --git a/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs index becd6a7db..23fa502ec 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; namespace Prowlarr.Http.Frontend.Mappers { @@ -13,19 +14,22 @@ public abstract class HtmlMapperBase : StaticResourceMapperBase private readonly Lazy _cacheBreakProviderFactory; private static readonly Regex ReplaceRegex = new Regex(@"(?:(?href|src)=\"")(?.*?(?css|js|png|ico|ics|svg|json))(?:\"")(?:\s(?data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private string _urlBase; private string _generatedContent; protected HtmlMapperBase(IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, Lazy cacheBreakProviderFactory, Logger logger) : base(diskProvider, logger) { _diskProvider = diskProvider; _cacheBreakProviderFactory = cacheBreakProviderFactory; + + _urlBase = configFileProvider.UrlBase; } - protected string HtmlPath; - protected string UrlBase; + protected abstract string HtmlPath { get; } protected override Stream GetContentStream(string filePath) { @@ -62,10 +66,10 @@ protected virtual string GetHtmlText() url = cacheBreakProvider.AddCacheBreakerToPath(match.Groups["path"].Value); } - return $"{match.Groups["attribute"].Value}=\"{UrlBase}{url}\""; + return $"{match.Groups["attribute"].Value}=\"{_urlBase}{url}\""; }); - text = text.Replace("__URL_BASE__", UrlBase); + text = text.Replace("__URL_BASE__", _urlBase); _generatedContent = text; diff --git a/src/Prowlarr.Http/Frontend/Mappers/IndexHtmlMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/IndexHtmlMapper.cs index a5801a6b2..b54a103ec 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -9,6 +9,7 @@ namespace Prowlarr.Http.Frontend.Mappers { public class IndexHtmlMapper : HtmlMapperBase { + private readonly IAppFolderInfo _appFolderInfo; private readonly IConfigFileProvider _configFileProvider; public IndexHtmlMapper(IAppFolderInfo appFolderInfo, @@ -16,15 +17,16 @@ public IndexHtmlMapper(IAppFolderInfo appFolderInfo, IConfigFileProvider configFileProvider, Lazy cacheBreakProviderFactory, Logger logger) - : base(diskProvider, cacheBreakProviderFactory, logger) + : base(diskProvider, configFileProvider, cacheBreakProviderFactory, logger) { + _appFolderInfo = appFolderInfo; _configFileProvider = configFileProvider; - - HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "index.html"); - UrlBase = configFileProvider.UrlBase; } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder); + protected override string HtmlPath => Path.Combine(FolderPath, "index.html"); + + protected override string MapPath(string resourceUrl) { return HtmlPath; } diff --git a/src/Prowlarr.Http/Frontend/Mappers/LogFileMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/LogFileMapper.cs index 5d4801347..4b7c0677d 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/LogFileMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/LogFileMapper.cs @@ -16,12 +16,14 @@ public LogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, L _appFolderInfo = appFolderInfo; } - public override string Map(string resourceUrl) + protected override string FolderPath => _appFolderInfo.GetLogFolder(); + + protected override string MapPath(string resourceUrl) { var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); path = Path.GetFileName(path); - return Path.Combine(_appFolderInfo.GetLogFolder(), path); + return Path.Combine(FolderPath, path); } public override bool CanHandle(string resourceUrl) diff --git a/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs index 112ad9383..2386f89e7 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -9,6 +9,7 @@ namespace Prowlarr.Http.Frontend.Mappers { public class LoginHtmlMapper : HtmlMapperBase { + private readonly IAppFolderInfo _appFolderInfo; private readonly IConfigFileProvider _configFileProvider; public LoginHtmlMapper(IAppFolderInfo appFolderInfo, @@ -16,14 +17,16 @@ public LoginHtmlMapper(IAppFolderInfo appFolderInfo, Lazy cacheBreakProviderFactory, IConfigFileProvider configFileProvider, Logger logger) - : base(diskProvider, cacheBreakProviderFactory, logger) + : base(diskProvider, configFileProvider, cacheBreakProviderFactory, logger) { + _appFolderInfo = appFolderInfo; _configFileProvider = configFileProvider; - HtmlPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); - UrlBase = configFileProvider.UrlBase; } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder); + protected override string HtmlPath => Path.Combine(FolderPath, "login.html"); + + protected override string MapPath(string resourceUrl) { return HtmlPath; } diff --git a/src/Prowlarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/ManifestMapper.cs index 76f2ac653..3fb96dceb 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/ManifestMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -8,6 +8,7 @@ namespace Prowlarr.Http.Frontend.Mappers { public class ManifestMapper : UrlBaseReplacementResourceMapperBase { + private readonly IAppFolderInfo _appFolderInfo; private readonly IConfigFileProvider _configFileProvider; private string _generatedContent; @@ -15,11 +16,14 @@ public class ManifestMapper : UrlBaseReplacementResourceMapperBase public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) : base(diskProvider, configFileProvider, logger) { + _appFolderInfo = appFolderInfo; _configFileProvider = configFileProvider; - FilePath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "Content", "manifest.json"); } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder); + protected override string FilePath => Path.Combine(FolderPath, "Content", "manifest.json"); + + protected override string MapPath(string resourceUrl) { return FilePath; } diff --git a/src/Prowlarr.Http/Frontend/Mappers/MediaCoverMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/MediaCoverMapper.cs index 7d0e185ae..fa5b523c0 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/MediaCoverMapper.cs @@ -22,7 +22,9 @@ public MediaCoverMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider _diskProvider = diskProvider; } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover"); + + protected override string MapPath(string resourceUrl) { var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); path = path.Trim(Path.DirectorySeparatorChar); diff --git a/src/Prowlarr.Http/Frontend/Mappers/RobotsTxtMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/RobotsTxtMapper.cs index 25950d24e..4d4ec132f 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/RobotsTxtMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/RobotsTxtMapper.cs @@ -18,11 +18,13 @@ public RobotsTxtMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, _configFileProvider = configFileProvider; } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder); + + protected override string MapPath(string resourceUrl) { var path = Path.Combine("Content", "robots.txt"); - return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); + return Path.Combine(FolderPath, path); } public override bool CanHandle(string resourceUrl) diff --git a/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapper.cs index 06685d082..1b635a102 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapper.cs @@ -18,12 +18,14 @@ public StaticResourceMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProv _configFileProvider = configFileProvider; } - public override string Map(string resourceUrl) + protected override string FolderPath => Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder); + + protected override string MapPath(string resourceUrl) { var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); path = path.Trim(Path.DirectorySeparatorChar); - return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); + return Path.Combine(FolderPath, path); } public override bool CanHandle(string resourceUrl) diff --git a/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 5e0d15b71..f7caf9213 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -27,14 +27,28 @@ protected StaticResourceMapperBase(IDiskProvider diskProvider, Logger logger) _caseSensitive = RuntimeInfo.IsProduction ? DiskProviderBase.PathStringComparison : StringComparison.OrdinalIgnoreCase; } - public abstract string Map(string resourceUrl); + protected abstract string FolderPath { get; } + protected abstract string MapPath(string resourceUrl); public abstract bool CanHandle(string resourceUrl); + public string Map(string resourceUrl) + { + var filePath = Path.GetFullPath(MapPath(resourceUrl)); + var parentPath = Path.GetFullPath(FolderPath) + Path.DirectorySeparatorChar; + + return filePath.StartsWith(parentPath) ? filePath : null; + } + public Task GetResponse(string resourceUrl) { var filePath = Map(resourceUrl); + if (filePath == null) + { + return Task.FromResult(null); + } + if (_diskProvider.FileExists(filePath, _caseSensitive)) { if (!_mimeTypeProvider.TryGetContentType(filePath, out var contentType)) diff --git a/src/Prowlarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs b/src/Prowlarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs index c160b72cd..05134b985 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs @@ -16,12 +16,14 @@ public UpdateLogFileMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvi _appFolderInfo = appFolderInfo; } - public override string Map(string resourceUrl) + protected override string FolderPath => _appFolderInfo.GetUpdateLogFolder(); + + protected override string MapPath(string resourceUrl) { var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); path = Path.GetFileName(path); - return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), path); + return Path.Combine(FolderPath, path); } public override bool CanHandle(string resourceUrl) diff --git a/src/Prowlarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs b/src/Prowlarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs index 2979be883..46d1c98fd 100644 --- a/src/Prowlarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs +++ b/src/Prowlarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs @@ -20,9 +20,9 @@ public UrlBaseReplacementResourceMapperBase(IDiskProvider diskProvider, IConfigF _urlBase = configFileProvider.UrlBase; } - protected string FilePath; + protected abstract string FilePath { get; } - public override string Map(string resourceUrl) + protected override string MapPath(string resourceUrl) { return FilePath; } diff --git a/src/Prowlarr.Http/Frontend/StaticResourceController.cs b/src/Prowlarr.Http/Frontend/StaticResourceController.cs index f1836d2d3..f5ac1e543 100644 --- a/src/Prowlarr.Http/Frontend/StaticResourceController.cs +++ b/src/Prowlarr.Http/Frontend/StaticResourceController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; @@ -16,6 +17,7 @@ public class StaticResourceController : Controller { private readonly IEnumerable _requestMappers; private readonly Logger _logger; + private static readonly Regex InvalidPathRegex = new(@"([\/\\]|%2f|%5c)\.\.|\.\.([\/\\]|%2f|%5c)", RegexOptions.IgnoreCase | RegexOptions.Compiled); public StaticResourceController(IEnumerable requestMappers, Logger logger) @@ -50,6 +52,11 @@ private async Task MapResource(string path) { path = "/" + (path ?? ""); + if (InvalidPathRegex.IsMatch(path)) + { + return NotFound(); + } + var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); if (mapper != null)