aboutsummaryrefslogtreecommitdiffstats
path: root/src/bobink_opcua_client.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/bobink_opcua_client.c')
-rw-r--r--src/bobink_opcua_client.c393
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;
+}