From cbbc703695f7fd3db76e6f3697dc4499fd5d203f Mon Sep 17 00:00:00 2001 From: Klaus Fischer <klaus.fischer@eloware.com> Date: Thu, 30 Jun 2022 13:23:21 +0200 Subject: [PATCH] Implemented proxy support --- E2ETests/E2ETests.csproj | 15 +-- E2ETests/HelperMethods.cs | 30 ++++++ E2ETests/OAuthServiceTest.cs | 21 +--- E2ETests/ProxyTest.cs | 80 +++++++++++++++ Encryption/DefaultEncryptor.cs | 24 ----- Encryption/JoseEncryptor.cs | 21 ---- Encryption/RsaEncryption.cs | 146 --------------------------- FitConnect/Client.cs | 26 ++++- FitConnect/DiContainer.cs | 2 + FitConnect/FunctionalBaseClass.cs | 110 -------------------- RestService/RestCallService.cs | 20 +++- Services/Interfaces/IOAuthService.cs | 3 +- Services/OAuthService.cs | 9 +- 13 files changed, 165 insertions(+), 342 deletions(-) create mode 100644 E2ETests/HelperMethods.cs create mode 100644 E2ETests/ProxyTest.cs delete mode 100644 Encryption/DefaultEncryptor.cs delete mode 100644 Encryption/JoseEncryptor.cs delete mode 100644 Encryption/RsaEncryption.cs delete mode 100644 FitConnect/FunctionalBaseClass.cs diff --git a/E2ETests/E2ETests.csproj b/E2ETests/E2ETests.csproj index b78e8852..67adfa9e 100644 --- a/E2ETests/E2ETests.csproj +++ b/E2ETests/E2ETests.csproj @@ -8,16 +8,17 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="FluentAssertions" Version="6.7.0"/> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"/> - <PackageReference Include="NUnit" Version="3.13.2"/> - <PackageReference Include="NUnit3TestAdapter" Version="4.0.0"/> - <PackageReference Include="coverlet.collector" Version="3.1.0"/> + <PackageReference Include="DotNet.Testcontainers" Version="1.6.0" /> + <PackageReference Include="FluentAssertions" Version="6.7.0" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" /> + <PackageReference Include="NUnit" Version="3.13.2" /> + <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> + <PackageReference Include="coverlet.collector" Version="3.1.0" /> </ItemGroup> <ItemGroup> - <ProjectReference Include="..\FitConnect\FitConnect.csproj"/> - <ProjectReference Include="..\Services\Services.csproj"/> + <ProjectReference Include="..\FitConnect\FitConnect.csproj" /> + <ProjectReference Include="..\Services\Services.csproj" /> </ItemGroup> </Project> diff --git a/E2ETests/HelperMethods.cs b/E2ETests/HelperMethods.cs new file mode 100644 index 00000000..ba5a5d96 --- /dev/null +++ b/E2ETests/HelperMethods.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using Newtonsoft.Json; + +namespace E2ETests; + +public static class HelperMethods { + public static (string id, string secret) GetSecrets() { + // relative to the project execution directory + const string secretFile = "../../../http-client.private.env.json"; + + if (!File.Exists(secretFile)) { + // If the secret file is not found, create it with the default values + // The file will be pretty in C#11, when """ is introduced + File.WriteAllText(secretFile, @" +{ + ""sender"": { + ""id"": ""00000000-0000-0000-0000-000000000000"", + ""secret"": ""0000000000000000000000000000000000000000000"", + ""scope"": ""send:region:DE"" + } +}"); + throw new Exception("Please fill the secret.json file with your sender credentials"); + } + + var jsonContent = File.ReadAllText(secretFile); + var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent); + return (secret.sender.id, secret.sender.secret); + } +} diff --git a/E2ETests/OAuthServiceTest.cs b/E2ETests/OAuthServiceTest.cs index b17b97d2..50c1314c 100644 --- a/E2ETests/OAuthServiceTest.cs +++ b/E2ETests/OAuthServiceTest.cs @@ -16,28 +16,9 @@ public class OAuthServiceTest { [OneTimeSetUp] public void OneTimeSetup() { - // relative to the project execution directory - const string secretFile = "../../../http-client.private.env.json"; - - if (!File.Exists(secretFile)) { - // If the secret file is not found, create it with the default values - // The file will be pretty in C#11, when """ is introduced - File.WriteAllText(secretFile, @" -{ - ""sender"": { - ""id"": ""00000000-0000-0000-0000-000000000000"", - ""secret"": ""0000000000000000000000000000000000000000000"", - ""scope"": ""send:region:DE"" + (_clientId, _clientSecret) = HelperMethods.GetSecrets(); } -}"); - throw new Exception("Please fill the secret.json file with your sender credentials"); - } - var jsonContent = File.ReadAllText(secretFile); - var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent); - _clientId = secret.sender.id; - _clientSecret = secret.sender.secret; - } [SetUp] diff --git a/E2ETests/ProxyTest.cs b/E2ETests/ProxyTest.cs new file mode 100644 index 00000000..03503db0 --- /dev/null +++ b/E2ETests/ProxyTest.cs @@ -0,0 +1,80 @@ +using System; +using System.IO; +using System.Net; +using System.Reflection; +using System.Threading; +using NUnit.Framework; +using DotNet.Testcontainers; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Containers; +using FitConnect; +using FitConnect.Services; +using FluentAssertions; + +namespace E2ETests; + +public class ProxyTest { + private TestcontainersContainer _container; + private Client _client; + private string _secret; + private string _id; + + [OneTimeSetUp] + public void OneTimeSetup() { + var path = $"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}/proxy"; + Console.WriteLine($"Creating directory: {path}"); + Directory.CreateDirectory(path); + (_id, _secret) = HelperMethods.GetSecrets(); + + _container = new TestcontainersBuilder<TestcontainersContainer>() + .WithImage("ubuntu/squid") + .WithPortBinding("3128", "3128") + .WithBindMount(path, @"/var/log/squid") + .Build(); + } + + [SetUp] + public void Setup() { + _container.StartAsync().Wait(); + + _client = + new Client(FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development), + _id, + _secret) + .WithProxy("localhost", 3128); + } + + [Test] + public void ContainerIsRunning() { + _container.Should().NotBeNull(); + _container.State.Should().Be(TestcontainersState.Running); + + var client = new WebClient() { + Proxy = + new WebProxy("http://localhost:3128") + }; + client.DownloadString("https://www.fitko.de/").Should().NotBeNull(); + } + + [Test] + public void RequestOAuthToken() { + // Arrange + var testUrl = FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development) + .TokenUrl; + + var oAuthService = new OAuthService(testUrl) { + Proxy = new WebProxy("http://localhost:3128") + }; + + // Act + var token = oAuthService.AuthenticateAsync(_id, _secret).Result; + + // Assert + token.Should().NotBeNull(); + } + + [OneTimeTearDown] + public void OneTimeTearDown() { + _container.StopAsync().Wait(); + } +} diff --git a/Encryption/DefaultEncryptor.cs b/Encryption/DefaultEncryptor.cs deleted file mode 100644 index ada93cf9..00000000 --- a/Encryption/DefaultEncryptor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Security.Cryptography; -using System.Text; -using Microsoft.IdentityModel.Tokens; - -namespace FitConnect.Encryption; - -public class DefaultEncryptor : IEncryptor { - public string Encrypt(string plain, string key, out object? passOver) { - var rsaKey = new JsonWebKey(key); - var rsa = RSA.Create(); - passOver = rsa; - - var cipher = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.OaepSHA512); - return Convert.ToBase64String(cipher); - } - - public string Decrypt(string cipher, string key, object? passover = null) { - var rsaKey = new JsonWebKey(key); - var rsa = passover as RSA ?? RSA.Create(); - - var plain = rsa.Decrypt(Convert.FromBase64String(cipher), RSAEncryptionPadding.OaepSHA512); - return Encoding.UTF8.GetString(plain); - } -} diff --git a/Encryption/JoseEncryptor.cs b/Encryption/JoseEncryptor.cs deleted file mode 100644 index 38de0f4c..00000000 --- a/Encryption/JoseEncryptor.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Jose; - -namespace JweTest; - -public class JoseEncryptor : IEncryptor { - public string Encrypt(string plain, string key, out object? passOver) { - passOver = null; - var jwk = Jwk.FromJson(key); - return JWE.Encrypt(plain, - new[] { new JweRecipient(JweAlgorithm.RSA_OAEP_256, jwk) }, - JweEncryption.A256GCM); - } - - public string Decrypt(string cipher, string key, object? passOver = null) { - var jwk = Jwk.FromJson(key); - var cipherBytes = Convert.FromBase64String(cipher); - var jwe = JWE.Decrypt(cipher, jwk); - return Convert.ToBase64String(jwe.Ciphertext); - } -} diff --git a/Encryption/RsaEncryption.cs b/Encryption/RsaEncryption.cs deleted file mode 100644 index eed61bdc..00000000 --- a/Encryption/RsaEncryption.cs +++ /dev/null @@ -1,146 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; - -namespace FitConnect.Encryption; - -public class RsaEncryption : IEncryption { - private readonly X509Certificate2? _certificate; - private readonly ILogger? _logger; - private readonly RSA _rsa; - private RSA? _privateKey; - private RSA? _publicKey; - - - public RsaEncryption(X509Certificate2? certificate, ILogger? logger) { - _logger = logger; - _rsa = RSA.Create(4096); - - if (certificate != null) { - _certificate = certificate; - ImportCertificate(certificate); - } - } - - - public void ImportCertificate(X509Certificate2 cert) { - if (!CheckCertificate(cert)) throw new Exception("Invalid certificate"); - - _publicKey = cert.GetRSAPublicKey(); - - if ((_publicKey?.KeySize ?? 0) == 0) - throw new Exception("Invalid certificate, no public key"); - - if (cert.HasPrivateKey) _privateKey = cert.GetRSAPrivateKey(); - - if (_privateKey != null) - _logger?.LogInformation("Certificate with private key imported"); - else - _logger?.LogInformation("Certificate has no private key"); - } - - /// <summary> - /// Import a public key from a PEM file - /// </summary> - /// <param name="certificatePath"></param> - /// <param name="password">Password for the certificate</param> - /// <exception cref="ArgumentException"></exception> - /// <exception cref="Exception"></exception> - public void ImportCertificate(string certificatePath, string password) { - _logger?.LogInformation("Importing certificate {CertPath}", certificatePath); - var cert = - new X509Certificate2(certificatePath, password, X509KeyStorageFlags.MachineKeySet); - - if (!CheckCertificate(cert)) throw new ArgumentException("Certificate is not valid"); - - var parameters = cert.PublicKey.GetRSAPublicKey()?.ExportParameters(true); - if (parameters == null) throw new Exception("Could not get public key from certificate"); - - _rsa.ImportParameters(parameters.Value); - } - - public byte[] DecryptData(byte[] data) { - return - (_privateKey ?? _rsa).Decrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - public string DecryptData(string data) { - return Encoding.UTF8.GetString(DecryptData(Convert.FromBase64String(data))); - } - - public byte[] ExportPublicKey() { - return _rsa.ExportRSAPublicKey(); - } - - public byte[] ExportPrivateKey() { - return _rsa.ExportRSAPrivateKey(); - } - - public byte[] EncryptData(byte[] data) { - return (_publicKey ?? _rsa).Encrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - public byte[] EncryptData(byte[] data, byte[] publicKey) { - _logger?.LogInformation( - "Encrypting data with public key: {}", - Convert.ToBase64String(_rsa.ExportRSAPublicKey())); - - _rsa.ImportRSAPublicKey(publicKey, out var read); - return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - /// <summary> - /// Creating a self signed certificate - /// </summary> - /// <param name="exportPath">Location for storing the certificate files</param> - /// <returns></returns> - /// <exception cref="Exception"></exception> - public static X509Certificate2 CreateSelfSignedCertificate(string? exportPath = null) { - var rsa = RSA.Create(4096); - - var req = new CertificateRequest("c=DE, cn=fitconnect.de", - rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - - var cert = req.CreateSelfSigned(DateTimeOffset.Now.AddSeconds(-5), - DateTimeOffset.Now.AddYears(5)); - - if (cert.GetRSAPublicKey() == null) - throw new Exception("Certificate does not contain a public key"); - - if (cert.GetRSAPrivateKey() == null) - throw new Exception("Certificate does not contain a private key"); - - // Export the certificate to a PEM file, just for - // additional extern testing - if (exportPath != null) ExportCertificateToFile(exportPath, cert); - - return cert; - } - - private static void ExportCertificateToFile(string exportPath, X509Certificate cert) { - // Create PFX (PKCS #12) with private key - File.WriteAllBytes($"{exportPath}/certificate.pfx", - cert.Export(X509ContentType.Pfx, "")); - - // Create Base 64 encoded CER (public key only) - File.WriteAllText($"{exportPath}/certificate.cer", - "-----BEGIN CERTIFICATE-----\r\n" - + Convert.ToBase64String(cert.Export(X509ContentType.Cert, ""), - Base64FormattingOptions.InsertLineBreaks) - + "\r\n-----END CERTIFICATE-----"); - } - - - /// <summary> - /// Checking the certificate for validity - /// </summary> - /// <param name="cert">Certificate to check</param> - /// <returns>true for a fully approved certificate</returns> - private bool CheckCertificate(X509Certificate2 cert) { - return true; - } -} diff --git a/FitConnect/Client.cs b/FitConnect/Client.cs index 5d45e246..ffad4ab2 100644 --- a/FitConnect/Client.cs +++ b/FitConnect/Client.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text.Json; using Autofac; @@ -13,12 +14,10 @@ namespace FitConnect; /// </summary> // ReSharper disable once UnusedType.Global public class Client { - private readonly X509Certificate2? _receiverCertificate; - private readonly X509Certificate2? _senderCertificate; private readonly FitConnectEndpoints _endpoints; - internal string ClientId; + internal readonly string ClientId; - internal string ClientSecret; + internal readonly string ClientSecret; private readonly ILogger? _logger; @@ -53,6 +52,25 @@ public class Client { _logger = logger; } + /// <summary> + /// + /// </summary> + /// <param name="host"></param> + /// <param name="port"></param> + /// <param name="username"></param> + /// <param name="password"></param> + /// <returns></returns> + public Client WithProxy(string host, int port, string? username = null, + string? password = null) { + var proxy = new WebProxy(host, port); + if (username != null && password != null) { + proxy.Credentials = new NetworkCredential(username, password); + } + + DiContainer.WebProxy = proxy; + return this; + } + public IFluentSubscriber GetSubscriber(string privateKeyEncryption, string privateKeySigning) { var deserializedRaw = JsonSerializer.Deserialize<string>(privateKeyEncryption); var deserialized = new X509Certificate2(deserializedRaw); diff --git a/FitConnect/DiContainer.cs b/FitConnect/DiContainer.cs index b3b99299..dd197139 100644 --- a/FitConnect/DiContainer.cs +++ b/FitConnect/DiContainer.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Security.Cryptography.X509Certificates; using Autofac; using FitConnect.Services; @@ -8,6 +9,7 @@ namespace FitConnect; public static class DiContainer { private static IContainer? _container; + public static IWebProxy? WebProxy { get; set; } public static void DisposeContainer() { _container?.Dispose(); diff --git a/FitConnect/FunctionalBaseClass.cs b/FitConnect/FunctionalBaseClass.cs deleted file mode 100644 index 980ddc3b..00000000 --- a/FitConnect/FunctionalBaseClass.cs +++ /dev/null @@ -1,110 +0,0 @@ -using System.Net.Http.Headers; -using System.Net.Http.Json; -using System.Security.Cryptography; -using System.Text; -using FitConnect.Models; -using Microsoft.Extensions.Logging; - -namespace FitConnect; - - -public abstract class FunctionalBaseClass { - protected readonly ILogger? logger; - private RSA _rsa; - public FitConnectEndpoints Endpoints { get; } - - protected FunctionalBaseClass(ILogger? logger, FitConnectEndpoints? endpoints) { - Endpoints = endpoints ?? - FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development); - - this.logger = logger; - } - - - /// <summary> - /// Requesting an OAuth token from the FitConnect API. - /// - /// You can get the Client ID and Client Secret from the FitConnect Self Service portal - /// under https://portal.auth-testing.fit-connect.fitko.dev - /// </summary> - /// <param name="clientId">Your client Id</param> - /// <param name="clientSecret">Your client Secret</param> - /// <param name="scope">Scope if needed</param> - /// <returns></returns> - public async Task<OAuthAccessToken?> GetTokenAsync(string clientId, string clientSecret, - string? scope = null) { - var client = new HttpClient(); - client.DefaultRequestHeaders.Accept.Add( - MediaTypeWithQualityHeaderValue.Parse("application/json")); - - var requestContent = new Dictionary<string, string> { - { "grant_type", "client_credentials" }, - { "client_id", clientId }, - { "client_secret", clientSecret } - }; - - if (scope != null) - requestContent["scope"] = scope; - - var content = new FormUrlEncodedContent(requestContent); - - var request = new HttpRequestMessage(HttpMethod.Post, Endpoints.TokenUrl) { - Content = content, - Method = HttpMethod.Post - }; - - var response = await client.SendAsync(request); - - return await response.Content.ReadFromJsonAsync<OAuthAccessToken>(); - } - - public Task<SecurityEventToken> GetSetDataAsync() { - throw new NotImplementedException(); - } - - public byte[] EncryptDataAsync(byte[] data, - byte[]? publicKey, - string? password, - int numberOfIterations) { - _rsa = RSA.Create(2048); - // _rsa.ImportRSAPublicKey(publicKey, out var read); - - logger?.LogInformation( - "Encrypting data with public key: {}", - Convert.ToBase64String(_rsa.ExportRSAPublicKey())); - - // var keyParams = new PbeParameters( - // PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, numberOfIterations); - // - // var privateKey = - // rsa.ExportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(password), keyParams); - // - // logger?.LogInformation( - // "Private key: {}", privateKey); - - return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - public byte[] DecryptDataAsync(byte[] data, byte[] privateKey) { - return _rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256); - } - - - private async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) { - var client = new HttpClient(); - client.DefaultRequestHeaders.Add("Accept", "application/json"); - client.DefaultRequestHeaders.Add("Content-Type", "application/json"); - - var request = new HttpRequestMessage(); - request.Method = method; - request.Content = new StringContent(body, Encoding.UTF8, "application/json"); - request.RequestUri = uri; - - var response = await client.SendAsync(request); - - if (response.IsSuccessStatusCode) - return (await response.Content.ReadFromJsonAsync<T>()); - - throw new HttpRequestException("Error calling FitConnect API"); - } -} diff --git a/RestService/RestCallService.cs b/RestService/RestCallService.cs index 685b8017..77f8a699 100644 --- a/RestService/RestCallService.cs +++ b/RestService/RestCallService.cs @@ -1,25 +1,37 @@ +using System.Net; using System.Net.Http.Json; using System.Text; namespace FitConnect.RestService; +public interface IRestCallService { + public IWebProxy? Proxy { get; set; } +} + public abstract class RestCallService { private readonly string _baseUrl; + public IWebProxy? Proxy { get; set; } protected RestCallService(string baseUrl) { _baseUrl = baseUrl; } - internal async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) { - var client = new HttpClient(); + protected HttpClient CreateClient() { + var clientHandler = new HttpClientHandler() { + Proxy = Proxy + }; + var client = new HttpClient(handler: clientHandler); client.DefaultRequestHeaders.Add("Accept", "application/json"); - client.DefaultRequestHeaders.Add("Content-Type", "application/json"); + return client; + } + + internal async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) { + var client = CreateClient(); var request = new HttpRequestMessage(); request.Method = method; request.Content = new StringContent(body, Encoding.UTF8, "application/json"); request.RequestUri = uri; - var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) diff --git a/Services/Interfaces/IOAuthService.cs b/Services/Interfaces/IOAuthService.cs index 5ea4616e..801fdd5c 100644 --- a/Services/Interfaces/IOAuthService.cs +++ b/Services/Interfaces/IOAuthService.cs @@ -1,8 +1,9 @@ +using FitConnect.RestService; using FitConnect.Services.Models; namespace FitConnect.Services; -public interface IOAuthService { +public interface IOAuthService: IRestCallService { /// <summary> /// Requesting an OAuth token from the FitConnect API. /// <para> diff --git a/Services/OAuthService.cs b/Services/OAuthService.cs index c9bb472d..00f97803 100644 --- a/Services/OAuthService.cs +++ b/Services/OAuthService.cs @@ -2,14 +2,15 @@ using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; using System.Security.Authentication; +using FitConnect.RestService; using FitConnect.Services.Models; namespace FitConnect.Services; -public class OAuthService : IOAuthService { +public class OAuthService : RestCallService, IOAuthService { private readonly string _tokenUrl; - public OAuthService(string tokenUrl) { + public OAuthService(string tokenUrl) : base(tokenUrl) { _tokenUrl = tokenUrl; } @@ -27,9 +28,7 @@ public class OAuthService : IOAuthService { /// <returns>The received token or null</returns> public async Task<OAuthAccessToken?> AuthenticateAsync(string clientId, string clientSecret, string? scope = null) { - var client = new HttpClient(); - client.DefaultRequestHeaders.Accept.Add( - MediaTypeWithQualityHeaderValue.Parse("application/json")); + var client = CreateClient(); var requestContent = new Dictionary<string, string> { { "grant_type", "client_credentials" }, -- GitLab