diff --git a/Dockerfile b/Dockerfile index 4378e17..456b0cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,52 +2,9 @@ FROM microsoft/dotnet:1.1-sdk COPY /src /build -RUN \ - set -ex \ - && \ - apt-get update && apt-get install -y \ - build-essential \ - git cmake \ - libcurl4-openssl-dev libssl-dev \ - libavahi-compat-libdnssd-dev \ - dbus rsyslog avahi-daemon avahi-utils \ - && \ - git clone https://github.com/marcschier/UA-LDS.git /lds \ - && cd /lds \ - && git submodule init \ - && git submodule update \ - && \ - rm -rf /lds/build && mkdir /lds/build && cd /lds/build \ - && sed 's/LogSystem[[:blank:]]*=.*/LogSystem=file/' /lds/etc/ualds.conf > /lds/etc/ualds.conf \ - && sed 's/LogFile[[:blank:]]*=.*/LogFile=\/app\/Logs\/opcualds.log/' /lds/etc/ualds.conf > /lds/etc/ualds.conf \ - && sed 's/LogFileSize[[:blank:]]*=.*/LogFileSize=10/' /lds/etc/ualds.conf > /lds/etc/ualds.conf \ - && sed 's/CertificateStorePath[[:blank:]]*=.*/CertificateStorePath=\/app\/Shared\/ualds/' /lds/etc/ualds.conf > /lds/etc/ualds.conf \ - && cmake .. && cmake --build . \ - && \ - cp /lds/docker-initd.sh /etc/init.d/lds \ - && echo "service rsyslog start" >> /etc/init.d/lds \ - && echo "service dbus start" >> /etc/init.d/lds \ - && echo "service avahi-daemon restart --no-drop-root --daemonize --syslog" >> /etc/init.d/lds \ - && echo "./lds/build/bin/ualds -c /lds/etc/ualds.conf " >> /etc/init.d/lds \ - && chmod +x /etc/init.d/lds \ - && \ - echo "#!/bin/bash" > /lds/start.sh \ - && echo "service lds start" >> /lds/start.sh \ - && echo "until [ -e /app/Shared/ualds/own/certs/ualdscert.der ]; do" >> /lds/start.sh \ - && echo " sleep 3 " >> /lds/start.sh \ - && echo "done" >> /lds/start.sh \ - && echo 'cp /app/Shared/ualds/own/certs/ualdscert.der "/app/Shared/CertificateStores/UA Applications/certs"' >> /lds/start.sh \ - && echo 'chmod u+x "/app/Shared/CertificateStores/UA Applications/certs/ualdscert.der"' >> /lds/start.sh \ - && echo 'rm -rf /app/Shared/ualds/trusted/certs' >> /lds/start.sh \ - && echo 'ln -s "/app/Shared/CertificateStores/UA Applications/certs" /app/Shared/ualds/trusted/certs' >> /lds/start.sh \ - && echo 'exec dotnet $@' >> /lds/start.sh \ - && chmod +x /lds/start.sh - -EXPOSE 5353 - WORKDIR /build RUN dotnet restore RUN dotnet publish -c Release -o out -WORKDIR /build/out -ENTRYPOINT ["/lds/start.sh"] +WORKDIR /build/out +ENTRYPOINT ["dotnet", "Opc.Ua.Publisher.dll"] diff --git a/src/ModuleConfiguration.cs b/src/ModuleConfiguration.cs index 24bfbd0..6aaae74 100644 --- a/src/ModuleConfiguration.cs +++ b/src/ModuleConfiguration.cs @@ -1,9 +1,12 @@ -using System; -using System.Collections.Generic; + +using System; using System.Security.Cryptography.X509Certificates; namespace Opc.Ua.Publisher { + using System.IO; + using static Opc.Ua.Workarounds.TraceWorkaround; + public class ModuleConfiguration { /// @@ -14,89 +17,95 @@ namespace Opc.Ua.Publisher public ModuleConfiguration(string applicationName) { // set reasonable defaults - Configuration = new ApplicationConfiguration(); - Configuration.ApplicationName = applicationName; + Configuration = new ApplicationConfiguration() + { + ApplicationName = applicationName + }; Configuration.ApplicationUri = "urn:" + Utils.GetHostName() + ":microsoft:" + Configuration.ApplicationName; Configuration.ApplicationType = ApplicationType.ClientAndServer; Configuration.TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }; Configuration.ClientConfiguration = new ClientConfiguration(); Configuration.ServerConfiguration = new ServerConfiguration(); - // enable logging - Configuration.TraceConfiguration = new TraceConfiguration(); - Configuration.TraceConfiguration.TraceMasks = Utils.TraceMasks.Error | Utils.TraceMasks.Security | Utils.TraceMasks.StackTrace | Utils.TraceMasks.StartStop; - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GW_LOGP"))) + // enable logging and enforce information flag + Program.OpcStackTraceMask |= Utils.TraceMasks.Information; + Configuration.TraceConfiguration = new TraceConfiguration() { - Configuration.TraceConfiguration.OutputFilePath = Environment.GetEnvironmentVariable("_GW_LOGP"); + TraceMasks = Program.OpcStackTraceMask + }; + // StdOutAndFile is not working correct, due to a bug in the stack. Need to workaround with own Trace for now. + Utils.SetTraceOutput(Utils.TraceOutput.FileOnly); + if (string.IsNullOrEmpty(Program.LogFileName)) + { + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GW_LOGP"))) + { + Configuration.TraceConfiguration.OutputFilePath = Environment.GetEnvironmentVariable("_GW_LOGP"); + } + else + { + Configuration.TraceConfiguration.OutputFilePath = "./Logs/" + Configuration.ApplicationName + ".log.txt"; + } } else { - Configuration.TraceConfiguration.OutputFilePath = "./Logs/" + Configuration.ApplicationName + ".log.txt"; + Configuration.TraceConfiguration.OutputFilePath = Program.LogFileName; } Configuration.TraceConfiguration.ApplySettings(); + Trace($"Current directory is: {Directory.GetCurrentDirectory()}"); + Trace($"Log file is: {Utils.GetAbsoluteFilePath(Configuration.TraceConfiguration.OutputFilePath, true, false, false, true)}"); + Trace($"Trace mask set to: 0x{Program.OpcStackTraceMask:X}"); - if (Configuration.SecurityConfiguration == null) - { - Configuration.SecurityConfiguration = new SecurityConfiguration(); - } + Configuration.SecurityConfiguration = new SecurityConfiguration(); - if (Configuration.SecurityConfiguration.TrustedPeerCertificates == null) + // Trusted cert store configuration. + Configuration.SecurityConfiguration.TrustedPeerCertificates = new CertificateTrustList(); + Configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType = Program.OpcTrustedCertStoreType; + if (string.IsNullOrEmpty(Program.OpcTrustedCertStorePath)) { - Configuration.SecurityConfiguration.TrustedPeerCertificates = new CertificateTrustList(); - } - - if (Configuration.SecurityConfiguration.TrustedIssuerCertificates == null) - { - Configuration.SecurityConfiguration.TrustedIssuerCertificates = new CertificateTrustList(); - } - - if (Configuration.SecurityConfiguration.RejectedCertificateStore == null) - { - Configuration.SecurityConfiguration.RejectedCertificateStore = new CertificateTrustList(); - } - - if (Configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType == null) - { - Configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType = "Directory"; - } - - if (Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath == null) - { - Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = "CertificateStores/UA Applications"; + // Set default. + Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = Program.OpcTrustedCertStoreType == CertificateStoreType.X509Store ? Program.OpcTrustedCertX509StorePathDefault : Program.OpcTrustedCertDirectoryStorePathDefault; if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_TPC_SP"))) { + // Use environment variable. Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = Environment.GetEnvironmentVariable("_TPC_SP"); } } - - if (Configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType == null) + else { - Configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType = "Directory"; + Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = Program.OpcTrustedCertStorePath; } + Trace($"Trusted Peer Certificate store type is: {Configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType}"); + Trace($"Trusted Peer Certificate store path is: {Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath}"); - if (Configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath == null) + // Trusted issuer cert store configuration. + Configuration.SecurityConfiguration.TrustedIssuerCertificates = new CertificateTrustList(); + Configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType = Program.OpcIssuerCertStoreType; + Configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath = Program.OpcIssuerCertStorePath; + Trace($"Trusted Issuer store type is: {Configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType}"); + Trace($"Trusted Issuer Certificate store path is: {Configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath}"); + + // Rejected cert store configuration. + Configuration.SecurityConfiguration.RejectedCertificateStore = new CertificateTrustList(); + Configuration.SecurityConfiguration.RejectedCertificateStore.StoreType = Program.OpcRejectedCertStoreType; + Configuration.SecurityConfiguration.RejectedCertificateStore.StorePath = Program.OpcRejectedCertStorePath; + Trace($"Rejected certificate store type is: {Configuration.SecurityConfiguration.RejectedCertificateStore.StoreType}"); + Trace($"Rejected Certificate store path is: {Configuration.SecurityConfiguration.RejectedCertificateStore.StorePath}"); + + Configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier() { - Configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath = "CertificateStores/UA Certificate Authorities"; - } - - if (Configuration.SecurityConfiguration.RejectedCertificateStore.StoreType == null) - { - Configuration.SecurityConfiguration.RejectedCertificateStore.StoreType = "Directory"; - } - - if (Configuration.SecurityConfiguration.RejectedCertificateStore.StorePath == null) - { - Configuration.SecurityConfiguration.RejectedCertificateStore.StorePath = "CertificateStores/Rejected Certificates"; - } - - Configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier(); - Configuration.SecurityConfiguration.ApplicationCertificate.StoreType = "X509Store"; - Configuration.SecurityConfiguration.ApplicationCertificate.StorePath = "CurrentUser\\UA_MachineDefault"; - Configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = Configuration.ApplicationName; + StoreType = Program.OpcOwnCertStoreType, + StorePath = Program.OpcOwnCertStorePath, + SubjectName = Configuration.ApplicationName + }; + Trace($"Application Certificate store type is: {Configuration.SecurityConfiguration.ApplicationCertificate.StoreType}"); + Trace($"Application Certificate store path is: {Configuration.SecurityConfiguration.ApplicationCertificate.StorePath}"); + // Use existing certificate, if it is there. X509Certificate2 certificate = Configuration.SecurityConfiguration.ApplicationCertificate.Find(true).Result; if (certificate == null) { + Trace($"Create a self-signed Application certificate valid from yesterday for {CertificateFactory.defaultLifeTime} months,"); + Trace($"with a {CertificateFactory.defaultKeySize} bit key and {CertificateFactory.defaultHashSize} bit hash."); certificate = CertificateFactory.CreateCertificate( Configuration.SecurityConfiguration.ApplicationCertificate.StoreType, Configuration.SecurityConfiguration.ApplicationCertificate.StorePath, @@ -113,75 +122,84 @@ namespace Opc.Ua.Publisher null, null ); - } - if (certificate == null) - { - throw new Exception("OPC UA application certificate could not be created, cannot continue without it!"); - } + Configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate ?? throw new Exception("OPC UA application certificate could not be created! Cannot continue without it!"); - Configuration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate; - Configuration.ApplicationUri = Utils.GetApplicationUriFromCertificate(certificate); - - // Ensure it is trusted - try - { - ICertificateStore store = Configuration.SecurityConfiguration.TrustedPeerCertificates.OpenStore(); - if (store == null) + // Trust myself if requested. + if (Program.TrustMyself) { - Program.Trace("Could not open trusted peer store. StorePath={0}", Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath); + // Ensure it is trusted + try + { + ICertificateStore store = Configuration.SecurityConfiguration.TrustedPeerCertificates.OpenStore(); + if (store == null) + { + Trace($"Could not open trusted peer store. StorePath={Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath}"); + } + else + { + try + { + Trace($"Adding publisher certificate to trusted peer store. StorePath={Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath}"); + X509Certificate2 publicKey = new X509Certificate2(certificate.RawData); + store.Add(publicKey).Wait(); + } + finally + { + store.Close(); + } + } + } + catch (Exception e) + { + Trace(e, $"Could not add publisher certificate to trusted peer store. StorePath={Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath}"); + } } else { - try - { - Program.Trace(Utils.TraceMasks.Information, "Adding certificate to trusted peer store. StorePath={0}", Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath); - X509Certificate2 publicKey = new X509Certificate2(certificate.RawData); - store.Add(publicKey).Wait(); - } - finally - { - store.Close(); - } + Trace("Publisher certificate is not added to trusted peer store."); } } - catch (Exception e) + else { - Program.Trace(e, "Could not add certificate to trusted peer store. StorePath={0}", Configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath); + Trace("Application certificate found in Application Certificate Store"); } - + Configuration.ApplicationUri = Utils.GetApplicationUriFromCertificate(certificate); + Trace($"Application certificate is for Application URI: {Configuration.ApplicationUri}"); + // patch our base address if (Configuration.ServerConfiguration.BaseAddresses.Count == 0) { - Configuration.ServerConfiguration.BaseAddresses.Add("opc.tcp://" + Configuration.ApplicationName.ToLowerInvariant() + ":62222/UA/Publisher"); + Configuration.ServerConfiguration.BaseAddresses.Add($"opc.tcp://{Configuration.ApplicationName.ToLowerInvariant()}:{Program.PublisherServerPort}{Program.PublisherServerPath}"); } - - // tighten security policy by removing security policy "none" - foreach (ServerSecurityPolicy policy in Configuration.ServerConfiguration.SecurityPolicies) + foreach (var endpoint in Configuration.ServerConfiguration.BaseAddresses) { - if (policy.SecurityMode == MessageSecurityMode.None) - { - Configuration.ServerConfiguration.SecurityPolicies.Remove(policy); - break; - } + Trace($"Publisher server Endpoint URL: {endpoint}"); } - // turn off LDS registration - Configuration.ServerConfiguration.MaxRegistrationInterval = 0; + // Set LDS registration interval + Configuration.ServerConfiguration.MaxRegistrationInterval = Program.LdsRegistrationInterval; + Trace($"LDS(-ME) registration intervall set to {Program.LdsRegistrationInterval} ms (0 means no registration)"); // add sign & encrypt policy - ServerSecurityPolicy newPolicy = new ServerSecurityPolicy(); - newPolicy.SecurityMode = MessageSecurityMode.SignAndEncrypt; - newPolicy.SecurityPolicyUri = SecurityPolicies.Basic128Rsa15; + ServerSecurityPolicy newPolicy = new ServerSecurityPolicy() + { + SecurityMode = MessageSecurityMode.SignAndEncrypt, + SecurityPolicyUri = SecurityPolicies.Basic256Sha256 + }; Configuration.ServerConfiguration.SecurityPolicies.Add(newPolicy); + Trace($"Security policy {newPolicy.SecurityPolicyUri} with mode {newPolicy.SecurityMode} added"); // the OperationTimeout should be twice the minimum value for PublishingInterval * KeepAliveCount, so set to 120s - Configuration.TransportQuotas.OperationTimeout = 120000; + Configuration.TransportQuotas.OperationTimeout = Program.OpcOperationTimeout; + Trace($"OperationTimeout set to {Configuration.TransportQuotas.OperationTimeout}"); // allow SHA1 certificates for now as many OPC Servers still use them Configuration.SecurityConfiguration.RejectSHA1SignedCertificates = false; + Trace($"Rejection of SHA1 signed certificates is {(Configuration.SecurityConfiguration.RejectSHA1SignedCertificates ? "enabled" : "disabled")}"); // allow 1024 minimum key size as many OPC Servers still use them Configuration.SecurityConfiguration.MinimumCertificateKeySize = 1024; + Trace($"Minimum certificate key size set to {Configuration.SecurityConfiguration.MinimumCertificateKeySize}"); // validate the configuration now Configuration.Validate(Configuration.ApplicationType).Wait(); diff --git a/src/NodeManager.cs b/src/NodeManager.cs index f30e4ae..e0189d2 100644 --- a/src/NodeManager.cs +++ b/src/NodeManager.cs @@ -15,9 +15,11 @@ namespace Publisher public PublisherNodeManager(Opc.Ua.Server.IServerInternal server, ApplicationConfiguration configuration) : base(server) { - List namespaceUris = new List(); - namespaceUris.Add(Namespaces.Publisher); - namespaceUris.Add(Namespaces.Publisher + "/Instance"); + List namespaceUris = new List + { + Namespaces.Publisher, + Namespaces.Publisher + "/Instance" + }; NamespaceUris = namespaceUris; m_typeNamespaceIndex = Server.NamespaceUris.GetIndexOrAppend(namespaceUris[0]); diff --git a/src/Opc.Ua.Publisher.csproj b/src/Opc.Ua.Publisher.csproj index 4c8783e..988f292 100644 --- a/src/Opc.Ua.Publisher.csproj +++ b/src/Opc.Ua.Publisher.csproj @@ -37,9 +37,11 @@ - - + + + + diff --git a/src/Program.cs b/src/Program.cs index 0b083c1..0247a50 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -2,159 +2,368 @@ using IoTHubCredentialTools; using Microsoft.Azure.Devices; using Microsoft.Azure.Devices.Client; +using Mono.Options; using Newtonsoft.Json; using Opc.Ua.Client; using Publisher; using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Text; using System.Threading.Tasks; namespace Opc.Ua.Publisher { + using static Opc.Ua.CertificateStoreType; + using static Opc.Ua.Workarounds.TraceWorkaround; + using static System.Console; + public class Program { public static ApplicationConfiguration m_configuration = null; public static List m_sessions = new List(); public static PublishedNodesCollection m_nodesLookups = new PublishedNodesCollection(); public static List m_endpointUrls = new List(); - public static string m_applicationName = string.Empty; + public static string ApplicationName { get; set; } public static DeviceClient m_deviceClient = null; + public static string IoTHubOwnerConnectionString { get; set; } + public static string LogFileName { get; set; } + public static ushort PublisherServerPort { get; set; } = 62222; + public static string PublisherServerPath { get; set; } = "/UA/Publisher"; + public static int LdsRegistrationInterval { get; set; } = 0; + public static int OpcOperationTimeout { get; set; } = 120000; + public static bool TrustMyself { get; set; } = true; + public static int OpcStackTraceMask { get; set; } = Utils.TraceMasks.Error | Utils.TraceMasks.Security | Utils.TraceMasks.StackTrace | Utils.TraceMasks.StartStop | Utils.TraceMasks.Information; + public static string PublisherServerSecurityPolicy { get; set; } = SecurityPolicies.Basic128Rsa15; - private static PublisherServer m_server = new PublisherServer(); + public static string OpcOwnCertStoreType { get; set; } = X509Store; + private const string _opcOwnCertDirectoryStorePathDefault = "CertificateStores/own"; + private const string _opcOwnCertX509StorePathDefault = "CurrentUser\\UA_MachineDefault"; + public static string OpcOwnCertStorePath { get; set; } = _opcOwnCertX509StorePathDefault; - /// - /// Trace message helper - /// - public static void Trace(string message, params object[] args) + public static string OpcTrustedCertStoreType { get; set; } = Directory; + public static string OpcTrustedCertDirectoryStorePathDefault = "CertificateStores/UA Applications"; + public static string OpcTrustedCertX509StorePathDefault = "CurrentUser\\UA_MachineDefault"; + public static string OpcTrustedCertStorePath { get; set; } = null; + + public static string OpcRejectedCertStoreType { get; set; } = Directory; + private const string _opcRejectedCertDirectoryStorePathDefault = "CertificateStores/Rejected Certificates"; + private const string _opcRejectedCertX509StorePathDefault = "CurrentUser\\UA_MachineDefault"; + public static string OpcRejectedCertStorePath { get; set; } = _opcRejectedCertDirectoryStorePathDefault; + + public static string OpcIssuerCertStoreType { get; set; } = Directory; + private const string _opcIssuerCertDirectoryStorePathDefault = "CertificateStores/UA Certificate Authorities"; + private const string _opcIssuerCertX509StorePathDefault = "CurrentUser\\UA_MachineDefault"; + public static string OpcIssuerCertStorePath { get; set; } = _opcIssuerCertDirectoryStorePathDefault; + + public static string IotDeviceCertStoreType { get; set; } = X509Store; + private const string _iotDeviceCertDirectoryStorePathDefault = "CertificateStores/IoTHub"; + private const string _iotDeviceCertX509StorePathDefault = "IoTHub"; + public static string IotDeviceCertStorePath { get; set; } = _iotDeviceCertX509StorePathDefault; + + public static string PublishedNodesAbsFilenameDefault = $"{System.IO.Directory.GetCurrentDirectory()}{Path.DirectorySeparatorChar}publishednodes.json"; + public static string PublishedNodesAbsFilename { get; set; } + + + private static PublisherServer _publishingServer = new PublisherServer(); + public static Microsoft.Azure.Devices.Client.TransportType IotHubProtocol { get; set; } = Microsoft.Azure.Devices.Client.TransportType.Mqtt; + + private static void Usage(Mono.Options.OptionSet options) { - Utils.Trace(Utils.TraceMasks.Error, message, args); - Console.WriteLine(DateTime.Now.ToString() + ": " + message, args); - } - public static void Trace(int traceMask, string format, params object[] args) - { - Utils.Trace(traceMask, format, args); - Console.WriteLine(DateTime.Now.ToString() + ": " + format, args); - } - - public static void Trace(Exception e, string format, params object[] args) - { - Utils.Trace(e, format, args); - Console.WriteLine(DateTime.Now.ToString() + ": " + e.Message.ToString()); - Console.WriteLine(DateTime.Now.ToString() + ": " + format, args); + // show usage + WriteLine(); + WriteLine("Usage: {0}.exe applicationname [iothubconnectionstring] [options]", Assembly.GetEntryAssembly().GetName().Name); + WriteLine(); + WriteLine("OPC Edge Publisher to subscribe to configured OPC UA servers and send telemetry to Azure IoTHub."); + WriteLine(); + WriteLine("applicationname: the OPC UA application name to use, required"); + WriteLine(" The application name is also used to register the publisher under this name in the"); + WriteLine(" IoTHub device registry."); + WriteLine(); + WriteLine("iothubconnectionstring: the IoTHub owner connectionstring, optional"); + WriteLine(); + WriteLine("There are a couple of environemnt variables which could be used to control the application:"); + WriteLine("_HUB_CS: sets the IoTHub owner connectionstring"); + WriteLine("_GW_LOGP: sets the filename of the log file to use"); + WriteLine("_TPC_SP: sets the path to store certificates of trusted stations"); + WriteLine("_GW_PNFP: sets the filename of the publishing configuration file"); + WriteLine(); + WriteLine("Notes:"); + WriteLine("If an environment variable is controlling the OPC UA stack configuration, they are only taken into account"); + WriteLine("if they are not set in the OPC UA configuration file."); + WriteLine("Command line arguments overrule OPC UA configuration file settings and environement variable settings."); + WriteLine(); + + // output the options + WriteLine("Options:"); + options.WriteOptionDescriptions(Console.Out); } public static void Main(string[] args) { + var opcTraceInitialized = false; try { - if ((args.Length == 0) || string.IsNullOrEmpty(args[0]) || args[0].Equals("localhost", StringComparison.OrdinalIgnoreCase)) + var shouldShowHelp = false; + + // these are the available options, not that they set the variables + Mono.Options.OptionSet options = new Mono.Options.OptionSet { + // Publishing configuration options + { "pf|publishfile=", $"the filename to configure the nodes to publish.\nDefault: '{PublishedNodesAbsFilenameDefault}'", (string p) => PublishedNodesAbsFilename = p }, + + // IoTHub specific options + { "ih|iothubprotocol=", $"the protocol to use for communication with Azure IoTHub (allowed values: {string.Join(", ", Enum.GetNames(IotHubProtocol.GetType()))}).\nDefault: {Enum.GetName(IotHubProtocol.GetType(), IotHubProtocol)}", + (Microsoft.Azure.Devices.Client.TransportType p) => IotHubProtocol = p + }, + + // opc server configuration options + { "lf|logfile=", $"the filename of the logfile to use.\nDefault: './logs/.log.txt'", (string l) => LogFileName = l }, + { "pn|portnum=", $"the server port of the publisher OPC server endpoint.\nDefault: {PublisherServerPort}", (ushort p) => PublisherServerPort = p }, + { "pa|path=", $"the enpoint URL path part of the publisher OPC server endpoint.\nDefault: '{PublisherServerPath}'", (string a) => PublisherServerPath = a }, + { "lr|ldsreginterval=", $"the LDS(-ME) registration interval in ms.\nDefault: {LdsRegistrationInterval}", (int i) => LdsRegistrationInterval = i }, + { "ot|operationtimeout=", $"the operation timeout of the publisher OPC UA client in ms.\nDefault: {OpcOperationTimeout}", (int i) => OpcOperationTimeout = i }, + { "st|opcstacktracemask=", $"the trace mask for the OPC stack. See github OPC .NET stack for definitions.\n(Information is enforced)\nDefault: 0x{OpcStackTraceMask:X}", (int i) => OpcStackTraceMask = i }, + + // trust own public cert option + { "tm|trustmyself=", $"the publisher certificate is put into the trusted certificate store automatically.\nDefault: {TrustMyself}", (bool b) => TrustMyself = b }, + + // own cert store options + { "at|appcertstoretype=", $"the own application cert store type. \n(allowed values: Directory, X509Store)\nDefault: '{OpcOwnCertStoreType}'", (string s) => { + if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) + { + OpcOwnCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; + } + else + { + throw new OptionException(); + } + } + }, + { "ap|appcertstorepath=", $"the path where the own application cert should be stored\nDefault (depends on store type):\n" + + $"X509Store: '{_opcOwnCertX509StorePathDefault}'\n" + + $"Directory: '{_opcOwnCertDirectoryStorePathDefault}'", (string s) => OpcOwnCertStorePath = s + }, + + // trusted cert store options + { + "tt|trustedcertstoretype=", $"the trusted cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcTrustedCertStoreType}", (string s) => { + if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) + { + OpcTrustedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; + } + else + { + throw new OptionException(); + } + } + }, + { "tp|trustedcertstorepath=", $"the path of the trusted cert store\nDefault (depends on store type):\n" + + $"X509Store: '{OpcTrustedCertX509StorePathDefault}'\n" + + $"Directory: '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s + }, + + // rejected cert store options + { "rt|rejectedcertstoretype=", $"the rejected cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcRejectedCertStoreType}", (string s) => { + if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) + { + OpcRejectedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; + } + else + { + throw new OptionException(); + } + } + }, + { "rp|rejectedcertstorepath=", $"the path of the rejected cert store\nDefault (depends on store type):\n" + + $"X509Store: '{_opcRejectedCertX509StorePathDefault}'\n" + + $"Directory: '{_opcRejectedCertDirectoryStorePathDefault}'", (string s) => OpcRejectedCertStorePath = s + }, + + // issuer cert store options + { + "it|issuercertstoretype=", $"the trusted issuer cert store type. \n(allowed values: Directory, X509Store)\nDefault: {OpcIssuerCertStoreType}", (string s) => { + if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) + { + OpcIssuerCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; + } + else + { + throw new OptionException(); + } + } + }, + { "p|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault (depends on store type):\n" + + $"X509Store: '{_opcIssuerCertX509StorePathDefault}'\n" + + $"Directory: '{_opcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s + }, + + // device connection string cert store options + { "dt|devicecertstoretype=", $"the iothub device cert store type. \n(allowed values: Directory, X509Store)\nDefault: {IotDeviceCertStoreType}", (string s) => { + if (s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) || s.Equals(Directory, StringComparison.OrdinalIgnoreCase)) + { + IotDeviceCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : Directory; + IotDeviceCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? _iotDeviceCertX509StorePathDefault : _iotDeviceCertDirectoryStorePathDefault; + } + else + { + throw new OptionException(); + } + } + }, + { "dp|devicecertstorepath=", $"the path of the iot device cert store\nDefault Default (depends on store type):\n" + + $"X509Store: '{_iotDeviceCertX509StorePathDefault}'\n" + + $"Directory: '{_iotDeviceCertDirectoryStorePathDefault}'", (string s) => IotDeviceCertStorePath = s + }, + + // misc + { "h|help", "show this message and exit", h => shouldShowHelp = h != null }, + }; + + List arguments; + try { - m_applicationName = Utils.GetHostName(); + // parse the command line + arguments = options.Parse(args); } - else + catch (OptionException e) { - m_applicationName = args[0]; + // show usage + Usage(options); + return; } - Trace("Publisher is starting up..."); - ModuleConfiguration moduleConfiguration = new ModuleConfiguration(m_applicationName); + // Validate and parse arguments. + if (arguments.Count > 2 || shouldShowHelp) + { + Usage(options); + return; + } + else if (arguments.Count == 2) + { + ApplicationName = arguments[0]; + IoTHubOwnerConnectionString = arguments[1]; + } + else if (arguments.Count == 1) + { + ApplicationName = arguments[0]; + } + else { + ApplicationName = Utils.GetHostName(); + } + + WriteLine("Publisher is starting up..."); + ModuleConfiguration moduleConfiguration = new ModuleConfiguration(ApplicationName); + opcTraceInitialized = true; m_configuration = moduleConfiguration.Configuration; m_configuration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation); // start our server interface try { - Trace("Starting server on endpoint " + m_configuration.ServerConfiguration.BaseAddresses[0].ToString() + "..."); - m_server.Start(m_configuration); + Trace($"Starting server on endpoint {m_configuration.ServerConfiguration.BaseAddresses[0].ToString()} ..."); + _publishingServer.Start(m_configuration); Trace("Server started."); } catch (Exception ex) { - Trace("Starting server failed with: " + ex.Message); + Trace($"Starting server failed with: {ex.Message}"); + Trace("exiting..."); + return; } // check if we also received an owner connection string - string ownerConnectionString = string.Empty; - if ((args.Length > 1) && !string.IsNullOrEmpty(args[1])) - { - ownerConnectionString = args[1]; - } - else + if (string.IsNullOrEmpty(IoTHubOwnerConnectionString)) { Trace("IoT Hub owner connection string not passed as argument."); // check if we have an environment variable to register ourselves with IoT Hub if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_HUB_CS"))) { - ownerConnectionString = Environment.GetEnvironmentVariable("_HUB_CS"); + IoTHubOwnerConnectionString = Environment.GetEnvironmentVariable("_HUB_CS"); + Trace("IoT Hub owner connection string read from environment."); } } // register ourselves with IoT Hub - if (ownerConnectionString != string.Empty) + string deviceConnectionString; + Trace($"IoTHub device cert store type is: {IotDeviceCertStoreType}"); + Trace($"IoTHub device cert path is: {IotDeviceCertStorePath}"); + if (IoTHubOwnerConnectionString != string.Empty) { - Trace("Attemping to register ourselves with IoT Hub using owner connection string: " + ownerConnectionString); - RegistryManager manager = RegistryManager.CreateFromConnectionString(ownerConnectionString); + Trace($"Attemping to register ourselves with IoT Hub using owner connection string: {IoTHubOwnerConnectionString}"); + RegistryManager manager = RegistryManager.CreateFromConnectionString(IoTHubOwnerConnectionString); // remove any existing device - Device existingDevice = manager.GetDeviceAsync(m_applicationName).Result; + Device existingDevice = manager.GetDeviceAsync(ApplicationName).Result; if (existingDevice != null) { - manager.RemoveDeviceAsync(m_applicationName).Wait(); + Trace($"Device '{ApplicationName}' found in IoTHub registry. Remove it."); + manager.RemoveDeviceAsync(ApplicationName).Wait(); } - Device newDevice = manager.AddDeviceAsync(new Device(m_applicationName)).Result; + Trace($"Adding device '{ApplicationName}' to IoTHub registry."); + Device newDevice = manager.AddDeviceAsync(new Device(ApplicationName)).Result; if (newDevice != null) { - string hostname = ownerConnectionString.Substring(0, ownerConnectionString.IndexOf(";")); - string deviceConnectionString = hostname + ";DeviceId=" + m_applicationName + ";SharedAccessKey=" + newDevice.Authentication.SymmetricKey.PrimaryKey; - SecureIoTHubToken.Write(m_applicationName, deviceConnectionString); + string hostname = IoTHubOwnerConnectionString.Substring(0, IoTHubOwnerConnectionString.IndexOf(";")); + deviceConnectionString = hostname + ";DeviceId=" + ApplicationName + ";SharedAccessKey=" + newDevice.Authentication.SymmetricKey.PrimaryKey; + Trace($"Device connection string is: {deviceConnectionString}"); + Trace($"Adding it to device cert store."); + SecureIoTHubToken.Write(ApplicationName, deviceConnectionString, IotDeviceCertStoreType, IotDeviceCertStoreType); } else { - Trace("Could not register ourselves with IoT Hub using owner connection string: " + ownerConnectionString); + Trace($"Could not register ourselves with IoT Hub using owner connection string: {IoTHubOwnerConnectionString}"); + Trace("exiting..."); + return; } } else { - Trace("IoT Hub owner connection string not found, registration with IoT Hub abandoned."); + Trace("IoT Hub owner connection string not specified. Assume device connection string already in cert store."); } // try to read connection string from secure store and open IoTHub client - Trace("Attemping to read connection string from secure store with certificate name: " + m_applicationName); - string connectionString = SecureIoTHubToken.Read(m_applicationName); - if (!string.IsNullOrEmpty(connectionString)) + Trace($"Attemping to read device connection string from cert store using subject name: {ApplicationName}"); + deviceConnectionString = SecureIoTHubToken.Read(ApplicationName, IotDeviceCertStoreType, IotDeviceCertStorePath); + if (!string.IsNullOrEmpty(deviceConnectionString)) { - Trace("Attemping to configure publisher with connection string: " + connectionString); - m_deviceClient = DeviceClient.CreateFromConnectionString(connectionString, Microsoft.Azure.Devices.Client.TransportType.Mqtt); + Trace($"Create Publisher IoTHub client with device connection string: '{deviceConnectionString}' using '{IotHubProtocol}' for communication."); + m_deviceClient = DeviceClient.CreateFromConnectionString(deviceConnectionString, IotHubProtocol); m_deviceClient.RetryPolicy = RetryPolicyType.Exponential_Backoff_With_Jitter; m_deviceClient.OpenAsync().Wait(); } else { - Trace("Device connection string not found in secure store."); + Trace("Device connection string not found in secure store. Could not connect to IoTHub."); + Trace("exiting..."); + return; } // get a list of persisted endpoint URLs and create a session for each. try { - // check if we have an env variable specifying the published nodes path, otherwise use current directory - string publishedNodesFilePath = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "publishednodes.json"; - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GW_PNFP"))) + if (string.IsNullOrEmpty(PublishedNodesAbsFilename)) { - publishedNodesFilePath = Environment.GetEnvironmentVariable("_GW_PNFP"); + // check if we have an env variable specifying the published nodes path, otherwise use the default + PublishedNodesAbsFilename = PublishedNodesAbsFilenameDefault; + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GW_PNFP"))) + { + Trace("Publishing node configuration file path read from environment."); + PublishedNodesAbsFilename = Environment.GetEnvironmentVariable("_GW_PNFP"); + } } - - Trace("Attemping to load nodes file from: " + publishedNodesFilePath); - m_nodesLookups = JsonConvert.DeserializeObject(File.ReadAllText(publishedNodesFilePath)); - Trace("Loaded " + m_nodesLookups.Count.ToString() + " nodes."); + Trace($"Attemping to load nodes file from: {PublishedNodesAbsFilename}"); + m_nodesLookups = JsonConvert.DeserializeObject(File.ReadAllText(PublishedNodesAbsFilename)); + Trace($"Loaded {m_nodesLookups.Count.ToString()} nodes."); } catch (Exception ex) { - Trace("Nodes file loading failed with: " + ex.Message); + Trace($"Nodes file loading failed with: {ex.Message}"); + Trace("exiting..."); + return; } foreach (NodeLookup nodeLookup in m_nodesLookups) @@ -172,7 +381,7 @@ namespace Opc.Ua.Publisher List connectionAttempts = new List(); foreach (Uri endpointUrl in m_endpointUrls) { - Trace("Connecting to server: " + endpointUrl); + Trace($"Connecting to server: {endpointUrl}"); connectionAttempts.Add(EndpointConnect(endpointUrl)); } @@ -181,7 +390,7 @@ namespace Opc.Ua.Publisher } catch (Exception ex) { - Trace("Exception: " + ex.ToString() + "\r\n" + ex.InnerException != null ? ex.InnerException.ToString() : null); + Trace($"Exception: {ex.ToString()}\r\n{ ex.InnerException?.ToString()}"); } // subscribe to preconfigured nodes @@ -196,7 +405,7 @@ namespace Opc.Ua.Publisher } catch (Exception ex) { - Trace("Unexpected error publishing node: " + ex.Message + "\r\nIgnoring node: " + nodeLookup.EndPointURL.AbsoluteUri + ", " + nodeLookup.NodeID.ToString()); + Trace($"Unexpected error publishing node: {ex.Message}\r\nIgnoring node: {nodeLookup.EndPointURL.AbsoluteUri}, {nodeLookup.NodeID.ToString()}"); } } } @@ -216,7 +425,16 @@ namespace Opc.Ua.Publisher } catch (Exception e) { - Trace(e, "Unhandled exception in Publisher, exiting!"); + if (opcTraceInitialized) + { + Trace(e, "Unhandled exception in Publisher. Exiting... "); + } + else + { + WriteLine($"{DateTime.Now.ToString()}: Unhandled exception in Publisher:"); + WriteLine($"{DateTime.Now.ToString()}: {e.Message.ToString()}"); + WriteLine($"{DateTime.Now.ToString()}: exiting..."); + } } } @@ -241,7 +459,7 @@ namespace Opc.Ua.Publisher if (newSession != null) { - Trace("Created session with updated endpoint " + selectedEndpoint.EndpointUrl + " from server!"); + Trace($"Created session with updated endpoint '{selectedEndpoint.EndpointUrl}' from server!"); newSession.KeepAlive += new KeepAliveEventHandler((sender, e) => StandardClient_KeepAlive(sender, e, newSession)); m_sessions.Add(newSession); } @@ -281,24 +499,24 @@ namespace Opc.Ua.Publisher } // add the new monitored item. - MonitoredItem monitoredItem = new MonitoredItem(subscription.DefaultItem); - - monitoredItem.StartNodeId = nodeLookup.NodeID; - monitoredItem.AttributeId = Attributes.Value; - monitoredItem.DisplayName = nodeDisplayName; - monitoredItem.MonitoringMode = MonitoringMode.Reporting; - monitoredItem.SamplingInterval = 1000; - monitoredItem.QueueSize = 0; - monitoredItem.DiscardOldest = true; - + MonitoredItem monitoredItem = new MonitoredItem(subscription.DefaultItem) + { + StartNodeId = nodeLookup.NodeID, + AttributeId = Attributes.Value, + DisplayName = nodeDisplayName, + MonitoringMode = MonitoringMode.Reporting, + SamplingInterval = 1000, + QueueSize = 0, + DiscardOldest = true + }; monitoredItem.Notification += new MonitoredItemNotificationEventHandler(MonitoredItem_Notification); subscription.AddItem(monitoredItem); subscription.ApplyChanges(); } else { - Trace("ERROR: Could not find endpoint URL " + nodeLookup.EndPointURL.ToString() + " in active server sessions, NodeID " + nodeLookup.NodeID.Identifier.ToString() + " NOT published!"); - Trace("To fix this, please update your publishednodes.json file with the updated endpoint URL!"); + Trace($"ERROR: Could not find endpoint URL '{nodeLookup.EndPointURL.ToString()}' in active server sessions, NodeID '{nodeLookup.NodeID.Identifier.ToString()}' NOT published!"); + Trace($"To fix this, please update '{PublishedNodesAbsFilename}' with the updated endpoint URL!"); } } @@ -344,7 +562,7 @@ namespace Opc.Ua.Publisher // publish eventMessage.Properties.Add("content-type", "application/opcua+uajson"); - eventMessage.Properties.Add("deviceName", m_applicationName); + eventMessage.Properties.Add("deviceName", ApplicationName); try { @@ -361,7 +579,7 @@ namespace Opc.Ua.Publisher } catch (Exception exception) { - Trace("Error processing monitored item notification: " + exception.ToString()); + Trace($"Error processing monitored item notification: {exception.ToString()}"); } } @@ -374,11 +592,7 @@ namespace Opc.Ua.Publisher { if (!ServiceResult.IsGood(e.Status)) { - Trace(String.Format( - "Status: {0}/t/tOutstanding requests: {1}/t/tDefunct requests: {2}", - e.Status, - session.OutstandingRequestCount, - session.DefunctRequestCount)); + Trace($"Status: {e.Status}/t/tOutstanding requests: {session.OutstandingRequestCount}/t/tDefunct requests: {session.DefunctRequestCount}"); } } } @@ -390,13 +604,10 @@ namespace Opc.Ua.Publisher { if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted) { - Trace("Certificate \"" - + e.Certificate.Subject - + "\" not trusted. If you want to trust this certificate, please copy it from the \"" - + m_configuration.SecurityConfiguration.RejectedCertificateStore.StorePath + "/certs" - + "\" to the \"" - + m_configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath + "/certs" - + "\" folder. A restart of the gateway is NOT required."); + Trace($"Certificate '{e.Certificate.Subject}' not trusted. If you want to trust this certificate, please copy it from the/r/n" + + $"'{m_configuration.SecurityConfiguration.RejectedCertificateStore.StorePath}/certs' to the " + + $"'{m_configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath}/certs' folder./r/n" + + "A restart of the gateway is NOT required."); } } diff --git a/src/PublisherServer.cs b/src/PublisherServer.cs index e3b4b5b..9b72f9d 100644 --- a/src/PublisherServer.cs +++ b/src/PublisherServer.cs @@ -18,7 +18,7 @@ namespace Publisher { ServerProperties properties = new ServerProperties(); properties.ManufacturerName = "Contoso"; - properties.ProductName = "OPC UA Factory Publisher"; + properties.ProductName = "IoT Edge OPC Publisher"; properties.ProductUri = ""; properties.SoftwareVersion = Utils.GetAssemblySoftwareVersion(); properties.BuildNumber = Utils.GetAssemblyBuildNumber(); diff --git a/src/PublisherState.cs b/src/PublisherState.cs index 5aa89b8..4257511 100644 --- a/src/PublisherState.cs +++ b/src/PublisherState.cs @@ -12,6 +12,8 @@ using System.Threading.Tasks; namespace Publisher { + using static Opc.Ua.Utils; + public partial class PublisherState { /// @@ -34,7 +36,7 @@ namespace Publisher { if (inputArguments[0] == null || inputArguments[1] == null) { - Program.Trace("PublishNodeMethod: Invalid Arguments!"); + Trace("PublishNodeMethod: Invalid Arguments!"); return ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!"); } @@ -42,19 +44,21 @@ namespace Publisher string uri = inputArguments[1] as string; if (string.IsNullOrEmpty(nodeID) || string.IsNullOrEmpty(uri)) { - Program.Trace("PublishNodeMethod: Arguments are not valid strings!"); + Trace("PublishNodeMethod: Arguments are not valid strings!"); return ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!"); } - NodeLookup lookup = new NodeLookup(); - lookup.NodeID = new NodeId(nodeID); + NodeLookup lookup = new NodeLookup() + { + NodeID = new NodeId(nodeID) + }; try { lookup.EndPointURL = new Uri(uri); } catch (UriFormatException) { - Program.Trace("PublishNodeMethod: Invalid endpoint URL!"); + Trace("PublishNodeMethod: Invalid endpoint URL!"); return ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!"); } @@ -65,9 +69,9 @@ namespace Publisher { Task.Run(() => { - Program.Trace("PublishNodeMethod: Session not found, creating one for " + lookup.EndPointURL); + Trace($"PublishNodeMethod: Session not found, creating one for endpoint '{lookup.EndPointURL}'"); Program.EndpointConnect(lookup.EndPointURL).Wait(); - Program.Trace("PublishNodeMethod: Session created."); + Trace("PublishNodeMethod: Session created."); return DoPublish(lookup); }); @@ -76,8 +80,8 @@ namespace Publisher } catch (Exception ex) { - Program.Trace("PublishNodeMethod: Exception: " + ex.ToString()); - return ServiceResult.Create(ex, StatusCodes.BadUnexpectedError, "Unexpected error publishing node: " + ex.Message); + Trace(ex, "PublishNodeMethod: Exception while trying to setup publishing"); + return ServiceResult.Create(ex, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {ex.Message}"); } } else @@ -109,10 +113,10 @@ namespace Publisher if (matchingSession == null) { - Program.Trace("PublishNodeMethod: No matching session found for " + lookup.EndPointURL.ToString()); + Trace($"DoPublish: No matching session found for endpoint '{lookup.EndPointURL.ToString()}'"); return ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session for published node not found!"); } - Program.Trace("PublishNodeMethod: Session found."); + Trace("DoPublish: Session found."); // check if the node has already been published @@ -120,14 +124,14 @@ namespace Publisher { if (item.StartNodeId == lookup.NodeID) { - Program.Trace("PublishNodeMethod: Node ID has already been published " + lookup.NodeID.ToString()); - return ServiceResult.Create(StatusCodes.BadNodeIdExists, "Node has already been published!"); + Trace($"DoPublish: Node ID '{lookup.NodeID.ToString()}' is already published!"); + return ServiceResult.Create(StatusCodes.BadNodeIdExists, $"Node ID '{lookup.NodeID.ToString()}' is already published!"); } } // subscribe to the node Program.CreateMonitoredItem(lookup); - Program.Trace("PublishNodeMethod: Monitored item created."); + Trace("DoPublish: Monitored item created."); // update our data Program.m_nodesLookups.Add(lookup); @@ -137,20 +141,15 @@ namespace Publisher } //serialize Program.m_nodesLookups to disk - string publishedNodesFilePath = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "publishednodes.json"; - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GW_PNFP"))) - { - publishedNodesFilePath = Environment.GetEnvironmentVariable("_GW_PNFP"); - } - File.WriteAllText(publishedNodesFilePath, JsonConvert.SerializeObject(Program.m_nodesLookups)); + File.WriteAllText(Program.PublishedNodesAbsFilename, JsonConvert.SerializeObject(Program.m_nodesLookups)); - Program.Trace("PublishNodeMethod: Successful publish: " + lookup.ToString()); + Trace($"DoPublish: Now publishing: {lookup.ToString()}"); return ServiceResult.Good; } catch (Exception ex) { - Program.Trace("PublishNodeMethod: Exception: " + ex.ToString()); - return ServiceResult.Create(ex, StatusCodes.BadUnexpectedError, "Unexpected error publishing node: " + ex.Message); + Trace(ex, $"DoPublish: Exception while trying to configure publishing node '{lookup.ToString()}'"); + return ServiceResult.Create(ex, StatusCodes.BadUnexpectedError, $"Unexpected error publishing node: {ex.Message}"); } } @@ -161,7 +160,7 @@ namespace Publisher { if (inputArguments[0] == null || inputArguments[1] == null) { - Program.Trace("UnPublishNodeMethod: Invalid arguments!"); + Trace("UnPublishNodeMethod: Invalid arguments!"); return ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments!"); } @@ -169,19 +168,21 @@ namespace Publisher string uri = inputArguments[1] as string; if (string.IsNullOrEmpty(nodeID) || string.IsNullOrEmpty(uri)) { - Program.Trace("UnPublishNodeMethod: Arguments are not valid strings!"); + Trace("UnPublishNodeMethod: Arguments are not valid strings!"); return ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!"); } - NodeLookup lookup = new NodeLookup(); - lookup.NodeID = new NodeId(nodeID); + NodeLookup lookup = new NodeLookup() + { + NodeID = new NodeId(nodeID) + }; try { lookup.EndPointURL = new Uri(uri); } catch (UriFormatException) { - Program.Trace("UnPublishNodeMethod: Invalid endpoint URL!"); + Trace("UnPublishNodeMethod: Invalid endpoint URL!"); return ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide a valid OPC UA endpoint URL as second argument!"); } @@ -199,7 +200,7 @@ namespace Publisher if (matchingSession == null) { - Program.Trace("UnPublishNodeMethod: Session for published node not found: " + lookup.EndPointURL.ToString()); + Trace("UnPublishNodeMethod: Session for published node not found: " + lookup.EndPointURL.ToString()); return ServiceResult.Create(StatusCodes.BadSessionIdInvalid, "Session for published node not found!"); } @@ -209,7 +210,7 @@ namespace Publisher if (item.StartNodeId == lookup.NodeID) { matchingSession.DefaultSubscription.RemoveItem(item); - Program.Trace("UnPublishNodeMethod: Successful unpublish: " + lookup.NodeID.ToString()); + Trace($"UnPublishNodeMethod: Stopping publishing of '{lookup.NodeID.ToString()}'"); // update our data on success only // we keep the session to the server, as there may be other nodes still published on it @@ -217,19 +218,14 @@ namespace Publisher Program.m_nodesLookups.Remove(itemToRemove); //serialize Program.m_nodesLookups to disk - string publishedNodesFilePath = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "publishednodes.json"; - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_GW_PNFP"))) - { - publishedNodesFilePath = Environment.GetEnvironmentVariable("_GW_PNFP"); - } - File.WriteAllText(publishedNodesFilePath, JsonConvert.SerializeObject(Program.m_nodesLookups)); + File.WriteAllText(Program.PublishedNodesAbsFilename, JsonConvert.SerializeObject(Program.m_nodesLookups)); return ServiceResult.Good; } } - Program.Trace("UnPublishNodeMethod: Monitored item for node ID not found " + lookup.NodeID.ToString()); - return ServiceResult.Create(StatusCodes.BadNodeIdInvalid, "Monitored item for node ID not found!"); + Trace($"UnPublishNodeMethod: Monitored item for NodeID '{lookup.NodeID.ToString()}' not found "); + return ServiceResult.Create(StatusCodes.BadNodeIdInvalid, $"Monitored item for NodeID '{lookup.NodeID.ToString()}' not found!"); } /// @@ -238,7 +234,7 @@ namespace Publisher private ServiceResult GetListOfPublishedNodesMethod(ISystemContext context, MethodState method, IList inputArguments, IList outputArguments) { outputArguments[0] = JsonConvert.SerializeObject(Program.m_nodesLookups); - Program.Trace("GetListOfPublishedNodesMethod: Success!"); + Trace("GetListOfPublishedNodesMethod: Success!"); return ServiceResult.Good; } @@ -251,7 +247,7 @@ namespace Publisher var connectionString = value as string; if (string.IsNullOrEmpty(connectionString)) { - Program.Trace("ConnectionStringWrite: Invalid Argument!"); + Trace("ConnectionStringWrite: Invalid Argument!"); return ServiceResult.Create(StatusCodes.BadArgumentsMissing, "Please provide all arguments as strings!"); } @@ -259,33 +255,33 @@ namespace Publisher timestamp = DateTime.Now; // read current connection string and compare to the one passed in - string currentConnectionString = SecureIoTHubToken.Read(Program.m_configuration.ApplicationName); + string currentConnectionString = SecureIoTHubToken.Read(Program.m_configuration.ApplicationName, Program.IotDeviceCertStoreType, Program.IotDeviceCertStorePath); if (string.Equals(connectionString, currentConnectionString, StringComparison.OrdinalIgnoreCase)) { - Program.Trace("ConnectionStringWrite: Connection string up to date!"); + Trace("ConnectionStringWrite: Connection string up to date!"); return ServiceResult.Create(StatusCodes.Bad, "Connection string already up-to-date!"); } - Program.Trace("Attemping to configure publisher with connection string: " + connectionString); + Trace($"ConnectionStringWrite: Attemping to configure publisher with connection string: {connectionString}"); // configure publisher and write connection string try { - DeviceClient newClient = DeviceClient.CreateFromConnectionString(connectionString, Microsoft.Azure.Devices.Client.TransportType.Mqtt); + DeviceClient newClient = DeviceClient.CreateFromConnectionString(connectionString, Program.IotHubProtocol); newClient.RetryPolicy = RetryPolicyType.Exponential_Backoff_With_Jitter; newClient.OpenAsync().Wait(); - SecureIoTHubToken.Write(Program.m_configuration.ApplicationName, connectionString); + SecureIoTHubToken.Write(Program.m_configuration.ApplicationName, connectionString, Program.IotDeviceCertStoreType, Program.IotDeviceCertStorePath); Program.m_deviceClient = newClient; } catch (Exception ex) { statusCode = StatusCodes.Bad; - Program.Trace("ConnectionStringWrite: Exception: " + ex.ToString()); + Trace(ex, $"ConnectionStringWrite: Exception while trying to create IoTHub client and store device connection string in cert store"); return ServiceResult.Create(StatusCodes.Bad, "Publisher registration failed: " + ex.Message); } statusCode = StatusCodes.Good; - Program.Trace("ConnectionStringWrite: Success!"); + Trace("ConnectionStringWrite: Success!"); return statusCode; } diff --git a/src/SecureIoTHubToken.cs b/src/SecureIoTHubToken.cs index dbd2904..fbf8ec2 100644 --- a/src/SecureIoTHubToken.cs +++ b/src/SecureIoTHubToken.cs @@ -1,9 +1,5 @@  -using System; -using System.IO; -using System.Collections; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; +using Opc.Ua; using Org.BouncyCastle.Asn1.X509; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Generators; @@ -14,48 +10,99 @@ using Org.BouncyCastle.Pkcs; using Org.BouncyCastle.Security; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.X509; +using System; +using System.Collections; +using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using System.Text; namespace IoTHubCredentialTools { public class SecureIoTHubToken { - public static string Read(string name) + public static string Read(string name, string storeType, string storePath) { - // load an existing key from a no-expired cert with the subject name passed in from the OS-provided X509Store - using (X509Store store = new X509Store("IoTHub", StoreLocation.CurrentUser)) + // handle each store type differently + switch (storeType) { - store.Open(OpenFlags.ReadOnly); - foreach (X509Certificate2 cert in store.Certificates) - { - if ((cert.SubjectName.Decode(X500DistinguishedNameFlags.None | X500DistinguishedNameFlags.DoNotUseQuotes).Equals("CN=" + name, StringComparison.OrdinalIgnoreCase)) && - (DateTime.Now < cert.NotAfter)) + case CertificateStoreType.Directory: { - using (RSA rsa = cert.GetRSAPrivateKey()) + // load an existing key from a no-expired cert with the subject name passed in from the OS-provided X509Store + using (DirectoryCertificateStore store = new DirectoryCertificateStore()) { - if (rsa != null) + store.Open(storePath); + X509CertificateCollection certificates = store.Enumerate().Result; + + foreach (X509Certificate2 cert in certificates) { - foreach (System.Security.Cryptography.X509Certificates.X509Extension extension in cert.Extensions) + if ((cert.SubjectName.Decode(X500DistinguishedNameFlags.None | X500DistinguishedNameFlags.DoNotUseQuotes).Equals("CN=" + name, StringComparison.OrdinalIgnoreCase)) && + (DateTime.Now < cert.NotAfter)) { - // check for instruction code extension - if ((extension.Oid.Value == "2.5.29.23") && (extension.RawData.Length >= 4)) + using (RSA rsa = cert.GetRSAPrivateKey()) { - byte[] bytes = new byte[extension.RawData.Length - 4]; - Array.Copy(extension.RawData, 4, bytes, 0, bytes.Length); - byte[] token = rsa.Decrypt(bytes, RSAEncryptionPadding.OaepSHA1); - return Encoding.ASCII.GetString(token); + if (rsa != null) + { + foreach (System.Security.Cryptography.X509Certificates.X509Extension extension in cert.Extensions) + { + // check for instruction code extension + if ((extension.Oid.Value == "2.5.29.23") && (extension.RawData.Length >= 4)) + { + byte[] bytes = new byte[extension.RawData.Length - 4]; + Array.Copy(extension.RawData, 4, bytes, 0, bytes.Length); + byte[] token = rsa.Decrypt(bytes, RSAEncryptionPadding.OaepSHA1); + return Encoding.ASCII.GetString(token); + } + } + } } } } } + break; + } + case CertificateStoreType.X509Store: + { + // load an existing key from a no-expired cert with the subject name passed in from the OS-provided X509Store + using (X509Store store = new X509Store(storePath, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadOnly); + foreach (X509Certificate2 cert in store.Certificates) + { + if ((cert.SubjectName.Decode(X500DistinguishedNameFlags.None | X500DistinguishedNameFlags.DoNotUseQuotes).Equals("CN=" + name, StringComparison.OrdinalIgnoreCase)) && + (DateTime.Now < cert.NotAfter)) + { + using (RSA rsa = cert.GetRSAPrivateKey()) + { + if (rsa != null) + { + foreach (System.Security.Cryptography.X509Certificates.X509Extension extension in cert.Extensions) + { + // check for instruction code extension + if ((extension.Oid.Value == "2.5.29.23") && (extension.RawData.Length >= 4)) + { + byte[] bytes = new byte[extension.RawData.Length - 4]; + Array.Copy(extension.RawData, 4, bytes, 0, bytes.Length); + byte[] token = rsa.Decrypt(bytes, RSAEncryptionPadding.OaepSHA1); + return Encoding.ASCII.GetString(token); + } + } + } + } + } + } + } + break; + } + default: + { + throw new Exception($"The requested store type '{storeType}' is not supported. Please change."); } - } } - return null; } - public static void Write(string name, string connectionString) + public static void Write(string name, string connectionString, string storeType, string storePath) { if (string.IsNullOrEmpty(connectionString)) { @@ -110,7 +157,7 @@ namespace IoTHubCredentialTools } } rsa.Dispose(); - + // sign the cert with the private key ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", keys.Private, random); Org.BouncyCastle.X509.X509Certificate x509 = cg.Generate(signatureFactory); @@ -129,23 +176,58 @@ namespace IoTHubCredentialTools // create X509Certificate2 object from PKCS12 file certificate = CreateCertificateFromPKCS12(pfxData.ToArray(), passcode); - // Add to X509Store - using (X509Store store = new X509Store("IoTHub", StoreLocation.CurrentUser)) + // handle each store type differently + switch (storeType) { - store.Open(OpenFlags.ReadWrite); - - // remove any existing cert with our name from the store - foreach (X509Certificate2 cert in store.Certificates) - { - if (cert.SubjectName.Decode(X500DistinguishedNameFlags.None | X500DistinguishedNameFlags.DoNotUseQuotes).Equals("CN=" + name, StringComparison.OrdinalIgnoreCase)) + case CertificateStoreType.Directory: { - store.Remove(cert); - } - } + // Add to DirectoryStore + using (DirectoryCertificateStore store = new DirectoryCertificateStore()) + { + store.Open(storePath); + X509CertificateCollection certificates = store.Enumerate().Result; - // add new one - store.Add(certificate); + // remove any existing cert with our name from the store + foreach (X509Certificate2 cert in certificates) + { + if (cert.SubjectName.Decode(X500DistinguishedNameFlags.None | X500DistinguishedNameFlags.DoNotUseQuotes).Equals("CN=" + name, StringComparison.OrdinalIgnoreCase)) + { + store.Delete(cert.Thumbprint); + } + } + + // add new one + store.Add(certificate); + } + break; + } + case CertificateStoreType.X509Store: + { + // Add to X509Store + using (X509Store store = new X509Store("IoTHub", StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + + // remove any existing cert with our name from the store + foreach (X509Certificate2 cert in store.Certificates) + { + if (cert.SubjectName.Decode(X500DistinguishedNameFlags.None | X500DistinguishedNameFlags.DoNotUseQuotes).Equals("CN=" + name, StringComparison.OrdinalIgnoreCase)) + { + store.Remove(cert); + } + } + + // add new one + store.Add(certificate); + } + break; + } + default: + { + throw new Exception($"The requested store type '{storeType}' is not supported. Please change."); + } } + return; } } diff --git a/src/TraceWorkaround.cs b/src/TraceWorkaround.cs new file mode 100644 index 0000000..822d089 --- /dev/null +++ b/src/TraceWorkaround.cs @@ -0,0 +1,31 @@ + +using System; +using static System.Console; + +namespace Opc.Ua.Workarounds +{ + public static class TraceWorkaround + { + /// + /// Trace message helper + /// + public static void Trace(string message, params object[] args) + { + Utils.Trace(Utils.TraceMasks.Error, message, args); + WriteLine(DateTime.Now.ToString() + ": " + message, args); + } + + public static void Trace(int traceMask, string format, params object[] args) + { + Utils.Trace(traceMask, format, args); + WriteLine(DateTime.Now.ToString() + ": " + format, args); + } + + public static void Trace(Exception e, string format, params object[] args) + { + Utils.Trace(e, format, args); + WriteLine(DateTime.Now.ToString() + ": " + e.Message.ToString()); + WriteLine(DateTime.Now.ToString() + ": " + format, args); + } + } +}