Files
open62541/src/client/ua_client_connect.c
T
2026-08-17 22:17:22 +02:00

3272 lines
135 KiB
C

/* 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 2017-2022 (c) Fraunhofer IOSB (Author: Julius Pfrommer)
* Copyright 2017-2019 (c) Fraunhofer IOSB (Author: Mark Giraud)
* Copyright 2025 (c) Siemens AG (Author: Tin Raic)
* Copyright 2025 (c) o6 Automation GmbH (Author: Julius Pfrommer)
* Copyright 2026 (c) o6 Automation GmbH (Author: Julius Pfrommer)
*/
#include <open62541/types.h>
#include "open62541/transport_generated.h"
#include "ua_client_internal.h"
#include "../ua_types_encoding_binary.h"
#include "mp_printf.h"
/* Some OPC UA servers only return all Endpoints if the EndpointURL used for
* discovery exactly matches -- including the path following the address and
* port. Hence for the first connection we only call FindServers and reopen the
* transport connection using the EndpointURL found there.
*
* The overall process is this:
* - Connect with the EndpointURL provided by the user
* - Call FindServers
* - If one of the results has an exactly matching EndpointUrl, continue.
* - Otherwise select a matching server, update the endpointURL member of
* UA_Client and reconnect.
* - Call GetEndpoints and select an Endpoint
* - Open a SecureChannel and Session for that Endpoint
* - Read the namespaces array of the server (and create the namespaces mapping)
*/
#define UA_MINMESSAGESIZE 8192
#define MAX_DATA_SIZE 4096
static void initConnect(UA_Client *client);
static UA_StatusCode createSessionAsync(UA_Client *client);
static UA_UserTokenPolicy *
findUserTokenPolicy(UA_Client *client, UA_EndpointDescription *endpoint,
char *logPrefix);
/* Get the EndpointUrl to be used right now.
* This is adjusted during the discovery process.
* We fall back if connecting to an EndpointUrl fails. */
static UA_String
getEndpointUrl(UA_Client *client) {
if(client->endpoint.endpointUrl.length > 0)
return client->endpoint.endpointUrl;
if(client->discoveryUrl.length > 0)
return client->discoveryUrl;
return client->config.endpointUrl;
}
/* If an EndpointUrl doesn't work (the transport connection fails), fall back to the
* initial EndpointUrl */
static UA_StatusCode
fallbackEndpointUrl(UA_Client* client) {
/* Cannot fallback, the initial EndpointUrl is already in use */
UA_String currentUrl = getEndpointUrl(client);
if(UA_String_equal(&currentUrl, &client->config.endpointUrl)) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Could not open a transport connection to the Endpoint at %S",
client->config.endpointUrl);
return UA_STATUSCODE_BADCONNECTIONREJECTED;
}
if(client->endpoint.endpointUrl.length > 0) {
/* Overwrite the EndpointUrl of the Endpoint */
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Could not open a transport connection to the Endpoint at %S. "
"Overriding the endpoint description with the initial "
"EndpointUrl at %S.",
client->config.endpoint.endpointUrl,
client->config.endpointUrl);
UA_String_clear(&client->endpoint.endpointUrl);
return UA_String_copy(&client->config.endpointUrl,
&client->endpoint.endpointUrl);
} else {
/* Overwrite the DiscoveryUrl returned by FindServers */
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The DiscoveryUrl returned by the FindServers service (%S) "
"could not be connected. Continuing with the initial EndpointUrl "
"%S for the GetEndpoints service.",
client->config.endpointUrl, client->config.endpointUrl);
UA_String_clear(&client->discoveryUrl);
return UA_String_copy(&client->config.endpointUrl, &client->discoveryUrl);
}
}
static UA_SecurityPolicy *
getSecurityPolicy(UA_Client *client, UA_String policyUri) {
for(size_t i = 0; i < client->config.securityPoliciesSize; i++) {
if(UA_String_equal(&policyUri, &client->config.securityPolicies[i].policyUri))
return &client->config.securityPolicies[i];
}
return NULL;
}
/* Match PolicyUri and certificate. The returned SecurityPolicy instance carries
* the private key to sign with the given certificate. When the certificate is
* NULL, only the PolicyUri is matched (used for non-X509 token types). */
static UA_SecurityPolicy *
getAuthSecurityPolicy(UA_Client *client, const UA_String policyUri,
const UA_ByteString *certificate) {
for(size_t i = 0; i < client->config.authSecurityPoliciesSize; i++) {
UA_SecurityPolicy *sp = &client->config.authSecurityPolicies[i];
if(!UA_String_equal(&policyUri, &sp->policyUri))
continue;
if(certificate && !UA_ByteString_equal(certificate, &sp->localCertificate))
continue;
return sp;
}
return NULL;
}
/* The endpoint is unconfigured if the description is all zeroed-out */
static UA_Boolean
endpointUnconfigured(const UA_EndpointDescription *endpoint) {
UA_EndpointDescription tmp;
UA_EndpointDescription_init(&tmp);
return UA_equal(&tmp, endpoint, &UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
}
UA_Boolean
isFullyConnected(UA_Client *client) {
/* No SecureChannel */
if(client->channel.state != UA_SECURECHANNELSTATE_OPEN)
return false;
/* GetEndpoints handshake ongoing or not yet done */
if(client->endpointsHandshake || endpointUnconfigured(&client->endpoint))
return false;
/* FindServers handshake ongoing or not yet done */
if(client->findServersHandshake || client->discoveryUrl.length == 0)
return false;
if(!client->config.noSession) {
/* No Session, but is required */
if(client->sessionState != UA_SESSIONSTATE_ACTIVATED)
return false;
/* NamespaceArray not yet read */
if(client->namespacesHandshake || !client->haveNamespaces)
return false;
}
return true;
}
static UA_StatusCode
initUserTokenPolicy(UA_Client *client, const UA_UserTokenPolicy **outUtp,
char *logPrefix) {
const UA_UserTokenPolicy *utp =
findUserTokenPolicy(client, &client->endpoint, logPrefix);
if(!utp) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: Could not find a matching UserTokenPolicy "
"in the endpoint", logPrefix);
return UA_STATUSCODE_BADSECURITYPOLICYREJECTED;
}
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: Using UserTokenPolicy %S", logPrefix, utp->policyId);
/* If not specifically defined in the UserTokenPolicy, then the
* SecurityPolicy of the underlying endpoint (SecureChannel) is used. */
UA_String tokenSecurityPolicyUri = (utp->securityPolicyUri.length > 0) ?
utp->securityPolicyUri : client->endpoint.securityPolicyUri;
/* Get the SecurityPolicy for the UserIdentityToken. X509 tokens sign with
* a private key, so they must use an authSecurityPolicy (matched by
* PolicyUri and certificate). Other tokens only encrypt with the server
* certificate, so fall back to the SecureChannel securityPolicies. */
UA_SecurityPolicy *utsp;
if(utp->tokenType == UA_USERTOKENTYPE_CERTIFICATE) {
UA_X509IdentityToken *token = (UA_X509IdentityToken*)
client->config.userIdentityToken.content.decoded.data;
utsp = getAuthSecurityPolicy(client, tokenSecurityPolicyUri,
&token->certificateData);
} else {
utsp = getAuthSecurityPolicy(client, tokenSecurityPolicyUri, NULL);
if(!utsp)
utsp = getSecurityPolicy(client, tokenSecurityPolicyUri);
}
if(!utsp) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: SecurityPolicy %S not available for the "
"UserTokenPolicy", logPrefix, tokenSecurityPolicyUri);
return UA_STATUSCODE_BADSECURITYPOLICYREJECTED;
}
/* ExistingSecurityPolicy matches? */
if(client->utpSp) {
if(!UA_String_equal(&client->utpSp->policyUri,
&tokenSecurityPolicyUri)) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: SecurityPolicy %S cannot be instantiated. "
"A different SecurityPolicy %S is in place already",
logPrefix, tokenSecurityPolicyUri,
client->utpSp->policyUri);
return UA_STATUSCODE_BADSECURITYPOLICYREJECTED;
}
/* Set the output and return */
*outUtp = utp;
return UA_STATUSCODE_GOOD;
}
/* Create an instance of the SecurityPolicy using the server certificate
* from the endpoint */
UA_StatusCode res =
utsp->newChannelContext(utsp, &client->endpoint.serverCertificate,
&client->utpSpContext);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: UserTokenPolicy %S could not be instantiated with the "
"server certificate", logPrefix, tokenSecurityPolicyUri);
return UA_STATUSCODE_BADSECURITYPOLICYREJECTED;
}
client->utpSp = utsp;
/* Set the output and return */
*outUtp = utp;
return UA_STATUSCODE_GOOD;
}
/* Legacy (pre-v1.05.07) signature: the leaf of the remote (peer) certificate
* concatenated with the server's session nonce, signed with the given
* SecurityPolicy/context. Shared by the application ClientSignature and the
* X.509 user-token signature. The leaf is used (not the raw, possibly-chained
* remoteCertificate) so the bytes match the server's verify, which is computed
* over its single localCertificate. */
static UA_StatusCode
signLegacyCertNonce(const UA_SecurityPolicy *sp, void *spContext,
const UA_ByteString *remoteCertificate,
const UA_ByteString *serverNonce, UA_ByteString *outSignature) {
UA_ByteString leaf = getLeafCertificate(*remoteCertificate);
size_t signDataSize = leaf.length + serverNonce->length;
if(signDataSize > MAX_DATA_SIZE)
return UA_STATUSCODE_BADINTERNALERROR;
UA_Byte buf[MAX_DATA_SIZE];
UA_ByteString signData = {signDataSize, buf};
memcpy(buf, leaf.data, leaf.length);
memcpy(buf + leaf.length, serverNonce->data, serverNonce->length);
return sp->asymSignatureAlgorithm.sign(sp, spContext, &signData, outSignature);
}
/* Function to create a signature using remote certificate and nonce.
* This uses the SecurityPolicy of the SecureChannel. */
static UA_StatusCode
signClientSignature(UA_Client *client, UA_ActivateSessionRequest *request) {
UA_SecureChannel *channel = &client->channel;
const UA_SecurityPolicy *sp = channel->securityPolicy;
void *cc = channel->channelContext;
UA_SignatureData *sd = &request->clientSignature;
const UA_SecurityPolicySignatureAlgorithm *signAlg = &sp->asymSignatureAlgorithm;
/* OPC UA Part 4 v1.05.07: for SecurityPolicies with
* secureChannelEnhancements = true, the algorithm field in
* SignatureData shall be NULL or empty and receivers shall
* ignore the value. */
UA_StatusCode retval = UA_STATUSCODE_GOOD;
if(!UA_SecurityPolicy_isEnhancedSecurity(sp))
retval = UA_String_copy(&signAlg->uri, &sd->algorithm);
if(retval != UA_STATUSCODE_GOOD)
return retval;
/* Allocate memory for the signature */
size_t signatureSize = signAlg->getLocalSignatureSize(sp, cc);
retval = UA_ByteString_allocBuffer(&sd->signature, signatureSize);
if(retval != UA_STATUSCODE_GOOD)
return retval;
if(UA_SecurityPolicy_isEnhancedSecurity(sp)) {
/* Channel-bound v1.05.07 ClientSignature (see the cert-role table at
* UA_SecureChannel_buildActivateSessionSignatureData). */
UA_ByteString signData = UA_BYTESTRING_NULL;
retval = UA_SecureChannel_buildActivateSessionSignatureData(
channel, &client->serverSessionNonce, &client->clientSessionNonce,
&channel->remoteCertificate, &channel->remoteCertificate,
&sp->localCertificate, &signData);
if(retval != UA_STATUSCODE_GOOD)
return retval;
retval = signAlg->sign(sp, cc, &signData, &sd->signature);
UA_ByteString_clear(&signData);
return retval;
}
/* Legacy (v1.05.06): leaf(serverCert) + serverNonce. */
return signLegacyCertNonce(sp, cc, &channel->remoteCertificate,
&client->serverSessionNonce, &sd->signature);
}
/* This uses the SecurityPolicy of the UserTokenPolicy */
static UA_StatusCode
signUserTokenSignature(UA_Client *client,
UA_ActivateSessionRequest *request) {
UA_assert(client->config.userIdentityToken.content.decoded.type ==
&UA_TYPES[UA_TYPES_X509IDENTITYTOKEN]);
UA_SecurityPolicy *utpSp = client->utpSp;
UA_assert(utpSp);
UA_SecureChannel *channel = &client->channel;
UA_SecurityPolicySignatureAlgorithm *signAlg = &utpSp->asymSignatureAlgorithm;
UA_SignatureData *utsd = &request->userTokenSignature;
/* Stricter servers reject an empty user-token algorithm - so always send
* the policy's signature URI (the AesGcm policy carries its SecurityPolicy
* URI there; RSA carries the RSA URI; plain ECC carries an empty
* string). */
UA_StatusCode retval = UA_String_copy(&signAlg->uri, &utsd->algorithm);
if(retval != UA_STATUSCODE_GOOD)
return retval;
/* Allocate memory for the signature */
size_t sigLen = signAlg->getLocalSignatureSize(utpSp, client->utpSpContext);
retval = UA_ByteString_allocBuffer(&utsd->signature, sigLen);
if(retval != UA_STATUSCODE_GOOD)
return retval;
/* Channel-bound v1.05.07 user-token signature (see the cert-role table at
* UA_SecureChannel_buildUserTokenSignatureData), produced with the user's
* private key (utpSp context). Used only when BOTH the SecureChannel AND the
* user-token SecurityPolicy are enhanced; a legacy RSA / old-ECC user token
* uses the legacy layout even over an enhanced (AesGcm) SecureChannel. */
if(UA_SecurityPolicy_isEnhancedSecurity(channel->securityPolicy) &&
UA_SecurityPolicy_isEnhancedSecurity(utpSp)) {
const UA_ByteString *clientCert = &channel->securityPolicy->localCertificate;
UA_ByteString signData = UA_BYTESTRING_NULL;
retval = UA_SecureChannel_buildUserTokenSignatureData(
channel, &client->serverSessionNonce, &client->clientSessionNonce,
&channel->remoteCertificate, &channel->remoteCertificate,
clientCert, clientCert, &signData);
if(retval == UA_STATUSCODE_GOOD)
retval = signAlg->sign(utpSp, client->utpSpContext, &signData, &utsd->signature);
UA_ByteString_clear(&signData);
return retval;
}
/* Legacy (v1.05.06): leaf(serverCert) + serverNonce (same as the
* application ClientSignature), signed with the user's key. */
return signLegacyCertNonce(utpSp, client->utpSpContext,
&channel->remoteCertificate,
&client->serverSessionNonce, &utsd->signature);
}
/* UserName and IssuedIdentity can be encrypted. This uses the SecurityPolicy
* instance for the UserTokenPolicy. So it can keep the ECC ephemeral keys
* across multiple SecureChannels. */
static UA_StatusCode
encryptUserIdentityToken(UA_Client *client, UA_ExtensionObject *userIdentityToken) {
UA_IssuedIdentityToken *iit = NULL;
UA_UserNameIdentityToken *unit = NULL;
UA_String *encryptionAlg;
UA_ByteString *tokenData;
const UA_DataType *tokenType = userIdentityToken->content.decoded.type;
if(tokenType == &UA_TYPES[UA_TYPES_ISSUEDIDENTITYTOKEN]) {
iit = (UA_IssuedIdentityToken*)userIdentityToken->content.decoded.data;
tokenData = &iit->tokenData;
encryptionAlg = &iit->encryptionAlgorithm;
} else { /* tokenType == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN] */
UA_assert(tokenType == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN]);
unit = (UA_UserNameIdentityToken*)userIdentityToken->content.decoded.data;
tokenData = &unit->password;
encryptionAlg = &unit->encryptionAlgorithm;
}
/* The sessionSp is instantiated during CreateSession */
UA_SecurityPolicy *utpSp = client->utpSp;
UA_assert(utpSp);
/* Nothing to encrypt */
if(utpSp->policyType == UA_SECURITYPOLICYTYPE_NONE) {
/* Warn if a token is transmitted in cleartext. This needs to be allowed
* with the allowNonePolicyPassword setting and is checked in
* findUserTokenPolicy. */
if(client->channel.securityMode != UA_MESSAGESECURITYMODE_SIGNANDENCRYPT)
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"!!! Warning !!! AuthenticationToken is transmitted "
"without encryption");
return UA_STATUSCODE_GOOD;
}
/* Encrypt the TokenData */
UA_StatusCode res = UA_STATUSCODE_GOOD;
if(UA_SecurityPolicy_isEcc(utpSp)) {
res = encryptUserIdentityTokenEcc(client->config.logging, &client->channel,
utpSp, client->utpSpContext, tokenData,
client->serverSessionNonce,
client->serverEphemeralPubKey);
/* Don't reuse the Ephemeral Public Key */
UA_ByteString_clear(&client->serverEphemeralPubKey);
/* The EccEncryptedSecret identifies its algorithm via the SecurityPolicy
* URI embedded in the secret header, so the UserIdentityToken's
* encryptionAlgorithm field is left EMPTY (matching the OPC UA reference
* stack, which sets it to null for the EncryptedSecret format). Sending
* the policy's asymEncryptionAlgorithm URI here makes strict servers
* reject the token with BadIdentityTokenInvalid */
return res;
}
res = encryptSecretLegacy(utpSp, client->utpSpContext,
client->serverSessionNonce, tokenData);
/* Legacy RSA EncryptedData: carry the asymmetric encryption algorithm URI */
return res | UA_String_copy(&utpSp->asymEncryptionAlgorithm.uri, encryptionAlg);
}
/* Verifies the signature corresponds to ClientNonce using the SecurityPolicy of
* the SecureChannel */
static UA_StatusCode
checkCreateSessionSignature(UA_Client *client, const UA_SecureChannel *channel,
const UA_CreateSessionResponse *response) {
if(!UA_SecureChannel_hasApplicationSecurity(channel))
return UA_STATUSCODE_GOOD;
if(!channel->securityPolicy)
return UA_STATUSCODE_BADINTERNALERROR;
const UA_SecurityPolicy *sp = channel->securityPolicy;
const UA_ByteString *lc = &sp->localCertificate;
UA_ByteString dataToVerify = UA_BYTESTRING_NULL;
UA_StatusCode retval;
if(UA_SecurityPolicy_isEnhancedSecurity(sp)) {
/* Channel-bound v1.05.07 CreateSession ServerSignature (see the
* cert-role table at UA_SecureChannel_buildCreateSessionSignatureData). */
retval = UA_SecureChannel_buildCreateSessionSignatureData(
channel, &client->clientSessionNonce, &response->serverNonce,
&channel->remoteCertificate, lc, &dataToVerify);
} else {
retval = UA_ByteString_allocBuffer(&dataToVerify,
lc->length + client->clientSessionNonce.length);
if(retval == UA_STATUSCODE_GOOD) {
memcpy(dataToVerify.data, lc->data, lc->length);
memcpy(dataToVerify.data + lc->length, client->clientSessionNonce.data,
client->clientSessionNonce.length);
}
}
if(retval != UA_STATUSCODE_GOOD)
return retval;
const UA_SecurityPolicySignatureAlgorithm *signAlg = &sp->asymSignatureAlgorithm;
retval = signAlg->verify(sp, channel->channelContext, &dataToVerify,
&response->serverSignature.signature);
UA_ByteString_clear(&dataToVerify);
return retval;
}
/***********************/
/* Open the Connection */
/***********************/
void
processERRResponse(UA_Client *client, const UA_ByteString *chunk) {
size_t offset = 0;
UA_TcpErrorMessage errMessage;
UA_StatusCode res =
UA_decodeBinaryInternal(chunk, &offset, &errMessage,
&UA_TRANSPORT[UA_TRANSPORT_TCPERRORMESSAGE], NULL);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR_CHANNEL(client->config.logging, &client->channel,
"Received an ERR response that could not be decoded "
"with StatusCode %s", UA_StatusCode_name(res));
setConnectStatus(client, res);
return;
}
UA_LOG_ERROR_CHANNEL(client->config.logging, &client->channel,
"Received an ERR response with StatusCode %s and "
"the following reason: \"%S\"",
UA_StatusCode_name(errMessage.error),
errMessage.reason);
setConnectStatus(client, errMessage.error);
UA_TcpErrorMessage_clear(&errMessage);
}
void
processACKResponse(UA_Client *client, const UA_ByteString *chunk) {
UA_SecureChannel *channel = &client->channel;
if(channel->state != UA_SECURECHANNELSTATE_HEL_SENT) {
UA_LOG_ERROR_CHANNEL(client->config.logging, channel,
"SecureChannel not in the HEL-sent state");
setConnectStatus(client, UA_STATUSCODE_BADSECURECHANNELCLOSED);
return;
}
/* Decode the message */
size_t offset = 0;
UA_TcpAcknowledgeMessage ackMessage;
UA_StatusCode res =
UA_decodeBinaryInternal(chunk, &offset, &ackMessage,
&UA_TRANSPORT[UA_TRANSPORT_TCPACKNOWLEDGEMESSAGE], NULL);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_NETWORK,
"Decoding ACK message failed");
setConnectStatus(client, res);
return;
}
res = UA_SecureChannel_processHELACK(channel, &ackMessage);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_NETWORK,
"Processing the ACK message failed with StatusCode %s",
UA_StatusCode_name(res));
setConnectStatus(client, res);
return;
}
client->channel.state = UA_SECURECHANNELSTATE_ACK_RECEIVED;
}
static UA_StatusCode
sendHELMessage(UA_Client *client) {
UA_ConnectionManager *cm = client->channel.connectionManager;
if(!UA_SecureChannel_isConnected(&client->channel))
return UA_STATUSCODE_BADNOTCONNECTED;
/* Get a buffer */
UA_ByteString message;
UA_StatusCode retval = cm->allocNetworkBuffer(cm, client->channel.connectionId,
&message, UA_MINMESSAGESIZE);
if(retval != UA_STATUSCODE_GOOD)
return retval;
/* Prepare the HEL message and encode at offset 8 */
UA_TcpHelloMessage hello;
hello.protocolVersion = 0;
hello.receiveBufferSize = client->config.localConnectionConfig.recvBufferSize;
hello.sendBufferSize = client->config.localConnectionConfig.sendBufferSize;
hello.maxMessageSize = client->config.localConnectionConfig.localMaxMessageSize;
hello.maxChunkCount = client->config.localConnectionConfig.localMaxChunkCount;
hello.endpointUrl = getEndpointUrl(client);
UA_Byte *bufPos = &message.data[8]; /* skip the header */
const UA_Byte *bufEnd = &message.data[message.length];
retval = UA_encodeBinaryInternal(&hello, &UA_TRANSPORT[UA_TRANSPORT_TCPHELLOMESSAGE],
&bufPos, &bufEnd, NULL, NULL, NULL);
if(retval != UA_STATUSCODE_GOOD) {
cm->freeNetworkBuffer(cm, client->channel.connectionId, &message);
return retval;
}
/* Encode the message header at offset 0 */
UA_TcpMessageHeader messageHeader;
messageHeader.messageTypeAndChunkType = UA_CHUNKTYPE_FINAL + UA_MESSAGETYPE_HEL;
messageHeader.messageSize = (UA_UInt32) ((uintptr_t)bufPos - (uintptr_t)message.data);
bufPos = message.data;
retval = UA_encodeBinaryInternal(&messageHeader,
&UA_TRANSPORT[UA_TRANSPORT_TCPMESSAGEHEADER],
&bufPos, &bufEnd, NULL, NULL, NULL);
if(retval != UA_STATUSCODE_GOOD) {
cm->freeNetworkBuffer(cm, client->channel.connectionId, &message);
return retval;
}
/* Send the HEL message */
message.length = messageHeader.messageSize;
retval = cm->sendWithConnection(cm, client->channel.connectionId,
&UA_KEYVALUEMAP_NULL, &message);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Sending HEL failed");
setConnectStatus(client, retval);
return retval;
}
UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Sent HEL message");
client->channel.state = UA_SECURECHANNELSTATE_HEL_SENT;
return UA_STATUSCODE_GOOD;
}
void processRHEMessage(UA_Client *client, const UA_ByteString *chunk) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT, "RHE received");
size_t offset = 0; /* Go to the beginning of the TcpHelloMessage */
UA_TcpReverseHelloMessage rheMessage;
static const UA_DataType *rheType = &UA_TRANSPORT[UA_TRANSPORT_TCPREVERSEHELLOMESSAGE];
UA_StatusCode res = UA_decodeBinaryInternal(chunk, &offset, &rheMessage, rheType, NULL);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_NETWORK,
"Decoding RHE message failed");
setConnectStatus(client, res);
return;
}
/* Set the internal DisocveryUrl */
UA_String_clear(&client->discoveryUrl);
client->discoveryUrl = rheMessage.endpointUrl;
UA_String_init(&rheMessage.endpointUrl);
UA_TcpReverseHelloMessage_clear(&rheMessage);
/* Send the HEL message */
setConnectStatus(client, sendHELMessage(client));
}
void
processOPNResponse(UA_Client *client, const UA_ByteString *message) {
size_t offset = 0;
UA_NodeId responseId;
UA_OpenSecureChannelResponse response;
UA_NodeId expectedId = UA_NS0ID(OPENSECURECHANNELRESPONSE_ENCODING_DEFAULTBINARY);
/* Decode the type NodeId */
UA_StatusCode res = UA_NodeId_decodeBinary(message, &offset, &responseId);
if(res != UA_STATUSCODE_GOOD)
goto finish_decode;
/* Is the response of the expected type? */
if(!UA_NodeId_equal(&responseId, &expectedId)) {
res = UA_STATUSCODE_BADDECODINGERROR;
goto finish_decode;
}
/* Decode the response */
res = UA_decodeBinaryInternal(message, &offset, &response,
&UA_TYPES[UA_TYPES_OPENSECURECHANNELRESPONSE], NULL);
finish_decode:
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR_CHANNEL(client->config.logging, &client->channel,
"Could not decode the OpenSecureChannelResponse");
setConnectStatus(client, res);
return;
}
/* Check whether the nonce was reused */
if(client->channel.securityMode != UA_MESSAGESECURITYMODE_NONE &&
UA_ByteString_equal(&client->channel.remoteNonce, &response.serverNonce)) {
UA_LOG_ERROR_CHANNEL(client->config.logging, &client->channel,
"The server reused the last nonce");
setConnectStatus(client, UA_STATUSCODE_BADSECURITYCHECKSFAILED);
return;
}
/* Check the nonce length */
if(response.serverNonce.length < client->channel.securityPolicy->nonceLength) {
UA_LOG_ERROR_CHANNEL(client->config.logging, &client->channel,
"The server nonce is too short");
setConnectStatus(client, UA_STATUSCODE_BADSECURITYCHECKSFAILED);
return;
}
/* Move the nonce out of the response */
UA_ByteString_clear(&client->channel.remoteNonce);
client->channel.remoteNonce = response.serverNonce;
UA_ByteString_init(&response.serverNonce);
UA_ResponseHeader_clear(&response.responseHeader);
/* Replace the token. Keep the current token as the old token. Messages
* might still arrive for the old token. */
client->channel.altSecurityToken = client->channel.securityToken;
client->channel.securityToken = response.securityToken;
client->channel.renewState = UA_SECURECHANNELRENEWSTATE_NEWTOKEN_CLIENT;
/* Log a warning if the SecurityToken is not "fresh". Use the normal system
* clock to do the comparison. */
UA_EventLoop *el = client->config.eventLoop;
UA_DateTime wallClockNow = el->dateTime_now(el);
UA_ChannelSecurityToken *st = &client->channel.securityToken;
if(wallClockNow - st->createdAt >= UA_DATETIME_SEC * 10 ||
wallClockNow - st->createdAt <= -UA_DATETIME_SEC * 10)
UA_LOG_WARNING_CHANNEL(client->config.logging, &client->channel,
"The \"CreatedAt\" timestamp of the received "
"ChannelSecurityToken does not match "
"with the local system clock");
/* The internal "monotonic" clock is used by the SecureChannel to validate
* that the SecurityToken is still valid. The monotonic clock is independent
* from the system clock getting changed or synchronized to a master clock
* during runtime. */
client->channel.securityToken.createdAt = el->dateTime_nowMonotonic(el);
/* Response.securityToken.revisedLifetime is UInt32, we need to cast it to
* DateTime=Int64. After 75% of the lifetime the renewal takes place as
* described in standard */
client->nextChannelRenewal = client->channel.securityToken.createdAt +
(UA_DateTime) (response.securityToken.revisedLifetime *
(UA_Double) UA_DATETIME_MSEC * 0.75);
/* Compute the new local keys. The remote keys are updated when a message
* with the new SecurityToken is received. */
res = UA_SecureChannel_generateLocalKeys(&client->channel);
if(res != UA_STATUSCODE_GOOD) {
setConnectStatus(client, res);
return;
}
UA_Float lifetime = (UA_Float)response.securityToken.revisedLifetime / 1000;
UA_Boolean renew = (client->channel.state == UA_SECURECHANNELSTATE_OPEN);
if(renew) {
UA_LOG_INFO_CHANNEL(client->config.logging, &client->channel, "SecureChannel "
"renewed with a revised lifetime of %.2fs", lifetime);
} else {
UA_LOG_INFO_CHANNEL(client->config.logging, &client->channel,
"SecureChannel opened with SecurityMode %s for "
"SecurityPolicy %S and a revised lifetime of %.2fs",
securityModeNames[client->channel.securityMode],
client->channel.securityPolicy->policyUri,
lifetime);
}
client->channel.state = UA_SECURECHANNELSTATE_OPEN;
}
/* OPN messges to renew the channel are sent asynchronous */
static UA_StatusCode
sendOPNAsync(UA_Client *client, UA_Boolean renew) {
if(!UA_SecureChannel_isConnected(&client->channel))
return UA_STATUSCODE_BADINTERNALERROR;
UA_StatusCode res = UA_SecureChannel_generateLocalNonce(&client->channel);
if(res != UA_STATUSCODE_GOOD)
return res;
UA_EventLoop *el = client->config.eventLoop;
/* Prepare the OpenSecureChannelRequest */
UA_OpenSecureChannelRequest opnSecRq;
UA_OpenSecureChannelRequest_init(&opnSecRq);
opnSecRq.requestHeader.timestamp = el->dateTime_now(el);
opnSecRq.requestHeader.authenticationToken = client->authenticationToken;
opnSecRq.securityMode = client->channel.securityMode;
opnSecRq.clientNonce = client->channel.localNonce;
opnSecRq.requestedLifetime = client->config.secureChannelLifeTime;
if(renew) {
opnSecRq.requestType = UA_SECURITYTOKENREQUESTTYPE_RENEW;
UA_LOG_DEBUG_CHANNEL(client->config.logging, &client->channel,
"Requesting to renew the SecureChannel");
} else {
opnSecRq.requestType = UA_SECURITYTOKENREQUESTTYPE_ISSUE;
UA_LOG_DEBUG_CHANNEL(client->config.logging, &client->channel,
"Requesting to open a SecureChannel");
}
/* Prepare the entry for the linked list */
UA_UInt32 requestId = __Client_nextRequestId(client);
/* Send the OPN message */
UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_SECURECHANNEL,
"Requesting to open a SecureChannel");
static const UA_DataType *opnReqType = &UA_TYPES[UA_TYPES_OPENSECURECHANNELREQUEST];
res = UA_SecureChannel_sendOPN(&client->channel, requestId, &opnSecRq, opnReqType);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_SECURECHANNEL,
"Sending OPN message failed with error %s",
UA_StatusCode_name(res));
return res;
}
/* Update the state */
client->channel.renewState = UA_SECURECHANNELRENEWSTATE_SENT;
if(client->channel.state < UA_SECURECHANNELSTATE_OPN_SENT)
client->channel.state = UA_SECURECHANNELSTATE_OPN_SENT;
UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_SECURECHANNEL,
"OPN message sent");
return UA_STATUSCODE_GOOD;
}
UA_StatusCode
__Client_renewSecureChannel(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
/* Direct HTTP has no OPN lifetime or token renewal exchange. The logical
* SecureChannel remains pinned to its endpoint, encoding and policy until
* the client closes the HTTP binding. */
if(client->channel.transport == UA_SECURECHANNEL_TRANSPORT_HTTP)
return UA_STATUSCODE_GOODCALLAGAIN;
UA_EventLoop *el = client->config.eventLoop;
UA_DateTime now = el->dateTime_nowMonotonic(el);
/* Check if OPN has been sent or the SecureChannel is still valid */
if(client->channel.state != UA_SECURECHANNELSTATE_OPEN ||
client->channel.renewState == UA_SECURECHANNELRENEWSTATE_SENT ||
client->nextChannelRenewal > now)
return UA_STATUSCODE_GOODCALLAGAIN;
UA_StatusCode res = sendOPNAsync(client, true);
setConnectStatus(client, res);
return res;
}
UA_StatusCode
UA_Client_renewSecureChannel(UA_Client *client) {
lockClient(client);
UA_StatusCode res = __Client_renewSecureChannel(client);
unlockClient(client);
return res;
}
static void
responseReadNamespacesArray(UA_Client *client, void *userdata,
UA_UInt32 requestId, void *response) {
client->namespacesHandshake = false;
client->haveNamespaces = true;
UA_ReadResponse *resp = (UA_ReadResponse *)response;
/* Validate the response before dereferencing results[0]. An
* empty-array encoding yields UA_EMPTY_ARRAY_SENTINEL (0x1, non-NULL),
* bypassing a bare "!resp->results" check. */
if(resp->responseHeader.serviceResult != UA_STATUSCODE_GOOD ||
resp->resultsSize < 1 ||
!resp->results ||
!resp->results[0].hasValue ||
!resp->results[0].value.data ||
resp->results[0].value.data == UA_EMPTY_ARRAY_SENTINEL) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"No result in the read namespace array response");
return;
}
if(!UA_Variant_hasArrayType(&resp->results[0].value, &UA_TYPES[UA_TYPES_STRING])) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Read NamespaceArray returned an unexpected type");
return;
}
UA_String *ns = (UA_String *)resp->results[0].value.data;
size_t nsSize = resp->results[0].value.arrayLength;
if(nsSize < 2) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Read NamespaceArray returned too few entries");
return;
}
UA_String_copy(&ns[1], &client->namespaces[1]);
for(size_t i = 2; i < nsSize; ++i) {
UA_UInt16 nsIndex = 0;
UA_Client_addNamespace(client, ns[i], &nsIndex);
}
/* Set up the mapping. */
UA_NamespaceMapping *nsMapping = (UA_NamespaceMapping*)
UA_calloc(1, sizeof(UA_NamespaceMapping));
if(!nsMapping) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Namespace mapping creation failed. Out of Memory.");
return;
}
UA_StatusCode retval =
UA_Array_copy(client->namespaces, client->namespacesSize,
(void**)&nsMapping->namespaceUris,
&UA_TYPES[UA_TYPES_STRING]);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Failed to copy the namespaces with StatusCode %s.",
UA_StatusCode_name(retval));
UA_NamespaceMapping_delete(nsMapping);
return;
}
nsMapping->namespaceUrisSize = client->namespacesSize;
nsMapping->remote2local = (UA_UInt16*)UA_calloc( nsSize, sizeof(UA_UInt16));
if(!nsMapping->remote2local) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Namespace mapping creation failed. Out of Memory.");
UA_NamespaceMapping_delete(nsMapping);
return;
}
nsMapping->remote2localSize = nsSize;
nsMapping->remote2local[0] = 0;
nsMapping->remote2local[1] = 1;
for(size_t i = 2; i < nsSize; ++i) {
UA_UInt16 nsIndex = 0;
UA_Client_getNamespaceIndex(client, ns[i], &nsIndex);
nsMapping->remote2local[i] = nsIndex;
}
size_t l2rSize = client->namespacesSize > nsSize ? client->namespacesSize : nsSize;
nsMapping->local2remote = (UA_UInt16*)UA_calloc(l2rSize, sizeof(UA_UInt16));
if(!nsMapping->local2remote) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Namespace mapping creation failed. Out of Memory.");
UA_NamespaceMapping_delete(nsMapping);
return;
}
nsMapping->local2remoteSize = l2rSize;
nsMapping->local2remote[0] = 0;
nsMapping->local2remote[1] = 1;
for(size_t i = 2; i < nsMapping->remote2localSize; ++i) {
UA_UInt16 localIndex = nsMapping->remote2local[i];
nsMapping->local2remote[localIndex] = (UA_UInt16)i;
}
client->channel.namespaceMapping = nsMapping;
}
/* We read the namespaces right after the session has opened. The user might
* already requests other services in parallel. That leaves a short time where
* requests can be made before the namespace mapping is configured. */
static void
readNamespacesArrayAsync(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
/* Check the connection status */
if(client->sessionState != UA_SESSIONSTATE_CREATED &&
client->sessionState != UA_SESSIONSTATE_ACTIVATED) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Cannot read the namespaces array, session neither created nor "
"activated. Actual state: '%u'", client->sessionState);
return;
}
/* Set up the read request */
UA_ReadRequest rr;
UA_ReadRequest_init(&rr);
UA_ReadValueId nodesToRead;
UA_ReadValueId_init(&nodesToRead);
nodesToRead.nodeId = UA_NS0ID(SERVER_NAMESPACEARRAY);
nodesToRead.attributeId = UA_ATTRIBUTEID_VALUE;
rr.nodesToRead = &nodesToRead;
rr.nodesToReadSize = 1;
/* Send the async read request */
UA_StatusCode res =
__Client_AsyncServiceInternal(client, &rr, &UA_TYPES[UA_TYPES_READREQUEST],
(UA_ClientAsyncServiceCallback)responseReadNamespacesArray,
&UA_TYPES[UA_TYPES_READRESPONSE],
NULL, NULL);
if(res == UA_STATUSCODE_GOOD)
client->namespacesHandshake = true;
else
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Could not read the namespace array with error code %s",
UA_StatusCode_name(res));
}
static void
extractEphemeralKeyFromAddHeader(UA_Client *client, UA_ExtensionObject *ah) {
const UA_DataType *ahType = &UA_TYPES[UA_TYPES_ADDITIONALPARAMETERSTYPE];
if(!UA_ExtensionObject_hasDecodedType(ah, ahType))
return;
UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_SESSION,
"Server Ephemeral Key in the response");
/* KeyValueMap has the identical structure the UA_AdditionalParametersType */
UA_KeyValueMap *map = (UA_KeyValueMap*)ah->content.decoded.data;
/* TODO: Handle the ECDHPolicyUri */
/* Get the Ephemeral Key */
UA_EphemeralKeyType *ephKey = (UA_EphemeralKeyType*)(uintptr_t)
UA_KeyValueMap_getScalar(map, UA_QUALIFIEDNAME(0, "ECDHKey"),
&UA_TYPES[UA_TYPES_EPHEMERALKEYTYPE]);
if(!ephKey)
return; /* TODO: Security error? */
/* Move the Ephemeral Key into the client */
UA_ByteString_clear(&client->serverEphemeralPubKey);
client->serverEphemeralPubKey = ephKey->publicKey;
UA_ByteString_init(&ephKey->publicKey);
/* TODO: Validate the signature of the ephemeral key */
}
static void
responseActivateSession(UA_Client *client, void *userdata,
UA_UInt32 requestId, void *response) {
UA_LOCK_ASSERT(&client->clientMutex);
UA_ActivateSessionResponse *ar = (UA_ActivateSessionResponse*)response;
if(ar->responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
/* Activating the Session failed */
cleanupSession(client);
/* Configuration option to not create a new Session */
if(client->config.noNewSession) {
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(ar->responseHeader.serviceResult));
setConnectStatus(client, ar->responseHeader.serviceResult);
return;
}
/* The Session is no longer usable. Create a brand new one. */
if(ar->responseHeader.serviceResult == UA_STATUSCODE_BADSESSIONIDINVALID ||
ar->responseHeader.serviceResult == UA_STATUSCODE_BADSESSIONCLOSED) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Session to be activated no longer exists. Create a new Session.");
setConnectStatus(client, createSessionAsync(client));
return;
}
/* Something else is wrong. Maybe the credentials no longer work. Give up. */
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Session cannot be activated with StatusCode %s. "
"The client cannot recover from this, closing the connection.",
UA_StatusCode_name(ar->responseHeader.serviceResult));
setConnectStatus(client, ar->responseHeader.serviceResult);
return;
}
/* Check the (session) nonce length. The session nonce is the application
* nonce (>= 32 bytes), independent of the SecurityPolicy's SecureChannel
* nonce length (e.g. the 64-byte ECC ephemeral key). */
if(ar->serverNonce.length < 32) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Session cannot be activated with a nonce "
"that is too short");
setConnectStatus(client, UA_STATUSCODE_BADSECURITYCHECKSFAILED);
return;
}
/* Replace the nonce */
UA_ByteString_clear(&client->serverSessionNonce);
client->serverSessionNonce = ar->serverNonce;
UA_ByteString_init(&ar->serverNonce);
/* Extract the server's ECC ephemeral public key from the ActivateSession
* response */
extractEphemeralKeyFromAddHeader(client, &ar->responseHeader.additionalHeader);
client->sessionState = UA_SESSIONSTATE_ACTIVATED;
notifyClientState(client);
/* Read the namespaces array if we don't already have it */
if(!client->haveNamespaces)
readNamespacesArrayAsync(client);
/* Immediately check if publish requests are outstanding - for example when
* an existing Session has been reattached / activated. */
#ifdef UA_ENABLE_SUBSCRIPTIONS
__Client_Subscriptions_backgroundPublish(client);
#endif
}
static UA_StatusCode
activateSessionAsync(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
/* We should not try to activate in the current state */
if(client->sessionState != UA_SESSIONSTATE_CREATED &&
client->sessionState != UA_SESSIONSTATE_ACTIVATED) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Can not activate session, session neither created nor activated. "
"Actual state: '%u'", client->sessionState);
return UA_STATUSCODE_BADSESSIONCLOSED;
}
/* Select the UserTokenPolicy based on the client configuration and the
* endpoint information. Also instantiates the UserTokenPolicy if not
* already there. This can happen when an existing Session is
* transferred. */
const UA_UserTokenPolicy *utp = NULL;
UA_StatusCode retval = initUserTokenPolicy(client, &utp, "ActivateSession");
if(retval != UA_STATUSCODE_GOOD)
return retval;
/* Initialize the request */
UA_ActivateSessionRequest request;
UA_ActivateSessionRequest_init(&request);
/* Set the requested LocaleIds */
if(client->config.sessionLocaleIdsSize && client->config.sessionLocaleIds) {
retval = UA_Array_copy(client->config.sessionLocaleIds,
client->config.sessionLocaleIdsSize,
(void **)&request.localeIds, &UA_TYPES[UA_TYPES_LOCALEID]);
if(retval != UA_STATUSCODE_GOOD)
return retval;
request.localeIdsSize = client->config.sessionLocaleIdsSize;
}
/* Set the User Identity Token. If not defined use an anonymous token. Use
* the PolicyId from the UserTokenPolicy. All token types have the PolicyId
* string as the first element. */
UA_AnonymousIdentityToken anonToken;
retval = UA_ExtensionObject_copy(&client->config.userIdentityToken,
&request.userIdentityToken);
if(request.userIdentityToken.encoding != UA_EXTENSIONOBJECT_ENCODED_NOBODY) {
UA_String *policyId = (UA_String*)request.userIdentityToken.content.decoded.data;
UA_String_clear(policyId);
retval = UA_String_copy(&utp->policyId, policyId);
if(retval != UA_STATUSCODE_GOOD) {
UA_ActivateSessionRequest_clear(&request);
return retval;
}
} else {
UA_AnonymousIdentityToken_init(&anonToken);
UA_ExtensionObject_setValueNoDelete(&request.userIdentityToken, &anonToken,
&UA_TYPES[UA_TYPES_ANONYMOUSIDENTITYTOKEN]);
anonToken.policyId = utp->policyId;
}
switch(utp->tokenType) {
case UA_USERTOKENTYPE_ANONYMOUS:
break; /* The anonymous token is not encrypted */
case UA_USERTOKENTYPE_USERNAME:
case UA_USERTOKENTYPE_ISSUEDTOKEN:
/* Encrypt the UserIdentityToken */
retval = encryptUserIdentityToken(client, &request.userIdentityToken);
break;
case UA_USERTOKENTYPE_CERTIFICATE:
/* Create the UserTokenSignature */
retval = signUserTokenSignature(client, &request);
break;
default:
retval = UA_STATUSCODE_BADINTERNALERROR;
}
if(retval != UA_STATUSCODE_GOOD) {
UA_ActivateSessionRequest_clear(&request);
return retval;
}
/* Create the client signature with the SecurityPolicy of the SecureChannel */
UA_SecureChannel *channel = &client->channel;
if(UA_SecureChannel_hasApplicationSecurity(channel))
retval |= signClientSignature(client, &request);
/* Send the request */
if(UA_LIKELY(retval == UA_STATUSCODE_GOOD))
retval = __Client_AsyncServiceInternal(client, &request,
&UA_TYPES[UA_TYPES_ACTIVATESESSIONREQUEST],
(UA_ClientAsyncServiceCallback)responseActivateSession,
&UA_TYPES[UA_TYPES_ACTIVATESESSIONRESPONSE],
NULL, NULL);
/* On success, advance the session state */
if(retval == UA_STATUSCODE_GOOD)
client->sessionState = UA_SESSIONSTATE_ACTIVATE_REQUESTED;
else
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"ActivateSession failed when sending the request with error code %s",
UA_StatusCode_name(retval));
/* Clean up */
UA_ActivateSessionRequest_clear(&request);
return retval;
}
typedef struct {
UA_String urlSchemePrefix;
UA_String connectionManagerProtocol;
UA_String profileUri;
UA_UInt16 defaultPort;
UA_SecureChannelTransport transport;
UA_SecureChannelEncoding encoding;
UA_Boolean useTls;
} UA_ClientTransport;
static const UA_ClientTransport clientTransports[] = {
{UA_STRING_STATIC("opc.tcp:"), UA_STRING_STATIC("tcp"),
UA_STRING_STATIC("http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary"),
4840, UA_SECURECHANNEL_TRANSPORT_UACP,
UA_SECURECHANNEL_ENCODING_BINARY, false},
#ifdef UA_ENABLE_LWS
{UA_STRING_STATIC("opc.wss:"), UA_STRING_STATIC("websocket"),
UA_STRING_STATIC("http://opcfoundation.org/UA-Profile/Transport/wss-uasc-uabinary"),
443, UA_SECURECHANNEL_TRANSPORT_UACP,
UA_SECURECHANNEL_ENCODING_BINARY, true},
#endif
{UA_STRING_STATIC("opc.https:"), UA_STRING_STATIC("http"),
UA_STRING_STATIC("http://opcfoundation.org/UA-Profile/Transport/https-uabinary"),
443, UA_SECURECHANNEL_TRANSPORT_HTTP,
UA_SECURECHANNEL_ENCODING_BINARY, true},
#ifdef UA_ENABLE_JSON_ENCODING
{UA_STRING_STATIC("opc.https:"), UA_STRING_STATIC("http"),
UA_STRING_STATIC("http://opcfoundation.org/UA-Profile/Transport/https-uajson"),
443, UA_SECURECHANNEL_TRANSPORT_HTTP,
UA_SECURECHANNEL_ENCODING_JSON, true},
#endif
{UA_STRING_STATIC("opc.http:"), UA_STRING_STATIC("http"),
UA_STRING_STATIC("http://open62541.org/UA-Profile/Transport/http-uabinary"),
80, UA_SECURECHANNEL_TRANSPORT_HTTP,
UA_SECURECHANNEL_ENCODING_BINARY, false},
#ifdef UA_ENABLE_JSON_ENCODING
{UA_STRING_STATIC("opc.http:"), UA_STRING_STATIC("http"),
UA_STRING_STATIC("http://open62541.org/UA-Profile/Transport/http-uajson"),
80, UA_SECURECHANNEL_TRANSPORT_HTTP,
UA_SECURECHANNEL_ENCODING_JSON, false},
#endif
};
static const UA_ClientTransport *
getClientTransport(const UA_String *endpointUrl,
const UA_String *profileUri) {
UA_Boolean jsonPath = false;
#ifdef UA_ENABLE_JSON_ENCODING
const UA_String jsonSuffix = UA_STRING_STATIC("/json");
jsonPath = (!profileUri || profileUri->length == 0) &&
endpointUrl->length >= jsonSuffix.length &&
memcmp(&endpointUrl->data[endpointUrl->length - jsonSuffix.length],
jsonSuffix.data, jsonSuffix.length) == 0;
#endif
for(size_t i = 0;
i < sizeof(clientTransports) / sizeof(clientTransports[0]); i++) {
const UA_String *prefix = &clientTransports[i].urlSchemePrefix;
if(endpointUrl->length >= prefix->length &&
memcmp(endpointUrl->data, prefix->data, prefix->length) == 0 &&
(!jsonPath || clientTransports[i].encoding ==
UA_SECURECHANNEL_ENCODING_JSON) &&
(!profileUri || profileUri->length == 0 ||
UA_String_equal(profileUri, &clientTransports[i].profileUri)))
return &clientTransports[i];
}
return NULL;
}
static UA_Boolean
isSupportedClientTransportProfile(const UA_String *profileUri) {
for(size_t i = 0;
i < sizeof(clientTransports) / sizeof(clientTransports[0]); i++) {
if(UA_String_equal(profileUri, &clientTransports[i].profileUri))
return true;
}
return false;
}
/* Find a matching endpoint -- the UserTokenPolicy is matched later */
static UA_Boolean
clientTransportMatchesCurrent(UA_Client *client, const UA_String *endpointUrl,
const UA_String *profileUri) {
UA_String activeUrl = getEndpointUrl(client);
const UA_ClientTransport *active = getClientTransport(&activeUrl, NULL);
const UA_ClientTransport *candidate =
getClientTransport(endpointUrl, profileUri);
return active && candidate &&
candidate->transport == client->channel.transport &&
candidate->transport == active->transport &&
candidate->encoding == client->channel.encoding &&
candidate->useTls == active->useTls;
}
static UA_Boolean
endpointMatchesTransport(UA_Client *client,
const UA_EndpointDescription *endpoint) {
/* Keep endpoint selection on the transport, encoding and TLS mode used for
* discovery. Direct HTTP Binary and JSON variants can share an origin but
* represent distinct logical channels. This also prevents an EndpointUrl
* returned by discovery from downgrading opc.https to opc.http. */
const UA_String *profileUri = endpoint->transportProfileUri.length > 0 ?
&endpoint->transportProfileUri : NULL;
return clientTransportMatchesCurrent(client, &endpoint->endpointUrl,
profileUri);
}
static UA_Boolean
matchEndpoint(UA_Client *client, const UA_EndpointDescription *endpoint, unsigned i) {
/* Matching ApplicationUri if defined */
if(client->config.applicationUri.length > 0 &&
!UA_String_equal(&client->config.applicationUri,
&endpoint->server.applicationUri)) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, the server's ApplicationUri %S "
"does not match the client configuration", i,
endpoint->server.applicationUri);
return false;
}
/* Look out for supported transport endpoints. Some servers return an
* empty ProfileUri, which is accepted only if the EndpointUrl remains
* compatible with the active transport below. */
if(endpoint->transportProfileUri.length != 0 &&
!isSupportedClientTransportProfile(&endpoint->transportProfileUri)) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, the TransportProfileUri %S "
"is not supported", i, endpoint->transportProfileUri);
return false;
}
if(!endpointMatchesTransport(client, endpoint)) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, transport, encoding or TLS mode "
"does not match the active endpoint %S", i,
getEndpointUrl(client));
return false;
}
/* Valid SecurityMode? */
if(endpoint->securityMode < 1 || endpoint->securityMode > 3) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, invalid SecurityMode %u",
i, (unsigned)endpoint->securityMode);
return false;
}
UA_MessageSecurityMode configuredSM = client->config.securityMode;
if(configuredSM > UA_MESSAGESECURITYMODE_SIGNANDENCRYPT) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Bad SecurityMode set in the client config");
return false;
}
/* Selected SecurityMode? */
if(configuredSM > 0 && configuredSM != endpoint->securityMode) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, the SecurityMode %s "
"does not match the client configuration (%s)",
i, securityModeNames[endpoint->securityMode],
securityModeNames[configuredSM]);
return false;
}
/* Matching SecurityPolicy? */
if(client->config.securityPolicyUri.length > 0 &&
!UA_String_equal(&client->config.securityPolicyUri,
&endpoint->securityPolicyUri)) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, the SecurityPolicy %S does not "
"match the configuration", i, endpoint->securityPolicyUri);
return false;
}
/* SecurityPolicy available? */
if(!getSecurityPolicy(client, endpoint->securityPolicyUri)) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, the SecurityPolicy %S not supported",
i, endpoint->securityPolicyUri);
return false;
}
return true;
}
/* Match the policy with the configured user token */
static UA_Boolean
matchUserToken(UA_Client *client,
const UA_UserTokenPolicy *tokenPolicy) {
const UA_DataType *tokenType =
client->config.userIdentityToken.content.decoded.type;
if(tokenPolicy->tokenType == UA_USERTOKENTYPE_ANONYMOUS &&
(tokenType == &UA_TYPES[UA_TYPES_ANONYMOUSIDENTITYTOKEN] || !tokenType))
return true;
if(tokenPolicy->tokenType == UA_USERTOKENTYPE_USERNAME &&
tokenType == &UA_TYPES[UA_TYPES_USERNAMEIDENTITYTOKEN])
return true;
if(tokenPolicy->tokenType == UA_USERTOKENTYPE_CERTIFICATE &&
tokenType == &UA_TYPES[UA_TYPES_X509IDENTITYTOKEN])
return true;
if(tokenPolicy->tokenType == UA_USERTOKENTYPE_ISSUEDTOKEN &&
tokenType == &UA_TYPES[UA_TYPES_ISSUEDIDENTITYTOKEN])
return true;
return false;
}
static UA_Boolean
matchUserTokenPolicy(UA_Client *client, UA_EndpointDescription *endpoint,
UA_UserTokenPolicy *utp, char *logPrefix) {
/* Match with the configured UserToken */
if(!matchUserToken(client, utp)) {
if(logPrefix) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: UserTokenPolicy %S rejected -- does not match "
"the type of UserIdentityToken configured in the client",
logPrefix, utp->policyId);
}
return false;
}
/* If not defined, use the SecurityPolicy of the SecureChannel */
UA_String tokenPolicyUri =
(UA_String_isEmpty(&utp->securityPolicyUri)) ?
endpoint->securityPolicyUri : utp->securityPolicyUri;
/* SecurityPolicyUri matches the configuration? */
if(!UA_String_isEmpty(&client->config.authSecurityPolicyUri) &&
!UA_String_equal(&client->config.authSecurityPolicyUri,
&tokenPolicyUri)) {
if(logPrefix) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: UserTokenPolicy %S rejected -- uses SecurityPolicy %S, "
"but the client configuration requires SecurityPolicy %S",
logPrefix, utp->policyId,
tokenPolicyUri, client->config.authSecurityPolicyUri);
}
return false;
}
/* Anoymous authentication */
if(utp->tokenType == UA_USERTOKENTYPE_ANONYMOUS)
return true;
/* Get the SecurityPolicy for the UserIdentityToken (see
* initUserTokenPolicy for the authSecurityPolicies/securityPolicies
* lookup rationale). */
UA_SecurityPolicy *utsp;
if(utp->tokenType == UA_USERTOKENTYPE_CERTIFICATE) {
UA_X509IdentityToken *token = (UA_X509IdentityToken*)
client->config.userIdentityToken.content.decoded.data;
utsp = getAuthSecurityPolicy(client, tokenPolicyUri,
&token->certificateData);
} else {
utsp = getAuthSecurityPolicy(client, tokenPolicyUri, NULL);
if(!utsp)
utsp = getSecurityPolicy(client, tokenPolicyUri);
}
if(!utsp) {
if(logPrefix) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: UserTokenPolicy %S rejected -- the "
"SecurityPolicy %S is not available",
logPrefix, utp->policyId, tokenPolicyUri);
}
return false;
}
/* Check if a secret is transmitted in cleartext */
if(endpoint->securityMode != UA_MESSAGESECURITYMODE_SIGNANDENCRYPT &&
utsp->policyType == UA_SECURITYPOLICYTYPE_NONE) {
if(!client->config.allowNonePolicyPassword) {
if(logPrefix) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: UserTokenPolicy %S rejected -- the "
"AuthenticationToken must not be transmitted "
"without encryption (override with"
"allowNonePolicyPassword setting)",
logPrefix, utp->policyId);
}
return false;
}
}
/* Found a match */
return true;
}
/* Returns a matching UserTokenPolicy from the EndpointDescription. If a
* UserTokenPolicy is configured in the client config, then we need an exact
* match. */
static UA_UserTokenPolicy *
findUserTokenPolicy(UA_Client *client, UA_EndpointDescription *endpoint,
char *logPrefix) {
/* Was a UserTokenPolicy configured? Then we need an exact match. */
UA_UserTokenPolicy *requiredTokenPolicy = NULL;
UA_UserTokenPolicy tmp;
UA_UserTokenPolicy_init(&tmp);
if(!UA_equal(&tmp, &client->config.userTokenPolicy,
&UA_TYPES[UA_TYPES_USERTOKENPOLICY]))
requiredTokenPolicy = &client->config.userTokenPolicy;
/* Return the first UserTokenPolicy matching the config */
for(size_t j = 0; j < endpoint->userIdentityTokensSize; ++j) {
UA_UserTokenPolicy *tokenPolicy = &endpoint->userIdentityTokens[j];
/* Need the extact configured policy */
if(requiredTokenPolicy &&
!UA_equal(requiredTokenPolicy, tokenPolicy,
&UA_TYPES[UA_TYPES_USERTOKENPOLICY])) {
if(logPrefix) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"%s: UserTokenPolicy %S rejected -- a different "
"UserTokenPolicy %S is specified in the client config",
logPrefix, tokenPolicy->policyId,
requiredTokenPolicy->policyId);
}
continue;
}
/* Match with the client configured */
if(matchUserTokenPolicy(client, endpoint, tokenPolicy, logPrefix))
return tokenPolicy;
}
return NULL;
}
/* Combination of UA_Client_getEndpointsInternal and getEndpoints */
static void
responseGetEndpoints(UA_Client *client, void *userdata,
UA_UInt32 requestId, void *response) {
UA_LOCK_ASSERT(&client->clientMutex);
client->endpointsHandshake = false;
UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Received GetEndpointsResponse");
UA_GetEndpointsResponse *resp = (UA_GetEndpointsResponse*)response;
/* GetEndpoints not possible. Fail the connection */
if(resp->responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
/* Fail the connection attempt if the SecureChannel is still connected.
* If the SecureChannel is (intentionally or unintentionally) closed,
* the connectStatus should come from there. */
if(UA_SecureChannel_isConnected(&client->channel)) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"GetEndpointRequest failed with error code %s",
UA_StatusCode_name(resp->responseHeader.serviceResult));
setConnectStatus(client, resp->responseHeader.serviceResult);
}
UA_GetEndpointsResponse_clear(resp);
return;
}
/* Warn if the Endpoints look incomplete / don't match the EndpointUrl */
Client_warnEndpointsResult(client, resp, &client->discoveryUrl);
const size_t notFound = (size_t)-1;
size_t bestEndpointIndex = notFound;
UA_Byte bestEndpointLevel = 0;
UA_UserTokenPolicy *utp = NULL;
/* Find a matching combination of Endpoint and UserTokenPolicy */
for(size_t i = 0; i < resp->endpointsSize; ++i) {
UA_EndpointDescription* endpoint = &resp->endpoints[i];
/* Do we already have a better candidate? */
if(endpoint->securityLevel < bestEndpointLevel) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, better SecurityLevel found before",
(unsigned)i);
continue;
}
/* Does the endpoint match the client configuration? */
if(!matchEndpoint(client, endpoint, (unsigned)i))
continue;
/* Do we want a session? If yes, then the endpoint needs to have a
* UserTokenPolicy that matches the configuration. */
if(!client->config.noSession) {
char logPrefix[32];
mp_snprintf(logPrefix, 32, "Endpoint %u", (unsigned)i);
utp = findUserTokenPolicy(client, endpoint, logPrefix);
if(!utp) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Rejected, no matching UserTokenPolicy",
(unsigned)i);
continue;
}
}
/* Best endpoint so far */
bestEndpointLevel = endpoint->securityLevel;
bestEndpointIndex = i;
if(utp) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Best endpoint so far with "
"UserTokenPolicy %S", (unsigned)i, utp->policyId);
} else {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u: Best endpoint so far", (unsigned)i);
}
}
/* No matching endpoint found */
if(bestEndpointIndex == notFound) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"No suitable endpoint found");
setConnectStatus(client, UA_STATUSCODE_BADIDENTITYTOKENREJECTED);
return;
}
/* Store the endpoint description in the client. It contains the
* ApplicationDescription and the UserTokenPolicies. We continue to look up
* the matching UserTokenPolicy from there. */
UA_EndpointDescription_clear(&client->endpoint);
client->endpoint = resp->endpoints[bestEndpointIndex];
UA_EndpointDescription_init(&resp->endpoints[bestEndpointIndex]);
if(utp) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u selected with SecurityMode "
"%s, SecurityPolicy %S and UserTokenPolicy %S",
bestEndpointIndex,
securityModeNames[client->endpoint.securityMode],
client->endpoint.securityPolicyUri,
utp->policyId);
} else {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Endpoint %u selected with SecurityMode "
"%s and SecurityPolicy %S", bestEndpointIndex,
securityModeNames[client->endpoint.securityMode],
client->endpoint.securityPolicyUri);
}
/* A different SecurityMode or SecurityPolicy is defined by the Endpoint.
* Close the SecureChannel and reconnect. */
UA_SecurityPolicy *sp = client->channel.securityPolicy;
if(client->endpoint.securityMode != client->channel.securityMode ||
!UA_String_equal(&client->endpoint.securityPolicyUri, &sp->policyUri)) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"A different SecurityMode or SecurityPolicy is defined "
"by the selected Endpoint. Close the SecureChannel "
"and reconnect.");
closeSecureChannel(client);
return;
}
/* We were using a distinct discovery URL and we are switching away from it.
* Close the SecureChannel to reopen with the EndpointUrl. If an endpoint
* was selected, then we use the endpointUrl for the HEL message. */
if(client->discoveryUrl.length > 0 &&
!UA_String_equal(&client->discoveryUrl, &client->endpoint.endpointUrl)) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The selected endpoint defines an EndpointUrl %S different "
"from the Url %S used to connect before calling "
"GetEndpoints. Close the SecureChannel and reconnect with "
"the new EndpointUrl to ensure the Endpoint is available.",
client->discoveryUrl, client->endpoint.endpointUrl);
closeSecureChannel(client);
return;
}
/* We have selected an endpoint that we can use to open a Session on the
* current SecureChannel. The remote certificate was validated during
* processing of the OPN response. But if client->endpoint.serverCertificate
* is empty we do not compare against the expectation from the endpoint. */
/* Is the same certificate used for the SecureChannel and the selected
* endpoint?
*
* Both the clientCertificate of this request and the remoteCertificate of
* the channel may contain a partial or a complete certificate chain. The
* compareCertificate function will compare the first certificate of each
* chain. The end certificate shall be located first in the chain according
* to the OPC UA specification Part 6 (1.04), chapter 6.2.3.*/
if(client->channel.securityMode != UA_MESSAGESECURITYMODE_NONE) {
void *cc = client->channel.channelContext;
UA_StatusCode res = sp->compareCertificate(sp, cc,
&client->endpoint.serverCertificate);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The selected endpoint defines a different server certificate "
"from then one used by the server for the initial SecureChannel "
"(to call GetEndpoints). Close the SecureChannel and reconnect.");
closeSecureChannel(client);
return;
}
}
/* Continue with the current SecureChannel */
}
static UA_StatusCode
requestGetEndpoints(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
UA_GetEndpointsRequest request;
UA_GetEndpointsRequest_init(&request);
request.endpointUrl = getEndpointUrl(client);
UA_StatusCode retval =
__Client_AsyncServiceInternal(client, &request, &UA_TYPES[UA_TYPES_GETENDPOINTSREQUEST],
(UA_ClientAsyncServiceCallback) responseGetEndpoints,
&UA_TYPES[UA_TYPES_GETENDPOINTSRESPONSE], NULL, NULL);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"RequestGetEndpoints failed when sending the request with error code %s",
UA_StatusCode_name(retval));
return retval;
}
client->endpointsHandshake = true;
return UA_STATUSCODE_GOOD;
}
static void
responseFindServers(UA_Client *client, void *userdata,
UA_UInt32 requestId, void *response) {
client->findServersHandshake = false;
UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Received FindServersResponse");
/* Error handling. Log the error but continue to connect with the current
* EndpointURL. */
UA_FindServersResponse *fsr = (UA_FindServersResponse*)response;
if(fsr->responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"FindServers failed with error code %s. Continue with the "
"EndpointURL %S.",
UA_StatusCode_name(fsr->responseHeader.serviceResult),
client->config.endpointUrl);
UA_String_clear(&client->discoveryUrl);
UA_String_copy(&client->config.endpointUrl, &client->discoveryUrl);
return;
}
/* Check if one of the returned servers matches the EndpointURL already used */
for(size_t i = 0; i < fsr->serversSize; i++) {
UA_ApplicationDescription *server = &fsr->servers[i];
/* Filter by the ApplicationUri if defined */
if(client->config.applicationUri.length > 0 &&
!UA_String_equal(&client->config.applicationUri, &server->applicationUri))
continue;
for(size_t j = 0; j < server->discoveryUrlsSize; j++) {
if(UA_String_equal(&client->config.endpointUrl, &server->discoveryUrls[j])) {
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The initially defined EndpointURL %S "
"is valid for the server", client->config.endpointUrl);
UA_String_clear(&client->discoveryUrl);
client->discoveryUrl = server->discoveryUrls[j];
UA_String_init(&server->discoveryUrls[j]);
return;
}
}
}
/* The current EndpointURL is not usable. Pick the first DiscoveryUrl with
* a transport supported by this client. */
for(size_t i = 0; i < fsr->serversSize; i++) {
UA_ApplicationDescription *server = &fsr->servers[i];
if(server->applicationType != UA_APPLICATIONTYPE_SERVER &&
server->applicationType != UA_APPLICATIONTYPE_CLIENTANDSERVER &&
server->applicationType != UA_APPLICATIONTYPE_DISCOVERYSERVER)
continue;
/* Filter by the ApplicationUri if defined */
if(client->config.applicationUri.length > 0 &&
!UA_String_equal(&client->config.applicationUri, &server->applicationUri))
continue;
for(size_t j = 0; j < server->discoveryUrlsSize; j++) {
/* Try to parse the DiscoveryUrl. This weeds out http schemas (etc.)
* and invalid DiscoveryUrls in general. */
UA_String hostname, path;
UA_UInt16 port;
UA_StatusCode res =
UA_parseEndpointUrl(&server->discoveryUrls[j], &hostname, &port, &path);
if(res != UA_STATUSCODE_GOOD ||
!clientTransportMatchesCurrent(client,
&server->discoveryUrls[j], NULL))
continue;
/* Use this DiscoveryUrl in the client */
UA_String_clear(&client->discoveryUrl);
client->discoveryUrl = server->discoveryUrls[j];
UA_String_init(&server->discoveryUrls[j]);
UA_LOG_INFO(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Use the EndpointURL %S returned from FindServers and reconnect",
client->discoveryUrl);
/* Close the SecureChannel to build it up new with the correct
* EndpointURL in the HEL/ACK handshake. In closeSecureChannel the
* received client->endpoint is reset also. */
closeSecureChannel(client);
return;
}
}
/* Could not find a suitable server. Try to continue with the
* original EndpointURL. */
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"FindServers did not returned a suitable DiscoveryURL. "
"Continue with the EndpointURL %S.", client->config.endpointUrl);
UA_String_clear(&client->discoveryUrl);
UA_String_copy(&client->config.endpointUrl, &client->discoveryUrl);
}
static UA_StatusCode
requestFindServers(UA_Client *client) {
UA_FindServersRequest request;
UA_FindServersRequest_init(&request);
request.requestHeader.timeoutHint = 10000;
request.endpointUrl = client->config.endpointUrl;
UA_StatusCode retval =
__Client_AsyncServiceInternal(client, &request, &UA_TYPES[UA_TYPES_FINDSERVERSREQUEST],
(UA_ClientAsyncServiceCallback) responseFindServers,
&UA_TYPES[UA_TYPES_FINDSERVERSRESPONSE], NULL, NULL);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"FindServers failed when sending the request with error code %s",
UA_StatusCode_name(retval));
return retval;
}
client->findServersHandshake = true;
return UA_STATUSCODE_GOOD;
}
static void
createSessionCallback(UA_Client *client, void *userdata,
UA_UInt32 requestId, void *response) {
UA_LOCK_ASSERT(&client->clientMutex);
UA_CreateSessionResponse *csr = (UA_CreateSessionResponse*)response;
UA_StatusCode res = csr->responseHeader.serviceResult;
if(res != UA_STATUSCODE_GOOD)
goto cleanup;
if(UA_SecureChannel_hasApplicationSecurity(&client->channel)) {
/* Verify the session response was created with the same certificate as
* the SecureChannel */
if(!UA_ByteString_equal(&csr->serverCertificate,
&client->channel.remoteCertificate)) {
res = UA_STATUSCODE_BADCERTIFICATEINVALID;
goto cleanup;
}
/* Verify the client signature */
res = checkCreateSessionSignature(client, &client->channel, csr);
if(res != UA_STATUSCODE_GOOD)
goto cleanup;
}
/* Copy the SessionId */
UA_NodeId_clear(&client->sessionId);
res |= UA_NodeId_copy(&csr->sessionId, &client->sessionId);
/* Check the (session) nonce length. The session nonce is the application
* nonce (>= 32 bytes), independent of the SecurityPolicy's SecureChannel
* nonce length (e.g. the 64-byte ECC ephemeral key). */
if(csr->serverNonce.length < 32) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Session cannot be created with a nonce "
"that is too short");
res = UA_STATUSCODE_BADSECURITYCHECKSFAILED;
goto cleanup;
}
/* Copy nonce and AuthenticationToken */
UA_ByteString_clear(&client->serverSessionNonce);
UA_NodeId_clear(&client->authenticationToken);
res |= UA_ByteString_copy(&csr->serverNonce, &client->serverSessionNonce);
res |= UA_NodeId_copy(&csr->authenticationToken, &client->authenticationToken);
if(res != UA_STATUSCODE_GOOD)
goto cleanup;
/* Extract the server's ECC ephemeral public key from the CreateSession
* response */
extractEphemeralKeyFromAddHeader(client, &csr->responseHeader.additionalHeader);
/* Activate the new Session */
client->sessionState = UA_SESSIONSTATE_CREATED;
cleanup:
setConnectStatus(client, res);
if(client->connectStatus != UA_STATUSCODE_GOOD)
client->sessionState = UA_SESSIONSTATE_CLOSED;
}
/* OPC UA Part 6 ECC: request an ephemeral ECDH key from the server by adding an
* "ECDHPolicyUri" entry (value = the user-token SecurityPolicy URI) to the
* request's AdditionalHeader. The server returns its ephemeral public key under
* "ECDHKey" in the response AdditionalHeader, which the client needs to encrypt
* an ECC UserIdentityToken. For non-ECC user-token policies this is a no-op.
* The caller must clear the AdditionalHeader after sending. */
static UA_StatusCode
requestServerEphemeralKey(UA_Client *client, UA_RequestHeader *rh) {
UA_SecurityPolicy *utpSp = client->utpSp;
if(!UA_SecurityPolicy_isEcc(utpSp))
return UA_STATUSCODE_GOOD;
UA_AdditionalParametersType *ap = UA_AdditionalParametersType_new();
if(!ap)
return UA_STATUSCODE_BADOUTOFMEMORY;
ap->parameters = (UA_KeyValuePair*)
UA_Array_new(1, &UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
if(!ap->parameters) {
UA_AdditionalParametersType_delete(ap);
return UA_STATUSCODE_BADOUTOFMEMORY;
}
ap->parametersSize = 1;
ap->parameters[0].key = UA_QUALIFIEDNAME_ALLOC(0, "ECDHPolicyUri");
UA_StatusCode res =
UA_Variant_setScalarCopy(&ap->parameters[0].value, &utpSp->policyUri,
&UA_TYPES[UA_TYPES_STRING]);
if(res != UA_STATUSCODE_GOOD) {
UA_AdditionalParametersType_delete(ap);
return res;
}
UA_ExtensionObject_setValue(&rh->additionalHeader, ap,
&UA_TYPES[UA_TYPES_ADDITIONALPARAMETERSTYPE]);
return UA_STATUSCODE_GOOD;
}
static UA_StatusCode
createSessionAsync(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
/* Select the UserTokenPolicy based on the client configuration and the
* endpoint information. Also instantiates the UserTokenPolicy if not
* already there. */
const UA_UserTokenPolicy *utp = NULL;
UA_StatusCode res = initUserTokenPolicy(client, &utp, "CreateSession");
if(res != UA_STATUSCODE_GOOD)
return res;
(void)utp;
/* Create the CreateSession ClientNonce: a 32-byte random nonce when the
* SecureChannel is secured. For ECC SecurityPolicies the ClientNonce is
* NOT the session ephemeral key — the ephemeral keys are exchanged
* separately (the server's arrives in the response AdditionalHeader; the
* client's, when needed for UserIdentityToken encryption, is carried in
* the EccEncryptedSecret). Sending the ephemeral key as the nonce is
* rejected by conformant servers with BadNonceInvalid. */
UA_SecurityPolicy *sp = client->channel.securityPolicy;
if(sp && client->channel.securityMode != UA_MESSAGESECURITYMODE_NONE) {
size_t nonceLength = 32;
if(client->clientSessionNonce.length != nonceLength) {
UA_ByteString_clear(&client->clientSessionNonce);
res = UA_ByteString_allocBuffer(&client->clientSessionNonce, nonceLength);
if(res != UA_STATUSCODE_GOOD)
return res;
}
/* Ensure data[0] is not the 'e' of the "eph" ECC ephemeral-key
* trigger, so generateNonce produces plain random data. */
client->clientSessionNonce.data[0] = 0;
res = sp->generateNonce(sp, client->channel.channelContext,
&client->clientSessionNonce);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"CreateSession could not create the client nonce");
return res;
}
}
/* Prepare the request */
UA_CreateSessionRequest request;
UA_CreateSessionRequest_init(&request);
request.clientNonce = client->clientSessionNonce;
request.requestedSessionTimeout = client->config.requestedSessionTimeout;
request.maxResponseMessageSize = UA_INT32_MAX;
request.endpointUrl = client->endpoint.endpointUrl;
request.clientDescription = client->config.clientDescription;
request.sessionName = client->config.sessionName;
/* Send the certificate that is used for the UserIdentiyToken as the
* ApplicationCertificate. (sp == client->channel.securityPolicy; guarded
* so static analysis sees it cannot be a NULL dereference.) */
if(sp)
request.clientCertificate = sp->localCertificate;
/* For ECC policies, request the server's ephemeral ECDH key (needed to
* encrypt the UserIdentityToken in ActivateSession). */
res = requestServerEphemeralKey(client, &request.requestHeader);
if(res != UA_STATUSCODE_GOOD)
return res;
/* Send the request */
res = __Client_AsyncServiceInternal(client, &request,
&UA_TYPES[UA_TYPES_CREATESESSIONREQUEST],
(UA_ClientAsyncServiceCallback)createSessionCallback,
&UA_TYPES[UA_TYPES_CREATESESSIONRESPONSE], NULL, NULL);
/* The request is encoded+sent synchronously; release the AdditionalHeader. */
UA_ExtensionObject_clear(&request.requestHeader.additionalHeader);
if(res == UA_STATUSCODE_GOOD)
client->sessionState = UA_SESSIONSTATE_CREATE_REQUESTED;
else
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"CreateSession failed when sending the request with "
"error code %s", UA_StatusCode_name(res));
return res;
}
UA_StatusCode
verifyServerCertificateEku(const UA_ClientConfig *config,
const UA_SecurityPolicy *securityPolicy,
const UA_ByteString *certificate) {
if(!securityPolicy || securityPolicy->policyType == UA_SECURITYPOLICYTYPE_NONE)
return UA_STATUSCODE_GOOD;
UA_Boolean ekuRequired =
(securityPolicy->policyType == UA_SECURITYPOLICYTYPE_RSA);
UA_StatusCode res = UA_CertificateUtils_checkExtendedKeyUsage(
certificate, UA_CERTIFICATEEKU_SERVERAUTH, ekuRequired);
if(res == UA_STATUSCODE_GOOD)
return UA_STATUSCODE_GOOD;
/* Parsing errors are certificate errors, not rule violations. */
if(res != UA_STATUSCODE_BADCERTIFICATEUSENOTALLOWED)
return res;
if(config->certificateEkuRule <= UA_RULEHANDLING_WARN) {
UA_LOG_WARNING(config->logging, UA_LOGCATEGORY_SECURITYPOLICY,
"The server certificate does not permit serverAuth");
}
if(config->certificateEkuRule == UA_RULEHANDLING_ABORT)
return res;
return UA_STATUSCODE_GOOD;
}
static UA_StatusCode
initSecurityPolicy(UA_Client *client, const UA_ClientTransport *transport) {
/* Find the SecurityPolicy. Use #None if the endpoint is not (yet)
* configured. */
UA_String secPolicyUri = client->endpoint.securityPolicyUri;
if(secPolicyUri.length == 0)
secPolicyUri = UA_SECURITY_POLICY_NONE_URI;
UA_SecurityPolicy *sp = getSecurityPolicy(client, secPolicyUri);
/* Unknown SecurityPolicy */
if(!sp) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"SecurityPolicy %S not available", secPolicyUri);
return UA_STATUSCODE_BADINTERNALERROR;
}
/* Already initialized -- check we are using the same SecurityPolicy */
if(client->channel.securityPolicy)
return (client->channel.securityPolicy == sp) ?
UA_STATUSCODE_GOOD : UA_STATUSCODE_BADINTERNALERROR;
/* Verify the remote server certificate */
UA_StatusCode res;
if(client->endpoint.serverCertificate.length > 0) {
res = client->config.certificateVerification.
verifyCertificate(&client->config.certificateVerification,
&client->endpoint.serverCertificate);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Cannot validate the server certificate "
"defined for the selected endpoint");
return res;
}
res = verifyServerCertificateEku(&client->config, sp,
&client->endpoint.serverCertificate);
if(res != UA_STATUSCODE_GOOD)
return res;
}
/* If the sender provides a chain of certificates then we shall extract the
* ApplicationInstanceCertificate and ignore the extra bytes. See also: OPC
* UA Part 6, V1.04, 6.7.2.3 Security Header, Table 42 - Asymmetric
* algorithm Security header */
UA_ByteString appInstCert =
getLeafCertificate(client->endpoint.serverCertificate);
/* Direct HTTP has no OPN exchange. Its MessageSecurityMode describes the
* confidentiality of the HTTP carrier, independently of the application
* SecurityPolicy used for Session signatures and identity tokens. */
UA_Boolean directHttp = transport &&
transport->transport == UA_SECURECHANNEL_TRANSPORT_HTTP;
if(directHttp) {
/* Plain HTTP cannot carry an application SecurityPolicy. Validate the
* policy selected from GetEndpoints, not only an explicitly configured
* policy URI. Enhanced policies are rejected by the no-OPN setter. */
if(!transport->useTls &&
sp->policyType != UA_SECURITYPOLICYTYPE_NONE) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_NETWORK,
"opc.http:// only supports SecurityPolicy None");
return UA_STATUSCODE_BADSECURITYPOLICYREJECTED;
}
UA_MessageSecurityMode mode =
UA_SecureChannel_httpSecurityMode(transport->useTls);
return UA_SecureChannel_setSecurityPolicyWithoutOPN(
&client->channel, sp, &appInstCert, mode);
}
/* Instantiate the UACP SecurityPolicy context with the remote certificate */
res = UA_SecureChannel_setSecurityPolicy(&client->channel, sp, &appInstCert);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Cannot instantiate the SecurityPolicy %S "
"with the supplied server certificate", sp->policyUri);
return res;
}
/* Set the SecurityMode. Replace INVALID with NONE as a workaround for
* misconfigured endpoints. */
UA_MessageSecurityMode securityMode = client->endpoint.securityMode;
if(securityMode == UA_MESSAGESECURITYMODE_INVALID)
securityMode = UA_MESSAGESECURITYMODE_NONE;
res = UA_SecureChannel_setSecurityMode(&client->channel, securityMode);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Client configuration uses mismatching "
"MessageSecurityMode==%u for SecurityPolicy %S",
securityMode, sp->policyUri);
}
return res;
}
void
connectActivity(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
UA_LOG_TRACE(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Client connect iterate");
/* Could not connect with an error that canot be recovered from */
if(client->connectStatus != UA_STATUSCODE_GOOD)
return;
/* Already connected */
if(client->sessionState == UA_SESSIONSTATE_ACTIVATED)
return;
/* Switch on the SecureChannel state */
switch(client->channel.state) {
/* Nothing to do if the connection has not opened fully */
case UA_SECURECHANNELSTATE_CONNECTING:
case UA_SECURECHANNELSTATE_REVERSE_CONNECTED:
case UA_SECURECHANNELSTATE_CLOSING:
case UA_SECURECHANNELSTATE_HEL_SENT:
case UA_SECURECHANNELSTATE_OPN_SENT:
return;
/* Send HEL */
case UA_SECURECHANNELSTATE_CONNECTED:
setConnectStatus(client, sendHELMessage(client));
return;
/* ACK receieved. Send OPN. */
case UA_SECURECHANNELSTATE_ACK_RECEIVED:
setConnectStatus(client, sendOPNAsync(client, false)); /* Send OPN */
return;
/* The channel is open -> continue with the Session handling */
case UA_SECURECHANNELSTATE_OPEN:
break;
/* The connection is closed. Reset the SecureChannel and open a new TCP
* connection */
case UA_SECURECHANNELSTATE_CLOSED:
if(client->config.noReconnect)
setConnectStatus(client, UA_STATUSCODE_BADNOTCONNECTED);
else
initConnect(client); /* Sets the connectStatus internally */
return;
/* These states should never occur for the client */
default:
setConnectStatus(client, UA_STATUSCODE_BADINTERNALERROR);
return;
}
/* <-- The SecureChannel is open --> */
/* Ongoing handshake -> Waiting for a response */
if(client->endpointsHandshake || client->findServersHandshake ||
client->namespacesHandshake)
return;
/* Call FindServers to see if we should reconnect with another EndpointUrl.
* This needs to be done before GetEndpoints, as the set of returned
* endpoints may depend on the EndpointUrl used during the initial HEL/ACK
* handshake. */
if(client->discoveryUrl.length == 0) {
setConnectStatus(client, requestFindServers(client));
return;
}
/* GetEndpoints to identify the remote side and/or reset the SecureChannel
* with encryption */
if(endpointUnconfigured(&client->endpoint)) {
setConnectStatus(client, requestGetEndpoints(client));
return;
}
/* Have the final SecureChannel but no session */
if(client->config.noSession)
return;
/* Create and Activate the Session */
switch(client->sessionState) {
/* Send a CreateSessionRequest */
case UA_SESSIONSTATE_CLOSED:
setConnectStatus(client, createSessionAsync(client));
return;
/* Activate the Session */
case UA_SESSIONSTATE_CREATED:
setConnectStatus(client, activateSessionAsync(client));
return;
case UA_SESSIONSTATE_CREATE_REQUESTED:
case UA_SESSIONSTATE_ACTIVATE_REQUESTED:
case UA_SESSIONSTATE_ACTIVATED:
case UA_SESSIONSTATE_CLOSING:
return; /* Nothing to do */
/* These states should never occur for the client */
default:
setConnectStatus(client, UA_STATUSCODE_BADINTERNALERROR);
break;
}
}
/* Verify the OPN response. Note that the client knows the remote server
* certificate before opening the SecureChannel (from the EndpointDescription).
*
* The server certificate is used to initialize the SecureChannel/SecurityPolicy
* before connecting in initSecurityPolicy. The certificate from the endpoint is
* checked to be the same used in the AsymHeader. */
static UA_StatusCode
verifyClientSecureChannelHeader(void *application, UA_SecureChannel *channel,
const UA_AsymmetricAlgorithmSecurityHeader *asymHeader) {
UA_Client *client = (UA_Client*)application;
const UA_SecurityPolicy *sp = channel->securityPolicy;
UA_assert(sp != NULL);
/* Check the SecurityPolicyUri if it is defined. If the SecurityMode is not
* None, then the SecurityPolicyUri must be defined. For None we allow an
* empty SecurityPolicyUri to support all (historical) server behaviors. */
if((asymHeader->securityPolicyUri.length > 0 ||
channel->securityMode != UA_MESSAGESECURITYMODE_NONE) &&
!UA_String_equal(&sp->policyUri, &asymHeader->securityPolicyUri)) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The server uses a different SecurityPolicy than "
"the SecureChannel/Endpoint configured in the client");
return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
}
/* In initSecurityPolicy we already verified
* client->endpoint.serverCertificate and loaded it into the channel context
* of the SecurityPolicy.
*
* If the securityPolicyUri is None (== SecurityMode is None), the server
* shall ignore the ApplicationInstanceCertificate. Otherwise compare that
* the certificate in OPN is the same as in the endpoint.
*
* Both the clientCertificate of this request and of the endpoint may
* contain a partial or a complete certificate chain. The compareCertificate
* function will compare the first certificate of each chain. The end
* certificate shall be located first in the chain according to the OPC UA
* specification Part 6 (1.04), chapter 6.2.3.*/
UA_StatusCode res;
if(channel->securityMode != UA_MESSAGESECURITYMODE_NONE) {
void *cc = channel->channelContext;
res = sp->compareCertificate(sp, cc, &asymHeader->senderCertificate);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The server certificate in the OPN message is "
"different from the EndpointDescription");
return UA_STATUSCODE_BADSECURITYCHECKSFAILED;
}
}
/* Verify the certificate the server assumes on our end */
res = sp->compareCertThumbprint(sp, &asymHeader->receiverCertificateThumbprint);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The client certificate thumprint in the OPN response "
"is incorrect");
return res;
}
return UA_STATUSCODE_GOOD;
}
/* The local ApplicationUri has to match the certificates of the
* SecurityPolicies */
static void
verifyClientApplicationUri(const UA_Client *client) {
#if UA_LOGLEVEL <= 400
const UA_ClientConfig *cc = &client->config;
for(size_t i = 0; i < cc->securityPoliciesSize; i++) {
UA_SecurityPolicy *sp = &cc->securityPolicies[i];
if(sp->policyType == UA_SECURITYPOLICYTYPE_NONE &&
!sp->localCertificate.data)
continue;
UA_StatusCode retval =
UA_CertificateUtils_verifyApplicationUri(&sp->localCertificate,
&cc->clientDescription.applicationUri);
if(retval != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(cc->logging, UA_LOGCATEGORY_CLIENT,
"The ApplicationUri %S in the client's ApplicationDescription "
"does not match the URI specified in the certificate "
"for the SecurityPolicy %S",
cc->clientDescription.applicationUri, sp->policyUri);
}
}
#endif
}
static void
delayedNetworkCallback(void *application, void *context);
static void
__Client_networkCallback(UA_ConnectionManager *cm, uintptr_t connectionId,
void *application, void **connectionContext,
UA_ConnectionState state, const UA_KeyValueMap *params,
UA_ByteString msg) {
/* Take the client lock */
UA_Client *client = (UA_Client*)application;
lockClient(client);
UA_LOG_TRACE(client->config.logging, UA_LOGCATEGORY_CLIENT, "Client network callback");
/* A new connection with no context pointer attached */
if(!*connectionContext) {
/* Inconsistent SecureChannel state. Has to be fresh for a new
* connection. */
if(client->channel.state != UA_SECURECHANNELSTATE_CLOSED &&
client->channel.state != UA_SECURECHANNELSTATE_REVERSE_LISTENING) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Cannot open a connection, the SecureChannel is already used");
setConnectStatus(client, UA_STATUSCODE_BADINTERNALERROR);
notifyClientState(client);
unlockClient(client);
return;
}
/* Initialize the client connection and attach to the EventLoop connection */
client->channel.connectionManager = cm;
client->channel.connectionId = connectionId;
*connectionContext = &client->channel;
}
/* The connection is closing in the EventLoop. This is the last callback
* from that connection. Clean up the SecureChannel in the client. */
if(state == UA_CONNECTIONSTATE_CLOSING) {
UA_LOG_INFO_CHANNEL(client->config.logging, &client->channel,
"SecureChannel closed");
/* Set to closing (could be done already in UA_SecureChannel_shutdown).
* This impacts the handling of cancelled requests below. */
UA_SecureChannelState oldState = client->channel.state;
client->channel.state = UA_SECURECHANNELSTATE_CLOSING;
/* Set the Session to CREATED if it was ACTIVATED */
if(client->sessionState == UA_SESSIONSTATE_ACTIVATED)
client->sessionState = UA_SESSIONSTATE_CREATED;
/* Delete outstanding async services - the RequestId is no longer valid. Do
* this after setting the Session state. Otherwise we send out new Publish
* Requests immediately. */
__Client_AsyncService_removeAll(client, UA_STATUSCODE_BADSECURECHANNELCLOSED);
/* Clean up the channel and set the status to CLOSED */
UA_SecureChannel_clear(&client->channel);
/* The connection closed before it actually opened. Since we are
* connecting asynchronously, this happens when the transport connection
* fails. Try to fall back on the initial EndpointUrl. */
if(oldState == UA_SECURECHANNELSTATE_CONNECTING &&
client->connectStatus == UA_STATUSCODE_GOOD)
setConnectStatus(client, fallbackEndpointUrl(client));
/* Try to reconnect. Duplicate the code instead of continue_connect to
* pacify compiler warnings. */
/* goto continue_connect; */
if(!isFullyConnected(client))
connectActivity(client);
notifyClientState(client);
unlockClient(client);
return;
}
/* Update the SecureChannel state */
if(UA_LIKELY(state == UA_CONNECTIONSTATE_ESTABLISHED)) {
/* The connection is now open on the TCP level. Set the SecureChannel
* state to reflect this. Otherwise later consistency checks for the
* received messages fail. */
if(client->channel.state < UA_SECURECHANNELSTATE_CONNECTED)
client->channel.state = UA_SECURECHANNELSTATE_CONNECTED;
} else /* state == UA_CONNECTIONSTATE_OPENING */ {
/* The connection was opened on our end only. Waiting for the TCP handshake
* to complete. */
client->channel.state = UA_SECURECHANNELSTATE_CONNECTING;
}
UA_EventLoop *el = client->config.eventLoop;
UA_DateTime nowMonotonic = el->dateTime_nowMonotonic(el);
/* Received a message. Process the message with the SecureChannel. */
UA_StatusCode res = UA_SecureChannel_loadBuffer(&client->channel, msg);
while(UA_LIKELY(res == UA_STATUSCODE_GOOD)) {
UA_MessageType messageType = UA_MESSAGETYPE_INVALID;
UA_UInt32 requestId = 0;
UA_ByteString payload = UA_BYTESTRING_NULL;
UA_Boolean copied = false;
res = UA_SecureChannel_getCompleteMessage(&client->channel, &messageType, &requestId,
&payload, &copied, nowMonotonic);
if(res != UA_STATUSCODE_GOOD || payload.length == 0)
break;
res = processServiceResponse(client, &client->channel,
messageType, requestId, &payload);
if(copied)
UA_ByteString_clear(&payload);
/* Abort after synchronous processing of a message.
* Add a delayed callback to process the remaining buffer ASAP. */
if(res == UA_STATUSCODE_GOODCOMPLETESASYNCHRONOUSLY) {
if(client->channel.unprocessed.length > client->channel.unprocessedOffset &&
client->channel.unprocessedDelayed.callback == NULL) {
client->channel.unprocessedDelayed.callback = delayedNetworkCallback;
client->channel.unprocessedDelayed.application = client;
client->channel.unprocessedDelayed.context = &client->channel;
el->addDelayedCallback(el, &client->channel.unprocessedDelayed);
}
res = UA_STATUSCODE_GOOD;
break;
}
}
res |= UA_SecureChannel_persistBuffer(&client->channel);
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Processing the message returned the error code %s",
UA_StatusCode_name(res));
/* If processing the buffer fails before the SecureChannel has opened,
* then the client cannot recover. Set the connectStatus to reflect
* this. The application is notified when the socket has closed. */
if(client->channel.state != UA_SECURECHANNELSTATE_OPEN)
setConnectStatus(client, res);
/* Close the SecureChannel, but don't notify the client right away.
* Return immediately. notifyClientState will be called in the next
* callback from the ConnectionManager when the connection closes with a
* StatusCode.
*
* If the connectStatus is still good (the SecureChannel was fully
* opened before), then a reconnect is attempted. */
closeSecureChannel(client);
unlockClient(client);
return;
}
/* continue_connect: */
/* Trigger the next action from our end to fully open up the connection */
if(!isFullyConnected(client))
connectActivity(client);
notifyClientState(client);
unlockClient(client);
}
static void
delayedNetworkCallback(void *application, void *context) {
UA_Client *client = (UA_Client*)application;
client->channel.unprocessedDelayed.callback = NULL;
if(client->channel.state >= UA_SECURECHANNELSTATE_CONNECTING)
__Client_networkCallback(client->channel.connectionManager,
client->channel.connectionId,
client, &context,
UA_CONNECTIONSTATE_ESTABLISHED,
&UA_KEYVALUEMAP_NULL, UA_BYTESTRING_NULL);
}
/* Initialize a transport connection. Writes the result to
* client->connectStatus. */
static void
initConnect(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
if(client->channel.state != UA_SECURECHANNELSTATE_CLOSED) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Client connection already initiated");
return;
}
UA_StatusCode res = UA_STATUSCODE_BADNOTSUPPORTED;
/* An exact endpoint was configured. Use it. */
if(!endpointUnconfigured(&client->config.endpoint)) {
UA_EndpointDescription_clear(&client->endpoint);
res = UA_EndpointDescription_copy(&client->config.endpoint, &client->endpoint);
if(res != UA_STATUSCODE_GOOD) {
setConnectStatus(client, res);
return;
}
}
/* Start the EventLoop if not already started */
setConnectStatus(client, __UA_Client_startup(client));
if(client->connectStatus != UA_STATUSCODE_GOOD)
return;
/* Consistency check the client's own ApplicationUri.
* Problems are only logged. */
verifyClientApplicationUri(client);
/* Initialize the SecureChannel */
UA_SecureChannel_clear(&client->channel);
client->channel.config = client->config.localConnectionConfig;
client->channel.processOPNHeader = verifyClientSecureChannelHeader;
client->channel.processOPNHeaderApplication = client;
/* Select the transport from the active EndpointUrl. FindServers and
* GetEndpoints can replace the initial URL with a compatible endpoint. */
UA_String endpointUrl;
const UA_ClientTransport *transport;
UA_String hostname = UA_STRING_NULL;
UA_String path = UA_STRING_NULL;
UA_UInt16 port;
while(true) {
endpointUrl = getEndpointUrl(client);
const UA_String *profileUri = endpointUnconfigured(&client->endpoint) ?
NULL : &client->endpoint.transportProfileUri;
transport = getClientTransport(&endpointUrl, profileUri);
if(!transport) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_NETWORK,
"Endpoint URL transport is not supported: %S",
endpointUrl);
res = UA_STATUSCODE_BADNOTSUPPORTED;
} else {
/* Extract hostname, port and path from the URL. */
port = transport->defaultPort;
res = UA_parseEndpointUrl(&endpointUrl, &hostname, &port, &path);
if(res == UA_STATUSCODE_GOOD)
break;
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_NETWORK,
"Endpoint URL is invalid: %S", endpointUrl);
}
/* Discovery can yield an unusable URL. Preserve the established
* fallback to the URL initially configured by the user. */
if(UA_String_equal(&endpointUrl, &client->config.endpointUrl)) {
setConnectStatus(client, res);
return;
}
res = fallbackEndpointUrl(client);
if(res != UA_STATUSCODE_GOOD) {
setConnectStatus(client, res);
return;
}
}
if(transport->transport == UA_SECURECHANNEL_TRANSPORT_HTTP) {
res = __Client_validateHttpConnection(client, transport->useTls);
if(res != UA_STATUSCODE_GOOD) {
setConnectStatus(client, res);
return;
}
}
/* Initialize the SecurityPolicy after selecting the transport. Direct HTTP
* derives its MessageSecurityMode from TLS and does not perform OPN. */
setConnectStatus(client, initSecurityPolicy(client, transport));
if(client->connectStatus != UA_STATUSCODE_GOOD)
return;
UA_String requestPath = UA_STRING_NULL;
res = UA_String_format(&requestPath, "/%S", path);
if(res != UA_STATUSCODE_GOOD) {
setConnectStatus(client, res);
return;
}
/* Initialize the selected transport connection. */
UA_Boolean haveConnectionManager = false;
for(UA_EventSource *es = client->config.eventLoop->eventSources;
es != NULL; es = es->next) {
/* Is this a usable connection manager? */
if(es->eventSourceType != UA_EVENTSOURCETYPE_CONNECTIONMANAGER)
continue;
UA_ConnectionManager *cm = (UA_ConnectionManager*)es;
if(!UA_String_equal(&transport->connectionManagerProtocol,
&cm->protocol))
continue;
haveConnectionManager = true;
UA_KeyValuePair params[12];
size_t paramsSize = 0;
params[paramsSize].key = UA_QUALIFIEDNAME(0, "port");
UA_Variant_setScalar(&params[paramsSize++].value, &port,
&UA_TYPES[UA_TYPES_UINT16]);
params[paramsSize].key = UA_QUALIFIEDNAME(0, "address");
UA_Variant_setScalar(&params[paramsSize++].value, &hostname,
&UA_TYPES[UA_TYPES_STRING]);
UA_Boolean useSSL = transport->useTls;
UA_String subprotocol = UA_STRING_STATIC("opcua+uacp");
UA_Boolean binaryOnly = true;
const UA_String websocketProtocol = UA_STRING_STATIC("websocket");
UA_Boolean webSocket = UA_String_equal(
&transport->connectionManagerProtocol, &websocketProtocol);
if(webSocket) {
params[paramsSize].key = UA_QUALIFIEDNAME(0, "path");
UA_Variant_setScalar(&params[paramsSize++].value, &requestPath,
&UA_TYPES[UA_TYPES_STRING]);
params[paramsSize].key = UA_QUALIFIEDNAME(0, "subprotocol");
UA_Variant_setScalar(&params[paramsSize++].value, &subprotocol,
&UA_TYPES[UA_TYPES_STRING]);
params[paramsSize].key = UA_QUALIFIEDNAME(0, "binary-only");
UA_Variant_setScalar(&params[paramsSize++].value, &binaryOnly,
&UA_TYPES[UA_TYPES_BOOLEAN]);
params[paramsSize].key = UA_QUALIFIEDNAME(0, "useSSL");
UA_Variant_setScalar(&params[paramsSize++].value, &useSSL,
&UA_TYPES[UA_TYPES_BOOLEAN]);
#ifdef UA_ENABLE_LWS
if(client->config.webSocketCaCertificate.length > 0) {
params[paramsSize].key =
UA_QUALIFIEDNAME(0, "ca-certificate");
UA_Variant_setScalar(
&params[paramsSize++].value,
&client->config.webSocketCaCertificate,
&UA_TYPES[UA_TYPES_BYTESTRING]);
}
#endif
params[paramsSize].key =
UA_QUALIFIEDNAME(0, "recv-max-message-size");
UA_Variant_setScalar(
&params[paramsSize++].value,
&client->config.localConnectionConfig.recvBufferSize,
&UA_TYPES[UA_TYPES_UINT32]);
params[paramsSize].key =
UA_QUALIFIEDNAME(0, "send-max-message-size");
UA_Variant_setScalar(
&params[paramsSize++].value,
&client->config.localConnectionConfig.sendBufferSize,
&UA_TYPES[UA_TYPES_UINT32]);
#ifdef UA_ENABLE_LWS
params[paramsSize].key =
UA_QUALIFIEDNAME(0, "send-max-queue-size");
UA_Variant_setScalar(
&params[paramsSize++].value,
&client->config.webSocketMaxQueueSize,
&UA_TYPES[UA_TYPES_UINT32]);
#endif
UA_KeyValueMap paramMap = {paramsSize, params};
res = cm->openConnection(cm, &paramMap, client, NULL,
__Client_networkCallback);
if(res == UA_STATUSCODE_GOOD)
break;
continue;
} else if(transport->transport == UA_SECURECHANNEL_TRANSPORT_UACP) {
params[paramsSize].key = UA_QUALIFIEDNAME(0, "reuse");
UA_Variant_setScalar(&params[paramsSize++].value,
&client->config.tcpReuseAddr,
&UA_TYPES[UA_TYPES_BOOLEAN]);
}
if(transport->transport == UA_SECURECHANNEL_TRANSPORT_HTTP) {
res = __Client_openHttpConnection(
client, cm, &hostname, port, &requestPath, useSSL,
transport->encoding);
if(res == UA_STATUSCODE_GOOD)
break;
continue;
}
UA_KeyValueMap paramMap = {paramsSize, params};
res = cm->openConnection(cm, &paramMap, client, NULL,
__Client_networkCallback);
if(res == UA_STATUSCODE_GOOD)
break;
}
/* No ConnectionManager supports the selected transport. */
if(!haveConnectionManager)
res = UA_STATUSCODE_BADNOTSUPPORTED;
/* Preserve the established public status for transport-open failures. */
if(haveConnectionManager &&
(res != UA_STATUSCODE_GOOD ||
client->channel.state == UA_SECURECHANNELSTATE_CLOSED))
res = UA_STATUSCODE_BADCONNECTIONCLOSED;
/* Opening the transport connection failed */
if(res != UA_STATUSCODE_GOOD || client->connectStatus != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Could not open a transport connection to %S",
endpointUrl);
if(client->connectStatus == UA_STATUSCODE_GOOD)
setConnectStatus(client, res);
}
UA_String_clear(&requestPath);
}
void
connectSync(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
/* Initialize the connection */
initConnect(client);
notifyClientState(client);
if(client->connectStatus != UA_STATUSCODE_GOOD)
return;
/* EventLoop is started. Otherwise initConnect would have failed. */
UA_EventLoop *el = client->config.eventLoop;
UA_assert(el);
UA_DateTime now = el->dateTime_nowMonotonic(el);
UA_DateTime maxDate = now + ((UA_DateTime)client->config.timeout * UA_DATETIME_MSEC);
/* Run the EventLoop until connected, connect fail or timeout. Continue
* to iterate when the channel is in CLOSING state so that the pending
* close callback is processed and the channel fully transitions to CLOSED
* before returning to the caller. */
while((client->connectStatus == UA_STATUSCODE_GOOD && !isFullyConnected(client)) ||
client->channel.state == UA_SECURECHANNELSTATE_CLOSING) {
/* Timeout -> abort */
now = el->dateTime_nowMonotonic(el);
if(maxDate < now) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The connection has timed out before it could be fully opened");
setConnectStatus(client, UA_STATUSCODE_BADTIMEOUT);
/* Continue to run. The loop continues while the channel is
* CLOSING so the close callback is processed. */
}
/* Drop into the EventLoop */
UA_StatusCode res = el->run(el, (UA_UInt32)((maxDate - now) / UA_DATETIME_MSEC));
if(res != UA_STATUSCODE_GOOD)
setConnectStatus(client, res);
}
}
UA_StatusCode
connectInternal(UA_Client *client, UA_Boolean async) {
UA_LOCK_ASSERT(&client->clientMutex);
/* Reset the connectStatus. This should be the only place where we can
* recover from a bad connectStatus. */
client->connectStatus = UA_STATUSCODE_GOOD;
if(async)
initConnect(client);
else
connectSync(client);
notifyClientState(client);
return client->connectStatus;
}
UA_StatusCode
connectSecureChannel(UA_Client *client, const char *endpointUrl) {
UA_LOCK_ASSERT(&client->clientMutex);
UA_ClientConfig *cc = UA_Client_getConfig(client);
cc->noSession = true;
UA_String_clear(&cc->endpointUrl);
cc->endpointUrl = UA_STRING_ALLOC(endpointUrl);
return connectInternal(client, false);
}
UA_StatusCode
__UA_Client_connect(UA_Client *client, UA_Boolean async, const char *endpointUrl) {
lockClient(client);
UA_ClientConfig *cc = UA_Client_getConfig(client);
if(endpointUrl) {
UA_String_clear(&cc->endpointUrl);
cc->endpointUrl = UA_STRING_ALLOC(endpointUrl);
}
connectInternal(client, async);
unlockClient(client);
return client->connectStatus;
}
static UA_StatusCode
activateSessionSync(UA_Client *client) {
UA_LOCK_ASSERT(&client->clientMutex);
/* EventLoop is started. Otherwise activateSessionAsync would have failed. */
UA_EventLoop *el = client->config.eventLoop;
UA_assert(el);
UA_DateTime now = el->dateTime_nowMonotonic(el);
UA_DateTime maxDate = now + ((UA_DateTime)client->config.timeout * UA_DATETIME_MSEC);
/* Try to activate */
UA_StatusCode res = activateSessionAsync(client);
if(res != UA_STATUSCODE_GOOD)
return res;
/* Continue iterating while the session is being activated or the
* channel is draining (CLOSING) after a timeout. */
while((client->sessionState != UA_SESSIONSTATE_ACTIVATED &&
client->connectStatus == UA_STATUSCODE_GOOD) ||
client->channel.state == UA_SECURECHANNELSTATE_CLOSING) {
/* Timeout -> abort */
now = el->dateTime_nowMonotonic(el);
if(maxDate < now) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"The connection has timed out before it could be fully opened");
setConnectStatus(client, UA_STATUSCODE_BADTIMEOUT);
/* Continue to run. The loop continues while the channel is
* CLOSING so the close callback is processed. */
}
/* Drop into the EventLoop */
res = el->run(el, (UA_UInt32)((maxDate - now) / UA_DATETIME_MSEC));
if(res != UA_STATUSCODE_GOOD)
setConnectStatus(client, res);
}
return client->connectStatus;
}
UA_StatusCode
UA_Client_activateCurrentSession(UA_Client *client) {
lockClient(client);
UA_StatusCode res = activateSessionSync(client);
notifyClientState(client);
unlockClient(client);
return res != UA_STATUSCODE_GOOD ? res : client->connectStatus;
}
UA_StatusCode
UA_Client_activateCurrentSessionAsync(UA_Client *client) {
lockClient(client);
UA_StatusCode res = activateSessionAsync(client);
notifyClientState(client);
unlockClient(client);
return res != UA_STATUSCODE_GOOD ? res : client->connectStatus;
}
UA_StatusCode
UA_Client_getSessionAuthenticationToken(UA_Client *client,
UA_NodeId *authenticationToken,
UA_ByteString *serverNonce) {
lockClient(client);
if(client->sessionState != UA_SESSIONSTATE_CREATED &&
client->sessionState != UA_SESSIONSTATE_ACTIVATED) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"There is no current session");
unlockClient(client);
return UA_STATUSCODE_BADSESSIONCLOSED;
}
UA_StatusCode res =
UA_NodeId_copy(&client->authenticationToken, authenticationToken);
res |= UA_ByteString_copy(&client->serverSessionNonce, serverNonce);
unlockClient(client);
return res;
}
static UA_StatusCode
switchSession(UA_Client *client,
const UA_NodeId authenticationToken,
const UA_ByteString serverNonce) {
/* Check that there is no pending session in the client */
if(client->sessionState != UA_SESSIONSTATE_CLOSED) {
UA_LOG_ERROR(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Cannot activate a session with a different AuthenticationToken "
"when the client already has a Session.");
return UA_STATUSCODE_BADINTERNALERROR;
}
/* Replace token and nonce */
UA_NodeId_clear(&client->authenticationToken);
UA_ByteString_clear(&client->serverSessionNonce);
UA_StatusCode res = UA_NodeId_copy(&authenticationToken,
&client->authenticationToken);
res |= UA_ByteString_copy(&serverNonce, &client->serverSessionNonce);
if(res != UA_STATUSCODE_GOOD)
return res;
/* Notify that we have now a created session */
client->sessionState = UA_SESSIONSTATE_CREATED;
notifyClientState(client);
return UA_STATUSCODE_GOOD;
}
UA_StatusCode
UA_Client_activateSession(UA_Client *client,
const UA_NodeId authenticationToken,
const UA_ByteString serverNonce) {
lockClient(client);
UA_StatusCode res = switchSession(client, authenticationToken, serverNonce);
if(res != UA_STATUSCODE_GOOD) {
unlockClient(client);
return res;
}
res = activateSessionSync(client);
notifyClientState(client);
unlockClient(client);
return res != UA_STATUSCODE_GOOD ? res : client->connectStatus;
}
UA_StatusCode
UA_Client_activateSessionAsync(UA_Client *client,
const UA_NodeId authenticationToken,
const UA_ByteString serverNonce) {
lockClient(client);
UA_StatusCode res = switchSession(client, authenticationToken, serverNonce);
if(res != UA_STATUSCODE_GOOD) {
unlockClient(client);
return res;
}
res = activateSessionAsync(client);
notifyClientState(client);
unlockClient(client);
return res != UA_STATUSCODE_GOOD ? res : client->connectStatus;
}
static void
disconnectListenSockets(UA_Client *client) {
UA_ConnectionManager *cm = client->reverseConnectionCM;
for(size_t i = 0; i < 16; i++) {
if(client->reverseConnectionIds[i] != 0)
cm->closeConnection(cm, client->reverseConnectionIds[i]);
}
}
/* ConnectionContext meaning:
* - NULL: New listen connection
* - &client->channel: Established active socket to a server
* - &client->reverseConnectionIds[*] == connectionId: Established listen socket
* - &client->reverseConnectionIds[*] != connectionId: New active socket */
static void
__Client_reverseConnectCallback(UA_ConnectionManager *cm, uintptr_t connectionId,
void *application, void **connectionContext,
UA_ConnectionState state, const UA_KeyValueMap *params,
UA_ByteString msg) {
UA_Client *client = (UA_Client*)application;
lockClient(client);
if(!*connectionContext) {
/* Store the new listen connection */
size_t i = 0;
for(; i < 16; i++) {
if(client->reverseConnectionIds[i] == 0) {
client->reverseConnectionIds[i] = connectionId;
client->reverseConnectionCM = cm;
*connectionContext = &client->reverseConnectionIds[i];
if(client->channel.state == UA_SECURECHANNELSTATE_CLOSED)
client->channel.state = UA_SECURECHANNELSTATE_REVERSE_LISTENING;
break;
}
}
/* All slots are full, close */
if(i == 16) {
cm->closeConnection(cm, connectionId);
unlockClient(client);
return;
}
} else if(*connectionContext == &client->channel ||
*(uintptr_t*)*connectionContext != connectionId) {
/* Active socket */
/* New active socket */
if(*connectionContext != &client->channel) {
/* The client already has an active connection */
if(client->channel.connectionId) {
cm->closeConnection(cm, connectionId);
unlockClient(client);
return;
}
/* Set the connection the SecureChannel */
client->channel.connectionId = connectionId;
client->channel.connectionManager = cm;
*connectionContext = &client->channel;
/* Don't keep the listen sockets when an active connection is open */
disconnectListenSockets(client);
/* Set the channel state. The notification callback is called within
* __Client_reverseConnectCallback. */
if(client->channel.state == UA_SECURECHANNELSTATE_REVERSE_LISTENING)
client->channel.state = UA_SECURECHANNELSTATE_REVERSE_CONNECTED;
}
/* Handle the active connection in the normal network callback */
unlockClient(client);
__Client_networkCallback(cm, connectionId, application,
connectionContext, state, params, msg);
return;
}
/* Close the listen socket. Was this the last one? */
if(state == UA_CONNECTIONSTATE_CLOSING) {
UA_Byte count = 0;
for(size_t i = 0; i < 16; i++) {
if(client->reverseConnectionIds[i] == connectionId)
client->reverseConnectionIds[i] = 0;
if(client->reverseConnectionIds[i] != 0)
count++;
}
/* The last connection was closed */
if(count == 0 && client->channel.connectionId == 0)
client->channel.state = UA_SECURECHANNELSTATE_CLOSED;
}
notifyClientState(client);
unlockClient(client);
}
UA_StatusCode
UA_Client_startListeningForReverseConnect(UA_Client *client,
const UA_String *listenHostnames,
size_t listenHostnamesLength,
UA_UInt16 port) {
lockClient(client);
if(client->channel.state != UA_SECURECHANNELSTATE_CLOSED) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Unable to listen for reverse connect while the client "
"is connected or already listening");
unlockClient(client);
return UA_STATUSCODE_BADINVALIDSTATE;
}
const UA_String tcpString = UA_STRING_STATIC("tcp");
UA_StatusCode res = UA_STATUSCODE_BADINTERNALERROR;
client->connectStatus = UA_STATUSCODE_GOOD;
client->channel.renewState = UA_SECURECHANNELRENEWSTATE_NORMAL;
UA_SecureChannel_init(&client->channel);
client->channel.config = client->config.localConnectionConfig;
client->channel.processOPNHeader = verifyClientSecureChannelHeader;
client->channel.processOPNHeaderApplication = client;
client->channel.connectionId = 0;
setConnectStatus(client, initSecurityPolicy(client, NULL));
if(client->connectStatus != UA_STATUSCODE_GOOD)
return client->connectStatus;
UA_EventLoop *el = client->config.eventLoop;
if(!el) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"No EventLoop configured");
unlockClient(client);
return UA_STATUSCODE_BADINTERNALERROR;
}
if(el->state != UA_EVENTLOOPSTATE_STARTED) {
res = el->start(el);
UA_CHECK_STATUS(res, unlockClient(client); return res);
}
UA_ConnectionManager *cm = NULL;
for(UA_EventSource *es = el->eventSources; es != NULL; es = es->next) {
if(es->eventSourceType != UA_EVENTSOURCETYPE_CONNECTIONMANAGER)
continue;
cm = (UA_ConnectionManager*)es;
if(UA_String_equal(&tcpString, &cm->protocol))
break;
cm = NULL;
}
if(!cm) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Could not find a TCP connection manager, unable to "
"listen for reverse connect");
unlockClient(client);
return UA_STATUSCODE_BADINTERNALERROR;
}
client->channel.connectionManager = cm;
UA_KeyValuePair params[4];
bool booleanTrue = true;
params[0].key = UA_QUALIFIEDNAME(0, "port");
UA_Variant_setScalar(&params[0].value, &port,
&UA_TYPES[UA_TYPES_UINT16]);
params[1].key = UA_QUALIFIEDNAME(0, "address");
UA_Variant_setArray(&params[1].value, (void *)(uintptr_t)listenHostnames,
listenHostnamesLength, &UA_TYPES[UA_TYPES_STRING]);
params[2].key = UA_QUALIFIEDNAME(0, "listen");
UA_Variant_setScalar(&params[2].value, &booleanTrue,
&UA_TYPES[UA_TYPES_BOOLEAN]);
params[3].key = UA_QUALIFIEDNAME(0, "reuse");
UA_Variant_setScalar(&params[3].value, &client->config.tcpReuseAddr,
&UA_TYPES[UA_TYPES_BOOLEAN]);
UA_KeyValueMap paramMap;
paramMap.map = params;
paramMap.mapSize = 4;
res = cm->openConnection(cm, &paramMap, client, NULL,
__Client_reverseConnectCallback);
/* Opening the TCP connection failed */
if(res != UA_STATUSCODE_GOOD) {
UA_LOG_WARNING(client->config.logging, UA_LOGCATEGORY_CLIENT,
"Failed to open a listening TCP socket for "
"reverse connect");
res = UA_STATUSCODE_BADCONNECTIONCLOSED;
}
unlockClient(client);
return res;
}
/************************/
/* Close the Connection */
/************************/
void
closeSecureChannel(UA_Client *client) {
/* If we close SecureChannel when the Session is still active, set to
* created. Otherwise the Session would remain active until the connection
* callback is called for the closing connection. */
if(client->sessionState == UA_SESSIONSTATE_ACTIVATED)
client->sessionState = UA_SESSIONSTATE_CREATED;
/* Prevent recursion */
if(client->channel.state == UA_SECURECHANNELSTATE_CLOSING ||
client->channel.state == UA_SECURECHANNELSTATE_CLOSED)
return;
UA_LOG_DEBUG_CHANNEL(client->config.logging, &client->channel,
"Closing the channel");
disconnectListenSockets(client);
/* Only UACP has a CloseSecureChannel message. Direct HTTP proceeds
* immediately to transport shutdown. */
if(client->channel.state == UA_SECURECHANNELSTATE_OPEN &&
client->channel.transport == UA_SECURECHANNEL_TRANSPORT_UACP) {
UA_LOG_DEBUG_CHANNEL(client->config.logging, &client->channel,
"Sending the CLO message");
UA_EventLoop *el = client->config.eventLoop;
/* Manually set up the header (otherwise done in sendRequest) */
UA_CloseSecureChannelRequest request;
UA_CloseSecureChannelRequest_init(&request);
request.requestHeader.requestHandle = ++client->requestHandle;
request.requestHeader.timestamp = el->dateTime_now(el);
request.requestHeader.timeoutHint = client->config.timeout;
request.requestHeader.authenticationToken = client->authenticationToken;
UA_SecureChannel_sendCLO(&client->channel,
__Client_nextRequestId(client), &request);
}
/* The connection is eventually closed in the next callback from the
* ConnectionManager with the appropriate status code. Don't set the
* connection closed right away! */
UA_SecureChannel_shutdown(&client->channel, UA_SHUTDOWNREASON_CLOSE);
}
static void
sendCloseSession(UA_Client *client) {
UA_CloseSessionRequest request;
UA_CloseSessionRequest_init(&request);
request.deleteSubscriptions = true;
UA_CloseSessionResponse response;
__Client_Service(client, &request, &UA_TYPES[UA_TYPES_CLOSESESSIONREQUEST],
&response, &UA_TYPES[UA_TYPES_CLOSESESSIONRESPONSE]);
UA_CloseSessionRequest_clear(&request);
UA_CloseSessionResponse_clear(&response);
/* Set after sending the message to prevent immediate reoping during the
* service call */
client->sessionState = UA_SESSIONSTATE_CLOSING;
}
void
cleanupSession(UA_Client *client) {
UA_NodeId_clear(&client->sessionId);
UA_NodeId_clear(&client->authenticationToken);
client->requestHandle = 0;
#ifdef UA_ENABLE_SUBSCRIPTIONS
/* We need to clean up the subscriptions */
__Client_Subscriptions_clear(client);
#endif
/* Delete outstanding async services */
__Client_AsyncService_removeAll(client, UA_STATUSCODE_BADSESSIONCLOSED);
#ifdef UA_ENABLE_SUBSCRIPTIONS
client->currentlyOutStandingPublishRequests = 0;
#endif
client->sessionState = UA_SESSIONSTATE_CLOSED;
/* Clean the latest server's ephemeral public key */
UA_ByteString_clear(&client->serverEphemeralPubKey);
if(client->utpSp && client->utpSpContext) {
client->utpSp->deleteChannelContext(client->utpSp, client->utpSpContext);
client->utpSp = NULL;
client->utpSpContext = NULL;
}
}
static void
disconnectSecureChannel(UA_Client *client, UA_Boolean sync) {
/* Clean the DiscoveryUrl and endpoint description when the connection is
* explicitly closed */
UA_String_clear(&client->discoveryUrl);
UA_EndpointDescription_clear(&client->endpoint);
/* Manually set the status to closed to prevent an automatic reconnection.
* Do this before closing because some ConnectionManagers report the close
* synchronously. Notify the client at the end. */
if(client->connectStatus == UA_STATUSCODE_GOOD)
client->connectStatus = UA_STATUSCODE_BADCONNECTIONCLOSED;
/* Close the SecureChannel */
closeSecureChannel(client);
/* In the synchronous case, loop until the client has actually closed. */
UA_EventLoop *el = client->config.eventLoop;
if(sync && el &&
el->state != UA_EVENTLOOPSTATE_FRESH &&
el->state != UA_EVENTLOOPSTATE_STOPPED) {
while(client->channel.state != UA_SECURECHANNELSTATE_CLOSED) {
UA_StatusCode runStatus = el->run(el, 100);
if(runStatus != UA_STATUSCODE_GOOD) {
UA_LOG_DEBUG(client->config.logging, UA_LOGCATEGORY_CLIENT,
"EventLoop run returned %s during synchronous disconnect, "
"stopping wait loop", UA_StatusCode_name(runStatus));
break;
}
}
}
notifyClientState(client);
}
UA_StatusCode
UA_Client_disconnectSecureChannel(UA_Client *client) {
lockClient(client);
disconnectSecureChannel(client, true);
unlockClient(client);
return UA_STATUSCODE_GOOD;
}
UA_StatusCode
UA_Client_disconnectSecureChannelAsync(UA_Client *client) {
lockClient(client);
disconnectSecureChannel(client, false);
unlockClient(client);
return UA_STATUSCODE_GOOD;
}
UA_StatusCode
UA_Client_disconnect(UA_Client *client) {
lockClient(client);
if(client->sessionState == UA_SESSIONSTATE_ACTIVATED)
sendCloseSession(client);
cleanupSession(client);
disconnectSecureChannel(client, true);
unlockClient(client);
return UA_STATUSCODE_GOOD;
}
static void
closeSessionCallback(UA_Client *client, void *userdata,
UA_UInt32 requestId, void *response) {
lockClient(client);
cleanupSession(client);
disconnectSecureChannel(client, false);
notifyClientState(client);
unlockClient(client);
}
UA_StatusCode
UA_Client_disconnectAsync(UA_Client *client) {
lockClient(client);
if(client->sessionState == UA_SESSIONSTATE_CLOSED ||
client->sessionState == UA_SESSIONSTATE_CLOSING) {
disconnectSecureChannel(client, false);
notifyClientState(client);
unlockClient(client);
return UA_STATUSCODE_GOOD;
}
/* Set before sending the message to prevent recursion */
client->sessionState = UA_SESSIONSTATE_CLOSING;
UA_CloseSessionRequest request;
UA_CloseSessionRequest_init(&request);
request.deleteSubscriptions = true;
UA_StatusCode res =
__Client_AsyncServiceInternal(client, &request, &UA_TYPES[UA_TYPES_CLOSESESSIONREQUEST],
(UA_ClientAsyncServiceCallback)closeSessionCallback,
&UA_TYPES[UA_TYPES_CLOSESESSIONRESPONSE], NULL, NULL);
if(res != UA_STATUSCODE_GOOD) {
/* Sending the close request failed. Continue to close the connection
* anyway. */
cleanupSession(client);
disconnectSecureChannel(client, false);
}
notifyClientState(client);
unlockClient(client);
return res;
}