/* ======================================================================== * Copyright (c) 2005-2020 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Drawing; using System.Threading.Tasks; using System.Windows.Forms; using Opc.Ua.Client.ComplexTypes; namespace Opc.Ua.Client.Controls { /// /// A tool bar used to connect to a server. /// public partial class ConnectServerCtrl : UserControl { #region Constructors /// /// Initializes the object. /// public ConnectServerCtrl() { InitializeComponent(); m_CertificateValidation = new CertificateValidationEventHandler(CertificateValidator_CertificateValidation); m_endpoints = new Dictionary(); } #endregion #region Private Fields private ApplicationConfiguration m_configuration; private Session m_session; private SessionReconnectHandler m_reconnectHandler; private CertificateValidationEventHandler m_CertificateValidation; private EventHandler m_ReconnectComplete; private EventHandler m_ReconnectStarting; private EventHandler m_KeepAliveComplete; private EventHandler m_ConnectComplete; private StatusStrip m_StatusStrip; private ToolStripItem m_ServerStatusLB; private ToolStripItem m_StatusUpateTimeLB; private Dictionary m_endpoints; #endregion #region Public Members /// /// Default session values. /// public static readonly uint DefaultSessionTimeout = 60000; public static readonly int DefaultDiscoverTimeout = 15000; public static readonly int DefaultReconnectPeriod = 10; /// /// A strip used to display session status information. /// public StatusStrip StatusStrip { get => m_StatusStrip; set { if (!Object.ReferenceEquals(m_StatusStrip, value)) { m_StatusStrip = value; if (value != null) { m_ServerStatusLB = new ToolStripStatusLabel(); m_StatusUpateTimeLB = new ToolStripStatusLabel(); m_StatusStrip.Items.Add(m_ServerStatusLB); m_StatusStrip.Items.Add(m_StatusUpateTimeLB); } } } } /// /// A control that contains the last time a keep alive was returned from the server. /// public ToolStripItem ServerStatusControl { get => m_ServerStatusLB; set => m_ServerStatusLB = value; } /// /// A control that contains the last time a keep alive was returned from the server. /// public ToolStripItem StatusUpateTimeControl { get => m_StatusUpateTimeLB; set => m_StatusUpateTimeLB = value; } /// /// The name of the session to create. /// public string SessionName { get; set; } /// /// Gets or sets a flag indicating that the domain checks should be ignored when connecting. /// public bool DisableDomainCheck { get; set; } /// /// Gets the cached EndpointDescription for a Url. /// public EndpointDescription GetEndpointDescription(Uri url) { EndpointDescription endpointDescription; if (m_endpoints.TryGetValue(url, out endpointDescription)) { return endpointDescription; } return null; } /// /// The URL displayed in the control. /// public string ServerUrl { get { if (UrlCB.SelectedIndex >= 0) { return (string)UrlCB.SelectedItem; } return UrlCB.Text; } set { UrlCB.SelectedIndex = -1; UrlCB.Text = value; } } /// /// Whether to use security when connecting. /// public bool UseSecurity { get => UseSecurityCK.Checked; set => UseSecurityCK.Checked = value; } /// /// The locales to use when creating the session. /// public string[] PreferredLocales { get; set; } /// /// The user identity to use when creating the session. /// public IUserIdentity UserIdentity { get; set; } /// /// The client application configuration. /// public ApplicationConfiguration Configuration { get => m_configuration; set { if (!Object.ReferenceEquals(m_configuration, value)) { if (m_configuration != null) { m_configuration.CertificateValidator.CertificateValidation -= m_CertificateValidation; } m_configuration = value; if (m_configuration != null) { m_configuration.CertificateValidator.CertificateValidation += m_CertificateValidation; } } } } /// /// The currently active session. /// public Session Session => m_session; /// /// The number of seconds between reconnect attempts (0 means reconnect is disabled). /// public int ReconnectPeriod { get; set; } = DefaultReconnectPeriod; /// /// The discover timeout. /// public int DiscoverTimeout { get; set; } = DefaultDiscoverTimeout; /// /// The session timeout. /// public uint SessionTimeout { get; set; } = DefaultSessionTimeout; /// /// Raised when a good keep alive from the server arrives. /// public event EventHandler KeepAliveComplete { add { m_KeepAliveComplete += value; } remove { m_KeepAliveComplete -= value; } } /// /// Raised when a reconnect operation starts. /// public event EventHandler ReconnectStarting { add { m_ReconnectStarting += value; } remove { m_ReconnectStarting -= value; } } /// /// Raised when a reconnect operation completes. /// public event EventHandler ReconnectComplete { add { m_ReconnectComplete += value; } remove { m_ReconnectComplete -= value; } } /// /// Raised after successfully connecting to or disconnecing from a server. /// public event EventHandler ConnectComplete { add { m_ConnectComplete += value; } remove { m_ConnectComplete -= value; } } /// /// Sets the URLs shown in the control. /// public void SetAvailableUrls(IList urls) { UrlCB.Items.Clear(); if (urls != null) { foreach (string url in urls) { int index = url.LastIndexOf("/discovery", StringComparison.InvariantCultureIgnoreCase); if (index != -1) { UrlCB.Items.Add(url.Substring(0, index)); continue; } UrlCB.Items.Add(url); } if (UrlCB.Items.Count > 0) { UrlCB.SelectedIndex = 0; } } } /// /// Creates a new session. /// /// The new session object. private async Task Connect( ITransportWaitingConnection connection, EndpointDescription endpointDescription, bool useSecurity, uint sessionTimeout = 0) { // disconnect from existing session. InternalDisconnect(); // select the best endpoint. if (endpointDescription == null) { endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, connection, useSecurity, DiscoverTimeout); } EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration); ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); m_session = await Session.Create( m_configuration, connection, endpoint, false, !DisableDomainCheck, (String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName, sessionTimeout, UserIdentity, PreferredLocales); // set up keep alive callback. m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive); // raise an event. DoConnectComplete(null); try { UpdateStatus(false, DateTime.Now, "Connected, loading complex type system."); var typeSystemLoader = new ComplexTypeSystem(m_session); await typeSystemLoader.Load(); } catch (Exception e) { UpdateStatus(true, DateTime.Now, "Connected, failed to load complex type system."); Utils.Trace(e, "Failed to load complex type system."); } // return the new session. return m_session; } /// /// Creates a new session. /// /// The new session object. public Task Connect() { // determine the URL that was selected. string serverUrl = UrlCB.Text; if (UrlCB.SelectedIndex >= 0) { serverUrl = (string)UrlCB.SelectedItem; } bool useSecurity = UseSecurityCK.Checked; return Connect(serverUrl, useSecurity); } /// /// Creates a new session. /// /// The new session object. private async Task Connect( string serverUrl, bool useSecurity, uint sessionTimeout = 0) { // disconnect from existing session. InternalDisconnect(); // select the best endpoint. var endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, useSecurity, DiscoverTimeout); var endpointConfiguration = EndpointConfiguration.Create(m_configuration); var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); m_session = await Session.Create( m_configuration, endpoint, false, !DisableDomainCheck, (String.IsNullOrEmpty(SessionName)) ? m_configuration.ApplicationName : SessionName, sessionTimeout == 0 ? DefaultSessionTimeout : sessionTimeout, UserIdentity, PreferredLocales); // set up keep alive callback. m_session.KeepAlive += new KeepAliveEventHandler(Session_KeepAlive); // raise an event. DoConnectComplete(null); try { UpdateStatus(false, DateTime.Now, "Connected, loading complex type system."); var typeSystemLoader = new ComplexTypeSystem(m_session); await typeSystemLoader.Load(); } catch (Exception e) { UpdateStatus(true, DateTime.Now, "Connected, failed to load complex type system."); Utils.Trace(e, "Failed to load complex type system."); } // return the new session. return m_session; } /// /// Creates a new session. /// /// The URL of a server endpoint. /// Whether to use security. /// The new session object. public async Task ConnectAsync( string serverUrl = null, bool useSecurity = false, uint sessionTimeout = 0 ) { if (serverUrl == null) { serverUrl = UrlCB.Text; if (UrlCB.SelectedIndex >= 0) { serverUrl = (string)UrlCB.SelectedItem; } useSecurity = UseSecurityCK.Checked; } else { UrlCB.Text = serverUrl; UseSecurityCK.Checked = useSecurity; } return await Task.Run(() => Connect(serverUrl, useSecurity, sessionTimeout)); } /// /// Create a new reverse connection. /// /// /// public async Task ConnectAsync( ITransportWaitingConnection connection, bool useSecurity, int discoverTimeout = -1, uint sessionTimeout = 0 ) { if (connection.EndpointUrl == null) { throw new ArgumentException("Endpoint URL is not valid."); } UrlCB.Text = connection.EndpointUrl.ToString(); UseSecurityCK.Checked = useSecurity; EndpointDescription endpointDescription = null; if (!m_endpoints.TryGetValue(connection.EndpointUrl, out endpointDescription)) { // Discovery uses the reverse connection and closes it // return and wait for next reverse hello endpointDescription = CoreClientUtils.SelectEndpoint(m_configuration, connection, useSecurity, discoverTimeout); m_endpoints[connection.EndpointUrl] = endpointDescription; return null; } return await Connect(connection, endpointDescription, UseSecurityCK.Checked, sessionTimeout); } /// /// Disconnects from the server. /// public Task DisconnectAsync() { UpdateStatus(false, DateTime.UtcNow, "Disconnected"); return Task.Run(() => InternalDisconnect()); } /// /// Disconnects from the server. /// private void InternalDisconnect() { // stop any reconnect operation. if (m_reconnectHandler != null) { m_reconnectHandler.Dispose(); m_reconnectHandler = null; } // disconnect any existing session. if (m_session != null) { m_session.KeepAlive -= Session_KeepAlive; m_session.Close(10000); m_session = null; } // raise an event. DoConnectComplete(null); } /// /// Disconnects from the server. /// public void Disconnect() { UpdateStatus(false, DateTime.UtcNow, "Disconnected"); // stop any reconnect operation. InternalDisconnect(); } /// /// Prompts the user to choose a server on another host. /// public void Discover(string hostName) { string endpointUrl = new DiscoverServerDlg().ShowDialog(m_configuration, hostName); if (endpointUrl != null) { ServerUrl = endpointUrl; } } #endregion #region Private Methods /// /// Raises the connect complete event on the main GUI thread. /// private void DoConnectComplete(object state) { if (m_ConnectComplete != null) { if (this.InvokeRequired) { this.BeginInvoke(new System.Threading.WaitCallback(DoConnectComplete), state); return; } m_ConnectComplete(this, null); } } /// /// Finds the endpoint that best matches the current settings. /// private EndpointDescription SelectEndpoint() { try { Cursor = Cursors.WaitCursor; // determine the URL that was selected. string discoveryUrl = UrlCB.Text; if (UrlCB.SelectedIndex >= 0) { discoveryUrl = (string)UrlCB.SelectedItem; } // return the selected endpoint. return CoreClientUtils.SelectEndpoint(discoveryUrl, UseSecurityCK.Checked, DiscoverTimeout); } finally { Cursor = Cursors.Default; } } #endregion #region Event Handlers private delegate void UpdateStatusCallback(bool error, DateTime time, string status, params object[] arg); /// /// Updates the status control. /// /// Whether the status represents an error. /// The time associated with the status. /// The status message. /// Arguments used to format the status message. private void UpdateStatus(bool error, DateTime time, string status, params object[] args) { if (this.InvokeRequired) { this.BeginInvoke(new UpdateStatusCallback(UpdateStatus), error, time, status, args); return; } if (m_ServerStatusLB != null) { m_ServerStatusLB.Text = String.Format(status, args); m_ServerStatusLB.ForeColor = (error) ? Color.Red : Color.Empty; } if (m_StatusUpateTimeLB != null) { m_StatusUpateTimeLB.Text = time.ToLocalTime().ToString("hh:mm:ss"); m_StatusUpateTimeLB.ForeColor = (error) ? Color.Red : Color.Empty; } } /// /// Handles a keep alive event from a session. /// private void Session_KeepAlive(Session session, KeepAliveEventArgs e) { if (this.InvokeRequired) { this.BeginInvoke(new KeepAliveEventHandler(Session_KeepAlive), session, e); return; } try { // check for events from discarded sessions. if (!Object.ReferenceEquals(session, m_session)) { return; } // start reconnect sequence on communication error. if (ServiceResult.IsBad(e.Status)) { if (ReconnectPeriod <= 0) { UpdateStatus(true, e.CurrentTime, "Communication Error ({0})", e.Status); return; } UpdateStatus(true, e.CurrentTime, "Reconnecting in {0}s", ReconnectPeriod); if (m_reconnectHandler == null) { if (m_ReconnectStarting != null) { m_ReconnectStarting(this, e); } m_reconnectHandler = new SessionReconnectHandler(); m_reconnectHandler.BeginReconnect(m_session, ReconnectPeriod * 1000, Server_ReconnectComplete); } return; } // update status. UpdateStatus(false, e.CurrentTime, "Connected [{0}]", session.Endpoint.EndpointUrl); // raise any additional notifications. if (m_KeepAliveComplete != null) { m_KeepAliveComplete(this, e); } } catch (Exception exception) { ClientUtils.HandleException(this.Text, exception); } } /// /// Handles a click on the connect button. /// private async void Server_ConnectMI_Click(object sender, EventArgs e) { try { await ConnectAsync(); } catch (ServiceResultException sre) { if (sre.StatusCode == StatusCodes.BadCertificateHostNameInvalid) { if (GuiUtils.HandleDomainCheckError(this.FindForm().Text, sre.Result)) { DisableDomainCheck = true; }; } } catch (Exception exception) { ClientUtils.HandleException(this.Text, exception); } } /// /// Handles a reconnect event complete from the reconnect handler. /// private void Server_ReconnectComplete(object sender, EventArgs e) { if (this.InvokeRequired) { this.BeginInvoke(new EventHandler(Server_ReconnectComplete), sender, e); return; } try { // ignore callbacks from discarded objects. if (!Object.ReferenceEquals(sender, m_reconnectHandler)) { return; } m_session = m_reconnectHandler.Session; m_reconnectHandler.Dispose(); m_reconnectHandler = null; // raise any additional notifications. if (m_ReconnectComplete != null) { m_ReconnectComplete(this, e); } } catch (Exception exception) { ClientUtils.HandleException(this.Text, exception); } } /// /// Handles a certificate validation error. /// private void CertificateValidator_CertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e) { if (this.InvokeRequired) { this.Invoke(new CertificateValidationEventHandler(CertificateValidator_CertificateValidation), sender, e); return; } try { if (!m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates) { GuiUtils.HandleCertificateValidationError(this.FindForm(), sender, e); } else { e.Accept = true; } } catch (Exception exception) { ClientUtils.HandleException(this.Text, exception); } } #endregion } }