Java tutorial
/* * Copyright (c) 2019 the Eclipse Milo Authors * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ package org.eclipse.milo.opcua.sdk.client.session; import java.nio.ByteBuffer; import java.security.KeyPair; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Stream; import com.digitalpetri.strictmachine.Fsm; import com.digitalpetri.strictmachine.FsmContext; import com.digitalpetri.strictmachine.dsl.ActionContext; import com.digitalpetri.strictmachine.dsl.FsmBuilder; import com.google.common.collect.ImmutableList; import com.google.common.collect.Streams; import com.google.common.primitives.Bytes; import org.eclipse.milo.opcua.sdk.client.OpcUaClient; import org.eclipse.milo.opcua.sdk.client.OpcUaSession; import org.eclipse.milo.opcua.sdk.client.api.ServiceFaultListener; import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig; import org.eclipse.milo.opcua.sdk.client.api.identity.SignedIdentityToken; import org.eclipse.milo.opcua.sdk.client.api.subscriptions.UaSubscription; import org.eclipse.milo.opcua.sdk.client.session.SessionFsm.SessionFuture; import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscriptionManager; import org.eclipse.milo.opcua.stack.client.UaStackClient; import org.eclipse.milo.opcua.stack.core.AttributeId; import org.eclipse.milo.opcua.stack.core.Identifiers; import org.eclipse.milo.opcua.stack.core.Stack; import org.eclipse.milo.opcua.stack.core.StatusCodes; import org.eclipse.milo.opcua.stack.core.UaException; import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; import org.eclipse.milo.opcua.stack.core.security.SecurityAlgorithm; import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject; import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; import org.eclipse.milo.opcua.stack.core.types.enumerated.ApplicationType; import org.eclipse.milo.opcua.stack.core.types.enumerated.ServerState; import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; import org.eclipse.milo.opcua.stack.core.types.structured.ActivateSessionRequest; import org.eclipse.milo.opcua.stack.core.types.structured.ActivateSessionResponse; import org.eclipse.milo.opcua.stack.core.types.structured.ApplicationDescription; import org.eclipse.milo.opcua.stack.core.types.structured.CloseSessionRequest; import org.eclipse.milo.opcua.stack.core.types.structured.CreateSessionRequest; import org.eclipse.milo.opcua.stack.core.types.structured.CreateSessionResponse; import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription; import org.eclipse.milo.opcua.stack.core.types.structured.ReadRequest; import org.eclipse.milo.opcua.stack.core.types.structured.ReadResponse; import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; import org.eclipse.milo.opcua.stack.core.types.structured.RequestHeader; import org.eclipse.milo.opcua.stack.core.types.structured.ServiceFault; import org.eclipse.milo.opcua.stack.core.types.structured.SignatureData; import org.eclipse.milo.opcua.stack.core.types.structured.SignedSoftwareCertificate; import org.eclipse.milo.opcua.stack.core.types.structured.TransferResult; import org.eclipse.milo.opcua.stack.core.types.structured.TransferSubscriptionsRequest; import org.eclipse.milo.opcua.stack.core.types.structured.TransferSubscriptionsResponse; import org.eclipse.milo.opcua.stack.core.types.structured.UserIdentityToken; import org.eclipse.milo.opcua.stack.core.util.CertificateUtil; import org.eclipse.milo.opcua.stack.core.util.NonceUtil; import org.eclipse.milo.opcua.stack.core.util.SignatureUtil; import org.eclipse.milo.opcua.stack.core.util.Unit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static java.util.concurrent.CompletableFuture.completedFuture; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_CLOSE_FUTURE; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_KEEP_ALIVE_FAILURE_COUNT; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_KEEP_ALIVE_SCHEDULED_FUTURE; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_SESSION; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_SESSION_ACTIVITY_LISTENERS; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_SESSION_FUTURE; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_SESSION_INITIALIZERS; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_WAIT_FUTURE; import static org.eclipse.milo.opcua.sdk.client.session.SessionFsm.KEY_WAIT_TIME; import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; import static org.eclipse.milo.opcua.stack.core.util.ConversionUtil.l; import static org.eclipse.milo.opcua.stack.core.util.FutureUtils.complete; import static org.eclipse.milo.opcua.stack.core.util.FutureUtils.failedFuture; public class SessionFsmFactory { private static final Logger LOGGER = LoggerFactory.getLogger(SessionFsm.LOGGER_NAME); private static final int MAX_WAIT_SECONDS = 16; private SessionFsmFactory() { } public static SessionFsm newSessionFsm(OpcUaClient client) { FsmBuilder<State, Event> builder = new FsmBuilder<>(client.getConfig().getExecutor(), SessionFsm.LOGGER_NAME); configureSessionFsm(builder, client); Fsm<State, Event> fsm = builder.build(State.Inactive); client.addFaultListener(new SessionFaultListener(fsm)); return new SessionFsm(fsm); } private static void configureSessionFsm(FsmBuilder<State, Event> fb, OpcUaClient client) { configureInactiveState(fb, client); configureCreatingWaitState(fb, client); configureCreatingState(fb, client); configureActivatingState(fb, client); configureTransferringState(fb, client); configureInitializingState(fb, client); configureActiveState(fb, client); configureClosingState(fb, client); } private static void configureInactiveState(FsmBuilder<State, Event> fb, @SuppressWarnings("unused") OpcUaClient client) { /* Transitions */ fb.when(State.Inactive).on(Event.OpenSession.class).transitionTo(State.Creating); /* External Transition Actions */ fb.onTransitionTo(State.Inactive).from(s -> s != State.Inactive).viaAny() .execute(FsmContext::processShelvedEvents); /* Internal Transition Actions */ fb.onInternalTransition(State.Inactive).via(Event.GetSession.class).execute(ctx -> { Event.GetSession event = (Event.GetSession) ctx.event(); event.future.completeExceptionally(new UaException(StatusCodes.Bad_SessionClosed)); }); fb.onInternalTransition(State.Inactive).via(Event.CloseSession.class).execute(ctx -> { Event.CloseSession event = (Event.CloseSession) ctx.event(); event.future.complete(Unit.VALUE); }); } private static void configureCreatingWaitState(FsmBuilder<State, Event> fb, @SuppressWarnings("unused") OpcUaClient client) { /* Transitions */ fb.when(State.CreatingWait).on(Event.CreatingWaitExpired.class).transitionTo(State.Creating); fb.when(State.CreatingWait).on(Event.CloseSession.class).transitionTo(State.Inactive); /* External Transition Actions */ fb.onTransitionTo(State.CreatingWait).from(s -> s != State.CreatingWait).viaAny() .execute(FsmContext::processShelvedEvents); fb.onTransitionTo(State.CreatingWait).from(s -> s != State.CreatingWait).viaAny().execute(ctx -> { SessionFuture sessionFuture = new SessionFuture(); KEY_SESSION_FUTURE.set(ctx, sessionFuture); Long waitTime = KEY_WAIT_TIME.get(ctx); if (waitTime == null) { waitTime = 1L; } else { waitTime = Math.min(MAX_WAIT_SECONDS, waitTime << 1); } KEY_WAIT_TIME.set(ctx, waitTime); ScheduledFuture<?> waitFuture = Stack.sharedScheduledExecutor() .schedule(() -> ctx.fireEvent(new Event.CreatingWaitExpired()), waitTime, TimeUnit.SECONDS); KEY_WAIT_FUTURE.set(ctx, waitFuture); }); fb.onTransitionFrom(State.CreatingWait).to(State.Inactive).via(Event.CloseSession.class).execute(ctx -> { ScheduledFuture<?> waitFuture = KEY_WAIT_FUTURE.remove(ctx); if (waitFuture != null) waitFuture.cancel(false); KEY_WAIT_TIME.remove(ctx); Event.CloseSession event = (Event.CloseSession) ctx.event(); event.future.complete(Unit.VALUE); }); /* Internal Transition Actions */ fb.onInternalTransition(State.CreatingWait).via(Event.GetSession.class) .execute(SessionFsmFactory::handleGetSessionEvent); fb.onInternalTransition(State.CreatingWait).via(Event.OpenSession.class) .execute(SessionFsmFactory::handleOpenSessionEvent); } private static void configureCreatingState(FsmBuilder<State, Event> fb, OpcUaClient client) { /* Transitions */ fb.when(State.Creating).on(Event.CreateSessionSuccess.class).transitionTo(State.Activating); fb.when(State.Creating).on(Event.CreateSessionFailure.class).transitionTo(State.CreatingWait); /* External Transition Actions */ fb.onTransitionTo(State.Creating).from(State.Inactive).via(Event.OpenSession.class).execute(ctx -> { SessionFuture sessionFuture = new SessionFuture(); KEY_SESSION_FUTURE.set(ctx, sessionFuture); handleOpenSessionEvent(ctx); //noinspection Duplicates createSession(ctx, client).whenComplete((csr, ex) -> { if (csr != null) { LOGGER.debug("[{}] CreateSession succeeded: {}", ctx.getInstanceId(), csr.getSessionId()); ctx.fireEvent(new Event.CreateSessionSuccess(csr)); } else { LOGGER.debug("[{}] CreateSession failed: {}", ctx.getInstanceId(), ex.getMessage(), ex); handleFailureToOpenSession(ctx, ex); ctx.fireEvent(new Event.CreateSessionFailure(ex)); } }); }); fb.onTransitionTo(State.Creating).from(State.CreatingWait).via(Event.CreatingWaitExpired.class) .execute(ctx -> { //noinspection Duplicates createSession(ctx, client).whenComplete((csr, ex) -> { if (csr != null) { LOGGER.debug("[{}] CreateSession succeeded: {}", ctx.getInstanceId(), csr.getSessionId()); ctx.fireEvent(new Event.CreateSessionSuccess(csr)); } else { LOGGER.debug("[{}] CreateSession failed: {}", ctx.getInstanceId(), ex.getMessage(), ex); handleFailureToOpenSession(ctx, ex); ctx.fireEvent(new Event.CreateSessionFailure(ex)); } }); }); /* Internal Transition Actions */ fb.onInternalTransition(State.Creating).via(Event.GetSession.class) .execute(SessionFsmFactory::handleGetSessionEvent); fb.onInternalTransition(State.Creating).via(Event.OpenSession.class) .execute(SessionFsmFactory::handleOpenSessionEvent); fb.onInternalTransition(State.Creating).via(Event.CloseSession.class) .execute(ctx -> ctx.shelveEvent(ctx.event())); } private static void configureActivatingState(FsmBuilder<State, Event> fb, OpcUaClient client) { /* Transitions */ fb.when(State.Activating).on(Event.ActivateSessionSuccess.class).transitionTo(State.Transferring); fb.when(State.Activating).on(Event.ActivateSessionFailure.class).transitionTo(State.CreatingWait); /* External Transition Actions */ fb.onTransitionTo(State.Activating).from(State.Creating).via(Event.CreateSessionSuccess.class) .execute(ctx -> { Event.CreateSessionSuccess event = (Event.CreateSessionSuccess) ctx.event(); activateSession(ctx, client, event.response).whenComplete((session, ex) -> { if (session != null) { LOGGER.debug("[{}] Session activated: {}", ctx.getInstanceId(), session); ctx.fireEvent(new Event.ActivateSessionSuccess(session)); } else { LOGGER.debug("[{}] ActivateSession failed: {}", ctx.getInstanceId(), ex.getMessage(), ex); handleFailureToOpenSession(ctx, ex); ctx.fireEvent(new Event.ActivateSessionFailure(ex)); } }); }); /* Internal Transition Actions */ fb.onInternalTransition(State.Activating).via(Event.GetSession.class) .execute(SessionFsmFactory::handleGetSessionEvent); fb.onInternalTransition(State.Activating).via(Event.OpenSession.class) .execute(SessionFsmFactory::handleOpenSessionEvent); fb.onInternalTransition(State.Activating).via(Event.CloseSession.class) .execute(ctx -> ctx.shelveEvent(ctx.event())); } private static void configureTransferringState(FsmBuilder<State, Event> fb, OpcUaClient client) { /* Transitions */ fb.when(State.Transferring).on(Event.TransferSubscriptionsSuccess.class).transitionTo(State.Initializing); fb.when(State.Transferring).on(Event.TransferSubscriptionsFailure.class).transitionTo(State.CreatingWait); /* External Transition Actions */ fb.onTransitionTo(State.Transferring).from(State.Activating).via(Event.ActivateSessionSuccess.class) .execute(ctx -> { Event.ActivateSessionSuccess event = (Event.ActivateSessionSuccess) ctx.event(); transferSubscriptions(ctx, client, event.session).whenComplete((u, ex) -> { if (u != null) { LOGGER.debug("[{}] TransferSubscriptions succeeded", ctx.getInstanceId()); ctx.fireEvent(new Event.TransferSubscriptionsSuccess(event.session)); } else { LOGGER.debug("[{}] TransferSubscriptions failed: {}", ctx.getInstanceId(), ex.getMessage(), ex); handleFailureToOpenSession(ctx, ex); ctx.fireEvent(new Event.TransferSubscriptionsFailure(ex)); } }); }); /* Internal Transition Actions */ fb.onInternalTransition(State.Transferring).via(Event.GetSession.class) .execute(SessionFsmFactory::handleGetSessionEvent); fb.onInternalTransition(State.Transferring).via(Event.OpenSession.class) .execute(SessionFsmFactory::handleOpenSessionEvent); fb.onInternalTransition(State.Transferring).via(Event.CloseSession.class) .execute(ctx -> ctx.shelveEvent(ctx.event())); } private static void configureInitializingState(FsmBuilder<State, Event> fb, OpcUaClient client) { /* Transitions */ fb.when(State.Initializing).on(Event.InitializeSuccess.class).transitionTo(State.Active); fb.when(State.Initializing).on(Event.InitializeFailure.class).transitionTo(State.CreatingWait); /* External Transition Actions */ fb.onTransitionTo(State.Initializing).from(State.Transferring).via(Event.TransferSubscriptionsSuccess.class) .execute(ctx -> { Event.TransferSubscriptionsSuccess event = (Event.TransferSubscriptionsSuccess) ctx.event(); OpcUaSession session = event.session; initialize(ctx, client, session).whenComplete((u, ex) -> { if (u != null) { LOGGER.debug("[{}] Initialization succeeded: {}", ctx.getInstanceId(), session); ctx.fireEvent(new Event.InitializeSuccess(session)); } else { LOGGER.warn("[{}] Initialization failed: {}", ctx.getInstanceId(), session, ex); handleFailureToOpenSession(ctx, ex); ctx.fireEvent(new Event.InitializeFailure(ex)); } }); }); /* Internal Transition Actions */ fb.onInternalTransition(State.Initializing).via(Event.GetSession.class) .execute(SessionFsmFactory::handleGetSessionEvent); fb.onInternalTransition(State.Initializing).via(Event.OpenSession.class) .execute(SessionFsmFactory::handleOpenSessionEvent); fb.onInternalTransition(State.Initializing).via(Event.CloseSession.class) .execute(ctx -> ctx.shelveEvent(ctx.event())); } private static void configureActiveState(FsmBuilder<State, Event> fb, OpcUaClient client) { /* Transitions */ fb.when(State.Active).on(Event.CloseSession.class).transitionTo(State.Closing); fb.when(State.Active) .on(e -> e.getClass() == Event.KeepAliveFailure.class || e.getClass() == Event.ServiceFault.class) .transitionTo(State.CreatingWait); /* External Transition Actions */ fb.onTransitionTo(State.Active).from(State.Initializing).via(Event.InitializeSuccess.class).execute(ctx -> { Event.InitializeSuccess event = (Event.InitializeSuccess) ctx.event(); // reset the wait time KEY_WAIT_TIME.remove(ctx); long keepAliveInterval = client.getConfig().getKeepAliveInterval().longValue(); KEY_KEEP_ALIVE_FAILURE_COUNT.set(ctx, 0L); ScheduledFuture<?> scheduledFuture = Stack.sharedScheduledExecutor().scheduleWithFixedDelay( () -> ctx.fireEvent(new Event.KeepAlive(event.session)), keepAliveInterval, keepAliveInterval, TimeUnit.MILLISECONDS); KEY_KEEP_ALIVE_SCHEDULED_FUTURE.set(ctx, scheduledFuture); KEY_SESSION.set(ctx, event.session); SessionFuture sessionFuture = KEY_SESSION_FUTURE.get(ctx); sessionFuture.future.complete(event.session); }); fb.onTransitionTo(State.Active).from(State.Initializing).via(Event.InitializeSuccess.class) .execute(FsmContext::processShelvedEvents); fb.onTransitionFrom(State.Active).to(s -> s == State.Closing || s == State.CreatingWait).viaAny() .execute(ctx -> { ScheduledFuture scheduledFuture = KEY_KEEP_ALIVE_SCHEDULED_FUTURE.remove(ctx); if (scheduledFuture != null) { scheduledFuture.cancel(false); } }); // onSessionActive() callbacks fb.onTransitionTo(State.Active).from(s -> s != State.Active).viaAny().execute(ctx -> { OpcUaSession session = KEY_SESSION.get(ctx); SessionFsm.SessionActivityListeners sessionActivityListeners = KEY_SESSION_ACTIVITY_LISTENERS.get(ctx); sessionActivityListeners.sessionActivityListeners .forEach(listener -> listener.onSessionActive(session)); }); // onSessionInactive() callbacks fb.onTransitionFrom(State.Active).to(s -> s != State.Active).viaAny().execute(ctx -> { OpcUaSession session = KEY_SESSION.get(ctx); SessionFsm.SessionActivityListeners sessionActivityListeners = KEY_SESSION_ACTIVITY_LISTENERS.get(ctx); sessionActivityListeners.sessionActivityListeners .forEach(listener -> listener.onSessionInactive(session)); }); /* Internal Transition Actions */ fb.onInternalTransition(State.Active).via(Event.KeepAlive.class).execute(ctx -> { Event.KeepAlive event = (Event.KeepAlive) ctx.event(); sendKeepAlive(client, event.session).whenComplete((response, ex) -> { if (response != null) { DataValue[] results = response.getResults(); if (results != null && results.length > 0) { Object value = results[0].getValue().getValue(); if (value instanceof Integer) { ServerState state = ServerState.from((Integer) value); LOGGER.debug("[{}] ServerState: {}", ctx.getInstanceId(), state); } } KEY_KEEP_ALIVE_FAILURE_COUNT.set(ctx, 0L); } else { Long keepAliveFailureCount = KEY_KEEP_ALIVE_FAILURE_COUNT.get(ctx); if (keepAliveFailureCount == null) { keepAliveFailureCount = 1L; } else { keepAliveFailureCount += 1L; } KEY_KEEP_ALIVE_FAILURE_COUNT.set(ctx, keepAliveFailureCount); long keepAliveFailuresAllowed = client.getConfig().getKeepAliveFailuresAllowed().longValue(); if (keepAliveFailureCount > keepAliveFailuresAllowed) { LOGGER.warn("[{}] Keep Alive failureCount={} exceeds failuresAllowed={}", ctx.getInstanceId(), keepAliveFailureCount, keepAliveFailuresAllowed); ctx.fireEvent(new Event.KeepAliveFailure()); } else { LOGGER.debug("[{}] Keep Alive failureCount={}", ctx.getInstanceId(), keepAliveFailureCount, ex); } } }); }); fb.onInternalTransition(State.Active).via(Event.GetSession.class) .execute(SessionFsmFactory::handleGetSessionEvent); fb.onInternalTransition(State.Active).via(Event.OpenSession.class) .execute(SessionFsmFactory::handleOpenSessionEvent); } private static void configureClosingState(FsmBuilder<State, Event> fb, OpcUaClient client) { /* Transitions */ fb.when(State.Closing).on(Event.CloseSessionSuccess.class).transitionTo(State.Inactive); /* External Transition Actions */ fb.onTransitionTo(State.Closing).from(State.Active).via(Event.CloseSession.class).execute(ctx -> { SessionFsm.CloseFuture closeFuture = new SessionFsm.CloseFuture(); KEY_CLOSE_FUTURE.set(ctx, closeFuture); Event.CloseSession closeSession = (Event.CloseSession) ctx.event(); complete(closeSession.future).with(closeFuture.future); OpcUaSession session = KEY_SESSION.get(ctx); closeSession(ctx, client, session).whenComplete((u, ex) -> { if (u != null) { LOGGER.debug("[{}] Session closed: {}", ctx.getInstanceId(), session); } else { LOGGER.debug("[{}] CloseSession failed: {}", ctx.getInstanceId(), ex.getMessage(), ex); } ctx.fireEvent(new Event.CloseSessionSuccess()); }); }); fb.onTransitionFrom(State.Closing).to(State.Inactive).via(Event.CloseSessionSuccess.class).execute(ctx -> { SessionFsm.CloseFuture closeFuture = KEY_CLOSE_FUTURE.get(ctx); if (closeFuture != null) { closeFuture.future.complete(Unit.VALUE); } }); /* Internal Transition Actions */ fb.onInternalTransition(State.Closing).via(Event.CloseSession.class).execute(ctx -> { Event.CloseSession event = (Event.CloseSession) ctx.event(); SessionFsm.CloseFuture closeFuture = KEY_CLOSE_FUTURE.get(ctx); if (closeFuture != null) { complete(event.future).with(closeFuture.future); } }); fb.onInternalTransition(State.Closing).via(e -> e.getClass() != Event.CloseSession.class) .execute(ctx -> ctx.shelveEvent(ctx.event())); } private static void handleGetSessionEvent(ActionContext<State, Event> ctx) { CompletableFuture<OpcUaSession> sessionFuture = KEY_SESSION_FUTURE.get(ctx).future; Event.GetSession event = (Event.GetSession) ctx.event(); complete(event.future).with(sessionFuture); } private static void handleOpenSessionEvent(ActionContext<State, Event> ctx) { CompletableFuture<OpcUaSession> sessionFuture = KEY_SESSION_FUTURE.get(ctx).future; Event.OpenSession event = (Event.OpenSession) ctx.event(); complete(event.future).with(sessionFuture); } private static void handleFailureToOpenSession(ActionContext<State, Event> ctx, Throwable failure) { SessionFuture sessionFuture = KEY_SESSION_FUTURE.remove(ctx); if (sessionFuture != null) { sessionFuture.future.completeExceptionally(failure); } } private static CompletableFuture<Unit> closeSession(FsmContext<State, Event> ctx, OpcUaClient client, OpcUaSession session) { CompletableFuture<Unit> closeFuture = new CompletableFuture<>(); UaStackClient stackClient = client.getStackClient(); RequestHeader requestHeader = stackClient.newRequestHeader(session.getAuthenticationToken(), uint(5000)); CloseSessionRequest request = new CloseSessionRequest(requestHeader, true); LOGGER.debug("[{}] Sending CloseSessionRequest...", ctx.getInstanceId()); stackClient.sendRequest(request).whenComplete((csr, ex2) -> closeFuture.complete(Unit.VALUE)); return closeFuture; } @SuppressWarnings("Duplicates") private static CompletableFuture<CreateSessionResponse> createSession(FsmContext<State, Event> ctx, OpcUaClient client) { UaStackClient stackClient = client.getStackClient(); EndpointDescription endpoint = stackClient.getConfig().getEndpoint(); String gatewayServerUri = endpoint.getServer().getGatewayServerUri(); String serverUri; if (gatewayServerUri != null && !gatewayServerUri.isEmpty()) { serverUri = endpoint.getServer().getApplicationUri(); } else { serverUri = null; } ByteString clientNonce = NonceUtil.generateNonce(32); ByteString clientCertificate = stackClient.getConfig().getCertificate().map(c -> { try { return ByteString.of(c.getEncoded()); } catch (CertificateEncodingException e) { return ByteString.NULL_VALUE; } }).orElse(ByteString.NULL_VALUE); ApplicationDescription clientDescription = new ApplicationDescription( client.getConfig().getApplicationUri(), client.getConfig().getProductUri(), client.getConfig().getApplicationName(), ApplicationType.Client, null, null, null); CreateSessionRequest request = new CreateSessionRequest(client.newRequestHeader(), clientDescription, serverUri, client.getConfig().getEndpoint().getEndpointUrl(), client.getConfig().getSessionName().get(), clientNonce, clientCertificate, client.getConfig().getSessionTimeout().doubleValue(), client.getConfig().getMaxResponseMessageSize()); LOGGER.debug("[{}] Sending CreateSessionRequest...", ctx.getInstanceId()); return stackClient.sendRequest(request).thenApply(CreateSessionResponse.class::cast) .thenCompose(response -> { try { SecurityPolicy securityPolicy = SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()); if (securityPolicy != SecurityPolicy.None) { if (response.getServerCertificate().isNullOrEmpty()) { throw new UaException(StatusCodes.Bad_SecurityChecksFailed, "Certificate missing from CreateSessionResponse"); } List<X509Certificate> serverCertificateChain = CertificateUtil .decodeCertificates(response.getServerCertificate().bytesOrEmpty()); X509Certificate serverCertificate = serverCertificateChain.get(0); X509Certificate certificateFromEndpoint = CertificateUtil .decodeCertificate(endpoint.getServerCertificate().bytesOrEmpty()); if (!serverCertificate.equals(certificateFromEndpoint)) { throw new UaException(StatusCodes.Bad_SecurityChecksFailed, "Certificate from CreateSessionResponse did not " + "match certificate from EndpointDescription!"); } CertificateValidator certificateValidator = client.getConfig() .getCertificateValidator(); certificateValidator.validate(serverCertificate); certificateValidator.verifyTrustChain(serverCertificateChain); SignatureData serverSignature = response.getServerSignature(); byte[] dataBytes = Bytes.concat(clientCertificate.bytesOrEmpty(), clientNonce.bytesOrEmpty()); byte[] signatureBytes = serverSignature.getSignature().bytesOrEmpty(); SignatureUtil.verify(SecurityAlgorithm.fromUri(serverSignature.getAlgorithm()), serverCertificate, dataBytes, signatureBytes); } return completedFuture(response); } catch (UaException e) { return failedFuture(e); } }); } @SuppressWarnings("Duplicates") private static CompletableFuture<OpcUaSession> activateSession(FsmContext<State, Event> ctx, OpcUaClient client, CreateSessionResponse csr) { UaStackClient stackClient = client.getStackClient(); try { EndpointDescription endpoint = client.getConfig().getEndpoint(); ByteString serverNonce = csr.getServerNonce(); SignedIdentityToken signedIdentityToken = client.getConfig().getIdentityProvider() .getIdentityToken(endpoint, serverNonce); UserIdentityToken userIdentityToken = signedIdentityToken.getToken(); SignatureData userTokenSignature = signedIdentityToken.getSignature(); ActivateSessionRequest request = new ActivateSessionRequest( client.newRequestHeader(csr.getAuthenticationToken()), buildClientSignature(client.getConfig(), serverNonce), new SignedSoftwareCertificate[0], new String[0], ExtensionObject.encode(userIdentityToken), userTokenSignature); LOGGER.debug("[{}] Sending ActivateSessionRequest...", ctx.getInstanceId()); return stackClient.sendRequest(request).thenApply(ActivateSessionResponse.class::cast) .thenCompose(asr -> { OpcUaSession session = new OpcUaSession(csr.getAuthenticationToken(), csr.getSessionId(), client.getConfig().getSessionName().get(), csr.getRevisedSessionTimeout(), csr.getMaxRequestMessageSize(), csr.getServerCertificate(), csr.getServerSoftwareCertificates()); session.setServerNonce(asr.getServerNonce()); return completedFuture(session); }); } catch (Exception ex) { return failedFuture(ex); } } @SuppressWarnings("Duplicates") private static CompletableFuture<Unit> transferSubscriptions(FsmContext<State, Event> ctx, OpcUaClient client, OpcUaSession session) { UaStackClient stackClient = client.getStackClient(); OpcUaSubscriptionManager subscriptionManager = client.getSubscriptionManager(); ImmutableList<UaSubscription> subscriptions = subscriptionManager.getSubscriptions(); if (subscriptions.isEmpty()) { return completedFuture(Unit.VALUE); } CompletableFuture<Unit> transferFuture = new CompletableFuture<>(); UInteger[] subscriptionIdsArray = subscriptions.stream().map(UaSubscription::getSubscriptionId) .toArray(UInteger[]::new); TransferSubscriptionsRequest request = new TransferSubscriptionsRequest( client.newRequestHeader(session.getAuthenticationToken()), subscriptionIdsArray, true); LOGGER.debug("[{}] Sending TransferSubscriptionsRequest...", ctx.getInstanceId()); stackClient.sendRequest(request).thenApply(TransferSubscriptionsResponse.class::cast) .whenComplete((tsr, ex) -> { if (tsr != null) { List<TransferResult> results = l(tsr.getResults()); LOGGER.debug("[{}] TransferSubscriptions supported: {}", ctx.getInstanceId(), tsr.getResponseHeader().getServiceResult()); if (LOGGER.isDebugEnabled()) { try { Stream<UInteger> subscriptionIds = subscriptions.stream() .map(UaSubscription::getSubscriptionId); Stream<StatusCode> statusCodes = results.stream() .map(TransferResult::getStatusCode); //noinspection UnstableApiUsage String[] ss = Streams .zip(subscriptionIds, statusCodes, (i, s) -> String .format("id=%s/%s", i, StatusCodes.lookup(s.getValue()).map(sa -> sa[0]) .orElse(s.toString()))) .toArray(String[]::new); LOGGER.debug("[{}] TransferSubscriptions results: {}", ctx.getInstanceId(), Arrays.toString(ss)); } catch (Throwable t) { LOGGER.error("[{}] error logging TransferSubscription results", ctx.getInstanceId(), t); } } client.getConfig().getExecutor().execute(() -> { for (int i = 0; i < results.size(); i++) { TransferResult result = results.get(i); if (!result.getStatusCode().isGood()) { UaSubscription subscription = subscriptions.get(i); subscriptionManager.transferFailed(subscription.getSubscriptionId(), result.getStatusCode()); } } }); transferFuture.complete(Unit.VALUE); } else { StatusCode statusCode = UaException.extract(ex).map(UaException::getStatusCode) .orElse(StatusCode.BAD); // Bad_ServiceUnsupported is the correct response when transfers aren't supported but // server implementations tend to interpret the spec in their own unique way... if (statusCode.getValue() == StatusCodes.Bad_NotImplemented || statusCode.getValue() == StatusCodes.Bad_NotSupported || statusCode.getValue() == StatusCodes.Bad_OutOfService || statusCode.getValue() == StatusCodes.Bad_ServiceUnsupported) { LOGGER.debug("[{}] TransferSubscriptions not supported: {}", ctx.getInstanceId(), statusCode); client.getConfig().getExecutor().execute(() -> { // transferFailed() will remove the subscription, but that is okay // because the list from getSubscriptions() above is a copy. for (UaSubscription subscription : subscriptions) { subscriptionManager.transferFailed(subscription.getSubscriptionId(), statusCode); } }); transferFuture.complete(Unit.VALUE); } else { transferFuture.completeExceptionally(ex); } } }); return transferFuture; } private static CompletableFuture<Unit> initialize(FsmContext<State, Event> ctx, OpcUaClient client, OpcUaSession session) { List<SessionFsm.SessionInitializer> initializers = KEY_SESSION_INITIALIZERS.get(ctx).sessionInitializers; if (initializers.isEmpty()) { return completedFuture(Unit.VALUE); } else { UaStackClient stackClient = client.getStackClient(); CompletableFuture[] futures = initializers.stream().map(i -> i.initialize(stackClient, session)) .toArray(CompletableFuture[]::new); return CompletableFuture.allOf(futures).thenApply(v -> Unit.VALUE); } } private static CompletableFuture<ReadResponse> sendKeepAlive(OpcUaClient client, OpcUaSession session) { ReadRequest keepAliveRequest = createKeepAliveRequest(client, session); return client.getStackClient().sendRequest(keepAliveRequest).thenApply(ReadResponse.class::cast); } private static ReadRequest createKeepAliveRequest(OpcUaClient client, OpcUaSession session) { RequestHeader requestHeader = client.getStackClient().newRequestHeader(session.getAuthenticationToken(), client.getConfig().getKeepAliveTimeout()); return new ReadRequest(requestHeader, 0.0, TimestampsToReturn.Neither, new ReadValueId[] { new ReadValueId(Identifiers.Server_ServerStatus_State, AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE) }); } @SuppressWarnings("Duplicates") private static SignatureData buildClientSignature(OpcUaClientConfig config, ByteString serverNonce) throws Exception { EndpointDescription endpoint = config.getEndpoint(); SecurityPolicy securityPolicy = SecurityPolicy.fromUri(endpoint.getSecurityPolicyUri()); if (securityPolicy == SecurityPolicy.None) { return new SignatureData(); } else { SecurityAlgorithm signatureAlgorithm = securityPolicy.getAsymmetricSignatureAlgorithm(); PrivateKey privateKey = config.getKeyPair().map(KeyPair::getPrivate).orElse(null); ByteString serverCertificate = endpoint.getServerCertificate(); // Signature data is serverCert + serverNonce signed with our private key. byte[] serverNonceBytes = serverNonce.bytesOrEmpty(); byte[] serverCertificateBytes = serverCertificate.bytesOrEmpty(); byte[] dataToSign = Bytes.concat(serverCertificateBytes, serverNonceBytes); byte[] signature = SignatureUtil.sign(signatureAlgorithm, privateKey, ByteBuffer.wrap(dataToSign)); return new SignatureData(signatureAlgorithm.getUri(), ByteString.of(signature)); } } private static class SessionFaultListener implements ServiceFaultListener { private static final Predicate<StatusCode> SESSION_ERROR = statusCode -> { long status = statusCode.getValue(); return status == StatusCodes.Bad_SessionClosed || status == StatusCodes.Bad_SessionIdInvalid || status == StatusCodes.Bad_SessionNotActivated; }; private static final Predicate<StatusCode> SECURE_CHANNEL_ERROR = statusCode -> { long status = statusCode.getValue(); return status == StatusCodes.Bad_SecureChannelIdInvalid || status == StatusCodes.Bad_SecurityChecksFailed || status == StatusCodes.Bad_TcpSecureChannelUnknown || status == StatusCodes.Bad_RequestTypeInvalid; }; private final Logger logger = LoggerFactory.getLogger(SessionFsm.LOGGER_NAME); private final Fsm<State, Event> fsm; private SessionFaultListener(Fsm<State, Event> fsm) { this.fsm = fsm; } @Override public void onServiceFault(ServiceFault serviceFault) { StatusCode serviceResult = serviceFault.getResponseHeader().getServiceResult(); if (SESSION_ERROR.or(SECURE_CHANNEL_ERROR).test(serviceResult)) { logger.debug("[{}] ServiceFault: {}", fsm.getFromContext(FsmContext::getInstanceId), serviceResult); fsm.fireEvent(new Event.ServiceFault(serviceResult)); } } } }