diff options
Diffstat (limited to 'src/bobink_opcua_client.c')
| -rw-r--r-- | src/bobink_opcua_client.c | 393 |
1 files changed, 393 insertions, 0 deletions
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 <open62541/client_config_default.h> +#include <open62541/client_highlevel.h> +#include <open62541/plugin/log_stdout.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* ======================================================================== + * 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 <config-file> <operation> <endpoint-url> " + "[log-level]\n" + " %s <config-file> download-cert <endpoint-url> " + "<output-file> [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 <config-file> download-cert " + "<endpoint-url> <output-file> [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 <config-file> <operation> <endpoint-url> " + "[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; +} |
