55 Commits

Author SHA1 Message Date
Developer 02
65c64a3f9a chore(API): Aktualisiert auf 2.0.1 2024-12-02 11:35:33 +01:00
Developer 02
1d600aa453 refactor: Nullbarkeitsannotation zu ControllerExtensions-Methoden hinzugefügt 2024-12-02 11:33:52 +01:00
Developer 02
816d5835f1 fix(BaseHttpClientService): Die Kodierung der Abfrageparameter wurde entfernt, da UriBuilder dies bereits tut.
- Hochgestuft auf 2.0.3
2024-11-26 23:45:06 +01:00
Developer 02
4a64a31d47 refactor(Abstraction): Hochgestuft auf 2.2.1. 2024-11-25 14:30:20 +01:00
Developer 02
e9b2ba788f refactor(IHttpClientOptions): Konvertiert Typ der Header von Key Value Pair Liste zu Dictionary. 2024-11-25 14:29:21 +01:00
Developer 02
e53813500a chore(Core.Client): aufgerüstet auf 2.0.1 2024-11-25 14:05:17 +01:00
Developer 02
25e3855de2 refactor(DIExtensions): BaseHttpClientService-Injektion entfernen 2024-11-25 14:02:54 +01:00
Developer 02
dd3d6e70cc chore(Client): hochgestuft auf 2.2.0 2024-11-25 11:49:41 +01:00
Developer 02
02a87309df chore(Abstraktionen): hochgestuft auf 2.2.0 2024-11-25 11:47:45 +01:00
Developer 02
0f7bdc9d0e refactor: Vereinfachung der Logik zur Zusammenführung von Headern und Abfrageparametern in BaseHttpClientService
- Verbesserung der Zusammenführung von Standard-Headern und Abfrageparametern durch Null-Prüfung und übersichtlichere Handhabung.
- Sicherstellung, dass die Logik einfacher zu verstehen und zu warten ist.
2024-11-25 11:46:08 +01:00
Developer 02
f9df2fb29e feat: DI-Erweiterung für HttpClient-Services verbessert
- `AddHttpClientService` refaktoriert, um `AddHttpClientServiceDefaults` für gemeinsame Setup-Logik einzuführen.
- Überladungen für `AddHttpClientService` hinzugefügt, um sowohl `IConfigurationSection` als auch direkte Options-Instanz zu unterstützen.
- Bessere Erweiterbarkeit und sauberere Service-Konfiguration sichergestellt.
2024-11-25 11:35:46 +01:00
Developer 02
ef7da0e52c refactor(HttpClientOptions): entfernt und durch IHttpClientOptions mit Klasseneinschränkung ersetzt 2024-11-25 11:19:41 +01:00
Developer 02
f602a842be revert: IHttpClientOptions entfernen 2024-11-25 11:03:15 +01:00
Developer 02
52a7664e57 fix(Client.DIExtensions): Generischer Typ zur Injektion mehrerer Client-Dienste hinzugefügt 2024-11-25 10:58:45 +01:00
Developer 02
ea3d1312b8 refactor(BaseHttpClientService): aktualisiert, um den Pfad zu initialisieren emty string, wenn es null ist und HttpClientOptions ohne IOptions hinzugefügt 2024-11-25 10:54:34 +01:00
Developer 02
3b8b315fea refactor(IHttpClientOptions): removed 2024-11-25 10:42:22 +01:00
Developer 02
c65eefb954 feat(IHttpClientOptions): Abfrage-Parameter und Header hinzugefügt.
- Geordnete DI-Erweiterungen.
2024-11-25 10:30:33 +01:00
Developer 02
997fd533ac feat(BaseHttpClientService): Path getter und intter entfernt und geschützte readonly _path Variable hinzugefügt. 2024-11-25 10:11:13 +01:00
Developer 02
bcfb5a8a70 feat(BaseHttpClientService): Uri Getter und Setter entfernt und geschützte readonly _uri Variable hinzugefügt. 2024-11-25 10:09:05 +01:00
Developer 02
049e9977f4 feat(BaseHttpClientService): Optionale Standard-Header und QueryParams hinzugefügt 2024-11-25 10:05:42 +01:00
Developer 02
0334fc4cdf refactor(BaseHttpClientService): Verwendung von IEnumerable<KeyValuePair<string, object>> anstelle von Dictionary<string, string> in queryParams, form und headers inputs der FetchAsync Methode 2024-11-25 09:28:47 +01:00
Developer 02
0c2334cefb refactor(BaseHttpClientService): Der Wert von query params wurde zum nullbaren Objekt, um Flag-Parameter hinzuzufügen.
- Aktualisierte Schnittstelle und Logik unter Berücksichtigung dieser Situation
2024-11-22 14:35:22 +01:00
Developer 02
dd7f1c1ea0 fix(BaseHttpClientService): Null-Kontrolle zum Pfad hinzugefügt 2024-11-22 13:19:42 +01:00
Developer 02
4bb242a4cc feat(Tests.Client.BaseHttpClientServiceTests): Test für Abfrageparameter hinzugefügt 2024-11-22 12:48:36 +01:00
Developer 02
b577067379 chore: gitignore aktualisieren 2024-11-22 12:47:24 +01:00
Developer 02
bd4d4856ea feat(IHttpClientOptions):
Basispfad zu http-Client-Optionen hinzugefügt
2024-11-22 12:05:13 +01:00
Developer 02
c3a12ba5b7 chore: hochgestuft auf 1.1.0 2024-11-22 10:30:26 +01:00
Developer 02
478bf13a4a Revert "chore(Client): hochgestuft auf 2,1"
This reverts commit d8849f48da.
2024-11-22 10:29:03 +01:00
Developer 02
d8849f48da chore(Client): hochgestuft auf 2,1 2024-11-22 10:26:49 +01:00
Developer 02
c466c553dc chore: hochgestuft auf 2.1 2024-11-22 10:25:32 +01:00
Developer 02
48afa6b433 feat(BaseHttpClientService.FetchAsync): Schema-, Port-, Pfad- und Query-Parameter-Optionen hinzugefügt 2024-11-22 10:12:40 +01:00
Developer 02
e44b2895c9 feat(IHttpClientOptions): erstellt zur Abstraktion 2024-11-22 09:18:19 +01:00
Developer 02
85e5fc4018 feat(BaseHttpClientService): Header hinzugefügt 2024-11-22 09:15:04 +01:00
Developer 02
70ccec9fef feat(RSAFactory.ReadRSADecryptorAsync): ReadRSADecryptorAsync-Methode hinzugefügt, um die pem-Datei zu lesen und den Decryptor asynchron zu erstellen 2024-11-22 09:06:58 +01:00
Developer 02
f7193594b1 fix(RSAFactory): Dateiname und seine Erweiterung aus der Methode DefaultRSAKeyNameFormatter entfernt 2024-11-20 17:14:12 +01:00
Developer 02
9c7319634a fix(RSAExtensions): Schlüssel in Dateinamen umbenennen 2024-11-20 17:11:44 +01:00
Developer 02
3becb208ec fix(RSAExtensions): Falsche Methoden zur Schlüsselbenennung entfernt 2024-11-20 17:10:57 +01:00
Developer 02
1b00f9afa2 feat(CryptFactory): Der Parameter seperator der Funktionseigenschaft CryptFactory.RSAKeyNameFormatter ist jetzt nullbar. 2024-11-20 16:47:26 +01:00
Developer 02
b58d4aed2f feat(RSAFactory): Statische Readonly-Eigenschaft hinzugefügt, um den Standard-RSA-Schlüsselnamen-Separator zu speichern 2024-11-20 16:43:27 +01:00
Developer 02
5adc67edf2 feat (CryptFactory): Verschieben der Standardparameter des RSA-Namensformatierers in die RSAFactory 2024-11-20 16:40:24 +01:00
Developer 02
0ff0de8159 feat (CryptFactory.RSADecryptorKeyFormatter): aktualisiert, um die erforderlichen Parameter als Eingabe zu nehmen, anstatt IRSADecryptor direkt als Eingabe zu nehmen 2024-11-20 16:37:09 +01:00
Developer 02
49b49271f3 feat(CryptFactory): ValidateForbidden ve ValidateSeparator Methoden in DefaultRSADecryptorKeyFormatter hinzugefügt 2024-11-20 15:13:05 +01:00
Developer 02
5c5a6bd181 feat(CryptFactory): RSADecryptorKeyFormatter Funktionseigenschaft hinzugefügt, um standardisierte Schlüsselnamen zu erstellen 2024-11-20 14:18:55 +01:00
Developer 02
6ab1777f7c refactor(RSADecryptor): aktualisiert, um im Passwort- und Versions-Tupel-Format zu initieren, um Datenintegrität zu gewährleisten.
- password und PasswordVersion initter entfernt.
2024-11-20 12:49:36 +01:00
Developer 02
103ddf5c2e feat(RSADecryptor): PasswordVersion-Eigenschaft hinzugefügt. Password.get intern gemacht.
- Password.get entfernt und PasswordVersion-Eigenschaft in IRSADecryptor hinzugefügt
2024-11-20 11:17:38 +01:00
Developer 02
f9c94e8464 refactor(IRSADecryptor): HasEncryptedPem getter-Methode hinzugefügt 2024-11-20 10:52:39 +01:00
Developer 02
cdb0009e7c refactor(RSADecryptor): statt der Verwendung einer separaten init-Methode zur Initialisierung von RSA, wurde Lazy Loading verwendet. 2024-11-20 10:51:18 +01:00
Developer 02
5010224500 feat(RSADecryptor): Eigenschaft hinzugefügt, um zu prüfen, ob der RSADecryptor pem verschlüsselt hat. 2024-11-20 10:38:34 +01:00
Developer 02
1ebdd7e5bb feat(RSADecryptor): Öffentliche Constructure-Methode gemacht. 2024-11-20 10:33:11 +01:00
Developer 02
0e0513e640 feat(RSAExtensions): Methoden zum Speichern von IRSACryptographer.Pem erstellt. 2024-11-20 10:32:25 +01:00
Developer 02
683b95c205 refactor(RSAExtensions): GetRSADecryptor, TryGetRSADecryptor, GetRSAEncryptor und TryGetRSADecryptor Methoden hinzugefügt. 2024-11-20 00:15:27 +01:00
Developer 02
f28b43cc06 refactor(RSADecryptor): Lazy Loading in Encryptor Getter integriert, um die Leistung zu verbessern. 2024-11-19 23:58:04 +01:00
Developer 02
777a8a73ac refactor: AddSecurity-Methode aktualisiert, um ICryptFactory direkt mit der CryptFactory-Instanz zu registrieren 2024-11-19 23:51:49 +01:00
Developer 02
77fc06991b feat(CryptFactory): Erstellung einer separaten RSAFactory zur Erzeugung einer statischen Instanz 2024-11-19 23:49:34 +01:00
Developer 02
eeb50e837d feat: Unterstützung für IRSADecryptor und Verwaltung der RSA-Entschlüsselung in den Klassen ICryptFactory und CryptFactory hinzugefügt
ICryptFactory:
- `IRSADecryptor this[string key]`-Indexer für den Zugriff auf Entschlüssler per Schlüssel hinzugefügt.
- Methode `TryGetRSADecryptor` für das sichere Abrufen von Entschlüsslern eingeführt.

CryptFactory:
- `IRSADecryptor`-Indexer für die Verwaltung von Entschlüsslern implementiert.
- Ein `Decryptors`-Dictionary hinzugefügt, um RSA-Entschlüssler nach Schlüssel zu speichern.
- Konstruktor aktualisiert, um `Decryptors` mit einem bereitgestellten oder leeren Dictionary zu initialisieren.
- `TryGetRSADecryptor` zur Entschlüssler-Abfrage implementiert.
2024-11-19 23:14:44 +01:00
24 changed files with 441 additions and 149 deletions

1
.gitignore vendored
View File

@@ -409,3 +409,4 @@ FodyWeavers.xsd
/DigitalData.Core.ConsoleApp/Program.cs
/DigitalData.Core.ConsoleApp/FooHttpOptions.cs
/DigitalData.Core.Tests/obj/
/DigitalData.Core.Terminal

View File

@@ -19,7 +19,7 @@ namespace DigitalData.Core.API
/// <param name="index">The key in the ViewData dictionary where the value will be stored.</param>
/// <param name="value">The value to be stored in the ViewData dictionary.</param>
/// <returns>The same ViewResult object with updated ViewData, allowing for additional chained operations.</returns>
public static ViewResult WithData(this ViewResult viewResult, string index, object value)
public static ViewResult WithData(this ViewResult viewResult, string index, object? value)
{
viewResult.ViewData[index] = value;
return viewResult;

View File

@@ -8,7 +8,7 @@
<OutputType>Library</OutputType>
<Description>This package provides a comprehensive set of API controllers and related utilities for the DigitalData.Core library. It includes generic CRUD controllers, localization extensions, middleware for security policies, and application model conventions.</Description>
<PackageId>DigitalData.Core.API</PackageId>
<Version>2.0.0.0</Version>
<Version>2.0.1</Version>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>DigitalData.Core.API</Product>
@@ -16,6 +16,8 @@
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
<PackageTags>digital data core api</PackageTags>
<PackageIcon>core_icon.png</PackageIcon>
<AssemblyVersion>2.0.1</AssemblyVersion>
<FileVersion>2.0.1</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@@ -4,15 +4,17 @@ namespace DigitalData.Core.Abstractions.Client
{
public interface IBaseHttpClientService
{
public string Uri { get; init; }
public CookieCollection GetCookies(string route = "");
CookieCollection GetCookies(string path = "");
Task<HttpResponseMessage> FetchAsync(
string route = "",
string? scheme = null,
int? port = null,
string path = "",
IEnumerable<KeyValuePair<string, object?>>? queryParams = null,
HttpMethod? method = null,
HttpContent? body = null,
Dictionary<string, string>? form = null,
IEnumerable<KeyValuePair<string, object>>? form = null,
IEnumerable<KeyValuePair<string, object>>? headers = null,
bool sendWithCookie = true,
bool saveCookie = true
);

View File

@@ -0,0 +1,13 @@
namespace DigitalData.Core.Abstractions.Client
{
public interface IHttpClientOptions
{
string Uri { get; init; }
string? Path { get; init; }
Dictionary<string, object>? Headers { get; init; }
Dictionary<string, object?>? QueryParams { get; init; }
}
}

View File

@@ -1,6 +1,6 @@
namespace DigitalData.Core.Abstractions.Client
{
public interface IHttpClientService<TClientOptions> : IBaseHttpClientService
public interface IHttpClientService<TClientOptions> : IBaseHttpClientService where TClientOptions : IHttpClientOptions
{
}
}

View File

@@ -16,9 +16,10 @@
<PackageProjectUrl></PackageProjectUrl>
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
<PackAsTool>False</PackAsTool>
<NeutralLanguage>aa-DJ</NeutralLanguage>
<PackageIcon>core_icon.png</PackageIcon>
<Version>2.0.0.0</Version>
<Version>2.2.1</Version>
<AssemblyVersion>2.2.1</AssemblyVersion>
<FileVersion>2.2.1</FileVersion>
</PropertyGroup>
<ItemGroup>

View File

@@ -4,19 +4,33 @@ namespace DigitalData.Core.Abstractions.Security
{
public interface ICryptFactory
{
public int KeySizeInBits { get; init; }
int KeySizeInBits { get; init; }
public string PbePassword { init; }
string PbePassword { init; }
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; }
PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; }
public HashAlgorithmName PbeHashAlgorithmName { get; init; }
HashAlgorithmName PbeHashAlgorithmName { get; init; }
public int PbeIterationCount { get; init; }
int PbeIterationCount { get; init; }
public PbeParameters PbeParameters { get; }
PbeParameters PbeParameters { get; }
public string EncryptedPrivateKeyPemLabel { get; init; }
string EncryptedPrivateKeyPemLabel { get; init; }
/// <summary>
/// Gets the formatter function for generating RSA key names.
/// This formatter takes an issuer, audience, isPrivate, and optional version and separator
/// to produce a formatted string used for the key naming convention.
/// </summary>
/// <param name="issuer">A string representing the issuer of the key. It should not contain invalid file name characters or the separator.</param>
/// <param name="audience">A string representing the audience for which the key is intended. It should not contain invalid file name characters or the separator.</param>
/// <param name="isPrivate">An bool to check if the key is private.</param>
/// <param name="version">An instance of the <see cref="Version?"/> interface, which is used to keep the version of Pbe password.</param>
/// <param name="separator">An optional string separator used to separate the issuer and audience. The default is "-_-". It should not be included in the issuer or audience strings.</param>
/// <returns>A formatted string combining the issuer, audience, and separator, which adheres to valid file naming rules.</returns>
/// <exception cref="ArgumentException">Thrown when the issuer, audience, or separator contains invalid characters or when the separator is present within the issuer or audience.</exception>
Func<string, string, bool, Version?, string?, string> RSAKeyNameFormatter { get; }
string CreateRSAPrivateKeyPem(int? keySizeInBits = null);
@@ -26,5 +40,9 @@ namespace DigitalData.Core.Abstractions.Security
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
HashAlgorithmName? hashAlgorithmName = null,
int? iterationCount = null);
IRSADecryptor this[string key] { get; }
bool TryGetRSADecryptor(string key, out IRSADecryptor? decryptor);
}
}

View File

@@ -2,12 +2,16 @@
{
public interface IRSADecryptor : IRSACryptographer
{
public string? Password { get; init; }
(string Value, Version Version)? VersionedPassword { init; }
public IRSAEncryptor Encryptor { get; }
Version? PasswordVersion { get; }
public byte[] Decrypt(byte[] data);
bool HasEncryptedPem { get; }
public string Decrypt(string data);
IRSAEncryptor Encryptor { get; }
byte[] Decrypt(byte[] data);
string Decrypt(string data);
}
}

View File

@@ -1,44 +1,113 @@
using DigitalData.Core.Abstractions.Client;
using Microsoft.Extensions.Options;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Web;
namespace DigitalData.Core.Client
{
public class BaseHttpClientService : IBaseHttpClientService
{
protected readonly HttpClient _client;
protected readonly CookieContainer _cookies;
protected readonly CookieContainer _cookies;
[StringSyntax("Uri")]
public string Uri { get; init; }
protected readonly string _uri;
public BaseHttpClientService(HttpClient client, CookieContainer cookieContainer, IOptions<HttpClientOptions> clientOptions)
protected readonly string _path;
protected IEnumerable<KeyValuePair<string, object>>? _headers;
protected IEnumerable<KeyValuePair<string, object?>>? _queryParams;
internal BaseHttpClientService(HttpClient client, CookieContainer cookieContainer, IHttpClientOptions clientOptions)
{
_client = client;
_cookies = cookieContainer;
Uri = clientOptions.Value.Uri;
_uri = clientOptions.Uri.Trim(URI_TRIM_CHARS);
_path = clientOptions.Path?.Trim(URI_TRIM_CHARS) ?? string.Empty;
_headers = clientOptions.Headers;
_queryParams = clientOptions.QueryParams;
}
public CookieCollection GetCookies(string route = "") => _cookies.GetCookies(uri: new Uri(Uri + route));
public CookieCollection GetCookies(string path = "") => _cookies.GetCookies(uri: new Uri(UriCombine(_uri, path, path.Trim(URI_TRIM_CHARS))));
public async Task<HttpResponseMessage> FetchAsync(
string route = "",
string? scheme = null,
int? port = null,
string path = "",
IEnumerable<KeyValuePair<string, object?>>? queryParams = null,
HttpMethod? method = null,
HttpContent? body = null,
Dictionary<string, string>? form = null,
IEnumerable<KeyValuePair<string, object>>? form = null,
IEnumerable<KeyValuePair<string, object>>? headers = null,
bool sendWithCookie = true,
bool saveCookie = true
)
{
// merge with default headers
if(_headers is not null)
{
if (headers is null)
headers = _headers;
else
{
var mergedHeaders = headers.ToList();
mergedHeaders.AddRange(_headers);
headers = mergedHeaders;
}
}
// Add default query parameters
if(_queryParams is not null)
{
if (queryParams is null)
queryParams = _queryParams;
else
{
var mergedQueryParams = queryParams.ToList();
mergedQueryParams.AddRange(_queryParams);
queryParams = mergedQueryParams;
}
}
// set default HTTP method as GET
method ??= HttpMethod.Get;
// create URL
var requestUriStr = Uri + route;
var requestUri = new Uri(requestUriStr);
var uriBuilder = new UriBuilder(_uri);
if (scheme is not null)
uriBuilder.Scheme = scheme;
if (port is int portInt)
uriBuilder.Port = portInt;
uriBuilder.Path = UriCombine(_path, path?.Trim(URI_TRIM_CHARS) ?? string.Empty);
var requestMessage = new HttpRequestMessage(method, requestUriStr);
// Add query parameters if provided
if (queryParams?.Any() ?? false)
{
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
var flagParams = queryParams.Where(param => param.Value is null).Select(param => param.Key);
var valueParams = queryParams.Where(param => param.Value is not null);
foreach (var param in valueParams)
query[param.Key] = param.Value switch
{
bool b => b.ToString().ToLower(),
_ => param.Value.ToString()
};
if (flagParams.Any())
uriBuilder.Query = string.Join(QUERY_SEPARATOR, query.ToString(), string.Join(QUERY_SEPARATOR, flagParams));
else uriBuilder.Query = query.ToString();
}
var requestUri = uriBuilder.Uri;
var requestMessage = new HttpRequestMessage(method, requestUri);
// Add headers if provided
headers?.ForEach(header => requestMessage.Headers.Add(header.Key, header.Value.ToString()));
// Add cookie to request
if (sendWithCookie)
@@ -56,7 +125,7 @@ namespace DigitalData.Core.Client
else if (body != null)
requestMessage.Content = body;
else if (form != null)
requestMessage.Content = new FormUrlEncodedContent(form);
requestMessage.Content = new FormUrlEncodedContent(form.Select(e => KeyValuePair.Create(e.Key, e.Value.ToString())));
var response = await _client.SendAsync(requestMessage);
@@ -68,5 +137,11 @@ namespace DigitalData.Core.Client
return response;
}
internal static readonly char[] URI_TRIM_CHARS = { '\\', '/', ' ' };
internal static string UriCombine(params string[] paths) => System.IO.Path.Combine(paths).Replace("\\", "/");
internal static readonly char QUERY_SEPARATOR = '&';
}
}

View File

@@ -1,33 +1,35 @@
using DigitalData.Core.Abstractions.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using System.Net;
namespace DigitalData.Core.Client
{
public static class DIExtensions
{
public static IServiceCollection AddHttpClientService(this IServiceCollection services, string uri)
internal static IServiceCollection AddHttpClientServiceDefaults(this IServiceCollection services)
{
services.TryAddSingleton<HttpClient>();
services.TryAddSingleton<CookieContainer>();
services.AddSingleton<IBaseHttpClientService, BaseHttpClientService>();
services.Configure<HttpClientOptions>(opt => opt.Uri = uri);
return services;
}
public static IServiceCollection AddHttpClientService<TClientOptions>(this IServiceCollection services, Action<TClientOptions>? clientOptions = null, bool setAsDefaultBase = false)
where TClientOptions : HttpClientOptions
public static IServiceCollection AddHttpClientService<THttpClientOptions>(this IServiceCollection services, IConfigurationSection section)
where THttpClientOptions : class, IHttpClientOptions
{
services.TryAddSingleton<HttpClient>();
services.TryAddSingleton<CookieContainer>();
services.AddSingleton<IHttpClientService<TClientOptions>, HttpClientService<TClientOptions>>();
services.Configure(clientOptions ?? (_ => { }));
if (setAsDefaultBase)
services.AddSingleton<IBaseHttpClientService, HttpClientService<TClientOptions>>();
services.AddHttpClientServiceDefaults();
services.TryAddSingleton<IHttpClientService<THttpClientOptions>, HttpClientService<THttpClientOptions>>();
return services.Configure<THttpClientOptions>(section);
}
public static IServiceCollection AddHttpClientService<THttpClientOptions>(this IServiceCollection services, THttpClientOptions options)
where THttpClientOptions : class, IHttpClientOptions
{
services.AddHttpClientServiceDefaults();
services.TryAddSingleton<IHttpClientService<THttpClientOptions>, HttpClientService<THttpClientOptions>>();
services.TryAddSingleton(Options.Create(options));
return services;
}
}

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0;net8.0</TargetFrameworks>
@@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
<Description>This package provides HTTP client extension methods for the DigitalData.Core library, offering simplified and asynchronous methods for fetching and handling HTTP responses. It includes utility methods for sending GET requests, reading response content as text or JSON, and deserializing JSON into dynamic or strongly-typed objects using Newtonsoft.Json. These extensions facilitate efficient and easy-to-read HTTP interactions in client applications.</Description>
<PackageId>DigitalData.Core.Client</PackageId>
<Version>1.0.1.1</Version>
<Version>2.0.3</Version>
<Authors>Digital Data GmbH</Authors>
<Company>Digital Data GmbH</Company>
<Product>Digital Data GmbH</Product>
@@ -15,6 +15,8 @@
<PackageIcon>core_icon.png</PackageIcon>
<RepositoryUrl>http://git.dd:3000/AppStd/WebCoreModules.git</RepositoryUrl>
<PackageTags>digital data core http client json serilization</PackageTags>
<AssemblyVersion>2.0.3</AssemblyVersion>
<FileVersion>2.0.3</FileVersion>
</PropertyGroup>
<ItemGroup>
@@ -27,6 +29,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

View File

@@ -1,7 +0,0 @@
namespace DigitalData.Core.Client
{
public class HttpClientOptions
{
public string Uri { get; set; } = string.Empty;
}
}

View File

@@ -5,9 +5,9 @@ using System.Net;
namespace DigitalData.Core.Client
{
public class HttpClientService<TClientOptions> : BaseHttpClientService, IHttpClientService<TClientOptions>, IBaseHttpClientService
where TClientOptions : HttpClientOptions
where TClientOptions : class, IHttpClientOptions
{
public HttpClientService(HttpClient client, CookieContainer cookieContainer, IOptions<TClientOptions> clientOptions) : base(client, cookieContainer, clientOptions)
public HttpClientService(HttpClient client, CookieContainer cookieContainer, IOptions<TClientOptions> clientOptions) : base(client, cookieContainer, clientOptions.Value)
{
}
}

View File

@@ -19,7 +19,7 @@ namespace DigitalData.Core.Client
public static T Provide<T>() where T : notnull => _lazyProvider.Value.GetRequiredService<T>();
public static IHttpClientService<TOptions> ProvideHttpClientService<TOptions>() where TOptions : notnull
public static IHttpClientService<TOptions> ProvideHttpClientService<TOptions>() where TOptions : IHttpClientOptions
=> _lazyProvider.Value.GetRequiredService<IHttpClientService<TOptions>>();
}
}

View File

@@ -1,4 +1,5 @@
using DigitalData.Core.Abstractions.Security;
using System.Collections.Concurrent;
using System.Security.Cryptography;
namespace DigitalData.Core.Security.Extensions
@@ -12,10 +13,54 @@ namespace DigitalData.Core.Security.Extensions
return rsa;
}
public static bool TryGetEncryptor(this IDictionary<string, IRSAEncryptor> pairs, string issuer, string audience, out IRSAEncryptor? encryptor)
=> pairs.TryGetValue($"{issuer}:{audience}", out encryptor);
public static IRSADecryptor GetRSADecryptor(this ICryptFactory factory, string issuer, string audience, Version? version = null, string? seperator = null)
=> factory[factory.RSAKeyNameFormatter(issuer, audience, true, version, seperator)];
public static IRSAEncryptor? GetEncryptor(this IDictionary<string, IRSAEncryptor> pairs, string issuer, string audience)
=> pairs.TryGetEncryptor(issuer: issuer, audience: audience, out var encryptor) ? encryptor : null;
public static bool TryGetRSADecryptor(this ICryptFactory factory, string issuer, string audience, out IRSADecryptor? decryptor, Version? version = null, string? seperator = null)
=> factory.TryGetRSADecryptor(factory.RSAKeyNameFormatter(issuer, audience, true, version, seperator), out decryptor);
private static string CreatePath(string filename, string? directory = null)
{
directory ??= Environment.CurrentDirectory;
if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
return Path.Combine(directory, $"{filename}.pem");
}
private static readonly ConcurrentDictionary<string, SemaphoreSlim> FileLocks = new();
public static void SavePem(this IRSACryptographer decryptor, string key, string? directory = null)
{
var filePath = CreatePath(filename: key, directory : directory);
var fileLock = FileLocks.GetOrAdd(filePath, _ => new (1, 1));
fileLock.Wait();
try
{
File.WriteAllText(filePath, decryptor.Pem);
}
finally
{
fileLock.Release();
}
}
public static async Task SavePemAsync(this IRSACryptographer decryptor, string key, string? directory = null)
{
var filePath = CreatePath(filename: key, directory: directory);
var fileLock = FileLocks.GetOrAdd(filePath, _ => new (1, 1));
await fileLock.WaitAsync();
try
{
await File.WriteAllTextAsync(filePath, decryptor.Pem);
}
finally
{
fileLock.Release();
}
}
}
}

View File

@@ -1,62 +1,25 @@
using DigitalData.Core.Abstractions.Security;
using Microsoft.Extensions.Logging;
using System.Security.Cryptography;
namespace DigitalData.Core.Security
{
public class CryptFactory : ICryptFactory
public class CryptFactory : RSAFactory, ICryptFactory
{
private static readonly Lazy<CryptFactory> LazyInstance = new (() => new ());
private readonly IDictionary<string, IRSADecryptor> _decryptors;
public static CryptFactory Instance => LazyInstance.Value;
public IRSADecryptor this[string key] { get => _decryptors[key]; set => _decryptors[key] = value; }
public int KeySizeInBits { get; init; } = 2048;
public Func<string, string, bool, Version?, string?, string> RSAKeyNameFormatter { get; }
public string PbePassword { private get; init; } = Secrets.PBE_PASSWORD;
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; } = PbeEncryptionAlgorithm.Aes256Cbc;
public HashAlgorithmName PbeHashAlgorithmName { get; init; } = HashAlgorithmName.SHA256;
public int PbeIterationCount { get; init; } = 100_000;
private readonly Lazy<PbeParameters> _lazyPbeParameters;
public PbeParameters PbeParameters => _lazyPbeParameters.Value;
public string EncryptedPrivateKeyPemLabel { get; init; } = "ENCRYPTED PRIVATE KEY";
public CryptFactory(ILogger<CryptFactory>? logger = null)
public CryptFactory(ILogger<CryptFactory> logger, IDictionary<string, IRSADecryptor> decryptors, Func<string, string, bool, Version?, string?, string> rsaKeyNameFormatter) : base()
{
_lazyPbeParameters = new(() => new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithmName, PbeIterationCount));
_decryptors = decryptors ?? new Dictionary<string, IRSADecryptor>();
logger?.LogInformation("CryptFactory initialized. Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
RSAKeyNameFormatter = rsaKeyNameFormatter;
logger?.LogInformation("Core.Secrets version: {Version}, Created on: {CreationDate}.", Secrets.Version, Secrets.CreationDate.ToString("dd.MM.yyyy"));
}
public string CreateRSAPrivateKeyPem(int? keySizeInBits = null)
=> RSA.Create(keySizeInBits ?? KeySizeInBits).ExportRSAPrivateKeyPem();
public string CreateEncryptedPrivateKeyPem(
int? keySizeInBits = null,
string? password = null,
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
HashAlgorithmName? hashAlgorithmName = null,
int? iterationCount = null)
{
password ??= PbePassword;
var pbeParameters = (pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null)
? new PbeParameters(
pbeEncryptionAlgorithm ?? PbeEncryptionAlgorithm,
hashAlgorithmName ?? PbeHashAlgorithmName,
iterationCount ?? PbeIterationCount)
: PbeParameters;
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
var pemChars = PemEncoding.Write(EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
return new string(pemChars);
}
public bool TryGetRSADecryptor(string key, out IRSADecryptor? decryptor) => _decryptors.TryGetValue(key, out decryptor);
}
}

View File

@@ -8,7 +8,7 @@ namespace DigitalData.Core.Security
{
public static IServiceCollection AddSecurity(this IServiceCollection services)
{
services.TryAddScoped<ICryptFactory>(_ => CryptFactory.Instance);
services.TryAddScoped<ICryptFactory, CryptFactory>();
return services;
}

View File

@@ -9,7 +9,7 @@ namespace DigitalData.Core.Security
public RSAEncryptionPadding Padding { get; init; } = RSAEncryptionPadding.OaepSHA256;
protected readonly RSA _rsa = RSA.Create();
protected virtual RSA RSA { get; } = RSA.Create();
internal RSACryptographer() { }
}

View File

@@ -1,47 +1,58 @@
using DigitalData.Core.Abstractions.Security;
using DigitalData.Core.Security.Extensions;
using System.Runtime.Serialization;
using System.Security.Cryptography;
namespace DigitalData.Core.Security
{
public class RSADecryptor : RSACryptographer, IRSADecryptor, IRSACryptographer
{
public string? Password { get; init; }
public bool IsEncrypted => Password is not null;
public IRSAEncryptor Encryptor
public (string Value, Version Version)? VersionedPassword
{
get
init
{
return new RSAEncryptor()
{
Pem = _rsa.ExportRSAPublicKeyPem(),
Padding = Padding
};
_password = value?.Value;
PasswordVersion = value?.Version;
}
}
internal RSADecryptor() { }
private string? _password;
[OnDeserialized]
private void OnDeserialized(StreamingContext context) => Init();
public Version? PasswordVersion { get; private init; } = null;
private IRSADecryptor Init()
public bool HasEncryptedPem => _password is not null;
public bool IsEncrypted => _password is not null;
private readonly Lazy<IRSAEncryptor> _lazyEncryptor;
public IRSAEncryptor Encryptor => _lazyEncryptor.Value;
private readonly Lazy<RSA> lazyRSA;
protected override RSA RSA => lazyRSA.Value;
public RSADecryptor()
{
if (string.IsNullOrWhiteSpace(Pem))
throw new InvalidOperationException("Pem cannot be null or empty.");
_lazyEncryptor = new(() => new RSAEncryptor()
{
Pem = RSA.ExportRSAPublicKeyPem(),
Padding = Padding
});
if (Password is null)
_rsa.ImportFromPem(Pem);
else
_rsa.ImportFromEncryptedPem(Pem, Password.AsSpan());
lazyRSA = new(() =>
{
var rsa = RSA.Create();
if (_password is null)
RSA.ImportFromPem(Pem);
else
RSA.ImportFromEncryptedPem(Pem, _password.AsSpan());
return this;
return rsa;
});
}
public byte[] Decrypt(byte[] data) => RSA.Decrypt(data, Padding);
public byte[] Decrypt(byte[] data) => _rsa.Decrypt(data, Padding);
public string Decrypt(string data) => _rsa.Decrypt(data.Base64ToByte(), Padding).BytesToString();
public string Decrypt(string data) => RSA.Decrypt(data.Base64ToByte(), Padding).BytesToString();
}
}

View File

@@ -10,17 +10,14 @@ namespace DigitalData.Core.Security
get => base.Pem;
init
{
if (string.IsNullOrWhiteSpace(Pem))
throw new InvalidOperationException("Pem cannot be null or empty.");
_rsa.ImportFromPem(base.Pem);
RSA.ImportFromPem(base.Pem);
base.Pem = value;
}
}
public byte[] Encrypt(byte[] data) => _rsa.Encrypt(data, Padding);
public byte[] Encrypt(byte[] data) => RSA.Encrypt(data, Padding);
public string Encrypt(string data) => _rsa.Encrypt(data.Base64ToByte(), Padding).BytesToString();
public string Encrypt(string data) => RSA.Encrypt(data.Base64ToByte(), Padding).BytesToString();
public bool Verify(string data, string signature) => Encrypt(data) == signature;
}

View File

@@ -0,0 +1,132 @@
using DigitalData.Core.Abstractions.Security;
using System.Security.Cryptography;
using System.Text;
namespace DigitalData.Core.Security
{
public class RSAFactory
{
private static readonly Lazy<RSAFactory> LazyInstance = new(() => new());
public static RSAFactory Static => LazyInstance.Value;
public static readonly string DefaultEncryptedPrivateKeyFileTag = "enc-private";
public static readonly string DefaultPrivateKeyFileTag = "private";
public static readonly string DefaultPublicKeyFileTag = "public";
public static readonly IEnumerable<string> KeyFileTags = new string[] { DefaultEncryptedPrivateKeyFileTag, DefaultPrivateKeyFileTag, DefaultPublicKeyFileTag };
private static readonly Lazy<IEnumerable<string>> LazyLowerFileTags = new(() => KeyFileTags.Select(tag => tag.ToLower()));
public static readonly string DefaultRSAKeyNameSeparator = "-_-";
//TODO: make the validation using regex
public static string DefaultRSAKeyNameFormatter(string issuer, string audience, bool isPrivate = true, Version? passwordVersion = null, string? separator = null)
{
separator ??= DefaultRSAKeyNameSeparator;
void ValidateForbidden(string value, string paramName)
{
if (Path.GetInvalidFileNameChars().Any(value.Contains) || LazyLowerFileTags.Value.Any(tag => value.ToLower().Contains(tag)))
throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains forbidden characters that are not allowed in file naming.", paramName);
}
static void ValidateSeparator(string value, string paramName, string separator)
{
if (value.Contains(separator))
throw new ArgumentException($"RSA decryptor key name creation is forbidden. The {paramName} contains separator characters ({separator}) that are not allowed in file naming.", paramName);
}
ValidateForbidden(issuer, nameof(issuer));
ValidateForbidden(audience, nameof(audience));
ValidateForbidden(separator, nameof(separator));
ValidateSeparator(issuer, nameof(issuer), separator);
ValidateSeparator(audience, nameof(audience), separator);
var sb = new StringBuilder(issuer.Length + audience.Length + separator.Length * 2 + 20);
sb.Append(issuer).Append(separator).Append(audience).Append(separator);
if (passwordVersion is null && isPrivate)
sb.Append(DefaultPrivateKeyFileTag);
else if (isPrivate)
sb.Append(DefaultEncryptedPrivateKeyFileTag).Append(separator).Append(passwordVersion);
else if (passwordVersion is null)
sb.Append(DefaultPublicKeyFileTag);
else
sb.Append(DefaultPublicKeyFileTag).Append(separator).Append(passwordVersion);
return sb.ToString();
}
public int KeySizeInBits { get; init; } = 2048;
public string PbePassword { private get; init; } = Secrets.PBE_PASSWORD;
public PbeEncryptionAlgorithm PbeEncryptionAlgorithm { get; init; } = PbeEncryptionAlgorithm.Aes256Cbc;
public HashAlgorithmName PbeHashAlgorithmName { get; init; } = HashAlgorithmName.SHA256;
public int PbeIterationCount { get; init; } = 100_000;
private readonly Lazy<PbeParameters> _lazyPbeParameters;
public PbeParameters PbeParameters => _lazyPbeParameters.Value;
public string EncryptedPrivateKeyPemLabel { get; init; } = "ENCRYPTED PRIVATE KEY";
internal RSAFactory()
{
_lazyPbeParameters = new(() => new PbeParameters(PbeEncryptionAlgorithm, PbeHashAlgorithmName, PbeIterationCount));
}
public string CreateRSAPrivateKeyPem(int? keySizeInBits = null)
=> RSA.Create(keySizeInBits ?? KeySizeInBits).ExportRSAPrivateKeyPem();
public string CreateEncryptedPrivateKeyPem(
int? keySizeInBits = null,
string? password = null,
PbeEncryptionAlgorithm? pbeEncryptionAlgorithm = null,
HashAlgorithmName? hashAlgorithmName = null,
int? iterationCount = null)
{
password ??= PbePassword;
var pbeParameters = (pbeEncryptionAlgorithm is null && hashAlgorithmName is null && iterationCount is null)
? new PbeParameters(
pbeEncryptionAlgorithm ?? PbeEncryptionAlgorithm,
hashAlgorithmName ?? PbeHashAlgorithmName,
iterationCount ?? PbeIterationCount)
: PbeParameters;
var encryptedPrivateKey = RSA.Create(keySizeInBits ?? KeySizeInBits).ExportEncryptedPkcs8PrivateKey(password.AsSpan(), pbeParameters);
var pemChars = PemEncoding.Write(EncryptedPrivateKeyPemLabel, encryptedPrivateKey);
return new string(pemChars);
}
public async Task<IRSADecryptor> ReadRSADecryptorAsync(string path, Version? version = null, CancellationToken cancellationToken = default)
{
var pem = await File.ReadAllTextAsync(path, cancellationToken);
(string Value, Version Version)? versionedPassword = null;
if(version is not null)
{
if (version != Secrets.Version)
throw new InvalidOperationException($"The provided version {version} does not match the expected version {Secrets.Version}.");
versionedPassword = (Secrets.PBE_PASSWORD, Secrets.Version);
}
return new RSADecryptor()
{
Pem = pem,
VersionedPassword = versionedPassword
};
}
}
}

View File

@@ -14,22 +14,46 @@ namespace DigitalData.Core.Tests.Client
public void SetUp()
{
_serviceProvider = new ServiceCollection()
.AddHttpClientService("https://jsonplaceholder.typicode.com/todos")
.AddHttpClientService("https://jsonplaceholder.typicode.com", "todos")
.BuildServiceProvider();
_service = _serviceProvider.GetRequiredService<IBaseHttpClientService>();
}
[Test]
public async Task FetchJsonAsync_ShouldReturnJsonResponse()
public async Task FetchJsonAsync_ShouldReturnJsonResponse_WithCorrectWithPath()
{
// Act
var expectedUserId = (int) await _service.FetchAsync("/1", sendWithCookie: false, saveCookie: false)
var expectedUserId = (int) await _service.FetchAsync(path: "/1", sendWithCookie: false, saveCookie: false)
.ThenAsync(res => res.Json())
.ThenAsync(todo => todo.userId);
// Assert
Assert.That(expectedUserId, Is.EqualTo(1), "The userId of the fetched JSON object should be 1.");
}
[Test]
public async Task FetchJsonAsync_ShouldReturnJsonResponse_WithQueryParams()
{
var queryParams = new Dictionary<string, object?>
{
{ "id", "1" }
};
// Act
var dyn_id = await _service.FetchAsync(queryParams: queryParams, sendWithCookie: false, saveCookie: false)
.ThenAsync(res => res.JsonList())
.ThenAsync(todo => todo.FirstOrDefault()?.userId);
try
{
Assert.That((int)dyn_id, Is.EqualTo(1), "The userId of the fetched JSON object should be 1.");
}
catch (InvalidCastException)
{
// Handle the case where the cast is not possible
Assert.Fail("The id could not be cast to an integer.");
}
}
}
}

View File

@@ -23,7 +23,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Legacy.Cli
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Security", "DigitalData.Core.Security\DigitalData.Core.Security.csproj", "{47D80C65-74A2-4EB8-96A5-D571A9108FB3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Security.Extensions", "DigitalData.Core.Security.Extensions\DigitalData.Core.Security.Extensions.csproj", "{D740182D-82DA-480A-9F87-BFB4A8620A00}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DigitalData.Core.Security.Extensions", "DigitalData.Core.Security.Extensions\DigitalData.Core.Security.Extensions.csproj", "{D740182D-82DA-480A-9F87-BFB4A8620A00}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DigitalData.Core.Terminal", "DigitalData.Core.Terminal\DigitalData.Core.Terminal.csproj", "{0FA93730-8084-4907-B172-87D610323796}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -51,8 +53,8 @@ Global
{0B051A5F-BD38-47D1-BAFF-D44BA30D3FB7}.Debug|Any CPU.Build.0 = Release|Any CPU
{0B051A5F-BD38-47D1-BAFF-D44BA30D3FB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B051A5F-BD38-47D1-BAFF-D44BA30D3FB7}.Release|Any CPU.Build.0 = Release|Any CPU
{6A80FFEC-9B83-40A7-8C78-124440B48B33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A80FFEC-9B83-40A7-8C78-124440B48B33}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A80FFEC-9B83-40A7-8C78-124440B48B33}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{6A80FFEC-9B83-40A7-8C78-124440B48B33}.Debug|Any CPU.Build.0 = Release|Any CPU
{6A80FFEC-9B83-40A7-8C78-124440B48B33}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A80FFEC-9B83-40A7-8C78-124440B48B33}.Release|Any CPU.Build.0 = Release|Any CPU
{13E40DF1-6123-4838-9BF8-086C94E6ADF6}.Debug|Any CPU.ActiveCfg = Release|Any CPU
@@ -74,6 +76,10 @@ Global
{D740182D-82DA-480A-9F87-BFB4A8620A00}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D740182D-82DA-480A-9F87-BFB4A8620A00}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D740182D-82DA-480A-9F87-BFB4A8620A00}.Release|Any CPU.Build.0 = Release|Any CPU
{0FA93730-8084-4907-B172-87D610323796}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0FA93730-8084-4907-B172-87D610323796}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0FA93730-8084-4907-B172-87D610323796}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0FA93730-8084-4907-B172-87D610323796}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE