diff --git a/UaClientGateway.sln b/UaClientGateway.sln new file mode 100644 index 0000000..020542b --- /dev/null +++ b/UaClientGateway.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11304.174 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Technosoftware.UaClientGateway", "tools\UaClientGateway\Technosoftware.UaClientGateway.csproj", "{F8CA4A25-A00B-6805-496A-881C91E69BE9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F8CA4A25-A00B-6805-496A-881C91E69BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F8CA4A25-A00B-6805-496A-881C91E69BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F8CA4A25-A00B-6805-496A-881C91E69BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F8CA4A25-A00B-6805-496A-881C91E69BE9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {519D77B6-EA9C-4A11-8248-ABCC01FD533C} + EndGlobalSection +EndGlobal diff --git a/nuget/packages/Technosoftware.UaSolutions.Client.5.0.0-rc.nupkg b/nuget/packages/Technosoftware.UaSolutions.Client.5.0.0-rc.nupkg new file mode 100644 index 0000000..43991c4 Binary files /dev/null and b/nuget/packages/Technosoftware.UaSolutions.Client.5.0.0-rc.nupkg differ diff --git a/nuget/packages/Technosoftware.UaSolutions.UaBindings.Https.5.0.0-rc.nupkg b/nuget/packages/Technosoftware.UaSolutions.UaBindings.Https.5.0.0-rc.nupkg index 5dc1de2..5d6e84f 100644 Binary files a/nuget/packages/Technosoftware.UaSolutions.UaBindings.Https.5.0.0-rc.nupkg and b/nuget/packages/Technosoftware.UaSolutions.UaBindings.Https.5.0.0-rc.nupkg differ diff --git a/nuget/packages/Technosoftware.UaSolutions.UaClient.5.0.0-rc.nupkg b/nuget/packages/Technosoftware.UaSolutions.UaClient.5.0.0-rc.nupkg index 70e3811..067ab7f 100644 Binary files a/nuget/packages/Technosoftware.UaSolutions.UaClient.5.0.0-rc.nupkg and b/nuget/packages/Technosoftware.UaSolutions.UaClient.5.0.0-rc.nupkg differ diff --git a/nuget/packages/Technosoftware.UaSolutions.UaConfiguration.5.0.0-rc.nupkg b/nuget/packages/Technosoftware.UaSolutions.UaConfiguration.5.0.0-rc.nupkg index a70911a..0f6fb1d 100644 Binary files a/nuget/packages/Technosoftware.UaSolutions.UaConfiguration.5.0.0-rc.nupkg and b/nuget/packages/Technosoftware.UaSolutions.UaConfiguration.5.0.0-rc.nupkg differ diff --git a/nuget/packages/Technosoftware.UaSolutions.UaCore.5.0.0-rc.nupkg b/nuget/packages/Technosoftware.UaSolutions.UaCore.5.0.0-rc.nupkg index 398eb23..59c10ff 100644 Binary files a/nuget/packages/Technosoftware.UaSolutions.UaCore.5.0.0-rc.nupkg and b/nuget/packages/Technosoftware.UaSolutions.UaCore.5.0.0-rc.nupkg differ diff --git a/nuget/packages/Technosoftware.UaSolutions.UaServer.5.0.0-rc.nupkg b/nuget/packages/Technosoftware.UaSolutions.UaServer.5.0.0-rc.nupkg index 178429f..803aa8c 100644 Binary files a/nuget/packages/Technosoftware.UaSolutions.UaServer.5.0.0-rc.nupkg and b/nuget/packages/Technosoftware.UaSolutions.UaServer.5.0.0-rc.nupkg differ diff --git a/nuget/packages/Technosoftware.UaSolutions.UaUtilities.5.0.0-rc.nupkg b/nuget/packages/Technosoftware.UaSolutions.UaUtilities.5.0.0-rc.nupkg index ea6c03e..e793102 100644 Binary files a/nuget/packages/Technosoftware.UaSolutions.UaUtilities.5.0.0-rc.nupkg and b/nuget/packages/Technosoftware.UaSolutions.UaUtilities.5.0.0-rc.nupkg differ diff --git a/targets.props b/targets.props index 949d728..8dcd3e5 100644 --- a/targets.props +++ b/targets.props @@ -137,6 +137,7 @@ net10.0;net9.0;net8.0;net48 net10.0 + net48 net472;net48;net8.0;net9.0;net10.0 net472;net48;net8.0;net9.0;net10.0 net472;net48;net8.0;net9.0;net10.0 diff --git a/tools/UaClientGateway/App.ico b/tools/UaClientGateway/App.ico new file mode 100644 index 0000000..5cbc2c8 Binary files /dev/null and b/tools/UaClientGateway/App.ico differ diff --git a/tools/UaClientGateway/ConsoleUtils.cs b/tools/UaClientGateway/ConsoleUtils.cs new file mode 100644 index 0000000..e992802 --- /dev/null +++ b/tools/UaClientGateway/ConsoleUtils.cs @@ -0,0 +1,361 @@ +#region Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +//----------------------------------------------------------------------------- +// Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +// Web: https://technosoftware.com +// +// The Software is based on the OPC Foundation MIT License. +// The complete license agreement for that can be found here: +// http://opcfoundation.org/License/MIT/1.00/ +//----------------------------------------------------------------------------- +#endregion Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved + +#region Using Directives +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Mono.Options; +using Opc.Ua; +using Serilog; +using Serilog.Events; +using Serilog.Templates; +#if NET5_0_OR_GREATER +using Microsoft.Extensions.Configuration; +#endif +#endregion Using Directives + +namespace Technosoftware.UaClientGateway +{ + /// + /// Simple console based telemetry + /// + public sealed class ConsoleTelemetry : ITelemetryContext, IDisposable + { + public ConsoleTelemetry(Action configure = null) + { + LoggerFactory = Microsoft.Extensions.Logging.LoggerFactory + .Create(builder => + { + builder.SetMinimumLevel(LogLevel.Information); + configure?.Invoke(builder); + }) + .AddSerilog(Log.Logger); + + ActivitySource = new ActivitySource("SampleCompany", "1.0.0"); + + m_logger = LoggerFactory.CreateLogger("Main"); + + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException += Unobserved_TaskException; + } + + /// + public ILoggerFactory LoggerFactory { get; internal set; } + + /// + public Meter CreateMeter() + { + return new Meter("SampleCompany", "1.0.0"); + } + + /// + public ActivitySource ActivitySource { get; } + + /// + public void Dispose() + { + CreateMeter().Dispose(); + ActivitySource.Dispose(); + LoggerFactory.Dispose(); + + AppDomain.CurrentDomain.UnhandledException -= CurrentDomain_UnhandledException; + TaskScheduler.UnobservedTaskException -= Unobserved_TaskException; + } + + /// + /// Configure the logging providers. + /// + /// + /// Replaces the Opc.Ua.Core default ILogger with a + /// Microsoft.Extension.Logger with a Serilog file, debug and console logger. + /// The debug logger is only enabled for debug builds. + /// The console logger is enabled by the logConsole flag at the consoleLogLevel. + /// The file logger uses the setting in the ApplicationConfiguration. + /// The Trace logLevel is chosen if required by the Tracemasks. + /// + /// The application configuration. + /// The context name for the logger. + /// Enable logging to the console. + /// Enable logging to a file. + /// Enable application logging. + /// The LogLevel to use for the console/debug.< + /// /param> + public void ConfigureLogging( + ApplicationConfiguration configuration, + string context, + bool logConsole, + bool logFile, + bool logApp, + LogLevel consoleLogLevel) + { + if (!logApp) + { + return; + } + + LoggerConfiguration loggerConfiguration = new LoggerConfiguration().Enrich + .FromLogContext(); + + if (logConsole) + { + loggerConfiguration.WriteTo.Console( + restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel, + formatProvider: CultureInfo.InvariantCulture); + } +#if DEBUG + else + { + loggerConfiguration.WriteTo.Debug( + restrictedToMinimumLevel: (LogEventLevel)consoleLogLevel, + formatProvider: CultureInfo.InvariantCulture); + } +#endif + LogLevel fileLevel = LogLevel.Information; + + // switch for Trace/Verbose output + int traceMasks = configuration.TraceConfiguration.TraceMasks; + if ((traceMasks & + ~( + Utils.TraceMasks.Information | + Utils.TraceMasks.Error | + Utils.TraceMasks.Security | + Utils.TraceMasks.StartStop | + Utils.TraceMasks.StackTrace + )) != 0) + { + fileLevel = LogLevel.Trace; + } + + // add file logging if configured + if (logFile) + { + string outputFilePath = configuration.TraceConfiguration.OutputFilePath; + if (!string.IsNullOrWhiteSpace(outputFilePath)) + { + loggerConfiguration.WriteTo.File( + new ExpressionTemplate( + "{UtcDateTime(@t):yyyy-MM-dd HH:mm:ss.fff} [{@l:u3}] {@m}\n{@x}"), + Utils.ReplaceSpecialFolderNames(outputFilePath), + restrictedToMinimumLevel: (LogEventLevel)fileLevel, + rollOnFileSizeLimit: true + ); + } + } + + // adjust minimum level + if (fileLevel < LogLevel.Information || consoleLogLevel < LogLevel.Information) + { + loggerConfiguration.MinimumLevel.Verbose(); + } + + // create the serilog logger + Serilog.Core.Logger serilogger = loggerConfiguration.CreateLogger(); + + // create the ILogger for Opc.Ua.Core + LoggerFactory = LoggerFactory.AddSerilog(serilogger); + m_logger = LoggerFactory.CreateLogger("Main"); + } + + private void CurrentDomain_UnhandledException( + object sender, + UnhandledExceptionEventArgs args) + { + m_logger.LogCritical( + args.ExceptionObject as Exception, + "Unhandled Exception: (IsTerminating: {IsTerminating})", + args.IsTerminating); + } + + private void Unobserved_TaskException( + object sender, + UnobservedTaskExceptionEventArgs args) + { + m_logger.LogCritical( + args.Exception, + "Unobserved Task Exception (Observed: {Observed})", + args.Observed); + } + + private Microsoft.Extensions.Logging.ILogger m_logger; + } + + /// + /// The error code why the application exit. + /// + public enum ExitCode + { + Ok = 0, + ErrorNotStarted = 0x80, + ErrorRunning = 0x81, + ErrorException = 0x82, + ErrorStopping = 0x83, + ErrorCertificate = 0x84, + ErrorInvalidCommandLine = 0x100 + } + + /// + /// An exception that occured and caused an exit of the application. + /// + [Serializable] + public class ErrorExitException : Exception + { + public ExitCode ExitCode { get; } + + public ErrorExitException(ExitCode exitCode) + { + ExitCode = exitCode; + } + + public ErrorExitException() + { + ExitCode = ExitCode.Ok; + } + + public ErrorExitException(string message) + : base(message) + { + ExitCode = ExitCode.Ok; + } + + public ErrorExitException(string message, ExitCode exitCode) + : base(message) + { + ExitCode = exitCode; + } + + public ErrorExitException(string message, Exception innerException) + : base(message, innerException) + { + ExitCode = ExitCode.Ok; + } + + public ErrorExitException(string message, Exception innerException, ExitCode exitCode) + : base(message, innerException) + { + ExitCode = exitCode; + } + } + + /// + /// Helper functions shared in various console applications. + /// + public static class ConsoleUtils + { + /// + /// Process a command line of the console sample application. + /// + /// + public static string ProcessCommandLine( + string[] args, + Mono.Options.OptionSet options, + ref bool showHelp, + string environmentPrefix, + bool noExtraArgs = true, + TextWriter output = null) + { + output ??= Console.Out; + +#if NET5_0_OR_GREATER + // Convert environment settings to command line flags + // because in some environments (e.g. docker cloud) it is + // the only supported way to pass arguments. + IConfigurationRoot config = new ConfigurationBuilder() + .AddEnvironmentVariables(environmentPrefix + "_") + .Build(); + + List argslist = [.. args]; + foreach (Option option in options) + { + string[] names = option.GetNames(); + string longest = names.MaxBy(s => s.Length); + if (longest != null && longest.Length >= 3) + { + string envKey = config[longest.ToUpperInvariant()]; + if (envKey != null) + { + if (string.IsNullOrWhiteSpace(envKey) || + option.OptionValueType == OptionValueType.None) + { + argslist.Add("--" + longest); + } + else + { + argslist.Add("--" + longest + "=" + envKey); + } + } + } + } + args = [.. argslist]; +#endif + + IList extraArgs = null; + try + { + extraArgs = options.Parse(args); + if (noExtraArgs) + { + foreach (string extraArg in extraArgs) + { + output.WriteLine("Error: Unknown option: {0}", extraArg); + showHelp = true; + } + } + } + catch (OptionException e) + { + output.WriteLine(e.Message); + showHelp = true; + } + + if (showHelp) + { + options.WriteOptionDescriptions(output); + throw new ErrorExitException( + "Invalid Commandline or help requested.", + ExitCode.ErrorInvalidCommandLine + ); + } + + return extraArgs.FirstOrDefault(); + } + + /// + /// Create an event which is set if a user + /// enters the Ctrl-C key combination. + /// + public static ManualResetEvent CtrlCHandler(CancellationTokenSource cts) + { + var quitEvent = new ManualResetEvent(false); + try + { + Console.CancelKeyPress += (_, eArgs) => + { + cts.Cancel(); + quitEvent.Set(); + eArgs.Cancel = true; + }; + } + catch + { + // intentionally left blank + } + return quitEvent; + } + } +} diff --git a/tools/UaClientGateway/MyUaServer.cs b/tools/UaClientGateway/MyUaServer.cs new file mode 100644 index 0000000..0f0e5a8 --- /dev/null +++ b/tools/UaClientGateway/MyUaServer.cs @@ -0,0 +1,386 @@ +#region Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +//----------------------------------------------------------------------------- +// Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +// Web: https://technosoftware.com +// +// The Software is based on the OPC Foundation MIT License. +// The complete license agreement for that can be found here: +// http://opcfoundation.org/License/MIT/1.00/ +//----------------------------------------------------------------------------- +#endregion Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved + +#region Using Directives +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Technosoftware.UaConfiguration; +using Technosoftware.UaServer; +#endregion Using Directives + +namespace Technosoftware.UaClientGateway +{ + /// + /// Main class for the Reference UA server + /// + /// Any class based on the UaStandardServer class. + public class MyUaServer + where T : UaStandardServer, new() + { + #region Public Properties + /// + /// Application instance used by the UA server. + /// + public ApplicationInstance Application { get; private set; } + + /// + /// Application configuration used by the UA server. + /// + public ApplicationConfiguration Configuration => Application.ApplicationConfiguration; + + /// + /// Specifies whether a certificate is automatically accepted (True) or not (False). + /// + public bool AutoAccept { get; set; } + + /// + /// In case the private key is protected by a password it is specified by this property. + /// + public char[] Password { get; set; } + + /// + /// The exit code at the time the server stopped. + /// + public ExitCode ExitCode { get; private set; } + + /// + /// The server object + /// + public T Server { get; private set; } + #endregion Public Properties + + #region Constructors, Destructor, Initialization + /// + /// Ctor of the server. + /// + /// The telemetry context. + public MyUaServer(ITelemetryContext telemetry) + { + m_telemetry = telemetry; + m_logger = telemetry.CreateLogger>(); + } + + /// + /// Load the application configuration. + /// + /// The name of the application. + /// The section name within the configuration. + /// + public async Task LoadAsync(string applicationName, string configSectionName) + { + try + { + ExitCode = ExitCode.ErrorNotStarted; + + ApplicationInstance.MessageDlg = new ApplicationMessageDlg(); + var passwordProvider = new CertificatePasswordProvider(Password); + Application = new ApplicationInstance(m_telemetry) + { + ApplicationName = applicationName, + ApplicationType = ApplicationType.Server, + ConfigSectionName = configSectionName, + CertificatePasswordProvider = passwordProvider, + DisableCertificateAutoCreation = false + }; + + // load the application configuration. + await Application.LoadApplicationConfigurationAsync(false).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new ErrorExitException(ex.Message, ExitCode); + } + } + + /// + /// Load the application configuration. + /// + /// Specifies whether the certificate should be renewed (true) or not (false) + /// + public async Task CheckCertificateAsync(bool renewCertificate) + { + try + { + ApplicationConfiguration config = Application.ApplicationConfiguration; + if (renewCertificate) + { + await Application.DeleteApplicationInstanceCertificateAsync().ConfigureAwait(false); + } + + // check the application certificate. + bool haveAppCertificate = await Application + .CheckApplicationInstanceCertificatesAsync(false) + .ConfigureAwait(false); + if (!haveAppCertificate) + { + throw new ErrorExitException("Application instance certificate invalid!"); + } + + if (!config.SecurityConfiguration.AutoAcceptUntrustedCertificates) + { + config.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler( + OnCertificateValidation + ); + } + } + catch (Exception ex) + { + throw new ErrorExitException(ex.Message, ExitCode); + } + } + + /// + /// Create server instance and add node managers. + /// + /// + public void Create(IList nodeManagerFactories) + { + try + { + // create the server. + Server = new T(); + if (nodeManagerFactories != null) + { + foreach (IUaNodeManagerFactory factory in nodeManagerFactories) + { + Server.AddNodeManager(factory); + } + } + } + catch (Exception ex) + { + throw new ErrorExitException(ex.Message, ExitCode); + } + } + + /// + /// Start the server. + /// + /// + public async Task StartAsync() + { + try + { + // create the server. + Server ??= new T(); + + // start the server + await Application.StartAsync(Server).ConfigureAwait(false); + + // save state + ExitCode = ExitCode.ErrorRunning; + + // print endpoint info + foreach (string endpoint in Application.Server.GetEndpoints().Select(e => e.EndpointUrl).Distinct()) + { + Console.WriteLine(endpoint); + } + + // start the status thread + m_status = Task.Run(StatusThreadAsync); + + // print notification on session events + Server.CurrentInstance.SessionManager.SessionActivated += OnEventStatus; + Server.CurrentInstance.SessionManager.SessionClosing += OnEventStatus; + Server.CurrentInstance.SessionManager.SessionCreated += OnEventStatus; + } + catch (Exception ex) + { + throw new ErrorExitException(ex.Message, ExitCode); + } + } + + /// + /// Stops the server. + /// + /// + public async Task StopAsync() + { + try + { + if (Server != null) + { + using T server = Server; + // Stop status thread + Server = null; + await m_status.ConfigureAwait(false); + + // Stop server and dispose + await server.StopAsync().ConfigureAwait(false); + } + + ExitCode = ExitCode.Ok; + } + catch (Exception ex) + { + throw new ErrorExitException(ex.Message, ExitCode.ErrorStopping); + } + } + + /// + /// The certificate validator is used + /// if auto accept is not selected in the configuration. + /// + private void OnCertificateValidation( + CertificateValidator validator, + CertificateValidationEventArgs e + ) + { + if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted && AutoAccept) + { + m_logger.LogInformation( + "Accepted Certificate: [{Subject}] [{Thumbprint}]", + e.Certificate.Subject, + e.Certificate.Thumbprint + ); + e.Accept = true; + return; + } + m_logger.LogInformation( + "Rejected Certificate: {Error} [{Subject}] [{Thumbprint}]", + e.Error, + e.Certificate.Subject, + e.Certificate.Thumbprint + ); + } + + /// + /// Update the session status. + /// + private void OnEventStatus(object sender, SessionEventArgs eventArgs) + { + IUaSession session = (IUaSession)sender; + + m_lastEventTime = DateTime.UtcNow; + LogSessionStatusFull(session, eventArgs.Reason.ToString()); + } + + /// + /// Output the status of a connected session. + /// + /// The session. + /// The reason + private void LogSessionStatusLastContact(IUaSession session, string reason) + { + lock (session.DiagnosticsLock) + { + m_logger.LogInformation( + "{Reason,9}:{Session,20}:Last Event:{LastContactTime:HH:mm:ss}", + reason, + session.SessionDiagnostics.SessionName, + session.SessionDiagnostics.ClientLastContactTime.ToLocalTime() + ); + } + } + + /// + /// Output the status of a connected session. + /// + private void LogSessionStatusFull(IUaSession session, string reason) + { + lock (session.DiagnosticsLock) + { + m_logger.LogInformation( + "{Reason,9}:{Session,20}:Last Event:{LastContactTime:HH:mm:ss}:{UserIdentity,20}:{SessionId}", + reason, + session.SessionDiagnostics.SessionName, + session.SessionDiagnostics.ClientLastContactTime.ToLocalTime(), + session.Identity?.DisplayName ?? "Anonymous", + session.Id + ); + } + } + + /// + /// Status thread, prints connection status every 10 seconds. + /// + private async Task StatusThreadAsync() + { + while (Server != null) + { + if (DateTime.UtcNow - m_lastEventTime > TimeSpan.FromMilliseconds(10000)) + { + IList sessions = Server.CurrentInstance.SessionManager.GetSessions(); + for (int ii = 0; ii < sessions.Count; ii++) + { + IUaSession session = sessions[ii]; + LogSessionStatusLastContact(session, "-Status-"); + } + m_lastEventTime = DateTime.UtcNow; + } + await Task.Delay(1000).ConfigureAwait(false); + } + } + #endregion Helper Methods + + /// + /// A dialog which asks for user input. + /// + public class ApplicationMessageDlg : IUaApplicationMessageDlg + { + private readonly TextWriter m_output; + private string m_message = string.Empty; + private bool m_ask; + + public ApplicationMessageDlg(TextWriter output = null) + { + m_output = output ?? Console.Out; + } + + public override void Message(string text, bool ask) + { + m_message = text; + m_ask = ask; + } + + public override async Task ShowAsync() + { + if (m_ask) + { + var message = new StringBuilder(m_message); + message.Append(" (y/n, default y): "); + m_output.Write(message.ToString()); + + try + { + ConsoleKeyInfo result = Console.ReadKey(); + m_output.WriteLine(); + return await Task.FromResult(result.KeyChar is 'y' or 'Y' or '\r') + .ConfigureAwait(false); + } + catch + { + // intentionally fall through + } + } + else + { + m_output.WriteLine(m_message); + } + + return await Task.FromResult(true).ConfigureAwait(false); + } + } + + #region Private Fields + private readonly ITelemetryContext m_telemetry; + private readonly ILogger m_logger; + private Task m_status; + private DateTime m_lastEventTime; + #endregion Private Fields + } +} diff --git a/tools/UaClientGateway/Opc.Ua.CertificateGenerator.exe b/tools/UaClientGateway/Opc.Ua.CertificateGenerator.exe deleted file mode 100644 index 48446d6..0000000 Binary files a/tools/UaClientGateway/Opc.Ua.CertificateGenerator.exe and /dev/null differ diff --git a/tools/UaClientGateway/Opc.Ua.Core.dll b/tools/UaClientGateway/Opc.Ua.Core.dll deleted file mode 100644 index c39e831..0000000 Binary files a/tools/UaClientGateway/Opc.Ua.Core.dll and /dev/null differ diff --git a/tools/UaClientGateway/Program.cs b/tools/UaClientGateway/Program.cs new file mode 100644 index 0000000..4fe5f8b --- /dev/null +++ b/tools/UaClientGateway/Program.cs @@ -0,0 +1,195 @@ +#region Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +//----------------------------------------------------------------------------- +// Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +// Web: https://technosoftware.com +// +// The Software is based on the OPC Foundation MIT License. +// The complete license agreement for that can be found here: +// http://opcfoundation.org/License/MIT/1.00/ +//----------------------------------------------------------------------------- +#endregion Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved + +#region Using Directives +using System; +using System.Globalization; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Opc.Ua; +using Technosoftware.Client; +#endregion Using Directives + +namespace Technosoftware.UaClientGateway +{ + /// + /// The program. + /// + public static class Program + { + /// + /// The main entry point for the application. + /// + /// The arguments. + public static async Task Main(string[] args) + { + Console.WriteLine("OPC UA Console Reference Server"); + + #region License validation + //const string licenseData = + // @""; + //bool licensed = Technosoftware.UaServer.LicenseHandler.Validate(licenseData); + //if (!licensed) + //{ + // Console.WriteLine("WARNING: No valid license applied."); + //} + + string licensedString = $" Licensed Product : {Technosoftware.UaUtilities.Licensing.LicenseHandler.LicensedProduct}"; + Console.WriteLine(licensedString); + licensedString = $" Licensed Features : {Technosoftware.UaUtilities.Licensing.LicenseHandler.LicensedFeatures}"; + Console.WriteLine(licensedString); + if (Technosoftware.UaUtilities.Licensing.LicenseHandler.IsEvaluation) + { + licensedString = $" Evaluation expires at: {Technosoftware.UaUtilities.Licensing.LicenseHandler.LicenseExpirationDate}"; + Console.WriteLine(licensedString); + licensedString = $" Days until Expiration: {Technosoftware.UaUtilities.Licensing.LicenseHandler.LicenseExpirationDays}"; + Console.WriteLine(licensedString); + } + licensedString = $" Support Included : {Technosoftware.UaUtilities.Licensing.LicenseHandler.Support}"; + Console.WriteLine(licensedString); + if (Technosoftware.UaUtilities.Licensing.LicenseHandler.Support != Technosoftware.UaUtilities.Licensing.SupportType.None) + { + licensedString = $" Support expire at : {Technosoftware.UaUtilities.Licensing.LicenseHandler.SupportExpirationDate}"; + Console.WriteLine(licensedString); + licensedString = $" Days until Expiration: {Technosoftware.UaUtilities.Licensing.LicenseHandler.SupportExpirationDays}"; + Console.WriteLine(licensedString); + } + if (Technosoftware.UaUtilities.Licensing.LicenseHandler.IsEvaluation) + { + licensedString = $" Evaluation Period : {Technosoftware.UaUtilities.Licensing.LicenseHandler.EvaluationPeriod} minutes."; + Console.WriteLine(licensedString); + } + + if (!Technosoftware.UaUtilities.Licensing.LicenseHandler.IsLicensed && !Technosoftware.UaUtilities.Licensing.LicenseHandler.IsEvaluation) + { + Console.WriteLine("ERROR: No valid license applied."); + } + #endregion License validation + + // The application name and config file name + const string applicationName = "Technosoftware.UaClientGateway"; + const string configSectionName = "Technosoftware.UaClientGateway"; + + // command line options + bool showHelp = false; + bool autoAccept = false; + bool logConsole = false; + bool appLog = true; + bool fileLog = false; + bool renewCertificate = false; + bool shadowConfig = false; + char[] password = null; + int timeout = -1; + + string usage = Utils.IsRunningOnMono() + ? $"Usage: mono {applicationName}.exe [OPTIONS]" + : $"Usage: dotnet {applicationName}.dll [OPTIONS]"; + var options = new Mono.Options.OptionSet + { + usage, + { "h|help", "show this message and exit", h => showHelp = h != null }, + { "a|autoaccept", "auto accept certificates (for testing only)", a => autoAccept = a != null }, + { "c|console", "log to console", c => logConsole = c != null }, + { "l|log", "log app output", c => appLog = c != null }, + { "f|file", "log to file", f => fileLog = f != null }, + { "p|password=", "optional password for private key", p => password = p.ToCharArray() }, + { "r|renew", "renew application certificate", r => renewCertificate = r != null }, + { "t|timeout=", "timeout in seconds to exit application", (int t) => timeout = t * 1000 }, + { "s|shadowconfig", "create configuration in pki root", s => shadowConfig = s != null }, + }; + + using var telemetry = new ConsoleTelemetry(); + ILogger logger = LoggerUtils.Null.Logger; + try + { + // parse command line and set options + ConsoleUtils.ProcessCommandLine(args, options, ref showHelp, "REFSERVER"); + + // log console output to logger + if (logConsole && appLog) + { + logger = telemetry.CreateLogger("Main"); + } + + // create the UA server + var server = new MyUaServer(telemetry) + { + AutoAccept = autoAccept, + Password = password + }; + + // load the server configuration, validate certificates + Console.WriteLine($"Loading configuration from {configSectionName}."); + await server.LoadAsync(applicationName, configSectionName).ConfigureAwait(false); + + // use the shadow config to map the config to an externally accessible location + if (shadowConfig) + { + Console.WriteLine("Using shadow configuration."); + string shadowPath = Directory + .GetParent( + Path.GetDirectoryName( + Utils.ReplaceSpecialFolderNames(server.Configuration.TraceConfiguration.OutputFilePath) + ) + ) + .FullName; + string shadowFilePath = Path.Combine( + shadowPath, + Path.GetFileName(server.Configuration.SourceFilePath) + ); + if (!File.Exists(shadowFilePath)) + { + Console.WriteLine("Create a copy of the config in the shadow location."); + File.Copy(server.Configuration.SourceFilePath, shadowFilePath, true); + } + Console.WriteLine($"Reloading configuration from shadow location {shadowFilePath}."); + await server + .LoadAsync(applicationName, Path.Combine(shadowPath, configSectionName)) + .ConfigureAwait(false); + } + + // setup the logging + telemetry.ConfigureLogging(server.Configuration, applicationName, logConsole, fileLog, appLog, LogLevel.Information); + + // check or renew the certificate + Console.WriteLine("Check the certificate."); + await server.CheckCertificateAsync(renewCertificate).ConfigureAwait(false); + + // Create and add the node managers + // server.Create(NodeManagerUtils.NodeManagerFactories); + + // start the server + Console.WriteLine("Start the server."); + await server.StartAsync().ConfigureAwait(false); + + Console.WriteLine("Server started. Press Ctrl-C to exit..."); + + // wait for timeout or Ctrl-C + var quitCTS = new CancellationTokenSource(); + ManualResetEvent quitEvent = ConsoleUtils.CtrlCHandler(quitCTS); + bool ctrlc = quitEvent.WaitOne(timeout); + + // stop server. May have to wait for clients to disconnect. + Console.WriteLine("Server stopped. Waiting for exit..."); + await server.StopAsync().ConfigureAwait(false); + + return (int)ExitCode.Ok; + } + catch (ErrorExitException eee) + { + Console.WriteLine($"The application exits with error: {eee.Message}"); + return (int)eee.ExitCode; + } + } + } +} diff --git a/tools/UaClientGateway/Properties/AssemblyInfo.cs b/tools/UaClientGateway/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..8c24dad --- /dev/null +++ b/tools/UaClientGateway/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +#region Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +//----------------------------------------------------------------------------- +// Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved +// Web: https://technosoftware.com +// +// The Software is based on the OPC Foundation MIT License. +// The complete license agreement for that can be found here: +// http://opcfoundation.org/License/MIT/1.00/ +//----------------------------------------------------------------------------- +#endregion Copyright (c) 2022-2026 Technosoftware GmbH. All rights reserved + +#region Using Directives +using System; +#endregion Using Directives + +[assembly: CLSCompliant(false)] diff --git a/tools/UaClientGateway/README.md b/tools/UaClientGateway/README.md index d00b309..5ba1cde 100644 --- a/tools/UaClientGateway/README.md +++ b/tools/UaClientGateway/README.md @@ -6,10 +6,33 @@ The OPC UA Client Gateway .NET allows any OPC UA Clients to access Classic OPC The OPC UA Client Gateway .NET can be configured with an integrated configuration tool. With this tool you can specify the Classic OPC Server to be used as well as starting node (folder) within the UA address space to be used for mapping the Classic OPC Server address space. -## Installation - -Use all files from this folder. The executable Technosoftware.UaClientGateway.exe will start the gateway and with the Technosoftware.UaClientGatewayConfigurationTool.exe you can configured the Classic OPC Servers to be used. - -The configuration itself is stored in the configuration file Technosoftware.UaClientGateway.Config.xml +## Configuration + +The configuration is done in the Config.xml file as an extension, e.g.: + + + + + + + opc.com://localhost/Technosoftware.DaSample + DA + 100000 + + + false + + + opc.com://localhost/Technosoftware.AeSample + AE + 100000 + + + + + + + + diff --git a/tools/UaClientGateway/Technosoftware.Client.dll b/tools/UaClientGateway/Technosoftware.Client.dll deleted file mode 100644 index d5d1023..0000000 Binary files a/tools/UaClientGateway/Technosoftware.Client.dll and /dev/null differ diff --git a/tools/UaClientGateway/Technosoftware.CommonControls.dll b/tools/UaClientGateway/Technosoftware.CommonControls.dll deleted file mode 100644 index d6d7288..0000000 Binary files a/tools/UaClientGateway/Technosoftware.CommonControls.dll and /dev/null differ diff --git a/tools/UaClientGateway/Technosoftware.UaClient.dll b/tools/UaClientGateway/Technosoftware.UaClient.dll deleted file mode 100644 index 9018c70..0000000 Binary files a/tools/UaClientGateway/Technosoftware.UaClient.dll and /dev/null differ diff --git a/tools/UaClientGateway/Technosoftware.UaClientGateway.Config.xml b/tools/UaClientGateway/Technosoftware.UaClientGateway.Config.xml index e679997..79b11de 100644 --- a/tools/UaClientGateway/Technosoftware.UaClientGateway.Config.xml +++ b/tools/UaClientGateway/Technosoftware.UaClientGateway.Config.xml @@ -1,32 +1,29 @@  - - + Technosoftware OPC UA Client Gateway .NET - - urn:localhost:Technosoftware:UaClientGateway + uri:technosoftware.com:UaClientGateway - - + + Server_0 - - + - - + Directory - - CN=Technosoftware OPC UA Client Gateway, C=CH, S=Aargau, O=Technosoftware GmbH, DC=localhost - - - - + false - - - false + true + + + true + + 2048 - - true + + false - - + true @@ -162,12 +160,12 @@ xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" Note that "localhost" is replace with the hostname when the configuration is loaded. Additional URLs are created by appending strings to the base address. For example, a URL used for an endpoint which uses the Basic256 security policy would look like this: - http://localhost:55401/TechnosoftwareUaClientGateway/Basic256 --> + https://localhost:55401/TechnosoftwareUaClientGateway/Basic256 --> opc.tcp://localhost:55400/TechnosoftwareUaClientGateway + opc.https://localhost:55401/TechnosoftwareUaClientGateway - - + + SignAndEncrypt_3 + http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 + None_1 http://opcfoundation.org/UA/SecurityPolicy#None + + Sign_2 + + + + SignAndEncrypt_3 + + + @@ -206,17 +234,23 @@ xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" Anonymous_0 - http://opcfoundation.org/UA/SecurityPolicy#None + UserName_1 + + + Certificate_2 + + + @@ -225,7 +259,7 @@ xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" 75 -1000 + 1000 10000 @@ -310,9 +344,8 @@ xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" 0 - - - Technosoftware.UaClientGateway.Endpoints.xml + + Technosoftware.UaClientGateway.Nodes.xml 10000 @@ -321,12 +354,17 @@ xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" 100 10000 - + false + 10000 + 10000 + 10 - Standard UA Server Profile - Data Access Server Facet - Method Server Facet + http://opcfoundation.org/UA-Profile/Server/StandardUA2017 + http://opcfoundation.org/UA-Profile/Server/DataAccess + http://opcfoundation.org/UA-Profile/Server/Methods + http://opcfoundation.org/UA-Profile/Server/ReverseConnect + http://opcfoundation.org/UA-Profile/Server/ClientRedundancy 5 @@ -338,11 +376,41 @@ xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" 0 false + + + + 2500 + 1000 + 1000 + 2500 + 1000 + 1000 + 2500 + 2500 + 2500 + 2500 + 2500 + 2500 + + true - - + + @@ -363,9 +431,10 @@ xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd" - + + - - + - -779 + - + + + 1023 - - - true - \ No newline at end of file diff --git a/tools/UaClientGateway/Technosoftware.UaClientGateway.csproj b/tools/UaClientGateway/Technosoftware.UaClientGateway.csproj new file mode 100644 index 0000000..2372fd2 --- /dev/null +++ b/tools/UaClientGateway/Technosoftware.UaClientGateway.csproj @@ -0,0 +1,44 @@ + + + Technosoftware.UaClientGateway + $(UaClientGatewayTargets) + Exe + Technosoftware.UaClientGateway + Technosoftware.UaClientGateway + Technosoftware GmbH + Technosoftware GmbH UA Client Gateway + Technosoftware GmbH ReferenceServer + Copyright © 2011 - 2026 Technosoftware GmbH + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/tools/UaClientGateway/Technosoftware.UaClientGateway.exe b/tools/UaClientGateway/Technosoftware.UaClientGateway.exe deleted file mode 100644 index 13d7c80..0000000 Binary files a/tools/UaClientGateway/Technosoftware.UaClientGateway.exe and /dev/null differ diff --git a/tools/UaClientGateway/Technosoftware.UaClientGateway.exe.config b/tools/UaClientGateway/Technosoftware.UaClientGateway.exe.config deleted file mode 100644 index d7b3990..0000000 --- a/tools/UaClientGateway/Technosoftware.UaClientGateway.exe.config +++ /dev/null @@ -1,73 +0,0 @@ - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Technosoftware.UaClientGateway.Config.xml - - - - diff --git a/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.Config.xml b/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.Config.xml deleted file mode 100644 index a2eec21..0000000 --- a/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.Config.xml +++ /dev/null @@ -1,190 +0,0 @@ - - - - Technosoftware OPC UA Client Gateway Configuration Tool .NET - - urn:localhost:TechnosoftwareUaClientGatewayConfigurationTool - - http://technosoftware.com/TechnosoftwareUaClientGatewayConfigurationTool/ - - Client_1 - - - - - - - Directory - - %CommonApplicationData%\Technosoftware\CertificateStores\MachineDefault - - Technosoftware OPC UA Client Gateway Configuration Tool .NET - - - - - - Directory - %CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications - - - - - Directory - %CommonApplicationData%\Technosoftware\CertificateStores\UA Certificate Authorities - - - - Directory - %CommonApplicationData%\Technosoftware\CertificateStores\RejectedCertificates - - - true - - - - - - - - - - - 600000 - - 1048576 - - 1048576 - - 65535 - - 4194304 - - 65535 - - 300000 - - 3600000 - - - - - - 60000 - - - opc.tcp://{0}:4840 - http://{0}:52601/UADiscovery - http://{0}/UADiscovery/Default.svc - - - - - Technosoftware.UaClientGatewayConfigurationTool.Endpoints.xml - - 10000 - - - - - - - - - Logs\Technosoftware.UaClientGatewayConfigurationTool.log - true - - - - - - - - 771 - - - - - - - - - true - - \ No newline at end of file diff --git a/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.exe b/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.exe deleted file mode 100644 index d4e6022..0000000 Binary files a/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.exe and /dev/null differ diff --git a/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.exe.config b/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.exe.config deleted file mode 100644 index f504860..0000000 --- a/tools/UaClientGateway/Technosoftware.UaClientGatewayConfigurationTool.exe.config +++ /dev/null @@ -1,50 +0,0 @@ - - - -
- - - - - - - - - - - - - - - Technosoftware.UaClientGatewayConfigurationTool.Config.xml - - - - diff --git a/tools/UaClientGateway/Technosoftware.UaConfiguration.dll b/tools/UaClientGateway/Technosoftware.UaConfiguration.dll deleted file mode 100644 index 1cf5d59..0000000 Binary files a/tools/UaClientGateway/Technosoftware.UaConfiguration.dll and /dev/null differ diff --git a/tools/UaClientGateway/Technosoftware.UaServer.dll b/tools/UaClientGateway/Technosoftware.UaServer.dll deleted file mode 100644 index 67b0ff5..0000000 Binary files a/tools/UaClientGateway/Technosoftware.UaServer.dll and /dev/null differ