aboutsummaryrefslogtreecommitdiffstats
path: root/src/client.c
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-17 19:06:22 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-17 19:06:22 +0100
commit827e90e0daabe32e058e08dd2a253425898a7e7a (patch)
treeecd3f31da63890ac029b7929eade88f38e078b3d /src/client.c
parente4ba24b3d24fdce36bc9dbd3c2c8f00b0ec23335 (diff)
downloadBobinkCOpcUa-827e90e0daabe32e058e08dd2a253425898a7e7a.tar.gz
BobinkCOpcUa-827e90e0daabe32e058e08dd2a253425898a7e7a.zip
Replace ClientFindServers with unified Client, use trust store directories
Replace the single-purpose ClientFindServers program with a unified Client that supports three operations via CLI: find-servers, get-endpoints, and read-time. This simplifies the architecture by using one client binary with a single config file instead of a monolithic program that did everything in one run. Split the ServerRegister config into separate server and client config files so the LDS-registration credentials are isolated from the server's own settings. The discovery URL moves from config to a CLI argument. Replace repeated trustList config entries with a single trustStore directory path. Each program now points to a directory under certs/trust/ containing .der files, so adding or removing trust is a file-copy operation rather than editing every config file. Add loadTrustStore()/freeTrustStore() to common.c and remove the now-unused configGetAll() from the config parser. Simplify the test matrix from 6 to 4 cases (security and auth are orthogonal, so the full 3x2 matrix is unnecessary). Update run_test.sh to invoke the new Client three times and use port-polling instead of sleep.
Diffstat (limited to 'src/client.c')
-rw-r--r--src/client.c328
1 files changed, 328 insertions, 0 deletions
diff --git a/src/client.c b/src/client.c
new file mode 100644
index 0000000..8234963
--- /dev/null
+++ b/src/client.c
@@ -0,0 +1,328 @@
+/**
+ * @file client.c
+ * @brief Unified OPC UA client for discovery and server interaction.
+ *
+ * Supports three operations selected via CLI:
+ * find-servers — queries a server's FindServers service
+ * get-endpoints — queries a server's GetEndpoints service
+ * read-time — connects to a server and reads the current time
+ */
+
+#include "common.h"
+#include "config.h"
+
+#include <open62541/client_highlevel.h>
+#include <open62541/plugin/log_stdout.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+/* ========================================================================
+ * Operation Dispatch
+ * ======================================================================== */
+
+typedef enum
+{
+ OP_FIND_SERVERS,
+ OP_GET_ENDPOINTS,
+ OP_READ_TIME,
+ OP_INVALID
+} Operation;
+
+static Operation
+parseOperation (const char *name)
+{
+ if (strcmp (name, "find-servers") == 0)
+ return OP_FIND_SERVERS;
+ if (strcmp (name, "get-endpoints") == 0)
+ return OP_GET_ENDPOINTS;
+ if (strcmp (name, "read-time") == 0)
+ return OP_READ_TIME;
+ return OP_INVALID;
+}
+
+/* ========================================================================
+ * Operations
+ * ======================================================================== */
+
+/**
+ * Calls the FindServers service and prints all discovered servers.
+ *
+ * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise.
+ */
+static int
+opFindServers (UA_Client *client, const char *url)
+{
+ size_t arraySize = 0;
+ UA_ApplicationDescription *array = NULL;
+
+ UA_StatusCode retval = UA_Client_findServers (client, url, 0, NULL, 0, NULL,
+ &arraySize, &array);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "FindServers failed: %s", UA_StatusCode_name (retval));
+ return EXIT_FAILURE;
+ }
+
+ for (size_t i = 0; i < arraySize; i++)
+ printApplicationDescription (&array[i], i);
+
+ UA_Array_delete (array, arraySize,
+ &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]);
+ return EXIT_SUCCESS;
+}
+
+/**
+ * Calls the GetEndpoints service and prints all endpoints.
+ *
+ * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise.
+ */
+static int
+opGetEndpoints (UA_Client *client, const char *url)
+{
+ size_t arraySize = 0;
+ UA_EndpointDescription *array = NULL;
+
+ UA_StatusCode retval
+ = UA_Client_getEndpoints (client, url, &arraySize, &array);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "GetEndpoints failed: %s", UA_StatusCode_name (retval));
+ return EXIT_FAILURE;
+ }
+
+ for (size_t i = 0; i < arraySize; i++)
+ printEndpoint (&array[i], i);
+
+ UA_Array_delete (array, arraySize, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
+ return EXIT_SUCCESS;
+}
+
+/**
+ * Connects to a server and reads the current time node.
+ *
+ * @param username Username for session auth, or NULL for anonymous.
+ * @param password Password for session auth (ignored when username is NULL).
+ * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise.
+ */
+static int
+opReadTime (UA_Client *client, const char *url, const char *username,
+ const char *password)
+{
+ UA_StatusCode retval;
+ if (username)
+ retval = UA_Client_connectUsername (client, url, username, password);
+ else
+ retval = UA_Client_connect (client, url);
+
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
+ "Could not connect: %s", UA_StatusCode_name (retval));
+ return EXIT_FAILURE;
+ }
+
+ UA_Variant value;
+ UA_Variant_init (&value);
+
+ const UA_NodeId nodeId = UA_NS0ID (SERVER_SERVERSTATUS_CURRENTTIME);
+ retval = UA_Client_readValueAttribute (client, nodeId, &value);
+
+ int rc = EXIT_SUCCESS;
+ 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));
+ rc = EXIT_FAILURE;
+ }
+
+ UA_Variant_clear (&value);
+ UA_Client_disconnect (client);
+ return rc;
+}
+
+/* ========================================================================
+ * Main
+ * ======================================================================== */
+
+int
+main (int argc, char **argv)
+{
+ if (argc < 4 || argc > 5)
+ {
+ UA_LOG_FATAL (
+ UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Usage: %s <config-file> <operation> <endpoint-url> [log-level]",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ Operation op = parseOperation (argv[2]);
+ if (op == OP_INVALID)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown operation: %s "
+ "(expected find-servers, get-endpoints, read-time)",
+ argv[2]);
+ return EXIT_FAILURE;
+ }
+
+ const char *endpointUrl = argv[3];
+
+ const char *logLevelStr = (argc == 5) ? argv[4] : "info";
+ int logLevel = parseLogLevel (logLevelStr);
+ if (logLevel < 0)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown log level: %s "
+ "(expected trace, debug, info, warning, error, fatal)",
+ logLevelStr);
+ return EXIT_FAILURE;
+ }
+
+ Config cfg;
+ if (configLoad (argv[1], &cfg) != 0)
+ return EXIT_FAILURE;
+
+ /* ---- Common config keys ---- */
+
+ const char *applicationUri
+ = configRequire (&cfg, "applicationUri", "Client");
+ const char *certPath = configRequire (&cfg, "certificate", "Client");
+ const char *keyPath = configRequire (&cfg, "privateKey", "Client");
+ const char *secModeStr = configRequire (&cfg, "securityMode", "Client");
+ const char *secPolStr = configRequire (&cfg, "securityPolicy", "Client");
+
+ if (!applicationUri || !certPath || !keyPath || !secModeStr || !secPolStr)
+ {
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+
+ UA_MessageSecurityMode secMode = parseSecurityMode (secModeStr);
+ if (secMode == UA_MESSAGESECURITYMODE_INVALID)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security mode: %s", secModeStr);
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+
+ const char *secPolUri = resolveSecurityPolicyUri (secPolStr);
+ if (!secPolUri)
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown security policy: %s", secPolStr);
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+
+ /* ---- Auth config (read-time only) ---- */
+
+ const char *username = NULL, *password = NULL;
+
+ if (op == OP_READ_TIME)
+ {
+ const char *authMode = configRequire (&cfg, "authMode", "Client");
+ if (!authMode)
+ {
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+
+ if (strcmp (authMode, "anonymous") == 0)
+ {
+ /* No credentials needed. */
+ }
+ else if (strcmp (authMode, "user") == 0)
+ {
+ username = configRequire (&cfg, "username", "Client");
+ password = configRequire (&cfg, "password", "Client");
+ if (!username || !password)
+ {
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+ }
+ else
+ {
+ UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
+ "Unknown auth mode: %s "
+ "(expected 'anonymous' or 'user')",
+ authMode);
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* ---- Trust store ---- */
+
+ const char *trustStore = configRequire (&cfg, "trustStore", "Client");
+ if (!trustStore)
+ {
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+
+ char **trustPaths = NULL;
+ size_t trustSize = 0;
+ if (loadTrustStore (trustStore, &trustPaths, &trustSize) != 0)
+ {
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+
+ /* ---- Create client ---- */
+
+ UA_Client *client = UA_Client_new ();
+ UA_StatusCode retval = createSecureClientConfig (
+ UA_Client_getConfig (client), applicationUri, certPath, keyPath,
+ trustPaths, trustSize, secMode, secPolUri);
+ if (retval != UA_STATUSCODE_GOOD)
+ {
+ UA_Client_delete (client);
+ freeTrustStore (trustPaths, trustSize);
+ configFree (&cfg);
+ return EXIT_FAILURE;
+ }
+ UA_Client_getConfig (client)->logging->context = (void *)(uintptr_t)logLevel;
+
+ /* ---- Dispatch operation ---- */
+
+ int rc;
+ switch (op)
+ {
+ case OP_FIND_SERVERS:
+ rc = opFindServers (client, endpointUrl);
+ break;
+ case OP_GET_ENDPOINTS:
+ rc = opGetEndpoints (client, endpointUrl);
+ break;
+ case OP_READ_TIME:
+ rc = opReadTime (client, endpointUrl, username, password);
+ break;
+ default:
+ rc = EXIT_FAILURE;
+ break;
+ }
+
+ /* ---- Cleanup ---- */
+
+ UA_Client_delete (client);
+ freeTrustStore (trustPaths, trustSize);
+ configFree (&cfg);
+
+ return rc;
+}