Files
open62541/tests/client/check_client_http.c
Julius Pfrommer 970e1f6f9c fix(core): Make session closure reentrant-safe
Model server sessions with explicit CREATED, ACTIVATED, and CLOSED states. Mark a session CLOSED, detach it, and remove it from lookup before callbacks can re-enter, while delaying destructive access-control and resource teardown until current jobs finish.

Reject sessions closed by service, notification, and access-control callbacks, stop multi-operation processing after closure, and preserve progress during UACP and HTTP channel teardown. Add regression coverage for callback reentrancy, shared TCP/WebSocket UACP teardown, and HTTP session cleanup.
2026-09-01 20:56:41 +02:00

881 lines
39 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 2026 (c) o6 Automation GmbH (Author: Julius Pfrommer)
*/
#include "ua_types_encoding_binary.h"
#include "ua_server_internal.h"
#include <open62541/client.h>
#include <open62541/client_config_default.h>
#include <open62541/client_highlevel.h>
#include <open62541/client_highlevel_async.h>
#include <open62541/client_subscriptions.h>
#include "client/ua_client_internal.h"
#include <open62541/server.h>
#include <open62541/server_config_default.h>
#include <check.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
size_t callbacks;
UA_StatusCode status;
} AsyncReadResult;
typedef struct {
size_t notifications;
} DataChangeResult;
static void
asyncReadResultCallback(UA_Client *client, void *userdata,
UA_UInt32 requestId, UA_ReadResponse *response) {
(void)client;
(void)requestId;
AsyncReadResult *result = (AsyncReadResult *)userdata;
result->callbacks++;
result->status = response->responseHeader.serviceResult;
}
static void
dataChangeResultCallback(UA_Client *client, UA_UInt32 subscriptionId,
void *subscriptionContext, UA_UInt32 monitoredItemId,
void *monitoredItemContext, UA_DataValue *value) {
(void)client;
(void)subscriptionId;
(void)subscriptionContext;
(void)monitoredItemId;
(void)value;
DataChangeResult *result = (DataChangeResult *)monitoredItemContext;
result->notifications++;
}
static size_t
countPendingResponses(UA_Client *client, const UA_DataType *responseType) {
size_t count = 0;
lockClient(client);
AsyncServiceCall *ac;
LIST_FOREACH(ac, &client->asyncServiceCalls, pointers) {
if(ac->responseType == responseType)
count++;
}
unlockClient(client);
return count;
}
static void
addSyntheticReadCall(UA_Client *client, UA_UInt32 requestId,
AsyncReadResult *result) {
AsyncServiceCall *ac = (AsyncServiceCall *)UA_calloc(1, sizeof(*ac));
ck_assert_ptr_nonnull(ac);
ac->callback = (UA_ClientAsyncServiceCallback)asyncReadResultCallback;
ac->userdata = result;
ac->responseType = &UA_TYPES[UA_TYPES_READRESPONSE];
ac->requestId = requestId;
ac->timeout = 1000;
lockClient(client);
LIST_INSERT_HEAD(&client->asyncServiceCalls, ac, pointers);
unlockClient(client);
}
static void
injectHttpClientResponseStatus(UA_Client *client, UA_UInt32 requestId,
UA_UInt16 statusCode,
const UA_String *contentType,
UA_ByteString body,
UA_Boolean includeRequestStatus) {
UA_Boolean complete = true;
UA_StatusCode requestStatus = UA_STATUSCODE_GOOD;
UA_KeyValuePair header = {UA_QUALIFIEDNAME(0, "content-type"), {0}};
if(contentType)
UA_Variant_setScalar(&header.value, (void *)(uintptr_t)contentType,
&UA_TYPES[UA_TYPES_STRING]);
UA_KeyValuePair params[5] = {0};
params[0].key = UA_QUALIFIEDNAME(0, "status-code");
UA_Variant_setScalar(&params[0].value, &statusCode,
&UA_TYPES[UA_TYPES_UINT16]);
params[1].key = UA_QUALIFIEDNAME(0, "headers");
UA_Variant_setArray(&params[1].value, &header, contentType ? 1 : 0,
&UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
params[2].key = UA_QUALIFIEDNAME(0, "handle");
UA_Variant_setScalar(&params[2].value, &requestId,
&UA_TYPES[UA_TYPES_UINT32]);
params[3].key = UA_QUALIFIEDNAME(0, "response-complete");
UA_Variant_setScalar(&params[3].value, &complete,
&UA_TYPES[UA_TYPES_BOOLEAN]);
size_t paramsSize = 4;
if(includeRequestStatus) {
params[4].key = UA_QUALIFIEDNAME(0, "request-status");
UA_Variant_setScalar(&params[4].value, &requestStatus,
&UA_TYPES[UA_TYPES_STATUSCODE]);
paramsSize++;
}
UA_KeyValueMap map = {paramsSize, params};
void *connectionContext = &client->channel;
__Client_httpConnectionCallback(
client->channel.connectionManager, client->channel.connectionId,
client, &connectionContext, UA_CONNECTIONSTATE_ESTABLISHED, &map,
body);
}
static void
injectHttpClientResponse(UA_Client *client, UA_UInt32 requestId,
const UA_String *contentType, UA_ByteString body,
UA_Boolean includeRequestStatus) {
injectHttpClientResponseStatus(client, requestId, 200, contentType, body,
includeRequestStatus);
}
static void
injectHttpClientResponseHeaders(UA_Client *client, UA_UInt32 requestId,
UA_KeyValuePair *headers, size_t headersSize,
UA_ByteString body) {
UA_UInt16 statusCode = 200;
UA_Boolean complete = true;
UA_StatusCode requestStatus = UA_STATUSCODE_GOOD;
UA_KeyValuePair params[5] = {0};
params[0].key = UA_QUALIFIEDNAME(0, "status-code");
UA_Variant_setScalar(&params[0].value, &statusCode,
&UA_TYPES[UA_TYPES_UINT16]);
params[1].key = UA_QUALIFIEDNAME(0, "headers");
UA_Variant_setArray(&params[1].value, headers, headersSize,
&UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
params[2].key = UA_QUALIFIEDNAME(0, "handle");
UA_Variant_setScalar(&params[2].value, &requestId,
&UA_TYPES[UA_TYPES_UINT32]);
params[3].key = UA_QUALIFIEDNAME(0, "response-complete");
UA_Variant_setScalar(&params[3].value, &complete,
&UA_TYPES[UA_TYPES_BOOLEAN]);
params[4].key = UA_QUALIFIEDNAME(0, "request-status");
UA_Variant_setScalar(&params[4].value, &requestStatus,
&UA_TYPES[UA_TYPES_STATUSCODE]);
UA_KeyValueMap map = {5, params};
void *connectionContext = &client->channel;
__Client_httpConnectionCallback(
client->channel.connectionManager, client->channel.connectionId,
client, &connectionContext, UA_CONNECTIONSTATE_ESTABLISHED, &map,
body);
}
static UA_ByteString
encodeServiceResponse(const void *response, const UA_DataType *responseType) {
size_t size = UA_calcSizeBinary(
&responseType->binaryEncodingId, &UA_TYPES[UA_TYPES_NODEID], NULL) +
UA_calcSizeBinary(response, responseType, NULL);
UA_ByteString body;
ck_assert_uint_eq(UA_ByteString_allocBuffer(&body, size),
UA_STATUSCODE_GOOD);
UA_Byte *pos = body.data;
const UA_Byte *end = &body.data[body.length];
ck_assert_uint_eq(UA_encodeBinaryInternal(
&responseType->binaryEncodingId,
&UA_TYPES[UA_TYPES_NODEID],
&pos, &end, NULL, NULL, NULL),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(UA_encodeBinaryInternal(
response, responseType, &pos, &end,
NULL, NULL, NULL), UA_STATUSCODE_GOOD);
return body;
}
static UA_UInt16
getAdvertisedPort(UA_ServerConfig *config, const char *prefix,
size_t prefixLength, UA_String *advertisedUrl) {
for(size_t i = 0; i < config->applicationDescription.discoveryUrlsSize;
i++) {
const UA_String *url = &config->applicationDescription.discoveryUrls[i];
if(url->length < prefixLength ||
memcmp(url->data, prefix, prefixLength) != 0)
continue;
UA_String hostname = UA_STRING_NULL;
UA_UInt16 port = 0;
ck_assert_uint_eq(UA_parseEndpointUrl(url, &hostname, &port, NULL),
UA_STATUSCODE_GOOD);
if(advertisedUrl)
ck_assert_uint_eq(UA_String_copy(url, advertisedUrl),
UA_STATUSCODE_GOOD);
return port;
}
return 0;
}
typedef enum {
HTTP_CLIENT_SCENARIO_SECURITY,
HTTP_CLIENT_SCENARIO_SERVICES,
HTTP_CLIENT_SCENARIO_SESSION_REENTRANCY,
HTTP_CLIENT_SCENARIO_ENDPOINT,
HTTP_CLIENT_SCENARIO_JSON
} HttpClientScenario;
static UA_Boolean closeHttpSessionAtServiceBegin;
static UA_StatusCode closeHttpSessionResult;
static void
closeHttpSessionFromServiceNotification(
UA_Server *server, UA_ApplicationNotificationType type,
const UA_KeyValueMap payload) {
if(type != UA_APPLICATIONNOTIFICATIONTYPE_SERVICE_BEGIN ||
!closeHttpSessionAtServiceBegin)
return;
closeHttpSessionAtServiceBegin = false;
const UA_NodeId *sessionId = (const UA_NodeId *)payload.map[1].value.data;
closeHttpSessionResult = UA_Server_closeSession(server, sessionId);
}
static void
runClientScenario(HttpClientScenario scenario) {
UA_Server *server = UA_Server_new();
ck_assert_ptr_nonnull(server);
UA_ServerConfig *serverConfig = UA_Server_getConfig(server);
serverConfig->tcpEnabled = false;
serverConfig->httpEnabled = true;
serverConfig->httpAllowUnencrypted = true;
serverConfig->httpListenAddress = UA_STRING_ALLOC("127.0.0.1");
UA_Array_delete(serverConfig->serverUrls, serverConfig->serverUrlsSize,
&UA_TYPES[UA_TYPES_STRING]);
serverConfig->serverUrls =
(UA_String *)UA_Array_new(1, &UA_TYPES[UA_TYPES_STRING]);
ck_assert_ptr_nonnull(serverConfig->serverUrls);
serverConfig->serverUrlsSize = 1;
serverConfig->serverUrls[0] =
UA_STRING_ALLOC("opc.http://127.0.0.1:0/client");
UA_ByteString largeValue = UA_BYTESTRING_NULL;
ck_assert_uint_eq(UA_ByteString_allocBuffer(&largeValue, 16 * 1024),
UA_STATUSCODE_GOOD);
memset(largeValue.data, 0x5a, largeValue.length);
UA_VariableAttributes largeAttributes = UA_VariableAttributes_default;
largeAttributes.displayName =
UA_LOCALIZEDTEXT_ALLOC("en-US", "Large HTTP value");
ck_assert_uint_eq(UA_Variant_setScalarCopy(
&largeAttributes.value, &largeValue,
&UA_TYPES[UA_TYPES_BYTESTRING]),
UA_STATUSCODE_GOOD);
const UA_NodeId largeValueNode = UA_NODEID_NUMERIC(1, 6001);
ck_assert_uint_eq(UA_Server_addVariableNode(
server, largeValueNode,
UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER),
UA_NODEID_NUMERIC(0, UA_NS0ID_ORGANIZES),
UA_QUALIFIEDNAME(1, "LargeHttpValue"),
UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE),
largeAttributes, NULL, NULL),
UA_STATUSCODE_GOOD);
UA_VariableAttributes_clear(&largeAttributes);
UA_ByteString_clear(&largeValue);
ck_assert_uint_eq(UA_Server_run_startup(server), UA_STATUSCODE_GOOD);
UA_String endpointUrl = UA_STRING_NULL;
ck_assert_uint_ne(getAdvertisedPort(serverConfig, "opc.http://", 11,
&endpointUrl), 0);
char *endpoint = (char *)UA_malloc(endpointUrl.length + 1);
ck_assert_ptr_nonnull(endpoint);
memcpy(endpoint, endpointUrl.data, endpointUrl.length);
endpoint[endpointUrl.length] = 0;
UA_ClientConfig clientConfig;
UA_Client *client = NULL;
UA_Variant value;
if(scenario == HTTP_CLIENT_SCENARIO_SECURITY) {
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, endpoint),
UA_STATUSCODE_BADSECURITYPOLICYREJECTED);
UA_Client_delete(client);
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
clientConfig.securityMode = UA_MESSAGESECURITYMODE_SIGN;
clientConfig.securityPolicyUri = UA_STRING_ALLOC(
"http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256");
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, endpoint),
UA_STATUSCODE_BADSECURITYPOLICYREJECTED);
UA_Client_delete(client);
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
clientConfig.httpClientCertificate = UA_BYTESTRING_ALLOC("certificate");
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, endpoint),
UA_STATUSCODE_BADINVALIDARGUMENT);
UA_Client_delete(client);
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
ck_assert_uint_eq(UA_ClientConfig_setAuthenticationUsername(
&clientConfig, "user", "password"),
UA_STATUSCODE_GOOD);
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, endpoint),
UA_STATUSCODE_BADIDENTITYTOKENREJECTED);
UA_Client_delete(client);
}
if(scenario == HTTP_CLIENT_SCENARIO_SERVICES) {
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
clientConfig.httpMaxMsgSize = 32 * 1024;
clientConfig.httpMaxDecompressedMsgSize = 32 * 1024;
clientConfig.outStandingPublishRequests = 3;
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, endpoint), UA_STATUSCODE_GOOD);
UA_MessageSecurityMode mode = UA_MESSAGESECURITYMODE_INVALID;
UA_QualifiedName modeKey = UA_QUALIFIEDNAME(0, "securityMode");
ck_assert_uint_eq(UA_Client_getConnectionAttribute_scalar(
client, modeKey,
&UA_TYPES[UA_TYPES_MESSAGESECURITYMODE], &mode),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(mode, UA_MESSAGESECURITYMODE_NONE);
UA_Variant_init(&value);
ck_assert_uint_eq(UA_Client_readValueAttribute(
client, UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE),
&value), UA_STATUSCODE_GOOD);
ck_assert(UA_Variant_isScalar(&value));
UA_Variant_clear(&value);
/* The shared RequestId generator skips zero when its 32-bit counter wraps. */
lockClient(client);
client->requestId = UA_UINT32_MAX;
unlockClient(client);
UA_ReadRequest rolloverRequest;
UA_ReadRequest_init(&rolloverRequest);
rolloverRequest.requestHeader.timeoutHint = 1000;
AsyncReadResult rolloverResult = {0};
UA_UInt32 rolloverRequestId = 0;
ck_assert_uint_eq(
UA_Client_sendAsyncReadRequest(client, &rolloverRequest,
asyncReadResultCallback, &rolloverResult,
&rolloverRequestId),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(rolloverRequestId, 1);
for(size_t i = 0; i < 100 && rolloverResult.callbacks == 0; i++)
ck_assert_uint_eq(UA_Client_run_iterate(client, 10),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(rolloverResult.callbacks, 1);
ck_assert_uint_eq(rolloverResult.status, UA_STATUSCODE_BADNOTHINGTODO);
/* OPC UA Binary uses identity content coding. Its encoded response therefore
* has to fit the ordinary HTTP message limit. */
UA_Variant_init(&value);
ck_assert_uint_eq(UA_Client_readValueAttribute(
client, largeValueNode, &value),
UA_STATUSCODE_GOOD);
ck_assert(UA_Variant_hasScalarType(&value,
&UA_TYPES[UA_TYPES_BYTESTRING]));
ck_assert_uint_eq(((UA_ByteString *)value.data)->length, 16 * 1024);
UA_Variant_clear(&value);
UA_CreateSubscriptionRequest subscriptionRequest =
UA_CreateSubscriptionRequest_default();
subscriptionRequest.requestedPublishingInterval = 100.0;
subscriptionRequest.requestedMaxKeepAliveCount = 100;
subscriptionRequest.requestedLifetimeCount = 1000;
UA_CreateSubscriptionResponse subscriptionResponse =
UA_Client_Subscriptions_create(client, subscriptionRequest,
NULL, NULL, NULL);
ck_assert_uint_eq(subscriptionResponse.responseHeader.serviceResult,
UA_STATUSCODE_GOOD);
ck_assert_uint_ne(subscriptionResponse.subscriptionId, 0);
DataChangeResult dataChanges = {0};
UA_MonitoredItemCreateRequest monitoredItemRequest =
UA_MonitoredItemCreateRequest_default(
UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_CURRENTTIME));
monitoredItemRequest.requestedParameters.samplingInterval = 50.0;
UA_MonitoredItemCreateResult monitoredItemResult =
UA_Client_MonitoredItems_createDataChange(
client, subscriptionResponse.subscriptionId,
UA_TIMESTAMPSTORETURN_BOTH, monitoredItemRequest, &dataChanges,
dataChangeResultCallback, NULL);
ck_assert_uint_eq(monitoredItemResult.statusCode, UA_STATUSCODE_GOOD);
UA_MonitoredItemCreateResult_clear(&monitoredItemResult);
/* Publish is represented by concurrent, long-standing HTTP requests.
* Establish all configured requests and verify that an ordinary service
* can complete without waiting for them. */
for(size_t i = 0; i < 100 &&
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]) < 3;
i++)
ck_assert_uint_eq(UA_Client_run_iterate(client, 10),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]), 3);
UA_Variant_init(&value);
ck_assert_uint_eq(UA_Client_readValueAttribute(
client, UA_NODEID_NUMERIC(
0, UA_NS0ID_SERVER_SERVERSTATUS_STATE),
&value), UA_STATUSCODE_GOOD);
UA_Variant_clear(&value);
ck_assert_uint_eq(
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]), 3);
/* One malformed request is a request-local failure. It must not close the
* binding, Session or the three outstanding Publish requests. */
UA_ReadRequest malformed;
UA_ReadRequest_init(&malformed);
malformed.requestHeader.timeoutHint = 1000;
AsyncReadResult malformedResult = {0};
UA_UInt32 malformedRequestId = 0;
ck_assert_uint_eq(
UA_Client_sendAsyncReadRequest(client, &malformed,
asyncReadResultCallback,
&malformedResult,
&malformedRequestId),
UA_STATUSCODE_GOOD);
ck_assert_uint_ne(malformedRequestId, 0);
for(size_t i = 0; i < 100 && malformedResult.callbacks == 0; i++)
ck_assert_uint_eq(UA_Client_run_iterate(client, 10),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(malformedResult.callbacks, 1);
ck_assert_uint_eq(malformedResult.status, UA_STATUSCODE_BADNOTHINGTODO);
ck_assert_uint_eq(
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]), 3);
/* Malformed transport metadata and service payloads consume their
* synthetic request exactly once and leave the Publish window untouched. */
UA_ReadResponse truncatedResponse;
UA_ReadResponse_init(&truncatedResponse);
UA_ByteString malformedBody = encodeServiceResponse(
&truncatedResponse, &UA_TYPES[UA_TYPES_READRESPONSE]);
ck_assert_uint_gt(malformedBody.length, 1);
malformedBody.length--;
AsyncReadResult missingStatusResult = {0};
addSyntheticReadCall(client, 0xf0000001, &missingStatusResult);
injectHttpClientResponse(client, 0xf0000001,
&UA_HTTP_CONTENTTYPE_BINARY, malformedBody,
false);
ck_assert_uint_eq(missingStatusResult.callbacks, 1);
ck_assert_uint_eq(missingStatusResult.status,
UA_STATUSCODE_BADDECODINGERROR);
injectHttpClientResponse(client, 0xf0000001,
&UA_HTTP_CONTENTTYPE_BINARY, malformedBody,
false);
ck_assert_uint_eq(missingStatusResult.callbacks, 1);
AsyncReadResult malformedBodyResult = {0};
addSyntheticReadCall(client, 0xf0000002, &malformedBodyResult);
injectHttpClientResponse(client, 0xf0000002,
&UA_HTTP_CONTENTTYPE_BINARY, malformedBody,
true);
ck_assert_uint_eq(malformedBodyResult.callbacks, 1);
ck_assert_uint_eq(malformedBodyResult.status,
UA_STATUSCODE_BADDECODINGERROR);
UA_ByteString_clear(&malformedBody);
UA_ReadResponse validReadResponse;
UA_ReadResponse_init(&validReadResponse);
UA_ByteString trailingBody = encodeServiceResponse(
&validReadResponse, &UA_TYPES[UA_TYPES_READRESPONSE]);
UA_Byte *trailingData = (UA_Byte *)UA_realloc(
trailingBody.data, trailingBody.length + 1);
ck_assert_ptr_nonnull(trailingData);
trailingBody.data = trailingData;
trailingBody.data[trailingBody.length++] = 0xff;
AsyncReadResult trailingBodyResult = {0};
addSyntheticReadCall(client, 0xf000000b, &trailingBodyResult);
injectHttpClientResponse(client, 0xf000000b,
&UA_HTTP_CONTENTTYPE_BINARY, trailingBody, true);
ck_assert_uint_eq(trailingBodyResult.callbacks, 1);
ck_assert_uint_eq(trailingBodyResult.status,
UA_STATUSCODE_BADDECODINGERROR);
UA_ByteString_clear(&trailingBody);
UA_ReadResponse_clear(&validReadResponse);
UA_String binaryType = UA_STRING_STATIC("application/octet-stream");
UA_KeyValuePair duplicateTypeHeaders[2] = {0};
duplicateTypeHeaders[0].key = UA_QUALIFIEDNAME(0, "content-type");
duplicateTypeHeaders[1].key = UA_QUALIFIEDNAME(0, "Content-Type");
UA_Variant_setScalar(&duplicateTypeHeaders[0].value, &binaryType,
&UA_TYPES[UA_TYPES_STRING]);
UA_Variant_setScalar(&duplicateTypeHeaders[1].value, &binaryType,
&UA_TYPES[UA_TYPES_STRING]);
AsyncReadResult duplicateTypeResult = {0};
addSyntheticReadCall(client, 0xf000000c, &duplicateTypeResult);
injectHttpClientResponseHeaders(client, 0xf000000c,
duplicateTypeHeaders, 2,
UA_BYTESTRING("body"));
ck_assert_uint_eq(duplicateTypeResult.callbacks, 1);
ck_assert_uint_eq(duplicateTypeResult.status,
UA_STATUSCODE_BADDECODINGERROR);
AsyncReadResult missingTypeResult = {0};
addSyntheticReadCall(client, 0xf0000006, &missingTypeResult);
injectHttpClientResponse(client, 0xf0000006, NULL,
UA_BYTESTRING("body"), true);
ck_assert_uint_eq(missingTypeResult.callbacks, 1);
ck_assert_uint_eq(missingTypeResult.status,
UA_STATUSCODE_BADDECODINGERROR);
AsyncReadResult wrongContentTypeResult = {0};
addSyntheticReadCall(client, 0xf000000a, &wrongContentTypeResult);
const UA_String jsonContentType = UA_STRING_STATIC("application/json");
injectHttpClientResponse(client, 0xf000000a, &jsonContentType,
UA_BYTESTRING("body"), true);
ck_assert_uint_eq(wrongContentTypeResult.callbacks, 1);
ck_assert_uint_eq(wrongContentTypeResult.status,
UA_STATUSCODE_BADDECODINGERROR);
AsyncReadResult emptyBodyResult = {0};
addSyntheticReadCall(client, 0xf0000007, &emptyBodyResult);
injectHttpClientResponse(client, 0xf0000007,
&UA_HTTP_CONTENTTYPE_BINARY,
UA_BYTESTRING_NULL, true);
ck_assert_uint_eq(emptyBodyResult.callbacks, 1);
ck_assert_uint_eq(emptyBodyResult.status,
UA_STATUSCODE_BADDECODINGERROR);
AsyncReadResult tooLargeResult = {0};
addSyntheticReadCall(client, 0xf0000008, &tooLargeResult);
injectHttpClientResponseStatus(client, 0xf0000008, 413, NULL,
UA_BYTESTRING_NULL, true);
ck_assert_uint_eq(tooLargeResult.callbacks, 1);
ck_assert_uint_eq(tooLargeResult.status,
UA_STATUSCODE_BADREQUESTTOOLARGE);
AsyncReadResult serverErrorResult = {0};
addSyntheticReadCall(client, 0xf0000009, &serverErrorResult);
injectHttpClientResponseStatus(client, 0xf0000009, 500, NULL,
UA_BYTESTRING_NULL, true);
ck_assert_uint_eq(serverErrorResult.callbacks, 1);
ck_assert_uint_eq(serverErrorResult.status,
UA_STATUSCODE_BADCOMMUNICATIONERROR);
UA_WriteResponse wrongResponse;
UA_WriteResponse_init(&wrongResponse);
UA_ByteString wrongBody = encodeServiceResponse(
&wrongResponse, &UA_TYPES[UA_TYPES_WRITERESPONSE]);
AsyncReadResult wrongTypeResult = {0};
addSyntheticReadCall(client, 0xf0000003, &wrongTypeResult);
injectHttpClientResponse(client, 0xf0000003,
&UA_HTTP_CONTENTTYPE_BINARY, wrongBody, true);
ck_assert_uint_eq(wrongTypeResult.callbacks, 1);
ck_assert_uint_eq(wrongTypeResult.status,
UA_STATUSCODE_BADCOMMUNICATIONERROR);
UA_ByteString_clear(&wrongBody);
UA_WriteResponse_clear(&wrongResponse);
UA_ServiceFault syntheticFault;
UA_ServiceFault_init(&syntheticFault);
syntheticFault.responseHeader.serviceResult =
UA_STATUSCODE_BADSERVICEUNSUPPORTED;
UA_ByteString faultBody = encodeServiceResponse(
&syntheticFault, &UA_TYPES[UA_TYPES_SERVICEFAULT]);
AsyncReadResult faultResult = {0};
addSyntheticReadCall(client, 0xf0000004, &faultResult);
injectHttpClientResponse(client, 0xf0000004,
&UA_HTTP_CONTENTTYPE_BINARY, faultBody, true);
ck_assert_uint_eq(faultResult.callbacks, 1);
ck_assert_uint_eq(faultResult.status,
UA_STATUSCODE_BADSERVICEUNSUPPORTED);
UA_ByteString_clear(&faultBody);
UA_ServiceFault_clear(&syntheticFault);
ck_assert_uint_eq(
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]), 3);
/* Drop the shared HTTP binding while Publish requests are pending. The
* client must reopen it, reactivate the same Session and replenish all
* Publish requests without recreating the subscription. */
UA_NodeId sessionTokenBeforeReconnect;
UA_NodeId_init(&sessionTokenBeforeReconnect);
uintptr_t bindingId = 0;
UA_ConnectionManager *bindingManager = NULL;
lockClient(client);
ck_assert_uint_eq(UA_NodeId_copy(&client->authenticationToken,
&sessionTokenBeforeReconnect),
UA_STATUSCODE_GOOD);
bindingId = client->channel.connectionId;
bindingManager = client->channel.connectionManager;
unlockClient(client);
ck_assert_ptr_nonnull(bindingManager);
ck_assert_uint_eq(bindingManager->closeConnection(bindingManager,
bindingId),
UA_STATUSCODE_GOOD);
UA_SecureChannelState channelState = UA_SECURECHANNELSTATE_CLOSED;
UA_SessionState sessionState = UA_SESSIONSTATE_CLOSED;
UA_StatusCode connectStatus = UA_STATUSCODE_GOOD;
for(size_t i = 0; i < 400; i++) {
ck_assert_uint_eq(UA_Client_run_iterate(client, 10),
UA_STATUSCODE_GOOD);
UA_Client_getState(client, &channelState, &sessionState,
&connectStatus);
if(channelState == UA_SECURECHANNELSTATE_OPEN &&
sessionState == UA_SESSIONSTATE_ACTIVATED &&
countPendingResponses(client,
&UA_TYPES[UA_TYPES_PUBLISHRESPONSE]) == 3)
break;
}
ck_assert_uint_eq(connectStatus, UA_STATUSCODE_GOOD);
ck_assert_uint_eq(channelState, UA_SECURECHANNELSTATE_OPEN);
ck_assert_uint_eq(sessionState, UA_SESSIONSTATE_ACTIVATED);
ck_assert_uint_eq(
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]), 3);
lockClient(client);
ck_assert(UA_NodeId_equal(&client->authenticationToken,
&sessionTokenBeforeReconnect));
unlockClient(client);
UA_NodeId_clear(&sessionTokenBeforeReconnect);
/* The initial monitored value must arrive through one of the replenished
* long-poll requests. Afterwards the client immediately restores the
* configured Publish window. */
for(size_t i = 0; i < 200 && dataChanges.notifications == 0; i++)
ck_assert_uint_eq(UA_Client_run_iterate(client, 10),
UA_STATUSCODE_GOOD);
ck_assert_uint_gt(dataChanges.notifications, 0);
for(size_t i = 0; i < 100 &&
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]) < 3;
i++)
ck_assert_uint_eq(UA_Client_run_iterate(client, 10),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]), 3);
/* Explicit shutdown must cancel the outstanding long-poll requests rather
* than waiting for their ten-minute Publish timeout. */
ck_assert_uint_eq(UA_Client_disconnect(client), UA_STATUSCODE_GOOD);
ck_assert_uint_eq(
countPendingResponses(client, &UA_TYPES[UA_TYPES_PUBLISHRESPONSE]), 0);
UA_CreateSubscriptionResponse_clear(&subscriptionResponse);
size_t notificationsAfterDisconnect = dataChanges.notifications;
UA_Client_delete(client);
for(size_t i = 0; i < 10; i++)
serverConfig->eventLoop->run(serverConfig->eventLoop, 1);
ck_assert_uint_eq(dataChanges.notifications, notificationsAfterDisconnect);
lockServer(server);
ck_assert_uint_eq(server->sessionCount, 0);
unlockServer(server);
/* The same identity-encoded response is rejected by a smaller client wire
* limit. The binding remains usable for subsequent requests. */
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
clientConfig.httpMaxMsgSize = 4096;
clientConfig.httpMaxDecompressedMsgSize = 8192;
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, endpoint), UA_STATUSCODE_GOOD);
UA_Variant_init(&value);
ck_assert_uint_eq(UA_Client_readValueAttribute(
client, largeValueNode, &value),
UA_STATUSCODE_BADRESPONSETOOLARGE);
UA_Variant_clear(&value);
UA_Variant_init(&value);
ck_assert_uint_eq(UA_Client_readValueAttribute(
client, UA_NODEID_NUMERIC(
0, UA_NS0ID_SERVER_SERVERSTATUS_STATE),
&value),
UA_STATUSCODE_GOOD);
UA_Variant_clear(&value);
ck_assert_uint_eq(UA_Client_disconnect(client), UA_STATUSCODE_GOOD);
UA_Client_delete(client);
}
if(scenario == HTTP_CLIENT_SCENARIO_SESSION_REENTRANCY) {
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
clientConfig.noReconnect = true;
clientConfig.noNewSession = true;
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, endpoint), UA_STATUSCODE_GOOD);
serverConfig->serviceNotificationCallback =
closeHttpSessionFromServiceNotification;
closeHttpSessionAtServiceBegin = true;
closeHttpSessionResult = UA_STATUSCODE_BADUNEXPECTEDERROR;
UA_Variant_init(&value);
ck_assert_uint_eq(UA_Client_readValueAttribute(
client, UA_NODEID_NUMERIC(
0, UA_NS0ID_SERVER_SERVERSTATUS_STATE),
&value),
UA_STATUSCODE_BADSESSIONCLOSED);
UA_Variant_clear(&value);
ck_assert_uint_eq(closeHttpSessionResult, UA_STATUSCODE_GOOD);
serverConfig->serviceNotificationCallback = NULL;
ck_assert_uint_eq(UA_Client_disconnect(client), UA_STATUSCODE_GOOD);
UA_Client_delete(client);
for(size_t i = 0; i < 10; i++)
serverConfig->eventLoop->run(serverConfig->eventLoop, 1);
lockServer(server);
ck_assert_uint_eq(server->sessionCount, 0);
unlockServer(server);
}
if(scenario == HTTP_CLIENT_SCENARIO_ENDPOINT) {
/* An explicitly selected EndpointDescription takes precedence over the
* initial URL. This exercises the same URL handoff used after discovery. */
UA_EndpointDescription *availableEndpoints = NULL;
size_t availableEndpointsSize = 0;
lockServer(server);
ck_assert_uint_eq(setCurrentEndpointsArray(
server, endpointUrl, NULL, 0, &availableEndpoints,
&availableEndpointsSize),
UA_STATUSCODE_GOOD);
unlockServer(server);
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
const UA_String binaryProfile = UA_STRING_STATIC(
"http://open62541.org/UA-Profile/Transport/http-uabinary");
for(size_t i = 0; i < availableEndpointsSize; i++) {
if(UA_String_equal(&availableEndpoints[i].transportProfileUri,
&binaryProfile)) {
ck_assert_uint_eq(UA_EndpointDescription_copy(
&availableEndpoints[i],
&clientConfig.endpoint),
UA_STATUSCODE_GOOD);
break;
}
}
ck_assert_uint_gt(clientConfig.endpoint.endpointUrl.length, 0);
UA_Array_delete(availableEndpoints, availableEndpointsSize,
&UA_TYPES[UA_TYPES_ENDPOINTDESCRIPTION]);
char wrongEndpoint[256];
int wrongLength = snprintf(wrongEndpoint, sizeof(wrongEndpoint),
"opc.http://127.0.0.1:%u/wrong-path",
(unsigned)getAdvertisedPort(
serverConfig, "opc.http://", 11, NULL));
ck_assert_int_gt(wrongLength, 0);
ck_assert_int_lt(wrongLength, (int)sizeof(wrongEndpoint));
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, wrongEndpoint),
UA_STATUSCODE_GOOD);
ck_assert_uint_eq(UA_Client_disconnect(client), UA_STATUSCODE_GOOD);
UA_Client_delete(client);
}
#ifdef UA_ENABLE_JSON_ENCODING
if(scenario == HTTP_CLIENT_SCENARIO_JSON) {
char *jsonEndpoint = (char *)UA_malloc(endpointUrl.length + 6);
ck_assert_ptr_nonnull(jsonEndpoint);
memcpy(jsonEndpoint, endpointUrl.data, endpointUrl.length);
memcpy(&jsonEndpoint[endpointUrl.length], "/json", 6);
memset(&clientConfig, 0, sizeof(clientConfig));
clientConfig.eventLoop = serverConfig->eventLoop;
clientConfig.externalEventLoop = true;
ck_assert_uint_eq(UA_ClientConfig_setDefault(&clientConfig),
UA_STATUSCODE_GOOD);
clientConfig.httpAllowUnencrypted = true;
client = UA_Client_newWithConfig(&clientConfig);
ck_assert_ptr_nonnull(client);
ck_assert_uint_eq(UA_Client_connect(client, jsonEndpoint),
UA_STATUSCODE_GOOD);
UA_Variant_init(&value);
ck_assert_uint_eq(UA_Client_readValueAttribute(
client, UA_NODEID_NUMERIC(0, UA_NS0ID_SERVER_SERVERSTATUS_STATE),
&value), UA_STATUSCODE_GOOD);
ck_assert(UA_Variant_isScalar(&value));
UA_Variant_clear(&value);
AsyncReadResult malformedJsonResult = {0};
addSyntheticReadCall(client, 0xf0000005, &malformedJsonResult);
UA_ByteString malformedJson = UA_BYTESTRING("{");
injectHttpClientResponse(client, 0xf0000005,
&UA_HTTP_CONTENTTYPE_JSON, malformedJson, true);
ck_assert_uint_eq(malformedJsonResult.callbacks, 1);
ck_assert_uint_eq(malformedJsonResult.status,
UA_STATUSCODE_BADDECODINGERROR);
ck_assert_uint_eq(UA_Client_disconnect(client), UA_STATUSCODE_GOOD);
UA_Client_delete(client);
UA_free(jsonEndpoint);
}
#else
(void)scenario;
#endif
UA_free(endpoint);
UA_String_clear(&endpointUrl);
ck_assert_uint_eq(UA_Server_run_shutdown(server), UA_STATUSCODE_GOOD);
UA_Server_delete(server);
}
START_TEST(httpClientSecurityValidation) {
runClientScenario(HTTP_CLIENT_SCENARIO_SECURITY);
}
END_TEST
START_TEST(httpClientServicesSubscriptionsAndReconnect) {
runClientScenario(HTTP_CLIENT_SCENARIO_SERVICES);
}
END_TEST
START_TEST(httpClientSessionNotificationCloseIsReentrant) {
runClientScenario(HTTP_CLIENT_SCENARIO_SESSION_REENTRANCY);
}
END_TEST
START_TEST(httpClientEndpointSelection) {
runClientScenario(HTTP_CLIENT_SCENARIO_ENDPOINT);
}
END_TEST
#ifdef UA_ENABLE_JSON_ENCODING
START_TEST(httpJsonClientServiceAndMalformedResponse) {
runClientScenario(HTTP_CLIENT_SCENARIO_JSON);
}
END_TEST
#endif
int main(void) {
Suite *suite = suite_create("OPC UA HTTP client");
TCase *tc = tcase_create("transport and services");
tcase_set_timeout(tc, 30);
tcase_add_test(tc, httpClientSecurityValidation);
tcase_add_test(tc, httpClientServicesSubscriptionsAndReconnect);
tcase_add_test(tc, httpClientSessionNotificationCloseIsReentrant);
tcase_add_test(tc, httpClientEndpointSelection);
#ifdef UA_ENABLE_JSON_ENCODING
tcase_add_test(tc, httpJsonClientServiceAndMalformedResponse);
#endif
suite_add_tcase(suite, tc);
SRunner *runner = srunner_create(suite);
srunner_run_all(runner, CK_NORMAL);
int failed = srunner_ntests_failed(runner);
srunner_free(runner);
return failed == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}