diff --git a/.gitignore b/.gitignore index 1755562..ae3bb31 100644 --- a/.gitignore +++ b/.gitignore @@ -354,3 +354,4 @@ MigrationBackup/ /pki /store /trustlist.zip +/customclientcert diff --git a/AzureFileStorage.cs b/AzureFileStorage.cs deleted file mode 100644 index ea7d79a..0000000 --- a/AzureFileStorage.cs +++ /dev/null @@ -1,164 +0,0 @@ - -namespace Opc.Ua.Cloud.Publisher -{ - using Azure.Storage.Blobs; - using Azure.Storage.Blobs.Models; - using Microsoft.Extensions.Logging; - using System; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using Opc.Ua.Cloud.Publisher.Interfaces; - - public class AzureFileStorage : IFileStorage - { - private readonly ILogger _logger; - - private string _blobContainerName = "uacloudpublisher"; - - public AzureFileStorage(ILoggerFactory logger) - { - _logger = logger.CreateLogger("AzureFileStorage"); - - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONTAINER_NAME"))) - { - _blobContainerName = Environment.GetEnvironmentVariable("STORAGE_CONTAINER_NAME"); - } - } - - public async Task FindFileAsync(string path, string name, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(name)) - { - return null; - } - - try - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"))) - { - // open blob storage - BlobContainerClient container = new BlobContainerClient(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"), _blobContainerName); - await container.CreateIfNotExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - - Diagnostics.Singleton.Info.ConnectedToCloudStorage = true; - - var resultSegment = container.GetBlobsAsync(); - await foreach (BlobItem blobItem in resultSegment.ConfigureAwait(false)) - { - if (blobItem.Name.Contains(path.TrimStart('/')) && blobItem.Name.Contains(name)) - { - return blobItem.Name; - } - } - } - - return null; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - - public async Task StoreFileAsync(string path, byte[] content, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(path) || (content == null)) - { - return null; - } - - try - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"))) - { - // open blob storage - BlobContainerClient container = new BlobContainerClient(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"), _blobContainerName); - await container.CreateIfNotExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - - Diagnostics.Singleton.Info.ConnectedToCloudStorage = true; - - // Get a reference to the blob - BlobClient blob = container.GetBlobClient(path); - - // Open the file and upload its data - using (MemoryStream file = new MemoryStream(content)) - { - await blob.DeleteIfExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - await blob.UploadAsync(file, cancellationToken).ConfigureAwait(false); - - // Verify uploaded - BlobProperties properties = await blob.GetPropertiesAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - if (file.Length != properties.ContentLength) - { - throw new Exception("Could not verify upload!"); - } - - return path; - } - } - - return null; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - - public async Task LoadFileAsync(string name, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(name)) - { - return null; - } - - try - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"))) - { - // open blob storage - BlobContainerClient container = new BlobContainerClient(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"), _blobContainerName); - await container.CreateIfNotExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - - Diagnostics.Singleton.Info.ConnectedToCloudStorage = true; - - var resultSegment = container.GetBlobsAsync(); - await foreach (BlobItem blobItem in resultSegment.ConfigureAwait(false)) - { - if (blobItem.Name.Equals(name)) - { - // Get a reference to the blob - BlobClient blob = container.GetBlobClient(blobItem.Name); - - // Download the blob's contents and save it to a file - BlobDownloadInfo download = await blob.DownloadAsync(cancellationToken).ConfigureAwait(false); - using (MemoryStream file = new MemoryStream()) - { - await download.Content.CopyToAsync(file, cancellationToken).ConfigureAwait(false); - - // Verify download - BlobProperties properties = await blob.GetPropertiesAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - if (file.Length != properties.ContentLength) - { - throw new Exception("Could not verify upload!"); - } - - return file.ToArray(); - } - } - } - } - - return null; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - } -} diff --git a/Controllers/CertManagerController.cs b/Controllers/CertManagerController.cs index 598e779..9ee83dc 100644 --- a/Controllers/CertManagerController.cs +++ b/Controllers/CertManagerController.cs @@ -6,11 +6,14 @@ namespace Opc.Ua.Cloud.Publisher.Controllers using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Logging; using Opc.Ua.Cloud.Publisher.Interfaces; + using Opc.Ua.Cloud.Publisher.Models; using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; + using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; + using System.Text; using System.Threading; using System.Threading.Tasks; @@ -27,10 +30,10 @@ public CertManagerController(IUAApplication app, ILoggerFactory loggerFactory) public IActionResult Index() { - return LoadTrustlist(); + return View("Index", new CertManagerModel() { Certs = new SelectList(LoadTrustlist()) }); } - private IActionResult LoadTrustlist() + private List LoadTrustlist() { List trustList = new(); CertificateTrustList ownTrustList = _app.UAApplicationInstance.ApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates; @@ -39,7 +42,7 @@ private IActionResult LoadTrustlist() trustList.Add(cert.Subject + " [" + cert.Thumbprint + "] "); } - return View("Index", new SelectList(trustList)); + return trustList; } [HttpPost] @@ -68,12 +71,12 @@ public async Task Load(IFormFile file) // store in our own trust list await _app.UAApplicationInstance.AddOwnCertificateToTrustedStoreAsync(certificate, CancellationToken.None).ConfigureAwait(false); - return LoadTrustlist(); + return View("Index", new CertManagerModel() { Certs = new SelectList(LoadTrustlist()) }); } catch (Exception ex) { _logger.LogError(ex.Message); - return View("Index", new SelectList(new List() { ex.Message })); + return View("Index", new CertManagerModel() { Certs = new SelectList(new List() { ex.Message }) }); } } @@ -97,7 +100,29 @@ public ActionResult DownloadTrustlist() catch (Exception ex) { _logger.LogError(ex.Message); - return View("Index", new SelectList(new List() { ex.Message })); + return View("Index", new CertManagerModel() { Certs = new SelectList(new List() { ex.Message }) }); + } + } + [HttpPost] + public ActionResult EncryptString(string plainTextString) + { + try + { + X509Certificate2 cert = _app.IssuerCert; + using RSA rsa = cert.GetRSAPublicKey(); + if (!string.IsNullOrEmpty(plainTextString) && (rsa != null)) + { + return View("Index", new CertManagerModel() { Encrypt = Convert.ToBase64String(rsa.Encrypt(Encoding.UTF8.GetBytes(plainTextString), RSAEncryptionPadding.Pkcs1)), Certs = new SelectList(LoadTrustlist()) }); + } + else + { + throw new Exception("Encryption failed"); + } + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return View("Index", new CertManagerModel() { Certs = new SelectList(new List() { ex.Message }) }); } } } diff --git a/Controllers/ConfigController.cs b/Controllers/ConfigController.cs index bd8504a..537594b 100644 --- a/Controllers/ConfigController.cs +++ b/Controllers/ConfigController.cs @@ -71,13 +71,13 @@ public async Task LocalCertOpen(IFormFile file) // update cert file hash and expiry X509Certificate2 cert = new X509Certificate2(filePath); - Settings.Instance.UACertThumbprint = cert.Thumbprint; - Settings.Instance.UACertExpiry = cert.NotAfter; + Settings.Instance.MQTTClientCertThumbprint = cert.Thumbprint; + Settings.Instance.MQTTClientCertExpiry = cert.NotAfter; } catch (Exception ex) { - Settings.Instance.UACertThumbprint = ex.Message; - Settings.Instance.UACertExpiry = DateTime.MinValue; + Settings.Instance.MQTTClientCertThumbprint = ex.Message; + Settings.Instance.MQTTClientCertExpiry = DateTime.MinValue; } return View("Index", Settings.Instance); diff --git a/Controllers/PublishedController.cs b/Controllers/PublishedController.cs index a129595..ace239a 100644 --- a/Controllers/PublishedController.cs +++ b/Controllers/PublishedController.cs @@ -12,24 +12,22 @@ namespace Opc.Ua.Cloud.Publisher.Controllers using System.Collections.Generic; using System.IO; using System.Text; + using System.Threading.Tasks; public class PublishedController : Controller { private readonly ILogger _logger; private readonly IPublishedNodesFileHandler _publishedNodesFileHandler; private readonly IUAClient _uaclient; - private readonly IFileStorage _storage; public PublishedController( ILoggerFactory loggerFactory, IPublishedNodesFileHandler publishedNodesFileHandler, - IUAClient client, - IFileStorage storage) + IUAClient client) { _logger = loggerFactory.CreateLogger("PublishedController"); _publishedNodesFileHandler = publishedNodesFileHandler; _uaclient = client; - _storage = storage; } public IActionResult Index() @@ -89,25 +87,22 @@ public IActionResult LoadPersisted() { try { - string persistencyFilePath = _storage.FindFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "settings"), "persistency.json").GetAwaiter().GetResult(); - byte[] persistencyFile = _storage.LoadFileAsync(persistencyFilePath).GetAwaiter().GetResult(); + byte[] persistencyFile = System.IO.File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "settings", "persistency.json")); if (persistencyFile == null) { // no file persisted yet - _logger.LogInformation("Persistency file not found."); + throw new Exception("Persistency file not found."); } else { - _logger.LogInformation($"Parsing persistency file..."); - _publishedNodesFileHandler.ParseFile(persistencyFile); - _logger.LogInformation("Persistency file parsed successfully."); + _ = Task.Run(() => _publishedNodesFileHandler.ParseFile(persistencyFile)); } return View("Index", GeneratePublishedNodesArray()); } catch (Exception ex) { - _logger.LogError(ex, "Persistency file not loaded!"); + _logger.LogError(ex.Message); return View("Index", new string[] { "Error: " + ex.Message }); } } diff --git a/Diagnostics.cs b/Diagnostics.cs index 0a04626..e07cdf0 100644 --- a/Diagnostics.cs +++ b/Diagnostics.cs @@ -52,7 +52,6 @@ private void Clear() { Info.PublisherStartTime = DateTime.UtcNow; Info.ConnectedToBroker = false; - Info.ConnectedToCloudStorage = false; Info.NumberOfOpcSessionsConnected = 0; Info.NumberOfOpcSubscriptionsConnected = 0; Info.NumberOfOpcMonitoredItemsMonitored = 0; @@ -98,7 +97,6 @@ public async Task RunAsync(CancellationToken cancellationToken = default) _hubClient.AddOrUpdateTableEntry("Publisher Start Time", Info.PublisherStartTime.ToString()); _hubClient.AddOrUpdateTableEntry("Connected to broker(s)", Info.ConnectedToBroker.ToString()); - _hubClient.AddOrUpdateTableEntry("Connected to cloud storage/OneLake", Info.ConnectedToCloudStorage.ToString()); _hubClient.AddOrUpdateTableEntry("OPC UA sessions", Info.NumberOfOpcSessionsConnected.ToString()); _hubClient.AddOrUpdateTableEntry("OPC UA subscriptions", Info.NumberOfOpcSubscriptionsConnected.ToString()); _hubClient.AddOrUpdateTableEntry("OPC UA monitored items", Info.NumberOfOpcMonitoredItemsMonitored.ToString()); @@ -131,7 +129,6 @@ public async Task RunAsync(CancellationToken cancellationToken = default) if (ticks % 10 == 0) { DiagnosticsSend("ConnectedToBroker", new DataValue(Info.ConnectedToBroker)); - DiagnosticsSend("ConnectedToCloudStorage", new DataValue(Info.ConnectedToCloudStorage)); DiagnosticsSend("NumOpcSessions", new DataValue(Info.NumberOfOpcSessionsConnected)); DiagnosticsSend("NumOpcSubscriptions", new DataValue(Info.NumberOfOpcSubscriptionsConnected)); DiagnosticsSend("NumOpcMonitoredItems", new DataValue(Info.NumberOfOpcMonitoredItemsMonitored)); diff --git a/Interfaces/IFileStorage.cs b/Interfaces/IFileStorage.cs deleted file mode 100644 index 4a4e976..0000000 --- a/Interfaces/IFileStorage.cs +++ /dev/null @@ -1,15 +0,0 @@ - -namespace Opc.Ua.Cloud.Publisher.Interfaces -{ - using System.Threading; - using System.Threading.Tasks; - - public interface IFileStorage - { - Task FindFileAsync(string path, string name, CancellationToken cancellationToken = default); - - Task StoreFileAsync(string name, byte[] content, CancellationToken cancellationToken = default); - - Task LoadFileAsync(string name, CancellationToken cancellationToken = default); - } -} diff --git a/LocalFileStorage.cs b/LocalFileStorage.cs deleted file mode 100644 index a396de6..0000000 --- a/LocalFileStorage.cs +++ /dev/null @@ -1,89 +0,0 @@ - -namespace Opc.Ua.Cloud.Publisher -{ - using Microsoft.Extensions.Logging; - using System; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - using Opc.Ua.Cloud.Publisher.Interfaces; - - public class LocalFileStorage : IFileStorage - { - private readonly ILogger _logger; - - public LocalFileStorage(ILoggerFactory logger) - { - _logger = logger.CreateLogger("LocalFileStorage"); - } - - public Task FindFileAsync(string path, string name, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(name)) - { - return null; - } - - try - { - foreach (string filePath in Directory.GetFiles(path)) - { - if (filePath.Contains(name)) - { - return Task.FromResult(filePath); - } - } - - return Task.FromResult(string.Empty); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return Task.FromResult(string.Empty); - } - } - - public async Task StoreFileAsync(string path, byte[] content, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(path) || (content == null)) - { - return null; - } - - try - { - if (!Directory.Exists(Path.GetDirectoryName(path))) - { - Directory.CreateDirectory(Path.GetDirectoryName(path)); - } - - await File.WriteAllBytesAsync(path, content).ConfigureAwait(false); - - return path; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - - public async Task LoadFileAsync(string path, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(path)) - { - return null; - } - - try - { - return await File.ReadAllBytesAsync(path).ConfigureAwait(false); - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - } -} diff --git a/Models/CertManagerModel.cs b/Models/CertManagerModel.cs new file mode 100644 index 0000000..b7f83b2 --- /dev/null +++ b/Models/CertManagerModel.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Opc.Ua.Cloud.Publisher.Models +{ + public class CertManagerModel + { + public SelectList Certs { get; set; } + + public string Encrypt { get; set; } + } +} diff --git a/OneLakeFileStorage.cs b/OneLakeFileStorage.cs deleted file mode 100644 index cdf55cc..0000000 --- a/OneLakeFileStorage.cs +++ /dev/null @@ -1,190 +0,0 @@ - -namespace Opc.Ua.Cloud.Publisher -{ - using Azure.Identity; - using Azure.Storage.Files.DataLake; - using Azure.Storage.Files.DataLake.Models; - using Microsoft.Extensions.Logging; - using Opc.Ua.Cloud.Publisher.Interfaces; - using System; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - - public class OneLakeFileStorage : IFileStorage - { - private readonly ILogger _logger; - - private string _blobContainerName = "uacloudpublisher"; - - private DeviceCodeCredential _credential; - - private DataLakeServiceClient _dataLakeServiceClient; - - private DataLakeFileSystemClient _fileSystemClient; - - private object _lock = new object(); - - private Task MyDeviceCodeCallback(DeviceCodeInfo info, CancellationToken cancellation) - { - _logger.LogInformation(info.Message); - - Settings.Instance.AuthenticationCode = info.UserCode; - - return Task.CompletedTask; - } - - public OneLakeFileStorage(ILoggerFactory logger) - { - _logger = logger.CreateLogger("OneLakeFileStorage"); - - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONTAINER_NAME"))) - { - _blobContainerName = Environment.GetEnvironmentVariable("STORAGE_CONTAINER_NAME"); - } - - DeviceCodeCredentialOptions options = new() - { - DeviceCodeCallback = MyDeviceCodeCallback - }; - _credential = new(options); - } - - public Task FindFileAsync(string path, string name, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(name)) - { - return null; - } - - try - { - lock (_lock) - { - VerifyOneLakeConnectivity(); - - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"))) - { - string[] connectionStringParts = Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING").Split("/"); - - string dirName = connectionStringParts[4] + "/Files/" + _blobContainerName + path; - foreach (var fspath in _fileSystemClient.GetPaths(dirName)) - { - if (fspath.Name.Contains(dirName + "/" + name)) - { - return Task.FromResult(fspath.Name); - } - } - } - } - - return null; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - - public Task StoreFileAsync(string path, byte[] content, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(path) || (content == null) || (content.Length == 0)) - { - return null; - } - - try - { - lock (_lock) - { - VerifyOneLakeConnectivity(); - - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"))) - { - string[] connectionStringParts = Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING").Split("/"); - - string filePath = connectionStringParts[4] + "/Files/" + _blobContainerName + path; - DataLakeFileClient client = _fileSystemClient.GetFileClient(filePath); - client.Upload(new MemoryStream(content), true); - - return Task.FromResult(path); - } - } - - return null; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - - public Task LoadFileAsync(string name, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(name)) - { - return null; - } - - try - { - lock (_lock) - { - VerifyOneLakeConnectivity(); - - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"))) - { - string[] connectionStringParts = Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING").Split("/"); - - DataLakeFileClient client = _fileSystemClient.GetFileClient(name); - Azure.Response response = client.Read(); - MemoryStream content = new(); - response.Value.Content.CopyTo(content); - return Task.FromResult(content.ToArray()); - } - } - - return null; - } - catch (Exception ex) - { - _logger.LogError(ex.Message); - return null; - } - } - - private void VerifyOneLakeConnectivity() - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING"))) - { - string[] connectionStringParts = Environment.GetEnvironmentVariable("STORAGE_CONNECTION_STRING").Split("/"); - - _dataLakeServiceClient = new DataLakeServiceClient(new Uri("https://" + connectionStringParts[2]), _credential); - _fileSystemClient = _dataLakeServiceClient.GetFileSystemClient(connectionStringParts[3]); - - // make sure our directory exists - string dirName = connectionStringParts[4] + "/Files/" + _blobContainerName; - string authNotification = "Not required - OneLake access authenticated!"; - bool found = false; - foreach (var fspath in _fileSystemClient.GetPaths(connectionStringParts[4] + "/Files")) - { - if (fspath.Name == dirName) - { - found = true; - Settings.Instance.AuthenticationCode = authNotification; - Diagnostics.Singleton.Info.ConnectedToCloudStorage = true; - } - } - - if (!found) - { - _fileSystemClient.CreateDirectory(connectionStringParts[4] + "/Files/" + _blobContainerName); - Settings.Instance.AuthenticationCode = authNotification; - Diagnostics.Singleton.Info.ConnectedToCloudStorage = true; - } - } - } - } -} diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 695da5c..f5a6415 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -31,12 +31,11 @@ "useSSL": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "STORAGE_TYPE": "", - "STORAGE_CONTAINER_NAME": "uacloudpublisher", - "STORAGE_CONNECTION_STRING": "DefaultEndpointsProtocol=https;AccountName=[yourstorageaccountname];AccountKey=[key];EndpointSuffix=core.windows.net", "AZURE_OPENAI_API_ENDPOINT": "https://[yourinstancename].openai.azure.com/", "AZURE_OPENAI_API_KEY": "", - "AZURE_OPENAI_API_DEPLOYMENT_NAME": "" + "AZURE_OPENAI_API_DEPLOYMENT_NAME": "", + "OPCUA_USERNAME": "", + "OPCUA_PASSWORD": "" } } } diff --git a/PublishedNodesFileHandler.cs b/PublishedNodesFileHandler.cs index f11f20f..5fde386 100644 --- a/PublishedNodesFileHandler.cs +++ b/PublishedNodesFileHandler.cs @@ -9,25 +9,52 @@ namespace Opc.Ua.Cloud.Publisher.Configuration using Opc.Ua.Cloud.Publisher.Models; using System; using System.Collections.Generic; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; using System.Text; public class PublishedNodesFileHandler : IPublishedNodesFileHandler { private readonly ILogger _logger; private readonly IUAClient _uaClient; + private readonly IUAApplication _uaApplication; private readonly StatusHubClient _hubClient; public PublishedNodesFileHandler( ILoggerFactory loggerFactory, - IUAClient client) + IUAClient client, + IUAApplication uaApplication) { _logger = loggerFactory.CreateLogger("PublishedNodesFileHandler"); _uaClient = client; + _uaApplication = uaApplication; _hubClient = new StatusHubClient((IHubContext)Program.AppHost.Services.GetService(typeof(IHubContext))); } + private string DecryptString(string encryptedString) + { + if (!string.IsNullOrEmpty(encryptedString)) + { + X509Certificate2 cert = _uaApplication.IssuerCert; + using RSA rsa = cert.GetRSAPrivateKey(); + bool isBase64String = Convert.TryFromBase64String(encryptedString, new Span(new byte[encryptedString.Length]), out int bytesParsed); + if (isBase64String && (rsa != null)) + { + return Encoding.UTF8.GetString(rsa.Decrypt(Convert.FromBase64String(encryptedString), RSAEncryptionPadding.Pkcs1)); + } + else + { + return encryptedString; + } + } + else + { + return string.Empty; + } + } public void ParseFile(byte[] content) { + _logger.LogInformation($"Processing persistency file..."); List _configurationFileEntries = JsonConvert.DeserializeObject>(Encoding.UTF8.GetString(content)); // process loaded config file entries @@ -35,9 +62,8 @@ public void ParseFile(byte[] content) { _logger.LogInformation($"Loaded {_configurationFileEntries.Count} config file entry/entries."); - // figure out how many nodes there are in total - // and capture all unique OPC UA server endpoints - Dictionary uniqueEndpoints = new(); + // figure out how many nodes there are in total and capture all unique OPC UA server endpoints + Dictionary uniqueEndpoints = new(); int totalNodeCount = 0; foreach (PublishNodesInterfaceModel configFileEntry in _configurationFileEntries) { @@ -66,7 +92,7 @@ public void ParseFile(byte[] content) { try { - _uaClient.GDSServerPush(server.EndpointUrl, server.UserName, server.Password); + _uaClient.GDSServerPush(server.EndpointUrl, server.UserName, DecryptString(server.Password)); } catch (Exception ex) { @@ -106,13 +132,21 @@ public void ParseFile(byte[] content) EndpointUrl = new Uri(configFileEntry.EndpointUrl).ToString(), OpcAuthenticationMode = configFileEntry.OpcAuthenticationMode, Username = configFileEntry.UserName, - Password = configFileEntry.Password + Password = DecryptString(configFileEntry.Password) }; publishingInfo.Filter = new List(); publishingInfo.Filter.AddRange(opcEvent.Filter); + try + { _uaClient.PublishNodeAsync(publishingInfo).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + // skip this event and log an error + _logger.LogError("Cannot publish event " + publishingInfo.ExpandedNodeId + " on server " + publishingInfo.EndpointUrl + "due to " + ex.Message); + } currentpublishedNodeCount++; _hubClient.UpdateClientProgressAsync(currentpublishedNodeCount * 100 / totalNodeCount).GetAwaiter().GetResult(); @@ -134,16 +168,26 @@ public void ParseFile(byte[] content) SkipFirst = opcNode.SkipFirst, OpcAuthenticationMode = configFileEntry.OpcAuthenticationMode, Username = configFileEntry.UserName, - Password = configFileEntry.Password + Password = DecryptString(configFileEntry.Password) }; + try + { _uaClient.PublishNodeAsync(publishingInfo).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + // skip this variable and log an error + _logger.LogError("Cannot publish variable " + publishingInfo.ExpandedNodeId + " on server " + publishingInfo.EndpointUrl + "due to " + ex.Message); + } currentpublishedNodeCount++; _hubClient.UpdateClientProgressAsync(currentpublishedNodeCount * 100 / totalNodeCount).GetAwaiter().GetResult(); } } } + + _logger.LogInformation("Publishednodes.json/persistency file processed successfully."); } } } diff --git a/README.md b/README.md index 5535499..a90db4e 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,6 @@ A cross-platform OPC UA cloud publisher reference implementation leveraging OPC - Publishing on data changes or on regular intervals - Supports `publishednodes.json` input file format - Support for storing configuration files locally -- Support for storing configuration files in the Azure cloud -- Support for storing configuration files in Microsoft OneLake - Support for Store & Forward during internet connection outages - Support for username and password authentication - Support for Intel/AMD `x64` and `arm64` architectures (Raspberry Pi4, etc.) with pre-built container images ready for use @@ -80,15 +78,11 @@ And then point your browser to . Note: We have also provided a [test environment](./TestEnvironment/readme.md) to get you started. -### Persisting Settings +### Persisting Logs, Settings, Published Nodes and OPC UA Certificates -UA Cloud Publisher settings and published nodes configuration can be persisted in the Cloud across Docker container restarts by running: +UA Cloud Publisher logs, settings, published nodes and OPC UA certificates can be persisted locally across Docker container restarts by running: -`docker run -itd -e STORAGE_TYPE="Azure" -e STORAGE_CONNECTION_STRING="yourCloudStorageConnectionString" -p 80:80 ghcr.io/barnstee/ua-cloudpublisher:main` - -UA Cloud Publisher settings and published nodes configuration can be persisted locally across Docker container restarts by running: - -`docker run -itd -v c:/publisher/logs:/app/logs -v c:/publisher/settings:/app/settings -p 80:80 ghcr.io/barnstee/ua-cloudpublisher:main` +`docker run -itd -v c:/publisher/logs:/app/logs -v c:/publisher/settings:/app/settings -v c:/publisher/pki:/app/pki -p 80:80 ghcr.io/barnstee/ua-cloudpublisher:main` For Linux hosts, remove the `c:` instances from the command above. @@ -100,10 +94,11 @@ UA Cloud Publisher contains a second broker client that can be used either to ** ## Optional Environment Variables -- `LOG_FILE_PATH` - path to the log file to use. Default is /app/logs/UACloudPublisher.log (in the Docker container). -- `STORAGE_TYPE` - type of storage to use for settings and configuration files. Current options are `Azure` and `OneLake`. Default is local file storage (under `/app/settings/` in the Docker container). -- `STORAGE_CONNECTION_STRING` - when using `STORAGE_TYPE`=`Azure` or `OneLake`, specifies the connection string to the cloud storage. For `OneLake`, this is called `URL` and can be retrieved from your Lakehouse `Files` folder properties in Microsoft Fabric. -- `STORAGE_CONTAINER_NAME` - when using STORAGE_TYPE="Azure" or "OneLake", specifies the storage container name. Default is "uacloudpublisher". +* AZURE_OPENAI_API_ENDPOINT - the endpoint URL of the Azure OpenAI instance to use in the form https://[yourinstancename].openai.azure.com/ +* AZURE_OPENAI_API_KEY - the key to use +* AZURE_OPENAI_API_DEPLOYMENT_NAME - the deployment to use +* OPCUA_USERNAME - OPC UA server username to use when none is specified in publishednodes.json file +* OPCUA_PASSWORD - OPC UA server password to use when none is specified in publishednodes.json file ## PublishedNodes.json File Format @@ -305,7 +300,6 @@ Response: { "PublisherStartTime": "2022-02-22T22:22:22.222Z", "ConnectedToBroker": false, - "ConnectedToCloudStorage": false, "NumberOfOpcSessionsConnected": 0, "NumberOfOpcSubscriptionsConnected": 0, "NumberOfOpcMonitoredItemsMonitored": 0, diff --git a/Settings.cs b/Settings.cs index e63d2e5..0cb4f67 100644 --- a/Settings.cs +++ b/Settings.cs @@ -31,7 +31,7 @@ public static Settings Instance { if (_instance == null) { - _instance = LoadAsync().GetAwaiter().GetResult(); + _instance = Load(); } } } @@ -47,16 +47,14 @@ public static Settings Instance } } - private static async Task LoadAsync() + private static Settings Load() { ILoggerFactory loggerFactory = (ILoggerFactory)Program.AppHost.Services.GetService(typeof(ILoggerFactory)); ILogger logger = loggerFactory.CreateLogger("Settings"); - IFileStorage storage = (IFileStorage)Program.AppHost.Services.GetService(typeof(IFileStorage)); try { - string settingsFilePath = await storage.FindFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "settings"), "settings.json").ConfigureAwait(false); - byte[] settingsFile = await storage.LoadFileAsync(settingsFilePath).ConfigureAwait(false); + byte[] settingsFile = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "settings", "settings.json")); if (settingsFile == null) { // no file persisted yet @@ -79,23 +77,32 @@ private static async Task LoadAsync() } } - public async void Save() + public void Save() { ILoggerFactory loggerFactory = (ILoggerFactory)Program.AppHost.Services.GetService(typeof(ILoggerFactory)); ILogger logger = loggerFactory.CreateLogger("Settings"); - IFileStorage storage = (IFileStorage)Program.AppHost.Services.GetService(typeof(IFileStorage)); - if (await storage.StoreFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "settings", "settings.json"), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(this, Formatting.Indented))).ConfigureAwait(false) == null) + try + { + File.WriteAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "settings", "settings.json"), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(this, Formatting.Indented))); + } + catch (Exception) { logger.LogError("Could not store settings file. Settings won't be persisted!"); } } - public string AuthenticationCode { get; set; } = "Not applicable"; + public string UAClientCertThumbprint { get; set; } = string.Empty; + + public DateTime UAClientCertExpiry { get; set; } = DateTime.MinValue; + + public string UAIssuerCertThumbprint { get; set; } = string.Empty; + + public DateTime UAIssuerCertExpiry { get; set; } = DateTime.MinValue; - public string UACertThumbprint { get; set; } = string.Empty; + public string MQTTClientCertThumbprint { get; set; } = "N/A"; - public DateTime UACertExpiry { get; set; } = DateTime.MinValue; + public DateTime MQTTClientCertExpiry { get; set; } = DateTime.MinValue; public string PublisherName { get; set; } = "UACloudPublisher"; diff --git a/Startup.cs b/Startup.cs index 60cb8f4..01f1281 100644 --- a/Startup.cs +++ b/Startup.cs @@ -37,14 +37,9 @@ public void ConfigureServices(IServiceCollection services) services.AddRazorPages(); services.AddServerSideBlazor(); - string logFilePath = Configuration["LOG_FILE_PATH"]; - if (string.IsNullOrEmpty(logFilePath)) - { - logFilePath = "./logs/UACloudPublisher.log"; - } services.AddLogging(logging => { - logging.AddFile(logFilePath); + logging.AddFile("./logs/UACloudPublisher.log"); }); // add our singletons @@ -75,14 +70,6 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - - // setup file storage - switch (Configuration["STORAGE_TYPE"]) - { - case "Azure": services.AddSingleton(); break; - case "OneLake": services.AddSingleton(); break; - default: services.AddSingleton(); break; - } } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -92,8 +79,7 @@ public void Configure(IApplicationBuilder app, IUAApplication uaApp, IMessageProcessor engine, Settings.BrokerResolver brokerResolver, - IPublishedNodesFileHandler publishedNodesFileHandler, - IFileStorage storage) + IPublishedNodesFileHandler publishedNodesFileHandler) { ILogger logger = loggerFactory.CreateLogger("Statup"); @@ -164,23 +150,20 @@ public void Configure(IApplicationBuilder app, { try { - string persistencyFilePath = storage.FindFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "settings"), "persistency.json").GetAwaiter().GetResult(); - byte[] persistencyFile = storage.LoadFileAsync(persistencyFilePath).GetAwaiter().GetResult(); + byte[] persistencyFile = File.ReadAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "settings", "persistency.json")); if (persistencyFile == null) { // no file persisted yet - logger.LogInformation("Persistency file not found."); + throw new Exception("Persistency file not found."); } else { - logger.LogInformation($"Parsing persistency file..."); - publishedNodesFileHandler.ParseFile(persistencyFile); - logger.LogInformation("Persistency file parsed successfully."); + _ = Task.Run(() => publishedNodesFileHandler.ParseFile(persistencyFile)); } } catch (Exception ex) { - logger.LogError(ex, "Persistency file not loaded!"); + logger.LogError(ex.Message); } } }); diff --git a/UA-CloudPublisher.csproj b/UA-CloudPublisher.csproj index a68601f..64c340e 100644 --- a/UA-CloudPublisher.csproj +++ b/UA-CloudPublisher.csproj @@ -45,9 +45,6 @@ - - - diff --git a/UAApplication.cs b/UAApplication.cs index 77135cf..0273ea0 100644 --- a/UAApplication.cs +++ b/UAApplication.cs @@ -8,6 +8,7 @@ namespace Opc.Ua.Cloud.Publisher using Opc.Ua.Configuration; using System; using System.IO; + using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; @@ -15,7 +16,6 @@ namespace Opc.Ua.Cloud.Publisher public class UAApplication : IUAApplication { private readonly ILogger _logger; - private readonly IFileStorage _storage; public X509Certificate2 IssuerCert { get; set; } @@ -23,67 +23,15 @@ public class UAApplication : IUAApplication public ReverseConnectManager ReverseConnectManager { get; set; } = new(); - public UAApplication(ILoggerFactory loggerFactory, IFileStorage storage) + public UAApplication(ILoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger("UAApplication"); - _storage = storage; } public async Task CreateAsync(CancellationToken cancellationToken = default) { _logger.LogInformation($"Creating OPC UA app named {Settings.Instance.PublisherName}"); - try - { - // load app cert from storage - string certFilePath = await _storage.FindFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "pki", "own", "certs"), Settings.Instance.PublisherName).ConfigureAwait(false); - byte[] certFile = await _storage.LoadFileAsync(certFilePath).ConfigureAwait(false); - if (certFile == null) - { - _logger.LogError("Could not load cert file, creating a new one. This means the new cert needs to be trusted by all OPC UA servers we connect to!"); - } - else - { - if (!Path.IsPathRooted(certFilePath)) - { - certFilePath = Path.DirectorySeparatorChar.ToString() + certFilePath; - } - - if (!Directory.Exists(Path.GetDirectoryName(certFilePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(certFilePath)); - } - - File.WriteAllBytes(certFilePath, certFile); - } - - // load app private key from storage - string keyFilePath = await _storage.FindFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "pki", "own", "private"), Settings.Instance.PublisherName).ConfigureAwait(false); - byte[] keyFile = await _storage.LoadFileAsync(keyFilePath).ConfigureAwait(false); - if (keyFile == null) - { - _logger.LogError("Could not load key file, creating a new one. This means the new cert generated from the key needs to be trusted by all OPC UA servers we connect to!"); - } - else - { - if (!Path.IsPathRooted(keyFilePath)) - { - keyFilePath = Path.DirectorySeparatorChar.ToString() + keyFilePath; - } - - if (!Directory.Exists(Path.GetDirectoryName(keyFilePath))) - { - Directory.CreateDirectory(Path.GetDirectoryName(keyFilePath)); - } - - File.WriteAllBytes(keyFilePath, keyFile); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Cloud not load cert or private key files, creating a new ones. This means the new cert needs to be trusted by all OPC UA servers we connect to!"); - } - // create UA app UAApplicationInstance = new ApplicationInstance { @@ -114,26 +62,8 @@ public async Task CreateAsync(CancellationToken cancellationToken = default) else { // store UA cert thumbprint - Settings.Instance.UACertThumbprint = UAApplicationInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate.Thumbprint; - Settings.Instance.UACertExpiry = UAApplicationInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate.NotAfter; - - // store app certs - foreach (string filePath in Directory.EnumerateFiles(Path.Combine(Directory.GetCurrentDirectory(), "pki", "own", "certs"), "*.der")) - { - await _storage.StoreFileAsync(filePath, await File.ReadAllBytesAsync(filePath).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); - } - - // store private keys - foreach (string filePath in Directory.EnumerateFiles(Path.Combine(Directory.GetCurrentDirectory(), "pki", "own", "private"), "*.pfx")) - { - await _storage.StoreFileAsync(filePath, await File.ReadAllBytesAsync(filePath).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); - } - - // store trusted certs - foreach (string filePath in Directory.EnumerateFiles(Path.Combine(Directory.GetCurrentDirectory(), "pki", "trusted", "certs"), "*.der")) - { - await _storage.StoreFileAsync(filePath, await File.ReadAllBytesAsync(filePath).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); - } + Settings.Instance.UAClientCertThumbprint = UAApplicationInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate.Thumbprint; + Settings.Instance.UAClientCertExpiry = UAApplicationInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate.NotAfter; } _logger.LogInformation($"Application Certificate subject name is: {UAApplicationInstance.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.SubjectName}"); @@ -147,9 +77,14 @@ public async Task CreateAsync(CancellationToken cancellationToken = default) private async Task CreateIssuerCert() { - string certFilePath = await _storage.FindFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "pki", "issuer", "private"), Settings.Instance.PublisherName).ConfigureAwait(false); - byte[] certFile = await _storage.LoadFileAsync(certFilePath).ConfigureAwait(false); - if (certFile == null) + string pathToIssuerStore = Path.Combine(Directory.GetCurrentDirectory(), "pki", "issuer", "private"); + if (!Directory.Exists(pathToIssuerStore)) + { + Directory.CreateDirectory(pathToIssuerStore); + } + + string[] issuerCerts = Directory.GetFiles(pathToIssuerStore); + if ((issuerCerts == null) || (issuerCerts.Count() == 0)) { _logger.LogError("Could not load issuer cert file, creating a new one. This means all conected OPC UA servers need to be issued a new cert!"); @@ -166,8 +101,11 @@ private async Task CreateIssuerCert() } else { - IssuerCert = new X509Certificate2(certFile); + IssuerCert = new X509Certificate2(File.ReadAllBytes(issuerCerts[0])); } + + Settings.Instance.UAIssuerCertThumbprint = IssuerCert.Thumbprint; + Settings.Instance.UAIssuerCertExpiry = IssuerCert.NotAfter; } private void OpcStackLoggingHandler(object sender, TraceEventArgs e) diff --git a/UAClient.cs b/UAClient.cs index 1bf62db..9ff3817 100644 --- a/UAClient.cs +++ b/UAClient.cs @@ -29,7 +29,6 @@ public class UAClient : IUAClient private readonly IUAApplication _app; private readonly ILogger _logger; private readonly ILoggerFactory _loggerFactory; - private readonly IFileStorage _storage; private IMessageSource _trigger; @@ -53,14 +52,12 @@ public class UAClient : IUAClient public UAClient( IUAApplication app, ILoggerFactory loggerFactory, - IMessageSource trigger, - IFileStorage storage) + IMessageSource trigger) { _logger = loggerFactory.CreateLogger("UAClient"); _loggerFactory = loggerFactory; _app = app; _trigger = trigger; - _storage = storage; } public void Dispose() @@ -282,7 +279,7 @@ public void UnpublishAllNodes(bool updatePersistencyFile = true) // update our persistency if (updatePersistencyFile) { - PersistPublishedNodesAsync().GetAwaiter().GetResult(); + PersistPublishedNodes(); } } @@ -629,7 +626,7 @@ public async Task PublishNodeAsync(NodePublishingModel nodeToPublish, Ca Diagnostics.Singleton.Info.NumberOfOpcMonitoredItemsMonitored++; // update our persistency - PersistPublishedNodesAsync().GetAwaiter().GetResult(); + PersistPublishedNodes(); return "Successfully published node " + nodeToPublish.ExpandedNodeId.ToString(); } @@ -716,7 +713,7 @@ public void UnpublishNode(NodePublishingModel nodeToUnpublish) } // update our persistency - PersistPublishedNodesAsync().GetAwaiter().GetResult(); + PersistPublishedNodes(); return; } @@ -848,7 +845,7 @@ public IEnumerable GetPublishedNodes() return publisherConfigurationFileEntries; } - private async Task PersistPublishedNodesAsync(CancellationToken cancellationToken = default) + private void PersistPublishedNodes(CancellationToken cancellationToken = default) { try { @@ -856,10 +853,7 @@ private async Task PersistPublishedNodesAsync(CancellationToken cancellationToke IEnumerable publisherNodeConfiguration = GetPublishedNodes(); // update the persistency file - if (await _storage.StoreFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "settings", "persistency.json"), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(publisherNodeConfiguration, Formatting.Indented)), cancellationToken).ConfigureAwait(false) == null) - { - _logger.LogError("Could not store persistency file. Published nodes won't be persisted!"); - } + File.WriteAllBytes(Path.Combine(Directory.GetCurrentDirectory(), "settings", "persistency.json"), Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(publisherNodeConfiguration, Formatting.Indented))); } catch (Exception ex) { @@ -1071,6 +1065,17 @@ public async Task GDSServerPush(string endpointURL, string adminUsername, string { ServerPushConfigurationClient serverPushClient = new(_app.UAApplicationInstance.ApplicationConfiguration); + // use environment variables if nothing else was specified + if (string.IsNullOrEmpty(adminUsername)) + { + adminUsername = Environment.GetEnvironmentVariable("OPCUA_USERNAME"); + } + + if (string.IsNullOrEmpty(adminPassword)) + { + adminPassword = Environment.GetEnvironmentVariable("OPCUA_PASSWORD"); + } + serverPushClient.AdminCredentials = new UserIdentity(adminUsername, adminPassword); await serverPushClient.Connect(endpointURL).ConfigureAwait(false); diff --git a/Views/CertManager/Index.cshtml b/Views/CertManager/Index.cshtml index a7eea4c..85e4e72 100644 --- a/Views/CertManager/Index.cshtml +++ b/Views/CertManager/Index.cshtml @@ -1,6 +1,6 @@ @using Opc.Ua.Cloud.Publisher.Models -@model IEnumerable +@model CertManagerModel @{ ViewData["Title"] = "Certificate Management"; @@ -13,7 +13,7 @@ Certificates currently in the trust list:

- @Html.ListBox("trust list certificates", Model, new { disabled = true }) + @Html.ListBox("trust list certificates", Model.Certs, new { disabled = true })



@@ -38,5 +38,21 @@ }

+
+
+
+
+

+ Encrypt a string (to encrypt the password for the user specified in the publishednodes.json file):
+ @Html.TextBox("plainTextString", "", new { style = "width:100%;background-color:grey;color:white;" })
+

+

+ +

+

+ Encrypted String (Base64-encoded):
+ @Html.Label("N/A", @Model.Encrypt, null)
+

+
diff --git a/Views/Config/Index.cshtml b/Views/Config/Index.cshtml index 52031f0..b93c545 100644 --- a/Views/Config/Index.cshtml +++ b/Views/Config/Index.cshtml @@ -8,39 +8,43 @@

@ViewData["Title"]

-

- Click here to use the code below for Microsoft OneLake authentication:
- @Html.TextBox("AuthCode", @Model.AuthenticationCode, new { style = "width:100%;background-color:grey;color:white;", @readonly = "true" })
-
- Note: UA Cloud Publisher will show up as Microsoft Azure Cross-platform Command Line Interface. When prompted "Are you trying to sign in to Microsoft Azure CLI?", select continue.
-

-
-
-

- By default, the OPC UA client cert is also used as the MQTT client cert, but a custom MQTT cert can also be provided here: + By default, the OPC UA client cert is also used as the MQTT client cert, but a custom MQTT client cert can be loaded here:

- +




+

+ OPC UA Client Certificate Thumbprint
+ @Html.Label("UAClientCertThumbprint", @Model.UAClientCertThumbprint, null)
+

- OPC UA (by default) & MQTT Client Certificate Thumbprint
- @Html.Label("UACertThumbprint", @Model.UACertThumbprint, null)
+ OPC UA Client Certificate Expiry
+ @Html.Label("UAClientCertExpiry", @Model.UAClientCertExpiry.ToString(), null)
+

+

+ OPC UA Issuer Certificate Thumbprint
+ @Html.Label("UAIssuerCertThumbprint", @Model.UAIssuerCertThumbprint, null)

- OPC UA (by default) & MQTT Client Certificate Expiry
- @Html.Label("UACertExpiry", @Model.UACertExpiry.ToString(), null)
+ OPC UA Issuer Certificate Expiry
+ @Html.Label("UAIssuerCertExpiry", @Model.UAIssuerCertExpiry.ToString(), null)
+

+

+ MQTT Client Certificate Thumbprint
+ @Html.Label("MQTTClientCertThumbprint", @Model.MQTTClientCertThumbprint, null)
+

+

+ MQTT Client Certificate Expiry
+ @Html.Label("MQTTClientCertExpiry", @Model.MQTTClientCertExpiry.ToString(), null)

-
-
-

diff --git a/Views/Published/Index.cshtml b/Views/Published/Index.cshtml index 8e7a5c3..42c1b36 100644 --- a/Views/Published/Index.cshtml +++ b/Views/Published/Index.cshtml @@ -19,7 +19,7 @@

Load previously published nodes

- If configured, UA Cloud Publisher stores its settings and the configuration of all publishes nodes in the cloud.
+ UA Cloud Publisher stores its settings and the configuration of all publishes nodes.