/** * @file client_find_servers.c * @brief OPC UA client that queries a Local Discovery Server for registered * servers. * * This program connects to an LDS and calls the FindServers service to * retrieve all registered servers. It then queries each server's endpoints * using the GetEndpoints service and displays the results in a human-readable * format. */ #include "common.h" #include "config.h" #include #include #include #include /* ======================================================================== * Discovery Service Calls * ======================================================================== */ /** * Calls the FindServers service on the LDS and prints all discovered servers. * * @param client The OPC UA client instance. * @param discoveryServerEndpoint The LDS endpoint URL. * @param applicationDescriptionArraySize Output: number of servers found. * @param applicationDescriptionArray Output: array of server descriptions. * @return UA_STATUSCODE_GOOD on success, error code otherwise. */ static UA_StatusCode findServers (UA_Client *client, const char *discoveryServerEndpoint, size_t *applicationDescriptionArraySize, UA_ApplicationDescription **applicationDescriptionArray) { UA_StatusCode retval = UA_Client_findServers ( client, discoveryServerEndpoint, 0, NULL, 0, NULL, applicationDescriptionArraySize, applicationDescriptionArray); if (retval != UA_STATUSCODE_GOOD) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "Could not call FindServers service. " "Is the discovery server started? StatusCode %s", UA_StatusCode_name (retval)); return retval; } for (size_t i = 0; i < *applicationDescriptionArraySize; i++) printApplicationDescription (&(*applicationDescriptionArray)[i], i); return UA_STATUSCODE_GOOD; } /** * Queries endpoints for each discovered server using the GetEndpoints service. * * For each server in the applicationDescriptionArray, this function extracts * the first discovery URL and calls GetEndpoints to retrieve all available * endpoints. Results are logged via UA_LOG_INFO. * * @param client The OPC UA client instance. * @param applicationDescriptionArray Array of server descriptions from * FindServers. * @param applicationDescriptionArraySize Number of servers in the array. */ static void getServersEndpoints (UA_Client *client, UA_ApplicationDescription *applicationDescriptionArray, size_t applicationDescriptionArraySize) { for (size_t i = 0; i < applicationDescriptionArraySize; i++) { UA_ApplicationDescription *description = &applicationDescriptionArray[i]; if (description->discoveryUrlsSize == 0) { UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "[GetEndpoints] Server %.*s did not provide any " "discovery urls. Skipping.", (int)description->applicationUri.length, description->applicationUri.data); continue; } /* UA_String is not null-terminated; build a C string for the API. */ char *discoveryUrl = (char *)UA_malloc ( sizeof (char) * description->discoveryUrls[0].length + 1); memcpy (discoveryUrl, description->discoveryUrls[0].data, description->discoveryUrls[0].length); discoveryUrl[description->discoveryUrls[0].length] = '\0'; UA_EndpointDescription *endpointArray = NULL; size_t endpointArraySize = 0; UA_StatusCode retval = UA_Client_getEndpoints ( client, discoveryUrl, &endpointArraySize, &endpointArray); UA_free (discoveryUrl); if (retval != UA_STATUSCODE_GOOD) { UA_Client_disconnect (client); break; } UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, "Endpoints for Server[%lu]: %.*s", (unsigned long)i, (int)description->applicationUri.length, description->applicationUri.data); for (size_t j = 0; j < endpointArraySize; j++) printEndpoint (&endpointArray[j], j); UA_Array_delete (endpointArray, endpointArraySize, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]); } } /* ======================================================================== * Node Reading * ======================================================================== */ /** * Connects to each non-discovery server and reads the current time node. * * For each server that is not a DiscoveryServer, this function establishes a * secure session, reads the Server_ServerStatus_CurrentTime variable, prints * the result, and disconnects. * * @param client The OPC UA client instance. * @param applicationDescriptionArray Array of server descriptions from * FindServers. * @param applicationDescriptionArraySize Number of servers in the array. * @param username Username for session auth, or NULL for anonymous. * @param password Password for session auth (ignored when username is NULL). */ static void readServerTime (UA_Client *client, UA_ApplicationDescription *applicationDescriptionArray, size_t applicationDescriptionArraySize, const char *username, const char *password) { for (size_t i = 0; i < applicationDescriptionArraySize; i++) { UA_ApplicationDescription *desc = &applicationDescriptionArray[i]; if (desc->applicationType == UA_APPLICATIONTYPE_DISCOVERYSERVER) continue; if (desc->discoveryUrlsSize == 0) { UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "[ReadTime] Server %.*s has no discovery URLs. " "Skipping.", (int)desc->applicationUri.length, desc->applicationUri.data); continue; } /* UA_String is not null-terminated; build a C string for the API. */ char *url = (char *)UA_malloc (desc->discoveryUrls[0].length + 1); memcpy (url, desc->discoveryUrls[0].data, desc->discoveryUrls[0].length); url[desc->discoveryUrls[0].length] = '\0'; UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "Connecting to %s to read current time...", url); UA_StatusCode retval; if (username) retval = UA_Client_connectUsername (client, url, username, password); else retval = UA_Client_connect (client, url); UA_free (url); if (retval != UA_STATUSCODE_GOOD) { UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "Could not connect: %s", UA_StatusCode_name (retval)); continue; } UA_Variant value; UA_Variant_init (&value); const UA_NodeId nodeId = UA_NS0ID (SERVER_SERVERSTATUS_CURRENTTIME); retval = UA_Client_readValueAttribute (client, nodeId, &value); 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)); } UA_Variant_clear (&value); UA_Client_disconnect (client); } } /* ======================================================================== * Main * ======================================================================== */ int main (int argc, char **argv) { if (argc < 2 || argc > 3) { UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Usage: %s [log-level]", argv[0]); return EXIT_FAILURE; } const char *logLevelStr = (argc == 3) ? argv[2] : "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; /* ---- Shared keys ---- */ const char *discoveryServerEndpoint = configRequire (&cfg, "discoveryEndpoint", "ClientFindServers"); const char *applicationUri = configRequire (&cfg, "applicationUri", "ClientFindServers"); if (!discoveryServerEndpoint || !applicationUri) { configFree (&cfg); return EXIT_FAILURE; } /* ---- Discovery-side config (LDS connection) ---- */ const char *discCertPath = configRequire (&cfg, "discoveryCertificate", "ClientFindServers"); const char *discKeyPath = configRequire (&cfg, "discoveryPrivateKey", "ClientFindServers"); const char *discSecModeStr = configRequire (&cfg, "discoverySecurityMode", "ClientFindServers"); const char *discSecPolStr = configRequire (&cfg, "discoverySecurityPolicy", "ClientFindServers"); const char *discAuthMode = configRequire (&cfg, "discoveryAuthMode", "ClientFindServers"); if (!discCertPath || !discKeyPath || !discSecModeStr || !discSecPolStr || !discAuthMode) { configFree (&cfg); return EXIT_FAILURE; } UA_MessageSecurityMode discSecMode = parseSecurityMode (discSecModeStr); if (discSecMode == UA_MESSAGESECURITYMODE_INVALID) { UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Unknown discovery security mode: %s", discSecModeStr); configFree (&cfg); return EXIT_FAILURE; } const char *discSecPolUri = resolveSecurityPolicyUri (discSecPolStr); if (!discSecPolUri) { UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Unknown discovery security policy: %s", discSecPolStr); configFree (&cfg); return EXIT_FAILURE; } /* ---- Server-side config (connections to discovered servers) ---- */ const char *srvCertPath = configRequire (&cfg, "serverCertificate", "ClientFindServers"); const char *srvKeyPath = configRequire (&cfg, "serverPrivateKey", "ClientFindServers"); const char *srvSecModeStr = configRequire (&cfg, "serverSecurityMode", "ClientFindServers"); const char *srvSecPolStr = configRequire (&cfg, "serverSecurityPolicy", "ClientFindServers"); const char *srvAuthMode = configRequire (&cfg, "serverAuthMode", "ClientFindServers"); if (!srvCertPath || !srvKeyPath || !srvSecModeStr || !srvSecPolStr || !srvAuthMode) { configFree (&cfg); return EXIT_FAILURE; } UA_MessageSecurityMode srvSecMode = parseSecurityMode (srvSecModeStr); if (srvSecMode == UA_MESSAGESECURITYMODE_INVALID) { UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Unknown server security mode: %s", srvSecModeStr); configFree (&cfg); return EXIT_FAILURE; } const char *srvSecPolUri = resolveSecurityPolicyUri (srvSecPolStr); if (!srvSecPolUri) { UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Unknown server security policy: %s", srvSecPolStr); configFree (&cfg); return EXIT_FAILURE; } /* ---- Server-side auth ---- */ const char *srvUsername = NULL, *srvPassword = NULL; if (strcmp (srvAuthMode, "anonymous") == 0) { /* No credentials needed */ } else if (strcmp (srvAuthMode, "user") == 0) { srvUsername = configRequire (&cfg, "serverUsername", "ClientFindServers"); srvPassword = configRequire (&cfg, "serverPassword", "ClientFindServers"); if (!srvUsername || !srvPassword) { configFree (&cfg); return EXIT_FAILURE; } } else { UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, "Unknown server auth mode: %s " "(expected 'anonymous' or 'user')", srvAuthMode); configFree (&cfg); return EXIT_FAILURE; } /* ---- Trust lists ---- */ char **discTrustPaths = NULL; size_t discTrustSize = 0; configGetAll (&cfg, "discoveryTrustList", &discTrustPaths, &discTrustSize); char **srvTrustPaths = NULL; size_t srvTrustSize = 0; configGetAll (&cfg, "serverTrustList", &srvTrustPaths, &srvTrustSize); /* ---- Create discovery client (LDS) ---- */ UA_Client *discoveryClient = UA_Client_new (); UA_StatusCode retval = createSecureClientConfig ( UA_Client_getConfig (discoveryClient), applicationUri, discCertPath, discKeyPath, discTrustPaths, discTrustSize, discSecMode, discSecPolUri); if (retval != UA_STATUSCODE_GOOD) { UA_Client_delete (discoveryClient); free (discTrustPaths); free (srvTrustPaths); configFree (&cfg); return EXIT_FAILURE; } UA_Client_getConfig (discoveryClient)->logging->context = (void *)(uintptr_t)logLevel; /* ---- Create server client (discovered servers) ---- */ UA_Client *serverClient = UA_Client_new (); retval = createSecureClientConfig ( UA_Client_getConfig (serverClient), applicationUri, srvCertPath, srvKeyPath, srvTrustPaths, srvTrustSize, srvSecMode, srvSecPolUri); if (retval != UA_STATUSCODE_GOOD) { UA_Client_delete (discoveryClient); UA_Client_delete (serverClient); free (discTrustPaths); free (srvTrustPaths); configFree (&cfg); return EXIT_FAILURE; } UA_Client_getConfig (serverClient)->logging->context = (void *)(uintptr_t)logLevel; /* ---- Discovery calls (use discoveryClient) ---- */ UA_ApplicationDescription *applicationDescriptionArray = NULL; size_t applicationDescriptionArraySize = 0; retval = findServers (discoveryClient, discoveryServerEndpoint, &applicationDescriptionArraySize, &applicationDescriptionArray); if (retval != UA_STATUSCODE_GOOD) { UA_Client_delete (discoveryClient); UA_Client_delete (serverClient); free (discTrustPaths); free (srvTrustPaths); configFree (&cfg); return EXIT_FAILURE; } getServersEndpoints (discoveryClient, applicationDescriptionArray, applicationDescriptionArraySize); /* ---- Server calls (use serverClient) ---- */ readServerTime (serverClient, applicationDescriptionArray, applicationDescriptionArraySize, srvUsername, srvPassword); /* ---- Cleanup ---- */ UA_Client_delete (discoveryClient); UA_Client_delete (serverClient); UA_Array_delete (applicationDescriptionArray, applicationDescriptionArraySize, &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]); free (discTrustPaths); free (srvTrustPaths); configFree (&cfg); return EXIT_SUCCESS; }