diff --git a/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs b/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs index ee3430d5ee..575e02fe73 100644 --- a/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs +++ b/SampleApplications/SDK/Opc.Ua.Server/Configuration/ConfigurationNodeManager.cs @@ -314,7 +314,7 @@ namespace Opc.Ua.Server // TODO: role based access UserIdentity user = context.UserIdentity as UserIdentity; - if (user.TokenType == UserTokenType.Anonymous) + if (user?.TokenType == UserTokenType.Anonymous) { throw new ServiceResultException(StatusCodes.BadUserAccessDenied, "Secure Application Administrator access required."); } @@ -352,6 +352,7 @@ namespace Opc.Ua.Server } #endregion + #region Private Fields private class ServerCertificateGroup { public string BrowseName; @@ -363,7 +364,6 @@ namespace Opc.Ua.Server public string TrustedStorePath; }; - #region Private Fields private ServerConfigurationState m_serverConfigurationNode; private IList m_certificateGroups; public readonly string m_rejectedStorePath; diff --git a/SampleApplications/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs b/SampleApplications/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs index fe3721e428..e965846f17 100644 --- a/SampleApplications/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs +++ b/SampleApplications/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs @@ -174,10 +174,12 @@ namespace Opc.Ua.Gds.Client NodeId requestId = null; if (!string.IsNullOrEmpty(m_application.CertificateStorePath)) { - CertificateIdentifier id = new CertificateIdentifier(); - id.StoreType = CertificateStoreIdentifier.DetermineStoreType(m_application.CertificateStorePath); - id.StorePath = m_application.CertificateStorePath; - id.SubjectName = m_application.CertificateSubjectName.Replace("localhost", Utils.GetHostName()); + CertificateIdentifier id = new CertificateIdentifier + { + StoreType = CertificateStoreIdentifier.DetermineStoreType(m_application.CertificateStorePath), + StorePath = m_application.CertificateStorePath, + SubjectName = m_application.CertificateSubjectName.Replace("localhost", Utils.GetHostName()) + }; m_certificate = await id.Find(true); if (m_certificate != null && m_certificate.HasPrivateKey) @@ -216,7 +218,7 @@ namespace Opc.Ua.Gds.Client else { string absoluteCertificatePrivateKeyPath = Utils.GetAbsoluteFilePath(m_application.CertificatePrivateKeyPath, true, false, false); - byte [] pkcsData = File.ReadAllBytes(absoluteCertificatePrivateKeyPath); + byte[] pkcsData = File.ReadAllBytes(absoluteCertificatePrivateKeyPath); if (m_application.GetPrivateKeyFormat(m_server?.GetSupportedKeyFormats()) == "PFX") { csrCertificate = CertificateFactory.CreateCertificateFromPKCS12(pkcsData, m_certificatePassword); @@ -261,7 +263,7 @@ namespace Opc.Ua.Gds.Client // request not done yet, try again in a few seconds return; } - + CertificateRequestTimer.Enabled = false; RequestProgressLabel.Visible = false; @@ -400,14 +402,13 @@ namespace Opc.Ua.Gds.Client } else { -#if TODO_SERVERPUSH if (privateKeyPFX != null && privateKeyPFX.Length > 0) { var x509 = new X509Certificate2(privateKeyPFX, m_certificatePassword, X509KeyStorageFlags.Exportable); privateKeyPFX = x509.Export(X509ContentType.Pfx); } - bool applyChanges = m_server.UpdateCertificate(null, null, certificate, GetPrivateKeyFormat(), privateKeyPFX, issuerCertificates); - + bool applyChanges = m_server.UpdateCertificate(null, null, certificate, (privateKeyPFX != null) ? "PFX" : null, privateKeyPFX, issuerCertificates); + if (applyChanges) { MessageBox.Show( @@ -419,9 +420,6 @@ namespace Opc.Ua.Gds.Client ApplyChangesButton.Enabled = true; } -#else - throw new ServiceResultException("Server Push is not yet implemented."); -#endif } CertificateControl.ShowValue(null, "Application Certificate", new CertificateWrapper() { Certificate = m_certificate }, true); @@ -439,7 +437,7 @@ namespace Opc.Ua.Gds.Client CertificateRequestTimer.Enabled = false; } } - + private void ApplyChangesButton_Click(object sender, EventArgs e) { try diff --git a/SampleApplications/Samples/GDS/Client/Controls/RegisterApplicationControl.cs b/SampleApplications/Samples/GDS/Client/Controls/RegisterApplicationControl.cs index 232ac06e83..dbef59c4e2 100644 --- a/SampleApplications/Samples/GDS/Client/Controls/RegisterApplicationControl.cs +++ b/SampleApplications/Samples/GDS/Client/Controls/RegisterApplicationControl.cs @@ -53,9 +53,8 @@ namespace Opc.Ua.Gds.Client RegistrationTypeComboBox.Items.Add("Client - Pull Management"); RegistrationTypeComboBox.Items.Add("Server - Pull Management"); -#if TODO_SERVERPUSH RegistrationTypeComboBox.Items.Add("Server - Push Management"); -#endif + RegistrationTypeComboBox.SelectedIndex = ServerPullManagement; m_promptOnRegistrationTypeChange = false; diff --git a/SampleApplications/Samples/GDS/Client/MainForm.cs b/SampleApplications/Samples/GDS/Client/MainForm.cs index f8d5212617..740a05f35f 100644 --- a/SampleApplications/Samples/GDS/Client/MainForm.cs +++ b/SampleApplications/Samples/GDS/Client/MainForm.cs @@ -408,7 +408,6 @@ namespace Opc.Ua.Gds.Client private void UpdateStatus(bool error, DateTime time, string status, params object[] args) { -#if TODO_SERVERPUSH if (error) { ServerStatusIcon.Image = global::Opc.Ua.Gds.Client.Properties.Resources.error; @@ -422,11 +421,6 @@ namespace Opc.Ua.Gds.Client ServerStatusLabel.ForeColor = (error) ? Color.Red : Color.Empty; ServerStatusTime.Text = (time != DateTime.MinValue) ? time.ToLocalTime().ToString("hh:mm:ss") : "---"; ServerStatusTime.ForeColor = (error) ? Color.Red : Color.Empty; -#else - ServerStatusTime.Visible = false; - ServerStatusLabel.Visible = false; - ServerStatusIcon.Visible = false; -#endif } private void DisconnectButton_Click(object sender, EventArgs e) diff --git a/SampleApplications/Samples/GDS/ClientCommon/GlobalDiscoveryServerClient.cs b/SampleApplications/Samples/GDS/ClientCommon/GlobalDiscoveryServerClient.cs index 61ac7c894a..3fc20badc0 100644 --- a/SampleApplications/Samples/GDS/ClientCommon/GlobalDiscoveryServerClient.cs +++ b/SampleApplications/Samples/GDS/ClientCommon/GlobalDiscoveryServerClient.cs @@ -624,7 +624,7 @@ namespace Opc.Ua.Gds.Client } /// - /// Gets the trust lists method. + /// Gets the certificate status. /// /// The application id. /// Type of the trust list. diff --git a/SampleApplications/Samples/GDS/ClientCommon/ServerPushConfigurationClient.cs b/SampleApplications/Samples/GDS/ClientCommon/ServerPushConfigurationClient.cs index e61a8520eb..d5d6506b6e 100644 --- a/SampleApplications/Samples/GDS/ClientCommon/ServerPushConfigurationClient.cs +++ b/SampleApplications/Samples/GDS/ClientCommon/ServerPushConfigurationClient.cs @@ -32,6 +32,7 @@ using System; using System.IO; using Opc.Ua.Client; using Opc.Ua.Configuration; +using System.Security.Cryptography.X509Certificates; namespace Opc.Ua.Gds.Client { @@ -75,6 +76,18 @@ namespace Opc.Ua.Gds.Client set { m_adminCredentials = value; } } + /// + /// Gets or sets the endpoint URL. + /// + /// + /// The endpoint URL. + /// + public string EndpointUrl + { + get { return m_endpointUrl; } + set { m_endpointUrl = value; } + } + /// /// Raised when admin credentials are required. /// @@ -170,7 +183,7 @@ namespace Opc.Ua.Gds.Client /// public void Connect() { - Connect((ConfiguredEndpoint)null); + Connect(EndpointUrl); } /// @@ -311,8 +324,6 @@ namespace Opc.Ua.Gds.Client /// /// Gets the supported key formats. /// - /// The public key formats. - /// The private key formats. /// Connection to server is not active. public string[] GetSupportedKeyFormats() { @@ -429,6 +440,7 @@ namespace Opc.Ua.Gds.Client } } + /// /// Updates the trust list. /// @@ -585,6 +597,40 @@ namespace Opc.Ua.Gds.Client } } + /// + /// Reads the rejected list. + /// + public X509Certificate2Collection GetRejectedList() + { + if (!IsConnected) + { + Connect(); + } + + IUserIdentity oldUser = ElevatePermissions(); + + try + { + var outputArguments = m_session.Call( + ExpandedNodeId.ToNodeId(Opc.Ua.ObjectIds.ServerConfiguration, m_session.NamespaceUris), + ExpandedNodeId.ToNodeId(Opc.Ua.MethodIds.ServerConfiguration_GetRejectedList, m_session.NamespaceUris) + ); + + byte[][] rawCertificates = (byte[][])outputArguments[0]; + X509Certificate2Collection collection = new X509Certificate2Collection(); + foreach (var rawCertificate in rawCertificates) + { + collection.Add(new X509Certificate(rawCertificate)); + } + return collection; + } + finally + { + RevertPermissions(oldUser); + } + } + + /// /// Restarts this instance. /// @@ -713,6 +759,7 @@ namespace Opc.Ua.Gds.Client private ApplicationInstance m_application; private ConfiguredEndpoint m_endpoint; private string[] m_preferredLocales; + private string m_endpointUrl; private Session m_session; private UserIdentity m_adminCredentials; #endregion diff --git a/SampleApplications/Samples/GDS/ClientTest/Client.cs b/SampleApplications/Samples/GDS/ClientTest/Client.cs index 8cc076c9c8..9fccd0de78 100644 --- a/SampleApplications/Samples/GDS/ClientTest/Client.cs +++ b/SampleApplications/Samples/GDS/ClientTest/Client.cs @@ -38,17 +38,20 @@ namespace Opc.Ua.Gds.Test public class GlobalDiscoveryTestClient { - GlobalDiscoveryServerClient m_gds; + GlobalDiscoveryServerClient m_gdsClient; + ServerPushConfigurationClient m_pushClient; + static bool autoAccept = false; - public GlobalDiscoveryServerClient GDSClient { get { return m_gds; } } + public GlobalDiscoveryServerClient GDSClient { get { return m_gdsClient; } } + public ServerPushConfigurationClient PushClient { get { return m_pushClient; } } public GlobalDiscoveryTestClient(bool _autoAccept) { autoAccept = _autoAccept; } - public async Task ConnectClient() + public async Task ConnectClient(bool gdsClient = true) { ApplicationInstance.MessageDlg = new ApplicationMessageDlg(); ApplicationInstance application = new ApplicationInstance @@ -70,25 +73,42 @@ namespace Opc.Ua.Gds.Test config.CertificateValidator.CertificateValidation += new CertificateValidationEventHandler(CertificateValidator_CertificateValidation); - // get the configuration. + // use same server for gds and push tests GlobalDiscoveryClientConfiguration gdsClientConfiguration = application.ApplicationConfiguration.ParseExtension(); - - m_gds = new GlobalDiscoveryServerClient(application, gdsClientConfiguration); - + if (gdsClient) + { + // get the configuration. + m_gdsClient = new GlobalDiscoveryServerClient(application, gdsClientConfiguration); + m_gdsClient.EndpointUrl = gdsClientConfiguration.GlobalDiscoveryServerUrl; + } + else + { + m_pushClient = new ServerPushConfigurationClient(application); + m_pushClient.EndpointUrl = gdsClientConfiguration.GlobalDiscoveryServerUrl; + } } public void DisconnectClient() - { - if (m_gds != null) + { + Console.WriteLine("Disconnect Session. Waiting for exit..."); + + if (m_gdsClient != null) { - Console.WriteLine("Disconnect Session. Waiting for exit..."); - GlobalDiscoveryServerClient gds = m_gds; - m_gds = null; - gds.Disconnect(); + GlobalDiscoveryServerClient gdsClient = m_gdsClient; + m_gdsClient = null; + gdsClient.Disconnect(); + } + + if (m_pushClient != null) + { + ServerPushConfigurationClient pushClient = m_pushClient; + m_pushClient = null; + pushClient.Disconnect(); } } + private static void CertificateValidator_CertificateValidation(CertificateValidator validator, CertificateValidationEventArgs e) { if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted) diff --git a/SampleApplications/Samples/GDS/ClientTest/PushTest.cs b/SampleApplications/Samples/GDS/ClientTest/PushTest.cs new file mode 100644 index 0000000000..f90a900918 --- /dev/null +++ b/SampleApplications/Samples/GDS/ClientTest/PushTest.cs @@ -0,0 +1,186 @@ +/* ======================================================================== + * Copyright (c) 2005-2017 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.Text.RegularExpressions; +using System.Threading; +using NUnit.Framework; +using Opc.Ua; +using Opc.Ua.Gds; +using Opc.Ua.Gds.Client; +using Opc.Ua.Gds.Test; +using Opc.Ua.Test; +using System.Security.Cryptography.X509Certificates; + +namespace NUnit.Opc.Ua.Gds.Test +{ + + [TestFixture] + public class PushTest + { + #region Test Setup + /// + /// Set up a Global Discovery Server and Client instance and connect the session + /// + [OneTimeSetUp] + protected void OneTimeSetUp() + { + _serverCapabilities = new ServerCapabilities(); + _randomSource = new RandomSource(randomStart); + _dataGenerator = new DataGenerator(_randomSource); + _server = new GlobalDiscoveryTestServer(true); + _server.StartServer().Wait(); + _client = new GlobalDiscoveryTestClient(true); + _client.ConnectClient(false).Wait(); + _client.PushClient.AdminCredentials = new UserIdentity("appadmin", "demo"); + _client.PushClient.Connect(); + } + + /// + /// Tear down the Global Discovery Server and disconnect the Client + /// + [OneTimeTearDown] + protected void OneTimeTearDown() + { + _client.DisconnectClient(); + _server.StopServer(); + } + #endregion + #region Test Methods + [Test, Order(100)] + public void GetSupportedKeyFormats() + { + _client.PushClient.GetSupportedKeyFormats(); + } + + [Test, Order(200)] + public void ReadTrustList() + { + TrustListDataType trustList = _client.PushClient.ReadTrustList(); + Assert.IsNotNull(trustList); + Assert.IsNotNull(trustList.IssuerCertificates); + Assert.IsNotNull(trustList.IssuerCrls); + Assert.IsNotNull(trustList.TrustedCertificates); + Assert.IsNotNull(trustList.TrustedCrls); + } + + [Test, Order(300)] + public void UpdateTrustList() + { + TrustListDataType trustList = _client.PushClient.ReadTrustList(); + Assert.IsTrue(_client.PushClient.UpdateTrustList(trustList)); + } + + + + [Test, Order(400)] + public void CreateCertificateRequest() + { + byte [] csr = _client.PushClient.CreateCertificateRequest(null,null,null,false,null); + Assert.IsNotNull(csr); + } + + [Test, Order(500)] + public void UpdateCertificate() + { + X509Certificate2 cert = CertificateFactory.CreateCertificate(null, null, null, "uri:x:y:z", "TestApp", "CN=Push Server Test", null, 2048, DateTime.UtcNow, 1, 256); + var success = _client.PushClient.UpdateCertificate(null, null, cert.RawData, null, null, null); + } + + [Test, Order(500)] + public void GetRejectedList() + { + var collection = _client.PushClient.GetRejectedList(); + } + + + [Test, Order(600)] + public void ApplyChanges() + { + _client.PushClient.ApplyChanges(); + } + + #endregion + #region Private Methods + + private string RandomLocalHost() + { + string localhost = Regex.Replace(_dataGenerator.GetRandomSymbol("en").Trim().ToLower(), @"[^\w\d]", ""); + if (localhost.Length >= 12) + { + localhost = localhost.Substring(0, 12); + } + return localhost; + } + + private string[] RandomDomainNames() + { + int count = _randomSource.NextInt32(8) + 1; + var result = new string[count]; + for (int i = 0; i < count; i++) + { + result[i] = RandomLocalHost(); + } + return result; + } + + private StringCollection RandomDiscoveryUrl(StringCollection domainNames, int port, string appUri) + { + var result = new StringCollection(); + foreach (var name in domainNames) + { + int random = _randomSource.NextInt32(7); + if ((result.Count == 0) || (random & 1) == 0) + { + result.Add(String.Format("opc.tcp://{0}:{1}/{2}", name, (port++).ToString(), appUri)); + } + if ((random & 2) == 0) + { + result.Add(String.Format("http://{0}:{1}/{2}", name, (port++).ToString(), appUri)); + } + if ((random & 4) == 0) + { + result.Add(String.Format("https://{0}:{1}/{2}", name, (port++).ToString(), appUri)); + } + } + return result; + } + #endregion + + #region Private Fields + private const int randomStart = 1; + private RandomSource _randomSource; + private DataGenerator _dataGenerator; + private GlobalDiscoveryTestServer _server; + private GlobalDiscoveryTestClient _client; + private ServerCapabilities _serverCapabilities; + #endregion + } +} \ No newline at end of file