/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * Copyright 2015-2020 (c) Fraunhofer IOSB (Author: Julius Pfrommer) * Copyright 2015-2016 (c) Sten GrĂ¼ner * Copyright 2015-2016 (c) Chris Iatrou * Copyright 2015 (c) hfaham * Copyright 2015-2017 (c) Florian Palm * Copyright 2017-2018 (c) Thomas Stalder, Blue Time Concept SA * Copyright 2015 (c) Holger Jeromin * Copyright 2015 (c) Oleksiy Vasylyev * Copyright 2016 (c) TorbenD * Copyright 2017 (c) Stefan Profanter, fortiss GmbH * Copyright 2016 (c) Lykurg * Copyright 2017 (c) Mark Giraud, Fraunhofer IOSB * Copyright 2018 (c) Kalycito Infotech Private Limited * Copyright 2020 (c) Christian von Arnim, ISW University of Stuttgart * Copyright 2021 (c) Fraunhofer IOSB (Author: Jan Hermes) * Copyright 2022 (c) Linutronix GmbH (Author: Muddasir Shakil) */ #include #include "ua_client_internal.h" #include "../ua_types_encoding_binary.h" static void clientHouseKeeping(UA_Client *client, void *_); /********************/ /* Client Lifecycle */ /********************/ UA_StatusCode UA_ClientConfig_copy(UA_ClientConfig const *src, UA_ClientConfig *dst){ UA_StatusCode retval = UA_STATUSCODE_GOOD; retval = UA_ApplicationDescription_copy(&src->clientDescription, &dst->clientDescription); if(retval != UA_STATUSCODE_GOOD) goto cleanup; retval = UA_ExtensionObject_copy(&src->userIdentityToken, &dst->userIdentityToken); if(retval != UA_STATUSCODE_GOOD) goto cleanup; retval = UA_String_copy(&src->securityPolicyUri, &dst->securityPolicyUri); if(retval != UA_STATUSCODE_GOOD) goto cleanup; retval = UA_EndpointDescription_copy(&src->endpoint, &dst->endpoint); if(retval != UA_STATUSCODE_GOOD) goto cleanup; retval = UA_UserTokenPolicy_copy(&src->userTokenPolicy, &dst->userTokenPolicy); if(retval != UA_STATUSCODE_GOOD) goto cleanup; retval = UA_Array_copy(src->sessionLocaleIds, src->sessionLocaleIdsSize, (void **)&dst->sessionLocaleIds, &UA_TYPES[UA_TYPES_LOCALEID]); if(retval != UA_STATUSCODE_GOOD) goto cleanup; dst->sessionLocaleIdsSize = src->sessionLocaleIdsSize; dst->connectivityCheckInterval = src->connectivityCheckInterval; dst->certificateVerification = src->certificateVerification; dst->clientContext = src->clientContext; dst->customDataTypes = src->customDataTypes; dst->eventLoop = src->eventLoop; dst->externalEventLoop = src->externalEventLoop; dst->inactivityCallback = src->inactivityCallback; dst->localConnectionConfig = src->localConnectionConfig; dst->logging = src->logging; if(src->certificateVerification.logging == NULL) dst->certificateVerification.logging = dst->logging; #ifdef UA_ENABLE_SUBSCRIPTIONS dst->outStandingPublishRequests = src->outStandingPublishRequests; #endif dst->requestedSessionTimeout = src->requestedSessionTimeout; dst->secureChannelLifeTime = src->secureChannelLifeTime; dst->securityMode = src->securityMode; dst->stateCallback = src->stateCallback; #ifdef UA_ENABLE_SUBSCRIPTIONS dst->subscriptionInactivityCallback = src->subscriptionInactivityCallback; #endif dst->timeout = src->timeout; dst->userTokenPolicy = src->userTokenPolicy; dst->securityPolicies = src->securityPolicies; dst->securityPoliciesSize = src->securityPoliciesSize; dst->authSecurityPolicies = src->authSecurityPolicies; dst->authSecurityPoliciesSize = src->authSecurityPoliciesSize; cleanup: if(retval != UA_STATUSCODE_GOOD) { /* _clear will free the plugins in dst that are a shallow copy from src. */ dst->authSecurityPolicies = NULL; dst->certificateVerification.context = NULL; dst->eventLoop = NULL; dst->logging = NULL; dst->securityPolicies = NULL; UA_ClientConfig_clear(dst); } return retval; } UA_Client * UA_Client_newWithConfig(const UA_ClientConfig *config) { if(!config) return NULL; UA_Client *client = (UA_Client*)UA_malloc(sizeof(UA_Client)); if(!client) return NULL; memset(client, 0, sizeof(UA_Client)); client->config = *config; UA_SecureChannel_init(&client->channel); client->channel.config = client->config.localConnectionConfig; client->connectStatus = UA_STATUSCODE_GOOD; #if UA_MULTITHREADING >= 100 UA_LOCK_INIT(&client->clientMutex); #endif /* Initialize the namespace mapping */ size_t initialNs = 2 + config->namespacesSize; client->namespaces = (UA_String*)UA_calloc(initialNs, sizeof(UA_String)); if(!client->namespaces) goto error; client->namespacesSize = initialNs; client->namespaces[0] = UA_STRING_ALLOC("http://opcfoundation.org/UA/"); client->namespaces[1] = UA_STRING_NULL; /* Gets set when we connect to the server */ UA_StatusCode res = UA_STATUSCODE_GOOD; for(size_t i = 0; i < config->namespacesSize; i++) { res |= UA_String_copy(&client->namespaces[i+2], &config->namespaces[i]); } if(res != UA_STATUSCODE_GOOD) goto error; return client; error: memset(&client->config, 0, sizeof(UA_ClientConfig)); UA_Client_delete(client); return NULL; } void UA_ClientConfig_clear(UA_ClientConfig *config) { UA_ApplicationDescription_clear(&config->clientDescription); UA_String_clear(&config->endpointUrl); UA_ExtensionObject_clear(&config->userIdentityToken); /* Delete the SecurityPolicies for Authentication */ if(config->authSecurityPolicies != 0) { for(size_t i = 0; i < config->authSecurityPoliciesSize; i++) config->authSecurityPolicies[i].clear(&config->authSecurityPolicies[i]); UA_free(config->authSecurityPolicies); config->authSecurityPolicies = 0; } UA_String_clear(&config->securityPolicyUri); UA_String_clear(&config->authSecurityPolicyUri); UA_EndpointDescription_clear(&config->endpoint); UA_UserTokenPolicy_clear(&config->userTokenPolicy); UA_String_clear(&config->applicationUri); if(config->certificateVerification.clear) config->certificateVerification.clear(&config->certificateVerification); /* Delete the SecurityPolicies */ if(config->securityPolicies != 0) { for(size_t i = 0; i < config->securityPoliciesSize; i++) config->securityPolicies[i].clear(&config->securityPolicies[i]); UA_free(config->securityPolicies); config->securityPolicies = 0; } /* Stop and delete the EventLoop */ UA_EventLoop *el = config->eventLoop; if(el && !config->externalEventLoop) { if(el->state != UA_EVENTLOOPSTATE_FRESH && el->state != UA_EVENTLOOPSTATE_STOPPED) { el->stop(el); while(el->state != UA_EVENTLOOPSTATE_STOPPED) { el->run(el, 100); } } el->free(el); config->eventLoop = NULL; } /* Logging */ if(config->logging != NULL && config->logging->clear != NULL) config->logging->clear(config->logging); config->logging = NULL; UA_String_clear(&config->sessionName); if(config->sessionLocaleIdsSize > 0 && config->sessionLocaleIds) { UA_Array_delete(config->sessionLocaleIds, config->sessionLocaleIdsSize, &UA_TYPES[UA_TYPES_LOCALEID]); } config->sessionLocaleIds = NULL; config->sessionLocaleIdsSize = 0; /* Custom Data Types */ UA_cleanupDataTypeWithCustom(config->customDataTypes); #ifdef UA_ENABLE_ENCRYPTION config->privateKeyPasswordCallback = NULL; #endif } void UA_ClientConfig_delete(UA_ClientConfig *config){ UA_assert(config != NULL); UA_ClientConfig_clear(config); UA_free(config); } static void UA_Client_clear(UA_Client *client) { /* Prevent new async service calls in UA_Client_AsyncService_removeAll */ UA_SessionState oldState = client->sessionState; client->sessionState = UA_SESSIONSTATE_CLOSING; /* Delete the async service calls with BADHSUTDOWN */ __Client_AsyncService_removeAll(client, UA_STATUSCODE_BADSHUTDOWN); /* Reset to the old state to properly close the session */ client->sessionState = oldState; UA_Client_disconnect(client); UA_String_clear(&client->discoveryUrl); UA_EndpointDescription_clear(&client->endpoint); UA_ByteString_clear(&client->serverSessionNonce); UA_ByteString_clear(&client->clientSessionNonce); /* Delete the subscriptions */ #ifdef UA_ENABLE_SUBSCRIPTIONS __Client_Subscriptions_clear(client); #endif /* Remove the internal regular callback */ UA_Client_removeCallback(client, client->houseKeepingCallbackId); client->houseKeepingCallbackId = 0; /* Clean up the SecureChannel */ UA_SecureChannel_clear(&client->channel); /* Free the namespace mapping */ UA_Array_delete(client->namespaces, client->namespacesSize, &UA_TYPES[UA_TYPES_STRING]); client->namespaces = NULL; client->namespacesSize = 0; #if UA_MULTITHREADING >= 100 UA_LOCK_DESTROY(&client->clientMutex); #endif } void UA_Client_delete(UA_Client* client) { UA_Client_disconnect(client); UA_Client_clear(client); UA_ClientConfig_clear(&client->config); UA_free(client); } void UA_Client_getState(UA_Client *client, UA_SecureChannelState *channelState, UA_SessionState *sessionState, UA_StatusCode *connectStatus) { UA_LOCK(&client->clientMutex); if(channelState) *channelState = client->channel.state; if(sessionState) *sessionState = client->sessionState; if(connectStatus) *connectStatus = client->connectStatus; UA_UNLOCK(&client->clientMutex); } UA_ClientConfig * UA_Client_getConfig(UA_Client *client) { if(!client) return NULL; return &client->config; } #if UA_LOGLEVEL <= 300 static const char *channelStateTexts[14] = { "Fresh", "ReverseListening", "Connecting", "Connected", "ReverseConnected", "RHESent", "HELSent", "HELReceived", "ACKSent", "AckReceived", "OPNSent", "Open", "Closing", "Closed"}; static const char *sessionStateTexts[6] = {"Closed", "CreateRequested", "Created", "ActivateRequested", "Activated", "Closing"}; #endif void notifyClientState(UA_Client *client) { UA_LOCK_ASSERT(&client->clientMutex); if(client->connectStatus == client->oldConnectStatus && client->channel.state == client->oldChannelState && client->sessionState == client->oldSessionState) return; #if UA_LOGLEVEL <= 300 UA_Boolean info = (client->connectStatus != UA_STATUSCODE_GOOD); if(client->oldChannelState != client->channel.state) info |= (client->channel.state == UA_SECURECHANNELSTATE_OPEN || client->channel.state == UA_SECURECHANNELSTATE_CLOSED); if(client->oldSessionState != client->sessionState) info |= (client->sessionState == UA_SESSIONSTATE_CREATED || client->sessionState == UA_SESSIONSTATE_ACTIVATED || client->sessionState == UA_SESSIONSTATE_CLOSED); const char *channelStateText = channelStateTexts[client->channel.state]; const char *sessionStateText = sessionStateTexts[client->sessionState]; const char *connectStatusText = UA_StatusCode_name(client->connectStatus); if(info) UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT, "Client Status: ChannelState: %s, SessionState: %s, ConnectStatus: %s", channelStateText, sessionStateText, connectStatusText); else UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT, "Client Status: ChannelState: %s, SessionState: %s, ConnectStatus: %s", channelStateText, sessionStateText, connectStatusText); #endif client->oldConnectStatus = client->connectStatus; client->oldChannelState = client->channel.state; client->oldSessionState = client->sessionState; UA_UNLOCK(&client->clientMutex); if(client->config.stateCallback) client->config.stateCallback(client, client->channel.state, client->sessionState, client->connectStatus); UA_LOCK(&client->clientMutex); } /****************/ /* Raw Services */ /****************/ /* For both synchronous and asynchronous service calls */ static UA_StatusCode sendRequest(UA_Client *client, const void *request, const UA_DataType *requestType, UA_UInt32 *requestId) { UA_LOCK_ASSERT(&client->clientMutex); /* Renew SecureChannel if necessary */ __Client_renewSecureChannel(client); if(client->connectStatus != UA_STATUSCODE_GOOD) return client->connectStatus; UA_EventLoop *el = client->config.eventLoop; /* Adjusting the request header. The const attribute is violated, but we * reset to the original state before returning. Use the AuthenticationToken * only once the session is active (or to activate / close it). */ UA_RequestHeader *rr = (UA_RequestHeader*)(uintptr_t)request; UA_NodeId oldToken = rr->authenticationToken; /* Put back in place later */ if(client->sessionState == UA_SESSIONSTATE_ACTIVATED || requestType == &UA_TYPES[UA_TYPES_ACTIVATESESSIONREQUEST] || requestType == &UA_TYPES[UA_TYPES_CLOSESESSIONREQUEST]) rr->authenticationToken = client->authenticationToken; rr->timestamp = el->dateTime_now(el); /* Create a unique handle >100,000 if not manually defined. The handle is * not necessarily unique when manually defined and used to cancel async * service requests. */ if(rr->requestHandle == 0) { if(UA_UNLIKELY(client->requestHandle < 100000)) client->requestHandle = 100000; rr->requestHandle = ++client->requestHandle; } /* Set the timeout hint if not manually defined */ if(rr->timeoutHint == 0) rr->timeoutHint = client->config.timeout; /* Generate the request id */ UA_UInt32 rqId = ++client->requestId; #ifdef UA_ENABLE_TYPEDESCRIPTION UA_LOG_DEBUG_CHANNEL(client->config.logging, &client->channel, "Sending request with RequestId %u of type %s", (unsigned)rqId, requestType->typeName); #else UA_LOG_DEBUG_CHANNEL(client->config.logging, &client->channel, "Sending request with RequestId %u of type %" PRIu32, (unsigned)rqId, requestType->binaryEncodingId.identifier.numeric); #endif /* Send the message */ UA_StatusCode retval = UA_SecureChannel_sendSymmetricMessage(&client->channel, rqId, UA_MESSAGETYPE_MSG, rr, requestType); rr->authenticationToken = oldToken; /* Set back to the original token */ /* Sending failed. The SecureChannel cannot recover from that. Call * closeSecureChannel to a) close from our end and b) set the session to * non-activated. */ if(retval != UA_STATUSCODE_GOOD) closeSecureChannel(client); /* Return the request id */ *requestId = rqId; return retval; } static const UA_NodeId serviceFaultId = {0, UA_NODEIDTYPE_NUMERIC, {UA_NS0ID_SERVICEFAULT_ENCODING_DEFAULTBINARY}}; /* Look for the async callback in the linked list, execute and delete it */ static UA_StatusCode processMSGResponse(UA_Client *client, UA_UInt32 requestId, const UA_ByteString *msg) { /* Find the callback */ AsyncServiceCall *ac; LIST_FOREACH(ac, &client->asyncServiceCalls, pointers) { if(ac->requestId == requestId) break; } /* Part 6, 6.7.6: After the security validation is complete the receiver * shall verify the RequestId and the SequenceNumber. If these checks fail a * Bad_SecurityChecksFailed error is reported. The RequestId only needs to * be verified by the Client since only the Client knows if it is valid or * not.*/ if(!ac) { UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT, "Request with unknown RequestId %u", requestId); return UA_STATUSCODE_BADSECURITYCHECKSFAILED; } UA_Response asyncResponse; UA_Response *response = (ac->syncResponse) ? ac->syncResponse : &asyncResponse; const UA_DataType *responseType = ac->responseType; /* Dequeue ac. We might disconnect the client (remove all ac) in the callback. */ LIST_REMOVE(ac, pointers); /* Decode the response type */ size_t offset = 0; UA_NodeId responseTypeId; UA_StatusCode retval = UA_NodeId_decodeBinary(msg, &offset, &responseTypeId); if(retval != UA_STATUSCODE_GOOD) goto process; /* Verify the type of the response */ if(!UA_NodeId_equal(&responseTypeId, &ac->responseType->binaryEncodingId)) { /* Initialize before switching the responseType to ServiceFault. * Otherwise the decoding will leave fields from the original response * type uninitialized. */ UA_init(response, ac->responseType); if(UA_NodeId_equal(&responseTypeId, &serviceFaultId)) { /* Decode as a ServiceFault, i.e. only the response header */ UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT, "Received a ServiceFault response"); responseType = &UA_TYPES[UA_TYPES_SERVICEFAULT]; } else { UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT, "Service response type does not match"); retval = UA_STATUSCODE_BADCOMMUNICATIONERROR; goto process; /* Do not decode */ } } /* Decode the response */ #ifdef UA_ENABLE_TYPEDESCRIPTION UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT, "Decode a message of type %s", responseType->typeName); #else UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT, "Decode a message of type %" PRIu32, responseTypeId.identifier.numeric); #endif UA_DecodeBinaryOptions opt; memset(&opt, 0, sizeof(UA_DecodeBinaryOptions)); opt.customTypes = client->config.customDataTypes; retval = UA_decodeBinaryInternal(msg, &offset, response, responseType, &opt); process: /* Process the received MSG response */ if(retval != UA_STATUSCODE_GOOD) { UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT, "Could not decode the response with RequestId %u with status %s", (unsigned)requestId, UA_StatusCode_name(retval)); response->responseHeader.serviceResult = retval; } /* The Session closed. The current response is processed with the return code. * The next request first recreates a session. */ if(responseType != &UA_TYPES[UA_TYPES_ACTIVATESESSIONRESPONSE] && (response->responseHeader.serviceResult == UA_STATUSCODE_BADSESSIONIDINVALID || response->responseHeader.serviceResult == UA_STATUSCODE_BADSESSIONCLOSED)) { /* Clean up the session information and reset the state */ cleanupSession(client); if(client->config.noNewSession) { /* Configuration option to not create a new Session. Disconnect the * client. */ client->connectStatus = response->responseHeader.serviceResult; UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT, "Session cannot be activated with StatusCode %s. " "The client is configured not to create a new Session.", UA_StatusCode_name(client->connectStatus)); closeSecureChannel(client); } else { UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT, "Session no longer valid. A new Session is created for the next " "Service request but we do not re-send the current request."); } } /* Call the async callback. This is the only thread with access to ac. So we * can just unlock for the callback into userland. */ UA_UNLOCK(&client->clientMutex); if(ac->callback) ac->callback(client, ac->userdata, requestId, response); UA_LOCK(&client->clientMutex); /* Clean up */ UA_NodeId_clear(&responseTypeId); if(!ac->syncResponse) { UA_clear(response, ac->responseType); UA_free(ac); } else { /* Return a special status code after processing a synchronous message. * This makes the client return control immediately. */ ac->syncResponse = NULL; /* Indicate that response was received */ if(retval == UA_STATUSCODE_GOOD) retval = UA_STATUSCODE_GOODCOMPLETESASYNCHRONOUSLY; } return retval; } UA_StatusCode processServiceResponse(UA_Client *client, UA_SecureChannel *channel, UA_MessageType messageType, UA_UInt32 requestId, UA_ByteString *message) { if(!UA_SecureChannel_isConnected(channel)) { if(messageType == UA_MESSAGETYPE_MSG) { UA_LOG_DEBUG_CHANNEL(client->config.logging, channel, "Discard MSG message " "with RequestId %u as the SecureChannel is not connected", requestId); } else { UA_LOG_DEBUG_CHANNEL(client->config.logging, channel, "Discard message " "as the SecureChannel is not connected"); } return UA_STATUSCODE_BADCONNECTIONCLOSED; } switch(messageType) { case UA_MESSAGETYPE_RHE: UA_LOG_DEBUG_CHANNEL(client->config.logging, channel, "Process RHE message"); processRHEMessage(client, message); return UA_STATUSCODE_GOOD; case UA_MESSAGETYPE_ACK: UA_LOG_DEBUG_CHANNEL(client->config.logging, channel, "Process ACK message"); processACKResponse(client, message); return UA_STATUSCODE_GOOD; case UA_MESSAGETYPE_OPN: UA_LOG_DEBUG_CHANNEL(client->config.logging, channel, "Process OPN message"); processOPNResponse(client, message); return UA_STATUSCODE_GOOD; case UA_MESSAGETYPE_ERR: UA_LOG_DEBUG_CHANNEL(client->config.logging, channel, "Process ERR message"); processERRResponse(client, message); return UA_STATUSCODE_GOOD; case UA_MESSAGETYPE_MSG: UA_LOG_DEBUG_CHANNEL(client->config.logging, channel, "Process MSG message " "with RequestId %u", requestId); return processMSGResponse(client, requestId, message); default: UA_LOG_TRACE_CHANNEL(client->config.logging, channel, "Invalid message type"); channel->state = UA_SECURECHANNELSTATE_CLOSING; return UA_STATUSCODE_BADTCPMESSAGETYPEINVALID; } } void __Client_Service(UA_Client *client, const void *request, const UA_DataType *requestType, void *response, const UA_DataType *responseType) { UA_ResponseHeader *respHeader = (UA_ResponseHeader*)response; /* Initialize. Response is valied in case of aborting. */ UA_init(response, responseType); /* Verify that the EventLoop is running */ UA_EventLoop *el = client->config.eventLoop; if(!el || el->state != UA_EVENTLOOPSTATE_STARTED) { respHeader->serviceResult = UA_STATUSCODE_BADINTERNALERROR; return; } /* Check that the SecureChannel is open and also a Session active (if we * want a Session). Otherwise reopen. */ if(!isFullyConnected(client)) { UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT, "Re-establish the connection for the synchronous service call"); connectSync(client); if(client->connectStatus != UA_STATUSCODE_GOOD) { respHeader->serviceResult = client->connectStatus; return; } } /* Store the channelId to detect if the channel was changed by a * reconnection within the EventLoop run method. */ UA_UInt32 channelId = client->channel.securityToken.channelId; /* Send the request */ UA_UInt32 requestId = 0; UA_StatusCode retval = sendRequest(client, request, requestType, &requestId); if(retval != UA_STATUSCODE_GOOD) { /* If sending failed, the status is set to closing. The SecureChannel is * the actually closed in the next iteration of the EventLoop. */ UA_assert(client->channel.state == UA_SECURECHANNELSTATE_CLOSING || client->channel.state == UA_SECURECHANNELSTATE_CLOSED); UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT, "Sending the request failed with status %s", UA_StatusCode_name(retval)); notifyClientState(client); respHeader->serviceResult = retval; return; } /* Temporarily insert an AsyncServiceCall */ const UA_RequestHeader *rh = (const UA_RequestHeader*)request; AsyncServiceCall ac; ac.callback = NULL; ac.userdata = NULL; ac.responseType = responseType; ac.syncResponse = (UA_Response*)response; ac.requestId = requestId; ac.start = el->dateTime_nowMonotonic(el); /* Start timeout after sending */ ac.timeout = rh->timeoutHint; ac.requestHandle = rh->requestHandle; if(ac.timeout == 0) ac.timeout = UA_UINT32_MAX; /* 0 -> unlimited */ LIST_INSERT_HEAD(&client->asyncServiceCalls, &ac, pointers); /* Time until which the request has to be answered */ UA_DateTime maxDate = ac.start + ((UA_DateTime)ac.timeout * UA_DATETIME_MSEC); /* Run the EventLoop until the request was processed, the request has timed * out or the client connection fails */ UA_UInt32 timeout_remaining = ac.timeout; while(true) { /* Unlock before dropping into the EventLoop. The client lock is * re-taken in the network callback if an event occurs. */ UA_UNLOCK(&client->clientMutex); retval = el->run(el, timeout_remaining); UA_LOCK(&client->clientMutex); /* Was the response received? In that case we can directly return. The * ac was already removed from the internal linked list. */ if(ac.syncResponse == NULL) return; /* Check the status. Do not try to resend if the connection breaks. * Leave this to the application-level user. For example, we do not want * to call a method twice is the connection broke after sending the * request. */ if(retval != UA_STATUSCODE_GOOD) break; /* The connection was lost */ retval = client->connectStatus; if(retval != UA_STATUSCODE_GOOD) break; /* The channel is no longer the same or was closed */ if(channelId != client->channel.securityToken.channelId) { retval = UA_STATUSCODE_BADSECURECHANNELCLOSED; break; } /* Update the remaining timeout or break */ UA_DateTime now = ac.start = el->dateTime_nowMonotonic(el); if(now > maxDate) { retval = UA_STATUSCODE_BADTIMEOUT; break; } timeout_remaining = (UA_UInt32)((maxDate - now) / UA_DATETIME_MSEC); } /* Detach from the internal async service list */ LIST_REMOVE(&ac, pointers); /* Return the status code */ respHeader->serviceResult = retval; } void __UA_Client_Service(UA_Client *client, const void *request, const UA_DataType *requestType, void *response, const UA_DataType *responseType) { UA_LOCK(&client->clientMutex); __Client_Service(client, request, requestType, response, responseType); UA_UNLOCK(&client->clientMutex); } /***********************************/ /* Handling of Async Service Calls */ /***********************************/ static void __Client_AsyncService_cancel(UA_Client *client, AsyncServiceCall *ac, UA_StatusCode statusCode) { /* Set the status for the synchronous service call. Don't free the ac. */ if(ac->syncResponse) { ac->syncResponse->responseHeader.serviceResult = statusCode; ac->syncResponse = NULL; /* Indicate the async service call was processed */ return; } if(ac->callback) { /* Create an empty response with the statuscode and call the callback */ UA_Response response; UA_init(&response, ac->responseType); response.responseHeader.serviceResult = statusCode; UA_UNLOCK(&client->clientMutex); ac->callback(client, ac->userdata, ac->requestId, &response); UA_LOCK(&client->clientMutex); /* Clean up the response. The user callback might move data into it. For * whatever reasons. */ UA_clear(&response, ac->responseType); } UA_free(ac); } void __Client_AsyncService_removeAll(UA_Client *client, UA_StatusCode statusCode) { /* Make this function reentrant. One of the async callbacks could indirectly * operate on the list. Moving all elements to a local list before iterating * that. */ UA_AsyncServiceList asyncServiceCalls = client->asyncServiceCalls; LIST_INIT(&client->asyncServiceCalls); if(asyncServiceCalls.lh_first) asyncServiceCalls.lh_first->pointers.le_prev = &asyncServiceCalls.lh_first; /* Cancel and remove the elements from the local list */ AsyncServiceCall *ac, *ac_tmp; LIST_FOREACH_SAFE(ac, &asyncServiceCalls, pointers, ac_tmp) { LIST_REMOVE(ac, pointers); __Client_AsyncService_cancel(client, ac, statusCode); } } UA_StatusCode UA_Client_modifyAsyncCallback(UA_Client *client, UA_UInt32 requestId, void *userdata, UA_ClientAsyncServiceCallback callback) { UA_LOCK(&client->clientMutex); AsyncServiceCall *ac; UA_StatusCode res = UA_STATUSCODE_BADNOTFOUND; LIST_FOREACH(ac, &client->asyncServiceCalls, pointers) { if(ac->requestId == requestId) { ac->callback = callback; ac->userdata = userdata; res = UA_STATUSCODE_GOOD; break; } } UA_UNLOCK(&client->clientMutex); return res; } UA_StatusCode __Client_AsyncService(UA_Client *client, const void *request, const UA_DataType *requestType, UA_ClientAsyncServiceCallback callback, const UA_DataType *responseType, void *userdata, UA_UInt32 *requestId) { UA_LOCK_ASSERT(&client->clientMutex); /* Is the SecureChannel connected? */ if(client->channel.state != UA_SECURECHANNELSTATE_OPEN) { UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT, "SecureChannel must be connected to send request"); return UA_STATUSCODE_BADSERVERNOTCONNECTED; } /* Prepare the entry for the linked list */ AsyncServiceCall *ac = (AsyncServiceCall*)UA_malloc(sizeof(AsyncServiceCall)); if(!ac) return UA_STATUSCODE_BADOUTOFMEMORY; /* Call the service and set the requestId */ UA_StatusCode retval = sendRequest(client, request, requestType, &ac->requestId); if(retval != UA_STATUSCODE_GOOD) { /* If sending failed, the status is set to closing. The SecureChannel is * the actually closed in the next iteration of the EventLoop. */ UA_assert(client->channel.state == UA_SECURECHANNELSTATE_CLOSING || client->channel.state == UA_SECURECHANNELSTATE_CLOSED); UA_free(ac); notifyClientState(client); return retval; } /* Set up the AsyncServiceCall for processing the response */ UA_EventLoop *el = client->config.eventLoop; const UA_RequestHeader *rh = (const UA_RequestHeader*)request; ac->callback = callback; ac->responseType = responseType; ac->userdata = userdata; ac->syncResponse = NULL; ac->start = el->dateTime_nowMonotonic(el); ac->timeout = rh->timeoutHint; ac->requestHandle = rh->requestHandle; if(ac->timeout == 0) ac->timeout = UA_UINT32_MAX; /* 0 -> unlimited */ LIST_INSERT_HEAD(&client->asyncServiceCalls, ac, pointers); /* Return the generated request id */ if(requestId) *requestId = ac->requestId; /* Notify the userland if a change happened */ notifyClientState(client); return UA_STATUSCODE_GOOD; } UA_StatusCode __UA_Client_AsyncService(UA_Client *client, const void *request, const UA_DataType *requestType, UA_ClientAsyncServiceCallback callback, const UA_DataType *responseType, void *userdata, UA_UInt32 *requestId) { UA_LOCK(&client->clientMutex); UA_StatusCode res = __Client_AsyncService(client, request, requestType, callback, responseType, userdata, requestId); UA_UNLOCK(&client->clientMutex); return res; } static UA_StatusCode cancelByRequestHandle(UA_Client *client, UA_UInt32 requestHandle, UA_UInt32 *cancelCount) { UA_CancelRequest creq; UA_CancelRequest_init(&creq); creq.requestHandle = requestHandle; UA_CancelResponse cresp; UA_CancelResponse_init(&cresp); __Client_Service(client, &creq, &UA_TYPES[UA_TYPES_CANCELREQUEST], &cresp, &UA_TYPES[UA_TYPES_CANCELRESPONSE]); if(cancelCount) *cancelCount = cresp.cancelCount; UA_StatusCode res = cresp.responseHeader.serviceResult; UA_CancelResponse_clear(&cresp); return res; } UA_StatusCode UA_Client_cancelByRequestHandle(UA_Client *client, UA_UInt32 requestHandle, UA_UInt32 *cancelCount) { UA_LOCK(&client->clientMutex); UA_StatusCode res = cancelByRequestHandle(client, requestHandle, cancelCount); UA_UNLOCK(&client->clientMutex); return res; } UA_StatusCode UA_Client_cancelByRequestId(UA_Client *client, UA_UInt32 requestId, UA_UInt32 *cancelCount) { UA_LOCK(&client->clientMutex); UA_StatusCode res = UA_STATUSCODE_BADNOTFOUND; AsyncServiceCall *ac; LIST_FOREACH(ac, &client->asyncServiceCalls, pointers) { if(ac->requestId != requestId) continue; res = cancelByRequestHandle(client, ac->requestHandle, cancelCount); break; } UA_UNLOCK(&client->clientMutex); return res; } /*******************/ /* Timed Callbacks */ /*******************/ UA_StatusCode UA_Client_addTimedCallback(UA_Client *client, UA_ClientCallback callback, void *data, UA_DateTime date, UA_UInt64 *callbackId) { if(!client->config.eventLoop) return UA_STATUSCODE_BADINTERNALERROR; UA_LOCK(&client->clientMutex); UA_StatusCode res = client->config.eventLoop-> addTimer(client->config.eventLoop, (UA_Callback)callback, client, data, 0.0, &date, UA_TIMERPOLICY_ONCE, callbackId); UA_UNLOCK(&client->clientMutex); return res; } UA_StatusCode UA_Client_addRepeatedCallback(UA_Client *client, UA_ClientCallback callback, void *data, UA_Double interval_ms, UA_UInt64 *callbackId) { if(!client->config.eventLoop) return UA_STATUSCODE_BADINTERNALERROR; UA_LOCK(&client->clientMutex); UA_StatusCode res = client->config.eventLoop-> addTimer(client->config.eventLoop, (UA_Callback)callback, client, data, interval_ms, NULL, UA_TIMERPOLICY_CURRENTTIME, callbackId); UA_UNLOCK(&client->clientMutex); return res; } UA_StatusCode UA_Client_changeRepeatedCallbackInterval(UA_Client *client, UA_UInt64 callbackId, UA_Double interval_ms) { if(!client->config.eventLoop) return UA_STATUSCODE_BADINTERNALERROR; UA_LOCK(&client->clientMutex); UA_StatusCode res = client->config.eventLoop-> modifyTimer(client->config.eventLoop, callbackId, interval_ms, NULL, UA_TIMERPOLICY_CURRENTTIME); UA_UNLOCK(&client->clientMutex); return res; } void UA_Client_removeCallback(UA_Client *client, UA_UInt64 callbackId) { if(!client->config.eventLoop) return; UA_LOCK(&client->clientMutex); client->config.eventLoop->removeTimer(client->config.eventLoop, callbackId); UA_UNLOCK(&client->clientMutex); } /**********************/ /* Housekeeping Tasks */ /**********************/ static void asyncServiceTimeoutCheck(UA_Client *client) { /* Make this function reentrant. One of the async callbacks could indirectly * operate on the list. Moving all elements to a local list before iterating * that. */ UA_EventLoop *el = client->config.eventLoop; UA_DateTime now = el->dateTime_nowMonotonic(el); UA_AsyncServiceList asyncServiceCalls; AsyncServiceCall *ac, *ac_tmp; LIST_INIT(&asyncServiceCalls); LIST_FOREACH_SAFE(ac, &client->asyncServiceCalls, pointers, ac_tmp) { if(!ac->timeout) continue; if(ac->start + (UA_DateTime)(ac->timeout * UA_DATETIME_MSEC) <= now) { LIST_REMOVE(ac, pointers); LIST_INSERT_HEAD(&asyncServiceCalls, ac, pointers); } } /* Cancel and remove the elements from the local list */ LIST_FOREACH_SAFE(ac, &asyncServiceCalls, pointers, ac_tmp) { LIST_REMOVE(ac, pointers); __Client_AsyncService_cancel(client, ac, UA_STATUSCODE_BADTIMEOUT); } } static void backgroundConnectivityCallback(UA_Client *client, void *userdata, UA_UInt32 requestId, const UA_ReadResponse *response) { UA_LOCK(&client->clientMutex); if(response->responseHeader.serviceResult == UA_STATUSCODE_BADTIMEOUT) { if(client->config.inactivityCallback) { UA_UNLOCK(&client->clientMutex); client->config.inactivityCallback(client); UA_LOCK(&client->clientMutex); } } UA_EventLoop *el = client->config.eventLoop; client->pendingConnectivityCheck = false; client->lastConnectivityCheck = el->dateTime_nowMonotonic(el); UA_UNLOCK(&client->clientMutex); } static void __Client_backgroundConnectivity(UA_Client *client) { if(!client->config.connectivityCheckInterval) return; if(client->pendingConnectivityCheck) return; UA_EventLoop *el = client->config.eventLoop; UA_DateTime now = el->dateTime_nowMonotonic(el); UA_DateTime nextDate = client->lastConnectivityCheck + (UA_DateTime)(client->config.connectivityCheckInterval * UA_DATETIME_MSEC); if(now <= nextDate) return; /* Prepare the request */ UA_ReadValueId rvid; UA_ReadValueId_init(&rvid); rvid.attributeId = UA_ATTRIBUTEID_VALUE; rvid.nodeId = UA_NS0ID(SERVER_SERVERSTATUS_STATE); UA_ReadRequest request; UA_ReadRequest_init(&request); request.nodesToRead = &rvid; request.nodesToReadSize = 1; UA_StatusCode retval = __Client_AsyncService(client, &request, &UA_TYPES[UA_TYPES_READREQUEST], (UA_ClientAsyncServiceCallback)backgroundConnectivityCallback, &UA_TYPES[UA_TYPES_READRESPONSE], NULL, NULL); if(retval == UA_STATUSCODE_GOOD) client->pendingConnectivityCheck = true; } /* Regular housekeeping activities in the client -- called via a cyclic callback */ static void clientHouseKeeping(UA_Client *client, void *_) { UA_LOCK(&client->clientMutex); UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT, "Internally check the the client state and " "required activities"); /* Renew Secure Channel */ __Client_renewSecureChannel(client); /* Send read requests from time to time to test the connectivity */ __Client_backgroundConnectivity(client); #ifdef UA_ENABLE_SUBSCRIPTIONS /* Feed the server PublishRequests for the Subscriptions */ __Client_Subscriptions_backgroundPublish(client); /* Check for inactive Subscriptions */ __Client_Subscriptions_backgroundPublishInactivityCheck(client); #endif /* Did async services time out? Process callbacks with an error code */ asyncServiceTimeoutCheck(client); /* Log and notify user if the client state has changed */ notifyClientState(client); UA_UNLOCK(&client->clientMutex); } UA_StatusCode __UA_Client_startup(UA_Client *client) { UA_LOCK_ASSERT(&client->clientMutex); UA_EventLoop *el = client->config.eventLoop; UA_CHECK_ERROR(el != NULL, return UA_STATUSCODE_BADINTERNALERROR, client->config.logging, UA_LOGCATEGORY_CLIENT, "No EventLoop configured"); /* Set up the repeated timer callback for checking the internal state. Like * in the public API UA_Client_addRepeatedCallback, but without locking the * mutex again */ UA_StatusCode rv = UA_STATUSCODE_GOOD; if(!client->houseKeepingCallbackId) { rv = el->addTimer(el, (UA_Callback)clientHouseKeeping, client, NULL, 1000.0, NULL, UA_TIMERPOLICY_CURRENTTIME, &client->houseKeepingCallbackId); UA_CHECK_STATUS(rv, return rv); } /* Start the EventLoop? */ if(el->state == UA_EVENTLOOPSTATE_FRESH) { rv = el->start(el); UA_CHECK_STATUS(rv, return rv); } return UA_STATUSCODE_GOOD; } UA_StatusCode UA_Client_run_iterate(UA_Client *client, UA_UInt32 timeout) { /* Make sure the EventLoop has been started */ UA_LOCK(&client->clientMutex); UA_StatusCode rv = __UA_Client_startup(client); UA_UNLOCK(&client->clientMutex); UA_CHECK_STATUS(rv, return rv); /* All timers and network events are triggered in the EventLoop. Release the * client lock before. The callbacks from the EventLoop take the lock * again. */ UA_EventLoop *el = client->config.eventLoop; rv = el->run(el, timeout); UA_CHECK_STATUS(rv, return rv); return client->connectStatus; } const UA_DataType * UA_Client_findDataType(UA_Client *client, const UA_NodeId *typeId) { return UA_findDataTypeWithCustom(typeId, client->config.customDataTypes); } /*************************/ /* Connection Attributes */ /*************************/ #define UA_CONNECTIONATTRIBUTESSIZE 3 static const UA_QualifiedName connectionAttributes[UA_CONNECTIONATTRIBUTESSIZE] = { {0, UA_STRING_STATIC("serverDescription")}, {0, UA_STRING_STATIC("securityPolicyUri")}, {0, UA_STRING_STATIC("securityMode")} }; static UA_StatusCode getConnectionttribute(UA_Client *client, const UA_QualifiedName key, UA_Variant *outValue, UA_Boolean copy) { if(!outValue) return UA_STATUSCODE_BADINTERNALERROR; UA_Variant localAttr; if(UA_QualifiedName_equal(&key, &connectionAttributes[0])) { /* ServerDescription */ UA_Variant_setScalar(&localAttr, &client->endpoint.server, &UA_TYPES[UA_TYPES_APPLICATIONDESCRIPTION]); } else if(UA_QualifiedName_equal(&key, &connectionAttributes[1])) { /* SecurityPolicyUri */ const UA_SecurityPolicy *sp = client->channel.securityPolicy; if(!sp) return UA_STATUSCODE_BADNOTCONNECTED; UA_Variant_setScalar(&localAttr, (void*)(uintptr_t)&sp->policyUri, &UA_TYPES[UA_TYPES_STRING]); } else if(UA_QualifiedName_equal(&key, &connectionAttributes[2])) { /* SecurityMode */ UA_Variant_setScalar(&localAttr, &client->channel.securityMode, &UA_TYPES[UA_TYPES_MESSAGESECURITYMODE]); } else { return UA_STATUSCODE_BADINTERNALERROR; } if(copy) return UA_Variant_copy(&localAttr, outValue); localAttr.storageType = UA_VARIANT_DATA_NODELETE; *outValue = localAttr; return UA_STATUSCODE_GOOD; } UA_StatusCode UA_Client_getConnectionAttribute(UA_Client *client, const UA_QualifiedName key, UA_Variant *outValue) { UA_LOCK(&client->clientMutex); UA_StatusCode res = getConnectionttribute(client, key, outValue, false); UA_UNLOCK(&client->clientMutex); return res; } UA_StatusCode UA_Client_getConnectionAttributeCopy(UA_Client *client, const UA_QualifiedName key, UA_Variant *outValue) { UA_LOCK(&client->clientMutex); UA_StatusCode res = getConnectionttribute(client, key, outValue, true); UA_UNLOCK(&client->clientMutex); return res; } UA_StatusCode UA_Client_getConnectionAttribute_scalar(UA_Client *client, const UA_QualifiedName key, const UA_DataType *type, void *outValue) { UA_LOCK(&client->clientMutex); UA_Variant attr; UA_StatusCode res = getConnectionttribute(client, key, &attr, false); if(res != UA_STATUSCODE_GOOD) { UA_UNLOCK(&client->clientMutex); return res; } if(!UA_Variant_hasScalarType(&attr, type)) { UA_UNLOCK(&client->clientMutex); return UA_STATUSCODE_BADNOTFOUND; } memcpy(outValue, attr.data, type->memSize); UA_UNLOCK(&client->clientMutex); return UA_STATUSCODE_GOOD; } /* Namespace Mapping */ UA_StatusCode UA_Client_getNamespaceUri(UA_Client *client, UA_UInt16 index, UA_String *nsUri) { UA_LOCK(&client->clientMutex); UA_StatusCode res = UA_STATUSCODE_GOOD; if(index > client->namespacesSize) res = UA_String_copy(&client->namespaces[index], nsUri); else res = UA_STATUSCODE_BADNOTFOUND; UA_UNLOCK(&client->clientMutex); return res; } UA_StatusCode UA_Client_getNamespaceIndex(UA_Client *client, const UA_String nsUri, UA_UInt16 *outIndex) { UA_LOCK(&client->clientMutex); for(size_t i = 0; i < client->namespacesSize; i++) { if(UA_String_equal(&nsUri, &client->namespaces[i])) { *outIndex = (UA_UInt16)i; UA_UNLOCK(&client->clientMutex); return UA_STATUSCODE_GOOD; } } UA_UNLOCK(&client->clientMutex); return UA_STATUSCODE_BADNOTFOUND; } UA_StatusCode UA_Client_addNamespace(UA_Client *client, const UA_String nsUri, UA_UInt16 *outIndex) { UA_StatusCode res = UA_Client_getNamespaceIndex(client, nsUri, outIndex); if(res == UA_STATUSCODE_GOOD) return res; UA_LOCK(&client->clientMutex); res = UA_Array_appendCopy((void**)&client->namespaces, &client->namespacesSize, &nsUri, &UA_TYPES[UA_TYPES_STRING]); if(res == UA_STATUSCODE_GOOD) *outIndex = (UA_UInt16)(client->namespacesSize - 1); UA_UNLOCK(&client->clientMutex); return res; }