//******************************************************************************************************
// SubscriberRequestViewModel.cs - Gbtc
//
// Copyright © 2010, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 11/29/2012 - Stephen C. Wills
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Xml.Linq;
using GSF.Collections;
using GSF.Console;
using GSF.Data;
using GSF.IO;
using GSF.PhasorProtocols.UI.DataModels;
using GSF.PhasorProtocols.UI.UserControls;
using GSF.Security.Cryptography;
using GSF.ServiceProcess;
using GSF.TimeSeries.UI;
using GSF.TimeSeries.UI.Commands;
using GSF.TimeSeries.UI.DataModels;
using GSF.Units;
using Microsoft.Win32;
using Random = GSF.Security.Cryptography.Random;
namespace GSF.TimeSeries.Transport.UI.ViewModels
{
internal class SubscriberRequestViewModel : ViewModelBase, IDisposable
{
#region [ Members ]
// Fields
private string m_publisherAcronym;
private string m_publisherName;
private string m_hostname;
private int m_publisherPort;
private SecurityMode m_securityMode;
private bool m_useSTTP;
private bool m_useGEP;
private bool m_useSourcePrefix;
private bool m_receiveExternalMetadata;
private bool m_receiveInternalMetadata;
private bool m_useUdpDataChannel;
private int m_udpDataChannelPort;
private bool m_enableDataGapRecovery;
private double m_recoveryStartDelay;
private double m_dataMonitoringInterval;
private double m_minimumRecoverySpan;
private double m_maximumRecoverySpan;
private int m_recoveryProcessingInterval;
private string m_loggingPath;
private bool m_disposed;
private string m_subscriberAcronym;
private string m_subscriberName;
private int m_internalDataPublisherPort;
private string m_sharedKey;
private string m_identityCertificate;
private string m_validIPAddresses;
private string m_localCertificateFile;
private string m_localCertificateServerPath;
private string m_importCertificatePath;
private byte[] m_localCertificateData;
private string m_remoteCertificateFile;
private byte[] m_remoteCertificateData;
private bool m_isRemoteCertificateSelfSigned;
private string m_validPolicyErrors;
private string m_validChainFlags;
private Visibility m_connectivityMessageVisibility;
private bool m_advancedTlsSettingsPopupIsOpen;
private AutoResetEvent m_responseComplete;
private ICommand m_localBrowseCommand;
private ICommand m_remoteBrowseCommand;
private ICommand m_importCertificateCommand;
private ICommand m_advancedTlsSettingsOpenCommand;
private ICommand m_advancedTlsSettingsCloseCommand;
private ICommand m_createCommand;
private ICommand m_saveCommand;
#endregion
#region [ Constructors ]
///
/// Creates a new instance of the class.
///
public SubscriberRequestViewModel()
{
m_internalDataPublisherPort = 7165;
m_securityMode = SecurityMode.TLS;
m_useSTTP = true;
m_useSourcePrefix = true;
m_receiveInternalMetadata = true;
m_udpDataChannelPort = 64170;
m_publisherPort = DefaultPort;
m_responseComplete = new AutoResetEvent(false);
Load();
}
///
/// Releases the unmanaged resources before the object is reclaimed by .
///
~SubscriberRequestViewModel()
{
Dispose(false);
}
#endregion
#region [ Properties ]
///
/// Gets or sets the string identifier used to identify the publisher.
///
public string PublisherAcronym
{
get => m_publisherAcronym;
set
{
bool updateRemoteCertificateFile = (string.IsNullOrEmpty(m_publisherAcronym) && string.IsNullOrEmpty(m_remoteCertificateFile)) ||
m_remoteCertificateFile.Equals($"{m_publisherAcronym}.cer", StringComparison.CurrentCultureIgnoreCase);
m_publisherAcronym = value;
OnPropertyChanged("PublisherAcronym");
if (updateRemoteCertificateFile)
RemoteCertificateFile = string.IsNullOrEmpty(m_publisherAcronym) ? string.Empty : $"{m_publisherAcronym}.cer";
}
}
///
/// Gets or sets the name of the publisher.
///
public string PublisherName
{
get => m_publisherName;
set
{
m_publisherName = value;
OnPropertyChanged("PublisherName");
}
}
///
/// Gets or sets the host name or IP address of the server hosting the data publisher.
///
public string Hostname
{
get => m_hostname;
set
{
m_hostname = value;
OnPropertyChanged("Hostname");
}
}
///
/// Gets or sets the port that the data publisher is listening on.
///
public int PublisherPort
{
get => m_publisherPort;
set
{
m_publisherPort = value;
OnPropertyChanged("PublisherPort");
}
}
///
/// Gets or sets the security mode used by the data publisher.
///
public SecurityMode SecurityMode
{
get => m_securityMode;
set
{
int oldDefaultPort = DefaultPort;
m_securityMode = value;
OnPropertyChanged("SecurityMode");
OnPropertyChanged("TransportLayerSecuritySelected");
OnPropertyChanged("GatewaySecuritySelected");
if (m_publisherPort == oldDefaultPort)
PublisherPort = DefaultPort;
}
}
///
/// Gets or sets flag that determines if STTP protocol should be used.
///
public bool UseSTTP
{
get => m_useSTTP;
set
{
m_useSTTP = value;
OnPropertyChanged("UseSTTP");
}
}
///
/// Gets or sets flag that determines if GEP protocol should be used.
///
public bool UseGEP
{
get => m_useGEP;
set
{
m_useGEP = value;
OnPropertyChanged("UseGEP");
}
}
///
/// Gets or sets flag that determines if source prefix should be used.
///
public bool UseSourcePrefix
{
get => m_useSourcePrefix;
set
{
m_useSourcePrefix = value;
OnPropertyChanged("UseSourcePrefix");
}
}
///
/// Gets or sets the flag that determines whether the subscriber is
/// requesting metadata for internal signals from the publisher.
///
public bool ReceiveInternalMetadata
{
get => m_receiveInternalMetadata;
set
{
m_receiveInternalMetadata = value;
OnPropertyChanged("ReceiveInternalMetadata");
if (!m_receiveInternalMetadata && !m_receiveExternalMetadata)
ReceiveExternalMetadata = true;
}
}
///
/// Gets or sets the flag that determines whether the subscriber is
/// requesting metadata for external signals from the publisher.
///
public bool ReceiveExternalMetadata
{
get => m_receiveExternalMetadata;
set
{
m_receiveExternalMetadata = value;
OnPropertyChanged("ReceiveExternalMetadata");
if (!m_receiveInternalMetadata && !m_receiveExternalMetadata)
ReceiveInternalMetadata = true;
}
}
///
/// Gets or sets the flag that determines whether the subscriber
/// should be configured with or without a UDP data channel.
///
public bool UseUdpDataChannel
{
get => m_useUdpDataChannel;
set
{
m_useUdpDataChannel = value;
OnPropertyChanged("UseUdpDataChannel");
}
}
///
/// Gets or sets the port used for the UDP data channel.
///
public int UdpDataChannelPort
{
get => m_udpDataChannelPort;
set
{
m_udpDataChannelPort = value;
OnPropertyChanged("UdpDataChannelPort");
}
}
///
/// Gets or sets flag that determines if data gap recovery will be enabled.
///
public bool EnableDataGapRecovery
{
get => m_enableDataGapRecovery;
set
{
m_enableDataGapRecovery = value;
OnPropertyChanged("EnableDataGapRecovery");
}
}
///
/// Gets or sets the minimum time delay, in seconds, to wait before starting the data recovery for an .
///
///
///
/// For some archiving systems it may take a few seconds for data to make it to disk and therefore be readily
/// available for a temporal subscription query. The should be adjusted based
/// on the nature of the system used to archive data. If the archival system makes data immediately available
/// because of internal caching or other means, this value can be zero.
///
///
/// Use of this value depends on the local clock, as such the value should be increased by the uncertainty of
/// accuracy of the local clock. For example, if it is know that the local clock floats +/-5 seconds from
/// real-time, then increase the desired value of the by 5 seconds.
///
///
public double RecoveryStartDelay
{
get => m_recoveryStartDelay;
set
{
m_recoveryStartDelay = value;
OnPropertyChanged("RecoveryStartDelay");
}
}
///
/// Gets or sets the interval, in seconds, over which the data monitor will check for new data.
///
///
/// Once a connection is established a timer is enabled to monitor for new incoming data. The data monitoring timer
/// exists to make sure data is being received so that the process of recovery does not wait endlessly for data that
/// may never come because of a possible error in the recovery process. The
/// allows the consumer to adjust the interval over which the timer will check for new incoming data.
///
/// It will take some time, perhaps a couple of seconds, to start the temporal subscription and begin the process
/// of recovering data for an . Make sure the value for is
/// sufficiently large enough to handle any initial delays in data transmission.
///
///
public double DataMonitoringInterval
{
get => m_dataMonitoringInterval;
set
{
m_dataMonitoringInterval = value;
OnPropertyChanged("DataMonitoringInterval");
}
}
///
/// Gets to sets the minimum time span, in seconds, for which a data recovery will be attempted.
/// Set to zero for no minimum.
///
public double MinimumRecoverySpan
{
get => m_minimumRecoverySpan;
set
{
m_minimumRecoverySpan = value;
OnPropertyChanged("MinimumRecoverySpan");
}
}
///
/// Gets to sets the maximum time span, in days for UI - API is seconds, for which a data recovery will be attempted.
/// Set to for no maximum.
///
public double MaximumRecoverySpan
{
get => m_maximumRecoverySpan;
set
{
m_maximumRecoverySpan = value;
OnPropertyChanged("MaximumRecoverySpan");
}
}
///
/// Gets or sets the data recovery processing interval, in whole milliseconds, to use in the temporal data
/// subscription when recovering data for an .
/// A value of -1 indicates the default processing interval will be requested.
/// A value of 0 indicates data will be processed as fast as possible.
///
///
/// With the exception of the values of -1 and 0, the value specifies
/// the desired historical data playback processing interval in milliseconds. This is basically a delay, or timer
/// interval, over which to process data. Setting this value to -1 means to use the default processing interval
/// while setting the value to 0 means to process data as fast as possible, i.e., as fast as the historian can
/// query the data. Depending on the available bandwidth, this parameter may need to be adjusted such that the
/// data being recovered does not adversely interfere with the ongoing transmission of real-time data.
///
public int RecoveryProcessingInterval
{
get => m_recoveryProcessingInterval;
set
{
m_recoveryProcessingInterval = value;
OnPropertyChanged("RecoveryProcessingInterval");
}
}
///
/// Gets or sets logging path to be used to be runtime and outage logs of the subscriber which are required for
/// automated data recovery.
///
///
/// Leave value blank for default path, i.e., installation folder. Can be a fully qualified path or a path that
/// is relative to the installation folder, e.g., a value of "ConfigurationCache" might resolve to
/// "C:\Program Files\MyTimeSeriespPp\ConfigurationCache\".
///
public string LoggingPath
{
get => m_loggingPath;
set
{
m_loggingPath = value;
OnPropertyChanged("LoggingPath");
}
}
///
/// Gets or sets the port used by the internal data publisher.
///
public int InternalDataPublisherPort
{
get => m_internalDataPublisherPort;
set
{
int oldDefaultPort = DefaultPort;
m_internalDataPublisherPort = value;
if (m_publisherPort == oldDefaultPort)
PublisherPort = DefaultPort;
}
}
///
/// Gets or sets the string identifier used to identify the subscriber.
///
public string SubscriberAcronym
{
get => m_subscriberAcronym;
set
{
m_subscriberAcronym = value;
OnPropertyChanged("SubscriberAcronym");
}
}
///
/// Gets or sets the name of the subscriber.
///
public string SubscriberName
{
get => m_subscriberName;
set
{
m_subscriberName = value;
OnPropertyChanged("SubscriberName");
}
}
///
/// Gets the flag that indicates whether the user has selected TLS as the security mode.
///
public bool TransportLayerSecuritySelected => m_securityMode == SecurityMode.TLS;
///
/// Gets or sets the path to the local certificate used to identify the subscriber.
///
public string LocalCertificateFile
{
get => m_localCertificateFile;
set
{
bool updateServerPath = (string.IsNullOrEmpty(m_localCertificateFile) && string.IsNullOrEmpty(m_localCertificateServerPath)) ||
((object)m_localCertificateFile != null && m_localCertificateFile.Equals(m_localCertificateServerPath, StringComparison.CurrentCultureIgnoreCase));
m_localCertificateFile = value;
OnPropertyChanged("LocalCertificateFile");
if (updateServerPath)
LocalCertificateServerPath = m_localCertificateFile;
}
}
///
/// Gets or sets the path on the server to the local certificate used to identify the subscriber.
///
public string LocalCertificateServerPath
{
get => m_localCertificateServerPath;
set
{
m_localCertificateServerPath = value;
OnPropertyChanged("LocalCertificateServerPath");
}
}
///
/// Gets or sets the path to the certificate to be imported to the service.
///
public string ImportCertificatePath
{
get => m_importCertificatePath;
set
{
m_importCertificatePath = value;
OnPropertyChanged("ImportCertificatePath");
}
}
///
/// Gets or sets the path to the remote certificate used to identify the publisher.
///
public string RemoteCertificateFile
{
get => m_remoteCertificateFile;
set
{
m_remoteCertificateFile = value;
OnPropertyChanged("RemoteCertificateFile");
}
}
///
/// Gets or sets the flag that indicates whether the remote certificate is a self-signed certificate.
///
public bool RemoteCertificateIsSelfSigned
{
get => m_isRemoteCertificateSelfSigned;
set
{
m_isRemoteCertificateSelfSigned = value;
OnPropertyChanged("SelfSigned");
}
}
///
/// Gets or sets the list of valid policy errors that can occur during remote certificate validation.
///
public string ValidPolicyErrors
{
get => m_validPolicyErrors;
set
{
m_validPolicyErrors = value;
OnPropertyChanged("ValidPolicyErrors");
}
}
///
/// Gets or sets the list of valid chain flags which can be set during remote certificate validation.
///
public string ValidChainFlags
{
get => m_validChainFlags;
set
{
m_validChainFlags = value;
OnPropertyChanged("ValidChainFlags");
}
}
///
/// Gets the flag that indicates whether the user has selected Gateway security.
///
public bool GatewaySecuritySelected => m_securityMode == SecurityMode.Gateway;
///
/// Gets or sets the shared key sent to the publisher for Gateway security encryption.
///
public string SharedKey
{
get => m_sharedKey;
set
{
m_sharedKey = value;
OnPropertyChanged("SharedKey");
}
}
///
/// Gets or sets the identity certificate exchanged during Gateway security authentication.
///
public string IdentityCertificate
{
get => m_identityCertificate;
set
{
m_identityCertificate = value;
OnPropertyChanged("IdentityCertificate");
}
}
///
/// Gets or sets the set of valid IP addresses used by the publisher in order to validate the subscriber's identity.
///
public string ValidIPAddresses
{
get => m_validIPAddresses;
set
{
m_validIPAddresses = value;
OnPropertyChanged("ValidIPAddresses");
}
}
///
/// Gets the command that executes when the user chooses to browse for a local certificate.
///
public ICommand LocalBrowseCommand
{
get
{
if ((object)m_localBrowseCommand == null)
m_localBrowseCommand = new RelayCommand(BrowseLocalCertificateFile, () => true);
return m_localBrowseCommand;
}
}
public ICommand RemoteBrowseCommand
{
get
{
return m_remoteBrowseCommand ?? (m_remoteBrowseCommand = new RelayCommand(BrowseRemoteCertificateFile, () => true));
}
}
///
/// Gets the command that executes when the user chooses to browse for a remote certificate.
///
public ICommand ImportCertificateCommand
{
get
{
if ((object)m_importCertificateCommand == null)
m_importCertificateCommand = new RelayCommand(ImportCertificateFile, () => true);
return m_importCertificateCommand;
}
}
///
/// Gets the command that executes when the user chooses to create the authentication request.
///
public ICommand CreateCommand
{
get
{
if ((object)m_createCommand == null)
m_createCommand = new RelayCommand(CreateAuthenticationRequest, () => true);
return m_createCommand;
}
}
///
/// Gets the command that executes when the user chooses to save their subscriber.
///
public ICommand NextCommand
{
get
{
if ((object)m_saveCommand == null)
m_saveCommand = new RelayCommand(Save, () => true);
return m_saveCommand;
}
}
///
/// Gets the default port based on the current security mode selection.
///
private int DefaultPort
{
get
{
switch (m_securityMode)
{
case SecurityMode.None:
return m_internalDataPublisherPort;
case SecurityMode.Gateway:
return m_internalDataPublisherPort + 1;
case SecurityMode.TLS:
return m_internalDataPublisherPort + 2;
default:
return m_internalDataPublisherPort;
}
}
}
///
/// Gets or sets the visibility of the connectivity message.
///
public Visibility ConnectivityMessageVisibility
{
get => m_connectivityMessageVisibility;
set
{
m_connectivityMessageVisibility = value;
OnPropertyChanged("ConnectivityMessageVisibility");
}
}
///
/// Gets or sets whether the advanced TLS settings popup is open.
///
public bool AdvancedTlsSettingsPopupIsOpen
{
get => m_advancedTlsSettingsPopupIsOpen;
set
{
m_advancedTlsSettingsPopupIsOpen = value;
OnPropertyChanged("AdvancedTlsSettingsPopupIsOpen");
}
}
///
/// Gets the command that is executed when the user chooses to view the advanced TLS settings.
///
public ICommand AdvancedTlsSettingsOpenCommand
{
get
{
return m_advancedTlsSettingsOpenCommand ?? (m_advancedTlsSettingsOpenCommand = new RelayCommand(() => AdvancedTlsSettingsPopupIsOpen = true, () => true));
}
}
///
/// Gets the command that is executed went he user chooses to save adv
///
public ICommand AdvancedTlsSettingsCloseCommand
{
get
{
return m_advancedTlsSettingsCloseCommand ?? (m_advancedTlsSettingsCloseCommand = new RelayCommand(() => AdvancedTlsSettingsPopupIsOpen = false, () => true));
}
}
#endregion
#region [ Methods ]
///
/// Releases all the resources used by the object.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the unmanaged resources used by the object and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected virtual void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
if (disposing)
{
if ((object)m_responseComplete != null)
{
// Release any waiting threads before disposing wait handle
m_responseComplete.Set();
m_responseComplete.Dispose();
}
m_responseComplete = null;
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
}
}
}
///
/// Initializes default settings for internal subscription.
///
public void InitializeDefaultInternalSubscriptionSettings()
{
RecoveryStartDelay = DataGapRecoverer.DefaultRecoveryStartDelay;
DataMonitoringInterval = DataGapRecoverer.DefaultDataMonitoringInterval;
MinimumRecoverySpan = DataGapRecoverer.DefaultMinimumRecoverySpan;
MaximumRecoverySpan = DataGapRecoverer.DefaultMaximumRecoverySpan / Time.SecondsPerDay;
RecoveryProcessingInterval = DataGapRecoverer.DefaultRecoveryProcessingInterval;
LoggingPath = DataSubscriber.DefaultLoggingPath;
}
private void Load()
{
// Try to populate defaults for subscriber acronym and name using company information from the host application configuration file
if (TryGetCompanyAcronym(out string companyAcronym))
{
SubscriberAcronym = companyAcronym;
SubscriberName = $"{companyAcronym} Subscription Authorization";
}
// Connect to database to retrieve company information for current node
using (AdoDataConnection database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory))
{
try
{
string query = database.ParameterizedQueryString("SELECT Company.Acronym, Company.Name FROM Company, Node WHERE Company.ID = Node.CompanyID AND Node.ID = {0}", "id");
DataRow row = database.Connection.RetrieveRow(database.AdapterType, query, database.CurrentNodeID());
PublisherAcronym = row.Field("Acronym");
PublisherName = row.Field("Name");
// Generate a default shared secret password for subscriber key and initialization vector
byte[] buffer = new byte[4];
Random.GetBytes(buffer);
string generatedSecret = Convert.ToBase64String(buffer).RemoveCrLfs();
if (generatedSecret.Contains("="))
generatedSecret = generatedSecret.Split('=')[0];
SharedKey = generatedSecret;
// Generate an identity for this subscriber
AesManaged sa = new AesManaged();
sa.GenerateKey();
IdentityCertificate = Convert.ToBase64String(sa.Key);
// Generate valid local IP addresses for this connection
IEnumerable addresses = Dns.GetHostAddresses(Dns.GetHostName()).OrderBy(key => key.AddressFamily);
ValidIPAddresses = addresses.ToDelimitedString("; ");
}
catch (Exception ex)
{
MessageBox.Show("ERROR: " + ex.Message, "Subscriber Request", MessageBoxButton.OK);
}
try
{
Dictionary settings;
string[] splitServer;
//IPAddress[] hostIPs = null;
//IEnumerable localIPs;
settings = database.DataPublisherConnectionString().ToNonNullString().ParseKeyValuePairs();
//localIPs = Dns.GetHostAddresses("localhost").Concat(Dns.GetHostAddresses(Dns.GetHostName()));
if (settings.TryGetValue("server", out string server))
{
splitServer = server.Split(':');
//hostIPs = Dns.GetHostAddresses(splitServer[0]);
if (splitServer.Length > 1 && int.TryParse(splitServer[1], out int dataPublisherPort))
InternalDataPublisherPort = dataPublisherPort;
}
// These messages show up when not desired and are not very useful anymore...
//// Check to see if entered host name corresponds to a local IP address
//if (hostIPs == null)
// MessageBox.Show("Failed to find service host address. If using Gateway security, secure key exchange may not succeed." + Environment.NewLine + "Please make sure to run manager application with administrative privileges on the server where service is hosted.", "Subscription Request", MessageBoxButton.OK, MessageBoxImage.Warning);
//else if (!hostIPs.Any(ip => localIPs.Contains(ip)))
// MessageBox.Show("If using Gateway security, secure key exchange may not succeed." + Environment.NewLine + "Please make sure to run manager application with administrative privileges on the server where service is hosted.", "Subscription Request", MessageBoxButton.OK, MessageBoxImage.Warning);
}
catch
{
MessageBox.Show("Please make sure to run manager application with administrative privileges on the server where service is hosted.", "Subscription Request", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
}
private void BrowseLocalCertificateFile()
{
OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.FileName = LocalCertificateFile;
fileDialog.DefaultExt = ".cer";
fileDialog.Filter = "Certificate files|*.cer|All Files|*.*";
fileDialog.CheckFileExists = false;
fileDialog.CheckPathExists = false;
if (fileDialog.ShowDialog() == true)
LocalCertificateFile = fileDialog.FileName;
}
private void BrowseRemoteCertificateFile()
{
OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.FileName = RemoteCertificateFile;
fileDialog.DefaultExt = ".cer";
fileDialog.Filter = "Certificate files|*.cer|All Files|*.*";
fileDialog.CheckFileExists = false;
fileDialog.CheckPathExists = false;
if (fileDialog.ShowDialog() == true)
RemoteCertificateFile = fileDialog.FileName;
}
private void ImportCertificateFile()
{
OpenFileDialog fileDialog = new OpenFileDialog();
fileDialog.FileName = RemoteCertificateFile;
fileDialog.DefaultExt = ".cer";
fileDialog.Filter = "Certificate files|*.cer|All Files|*.*";
fileDialog.CheckFileExists = false;
fileDialog.CheckPathExists = false;
if (fileDialog.ShowDialog() == true && File.Exists(fileDialog.FileName))
{
ImportCertificatePath = fileDialog.FileName;
m_remoteCertificateData = File.ReadAllBytes(m_importCertificatePath);
}
}
private void CreateAuthenticationRequest()
{
try
{
ExportAuthorizationRequest();
}
catch (Exception ex)
{
if (ex.InnerException != null)
{
Popup(ex.Message + Environment.NewLine + "Inner Exception: " + ex.InnerException.Message, "Subscription Request Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Subscription Request", ex.InnerException);
}
else
{
Popup(ex.Message, "Subscription Request Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Subscription Request", ex);
}
}
}
// Export the authorization request.
private void ExportAuthorizationRequest()
{
const string MessageFormat = "Data subscription adapter \"{0}\" already exists. Unable to create subscription request.";
Device device;
if (!string.IsNullOrWhiteSpace(PublisherAcronym))
{
// Check if the device already exists
device = GetDeviceByAcronym(PublisherAcronym.Replace(" ", ""));
if ((object)device != null)
throw new Exception(string.Format(MessageFormat, device.Acronym));
// Save the associated device
if (TryCreateRequest())
SaveDevice();
}
else
{
MessageBox.Show("Acronym is a required field. Please provide value.");
}
}
private bool TryCreateRequest()
{
try
{
// Generate authorization request
SaveFileDialog saveFileDialog;
WindowsServiceClient serviceClient;
ClientRequest clientRequest;
AuthenticationRequest request;
string[] keyIV;
saveFileDialog = new SaveFileDialog();
saveFileDialog.DefaultExt = ".srq";
saveFileDialog.Filter = "Subscription Requests|*.srq|All Files|*.*";
if (saveFileDialog.ShowDialog() == true)
{
request = new AuthenticationRequest();
// Set up the request
request.Acronym = SubscriberAcronym;
request.Name = SubscriberName;
request.ValidIPAddresses = ValidIPAddresses;
// Cipher key only applies to Gateway security
if (SecurityMode == SecurityMode.Gateway)
{
// Export cipher key to common crypto cache
if (!ExportCipherKey(SharedKey, 256))
throw new Exception("Failed to export cipher keys from common key cache.");
// Reload local crypto cache and get key and IV
// that go into the authentication request
Cipher.ReloadCache();
keyIV = Cipher.ExportKeyIV(SharedKey, 256).Split('|');
// Set up crypto settings in the request
request.SharedSecret = SharedKey;
request.AuthenticationID = IdentityCertificate;
request.Key = keyIV[0];
request.IV = keyIV[1];
}
// Local certificate only applies to TLS security
if (SecurityMode == SecurityMode.TLS)
{
if (File.Exists(m_localCertificateFile))
{
request.CertificateFile = File.ReadAllBytes(m_localCertificateFile);
}
else
{
try
{
serviceClient = CommonFunctions.GetWindowsServiceClient();
serviceClient.Helper.ReceivedServiceResponse += Helper_ReceivedServiceResponse;
serviceClient.Helper.SendRequest("INVOKE TLS!DATAPUBLISHER GetLocalCertificate");
// Wait for command response allowing for processing time
if ((object)m_responseComplete != null)
{
if (!m_responseComplete.WaitOne(5000))
throw new TimeoutException("Timed-out after 5 seconds waiting for service response.");
}
request.CertificateFile = m_localCertificateData;
}
catch (Exception ex)
{
string message = $"Unable to get the local certificate used by the service: {ex.Message}";
throw new InvalidOperationException(message, ex);
}
}
if ((object)m_remoteCertificateData != null)
{
serviceClient = CommonFunctions.GetWindowsServiceClient();
serviceClient.Helper.ReceivedServiceResponse += Helper_ReceivedServiceResponse;
clientRequest = new ClientRequest("INVOKE");
clientRequest.Arguments = new Arguments($"TLS!DATAPUBLISHER ImportCertificate {m_remoteCertificateFile}");
clientRequest.Attachments.Add(m_remoteCertificateData);
serviceClient.Helper.SendRequest(clientRequest);
if ((object)m_responseComplete != null)
{
if (!m_responseComplete.WaitOne(5000))
throw new InvalidOperationException("Timeout waiting for response to ImportCertificate command.");
}
m_remoteCertificateData = null;
}
}
// Create the request
using (FileStream requestStream = File.OpenWrite(saveFileDialog.FileName))
{
Serialization.Serialize(request, SerializationFormat.Binary, requestStream);
}
// Send ReloadCryptoCache to service
if (SecurityMode == SecurityMode.Gateway)
ReloadServiceCryptoCache();
return true;
}
}
catch (Exception ex)
{
string message = $"Error creating authorization request: {ex.Message}";
Popup(message, "Subscription Request Error", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Subscription Request", ex);
}
return false;
}
// Gets the device from the database with the given acronym for the currently selected node.
private Device GetDeviceByAcronym(string acronym)
{
AdoDataConnection database = null;
string nodeID;
try
{
database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory);
nodeID = database.CurrentNodeID().ToString();
return Device.GetDevice(database, $" WHERE NodeID = '{nodeID}' AND Acronym = '{acronym}'");
}
finally
{
database?.Dispose();
}
}
// Attempt to get the company acronym stored in the the configuration file.
private bool TryGetCompanyAcronym(out string acronym)
{
XDocument document;
// Initial value if acronym
// never gets set explicitly
acronym = null;
try
{
// Check all application config files for company info
foreach (string configFilePath in FilePath.GetFileList(FilePath.GetAbsolutePath("*.exe.config")))
{
try
{
// Load the configuration file and search for CompanyAcronym
document = XDocument.Load(configFilePath);
if ((object)document.Root != null)
{
acronym = document.Root
.Descendants("systemSettings")
.Elements("add")
.Where(e => (string)e.Attribute("name") == "CompanyAcronym")
.Select(e => (string)e.Attribute("value"))
.SingleOrDefault();
}
}
catch
{
// Ignore exceptions here - simply check the next config file
continue;
}
if ((object)acronym != null)
break;
}
// Indicate success or failure
return ((object)acronym != null);
}
catch
{
// Company info retrieval failed
return false;
}
}
// Exports the given cipher key from the common key cache.
private bool ExportCipherKey(string password, int keySize)
{
ProcessStartInfo configCrypterInfo = new ProcessStartInfo();
Process configCrypter;
configCrypterInfo.FileName = FilePath.GetAbsolutePath("ConfigCrypter.exe");
configCrypterInfo.Arguments = $"-password {password} -keySize {keySize}";
configCrypterInfo.CreateNoWindow = true;
configCrypter = Process.Start(configCrypterInfo);
configCrypter?.WaitForExit();
return configCrypter?.ExitCode == 0;
}
// Send service command to reload crypto cache.
private void ReloadServiceCryptoCache()
{
try
{
CommonFunctions.SendCommandToService("ReloadCryptoCache");
}
catch (Exception ex)
{
string message = "Unable to notify service about updated crypto cache:" + Environment.NewLine;
if (ex.InnerException != null)
{
message += ex.Message + Environment.NewLine;
message += "Inner Exception: " + ex.InnerException.Message;
Popup(message, "Subscription Request Exception:", MessageBoxImage.Information);
CommonFunctions.LogException(null, "Subscription Request", ex.InnerException);
}
else
{
message += ex.Message;
Popup(message, "Subscription Request Exception:", MessageBoxImage.Information);
CommonFunctions.LogException(null, "Subscription Request", ex);
}
}
}
private void Save()
{
try
{
SaveDevice();
}
catch (Exception ex)
{
if (ex.InnerException != null)
{
Popup(ex.Message + Environment.NewLine + "Inner Exception: " + ex.InnerException.Message, "Internal Subscription Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Internal Subscription", ex.InnerException);
}
else
{
Popup(ex.Message, "Internal Subscription Exception:", MessageBoxImage.Error);
CommonFunctions.LogException(null, "Internal Subscription", ex);
}
}
}
// Associate the given device with the authorization request and save it.
private void SaveDevice()
{
string securitySpecificSettings = "";
string dataGapRecoverySettings = "";
if (string.IsNullOrWhiteSpace(PublisherAcronym))
throw new NoNullAllowedException("Acronym is a required field");
if (UseGEP && string.IsNullOrWhiteSpace(Hostname))
throw new NoNullAllowedException("Hostname is required when using GEP. Only STTP supports reverse server-based data subscription connection mode.");
switch (SecurityMode)
{
case SecurityMode.None:
securitySpecificSettings = $"internal={!(ReceiveExternalMetadata && !ReceiveInternalMetadata)}; receiveInternalMetadata={ReceiveInternalMetadata}; receiveExternalMetadata={ReceiveExternalMetadata}; outputMeasurements={{FILTER ActiveMeasurements WHERE Protocol = '{(UseSTTP ? "STTP" : "GatewayTransport")}'}}";
break;
case SecurityMode.Gateway:
securitySpecificSettings = $"sharedSecret={SharedKey}; authenticationID={{{IdentityCertificate}}}";
break;
case SecurityMode.TLS:
{
if (!Enum.TryParse(ValidPolicyErrors, out SslPolicyErrors validPolicyErrors))
validPolicyErrors = SslPolicyErrors.None;
if (!Enum.TryParse(ValidChainFlags, out X509ChainStatusFlags validChainFlags))
validChainFlags = X509ChainStatusFlags.NoError;
if (RemoteCertificateIsSelfSigned)
{
validPolicyErrors |= SslPolicyErrors.RemoteCertificateChainErrors;
validChainFlags |= X509ChainStatusFlags.UntrustedRoot;
}
securitySpecificSettings = $"{GetLocalCertificateSetting()}remoteCertificate={RemoteCertificateFile}; validPolicyErrors={validPolicyErrors}; validChainFlags={validChainFlags}; checkCertificateRevocation={!m_isRemoteCertificateSelfSigned}";
break;
}
}
if (m_enableDataGapRecovery)
dataGapRecoverySettings = $"; dataGapRecovery={{enabled=true; recoveryStartDelay={RecoveryStartDelay}; dataMonitoringInterval={DataMonitoringInterval}; minimumRecoverySpan={MinimumRecoverySpan}; maximumRecoverySpan={MaximumRecoverySpan * Time.SecondsPerDay}; recoveryProcessingInterval={RecoveryProcessingInterval}}}{(string.IsNullOrWhiteSpace(LoggingPath) ? "" : "; loggingPath=" + LoggingPath)}";
Device device = new Device
{
Acronym = PublisherAcronym.Replace(" ", ""),
Name = PublisherName,
Enabled = m_securityMode == SecurityMode.None,
IsConcentrator = true,
ProtocolID = UseSTTP ? GetSTTPProtocolID() : GetGEPProtocolID()
};
IList historianIDs = Historian.LoadKeys(null);
IList historians = Historian.Load(null, historianIDs);
int? historianID = historians
.Where(historian => historian.Acronym != "STAT")
.Select(historian => (int?)historian.ID)
.FirstOrDefault();
if ((object)historianID != null)
device.HistorianID = historianID.GetValueOrDefault();
string commandChannelConnectionString = string.IsNullOrWhiteSpace(Hostname) ? $"port={PublisherPort}" : $"server={Hostname}:{PublisherPort}";
string useSourcePrefixNames = UseSourcePrefix.ToString().ToLower();
if (UseUdpDataChannel)
device.ConnectionString = $"interface=0.0.0.0; useSourcePrefixNames={useSourcePrefixNames}; compression=true; autoConnect=true; securityMode={SecurityMode}; localPort={UdpDataChannelPort}; transportProtocol=UDP; commandChannel={{{commandChannelConnectionString}; interface=0.0.0.0}}; {securitySpecificSettings}{dataGapRecoverySettings}";
else
device.ConnectionString = $"interface=0.0.0.0; useSourcePrefixNames={useSourcePrefixNames}; compression=true; autoConnect=true; securityMode={SecurityMode}; {commandChannelConnectionString}; {securitySpecificSettings}{dataGapRecoverySettings}";
try
{
Device.SaveWithAnalogsDigitals(null, device, false, 0, 0);
}
catch (ApplicationException ex)
{
CommonFunctions.LogException(null, "Save Subscriber Device", ex);
}
device = Device.GetDevice(null, "WHERE Acronym = '" + device.Acronym + "'");
CommonFunctions.LoadUserControl("Manage Device Configuration", typeof(DeviceUserControl), device);
}
// Get the setting for the local certificate path
private string GetLocalCertificateSetting()
{
if (string.IsNullOrEmpty(LocalCertificateServerPath))
return string.Empty;
return $"localCertificate={LocalCertificateServerPath}; ";
}
// Get the STTP protocol ID by querying the database.
private int? GetSTTPProtocolID()
{
const string Query = "SELECT ID FROM Protocol WHERE Acronym = 'STTP'";
AdoDataConnection database = null;
object queryResult;
try
{
database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory);
queryResult = database.Connection.ExecuteScalar(Query);
return (queryResult != null) ? Convert.ToInt32(queryResult) : 8;
}
finally
{
database?.Dispose();
}
}
// Get the GEP protocol ID by querying the database.
private int? GetGEPProtocolID()
{
const string Query = "SELECT ID FROM Protocol WHERE Acronym = 'GatewayTransport'";
AdoDataConnection database = null;
object queryResult;
try
{
database = new AdoDataConnection(CommonFunctions.DefaultSettingsCategory);
queryResult = database.Connection.ExecuteScalar(Query);
return (queryResult != null) ? Convert.ToInt32(queryResult) : 8;
}
finally
{
database?.Dispose();
}
}
///
/// Handles ReceivedServiceResponse event.
///
/// Source of the event.
/// Event arguments.
private void Helper_ReceivedServiceResponse(object sender, EventArgs e)
{
if ((object)e != null)
{
ServiceResponse response = e.Argument;
if ((object)response != null)
{
if (ClientHelper.TryParseActionableResponse(response, out string sourceCommand, out bool responseSuccess) && responseSuccess)
{
if (!string.IsNullOrWhiteSpace(sourceCommand) && string.Compare(sourceCommand.Trim(), "INVOKE", StringComparison.OrdinalIgnoreCase) == 0)
{
List