using System; using System.Collections.Generic; using System.Linq; using System.Net; using Microsoft.AspNetCore.Http; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; using NzbDrone.Core.Exceptions; namespace Lidarr.Http.Extensions { public static class RequestExtensions { // See src/Lidarr.Api.V1/Queue/QueueModule.cs private static readonly HashSet VALID_SORT_KEYS = new HashSet(StringComparer.OrdinalIgnoreCase) { "artists.sortname", // Workaround authors table properties not being added on isValidSortKey call "timeleft", "estimatedCompletionTime", "protocol", "indexer", "downloadClient", "quality", "status", "title", "progress" }; private static readonly HashSet EXCLUDED_KEYS = new HashSet(StringComparer.InvariantCultureIgnoreCase) { "page", "pageSize", "sortKey", "sortDirection", "filterKey", "filterValue", }; public static bool IsApiRequest(this HttpRequest request) { return request.Path.StartsWithSegments("/api", StringComparison.InvariantCultureIgnoreCase); } public static bool GetBooleanQueryParameter(this HttpRequest request, string parameter, bool defaultValue = false) { var parameterValue = request.Query[parameter]; if (parameterValue.Any()) { return bool.Parse(parameterValue.ToString()); } return defaultValue; } public static PagingResource ReadPagingResourceFromRequest(this HttpRequest request) { if (!int.TryParse(request.Query["PageSize"].ToString(), out var pageSize)) { pageSize = 10; } if (!int.TryParse(request.Query["Page"].ToString(), out var page)) { page = 1; } var pagingResource = new PagingResource { PageSize = pageSize, Page = page, Filters = new List() }; if (request.Query["SortKey"].Any()) { var sortKey = request.Query["SortKey"].ToString(); if (!VALID_SORT_KEYS.Contains(sortKey) && !TableMapping.Mapper.IsValidSortKey(sortKey)) { throw new BadRequestException($"Invalid sort key {sortKey}"); } pagingResource.SortKey = sortKey; if (request.Query["SortDirection"].Any()) { pagingResource.SortDirection = request.Query["SortDirection"].ToString() .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) ? SortDirection.Ascending : SortDirection.Descending; } } // For backwards compatibility with v2 if (request.Query["FilterKey"].Any()) { var filter = new PagingResourceFilter { Key = request.Query["FilterKey"].ToString() }; if (request.Query["FilterValue"].Any()) { filter.Value = request.Query["FilterValue"].ToString(); } pagingResource.Filters.Add(filter); } // v3 uses filters in key=value format foreach (var pair in request.Query) { if (EXCLUDED_KEYS.Contains(pair.Key)) { continue; } pagingResource.Filters.Add(new PagingResourceFilter { Key = pair.Key, Value = pair.Value.ToString() }); } return pagingResource; } public static PagingResource ApplyToPage(this PagingSpec pagingSpec, Func, PagingSpec> function, Converter mapper) { pagingSpec = function(pagingSpec); return new PagingResource { Page = pagingSpec.Page, PageSize = pagingSpec.PageSize, SortDirection = pagingSpec.SortDirection, SortKey = pagingSpec.SortKey, TotalRecords = pagingSpec.TotalRecords, Records = pagingSpec.Records.ConvertAll(mapper) }; } public static string GetRemoteIP(this HttpContext context) { return context?.Request?.GetRemoteIP() ?? "Unknown"; } public static string GetRemoteIP(this HttpRequest request) { if (request == null) { return "Unknown"; } var remoteIP = request.HttpContext.Connection.RemoteIpAddress; if (remoteIP.IsIPv4MappedToIPv6) { remoteIP = remoteIP.MapToIPv4(); } var remoteAddress = remoteIP.ToString(); // Only check if forwarded by a local network reverse proxy if (remoteIP.IsLocalAddress()) { var realIPHeader = request.Headers["X-Real-IP"]; if (realIPHeader.Any()) { return realIPHeader.First().ToString(); } var forwardedForHeader = request.Headers["X-Forwarded-For"]; if (forwardedForHeader.Any()) { // Get the first address that was forwarded by a local IP to prevent remote clients faking another proxy foreach (var forwardedForAddress in forwardedForHeader.SelectMany(v => v.Split(',')).Select(v => v.Trim()).Reverse()) { if (!IPAddress.TryParse(forwardedForAddress, out remoteIP)) { return remoteAddress; } if (!remoteIP.IsLocalAddress()) { return forwardedForAddress; } remoteAddress = forwardedForAddress; } } } return remoteAddress; } public static void DisableCache(this IHeaderDictionary headers) { headers.Remove("Last-Modified"); headers["Cache-Control"] = "no-cache, no-store"; headers["Expires"] = "-1"; headers["Pragma"] = "no-cache"; } public static void EnableCache(this IHeaderDictionary headers) { headers["Cache-Control"] = "max-age=31536000, public"; headers["Last-Modified"] = BuildInfo.BuildDateTime.ToString("r"); } } }