├── .github └── workflows │ └── maven.yml ├── .gitignore ├── Dockerfile ├── README.md ├── config ├── checkstyle │ └── google_checks.xml └── ide │ └── intellij-java-google-style.xml ├── pom.xml └── src └── main ├── java └── com │ └── digitalpetri │ └── opcua │ └── server │ ├── DemoConfigLimits.java │ ├── OpcUaDemoServer.java │ ├── RsaSha256CertificateFactoryImpl.java │ ├── namespace │ ├── demo │ │ ├── AccessControlFilter.java │ │ ├── CttNodesFragment.java │ │ ├── DataTypeTestNodesFragment.java │ │ ├── DemoNamespace.java │ │ ├── DynamicNodesFragment.java │ │ ├── MassNodesFragment.java │ │ ├── NullNodesFragment.java │ │ ├── RbacNodesFragment.java │ │ ├── TurtleNodesFragment.java │ │ ├── Util.java │ │ ├── VariantNodesFragment.java │ │ └── debug │ │ │ ├── DebugNodesFragment.java │ │ │ └── DeleteSubscriptionMethod.java │ └── test │ │ └── DataTypeTestNamespace.java │ └── objects │ ├── FileObject.java │ ├── SecurityAdminFilter.java │ ├── ServerConfigurationObject.java │ └── TrustListObject.java └── resources ├── DataTypeTest.NodeSet.xml ├── default-logback.xml ├── default-server.conf └── turtle-icon.png /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Maven 2 | 3 | on: 4 | push: 5 | branches: [ "1.0" ] 6 | pull_request: 7 | branches: [ "1.0" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set up JDK 24 16 | uses: actions/setup-java@v4 17 | with: 18 | java-version: '24' 19 | distribution: 'temurin' 20 | cache: maven 21 | 22 | - name: Build with Maven 23 | run: mvn -B package 24 | 25 | - name: Upload OPC UA Demo Server Artifact 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: opc-ua-demo-server 29 | path: target/opc-ua-demo-server.jar 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ # 2 | *.iml 3 | .idea/* 4 | 5 | # Maven # 6 | **/target/* 7 | target/* 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the application 2 | FROM maven:3.9-eclipse-temurin-24 AS builder 3 | 4 | # Set working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the project files 8 | COPY . . 9 | 10 | # See https://bugs.openjdk.org/browse/JDK-834529 11 | ARG TARGETPLATFORM 12 | ARG ENV_JTO=${TARGETPLATFORM/linux\/arm64/-XX:UseSVE=0} 13 | ARG ENV_JTO=${ENV_JTO/$TARGETPLATFORM/} 14 | 15 | RUN echo "ENV_JTO is set to: $ENV_JTO" 16 | ENV JAVA_TOOL_OPTIONS=$ENV_JTO 17 | 18 | # Build the application using Maven 19 | RUN mvn clean package 20 | 21 | # Stage 2: Run the application using a minimal Java runtime image 22 | FROM bellsoft/liberica-openjdk-alpine:24 AS runtime 23 | 24 | # Set working directory inside the container 25 | WORKDIR /app 26 | 27 | # Copy the compiled JAR from the build stage 28 | COPY --from=builder /app/target/opc-ua-demo-server.jar opc-ua-demo-server.jar 29 | 30 | # See https://bugs.openjdk.org/browse/JDK-834529 31 | ARG TARGETPLATFORM 32 | ARG ENV_JTO=${TARGETPLATFORM/linux\/arm64/-XX:UseSVE=0} 33 | ARG ENV_JTO=${ENV_JTO/$TARGETPLATFORM/} 34 | 35 | RUN echo "ENV_JTO is set to: $ENV_JTO" 36 | ENV JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders $ENV_JTO" 37 | 38 | # Define the entry point to run server 39 | ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar opc-ua-demo-server.jar"] 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Docker Pulls](https://img.shields.io/docker/pulls/digitalpetri/opc-ua-demo-server) 2 | ![Docker Image Version (tag)](https://img.shields.io/docker/v/digitalpetri/opc-ua-demo-server/1.0.0-M1) 3 | 4 | # Eclipse Milo OPC UA Demo Server 5 | 6 | This is a standalone OPC UA demo server built 7 | using [Eclipse Milo](https://github.com/eclipse-milo/milo). 8 | 9 | An internet-facing instance of this demo server is accessible at 10 | `opc.tcp://milo.digitalpetri.com:62541/milo`. 11 | 12 | It accepts both unsecured and secured connections. All incoming client certificates are automatically trusted. 13 | 14 | Authenticate anonymously or with one of the following credential pairs: 15 | 16 | - `User` / `password` 17 | - roles: `WellKnownRole_AuthenticatedUser` 18 | - `UserA` / `password` 19 | - roles: `SiteA_Read`, `SiteA_Write` 20 | - `UserB` / `password` 21 | - roles: `SiteB_Read`, `SiteB_Write` 22 | - `SiteAdmin` / `password` 23 | - roles: `SiteA_Read`, `SiteB_Read` 24 | - `SecurityAdmin` / `password` 25 | - roles: `WellKnownRole_SecurityAdmin` 26 | 27 | ## Building 28 | 29 | ### Docker 30 | 31 | Build the Docker image: 32 | 33 | ```bash 34 | docker build . -t opc-ua-demo-server 35 | ``` 36 | 37 | Start the server: 38 | 39 | ```bash 40 | docker run --rm -it -p 4840:4840 opc-ua-demo-server 41 | ``` 42 | 43 | In order to have access to the `server.conf` file and security directories, you may want to mount a 44 | volume mapped to the container's `/app/data` directory: 45 | 46 | ```bash 47 | docker run --rm -it -p 4840:4840 -v /tmp/opc-ua-demo-server-data:/app/data opc-ua-demo-server 48 | ``` 49 | 50 | ### Maven + JDK 24 51 | 52 | **Using JDK 24**, run `mvn clean package` in the root directory. 53 | 54 | An executable JAR file will be created in the `target` directory. This JAR file can be run with 55 | `java -jar target/opc-ua-demo-server.jar`. 56 | 57 | ## Configuration 58 | 59 | ### Server 60 | 61 | On startup the server loads its configuration from `/app/data/server.conf`. If it doesn't exist, the 62 | default configuration from `src/main/resources/default-server.conf` will be copied to that location. 63 | 64 | The server configuration file is in HOCON format and its configuration keys and values are 65 | documented with comments. 66 | 67 | ### Security 68 | 69 | The server's application instance certificate is stored in the KeyStore at 70 | `/app/data/security/pki/certificates.pfx`. If the server starts and this file doesn't exist it will 71 | generate a new one. 72 | 73 | Issuer and trusted certificates are managed using the standard OPC UA PKI layout found at 74 | `/app/data/security/pki/issuer` and `/app/data/security/pki/trusted`. 75 | 76 | Certificates from untrusted clients can be found at `/app/data/security/rejected` after they have 77 | attempted to connect at least once. Moving a client certificate to 78 | `/app/data/security/pki/trusted/certs` will mark it "trusted" and allow the client to connect with 79 | security enabled. 80 | 81 | These directories are monitored by the server and changes will be picked up automatically. 82 | -------------------------------------------------------------------------------- /config/checkstyle/google_checks.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 78 | 79 | 80 | 86 | 87 | 88 | 89 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 | 106 | 107 | 108 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 129 | 132 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 180 | 181 | 182 | 184 | 186 | 187 | 188 | 189 | 191 | 192 | 193 | 194 | 196 | 197 | 198 | 199 | 201 | 202 | 203 | 204 | 206 | 207 | 208 | 209 | 211 | 212 | 213 | 214 | 216 | 217 | 218 | 219 | 221 | 222 | 223 | 224 | 226 | 227 | 228 | 229 | 231 | 232 | 233 | 234 | 236 | 237 | 238 | 239 | 241 | 242 | 243 | 244 | 246 | 248 | 250 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 282 | 283 | 284 | 287 | 288 | 289 | 290 | 296 | 297 | 298 | 299 | 303 | 304 | 305 | 306 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 321 | 322 | 323 | 324 | 325 | 326 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 342 | 343 | 344 | 345 | 348 | 349 | 350 | 351 | 352 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.digitalpetri.opcua 8 | opc-ua-demo-server 9 | 1.0.1-SNAPSHOT 10 | 11 | Eclipse Milo OPC UA Demo Server 12 | Support for working with UANodeSet XML files. 13 | https://github.com/digitalpetri/opc-ua-demo-server 14 | 15 | 16 | 17 | kevinherron 18 | Kevin Herron 19 | kevinherron@gmail.com 20 | 21 | 22 | 23 | 24 | 25 | Eclipse Public License - v 2.0 26 | https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html 27 | repo 28 | 29 | 30 | 31 | 32 | https://github.com/digitalpetri/opc-ua-demo-server 33 | scm:git:git://github.com/digitalpetri/opc-ua-demo-server.git 34 | scm:git:git@github.com:digitalpetri/opc-ua-demo-server.git 35 | 36 | HEAD 37 | 38 | 39 | 40 | 24 41 | 24 42 | UTF-8 43 | 44 | 45 | 0.3 46 | 4.0.2 47 | 24.1.0 48 | 1.5.17 49 | 1.0.0 50 | 1.4.3 51 | 0.3 52 | 53 | 54 | 3.2.1 55 | 10.21.4 56 | 3.6.0 57 | 3.5.0 58 | 3.6.0 59 | 60 | 61 | 62 | 63 | 64 | org.codehaus.mojo 65 | buildnumber-maven-plugin 66 | ${buildnumber-maven-plugin.version} 67 | 68 | 69 | validate 70 | 71 | create 72 | 73 | 74 | 75 | 76 | yyyy-MM-dd'T'HH:mm:ss'Z' 77 | UTC 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-checkstyle-plugin 83 | ${maven-checkstyle-plugin.version} 84 | 85 | config/checkstyle/google_checks.xml 86 | true 87 | true 88 | true 89 | 90 | false 91 | 92 | 93 | 94 | validate 95 | validate 96 | 97 | check 98 | 99 | 100 | 101 | 102 | 103 | com.puppycrawl.tools 104 | checkstyle 105 | ${checkstyle.version} 106 | 107 | 108 | 109 | 110 | org.apache.maven.plugins 111 | maven-enforcer-plugin 112 | ${maven-enforcer-plugin.version} 113 | 114 | 115 | enforce-maven 116 | 117 | enforce 118 | 119 | 120 | 121 | 122 | 3.2.5 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | org.apache.maven.plugins 131 | maven-shade-plugin 132 | ${maven-shade-plugin.version} 133 | 134 | 135 | package 136 | 137 | shade 138 | 139 | 140 | opc-ua-demo-server 141 | false 142 | 143 | 145 | 146 | com.digitalpetri.opcua.server.OpcUaDemoServer 147 | ${timestamp} 148 | ${buildNumber} 149 | ${project.version} 150 | ${milo.version} 151 | ${milo.version} 152 | 153 | 154 | 155 | 156 | 157 | *:* 158 | 159 | module-info.class 160 | META-INF/MANIFEST.MF 161 | META-INF/*.SF 162 | META-INF/*.DSA 163 | META-INF/*.RSA 164 | META-INF/versions/9/OSGI-INF/MANIFEST.MF 165 | META-INF/versions/9/module-info.class 166 | META-INF/io.netty.versions.properties 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | org.eclipse.milo 180 | milo-sdk-server 181 | ${milo.version} 182 | 183 | 184 | com.typesafe 185 | config 186 | ${typesafe-config.version} 187 | 188 | 189 | ch.qos.logback 190 | logback-classic 191 | ${logback.version} 192 | 193 | 194 | com.digitalpetri.opcua 195 | uanodeset-core 196 | ${uanodeset.version} 197 | 198 | 199 | com.digitalpetri.opcua 200 | uanodeset-namespace 201 | ${uanodeset.version} 202 | 203 | 204 | com.digitalpetri.opcua 205 | data-type-test 206 | ${data-type-test.version} 207 | 208 | 209 | 210 | 211 | 212 | sonatype-snapshots 213 | https://oss.sonatype.org/content/repositories/snapshots/ 214 | 215 | false 216 | 217 | 218 | true 219 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/DemoConfigLimits.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server; 2 | 3 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 4 | 5 | import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfigLimits; 6 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 7 | 8 | public class DemoConfigLimits implements OpcUaServerConfigLimits { 9 | 10 | @Override 11 | public Double getMinPublishingInterval() { 12 | return 100.0; 13 | } 14 | 15 | @Override 16 | public Double getDefaultPublishingInterval() { 17 | return 100.0; 18 | } 19 | 20 | @Override 21 | public UInteger getMaxSessions() { 22 | return uint(200); 23 | } 24 | 25 | @Override 26 | public Double getMaxSessionTimeout() { 27 | return 30_000.0; 28 | } 29 | 30 | @Override 31 | public UInteger getMaxMonitoredItems() { 32 | return uint(500_000); 33 | } 34 | 35 | @Override 36 | public UInteger getMaxMonitoredItemsPerSession() { 37 | return uint(100_000); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/OpcUaDemoServer.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server; 2 | 3 | import static org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS; 4 | import static org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME; 5 | import static org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig.USER_TOKEN_POLICY_X509; 6 | 7 | import ch.qos.logback.classic.LoggerContext; 8 | import ch.qos.logback.classic.joran.JoranConfigurator; 9 | import ch.qos.logback.core.util.StatusPrinter2; 10 | import com.digitalpetri.opcua.server.namespace.demo.DemoNamespace; 11 | import com.digitalpetri.opcua.server.namespace.test.DataTypeTestNamespace; 12 | import com.digitalpetri.opcua.server.objects.ServerConfigurationObject; 13 | import com.typesafe.config.Config; 14 | import com.typesafe.config.ConfigFactory; 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.InputStreamReader; 19 | import java.nio.file.Files; 20 | import java.nio.file.Path; 21 | import java.nio.file.StandardOpenOption; 22 | import java.security.Security; 23 | import java.security.cert.X509Certificate; 24 | import java.text.SimpleDateFormat; 25 | import java.util.Collections; 26 | import java.util.LinkedHashSet; 27 | import java.util.List; 28 | import java.util.Set; 29 | import java.util.TimeZone; 30 | import java.util.UUID; 31 | import java.util.concurrent.CountDownLatch; 32 | import java.util.concurrent.TimeUnit; 33 | import java.util.function.Predicate; 34 | import java.util.function.Supplier; 35 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 36 | import org.eclipse.milo.opcua.sdk.server.AbstractLifecycle; 37 | import org.eclipse.milo.opcua.sdk.server.EndpointConfig; 38 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 39 | import org.eclipse.milo.opcua.sdk.server.OpcUaServerConfig; 40 | import org.eclipse.milo.opcua.sdk.server.RoleMapper; 41 | import org.eclipse.milo.opcua.sdk.server.identity.AnonymousIdentityValidator; 42 | import org.eclipse.milo.opcua.sdk.server.identity.CompositeValidator; 43 | import org.eclipse.milo.opcua.sdk.server.identity.Identity; 44 | import org.eclipse.milo.opcua.sdk.server.identity.Identity.AnonymousIdentity; 45 | import org.eclipse.milo.opcua.sdk.server.identity.Identity.UsernameIdentity; 46 | import org.eclipse.milo.opcua.sdk.server.identity.UsernameIdentityValidator; 47 | import org.eclipse.milo.opcua.sdk.server.identity.X509IdentityValidator; 48 | import org.eclipse.milo.opcua.sdk.server.model.objects.ServerConfigurationTypeNode; 49 | import org.eclipse.milo.opcua.sdk.server.nodes.UaNode; 50 | import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil; 51 | import org.eclipse.milo.opcua.stack.core.NodeIds; 52 | import org.eclipse.milo.opcua.stack.core.Stack; 53 | import org.eclipse.milo.opcua.stack.core.security.CertificateManager; 54 | import org.eclipse.milo.opcua.stack.core.security.CertificateQuarantine; 55 | import org.eclipse.milo.opcua.stack.core.security.CertificateValidator; 56 | import org.eclipse.milo.opcua.stack.core.security.DefaultApplicationGroup; 57 | import org.eclipse.milo.opcua.stack.core.security.DefaultCertificateManager; 58 | import org.eclipse.milo.opcua.stack.core.security.DefaultServerCertificateValidator; 59 | import org.eclipse.milo.opcua.stack.core.security.FileBasedCertificateQuarantine; 60 | import org.eclipse.milo.opcua.stack.core.security.FileBasedTrustListManager; 61 | import org.eclipse.milo.opcua.stack.core.security.KeyStoreCertificateStore; 62 | import org.eclipse.milo.opcua.stack.core.security.MemoryCertificateQuarantine; 63 | import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy; 64 | import org.eclipse.milo.opcua.stack.core.security.TrustListManager; 65 | import org.eclipse.milo.opcua.stack.core.transport.TransportProfile; 66 | import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; 67 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 68 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 69 | import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; 70 | import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo; 71 | import org.eclipse.milo.opcua.stack.core.util.ManifestUtil; 72 | import org.eclipse.milo.opcua.stack.core.util.validation.ValidationCheck; 73 | import org.eclipse.milo.opcua.stack.transport.server.OpcServerTransportFactory; 74 | import org.eclipse.milo.opcua.stack.transport.server.tcp.OpcTcpServerTransport; 75 | import org.eclipse.milo.opcua.stack.transport.server.tcp.OpcTcpServerTransportConfig; 76 | import org.slf4j.Logger; 77 | import org.slf4j.LoggerFactory; 78 | 79 | public class OpcUaDemoServer extends AbstractLifecycle { 80 | 81 | private static final String APPLICATION_URI_BASE = "urn:opc:eclipse:milo:opc-ua-demo-server"; 82 | 83 | private static final String PRODUCT_URI = "https://github.com/digitalpetri/opc-ua-demo-server"; 84 | 85 | private static final String PROPERTY_BUILD_DATE = "X-Server-Build-Date"; 86 | private static final String PROPERTY_BUILD_NUMBER = "X-Server-Build-Number"; 87 | private static final String PROPERTY_SOFTWARE_VERSION = "X-Server-Software-Version"; 88 | 89 | private final OpcUaServer server; 90 | 91 | public OpcUaDemoServer(Path dataDirPath) throws Exception { 92 | Path securityDirPath = dataDirPath.resolve("security"); 93 | Path pkiDirPath = securityDirPath.resolve("pki"); 94 | Path userPkiDirPath = securityDirPath.resolve("pki-user"); 95 | 96 | if (!pkiDirPath.toFile().exists() && !pkiDirPath.toFile().mkdirs()) { 97 | throw new RuntimeException("failed to resolve or create pki dir: " + pkiDirPath); 98 | } 99 | if (!userPkiDirPath.toFile().exists() && !userPkiDirPath.toFile().mkdirs()) { 100 | throw new RuntimeException("failed to resolve or create user pki dir: " + userPkiDirPath); 101 | } 102 | 103 | Path configFilePath = dataDirPath.resolve("server.conf"); 104 | 105 | InputStream defaultConfigInputStream = 106 | OpcUaDemoServer.class.getClassLoader().getResourceAsStream("default-server.conf"); 107 | 108 | assert defaultConfigInputStream != null; 109 | 110 | // If the config file doesn't exist, copy the default from the classpath. 111 | if (!configFilePath.toFile().exists()) { 112 | Files.copy(defaultConfigInputStream, configFilePath); 113 | } 114 | 115 | Config defaultConfig = 116 | ConfigFactory.parseReader(new InputStreamReader(defaultConfigInputStream)); 117 | 118 | Config userConfig = ConfigFactory.parseFile(configFilePath.toFile()); 119 | 120 | // Load the user config and merge it with the default config in case anything is missing. 121 | // This also allows the user config to contain only override values. 122 | Config config = userConfig.withFallback(defaultConfig); 123 | 124 | Stack.ConnectionLimits.RATE_LIMIT_ENABLED = config.getBoolean("rate-limit-enabled"); 125 | 126 | UUID applicationUuid = readOrCreateApplicationUuid(dataDirPath); 127 | String applicationUri = "%s:%s".formatted(APPLICATION_URI_BASE, applicationUuid); 128 | 129 | var certificateStore = 130 | KeyStoreCertificateStore.createAndInitialize( 131 | new KeyStoreCertificateStore.Settings( 132 | pkiDirPath.resolve("certificates.pfx"), 133 | "password"::toCharArray, 134 | alias -> "password".toCharArray())); 135 | 136 | Path rejectedDirPath = securityDirPath.resolve("rejected"); 137 | if (!rejectedDirPath.toFile().exists() && !rejectedDirPath.toFile().mkdirs()) { 138 | throw new RuntimeException("failed to resolve or create rejected dir: " + rejectedDirPath); 139 | } 140 | CertificateQuarantine certificateQuarantine = 141 | new FileBasedCertificateQuarantine(rejectedDirPath.toFile()); 142 | 143 | TrustListManager trustListManager = FileBasedTrustListManager.createAndInitialize(pkiDirPath); 144 | 145 | final CertificateValidator certificateValidator; 146 | 147 | if (config.getBoolean("trust-all-certificates")) { 148 | certificateValidator = 149 | (chain, uri, hostnames) -> { 150 | 151 | // No validation, just accept all certificates. 152 | LoggerFactory.getLogger(OpcUaDemoServer.class) 153 | .info("Skipping validation for certificate chain:"); 154 | 155 | for (int i = 0; i < chain.size(); i++) { 156 | X509Certificate certificate = chain.get(i); 157 | 158 | trustListManager.addTrustedCertificate(certificate); 159 | 160 | LoggerFactory.getLogger(OpcUaDemoServer.class) 161 | .info(" certificate[{}]: {}", i, certificate.getSubjectX500Principal()); 162 | } 163 | }; 164 | } else { 165 | certificateValidator = 166 | new DefaultServerCertificateValidator( 167 | trustListManager, ValidationCheck.ALL_OPTIONAL_CHECKS, certificateQuarantine); 168 | } 169 | 170 | DefaultApplicationGroup defaultApplicationGroup = 171 | new DefaultApplicationGroup( 172 | trustListManager, 173 | certificateStore, 174 | new RsaSha256CertificateFactoryImpl( 175 | applicationUri, () -> getCertificateHostnames(config)), 176 | certificateValidator); 177 | 178 | defaultApplicationGroup.initialize(); 179 | 180 | CertificateManager certificateManager = 181 | new DefaultCertificateManager(certificateQuarantine, defaultApplicationGroup); 182 | 183 | Supplier certificateSupplier = 184 | () -> { 185 | X509Certificate[] certificateChain = 186 | certificateManager 187 | .getDefaultApplicationGroup() 188 | .orElseThrow() 189 | .getCertificateChain(NodeIds.RsaSha256ApplicationCertificateType) 190 | .orElseThrow(); 191 | 192 | return certificateChain[0]; 193 | }; 194 | 195 | var serverConfigBuilder = OpcUaServerConfig.builder(); 196 | serverConfigBuilder 197 | .setProductUri(PRODUCT_URI) 198 | .setApplicationUri(applicationUri) 199 | .setApplicationName(LocalizedText.english("Eclipse Milo OPC UA Demo Server")) 200 | .setBuildInfo(createBuildInfo()) 201 | .setEndpoints(createEndpointConfigs(config, certificateSupplier)) 202 | .setCertificateManager(certificateManager) 203 | .setIdentityValidator( 204 | new CompositeValidator( 205 | AnonymousIdentityValidator.INSTANCE, 206 | createUsernameIdentityValidator(), 207 | createX509IdentityValidator(userPkiDirPath))) 208 | .setRoleMapper(new DemoRoleMapper()) 209 | .setLimits(new DemoConfigLimits()) 210 | .build(); 211 | 212 | OpcServerTransportFactory transportFactory = 213 | transportProfile -> { 214 | if (transportProfile == TransportProfile.TCP_UASC_UABINARY) { 215 | OpcTcpServerTransportConfig transportConfig = 216 | OpcTcpServerTransportConfig.newBuilder().build(); 217 | 218 | return new OpcTcpServerTransport(transportConfig); 219 | } 220 | return null; 221 | }; 222 | 223 | server = new OpcUaServer(serverConfigBuilder.build(), transportFactory); 224 | 225 | server.getNamespaceTable().set(2, DemoNamespace.NAMESPACE_URI); 226 | 227 | server.getNamespaceTable().set(3, DataTypeTestNamespace.NAMESPACE_URI); 228 | var dataTypeTestNamespace = DataTypeTestNamespace.create(server); 229 | dataTypeTestNamespace.startup(); 230 | 231 | var demoNamespace = new DemoNamespace(server, config); 232 | demoNamespace.startup(); 233 | 234 | boolean gdsPushEnabled = config.getBoolean("gds-push-enabled"); 235 | 236 | if (gdsPushEnabled) { 237 | ServerConfigurationTypeNode serverConfigurationNode = 238 | server 239 | .getAddressSpaceManager() 240 | .getManagedNode(NodeIds.ServerConfiguration) 241 | .map(ServerConfigurationTypeNode.class::cast) 242 | .orElseThrow(); 243 | 244 | var serverConfigurationObject = 245 | new ServerConfigurationObject(server, serverConfigurationNode); 246 | serverConfigurationObject.startup(); 247 | } 248 | 249 | server.getAddressSpaceManager().getManagedNode(NodeIds.Aliases).ifPresent(UaNode::delete); 250 | server.getAddressSpaceManager().getManagedNode(NodeIds.Locations).ifPresent(UaNode::delete); 251 | } 252 | 253 | @Override 254 | protected void onStartup() { 255 | server.startup(); 256 | } 257 | 258 | @Override 259 | protected void onShutdown() { 260 | server.shutdown(); 261 | } 262 | 263 | private UsernameIdentityValidator createUsernameIdentityValidator() { 264 | return new UsernameIdentityValidator( 265 | authenticationChallenge -> { 266 | String username = authenticationChallenge.getUsername(); 267 | String password = authenticationChallenge.getPassword(); 268 | 269 | var validUser = "User".equals(username) && "password".equals(password); 270 | var validUserA = "UserA".equals(username) && "password".equals(password); 271 | var validUserB = "UserB".equals(username) && "password".equals(password); 272 | var validSiteAdmin = "SiteAdmin".equals(username) && "password".equals(password); 273 | var validSecurityAdmin = "SecurityAdmin".equals(username) && "password".equals(password); 274 | 275 | return validUser || validUserA || validUserB || validSiteAdmin || validSecurityAdmin; 276 | }); 277 | } 278 | 279 | private X509IdentityValidator createX509IdentityValidator(Path userPkiDirPath) 280 | throws IOException { 281 | 282 | var userTrustListManager = FileBasedTrustListManager.createAndInitialize(userPkiDirPath); 283 | 284 | var validator = 285 | new DefaultServerCertificateValidator( 286 | userTrustListManager, 287 | Set.of(ValidationCheck.VALIDITY, ValidationCheck.REVOCATION), 288 | new MemoryCertificateQuarantine()); 289 | 290 | Predicate validate = 291 | certificate -> { 292 | try { 293 | validator.validateCertificateChain(List.of(certificate), null, null); 294 | return true; 295 | } catch (Exception e) { 296 | return false; 297 | } 298 | }; 299 | 300 | return new X509IdentityValidator(validate); 301 | } 302 | 303 | private BuildInfo createBuildInfo() { 304 | var dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 305 | dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); 306 | 307 | var manufacturerName = "digitalpetri"; 308 | var productName = "Eclipse Milo OPC UA Demo Server"; 309 | 310 | var softwareVersion = ManifestUtil.read(PROPERTY_SOFTWARE_VERSION).orElse("dev"); 311 | var buildNumber = ManifestUtil.read(PROPERTY_BUILD_NUMBER).orElse("dev"); 312 | var buildDate = 313 | ManifestUtil.read(PROPERTY_BUILD_DATE) 314 | .map( 315 | date -> { 316 | try { 317 | return new DateTime(dateFormat.parse(date)); 318 | } catch (Throwable t) { 319 | return DateTime.NULL_VALUE; 320 | } 321 | }) 322 | .orElse(DateTime.NULL_VALUE); 323 | 324 | return new BuildInfo( 325 | PRODUCT_URI, manufacturerName, productName, softwareVersion, buildNumber, buildDate); 326 | } 327 | 328 | private Set createEndpointConfigs( 329 | Config config, Supplier certificate) { 330 | var endpointConfigs = new LinkedHashSet(); 331 | 332 | List bindAddresses = config.getStringList("bind-address-list"); 333 | int bindPort = config.getInt("bind-port"); 334 | List securityPolicies = config.getStringList("security-policy-list"); 335 | List securityModes = config.getStringList("security-mode-list"); 336 | 337 | for (String bindAddress : bindAddresses) { 338 | Set hostnames = getEndpointHostnames(config); 339 | 340 | for (String hostname : hostnames) { 341 | EndpointConfig.Builder builder = EndpointConfig.newBuilder(); 342 | builder 343 | .setTransportProfile(TransportProfile.TCP_UASC_UABINARY) 344 | .setBindAddress(bindAddress) 345 | .setBindPort(bindPort) 346 | .setHostname(hostname) 347 | .setPath("/milo") 348 | .setCertificate(certificate) 349 | .addTokenPolicies( 350 | USER_TOKEN_POLICY_ANONYMOUS, USER_TOKEN_POLICY_USERNAME, USER_TOKEN_POLICY_X509); 351 | 352 | for (String securityPolicyString : securityPolicies) { 353 | SecurityPolicy securityPolicy = SecurityPolicy.valueOf(securityPolicyString); 354 | 355 | if (securityPolicy == SecurityPolicy.None) { 356 | // No need to iterate over security modes for the None policy. 357 | builder.setSecurityPolicy(securityPolicy).setSecurityMode(MessageSecurityMode.None); 358 | 359 | endpointConfigs.add(builder.build()); 360 | } else { 361 | for (String securityModeString : securityModes) { 362 | MessageSecurityMode securityMode = MessageSecurityMode.valueOf(securityModeString); 363 | 364 | if (securityMode == MessageSecurityMode.None) { 365 | // At this point, SecurityPolicy != None, so we ignore MessageSecurityMode.None. 366 | continue; 367 | } 368 | 369 | builder.setSecurityPolicy(securityPolicy).setSecurityMode(securityMode); 370 | 371 | endpointConfigs.add(builder.build()); 372 | } 373 | } 374 | } 375 | 376 | // Expose a discovery-specific endpoint with no security. 377 | // Usage of the "/discovery" suffix is defined by OPC UA Part 6. 378 | 379 | EndpointConfig.Builder discoveryBuilder = 380 | builder 381 | .setPath("/milo/discovery") 382 | .setSecurityPolicy(SecurityPolicy.None) 383 | .setSecurityMode(MessageSecurityMode.None); 384 | 385 | endpointConfigs.add(discoveryBuilder.build()); 386 | } 387 | } 388 | 389 | return endpointConfigs; 390 | } 391 | 392 | private Set getEndpointHostnames(Config config) { 393 | Set hostnames = new LinkedHashSet<>(); 394 | 395 | for (String hostname : config.getStringList("endpoint-address-list")) { 396 | if (hostname.startsWith("<") && hostname.endsWith(">")) { 397 | String name = hostname.substring(1, hostname.length() - 1); 398 | 399 | hostnames.addAll(HostnameUtil.getHostnames(name, true, false)); 400 | 401 | if (name.equals("localhost")) { 402 | // Make sure "localhost" appears in whatever hostname list we end up with for 403 | // "". 404 | // On Windows `HostnameUtil.getHostnames` tends not to return "localhost", just 405 | // "127.0.0.1". 406 | hostnames.add("localhost"); 407 | } 408 | } else { 409 | hostnames.add(hostname); 410 | } 411 | } 412 | 413 | return hostnames; 414 | } 415 | 416 | private Set getCertificateHostnames(Config config) { 417 | Set hostnames = new LinkedHashSet<>(); 418 | 419 | for (String hostname : config.getStringList("certificate-hostname-list")) { 420 | if (hostname.startsWith("<") && hostname.endsWith(">")) { 421 | hostnames.addAll( 422 | HostnameUtil.getHostnames(hostname.substring(1, hostname.length() - 1), true, false)); 423 | } else { 424 | hostnames.add(hostname); 425 | } 426 | } 427 | 428 | return hostnames; 429 | } 430 | 431 | private static UUID readOrCreateApplicationUuid(Path dataDirPath) { 432 | Path uuidPath = dataDirPath.resolve(".uuid"); 433 | 434 | if (uuidPath.toFile().exists()) { 435 | try { 436 | return UUID.fromString(Files.readString(uuidPath)); 437 | } catch (IllegalArgumentException | IOException e) { 438 | return createApplicationUuidFile(uuidPath); 439 | } 440 | } else { 441 | return createApplicationUuidFile(uuidPath); 442 | } 443 | } 444 | 445 | private static UUID createApplicationUuidFile(Path uuidPath) { 446 | UUID uuid = UUID.randomUUID(); 447 | try { 448 | Files.writeString( 449 | uuidPath, 450 | uuid.toString(), 451 | StandardOpenOption.CREATE, 452 | StandardOpenOption.TRUNCATE_EXISTING); 453 | } catch (IOException ex) { 454 | throw new RuntimeException("failed to create application UUID", ex); 455 | } 456 | return uuid; 457 | } 458 | 459 | private static class DemoRoleMapper implements RoleMapper { 460 | 461 | public static final NodeId ROLE_SITE_A_READ = NodeId.parse("ns=1;s=SiteA_Read"); 462 | public static final NodeId ROLE_SITE_A_WRITE = NodeId.parse("ns=1;s=SiteA_Write"); 463 | public static final NodeId ROLE_SITE_B_READ = NodeId.parse("ns=1;s=SiteB_Read"); 464 | public static final NodeId ROLE_SITE_B_WRITE = NodeId.parse("ns=1;s=SiteB_Write"); 465 | public static final NodeId ROLE_SITE_ADMIN = NodeId.parse("ns=1;s=SiteAdmin"); 466 | 467 | @Override 468 | public List getRoleIds(Identity identity) { 469 | if (identity instanceof AnonymousIdentity) { 470 | return List.of(NodeIds.WellKnownRole_Anonymous); 471 | } else if (identity instanceof UsernameIdentity ui) { 472 | return switch (ui.getUsername()) { 473 | case "User" -> List.of(NodeIds.WellKnownRole_AuthenticatedUser); 474 | 475 | case "UserA" -> List.of(ROLE_SITE_A_READ, ROLE_SITE_A_WRITE); 476 | 477 | case "UserB" -> List.of(ROLE_SITE_B_READ, ROLE_SITE_B_WRITE); 478 | 479 | case "SiteAdmin" -> List.of(ROLE_SITE_ADMIN); 480 | 481 | case "SecurityAdmin" -> List.of(NodeIds.WellKnownRole_SecurityAdmin); 482 | 483 | case null, default -> List.of(); 484 | }; 485 | } else { 486 | return Collections.emptyList(); 487 | } 488 | } 489 | } 490 | 491 | // region Bootstrap 492 | 493 | public static void main(String[] args) throws Exception { 494 | // start running this static initializer ASAP, it measurably affects startup time. 495 | new Thread( 496 | () -> { 497 | var ignored = NodeIds.Boolean; 498 | }) 499 | .start(); 500 | 501 | // Needed for `SecurityPolicy.Aes256_Sha256_RsaPss` 502 | Security.addProvider(new BouncyCastleProvider()); 503 | 504 | final long startTime = System.nanoTime(); 505 | 506 | Path userDirPath = new File(System.getProperty("user.dir")).toPath(); 507 | 508 | Path dataDirPath = userDirPath.resolve("data"); 509 | if (!dataDirPath.toFile().exists()) { 510 | if (!dataDirPath.toFile().mkdir()) { 511 | throw new RuntimeException("failed to resolve or create data dir: " + dataDirPath); 512 | } 513 | } 514 | 515 | File logbackXmlFile = dataDirPath.resolve("logback.xml").toFile(); 516 | if (!logbackXmlFile.exists()) { 517 | InputStream inputStream = 518 | OpcUaDemoServer.class.getClassLoader().getResourceAsStream("default-logback.xml"); 519 | assert inputStream != null; 520 | 521 | Files.copy(inputStream, logbackXmlFile.toPath()); 522 | } 523 | 524 | configureLogback(logbackXmlFile); 525 | 526 | var server = new OpcUaDemoServer(dataDirPath); 527 | server.startup(); 528 | 529 | long startupDuration = 530 | TimeUnit.MILLISECONDS.convert(System.nanoTime() - startTime, TimeUnit.NANOSECONDS); 531 | 532 | String version = 533 | ManifestUtil.read(PROPERTY_SOFTWARE_VERSION).map("v%s"::formatted).orElse("(dev version)"); 534 | 535 | Logger logger = LoggerFactory.getLogger(OpcUaDemoServer.class); 536 | logger.info("Eclipse Milo OPC UA Demo Server {} started in {}ms", version, startupDuration); 537 | logger.info("user dir: {}", userDirPath); 538 | logger.info("data dir: {}", dataDirPath); 539 | logger.info("security dir: {}", dataDirPath.resolve("security")); 540 | logger.info("security pki dir: {}", dataDirPath.resolve("security").resolve("pki")); 541 | 542 | waitForShutdownHook(server); 543 | } 544 | 545 | private static void waitForShutdownHook(OpcUaDemoServer server) throws InterruptedException { 546 | var shutdownLatch = new CountDownLatch(1); 547 | Runtime.getRuntime() 548 | .addShutdownHook( 549 | new Thread( 550 | () -> { 551 | System.out.println("Shutting down server..."); 552 | try { 553 | server.shutdown(); 554 | } finally { 555 | shutdownLatch.countDown(); 556 | } 557 | })); 558 | shutdownLatch.await(); 559 | } 560 | 561 | private static void configureLogback(File logbackXmlFile) { 562 | var context = (LoggerContext) LoggerFactory.getILoggerFactory(); 563 | 564 | try { 565 | var configurator = new JoranConfigurator(); 566 | configurator.setContext(context); 567 | context.reset(); 568 | 569 | configurator.doConfigure(logbackXmlFile); 570 | } catch (Exception e) { 571 | System.err.println("Error configuring logback: " + e.getMessage()); 572 | throw new RuntimeException(e); 573 | } 574 | 575 | new StatusPrinter2().printInCaseOfErrorsOrWarnings(context); 576 | } 577 | 578 | // endregion 579 | 580 | } 581 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/RsaSha256CertificateFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server; 2 | 3 | import java.security.KeyPair; 4 | import java.security.NoSuchAlgorithmException; 5 | import java.security.cert.X509Certificate; 6 | import java.util.Set; 7 | import java.util.function.Supplier; 8 | import java.util.regex.Pattern; 9 | import org.eclipse.milo.opcua.stack.core.NodeIds; 10 | import org.eclipse.milo.opcua.stack.core.security.RsaSha256CertificateFactory; 11 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 12 | import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateBuilder; 13 | import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator; 14 | 15 | public class RsaSha256CertificateFactoryImpl extends RsaSha256CertificateFactory { 16 | 17 | private static final Pattern IP_ADDR_PATTERN = 18 | Pattern.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"); 19 | 20 | /** 21 | * Default RSA key length. 22 | * 23 | *

A key length of 2048 is required to support both deprecated and non-deprecated security 24 | * policies. Applications that don't need to support the old security policies can use a larger 25 | * key length, e.g. 4096. 26 | */ 27 | private static final int RSA_KEY_LENGTH = 2048; 28 | 29 | private final String applicationUri; 30 | private final Supplier> hostnames; 31 | 32 | public RsaSha256CertificateFactoryImpl(String applicationUri, Supplier> hostnames) { 33 | this.applicationUri = applicationUri; 34 | this.hostnames = hostnames; 35 | } 36 | 37 | @Override 38 | public KeyPair createKeyPair(NodeId certificateTypeId) { 39 | if (!certificateTypeId.equals(NodeIds.RsaSha256ApplicationCertificateType)) { 40 | throw new UnsupportedOperationException("certificateTypeId: " + certificateTypeId); 41 | } 42 | 43 | try { 44 | return SelfSignedCertificateGenerator.generateRsaKeyPair(RSA_KEY_LENGTH); 45 | } catch (NoSuchAlgorithmException e) { 46 | throw new RuntimeException(e); 47 | } 48 | } 49 | 50 | @Override 51 | protected X509Certificate[] createRsaSha256CertificateChain(KeyPair keyPair) throws Exception { 52 | SelfSignedCertificateBuilder builder = 53 | new SelfSignedCertificateBuilder(keyPair) 54 | .setCommonName("Eclipse Milo OPC UA Demo Server") 55 | .setOrganization("digitalpetri") 56 | .setOrganizationalUnit("dev") 57 | .setLocalityName("Folsom") 58 | .setStateName("CA") 59 | .setCountryCode("US") 60 | .setApplicationUri(applicationUri); 61 | 62 | for (String hostname : hostnames.get()) { 63 | if (IP_ADDR_PATTERN.matcher(hostname).matches()) { 64 | builder.addIpAddress(hostname); 65 | } else { 66 | builder.addDnsName(hostname); 67 | } 68 | } 69 | 70 | return new X509Certificate[] {builder.build()}; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/AccessControlFilter.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import com.typesafe.config.Config; 4 | import java.util.Collections; 5 | import java.util.HashSet; 6 | import java.util.List; 7 | import org.eclipse.milo.opcua.sdk.core.AccessLevel; 8 | import org.eclipse.milo.opcua.sdk.server.Session; 9 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter; 10 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext; 11 | import org.eclipse.milo.opcua.stack.core.AttributeId; 12 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 13 | import org.eclipse.milo.opcua.stack.core.types.structured.PermissionType; 14 | import org.eclipse.milo.opcua.stack.core.types.structured.PermissionType.Field; 15 | import org.eclipse.milo.opcua.stack.core.types.structured.RolePermissionType; 16 | import org.jspecify.annotations.Nullable; 17 | 18 | public class AccessControlFilter implements AttributeFilter { 19 | 20 | private final List rolePermissions; 21 | 22 | public AccessControlFilter(Config config, String key) { 23 | this.rolePermissions = 24 | config.getConfigList(key).stream() 25 | .map( 26 | roleConfig -> { 27 | String roleIdString = roleConfig.getString("role-id"); 28 | List permissionsString = roleConfig.getStringList("permissions"); 29 | 30 | NodeId roleId = NodeId.parse(roleIdString); 31 | Field[] permissions = 32 | permissionsString.stream().map(Field::valueOf).toArray(Field[]::new); 33 | 34 | return new RolePermissionType(roleId, PermissionType.of(permissions)); 35 | }) 36 | .toList(); 37 | } 38 | 39 | @Override 40 | public @Nullable Object getAttribute(AttributeFilterContext ctx, AttributeId attributeId) { 41 | return switch (attributeId) { 42 | case UserAccessLevel -> { 43 | Session session = ctx.getSession().orElseThrow(); 44 | 45 | List rolePermissions = getSessionRolePermissions(session); 46 | 47 | var accessLevels = new HashSet(); 48 | if (rolePermissions.stream().anyMatch(rpt -> rpt.getPermissions().getRead())) { 49 | accessLevels.add(AccessLevel.CurrentRead); 50 | } 51 | if (rolePermissions.stream().anyMatch(rpt -> rpt.getPermissions().getWrite())) { 52 | accessLevels.add(AccessLevel.CurrentWrite); 53 | } 54 | 55 | yield AccessLevel.toValue(accessLevels); 56 | } 57 | case UserExecutable -> { 58 | Session session = ctx.getSession().orElseThrow(); 59 | 60 | List rolePermissions = getSessionRolePermissions(session); 61 | 62 | yield rolePermissions.stream().anyMatch(rpt -> rpt.getPermissions().getCall()); 63 | } 64 | case RolePermissions -> rolePermissions.toArray(new RolePermissionType[0]); 65 | case UserRolePermissions -> { 66 | Session session = ctx.getSession().orElseThrow(); 67 | List rolePermissions = getSessionRolePermissions(session); 68 | 69 | yield rolePermissions.toArray(new RolePermissionType[0]); 70 | } 71 | default -> ctx.getAttribute(attributeId); 72 | }; 73 | } 74 | 75 | private List getSessionRolePermissions(Session session) { 76 | List roleIds = session.getRoleIds().orElse(Collections.emptyList()); 77 | 78 | return rolePermissions.stream().filter(rpt -> roleIds.contains(rpt.getRoleId())).toList(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/DemoNamespace.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort; 4 | 5 | import com.digitalpetri.opcua.server.namespace.demo.debug.DebugNodesFragment; 6 | import com.typesafe.config.Config; 7 | import java.util.List; 8 | import java.util.Random; 9 | import java.util.UUID; 10 | import org.eclipse.milo.opcua.sdk.core.Reference; 11 | import org.eclipse.milo.opcua.sdk.core.Reference.Direction; 12 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceComposite; 13 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 14 | import org.eclipse.milo.opcua.sdk.server.Lifecycle; 15 | import org.eclipse.milo.opcua.sdk.server.LifecycleManager; 16 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 17 | import org.eclipse.milo.opcua.sdk.server.Namespace; 18 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 19 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 20 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 21 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 22 | import org.eclipse.milo.opcua.sdk.server.model.objects.BaseEventTypeNode; 23 | import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; 24 | import org.eclipse.milo.opcua.sdk.server.nodes.UaNode; 25 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 26 | import org.eclipse.milo.opcua.stack.core.NodeIds; 27 | import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; 28 | import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; 29 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 30 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 31 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 32 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | public class DemoNamespace extends AddressSpaceComposite implements Namespace, Lifecycle { 37 | 38 | public static final String NAMESPACE_URI = 39 | "urn:opc:eclipse:milo:opc-ua-demo-server:namespace:demo"; 40 | 41 | private final Logger logger = LoggerFactory.getLogger(DemoNamespace.class); 42 | 43 | private final LifecycleManager lifecycleManager = new LifecycleManager(); 44 | 45 | private final DemoFragment demoFragment; 46 | 47 | private final UShort namespaceIndex; 48 | 49 | private final Config config; 50 | 51 | public DemoNamespace(OpcUaServer server, Config config) { 52 | super(server); 53 | 54 | this.config = config; 55 | 56 | namespaceIndex = server.getNamespaceTable().add(NAMESPACE_URI); 57 | 58 | lifecycleManager.addLifecycle( 59 | new Lifecycle() { 60 | @Override 61 | public void startup() { 62 | server.getAddressSpaceManager().register(DemoNamespace.this); 63 | } 64 | 65 | @Override 66 | public void shutdown() { 67 | server.getAddressSpaceManager().unregister(DemoNamespace.this); 68 | } 69 | }); 70 | 71 | demoFragment = new DemoFragment(server, this, namespaceIndex); 72 | lifecycleManager.addLifecycle(demoFragment); 73 | 74 | boolean cttEnabled = config.getBoolean("address-space.ctt.enabled"); 75 | if (cttEnabled) { 76 | var cttFragment = new CttNodesFragment(server, this); 77 | lifecycleManager.addLifecycle(cttFragment); 78 | } 79 | 80 | boolean massNodesEnabled = config.getBoolean("address-space.mass.enabled"); 81 | if (massNodesEnabled) { 82 | var massFragment = new MassNodesFragment(server, this); 83 | lifecycleManager.addLifecycle(massFragment); 84 | } 85 | 86 | boolean dataTypeTestEnabled = config.getBoolean("address-space.data-type-test.enabled"); 87 | if (dataTypeTestEnabled) { 88 | var dataTypeTestFragment = new DataTypeTestNodesFragment(server, this); 89 | lifecycleManager.addLifecycle(dataTypeTestFragment); 90 | } 91 | 92 | boolean dynamicNodesEnabled = config.getBoolean("address-space.dynamic.enabled"); 93 | if (dynamicNodesEnabled) { 94 | var dynamicFragment = new DynamicNodesFragment(server, this); 95 | lifecycleManager.addLifecycle(dynamicFragment); 96 | } 97 | 98 | boolean nullNodesEnabled = config.getBoolean("address-space.null.enabled"); 99 | if (nullNodesEnabled) { 100 | var nullFragment = new NullNodesFragment(server, this); 101 | lifecycleManager.addLifecycle(nullFragment); 102 | } 103 | 104 | boolean turtleNodesEnabled = config.getBoolean("address-space.turtles.enabled"); 105 | if (turtleNodesEnabled) { 106 | var turtleFragment = new TurtleNodesFragment(server, this); 107 | lifecycleManager.addLifecycle(turtleFragment); 108 | } 109 | 110 | var rbacFragment = new RbacNodesFragment(server, this); 111 | lifecycleManager.addLifecycle(rbacFragment); 112 | 113 | var debugFragment = new DebugNodesFragment(server, this); 114 | lifecycleManager.addLifecycle(debugFragment); 115 | 116 | var variantFragment = new VariantNodesFragment(server, this); 117 | lifecycleManager.addLifecycle(variantFragment); 118 | 119 | lifecycleManager.addLifecycle(new BogusEventNotifier()); 120 | } 121 | 122 | @Override 123 | public UShort getNamespaceIndex() { 124 | return namespaceIndex; 125 | } 126 | 127 | @Override 128 | public String getNamespaceUri() { 129 | return NAMESPACE_URI; 130 | } 131 | 132 | @Override 133 | public void startup() { 134 | lifecycleManager.startup(); 135 | } 136 | 137 | @Override 138 | public void shutdown() { 139 | lifecycleManager.shutdown(); 140 | } 141 | 142 | public Config getConfig() { 143 | return config; 144 | } 145 | 146 | public UaFolderNode getDemoFolder() { 147 | return demoFragment.getDemoFolder(); 148 | } 149 | 150 | private class BogusEventNotifier implements Lifecycle { 151 | 152 | private final Random random = new Random(); 153 | 154 | private volatile Thread eventThread; 155 | private volatile boolean keepPostingEvents; 156 | 157 | @Override 158 | public void startup() { 159 | keepPostingEvents = true; 160 | eventThread = new Thread(this::fireEventLoop, "bogus-event-notifier"); 161 | eventThread.start(); 162 | } 163 | 164 | private void fireEventLoop() { 165 | try { 166 | Thread.sleep(5000); 167 | } catch (InterruptedException e) { 168 | throw new RuntimeException(e); 169 | } 170 | while (keepPostingEvents) { 171 | fireEvent(); 172 | } 173 | } 174 | 175 | private void fireEvent() { 176 | try { 177 | UaNode serverNode = 178 | getServer().getAddressSpaceManager().getManagedNode(NodeIds.Server).orElseThrow(); 179 | 180 | BaseEventTypeNode eventNode = 181 | getServer() 182 | .getEventFactory() 183 | .createEvent(new NodeId(namespaceIndex, UUID.randomUUID()), NodeIds.BaseEventType); 184 | 185 | byte[] eventId = new byte[4]; 186 | random.nextBytes(eventId); 187 | 188 | eventNode.setBrowseName(new QualifiedName(1, "foo")); 189 | eventNode.setDisplayName(LocalizedText.english("foo")); 190 | eventNode.setEventId(ByteString.of(eventId)); 191 | eventNode.setEventType(NodeIds.BaseEventType); 192 | eventNode.setSourceNode(serverNode.getNodeId()); 193 | eventNode.setSourceName(serverNode.getDisplayName().text()); 194 | eventNode.setTime(DateTime.now()); 195 | eventNode.setReceiveTime(DateTime.NULL_VALUE); 196 | eventNode.setMessage(LocalizedText.english("event message!")); 197 | eventNode.setSeverity(ushort(random.nextInt(10))); 198 | 199 | getServer().getEventNotifier().fire(eventNode); 200 | 201 | eventNode.delete(); 202 | } catch (Throwable e) { 203 | logger.error("Error creating EventNode: {}", e.getMessage(), e); 204 | } 205 | 206 | try { 207 | Thread.sleep(2_000); 208 | } catch (InterruptedException ignored) { 209 | } 210 | } 211 | 212 | @Override 213 | public void shutdown() { 214 | keepPostingEvents = false; 215 | if (eventThread != null) { 216 | try { 217 | eventThread.interrupt(); 218 | eventThread.join(); 219 | } catch (InterruptedException e) { 220 | throw new RuntimeException(e); 221 | } 222 | } 223 | } 224 | } 225 | 226 | private static class DemoFragment extends ManagedAddressSpaceFragmentWithLifecycle { 227 | 228 | private final AddressSpaceFilter filter = 229 | SimpleAddressSpaceFilter.create(getNodeManager()::containsNode); 230 | 231 | private final UaFolderNode demoFolder; 232 | 233 | private final SubscriptionModel subscriptionModel; 234 | 235 | public DemoFragment( 236 | OpcUaServer server, AddressSpaceComposite composite, UShort namespaceIndex) { 237 | 238 | super(server, composite); 239 | 240 | subscriptionModel = new SubscriptionModel(server, composite); 241 | getLifecycleManager().addLifecycle(subscriptionModel); 242 | 243 | demoFolder = 244 | new UaFolderNode( 245 | getNodeContext(), 246 | new NodeId(namespaceIndex, "Demo"), 247 | new QualifiedName(namespaceIndex, "Demo"), 248 | new LocalizedText("Demo")); 249 | 250 | getLifecycleManager() 251 | .addStartupTask( 252 | () -> { 253 | getNodeManager().addNode(demoFolder); 254 | 255 | demoFolder.addReference( 256 | new Reference( 257 | demoFolder.getNodeId(), 258 | NodeIds.Organizes, 259 | NodeIds.ObjectsFolder.expanded(), 260 | Direction.INVERSE)); 261 | }); 262 | } 263 | 264 | public UaFolderNode getDemoFolder() { 265 | return demoFolder; 266 | } 267 | 268 | @Override 269 | public AddressSpaceFilter getFilter() { 270 | return filter; 271 | } 272 | 273 | @Override 274 | public void onDataItemsCreated(List dataItems) { 275 | subscriptionModel.onDataItemsCreated(dataItems); 276 | } 277 | 278 | @Override 279 | public void onDataItemsModified(List dataItems) { 280 | subscriptionModel.onDataItemsModified(dataItems); 281 | } 282 | 283 | @Override 284 | public void onDataItemsDeleted(List dataItems) { 285 | subscriptionModel.onDataItemsDeleted(dataItems); 286 | } 287 | 288 | @Override 289 | public void onMonitoringModeChanged(List monitoredItems) { 290 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/DynamicNodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static com.digitalpetri.opcua.server.namespace.demo.Util.deriveChildNodeId; 4 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; 5 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 6 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong; 7 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.UUID; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.concurrent.ScheduledFuture; 14 | import java.util.concurrent.TimeUnit; 15 | import org.eclipse.milo.opcua.sdk.core.AccessLevel; 16 | import org.eclipse.milo.opcua.sdk.core.Reference; 17 | import org.eclipse.milo.opcua.sdk.core.Reference.Direction; 18 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 19 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 20 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 21 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 22 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 23 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 24 | import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; 25 | import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode.UaVariableNodeBuilder; 26 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters; 27 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 28 | import org.eclipse.milo.opcua.stack.core.OpcUaDataType; 29 | import org.eclipse.milo.opcua.stack.core.ReferenceTypes; 30 | import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; 31 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 32 | import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; 33 | import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject; 34 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 35 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 36 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 37 | import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; 38 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 39 | import org.eclipse.milo.opcua.stack.core.types.builtin.XmlElement; 40 | 41 | public class DynamicNodesFragment extends ManagedAddressSpaceFragmentWithLifecycle { 42 | 43 | private final Map randomValues = new ConcurrentHashMap<>(); 44 | 45 | private final AddressSpaceFilter filter; 46 | private final SubscriptionModel subscriptionModel; 47 | 48 | private final DemoNamespace namespace; 49 | 50 | public DynamicNodesFragment(OpcUaServer server, DemoNamespace namespace) { 51 | super(server, namespace); 52 | 53 | this.namespace = namespace; 54 | 55 | filter = SimpleAddressSpaceFilter.create(getNodeManager()::containsNode); 56 | 57 | subscriptionModel = new SubscriptionModel(server, this); 58 | getLifecycleManager().addLifecycle(subscriptionModel); 59 | 60 | ScheduledFuture scheduledFuture = 61 | server 62 | .getConfig() 63 | .getScheduledExecutorService() 64 | .scheduleAtFixedRate( 65 | () -> getServer().getConfig().getExecutor().execute(this::updateRandomValues), 66 | 0, 67 | 100, 68 | TimeUnit.MILLISECONDS); 69 | 70 | getLifecycleManager().addShutdownTask(() -> scheduledFuture.cancel(true)); 71 | 72 | getLifecycleManager().addStartupTask(this::addDynamicNodes); 73 | } 74 | 75 | @Override 76 | public AddressSpaceFilter getFilter() { 77 | return filter; 78 | } 79 | 80 | @Override 81 | public void onDataItemsCreated(List dataItems) { 82 | subscriptionModel.onDataItemsCreated(dataItems); 83 | } 84 | 85 | @Override 86 | public void onDataItemsModified(List dataItems) { 87 | subscriptionModel.onDataItemsModified(dataItems); 88 | } 89 | 90 | @Override 91 | public void onDataItemsDeleted(List dataItems) { 92 | subscriptionModel.onDataItemsDeleted(dataItems); 93 | } 94 | 95 | @Override 96 | public void onMonitoringModeChanged(List monitoredItems) { 97 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 98 | } 99 | 100 | private void addDynamicNodes() { 101 | var dynamicFolder = 102 | new UaFolderNode( 103 | getNodeContext(), 104 | deriveChildNodeId(namespace.getDemoFolder().getNodeId(), "Dynamic"), 105 | new QualifiedName(namespace.getNamespaceIndex(), "Dynamic"), 106 | new LocalizedText("Dynamic")); 107 | 108 | getNodeManager().addNode(dynamicFolder); 109 | 110 | dynamicFolder.addReference( 111 | new Reference( 112 | dynamicFolder.getNodeId(), 113 | ReferenceTypes.Organizes, 114 | namespace.getDemoFolder().getNodeId().expanded(), 115 | Direction.INVERSE)); 116 | 117 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 118 | if (dataType == OpcUaDataType.DiagnosticInfo) continue; 119 | 120 | var builder = new UaVariableNodeBuilder(getNodeContext()); 121 | builder 122 | .setNodeId(deriveChildNodeId(dynamicFolder.getNodeId(), dataType.name())) 123 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), dataType.name())) 124 | .setDisplayName(new LocalizedText(dataType.name())) 125 | .setDataType(dataType.getNodeId()) 126 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_ONLY)) 127 | .setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_ONLY)) 128 | .setMinimumSamplingInterval(100.0); 129 | 130 | var variableNode = builder.build(); 131 | 132 | variableNode 133 | .getFilterChain() 134 | .addLast( 135 | AttributeFilters.getValue( 136 | ctx -> randomValues.getOrDefault(dataType, new DataValue(Variant.NULL_VALUE)))); 137 | 138 | getNodeManager().addNode(variableNode); 139 | 140 | variableNode.addReference( 141 | new Reference( 142 | variableNode.getNodeId(), 143 | ReferenceTypes.HasComponent, 144 | dynamicFolder.getNodeId().expanded(), 145 | Direction.INVERSE)); 146 | } 147 | } 148 | 149 | private void updateRandomValues() { 150 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 151 | randomValues.put(dataType, getRandomValue(dataType)); 152 | } 153 | } 154 | 155 | private static DataValue getRandomValue(OpcUaDataType dataType) { 156 | Object v = 157 | switch (dataType) { 158 | case Boolean -> Math.random() > 0.5; 159 | case SByte -> (byte) (Math.random() * 256 - 128); 160 | case Int16 -> (short) (Math.random() * 65536 - 32768); 161 | case Int32 -> (int) (Math.random() * Integer.MAX_VALUE * 2 - Integer.MAX_VALUE); 162 | case Int64 -> (long) (Math.random() * Long.MAX_VALUE); 163 | case Byte -> ubyte((short) (Math.random() * 256)); 164 | case UInt16 -> ushort((int) (Math.random() * 65536)); 165 | case UInt32 -> uint((long) (Math.random() * Integer.MAX_VALUE)); 166 | case UInt64 -> ulong(Math.round(Math.random() * Long.MAX_VALUE)); 167 | case Float -> (float) Math.random() * 1000; 168 | case Double -> Math.random() * 1000; 169 | case String -> UUID.randomUUID().toString(); 170 | case DateTime -> new DateTime(); 171 | case Guid -> UUID.randomUUID(); 172 | case ByteString -> { 173 | byte[] bytes = new byte[16]; 174 | new java.util.Random().nextBytes(bytes); 175 | yield ByteString.of(bytes); 176 | } 177 | case XmlElement -> new XmlElement("" + UUID.randomUUID() + ""); 178 | case NodeId -> new NodeId(1, (int) (Math.random() * 1000)); 179 | case ExpandedNodeId -> new NodeId(1, (int) (Math.random() * 1000)).expanded(); 180 | case StatusCode -> new StatusCode((int) (Math.random() * 0xFFFF)); 181 | case QualifiedName -> 182 | new QualifiedName(1, "Random-" + UUID.randomUUID().toString().substring(0, 8)); 183 | case LocalizedText -> 184 | new LocalizedText("en", "Random-" + UUID.randomUUID().toString().substring(0, 8)); 185 | case ExtensionObject -> { 186 | byte[] bytes = new byte[8]; 187 | new java.util.Random().nextBytes(bytes); 188 | yield ExtensionObject.of(ByteString.of(bytes), NodeId.NULL_VALUE); 189 | } 190 | case DataValue -> new DataValue(Variant.of(Math.random() * 100)); 191 | case Variant -> Variant.of(Math.random() * 100); 192 | case DiagnosticInfo -> null; 193 | }; 194 | 195 | if (v instanceof Variant variant) { 196 | return new DataValue(variant); 197 | } else { 198 | return new DataValue(Variant.of(v)); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/MassNodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static com.digitalpetri.opcua.server.namespace.demo.Util.deriveChildNodeId; 4 | 5 | import java.util.List; 6 | import org.eclipse.milo.opcua.sdk.core.Reference; 7 | import org.eclipse.milo.opcua.sdk.core.Reference.Direction; 8 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 9 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 10 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 11 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 12 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 13 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 14 | import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; 15 | import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode; 16 | import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode.UaObjectNodeBuilder; 17 | import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode; 18 | import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode.UaVariableNodeBuilder; 19 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 20 | import org.eclipse.milo.opcua.stack.core.NodeIds; 21 | import org.eclipse.milo.opcua.stack.core.ReferenceTypes; 22 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 23 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 24 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 25 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 26 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 27 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort; 28 | 29 | public class MassNodesFragment extends ManagedAddressSpaceFragmentWithLifecycle { 30 | 31 | private final SimpleAddressSpaceFilter filter; 32 | private final SubscriptionModel subscriptionModel; 33 | 34 | private final DemoNamespace namespace; 35 | private final UShort namespaceIndex; 36 | 37 | public MassNodesFragment(OpcUaServer server, DemoNamespace namespace) { 38 | super(server, namespace); 39 | 40 | this.namespace = namespace; 41 | this.namespaceIndex = namespace.getNamespaceIndex(); 42 | 43 | filter = SimpleAddressSpaceFilter.create(getNodeManager()::containsNode); 44 | 45 | subscriptionModel = new SubscriptionModel(server, this); 46 | getLifecycleManager().addLifecycle(subscriptionModel); 47 | 48 | getLifecycleManager().addStartupTask(this::addMassNodes); 49 | } 50 | 51 | @Override 52 | public AddressSpaceFilter getFilter() { 53 | return filter; 54 | } 55 | 56 | @Override 57 | public void onDataItemsCreated(List dataItems) { 58 | subscriptionModel.onDataItemsCreated(dataItems); 59 | } 60 | 61 | @Override 62 | public void onDataItemsModified(List dataItems) { 63 | subscriptionModel.onDataItemsModified(dataItems); 64 | } 65 | 66 | @Override 67 | public void onDataItemsDeleted(List dataItems) { 68 | subscriptionModel.onDataItemsDeleted(dataItems); 69 | } 70 | 71 | @Override 72 | public void onMonitoringModeChanged(List monitoredItems) { 73 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 74 | } 75 | 76 | private void addMassNodes() { 77 | var massFolder = 78 | new UaFolderNode( 79 | getNodeContext(), 80 | deriveChildNodeId(namespace.getDemoFolder().getNodeId(), "Mass"), 81 | new QualifiedName(namespaceIndex, "Mass"), 82 | new LocalizedText("Mass")); 83 | 84 | getNodeManager().addNode(massFolder); 85 | 86 | massFolder.addReference( 87 | new Reference( 88 | massFolder.getNodeId(), 89 | ReferenceTypes.HasComponent, 90 | namespace.getDemoFolder().getNodeId().expanded(), 91 | Direction.INVERSE)); 92 | 93 | addFlatNodes(massFolder.getNodeId()); 94 | addNestedNodes(massFolder.getNodeId()); 95 | } 96 | 97 | private void addNestedNodes(NodeId parentNodeId) { 98 | var nestedFolder = 99 | new UaFolderNode( 100 | getNodeContext(), 101 | deriveChildNodeId(parentNodeId, "Nested"), 102 | new QualifiedName(namespaceIndex, "Nested"), 103 | new LocalizedText("Nested")); 104 | 105 | getNodeManager().addNode(nestedFolder); 106 | 107 | nestedFolder.addReference( 108 | new Reference( 109 | nestedFolder.getNodeId(), 110 | ReferenceTypes.HasComponent, 111 | parentNodeId.expanded(), 112 | Direction.INVERSE)); 113 | 114 | int nestedQuantity1 = namespace.getConfig().getInt("address-space.mass.nested-quantity1"); 115 | int nestedQuantity2 = namespace.getConfig().getInt("address-space.mass.nested-quantity2"); 116 | var formatString1 = "%%0%dd".formatted((int) Math.log10(nestedQuantity1 - 1) + 1); 117 | var formatString2 = "%%0%dd".formatted((int) Math.log10(nestedQuantity2 - 1) + 1); 118 | 119 | for (int i = 0; i < nestedQuantity1; i++) { 120 | String outerName = formatString1.formatted(i); 121 | var folder = 122 | new UaFolderNode( 123 | getNodeContext(), 124 | deriveChildNodeId(nestedFolder.getNodeId(), outerName), 125 | new QualifiedName(namespaceIndex, outerName), 126 | new LocalizedText(outerName)); 127 | 128 | getNodeManager().addNode(folder); 129 | 130 | folder.addReference( 131 | new Reference( 132 | folder.getNodeId(), 133 | ReferenceTypes.HasComponent, 134 | nestedFolder.getNodeId().expanded(), 135 | Direction.INVERSE)); 136 | 137 | for (int j = 0; j < nestedQuantity2; j++) { 138 | String innerName = formatString2.formatted(j); 139 | var builder = new UaVariableNodeBuilder(getNodeContext()); 140 | builder 141 | .setNodeId(deriveChildNodeId(folder.getNodeId(), innerName)) 142 | .setBrowseName(new QualifiedName(namespaceIndex, innerName)) 143 | .setDisplayName(new LocalizedText(innerName)) 144 | .setDataType(NodeIds.Int32); 145 | 146 | builder.setValue(new DataValue(Variant.ofInt32(j))); 147 | 148 | UaVariableNode variableNode = builder.build(); 149 | 150 | getNodeManager().addNode(variableNode); 151 | 152 | variableNode.addReference( 153 | new Reference( 154 | variableNode.getNodeId(), 155 | ReferenceTypes.HasComponent, 156 | folder.getNodeId().expanded(), 157 | Direction.INVERSE)); 158 | } 159 | } 160 | } 161 | 162 | private void addFlatNodes(NodeId parentNodeId) { 163 | var flatFolder = 164 | new UaFolderNode( 165 | getNodeContext(), 166 | deriveChildNodeId(parentNodeId, "Flat"), 167 | new QualifiedName(namespaceIndex, "Flat"), 168 | new LocalizedText("Flat")); 169 | 170 | getNodeManager().addNode(flatFolder); 171 | 172 | flatFolder.addReference( 173 | new Reference( 174 | flatFolder.getNodeId(), 175 | ReferenceTypes.HasComponent, 176 | parentNodeId.expanded(), 177 | Direction.INVERSE)); 178 | 179 | int flatQuantity = namespace.getConfig().getInt("address-space.mass.flat-quantity"); 180 | var formatString = "%%0%dd".formatted((int) Math.log10(flatQuantity - 1) + 1); 181 | 182 | for (int i = 0; i < flatQuantity; i++) { 183 | String name = formatString.formatted(i); 184 | var builder = new UaObjectNodeBuilder(getNodeContext()); 185 | builder 186 | .setNodeId(deriveChildNodeId(flatFolder.getNodeId(), name)) 187 | .setBrowseName(new QualifiedName(namespaceIndex, name)) 188 | .setDisplayName(new LocalizedText(name)); 189 | 190 | UaObjectNode objectNode = builder.build(); 191 | 192 | getNodeManager().addNode(objectNode); 193 | 194 | objectNode.addReference( 195 | new Reference( 196 | objectNode.getNodeId(), 197 | ReferenceTypes.HasComponent, 198 | flatFolder.getNodeId().expanded(), 199 | Direction.INVERSE)); 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/NullNodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static com.digitalpetri.opcua.server.namespace.demo.Util.deriveChildNodeId; 4 | 5 | import java.util.List; 6 | import org.eclipse.milo.opcua.sdk.core.AccessLevel; 7 | import org.eclipse.milo.opcua.sdk.core.Reference; 8 | import org.eclipse.milo.opcua.sdk.core.Reference.Direction; 9 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 10 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 11 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 12 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 13 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 14 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 15 | import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; 16 | import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode.UaVariableNodeBuilder; 17 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 18 | import org.eclipse.milo.opcua.stack.core.OpcUaDataType; 19 | import org.eclipse.milo.opcua.stack.core.ReferenceTypes; 20 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 21 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 22 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 23 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 24 | 25 | public class NullNodesFragment extends ManagedAddressSpaceFragmentWithLifecycle { 26 | 27 | private final SimpleAddressSpaceFilter filter; 28 | private final SubscriptionModel subscriptionModel; 29 | 30 | private final DemoNamespace namespace; 31 | 32 | public NullNodesFragment(OpcUaServer server, DemoNamespace namespace) { 33 | super(server, namespace); 34 | 35 | this.namespace = namespace; 36 | 37 | filter = SimpleAddressSpaceFilter.create(getNodeManager()::containsNode); 38 | 39 | subscriptionModel = new SubscriptionModel(server, this); 40 | getLifecycleManager().addLifecycle(subscriptionModel); 41 | 42 | getLifecycleManager().addStartupTask(this::addNullNodes); 43 | } 44 | 45 | private void addNullNodes() { 46 | var nullFolder = 47 | new UaFolderNode( 48 | getNodeContext(), 49 | deriveChildNodeId(namespace.getDemoFolder().getNodeId(), "Null"), 50 | new QualifiedName(namespace.getNamespaceIndex(), "Null"), 51 | new LocalizedText("Null")); 52 | 53 | getNodeManager().addNode(nullFolder); 54 | 55 | nullFolder.addReference( 56 | new Reference( 57 | nullFolder.getNodeId(), 58 | ReferenceTypes.Organizes, 59 | namespace.getDemoFolder().getNodeId().expanded(), 60 | Direction.INVERSE)); 61 | 62 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 63 | if (dataType == OpcUaDataType.DiagnosticInfo) continue; 64 | 65 | var builder = new UaVariableNodeBuilder(getNodeContext()); 66 | builder 67 | .setNodeId(deriveChildNodeId(nullFolder.getNodeId(), dataType.name())) 68 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), dataType.name())) 69 | .setDisplayName(new LocalizedText(dataType.name())) 70 | .setDataType(dataType.getNodeId()) 71 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_ONLY)) 72 | .setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_ONLY)) 73 | .setMinimumSamplingInterval(100.0); 74 | 75 | var variableNode = builder.build(); 76 | 77 | variableNode.setValue(new DataValue(Variant.NULL_VALUE)); 78 | 79 | getNodeManager().addNode(variableNode); 80 | 81 | variableNode.addReference( 82 | new Reference( 83 | variableNode.getNodeId(), 84 | ReferenceTypes.HasComponent, 85 | nullFolder.getNodeId().expanded(), 86 | Direction.INVERSE)); 87 | } 88 | } 89 | 90 | @Override 91 | public AddressSpaceFilter getFilter() { 92 | return filter; 93 | } 94 | 95 | @Override 96 | public void onDataItemsCreated(List dataItems) { 97 | subscriptionModel.onDataItemsCreated(dataItems); 98 | } 99 | 100 | @Override 101 | public void onDataItemsModified(List dataItems) { 102 | subscriptionModel.onDataItemsModified(dataItems); 103 | } 104 | 105 | @Override 106 | public void onDataItemsDeleted(List dataItems) { 107 | subscriptionModel.onDataItemsDeleted(dataItems); 108 | } 109 | 110 | @Override 111 | public void onMonitoringModeChanged(List monitoredItems) { 112 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/RbacNodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static com.digitalpetri.opcua.server.namespace.demo.Util.deriveChildNodeId; 4 | 5 | import java.util.List; 6 | import org.eclipse.milo.opcua.sdk.core.AccessLevel; 7 | import org.eclipse.milo.opcua.sdk.core.Reference; 8 | import org.eclipse.milo.opcua.sdk.core.Reference.Direction; 9 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 10 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 11 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 12 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 13 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 14 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 15 | import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; 16 | import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode; 17 | import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode.UaVariableNodeBuilder; 18 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 19 | import org.eclipse.milo.opcua.stack.core.NodeIds; 20 | import org.eclipse.milo.opcua.stack.core.ReferenceTypes; 21 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 22 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 23 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 24 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 25 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 26 | 27 | public class RbacNodesFragment extends ManagedAddressSpaceFragmentWithLifecycle { 28 | 29 | private final AddressSpaceFilter filter = 30 | SimpleAddressSpaceFilter.create(getNodeManager()::containsNode); 31 | 32 | private final SubscriptionModel subscriptionModel; 33 | 34 | private final DemoNamespace namespace; 35 | 36 | public RbacNodesFragment(OpcUaServer server, DemoNamespace namespace) { 37 | super(server, namespace); 38 | 39 | this.namespace = namespace; 40 | 41 | subscriptionModel = new SubscriptionModel(server, this); 42 | getLifecycleManager().addLifecycle(subscriptionModel); 43 | 44 | getLifecycleManager().addStartupTask(this::addRbacNodes); 45 | } 46 | 47 | @Override 48 | public AddressSpaceFilter getFilter() { 49 | return filter; 50 | } 51 | 52 | @Override 53 | public void onDataItemsCreated(List dataItems) { 54 | subscriptionModel.onDataItemsCreated(dataItems); 55 | } 56 | 57 | @Override 58 | public void onDataItemsModified(List dataItems) { 59 | subscriptionModel.onDataItemsModified(dataItems); 60 | } 61 | 62 | @Override 63 | public void onDataItemsDeleted(List dataItems) { 64 | subscriptionModel.onDataItemsDeleted(dataItems); 65 | } 66 | 67 | @Override 68 | public void onMonitoringModeChanged(List monitoredItems) { 69 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 70 | } 71 | 72 | private void addRbacNodes() { 73 | var rbacFolder = 74 | new UaFolderNode( 75 | getNodeContext(), 76 | deriveChildNodeId(namespace.getDemoFolder().getNodeId(), "RBAC"), 77 | new QualifiedName(namespace.getNamespaceIndex(), "RBAC"), 78 | new LocalizedText("RBAC")); 79 | 80 | getNodeManager().addNode(rbacFolder); 81 | 82 | rbacFolder.addReference( 83 | new Reference( 84 | rbacFolder.getNodeId(), 85 | ReferenceTypes.Organizes, 86 | namespace.getDemoFolder().getNodeId().expanded(), 87 | Direction.INVERSE)); 88 | 89 | addSiteNode(rbacFolder.getNodeId(), "SiteA", "rbac.site-a"); 90 | addSiteNode(rbacFolder.getNodeId(), "SiteB", "rbac.site-b"); 91 | } 92 | 93 | private void addSiteNode(NodeId parentNodeId, String site, String key) { 94 | var siteFolder = 95 | new UaFolderNode( 96 | getNodeContext(), 97 | deriveChildNodeId(parentNodeId, site), 98 | new QualifiedName(namespace.getNamespaceIndex(), site), 99 | new LocalizedText(site)); 100 | 101 | getNodeManager().addNode(siteFolder); 102 | 103 | siteFolder.addReference( 104 | new Reference( 105 | siteFolder.getNodeId(), 106 | ReferenceTypes.Organizes, 107 | parentNodeId.expanded(), 108 | Direction.INVERSE)); 109 | 110 | for (int i = 0; i < 5; i++) { 111 | var builder = new UaVariableNodeBuilder(getNodeContext()); 112 | builder 113 | .setNodeId(deriveChildNodeId(siteFolder.getNodeId(), "Variable" + i)) 114 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), "Variable" + i)) 115 | .setDisplayName(new LocalizedText("Variable" + i)) 116 | .setDataType(NodeIds.Int32) 117 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 118 | .setMinimumSamplingInterval(100.0); 119 | 120 | builder.setValue(new DataValue(Variant.ofInt32(i))); 121 | 122 | UaVariableNode variableNode = builder.build(); 123 | 124 | variableNode.getFilterChain().addLast(new AccessControlFilter(namespace.getConfig(), key)); 125 | 126 | getNodeManager().addNode(variableNode); 127 | 128 | variableNode.addReference( 129 | new Reference( 130 | variableNode.getNodeId(), 131 | ReferenceTypes.HasComponent, 132 | siteFolder.getNodeId().expanded(), 133 | Direction.INVERSE)); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/TurtleNodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; 4 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import org.eclipse.milo.opcua.sdk.core.Reference; 9 | import org.eclipse.milo.opcua.sdk.core.Reference.Direction; 10 | import org.eclipse.milo.opcua.sdk.core.nodes.ObjectNodeProperties; 11 | import org.eclipse.milo.opcua.sdk.server.AddressSpace.ReferenceResult.ReferenceList; 12 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 13 | import org.eclipse.milo.opcua.sdk.server.AttributeReader; 14 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 15 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 16 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 17 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 18 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 19 | import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; 20 | import org.eclipse.milo.opcua.sdk.server.nodes.UaNode; 21 | import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode; 22 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 23 | import org.eclipse.milo.opcua.stack.core.NodeIds; 24 | import org.eclipse.milo.opcua.stack.core.ReferenceTypes; 25 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 26 | import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; 27 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 28 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 29 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 30 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 31 | import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn; 32 | import org.eclipse.milo.opcua.stack.core.types.structured.ReadValueId; 33 | import org.eclipse.milo.opcua.stack.core.types.structured.ViewDescription; 34 | import org.jspecify.annotations.Nullable; 35 | 36 | public class TurtleNodesFragment extends ManagedAddressSpaceFragmentWithLifecycle { 37 | 38 | private final long depth; 39 | private final AddressSpaceFilter filter; 40 | private final SubscriptionModel subscriptionModel; 41 | 42 | private final DemoNamespace namespace; 43 | 44 | public TurtleNodesFragment(OpcUaServer server, DemoNamespace namespace) { 45 | super(server, namespace); 46 | 47 | this.namespace = namespace; 48 | 49 | depth = namespace.getConfig().getLong("address-space.turtles.depth"); 50 | 51 | filter = 52 | SimpleAddressSpaceFilter.create( 53 | nodeId -> getNodeManager().containsNode(nodeId) || validTurtleNode(nodeId, depth)); 54 | 55 | subscriptionModel = new SubscriptionModel(server, this); 56 | getLifecycleManager().addLifecycle(subscriptionModel); 57 | 58 | getLifecycleManager().addStartupTask(this::addTurtleNodes); 59 | } 60 | 61 | @Override 62 | public AddressSpaceFilter getFilter() { 63 | return filter; 64 | } 65 | 66 | @Override 67 | public List browse( 68 | BrowseContext context, ViewDescription view, List nodeIds) { 69 | 70 | var results = new ArrayList(); 71 | 72 | for (NodeId nodeId : nodeIds) { 73 | UaNode node = getNodeManager().get(nodeId); 74 | 75 | if (node != null) { 76 | results.add(ReferenceResult.of(node.getReferences())); 77 | } else if (validTurtleNode(nodeId, depth)) { 78 | results.add(ReferenceResult.of(turtleReferences(nodeId))); 79 | } else { 80 | results.add(ReferenceResult.unknown()); 81 | } 82 | } 83 | 84 | return results; 85 | } 86 | 87 | @Override 88 | public ReferenceList gather( 89 | BrowseContext context, ViewDescription viewDescription, NodeId nodeId) { 90 | 91 | var references = new ArrayList(); 92 | references.addAll(getNodeManager().getReferences(nodeId)); 93 | references.addAll(turtleReferences(nodeId)); 94 | 95 | return ReferenceResult.of(references); 96 | } 97 | 98 | @Override 99 | public List read( 100 | ReadContext context, 101 | Double maxAge, 102 | TimestampsToReturn timestamps, 103 | List readValueIds) { 104 | 105 | var values = new ArrayList(); 106 | 107 | for (ReadValueId readValueId : readValueIds) { 108 | UaNode node = getNodeManager().get(readValueId.getNodeId()); 109 | if (node == null) { 110 | node = turtleNode(readValueId.getNodeId()); 111 | } 112 | 113 | if (node != null) { 114 | DataValue value = 115 | AttributeReader.readAttribute( 116 | context, 117 | node, 118 | readValueId.getAttributeId(), 119 | timestamps, 120 | readValueId.getIndexRange(), 121 | readValueId.getDataEncoding()); 122 | 123 | values.add(value); 124 | } else { 125 | values.add(new DataValue(StatusCodes.Bad_NodeIdUnknown)); 126 | } 127 | } 128 | 129 | return values; 130 | } 131 | 132 | @Override 133 | public void onDataItemsCreated(List dataItems) { 134 | subscriptionModel.onDataItemsCreated(dataItems); 135 | } 136 | 137 | @Override 138 | public void onDataItemsModified(List dataItems) { 139 | subscriptionModel.onDataItemsModified(dataItems); 140 | } 141 | 142 | @Override 143 | public void onDataItemsDeleted(List dataItems) { 144 | subscriptionModel.onDataItemsDeleted(dataItems); 145 | } 146 | 147 | @Override 148 | public void onMonitoringModeChanged(List monitoredItems) { 149 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 150 | } 151 | 152 | private void addTurtleNodes() { 153 | var turtlesFolder = 154 | new UaFolderNode( 155 | getNodeContext(), 156 | new NodeId(namespace.getNamespaceIndex(), "[turtles]"), 157 | new QualifiedName(namespace.getNamespaceIndex(), "Turtles"), 158 | new LocalizedText("Turtles")); 159 | 160 | turtlesFolder.setDescription(new LocalizedText("Turtles all the way down!")); 161 | 162 | try (var inputStream = DemoNamespace.class.getResourceAsStream("/turtle-icon.png")) { 163 | if (inputStream != null) { 164 | turtlesFolder.setIcon(ByteString.of(inputStream.readAllBytes())); 165 | turtlesFolder 166 | .getPropertyNode(ObjectNodeProperties.Icon) 167 | .ifPresent(node -> node.setDataType(NodeIds.ImagePNG)); 168 | } 169 | } catch (Exception ignored) { 170 | } 171 | 172 | getNodeManager().addNode(turtlesFolder); 173 | 174 | turtlesFolder.addReference( 175 | new Reference( 176 | turtlesFolder.getNodeId(), 177 | ReferenceTypes.Organizes, 178 | namespace.getDemoFolder().getNodeId().expanded(), 179 | Direction.INVERSE)); 180 | 181 | turtlesFolder.addReference( 182 | new Reference( 183 | turtlesFolder.getNodeId(), 184 | ReferenceTypes.Organizes, 185 | new NodeId(namespace.getNamespaceIndex(), "[turtles]0").expanded(), 186 | Direction.FORWARD)); 187 | } 188 | 189 | private @Nullable UaObjectNode turtleNode(NodeId nodeId) { 190 | try { 191 | long turtleNumber = Long.parseLong(nodeId.getIdentifier().toString().substring(9)); 192 | 193 | if (turtleNumber < depth) { 194 | return new UaObjectNode( 195 | getNodeContext(), 196 | new NodeId(namespace.getNamespaceIndex(), "[turtles]" + turtleNumber), 197 | new QualifiedName(namespace.getNamespaceIndex(), "Turtle" + turtleNumber), 198 | new LocalizedText("Turtle" + turtleNumber), 199 | LocalizedText.NULL_VALUE, 200 | uint(0), 201 | uint(0), 202 | ubyte(0)); 203 | } 204 | } catch (Exception ignored) { 205 | } 206 | 207 | return null; 208 | } 209 | 210 | private List turtleReferences(NodeId nodeId) { 211 | try { 212 | long turtleNumber = Long.parseLong(nodeId.getIdentifier().toString().substring(9)); 213 | long previousTurtle = turtleNumber - 1; 214 | long nextTurtle = turtleNumber + 1; 215 | var references = new ArrayList(); 216 | 217 | if (previousTurtle >= 0) { 218 | references.add( 219 | new Reference( 220 | nodeId, 221 | ReferenceTypes.Organizes, 222 | new NodeId(namespace.getNamespaceIndex(), "[turtles]" + previousTurtle).expanded(), 223 | Direction.INVERSE)); 224 | } 225 | if (nextTurtle < depth) { 226 | references.add( 227 | new Reference( 228 | nodeId, 229 | ReferenceTypes.Organizes, 230 | new NodeId(namespace.getNamespaceIndex(), "[turtles]" + nextTurtle).expanded(), 231 | Direction.FORWARD)); 232 | } 233 | 234 | return references; 235 | } catch (Exception e) { 236 | return List.of(); 237 | } 238 | } 239 | 240 | private static boolean validTurtleNode(NodeId nodeId, long depth) { 241 | String id = nodeId.getIdentifier().toString(); 242 | 243 | if (id.startsWith("[turtles]")) { 244 | String idWithoutPrefix = id.substring(9); 245 | try { 246 | return Long.parseLong(idWithoutPrefix) < depth; 247 | } catch (NumberFormatException ignored) { 248 | } 249 | } 250 | return false; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/Util.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; 4 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 5 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong; 6 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort; 7 | 8 | import java.lang.reflect.Array; 9 | import java.util.UUID; 10 | import org.eclipse.milo.opcua.stack.core.OpcUaDataType; 11 | import org.eclipse.milo.opcua.stack.core.encoding.DefaultEncodingContext; 12 | import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; 13 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 14 | import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; 15 | import org.eclipse.milo.opcua.stack.core.types.builtin.DiagnosticInfo; 16 | import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId; 17 | import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject; 18 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 19 | import org.eclipse.milo.opcua.stack.core.types.builtin.Matrix; 20 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 21 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 22 | import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode; 23 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 24 | import org.eclipse.milo.opcua.stack.core.types.builtin.XmlElement; 25 | import org.eclipse.milo.opcua.stack.core.types.structured.XVType; 26 | 27 | public class Util { 28 | 29 | private Util() {} 30 | 31 | /** 32 | * Derives a child NodeId from a parent NodeId and a name. 33 | * 34 | *

The derived NodeId will have the same namespace index as the parent NodeId, and its 35 | * identifier will be a concatenation of the parent's identifier and the provided name, separated 36 | * by a dot. 37 | * 38 | * @param parentNodeId the parent NodeId. 39 | * @param name the name to derive the child NodeId from. 40 | * @return the derived child NodeId. 41 | */ 42 | public static NodeId deriveChildNodeId(NodeId parentNodeId, String name) { 43 | return new NodeId( 44 | parentNodeId.getNamespaceIndex(), "%s.%s".formatted(parentNodeId.getIdentifier(), name)); 45 | } 46 | 47 | static Object getDefaultScalarValue(OpcUaDataType dataType) { 48 | return switch (dataType) { 49 | case Boolean -> Boolean.FALSE; 50 | case SByte -> (byte) 0; 51 | case Int16 -> (short) 0; 52 | case Int32 -> 0; 53 | case Int64 -> 0L; 54 | case Byte -> ubyte(0); 55 | case UInt16 -> ushort(0); 56 | case UInt32 -> uint(0); 57 | case UInt64 -> ulong(0); 58 | case Float -> 0f; 59 | case Double -> 0.0; 60 | case String -> "hello"; 61 | case DateTime -> DateTime.now(); 62 | case Guid -> UUID.randomUUID(); 63 | case ByteString -> ByteString.of(new byte[] {1, 2, 3, 4}); 64 | case XmlElement -> new XmlElement(""); 65 | case NodeId -> new NodeId(1, "DoesNotExist"); 66 | case ExpandedNodeId -> ExpandedNodeId.of(DemoNamespace.NAMESPACE_URI, "DoesNotExist"); 67 | case StatusCode -> StatusCode.GOOD; 68 | case QualifiedName -> QualifiedName.parse("1:QualifiedName"); 69 | case LocalizedText -> LocalizedText.english("hello"); 70 | case ExtensionObject -> 71 | ExtensionObject.encode(DefaultEncodingContext.INSTANCE, new XVType(1.0, 2.0f)); 72 | case DataValue -> new DataValue(Variant.ofInt32(42)); 73 | case Variant -> Variant.ofInt32(42); 74 | case DiagnosticInfo -> DiagnosticInfo.NULL_VALUE; 75 | }; 76 | } 77 | 78 | static Object getDefaultArrayValue(OpcUaDataType dataType) { 79 | Object value = getDefaultScalarValue(dataType); 80 | Object array = Array.newInstance(value.getClass(), 5); 81 | for (int i = 0; i < 5; i++) { 82 | Array.set(array, i, value); 83 | } 84 | return array; 85 | } 86 | 87 | static Matrix getDefaultMatrixValue(OpcUaDataType dataType) { 88 | Object value = getDefaultScalarValue(dataType); 89 | Object array = Array.newInstance(value.getClass(), 5, 5); 90 | for (int i = 0; i < 5; i++) { 91 | Object innerArray = Array.newInstance(value.getClass(), 5); 92 | for (int j = 0; j < 5; j++) { 93 | Array.set(innerArray, j, value); 94 | } 95 | Array.set(array, i, innerArray); 96 | } 97 | return new Matrix(array); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/VariantNodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo; 2 | 3 | import static com.digitalpetri.opcua.server.namespace.demo.Util.deriveChildNodeId; 4 | import static com.digitalpetri.opcua.server.namespace.demo.Util.getDefaultScalarValue; 5 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import org.eclipse.milo.opcua.sdk.core.AccessLevel; 10 | import org.eclipse.milo.opcua.sdk.core.Reference; 11 | import org.eclipse.milo.opcua.sdk.core.ValueRanks; 12 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 13 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 14 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 15 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 16 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 17 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 18 | import org.eclipse.milo.opcua.sdk.server.nodes.UaFolderNode; 19 | import org.eclipse.milo.opcua.sdk.server.nodes.UaVariableNode.UaVariableNodeBuilder; 20 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 21 | import org.eclipse.milo.opcua.stack.core.OpcUaDataType; 22 | import org.eclipse.milo.opcua.stack.core.ReferenceTypes; 23 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 24 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 25 | import org.eclipse.milo.opcua.stack.core.types.builtin.Matrix; 26 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 27 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 28 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 29 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 30 | 31 | public class VariantNodesFragment extends ManagedAddressSpaceFragmentWithLifecycle { 32 | 33 | private final SimpleAddressSpaceFilter filter; 34 | private final SubscriptionModel subscriptionModel; 35 | 36 | private final DemoNamespace namespace; 37 | 38 | public VariantNodesFragment(OpcUaServer server, DemoNamespace namespace) { 39 | super(server, namespace); 40 | 41 | this.namespace = namespace; 42 | 43 | filter = SimpleAddressSpaceFilter.create(getNodeManager()::containsNode); 44 | 45 | subscriptionModel = new SubscriptionModel(server, this); 46 | getLifecycleManager().addLifecycle(subscriptionModel); 47 | 48 | getLifecycleManager().addStartupTask(this::addVariantNodes); 49 | } 50 | 51 | @Override 52 | public AddressSpaceFilter getFilter() { 53 | return filter; 54 | } 55 | 56 | @Override 57 | public void onDataItemsCreated(List dataItems) { 58 | subscriptionModel.onDataItemsCreated(dataItems); 59 | } 60 | 61 | @Override 62 | public void onDataItemsModified(List dataItems) { 63 | subscriptionModel.onDataItemsModified(dataItems); 64 | } 65 | 66 | @Override 67 | public void onDataItemsDeleted(List dataItems) { 68 | subscriptionModel.onDataItemsDeleted(dataItems); 69 | } 70 | 71 | @Override 72 | public void onMonitoringModeChanged(List monitoredItems) { 73 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 74 | } 75 | 76 | private void addVariantNodes() { 77 | var variantsFolder = 78 | new UaFolderNode( 79 | getNodeContext(), 80 | deriveChildNodeId(namespace.getDemoFolder().getNodeId(), "Variants"), 81 | new QualifiedName(namespace.getNamespaceIndex(), "Variants"), 82 | new LocalizedText("Variants")); 83 | 84 | getNodeManager().addNode(variantsFolder); 85 | 86 | variantsFolder.addReference( 87 | new Reference( 88 | variantsFolder.getNodeId(), 89 | ReferenceTypes.Organizes, 90 | namespace.getDemoFolder().getNodeId().expanded(), 91 | Reference.Direction.INVERSE)); 92 | 93 | addScalarVariants(variantsFolder.getNodeId()); 94 | addArrayVariants(variantsFolder.getNodeId()); 95 | addMatrixVariants(variantsFolder.getNodeId()); 96 | } 97 | 98 | private void addScalarVariants(NodeId parentNodeId) { 99 | var scalarFolder = 100 | new UaFolderNode( 101 | getNodeContext(), 102 | deriveChildNodeId(parentNodeId, "Scalar"), 103 | new QualifiedName(namespace.getNamespaceIndex(), "Scalar"), 104 | new LocalizedText("Scalar")); 105 | 106 | getNodeManager().addNode(scalarFolder); 107 | 108 | scalarFolder.addReference( 109 | new Reference( 110 | scalarFolder.getNodeId(), 111 | ReferenceTypes.Organizes, 112 | parentNodeId.expanded(), 113 | Reference.Direction.INVERSE)); 114 | 115 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 116 | if (dataType == OpcUaDataType.Variant || dataType == OpcUaDataType.DiagnosticInfo) { 117 | continue; 118 | } 119 | 120 | var builder = new UaVariableNodeBuilder(getNodeContext()); 121 | builder 122 | .setNodeId(deriveChildNodeId(scalarFolder.getNodeId(), dataType.name())) 123 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), dataType.name())) 124 | .setDisplayName(new LocalizedText(dataType.name())) 125 | .setDataType(OpcUaDataType.Variant.getNodeId()) 126 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 127 | .setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 128 | .setMinimumSamplingInterval(100.0); 129 | 130 | builder.setValue(new DataValue(Variant.of(getDefaultScalarValue(dataType)))); 131 | 132 | var variableNode = builder.build(); 133 | 134 | getNodeManager().addNode(variableNode); 135 | 136 | variableNode.addReference( 137 | new Reference( 138 | variableNode.getNodeId(), 139 | ReferenceTypes.HasComponent, 140 | scalarFolder.getNodeId().expanded(), 141 | Reference.Direction.INVERSE)); 142 | } 143 | } 144 | 145 | private void addArrayVariants(NodeId parentNodeId) { 146 | var arrayFolder = 147 | new UaFolderNode( 148 | getNodeContext(), 149 | deriveChildNodeId(parentNodeId, "Array"), 150 | new QualifiedName(namespace.getNamespaceIndex(), "Array"), 151 | new LocalizedText("Array")); 152 | 153 | getNodeManager().addNode(arrayFolder); 154 | 155 | arrayFolder.addReference( 156 | new Reference( 157 | arrayFolder.getNodeId(), 158 | ReferenceTypes.Organizes, 159 | parentNodeId.expanded(), 160 | Reference.Direction.INVERSE)); 161 | 162 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 163 | if (dataType == OpcUaDataType.DiagnosticInfo || dataType == OpcUaDataType.Variant) { 164 | continue; 165 | } 166 | 167 | var builder = new UaVariableNodeBuilder(getNodeContext()); 168 | builder 169 | .setNodeId(deriveChildNodeId(arrayFolder.getNodeId(), dataType.name())) 170 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), dataType.name())) 171 | .setDisplayName(new LocalizedText(dataType.name())) 172 | .setDataType(OpcUaDataType.Variant.getNodeId()) 173 | .setValueRank(ValueRanks.OneDimension) 174 | .setArrayDimensions(new UInteger[] {uint(0)}) 175 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 176 | .setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 177 | .setMinimumSamplingInterval(100.0); 178 | 179 | Variant[] variants = new Variant[5]; 180 | for (int i = 0; i < variants.length; i++) { 181 | variants[i] = Variant.of(getDefaultScalarValue(dataType)); 182 | } 183 | builder.setValue(new DataValue(Variant.ofVariantArray(variants))); 184 | 185 | var variableNode = builder.build(); 186 | 187 | getNodeManager().addNode(variableNode); 188 | 189 | variableNode.addReference( 190 | new Reference( 191 | variableNode.getNodeId(), 192 | ReferenceTypes.HasComponent, 193 | arrayFolder.getNodeId().expanded(), 194 | Reference.Direction.INVERSE)); 195 | } 196 | 197 | // Add an array that contains Variant elements of each scalar type. 198 | { 199 | var builder = new UaVariableNodeBuilder(getNodeContext()); 200 | builder 201 | .setNodeId(deriveChildNodeId(arrayFolder.getNodeId(), "Variant")) 202 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), "Variant")) 203 | .setDisplayName(new LocalizedText("Variant")) 204 | .setDataType(OpcUaDataType.Variant.getNodeId()) 205 | .setValueRank(ValueRanks.OneDimension) 206 | .setArrayDimensions(new UInteger[] {uint(0)}) 207 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 208 | .setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 209 | .setMinimumSamplingInterval(100.0); 210 | 211 | var variants = new ArrayList(); 212 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 213 | if (dataType == OpcUaDataType.DiagnosticInfo || dataType == OpcUaDataType.Variant) { 214 | continue; 215 | } 216 | variants.add(Variant.of(getDefaultScalarValue(dataType))); 217 | } 218 | builder.setValue(new DataValue(Variant.ofVariantArray(variants.toArray(new Variant[0])))); 219 | 220 | var variableNode = builder.build(); 221 | 222 | getNodeManager().addNode(variableNode); 223 | 224 | variableNode.addReference( 225 | new Reference( 226 | variableNode.getNodeId(), 227 | ReferenceTypes.HasComponent, 228 | arrayFolder.getNodeId().expanded(), 229 | Reference.Direction.INVERSE)); 230 | } 231 | } 232 | 233 | private void addMatrixVariants(NodeId parentNodeId) { 234 | var matrixFolder = 235 | new UaFolderNode( 236 | getNodeContext(), 237 | deriveChildNodeId(parentNodeId, "Matrix"), 238 | new QualifiedName(namespace.getNamespaceIndex(), "Matrix"), 239 | new LocalizedText("Matrix")); 240 | 241 | getNodeManager().addNode(matrixFolder); 242 | 243 | matrixFolder.addReference( 244 | new Reference( 245 | matrixFolder.getNodeId(), 246 | ReferenceTypes.Organizes, 247 | parentNodeId.expanded(), 248 | Reference.Direction.INVERSE)); 249 | 250 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 251 | if (dataType == OpcUaDataType.DiagnosticInfo || dataType == OpcUaDataType.Variant) { 252 | continue; 253 | } 254 | 255 | var builder = new UaVariableNodeBuilder(getNodeContext()); 256 | builder 257 | .setNodeId(deriveChildNodeId(matrixFolder.getNodeId(), dataType.name())) 258 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), dataType.name())) 259 | .setDisplayName(new LocalizedText(dataType.name())) 260 | .setDataType(OpcUaDataType.Variant.getNodeId()) 261 | .setValueRank(2) 262 | .setArrayDimensions(new UInteger[] {uint(0), uint(0)}) 263 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 264 | .setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 265 | .setMinimumSamplingInterval(100.0); 266 | 267 | Variant[][] variants = new Variant[5][5]; 268 | 269 | for (int i = 0; i < variants.length; i++) { 270 | for (int j = 0; j < variants[i].length; j++) { 271 | variants[i][j] = Variant.of(getDefaultScalarValue(dataType)); 272 | } 273 | } 274 | 275 | builder.setValue(new DataValue(Variant.ofMatrix(Matrix.ofVariant(variants)))); 276 | 277 | var variableNode = builder.build(); 278 | 279 | getNodeManager().addNode(variableNode); 280 | 281 | variableNode.addReference( 282 | new Reference( 283 | variableNode.getNodeId(), 284 | ReferenceTypes.HasComponent, 285 | matrixFolder.getNodeId().expanded(), 286 | Reference.Direction.INVERSE)); 287 | } 288 | 289 | // Add a Matrix that contains Variant elements of each scalar type. 290 | { 291 | var builder = new UaVariableNodeBuilder(getNodeContext()); 292 | builder 293 | .setNodeId(deriveChildNodeId(matrixFolder.getNodeId(), "Variant")) 294 | .setBrowseName(new QualifiedName(namespace.getNamespaceIndex(), "Variant")) 295 | .setDisplayName(new LocalizedText("Variant")) 296 | .setDataType(OpcUaDataType.Variant.getNodeId()) 297 | .setValueRank(2) 298 | .setArrayDimensions(new UInteger[] {uint(0), uint(0)}) 299 | .setAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 300 | .setUserAccessLevel(AccessLevel.toValue(AccessLevel.READ_WRITE)) 301 | .setMinimumSamplingInterval(100.0); 302 | 303 | var variants = new ArrayList(); 304 | for (OpcUaDataType dataType : OpcUaDataType.values()) { 305 | if (dataType == OpcUaDataType.DiagnosticInfo || dataType == OpcUaDataType.Variant) { 306 | continue; 307 | } 308 | variants.add( 309 | new Variant[] { 310 | Variant.of(getDefaultScalarValue(dataType)), 311 | Variant.of(getDefaultScalarValue(dataType)), 312 | Variant.of(getDefaultScalarValue(dataType)), 313 | Variant.of(getDefaultScalarValue(dataType)) 314 | }); 315 | } 316 | builder.setValue( 317 | new DataValue(Variant.ofMatrix(Matrix.ofVariant(variants.toArray(new Variant[0][]))))); 318 | 319 | var variableNode = builder.build(); 320 | 321 | getNodeManager().addNode(variableNode); 322 | 323 | variableNode.addReference( 324 | new Reference( 325 | variableNode.getNodeId(), 326 | ReferenceTypes.HasComponent, 327 | matrixFolder.getNodeId().expanded(), 328 | Reference.Direction.INVERSE)); 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/debug/DebugNodesFragment.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo.debug; 2 | 3 | import static com.digitalpetri.opcua.server.namespace.demo.Util.deriveChildNodeId; 4 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; 5 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 6 | 7 | import com.digitalpetri.opcua.server.namespace.demo.DemoNamespace; 8 | import java.util.List; 9 | import org.eclipse.milo.opcua.sdk.core.Reference; 10 | import org.eclipse.milo.opcua.sdk.core.Reference.Direction; 11 | import org.eclipse.milo.opcua.sdk.server.AddressSpaceFilter; 12 | import org.eclipse.milo.opcua.sdk.server.ManagedAddressSpaceFragmentWithLifecycle; 13 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 14 | import org.eclipse.milo.opcua.sdk.server.SimpleAddressSpaceFilter; 15 | import org.eclipse.milo.opcua.sdk.server.items.DataItem; 16 | import org.eclipse.milo.opcua.sdk.server.items.MonitoredItem; 17 | import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode; 18 | import org.eclipse.milo.opcua.sdk.server.nodes.UaObjectNode; 19 | import org.eclipse.milo.opcua.sdk.server.util.SubscriptionModel; 20 | import org.eclipse.milo.opcua.stack.core.NodeIds; 21 | import org.eclipse.milo.opcua.stack.core.ReferenceTypes; 22 | import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText; 23 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 24 | import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName; 25 | import org.eclipse.milo.opcua.stack.core.types.structured.Argument; 26 | 27 | public class DebugNodesFragment extends ManagedAddressSpaceFragmentWithLifecycle { 28 | 29 | private final AddressSpaceFilter filter; 30 | private final SubscriptionModel subscriptionModel; 31 | 32 | private final DemoNamespace namespace; 33 | 34 | public DebugNodesFragment(OpcUaServer server, DemoNamespace namespace) { 35 | super(server, namespace); 36 | 37 | this.namespace = namespace; 38 | 39 | filter = SimpleAddressSpaceFilter.create(getNodeManager()::containsNode); 40 | 41 | subscriptionModel = new SubscriptionModel(server, this); 42 | getLifecycleManager().addLifecycle(subscriptionModel); 43 | 44 | getLifecycleManager().addStartupTask(this::addDebugNodes); 45 | } 46 | 47 | @Override 48 | public AddressSpaceFilter getFilter() { 49 | return filter; 50 | } 51 | 52 | @Override 53 | public void onDataItemsCreated(List dataItems) { 54 | subscriptionModel.onDataItemsCreated(dataItems); 55 | } 56 | 57 | @Override 58 | public void onDataItemsModified(List dataItems) { 59 | subscriptionModel.onDataItemsModified(dataItems); 60 | } 61 | 62 | @Override 63 | public void onDataItemsDeleted(List dataItems) { 64 | subscriptionModel.onDataItemsDeleted(dataItems); 65 | } 66 | 67 | @Override 68 | public void onMonitoringModeChanged(List monitoredItems) { 69 | subscriptionModel.onMonitoringModeChanged(monitoredItems); 70 | } 71 | 72 | private void addDebugNodes() { 73 | UaObjectNode debugNode = 74 | new UaObjectNode( 75 | getNodeContext(), 76 | new NodeId(namespace.getNamespaceIndex(), "Debug"), 77 | new QualifiedName(namespace.getNamespaceIndex(), "Debug"), 78 | LocalizedText.english("Debug"), 79 | LocalizedText.NULL_VALUE, 80 | uint(0), 81 | uint(0), 82 | ubyte(0)); 83 | 84 | getNodeManager().addNode(debugNode); 85 | 86 | debugNode.addReference( 87 | new Reference( 88 | debugNode.getNodeId(), 89 | ReferenceTypes.HasComponent, 90 | NodeIds.ObjectsFolder.expanded(), 91 | Direction.INVERSE)); 92 | 93 | addDeleteSubscriptionMethod(debugNode.getNodeId()); 94 | } 95 | 96 | private void addDeleteSubscriptionMethod(NodeId parentNodeId) { 97 | UaMethodNode deleteSubscriptionNode = 98 | new UaMethodNode( 99 | getNodeContext(), 100 | deriveChildNodeId(parentNodeId, "DeleteSubscription"), 101 | new QualifiedName(namespace.getNamespaceIndex(), "DeleteSubscription"), 102 | LocalizedText.english("DeleteSubscription"), 103 | LocalizedText.NULL_VALUE, 104 | uint(0), 105 | uint(0), 106 | true, 107 | true); 108 | 109 | deleteSubscriptionNode.setInputArguments( 110 | new Argument[] {DeleteSubscriptionMethod.SUBSCRIPTION_ID}); 111 | deleteSubscriptionNode.setOutputArguments(new Argument[0]); 112 | deleteSubscriptionNode.setInvocationHandler( 113 | new DeleteSubscriptionMethod(deleteSubscriptionNode)); 114 | 115 | getNodeManager().addNode(deleteSubscriptionNode); 116 | 117 | deleteSubscriptionNode.addReference( 118 | new Reference( 119 | deleteSubscriptionNode.getNodeId(), 120 | NodeIds.HasComponent, 121 | parentNodeId.expanded(), 122 | Direction.INVERSE)); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/demo/debug/DeleteSubscriptionMethod.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.demo.debug; 2 | 3 | import org.eclipse.milo.opcua.sdk.core.ValueRanks; 4 | import org.eclipse.milo.opcua.sdk.server.Session; 5 | import org.eclipse.milo.opcua.sdk.server.methods.AbstractMethodInvocationHandler; 6 | import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode; 7 | import org.eclipse.milo.opcua.sdk.server.subscriptions.Subscription; 8 | import org.eclipse.milo.opcua.stack.core.NodeIds; 9 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 10 | import org.eclipse.milo.opcua.stack.core.UaException; 11 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 12 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 13 | import org.eclipse.milo.opcua.stack.core.types.structured.Argument; 14 | 15 | public class DeleteSubscriptionMethod extends AbstractMethodInvocationHandler { 16 | 17 | public static final Argument SUBSCRIPTION_ID = 18 | new Argument("SubscriptionId", NodeIds.UInt32, ValueRanks.Scalar, null, null); 19 | 20 | public DeleteSubscriptionMethod(UaMethodNode node) { 21 | super(node); 22 | } 23 | 24 | @Override 25 | public Argument[] getInputArguments() { 26 | return new Argument[] {SUBSCRIPTION_ID}; 27 | } 28 | 29 | @Override 30 | public Argument[] getOutputArguments() { 31 | return new Argument[0]; 32 | } 33 | 34 | @Override 35 | protected Variant[] invoke(InvocationContext invocationContext, Variant[] inputValues) 36 | throws UaException { 37 | 38 | Session session = invocationContext.getSession().orElseThrow(); 39 | 40 | Object iv0 = inputValues[0].getValue(); 41 | 42 | if (iv0 instanceof UInteger subscriptionId) { 43 | Subscription subscription = 44 | session.getSubscriptionManager().removeSubscription(subscriptionId); 45 | 46 | if (subscription != null) { 47 | subscription.deleteSubscription(); 48 | return new Variant[0]; 49 | } else { 50 | throw new UaException(StatusCodes.Bad_SubscriptionIdInvalid); 51 | } 52 | } else { 53 | throw new UaException(StatusCodes.Bad_InvalidArgument); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/namespace/test/DataTypeTestNamespace.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.namespace.test; 2 | 3 | import com.digitalpetri.opcua.test.DataTypeInitializer; 4 | import com.digitalpetri.opcua.uanodeset.namespace.NodeSetNamespace; 5 | import java.io.InputStream; 6 | import java.util.List; 7 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 8 | import org.eclipse.milo.opcua.stack.core.encoding.EncodingContext; 9 | 10 | public class DataTypeTestNamespace extends NodeSetNamespace { 11 | 12 | public static final String NAMESPACE_URI = "https://github.com/digitalpetri/DataTypeTest"; 13 | 14 | public DataTypeTestNamespace(OpcUaServer server) { 15 | super(server, NAMESPACE_URI); 16 | } 17 | 18 | @Override 19 | protected EncodingContext getEncodingContext() { 20 | return getServer().getStaticEncodingContext(); 21 | } 22 | 23 | @Override 24 | protected List getNodeSetInputStreams() { 25 | InputStream inputStream = 26 | DataTypeTestNamespace.class.getResourceAsStream("/DataTypeTest.NodeSet.xml"); 27 | assert inputStream != null; 28 | 29 | return List.of(inputStream); 30 | } 31 | 32 | public static DataTypeTestNamespace create(OpcUaServer server) { 33 | var namespace = new DataTypeTestNamespace(server); 34 | 35 | new DataTypeInitializer() 36 | .initialize(server.getNamespaceTable(), server.getStaticDataTypeManager()); 37 | 38 | return namespace; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/objects/FileObject.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.objects; 2 | 3 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 4 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ulong; 5 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ushort; 6 | 7 | import java.io.File; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.io.RandomAccessFile; 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | import java.util.concurrent.atomic.AtomicLong; 13 | import org.eclipse.milo.opcua.sdk.server.AbstractLifecycle; 14 | import org.eclipse.milo.opcua.sdk.server.Session; 15 | import org.eclipse.milo.opcua.sdk.server.SessionListener; 16 | import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler; 17 | import org.eclipse.milo.opcua.sdk.server.methods.Out; 18 | import org.eclipse.milo.opcua.sdk.server.model.objects.FileType; 19 | import org.eclipse.milo.opcua.sdk.server.model.objects.FileTypeNode; 20 | import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode; 21 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter; 22 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters; 23 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 24 | import org.eclipse.milo.opcua.stack.core.UaException; 25 | import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; 26 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 27 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 28 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 29 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte; 30 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 31 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.ULong; 32 | import org.eclipse.milo.shaded.com.google.common.collect.HashBasedTable; 33 | import org.eclipse.milo.shaded.com.google.common.collect.Table; 34 | import org.eclipse.milo.shaded.com.google.common.collect.Tables; 35 | import org.slf4j.Logger; 36 | import org.slf4j.LoggerFactory; 37 | 38 | /** 39 | * Implementation behavior for an instance of the {@link FileType} Object. 40 | * 41 | * @see 42 | * https://reference.opcfoundation.org/Core/Part20/v105/docs/4.2 43 | */ 44 | public class FileObject extends AbstractLifecycle { 45 | 46 | /** Mask that isolates Read in the mode argument. */ 47 | protected static final int MASK_READ = 0b0001; 48 | 49 | /** Mask that isolates Write in the mode argument. */ 50 | protected static final int MASK_WRITE = 0b00010; 51 | 52 | /** Mask that isolates EraseExisting in the mode argument. */ 53 | protected static final int MASK_ERASE_EXISTING = 0b0100; 54 | 55 | /** Mask that isolates Append in the mode argument. */ 56 | protected static final int MASK_APPEND = 0b1000; 57 | 58 | protected final Logger logger = LoggerFactory.getLogger(getClass()); 59 | 60 | protected final Table handles = 61 | Tables.synchronizedTable(HashBasedTable.create()); 62 | 63 | private volatile SessionListener sessionListener; 64 | 65 | private final FileTypeNode fileNode; 66 | private final FileSupplier fileSupplier; 67 | 68 | public FileObject(FileTypeNode fileNode, File file) { 69 | this(fileNode, () -> file); 70 | } 71 | 72 | public FileObject(FileTypeNode fileNode, FileSupplier fileSupplier) { 73 | this.fileNode = fileNode; 74 | this.fileSupplier = fileSupplier; 75 | } 76 | 77 | @Override 78 | protected void onStartup() { 79 | UaMethodNode openMethodNode = fileNode.getOpenMethodNode(); 80 | openMethodNode.setInvocationHandler(newOpenMethod(openMethodNode)); 81 | 82 | UaMethodNode closeMethodNode = fileNode.getCloseMethodNode(); 83 | closeMethodNode.setInvocationHandler(newCloseMethod(closeMethodNode)); 84 | 85 | UaMethodNode readMethodNode = fileNode.getReadMethodNode(); 86 | readMethodNode.setInvocationHandler(newReadMethod(readMethodNode)); 87 | 88 | UaMethodNode writeMethodNode = fileNode.getWriteMethodNode(); 89 | writeMethodNode.setInvocationHandler(newWriteMethod(writeMethodNode)); 90 | 91 | UaMethodNode getPositionMethodNode = fileNode.getGetPositionMethodNode(); 92 | getPositionMethodNode.setInvocationHandler(newGetPositionMethod(getPositionMethodNode)); 93 | 94 | UaMethodNode setPositionMethodNode = fileNode.getSetPositionMethodNode(); 95 | setPositionMethodNode.setInvocationHandler(newSetPositionMethod(setPositionMethodNode)); 96 | 97 | fileNode.getOpenCountNode().getFilterChain().addLast(newOpenCountAttributeFilter()); 98 | fileNode.getSizeNode().getFilterChain().addLast(newSizeAttributeFilter()); 99 | 100 | // TODO remove AttributeFilters on shutdown 101 | 102 | fileNode 103 | .getNodeContext() 104 | .getServer() 105 | .getSessionManager() 106 | .addSessionListener( 107 | sessionListener = 108 | new SessionListener() { 109 | @Override 110 | public void onSessionClosed(Session session) { 111 | handles.row(session.getSessionId()).clear(); 112 | } 113 | }); 114 | 115 | logger.debug("FileObject started: {}", fileNode.getNodeId()); 116 | } 117 | 118 | @Override 119 | protected void onShutdown() { 120 | fileNode.getOpenMethodNode().setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 121 | fileNode.getCloseMethodNode().setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 122 | fileNode.getReadMethodNode().setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 123 | fileNode.getWriteMethodNode().setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 124 | fileNode 125 | .getGetPositionMethodNode() 126 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 127 | fileNode 128 | .getSetPositionMethodNode() 129 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 130 | 131 | fileNode 132 | .getNodeContext() 133 | .getServer() 134 | .getSessionManager() 135 | .removeSessionListener(sessionListener); 136 | 137 | logger.debug("FileObject stopped: {}", fileNode.getNodeId()); 138 | } 139 | 140 | /** 141 | * @return {@code true} if any file handle is open. 142 | */ 143 | protected boolean isOpen() { 144 | return !handles.isEmpty(); 145 | } 146 | 147 | /** 148 | * @return {@code true} if any file handle is open for writing. 149 | */ 150 | protected boolean isOpenForWriting() { 151 | return handles.values().stream() 152 | .anyMatch(handle -> (handle.mode.intValue() & MASK_WRITE) == MASK_WRITE); 153 | } 154 | 155 | protected FileType.OpenMethod newOpenMethod(UaMethodNode methodNode) { 156 | return new OpenMethodImpl(methodNode); 157 | } 158 | 159 | protected FileType.CloseMethod newCloseMethod(UaMethodNode methodNode) { 160 | return new CloseMethodImpl(methodNode); 161 | } 162 | 163 | protected FileType.ReadMethod newReadMethod(UaMethodNode methodNode) { 164 | return new ReadMethodImpl(methodNode); 165 | } 166 | 167 | protected FileType.WriteMethod newWriteMethod(UaMethodNode methodNode) { 168 | return new WriteMethodImpl(methodNode); 169 | } 170 | 171 | protected FileType.GetPositionMethod newGetPositionMethod(UaMethodNode methodNode) { 172 | return new GetPositionMethodImpl(methodNode); 173 | } 174 | 175 | protected FileType.SetPositionMethod newSetPositionMethod(UaMethodNode methodNode) { 176 | return new SetPositionMethodImpl(methodNode); 177 | } 178 | 179 | protected AttributeFilter newOpenCountAttributeFilter() { 180 | return AttributeFilters.getValue( 181 | ctx -> { 182 | var openCount = ushort(handles.size()); 183 | 184 | return new DataValue(new Variant(openCount)); 185 | }); 186 | } 187 | 188 | protected AttributeFilter newSizeAttributeFilter() { 189 | return AttributeFilters.getValue( 190 | ctx -> { 191 | var length = 0L; 192 | try { 193 | length = fileSupplier.get().length(); 194 | } catch (IOException ignored) { 195 | } 196 | 197 | var size = ulong(length); 198 | 199 | return new DataValue(new Variant(size)); 200 | }); 201 | } 202 | 203 | @FunctionalInterface 204 | public interface FileSupplier { 205 | 206 | /** 207 | * Get a {@link File} instance to be represented by this {@link FileObject}. 208 | * 209 | *

This method will be called each time a new file handle is opened. 210 | * 211 | * @return the {@link File} instance represented by this {@link FileObject}. 212 | * @throws IOException if an I/O error occurs getting the file. 213 | */ 214 | File get() throws IOException; 215 | } 216 | 217 | protected static class FileHandle { 218 | 219 | private final AtomicLong handleSequence = new AtomicLong(0L); 220 | 221 | final UInteger handle = uint(handleSequence.getAndIncrement()); 222 | 223 | final UByte mode; 224 | final RandomAccessFile file; 225 | 226 | public FileHandle(UByte mode, RandomAccessFile file) { 227 | this.mode = mode; 228 | this.file = file; 229 | } 230 | } 231 | 232 | /** 233 | * Default implementation of {@link FileType.OpenMethod}. 234 | * 235 | *

File operations are executed via a {@link RandomAccessFile} constructed using the {@link 236 | * File} instance represented by this {@link FileObject}. 237 | * 238 | * @see 239 | * https://reference.opcfoundation.org/Core/Part20/v105/docs/4.2.2 240 | */ 241 | public class OpenMethodImpl extends FileType.OpenMethod { 242 | 243 | public OpenMethodImpl(UaMethodNode node) { 244 | super(node); 245 | } 246 | 247 | @Override 248 | protected void invoke(InvocationContext context, UByte mode, Out fileHandle) 249 | throws UaException { 250 | 251 | Session session = context.getSession().orElseThrow(); 252 | 253 | if (mode.intValue() == 0) { 254 | throw new UaException(StatusCodes.Bad_InvalidArgument, "invalid mode: " + mode); 255 | } 256 | 257 | // bits: Read, Write, EraseExisting, Append 258 | var modeString = ""; 259 | var erase = false; 260 | 261 | if ((mode.intValue() & MASK_READ) == MASK_READ) { 262 | if (isOpenForWriting()) { 263 | throw new UaException(StatusCodes.Bad_NotReadable, "already open for writing"); 264 | } 265 | modeString += "r"; 266 | } 267 | 268 | if ((mode.intValue() & MASK_WRITE) == MASK_WRITE) { 269 | if (isOpen()) { 270 | throw new UaException(StatusCodes.Bad_NotWritable, "already open"); 271 | } 272 | if (modeString.startsWith("r")) { 273 | modeString += "ws"; 274 | } else { 275 | modeString += "rws"; 276 | } 277 | } 278 | 279 | if ((mode.intValue() & MASK_ERASE_EXISTING) == MASK_ERASE_EXISTING) { 280 | if ((mode.intValue() & MASK_WRITE) != MASK_WRITE) { 281 | throw new UaException(StatusCodes.Bad_InvalidArgument, "EraseExisting requires Write"); 282 | } 283 | erase = true; 284 | } 285 | 286 | try { 287 | File file = fileSupplier.get(); 288 | 289 | if (erase) { 290 | try { 291 | new FileOutputStream(file).close(); 292 | } catch (IOException e) { 293 | throw new UaException(StatusCodes.Bad_NotWritable, e); 294 | } 295 | } 296 | 297 | var handle = new FileHandle(mode, new RandomAccessFile(file, modeString)); 298 | handles.put(session.getSessionId(), handle.handle, handle); 299 | 300 | fileHandle.set(handle.handle); 301 | } catch (IOException e) { 302 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 303 | } 304 | } 305 | } 306 | 307 | /** 308 | * Default implementation of {@link FileType.CloseMethod}. 309 | * 310 | * @see 311 | * https://reference.opcfoundation.org/Core/Part20/v105/docs/4.2.3 312 | */ 313 | public class CloseMethodImpl extends FileType.CloseMethod { 314 | 315 | public CloseMethodImpl(UaMethodNode node) { 316 | super(node); 317 | } 318 | 319 | @Override 320 | protected void invoke(InvocationContext context, UInteger fileHandle) throws UaException { 321 | Session session = context.getSession().orElseThrow(); 322 | 323 | FileHandle handle = handles.remove(session.getSessionId(), fileHandle); 324 | 325 | if (handle == null) { 326 | throw new UaException(StatusCodes.Bad_NotFound); 327 | } 328 | 329 | try { 330 | handle.file.close(); 331 | } catch (IOException e) { 332 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 333 | } 334 | } 335 | } 336 | 337 | /** 338 | * Default implementation of {@link FileType.ReadMethod}. 339 | * 340 | * @see 341 | * https://reference.opcfoundation.org/Core/Part20/v105/docs/4.2.4 342 | */ 343 | public class ReadMethodImpl extends FileType.ReadMethod { 344 | 345 | public ReadMethodImpl(UaMethodNode node) { 346 | super(node); 347 | } 348 | 349 | @Override 350 | protected void invoke( 351 | InvocationContext context, UInteger fileHandle, Integer length, Out data) 352 | throws UaException { 353 | 354 | Session session = context.getSession().orElseThrow(); 355 | 356 | FileHandle handle = handles.get(session.getSessionId(), fileHandle); 357 | 358 | if (handle == null) { 359 | throw new UaException(StatusCodes.Bad_NotFound); 360 | } 361 | 362 | if ((handle.mode.intValue() & MASK_READ) != MASK_READ) { 363 | throw new UaException(StatusCodes.Bad_NotReadable); 364 | } 365 | 366 | try { 367 | byte[] bs = new byte[length]; 368 | int read = handle.file.read(bs); 369 | 370 | if (read == -1) { 371 | data.set(ByteString.of(new byte[0])); 372 | } else if (read < length) { 373 | byte[] partial = new byte[read]; 374 | System.arraycopy(bs, 0, partial, 0, read); 375 | data.set(ByteString.of(partial)); 376 | } else { 377 | data.set(ByteString.of(bs)); 378 | } 379 | } catch (IOException e) { 380 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 381 | } 382 | } 383 | } 384 | 385 | /** 386 | * Default implementation of {@link FileType.WriteMethod}. 387 | * 388 | * @see 389 | * https://reference.opcfoundation.org/Core/Part20/v105/docs/4.2.5 390 | */ 391 | public class WriteMethodImpl extends FileType.WriteMethod { 392 | 393 | /** Tracks if the file has been repositioned for append before the first write. */ 394 | private final AtomicBoolean repositioned = new AtomicBoolean(false); 395 | 396 | public WriteMethodImpl(UaMethodNode node) { 397 | super(node); 398 | } 399 | 400 | @Override 401 | protected void invoke(InvocationContext context, UInteger fileHandle, ByteString data) 402 | throws UaException { 403 | 404 | Session session = context.getSession().orElseThrow(); 405 | FileHandle handle = handles.get(session.getSessionId(), fileHandle); 406 | 407 | if (handle == null) { 408 | throw new UaException(StatusCodes.Bad_NotFound); 409 | } 410 | 411 | if ((handle.mode.intValue() & MASK_WRITE) != MASK_WRITE) { 412 | throw new UaException(StatusCodes.Bad_NotWritable); 413 | } 414 | 415 | if ((handle.mode.intValue() & MASK_APPEND) == MASK_APPEND) { 416 | if (repositioned.compareAndSet(false, true)) { 417 | try { 418 | handle.file.seek(handle.file.length()); 419 | } catch (IOException e) { 420 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 421 | } 422 | } 423 | } 424 | 425 | try { 426 | handle.file.write(data.bytes()); 427 | } catch (IOException e) { 428 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 429 | } 430 | } 431 | } 432 | 433 | /** 434 | * Default implementation of {@link FileType.GetPositionMethod}. 435 | * 436 | * @see 437 | * https://reference.opcfoundation.org/Core/Part20/v105/docs/4.2.6 438 | */ 439 | public class GetPositionMethodImpl extends FileType.GetPositionMethod { 440 | 441 | public GetPositionMethodImpl(UaMethodNode node) { 442 | super(node); 443 | } 444 | 445 | @Override 446 | protected void invoke(InvocationContext context, UInteger fileHandle, Out position) 447 | throws UaException { 448 | 449 | Session session = context.getSession().orElseThrow(); 450 | 451 | FileHandle handle = handles.get(session.getSessionId(), fileHandle); 452 | 453 | if (handle == null) { 454 | throw new UaException(StatusCodes.Bad_NotFound); 455 | } 456 | 457 | try { 458 | position.set(ulong(handle.file.getFilePointer())); 459 | } catch (IOException e) { 460 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 461 | } 462 | } 463 | } 464 | 465 | /** 466 | * Default implementation of {@link FileType.SetPositionMethod}. 467 | * 468 | * @see 469 | * https://reference.opcfoundation.org/Core/Part20/v105/docs/4.2.7 470 | */ 471 | public class SetPositionMethodImpl extends FileType.SetPositionMethod { 472 | 473 | public SetPositionMethodImpl(UaMethodNode node) { 474 | super(node); 475 | } 476 | 477 | @Override 478 | protected void invoke(InvocationContext context, UInteger fileHandle, ULong position) 479 | throws UaException { 480 | 481 | Session session = context.getSession().orElseThrow(); 482 | 483 | FileHandle handle = handles.get(session.getSessionId(), fileHandle); 484 | 485 | if (handle == null) { 486 | throw new UaException(StatusCodes.Bad_NotFound); 487 | } 488 | 489 | try { 490 | handle.file.seek(position.longValue()); 491 | } catch (IOException e) { 492 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 493 | } 494 | } 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/objects/SecurityAdminFilter.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.objects; 2 | 3 | import java.util.Collections; 4 | import org.eclipse.milo.opcua.sdk.server.Session; 5 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter; 6 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilterContext; 7 | import org.eclipse.milo.opcua.stack.core.AttributeId; 8 | import org.eclipse.milo.opcua.stack.core.NodeIds; 9 | 10 | public class SecurityAdminFilter implements AttributeFilter { 11 | 12 | @Override 13 | public Object getAttribute(AttributeFilterContext ctx, AttributeId attributeId) { 14 | return switch (attributeId) { 15 | case Executable -> true; 16 | 17 | case UserExecutable -> 18 | ctx.getSession() 19 | .flatMap(Session::getRoleIds) 20 | .orElse(Collections.emptyList()) 21 | .contains(NodeIds.WellKnownRole_SecurityAdmin); 22 | 23 | default -> ctx.getAttribute(attributeId); 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/objects/ServerConfigurationObject.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.objects; 2 | 3 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 4 | 5 | import java.io.ByteArrayInputStream; 6 | import java.io.InputStreamReader; 7 | import java.security.*; 8 | import java.security.cert.CertificateEncodingException; 9 | import java.security.cert.X509Certificate; 10 | import java.security.spec.PKCS8EncodedKeySpec; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.stream.Collectors; 17 | import org.bouncycastle.asn1.x500.X500Name; 18 | import org.bouncycastle.asn1.x500.style.IETFUtils; 19 | import org.bouncycastle.asn1.x500.style.RFC4519Style; 20 | import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; 21 | import org.bouncycastle.util.io.pem.PemReader; 22 | import org.eclipse.milo.opcua.sdk.server.AbstractLifecycle; 23 | import org.eclipse.milo.opcua.sdk.server.OpcUaServer; 24 | import org.eclipse.milo.opcua.sdk.server.Session; 25 | import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler; 26 | import org.eclipse.milo.opcua.sdk.server.methods.Out; 27 | import org.eclipse.milo.opcua.sdk.server.model.objects.CertificateGroupTypeNode; 28 | import org.eclipse.milo.opcua.sdk.server.model.objects.ServerConfigurationType; 29 | import org.eclipse.milo.opcua.sdk.server.model.objects.ServerConfigurationTypeNode; 30 | import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode; 31 | import org.eclipse.milo.opcua.sdk.server.nodes.UaNode; 32 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters; 33 | import org.eclipse.milo.opcua.stack.core.NodeIds; 34 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 35 | import org.eclipse.milo.opcua.stack.core.UaException; 36 | import org.eclipse.milo.opcua.stack.core.security.CertificateGroup; 37 | import org.eclipse.milo.opcua.stack.core.security.CertificateQuarantine; 38 | import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; 39 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 40 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 41 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 42 | import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode; 43 | import org.eclipse.milo.opcua.stack.core.util.CertificateUtil; 44 | import org.slf4j.Logger; 45 | import org.slf4j.LoggerFactory; 46 | 47 | /** Implementation behavior for an instance of the {@link ServerConfigurationType} Object. */ 48 | public class ServerConfigurationObject extends AbstractLifecycle { 49 | 50 | private final Logger logger = LoggerFactory.getLogger(getClass()); 51 | 52 | /** 53 | * Temporary storage of PrivateKeys generated during CreateSigningRequest, for subsequent use in 54 | * UpdateCertificate. 55 | */ 56 | private final Map regeneratedPrivateKeys = new ConcurrentHashMap<>(); 57 | 58 | private final OpcUaServer server; 59 | private final ServerConfigurationTypeNode serverConfigurationTypeNode; 60 | 61 | public ServerConfigurationObject( 62 | OpcUaServer server, ServerConfigurationTypeNode serverConfigurationTypeNode) { 63 | 64 | this.server = server; 65 | this.serverConfigurationTypeNode = serverConfigurationTypeNode; 66 | } 67 | 68 | @Override 69 | protected void onStartup() { 70 | { // UpdateCertificateMethod 71 | UaMethodNode methodNode = serverConfigurationTypeNode.getUpdateCertificateMethodNode(); 72 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 73 | methodNode.setInvocationHandler(new UpdateCertificateMethodImpl(methodNode)); 74 | } 75 | 76 | { // ApplyChangesMethod 77 | UaMethodNode methodNode = serverConfigurationTypeNode.getApplyChangesMethodNode(); 78 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 79 | methodNode.setInvocationHandler(new ApplyChangesMethodImpl(methodNode)); 80 | } 81 | 82 | { // CreateSigningRequestMethod 83 | UaMethodNode methodNode = serverConfigurationTypeNode.getCreateSigningRequestMethodNode(); 84 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 85 | methodNode.setInvocationHandler(new CreateSigningRequestMethodImpl(methodNode)); 86 | } 87 | 88 | { // GetRejectedListMethod 89 | UaMethodNode methodNode = serverConfigurationTypeNode.getGetRejectedListMethodNode(); 90 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 91 | methodNode.setInvocationHandler(new GetRejectedListMethodImpl(methodNode)); 92 | } 93 | 94 | serverConfigurationTypeNode.setServerCapabilities(new String[] {""}); 95 | serverConfigurationTypeNode.setSupportedPrivateKeyFormats(new String[] {"PEM", "PFX"}); 96 | serverConfigurationTypeNode.setMaxTrustListSize(uint(0)); 97 | serverConfigurationTypeNode.setMulticastDnsEnabled(false); 98 | serverConfigurationTypeNode.setHasSecureElement(false); 99 | 100 | List certificateGroups = 101 | server.getConfig().getCertificateManager().getCertificateGroups(); 102 | 103 | Set supportedGroups = 104 | certificateGroups.stream() 105 | .map(CertificateGroup::getCertificateGroupId) 106 | .collect(Collectors.toSet()); 107 | 108 | if (!supportedGroups.contains( 109 | NodeIds.ServerConfiguration_CertificateGroups_DefaultUserTokenGroup)) { 110 | 111 | server 112 | .getAddressSpaceManager() 113 | .getManagedNode(NodeIds.ServerConfiguration_CertificateGroups_DefaultUserTokenGroup) 114 | .ifPresent(UaNode::delete); 115 | } 116 | 117 | if (!supportedGroups.contains( 118 | NodeIds.ServerConfiguration_CertificateGroups_DefaultHttpsGroup)) { 119 | 120 | server 121 | .getAddressSpaceManager() 122 | .getManagedNode(NodeIds.ServerConfiguration_CertificateGroups_DefaultHttpsGroup) 123 | .ifPresent(UaNode::delete); 124 | } 125 | 126 | certificateGroups.forEach( 127 | group -> { 128 | CertificateGroupTypeNode groupNode = 129 | server 130 | .getAddressSpaceManager() 131 | .getManagedNode(group.getCertificateGroupId()) 132 | .filter(node -> node instanceof CertificateGroupTypeNode) 133 | .map(CertificateGroupTypeNode.class::cast) 134 | .orElse(null); 135 | 136 | if (groupNode != null) { 137 | var trustListObject = 138 | new TrustListObject( 139 | server.getConfig().getCertificateManager().getCertificateQuarantine(), 140 | group.getTrustListManager(), 141 | groupNode.getTrustListNode()); 142 | trustListObject.startup(); 143 | 144 | groupNode 145 | .getCertificateTypesNode() 146 | .getFilterChain() 147 | .addLast( 148 | AttributeFilters.getValue( 149 | ctx -> { 150 | NodeId[] certificateTypeIds = 151 | group.getSupportedCertificateTypeIds().toArray(NodeId[]::new); 152 | return new DataValue(new Variant(certificateTypeIds)); 153 | })); 154 | } 155 | }); 156 | 157 | logger.debug("ServerConfigurationObject started: {}", serverConfigurationTypeNode.getNodeId()); 158 | } 159 | 160 | @Override 161 | protected void onShutdown() { 162 | serverConfigurationTypeNode 163 | .getUpdateCertificateMethodNode() 164 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 165 | serverConfigurationTypeNode 166 | .getApplyChangesMethodNode() 167 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 168 | serverConfigurationTypeNode 169 | .getCreateSigningRequestMethodNode() 170 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 171 | serverConfigurationTypeNode 172 | .getGetRejectedListMethodNode() 173 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 174 | 175 | logger.debug("ServerConfigurationObject stopped: {}", serverConfigurationTypeNode.getNodeId()); 176 | } 177 | 178 | /** 179 | * @see 180 | * https://reference.opcfoundation.org/GDS/v105/docs/7.10.4 181 | */ 182 | public class UpdateCertificateMethodImpl 183 | extends ServerConfigurationTypeNode.UpdateCertificateMethod { 184 | 185 | public UpdateCertificateMethodImpl(UaMethodNode node) { 186 | super(node); 187 | } 188 | 189 | @Override 190 | protected void invoke( 191 | InvocationContext context, 192 | NodeId certificateGroupId, 193 | NodeId certificateTypeId, 194 | ByteString certificate, 195 | ByteString[] issuerCertificates, 196 | String privateKeyFormat, 197 | ByteString privateKey, 198 | Out applyChangesRequired) 199 | throws UaException { 200 | 201 | Session session = context.getSession().orElseThrow(); 202 | 203 | if (session.getSecurityConfiguration().getSecurityMode() 204 | != MessageSecurityMode.SignAndEncrypt) { 205 | throw new UaException(StatusCodes.Bad_SecurityModeInsufficient); 206 | } 207 | 208 | if (certificateGroupId == null || certificateGroupId.isNull()) { 209 | certificateGroupId = NodeIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup; 210 | } 211 | 212 | CertificateGroup certificateGroup = 213 | server 214 | .getConfig() 215 | .getCertificateManager() 216 | .getCertificateGroup(certificateGroupId) 217 | .orElseThrow( 218 | () -> new UaException(StatusCodes.Bad_InvalidArgument, "certificateGroupId")); 219 | 220 | var certificateChain = new ArrayList(); 221 | 222 | try { 223 | certificateChain.add(CertificateUtil.decodeCertificate(certificate.bytesOrEmpty())); 224 | } catch (Exception e) { 225 | throw new UaException(StatusCodes.Bad_InvalidArgument, "certificate", e); 226 | } 227 | 228 | try { 229 | if (issuerCertificates != null) { 230 | for (ByteString bs : issuerCertificates) { 231 | certificateChain.add(CertificateUtil.decodeCertificate(bs.bytesOrEmpty())); 232 | } 233 | } 234 | } catch (Exception e) { 235 | throw new UaException(StatusCodes.Bad_InvalidArgument, "issuerCertificates", e); 236 | } 237 | 238 | KeyPair newKeyPair; 239 | if (privateKey == null || privateKey.isNullOrEmpty()) { 240 | PrivateKey key; 241 | if ((key = regeneratedPrivateKeys.remove(certificateTypeId)) != null) { 242 | // Use previously generated PrivateKey + new certificate PublicKey 243 | newKeyPair = new KeyPair(certificateChain.get(0).getPublicKey(), key); 244 | } else { 245 | // Use current PrivateKey + new certificate PublicKey 246 | KeyPair keyPair = 247 | certificateGroup 248 | .getKeyPair(certificateTypeId) 249 | .orElseThrow( 250 | () -> new UaException(StatusCodes.Bad_InvalidArgument, "certificateTypeId")); 251 | 252 | newKeyPair = new KeyPair(certificateChain.get(0).getPublicKey(), keyPair.getPrivate()); 253 | } 254 | } else { 255 | // Use new PrivateKey + new certificate PublicKey 256 | try { 257 | PrivateKey newPrivateKey = 258 | switch (privateKeyFormat) { 259 | case "PEM" -> readPemEncodedPrivateKey(privateKey); 260 | case "PFX" -> readPfxEncodedPrivateKey(privateKey); 261 | default -> 262 | throw new UaException(StatusCodes.Bad_InvalidArgument, "privateKeyFormat"); 263 | }; 264 | 265 | newKeyPair = new KeyPair(certificateChain.get(0).getPublicKey(), newPrivateKey); 266 | } catch (Exception e) { 267 | throw new UaException(StatusCodes.Bad_InvalidArgument, "privateKey", e); 268 | } 269 | } 270 | 271 | try { 272 | certificateGroup.updateCertificate( 273 | certificateTypeId, newKeyPair, certificateChain.toArray(new X509Certificate[0])); 274 | } catch (Exception e) { 275 | throw new UaException(StatusCodes.Bad_InvalidArgument, "certificateTypeId", e); 276 | } 277 | 278 | // TODO force existing clients to reconnect? 279 | 280 | applyChangesRequired.set(false); 281 | } 282 | 283 | private static PrivateKey readPemEncodedPrivateKey(ByteString privateKey) throws Exception { 284 | var reader = 285 | new PemReader(new InputStreamReader(new ByteArrayInputStream(privateKey.bytesOrEmpty()))); 286 | 287 | byte[] encodedKey = reader.readPemObject().getContent(); 288 | var keySpec = new PKCS8EncodedKeySpec(encodedKey); 289 | var keyFactory = KeyFactory.getInstance("RSA"); 290 | 291 | return keyFactory.generatePrivate(keySpec); 292 | } 293 | 294 | private static PrivateKey readPfxEncodedPrivateKey(ByteString privateKey) throws Exception { 295 | var keyStore = KeyStore.getInstance("PKCS12"); 296 | keyStore.load(new ByteArrayInputStream(privateKey.bytesOrEmpty()), null); 297 | 298 | while (keyStore.aliases().hasMoreElements()) { 299 | String alias = keyStore.aliases().nextElement(); 300 | if (keyStore.isKeyEntry(alias)) { 301 | Key key = keyStore.getKey(alias, null); 302 | if (key instanceof PrivateKey) { 303 | return (PrivateKey) key; 304 | } 305 | } 306 | } 307 | 308 | throw new Exception("no PrivateKey found in PKCS12 keystore"); 309 | } 310 | } 311 | 312 | /** 313 | * @see 314 | * https://reference.opcfoundation.org/GDS/v105/docs/7.10.6 315 | */ 316 | public static class ApplyChangesMethodImpl 317 | extends ServerConfigurationTypeNode.ApplyChangesMethod { 318 | 319 | public ApplyChangesMethodImpl(UaMethodNode node) { 320 | super(node); 321 | } 322 | 323 | @Override 324 | protected void invoke(InvocationContext context) throws UaException { 325 | Session session = context.getSession().orElseThrow(); 326 | 327 | MessageSecurityMode securityMode = session.getSecurityConfiguration().getSecurityMode(); 328 | 329 | if (securityMode != MessageSecurityMode.Sign 330 | && securityMode != MessageSecurityMode.SignAndEncrypt) { 331 | throw new UaException(StatusCodes.Bad_SecurityModeInsufficient); 332 | } 333 | 334 | // nothing else to do here; changes are applied immediately. 335 | } 336 | } 337 | 338 | /** 339 | * @see h 340 | * ttps://reference.opcfoundation.org/GDS/v105/docs/7.10.7 341 | */ 342 | public class CreateSigningRequestMethodImpl 343 | extends ServerConfigurationTypeNode.CreateSigningRequestMethod { 344 | 345 | public CreateSigningRequestMethodImpl(UaMethodNode node) { 346 | super(node); 347 | } 348 | 349 | @Override 350 | protected void invoke( 351 | InvocationContext context, 352 | NodeId certificateGroupId, 353 | NodeId certificateTypeId, 354 | String subjectName, 355 | Boolean regeneratePrivateKey, 356 | ByteString nonce, 357 | Out certificateRequest) 358 | throws UaException { 359 | 360 | Session session = context.getSession().orElseThrow(); 361 | 362 | if (session.getSecurityConfiguration().getSecurityMode() 363 | != MessageSecurityMode.SignAndEncrypt) { 364 | throw new UaException(StatusCodes.Bad_SecurityModeInsufficient); 365 | } 366 | 367 | if (certificateGroupId == null || certificateGroupId.isNull()) { 368 | certificateGroupId = NodeIds.ServerConfiguration_CertificateGroups_DefaultApplicationGroup; 369 | } 370 | 371 | CertificateGroup certificateGroup = 372 | server 373 | .getConfig() 374 | .getCertificateManager() 375 | .getCertificateGroup(certificateGroupId) 376 | .orElseThrow( 377 | () -> new UaException(StatusCodes.Bad_InvalidArgument, "certificateGroupId")); 378 | 379 | try { 380 | KeyPair keyPair = 381 | certificateGroup 382 | .getKeyPair(certificateTypeId) 383 | .orElseThrow( 384 | () -> new UaException(StatusCodes.Bad_InvalidArgument, "certificateTypeId")); 385 | 386 | X509Certificate certificate = 387 | certificateGroup 388 | .getCertificateChain(certificateTypeId) 389 | .map(certificateChain -> certificateChain[0]) 390 | .orElseThrow( 391 | () -> new UaException(StatusCodes.Bad_InvalidArgument, "certificateTypeId")); 392 | 393 | if (regeneratePrivateKey) { 394 | try { 395 | keyPair = certificateGroup.getCertificateFactory().createKeyPair(certificateTypeId); 396 | 397 | regeneratedPrivateKeys.put(certificateTypeId, keyPair.getPrivate()); 398 | } catch (Exception e) { 399 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 400 | } 401 | } 402 | 403 | X500Name subject; 404 | if (subjectName == null || subjectName.isEmpty()) { 405 | subject = new JcaX509CertificateHolder(certificate).getSubject(); 406 | } else { 407 | subject = new X500Name(IETFUtils.rDNsFromString(subjectName, RFC4519Style.INSTANCE)); 408 | } 409 | 410 | ByteString csr = 411 | certificateGroup 412 | .getCertificateFactory() 413 | .createSigningRequest( 414 | certificateTypeId, 415 | keyPair, 416 | subject, 417 | CertificateUtil.getSanUri(certificate) 418 | .orElse(server.getConfig().getApplicationUri()), 419 | CertificateUtil.getSanDnsNames(certificate), 420 | CertificateUtil.getSanIpAddresses(certificate)); 421 | 422 | certificateRequest.set(csr); 423 | } catch (Exception e) { 424 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 425 | } 426 | } 427 | } 428 | 429 | /** 430 | * @see 431 | * https://reference.opcfoundation.org/GDS/v105/docs/7.10.9 432 | */ 433 | public class GetRejectedListMethodImpl extends ServerConfigurationTypeNode.GetRejectedListMethod { 434 | 435 | public GetRejectedListMethodImpl(UaMethodNode node) { 436 | super(node); 437 | } 438 | 439 | @Override 440 | protected void invoke(InvocationContext context, Out certificates) 441 | throws UaException { 442 | 443 | Session session = context.getSession().orElseThrow(); 444 | 445 | MessageSecurityMode securityMode = session.getSecurityConfiguration().getSecurityMode(); 446 | 447 | if (securityMode != MessageSecurityMode.Sign 448 | && securityMode != MessageSecurityMode.SignAndEncrypt) { 449 | throw new UaException(StatusCodes.Bad_SecurityModeInsufficient); 450 | } 451 | 452 | var certificateBytes = new ArrayList(); 453 | 454 | CertificateQuarantine certificateQuarantine = 455 | server.getConfig().getCertificateManager().getCertificateQuarantine(); 456 | 457 | for (X509Certificate certificate : certificateQuarantine.getRejectedCertificates()) { 458 | try { 459 | certificateBytes.add(ByteString.of(certificate.getEncoded())); 460 | } catch (CertificateEncodingException e) { 461 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 462 | } 463 | } 464 | 465 | certificates.set(certificateBytes.toArray(new ByteString[0])); 466 | } 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /src/main/java/com/digitalpetri/opcua/server/objects/TrustListObject.java: -------------------------------------------------------------------------------- 1 | package com.digitalpetri.opcua.server.objects; 2 | 3 | import static java.util.Objects.requireNonNullElse; 4 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; 5 | import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; 6 | 7 | import java.io.*; 8 | import java.security.cert.*; 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import org.bouncycastle.util.encoders.Hex; 12 | import org.eclipse.milo.opcua.sdk.server.Session; 13 | import org.eclipse.milo.opcua.sdk.server.methods.MethodInvocationHandler; 14 | import org.eclipse.milo.opcua.sdk.server.methods.Out; 15 | import org.eclipse.milo.opcua.sdk.server.model.objects.FileType; 16 | import org.eclipse.milo.opcua.sdk.server.model.objects.TrustListType; 17 | import org.eclipse.milo.opcua.sdk.server.model.objects.TrustListTypeNode; 18 | import org.eclipse.milo.opcua.sdk.server.nodes.UaMethodNode; 19 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilter; 20 | import org.eclipse.milo.opcua.sdk.server.nodes.filters.AttributeFilters; 21 | import org.eclipse.milo.opcua.stack.core.StatusCodes; 22 | import org.eclipse.milo.opcua.stack.core.UaException; 23 | import org.eclipse.milo.opcua.stack.core.encoding.DefaultEncodingContext; 24 | import org.eclipse.milo.opcua.stack.core.security.CertificateQuarantine; 25 | import org.eclipse.milo.opcua.stack.core.security.TrustListManager; 26 | import org.eclipse.milo.opcua.stack.core.types.UaStructuredType; 27 | import org.eclipse.milo.opcua.stack.core.types.builtin.ByteString; 28 | import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue; 29 | import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime; 30 | import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject; 31 | import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId; 32 | import org.eclipse.milo.opcua.stack.core.types.builtin.Variant; 33 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte; 34 | import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger; 35 | import org.eclipse.milo.opcua.stack.core.types.enumerated.TrustListMasks; 36 | import org.eclipse.milo.opcua.stack.core.types.structured.TrustListDataType; 37 | import org.eclipse.milo.opcua.stack.core.util.CertificateUtil; 38 | import org.slf4j.Logger; 39 | import org.slf4j.LoggerFactory; 40 | 41 | /** 42 | * Implementation behavior for an instance of the {@link TrustListType} Object. 43 | * 44 | * @see 45 | * https://reference.opcfoundation.org/GDS/v105/docs/7.8.2.1 46 | */ 47 | public class TrustListObject extends FileObject { 48 | 49 | private static final int MASK_TRUSTED_CERTIFICATES = 50 | TrustListMasks.TrustedCertificates.getValue(); 51 | private static final int MASK_TRUSTED_CRLS = TrustListMasks.TrustedCrls.getValue(); 52 | private static final int MASK_ISSUER_CERTIFICATES = TrustListMasks.IssuerCertificates.getValue(); 53 | private static final int MASK_ISSUER_CRLS = TrustListMasks.IssuerCrls.getValue(); 54 | private static final int MASK_ALL = TrustListMasks.All.getValue(); 55 | 56 | private final Logger logger = LoggerFactory.getLogger(getClass()); 57 | 58 | private final CertificateQuarantine certificateQuarantine; 59 | private final TrustListManager trustListManager; 60 | private final TrustListTypeNode trustListTypeNode; 61 | 62 | public TrustListObject( 63 | CertificateQuarantine certificateQuarantine, 64 | TrustListManager trustListManager, 65 | TrustListTypeNode fileNode) { 66 | 67 | super(fileNode, () -> newTemporaryTrustListFile(trustListManager, MASK_ALL)); 68 | 69 | this.certificateQuarantine = certificateQuarantine; 70 | this.trustListManager = trustListManager; 71 | this.trustListTypeNode = fileNode; 72 | } 73 | 74 | @Override 75 | protected void onStartup() { 76 | super.onStartup(); 77 | 78 | { // OpenMethod 79 | UaMethodNode methodNode = trustListTypeNode.getOpenMethodNode(); 80 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 81 | methodNode.setInvocationHandler(new OpenMethodImpl(methodNode)); 82 | } 83 | 84 | { // OpenWithMasksMethod 85 | UaMethodNode methodNode = trustListTypeNode.getOpenWithMasksMethodNode(); 86 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 87 | methodNode.setInvocationHandler(new OpenWithMasksMethodImpl(methodNode)); 88 | } 89 | 90 | { // CloseAndUpdateMethod 91 | UaMethodNode methodNode = trustListTypeNode.getCloseAndUpdateMethodNode(); 92 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 93 | methodNode.setInvocationHandler(new CloseAndUpdateMethodImpl(methodNode)); 94 | } 95 | 96 | { // AddCertificateMethod 97 | UaMethodNode methodNode = trustListTypeNode.getAddCertificateMethodNode(); 98 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 99 | methodNode.setInvocationHandler(new AddCertificateMethodImpl(methodNode)); 100 | } 101 | 102 | { // RemoveCertificateMethod 103 | UaMethodNode methodNode = trustListTypeNode.getRemoveCertificateMethodNode(); 104 | methodNode.getFilterChain().addLast(new SecurityAdminFilter()); 105 | methodNode.setInvocationHandler(new RemoveCertificateMethodImpl(methodNode)); 106 | } 107 | 108 | trustListTypeNode 109 | .getLastUpdateTimeNode() 110 | .getFilterChain() 111 | .addLast( 112 | AttributeFilters.getValue( 113 | ctx -> { 114 | DateTime lastUpdateTime = trustListManager.getLastUpdateTime(); 115 | 116 | return new DataValue(new Variant(lastUpdateTime)); 117 | })); 118 | 119 | logger.debug("TrustListObject started: {}", trustListTypeNode.getNodeId()); 120 | } 121 | 122 | @Override 123 | protected void onShutdown() { 124 | trustListTypeNode 125 | .getOpenMethodNode() 126 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 127 | trustListTypeNode 128 | .getOpenWithMasksMethodNode() 129 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 130 | trustListTypeNode 131 | .getCloseAndUpdateMethodNode() 132 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 133 | trustListTypeNode 134 | .getAddCertificateMethodNode() 135 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 136 | trustListTypeNode 137 | .getRemoveCertificateMethodNode() 138 | .setInvocationHandler(MethodInvocationHandler.NOT_IMPLEMENTED); 139 | 140 | logger.debug("TrustListObject stopped: {}", trustListTypeNode.getNodeId()); 141 | 142 | super.onShutdown(); 143 | } 144 | 145 | @Override 146 | protected FileType.OpenMethod newOpenMethod(UaMethodNode methodNode) { 147 | return new OpenMethodImpl(methodNode); 148 | } 149 | 150 | @Override 151 | protected AttributeFilter newSizeAttributeFilter() { 152 | // creating a temporary TrustList file just to calculate the size is expensive, so let's just 153 | // tell the client don't support it. 154 | return AttributeFilters.getValue(ctx -> new DataValue(StatusCodes.Bad_NotSupported)); 155 | } 156 | 157 | /** 158 | * Restricts the implementation of {@link FileObject.OpenMethodImpl} to only allow {@link 159 | * #MASK_READ} or {@link #MASK_WRITE} + {@link #MASK_ERASE_EXISTING}. 160 | */ 161 | class OpenMethodImpl extends FileObject.OpenMethodImpl { 162 | 163 | public OpenMethodImpl(UaMethodNode node) { 164 | super(node); 165 | } 166 | 167 | @Override 168 | protected void invoke(InvocationContext context, UByte mode, Out fileHandle) 169 | throws UaException { 170 | 171 | if (mode.intValue() != MASK_READ && mode.intValue() != (MASK_WRITE | MASK_ERASE_EXISTING)) { 172 | throw new UaException( 173 | StatusCodes.Bad_InvalidArgument, "mode must be Read or Write+EraseExisting"); 174 | } 175 | 176 | super.invoke(context, mode, fileHandle); 177 | } 178 | } 179 | 180 | /** 181 | * Allows a Client to read only a portion of the Trust List. 182 | * 183 | *

This Method can only be used to read. 184 | * 185 | * @see 186 | * https://reference.opcfoundation.org/GDS/v105/docs/7.8.2.2 187 | */ 188 | class OpenWithMasksMethodImpl extends TrustListType.OpenWithMasksMethod { 189 | 190 | public OpenWithMasksMethodImpl(UaMethodNode node) { 191 | super(node); 192 | } 193 | 194 | @Override 195 | protected void invoke(InvocationContext context, UInteger masks, Out fileHandle) 196 | throws UaException { 197 | 198 | Session session = context.getSession().orElseThrow(); 199 | 200 | // TODO For PullManagement, this Method shall be called from an authenticated SecureChannel 201 | // and from a Client that has access to the CertificateAuthorityAdmin Role, the 202 | // ApplicationSelfAdmin Privilege, or the ApplicationAdmin Privilege. 203 | 204 | // TODO For PushManagement, this Method shall be called from an authenticated SecureChannel 205 | // and from a Client that has access to the SecurityAdmin Role. 206 | 207 | try { 208 | File file = newTemporaryTrustListFile(trustListManager, masks.intValue()); 209 | file.deleteOnExit(); 210 | 211 | var handle = new FileHandle(ubyte(MASK_READ), new RandomAccessFile(file, "r")); 212 | handles.put(session.getSessionId(), handle.handle, handle); 213 | 214 | fileHandle.set(handle.handle); 215 | } catch (IOException e) { 216 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * @see 223 | * https://reference.opcfoundation.org/GDS/v105/docs/7.8.2.3 224 | */ 225 | class CloseAndUpdateMethodImpl extends TrustListType.CloseAndUpdateMethod { 226 | 227 | public CloseAndUpdateMethodImpl(UaMethodNode node) { 228 | super(node); 229 | } 230 | 231 | @Override 232 | protected void invoke( 233 | InvocationContext context, UInteger fileHandle, Out applyChangesRequired) 234 | throws UaException { 235 | 236 | Session session = context.getSession().orElseThrow(); 237 | 238 | FileHandle handle = handles.remove(session.getSessionId(), fileHandle); 239 | 240 | if (handle == null) { 241 | throw new UaException(StatusCodes.Bad_InvalidArgument); 242 | } 243 | 244 | try (RandomAccessFile file = handle.file) { 245 | if ((MASK_WRITE & handle.mode.intValue()) != MASK_WRITE) { 246 | throw new UaException(StatusCodes.Bad_InvalidState); 247 | } 248 | 249 | file.seek(0L); 250 | byte[] bs = new byte[(int) file.length()]; 251 | file.readFully(bs); 252 | 253 | NodeId encodingId = 254 | TrustListDataType.BINARY_ENCODING_ID 255 | .toNodeId(context.getServer().getNamespaceTable()) 256 | .orElseThrow(); 257 | 258 | ExtensionObject xo = ExtensionObject.of(ByteString.of(bs), encodingId); 259 | 260 | UaStructuredType decoded = xo.decode(DefaultEncodingContext.INSTANCE); 261 | 262 | if (decoded instanceof TrustListDataType trustList) { 263 | int specifiedLists = trustList.getSpecifiedLists().intValue(); 264 | 265 | if ((specifiedLists & MASK_TRUSTED_CERTIFICATES) != 0) { 266 | updateTrustedCertificates(trustList, trustListManager); 267 | } 268 | 269 | if ((specifiedLists & MASK_TRUSTED_CRLS) != 0) { 270 | updateTrustedCrls(trustList, trustListManager); 271 | } 272 | 273 | if ((specifiedLists & MASK_ISSUER_CERTIFICATES) != 0) { 274 | updateIssuerCertificates(trustList, trustListManager); 275 | } 276 | 277 | if ((specifiedLists & MASK_ISSUER_CRLS) != 0) { 278 | updateIssuerCrls(trustList, trustListManager); 279 | } 280 | 281 | trustListTypeNode.setLastUpdateTime(DateTime.now()); 282 | 283 | // TODO force existing clients to reconnect? 284 | 285 | applyChangesRequired.set(false); 286 | } else { 287 | throw new UaException(StatusCodes.Bad_InvalidArgument); 288 | } 289 | } catch (IOException e) { 290 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 291 | } 292 | } 293 | 294 | private static void updateTrustedCertificates( 295 | TrustListDataType trustList, TrustListManager trustListManager) throws UaException { 296 | 297 | var trustedCertificates = new ArrayList(); 298 | 299 | for (ByteString certificateBytes : 300 | requireNonNullElse(trustList.getTrustedCertificates(), new ByteString[0])) { 301 | 302 | try { 303 | X509Certificate certificate = 304 | CertificateUtil.decodeCertificate(certificateBytes.bytesOrEmpty()); 305 | trustedCertificates.add(certificate); 306 | } catch (UaException e) { 307 | throw new UaException(StatusCodes.Bad_InvalidArgument, e); 308 | } 309 | } 310 | 311 | trustListManager.setTrustedCertificates(trustedCertificates); 312 | } 313 | 314 | private static void updateTrustedCrls( 315 | TrustListDataType trustList, TrustListManager trustListManager) throws UaException { 316 | 317 | try { 318 | var factory = CertificateFactory.getInstance("X.509"); 319 | 320 | var trustedCrls = new ArrayList(); 321 | 322 | for (ByteString crlBytes : 323 | requireNonNullElse(trustList.getTrustedCrls(), new ByteString[0])) { 324 | 325 | try { 326 | Collection crls = 327 | factory.generateCRLs(new ByteArrayInputStream(crlBytes.bytesOrEmpty())); 328 | crls.forEach( 329 | crl -> { 330 | if (crl instanceof X509CRL x509CRL) { 331 | trustedCrls.add(x509CRL); 332 | } 333 | }); 334 | } catch (CRLException e) { 335 | throw new UaException(StatusCodes.Bad_InvalidArgument, e); 336 | } 337 | } 338 | 339 | trustListManager.setTrustedCrls(trustedCrls); 340 | } catch (CertificateException e) { 341 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 342 | } 343 | } 344 | 345 | private static void updateIssuerCertificates( 346 | TrustListDataType trustList, TrustListManager trustListManager) throws UaException { 347 | 348 | var issuerCertificates = new ArrayList(); 349 | 350 | for (ByteString certificateBytes : 351 | requireNonNullElse(trustList.getIssuerCertificates(), new ByteString[0])) { 352 | 353 | try { 354 | X509Certificate certificate = 355 | CertificateUtil.decodeCertificate(certificateBytes.bytesOrEmpty()); 356 | issuerCertificates.add(certificate); 357 | } catch (UaException e) { 358 | throw new UaException(StatusCodes.Bad_InvalidArgument, e); 359 | } 360 | } 361 | 362 | trustListManager.setIssuerCertificates(issuerCertificates); 363 | } 364 | 365 | private static void updateIssuerCrls( 366 | TrustListDataType trustList, TrustListManager trustListManager) throws UaException { 367 | 368 | try { 369 | var factory = CertificateFactory.getInstance("X.509"); 370 | 371 | var issuerCrls = new ArrayList(); 372 | 373 | for (ByteString crlBytes : 374 | requireNonNullElse(trustList.getIssuerCrls(), new ByteString[0])) { 375 | 376 | try { 377 | Collection crls = 378 | factory.generateCRLs(new ByteArrayInputStream(crlBytes.bytesOrEmpty())); 379 | crls.forEach( 380 | crl -> { 381 | if (crl instanceof X509CRL x509CRL) { 382 | issuerCrls.add(x509CRL); 383 | } 384 | }); 385 | } catch (CRLException e) { 386 | throw new UaException(StatusCodes.Bad_InvalidArgument, e); 387 | } 388 | } 389 | 390 | trustListManager.setIssuerCrls(issuerCrls); 391 | } catch (CertificateException e) { 392 | throw new UaException(StatusCodes.Bad_UnexpectedError, e); 393 | } 394 | } 395 | } 396 | 397 | /** 398 | * @see 399 | * https://reference.opcfoundation.org/GDS/v105/docs/7.8.2.4 400 | */ 401 | class AddCertificateMethodImpl extends TrustListType.AddCertificateMethod { 402 | 403 | public AddCertificateMethodImpl(UaMethodNode node) { 404 | super(node); 405 | } 406 | 407 | @Override 408 | protected void invoke( 409 | InvocationContext context, ByteString certificate, Boolean isTrustedCertificate) 410 | throws UaException { 411 | 412 | try { 413 | X509Certificate x509Certificate = 414 | CertificateUtil.decodeCertificate(certificate.bytesOrEmpty()); 415 | 416 | if (isTrustedCertificate) { 417 | trustListManager.addTrustedCertificate(x509Certificate); 418 | } else { 419 | trustListManager.addIssuerCertificate(x509Certificate); 420 | } 421 | 422 | certificateQuarantine.removeRejectedCertificate(x509Certificate); 423 | } catch (Exception e) { 424 | throw new UaException(StatusCodes.Bad_InvalidArgument, e); 425 | } 426 | } 427 | } 428 | 429 | /** 430 | * @see 431 | * https://reference.opcfoundation.org/GDS/v105/docs/7.8.2.5 432 | */ 433 | class RemoveCertificateMethodImpl extends TrustListType.RemoveCertificateMethod { 434 | 435 | public RemoveCertificateMethodImpl(UaMethodNode node) { 436 | super(node); 437 | } 438 | 439 | @Override 440 | protected void invoke( 441 | InvocationContext context, String thumbprint, Boolean isTrustedCertificate) 442 | throws UaException { 443 | 444 | ByteString thumbprintBytes = ByteString.of(Hex.decode(thumbprint)); 445 | 446 | if (isTrustedCertificate) { 447 | if (!trustListManager.removeTrustedCertificate(thumbprintBytes)) { 448 | throw new UaException(StatusCodes.Bad_InvalidArgument); 449 | } 450 | } else { 451 | if (!trustListManager.removeIssuerCertificate(thumbprintBytes)) { 452 | throw new UaException(StatusCodes.Bad_InvalidArgument); 453 | } 454 | } 455 | } 456 | } 457 | 458 | private static File newTemporaryTrustListFile(TrustListManager trustListManager, int masks) 459 | throws IOException { 460 | 461 | var trustedCertificates = new ArrayList(); 462 | if ((masks & MASK_TRUSTED_CERTIFICATES) != 0) { 463 | for (X509Certificate certificate : trustListManager.getTrustedCertificates()) { 464 | try { 465 | trustedCertificates.add(ByteString.of(certificate.getEncoded())); 466 | } catch (CertificateEncodingException e) { 467 | throw new IOException(e); 468 | } 469 | } 470 | } 471 | 472 | var trustedCrls = new ArrayList(); 473 | if ((masks & MASK_TRUSTED_CRLS) != 0) { 474 | for (X509CRL crl : trustListManager.getTrustedCrls()) { 475 | try { 476 | trustedCrls.add(ByteString.of(crl.getEncoded())); 477 | } catch (CRLException e) { 478 | throw new IOException(e); 479 | } 480 | } 481 | } 482 | 483 | var issuerCertificates = new ArrayList(); 484 | if ((masks & MASK_ISSUER_CERTIFICATES) != 0) { 485 | for (X509Certificate certificate : trustListManager.getIssuerCertificates()) { 486 | try { 487 | issuerCertificates.add(ByteString.of(certificate.getEncoded())); 488 | } catch (CertificateEncodingException e) { 489 | throw new IOException(e); 490 | } 491 | } 492 | } 493 | 494 | var issuerCrls = new ArrayList(); 495 | if ((masks & MASK_ISSUER_CRLS) != 0) { 496 | for (X509CRL crl : trustListManager.getIssuerCrls()) { 497 | try { 498 | issuerCrls.add(ByteString.of(crl.getEncoded())); 499 | } catch (CRLException e) { 500 | throw new IOException(e); 501 | } 502 | } 503 | } 504 | 505 | var trustList = 506 | new TrustListDataType( 507 | uint(masks), 508 | trustedCertificates.toArray(new ByteString[0]), 509 | trustedCrls.toArray(new ByteString[0]), 510 | issuerCertificates.toArray(new ByteString[0]), 511 | issuerCrls.toArray(new ByteString[0])); 512 | 513 | ExtensionObject encoded = ExtensionObject.encode(DefaultEncodingContext.INSTANCE, trustList); 514 | 515 | ByteString encodedBytes = (ByteString) encoded.getBody(); 516 | 517 | File file = File.createTempFile("TrustListDataType", ".bin"); 518 | 519 | try (FileOutputStream fos = new FileOutputStream(file)) { 520 | fos.write(encodedBytes.bytesOrEmpty()); 521 | } 522 | 523 | return file; 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /src/main/resources/default-logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/default-server.conf: -------------------------------------------------------------------------------- 1 | # This file is in "HOCON" format, which is a superset of JSON. 2 | 3 | # List of addresses to bind to. 4 | bind-address-list = ["0.0.0.0"] 5 | 6 | # Port to bind to. 7 | bind-port = 4840 8 | 9 | # List of addresses to create endpoints for. 10 | # Surrounding a hostname or IP address with < and > is special syntax that indicates the address 11 | # should be passed to `HostnameUtil.getHostnames`, which tries to find all hostnames associated 12 | # with an address. 13 | endpoint-address-list = ["<0.0.0.0>", "localhost"] 14 | 15 | # A list of hostnames and IP addresses to include in the server certificate when it is created. 16 | # Surrounding a hostname or IP address with < and > is special syntax that indicates the address 17 | # should be passed to `HostnameUtil.getHostnames`, which tries to find all hostnames associated 18 | # with an address. 19 | certificate-hostname-list = ["<0.0.0.0>"] 20 | 21 | # List of SecurityPolicy to support. 22 | security-policy-list = [ 23 | "None", 24 | "Aes128_Sha256_RsaOaep", 25 | "Basic256Sha256", 26 | "Aes256_Sha256_RsaPss" 27 | ] 28 | 29 | # List of MessageSecurityMode to support. 30 | security-mode-list = [ 31 | "None", 32 | "Sign", 33 | "SignAndEncrypt" 34 | ] 35 | 36 | # Allow certificates to be managed by a GDS. 37 | gds-push-enabled = true 38 | 39 | # Trust all incoming certificates automatically. 40 | # This is not recommended for production systems. 41 | trust-all-certificates = false 42 | 43 | # Enable the "Rate Limiting" feature. 44 | rate-limit-enabled = false 45 | 46 | # Enable/disable certain parts of the address space or control certain attributes of those 47 | # fragments if enabled. 48 | address-space { 49 | ctt.enabled = true 50 | data-type-test.enabled = true 51 | dynamic.enabled = true 52 | mass { 53 | enabled = true 54 | flat-quantity = 25000 55 | nested-quantity1 = 100 56 | nested-quantity2 = 1000 57 | } 58 | null.enabled = true 59 | turtles { 60 | enabled = true 61 | depth = 1000000 62 | } 63 | } 64 | 65 | # Role-based Access Control 66 | rbac { 67 | # These Role-Permission mappings are applied to Nodes in the "SiteA" folder. 68 | site-a = [ 69 | { 70 | role-id = "ns=1;s=SiteA_Read" 71 | permissions = ["Browse", "Read"] 72 | }, 73 | { 74 | role-id = "ns=1;s=SiteA_Write" 75 | permissions = ["Write", "Call"] 76 | }, 77 | { 78 | role-id = "ns=1;s=SiteAdmin" 79 | permissions = ["Browse", "Read", "ReadRolePermissions"] 80 | } 81 | ] 82 | 83 | # These Role-Permission mappings are applied to Nodes in the "SiteB" folder. 84 | site-b = [ 85 | { 86 | role-id = "ns=1;s=SiteB_Read" 87 | permissions = ["Browse", "Read"] 88 | }, 89 | { 90 | role-id = "ns=1;s=SiteB_Write" 91 | permissions = ["Write", "Call"] 92 | }, 93 | { 94 | role-id = "ns=1;s=SiteAdmin" 95 | permissions = ["Browse", "Read", "ReadRolePermissions"] 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /src/main/resources/turtle-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/digitalpetri/opc-ua-demo-server/60a07717eaa0c10db3191ea17848ea8a8a047ce2/src/main/resources/turtle-icon.png --------------------------------------------------------------------------------