//****************************************************************************************************** // 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 attachments = response.Attachments; // A GetHighestSeverityAlarms INVOKE will have two attachments: an alarm array, item 0, and the original command arguments, item 1 if ((object)attachments != null && attachments.Count > 1) { Arguments arguments = attachments[1] as Arguments; // Check the method that was invoked - the second argument after the adapter ID if ((object)arguments != null && string.Compare(arguments["OrderedArg2"], "GetLocalCertificate", StringComparison.OrdinalIgnoreCase) == 0) { m_localCertificateData = attachments[0] as byte[]; // Release waiting thread once desired response has been received if ((object)m_responseComplete != null) m_responseComplete.Set(); } else if ((object)arguments == null || string.Compare(arguments["OrderedArg2"], "ImportCertificate", StringComparison.OrdinalIgnoreCase) != 0) { m_remoteCertificateFile = attachments[0] as string; // Release waiting thread once desired response has been received if ((object)m_responseComplete != null) m_responseComplete.Set(); } } } } } } } #endregion } }