aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-17 02:27:51 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-17 02:27:51 +0100
commitc35eb35bb63a97b7c46e879819757a9cb48165b5 (patch)
treeabc7f07740fae388f4ff6776585b56f56ec558c9 /src
downloadBobinkCOpcUa-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.
Diffstat (limited to 'src')
-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
5 files changed, 927 insertions, 0 deletions
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;
+}