From 1772e7c9fddb37ff8ad230de26fa3a0f6280a64e Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Thu, 28 Oct 2021 16:57:29 -0500 Subject: [PATCH] refactor: sanitized logs for http communication HTTP requests and responses are logged at DBG level. Host name and API key are obfuscated for user privacy. --- src/Trash/Command/Helpers/ServiceCommand.cs | 15 ++++---- src/TrashLib/Config/IServerInfo.cs | 4 ++- src/TrashLib/Config/ServerInfo.cs | 12 +++++-- src/TrashLib/Extensions/FlurlExtensions.cs | 36 +++++++++++++++++++ src/TrashLib/Extensions/FlurlLogging.cs | 28 +++++++++++++++ .../CustomFormat/Api/CustomFormatService.cs | 13 ++++--- .../CustomFormat/Api/QualityProfileService.cs | 9 +++-- .../Api/QualityDefinitionService.cs | 9 +++-- src/TrashLib/Radarr/RadarrAutofacModule.cs | 4 ++- src/TrashLib/Sonarr/Api/SonarrApi.cs | 12 +++++-- src/TrashLib/Sonarr/SonarrCompatibility.cs | 3 +- 11 files changed, 111 insertions(+), 34 deletions(-) create mode 100644 src/TrashLib/Extensions/FlurlExtensions.cs create mode 100644 src/TrashLib/Extensions/FlurlLogging.cs diff --git a/src/Trash/Command/Helpers/ServiceCommand.cs b/src/Trash/Command/Helpers/ServiceCommand.cs index 7566d18e..4534096c 100644 --- a/src/Trash/Command/Helpers/ServiceCommand.cs +++ b/src/Trash/Command/Helpers/ServiceCommand.cs @@ -12,27 +12,27 @@ using Newtonsoft.Json; using Serilog; using Serilog.Core; using Serilog.Events; +using TrashLib.Extensions; using YamlDotNet.Core; namespace Trash.Command.Helpers { public abstract class ServiceCommand : ICommand, IServiceCommand { + private readonly ILogger _log; private readonly LoggingLevelSwitch _loggingLevelSwitch; private readonly ILogJanitor _logJanitor; protected ServiceCommand( - ILogger logger, + ILogger log, LoggingLevelSwitch loggingLevelSwitch, ILogJanitor logJanitor) { _loggingLevelSwitch = loggingLevelSwitch; _logJanitor = logJanitor; - Log = logger; + _log = log; } - protected ILogger Log { get; } - public async ValueTask ExecuteAsync(IConsole console) { SetupLogging(); @@ -50,13 +50,13 @@ namespace Trash.Command.Helpers throw; } - Log.Error("Found Unrecognized YAML Property: {ErrorMsg}", inner.Message); - Log.Error("Please remove the property quoted in the above message from your YAML file"); + _log.Error("Found Unrecognized YAML Property: {ErrorMsg}", inner.Message); + _log.Error("Please remove the property quoted in the above message from your YAML file"); throw new CommandException("Exiting due to invalid configuration"); } catch (Exception e) when (e is not CommandException) { - Log.Error(e, "Unrecoverable Exception"); + _log.Error(e, "Unrecoverable Exception"); ExitDueToFailure(); } finally @@ -109,6 +109,7 @@ namespace Trash.Command.Helpers }; settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings); + FlurlLogging.SetupLogging(settings, _log); }); } diff --git a/src/TrashLib/Config/IServerInfo.cs b/src/TrashLib/Config/IServerInfo.cs index c76b6b0d..063fbac6 100644 --- a/src/TrashLib/Config/IServerInfo.cs +++ b/src/TrashLib/Config/IServerInfo.cs @@ -1,7 +1,9 @@ +using Flurl.Http; + namespace TrashLib.Config { public interface IServerInfo { - string BuildUrl(); + IFlurlRequest BuildRequest(); } } diff --git a/src/TrashLib/Config/ServerInfo.cs b/src/TrashLib/Config/ServerInfo.cs index f4978ec7..a6f1ac37 100644 --- a/src/TrashLib/Config/ServerInfo.cs +++ b/src/TrashLib/Config/ServerInfo.cs @@ -1,4 +1,7 @@ using Flurl; +using Flurl.Http; +using Serilog; +using TrashLib.Extensions; namespace TrashLib.Config { @@ -6,18 +9,21 @@ namespace TrashLib.Config { private readonly string _apiKey; private readonly string _baseUrl; + private readonly ILogger _log; - public ServerInfo(string baseUrl, string apiKey) + public ServerInfo(string baseUrl, string apiKey, ILogger log) { _baseUrl = baseUrl; _apiKey = apiKey; + _log = log; } - public string BuildUrl() + public IFlurlRequest BuildRequest() { return _baseUrl .AppendPathSegment("api/v3") - .SetQueryParams(new {apikey = _apiKey}); + .SetQueryParams(new {apikey = _apiKey}) + .SanitizedLogging(_log); } } } diff --git a/src/TrashLib/Extensions/FlurlExtensions.cs b/src/TrashLib/Extensions/FlurlExtensions.cs new file mode 100644 index 00000000..9cfb9c21 --- /dev/null +++ b/src/TrashLib/Extensions/FlurlExtensions.cs @@ -0,0 +1,36 @@ +using System; +using Flurl; +using Flurl.Http; +using Serilog; + +namespace TrashLib.Extensions +{ + public static class FlurlExtensions + { + public static IFlurlRequest SanitizedLogging(this Uri url, ILogger log) + => new FlurlRequest(url).SanitizedLogging(log); + + public static IFlurlRequest SanitizedLogging(this Url url, ILogger log) + => new FlurlRequest(url).SanitizedLogging(log); + + public static IFlurlRequest SanitizedLogging(this string url, ILogger log) + => new FlurlRequest(url).SanitizedLogging(log); + + public static IFlurlRequest SanitizedLogging(this IFlurlRequest request, ILogger log) + { + return request.ConfigureRequest(settings => FlurlLogging.SetupLogging(settings, log, SanitizeUrl)); + } + + private static Url SanitizeUrl(Url url) + { + // Replace hostname and API key for user privacy + url.Host = "hostname"; + if (url.QueryParams.Contains("apikey")) + { + url.QueryParams.AddOrReplace("apikey", "SNIP"); + } + + return url; + } + } +} diff --git a/src/TrashLib/Extensions/FlurlLogging.cs b/src/TrashLib/Extensions/FlurlLogging.cs new file mode 100644 index 00000000..b6ec5052 --- /dev/null +++ b/src/TrashLib/Extensions/FlurlLogging.cs @@ -0,0 +1,28 @@ +using System; +using Flurl; +using Flurl.Http.Configuration; +using Serilog; + +namespace TrashLib.Extensions +{ + public static class FlurlLogging + { + public static void SetupLogging(FlurlHttpSettings settings, ILogger log, Func? urlInterceptor = null) + { + urlInterceptor ??= url => url; + + settings.BeforeCall = call => + { + var url = urlInterceptor(call.Request.Url.Clone()); + log.Debug("HTTP Request to {Url}", url); + }; + + settings.AfterCall = call => + { + var statusCode = call.Response?.StatusCode.ToString() ?? "(No response)"; + var url = urlInterceptor(call.Request.Url.Clone()); + log.Debug("HTTP Response {Status} from {Url}", statusCode, url); + }; + } + } +} diff --git a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs index 695e8fbb..3757a2cf 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Flurl; using Flurl.Http; using Newtonsoft.Json.Linq; using TrashLib.Config; @@ -17,18 +16,16 @@ namespace TrashLib.Radarr.CustomFormat.Api _serverInfo = serverInfo; } - private string BaseUrl => _serverInfo.BuildUrl(); - public async Task> GetCustomFormats() { - return await BaseUrl + return await BuildRequest() .AppendPathSegment("customformat") .GetJsonAsync>(); } public async Task CreateCustomFormat(ProcessedCustomFormatData cf) { - var response = await BaseUrl + var response = await BuildRequest() .AppendPathSegment("customformat") .PostJsonAsync(cf.Json) .ReceiveJson(); @@ -41,7 +38,7 @@ namespace TrashLib.Radarr.CustomFormat.Api public async Task UpdateCustomFormat(ProcessedCustomFormatData cf) { - await BaseUrl + await BuildRequest() .AppendPathSegment($"customformat/{cf.GetCustomFormatId()}") .PutJsonAsync(cf.Json) .ReceiveJson(); @@ -49,9 +46,11 @@ namespace TrashLib.Radarr.CustomFormat.Api public async Task DeleteCustomFormat(int customFormatId) { - await BaseUrl + await BuildRequest() .AppendPathSegment($"customformat/{customFormatId}") .DeleteAsync(); } + + private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs b/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs index fa2e9898..8131c7bc 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Flurl; using Flurl.Http; using Newtonsoft.Json.Linq; using TrashLib.Config; @@ -16,21 +15,21 @@ namespace TrashLib.Radarr.CustomFormat.Api _serverInfo = serverInfo; } - private string BaseUrl => _serverInfo.BuildUrl(); - public async Task> GetQualityProfiles() { - return await BaseUrl + return await BuildRequest() .AppendPathSegment("qualityprofile") .GetJsonAsync>(); } public async Task UpdateQualityProfile(JObject profileJson, int id) { - return await BaseUrl + return await BuildRequest() .AppendPathSegment($"qualityprofile/{id}") .PutJsonAsync(profileJson) .ReceiveJson(); } + + private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); } } diff --git a/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs b/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs index f38c6e8a..e2433d38 100644 --- a/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs +++ b/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Flurl; using Flurl.Http; using TrashLib.Config; using TrashLib.Radarr.QualityDefinition.Api.Objects; @@ -16,11 +15,9 @@ namespace TrashLib.Radarr.QualityDefinition.Api _serverInfo = serverInfo; } - private string BaseUrl => _serverInfo.BuildUrl(); - public async Task> GetQualityDefinition() { - return await BaseUrl + return await BuildRequest() .AppendPathSegment("qualitydefinition") .GetJsonAsync>(); } @@ -28,10 +25,12 @@ namespace TrashLib.Radarr.QualityDefinition.Api public async Task> UpdateQualityDefinition( IList newQuality) { - return await BaseUrl + return await BuildRequest() .AppendPathSegment("qualityDefinition/update") .PutJsonAsync(newQuality) .ReceiveJson>(); } + + private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); } } diff --git a/src/TrashLib/Radarr/RadarrAutofacModule.cs b/src/TrashLib/Radarr/RadarrAutofacModule.cs index 29797567..efc18a30 100644 --- a/src/TrashLib/Radarr/RadarrAutofacModule.cs +++ b/src/TrashLib/Radarr/RadarrAutofacModule.cs @@ -1,5 +1,6 @@ using Autofac; using Autofac.Extras.AggregateService; +using Serilog; using TrashLib.Config; using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat; @@ -27,7 +28,8 @@ namespace TrashLib.Radarr builder.Register(c => { var config = c.Resolve().ActiveConfiguration; - return new ServerInfo(config.BaseUrl, config.ApiKey); + var log = c.Resolve(); + return new ServerInfo(config.BaseUrl, config.ApiKey, log); }) .As(); diff --git a/src/TrashLib/Sonarr/Api/SonarrApi.cs b/src/TrashLib/Sonarr/Api/SonarrApi.cs index 8048dcdf..babdc206 100644 --- a/src/TrashLib/Sonarr/Api/SonarrApi.cs +++ b/src/TrashLib/Sonarr/Api/SonarrApi.cs @@ -1,23 +1,29 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Flurl; using Flurl.Http; using Newtonsoft.Json.Linq; +using Serilog; using TrashLib.Config; +using TrashLib.Extensions; using TrashLib.Sonarr.Api.Objects; namespace TrashLib.Sonarr.Api { public class SonarrApi : ISonarrApi { + private readonly ILogger _log; private readonly ISonarrReleaseProfileCompatibilityHandler _profileHandler; private readonly IServerInfo _serverInfo; - public SonarrApi(IServerInfo serverInfo, ISonarrReleaseProfileCompatibilityHandler profileHandler) + public SonarrApi( + IServerInfo serverInfo, + ISonarrReleaseProfileCompatibilityHandler profileHandler, + ILogger log) { _serverInfo = serverInfo; _profileHandler = profileHandler; + _log = log; } public async Task> GetTags() @@ -79,6 +85,6 @@ namespace TrashLib.Sonarr.Api .ReceiveJson>(); } - private string BaseUrl() => _serverInfo.BuildUrl(); + private IFlurlRequest BaseUrl() => _serverInfo.BuildRequest().SanitizedLogging(_log); } } diff --git a/src/TrashLib/Sonarr/SonarrCompatibility.cs b/src/TrashLib/Sonarr/SonarrCompatibility.cs index 9c79fa9b..34523bad 100644 --- a/src/TrashLib/Sonarr/SonarrCompatibility.cs +++ b/src/TrashLib/Sonarr/SonarrCompatibility.cs @@ -1,7 +1,6 @@ using System; using System.Reactive.Linq; using System.Reactive.Threading.Tasks; -using Flurl; using Flurl.Http; using TrashLib.Config; @@ -13,7 +12,7 @@ namespace TrashLib.Sonarr public SonarrCompatibility(IServerInfo serverInfo) { - var task = serverInfo.BuildUrl() + var task = serverInfo.BuildRequest() .AppendPathSegment("system/status") .GetJsonAsync();