Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 7 additions & 20 deletions plc4j/tools/opcua-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,18 @@
</properties>

<dependencies>
<!-- Milo OPC UA Dependencies -->
<!-- Milo OPC UA Dependencies (1.1.x). milo-sdk-server transitively brings
milo-sdk-core / milo-stack-core; milo-transport provides the
OpcTcpServerTransport used to bind the server in 1.x. -->
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>dictionary-manager</artifactId>
<version>0.6.16</version>
<artifactId>milo-sdk-server</artifactId>
<version>1.1.8</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-server</artifactId>
<version>0.6.16</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>stack-server</artifactId>
<version>0.6.16</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>stack-core</artifactId>
<version>0.6.16</version>
</dependency>
<dependency>
<groupId>org.eclipse.milo</groupId>
<artifactId>sdk-core</artifactId>
<version>0.6.16</version>
<artifactId>milo-transport</artifactId>
<version>1.1.8</version>
</dependency>

<!-- Parsing Libraries -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.FileSystems;

Expand All @@ -37,30 +38,39 @@
import org.apache.plc4x.java.opcuaserver.context.CertificateKeyPair;
import org.apache.plc4x.java.opcuaserver.context.CertificateGenerator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.milo.opcua.sdk.server.EndpointConfig;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig;
import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig;
import org.eclipse.milo.opcua.sdk.server.identity.AnonymousIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.CompositeValidator;
import org.eclipse.milo.opcua.sdk.server.identity.UsernameIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.X509IdentityValidator;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaRuntimeException;
import org.eclipse.milo.opcua.stack.core.security.DefaultApplicationGroup;
import org.eclipse.milo.opcua.stack.core.security.DefaultCertificateManager;
import org.eclipse.milo.opcua.stack.core.security.DefaultTrustListManager;
import org.eclipse.milo.opcua.stack.core.security.DefaultServerCertificateValidator;
import org.eclipse.milo.opcua.stack.core.security.FileBasedCertificateQuarantine;
import org.eclipse.milo.opcua.stack.core.security.FileBasedTrustListManager;
import org.eclipse.milo.opcua.stack.core.security.KeyStoreCertificateStore;
import org.eclipse.milo.opcua.stack.core.security.RsaSha256CertificateFactory;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.transport.TransportProfile;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo;
import org.eclipse.milo.opcua.stack.core.util.CertificateUtil;
import org.eclipse.milo.opcua.stack.server.EndpointConfiguration;
import org.eclipse.milo.opcua.stack.server.security.DefaultServerCertificateValidator;
import org.eclipse.milo.opcua.stack.transport.server.tcp.OpcTcpServerTransport;
import org.eclipse.milo.opcua.stack.transport.server.tcp.OpcTcpServerTransportConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS;
import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME;
import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_X509;
import static org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS;
import static org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME;
import static org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig.USER_TOKEN_POLICY_X509;

import org.apache.commons.cli.*;

Expand Down Expand Up @@ -210,6 +220,7 @@ public static void main(String[] args) throws Exception {
private final OpcUaServer server;
private final Plc4xNamespace plc4xNamespace;
private final String certificateFileName = "plc4x-opcuaserver.pfx";
private final String certificateAlias = "plc4x-certificate-alias";

public OPCUAServer(String[] args) throws Exception {

Expand All @@ -222,14 +233,27 @@ public OPCUAServer(String[] args) throws Exception {
}
logger.info("Security Directory is: {}", securityTempDir.getAbsolutePath()); //

// 1.x cert/transport bootstrap follows Milo's ExampleServer pattern.
File pkiDir = FileSystems.getDefault().getPath(config.getDir()).resolve("pki").toFile();
DefaultTrustListManager trustListManager = new DefaultTrustListManager(pkiDir);
// Keep the directory layout of the Milo 0.6 trust list ("issuers", rejected certificates directly in
// "rejected") instead of the 1.x default ("issuer", "rejected/certs"), so existing installations keep
// their issuer certificates and CRLs.
Path issuerCertsDir = Files.createDirectories(pkiDir.toPath().resolve("issuers").resolve("certs"));
Path issuerCrlDir = Files.createDirectories(pkiDir.toPath().resolve("issuers").resolve("crl"));
Path trustedCertsDir = Files.createDirectories(pkiDir.toPath().resolve("trusted").resolve("certs"));
Path trustedCrlDir = Files.createDirectories(pkiDir.toPath().resolve("trusted").resolve("crl"));
FileBasedTrustListManager trustListManager =
new FileBasedTrustListManager(issuerCertsDir, issuerCrlDir, trustedCertsDir, trustedCrlDir);
trustListManager.initialize();
logger.info("Certificate directory is: {}, Please move certificates from the reject dir to the trusted directory to allow encrypted access", pkiDir.getAbsolutePath());

DefaultServerCertificateValidator certificateValidator = new DefaultServerCertificateValidator(trustListManager);
FileBasedCertificateQuarantine certificateQuarantine =
FileBasedCertificateQuarantine.create(pkiDir.toPath().resolve("rejected"));

DefaultServerCertificateValidator certificateValidator =
new DefaultServerCertificateValidator(trustListManager, certificateQuarantine);

UsernameIdentityValidator identityValidator = new UsernameIdentityValidator(
true,
authChallenge -> {
boolean check = passwordConfig.checkPassword(authChallenge.getUsername(), authChallenge.getPassword());
if (!check) {
Expand All @@ -254,7 +278,7 @@ public OPCUAServer(String[] args) throws Exception {
certificate = CertificateGenerator.generateCertificate();
logger.info("Creating new KeyStore at {}", serverKeyStore);
keyStore.load(null, passwordConfig.getSecurityPassword().toCharArray());
keyStore.setKeyEntry("plc4x-certificate-alias", certificate.getKeyPair().getPrivate(), passwordConfig.getSecurityPassword().toCharArray(), new X509Certificate[] { certificate.getCertificate() });
keyStore.setKeyEntry(certificateAlias, certificate.getKeyPair().getPrivate(), passwordConfig.getSecurityPassword().toCharArray(), new X509Certificate[] { certificate.getCertificate() });
keyStore.store(new FileOutputStream(serverKeyStore), passwordConfig.getSecurityPassword().toCharArray());
} else {
logger.info("Loading KeyStore at {}", serverKeyStore);
Expand All @@ -271,22 +295,27 @@ public OPCUAServer(String[] args) throws Exception {
StatusCodes.Bad_ConfigurationError,
"certificate is missing the application URI"));

Set<EndpointConfiguration> endpointConfigurations = new LinkedHashSet<>();
Set<EndpointConfig> endpointConfigurations = new LinkedHashSet<>();

String hostname = InetAddress.getLocalHost().getHostName();

EndpointConfiguration.Builder builder = EndpointConfiguration.newBuilder()
EndpointConfig.Builder builder = EndpointConfig.newBuilder()
.setBindAddress("0.0.0.0")
.setHostname(hostname)
.setPath("/plc4x")
.setCertificate(certificate.getCertificate())
.setBindPort(config.getTcpPort())
.setTransportProfile(TransportProfile.TCP_UASC_UABINARY)
.setSecurityMode(MessageSecurityMode.None)
.addTokenPolicies(
USER_TOKEN_POLICY_ANONYMOUS,
USER_TOKEN_POLICY_USERNAME,
USER_TOKEN_POLICY_X509);

// Anonymous clients can read and write every configured tag, so they are only offered when asked for.
if (config.getAllowAnonymousAccess()) {
builder.addTokenPolicies(USER_TOKEN_POLICY_ANONYMOUS);
}

endpointConfigurations.add(
builder.copy()
.setSecurityPolicy(SecurityPolicy.Basic256Sha256)
Expand All @@ -302,22 +331,22 @@ public OPCUAServer(String[] args) throws Exception {
.build()
);

EndpointConfiguration.Builder discoveryBuilder = builder.copy()
EndpointConfig.Builder discoveryBuilder = builder.copy()
.setPath("/discovery")
.setSecurityPolicy(SecurityPolicy.None)
.setSecurityMode(MessageSecurityMode.None);

endpointConfigurations.add(discoveryBuilder.build());

EndpointConfiguration.Builder discoveryLocalBuilder = builder.copy()
EndpointConfig.Builder discoveryLocalBuilder = builder.copy()
.setPath("/discovery")
.setHostname("127.0.0.1")
.setSecurityPolicy(SecurityPolicy.None)
.setSecurityMode(MessageSecurityMode.None);

endpointConfigurations.add(discoveryLocalBuilder.build());

EndpointConfiguration.Builder discoveryLocalPlc4xBuilder = builder.copy()
EndpointConfig.Builder discoveryLocalPlc4xBuilder = builder.copy()
.setPath("/plc4x/discovery")
.setHostname("127.0.0.1")
.setSecurityPolicy(SecurityPolicy.None)
Expand All @@ -326,26 +355,64 @@ public OPCUAServer(String[] args) throws Exception {
endpointConfigurations.add(discoveryLocalPlc4xBuilder.build());

if (!config.getDisableInsecureEndpoint()) {
EndpointConfiguration.Builder noSecurityBuilder = builder.copy()
EndpointConfig.Builder noSecurityBuilder = builder.copy()
.setSecurityPolicy(SecurityPolicy.None)
.setTransportProfile(TransportProfile.TCP_UASC_UABINARY);
endpointConfigurations.add(noSecurityBuilder.build());
}

//Always add an unsecured endpoint to localhost, this is a work around for Milo throwing an exception if it isn't here.
EndpointConfiguration.Builder noSecurityBuilder = builder.copy()
EndpointConfig.Builder noSecurityBuilder = builder.copy()
.setSecurityPolicy(SecurityPolicy.None)
.setHostname("127.0.0.1")
.setTransportProfile(TransportProfile.TCP_UASC_UABINARY)
.setSecurityMode(MessageSecurityMode.None);
endpointConfigurations.add(noSecurityBuilder.build());

DefaultCertificateManager certificateManager = new DefaultCertificateManager(
certificate.getKeyPair(),
Arrays.stream(keyStore.getCertificateChain(keyStore.getCertificateAlias(certificate.getCertificate())))// Added so that existing certificates are loaded on startup
// In Milo 1.x the application's own certificate/key live in a CertificateStore, and the
// CertificateManager is assembled from a quarantine + an application group. We reuse the
// key pair and chain already loaded from the PKCS#12 keystore above.
final KeyPair serverKeyPair = certificate.getKeyPair();
final String serverCertificateAlias = keyStore.getCertificateAlias(certificate.getCertificate());
final X509Certificate[] serverCertificateChain =
Arrays.stream(keyStore.getCertificateChain(serverCertificateAlias))
.map(X509Certificate.class::cast)
.toArray(X509Certificate[]::new)
);
.toArray(X509Certificate[]::new);

KeyStoreCertificateStore certificateStore = new KeyStoreCertificateStore(
new KeyStoreCertificateStore.Settings(
serverKeyStore.toPath(),
() -> passwordConfig.getSecurityPassword().toCharArray(),
alias -> passwordConfig.getSecurityPassword().toCharArray())) {
// Milo 1.x looks the server certificate up as "server-rsa-sha256". Point it at the entry loaded
// above instead (the first alias of an existing keystore, as before, or certificateAlias for a
// new one), so an existing keystore is used as is instead of gaining a second entry.
@Override
protected String getAlias(NodeId certificateTypeId) {
return NodeIds.RsaSha256ApplicationCertificateType.equals(certificateTypeId)
? serverCertificateAlias
: super.getAlias(certificateTypeId);
}
};
certificateStore.initialize();

RsaSha256CertificateFactory certificateFactory = new RsaSha256CertificateFactory() {
@Override
protected KeyPair createRsaSha256KeyPair() {
return serverKeyPair;
}

@Override
protected X509Certificate[] createRsaSha256CertificateChain(KeyPair keyPair) {
return serverCertificateChain;
}
};

DefaultApplicationGroup defaultApplicationGroup = DefaultApplicationGroup.createAndInitialize(
trustListManager, certificateStore, certificateFactory, certificateValidator);

DefaultCertificateManager certificateManager =
new DefaultCertificateManager(certificateQuarantine, defaultApplicationGroup);

OpcUaServerConfig serverConfig = OpcUaServerConfig.builder()
.setApplicationUri(applicationUri)
Expand All @@ -359,13 +426,16 @@ public OPCUAServer(String[] args) throws Exception {
OpcUaServer.SDK_VERSION,
"", DateTime.now()))
.setCertificateManager(certificateManager)
.setTrustListManager(trustListManager)
.setCertificateValidator(certificateValidator)
.setIdentityValidator(new CompositeValidator(identityValidator, x509IdentityValidator))
.setIdentityValidator(config.getAllowAnonymousAccess()
? new CompositeValidator(AnonymousIdentityValidator.INSTANCE, identityValidator, x509IdentityValidator)
: new CompositeValidator(identityValidator, x509IdentityValidator))
.setProductUri("urn:eclipse:milo:plc4x:server")
.build();

server = new OpcUaServer(serverConfig);
server = new OpcUaServer(serverConfig, transportProfile -> {
OpcTcpServerTransportConfig transportConfig = OpcTcpServerTransportConfig.newBuilder().build();
return new OpcTcpServerTransport(transportConfig);
});

plc4xNamespace = new Plc4xNamespace(server, config);
plc4xNamespace.startup();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
import org.apache.plc4x.java.api.types.PlcValueType;
import org.apache.plc4x.java.utils.cache.PlcConnectionCache;
import org.eclipse.milo.opcua.sdk.server.AbstractLifecycle;
import org.eclipse.milo.opcua.sdk.server.api.DataItem;
import org.eclipse.milo.opcua.sdk.server.items.DataItem;
import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext;
import org.eclipse.milo.opcua.stack.core.Identifiers;
import org.eclipse.milo.opcua.stack.core.NodeIds;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
Expand Down Expand Up @@ -123,42 +123,42 @@ public void removeTag(DataItem item) {
public static NodeId getNodeId(PlcValueType plcValueType) {
switch (plcValueType) {
case BOOL:
return Identifiers.Boolean;
return NodeIds.Boolean;
case BYTE:
case USINT:
return Identifiers.Byte;
return NodeIds.Byte;
case SINT:
return Identifiers.SByte;
return NodeIds.SByte;
case INT:
return Identifiers.Int16;
return NodeIds.Int16;
case WORD:
case UINT:
return Identifiers.UInt16;
return NodeIds.UInt16;
case DINT:
return Identifiers.Int32;
return NodeIds.Int32;
case DWORD:
case UDINT:
return Identifiers.UInt32;
return NodeIds.UInt32;
case LINT:
return Identifiers.Int64;
return NodeIds.Int64;
case ULINT:
case LWORD:
return Identifiers.UInt64;
return NodeIds.UInt64;
case REAL:
return Identifiers.Float;
return NodeIds.Float;
case LREAL:
return Identifiers.Double;
return NodeIds.Double;
case CHAR:
case WCHAR:
case STRING:
case WSTRING:
return Identifiers.String;
return NodeIds.String;
default:
return Identifiers.BaseDataType;
return NodeIds.BaseDataType;
}
}

public DataValue getValue(AttributeFilterContext.GetAttributeContext ctx, String tag, String connectionString) {
public DataValue getValue(AttributeFilterContext ctx, String tag, String connectionString) {
PlcConnection connection = null;
try {

Expand Down
Loading