/** * @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); 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); 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; }