aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.clang-format297
-rw-r--r--.gitignore14
-rw-r--r--.gitmodules3
-rw-r--r--CLAUDE.md129
-rw-r--r--CMakeLists.txt27
-rw-r--r--cmake/BuildDeps.cmake50
m---------deps/open625410
-rw-r--r--src/client_find_servers.c281
-rw-r--r--src/common.c280
-rw-r--r--src/common.h115
-rw-r--r--src/server_lds.c81
-rw-r--r--src/server_register.c170
-rwxr-xr-xtools/generate_certificate.sh56
-rwxr-xr-xtools/launch.sh148
14 files changed, 1651 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..c2cc785
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,297 @@
+---
+Language: Cpp
+AccessModifierOffset: -2
+AlignAfterOpenBracket: Align
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: true
+AlignConsecutiveBitFields:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveDeclarations:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: true
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveMacros:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveShortCaseStatements:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCaseArrows: false
+ AlignCaseColons: false
+AlignConsecutiveTableGenBreakingDAGArgColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenCondOperatorColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignConsecutiveTableGenDefinitionColons:
+ Enabled: false
+ AcrossEmptyLines: false
+ AcrossComments: false
+ AlignCompound: false
+ AlignFunctionDeclarations: false
+ AlignFunctionPointers: false
+ PadOperators: false
+AlignEscapedNewlines: Right
+AlignOperands: Align
+AlignTrailingComments:
+ Kind: Always
+ OverEmptyLines: 0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseExpressionOnASingleLine: true
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortFunctionsOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: false
+AllowShortNamespacesOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: All
+AlwaysBreakBeforeMultilineStrings: false
+AttributeMacros:
+ - __capability
+BinPackArguments: true
+BinPackLongBracedList: true
+BinPackParameters: BinPack
+BitFieldColonSpacing: Both
+BracedInitializerIndentWidth: -1
+BraceWrapping:
+ AfterCaseLabel: true
+ AfterClass: true
+ AfterControlStatement: Always
+ AfterEnum: true
+ AfterExternBlock: true
+ AfterFunction: true
+ AfterNamespace: true
+ AfterObjCDeclaration: true
+ AfterStruct: true
+ AfterUnion: true
+ BeforeCatch: true
+ BeforeElse: true
+ BeforeLambdaBody: true
+ BeforeWhile: true
+ IndentBraces: true
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakAfterReturnType: AllDefinitions
+BreakArrays: true
+BreakBeforeBinaryOperators: All
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: GNU
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTemplateCloser: false
+BreakBeforeTernaryOperators: true
+BreakBinaryOperations: Never
+BreakConstructorInitializers: BeforeColon
+BreakFunctionDefinitionParameters: false
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: true
+BreakTemplateDeclarations: MultiLine
+ColumnLimit: 79
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: false
+DerivePointerAlignment: false
+DisableFormat: false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+EnumTrailingComma: Leave
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: false
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IfMacros:
+ - KJ_IF_MAYBE
+IncludeBlocks: Preserve
+IncludeCategories:
+ - Regex: '^"(llvm|llvm-c|clang|clang-c)/'
+ Priority: 2
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '^(<|"(gtest|gmock|isl|json)/)'
+ Priority: 3
+ SortPriority: 0
+ CaseSensitive: false
+ - Regex: '.*'
+ Priority: 1
+ SortPriority: 0
+ CaseSensitive: false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: false
+IndentCaseLabels: false
+IndentExportBlock: true
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentRequiresClause: true
+IndentWidth: 2
+IndentWrappedFunctionNames: false
+InsertBraces: false
+InsertNewlineAtEOF: false
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+ Binary: 0
+ BinaryMinDigits: 0
+ Decimal: 0
+ DecimalMinDigits: 0
+ Hex: 0
+ HexMinDigits: 0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLines:
+ AtEndOfFile: false
+ AtStartOfBlock: true
+ AtStartOfFile: true
+KeepFormFeed: true
+LambdaBodyIndentation: Signature
+LineEnding: DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MainIncludeChar: Quote
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+OneLineFormatOffRegex: ''
+PackConstructorInitializers: BinPack
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakBeforeMemberAccess: 150
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Right
+PPIndentWidth: -1
+QualifierAlignment: Leave
+ReferenceAlignment: Pointer
+ReflowComments: Always
+RemoveBracesLLVM: false
+RemoveEmptyLinesInUnwrappedLines: false
+RemoveParentheses: Leave
+RemoveSemicolon: false
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes:
+ Enabled: true
+ IgnoreCase: false
+SortJavaStaticImport: Before
+SortUsingDeclarations: LexicographicNumeric
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterOperatorKeyword: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: Always
+SpaceBeforeParensOptions:
+ AfterControlStatements: false
+ AfterForeachMacros: false
+ AfterFunctionDefinitionName: false
+ AfterFunctionDeclarationName: false
+ AfterIfMacros: false
+ AfterNot: false
+ AfterOverloadedOperator: false
+ AfterPlacementOperator: true
+ AfterRequiresInClause: false
+ AfterRequiresInExpression: false
+ BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: Never
+SpacesInContainerLiterals: true
+SpacesInLineCommentPrefix:
+ Minimum: 1
+ Maximum: -1
+SpacesInParens: Never
+SpacesInParensOptions:
+ ExceptDoubleParentheses: false
+ InCStyleCasts: false
+ InConditionalStatements: false
+ InEmptyParentheses: false
+ Other: false
+SpacesInSquareBrackets: false
+Standard: Latest
+StatementAttributeLikeMacros:
+ - Q_EMIT
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TableGenBreakInsideDAGArg: DontBreak
+TabWidth: 8
+UseTab: Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+ - BOOST_PP_STRINGIZE
+ - CF_SWIFT_NAME
+ - NS_SWIFT_NAME
+ - PP_STRINGIZE
+ - STRINGIZE
+WrapNamespaceBodyWithEmptyLines: Leave
+...
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..285b7d5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+# Build output
+build/
+
+# Symlink to build artifact
+compile_commands.json
+
+# Editor / IDE caches
+.cache/
+
+# Certificates (generated, contain private keys)
+certs/
+
+# Claude Code local state
+.claude/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..9760c40
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "deps/open62541"]
+ path = deps/open62541
+ url = https://github.com/open62541/open62541.git
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..b3ee66f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,129 @@
+# CLAUDE.md — OPC UA Discovery Project
+
+## Project Overview
+
+C project exploring OPC UA discovery using the open62541 library (git submodule at `deps/open62541`). CMake build system (C11). Source files live in `src/`, certificates in `certs/`, helper scripts in `tools/`.
+
+## Build
+
+```sh
+cmake --build build --parallel
+```
+
+If the build directory doesn't exist yet or dependencies need reconfiguring:
+
+```sh
+cmake -B build
+cmake --build build --parallel
+```
+
+open62541 is built automatically via `cmake/BuildDeps.cmake` with `UA_ENABLE_ENCRYPTION=OPENSSL` and `UA_ENABLE_DISCOVERY=ON`, installed to `build/deps/open62541-install`. The open62541 build directory is `build/deps/open62541`.
+
+After building, verify that `compile_commands.json` is symlinked from `build/` to the project root.
+
+### Building Documentation
+
+To build the open62541 HTML documentation (requires `python3-sphinx`, `python3-sphinx-rtd-theme`, `graphviz`):
+
+```sh
+cmake -B build -DBUILD_DOC=ON
+cmake --build build --target doc
+```
+
+Output goes to `build/deps/open62541/doc/index.html`.
+
+## Code Style
+
+- Follow the `.clang-format` file in the project root (GNU-based style).
+- Key points: 2-space indent, braces on own line (`BreakBeforeBraces: GNU`), space before parens (`SpaceBeforeParens: Always`), pointer star on the right (`PointerAlignment: Right`), 79-column limit, return type on its own line for definitions.
+- Do **not** reformat code you didn't change.
+
+## Workflow Preferences
+
+- **Ask before committing.** Never commit without explicit confirmation.
+- **Ask when ambiguous.** If a task or requirement is unclear, ask rather than guess.
+- **Plan mode:** Ask before entering plan mode — don't assume it's needed.
+- **Validation:** After making changes, confirm the project compiles with `cmake --build build --parallel`. When asked to run or test, start all three programs (LDS, ServerRegister, ClientFindServers) using the commands in the **Running** section below, then verify the client output.
+- **Verbosity:** Give detailed explanations of what was done and why.
+
+## Certificates
+
+Generate DER certificates with `tools/generate_certificate.sh <certs_dir> <name>`. This creates `<name>_cert.der` and `<name>_key.der` in the given directory. Four identities are needed:
+
+```sh
+tools/generate_certificate.sh certs ServerLDS
+tools/generate_certificate.sh certs ServerRegister
+tools/generate_certificate.sh certs ServerRegisterClient
+tools/generate_certificate.sh certs ClientFindServers
+```
+
+Existing certs live in `certs/`. Only regenerate if missing.
+
+## Running
+
+All three programs run from the project root. Start them in order in separate terminals:
+
+**1. Local Discovery Server (LDS)**
+
+```sh
+build/ServerLDS 4840 "urn:bobink.ServerLDS" \
+ certs/ServerLDS_cert.der certs/ServerLDS_key.der \
+ 60 \
+ certs/ServerRegisterClient_cert.der certs/ClientFindServers_cert.der
+```
+
+Args: `<port> <applicationUri> <cert> <key> <cleanup-timeout-seconds> [trustlist...]`
+
+The trustlist must include the client certs that will connect over encrypted channels: `ServerRegisterClient_cert.der` (used by `ServerRegister`) and `ClientFindServers_cert.der` (used by `ClientFindServers`). Cleanup timeout must be > 10.
+
+**2. Register Server**
+
+```sh
+build/ServerRegister 4841 "urn:bobink.ServerRegister" \
+ certs/ServerRegister_cert.der certs/ServerRegister_key.der \
+ certs/ServerRegisterClient_cert.der certs/ServerRegisterClient_key.der \
+ "opc.tcp://localhost:4840" 10 \
+ SignAndEncrypt Aes128_Sha256_RsaOaep \
+ certs/ServerLDS_cert.der certs/ClientFindServers_cert.der
+```
+
+Args: `<port> <applicationUri> <server-cert> <server-key> <client-cert> <client-key> <discovery-endpoint> <register-interval-seconds> <security-mode> <security-policy> [trustlist...]`
+
+Uses separate server/client certificate pairs. The client cert+key are for the secure channel to the LDS. Re-registers periodically at the given interval. Trustlist should include the LDS cert and any client certs that will query this server's endpoints (e.g. `ClientFindServers_cert.der`).
+
+**3. Find Servers Client**
+
+```sh
+build/ClientFindServers "opc.tcp://localhost:4840" \
+ "urn:bobink.ClientFindServers" \
+ certs/ClientFindServers_cert.der certs/ClientFindServers_key.der \
+ SignAndEncrypt Aes128_Sha256_RsaOaep \
+ certs/ServerLDS_cert.der certs/ServerRegister_cert.der
+```
+
+Args: `<discovery-server-endpoint> <applicationUri> <cert> <key> <security-mode> <security-policy> [trustlist...]`
+
+Queries the LDS and prints all registered servers and their endpoints. The trustlist should include the certs of all servers whose endpoints will be queried.
+
+**Security options** (for both ServerRegister and ClientFindServers):
+
+| Security modes | Security policies |
+|----------------|-------------------|
+| `None` | `None` |
+| `Sign` | `Basic256Sha256` |
+| `SignAndEncrypt` | `Aes256_Sha256_RsaPss` |
+| | `Aes128_Sha256_RsaOaep` |
+| | `ECC_nistP256` |
+
+## Project Structure
+
+| Path | Purpose |
+|------|---------|
+| `src/common.h` / `src/common.c` | Shared helpers: `loadFile()`, `createSecureServer()`, `createSecureClientConfig()`, `parseSecurityMode()`, `resolveSecurityPolicyUri()` |
+| `src/server_lds.c` | Local Discovery Server |
+| `src/server_register.c` | Server that registers with LDS |
+| `src/client_find_servers.c` | Client that queries LDS and displays endpoints |
+| `certs/` | TLS certificates for server, client, and LDS |
+| `tools/` | Helper scripts |
+| `cmake/BuildDeps.cmake` | Configures, builds, and installs open62541 |
+| `deps/open62541` | open62541 git submodule |
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..4075e49
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 4.0)
+project(OpcUaDiscovery C)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+include(cmake/BuildDeps.cmake)
+
+add_library(DiscoveryCommon STATIC src/common.c)
+target_link_libraries(DiscoveryCommon open62541::open62541)
+
+add_executable(ClientFindServers src/client_find_servers.c)
+target_link_libraries(ClientFindServers DiscoveryCommon)
+
+add_executable(ServerLDS src/server_lds.c)
+target_link_libraries(ServerLDS DiscoveryCommon)
+
+add_executable(ServerRegister src/server_register.c)
+target_link_libraries(ServerRegister DiscoveryCommon)
+
+option(BUILD_DOC "Build open62541 HTML documentation" OFF)
+if(BUILD_DOC)
+ add_custom_target(doc
+ COMMAND ${CMAKE_COMMAND} --build "${OPEN62541_BUILD_DIR}" --target doc
+ COMMENT "Building open62541 HTML documentation"
+ VERBATIM)
+endif()
diff --git a/cmake/BuildDeps.cmake b/cmake/BuildDeps.cmake
new file mode 100644
index 0000000..d535e0a
--- /dev/null
+++ b/cmake/BuildDeps.cmake
@@ -0,0 +1,50 @@
+set(OPEN62541_SOURCE_DIR "${CMAKE_SOURCE_DIR}/deps/open62541")
+set(OPEN62541_BUILD_DIR "${CMAKE_BINARY_DIR}/deps/open62541")
+set(OPEN62541_INSTALL_DIR "${CMAKE_BINARY_DIR}/deps/open62541-install")
+
+# --- open62541 ---
+
+if(NOT EXISTS "${OPEN62541_SOURCE_DIR}/CMakeLists.txt")
+ message(
+ FATAL_ERROR
+ "open62541 submodule not initialized. Run: git submodule update --init --recursive"
+ )
+endif()
+
+if(NOT EXISTS "${OPEN62541_INSTALL_DIR}/lib/libopen62541.so")
+
+ message(STATUS "Configuring open62541 in ${OPEN62541_BUILD_DIR}...")
+ set(_cmd
+ ${CMAKE_COMMAND}
+ -S
+ "${OPEN62541_SOURCE_DIR}"
+ -B
+ "${OPEN62541_BUILD_DIR}"
+ -G
+ Ninja
+ -DCMAKE_INSTALL_PREFIX=${OPEN62541_INSTALL_DIR}
+ -DUA_ENABLE_ENCRYPTION=OPENSSL
+ -DUA_ENABLE_DISCOVERY=ON)
+ execute_process(COMMAND ${_cmd} RESULT_VARIABLE _result)
+ if(_result)
+ message(FATAL_ERROR "${_cmd} failed: ${_result}")
+ endif()
+
+ message(STATUS "Building open62541 in ${OPEN62541_BUILD_DIR}...")
+ set(_cmd ${CMAKE_COMMAND} --build "${OPEN62541_BUILD_DIR}" --parallel)
+ execute_process(COMMAND ${_cmd} RESULT_VARIABLE _result)
+ if(_result)
+ message(FATAL_ERROR "${_cmd} failed: ${_result}")
+ endif()
+
+ message(STATUS "Installing open62541 to ${OPEN62541_INSTALL_DIR}...")
+ set(_cmd ${CMAKE_COMMAND} --install "${OPEN62541_BUILD_DIR}")
+ execute_process(COMMAND ${_cmd} RESULT_VARIABLE _result)
+ if(_result)
+ message(FATAL_ERROR "${_cmd} failed: ${_result}")
+ endif()
+else()
+ message(STATUS "open62541 already built, skipping")
+endif()
+
+find_package(open62541 REQUIRED PATHS "${OPEN62541_INSTALL_DIR}" NO_DEFAULT_PATH)
diff --git a/deps/open62541 b/deps/open62541
new file mode 160000
+Subproject 4f887b8fdde6b49a690349a0b72fe03537f36bf
diff --git a/src/client_find_servers.c b/src/client_find_servers.c
new file mode 100644
index 0000000..c62fc15
--- /dev/null
+++ b/src/client_find_servers.c
@@ -0,0 +1,281 @@
+/**
+ * @file client_find_servers.c
+ * @brief OPC UA client that queries a Local Discovery Server for registered
+ * servers.
+ *
+ * This program connects to an LDS and calls the FindServers service to
+ * retrieve all registered servers. It then queries each server's endpoints
+ * using the GetEndpoints service and displays the results in a human-readable
+ * format.
+ */
+
+#include "common.h"
+
+#include <open62541/client_highlevel.h>
+#include <open62541/plugin/log_stdout.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+/* ========================================================================
+ * Discovery Service Calls
+ * ======================================================================== */
+
+/**
+ * Calls the FindServers service on the LDS and prints all discovered servers.
+ *
+ * @param client The OPC UA client instance.
+ * @param discoveryServerEndpoint The LDS endpoint URL.
+ * @param applicationDescriptionArraySize Output: number of servers found.
+ * @param applicationDescriptionArray Output: array of server descriptions.
+ * @return UA_STATUSCODE_GOOD on success, error code otherwise.
+ */
+static UA_StatusCode
+findServers (UA_Client *client, const char *discoveryServerEndpoint,
+ size_t *applicationDescriptionArraySize,
+ UA_ApplicationDescription **applicationDescriptionArray)
+{
+ UA_StatusCode retval = UA_Client_findServers (
+ client, discoveryServerEndpoint, 0, NULL, 0, NULL,
+ applicationDescriptionArraySize, applicationDescriptionArray);
+
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "Could not call FindServers service. "
+ "Is the discovery server started? StatusCode %s",
+ UA_StatusCode_name (retval));
+ return retval;
+ }
+
+ for (size_t i = 0; i < *applicationDescriptionArraySize; i++)
+ printApplicationDescription (&(*applicationDescriptionArray)[i], i);
+
+ return UA_STATUSCODE_GOOD;
+}
+
+/**
+ * Queries endpoints for each discovered server using the GetEndpoints service.
+ *
+ * For each server in the applicationDescriptionArray, this function extracts
+ * the first discovery URL and calls GetEndpoints to retrieve all available
+ * endpoints. Results are logged via UA_LOG_INFO.
+ *
+ * @param client The OPC UA client instance.
+ * @param applicationDescriptionArray Array of server descriptions from
+ * FindServers.
+ * @param applicationDescriptionArraySize Number of servers in the array.
+ */
+static void
+getServersEndpoints (UA_Client *client,
+ UA_ApplicationDescription *applicationDescriptionArray,
+ size_t applicationDescriptionArraySize)
+{
+ for (size_t i = 0; i < applicationDescriptionArraySize; i++)
+ {
+ UA_ApplicationDescription *description = &applicationDescriptionArray[i];
+ if (description->discoveryUrlsSize == 0)
+ {
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "[GetEndpoints] Server %.*s did not provide any "
+ "discovery urls. Skipping.",
+ (int)description->applicationUri.length,
+ description->applicationUri.data);
+ continue;
+ }
+
+ /* UA_String is not null-terminated; build a C string for the API. */
+ char *discoveryUrl = (char *)UA_malloc (
+ sizeof (char) * description->discoveryUrls[0].length + 1);
+ memcpy (discoveryUrl, description->discoveryUrls[0].data,
+ description->discoveryUrls[0].length);
+ discoveryUrl[description->discoveryUrls[0].length] = '\0';
+
+ UA_EndpointDescription *endpointArray = NULL;
+ size_t endpointArraySize = 0;
+ UA_StatusCode retval = UA_Client_getEndpoints (
+ client, discoveryUrl, &endpointArraySize, &endpointArray);
+ UA_free (discoveryUrl);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Client_disconnect (client);
+ break;
+ }
+
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ "Endpoints for Server[%lu]: %.*s", (unsigned long)i,
+ (int)description->applicationUri.length,
+ description->applicationUri.data);
+ for (size_t j = 0; j < endpointArraySize; j++)
+ printEndpoint (&endpointArray[j], j);
+
+ UA_Array_delete (endpointArray, endpointArraySize,
+ &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
+ }
+}
+
+/* ========================================================================
+ * Node Reading
+ * ======================================================================== */
+
+/**
+ * Connects to each non-discovery server and reads the current time node.
+ *
+ * For each server that is not a DiscoveryServer, this function establishes a
+ * secure session, reads the Server_ServerStatus_CurrentTime variable, prints
+ * the result, and disconnects.
+ *
+ * @param client The OPC UA client instance.
+ * @param applicationDescriptionArray Array of server descriptions from
+ * FindServers.
+ * @param applicationDescriptionArraySize Number of servers in the array.
+ */
+static void
+readServerTime (UA_Client *client,
+ UA_ApplicationDescription *applicationDescriptionArray,
+ size_t applicationDescriptionArraySize)
+{
+ for (size_t i = 0; i < applicationDescriptionArraySize; i++)
+ {
+ UA_ApplicationDescription *desc = &applicationDescriptionArray[i];
+
+ if (desc->applicationType == UA_APPLICATIONTYPE_DISCOVERYSERVER)
+ continue;
+
+ if (desc->discoveryUrlsSize == 0)
+ {
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "[ReadTime] Server %.*s has no discovery URLs. "
+ "Skipping.",
+ (int)desc->applicationUri.length,
+ desc->applicationUri.data);
+ continue;
+ }
+
+ /* UA_String is not null-terminated; build a C string for the API. */
+ char *url = (char *)UA_malloc (desc->discoveryUrls[0].length + 1);
+ memcpy (url, desc->discoveryUrls[0].data, desc->discoveryUrls[0].length);
+ url[desc->discoveryUrls[0].length] = '\0';
+
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Connecting to %s to read current time...", url);
+
+ UA_StatusCode retval = UA_Client_connect (client, url);
+ UA_free (url);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Could not connect: %s", UA_StatusCode_name (retval));
+ continue;
+ }
+
+ UA_Variant value;
+ UA_Variant_init (&value);
+
+ const UA_NodeId nodeId = UA_NS0ID (SERVER_SERVERSTATUS_CURRENTTIME);
+ retval = UA_Client_readValueAttribute (client, nodeId, &value);
+
+ if (retval == UA_STATUSCODE_GOOD
+ && UA_Variant_hasScalarType (&value, &UA_TYPES[UA_TYPES_DATETIME]))
+ {
+ UA_DateTime raw_date = *(UA_DateTime *)value.data;
+ UA_DateTimeStruct dts = UA_DateTime_toStruct (raw_date);
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ "date is: %u-%u-%u %u:%u:%u.%03u", dts.day, dts.month,
+ dts.year, dts.hour, dts.min, dts.sec, dts.milliSec);
+ }
+ else
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Could not read current time: %s",
+ UA_StatusCode_name (retval));
+ }
+
+ UA_Variant_clear (&value);
+ UA_Client_disconnect (client);
+ }
+}
+
+/* ========================================================================
+ * Main
+ * ======================================================================== */
+
+int
+main (int argc, char **argv)
+{
+ if (argc < 7)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Usage: %s <discovery-server-endpoint>\n"
+ " <applicationUri>\n"
+ " <certificate.der> <private-key.der>\n"
+ " <security-mode> <security-policy>\n"
+ " [<trustlist1.der>, ...]\n"
+ "\n"
+ "Security modes : None, Sign, SignAndEncrypt\n"
+ "Security policies: None, Basic256Sha256, "
+ "Aes256_Sha256_RsaPss,\n"
+ " Aes128_Sha256_RsaOaep, ECC_nistP256",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ const char *discoveryServerEndpoint = argv[1];
+ const char *applicationUri = argv[2];
+ const char *certPath = argv[3];
+ const char *keyPath = argv[4];
+
+ UA_MessageSecurityMode securityMode = parseSecurityMode (argv[5]);
+ if (securityMode == UA_MESSAGESECURITYMODE_INVALID)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security mode: %s", argv[5]);
+ return EXIT_FAILURE;
+ }
+
+ const char *securityPolicyUri = resolveSecurityPolicyUri (argv[6]);
+ if (!securityPolicyUri)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security policy: %s", argv[6]);
+ return EXIT_FAILURE;
+ }
+
+ char **trustPaths = argv + 7;
+ size_t trustSize = (argc > 7) ? (size_t)argc - 7 : 0;
+
+ UA_Client *client = UA_Client_new ();
+ UA_StatusCode retval = createSecureClientConfig (
+ UA_Client_getConfig (client), applicationUri, certPath, keyPath,
+ trustPaths, trustSize, securityMode, securityPolicyUri);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Client_delete (client);
+ return EXIT_FAILURE;
+ }
+
+ UA_ApplicationDescription *applicationDescriptionArray = NULL;
+ size_t applicationDescriptionArraySize = 0;
+
+ retval = findServers (client, discoveryServerEndpoint,
+ &applicationDescriptionArraySize,
+ &applicationDescriptionArray);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Client_delete (client);
+ return EXIT_FAILURE;
+ }
+
+ getServersEndpoints (client, applicationDescriptionArray,
+ applicationDescriptionArraySize);
+
+ readServerTime (client, applicationDescriptionArray,
+ applicationDescriptionArraySize);
+
+ UA_Client_delete (client);
+ UA_Array_delete (applicationDescriptionArray,
+ applicationDescriptionArraySize,
+ &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/common.c b/src/common.c
new file mode 100644
index 0000000..d102868
--- /dev/null
+++ b/src/common.c
@@ -0,0 +1,280 @@
+/**
+ * @file common.c
+ * @brief Implements shared helpers declared in common.h.
+ */
+
+#include "common.h"
+
+#include <open62541/client_config_default.h>
+#include <open62541/plugin/log_stdout.h>
+#include <open62541/server_config_default.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* ========================================================================
+ * File Loading
+ * ======================================================================== */
+
+UA_ByteString
+loadFile (const char *const path)
+{
+ UA_ByteString fileContents = UA_STRING_NULL;
+
+ FILE *fp = fopen (path, "rb");
+ if (!fp)
+ {
+ /* fopen sets errno on failure. Callers like createSecureServer use
+ loadFile for optional trustlist entries where a missing file is not
+ an error. Clear errno so open62541's logging does not pick up
+ a stale value and emit misleading error messages. */
+ errno = 0;
+ return fileContents;
+ }
+
+ fseek (fp, 0, SEEK_END);
+ fileContents.length = (size_t)ftell (fp);
+ fileContents.data
+ = (UA_Byte *)UA_malloc (fileContents.length * sizeof (UA_Byte));
+ if (fileContents.data)
+ {
+ fseek (fp, 0, SEEK_SET);
+ size_t read = fread (fileContents.data, sizeof (UA_Byte),
+ fileContents.length, fp);
+ if (read != fileContents.length)
+ UA_ByteString_clear (&fileContents);
+ }
+ else
+ {
+ fileContents.length = 0;
+ }
+ fclose (fp);
+
+ return fileContents;
+}
+
+/* ========================================================================
+ * Security Helpers
+ * ======================================================================== */
+
+UA_MessageSecurityMode
+parseSecurityMode (const char *name)
+{
+ if (strcmp (name, "None") == 0)
+ return UA_MESSAGESECURITYMODE_NONE;
+ if (strcmp (name, "Sign") == 0)
+ return UA_MESSAGESECURITYMODE_SIGN;
+ if (strcmp (name, "SignAndEncrypt") == 0)
+ return UA_MESSAGESECURITYMODE_SIGNANDENCRYPT;
+ return UA_MESSAGESECURITYMODE_INVALID;
+}
+
+const char *
+resolveSecurityPolicyUri (const char *shortName)
+{
+ static const struct
+ {
+ const char *name;
+ const char *uri;
+ } policies[] = {
+ { "None", "http://opcfoundation.org/UA/SecurityPolicy#None" },
+ { "Basic256Sha256",
+ "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256" },
+ { "Aes256_Sha256_RsaPss",
+ "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss" },
+ { "Aes128_Sha256_RsaOaep",
+ "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep" },
+ { "ECC_nistP256",
+ "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" },
+ };
+ for (size_t i = 0; i < sizeof (policies) / sizeof (policies[0]); i++)
+ {
+ if (strcmp (shortName, policies[i].name) == 0)
+ return policies[i].uri;
+ }
+ return NULL;
+}
+
+/* ========================================================================
+ * Output Formatting
+ * ======================================================================== */
+
+void
+printApplicationDescription (const UA_ApplicationDescription *description,
+ size_t index)
+{
+ const char *type = "Unknown";
+ switch (description->applicationType)
+ {
+ case UA_APPLICATIONTYPE_SERVER:
+ type = "Server";
+ break;
+ case UA_APPLICATIONTYPE_CLIENT:
+ type = "Client";
+ break;
+ case UA_APPLICATIONTYPE_CLIENTANDSERVER:
+ type = "Client and Server";
+ break;
+ case UA_APPLICATIONTYPE_DISCOVERYSERVER:
+ type = "Discovery Server";
+ break;
+ default:
+ break;
+ }
+
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, "Server[%lu]: %.*s",
+ (unsigned long)index, (int)description->applicationUri.length,
+ description->applicationUri.data);
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, " Name: %.*s",
+ (int)description->applicationName.text.length,
+ description->applicationName.text.data);
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ " Application URI: %.*s",
+ (int)description->applicationUri.length,
+ description->applicationUri.data);
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ " Product URI: %.*s", (int)description->productUri.length,
+ description->productUri.data);
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, " Type: %s", type);
+ for (size_t j = 0; j < description->discoveryUrlsSize; j++)
+ {
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ " Discovery URL[%lu]: %.*s", (unsigned long)j,
+ (int)description->discoveryUrls[j].length,
+ description->discoveryUrls[j].data);
+ }
+}
+
+void
+printEndpoint (const UA_EndpointDescription *endpoint, size_t index)
+{
+ const char *mode = "Unknown";
+ switch (endpoint->securityMode)
+ {
+ case UA_MESSAGESECURITYMODE_NONE:
+ mode = "None";
+ break;
+ case UA_MESSAGESECURITYMODE_SIGN:
+ mode = "Sign";
+ break;
+ case UA_MESSAGESECURITYMODE_SIGNANDENCRYPT:
+ mode = "SignAndEncrypt";
+ break;
+ default:
+ break;
+ }
+
+ /* Extract policy name after the '#' */
+ const char *policy = (const char *)endpoint->securityPolicyUri.data;
+ size_t policyLen = endpoint->securityPolicyUri.length;
+ for (size_t k = 0; k < endpoint->securityPolicyUri.length; k++)
+ {
+ if (endpoint->securityPolicyUri.data[k] == '#')
+ {
+ policy = (const char *)&endpoint->securityPolicyUri.data[k + 1];
+ policyLen = endpoint->securityPolicyUri.length - k - 1;
+ break;
+ }
+ }
+
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ " [%4lu] %.*s | Level: %2d | %-14s | %.*s",
+ (unsigned long)index, (int)endpoint->endpointUrl.length,
+ endpoint->endpointUrl.data, endpoint->securityLevel, mode,
+ (int)policyLen, policy);
+}
+
+/* ========================================================================
+ * Factory Functions
+ * ======================================================================== */
+
+UA_Server *
+createSecureServer (UA_UInt16 port, const char *applicationUri,
+ const char *certPath, const char *keyPath,
+ char **trustPaths, size_t trustSize, UA_StatusCode *retval)
+{
+ UA_ByteString certificate = loadFile (certPath);
+ UA_ByteString privateKey = loadFile (keyPath);
+
+ /* +1: UA_STACKARRAY requires a strictly positive size for VLA. */
+ UA_STACKARRAY (UA_ByteString, trustList, trustSize + 1);
+ for (size_t i = 0; i < trustSize; i++)
+ trustList[i] = loadFile (trustPaths[i]);
+
+ /* Issuer and revocation lists are unused in this demo. */
+ size_t issuerListSize = 0;
+ UA_ByteString *issuerList = NULL;
+ UA_ByteString *revocationList = NULL;
+ size_t revocationListSize = 0;
+
+ UA_Server *server = UA_Server_new ();
+ UA_ServerConfig *config = UA_Server_getConfig (server);
+
+ *retval = UA_ServerConfig_setDefaultWithSecurityPolicies (
+ config, port, &certificate, &privateKey, trustList, trustSize,
+ issuerList, issuerListSize, revocationList, revocationListSize);
+
+ UA_ByteString_clear (&certificate);
+ UA_ByteString_clear (&privateKey);
+ for (size_t i = 0; i < trustSize; i++)
+ UA_ByteString_clear (&trustList[i]);
+
+ if (*retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Server_delete (server);
+ return NULL;
+ }
+
+ UA_String_clear (&config->applicationDescription.applicationUri);
+ config->applicationDescription.applicationUri
+ = UA_String_fromChars (applicationUri);
+
+ return server;
+}
+
+UA_StatusCode
+createSecureClientConfig (UA_ClientConfig *cc, const char *applicationUri,
+ const char *certPath, const char *keyPath,
+ char **trustPaths, size_t trustSize,
+ UA_MessageSecurityMode securityMode,
+ const char *securityPolicyUri)
+{
+ UA_ByteString certificate = loadFile (certPath);
+ UA_ByteString privateKey = loadFile (keyPath);
+
+ /* +1: UA_STACKARRAY requires a strictly positive size for VLA. */
+ UA_STACKARRAY (UA_ByteString, trustList, trustSize + 1);
+ for (size_t i = 0; i < trustSize; i++)
+ trustList[i] = loadFile (trustPaths[i]);
+
+ /* Revocation list is unused in this demo. */
+ UA_ByteString *revocationList = NULL;
+ size_t revocationListSize = 0;
+
+ UA_StatusCode retval = UA_ClientConfig_setDefaultEncryption (
+ cc, certificate, privateKey, trustList, trustSize, revocationList,
+ revocationListSize);
+
+ UA_ByteString_clear (&certificate);
+ UA_ByteString_clear (&privateKey);
+ for (size_t i = 0; i < trustSize; i++)
+ UA_ByteString_clear (&trustList[i]);
+
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION,
+ "Failed to set client encryption. StatusCode %s",
+ UA_StatusCode_name (retval));
+ return retval;
+ }
+
+ UA_String_clear (&cc->clientDescription.applicationUri);
+ cc->clientDescription.applicationUri = UA_String_fromChars (applicationUri);
+
+ cc->securityMode = securityMode;
+ cc->securityPolicyUri = UA_String_fromChars (securityPolicyUri);
+
+ return retval;
+}
diff --git a/src/common.h b/src/common.h
new file mode 100644
index 0000000..e3d2f4c
--- /dev/null
+++ b/src/common.h
@@ -0,0 +1,115 @@
+#ifndef DISCOVERY_COMMON_H
+#define DISCOVERY_COMMON_H
+
+/**
+ * @file common.h
+ * @brief Shared helpers for the OPC UA discovery demo programs.
+ *
+ * Provides file-loading, factory, and output formatting functions used by
+ * the LDS, the registering server, and the FindServers client.
+ */
+
+#include <open62541/client.h>
+#include <open62541/server.h>
+#include <open62541/types.h>
+
+/**
+ * @brief Loads a DER-encoded certificate or key file into a UA_ByteString.
+ *
+ * @param path File path to read.
+ * @return The file contents, or UA_BYTESTRING_NULL on error.
+ */
+UA_ByteString loadFile (const char *const path);
+
+/**
+ * @brief Creates a UA_Server configured with security policies and encryption.
+ *
+ * The server is initialized with the specified port, certificate, private key,
+ * and trustlist. The applicationUri is set in the server's application
+ * description.
+ *
+ * @param port Server port number.
+ * @param applicationUri OPC UA application URI.
+ * @param certPath Path to server certificate (.der).
+ * @param keyPath Path to private key (.der).
+ * @param trustPaths Array of trustlist file paths (may be NULL if trustSize is
+ * 0).
+ * @param trustSize Number of entries in trustPaths.
+ * @param retval Output parameter set to the status code on failure.
+ * @return A configured UA_Server, or NULL on error.
+ */
+UA_Server *createSecureServer (UA_UInt16 port, const char *applicationUri,
+ const char *certPath, const char *keyPath,
+ char **trustPaths, size_t trustSize,
+ UA_StatusCode *retval);
+
+/**
+ * @brief Parses a security mode name into the corresponding enum value.
+ *
+ * Accepted names: "None", "Sign", "SignAndEncrypt".
+ *
+ * @param name Mode name string.
+ * @return The matching UA_MessageSecurityMode, or
+ * UA_MESSAGESECURITYMODE_INVALID if the name is not recognized.
+ */
+UA_MessageSecurityMode parseSecurityMode (const char *name);
+
+/**
+ * @brief Maps a short security policy name to its full OPC UA URI.
+ *
+ * Accepted names: "None", "Basic256Sha256", "Aes256_Sha256_RsaPss",
+ * "Aes128_Sha256_RsaOaep", "ECC_nistP256".
+ *
+ * @param shortName Short policy name.
+ * @return The full URI string, or NULL if the name is not recognized.
+ */
+const char *resolveSecurityPolicyUri (const char *shortName);
+
+/**
+ * @brief Initializes a UA_ClientConfig with encryption from file paths.
+ *
+ * The config must be zero-initialized by the caller before calling this
+ * function. Loads the certificate, private key, and trustlist, then applies
+ * default encryption settings.
+ *
+ * @param cc Pointer to a zero-initialized UA_ClientConfig.
+ * @param applicationUri OPC UA application URI.
+ * @param certPath Path to client certificate (.der).
+ * @param keyPath Path to private key (.der).
+ * @param trustPaths Array of trustlist file paths (may be NULL if trustSize is
+ * 0).
+ * @param trustSize Number of entries in trustPaths.
+ * @param securityMode Requested message security mode.
+ * @param securityPolicyUri Security policy URI string (e.g.
+ * "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256").
+ * @return UA_STATUSCODE_GOOD on success, error code otherwise.
+ */
+UA_StatusCode createSecureClientConfig (
+ UA_ClientConfig *cc, const char *applicationUri, const char *certPath,
+ const char *keyPath, char **trustPaths, size_t trustSize,
+ UA_MessageSecurityMode securityMode, const char *securityPolicyUri);
+
+/**
+ * @brief Logs a UA_ApplicationDescription (server info from FindServers).
+ *
+ * Outputs the application URI, name, product URI, type, and discovery URLs
+ * via UA_LOG_INFO.
+ *
+ * @param description The application description to print.
+ * @param index Display index (e.g. position in the FindServers result array).
+ */
+void printApplicationDescription (const UA_ApplicationDescription *description,
+ size_t index);
+
+/**
+ * @brief Logs a UA_EndpointDescription in a compact one-line format.
+ *
+ * Outputs the endpoint URL, security level, security mode, and the short
+ * policy name (the part after '#') via UA_LOG_INFO.
+ *
+ * @param endpoint The endpoint description to print.
+ * @param index Display index (e.g. position in the GetEndpoints result array).
+ */
+void printEndpoint (const UA_EndpointDescription *endpoint, size_t index);
+
+#endif /* DISCOVERY_COMMON_H */
diff --git a/src/server_lds.c b/src/server_lds.c
new file mode 100644
index 0000000..a7794aa
--- /dev/null
+++ b/src/server_lds.c
@@ -0,0 +1,81 @@
+/**
+ * @file server_lds.c
+ * @brief Local Discovery Server implementation.
+ *
+ * This program runs an OPC UA Local Discovery Server (LDS) configured with
+ * encryption and a configurable cleanup timeout. Other OPC UA servers register
+ * with this LDS using the RegisterServer2 service. Clients can query this LDS
+ * using the FindServers service to discover registered servers.
+ */
+
+#include "common.h"
+
+#include <open62541/plugin/log_stdout.h>
+#include <open62541/server.h>
+#include <open62541/server_config_default.h>
+
+#include <signal.h>
+#include <stdlib.h>
+
+UA_Boolean running = true;
+
+static void
+stopHandler (int sig)
+{
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
+ running = false;
+}
+
+int
+main (int argc, char *argv[])
+{
+ signal (SIGINT, stopHandler);
+ signal (SIGTERM, stopHandler);
+
+ if (argc < 6)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Usage: %s\n"
+ " <port> <applicationUri>\n"
+ " <server-certificate.der> <private-key.der>\n"
+ " <cleanup-timeout-seconds>\n"
+ " [<trustlist1.der>, ...]",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ UA_UInt16 port = (UA_UInt16)atoi (argv[1]);
+ int cleanupTimeout = atoi (argv[5]);
+
+ /* The OPC UA specification requires the cleanup timeout to exceed the
+ register-server interval. open62541 enforces a floor of 10 seconds. */
+ if (cleanupTimeout <= 10)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Cleanup timeout must be > 10 seconds (got %d)",
+ cleanupTimeout);
+ return EXIT_FAILURE;
+ }
+ size_t trustSize = (argc > 6) ? (size_t)argc - 6 : 0;
+
+ UA_StatusCode retval;
+ UA_Server *server = createSecureServer (port, argv[2], argv[3], argv[4],
+ argv + 6, trustSize, &retval);
+ if (!server)
+ return EXIT_FAILURE;
+
+ UA_ServerConfig *serverConfig = UA_Server_getConfig (server);
+
+ /* Mark this server as a Discovery Server so clients can identify it. */
+ serverConfig->applicationDescription.applicationType
+ = UA_APPLICATIONTYPE_DISCOVERYSERVER;
+
+ /* Time (seconds) after which stale registrations are removed. Must
+ exceed the registering server's re-register interval. */
+ serverConfig->discoveryCleanupTimeout = cleanupTimeout;
+
+ retval = UA_Server_run (server, &running);
+
+ UA_Server_delete (server);
+ return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/src/server_register.c b/src/server_register.c
new file mode 100644
index 0000000..e1defd0
--- /dev/null
+++ b/src/server_register.c
@@ -0,0 +1,170 @@
+/**
+ * @file server_register.c
+ * @brief OPC UA Server that registers with a Local Discovery Server.
+ *
+ * This program runs an OPC UA server configured with security and periodically
+ * registers itself with a remote LDS using the RegisterServer2 service. It
+ * uses separate certificate pairs for the server and for the client connection
+ * to the LDS. On shutdown, it deregisters from the LDS.
+ */
+
+#include "common.h"
+
+#include <open62541/client.h>
+#include <open62541/client_config_default.h>
+#include <open62541/plugin/log_stdout.h>
+#include <open62541/server.h>
+#include <open62541/server_config_default.h>
+
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+UA_Boolean running = true;
+
+static void
+stopHandler (int sign)
+{
+ UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c");
+ running = false;
+}
+
+/* ========================================================================
+ * Main
+ * ======================================================================== */
+
+int
+main (int argc, char **argv)
+{
+ signal (SIGINT, stopHandler);
+ signal (SIGTERM, stopHandler);
+
+ if (argc < 11)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Usage: %s\n"
+ " <port> <applicationUri>\n"
+ " <server-certificate.der> <server-private-key.der>\n"
+ " <client-certificate.der> <client-private-key.der>\n"
+ " <discovery-server-endpoint>\n"
+ " <register-interval-seconds>\n"
+ " <security-mode> <security-policy>\n"
+ " [<trustlist1.der>, ...]\n"
+ "\n"
+ "Security modes : None, Sign, SignAndEncrypt\n"
+ "Security policies: None, Basic256Sha256, "
+ "Aes256_Sha256_RsaPss,\n"
+ " Aes128_Sha256_RsaOaep, ECC_nistP256",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ UA_UInt16 port = (UA_UInt16)atoi (argv[1]);
+ const char *applicationUri = argv[2];
+ const char *clientCertPath = argv[5];
+ const char *clientKeyPath = argv[6];
+ const char *discoveryEndpoint = argv[7];
+ int registerInterval = atoi (argv[8]);
+
+ UA_MessageSecurityMode securityMode = parseSecurityMode (argv[9]);
+ if (securityMode == UA_MESSAGESECURITYMODE_INVALID)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security mode: %s", argv[9]);
+ return EXIT_FAILURE;
+ }
+
+ const char *securityPolicyUri = resolveSecurityPolicyUri (argv[10]);
+ if (!securityPolicyUri)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security policy: %s", argv[10]);
+ return EXIT_FAILURE;
+ }
+
+ size_t trustSize = (argc > 11) ? (size_t)argc - 11 : 0;
+
+ UA_StatusCode retval;
+ UA_Server *server = createSecureServer (
+ port, applicationUri, argv[3], argv[4], argv + 11, trustSize, &retval);
+ if (!server)
+ return EXIT_FAILURE;
+
+ UA_ServerConfig *serverConfig = UA_Server_getConfig (server);
+
+ serverConfig->applicationDescription.applicationType
+ = UA_APPLICATIONTYPE_SERVER;
+
+ UA_Server_run_startup (server);
+
+ /* UA_Server_registerDiscovery consumes (clears) the client config,
+ so a fresh zero-initialized config is needed for every call. */
+ UA_ClientConfig clientConfig;
+ memset (&clientConfig, 0, sizeof (UA_ClientConfig));
+ retval = createSecureClientConfig (
+ &clientConfig, applicationUri, clientCertPath, clientKeyPath, argv + 11,
+ trustSize, securityMode, securityPolicyUri);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Server_run_shutdown (server);
+ UA_Server_delete (server);
+ return EXIT_FAILURE;
+ }
+
+ UA_String discoveryUrl = UA_STRING_ALLOC (discoveryEndpoint);
+ retval = UA_Server_registerDiscovery (server, &clientConfig, discoveryUrl,
+ UA_STRING_NULL);
+ UA_String_clear (&discoveryUrl);
+ if (retval != UA_STATUSCODE_GOOD)
+ UA_LOG_WARNING (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "Initial register failed: %s",
+ UA_StatusCode_name (retval));
+
+ time_t lastRegister = time (NULL);
+
+ while (running)
+ {
+ UA_Server_run_iterate (server, true);
+
+ time_t now = time (NULL);
+ if (now - lastRegister >= registerInterval)
+ {
+ memset (&clientConfig, 0, sizeof (UA_ClientConfig));
+ retval = createSecureClientConfig (
+ &clientConfig, applicationUri, clientCertPath, clientKeyPath,
+ argv + 11, trustSize, securityMode, securityPolicyUri);
+ if (retval == UA_STATUSCODE_GOOD)
+ {
+ UA_String reregUrl = UA_STRING_ALLOC (discoveryEndpoint);
+ retval = UA_Server_registerDiscovery (server, &clientConfig,
+ reregUrl, UA_STRING_NULL);
+ UA_String_clear (&reregUrl);
+ if (retval != UA_STATUSCODE_GOOD)
+ UA_LOG_WARNING (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "Re-register failed: %s",
+ UA_StatusCode_name (retval));
+ }
+ lastRegister = now;
+ }
+ }
+
+ memset (&clientConfig, 0, sizeof (UA_ClientConfig));
+ retval = createSecureClientConfig (
+ &clientConfig, applicationUri, clientCertPath, clientKeyPath, argv + 11,
+ trustSize, securityMode, securityPolicyUri);
+ if (retval == UA_STATUSCODE_GOOD)
+ {
+ UA_String deregUrl = UA_STRING_ALLOC (discoveryEndpoint);
+ retval = UA_Server_deregisterDiscovery (server, &clientConfig, deregUrl);
+ UA_String_clear (&deregUrl);
+ if (retval != UA_STATUSCODE_GOOD)
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER,
+ "Could not unregister from discovery server: %s",
+ UA_StatusCode_name (retval));
+ }
+
+ UA_Server_run_shutdown (server);
+ UA_Server_delete (server);
+ return EXIT_SUCCESS;
+}
diff --git a/tools/generate_certificate.sh b/tools/generate_certificate.sh
new file mode 100755
index 0000000..08bfd28
--- /dev/null
+++ b/tools/generate_certificate.sh
@@ -0,0 +1,56 @@
+#!/bin/bash
+# generate_certificate.sh — Create a self-signed X.509 certificate for
+# open62541 OPC UA applications. Outputs DER-encoded certificate and
+# private-key files suitable for the demo programs in this project.
+
+set -euo pipefail
+
+if [ $# -lt 2 ] || [ $# -gt 3 ]; then
+ echo "Usage: generate_certificate.sh <certs_dir> <name> [uri]" >&2
+ exit 1
+fi
+
+certs_dir="$1"
+name="$2"
+cn="${name}@localhost"
+uri="${3:-urn:bobink.${name}}"
+
+mkdir -p "$certs_dir"
+
+cnf="$certs_dir/${name}.cnf"
+cat >"$cnf" <<EOF
+[req]
+distinguished_name = req_dn
+x509_extensions = v3_ext
+prompt = no
+
+[req_dn]
+C = FR
+O = Bobink
+CN = ${cn}
+
+[v3_ext]
+basicConstraints = CA:FALSE
+keyUsage = digitalSignature, keyEncipherment, nonRepudiation, dataEncipherment
+extendedKeyUsage = serverAuth, clientAuth
+subjectAltName = DNS:localhost, URI:${uri}
+EOF
+
+openssl req -x509 -newkey rsa:2048 -nodes -sha256 \
+ -days 365 \
+ -config "$cnf" \
+ -keyout "$certs_dir/${name}_key.pem" \
+ -out "$certs_dir/${name}_cert.pem" \
+ 2>/dev/null
+
+openssl x509 -in "$certs_dir/${name}_cert.pem" -outform der \
+ -out "$certs_dir/${name}_cert.der"
+openssl rsa -in "$certs_dir/${name}_key.pem" -outform der \
+ -out "$certs_dir/${name}_key.der" 2>/dev/null
+
+rm -f "$certs_dir/${name}_cert.pem" "$certs_dir/${name}_key.pem" "$cnf"
+
+echo "Generated certificate '$name' (CN=$cn, URI=$uri):"
+echo " $certs_dir/${name}_cert.der"
+echo " $certs_dir/${name}_key.der"
+echo " $certs_dir/${name}_key.der"
diff --git a/tools/launch.sh b/tools/launch.sh
new file mode 100755
index 0000000..7691baf
--- /dev/null
+++ b/tools/launch.sh
@@ -0,0 +1,148 @@
+#!/bin/bash
+# launch.sh — Generate certificates and launch N RegisterServers + 1 LDS.
+#
+# Usage: tools/launch.sh [N] [extra_cert1.der ...]
+# N Number of RegisterServer instances to launch (default: 1).
+# extra certs Additional certificates to add to the LDS and every
+# RegisterServer trustlist (e.g. external client certs).
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+BUILD_DIR="$PROJECT_DIR/build"
+CERTS_DIR="$PROJECT_DIR/certs"
+GEN_CERT="$SCRIPT_DIR/generate_certificate.sh"
+
+N="${1:-1}"
+
+if ! [[ "$N" =~ ^[1-9][0-9]*$ ]]; then
+ echo "Usage: $0 [N] [extra_cert1.der ...]" >&2
+ echo " N = number of RegisterServers (default 1)" >&2
+ exit 1
+fi
+
+shift || true
+EXTRA_TRUST=("$@")
+
+LDS_PORT=4840
+BASE_REGISTER_PORT=4841
+
+# ------------------------------------------------------------------
+# Certificate generation (only creates missing ones)
+# ------------------------------------------------------------------
+
+generate_if_missing() {
+ local name="$1"
+ local uri="${2:-}"
+ if [ ! -f "$CERTS_DIR/${name}_cert.der" ] ||
+ [ ! -f "$CERTS_DIR/${name}_key.der" ]; then
+ "$GEN_CERT" "$CERTS_DIR" "$name" ${uri:+"$uri"}
+ fi
+}
+
+generate_if_missing "ServerLDS"
+generate_if_missing "ClientFindServers"
+
+for i in $(seq 1 "$N"); do
+ generate_if_missing "ServerRegister${i}"
+ # The client cert must carry the server's ApplicationUri so the LDS
+ # can verify the certificate against the ApplicationDescription.
+ generate_if_missing "ServerRegisterClient${i}" "urn:bobink.ServerRegister${i}"
+done
+
+# ------------------------------------------------------------------
+# Cleanup on exit
+# ------------------------------------------------------------------
+
+pids=()
+
+cleanup() {
+ echo ""
+ echo "Stopping all servers..."
+ for pid in "${pids[@]}"; do
+ kill "$pid" 2>/dev/null || true
+ done
+ wait 2>/dev/null
+ echo "All servers stopped."
+}
+
+trap cleanup EXIT INT TERM
+
+# ------------------------------------------------------------------
+# Launch LDS
+# ------------------------------------------------------------------
+
+# LDS trustlist: every RegisterServer client cert + the FindServers client cert.
+lds_trustlist=()
+for i in $(seq 1 "$N"); do
+ lds_trustlist+=("$CERTS_DIR/ServerRegisterClient${i}_cert.der")
+done
+lds_trustlist+=("$CERTS_DIR/ClientFindServers_cert.der")
+lds_trustlist+=(${EXTRA_TRUST[@]+"${EXTRA_TRUST[@]}"})
+
+echo "Starting LDS on port $LDS_PORT..."
+"$BUILD_DIR/ServerLDS" \
+ "$LDS_PORT" \
+ "urn:bobink.ServerLDS" \
+ "$CERTS_DIR/ServerLDS_cert.der" \
+ "$CERTS_DIR/ServerLDS_key.der" \
+ 60 \
+ "${lds_trustlist[@]}" &
+pids+=($!)
+sleep 1
+
+# ------------------------------------------------------------------
+# Launch RegisterServers
+# ------------------------------------------------------------------
+
+for i in $(seq 1 "$N"); do
+ port=$((BASE_REGISTER_PORT + i - 1))
+
+ echo "Starting ServerRegister${i} on port $port..."
+ "$BUILD_DIR/ServerRegister" \
+ "$port" \
+ "urn:bobink.ServerRegister${i}" \
+ "$CERTS_DIR/ServerRegister${i}_cert.der" \
+ "$CERTS_DIR/ServerRegister${i}_key.der" \
+ "$CERTS_DIR/ServerRegisterClient${i}_cert.der" \
+ "$CERTS_DIR/ServerRegisterClient${i}_key.der" \
+ "opc.tcp://localhost:$LDS_PORT" \
+ 10 \
+ SignAndEncrypt Aes128_Sha256_RsaOaep \
+ "$CERTS_DIR/ServerLDS_cert.der" \
+ "$CERTS_DIR/ClientFindServers_cert.der" \
+ ${EXTRA_TRUST[@]+"${EXTRA_TRUST[@]}"} &
+ pids+=($!)
+ sleep 0.5
+done
+
+# ------------------------------------------------------------------
+# Summary
+# ------------------------------------------------------------------
+
+echo ""
+echo "=== All servers running ==="
+echo " LDS: port $LDS_PORT"
+for i in $(seq 1 "$N"); do
+ port=$((BASE_REGISTER_PORT + i - 1))
+ echo " ServerRegister${i}: port $port"
+done
+
+# Build the client command hint with the correct trustlist.
+client_trust="certs/ServerLDS_cert.der"
+for i in $(seq 1 "$N"); do
+ client_trust="$client_trust certs/ServerRegister${i}_cert.der"
+done
+
+echo ""
+echo "Run the client with:"
+echo " build/ClientFindServers \"opc.tcp://localhost:$LDS_PORT\" \\"
+echo " \"urn:bobink.ClientFindServers\" \\"
+echo " certs/ClientFindServers_cert.der certs/ClientFindServers_key.der \\"
+echo " SignAndEncrypt Aes128_Sha256_RsaOaep \\"
+echo " $client_trust"
+echo ""
+echo "Press Ctrl+C to stop all servers."
+
+wait