This commit is contained in:
Hans Gschossmann 2017-08-22 14:33:06 +02:00
Родитель 77183f413c
Коммит d52fc83e39
9 изменённых файлов: 628 добавлений и 329 удалений

Просмотреть файл

@ -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"]

Просмотреть файл

@ -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
{
/// <summary>
@ -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();

Просмотреть файл

@ -15,9 +15,11 @@ namespace Publisher
public PublisherNodeManager(Opc.Ua.Server.IServerInternal server, ApplicationConfiguration configuration)
: base(server)
{
List<string> namespaceUris = new List<string>();
namespaceUris.Add(Namespaces.Publisher);
namespaceUris.Add(Namespaces.Publisher + "/Instance");
List<string> namespaceUris = new List<string>
{
Namespaces.Publisher,
Namespaces.Publisher + "/Instance"
};
NamespaceUris = namespaceUris;
m_typeNamespaceIndex = Server.NamespaceUris.GetIndexOrAppend(namespaceUris[0]);

Просмотреть файл

@ -37,9 +37,11 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Azure.Devices" Version="1.3.0" />
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.4.0" />
<PackageReference Include="Microsoft.Azure.Devices" Version="1.3.1" />
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.4.1" />
<PackageReference Include="Mono.Options" Version="5.3.0.1" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua" Version="0.4.1" />
<PackageReference Include="System.CommandLine" Version="0.1.0-e170819-1" />
</ItemGroup>
<ItemGroup>

Просмотреть файл

@ -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<Session> m_sessions = new List<Session>();
public static PublishedNodesCollection m_nodesLookups = new PublishedNodesCollection();
public static List<Uri> m_endpointUrls = new List<Uri>();
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;
/// <summary>
/// Trace message helper
/// </summary>
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/<applicationname>.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<string> 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<PublishedNodesCollection>(File.ReadAllText(publishedNodesFilePath));
Trace("Loaded " + m_nodesLookups.Count.ToString() + " nodes.");
Trace($"Attemping to load nodes file from: {PublishedNodesAbsFilename}");
m_nodesLookups = JsonConvert.DeserializeObject<PublishedNodesCollection>(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<Task> connectionAttempts = new List<Task>();
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.");
}
}

Просмотреть файл

@ -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();

Просмотреть файл

@ -12,6 +12,8 @@ using System.Threading.Tasks;
namespace Publisher
{
using static Opc.Ua.Utils;
public partial class PublisherState
{
/// <summary>
@ -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!");
}
/// <summary>
@ -238,7 +234,7 @@ namespace Publisher
private ServiceResult GetListOfPublishedNodesMethod(ISystemContext context, MethodState method, IList<object> inputArguments, IList<object> 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;
}

Просмотреть файл

@ -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;
}
}

31
src/TraceWorkaround.cs Normal file
Просмотреть файл

@ -0,0 +1,31 @@

using System;
using static System.Console;
namespace Opc.Ua.Workarounds
{
public static class TraceWorkaround
{
/// <summary>
/// Trace message helper
/// </summary>
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);
}
}
}