diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 02:27:51 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 02:27:51 +0100 |
| commit | c35eb35bb63a97b7c46e879819757a9cb48165b5 (patch) | |
| tree | abc7f07740fae388f4ff6776585b56f56ec558c9 | |
| download | BobinkCOpcUa-c35eb35bb63a97b7c46e879819757a9cb48165b5.tar.gz BobinkCOpcUa-c35eb35bb63a97b7c46e879819757a9cb48165b5.zip | |
Initial commit: OPC UA discovery project
CMake-based C project using open62541 for OPC UA discovery.
Includes Local Discovery Server, register server, and find
servers client with OpenSSL encryption support.
| -rw-r--r-- | .clang-format | 297 | ||||
| -rw-r--r-- | .gitignore | 14 | ||||
| -rw-r--r-- | .gitmodules | 3 | ||||
| -rw-r--r-- | CLAUDE.md | 129 | ||||
| -rw-r--r-- | CMakeLists.txt | 27 | ||||
| -rw-r--r-- | cmake/BuildDeps.cmake | 50 | ||||
| m--------- | deps/open62541 | 0 | ||||
| -rw-r--r-- | src/client_find_servers.c | 281 | ||||
| -rw-r--r-- | src/common.c | 280 | ||||
| -rw-r--r-- | src/common.h | 115 | ||||
| -rw-r--r-- | src/server_lds.c | 81 | ||||
| -rw-r--r-- | src/server_register.c | 170 | ||||
| -rwxr-xr-x | tools/generate_certificate.sh | 56 | ||||
| -rwxr-xr-x | tools/launch.sh | 148 |
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 |
