diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 02:27:51 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-02-17 02:27:51 +0100 |
| commit | c35eb35bb63a97b7c46e879819757a9cb48165b5 (patch) | |
| tree | abc7f07740fae388f4ff6776585b56f56ec558c9 /src | |
| download | BobinkCOpcUa-c35eb35bb63a97b7c46e879819757a9cb48165b5.tar.gz BobinkCOpcUa-c35eb35bb63a97b7c46e879819757a9cb48165b5.zip | |
Initial commit: OPC UA discovery project
CMake-based C project using open62541 for OPC UA discovery.
Includes Local Discovery Server, register server, and find
servers client with OpenSSL encryption support.
Diffstat (limited to 'src')
| -rw-r--r-- | src/client_find_servers.c | 281 | ||||
| -rw-r--r-- | src/common.c | 280 | ||||
| -rw-r--r-- | src/common.h | 115 | ||||
| -rw-r--r-- | src/server_lds.c | 81 | ||||
| -rw-r--r-- | src/server_register.c | 170 |
5 files changed, 927 insertions, 0 deletions
diff --git a/src/client_find_servers.c b/src/client_find_servers.c new file mode 100644 index 0000000..c62fc15 --- /dev/null +++ b/src/client_find_servers.c @@ -0,0 +1,281 @@ +/** + * @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 <open62541/client_highlevel.h> +#include <open62541/plugin/log_stdout.h> + +#include <stdlib.h> +#include <string.h> + +/* ======================================================================== + * 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. + */ +static void +readServerTime (UA_Client *client, + UA_ApplicationDescription *applicationDescriptionArray, + size_t applicationDescriptionArraySize) +{ + 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 = 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 < 7) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Usage: %s <discovery-server-endpoint>\n" + " <applicationUri>\n" + " <certificate.der> <private-key.der>\n" + " <security-mode> <security-policy>\n" + " [<trustlist1.der>, ...]\n" + "\n" + "Security modes : None, Sign, SignAndEncrypt\n" + "Security policies: None, Basic256Sha256, " + "Aes256_Sha256_RsaPss,\n" + " Aes128_Sha256_RsaOaep, ECC_nistP256", + argv[0]); + return EXIT_FAILURE; + } + + const char *discoveryServerEndpoint = argv[1]; + const char *applicationUri = argv[2]; + const char *certPath = argv[3]; + const char *keyPath = argv[4]; + + UA_MessageSecurityMode securityMode = parseSecurityMode (argv[5]); + if (securityMode == UA_MESSAGESECURITYMODE_INVALID) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Unknown security mode: %s", argv[5]); + return EXIT_FAILURE; + } + + const char *securityPolicyUri = resolveSecurityPolicyUri (argv[6]); + if (!securityPolicyUri) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Unknown security policy: %s", argv[6]); + return EXIT_FAILURE; + } + + char **trustPaths = argv + 7; + size_t trustSize = (argc > 7) ? (size_t)argc - 7 : 0; + + UA_Client *client = UA_Client_new (); + UA_StatusCode retval = createSecureClientConfig ( + UA_Client_getConfig (client), applicationUri, certPath, keyPath, + trustPaths, trustSize, securityMode, securityPolicyUri); + if (retval != UA_STATUSCODE_GOOD) + { + UA_Client_delete (client); + return EXIT_FAILURE; + } + + UA_ApplicationDescription *applicationDescriptionArray = NULL; + size_t applicationDescriptionArraySize = 0; + + retval = findServers (client, discoveryServerEndpoint, + &applicationDescriptionArraySize, + &applicationDescriptionArray); + if (retval != UA_STATUSCODE_GOOD) + { + UA_Client_delete (client); + return EXIT_FAILURE; + } + + getServersEndpoints (client, applicationDescriptionArray, + applicationDescriptionArraySize); + + readServerTime (client, applicationDescriptionArray, + applicationDescriptionArraySize); + + UA_Client_delete (client); + UA_Array_delete (applicationDescriptionArray, + applicationDescriptionArraySize, + &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]); + + return EXIT_SUCCESS; +} diff --git a/src/common.c b/src/common.c new file mode 100644 index 0000000..d102868 --- /dev/null +++ b/src/common.c @@ -0,0 +1,280 @@ +/** + * @file common.c + * @brief Implements shared helpers declared in common.h. + */ + +#include "common.h" + +#include <open62541/client_config_default.h> +#include <open62541/plugin/log_stdout.h> +#include <open62541/server_config_default.h> + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +/* ======================================================================== + * File Loading + * ======================================================================== */ + +UA_ByteString +loadFile (const char *const path) +{ + UA_ByteString fileContents = UA_STRING_NULL; + + FILE *fp = fopen (path, "rb"); + if (!fp) + { + /* fopen sets errno on failure. Callers like createSecureServer use + loadFile for optional trustlist entries where a missing file is not + an error. Clear errno so open62541's logging does not pick up + a stale value and emit misleading error messages. */ + errno = 0; + return fileContents; + } + + fseek (fp, 0, SEEK_END); + fileContents.length = (size_t)ftell (fp); + fileContents.data + = (UA_Byte *)UA_malloc (fileContents.length * sizeof (UA_Byte)); + if (fileContents.data) + { + fseek (fp, 0, SEEK_SET); + size_t read = fread (fileContents.data, sizeof (UA_Byte), + fileContents.length, fp); + if (read != fileContents.length) + UA_ByteString_clear (&fileContents); + } + else + { + fileContents.length = 0; + } + fclose (fp); + + return fileContents; +} + +/* ======================================================================== + * Security Helpers + * ======================================================================== */ + +UA_MessageSecurityMode +parseSecurityMode (const char *name) +{ + if (strcmp (name, "None") == 0) + return UA_MESSAGESECURITYMODE_NONE; + if (strcmp (name, "Sign") == 0) + return UA_MESSAGESECURITYMODE_SIGN; + if (strcmp (name, "SignAndEncrypt") == 0) + return UA_MESSAGESECURITYMODE_SIGNANDENCRYPT; + return UA_MESSAGESECURITYMODE_INVALID; +} + +const char * +resolveSecurityPolicyUri (const char *shortName) +{ + static const struct + { + const char *name; + const char *uri; + } policies[] = { + { "None", "http://opcfoundation.org/UA/SecurityPolicy#None" }, + { "Basic256Sha256", + "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256" }, + { "Aes256_Sha256_RsaPss", + "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss" }, + { "Aes128_Sha256_RsaOaep", + "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep" }, + { "ECC_nistP256", + "http://opcfoundation.org/UA/SecurityPolicy#ECC_nistP256" }, + }; + for (size_t i = 0; i < sizeof (policies) / sizeof (policies[0]); i++) + { + if (strcmp (shortName, policies[i].name) == 0) + return policies[i].uri; + } + return NULL; +} + +/* ======================================================================== + * Output Formatting + * ======================================================================== */ + +void +printApplicationDescription (const UA_ApplicationDescription *description, + size_t index) +{ + const char *type = "Unknown"; + switch (description->applicationType) + { + case UA_APPLICATIONTYPE_SERVER: + type = "Server"; + break; + case UA_APPLICATIONTYPE_CLIENT: + type = "Client"; + break; + case UA_APPLICATIONTYPE_CLIENTANDSERVER: + type = "Client and Server"; + break; + case UA_APPLICATIONTYPE_DISCOVERYSERVER: + type = "Discovery Server"; + break; + default: + break; + } + + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, "Server[%lu]: %.*s", + (unsigned long)index, (int)description->applicationUri.length, + description->applicationUri.data); + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, " Name: %.*s", + (int)description->applicationName.text.length, + description->applicationName.text.data); + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, + " Application URI: %.*s", + (int)description->applicationUri.length, + description->applicationUri.data); + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, + " Product URI: %.*s", (int)description->productUri.length, + description->productUri.data); + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, " Type: %s", type); + for (size_t j = 0; j < description->discoveryUrlsSize; j++) + { + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, + " Discovery URL[%lu]: %.*s", (unsigned long)j, + (int)description->discoveryUrls[j].length, + description->discoveryUrls[j].data); + } +} + +void +printEndpoint (const UA_EndpointDescription *endpoint, size_t index) +{ + const char *mode = "Unknown"; + switch (endpoint->securityMode) + { + case UA_MESSAGESECURITYMODE_NONE: + mode = "None"; + break; + case UA_MESSAGESECURITYMODE_SIGN: + mode = "Sign"; + break; + case UA_MESSAGESECURITYMODE_SIGNANDENCRYPT: + mode = "SignAndEncrypt"; + break; + default: + break; + } + + /* Extract policy name after the '#' */ + const char *policy = (const char *)endpoint->securityPolicyUri.data; + size_t policyLen = endpoint->securityPolicyUri.length; + for (size_t k = 0; k < endpoint->securityPolicyUri.length; k++) + { + if (endpoint->securityPolicyUri.data[k] == '#') + { + policy = (const char *)&endpoint->securityPolicyUri.data[k + 1]; + policyLen = endpoint->securityPolicyUri.length - k - 1; + break; + } + } + + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, + " [%4lu] %.*s | Level: %2d | %-14s | %.*s", + (unsigned long)index, (int)endpoint->endpointUrl.length, + endpoint->endpointUrl.data, endpoint->securityLevel, mode, + (int)policyLen, policy); +} + +/* ======================================================================== + * Factory Functions + * ======================================================================== */ + +UA_Server * +createSecureServer (UA_UInt16 port, const char *applicationUri, + const char *certPath, const char *keyPath, + char **trustPaths, size_t trustSize, UA_StatusCode *retval) +{ + UA_ByteString certificate = loadFile (certPath); + UA_ByteString privateKey = loadFile (keyPath); + + /* +1: UA_STACKARRAY requires a strictly positive size for VLA. */ + UA_STACKARRAY (UA_ByteString, trustList, trustSize + 1); + for (size_t i = 0; i < trustSize; i++) + trustList[i] = loadFile (trustPaths[i]); + + /* Issuer and revocation lists are unused in this demo. */ + size_t issuerListSize = 0; + UA_ByteString *issuerList = NULL; + UA_ByteString *revocationList = NULL; + size_t revocationListSize = 0; + + UA_Server *server = UA_Server_new (); + UA_ServerConfig *config = UA_Server_getConfig (server); + + *retval = UA_ServerConfig_setDefaultWithSecurityPolicies ( + config, port, &certificate, &privateKey, trustList, trustSize, + issuerList, issuerListSize, revocationList, revocationListSize); + + UA_ByteString_clear (&certificate); + UA_ByteString_clear (&privateKey); + for (size_t i = 0; i < trustSize; i++) + UA_ByteString_clear (&trustList[i]); + + if (*retval != UA_STATUSCODE_GOOD) + { + UA_Server_delete (server); + return NULL; + } + + UA_String_clear (&config->applicationDescription.applicationUri); + config->applicationDescription.applicationUri + = UA_String_fromChars (applicationUri); + + return server; +} + +UA_StatusCode +createSecureClientConfig (UA_ClientConfig *cc, const char *applicationUri, + const char *certPath, const char *keyPath, + char **trustPaths, size_t trustSize, + UA_MessageSecurityMode securityMode, + const char *securityPolicyUri) +{ + UA_ByteString certificate = loadFile (certPath); + UA_ByteString privateKey = loadFile (keyPath); + + /* +1: UA_STACKARRAY requires a strictly positive size for VLA. */ + UA_STACKARRAY (UA_ByteString, trustList, trustSize + 1); + for (size_t i = 0; i < trustSize; i++) + trustList[i] = loadFile (trustPaths[i]); + + /* Revocation list is unused in this demo. */ + UA_ByteString *revocationList = NULL; + size_t revocationListSize = 0; + + UA_StatusCode retval = UA_ClientConfig_setDefaultEncryption ( + cc, certificate, privateKey, trustList, trustSize, revocationList, + revocationListSize); + + UA_ByteString_clear (&certificate); + UA_ByteString_clear (&privateKey); + for (size_t i = 0; i < trustSize; i++) + UA_ByteString_clear (&trustList[i]); + + if (retval != UA_STATUSCODE_GOOD) + { + UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_APPLICATION, + "Failed to set client encryption. StatusCode %s", + UA_StatusCode_name (retval)); + return retval; + } + + UA_String_clear (&cc->clientDescription.applicationUri); + cc->clientDescription.applicationUri = UA_String_fromChars (applicationUri); + + cc->securityMode = securityMode; + cc->securityPolicyUri = UA_String_fromChars (securityPolicyUri); + + return retval; +} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..e3d2f4c --- /dev/null +++ b/src/common.h @@ -0,0 +1,115 @@ +#ifndef DISCOVERY_COMMON_H +#define DISCOVERY_COMMON_H + +/** + * @file common.h + * @brief Shared helpers for the OPC UA discovery demo programs. + * + * Provides file-loading, factory, and output formatting functions used by + * the LDS, the registering server, and the FindServers client. + */ + +#include <open62541/client.h> +#include <open62541/server.h> +#include <open62541/types.h> + +/** + * @brief Loads a DER-encoded certificate or key file into a UA_ByteString. + * + * @param path File path to read. + * @return The file contents, or UA_BYTESTRING_NULL on error. + */ +UA_ByteString loadFile (const char *const path); + +/** + * @brief Creates a UA_Server configured with security policies and encryption. + * + * The server is initialized with the specified port, certificate, private key, + * and trustlist. The applicationUri is set in the server's application + * description. + * + * @param port Server port number. + * @param applicationUri OPC UA application URI. + * @param certPath Path to server certificate (.der). + * @param keyPath Path to private key (.der). + * @param trustPaths Array of trustlist file paths (may be NULL if trustSize is + * 0). + * @param trustSize Number of entries in trustPaths. + * @param retval Output parameter set to the status code on failure. + * @return A configured UA_Server, or NULL on error. + */ +UA_Server *createSecureServer (UA_UInt16 port, const char *applicationUri, + const char *certPath, const char *keyPath, + char **trustPaths, size_t trustSize, + UA_StatusCode *retval); + +/** + * @brief Parses a security mode name into the corresponding enum value. + * + * Accepted names: "None", "Sign", "SignAndEncrypt". + * + * @param name Mode name string. + * @return The matching UA_MessageSecurityMode, or + * UA_MESSAGESECURITYMODE_INVALID if the name is not recognized. + */ +UA_MessageSecurityMode parseSecurityMode (const char *name); + +/** + * @brief Maps a short security policy name to its full OPC UA URI. + * + * Accepted names: "None", "Basic256Sha256", "Aes256_Sha256_RsaPss", + * "Aes128_Sha256_RsaOaep", "ECC_nistP256". + * + * @param shortName Short policy name. + * @return The full URI string, or NULL if the name is not recognized. + */ +const char *resolveSecurityPolicyUri (const char *shortName); + +/** + * @brief Initializes a UA_ClientConfig with encryption from file paths. + * + * The config must be zero-initialized by the caller before calling this + * function. Loads the certificate, private key, and trustlist, then applies + * default encryption settings. + * + * @param cc Pointer to a zero-initialized UA_ClientConfig. + * @param applicationUri OPC UA application URI. + * @param certPath Path to client certificate (.der). + * @param keyPath Path to private key (.der). + * @param trustPaths Array of trustlist file paths (may be NULL if trustSize is + * 0). + * @param trustSize Number of entries in trustPaths. + * @param securityMode Requested message security mode. + * @param securityPolicyUri Security policy URI string (e.g. + * "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256"). + * @return UA_STATUSCODE_GOOD on success, error code otherwise. + */ +UA_StatusCode createSecureClientConfig ( + UA_ClientConfig *cc, const char *applicationUri, const char *certPath, + const char *keyPath, char **trustPaths, size_t trustSize, + UA_MessageSecurityMode securityMode, const char *securityPolicyUri); + +/** + * @brief Logs a UA_ApplicationDescription (server info from FindServers). + * + * Outputs the application URI, name, product URI, type, and discovery URLs + * via UA_LOG_INFO. + * + * @param description The application description to print. + * @param index Display index (e.g. position in the FindServers result array). + */ +void printApplicationDescription (const UA_ApplicationDescription *description, + size_t index); + +/** + * @brief Logs a UA_EndpointDescription in a compact one-line format. + * + * Outputs the endpoint URL, security level, security mode, and the short + * policy name (the part after '#') via UA_LOG_INFO. + * + * @param endpoint The endpoint description to print. + * @param index Display index (e.g. position in the GetEndpoints result array). + */ +void printEndpoint (const UA_EndpointDescription *endpoint, size_t index); + +#endif /* DISCOVERY_COMMON_H */ diff --git a/src/server_lds.c b/src/server_lds.c new file mode 100644 index 0000000..a7794aa --- /dev/null +++ b/src/server_lds.c @@ -0,0 +1,81 @@ +/** + * @file server_lds.c + * @brief Local Discovery Server implementation. + * + * This program runs an OPC UA Local Discovery Server (LDS) configured with + * encryption and a configurable cleanup timeout. Other OPC UA servers register + * with this LDS using the RegisterServer2 service. Clients can query this LDS + * using the FindServers service to discover registered servers. + */ + +#include "common.h" + +#include <open62541/plugin/log_stdout.h> +#include <open62541/server.h> +#include <open62541/server_config_default.h> + +#include <signal.h> +#include <stdlib.h> + +UA_Boolean running = true; + +static void +stopHandler (int sig) +{ + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c"); + running = false; +} + +int +main (int argc, char *argv[]) +{ + signal (SIGINT, stopHandler); + signal (SIGTERM, stopHandler); + + if (argc < 6) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Usage: %s\n" + " <port> <applicationUri>\n" + " <server-certificate.der> <private-key.der>\n" + " <cleanup-timeout-seconds>\n" + " [<trustlist1.der>, ...]", + argv[0]); + return EXIT_FAILURE; + } + + UA_UInt16 port = (UA_UInt16)atoi (argv[1]); + int cleanupTimeout = atoi (argv[5]); + + /* The OPC UA specification requires the cleanup timeout to exceed the + register-server interval. open62541 enforces a floor of 10 seconds. */ + if (cleanupTimeout <= 10) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Cleanup timeout must be > 10 seconds (got %d)", + cleanupTimeout); + return EXIT_FAILURE; + } + size_t trustSize = (argc > 6) ? (size_t)argc - 6 : 0; + + UA_StatusCode retval; + UA_Server *server = createSecureServer (port, argv[2], argv[3], argv[4], + argv + 6, trustSize, &retval); + if (!server) + return EXIT_FAILURE; + + UA_ServerConfig *serverConfig = UA_Server_getConfig (server); + + /* Mark this server as a Discovery Server so clients can identify it. */ + serverConfig->applicationDescription.applicationType + = UA_APPLICATIONTYPE_DISCOVERYSERVER; + + /* Time (seconds) after which stale registrations are removed. Must + exceed the registering server's re-register interval. */ + serverConfig->discoveryCleanupTimeout = cleanupTimeout; + + retval = UA_Server_run (server, &running); + + UA_Server_delete (server); + return retval == UA_STATUSCODE_GOOD ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/server_register.c b/src/server_register.c new file mode 100644 index 0000000..e1defd0 --- /dev/null +++ b/src/server_register.c @@ -0,0 +1,170 @@ +/** + * @file server_register.c + * @brief OPC UA Server that registers with a Local Discovery Server. + * + * This program runs an OPC UA server configured with security and periodically + * registers itself with a remote LDS using the RegisterServer2 service. It + * uses separate certificate pairs for the server and for the client connection + * to the LDS. On shutdown, it deregisters from the LDS. + */ + +#include "common.h" + +#include <open62541/client.h> +#include <open62541/client_config_default.h> +#include <open62541/plugin/log_stdout.h> +#include <open62541/server.h> +#include <open62541/server_config_default.h> + +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +UA_Boolean running = true; + +static void +stopHandler (int sign) +{ + UA_LOG_INFO (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, "received ctrl-c"); + running = false; +} + +/* ======================================================================== + * Main + * ======================================================================== */ + +int +main (int argc, char **argv) +{ + signal (SIGINT, stopHandler); + signal (SIGTERM, stopHandler); + + if (argc < 11) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Usage: %s\n" + " <port> <applicationUri>\n" + " <server-certificate.der> <server-private-key.der>\n" + " <client-certificate.der> <client-private-key.der>\n" + " <discovery-server-endpoint>\n" + " <register-interval-seconds>\n" + " <security-mode> <security-policy>\n" + " [<trustlist1.der>, ...]\n" + "\n" + "Security modes : None, Sign, SignAndEncrypt\n" + "Security policies: None, Basic256Sha256, " + "Aes256_Sha256_RsaPss,\n" + " Aes128_Sha256_RsaOaep, ECC_nistP256", + argv[0]); + return EXIT_FAILURE; + } + + UA_UInt16 port = (UA_UInt16)atoi (argv[1]); + const char *applicationUri = argv[2]; + const char *clientCertPath = argv[5]; + const char *clientKeyPath = argv[6]; + const char *discoveryEndpoint = argv[7]; + int registerInterval = atoi (argv[8]); + + UA_MessageSecurityMode securityMode = parseSecurityMode (argv[9]); + if (securityMode == UA_MESSAGESECURITYMODE_INVALID) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Unknown security mode: %s", argv[9]); + return EXIT_FAILURE; + } + + const char *securityPolicyUri = resolveSecurityPolicyUri (argv[10]); + if (!securityPolicyUri) + { + UA_LOG_FATAL (UA_Log_Stdout, UA_LOGCATEGORY_USERLAND, + "Unknown security policy: %s", argv[10]); + return EXIT_FAILURE; + } + + size_t trustSize = (argc > 11) ? (size_t)argc - 11 : 0; + + UA_StatusCode retval; + UA_Server *server = createSecureServer ( + port, applicationUri, argv[3], argv[4], argv + 11, trustSize, &retval); + if (!server) + return EXIT_FAILURE; + + UA_ServerConfig *serverConfig = UA_Server_getConfig (server); + + serverConfig->applicationDescription.applicationType + = UA_APPLICATIONTYPE_SERVER; + + UA_Server_run_startup (server); + + /* UA_Server_registerDiscovery consumes (clears) the client config, + so a fresh zero-initialized config is needed for every call. */ + UA_ClientConfig clientConfig; + memset (&clientConfig, 0, sizeof (UA_ClientConfig)); + retval = createSecureClientConfig ( + &clientConfig, applicationUri, clientCertPath, clientKeyPath, argv + 11, + trustSize, securityMode, securityPolicyUri); + if (retval != UA_STATUSCODE_GOOD) + { + UA_Server_run_shutdown (server); + UA_Server_delete (server); + return EXIT_FAILURE; + } + + UA_String discoveryUrl = UA_STRING_ALLOC (discoveryEndpoint); + retval = UA_Server_registerDiscovery (server, &clientConfig, discoveryUrl, + UA_STRING_NULL); + UA_String_clear (&discoveryUrl); + if (retval != UA_STATUSCODE_GOOD) + UA_LOG_WARNING (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, + "Initial register failed: %s", + UA_StatusCode_name (retval)); + + time_t lastRegister = time (NULL); + + while (running) + { + UA_Server_run_iterate (server, true); + + time_t now = time (NULL); + if (now - lastRegister >= registerInterval) + { + memset (&clientConfig, 0, sizeof (UA_ClientConfig)); + retval = createSecureClientConfig ( + &clientConfig, applicationUri, clientCertPath, clientKeyPath, + argv + 11, trustSize, securityMode, securityPolicyUri); + if (retval == UA_STATUSCODE_GOOD) + { + UA_String reregUrl = UA_STRING_ALLOC (discoveryEndpoint); + retval = UA_Server_registerDiscovery (server, &clientConfig, + reregUrl, UA_STRING_NULL); + UA_String_clear (&reregUrl); + if (retval != UA_STATUSCODE_GOOD) + UA_LOG_WARNING (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, + "Re-register failed: %s", + UA_StatusCode_name (retval)); + } + lastRegister = now; + } + } + + memset (&clientConfig, 0, sizeof (UA_ClientConfig)); + retval = createSecureClientConfig ( + &clientConfig, applicationUri, clientCertPath, clientKeyPath, argv + 11, + trustSize, securityMode, securityPolicyUri); + if (retval == UA_STATUSCODE_GOOD) + { + UA_String deregUrl = UA_STRING_ALLOC (discoveryEndpoint); + retval = UA_Server_deregisterDiscovery (server, &clientConfig, deregUrl); + UA_String_clear (&deregUrl); + if (retval != UA_STATUSCODE_GOOD) + UA_LOG_ERROR (UA_Log_Stdout, UA_LOGCATEGORY_SERVER, + "Could not unregister from discovery server: %s", + UA_StatusCode_name (retval)); + } + + UA_Server_run_shutdown (server); + UA_Server_delete (server); + return EXIT_SUCCESS; +} |
