diff --git a/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs index 17027aeab..f1cf7f1d2 100644 --- a/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs @@ -15,11 +15,13 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs index 3cb1488df..8b9548dc6 100644 --- a/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs +++ b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs @@ -8,13 +8,20 @@ namespace Sonarr.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/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs index b3f635049..fd3f231a2 100644 --- a/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs +++ b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs @@ -37,6 +37,12 @@ namespace Sonarr.Http.Frontend.Mappers 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/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs b/src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs index ef5d8bb17..bb689506d 100644 --- a/src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs @@ -18,7 +18,9 @@ namespace Sonarr.Http.Frontend.Mappers _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 @@ namespace Sonarr.Http.Frontend.Mappers 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/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs index efa8a7882..7f0dd107d 100644 --- a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; namespace Sonarr.Http.Frontend.Mappers { @@ -13,19 +14,22 @@ namespace Sonarr.Http.Frontend.Mappers 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 @@ namespace Sonarr.Http.Frontend.Mappers 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/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs index d8cbaab63..1dcd8dc3b 100644 --- a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -9,6 +9,7 @@ namespace Sonarr.Http.Frontend.Mappers { public class IndexHtmlMapper : HtmlMapperBase { + private readonly IAppFolderInfo _appFolderInfo; private readonly IConfigFileProvider _configFileProvider; public IndexHtmlMapper(IAppFolderInfo appFolderInfo, @@ -16,15 +17,16 @@ namespace Sonarr.Http.Frontend.Mappers 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/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs index 269e0243d..3ef9f7aae 100644 --- a/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs @@ -16,12 +16,14 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs index ca325add6..ae68a621d 100644 --- a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -9,6 +9,7 @@ namespace Sonarr.Http.Frontend.Mappers { public class LoginHtmlMapper : HtmlMapperBase { + private readonly IAppFolderInfo _appFolderInfo; private readonly IConfigFileProvider _configFileProvider; public LoginHtmlMapper(IAppFolderInfo appFolderInfo, @@ -16,14 +17,16 @@ namespace Sonarr.Http.Frontend.Mappers 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/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs index ca69d0430..a533268be 100644 --- a/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -8,6 +8,7 @@ namespace Sonarr.Http.Frontend.Mappers { public class ManifestMapper : UrlBaseReplacementResourceMapperBase { + private readonly IAppFolderInfo _appFolderInfo; private readonly IConfigFileProvider _configFileProvider; private string _generatedContent; @@ -15,11 +16,14 @@ namespace Sonarr.Http.Frontend.Mappers 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/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs b/src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs index afabdcf00..a78d83f6b 100644 --- a/src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs @@ -22,7 +22,9 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs b/src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs index e17fb267e..9dd9470b8 100644 --- a/src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs @@ -18,11 +18,13 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs index e3dbabcb0..89b61c9e8 100644 --- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs @@ -18,12 +18,14 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 3f3982ac1..e269a5dae 100644 --- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -27,14 +27,28 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs index 2b418f981..1c56894c1 100644 --- a/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs @@ -16,12 +16,14 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs index c79d16464..5180918d5 100644 --- a/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs @@ -20,9 +20,9 @@ namespace Sonarr.Http.Frontend.Mappers _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/Sonarr.Http/Frontend/StaticResourceController.cs b/src/Sonarr.Http/Frontend/StaticResourceController.cs index 49bc495b7..20f817019 100644 --- a/src/Sonarr.Http/Frontend/StaticResourceController.cs +++ b/src/Sonarr.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 @@ namespace Sonarr.Http.Frontend { 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 @@ namespace Sonarr.Http.Frontend { path = "/" + (path ?? ""); + if (InvalidPathRegex.IsMatch(path)) + { + return NotFound(); + } + var mapper = _requestMappers.SingleOrDefault(m => m.CanHandle(path)); if (mapper != null)