Skip to content

Commit

Permalink
Added support for including a client certificate for certificate vali…
Browse files Browse the repository at this point in the history
…dation
  • Loading branch information
lindsve committed Jun 27, 2023
1 parent 04edb9a commit c3581cd
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 117 deletions.
33 changes: 33 additions & 0 deletions Src/Witsml/QueryLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Serilog;
using Serilog.Core;

namespace Witsml;

public interface IQueryLogger
{
void LogQuery(string querySent, bool isSuccessful, string xmlReceived = null);
}

public class DefaultQueryLogger : IQueryLogger
{
private readonly Logger _queryLogger;

public DefaultQueryLogger()
{
_queryLogger = new LoggerConfiguration()
.WriteTo.File("queries.log", rollOnFileSizeLimit: true, retainedFileCountLimit: 1, fileSizeLimitBytes: 50000000)
.CreateLogger();
}

public void LogQuery(string querySent, bool isSuccessful, string xmlReceived = null)
{
if (xmlReceived != null)
{
_queryLogger.Information("Query: \n{Query}\nReceived: \n{Response}\nIsSuccessful: {IsSuccessful}", querySent, xmlReceived, isSuccessful);
}
else
{
_queryLogger.Information("Query: \n{Query}\nIsSuccessful: {IsSuccessful}", querySent, isSuccessful);
}
}
}
13 changes: 13 additions & 0 deletions Src/Witsml/QueryResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Witsml;

public class QueryResult
{
public bool IsSuccessful { get; }
public string Reason { get; }

public QueryResult(bool isSuccessful, string reason = null)
{
IsSuccessful = isSuccessful;
Reason = reason;
}
}
142 changes: 25 additions & 117 deletions Src/Witsml/WitsmlClient.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Security;
using System.Threading.Tasks;
using System.Xml;

using Serilog;
using Serilog.Core;

using Witsml.Data;
using Witsml.Extensions;
Expand All @@ -28,50 +26,43 @@ public interface IWitsmlClient
Uri GetServerHostname();
}

public class WitsmlClient : IWitsmlClient
public class WitsmlClient : WitsmlClientBase, IWitsmlClient
{
private readonly string _clientCapabilities;
private readonly StoreSoapPortClient _client;
private readonly Uri _serverUrl;
private IQueryLogger _queryLogger;

public WitsmlClient(string hostname, string username, string password, WitsmlClientCapabilities clientCapabilities, TimeSpan? requestTimeout = null, bool logQueries = false)
{
if (string.IsNullOrEmpty(hostname))
{
throw new ArgumentNullException(nameof(hostname), "Hostname is required");
}

if (string.IsNullOrEmpty(username))
[Obsolete("Use the WitsmlClientOptions based constructor instead")]
public WitsmlClient(string hostname, string username, string password, WitsmlClientCapabilities clientCapabilities, TimeSpan? requestTimeout = null,
bool logQueries = false)
: this(new WitsmlClientOptions
{
throw new ArgumentNullException(nameof(username), "Username is required");
}

if (string.IsNullOrEmpty(password))
{
throw new ArgumentNullException(nameof(password), "Password is required");
}

_clientCapabilities = clientCapabilities.ToXml();
Hostname = hostname,
Credentials = new WitsmlCredentials(username, password),
ClientCapabilities = clientCapabilities,
RequestTimeOut = requestTimeout ?? TimeSpan.FromMinutes(1),
LogQueries = logQueries
}) { }

public WitsmlClient(WitsmlClientOptions options)
{
ArgumentNullException.ThrowIfNull(options.Hostname);
ArgumentNullException.ThrowIfNull(options.Credentials.Username);
ArgumentNullException.ThrowIfNull(options.Credentials.Password);

BasicHttpsBinding serviceBinding = CreateBinding(requestTimeout ?? TimeSpan.FromMinutes(1));
EndpointAddress endpointAddress = new(hostname);
_clientCapabilities = options.ClientCapabilities.ToXml();
_serverUrl = new Uri(options.Hostname);

_client = new StoreSoapPortClient(serviceBinding, endpointAddress);
_client.ClientCredentials.UserName.UserName = username;
_client.ClientCredentials.UserName.Password = password;
_serverUrl = new Uri(hostname);
_client = CreateSoapClient(options);

_client.Endpoint.EndpointBehaviors.Add(new EndpointBehavior());
SetupQueryLogging(logQueries);
SetupQueryLogging(options.LogQueries);
}

private void SetupQueryLogging(bool logQueries)
{
if (!logQueries)
{
return;
}

SetQueryLogger(new DefaultQueryLogger());
}
Expand All @@ -81,24 +72,6 @@ public void SetQueryLogger(IQueryLogger queryLogger)
_queryLogger = queryLogger;
}

private static BasicHttpsBinding CreateBinding(TimeSpan requestTimeout)
{
BasicHttpsBinding binding = new()
{
Security =
{
Mode = BasicHttpsSecurityMode.Transport,
Transport =
{
ClientCredentialType = HttpClientCredentialType.Basic
}
},
MaxReceivedMessageSize = int.MaxValue,
SendTimeout = requestTimeout
};
return binding;
}

/// <summary>
/// Returns one or more WITSML data-objects from the server
/// </summary>
Expand Down Expand Up @@ -154,9 +127,7 @@ private async Task<T> GetFromStoreInnerAsync<T>(T query, OptionsIn optionsIn) wh
LogQueriesSentAndReceived(request.QueryIn, response.IsSuccessful(), response.XMLout);

if (response.IsSuccessful())
{
return XmlHelper.Deserialize(response.XMLout, query);
}

WMLS_GetBaseMsgResponse errorResponse = await _client.WMLS_GetBaseMsgAsync(response.Result);
throw new Exception($"Error while querying store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}");
Expand Down Expand Up @@ -186,9 +157,7 @@ private async Task<T> GetFromStoreInnerAsync<T>(T query, OptionsIn optionsIn) wh
LogQueriesSentAndReceived(request.QueryIn, response.IsSuccessful(), response.XMLout);

if (response.IsSuccessful())
{
return (XmlHelper.Deserialize(response.XMLout, query), response.Result);
}

WMLS_GetBaseMsgResponse errorResponse = await _client.WMLS_GetBaseMsgAsync(response.Result);
throw new Exception($"Error while querying store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}");
Expand Down Expand Up @@ -221,9 +190,7 @@ public async Task<string> GetFromStoreAsync(string query, OptionsIn optionsIn)
WMLS_GetFromStoreResponse response = await _client.WMLS_GetFromStoreAsync(request);

if (response.IsSuccessful())
{
return response.XMLout;
}

WMLS_GetBaseMsgResponse errorResponse = await _client.WMLS_GetBaseMsgAsync(response.Result);
throw new Exception($"Error while querying store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}");
Expand All @@ -246,9 +213,7 @@ public async Task<QueryResult> AddToStoreAsync<T>(T query) where T : IWitsmlQuer
LogQueriesSentAndReceived(request.XMLin, response.IsSuccessful());

if (response.IsSuccessful())
{
return new QueryResult(true);
}

WMLS_GetBaseMsgResponse errorResponse = await _client.WMLS_GetBaseMsgAsync(response.Result);
string message = $"Error while adding to store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}";
Expand Down Expand Up @@ -278,9 +243,7 @@ public async Task<QueryResult> UpdateInStoreAsync<T>(T query) where T : IWitsmlQ
LogQueriesSentAndReceived(request.XMLin, response.IsSuccessful());

if (response.IsSuccessful())
{
return new QueryResult(true);
}

WMLS_GetBaseMsgResponse errorResponse = await _client.WMLS_GetBaseMsgAsync(response.Result);
string message = $"Error while updating store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}";
Expand Down Expand Up @@ -310,9 +273,7 @@ public async Task<QueryResult> DeleteFromStoreAsync<T>(T query) where T : IWitsm
LogQueriesSentAndReceived(request.QueryIn, response.IsSuccessful());

if (response.IsSuccessful())
{
return new QueryResult(true);
}

WMLS_GetBaseMsgResponse errorResponse = await _client.WMLS_GetBaseMsgAsync(response.Result);
string message = $"Error while deleting from store: {response.Result} - {errorResponse.Result}. {response.SuppMsgOut}";
Expand All @@ -335,71 +296,18 @@ public async Task<QueryResult> TestConnectionAsync()
}

// Spec requires a comma-seperated list of supported versions without spaces
string[] versions = response.Result.Split(',');

if (!versions.Any(v => v == "1.4.1.1"))
{
var versions = response.Result.Split(',');
if (versions.All(v => v != "1.4.1.1"))
throw new Exception("Error while testing connection: Server does not indicate support for WITSML 1.4.1.1");
}

return new QueryResult(true);
}

private void LogQueriesSentAndReceived(string querySent, bool isSuccessful, string xmlReceived = null)
{
if (_queryLogger == null)
{
return;
}

_queryLogger.LogQuery(querySent, isSuccessful, xmlReceived);
_queryLogger?.LogQuery(querySent, isSuccessful, xmlReceived);
}

public Uri GetServerHostname()
{
return _serverUrl;
}

}

public class QueryResult
{
public bool IsSuccessful { get; }
public string Reason { get; }

public QueryResult(bool isSuccessful, string reason = null)
{
IsSuccessful = isSuccessful;
Reason = reason;
}
}

public interface IQueryLogger
{
void LogQuery(string querySent, bool isSuccessful, string xmlReceived = null);
}

public class DefaultQueryLogger : IQueryLogger
{
private readonly Logger _queryLogger;

public DefaultQueryLogger()
{
_queryLogger = new LoggerConfiguration()
.WriteTo.File("queries.log", rollOnFileSizeLimit: true, retainedFileCountLimit: 1, fileSizeLimitBytes: 50000000)
.CreateLogger();
}

public void LogQuery(string querySent, bool isSuccessful, string xmlReceived = null)
{
if (xmlReceived != null)
{
_queryLogger.Information("Query: \n{Query}\nReceived: \n{Response}\nIsSuccessful: {IsSuccessful}", querySent, xmlReceived, isSuccessful);
}
else
{
_queryLogger.Information("Query: \n{Query}\nIsSuccessful: {IsSuccessful}", querySent, isSuccessful);
}
}
public Uri GetServerHostname() => _serverUrl;
}
}
75 changes: 75 additions & 0 deletions Src/Witsml/WitsmlClientBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;

using Serilog;

using Witsml.ServiceReference;

namespace Witsml;

public abstract class WitsmlClientBase
{
internal static StoreSoapPortClient CreateSoapClient(WitsmlClientOptions options)
{
EndpointAddress endpointAddress = new(options.Hostname);

Binding serviceBinding = options.ClientCertificate == null
? CreateBasicBinding(options.RequestTimeOut)
: CreateCertificateAndBasicBinding();

var client = new StoreSoapPortClient(serviceBinding, endpointAddress);
client.ClientCredentials.UserName.UserName = options.Credentials.Username;
client.ClientCredentials.UserName.Password = options.Credentials.Password;

if (options.ClientCertificate != null)
{
client.ClientCredentials.ClientCertificate.Certificate = options.ClientCertificate;
Log.Information($"Configured client to use client certificate. CN={options.ClientCertificate.SubjectName.Name}");
if (!options.ClientCertificate.HasPrivateKey)
Log.Warning("Configured client certificate does not contain a private key");
}

client.Endpoint.EndpointBehaviors.Add(new EndpointBehavior());

return client;
}

private static BasicHttpsBinding CreateBasicBinding(TimeSpan requestTimeout)
{
return new BasicHttpsBinding
{
Security =
{
Mode = BasicHttpsSecurityMode.Transport,
Transport =
{
ClientCredentialType = HttpClientCredentialType.Basic
}
},
MaxReceivedMessageSize = int.MaxValue,
SendTimeout = requestTimeout
};
}

private static CustomBinding CreateCertificateAndBasicBinding()
{
return new CustomBinding
{
Elements =
{
new TextMessageEncodingBindingElement
{
MessageVersion = MessageVersion.Soap11
},
new HttpsTransportBindingElement
{
RequireClientCertificate = true,
AuthenticationScheme = AuthenticationSchemes.Basic,
MaxReceivedMessageSize = int.MaxValue
}
}
};
}
}
Loading

0 comments on commit c3581cd

Please sign in to comment.