Skip to content

Commit

Permalink
refactor(plc4j/opcua): reworked configuration/added driver context
Browse files Browse the repository at this point in the history
  • Loading branch information
sruehl committed Aug 9, 2023
1 parent 943e4a1 commit 3ac65c0
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 403 deletions.
Expand Up @@ -19,41 +19,23 @@
package org.apache.plc4x.java.opcua;

import io.netty.buffer.ByteBuf;
import org.apache.plc4x.java.api.PlcConnection;
import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
import org.apache.plc4x.java.opcua.config.OpcuaConfiguration;
import org.apache.plc4x.java.opcua.context.OpcuaDriverContext;
import org.apache.plc4x.java.opcua.optimizer.OpcuaOptimizer;
import org.apache.plc4x.java.opcua.protocol.OpcuaProtocolLogic;
import org.apache.plc4x.java.opcua.readwrite.OpcuaAPU;
import org.apache.plc4x.java.opcua.tag.OpcuaPlcTagHandler;
import org.apache.plc4x.java.opcua.tag.OpcuaTag;
import org.apache.plc4x.java.spi.configuration.Configuration;
import org.apache.plc4x.java.spi.configuration.ConfigurationFactory;
import org.apache.plc4x.java.spi.connection.*;
import org.apache.plc4x.java.spi.transport.Transport;
import org.apache.plc4x.java.spi.connection.GeneratedDriverBase;
import org.apache.plc4x.java.spi.connection.ProtocolStackConfigurer;
import org.apache.plc4x.java.spi.connection.SingleProtocolStackConfigurer;
import org.apache.plc4x.java.spi.values.PlcValueHandler;

import java.util.ServiceLoader;
import java.util.function.ToIntFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.apache.plc4x.java.spi.configuration.ConfigurationFactory.configure;

public class OpcuaPlcDriver extends GeneratedDriverBase<OpcuaAPU> {

public static final Pattern INET_ADDRESS_PATTERN = Pattern.compile("(:(?<transportCode>[a-z0-9]*))?://" +
"(?<transportHost>[\\w.-]+)(:" +
"(?<transportPort>\\d*))?");

public static final Pattern URI_PATTERN = Pattern.compile("^(?<protocolCode>opcua)" +
INET_ADDRESS_PATTERN +
"(?<transportEndpoint>[\\w/=]*)[?]?" +
"(?<paramString>([^=]+=[^=&]+&?)*)"
);

private boolean isEncrypted;

@Override
public String getProtocolCode() {
return "opcua";
Expand All @@ -74,11 +56,6 @@ protected String getDefaultTransport() {
return "tcp";
}

@Override
protected boolean fireDiscoverEvent() {
return isEncrypted;
}

@Override
protected boolean canRead() {
return true;
Expand Down Expand Up @@ -119,122 +96,11 @@ protected ProtocolStackConfigurer<OpcuaAPU> getStackConfigurer() {
.withProtocol(OpcuaProtocolLogic.class)
.withPacketSizeEstimator(ByteLengthEstimator.class)
.withParserArgs(true)
.withDriverContext(OpcuaDriverContext.class)
.littleEndian()
.build();
}

@Override
public PlcConnection getConnection(String connectionString) throws PlcConnectionException {
// Split up the connection string into it's individual segments.
Matcher matcher = URI_PATTERN.matcher(connectionString);
if (!matcher.matches()) {
throw new PlcConnectionException(
"Connection string doesn't match the format '{protocol-code}:({transport-code})?//{transport-host}(:{transport-port})(/{transport-endpoint})(?{parameter-string)?'");
}
final String protocolCode = matcher.group("protocolCode");
final String transportCode = (matcher.group("transportCode") != null) ?
matcher.group("transportCode") : getDefaultTransport();
final String transportHost = matcher.group("transportHost");
final String transportPort = matcher.group("transportPort");
final String transportEndpoint = matcher.group("transportEndpoint");
final String paramString = matcher.group("paramString");

// Check if the protocol code matches this driver.
if (!protocolCode.equals(getProtocolCode())) {
// Actually this shouldn't happen as the DriverManager should have not used this driver in the first place.
throw new PlcConnectionException(
"This driver is not suited to handle this connection string");
}

// Create the configuration object.
OpcuaConfiguration configuration = (OpcuaConfiguration) new ConfigurationFactory().createConfiguration(
getConfigurationType(), paramString);
if (configuration == null) {
throw new PlcConnectionException("Unsupported configuration");
}
configuration.setTransportCode(transportCode);
configuration.setHost(transportHost);
configuration.setPort(transportPort);
configuration.setTransportEndpoint(transportEndpoint);
String portAddition = transportPort != null ? ":" + transportPort : "";
configuration.setEndpoint("opc." + transportCode + "://" + transportHost + portAddition + transportEndpoint);

// Try to find a transport in order to create a communication channel.
Transport transport = null;
ServiceLoader<Transport> transportLoader = ServiceLoader.load(
Transport.class, Thread.currentThread().getContextClassLoader());
for (Transport curTransport : transportLoader) {
if (curTransport.getTransportCode().equals(transportCode)) {
transport = curTransport;
break;
}
}
if (transport == null) {
throw new PlcConnectionException("Unsupported transport " + transportCode);
}

// Inject the configuration into the transport.
configure(configuration, transport);

// Create an instance of the communication channel which the driver should use.
ChannelFactory channelFactory = transport.createChannelFactory(transportHost + ":" + transportPort);
if (channelFactory == null) {
throw new PlcConnectionException("Unable to get channel factory from url " + transportHost + ":" + transportPort);
}
configure(configuration, channelFactory);

// Give drivers the option to customize the channel.
initializePipeline(channelFactory);

// Make the "fire discover event" overridable via system property.
boolean fireDiscoverEvent = fireDiscoverEvent();
if(System.getProperty(PROPERTY_PLC4X_FORCE_FIRE_DISCOVER_EVENT) != null) {
fireDiscoverEvent = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_FIRE_DISCOVER_EVENT));
}

// Make the "await setup complete" overridable via system property.
boolean awaitSetupComplete = awaitSetupComplete();
if (System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_SETUP_COMPLETE) != null) {
awaitSetupComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_SETUP_COMPLETE));
}

// Make the "await disconnect complete" overridable via system property.
boolean awaitDisconnectComplete = awaitDisconnectComplete();
if (System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE) != null) {
awaitDisconnectComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCONNECT_COMPLETE));
}

if (configuration.getSecurityPolicy() != null && !(configuration.getSecurityPolicy().equals("None"))) {
try {
configuration.openKeyStore();
} catch (Exception e) {
throw new PlcConnectionException("Unable to open keystore, please confirm you have the correct permissions");
}
}

this.isEncrypted = configuration.isEncrypted();

// Make the "await disconnect complete" overridable via system property.
boolean awaitDiscoverComplete = awaitDiscoverComplete();
if (System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE) != null) {
awaitDiscoverComplete = Boolean.parseBoolean(System.getProperty(PROPERTY_PLC4X_FORCE_AWAIT_DISCOVER_COMPLETE));
}

return new DefaultNettyPlcConnection(
canRead(), canWrite(), canSubscribe(), canBrowse(),
getTagHandler(),
getValueHandler(),
configuration,
channelFactory,
fireDiscoverEvent,
awaitSetupComplete,
awaitDisconnectComplete,
awaitDiscoverComplete,
getStackConfigurer(),
getOptimizer(),
null);
}

/**
* Estimate the Length of a Packet
*/
Expand Down
Expand Up @@ -18,47 +18,22 @@
*/
package org.apache.plc4x.java.opcua.config;

import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
import org.apache.plc4x.java.opcua.context.CertificateGenerator;
import org.apache.plc4x.java.opcua.context.CertificateKeyPair;
import org.apache.plc4x.java.opcua.readwrite.PascalByteString;
import org.apache.plc4x.java.spi.configuration.Configuration;
import org.apache.plc4x.java.spi.configuration.annotations.ConfigurationParameter;
import org.apache.plc4x.java.spi.configuration.annotations.defaults.BooleanDefaultValue;
import org.apache.plc4x.java.spi.configuration.annotations.defaults.StringDefaultValue;
import org.apache.plc4x.java.transport.tcp.TcpTransportConfiguration;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.file.FileSystems;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.X509Certificate;

public class OpcuaConfiguration implements Configuration, TcpTransportConfiguration {

static {
// Required for SecurityPolicy.Aes256_Sha256_RsaPss
Security.addProvider(new BouncyCastleProvider());
}
@ConfigurationParameter("protocolCode")
private String protocolCode;

private static final Logger LOGGER = LoggerFactory.getLogger(OpcuaConfiguration.class);
@ConfigurationParameter("transportCode")
private String transportCode;

private String code;
private String host;
private String port;
private String endpoint;
private String transportEndpoint;
private String params;
private Boolean isEncrypted = false;
private PascalByteString thumbprint;
private byte[] senderCertificate;
@ConfigurationParameter("transportConfig")
private String transportConfig;

@ConfigurationParameter("discovery")
@BooleanDefaultValue(true)
Expand All @@ -83,7 +58,17 @@ public class OpcuaConfiguration implements Configuration, TcpTransportConfigurat
@ConfigurationParameter("keyStorePassword")
private String keyStorePassword;

private CertificateKeyPair ckp;
public String getProtocolCode() {
return protocolCode;
}

public String getTransportCode() {
return transportCode;
}

public String getTransportConfig() {
return transportConfig;
}

public boolean isDiscovery() {
return discovery;
Expand Down Expand Up @@ -113,128 +98,18 @@ public String getKeyStorePassword() {
return keyStorePassword;
}

public PascalByteString getThumbprint() {
return thumbprint;
}

public CertificateKeyPair getCertificateKeyPair() {
return ckp;
}

public boolean isEncrypted() {
return isEncrypted;
}

public void setDiscovery(boolean discovery) {
this.discovery = discovery;
}

public void setUsername(String username) {
this.username = username;
}

public void setPassword(String password) {
this.password = password;
}

public void setCertDirectory(String certDirectory) {
this.certDirectory = certDirectory;
}

public void setSecurityPolicy(String securityPolicy) {
this.securityPolicy = securityPolicy;
}

public void setKeyStoreFile(String keyStoreFile) {
this.keyStoreFile = keyStoreFile;
}

public void setKeyStorePassword(String keyStorePassword) {
this.keyStorePassword = keyStorePassword;
}

public void setThumbprint(PascalByteString thumbprint) {
this.thumbprint = thumbprint;
}

public String getTransportCode() {
return code;
}

public String getHost() {
return host;
}

public String getPort() {
return port;
}

public String getEndpoint() {
return endpoint;
}

public String getTransportEndpoint() {
return transportEndpoint;
}

public byte[] getSenderCertificate() {
return this.senderCertificate;
}

public void setTransportCode(String code) {
this.code = code;
}

public void setHost(String host) {
this.host = host;
}

public void setPort(String port) {
this.port = port;
}

public void setEndpoint(String endpoint) {
this.endpoint = endpoint;
}

public void setTransportEndpoint(String transportEndpoint) {
this.transportEndpoint = transportEndpoint;
}

public void openKeyStore() throws Exception {
this.isEncrypted = true;
File securityTempDir = new File(certDirectory, "security");
if (!securityTempDir.exists() && !securityTempDir.mkdirs()) {
throw new PlcConnectionException("Unable to create directory please confirm folder permissions on " + certDirectory);
}
KeyStore keyStore = KeyStore.getInstance("PKCS12");
File serverKeyStore = securityTempDir.toPath().resolve(keyStoreFile).toFile();

File pkiDir = FileSystems.getDefault().getPath(certDirectory).resolve("pki").toFile();
if (!serverKeyStore.exists()) {
ckp = CertificateGenerator.generateCertificate();
LOGGER.info("Creating new KeyStore at {}", serverKeyStore);
keyStore.load(null, keyStorePassword.toCharArray());
keyStore.setKeyEntry("plc4x-certificate-alias", ckp.getKeyPair().getPrivate(), keyStorePassword.toCharArray(), new X509Certificate[]{ckp.getCertificate()});
keyStore.store(new FileOutputStream(serverKeyStore), keyStorePassword.toCharArray());
} else {
LOGGER.info("Loading KeyStore at {}", serverKeyStore);
keyStore.load(new FileInputStream(serverKeyStore), keyStorePassword.toCharArray());
String alias = keyStore.aliases().nextElement();
KeyPair kp = new KeyPair(keyStore.getCertificate(alias).getPublicKey(),
(PrivateKey) keyStore.getKey(alias, keyStorePassword.toCharArray()));
ckp = new CertificateKeyPair(kp, (X509Certificate) keyStore.getCertificate(alias));
}
}

@Override
public String toString() {
return "Configuration{" + '}';
return "OpcuaConfiguration{" +
"discovery=" + discovery +
", username='" + username + '\'' +
", password='" + (password != null ? "******" : null) + '\'' +
", securityPolicy='" + securityPolicy + '\'' +
", keyStoreFile='" + keyStoreFile + '\'' +
", certDirectory='" + certDirectory + '\'' +
", keyStorePassword='" + (keyStorePassword != null ? "******" : null) + '\'' +
'}';
}

public void setSenderCertificate(byte[] certificate) {
this.senderCertificate = certificate;
}

}

0 comments on commit 3ac65c0

Please sign in to comment.