blob: a05069cb874d7e1a7006adfb64f09fae9f9e5912 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.plc4x.java.opcua.context;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
import org.apache.plc4x.java.api.exceptions.PlcRuntimeException;
import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
import org.apache.plc4x.java.opcua.readwrite.*;
import org.apache.plc4x.java.opcua.readwrite.io.ExtensionObjectIO;
import org.apache.plc4x.java.opcua.readwrite.io.OpcuaAPUIO;
import org.apache.plc4x.java.opcua.readwrite.types.*;
import org.apache.plc4x.java.spi.ConversationContext;
import org.apache.plc4x.java.spi.context.DriverContext;
import org.apache.plc4x.java.spi.generation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
public class SecureChannel {
private static final Logger LOGGER = LoggerFactory.getLogger(SecureChannel.class);
private static final String FINAL_CHUNK = "F";
private static final String CONTINUATION_CHUNK = "C";
private static final String ABORT_CHUNK = "A";
private static final int VERSION = 0;
private static final int DEFAULT_MAX_CHUNK_COUNT = 64;
private static final int DEFAULT_MAX_MESSAGE_SIZE = 2097152;
private static final int DEFAULT_RECEIVE_BUFFER_SIZE = 65535;
private static final int DEFAULT_SEND_BUFFER_SIZE = 65535;
public static final Duration REQUEST_TIMEOUT = Duration.ofMillis(1000000);
public static final long REQUEST_TIMEOUT_LONG = 10000L;
private static final String PASSWORD_ENCRYPTION_ALGORITHM = "http://www.w3.org/2001/04/xmlenc#rsa-oaep";
private static final PascalString SECURITY_POLICY_NONE = new PascalString("http://opcfoundation.org/UA/SecurityPolicy#None");
protected static final PascalString NULL_STRING = new PascalString( "");
private static final PascalByteString NULL_BYTE_STRING = new PascalByteString( -1, null);
private static ExpandedNodeId NULL_EXPANDED_NODEID = new ExpandedNodeId(false,
false,
new NodeIdTwoByte((short) 0),
null,
null
);
protected static final ExtensionObject NULL_EXTENSION_OBJECT = new ExtensionObject(
NULL_EXPANDED_NODEID,
new ExtensionObjectEncodingMask(false, false, false),
new NullExtension()); // Body
private static final long EPOCH_OFFSET = 116444736000000000L; //Offset between OPC UA epoch time and linux epoch time.
private static final PascalString APPLICATION_URI = new PascalString("urn:apache:plc4x:client");
private static final PascalString PRODUCT_URI = new PascalString("urn:apache:plc4x:client");
private static final PascalString APPLICATION_TEXT = new PascalString("OPCUA client for the Apache PLC4X:PLC4J project");
private static final long DEFAULT_CONNECTION_LIFETIME = 36000000;
private final String sessionName = "UaSession:" + APPLICATION_TEXT.getStringValue() + ":" + RandomStringUtils.random(20, true, true);
private final byte[] clientNonce = RandomUtils.nextBytes(40);
private AtomicInteger requestHandleGenerator = new AtomicInteger(1);
private PascalString policyId;
private PascalString endpoint;
private boolean discovery;
private String username;
private String password;
private String certFile;
private String securityPolicy;
private String keyStoreFile;
private CertificateKeyPair ckp;
private PascalByteString publicCertificate;
private PascalByteString thumbprint;
private boolean isEncrypted;
private byte[] senderCertificate = null;
private byte[] senderNonce = null;
private PascalByteString certificateThumbprint = null;
private boolean checkedEndpoints = false;
private EncryptionHandler encryptionHandler = null;
private OpcuaConfiguration configuration;
private AtomicInteger channelId = new AtomicInteger(1);
private AtomicInteger tokenId = new AtomicInteger(1);
private NodeIdTypeDefinition authenticationToken = new NodeIdTwoByte((short) 0);
private DriverContext driverContext;
ConversationContext<OpcuaAPU> context;
private SecureChannelTransactionManager channelTransactionManager = new SecureChannelTransactionManager();
private long lifetime = DEFAULT_CONNECTION_LIFETIME;
private CompletableFuture<Void> keepAlive;
private int sendBufferSize;
private int maxMessageSize;
private long senderSequenceNumber;
public SecureChannel(DriverContext driverContext, OpcuaConfiguration configuration) {
this.driverContext = driverContext;
this.configuration = configuration;
this.endpoint = new PascalString(configuration.getEndpoint());
this.discovery = configuration.isDiscovery();
this.username = configuration.getUsername();
this.password = configuration.getPassword();
this.certFile = configuration.getCertDirectory();
this.securityPolicy = "http://opcfoundation.org/UA/SecurityPolicy#" + configuration.getSecurityPolicy();
this.ckp = configuration.getCertificateKeyPair();
if (configuration.getSecurityPolicy() != null && configuration.getSecurityPolicy().equals("Basic256Sha256")) {
//Sender Certificate gets populated during the discover phase when encryption is enabled.
this.senderCertificate = configuration.getSenderCertificate();
this.encryptionHandler = new EncryptionHandler(this.ckp, this.senderCertificate, configuration.getSecurityPolicy());
try {
this.publicCertificate = new PascalByteString(this.ckp.getCertificate().getEncoded().length, this.ckp.getCertificate().getEncoded());
this.isEncrypted = true;
} catch (CertificateEncodingException e) {
throw new PlcRuntimeException("Failed to encode the certificate");
}
this.thumbprint = configuration.getThumbprint();
} else {
this.encryptionHandler = new EncryptionHandler(this.ckp, this.senderCertificate, configuration.getSecurityPolicy());
this.publicCertificate = NULL_BYTE_STRING;
this.thumbprint = NULL_BYTE_STRING;
this.isEncrypted = false;
}
this.keyStoreFile = configuration.getKeyStoreFile();
}
public void submit(ConversationContext<OpcuaAPU> context, Consumer<TimeoutException> onTimeout, BiConsumer<OpcuaAPU, Throwable> error, Consumer<byte[]> consumer, WriteBufferByteBased buffer) {
int transactionId = channelTransactionManager.getTransactionIdentifier();
OpcuaMessageRequest messageRequest = new OpcuaMessageRequest(FINAL_CHUNK,
channelId.get(),
tokenId.get(),
transactionId,
transactionId,
buffer.getData());
final OpcuaAPU apu;
try {
if (this.isEncrypted) {
apu = OpcuaAPUIO.staticParse(encryptionHandler.encodeMessage(messageRequest, buffer.getData()), false);
} else {
apu = new OpcuaAPU(messageRequest);
}
} catch (ParseException e) {
throw new PlcRuntimeException("Unable to encrypt message before sending");
}
Consumer<Integer> requestConsumer = t -> {
try {
ByteArrayOutputStream messageBuffer = new ByteArrayOutputStream();
context.sendRequest(apu)
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.onTimeout(onTimeout)
.onError(error)
.unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage))
.unwrap(p -> (OpcuaMessageResponse) p.getMessage())
.check(p -> {
if (p.getRequestId() == transactionId) {
try {
messageBuffer.write(p.getMessage());
} catch (IOException e) {
LOGGER.debug("Failed to store incoming message in buffer {}");
throw new PlcRuntimeException("Error while sending message");
}
if (p.getChunk().equals(FINAL_CHUNK)) {
return true;
} else {
return false;
}
} else {
return false;
}
})
.handle(opcuaResponse -> {
if (opcuaResponse.getChunk().equals(FINAL_CHUNK)) {
tokenId.set(opcuaResponse.getSecureTokenId());
channelId.set(opcuaResponse.getSecureChannelId());
if (!(transactionId == (opcuaResponse.getSequenceNumber() + 1))) {
LOGGER.error("Sequence number isn't as expected, we might have missed a packet. - {} != {}", transactionId, opcuaResponse.getSequenceNumber() + 1);
context.fireDisconnected();
}
consumer.accept(messageBuffer.toByteArray());
}
});
} catch (Exception e) {
throw new PlcRuntimeException("Error while sending message");
}
};
LOGGER.debug("Submitting Transaction to TransactionManager {}", transactionId);
channelTransactionManager.submit(requestConsumer, transactionId);
}
public void onConnect(ConversationContext<OpcuaAPU> context) {
// Only the TCP transport supports login.
LOGGER.debug("Opcua Driver running in ACTIVE mode.");
this.context = context;
OpcuaHelloRequest hello = new OpcuaHelloRequest(FINAL_CHUNK,
VERSION,
DEFAULT_RECEIVE_BUFFER_SIZE,
DEFAULT_SEND_BUFFER_SIZE,
DEFAULT_MAX_MESSAGE_SIZE,
DEFAULT_MAX_CHUNK_COUNT,
this.endpoint);
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(new OpcuaAPU(hello))
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.check(p -> p.getMessage() instanceof OpcuaAcknowledgeResponse)
.unwrap(p -> (OpcuaAcknowledgeResponse) p.getMessage())
.handle(opcuaAcknowledgeResponse -> {
sendBufferSize = Math.min(opcuaAcknowledgeResponse.getReceiveBufferSize(), DEFAULT_SEND_BUFFER_SIZE);
maxMessageSize = Math.min(opcuaAcknowledgeResponse.getMaxMessageSize(), DEFAULT_MAX_MESSAGE_SIZE);
onConnectOpenSecureChannel(context, opcuaAcknowledgeResponse);
});
};
channelTransactionManager.submit(requestConsumer, channelTransactionManager.getTransactionIdentifier());
}
public void onConnectOpenSecureChannel(ConversationContext<OpcuaAPU> context, OpcuaAcknowledgeResponse opcuaAcknowledgeResponse) {
int transactionId = channelTransactionManager.getTransactionIdentifier();
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
0L, //RequestHandle
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
OpenSecureChannelRequest openSecureChannelRequest = null;
if (this.isEncrypted) {
openSecureChannelRequest = new OpenSecureChannelRequest(
requestHeader,
VERSION,
SecurityTokenRequestType.securityTokenRequestTypeIssue,
MessageSecurityMode.messageSecurityModeSignAndEncrypt,
new PascalByteString(clientNonce.length, clientNonce),
lifetime);
} else {
openSecureChannelRequest = new OpenSecureChannelRequest(
requestHeader,
VERSION,
SecurityTokenRequestType.securityTokenRequestTypeIssue,
MessageSecurityMode.messageSecurityModeNone,
NULL_BYTE_STRING,
lifetime);
}
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(openSecureChannelRequest.getIdentifier())),
null,
null);
ExtensionObject extObject = new ExtensionObject(
expandedNodeId,
null,
openSecureChannelRequest);
try {
WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
ExtensionObjectIO.staticSerialize(buffer, extObject);
OpcuaOpenRequest openRequest = new OpcuaOpenRequest(FINAL_CHUNK,
0,
new PascalString(this.securityPolicy),
this.publicCertificate,
this.thumbprint,
transactionId,
transactionId,
buffer.getData());
final OpcuaAPU apu;
if (this.isEncrypted) {
apu = OpcuaAPUIO.staticParse(encryptionHandler.encodeMessage(openRequest, buffer.getData()), false);
} else {
apu = new OpcuaAPU(openRequest);
}
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(apu)
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage))
.check(p -> p.getMessage() instanceof OpcuaOpenResponse)
.unwrap(p -> (OpcuaOpenResponse) p.getMessage())
.check(p -> {
if (p.getRequestId() == transactionId) {
return true;
} else {
return false;
}
})
.handle(opcuaOpenResponse -> {
try {
ReadBuffer readBuffer = new ReadBufferByteBased(opcuaOpenResponse.getMessage(), true);
ExtensionObject message = ExtensionObjectIO.staticParse(readBuffer, false);
//Store the initial sequence number from the server. there's no requirement for the server and client to use the same starting number.
senderSequenceNumber = opcuaOpenResponse.getSequenceNumber();
certificateThumbprint = opcuaOpenResponse.getReceiverCertificateThumbprint();
if (message.getBody() instanceof ServiceFault) {
ServiceFault fault = (ServiceFault) message.getBody();
LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
} else {
LOGGER.debug("Got Secure Response Connection Response");
try {
OpenSecureChannelResponse openSecureChannelResponse = (OpenSecureChannelResponse) message.getBody();
tokenId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getTokenId());
channelId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getChannelId());
onConnectCreateSessionRequest(context);
} catch (PlcConnectionException e) {
LOGGER.error("Error occurred while connecting to OPC UA server");
e.printStackTrace();
}
}
} catch (ParseException e) {
e.printStackTrace();
}
});
};
LOGGER.debug("Submitting OpenSecureChannel with id of {}", transactionId);
channelTransactionManager.submit(requestConsumer, transactionId);
} catch (ParseException e) {
LOGGER.error("Unable to to Parse Open Secure Request");
}
}
public void onConnectCreateSessionRequest(ConversationContext<OpcuaAPU> context) throws PlcConnectionException {
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
0L,
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
LocalizedText applicationName = new LocalizedText(
true,
true,
new PascalString("en"),
APPLICATION_TEXT);
PascalString gatewayServerUri = NULL_STRING;
PascalString discoveryProfileUri = NULL_STRING;
int noOfDiscoveryUrls = -1;
PascalString[] discoveryUrls = new PascalString[0];
ApplicationDescription clientDescription = new ApplicationDescription(APPLICATION_URI,
PRODUCT_URI,
applicationName,
ApplicationType.applicationTypeClient,
gatewayServerUri,
discoveryProfileUri,
noOfDiscoveryUrls,
discoveryUrls);
CreateSessionRequest createSessionRequest = new CreateSessionRequest(
requestHeader,
clientDescription,
NULL_STRING,
this.endpoint,
new PascalString(sessionName),
new PascalByteString(clientNonce.length, clientNonce),
NULL_BYTE_STRING,
120000L,
0L);
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(createSessionRequest.getIdentifier())),
null,
null);
ExtensionObject extObject = new ExtensionObject(
expandedNodeId,
null,
createSessionRequest);
try {
WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
ExtensionObjectIO.staticSerialize(buffer, extObject);
Consumer<byte[]> consumer = opcuaResponse -> {
try {
ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false);
if (message.getBody() instanceof ServiceFault) {
ServiceFault fault = (ServiceFault) message.getBody();
LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
} else {
LOGGER.debug("Got Create Session Response Connection Response");
try {
CreateSessionResponse responseMessage;
ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
if (unknownExtensionObject instanceof CreateSessionResponse) {
responseMessage = (CreateSessionResponse) unknownExtensionObject;
authenticationToken = responseMessage.getAuthenticationToken().getNodeId();
onConnectActivateSessionRequest(context, responseMessage, (CreateSessionResponse) message.getBody());
} else {
ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
}
} catch (PlcConnectionException e) {
LOGGER.error("Error occurred while connecting to OPC UA server");
} catch (ParseException e) {
LOGGER.error("Unable to parse the returned Subscription response");
e.printStackTrace();
}
}
} catch (ParseException e) {
e.printStackTrace();
}
};
Consumer<TimeoutException> timeout = e -> {
LOGGER.error("Timeout while waiting for subscription response");
e.printStackTrace();
};
BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
LOGGER.error("Error while waiting for subscription response");
e.printStackTrace();
};
submit(context, timeout, error, consumer, buffer);
} catch (ParseException e) {
LOGGER.error("Unable to to Parse Create Session Request");
}
}
private void onConnectActivateSessionRequest(ConversationContext<OpcuaAPU> context, CreateSessionResponse opcuaMessageResponse, CreateSessionResponse sessionResponse) throws PlcConnectionException {
senderCertificate = sessionResponse.getServerCertificate().getStringValue();
senderNonce = sessionResponse.getServerNonce().getStringValue();
UserTokenType tokenType = UserTokenType.userTokenTypeAnonymous;
for (ExtensionObjectDefinition extensionObject: sessionResponse.getServerEndpoints()) {
EndpointDescription endpointDescription = (EndpointDescription) extensionObject;
if (endpointDescription.getEndpointUrl().getStringValue().equals(this.endpoint.getStringValue())) {
for (ExtensionObjectDefinition userTokenCast : endpointDescription.getUserIdentityTokens()) {
UserTokenPolicy identityToken = (UserTokenPolicy) userTokenCast;
if ((identityToken.getTokenType() == UserTokenType.userTokenTypeAnonymous) && (this.username == null)) {
LOGGER.info("Using Endpoint {} with security {}", endpointDescription.getEndpointUrl().getStringValue(), identityToken.getPolicyId().getStringValue());
policyId = identityToken.getPolicyId();
tokenType = identityToken.getTokenType();
} else if ((identityToken.getTokenType() == UserTokenType.userTokenTypeUserName) && (this.username != null)) {
LOGGER.info("Using Endpoint {} with security {}", endpointDescription.getEndpointUrl().getStringValue(), identityToken.getPolicyId().getStringValue());
policyId = identityToken.getPolicyId();
tokenType = identityToken.getTokenType();
}
}
}
}
ExtensionObject userIdentityToken = getIdentityToken(tokenType, policyId.getStringValue());
int requestHandle = getRequestHandle();
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
requestHandle,
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
SignatureData clientSignature = new SignatureData(NULL_STRING, NULL_BYTE_STRING);
SignedSoftwareCertificate[] signedSoftwareCertificate = new SignedSoftwareCertificate[1];
signedSoftwareCertificate[0] = new SignedSoftwareCertificate(NULL_BYTE_STRING, NULL_BYTE_STRING);
ActivateSessionRequest activateSessionRequest = new ActivateSessionRequest(
requestHeader,
clientSignature,
0,
null,
0,
null,
userIdentityToken,
clientSignature);
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(activateSessionRequest.getIdentifier())),
null,
null);
ExtensionObject extObject = new ExtensionObject(
expandedNodeId,
null,
activateSessionRequest);
try {
WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
ExtensionObjectIO.staticSerialize(buffer, extObject);
Consumer<byte[]> consumer = opcuaResponse -> {
try {
ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false);
if (message.getBody() instanceof ServiceFault) {
ServiceFault fault = (ServiceFault) message.getBody();
LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
} else {
LOGGER.debug("Got Activate Session Response Connection Response");
try {
ActivateSessionResponse responseMessage;
ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
if (unknownExtensionObject instanceof ActivateSessionResponse) {
responseMessage = (ActivateSessionResponse) unknownExtensionObject;
long returnedRequestHandle = ((ResponseHeader) responseMessage.getResponseHeader()).getRequestHandle();
if (!(requestHandle == returnedRequestHandle)) {
LOGGER.error("Request handle isn't as expected, we might have missed a packet. {} != {}", requestHandle, returnedRequestHandle);
}
// Send an event that connection setup is complete.
keepAlive();
context.fireConnected();
} else {
ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
}
} catch (ParseException e) {
LOGGER.error("Unable to parse the returned Subscription response");
e.printStackTrace();
}
}
} catch (ParseException e) {
e.printStackTrace();
}
};
Consumer<TimeoutException> timeout = e -> {
LOGGER.error("Timeout while waiting for activate session response");
e.printStackTrace();
};
BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
LOGGER.error("Error while waiting for activate session response");
e.printStackTrace();
};
submit(context, timeout, error, consumer, buffer);
} catch (ParseException e) {
LOGGER.error("Unable to to Parse Activate Session Request");
}
}
public void onDisconnect(ConversationContext<OpcuaAPU> context) {
LOGGER.info("Disconnecting");
int requestHandle = getRequestHandle();
if (keepAlive != null) {
keepAlive.complete(null);
}
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, 473),
null,
null); //Identifier for OpenSecureChannel
RequestHeader requestHeader = new RequestHeader(
new NodeId(authenticationToken),
getCurrentDateTime(),
requestHandle, //RequestHandle
0L,
NULL_STRING,
5000L,
NULL_EXTENSION_OBJECT);
CloseSessionRequest closeSessionRequest = new CloseSessionRequest(
requestHeader,
true);
ExtensionObject extObject = new ExtensionObject(
expandedNodeId,
null,
closeSessionRequest);
try {
WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
ExtensionObjectIO.staticSerialize(buffer, extObject);
Consumer<byte[]> consumer = opcuaResponse -> {
try {
ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false);
if (message.getBody() instanceof ServiceFault) {
ServiceFault fault = (ServiceFault) message.getBody();
LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
} else {
LOGGER.debug("Got Close Session Response Connection Response");
try {
CloseSessionResponse responseMessage;
ExtensionObjectDefinition unknownExtensionObject = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaResponse, true), false).getBody();
if (unknownExtensionObject instanceof CloseSessionResponse) {
responseMessage = (CloseSessionResponse) unknownExtensionObject;
LOGGER.trace("Got Close Session Response Connection Response" + responseMessage.toString());
onDisconnectCloseSecureChannel(context);
} else {
ServiceFault serviceFault = (ServiceFault) unknownExtensionObject;
ResponseHeader header = (ResponseHeader) serviceFault.getResponseHeader();
LOGGER.error("Subscription ServiceFault returned from server with error code, '{}'", header.getServiceResult().toString());
}
} catch (ParseException e) {
LOGGER.error("Unable to parse the returned Close Session response");
e.printStackTrace();
}
}
} catch (ParseException e) {
e.printStackTrace();
}
};
Consumer<TimeoutException> timeout = e -> {
LOGGER.error("Timeout while waiting for close session response");
e.printStackTrace();
};
BiConsumer<OpcuaAPU, Throwable> error = (message, e) -> {
LOGGER.error("Error while waiting for close session response");
e.printStackTrace();
};
submit(context, timeout, error, consumer, buffer);
} catch (ParseException e) {
LOGGER.error("Unable to to Parse Close Session Request");
}
}
private void onDisconnectCloseSecureChannel(ConversationContext<OpcuaAPU> context) {
int transactionId = channelTransactionManager.getTransactionIdentifier();
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
0L, //RequestHandle
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
CloseSecureChannelRequest closeSecureChannelRequest = new CloseSecureChannelRequest(requestHeader);
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(closeSecureChannelRequest.getIdentifier())),
null,
null);
OpcuaCloseRequest closeRequest = new OpcuaCloseRequest(FINAL_CHUNK,
channelId.get(),
tokenId.get(),
transactionId,
transactionId,
new ExtensionObject(
expandedNodeId,
null,
closeSecureChannelRequest));
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(new OpcuaAPU(closeRequest))
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.check(p -> p.getMessage() instanceof OpcuaMessageResponse)
.unwrap(p -> (OpcuaMessageResponse) p.getMessage())
.check(p -> {
if (p.getRequestId() == transactionId) {
return true;
} else {
return false;
}
})
.handle(opcuaMessageResponse -> {
LOGGER.trace("Got Close Secure Channel Response" + opcuaMessageResponse.toString());
});
context.fireDisconnected();
};
channelTransactionManager.submit(requestConsumer, transactionId);
}
public void onDiscover(ConversationContext<OpcuaAPU> context) {
// Only the TCP transport supports login.
LOGGER.debug("Opcua Driver running in ACTIVE mode, discovering endpoints");
OpcuaHelloRequest hello = new OpcuaHelloRequest(FINAL_CHUNK,
VERSION,
DEFAULT_RECEIVE_BUFFER_SIZE,
DEFAULT_SEND_BUFFER_SIZE,
DEFAULT_MAX_MESSAGE_SIZE,
DEFAULT_MAX_CHUNK_COUNT,
this.endpoint);
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(new OpcuaAPU(hello))
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.check(p -> p.getMessage() instanceof OpcuaAcknowledgeResponse)
.unwrap(p -> (OpcuaAcknowledgeResponse) p.getMessage())
.handle(opcuaAcknowledgeResponse -> {
LOGGER.debug("Got Hello Response Connection Response");
onDiscoverOpenSecureChannel(context, opcuaAcknowledgeResponse);
});
};
channelTransactionManager.submit(requestConsumer, 1);
}
public void onDiscoverOpenSecureChannel(ConversationContext<OpcuaAPU> context, OpcuaAcknowledgeResponse opcuaAcknowledgeResponse) {
int transactionId = channelTransactionManager.getTransactionIdentifier();
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
0L, //RequestHandle
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
OpenSecureChannelRequest openSecureChannelRequest = new OpenSecureChannelRequest(
requestHeader,
VERSION,
SecurityTokenRequestType.securityTokenRequestTypeIssue,
MessageSecurityMode.messageSecurityModeNone,
NULL_BYTE_STRING,
lifetime);
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(openSecureChannelRequest.getIdentifier())),
null,
null);
try {
WriteBufferByteBased buffer = new WriteBufferByteBased(openSecureChannelRequest.getLengthInBytes(), true);
ExtensionObjectIO.staticSerialize(buffer, new ExtensionObject(
expandedNodeId,
null,
openSecureChannelRequest));
OpcuaOpenRequest openRequest = new OpcuaOpenRequest(FINAL_CHUNK,
0,
SECURITY_POLICY_NONE,
NULL_BYTE_STRING,
NULL_BYTE_STRING,
transactionId,
transactionId,
buffer.getData());
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(new OpcuaAPU(openRequest))
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.check(p -> p.getMessage() instanceof OpcuaOpenResponse)
.unwrap(p -> (OpcuaOpenResponse) p.getMessage())
.check(p -> {
if (p.getRequestId() == transactionId) {
return true;
} else {
return false;
}
})
.handle(opcuaOpenResponse -> {
try {
ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaOpenResponse.getMessage(), true), false);
if (message.getBody() instanceof ServiceFault) {
ServiceFault fault = (ServiceFault) message.getBody();
LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
} else {
LOGGER.debug("Got Secure Response Connection Response");
try {
onDiscoverGetEndpointsRequest(context, opcuaOpenResponse, (OpenSecureChannelResponse) message.getBody());
} catch (PlcConnectionException e) {
LOGGER.error("Error occurred while connecting to OPC UA server");
}
}
} catch (ParseException e) {
e.printStackTrace();
}
});
};
channelTransactionManager.submit(requestConsumer, transactionId);
} catch (ParseException e) {
LOGGER.error("Unable to to Parse Create Session Request");
}
}
public void onDiscoverGetEndpointsRequest(ConversationContext<OpcuaAPU> context, OpcuaOpenResponse opcuaOpenResponse, OpenSecureChannelResponse openSecureChannelResponse) throws PlcConnectionException {
certificateThumbprint = opcuaOpenResponse.getReceiverCertificateThumbprint();
tokenId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getTokenId());
channelId.set((int) ((ChannelSecurityToken) openSecureChannelResponse.getSecurityToken()).getChannelId());
int transactionId = channelTransactionManager.getTransactionIdentifier();
Integer nextSequenceNumber = opcuaOpenResponse.getSequenceNumber() + 1;
Integer nextRequestId = opcuaOpenResponse.getRequestId() + 1;
if (!(transactionId == nextSequenceNumber)) {
LOGGER.error("Sequence number isn't as expected, we might have missed a packet. - " + transactionId + " != " + nextSequenceNumber);
throw new PlcConnectionException("Sequence number isn't as expected, we might have missed a packet. - " + transactionId + " != " + nextSequenceNumber);
}
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
0L,
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
GetEndpointsRequest endpointsRequest = new GetEndpointsRequest(
requestHeader,
this.endpoint,
0,
null,
0,
null);
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(endpointsRequest.getIdentifier())),
null,
null);
try {
WriteBufferByteBased buffer = new WriteBufferByteBased(endpointsRequest.getLengthInBytes(), true);
ExtensionObjectIO.staticSerialize(buffer, new ExtensionObject(
expandedNodeId,
null,
endpointsRequest));
OpcuaMessageRequest messageRequest = new OpcuaMessageRequest(FINAL_CHUNK,
channelId.get(),
tokenId.get(),
nextSequenceNumber,
nextRequestId,
buffer.getData());
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(new OpcuaAPU(messageRequest))
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.check(p -> p.getMessage() instanceof OpcuaMessageResponse)
.unwrap(p -> (OpcuaMessageResponse) p.getMessage())
.check(p -> {
if (p.getRequestId() == transactionId) {
return true;
} else {
return false;
}
})
.handle(opcuaMessageResponse -> {
try {
ExtensionObject message = ExtensionObjectIO.staticParse(new ReadBufferByteBased(opcuaMessageResponse.getMessage(), true), false);
if (message.getBody() instanceof ServiceFault) {
ServiceFault fault = (ServiceFault) message.getBody();
LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
} else {
LOGGER.debug("Got Create Session Response Connection Response");
GetEndpointsResponse response = (GetEndpointsResponse) message.getBody();
EndpointDescription[] endpoints = (EndpointDescription[]) response.getEndpoints();
for (EndpointDescription endpoint : endpoints) {
if (endpoint.getEndpointUrl().getStringValue().equals(this.endpoint.getStringValue()) && endpoint.getSecurityPolicyUri().getStringValue().equals(this.securityPolicy)) {
LOGGER.info("Found OPC UA endpoint {}", this.endpoint.getStringValue());
this.configuration.setSenderCertificate(endpoint.getServerCertificate().getStringValue());
}
}
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
byte[] digest = messageDigest.digest(this.configuration.getSenderCertificate());
this.configuration.setThumbprint(new PascalByteString(digest.length, digest));
} catch (NoSuchAlgorithmException e) {
LOGGER.error("Failed to find hashing algorithm");
}
onDiscoverCloseSecureChannel(context, response);
}
} catch (ParseException e) {
e.printStackTrace();
}
});
};
channelTransactionManager.submit(requestConsumer, transactionId);
} catch (ParseException e) {
LOGGER.error("Unable to to Parse Create Session Request");
}
}
private void onDiscoverCloseSecureChannel(ConversationContext<OpcuaAPU> context, GetEndpointsResponse message) {
int transactionId = channelTransactionManager.getTransactionIdentifier();
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
0L, //RequestHandle
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
CloseSecureChannelRequest closeSecureChannelRequest = new CloseSecureChannelRequest(requestHeader);
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(closeSecureChannelRequest.getIdentifier())),
null,
null);
OpcuaCloseRequest closeRequest = new OpcuaCloseRequest(FINAL_CHUNK,
channelId.get(),
tokenId.get(),
transactionId,
transactionId,
new ExtensionObject(
expandedNodeId,
null,
closeSecureChannelRequest));
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(new OpcuaAPU(closeRequest))
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.check(p -> p.getMessage() instanceof OpcuaMessageResponse)
.unwrap(p -> (OpcuaMessageResponse) p.getMessage())
.check(p -> {
if (p.getRequestId() == transactionId) {
return true;
} else {
return false;
}
})
.handle(opcuaMessageResponse -> {
LOGGER.trace("Got Close Secure Channel Response" + opcuaMessageResponse.toString());
// Send an event that connection setup is complete.
context.fireDiscovered(this.configuration);
});
};
channelTransactionManager.submit(requestConsumer, transactionId);
}
private void keepAlive() {
keepAlive = CompletableFuture.supplyAsync(() -> {
while(true) {
try {
Thread.sleep((long) Math.ceil(this.lifetime * 0.75f));
} catch (InterruptedException e) {
LOGGER.trace("Interrupted Exception");
}
int transactionId = channelTransactionManager.getTransactionIdentifier();
RequestHeader requestHeader = new RequestHeader(new NodeId(authenticationToken),
getCurrentDateTime(),
0L, //RequestHandle
0L,
NULL_STRING,
REQUEST_TIMEOUT_LONG,
NULL_EXTENSION_OBJECT);
OpenSecureChannelRequest openSecureChannelRequest = null;
if (this.isEncrypted) {
openSecureChannelRequest = new OpenSecureChannelRequest(
requestHeader,
VERSION,
SecurityTokenRequestType.securityTokenRequestTypeIssue,
MessageSecurityMode.messageSecurityModeSignAndEncrypt,
new PascalByteString(clientNonce.length, clientNonce),
lifetime);
} else {
openSecureChannelRequest = new OpenSecureChannelRequest(
requestHeader,
VERSION,
SecurityTokenRequestType.securityTokenRequestTypeIssue,
MessageSecurityMode.messageSecurityModeNone,
NULL_BYTE_STRING,
lifetime);
}
ExpandedNodeId expandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, Integer.valueOf(openSecureChannelRequest.getIdentifier())),
null,
null);
ExtensionObject extObject = new ExtensionObject(
expandedNodeId,
null,
openSecureChannelRequest);
try {
WriteBufferByteBased buffer = new WriteBufferByteBased(extObject.getLengthInBytes(), true);
ExtensionObjectIO.staticSerialize(buffer, extObject);
OpcuaOpenRequest openRequest = new OpcuaOpenRequest(FINAL_CHUNK,
0,
new PascalString(this.securityPolicy),
this.publicCertificate,
this.thumbprint,
transactionId,
transactionId,
buffer.getData());
final OpcuaAPU apu;
if (this.isEncrypted) {
apu = OpcuaAPUIO.staticParse(encryptionHandler.encodeMessage(openRequest, buffer.getData()), false);
} else {
apu = new OpcuaAPU(openRequest);
}
Consumer<Integer> requestConsumer = t -> {
context.sendRequest(apu)
.expectResponse(OpcuaAPU.class, REQUEST_TIMEOUT)
.unwrap(apuMessage -> encryptionHandler.decodeMessage(apuMessage))
.check(p -> p.getMessage() instanceof OpcuaOpenResponse)
.unwrap(p -> (OpcuaOpenResponse) p.getMessage())
.check(p -> {
if (p.getRequestId() == transactionId) {
return true;
} else {
return false;
}
})
.handle(opcuaOpenResponse -> {
try {
ReadBufferByteBased readBuffer = new ReadBufferByteBased(opcuaOpenResponse.getMessage(), true);
ExtensionObject message = ExtensionObjectIO.staticParse(readBuffer, false);
if (message.getBody() instanceof ServiceFault) {
ServiceFault fault = (ServiceFault) message.getBody();
LOGGER.error("Failed to connect to opc ua server for the following reason:- {}, {}", ((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode(), OpcuaStatusCode.enumForValue(((ResponseHeader) fault.getResponseHeader()).getServiceResult().getStatusCode()));
} else {
LOGGER.debug("Got Secure Response Connection Response");
OpenSecureChannelResponse openSecureChannelResponse = (OpenSecureChannelResponse) message.getBody();
ChannelSecurityToken token = (ChannelSecurityToken) openSecureChannelResponse.getSecurityToken();
certificateThumbprint = opcuaOpenResponse.getReceiverCertificateThumbprint();
tokenId.set((int) token.getTokenId());
channelId.set((int) token.getChannelId());
lifetime = token.getRevisedLifetime();
}
} catch (ParseException e) {
e.printStackTrace();
}
});
};
channelTransactionManager.submit(requestConsumer, transactionId);
} catch (ParseException e) {
LOGGER.error("Unable to to Parse Open Secure Request");
}
}
}
);
}
/**
* Returns the next request handle
*
* @return the next sequential request handle
*/
public int getRequestHandle() {
int transactionId = requestHandleGenerator.getAndIncrement();
if(requestHandleGenerator.get() == SecureChannelTransactionManager.DEFAULT_MAX_REQUEST_ID) {
requestHandleGenerator.set(1);
}
return transactionId;
}
/**
* Returns the authentication token for the current connection
*
* @return a NodeId Authentication token
*/
public NodeId getAuthenticationToken() {
return new NodeId(this.authenticationToken);
}
/**
* Gets the Channel identifier for the current channel
*
* @return int representing the channel identifier
*/
public int getChannelId() {
return this.channelId.get();
}
/**
* Gets the Token Identifier
*
* @return int representing the token identifier
*/
public int getTokenId() {
return this.tokenId.get();
}
/**
* Creates an IdentityToken to authenticate with a server.
* @param securityPolicy
* @return returns an ExtensionObject with an IdentityToken.
*/
private ExtensionObject getIdentityToken(UserTokenType tokenType, String securityPolicy) {
ExpandedNodeId extExpandedNodeId = null;
ExtensionObject userIdentityToken = null;
switch (tokenType) {
case userTokenTypeAnonymous:
//If we aren't using authentication tell the server we would like to login anonymously
AnonymousIdentityToken anonymousIdentityToken = new AnonymousIdentityToken();
extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, OpcuaNodeIdServices.AnonymousIdentityToken_Encoding_DefaultBinary.getValue()),
null,
null);
return new ExtensionObject(
extExpandedNodeId,
new ExtensionObjectEncodingMask(false, false, true),
new UserIdentityToken(new PascalString(securityPolicy), anonymousIdentityToken));
case userTokenTypeUserName:
//Encrypt the password using the server nonce and server public key
byte[] passwordBytes = this.password.getBytes();
ByteBuffer encodeableBuffer = ByteBuffer.allocate(4 + passwordBytes.length + this.senderNonce.length);
encodeableBuffer.order(ByteOrder.LITTLE_ENDIAN);
encodeableBuffer.putInt(passwordBytes.length + this.senderNonce.length);
encodeableBuffer.put(passwordBytes);
encodeableBuffer.put(this.senderNonce);
byte[] encodeablePassword = new byte[4 + passwordBytes.length + this.senderNonce.length];
encodeableBuffer.position(0);
encodeableBuffer.get(encodeablePassword);
byte[] encryptedPassword = encryptionHandler.encryptPassword(encodeablePassword);
UserNameIdentityToken userNameIdentityToken = new UserNameIdentityToken(
new PascalString(this.username),
new PascalByteString(encryptedPassword.length, encryptedPassword),
new PascalString(PASSWORD_ENCRYPTION_ALGORITHM)
);
extExpandedNodeId = new ExpandedNodeId(false, //Namespace Uri Specified
false, //Server Index Specified
new NodeIdFourByte((short) 0, OpcuaNodeIdServices.UserNameIdentityToken_Encoding_DefaultBinary.getValue()),
NULL_STRING,
1L);
return new ExtensionObject(
extExpandedNodeId,
new ExtensionObjectEncodingMask(false, false, true),
new UserIdentityToken(new PascalString(securityPolicy), userNameIdentityToken));
}
return null;
}
public static long getCurrentDateTime() {
return (System.currentTimeMillis() * 10000) + EPOCH_OFFSET;
}
}