Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions PortaCapena.OdooJsonRpcClient.Example/OdooHttpClientTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using PortaCapena.OdooJsonRpcClient.Configurations;
using PortaCapena.OdooJsonRpcClient.Shared;
using Xunit;

namespace PortaCapena.OdooJsonRpcClient.Example;

public class OdooHttpClientTest : RequestTestBase, IDisposable
{
// Tear down
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
OdooHttpClient.ClearHttpMessageHandlers();
InitializeHttpClient();
}

[Fact]
public async Task Can_call_custom_http_message_handler()
{
var customHandler = new TestMessageHandler();
OdooHttpClient.Configure(f =>
{
f.AddHttpMessageHandler(customHandler);
});
InitializeHttpClient();
var client = new OdooClient(TestConfig);

await client.GetVersionAsync();

client.Should().NotBeNull();
customHandler.IsCalled.Should().BeTrue();
}

//FIFO: First In First Out
[Fact]
public async Task Can_call_chain_of_handlers_in_FIFO_order()
{
var expectedOrderOfExecution = new List<int> {1, 2};
var uotExecutionOrder = new List<int>();
var firstHandler = new TestMessageHandler(1, uotExecutionOrder);
var secondHandler = new TestMessageHandler(2, uotExecutionOrder);
OdooHttpClient.Configure(f =>
{
f.AddHttpMessageHandler(firstHandler);
f.AddHttpMessageHandler(secondHandler);
});

InitializeHttpClient();
var client = new OdooClient(TestConfig);

await client.GetVersionAsync();

client.Should().NotBeNull();
uotExecutionOrder.Should().BeEquivalentTo(expectedOrderOfExecution);
}

// making the internal httpclient private is affecting
// all the tests in the whole project.
// Since, it's only instantiated once and keeping old states.
private static void InitializeHttpClient()
{
// this internally resets the private httpclient
OdooClient.BasicAuthenticationUsernamePassword = "admin";
}
}

public class TestMessageHandler : DelegatingHandler
{
private readonly List<int> _orderOfExecution = new();
private int Order { get; }

public bool IsCalled { get; private set; }

public TestMessageHandler()
{
}

public TestMessageHandler(int order, List<int> orderOfExecution)
{
Order = order;
_orderOfExecution = orderOfExecution;
}

protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
)
{
IsCalled = true;
_orderOfExecution.Add(Order);
Console.WriteLine($"Order: {Order}");
var response = await base.SendAsync(request, cancellationToken);
return response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace PortaCapena.OdooJsonRpcClient.Configurations
{
/// <summary>
/// Constructs OdooClient HttpClient
/// </summary>
internal static class OdooClientHttpFactory
{
/// <summary>
/// Creates and configures a new HttpClient as needed when a new Odoo instance is created.
/// </summary>
internal static HttpClient CreateHttpClient()
{
return CreateHttpClient(CreateInnerHandler());
}

private static HttpClient CreateHttpClient(HttpMessageHandler handler)
{
var client = new HttpClient(handler);

if (!string.IsNullOrEmpty(OdooClient.BasicAuthenticationUsernamePassword))
{
var byteArray = Encoding.ASCII.GetBytes(OdooClient.BasicAuthenticationUsernamePassword);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
}

client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
return client;
}

private static HttpMessageHandler CreateInnerHandler()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = false,
ClientCertificateOptions = ClientCertificateOption.Manual,
ServerCertificateCustomValidationCallback = ServerCertificateValidation
};
return OdooHttpClient.ChainClientHandler(handler);
}

private static bool ServerCertificateValidation(
HttpRequestMessage httpRequestMessage,
X509Certificate2 x509Certificate2,
X509Chain x509Chain,
SslPolicyErrors sslPolicyErrors
)
{
if (!OdooClient.ValidateServerCertificate)
{
return true;
}
return sslPolicyErrors == SslPolicyErrors.None;
}
}
}
111 changes: 111 additions & 0 deletions PortaCapena.OdooJsonRpcClient/Configurations/OdooHttpClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Net.Http;

namespace PortaCapena.OdooJsonRpcClient.Configurations
{
/// <summary>
/// Add custom delegating handler to the HttpClient pipeline
/// The order of the http message handler pipeline is important.
/// The execution order is FIFO (First in First out)
///
/// Configure the http message handlers before creating an instance of OdooClient
/// </summary>
public sealed class OdooHttpClient
{
private static OdooHttpClient _instance;
private static readonly object Lock = new object();
private readonly object _handlersLock = new object();
private readonly List<DelegatingHandler> _httpMessageHandlers = new List<DelegatingHandler>();

private OdooHttpClient()
{
}

private static OdooHttpClient Instance
{
get
{
if (_instance != null)
{
return _instance;
}

lock (Lock)
{
return _instance ?? (_instance = new OdooHttpClient());
}
}
}

#region Internals

internal static HttpMessageHandler ChainClientHandler(HttpClientHandler clientHandler)
{
var instance = Instance;
lock (instance._handlersLock)
{
return BuildChain(clientHandler, instance);
}
}

private static HttpMessageHandler BuildChain(
HttpClientHandler clientHandler,
OdooHttpClient instance
)
{
if (instance._httpMessageHandlers.Count == 0)
{
return clientHandler;
}
HttpMessageHandler current = clientHandler;
for (var i = instance._httpMessageHandlers.Count - 1; i >= 0; i--)
{
var handler = instance._httpMessageHandlers[i];
handler.InnerHandler = current;
current = handler;
}

return current;
}

#endregion

#region Public

public static void Configure(Action<OdooHttpClient> configure)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}

configure(Instance);
}

public void AddHttpMessageHandler(DelegatingHandler delegatingHandler)
{
if (delegatingHandler == null)
{
throw new ArgumentNullException(nameof(delegatingHandler));
}

lock (_handlersLock)
{
_httpMessageHandlers.Add(delegatingHandler);
}
}

public static void ClearHttpMessageHandlers()
{
var instance = Instance;
lock (instance._handlersLock)
{
instance._httpMessageHandlers.Clear();
}
}

#endregion
}

}
60 changes: 13 additions & 47 deletions PortaCapena.OdooJsonRpcClient/OdooClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using PortaCapena.OdooJsonRpcClient.Configurations;
using PortaCapena.OdooJsonRpcClient.Consts;
using PortaCapena.OdooJsonRpcClient.Extensions;
using PortaCapena.OdooJsonRpcClient.Models;
Expand All @@ -23,20 +21,20 @@ public sealed class OdooClient
private static HttpClient _client;
public OdooConfig Config { get; }

[ThreadStatic] private static int? _userUid;
/// <summary>
/// Can be set to false, if server certificate shall not be validated.
/// Useful for test systems without valid SSL certificate
/// </summary>
[ThreadStatic] private static int? _userUid;

/// <summary>
/// Can be set to false, if server certificate shall not be validated.
/// Useful for test systems without valid SSL certificate
/// </summary>
public static bool ValidateServerCertificate { get; set; } = true;

private static string basicAuthenticationUsernamePassword;
/// <summary>
/// Username and Password for Basic Authentication (htaccess).
/// Syntax: username:password
/// </summary>
public static string BasicAuthenticationUsernamePassword
public static string BasicAuthenticationUsernamePassword
{
get => basicAuthenticationUsernamePassword;
set
Expand All @@ -50,43 +48,11 @@ static OdooClient()
{
System.Net.ServicePointManager.Expect100Continue = false;
InitializeHttpClient();
}

private static void InitializeHttpClient()
{
var handler = new HttpClientHandler
{
AllowAutoRedirect = false,
ClientCertificateOptions = ClientCertificateOption.Manual
};

handler.ServerCertificateCustomValidationCallback = ServerCertificateValidation;

_client = new HttpClient(handler);

if (!string.IsNullOrEmpty(BasicAuthenticationUsernamePassword))
{
var byteArray = Encoding.ASCII.GetBytes(BasicAuthenticationUsernamePassword);
_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray));
}

_client.DefaultRequestHeaders.Accept.Clear();
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

private static bool ServerCertificateValidation(HttpRequestMessage httpRequestMessage, X509Certificate2 x509Certificate2,
X509Chain x509Chain, SslPolicyErrors sslPolicyErrors)
private static void InitializeHttpClient()
{
if (!ValidateServerCertificate)
{
return true;
}

if (sslPolicyErrors == SslPolicyErrors.None)
{
return true;
}
return false;
_client = OdooClientHttpFactory.CreateHttpClient();
}

public OdooClient(OdooConfig config)
Expand All @@ -111,16 +77,16 @@ public OdooClient(OdooConfig config)
var tableName = OdooExtensions.GetOdooTableName<T>();
var request = OdooRequestModel.SearchRead(odooConfig, userUid, tableName, query, context);
return await CallAndDeserializeAsync<T[]>(request, cancellationToken);
}
}

public async Task<OdooResult<OdooDictionaryModel[]>> GetAsync(string tableName, OdooQuery query = null, OdooContext context = null, CancellationToken cancellationToken = default)
{
return await ExecuteWitrAccesDenideRetryAsync(userUid => GetAsync(tableName, userUid, query, SelectContext(context, Config.Context), cancellationToken));
}
public async Task<OdooResult<OdooDictionaryModel[]>> GetAsync(string tableName, int userUid, OdooQuery query = null, OdooContext context = null, CancellationToken cancellationToken = default)
{
return await GetAsync(tableName, Config, userUid, query, SelectContext(context, Config.Context), cancellationToken);
}
}
public static async Task<OdooResult<OdooDictionaryModel[]>> GetAsync(string tableName, OdooConfig odooConfig, int userUid, OdooQuery query = null, OdooContext context = null, CancellationToken cancellationToken = default)
{
var request = OdooRequestModel.SearchRead(odooConfig, userUid, tableName, query, context);
Expand Down
Loading