diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 19:06:22 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 19:06:22 +0100 |
| commit | 827e90e0daabe32e058e08dd2a253425898a7e7a (patch) | |
| tree | ecd3f31da63890ac029b7929eade88f38e078b3d /src/client.c | |
| parent | e4ba24b3d24fdce36bc9dbd3c2c8f00b0ec23335 (diff) | |
| download | BobinkCOpcUa-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.c | 328 |
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; +} |
