From a9ebc3b434b7979163fdf83984b32f1e513dacb8 Mon Sep 17 00:00:00 2001 From: Thomas Vanbesien Date: Thu, 19 Feb 2026 00:14:25 +0100 Subject: Rename client executable to bobink_opcua_client --- src/bobink_opcua_client.c | 393 ++++++++++++++++++++++++++++++++++++++++++++++ src/client.c | 393 ---------------------------------------------- 2 files changed, 393 insertions(+), 393 deletions(-) create mode 100644 src/bobink_opcua_client.c delete mode 100644 src/client.c (limited to 'src') diff --git a/src/bobink_opcua_client.c b/src/bobink_opcua_client.c new file mode 100644 index 0000000..35a3e6f --- /dev/null +++ b/src/bobink_opcua_client.c @@ -0,0 +1,393 @@ +/** + * @file client.c + * @brief Unified OPC UA client for discovery and server interaction. + * + * Supports four 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 + * download-cert — downloads a server's certificate to a local file + * + * Encryption is optional: when certificate, privateKey, and trustStore are + * provided, the client uses the configured security policy; otherwise it + * connects without encryption. + */ + +#include "common.h" +#include "config.h" + +#include +#include +#include + +#include +#include +#include + +/* ======================================================================== + * Operation Dispatch + * ======================================================================== */ + +typedef enum +{ + OP_FIND_SERVERS, + OP_GET_ENDPOINTS, + OP_READ_TIME, + OP_DOWNLOAD_CERT, + OP_INVALID +} operation; + +static operation +_s_parse_operation (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; + if (strcmp (name, "download-cert") == 0) + return OP_DOWNLOAD_CERT; + return OP_INVALID; +} + +/* ======================================================================== + * Operations + * ======================================================================== */ + +/** + * Calls the FindServers service and prints all discovered servers. + * + * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise. + */ +static int +_s_op_find_servers (UA_Client *client, const char *url) +{ + size_t array_size = 0; + UA_ApplicationDescription *array = NULL; + + UA_StatusCode retval = UA_Client_findServers (client, url, 0, NULL, 0, NULL, + &array_size, &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 < array_size; i++) + print_application_description (&array[i], i); + + UA_Array_delete (array, array_size, + &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 +_s_op_get_endpoints (UA_Client *client, const char *url) +{ + size_t array_size = 0; + UA_EndpointDescription *array = NULL; + + UA_StatusCode retval + = UA_Client_getEndpoints (client, url, &array_size, &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 < array_size; i++) + print_endpoint (&array[i], i); + + UA_Array_delete (array, array_size, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); + return EXIT_SUCCESS; +} + +/** + * Connects to a server and reads the current time node. + * + * Authentication (anonymous, username/password, or X509 certificate) is + * configured in the client config before this function is called. + * + * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise. + */ +static int +_s_op_read_time (UA_Client *client, const char *url) +{ + UA_StatusCode 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; +} + +/** + * Downloads the server's certificate via GetEndpoints and writes it to a file. + * + * Picks the first endpoint that carries a non-empty serverCertificate. + * + * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise. + */ +static int +_s_op_download_cert (UA_Client *client, const char *url, + const char *output_path) +{ + size_t array_size = 0; + UA_EndpointDescription *array = NULL; + + UA_StatusCode retval + = UA_Client_getEndpoints (client, url, &array_size, &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; + } + + UA_ByteString *cert = NULL; + for (size_t i = 0; i < array_size; i++) + { + if (array[i].serverCertificate.length > 0) + { + cert = &array[i].serverCertificate; + break; + } + } + + if (!cert) + { + UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, + "No endpoint returned a server certificate"); + UA_Array_delete (array, array_size, + &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); + return EXIT_FAILURE; + } + + FILE *fp = fopen (output_path, "wb"); + if (!fp) + { + UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, + "Could not open output file: %s", output_path); + UA_Array_delete (array, array_size, + &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); + return EXIT_FAILURE; + } + + size_t written = fwrite (cert->data, 1, cert->length, fp); + fclose (fp); + + int rc; + if (written != cert->length) + { + UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, + "Failed to write certificate (%zu of %zu bytes)", written, + cert->length); + rc = EXIT_FAILURE; + } + else + { + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, + "Certificate saved to %s (%zu bytes)", output_path, + cert->length); + rc = EXIT_SUCCESS; + } + + UA_Array_delete (array, array_size, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); + return rc; +} + +/* ======================================================================== + * Main + * ======================================================================== */ + +int +main (int argc, char **argv) +{ + if (argc < 4) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Usage: %s " + "[log-level]\n" + " %s download-cert " + " [log-level]\n" + "Operations: find-servers, get-endpoints, read-time, " + "download-cert", + argv[0], argv[0]); + return EXIT_FAILURE; + } + + operation op = _s_parse_operation (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, download-cert)", + argv[2]); + return EXIT_FAILURE; + } + + const char *endpoint_url = argv[3]; + const char *output_path = NULL; + const char *log_level_str = "info"; + + if (op == OP_DOWNLOAD_CERT) + { + if (argc < 5 || argc > 6) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Usage: %s download-cert " + " [log-level]", + argv[0]); + return EXIT_FAILURE; + } + output_path = argv[4]; + log_level_str = (argc == 6) ? argv[5] : "info"; + } + else + { + if (argc > 5) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Usage: %s " + "[log-level]", + argv[0]); + return EXIT_FAILURE; + } + log_level_str = (argc == 5) ? argv[4] : "info"; + } + int log_level = parse_log_level (log_level_str); + if (log_level < 0) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Unknown log level: %s " + "(expected trace, debug, info, warning, error, fatal)", + log_level_str); + return EXIT_FAILURE; + } + + config cfg; + if (config_load (argv[1], &cfg) != 0) + return EXIT_FAILURE; + + /* ---- Common config keys ---- */ + + const char *application_uri + = config_require (&cfg, "applicationUri", "Client"); + if (!application_uri) + { + config_free (&cfg); + return EXIT_FAILURE; + } + + security_config sec; + if (parse_security_config (&cfg, "Client", true, &sec) != 0) + { + config_free (&cfg); + return EXIT_FAILURE; + } + + /* ---- Auth config (read-time only) ---- */ + + auth_config auth = { .mode = AUTH_ANONYMOUS }; + + if (op == OP_READ_TIME && parse_auth_config (&cfg, "Client", &auth) != 0) + { + free_trust_store (sec.trust_paths, sec.trust_size); + config_free (&cfg); + return EXIT_FAILURE; + } + + /* ---- Create client ---- */ + + UA_Client *client = UA_Client_new (); + + UA_StatusCode retval; + if (op == OP_DOWNLOAD_CERT) + retval = create_unsecure_client_config (UA_Client_getConfig (client), + application_uri, NULL); + else if (sec.cert_path) + retval = create_secure_client_config (UA_Client_getConfig (client), + application_uri, &sec, &auth); + else + retval = create_unsecure_client_config (UA_Client_getConfig (client), + application_uri, &auth); + + if (retval != UA_STATUSCODE_GOOD) + { + UA_Client_delete (client); + free_trust_store (sec.trust_paths, sec.trust_size); + config_free (&cfg); + return EXIT_FAILURE; + } + + UA_Client_getConfig (client)->logging->context + = (void *)(uintptr_t)log_level; + + /* ---- Dispatch operation ---- */ + + int rc; + switch (op) + { + case OP_FIND_SERVERS: + rc = _s_op_find_servers (client, endpoint_url); + break; + case OP_GET_ENDPOINTS: + rc = _s_op_get_endpoints (client, endpoint_url); + break; + case OP_READ_TIME: + rc = _s_op_read_time (client, endpoint_url); + break; + case OP_DOWNLOAD_CERT: + rc = _s_op_download_cert (client, endpoint_url, output_path); + break; + default: + rc = EXIT_FAILURE; + break; + } + + /* ---- Cleanup ---- */ + + UA_Client_delete (client); + free_trust_store (sec.trust_paths, sec.trust_size); + config_free (&cfg); + + return rc; +} diff --git a/src/client.c b/src/client.c deleted file mode 100644 index 35a3e6f..0000000 --- a/src/client.c +++ /dev/null @@ -1,393 +0,0 @@ -/** - * @file client.c - * @brief Unified OPC UA client for discovery and server interaction. - * - * Supports four 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 - * download-cert — downloads a server's certificate to a local file - * - * Encryption is optional: when certificate, privateKey, and trustStore are - * provided, the client uses the configured security policy; otherwise it - * connects without encryption. - */ - -#include "common.h" -#include "config.h" - -#include -#include -#include - -#include -#include -#include - -/* ======================================================================== - * Operation Dispatch - * ======================================================================== */ - -typedef enum -{ - OP_FIND_SERVERS, - OP_GET_ENDPOINTS, - OP_READ_TIME, - OP_DOWNLOAD_CERT, - OP_INVALID -} operation; - -static operation -_s_parse_operation (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; - if (strcmp (name, "download-cert") == 0) - return OP_DOWNLOAD_CERT; - return OP_INVALID; -} - -/* ======================================================================== - * Operations - * ======================================================================== */ - -/** - * Calls the FindServers service and prints all discovered servers. - * - * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise. - */ -static int -_s_op_find_servers (UA_Client *client, const char *url) -{ - size_t array_size = 0; - UA_ApplicationDescription *array = NULL; - - UA_StatusCode retval = UA_Client_findServers (client, url, 0, NULL, 0, NULL, - &array_size, &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 < array_size; i++) - print_application_description (&array[i], i); - - UA_Array_delete (array, array_size, - &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 -_s_op_get_endpoints (UA_Client *client, const char *url) -{ - size_t array_size = 0; - UA_EndpointDescription *array = NULL; - - UA_StatusCode retval - = UA_Client_getEndpoints (client, url, &array_size, &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 < array_size; i++) - print_endpoint (&array[i], i); - - UA_Array_delete (array, array_size, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); - return EXIT_SUCCESS; -} - -/** - * Connects to a server and reads the current time node. - * - * Authentication (anonymous, username/password, or X509 certificate) is - * configured in the client config before this function is called. - * - * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise. - */ -static int -_s_op_read_time (UA_Client *client, const char *url) -{ - UA_StatusCode 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; -} - -/** - * Downloads the server's certificate via GetEndpoints and writes it to a file. - * - * Picks the first endpoint that carries a non-empty serverCertificate. - * - * @return EXIT_SUCCESS on success, EXIT_FAILURE otherwise. - */ -static int -_s_op_download_cert (UA_Client *client, const char *url, - const char *output_path) -{ - size_t array_size = 0; - UA_EndpointDescription *array = NULL; - - UA_StatusCode retval - = UA_Client_getEndpoints (client, url, &array_size, &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; - } - - UA_ByteString *cert = NULL; - for (size_t i = 0; i < array_size; i++) - { - if (array[i].serverCertificate.length > 0) - { - cert = &array[i].serverCertificate; - break; - } - } - - if (!cert) - { - UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, - "No endpoint returned a server certificate"); - UA_Array_delete (array, array_size, - &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); - return EXIT_FAILURE; - } - - FILE *fp = fopen (output_path, "wb"); - if (!fp) - { - UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, - "Could not open output file: %s", output_path); - UA_Array_delete (array, array_size, - &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); - return EXIT_FAILURE; - } - - size_t written = fwrite (cert->data, 1, cert->length, fp); - fclose (fp); - - int rc; - if (written != cert->length) - { - UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, - "Failed to write certificate (%zu of %zu bytes)", written, - cert->length); - rc = EXIT_FAILURE; - } - else - { - UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, - "Certificate saved to %s (%zu bytes)", output_path, - cert->length); - rc = EXIT_SUCCESS; - } - - UA_Array_delete (array, array_size, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); - return rc; -} - -/* ======================================================================== - * Main - * ======================================================================== */ - -int -main (int argc, char **argv) -{ - if (argc < 4) - { - UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, - "Usage: %s " - "[log-level]\n" - " %s download-cert " - " [log-level]\n" - "Operations: find-servers, get-endpoints, read-time, " - "download-cert", - argv[0], argv[0]); - return EXIT_FAILURE; - } - - operation op = _s_parse_operation (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, download-cert)", - argv[2]); - return EXIT_FAILURE; - } - - const char *endpoint_url = argv[3]; - const char *output_path = NULL; - const char *log_level_str = "info"; - - if (op == OP_DOWNLOAD_CERT) - { - if (argc < 5 || argc > 6) - { - UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, - "Usage: %s download-cert " - " [log-level]", - argv[0]); - return EXIT_FAILURE; - } - output_path = argv[4]; - log_level_str = (argc == 6) ? argv[5] : "info"; - } - else - { - if (argc > 5) - { - UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, - "Usage: %s " - "[log-level]", - argv[0]); - return EXIT_FAILURE; - } - log_level_str = (argc == 5) ? argv[4] : "info"; - } - int log_level = parse_log_level (log_level_str); - if (log_level < 0) - { - UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, - "Unknown log level: %s " - "(expected trace, debug, info, warning, error, fatal)", - log_level_str); - return EXIT_FAILURE; - } - - config cfg; - if (config_load (argv[1], &cfg) != 0) - return EXIT_FAILURE; - - /* ---- Common config keys ---- */ - - const char *application_uri - = config_require (&cfg, "applicationUri", "Client"); - if (!application_uri) - { - config_free (&cfg); - return EXIT_FAILURE; - } - - security_config sec; - if (parse_security_config (&cfg, "Client", true, &sec) != 0) - { - config_free (&cfg); - return EXIT_FAILURE; - } - - /* ---- Auth config (read-time only) ---- */ - - auth_config auth = { .mode = AUTH_ANONYMOUS }; - - if (op == OP_READ_TIME && parse_auth_config (&cfg, "Client", &auth) != 0) - { - free_trust_store (sec.trust_paths, sec.trust_size); - config_free (&cfg); - return EXIT_FAILURE; - } - - /* ---- Create client ---- */ - - UA_Client *client = UA_Client_new (); - - UA_StatusCode retval; - if (op == OP_DOWNLOAD_CERT) - retval = create_unsecure_client_config (UA_Client_getConfig (client), - application_uri, NULL); - else if (sec.cert_path) - retval = create_secure_client_config (UA_Client_getConfig (client), - application_uri, &sec, &auth); - else - retval = create_unsecure_client_config (UA_Client_getConfig (client), - application_uri, &auth); - - if (retval != UA_STATUSCODE_GOOD) - { - UA_Client_delete (client); - free_trust_store (sec.trust_paths, sec.trust_size); - config_free (&cfg); - return EXIT_FAILURE; - } - - UA_Client_getConfig (client)->logging->context - = (void *)(uintptr_t)log_level; - - /* ---- Dispatch operation ---- */ - - int rc; - switch (op) - { - case OP_FIND_SERVERS: - rc = _s_op_find_servers (client, endpoint_url); - break; - case OP_GET_ENDPOINTS: - rc = _s_op_get_endpoints (client, endpoint_url); - break; - case OP_READ_TIME: - rc = _s_op_read_time (client, endpoint_url); - break; - case OP_DOWNLOAD_CERT: - rc = _s_op_download_cert (client, endpoint_url, output_path); - break; - default: - rc = EXIT_FAILURE; - break; - } - - /* ---- Cleanup ---- */ - - UA_Client_delete (client); - free_trust_store (sec.trust_paths, sec.trust_size); - config_free (&cfg); - - return rc; -} -- cgit v1.2.3