/* * Copyright 2016 Kevin Herron * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.digitalpetri.opcua.sdk.client; import java.util.Arrays; import java.util.EnumSet; import java.util.List; import java.util.Random; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import com.digitalpetri.opcua.sdk.client.api.UaSession; import com.digitalpetri.opcua.sdk.client.api.config.OpcUaClientConfig; import com.digitalpetri.opcua.sdk.client.api.identity.UsernameProvider; import com.digitalpetri.opcua.sdk.client.api.nodes.attached.UaVariableNode; import com.digitalpetri.opcua.sdk.client.api.subscriptions.UaMonitoredItem; import com.digitalpetri.opcua.sdk.client.api.subscriptions.UaSubscription; import com.digitalpetri.opcua.sdk.client.api.subscriptions.UaSubscriptionManager.SubscriptionListener; import com.digitalpetri.opcua.sdk.server.OpcUaServer; import com.digitalpetri.opcua.sdk.server.api.config.OpcUaServerConfig; import com.digitalpetri.opcua.sdk.server.identity.UsernameIdentityValidator; import com.digitalpetri.opcua.server.ctt.CttNamespace; import com.digitalpetri.opcua.stack.client.UaTcpStackClient; import com.digitalpetri.opcua.stack.core.AttributeId; import com.digitalpetri.opcua.stack.core.Identifiers; import com.digitalpetri.opcua.stack.core.StatusCodes; import com.digitalpetri.opcua.stack.core.UaException; import com.digitalpetri.opcua.stack.core.UaServiceFaultException; import com.digitalpetri.opcua.stack.core.security.SecurityPolicy; import com.digitalpetri.opcua.stack.core.types.builtin.DataValue; import com.digitalpetri.opcua.stack.core.types.builtin.DateTime; import com.digitalpetri.opcua.stack.core.types.builtin.LocalizedText; import com.digitalpetri.opcua.stack.core.types.builtin.NodeId; import com.digitalpetri.opcua.stack.core.types.builtin.QualifiedName; import com.digitalpetri.opcua.stack.core.types.builtin.StatusCode; import com.digitalpetri.opcua.stack.core.types.builtin.Variant; import com.digitalpetri.opcua.stack.core.types.enumerated.MonitoringMode; import com.digitalpetri.opcua.stack.core.types.enumerated.TimestampsToReturn; import com.digitalpetri.opcua.stack.core.types.structured.EndpointDescription; import com.digitalpetri.opcua.stack.core.types.structured.MonitoredItemCreateRequest; import com.digitalpetri.opcua.stack.core.types.structured.MonitoringParameters; import com.digitalpetri.opcua.stack.core.types.structured.ReadValueId; import com.digitalpetri.opcua.stack.core.types.structured.UserTokenPolicy; import com.digitalpetri.opcua.stack.server.tcp.SocketServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; import static com.digitalpetri.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; import static com.google.common.collect.Lists.newArrayList; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; public class OpcUaClientIT { private final Logger logger = LoggerFactory.getLogger(getClass()); private OpcUaClient client; private OpcUaServer server; @BeforeTest public void startClientAndServer() throws Exception { logger.info("startClientAndServer()"); UsernameIdentityValidator identityValidator = new UsernameIdentityValidator( true, // allow anonymous access challenge -> { String user0 = "user"; String pass0 = "password"; char[] cs = new char[1000]; Arrays.fill(cs, 'a'); String user1 = new String(cs); String pass1 = new String(cs); boolean match0 = user0.equals(challenge.getUsername()) && pass0.equals(challenge.getPassword()); boolean match1 = user1.equals(challenge.getUsername()) && pass1.equals(challenge.getPassword()); return match0 || match1; } ); List<UserTokenPolicy> userTokenPolicies = newArrayList( OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS, OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME ); KeyStoreLoader loader = new KeyStoreLoader().load(); TestCertificateManager certificateManager = new TestCertificateManager( loader.getServerKeyPair(), loader.getServerCertificate() ); TestCertificateValidator certificateValidator = new TestCertificateValidator( loader.getClientCertificate() ); OpcUaServerConfig serverConfig = OpcUaServerConfig.builder() .setApplicationName(LocalizedText.english("digitalpetri opc-ua server")) .setApplicationUri("urn:digitalpetri:opcua:server") .setBindAddresses(newArrayList("localhost")) .setBindPort(12686) .setCertificateManager(certificateManager) .setCertificateValidator(certificateValidator) .setSecurityPolicies(EnumSet.of(SecurityPolicy.None, SecurityPolicy.Basic128Rsa15)) .setProductUri("urn:digitalpetri:opcua:sdk") .setServerName("test-server") .setUserTokenPolicies(userTokenPolicies) .setIdentityValidator(identityValidator) .build(); server = new OpcUaServer(serverConfig); // register a CttNamespace so we have some nodes to play with server.getNamespaceManager().registerAndAdd( CttNamespace.NAMESPACE_URI, idx -> new CttNamespace(server, idx)); server.startup(); EndpointDescription[] endpoints = UaTcpStackClient.getEndpoints("opc.tcp://localhost:12686/test-server").get(); EndpointDescription endpoint = Arrays.stream(endpoints) .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getSecurityPolicyUri())) .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned")); OpcUaClientConfig clientConfig = OpcUaClientConfig.builder() .setApplicationName(LocalizedText.english("digitalpetri opc-ua client")) .setApplicationUri("urn:digitalpetri:opcua:client") .setEndpoint(endpoint) .setRequestTimeout(uint(60000)) .build(); client = new OpcUaClient(clientConfig); } @AfterTest public void stopClientAndServer() { logger.info("stopClientAndServer()"); try { client.disconnect().get(); } catch (InterruptedException | ExecutionException e) { logger.warn("Error disconnecting client.", e); } server.shutdown(); SocketServer.shutdownAll(); } @Test public void testRead() throws Exception { logger.info("testRead()"); UaVariableNode currentTimeNode = client.getAddressSpace() .getVariableNode(Identifiers.Server_ServerStatus_CurrentTime); assertNotNull(currentTimeNode.readValueAttribute().get()); } @Test public void testWrite() throws Exception { logger.info("testWrite()"); NodeId nodeId = new NodeId(2, "/Static/AllProfiles/Scalar/Int32"); UaVariableNode variableNode = client.getAddressSpace().getVariableNode(nodeId); // read the existing value Object valueBefore = variableNode.readValueAttribute().get(); assertNotNull(valueBefore); // write a new random value DataValue newValue = new DataValue(new Variant(new Random().nextInt())); StatusCode writeStatus = variableNode.writeValue(newValue).get(); assertTrue(writeStatus.isGood()); // read the value again Object valueAfter = variableNode.readValueAttribute().get(); assertNotNull(valueAfter); assertNotEquals(valueBefore, valueAfter); } @Test public void testSubscribe() throws Exception { logger.info("testSubscribe()"); // create a subscription and a monitored item UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get(); ReadValueId readValueId = new ReadValueId( Identifiers.Server_ServerStatus_CurrentTime, AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE); MonitoringParameters parameters = new MonitoringParameters( uint(1), // client handle 1000.0, // sampling interval null, // no (default) filter uint(10), // queue size true); // discard oldest MonitoredItemCreateRequest request = new MonitoredItemCreateRequest( readValueId, MonitoringMode.Reporting, parameters); List<UaMonitoredItem> items = subscription .createMonitoredItems(TimestampsToReturn.Both, newArrayList(request)).get(); // do something with the value updates UaMonitoredItem item = items.get(0); CompletableFuture<DataValue> f = new CompletableFuture<>(); item.setValueConsumer(f::complete); assertNotNull(f.get(5, TimeUnit.SECONDS)); } @Test public void testTransferSubscriptions() throws Exception { logger.info("testTransferSubscriptions()"); // create a subscription and a monitored item UaSubscription subscription = client.getSubscriptionManager().createSubscription(1000.0).get(); NodeId nodeId = new NodeId(2, "/Static/AllProfiles/Scalar/Int32"); ReadValueId readValueId = new ReadValueId( nodeId, AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE); MonitoringParameters parameters = new MonitoringParameters( uint(1), // client handle 100.0, // sampling interval null, // no (default) filter uint(10), // queue size true); // discard oldest MonitoredItemCreateRequest request = new MonitoredItemCreateRequest( readValueId, MonitoringMode.Reporting, parameters); List<UaMonitoredItem> items = subscription .createMonitoredItems(TimestampsToReturn.Both, newArrayList(request)).get(); // do something with the value updates UaMonitoredItem item = items.get(0); AtomicInteger updateCount = new AtomicInteger(0); item.setValueConsumer(v -> { int count = updateCount.incrementAndGet(); logger.info("updateCount={}", count); }); AtomicBoolean subscriptionTransferred = new AtomicBoolean(true); client.getSubscriptionManager().addSubscriptionListener(new SubscriptionListener() { @Override public void onKeepAlive(UaSubscription subscription, DateTime publishTime) { } @Override public void onStatusChanged(UaSubscription subscription, StatusCode status) { } @Override public void onPublishFailure(UaException exception) { } @Override public void onNotificationDataLost(UaSubscription subscription) { } @Override public void onSubscriptionTransferFailed(UaSubscription subscription, StatusCode statusCode) { subscriptionTransferred.set(false); } }); logger.info("killing the session..."); UaSession uaSession = client.getSession().get(); server.getSessionManager().killSession(uaSession.getSessionId(), false); logger.info("sleeping while waiting for an update"); Thread.sleep(5000); // one update for the initial subscribe, another after transfer assertEquals(updateCount.get(), 2); assertTrue(subscriptionTransferred.get()); client.disconnect().get(); } @Test public void testUsernamePassword() throws Exception { logger.info("testUsernamePassword()"); EndpointDescription[] endpoints = UaTcpStackClient.getEndpoints("opc.tcp://localhost:12686/test-server").get(); EndpointDescription endpoint = Arrays.stream(endpoints) .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getSecurityPolicyUri())) .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned")); KeyStoreLoader loader = new KeyStoreLoader().load(); OpcUaClientConfig clientConfig = OpcUaClientConfig.builder() .setApplicationName(LocalizedText.english("digitalpetri opc-ua client")) .setApplicationUri("urn:digitalpetri:opcua:client") .setCertificate(loader.getClientCertificate()) .setKeyPair(loader.getClientKeyPair()) .setEndpoint(endpoint) .setRequestTimeout(uint(60000)) .setIdentityProvider(new UsernameProvider("user", "password")) .build(); OpcUaClient client = new OpcUaClient(clientConfig); client.connect().get(); } /** * Test using a username and password long enough that the encryption requires multiple ciphertext blocks. * * @throws Exception */ @Test public void testUsernamePassword_MultiBlock() throws Exception { logger.info("testUsernamePassword_MultiBlock()"); EndpointDescription[] endpoints = UaTcpStackClient.getEndpoints("opc.tcp://localhost:12686/test-server").get(); EndpointDescription endpoint = Arrays.stream(endpoints) .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getSecurityPolicyUri())) .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned")); char[] cs = new char[1000]; Arrays.fill(cs, 'a'); String user = new String(cs); String pass = new String(cs); OpcUaClientConfig clientConfig = OpcUaClientConfig.builder() .setApplicationName(LocalizedText.english("digitalpetri opc-ua client")) .setApplicationUri("urn:digitalpetri:opcua:client") .setEndpoint(endpoint) .setRequestTimeout(uint(60000)) .setIdentityProvider(new UsernameProvider(user, pass)) .build(); OpcUaClient client = new OpcUaClient(clientConfig); client.connect().get(); } @Test public void testConnectAndDisconnect() throws Exception { logger.info("testConnectAndDisconnect()"); EndpointDescription[] endpoints = UaTcpStackClient.getEndpoints("opc.tcp://localhost:12686/test-server").get(); EndpointDescription endpoint = Arrays.stream(endpoints) .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getSecurityPolicyUri())) .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned")); class ConnectDisconnect implements Runnable { private final int threadNumber; private ConnectDisconnect(int threadNumber) { this.threadNumber = threadNumber; } private OpcUaClientConfig clientConfig = OpcUaClientConfig.builder() .setApplicationName(LocalizedText.english("digitalpetri opc-ua client")) .setApplicationUri("urn:digitalpetri:opcua:client") .setEndpoint(endpoint) .setRequestTimeout(uint(10000)) .build(); private OpcUaClient client = new OpcUaClient(clientConfig); @Override public void run() { for (int i = 0; i < 100; i++) { try { client.connect().get(); client.readValues( 0.0, TimestampsToReturn.Both, newArrayList(Identifiers.Server_ServerStatus_CurrentTime) ).get(); client.disconnect().get(); Thread.sleep(10); } catch (InterruptedException | ExecutionException e) { fail(e.getMessage(), e); } } logger.info("Thread {} done.", threadNumber); } } Thread[] threads = new Thread[4]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new ConnectDisconnect(i)); threads[i].start(); } for (Thread thread : threads) { thread.join(); } } @Test public void testReactivate() throws Exception { logger.info("testReactivate()"); EndpointDescription[] endpoints = UaTcpStackClient.getEndpoints("opc.tcp://localhost:12686/test-server").get(); EndpointDescription endpoint = Arrays.stream(endpoints) .filter(e -> e.getSecurityPolicyUri().equals(SecurityPolicy.None.getSecurityPolicyUri())) .findFirst().orElseThrow(() -> new Exception("no desired endpoints returned")); OpcUaClientConfig clientConfig = OpcUaClientConfig.builder() .setApplicationName(LocalizedText.english("digitalpetri opc-ua client")) .setApplicationUri("urn:digitalpetri:opcua:client") .setEndpoint(endpoint) .setRequestTimeout(uint(10000)) .build(); OpcUaClient client = new OpcUaClient(clientConfig); UaVariableNode currentTimeNode = client.getAddressSpace() .getVariableNode(Identifiers.Server_ServerStatus_CurrentTime); assertNotNull(currentTimeNode.readValueAttribute().get()); // Kill the session. Client can't and won't be notified of this. logger.info("killing session..."); UaSession session = client.getSession().get(); server.getSessionManager().killSession(session.getSessionId(), true); // Expect the next action to fail because the session is no longer valid. try { logger.info("reading, expecting failure..."); currentTimeNode.readValueAttribute().get(); } catch (Throwable t) { StatusCode statusCode = UaServiceFaultException.extract(t) .map(UaException::getStatusCode) .orElse(StatusCode.BAD); assertEquals(statusCode.getValue(), StatusCodes.Bad_SessionIdInvalid); } Thread.sleep(1000); // Force a reactivate and read. logger.info("reconnecting..."); client.connect().get(); logger.info("reading, expecting success..."); assertNotNull(currentTimeNode.readValueAttribute().get()); client.disconnect().get(); } }