add command line options for certificate management

This commit is contained in:
Hans Gschossmann 2018-11-13 11:34:34 -08:00
Родитель e15d2a640e
Коммит 0d418c3d47
12 изменённых файлов: 1773 добавлений и 678 удалений

543
README.md
Просмотреть файл

@ -281,249 +281,291 @@ The syntax of the configuration file is as follows:
## Command line options
The complete usage of the application can be shown using the `--help` command line option and is as follows:
Usage: opcpublisher.exe <applicationname> [<iothubconnectionstring>] [<options>]
Current directory is: <current-directory>
Log file is: <hostname>-publisher.log
Log level is: info
OPC Edge Publisher to subscribe to configured OPC UA servers and send telemetry to Azure IoTHub.
To exit the application, just press ENTER while it is running.
Usage: opcpublisher.exe <applicationname> [<iothubconnectionstring>] [<options>]
applicationname: the OPC UA application name to use, required
The application name is also used to register the publisher under this name in the
IoTHub device registry.
OPC Edge Publisher to subscribe to configured OPC UA servers and send telemetry to Azure IoTHub.
To exit the application, just press CTRL-C while it is running.
iothubconnectionstring: the IoTHub owner connectionstring, optional
applicationname: the OPC UA application name to use, required
The application name is also used to register the publisher under this name in the
IoTHub device registry.
There are a couple of environment variables which can be used to control the application:
_HUB_CS: sets the IoTHub owner connectionstring
_GW_LOGP: sets the filename of the log file to use
_TPC_SP: sets the path to store certificates of trusted stations
_GW_PNFP: sets the filename of the publishing configuration file
iothubconnectionstring: the IoTHub owner connectionstring, optional
Command line arguments overrule environment variable settings.
There are a couple of environment variables which can be used to control the application:
_HUB_CS: sets the IoTHub owner connectionstring
_GW_LOGP: sets the filename of the log file to use
_TPC_SP: sets the path to store certificates of trusted stations
_GW_PNFP: sets the filename of the publishing configuration file
Options:
--pf, --publishfile=VALUE
the filename to configure the nodes to publish.
Default: '/appdata/publishednodes.json'
--tc, --telemetryconfigfile=VALUE
the filename to configure the ingested telemetry
Default: ''
-s, --site=VALUE the site OPC Publisher is working in. if specified
this domain is appended (delimited by a ':' to
the 'ApplicationURI' property when telemetry is
sent to IoTHub.
The value must follow the syntactical rules of a
DNS hostname.
Default: not set
--sd, --shopfloordomain=VALUE
same as site option, only there for backward
compatibility
The value must follow the syntactical rules of a
DNS hostname.
Default: not set
--ic, --iotcentral publisher will send OPC UA data in IoTCentral
compatible format (DisplayName of a node is used
as key, this key is the Field name in IoTCentral)
. you need to ensure that all DisplayName's are
unique. (Auto enables fetch display name)
Default: False
--sw, --sessionconnectwait=VALUE
specify the wait time in seconds publisher is
trying to connect to disconnected endpoints and
starts monitoring unmonitored items
Min: 10
Default: 10
--mq, --monitoreditemqueuecapacity=VALUE
specify how many notifications of monitored items
can be stored in the internal queue, if the data
can not be sent quick enough to IoTHub
Min: 1024
Default: 8192
--di, --diagnosticsinterval=VALUE
shows publisher diagnostic info at the specified
interval in seconds (need log level info). 0
disables diagnostic output.
Default: 0
--vc, --verboseconsole=VALUE
ignored, only supported for backward comaptibility.
--ns, --noshutdown=VALUE
same as runforever.
Default: False
--rf, --runforwver publisher can not be stopped by pressing a key on
the console, but will run forever.
Default: False
--lf, --logfile=VALUE the filename of the logfile to use.
Default: './<hostname>-publisher.log'
--lt, --logflushtimespan=VALUE
the timespan in seconds when the logfile should be
flushed.
Default: on logfile rollover.
--ll, --loglevel=VALUE the loglevel to use (allowed: fatal, error, warn,
info, debug, verbose).
Default: info
--ih, --iothubprotocol=VALUE
the protocol to use for communication with Azure
IoTHub (allowed values: Amqp, Http1, Amqp_
WebSocket_Only, Amqp_Tcp_Only, Mqtt, Mqtt_
WebSocket_Only, Mqtt_Tcp_Only).
Default: Mqtt_WebSocket_Only
--ms, --iothubmessagesize=VALUE
the max size of a message which can be send to
IoTHub. when telemetry of this size is available
it will be sent.
0 will enforce immediate send when telemetry is
available
Min: 0
Max: 262144
Default: 262144
--si, --iothubsendinterval=VALUE
the interval in seconds when telemetry should be
send to IoTHub. If 0, then only the
iothubmessagesize parameter controls when
telemetry is sent.
Default: '10'
--dc, --deviceconnectionstring=VALUE
if publisher is not able to register itself with
IoTHub, you can create a device with name <
applicationname> manually and pass in the
connectionstring of this device.
Default: none
-c, --connectionstring=VALUE
the IoTHub owner connectionstring.
Default: none
--pn, --portnum=VALUE the server port of the publisher OPC server
endpoint.
Default: 62222
--pa, --path=VALUE the enpoint URL path part of the publisher OPC
server endpoint.
Default: '/UA/Publisher'
--lr, --ldsreginterval=VALUE
the LDS(-ME) registration interval in ms. If 0,
then the registration is disabled.
Default: 0
--ol, --opcmaxstringlen=VALUE
the max length of a string opc can transmit/
receive.
Default: 1048576
--ot, --operationtimeout=VALUE
the operation timeout of the publisher OPC UA
client in ms.
Default: 120000
--oi, --opcsamplinginterval=VALUE
the publisher is using this as default value in
milliseconds to request the servers to sample
the nodes with this interval
this value might be revised by the OPC UA
servers to a supported sampling interval.
please check the OPC UA specification for
details how this is handled by the OPC UA stack.
a negative value will set the sampling interval
to the publishing interval of the subscription
this node is on.
0 will configure the OPC UA server to sample in
the highest possible resolution and should be
taken with care.
Default: 1000
--op, --opcpublishinginterval=VALUE
the publisher is using this as default value in
milliseconds for the publishing interval setting
of the subscriptions established to the OPC UA
servers.
please check the OPC UA specification for
details how this is handled by the OPC UA stack.
a value less than or equal zero will let the
server revise the publishing interval.
Default: 0
--ct, --createsessiontimeout=VALUE
specify the timeout in seconds used when creating
a session to an endpoint. On unsuccessful
connection attemps a backoff up to 5 times the
specified timeout value is used.
Min: 1
Default: 10
--ki, --keepaliveinterval=VALUE
specify the interval in seconds the publisher is
sending keep alive messages to the OPC servers
on the endpoints it is connected to.
Min: 2
Default: 2
--kt, --keepalivethreshold=VALUE
specify the number of keep alive packets a server
can miss, before the session is disconneced
Min: 1
Default: 5
--st, --opcstacktracemask=VALUE
ignored, only supported for backward comaptibility.
--as, --autotrustservercerts=VALUE
same as autoaccept, only supported for backward
cmpatibility.
Default: False
--aa, --autoaccept the publisher trusts all servers it is
establishing a connection to.
Default: False
--tm, --trustmyself=VALUE
same as trustowncert.
Default: True
--to, --trustowncert the publisher certificate is put into the trusted
certificate store automatically.
Default: True
--fd, --fetchdisplayname=VALUE
same as fetchname.
Default: False
--fn, --fetchname=VALUE
enable to read the display name of a published
node from the server. this will increase the
runtime.
Default: False
--at, --appcertstoretype=VALUE
the own application cert store type.
(allowed values: Directory, X509Store)
Default: 'X509Store'
--ap, --appcertstorepath=VALUE
the path where the own application cert should be
stored
Default (depends on store type):
X509Store: 'CurrentUser\UA_MachineDefault'
Directory: 'CertificateStores/own'
--tt, --trustedcertstoretype=VALUE
the trusted cert store type.
(allowed values: Directory, X509Store)
Default: Directory
--tp, --trustedcertstorepath=VALUE
the path of the trusted cert store
Default (depends on store type):
X509Store: 'CurrentUser\UA_MachineDefault'
Directory: 'CertificateStores/trusted'
--rt, --rejectedcertstoretype=VALUE
the rejected cert store type.
(allowed values: Directory, X509Store)
Default: Directory
--rp, --rejectedcertstorepath=VALUE
the path of the rejected cert store
Default (depends on store type):
X509Store: 'CurrentUser\UA_MachineDefault'
Directory: 'CertificateStores/rejected'
--it, --issuercertstoretype=VALUE
the trusted issuer cert store type.
(allowed values: Directory, X509Store)
Default: Directory
--ip, --issuercertstorepath=VALUE
the path of the trusted issuer cert store
Default (depends on store type):
X509Store: 'CurrentUser\UA_MachineDefault'
Directory: 'CertificateStores/issuers'
--dt, --devicecertstoretype=VALUE
the iothub device cert store type.
(allowed values: Directory, X509Store)
Default: X509Store
--dp, --devicecertstorepath=VALUE
the path of the iot device cert store
Default Default (depends on store type):
X509Store: 'My'
Directory: 'CertificateStores/IoTHub'
-i, --install register OPC Publisher with IoTHub and then exits.
Default: False
-h, --help show this message and exit
Command line arguments overrule environment variable settings.
Options:
--pf, --publishfile=VALUE
the filename to configure the nodes to publish.
Default: './publishednodes.json'
--tc, --telemetryconfigfile=VALUE
the filename to configure the ingested telemetry
Default: ''
-s, --site=VALUE the site OPC Publisher is working in. if specified
this domain is appended (delimited by a ':' to
the 'ApplicationURI' property when telemetry is
sent to IoTHub.
The value must follow the syntactical rules of a
DNS hostname.
Default: not set
--ic, --iotcentral publisher will send OPC UA data in IoTCentral
compatible format (DisplayName of a node is used
as key, this key is the Field name in IoTCentral)
. you need to ensure that all DisplayName's are
unique. (Auto enables fetch display name)
Default: False
--sw, --sessionconnectwait=VALUE
specify the wait time in seconds publisher is
trying to connect to disconnected endpoints and
starts monitoring unmonitored items
Min: 10
Default: 10
--mq, --monitoreditemqueuecapacity=VALUE
specify how many notifications of monitored items
can be stored in the internal queue, if the data
can not be sent quick enough to IoTHub
Min: 1024
Default: 8192
--di, --diagnosticsinterval=VALUE
shows publisher diagnostic info at the specified
interval in seconds (need log level info). 0
disables diagnostic output.
Default: 0
--ns, --noshutdown=VALUE
same as runforever.
Default: False
--rf, --runforever publisher can not be stopped by pressing a key on
the console, but will run forever.
Default: False
--lf, --logfile=VALUE the filename of the logfile to use.
Default: './<hostname>-publisher.log'
--lt, --logflushtimespan=VALUE
the timespan in seconds when the logfile should be
flushed.
Default: 00:00:30 sec
--ll, --loglevel=VALUE the loglevel to use (allowed: fatal, error, warn,
info, debug, verbose).
Default: info
--ih, --iothubprotocol=VALUE
the protocol to use for communication with Azure
IoTHub (allowed values: Amqp, Http1, Amqp_
WebSocket_Only, Amqp_Tcp_Only, Mqtt, Mqtt_
WebSocket_Only, Mqtt_Tcp_Only).
Default: Mqtt_WebSocket_Only
--ms, --iothubmessagesize=VALUE
the max size of a message which can be send to
IoTHub. when telemetry of this size is available
it will be sent.
0 will enforce immediate send when telemetry is
available
Min: 0
Max: 262144
Default: 262144
--si, --iothubsendinterval=VALUE
the interval in seconds when telemetry should be
send to IoTHub. If 0, then only the
iothubmessagesize parameter controls when
telemetry is sent.
Default: '10'
--dc, --deviceconnectionstring=VALUE
if publisher is not able to register itself with
IoTHub, you can create a device with name <
applicationname> manually and pass in the
connectionstring of this device.
Default: none
-c, --connectionstring=VALUE
the IoTHub owner connectionstring.
Default: none
--pn, --portnum=VALUE the server port of the publisher OPC server
endpoint.
Default: 62222
--pa, --path=VALUE the enpoint URL path part of the publisher OPC
server endpoint.
Default: '/UA/Publisher'
--lr, --ldsreginterval=VALUE
the LDS(-ME) registration interval in ms. If 0,
then the registration is disabled.
Default: 0
--ol, --opcmaxstringlen=VALUE
the max length of a string opc can transmit/
receive.
Default: 4194304
--ot, --operationtimeout=VALUE
the operation timeout of the publisher OPC UA
client in ms.
Default: 120000
--oi, --opcsamplinginterval=VALUE
the publisher is using this as default value in
milliseconds to request the servers to sample
the nodes with this interval
this value might be revised by the OPC UA
servers to a supported sampling interval.
please check the OPC UA specification for
details how this is handled by the OPC UA stack.
a negative value will set the sampling interval
to the publishing interval of the subscription
this node is on.
0 will configure the OPC UA server to sample in
the highest possible resolution and should be
taken with care.
Default: 1000
--op, --opcpublishinginterval=VALUE
the publisher is using this as default value in
milliseconds for the publishing interval setting
of the subscriptions established to the OPC UA
servers.
please check the OPC UA specification for
details how this is handled by the OPC UA stack.
a value less than or equal zero will let the
server revise the publishing interval.
Default: 0
--ct, --createsessiontimeout=VALUE
specify the timeout in seconds used when creating
a session to an endpoint. On unsuccessful
connection attemps a backoff up to 5 times the
specified timeout value is used.
Min: 1
Default: 10
--ki, --keepaliveinterval=VALUE
specify the interval in seconds the publisher is
sending keep alive messages to the OPC servers
on the endpoints it is connected to.
Min: 2
Default: 2
--kt, --keepalivethreshold=VALUE
specify the number of keep alive packets a server
can miss, before the session is disconneced
Min: 1
Default: 5
--aa, --autoaccept the publisher trusts all servers it is
establishing a connection to.
Default: False
--tm, --trustmyself=VALUE
same as trustowncert.
Default: False
--to, --trustowncert the publisher certificate is put into the trusted
certificate store automatically.
Default: False
--fd, --fetchdisplayname=VALUE
same as fetchname.
Default: False
--fn, --fetchname enable to read the display name of a published
node from the server. this will increase the
runtime.
Default: False
--at, --appcertstoretype=VALUE
the own application cert store type.
(allowed values: Directory, X509Store)
Default: 'Directory'
--ap, --appcertstorepath=VALUE
the path where the own application cert should be
stored
Default (depends on store type):
X509Store: 'CurrentUser\UA_MachineDefault'
Directory: 'pki/own'
--tp, --trustedcertstorepath=VALUE
the path of the trusted cert store
Default: 'pki/trusted'
--rp, --rejectedcertstorepath=VALUE
the path of the rejected cert store
Default 'pki/rejected'
--ip, --issuercertstorepath=VALUE
the path of the trusted issuer cert store
Default 'pki/issuer'
--csr show data to create a certificate signing request
Default 'False'
--ab, --applicationcertbase64=VALUE
update/set this applications certificate with the
certificate passed in as bas64 string
--af, --applicationcertfile=VALUE
update/set this applications certificate with the
certificate file specified
--pb, --privatekeybase64=VALUE
initial provisioning of the application
certificate (with a PEM or PFX fomat) requires a
private key passed in as base64 string
--pk, --privatekeyfile=VALUE
initial provisioning of the application
certificate (with a PEM or PFX fomat) requires a
private key passed in as file
--cp, --certpassword=VALUE
the optional password for the PEM or PFX or the
installed application certificate
--tb, --addtrustedcertbase64=VALUE
adds the certificate to the applications trusted
cert store passed in as base64 string (multiple
strings supported)
--tf, --addtrustedcertfile=VALUE
adds the certificate file(s) to the applications
trusted cert store passed in as base64 string (
multiple filenames supported)
--ib, --addissuercertbase64=VALUE
adds the specified issuer certificate to the
applications trusted issuer cert store passed in
as base64 string (multiple strings supported)
--if, --addissuercertfile=VALUE
adds the specified issuer certificate file(s) to
the applications trusted issuer cert store (
multiple filenames supported)
--rb, --updatecrlbase64=VALUE
update the CRL passed in as base64 string to the
corresponding cert store (trusted or trusted
issuer)
--uc, --updatecrlfile=VALUE
update the CRL passed in as file to the
corresponding cert store (trusted or trusted
issuer)
--rc, --removecert=VALUE
remove cert(s) with the given thumbprint(s) (
multiple thumbprints supported)
--dt, --devicecertstoretype=VALUE
the iothub device cert store type.
(allowed values: Directory, X509Store)
Default: X509Store
--dp, --devicecertstorepath=VALUE
the path of the iot device cert store
Default Default (depends on store type):
X509Store: 'My'
Directory: 'CertificateStores/IoTHub'
-i, --install register OPC Publisher with IoTHub and then exits.
Default: False
-h, --help show this message and exit
--st, --opcstacktracemask=VALUE
ignored, only supported for backward comaptibility.
--sd, --shopfloordomain=VALUE
same as site option, only there for backward
compatibility
The value must follow the syntactical rules of a
DNS hostname.
Default: not set
--vc, --verboseconsole=VALUE
ignored, only supported for backward comaptibility.
--as, --autotrustservercerts=VALUE
same as autoaccept, only supported for backward
cmpatibility.
Default: False
--tt, --trustedcertstoretype=VALUE
ignored, only supported for backward compatibility.
the trusted cert store will always reside in a
directory.
--rt, --rejectedcertstoretype=VALUE
ignored, only supported for backward compatibility.
the rejected cert store will always reside in a
directory.
--it, --issuercertstoretype=VALUE
ignored, only supported for backward compatibility.
the trusted issuer cert store will always
reside in a directory.
Typically you specify the IoTHub owner connectionstring only on the first start of the application. The connectionstring will be encrypted and stored in the platforms certificiate store.
On subsequent calls it will be read from there and reused. If you specify the connectionstring on each start, the device which is created for the application in the IoTHub device registry will be removed and recreated each time.
@ -661,7 +703,34 @@ OPC Publisher uses the hostname of the machine is running on for certificate and
docker run -h publisher mcr.microsoft.com/iotedge/opc-publisher <applicationname> [<iothubconnectionstring>] [options]
### Using bind mounts (shared filesystem)
In certain use cases it may make sense to read configuration information from or write log files to locations on the host and not keep them in the container file system only. To achieve this you need to use the `-v` option of `docker run` in the bind mount mode.
In certain use cases it may make sense to read configuration information from or write log files to locations on the host and not keep
them in the container file system only. To achieve this you need to use the `-v` option of `docker run` in the bind mount mode.
## OPC UA X.509 certificates
As you know, OPC UA is using X.509 certificates to authenticate OPC UA client and server during establishment of a connection and
to encrypt the communication between the two parties.
OPC Publisher does use certificate stores maintained by the OPC UA stack to manage all certificates.
On startup OPC Publisher checks if there is a certificate for itself (see `InitApplicationSecurityAsync`
in `OpcApplicationConfigurationSecurity.cs`) and creates a self-signed certificate if there is none or if there is not one passed in
via command line options.
Self-signed certificates do not provide any security, since they are not signed by a trusted CA.
OPC Publisher does provide several command line options to:
* retrieve CSR information of the current application certificate used by OPC Publisher
* provision OPC Publisher with a CA signed certificate
* provision OPC Publisher with a new key pair and matching CA signed certificate
* add certificates to the trusted peer or trusted issuer cert store with certificates from an OPC UA application or from a CA
* add a CRL
* remove a certificate from the trusted peer or trusted issuers cert store
All these options allow to pass in parameters via files or base64 encoded strings.
The default store type for all cert stores is the file system. You can change that via command line options. Especially when you run OPC Publisher
in a container, then the persistency of the certificates is important, since the container does not provide persistency. You need to use docker's `-v` option
to persist the certificate stores in the host file system or a docker volume. If you are use a docker volume, you can pass in certificate relevant
data via base64 encoded strings.
If you want to see how a CA signed certificate can be used, please open an issue on this repo and we follow up.
## Performance and memory considerations
### Commandline parameters contolling performance and memory

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

@ -15,10 +15,10 @@ namespace OpcPublisher
using System.IO;
using System.Linq;
using System.Net;
using static OpcApplicationConfiguration;
using static OpcPublisher.OpcMonitoredItem;
using static OpcPublisher.PublisherNodeConfiguration;
using static OpcPublisher.PublisherTelemetryConfiguration;
using static OpcStackConfiguration;
using static Program;
/// <summary>

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

@ -8,7 +8,7 @@ namespace OpcPublisher
using Microsoft.Azure.Devices.Client;
using Opc.Ua;
using System;
using static OpcStackConfiguration;
using static OpcApplicationConfiguration;
using static Program;
/// <summary>

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

@ -0,0 +1,240 @@

using Opc.Ua;
using System;
using System.Security.Cryptography.X509Certificates;
namespace OpcPublisher
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using static Opc.Ua.CertificateStoreType;
using static Program;
/// <summary>
/// Class for OPC Application configuration.
/// </summary>
public partial class OpcApplicationConfiguration
{
/// <summary>
/// Configuration info for the OPC application.
/// </summary>
public static ApplicationConfiguration ApplicationConfiguration { get; private set; }
public static string Hostname
{
get => _hostname;
set => _hostname = value.ToLowerInvariant();
}
public static string ApplicationName { get; set; } = "publisher";
public static string ApplicationUri => $"urn:{Hostname}:{ApplicationName}:microsoft:";
public static string ProductUri => $"https://github.com/azure-samples/iot-edge-opc-publisher";
public static ushort ServerPort { get; set; } = 62222;
public static string ServerPath { get; set; } = "/UA/Publisher";
/// <summary>
/// Default endpoint security of the application.
/// </summary>
public static string ServerSecurityPolicy { get; set; } = SecurityPolicies.Basic128Rsa15;
/// <summary>
/// Enables unsecure endpoint access to the application.
/// </summary>
public static bool EnableUnsecureTransport { get; set; } = false;
/// <summary>
/// Sets the LDS registration interval.
/// </summary>
public static int LdsRegistrationInterval { get; set; } = 0;
/// <summary>
/// Set the max string length the OPC stack supports.
/// </summary>
public static int OpcMaxStringLength { get; set; } = 4 * 1024 * 1024;
/// <summary>
/// <summary>
/// Mapping of the application logging levels to OPC stack logging levels.
/// </summary>
public static int OpcTraceToLoggerVerbose = 0;
public static int OpcTraceToLoggerDebug = 0;
public static int OpcTraceToLoggerInformation = 0;
public static int OpcTraceToLoggerWarning = 0;
public static int OpcTraceToLoggerError = 0;
public static int OpcTraceToLoggerFatal = 0;
/// <summary>
/// Set the OPC stack log level.
/// </summary>
public static int OpcStackTraceMask { get; set; } = Utils.TraceMasks.Error | Utils.TraceMasks.Security | Utils.TraceMasks.StackTrace | Utils.TraceMasks.StartStop;
/// <summary>
/// Timeout for OPC operations.
/// </summary>
public static int OpcOperationTimeout { get; set; } = 120000;
public static bool OpcPublisherAutoTrustServerCerts { get; set; } = false;
public static uint OpcSessionCreationTimeout { get; set; } = 10;
public static uint OpcSessionCreationBackoffMax { get; set; } = 5;
public static uint OpcKeepAliveDisconnectThreshold { get; set; } = 5;
public static int OpcKeepAliveIntervalInSec { get; set; } = 2;
public const int OpcSamplingIntervalDefault = 1000;
public static int OpcSamplingInterval { get; set; } = OpcSamplingIntervalDefault;
public const int OpcPublishingIntervalDefault = 0;
public static int OpcPublishingInterval { get; set; } = OpcPublishingIntervalDefault;
public static string PublisherServerSecurityPolicy { get; set; } = SecurityPolicies.Basic128Rsa15;
/// <summary>
/// Ctor of the OPC application configuration.
/// </summary>
public OpcApplicationConfiguration()
{
}
/// <summary>
/// Configures all OPC stack settings.
/// </summary>
public async Task<ApplicationConfiguration> ConfigureAsync()
{
// instead of using a configuration XML file, we configure everything programmatically
// passed in as command line argument
ApplicationConfiguration = new ApplicationConfiguration();
ApplicationConfiguration.ApplicationName = ApplicationName;
ApplicationConfiguration.ApplicationUri = ApplicationUri;
ApplicationConfiguration.ProductUri = ProductUri;
ApplicationConfiguration.ApplicationType = ApplicationType.Server;
// configure OPC stack tracing
ApplicationConfiguration.TraceConfiguration = new TraceConfiguration();
ApplicationConfiguration.TraceConfiguration.TraceMasks = OpcStackTraceMask;
ApplicationConfiguration.TraceConfiguration.ApplySettings();
Utils.Tracing.TraceEventHandler += new EventHandler<TraceEventArgs>(LoggerOpcUaTraceHandler);
Logger.Information($"opcstacktracemask set to: 0x{OpcStackTraceMask:X}");
// configure transport settings
ApplicationConfiguration.TransportQuotas = new TransportQuotas();
ApplicationConfiguration.TransportQuotas.MaxStringLength = OpcMaxStringLength;
ApplicationConfiguration.TransportQuotas.MaxMessageSize = 4 * 1024 * 1024;
// configure OPC UA server
ApplicationConfiguration.ServerConfiguration = new ServerConfiguration();
// configure server base addresses
if (ApplicationConfiguration.ServerConfiguration.BaseAddresses.Count == 0)
{
// we do not use the localhost replacement mechanism of the configuration loading, to immediately show the base address here
ApplicationConfiguration.ServerConfiguration.BaseAddresses.Add($"opc.tcp://{Hostname}:{ServerPort}{ServerPath}");
}
foreach (var endpoint in ApplicationConfiguration.ServerConfiguration.BaseAddresses)
{
Logger.Information($"OPC UA server base address: {endpoint}");
}
// by default use high secure transport
ServerSecurityPolicy newPolicy = new ServerSecurityPolicy()
{
SecurityMode = MessageSecurityMode.SignAndEncrypt,
SecurityPolicyUri = SecurityPolicies.Basic256Sha256
};
ApplicationConfiguration.ServerConfiguration.SecurityPolicies.Add(newPolicy);
Logger.Information($"Security policy {newPolicy.SecurityPolicyUri} with mode {newPolicy.SecurityMode} added");
// add none secure transport on request
if (EnableUnsecureTransport)
{
newPolicy = new ServerSecurityPolicy()
{
SecurityMode = MessageSecurityMode.None,
SecurityPolicyUri = SecurityPolicies.None
};
ApplicationConfiguration.ServerConfiguration.SecurityPolicies.Add(newPolicy);
Logger.Information($"Unsecure security policy {newPolicy.SecurityPolicyUri} with mode {newPolicy.SecurityMode} added");
Logger.Warning($"Note: This is a security risk and needs to be disabled for production use");
}
// security configuration
await InitApplicationSecurityAsync();
// set LDS registration interval
ApplicationConfiguration.ServerConfiguration.MaxRegistrationInterval = LdsRegistrationInterval;
Logger.Information($"LDS(-ME) registration intervall set to {LdsRegistrationInterval} ms (0 means no registration)");
// show certificate store information
await ShowCertificateStoreInformationAsync();
return ApplicationConfiguration;
}
/// <summary>
/// Event handler to log OPC UA stack trace messages into own logger.
/// </summary>
private static void LoggerOpcUaTraceHandler(object sender, TraceEventArgs e)
{
// return fast if no trace needed
if ((e.TraceMask & OpcStackTraceMask) == 0)
{
return;
}
// e.Exception and e.Message are always null
// format the trace message
string message = string.Empty;
message = string.Format(e.Format, e.Arguments).Trim();
message = "OPC: " + message;
// map logging level
if ((e.TraceMask & OpcTraceToLoggerVerbose) != 0)
{
Logger.Verbose(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerDebug) != 0)
{
Logger.Debug(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerInformation) != 0)
{
Logger.Information(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerWarning) != 0)
{
Logger.Warning(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerError) != 0)
{
Logger.Error(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerFatal) != 0)
{
Logger.Fatal(message);
return;
}
return;
}
private static string _hostname = $"{Utils.GetHostName().ToLowerInvariant()}";
}
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

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

@ -14,7 +14,7 @@ namespace OpcPublisher
using static HubCommunication;
using static OpcPublisher.OpcMonitoredItem;
using static OpcPublisher.PublisherTelemetryConfiguration;
using static OpcStackConfiguration;
using static OpcApplicationConfiguration;
using static Program;
using static PublisherNodeConfiguration;
@ -606,15 +606,15 @@ namespace OpcPublisher
// start connecting
selectedEndpoint = CoreClientUtils.SelectEndpoint(EndpointUrl.AbsoluteUri, UseSecurity);
configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, EndpointConfiguration.Create(PublisherOpcApplicationConfiguration));
configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, EndpointConfiguration.Create(OpcApplicationConfiguration.ApplicationConfiguration));
uint timeout = SessionTimeout * ((UnsuccessfulConnectionCount >= OpcSessionCreationBackoffMax) ? OpcSessionCreationBackoffMax : UnsuccessfulConnectionCount + 1);
Logger.Information($"Create {(UseSecurity ? "secured" : "unsecured")} session for endpoint URI '{EndpointUrl.AbsoluteUri}' with timeout of {timeout} ms.");
OpcUaClientSession = await Session.Create(
PublisherOpcApplicationConfiguration,
OpcApplicationConfiguration.ApplicationConfiguration,
configuredEndpoint,
true,
false,
PublisherOpcApplicationConfiguration.ApplicationName,
OpcApplicationConfiguration.ApplicationConfiguration.ApplicationName,
timeout,
new UserIdentity(new AnonymousIdentityToken()),
null);

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

@ -1,367 +0,0 @@

using Opc.Ua;
using System;
using System.Security.Cryptography.X509Certificates;
namespace OpcPublisher
{
using System.Threading.Tasks;
using static Opc.Ua.CertificateStoreType;
using static Program;
public class OpcStackConfiguration
{
public static ApplicationConfiguration PublisherOpcApplicationConfiguration { get; private set; }
public static string ApplicationName { get; set; } = "publisher";
public static ushort PublisherServerPort { get; set; } = 62222;
public static string PublisherServerPath { get; set; } = "/UA/Publisher";
public static int OpcMaxStringLength { get; set; } = 1024 * 1024;
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;
public static bool OpcPublisherAutoTrustServerCerts { get; set; } = false;
public static uint OpcSessionCreationTimeout { get; set; } = 10;
public static uint OpcSessionCreationBackoffMax { get; set; } = 5;
public static uint OpcKeepAliveDisconnectThreshold { get; set; } = 5;
public static int OpcKeepAliveIntervalInSec { get; set; } = 2;
public const int OpcSamplingIntervalDefault = 1000;
public static int OpcSamplingInterval { get; set; } = OpcSamplingIntervalDefault;
public const int OpcPublishingIntervalDefault = 0;
public static int OpcPublishingInterval { get; set; } = OpcPublishingIntervalDefault;
public static string PublisherServerSecurityPolicy { get; set; } = SecurityPolicies.Basic128Rsa15;
public static string OpcOwnCertStoreType { get; set; } = Directory;
public static string OpcOwnCertDirectoryStorePathDefault => "CertificateStores/own";
public static string OpcOwnCertX509StorePathDefault => "CurrentUser\\UA_MachineDefault";
public static string OpcOwnCertStorePath { get; set; } = OpcOwnCertDirectoryStorePathDefault;
public static string OpcTrustedCertStoreType { get; set; } = Directory;
public static string OpcTrustedCertDirectoryStorePathDefault => "CertificateStores/trusted";
public static string OpcTrustedCertX509StorePathDefault => "CurrentUser\\UA_MachineDefault";
public static string OpcTrustedCertStorePath { get; set; } = null;
public static string OpcRejectedCertStoreType { get; set; } = Directory;
public static string OpcRejectedCertDirectoryStorePathDefault => "CertificateStores/rejected";
public static string OpcRejectedCertX509StorePathDefault => "CurrentUser\\UA_MachineDefault";
public static string OpcRejectedCertStorePath { get; set; } = OpcRejectedCertDirectoryStorePathDefault;
public static string OpcIssuerCertStoreType { get; set; } = Directory;
public static string OpcIssuerCertDirectoryStorePathDefault => "CertificateStores/issuers";
public static string OpcIssuerCertX509StorePathDefault => "CurrentUser\\UA_MachineDefault";
public static string OpcIssuerCertStorePath { get; set; } = OpcIssuerCertDirectoryStorePathDefault;
public static int LdsRegistrationInterval { get; set; } = 0;
public static int OpcTraceToLoggerVerbose { get; set; } = 0;
public static int OpcTraceToLoggerDebug { get; set; } = 0;
public static int OpcTraceToLoggerInformation { get; set; } = 0;
public static int OpcTraceToLoggerWarning { get; set; } = 0;
public static int OpcTraceToLoggerError { get; set; } = 0;
public static int OpcTraceToLoggerFatal { get; set; } = 0;
/// <summary>
/// Configures all OPC stack settings
/// </summary>
public async Task ConfigureAsync()
{
// Instead of using a Config.xml we configure everything programmatically.
//
// OPC UA Application configuration
//
PublisherOpcApplicationConfiguration = new ApplicationConfiguration();
// Passed in as command line argument
PublisherOpcApplicationConfiguration.ApplicationName = ApplicationName;
PublisherOpcApplicationConfiguration.ApplicationUri = $"urn:{Utils.GetHostName()}:{PublisherOpcApplicationConfiguration.ApplicationName}:microsoft:";
PublisherOpcApplicationConfiguration.ProductUri = "https://github.com/Azure/iot-edge-opc-publisher";
PublisherOpcApplicationConfiguration.ApplicationType = ApplicationType.ClientAndServer;
//
// Security configuration
//
PublisherOpcApplicationConfiguration.SecurityConfiguration = new SecurityConfiguration();
// Application certificate
PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier();
PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.StoreType = OpcOwnCertStoreType;
PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.StorePath = OpcOwnCertStorePath;
PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.SubjectName = PublisherOpcApplicationConfiguration.ApplicationName;
Logger.Information($"Application Certificate store type is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.StoreType}");
Logger.Information($"Application Certificate store path is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.StorePath}");
Logger.Information($"Application Certificate subject name is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.SubjectName}");
// Use existing certificate, if it is there.
X509Certificate2 certificate = await PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Find(true);
if (certificate == null)
{
Logger.Information($"No existing Application certificate found. Create a self-signed Application certificate valid from yesterday for {CertificateFactory.defaultLifeTime} months,");
Logger.Information($"with a {CertificateFactory.defaultKeySize} bit key and {CertificateFactory.defaultHashSize} bit hash.");
certificate = CertificateFactory.CreateCertificate(
PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.StoreType,
PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.StorePath,
null,
PublisherOpcApplicationConfiguration.ApplicationUri,
PublisherOpcApplicationConfiguration.ApplicationName,
PublisherOpcApplicationConfiguration.ApplicationName,
null,
CertificateFactory.defaultKeySize,
DateTime.UtcNow - TimeSpan.FromDays(1),
CertificateFactory.defaultLifeTime,
CertificateFactory.defaultHashSize,
false,
null,
null
);
PublisherOpcApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate = certificate ?? throw new Exception("OPC UA application certificate can not be created! Cannot continue without it!");
}
else
{
Logger.Information("Application certificate found in Application Certificate Store");
}
PublisherOpcApplicationConfiguration.ApplicationUri = Utils.GetApplicationUriFromCertificate(certificate);
Logger.Information($"Application certificate is for Application URI '{PublisherOpcApplicationConfiguration.ApplicationUri}', Application '{PublisherOpcApplicationConfiguration.ApplicationName} and has Subject '{PublisherOpcApplicationConfiguration.ApplicationName}'");
// TrustedIssuerCertificates
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedIssuerCertificates = new CertificateTrustList();
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedIssuerCertificates.StoreType = OpcIssuerCertStoreType;
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedIssuerCertificates.StorePath = OpcIssuerCertStorePath;
Logger.Information($"Trusted Issuer store type is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedIssuerCertificates.StoreType}");
Logger.Information($"Trusted Issuer Certificate store path is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedIssuerCertificates.StorePath}");
// TrustedPeerCertificates
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates = new CertificateTrustList();
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StoreType = OpcTrustedCertStoreType;
if (string.IsNullOrEmpty(OpcTrustedCertStorePath))
{
// Set default.
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath = OpcTrustedCertStoreType == X509Store ? OpcTrustedCertX509StorePathDefault : OpcTrustedCertDirectoryStorePathDefault;
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("_TPC_SP")))
{
// Use environment variable.
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath = Environment.GetEnvironmentVariable("_TPC_SP");
}
}
else
{
PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath = OpcTrustedCertStorePath;
}
Logger.Information($"Trusted Peer Certificate store type is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StoreType}");
Logger.Information($"Trusted Peer Certificate store path is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath}");
// RejectedCertificateStore
PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectedCertificateStore = new CertificateTrustList();
PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectedCertificateStore.StoreType = OpcRejectedCertStoreType;
PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectedCertificateStore.StorePath = OpcRejectedCertStorePath;
Logger.Information($"Rejected certificate store type is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectedCertificateStore.StoreType}");
Logger.Information($"Rejected Certificate store path is: {PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectedCertificateStore.StorePath}");
// AutoAcceptUntrustedCertificates
// This is a security risk and should be set to true only for debugging purposes.
PublisherOpcApplicationConfiguration.SecurityConfiguration.AutoAcceptUntrustedCertificates = false;
// RejectSHA1SignedCertificates
// We allow SHA1 certificates for now as many OPC Servers still use them
PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectSHA1SignedCertificates = false;
Logger.Information($"Rejection of SHA1 signed certificates is {(PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectSHA1SignedCertificates ? "enabled" : "disabled")}");
// MinimunCertificatesKeySize
// We allow a minimum key size of 1024 bit, as many OPC UA servers still use them
PublisherOpcApplicationConfiguration.SecurityConfiguration.MinimumCertificateKeySize = 1024;
Logger.Information($"Minimum certificate key size set to {PublisherOpcApplicationConfiguration.SecurityConfiguration.MinimumCertificateKeySize}");
// We make the default reference stack behavior configurable to put our own certificate into the trusted peer store.
if (TrustMyself)
{
// Ensure it is trusted
try
{
ICertificateStore store = PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.OpenStore();
if (store == null)
{
Logger.Information($"Can not open trusted peer store. StorePath={PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath}");
}
else
{
try
{
Logger.Information($"Adding publisher certificate to trusted peer store. StorePath={PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath}");
X509Certificate2 publicKey = new X509Certificate2(certificate.RawData);
X509Certificate2Collection certCollection = await store.FindByThumbprint(publicKey.Thumbprint);
if (certCollection.Count > 0)
{
Logger.Information($"A certificate with the same thumbprint is already in the trusted store.");
}
else
{
await store.Add(publicKey);
}
}
finally
{
store.Close();
}
}
}
catch (Exception e)
{
Logger.Error(e, $"Can not add publisher certificate to trusted peer store. StorePath={PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath}");
}
}
else
{
Logger.Information("Publisher certificate is not added to trusted peer store.");
}
//
// TransportConfigurations
//
PublisherOpcApplicationConfiguration.TransportQuotas = new TransportQuotas();
PublisherOpcApplicationConfiguration.TransportQuotas.MaxByteStringLength = 4 * 1024 * 1024;
PublisherOpcApplicationConfiguration.TransportQuotas.MaxMessageSize = 4 * 1024 * 1024;
// the maximum string length could be set to ajust for large number of nodes when reading the list of published nodes
PublisherOpcApplicationConfiguration.TransportQuotas.MaxStringLength = OpcMaxStringLength;
// the OperationTimeout should be twice the minimum value for PublishingInterval * KeepAliveCount, so set to 120s
PublisherOpcApplicationConfiguration.TransportQuotas.OperationTimeout = OpcOperationTimeout;
Logger.Information($"OperationTimeout set to {PublisherOpcApplicationConfiguration.TransportQuotas.OperationTimeout}");
//
// ServerConfiguration
//
PublisherOpcApplicationConfiguration.ServerConfiguration = new ServerConfiguration();
// BaseAddresses
if (PublisherOpcApplicationConfiguration.ServerConfiguration.BaseAddresses.Count == 0)
{
// We do not use the localhost replacement mechanism of the configuration loading, to immediately show the base address here
PublisherOpcApplicationConfiguration.ServerConfiguration.BaseAddresses.Add($"opc.tcp://{Utils.GetHostName()}:{PublisherServerPort}{PublisherServerPath}");
}
foreach (var endpoint in PublisherOpcApplicationConfiguration.ServerConfiguration.BaseAddresses)
{
Logger.Information($"Publisher server base address: {endpoint}");
}
// SecurityPolicies
// We do not allow security policy SecurityPolicies.None, but always high security
ServerSecurityPolicy newPolicy = new ServerSecurityPolicy()
{
SecurityMode = MessageSecurityMode.SignAndEncrypt,
SecurityPolicyUri = SecurityPolicies.Basic256Sha256
};
PublisherOpcApplicationConfiguration.ServerConfiguration.SecurityPolicies.Add(newPolicy);
Logger.Information($"Security policy {newPolicy.SecurityPolicyUri} with mode {newPolicy.SecurityMode} added");
// MaxRegistrationInterval
PublisherOpcApplicationConfiguration.ServerConfiguration.MaxRegistrationInterval = LdsRegistrationInterval;
Logger.Information($"LDS(-ME) registration intervall set to {LdsRegistrationInterval} ms (0 means no registration)");
//
// TraceConfiguration
//
//
// TraceConfiguration
//
PublisherOpcApplicationConfiguration.TraceConfiguration = new TraceConfiguration();
PublisherOpcApplicationConfiguration.TraceConfiguration.TraceMasks = OpcStackTraceMask;
PublisherOpcApplicationConfiguration.TraceConfiguration.ApplySettings();
Utils.Tracing.TraceEventHandler += new EventHandler<TraceEventArgs>(LoggerOpcUaTraceHandler);
Logger.Information($"opcstacktracemask set to: 0x{OpcStackTraceMask:X}");
// add default client configuration
PublisherOpcApplicationConfiguration.ClientConfiguration = new ClientConfiguration();
// validate the configuration now
await PublisherOpcApplicationConfiguration.Validate(PublisherOpcApplicationConfiguration.ApplicationType);
}
/// <summary>
/// Event handler to log OPC UA stack trace messages into own logger.
/// </summary>
private static void LoggerOpcUaTraceHandler(object sender, TraceEventArgs e)
{
// return fast if no trace needed
if ((e.TraceMask & OpcStackTraceMask) == 0)
{
return;
}
// e.Exception and e.Message are always null
// format the trace message
string message = string.Empty;
message = string.Format(e.Format, e.Arguments)?.Trim();
message = "OPC: " + message;
// map logging level
if ((e.TraceMask & OpcTraceToLoggerVerbose) != 0)
{
Logger.Verbose(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerDebug) != 0)
{
Logger.Debug(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerInformation) != 0)
{
Logger.Information(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerWarning) != 0)
{
Logger.Warning(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerError) != 0)
{
Logger.Error(message);
return;
}
if ((e.TraceMask & OpcTraceToLoggerFatal) != 0)
{
Logger.Fatal(message);
return;
}
return;
}
}
}

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

@ -21,21 +21,37 @@ namespace OpcPublisher
using static IotEdgeHubCommunication;
using static Opc.Ua.CertificateStoreType;
using static OpcSession;
using static OpcStackConfiguration;
using static OpcApplicationConfiguration;
using static PublisherNodeConfiguration;
using static PublisherTelemetryConfiguration;
using static System.Console;
public class Program
{
/// <summary>
/// IoTHub/EdgeHub communication objects.
/// </summary>
public static IotHubCommunication IotHubCommunication;
public static IotEdgeHubCommunication IotEdgeHubCommunication;
/// <summary>
/// Shutdown token source.
/// </summary>
public static CancellationTokenSource ShutdownTokenSource;
/// <summary>
/// Used as delay in sec when shutting down the application.
/// </summary>
public static uint PublisherShutdownWaitPeriod { get; } = 10;
/// <summary>
/// Stores startup time.
/// </summary>
public static DateTime PublisherStartTime { get; set; } = DateTime.UtcNow;
/// <summary>
/// Logging object.
/// </summary>
public static Serilog.Core.Logger Logger { get; set; } = null;
/// <summary>
@ -91,19 +107,6 @@ namespace OpcPublisher
}
}
},
{ "sd|shopfloordomain=", $"same as site option, only there for backward compatibility\n" +
"The value must follow the syntactical rules of a DNS hostname.\nDefault: not set", (string s) => {
Regex siteNameRegex = new Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$");
if (siteNameRegex.IsMatch(s))
{
PublisherSite = s;
}
else
{
throw new OptionException("The shopfloor domain is not a valid DNS hostname.", "shopfloordomain");
}
}
},
{ "ic|iotcentral", $"publisher will send OPC UA data in IoTCentral compatible format (DisplayName of a node is used as key, this key is the Field name in IoTCentral). you need to ensure that all DisplayName's are unique. (Auto enables fetch display name)\nDefault: {IotCentralMode}", b => IotCentralMode = FetchOpcNodeDisplayName = b != null },
{ "sw|sessionconnectwait=", $"specify the wait time in seconds publisher is trying to connect to disconnected endpoints and starts monitoring unmonitored items\nMin: 10\nDefault: {SessionConnectWaitSec}", (int i) => {
if (i > 10)
@ -129,8 +132,6 @@ namespace OpcPublisher
},
{ "di|diagnosticsinterval=", $"shows publisher diagnostic info at the specified interval in seconds (need log level info). 0 disables diagnostic output.\nDefault: {DiagnosticsInterval}", (uint u) => DiagnosticsInterval = u },
{ "vc|verboseconsole=", $"ignored, only supported for backward comaptibility.", b => {}},
{ "ns|noshutdown=", $"same as runforever.\nDefault: {_noShutdown}", (bool b) => _noShutdown = b },
{ "rf|runforever", $"publisher can not be stopped by pressing a key on the console, but will run forever.\nDefault: {_noShutdown}", b => _noShutdown = b != null },
@ -159,7 +160,7 @@ namespace OpcPublisher
}
},
// IoTHub specific options
{ "ih|iothubprotocol=", $"{(IsIotEdgeModule ? "not supported when running as IoTEdge module (Mqtt_Tcp_Only is enforced)\n" : $"the protocol to use for communication with Azure IoTHub (allowed values: {string.Join(", ", Enum.GetNames(IotHubProtocol.GetType()))}).\nDefault: {Enum.GetName(IotHubProtocol.GetType(), IotHubProtocol)}")}",
{ "ih|iothubprotocol=", $"{(IsIotEdgeModule ? "not supported when running as IoT Edge module\n" : $"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) => {
if (IsIotEdgeModule)
{
@ -205,8 +206,8 @@ namespace OpcPublisher
},
// opc server configuration options
{ "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 },
{ "pn|portnum=", $"the server port of the publisher OPC server endpoint.\nDefault: {ServerPort}", (ushort p) => ServerPort = p },
{ "pa|path=", $"the enpoint URL path part of the publisher OPC server endpoint.\nDefault: '{ServerPath}'", (string a) => ServerPath = a },
{ "lr|ldsreginterval=", $"the LDS(-ME) registration interval in ms. If 0, then the registration is disabled.\nDefault: {LdsRegistrationInterval}", (int i) => {
if (i >= 0)
{
@ -299,9 +300,7 @@ namespace OpcPublisher
}
}
},
{ "st|opcstacktracemask=", $"ignored, only supported for backward comaptibility.", i => {}},
{ "as|autotrustservercerts=", $"same as autoaccept, only supported for backward cmpatibility.\nDefault: {OpcPublisherAutoTrustServerCerts}", (bool b) => OpcPublisherAutoTrustServerCerts = b },
{ "aa|autoaccept", $"the publisher trusts all servers it is establishing a connection to.\nDefault: {OpcPublisherAutoTrustServerCerts}", b => OpcPublisherAutoTrustServerCerts = b != null },
// trust own public cert option
@ -311,7 +310,7 @@ namespace OpcPublisher
{ "fd|fetchdisplayname=", $"same as fetchname.\nDefault: {FetchOpcNodeDisplayName}", (bool b) => FetchOpcNodeDisplayName = IotCentralMode ? true : b },
{ "fn|fetchname", $"enable to read the display name of a published node from the server. this will increase the runtime.\nDefault: {FetchOpcNodeDisplayName}", b => FetchOpcNodeDisplayName = IotCentralMode ? true : b != null },
// own cert store options
// 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(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase))
{
@ -329,60 +328,100 @@ namespace OpcPublisher
$"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(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase))
{ "tp|trustedcertstorepath=", $"the path of the trusted cert store\nDefault: '{OpcTrustedCertDirectoryStorePathDefault}'", (string s) => OpcTrustedCertStorePath = s },
{ "rp|rejectedcertstorepath=", $"the path of the rejected cert store\nDefault '{OpcRejectedCertDirectoryStorePathDefault}'", (string s) => OpcRejectedCertStorePath = s },
{ "ip|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault '{OpcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s },
{ "csr", $"show data to create a certificate signing request\nDefault '{ShowCreateSigningRequestInfo}'", c => ShowCreateSigningRequestInfo = c != null },
{ "ab|applicationcertbase64=", $"update/set this applications certificate with the certificate passed in as bas64 string", (string s) =>
{
NewCertificateBase64String = s;
}
},
{ "af|applicationcertfile=", $"update/set this applications certificate with the certificate file specified", (string s) =>
{
if (File.Exists(s))
{
OpcTrustedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory;
OpcTrustedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcTrustedCertX509StorePathDefault : OpcTrustedCertDirectoryStorePathDefault;
NewCertificateFileName = s;
}
else
{
throw new OptionException();
throw new OptionException("The file '{s}' does not exist.", "applicationcertfile");
}
}
},
{ "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(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase))
{ "pb|privatekeybase64=", $"initial provisioning of the application certificate (with a PEM or PFX fomat) requires a private key passed in as base64 string", (string s) =>
{
PrivateKeyBase64String = s;
}
},
{ "pk|privatekeyfile=", $"initial provisioning of the application certificate (with a PEM or PFX fomat) requires a private key passed in as file", (string s) =>
{
if (File.Exists(s))
{
OpcRejectedCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory;
OpcRejectedCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcRejectedCertX509StorePathDefault : OpcRejectedCertDirectoryStorePathDefault;
PrivateKeyFileName = s;
}
else
{
throw new OptionException();
throw new OptionException("The file '{s}' does not exist.", "privatekeyfile");
}
}
},
{ "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(CertificateStoreType.Directory, StringComparison.OrdinalIgnoreCase))
{ "cp|certpassword=", $"the optional password for the PEM or PFX or the installed application certificate", (string s) =>
{
CertificatePassword = s;
}
},
{ "tb|addtrustedcertbase64=", $"adds the certificate to the applications trusted cert store passed in as base64 string (multiple strings supported)", (string s) =>
{
TrustedCertificateBase64Strings = ParseListOfStrings(s);
}
},
{ "tf|addtrustedcertfile=", $"adds the certificate file(s) to the applications trusted cert store passed in as base64 string (multiple filenames supported)", (string s) =>
{
TrustedCertificateFileNames = ParseListOfFileNames(s, "addtrustedcertfile");
}
},
{ "ib|addissuercertbase64=", $"adds the specified issuer certificate to the applications trusted issuer cert store passed in as base64 string (multiple strings supported)", (string s) =>
{
IssuerCertificateBase64Strings = ParseListOfStrings(s);
}
},
{ "if|addissuercertfile=", $"adds the specified issuer certificate file(s) to the applications trusted issuer cert store (multiple filenames supported)", (string s) =>
{
IssuerCertificateFileNames = ParseListOfFileNames(s, "addissuercertfile");
}
},
{ "rb|updatecrlbase64=", $"update the CRL passed in as base64 string to the corresponding cert store (trusted or trusted issuer)", (string s) =>
{
CrlBase64String = s;
}
},
{ "uc|updatecrlfile=", $"update the CRL passed in as file to the corresponding cert store (trusted or trusted issuer)", (string s) =>
{
if (File.Exists(s))
{
OpcIssuerCertStoreType = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? X509Store : CertificateStoreType.Directory;
OpcIssuerCertStorePath = s.Equals(X509Store, StringComparison.OrdinalIgnoreCase) ? OpcIssuerCertX509StorePathDefault : OpcIssuerCertDirectoryStorePathDefault;
CrlFileName = s;
}
else
{
throw new OptionException();
throw new OptionException("The file '{s}' does not exist.", "updatecrlfile");
}
}
},
{ "ip|issuercertstorepath=", $"the path of the trusted issuer cert store\nDefault (depends on store type):\n" +
$"X509Store: '{OpcIssuerCertX509StorePathDefault}'\n" +
$"Directory: '{OpcIssuerCertDirectoryStorePathDefault}'", (string s) => OpcIssuerCertStorePath = s
{ "rc|removecert=", $"remove cert(s) with the given thumbprint(s) (multiple thumbprints supported)", (string s) =>
{
ThumbprintsToRemove = ParseListOfStrings(s);
}
},
// device connection string cert store options
@ -406,6 +445,28 @@ namespace OpcPublisher
// misc
{ "i|install", $"register OPC Publisher with IoTHub and then exits.\nDefault: {_installOnly}", i => _installOnly = i != null },
{ "h|help", "show this message and exit", h => shouldShowHelp = h != null },
// all the following are only supported to not break existing command lines, but some of them are just ignored
{ "st|opcstacktracemask=", $"ignored, only supported for backward comaptibility.", i => {}},
{ "sd|shopfloordomain=", $"same as site option, only there for backward compatibility\n" +
"The value must follow the syntactical rules of a DNS hostname.\nDefault: not set", (string s) => {
Regex siteNameRegex = new Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$");
if (siteNameRegex.IsMatch(s))
{
PublisherSite = s;
}
else
{
throw new OptionException("The shopfloor domain is not a valid DNS hostname.", "shopfloordomain");
}
}
},
{ "vc|verboseconsole=", $"ignored, only supported for backward comaptibility.", b => {}},
{ "as|autotrustservercerts=", $"same as autoaccept, only supported for backward cmpatibility.\nDefault: {OpcPublisherAutoTrustServerCerts}", (bool b) => OpcPublisherAutoTrustServerCerts = b },
{ "tt|trustedcertstoretype=", $"ignored, only supported for backward compatibility. the trusted cert store will always reside in a directory.", s => { }},
{ "rt|rejectedcertstoretype=", $"ignored, only supported for backward compatibility. the rejected cert store will always reside in a directory.", s => { }},
{ "it|issuercertstoretype=", $"ignored, only supported for backward compatibility. the trusted issuer cert store will always reside in a directory.", s => { }},
};
@ -508,8 +569,8 @@ namespace OpcPublisher
}
// init OPC configuration and tracing
OpcStackConfiguration opcStackConfiguration = new OpcStackConfiguration();
await opcStackConfiguration.ConfigureAsync();
OpcApplicationConfiguration opcApplicationConfiguration = new OpcApplicationConfiguration();
await opcApplicationConfiguration.ConfigureAsync();
// log shopfloor site setting
if (string.IsNullOrEmpty(PublisherSite))
@ -525,20 +586,20 @@ namespace OpcPublisher
if (OpcPublisherAutoTrustServerCerts)
{
Logger.Information("Publisher configured to auto trust server certificates of the servers it is connecting to.");
PublisherOpcApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_AutoTrustServerCerts);
OpcApplicationConfiguration.ApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_AutoTrustServerCerts);
}
else
{
Logger.Information("Publisher configured to not auto trust server certificates. When connecting to servers, you need to manually copy the rejected server certs to the trusted store to trust them.");
PublisherOpcApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_Default);
OpcApplicationConfiguration.ApplicationConfiguration.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_Default);
}
// start our server interface
try
{
Logger.Information($"Starting server on endpoint {PublisherOpcApplicationConfiguration.ServerConfiguration.BaseAddresses[0].ToString()} ...");
Logger.Information($"Starting server on endpoint {OpcApplicationConfiguration.ApplicationConfiguration.ServerConfiguration.BaseAddresses[0].ToString()} ...");
_publisherServer = new PublisherServer();
_publisherServer.Start(PublisherOpcApplicationConfiguration);
_publisherServer.Start(OpcApplicationConfiguration.ApplicationConfiguration);
Logger.Information("Server started.");
}
catch (Exception e)
@ -767,9 +828,9 @@ namespace OpcPublisher
{
Logger.Information($"OPC Publisher does not trust the server with the certificate subject '{e.Certificate.Subject}'.");
Logger.Information("If you want to trust this certificate, please copy it from the directory:");
Logger.Information($"{PublisherOpcApplicationConfiguration.SecurityConfiguration.RejectedCertificateStore.StorePath}/certs");
Logger.Information($"{OpcApplicationConfiguration.ApplicationConfiguration.SecurityConfiguration.RejectedCertificateStore.StorePath}/certs");
Logger.Information("to the directory:");
Logger.Information($"{PublisherOpcApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath}/certs");
Logger.Information($"{OpcApplicationConfiguration.ApplicationConfiguration.SecurityConfiguration.TrustedPeerCertificates.StorePath}/certs");
}
}
@ -871,6 +932,94 @@ namespace OpcPublisher
return;
}
/// <summary>
/// Helper to build a list of byte arrays out of a comma separated list of base64 strings (optional in double quotes).
/// </summary>
private static List<string> ParseListOfStrings(string s)
{
List<string> strings = new List<string>();
if (s[0] == '"' && (s.Count(c => c.Equals('"')) % 2 == 0))
{
while (s.Contains('"'))
{
int first = 0;
int next = 0;
first = s.IndexOf('"', next);
next = s.IndexOf('"', ++first);
strings.Add(s.Substring(first, next - first));
s = s.Substring(++next);
}
}
else if (s.Contains(','))
{
strings = s.Split(',').ToList();
strings.ForEach(st => st.Trim());
strings = strings.Select(st => st.Trim()).ToList();
}
else
{
strings.Add(s);
}
return strings;
}
/// <summary>
/// Helper to build a list of filenames out of a comma separated list of filenames (optional in double quotes).
/// </summary>
private static List<string> ParseListOfFileNames(string s, string option)
{
List<string> fileNames = new List<string>();
if (s[0] == '"' && (s.Count(c => c.Equals('"')) % 2 == 0))
{
while (s.Contains('"'))
{
int first = 0;
int next = 0;
first = s.IndexOf('"', next);
next = s.IndexOf('"', ++first);
var fileName = s.Substring(first, next - first);
if (File.Exists(fileName))
{
fileNames.Add(fileName);
}
else
{
throw new OptionException($"The file '{fileName}' does not exist.", option);
}
s = s.Substring(++next);
}
}
else if (s.Contains(','))
{
List<string> parsedFileNames = s.Split(',').ToList();
parsedFileNames = parsedFileNames.Select(st => st.Trim()).ToList();
foreach (var fileName in parsedFileNames)
{
if (File.Exists(fileName))
{
fileNames.Add(fileName);
}
else
{
throw new OptionException($"The file '{fileName}' does not exist.", option);
}
}
}
else
{
if (File.Exists(s))
{
fileNames.Add(s);
}
else
{
throw new OptionException($"The file '{s}' does not exist.", option);
}
}
return fileNames;
}
private static PublisherServer _publisherServer;
private static bool _noShutdown = false;
private static bool _installOnly = false;

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

@ -7,13 +7,12 @@ using System.Threading.Tasks;
namespace OpcPublisher
{
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading;
using static OpcApplicationConfiguration;
using static OpcMonitoredItem;
using static OpcSession;
using static OpcStackConfiguration;
using static Program;
public static class PublisherNodeConfiguration

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

@ -9,8 +9,8 @@ namespace OpcPublisher
using Newtonsoft.Json;
using System.Linq;
using System.Net;
using static OpcApplicationConfiguration;
using static OpcPublisher.Program;
using static OpcStackConfiguration;
using static PublisherNodeConfiguration;
public class PublisherNodeManager : CustomNodeManager2

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

@ -130,11 +130,9 @@ namespace OpcPublisher
}
else
{
RsaUtils.RSADispose(rsa);
throw new CryptographicException("Can not encrypt IoTHub security token using generated public key!");
}
}
RsaUtils.RSADispose(rsa);
// sign the cert with the private key
ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", keys.Private, random);
@ -240,7 +238,6 @@ namespace OpcPublisher
return Encoding.ASCII.GetString(token);
}
}
RsaUtils.RSADispose(rsa);
}
}
return null;

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

@ -42,7 +42,7 @@
<PackageReference Include="Microsoft.Azure.Devices.Client" Version="1.18.1" />
<PackageReference Include="Mono.Options" Version="5.3.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua" Version="1.4.353.15" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua" Version="1.4.354.23" />
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.0.0" />