diff --git a/src/Lidarr.Http/Frontend/Mappers/BackupFileMapper.cs b/src/Lidarr.Http/Frontend/Mappers/BackupFileMapper.cs index 610fdf47e..1b160fac3 100644 --- a/src/Lidarr.Http/Frontend/Mappers/BackupFileMapper.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/BrowserConfig.cs b/src/Lidarr.Http/Frontend/Mappers/BrowserConfig.cs index e67598e7e..fad44f9a2 100644 --- a/src/Lidarr.Http/Frontend/Mappers/BrowserConfig.cs +++ b/src/Lidarr.Http/Frontend/Mappers/BrowserConfig.cs @@ -8,13 +8,20 @@ namespace Lidarr.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/Lidarr.Http/Frontend/Mappers/CacheBreakerProvider.cs b/src/Lidarr.Http/Frontend/Mappers/CacheBreakerProvider.cs index 20f5e37ad..fadcde4e0 100644 --- a/src/Lidarr.Http/Frontend/Mappers/CacheBreakerProvider.cs +++ b/src/Lidarr.Http/Frontend/Mappers/CacheBreakerProvider.cs @@ -37,6 +37,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/Lidarr.Http/Frontend/Mappers/FaviconMapper.cs b/src/Lidarr.Http/Frontend/Mappers/FaviconMapper.cs index 8afc3bf40..2f3bd0978 100644 --- a/src/Lidarr.Http/Frontend/Mappers/FaviconMapper.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs index 0a22855b5..91b6fec59 100644 --- a/src/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs +++ b/src/Lidarr.Http/Frontend/Mappers/HtmlMapperBase.cs @@ -4,6 +4,7 @@ using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; namespace Lidarr.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/Lidarr.Http/Frontend/Mappers/IndexHtmlMapper.cs b/src/Lidarr.Http/Frontend/Mappers/IndexHtmlMapper.cs index 742c09b14..c0977e1c6 100644 --- a/src/Lidarr.Http/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -9,6 +9,7 @@ namespace Lidarr.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/Lidarr.Http/Frontend/Mappers/LogFileMapper.cs b/src/Lidarr.Http/Frontend/Mappers/LogFileMapper.cs index 88644438d..6225eda74 100644 --- a/src/Lidarr.Http/Frontend/Mappers/LogFileMapper.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Lidarr.Http/Frontend/Mappers/LoginHtmlMapper.cs index 20a9ef0cf..2f2038884 100644 --- a/src/Lidarr.Http/Frontend/Mappers/LoginHtmlMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -9,6 +9,7 @@ namespace Lidarr.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/Lidarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Lidarr.Http/Frontend/Mappers/ManifestMapper.cs index 723d7dc2e..eda00eefa 100644 --- a/src/Lidarr.Http/Frontend/Mappers/ManifestMapper.cs +++ b/src/Lidarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -8,6 +8,7 @@ namespace Lidarr.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/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs b/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs index 1099b5666..e9c649cdd 100644 --- a/src/Lidarr.Http/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/RobotsTxtMapper.cs b/src/Lidarr.Http/Frontend/Mappers/RobotsTxtMapper.cs index d6bfedb5f..40e0d3136 100644 --- a/src/Lidarr.Http/Frontend/Mappers/RobotsTxtMapper.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/StaticResourceMapper.cs b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapper.cs index 47469f88f..e8715ab7a 100644 --- a/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 5774df28e..ccdc912fd 100644 --- a/src/Lidarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs b/src/Lidarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs index 9ed755e96..723218c57 100644 --- a/src/Lidarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs b/src/Lidarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs index 5eee82fbf..8bfec99c6 100644 --- a/src/Lidarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs +++ b/src/Lidarr.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/Lidarr.Http/Frontend/StaticResourceController.cs b/src/Lidarr.Http/Frontend/StaticResourceController.cs index 877c5c39e..78af72beb 100644 --- a/src/Lidarr.Http/Frontend/StaticResourceController.cs +++ b/src/Lidarr.Http/Frontend/StaticResourceController.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Lidarr.Http.Extensions; using Lidarr.Http.Frontend.Mappers; @@ -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) diff --git a/src/NzbDrone.Common/Http/HttpUri.cs b/src/NzbDrone.Common/Http/HttpUri.cs index 31626b925..a06df002d 100644 --- a/src/NzbDrone.Common/Http/HttpUri.cs +++ b/src/NzbDrone.Common/Http/HttpUri.cs @@ -6,13 +6,14 @@ namespace NzbDrone.Common.Http { - public class HttpUri : IEquatable + public partial class HttpUri : IEquatable { - private static readonly Regex RegexUri = new Regex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); - private readonly string _uri; public string FullUri => _uri; + [GeneratedRegex(@"^(?:(?[a-z]+):)?(?://(?[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?[0-9]{1,5}))?)?(?(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?[^#\r\n]*))?(?:\#(?.*))?$", RegexOptions.IgnoreCase | RegexOptions.Compiled)] + private static partial Regex UriRegex(); + public HttpUri(string uri) { _uri = uri ?? string.Empty; @@ -70,9 +71,9 @@ public HttpUri(string scheme, string host, int? port, string path, string query, private void Parse() { - var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri); + var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out _); - var match = RegexUri.Match(_uri); + var match = UriRegex().Match(_uri); var scheme = match.Groups["scheme"]; var host = match.Groups["host"]; diff --git a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs index 77320c5ec..57e750728 100644 --- a/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs +++ b/src/NzbDrone.Core.Test/MetadataSource/SkyHook/SkyHookProxySearchFixture.cs @@ -107,8 +107,8 @@ public void no_artist_search_result(string term) } [TestCase("Eminem", 0, typeof(Artist), "Eminem")] - [TestCase("Eminem Kamikaze", 0, typeof(Artist), "Eminem")] - [TestCase("Eminem Kamikaze", 1, typeof(Album), "Kamikaze")] + [TestCase("Eminem Kamikaze", 0, typeof(Album), "Kamikaze")] + [TestCase("Eminem Kamikaze", 1, typeof(Artist), "Eminem")] [TestCase("lidarr:f59c5520-5f46-4d2c-b2c4-822eabf53419", 0, typeof(Artist), "Linkin Park")] [TestCase("lidarr: d77df681-b779-3d6d-b66a-3bfd15985e3e", 0, typeof(Album), "Pyromania")] public void successful_combined_search(string query, int position, Type resultType, string expected) diff --git a/src/NzbDrone.Core/Lidarr.Core.csproj b/src/NzbDrone.Core/Lidarr.Core.csproj index 048cb51df..d5eed7f10 100644 --- a/src/NzbDrone.Core/Lidarr.Core.csproj +++ b/src/NzbDrone.Core/Lidarr.Core.csproj @@ -6,7 +6,7 @@ - + @@ -27,7 +27,7 @@ - +