Merge pull request #23 from xpsony/multiInstance_serilog

Multi *Arr Instance Support , Add Serilog for better logging, Add Fluent Validator
This commit is contained in:
Jonas F
2024-09-04 19:14:53 +02:00
committed by GitHub
26 changed files with 1366 additions and 752 deletions

View File

@@ -0,0 +1,69 @@
using System.Collections;
using System.Text.RegularExpressions;
using Serilog.Core;
using Serilog.Events;
namespace UmlautAdaptarr.Utilities;
public class ApiKeyMaskingEnricher : ILogEventEnricher
{
private readonly List<string> apiKeys = new();
public ApiKeyMaskingEnricher(string appsetting)
{
ExtractApiKeysFromAppSettings(appsetting);
ExtractApiKeysFromEnvironmentVariables();
apiKeys = new List<string>(apiKeys.Distinct());
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
//if (logEvent.Properties.TryGetValue("apikey", out var value) && value is ScalarValue scalarValue)
//{
var maskedValue = new ScalarValue("**Hidden Api Key**");
foreach (var apikey in apiKeys) logEvent.AddOrUpdateProperty(new LogEventProperty(apikey, maskedValue));
// }
}
/// <summary>
/// Scan all Env Variabels for known Apikeys
/// </summary>
/// <returns>List of all Apikeys</returns>
public List<string> ExtractApiKeysFromEnvironmentVariables()
{
var envVariables = Environment.GetEnvironmentVariables();
foreach (DictionaryEntry envVariable in envVariables)
if (envVariable.Key.ToString()!.Contains("ApiKey"))
apiKeys.Add(envVariable.Value.ToString());
return apiKeys;
}
public List<string> ExtractApiKeysFromAppSettings(string filePath)
{
try
{
if (File.Exists(filePath))
{
var fileContent = File.ReadAllText(filePath);
var pattern = "\"ApiKey\": \"(.*?)\"";
var regex = new Regex(pattern);
var matches = regex.Matches(fileContent);
foreach (Match match in matches) apiKeys.Add(match.Groups[1].Value);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
return apiKeys;
}
}

View File

@@ -0,0 +1,88 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace UmlautAdaptarr.Utilities;
public static class Helper
{
public static void ShowLogo()
{
Console.WriteLine(
"\r\n _ _ _ _ ___ _ _ \r\n| | | | | | | | / _ \\ | | | | \r\n| | | |_ __ ___ | | __ _ _ _| |_/ /_\\ \\ __| | __ _ _ __ | |_ __ _ _ __ _ __ \r\n| | | | '_ ` _ \\| |/ _` | | | | __| _ |/ _` |/ _` | '_ \\| __/ _` | '__| '__|\r\n| |_| | | | | | | | (_| | |_| | |_| | | | (_| | (_| | |_) | || (_| | | | | \r\n \\___/|_| |_| |_|_|\\__,_|\\__,_|\\__\\_| |_/\\__,_|\\__,_| .__/ \\__\\__,_|_| |_| \r\n | | \r\n |_| \r\n");
}
public static void ShowInformation()
{
Console.WriteLine("--------------------------[IP Leak Test]-----------------------------");
var ipInfo = GetPublicIpAddressInfoAsync().GetAwaiter().GetResult();
if (ipInfo != null)
{
Console.WriteLine($"Your Public IP Address is '{ipInfo.Ip}'");
Console.WriteLine($"Hostname: {ipInfo.Hostname}");
Console.WriteLine($"City: {ipInfo.City}");
Console.WriteLine($"Region: {ipInfo.Region}");
Console.WriteLine($"Country: {ipInfo.Country}");
Console.WriteLine($"Provider: {ipInfo.Org}");
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Error: Could not retrieve public IP information.");
Console.ResetColor();
}
Console.WriteLine("--------------------------------------------------------------------");
}
private static async Task<IpInfo?> GetPublicIpAddressInfoAsync()
{
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(10);
try
{
var response = await client.GetAsync("https://ipinfo.io/json");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<IpInfo>(content);
}
catch
{
return null;
}
}
}
}
public class IpInfo
{
[JsonPropertyName("ip")]
public string Ip { get; set; }
[JsonPropertyName("hostname")]
public string Hostname { get; set; }
[JsonPropertyName("city")]
public string City { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; }
[JsonPropertyName("country")]
public string Country { get; set; }
[JsonPropertyName("loc")]
public string Loc { get; set; }
[JsonPropertyName("org")]
public string Org { get; set; }
[JsonPropertyName("postal")]
public string Postal { get; set; }
[JsonPropertyName("timezone")]
public string Timezone { get; set; }
}

View File

@@ -0,0 +1,74 @@
using System.Collections;
using System.Collections.ObjectModel;
namespace UmlautAdaptarr.Utilities
{
// License: This code is published under the MIT license.
// Source: https://stackoverflow.com/questions/77559201/
public static class KeyedServiceExtensions
{
public static void AllowResolvingKeyedServicesAsDictionary(
this IServiceCollection sc)
{
// KeyedServiceCache caches all the keys of a given type for a
// specific service type. By making it a singleton we only have
// determine the keys once, which makes resolving the dict very fast.
sc.AddSingleton(typeof(KeyedServiceCache<,>));
// KeyedServiceCache depends on the IServiceCollection to get
// the list of keys. That's why we register that here as well, as it
// is not registered by default in MS.DI.
sc.AddSingleton(sc);
// Last we make the registration for the dictionary itself, which maps
// to our custom type below. This registration must be transient, as
// the containing services could have any lifetime and this registration
// should by itself not cause Captive Dependencies.
sc.AddTransient(typeof(IDictionary<,>), typeof(KeyedServiceDictionary<,>));
// For completeness, let's also allow IReadOnlyDictionary to be resolved.
sc.AddTransient(
typeof(IReadOnlyDictionary<,>), typeof(KeyedServiceDictionary<,>));
}
// We inherit from ReadOnlyDictionary, to disallow consumers from changing
// the wrapped dependencies while reusing all its functionality. This way
// we don't have to implement IDictionary<T,V> ourselves; too much work.
private sealed class KeyedServiceDictionary<TKey, TService>(
KeyedServiceCache<TKey, TService> keys, IServiceProvider provider)
: ReadOnlyDictionary<TKey, TService>(Create(keys, provider))
where TKey : notnull
where TService : notnull
{
private static Dictionary<TKey, TService> Create(
KeyedServiceCache<TKey, TService> keys, IServiceProvider provider)
{
var dict = new Dictionary<TKey, TService>(capacity: keys.Keys.Length);
foreach (TKey key in keys.Keys)
{
dict[key] = provider.GetRequiredKeyedService<TService>(key);
}
return dict;
}
}
private sealed class KeyedServiceCache<TKey, TService>(IServiceCollection sc)
where TKey : notnull
where TService : notnull
{
// Once this class is resolved, all registrations are guaranteed to be
// made, so we can, at that point, safely iterate the collection to get
// the keys for the service type.
public TKey[] Keys { get; } = (
from service in sc
where service.ServiceKey != null
where service.ServiceKey!.GetType() == typeof(TKey)
where service.ServiceType == typeof(TService)
select (TKey)service.ServiceKey!)
.ToArray();
}
}
}

View File

@@ -1,92 +1,197 @@
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Options.ArrOptions;
using FluentValidation;
using System.Linq.Expressions;
using UmlautAdaptarr.Interfaces;
using UmlautAdaptarr.Options;
using UmlautAdaptarr.Options.ArrOptions.InstanceOptions;
using UmlautAdaptarr.Providers;
using UmlautAdaptarr.Services;
using UmlautAdaptarr.Validator;
namespace UmlautAdaptarr.Utilities
namespace UmlautAdaptarr.Utilities;
/// <summary>
/// Extension methods for configuring services related to ARR Applications
/// </summary>
public static class ServicesExtensions
{
/// <summary>
/// Extension methods for configuring services related to ARR Applications
/// Logger instance for logging proxy configurations.
/// </summary>
public static class ServicesExtensions
private static ILogger Logger = GlobalStaticLogger.Logger;
/// <summary>
/// Adds a service with specified options and service to the service collection.
/// </summary>
/// <typeparam name="TOptions">The options type for the service.</typeparam>
/// <typeparam name="TService">The service type for the service.</typeparam>
/// <typeparam name="TInterface">The Interface of the service type</typeparam>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <param name="sectionName">The name of the configuration section containing service options.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
private static WebApplicationBuilder AddServicesWithOptions<TOptions, TService, TInterface>(
this WebApplicationBuilder builder, string sectionName)
where TOptions : class, new()
where TService : class, TInterface
where TInterface : class
{
/// <summary>
/// Adds a service with specified options and service to the service collection.
/// </summary>
/// <typeparam name="TOptions">The options type for the service.</typeparam>
/// <typeparam name="TService">The service type for the service.</typeparam>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <param name="sectionName">The name of the configuration section containing service options.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
private static WebApplicationBuilder AddServiceWithOptions<TOptions, TService>(this WebApplicationBuilder builder, string sectionName)
where TOptions : class
where TService : class
try
{
if (builder.Services == null)
if (builder.Services == null) throw new ArgumentNullException(nameof(builder), "Service collection is null.");
var singleInstance = builder.Configuration.GetSection(sectionName).Get<TOptions>();
var singleHost = (string?)typeof(TOptions).GetProperty("Host")?.GetValue(singleInstance, null);
// If we have no Single Instance , we try to parse for a Array
var optionsArray = singleHost == null
? builder.Configuration.GetSection(sectionName).Get<TOptions[]>()
:
[
singleInstance
];
if (optionsArray == null || !optionsArray.Any())
throw new InvalidOperationException(
$"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable.");
foreach (var option in optionsArray)
{
throw new ArgumentNullException(nameof(builder), "Service collection is null.");
GlobalInstanceOptionsValidator validator = new GlobalInstanceOptionsValidator();
var results = validator.Validate(option as GlobalInstanceOptions);
if (!results.IsValid)
{
foreach (var failure in results.Errors)
{
Console.WriteLine(($"Property {failure.PropertyName } failed validation. Error was: {failure.ErrorMessage}"));
}
throw new Exception("Please fix first you config and then Start UmlautAdaptarr again");
}
var instanceState = (bool)(typeof(TOptions).GetProperty("Enabled")?.GetValue(option, null) ?? false);
// We only want to create instances that are enabled in the Configs
if (instanceState)
{
// User can give the Instance a readable Name otherwise we use the Host Property
var instanceName = (string)(typeof(TOptions).GetProperty("Name")?.GetValue(option, null) ??
(string)typeof(TOptions).GetProperty("Host")?.GetValue(option, null)!);
// Dark Magic , we don't know the Property's of TOptions , and we won't cast them for each Options
// Todo eventuell schönere Lösung finden
var paraexpression = Expression.Parameter(Type.GetType(option.GetType().FullName), "x");
foreach (var prop in option.GetType().GetProperties())
{
var val = Expression.Constant(prop.GetValue(option));
var memberexpression = Expression.PropertyOrField(paraexpression, prop.Name);
if (prop.PropertyType == typeof(int) || prop.PropertyType == typeof(string) || prop.PropertyType == typeof(bool))
{
var assign = Expression.Assign(memberexpression, Expression.Convert(val, prop.PropertyType));
var exp = Expression.Lambda<Action<TOptions>>(assign, paraexpression);
builder.Services.Configure(instanceName, exp.Compile());
}
else
{
Logger.LogWarning((prop.PropertyType + "No Support"));
}
}
builder.Services.AddKeyedSingleton<TInterface, TService>(instanceName);
}
}
var options = builder.Configuration.GetSection(sectionName).Get<TOptions>();
if (options == null)
{
throw new InvalidOperationException($"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable.");
}
builder.Services.Configure<TOptions>(builder.Configuration.GetSection(sectionName));
builder.Services.AddSingleton<TService>();
return builder;
}
/// <summary>
/// Adds support for Sonarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddSonarrSupport(this WebApplicationBuilder builder)
catch (Exception e)
{
return builder.AddServiceWithOptions<SonarrInstanceOptions, SonarrClient>("Sonarr");
Console.WriteLine("Error while Init UmlautAdaptrr");
throw;
}
/// <summary>
/// Adds support for Lidarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddLidarrSupport(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<LidarrInstanceOptions, LidarrClient>("Lidarr");
}
/// <summary>
/// Adds support for Readarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddReadarrSupport(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<ReadarrInstanceOptions, ReadarrClient>("Readarr");
}
/// <summary>
/// Adds a title lookup service to the service collection.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddTitleLookupService(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<GlobalOptions, TitleApiService>("Settings");
}
/// <summary>
/// Adds a proxy request service to the service collection.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder"/> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder"/>.</returns>
public static WebApplicationBuilder AddProxyRequestService(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<GlobalOptions, ProxyRequestService>("Settings");
}
}
}
/// <summary>
/// Adds a service with specified options and service to the service collection.
/// </summary>
/// <typeparam name="TOptions">The options type for the service.</typeparam>
/// <typeparam name="TService">The service type for the service.</typeparam>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <param name="sectionName">The name of the configuration section containing service options.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
private static WebApplicationBuilder AddServiceWithOptions<TOptions, TService>(this WebApplicationBuilder builder,
string sectionName)
where TOptions : class
where TService : class
{
if (builder.Services == null) throw new ArgumentNullException(nameof(builder), "Service collection is null.");
var options = builder.Configuration.GetSection(sectionName).Get<TOptions>();
if (options == null)
throw new InvalidOperationException(
$"{typeof(TService).Name} options could not be loaded from Configuration or ENV Variable.");
builder.Services.Configure<TOptions>(builder.Configuration.GetSection(sectionName));
builder.Services.AddSingleton<TService>();
return builder;
}
/// <summary>
/// Adds support for Sonarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddSonarrSupport(this WebApplicationBuilder builder)
{
// builder.Serviceses.AddSingleton<IOptionsMonitoSonarrInstanceOptionsns>, OptionsMonitoSonarrInstanceOptionsns>>();
return builder.AddServicesWithOptions<SonarrInstanceOptions, SonarrClient, IArrApplication>("Sonarr");
}
/// <summary>
/// Adds support for Lidarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddLidarrSupport(this WebApplicationBuilder builder)
{
return builder.AddServicesWithOptions<LidarrInstanceOptions, LidarrClient, IArrApplication>("Lidarr");
}
/// <summary>
/// Adds support for Readarr with default options and client.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddReadarrSupport(this WebApplicationBuilder builder)
{
return builder.AddServicesWithOptions<ReadarrInstanceOptions, ReadarrClient, IArrApplication>("Readarr");
}
/// <summary>
/// Adds a title lookup service to the service collection.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddTitleLookupService(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<GlobalOptions, TitleApiService>("Settings");
}
/// <summary>
/// Adds a proxy request service to the service collection.
/// </summary>
/// <param name="builder">The <see cref="WebApplicationBuilder" /> to configure the service collection.</param>
/// <returns>The configured <see cref="WebApplicationBuilder" />.</returns>
public static WebApplicationBuilder AddProxyRequestService(this WebApplicationBuilder builder)
{
return builder.AddServiceWithOptions<GlobalOptions, ProxyRequestService>("Settings");
}
}