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 extends CRL> 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 extends CRL> 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
--------------------------------------------------------------------------------