mirror of
https://github.com/open62541/open62541.git
synced 2026-10-29 05:11:36 +08:00
Limit accepted HTTP peers to 100 per listener by default, before the application creates per-peer server state. Give each request an absolute receive deadline based on the configured timeout, so trickled headers or bodies cannot extend an incomplete request indefinitely. Preserve the inactivity timeout for idle keep-alive connections.
2252 lines
98 KiB
C
2252 lines
98 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 <open62541/plugin/eventloop.h>
|
|
#include <open62541/plugin/log_stdout.h>
|
|
#include "open62541/types.h"
|
|
#include "open62541/types_generated.h"
|
|
#include "open62541/util.h"
|
|
#include "eventloop_posix_http_compression.h"
|
|
|
|
#include "testing_clock.h"
|
|
#include <stdio.h>
|
|
#include <time.h>
|
|
#include <stdlib.h>
|
|
#include <check.h>
|
|
|
|
#ifndef _WIN32
|
|
# include <arpa/inet.h>
|
|
# include <errno.h>
|
|
# include <sys/socket.h>
|
|
# include <unistd.h>
|
|
#endif
|
|
|
|
#include "testing-plugins/testing_clock.h"
|
|
|
|
#define COUNT 2
|
|
|
|
unsigned int messageCount = 0;
|
|
static UA_UInt16 httpPort = 8000;
|
|
|
|
static void
|
|
setup(void) {
|
|
messageCount = 0;
|
|
const char *portString = getenv("OPEN62541_TEST_HTTP_PORT");
|
|
if(!portString)
|
|
return;
|
|
|
|
unsigned long port = strtoul(portString, NULL, 10);
|
|
ck_assert_uint_gt(port, 0);
|
|
ck_assert_uint_le(port, UINT16_MAX);
|
|
httpPort = (UA_UInt16)port;
|
|
}
|
|
|
|
static void teardown(void) {}
|
|
|
|
static UA_ByteString
|
|
loadFile(const char *path) {
|
|
UA_ByteString out = UA_BYTESTRING_NULL;
|
|
FILE *f = fopen(path, "rb");
|
|
if(!f || fseek(f, 0, SEEK_END) || (out.length = (size_t)ftell(f)) == 0 ||
|
|
fseek(f, 0, SEEK_SET) ||
|
|
UA_ByteString_allocBuffer(&out, out.length) != UA_STATUSCODE_GOOD ||
|
|
fread(out.data, 1, out.length, f) != out.length)
|
|
UA_ByteString_clear(&out);
|
|
if(f)
|
|
fclose(f);
|
|
return out;
|
|
}
|
|
|
|
static void
|
|
stopEventLoop(UA_EventLoop *eventLoop) {
|
|
eventLoop->stop(eventLoop);
|
|
for(size_t i = 0; i < 200 &&
|
|
eventLoop->state != UA_EVENTLOOPSTATE_STOPPED; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
ck_assert_uint_eq(eventLoop->state, UA_EVENTLOOPSTATE_STOPPED);
|
|
eventLoop->free(eventLoop);
|
|
}
|
|
|
|
|
|
static void
|
|
connectionCallback(UA_ConnectionManager *cm, uintptr_t connectionId,
|
|
void *application, void **connectionContext,
|
|
UA_ConnectionState status,
|
|
const UA_KeyValueMap *params,
|
|
UA_ByteString msg) {
|
|
const UA_UInt64 *contentLength;
|
|
if(params) {
|
|
contentLength = (const UA_UInt64 *)
|
|
UA_KeyValueMap_getScalar(params,
|
|
UA_QUALIFIEDNAME(0, "content-length"),
|
|
&UA_TYPES[UA_TYPES_UINT64]);
|
|
}
|
|
|
|
if(status == UA_CONNECTIONSTATE_CLOSED) {
|
|
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
|
|
"USER\t| Closing connection");
|
|
messageCount = 0;
|
|
} else if(msg.length > 0) {
|
|
static UA_UInt64 consumed = 0;
|
|
char data[msg.length + 1];
|
|
memcpy(data, msg.data, msg.length);
|
|
data[msg.length] = '\0';
|
|
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
|
|
"USER %lu\t| Received Data %zu bytes: %s", connectionId, msg.length, data);
|
|
|
|
consumed += msg.length;
|
|
if(consumed >= *contentLength) {
|
|
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
|
|
"USER %lu\t| End of Response", connectionId);
|
|
consumed = 0;
|
|
messageCount++;
|
|
}
|
|
} else if(status == UA_CONNECTIONSTATE_OPENING) {
|
|
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
|
|
"USER\t| Opening connection");
|
|
uintptr_t *id = *(uintptr_t**)connectionContext;
|
|
*id = connectionId;
|
|
} else if(status == UA_CONNECTIONSTATE_ESTABLISHED) {
|
|
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_USERLAND,
|
|
"USER\t| Established connection");
|
|
}
|
|
}
|
|
|
|
START_TEST(sendGetRequest) {
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("httpCM"));
|
|
UA_EventLoop *el = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
el->registerEventSource(el, &cm->eventSource);
|
|
el->start(el);
|
|
|
|
UA_String address = UA_STRING("127.0.0.1");
|
|
UA_UInt16 port = httpPort;
|
|
UA_Boolean useSSL = false;
|
|
UA_UInt16 timeout = 30;
|
|
|
|
size_t paramsSize = 4;
|
|
UA_KeyValuePair params[paramsSize];
|
|
params[0].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(¶ms[0].value, &address, &UA_TYPES[UA_TYPES_STRING]);
|
|
params[1].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶ms[1].value, &port, &UA_TYPES[UA_TYPES_UINT16]);
|
|
params[2].key = UA_QUALIFIEDNAME(0, "useSSL");
|
|
UA_Variant_setScalar(¶ms[2].value, &useSSL, &UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
params[3].key = UA_QUALIFIEDNAME(0, "timeout");
|
|
UA_Variant_setScalar(¶ms[3].value, &timeout, &UA_TYPES[UA_TYPES_UINT16]);
|
|
UA_KeyValueMap kvm = {paramsSize, params};
|
|
|
|
uintptr_t connectionIds[COUNT] = {0};
|
|
UA_StatusCode res =
|
|
cm->openConnection(cm, &kvm, NULL, &connectionIds[0], connectionCallback);
|
|
ck_assert(res == UA_STATUSCODE_GOOD);
|
|
|
|
/* A second connection on the shared context can override its timeout. */
|
|
timeout = 15;
|
|
res = cm->openConnection(cm, &kvm, NULL, &connectionIds[1], connectionCallback);
|
|
ck_assert(res == UA_STATUSCODE_GOOD);
|
|
|
|
for(int i = 0; i < COUNT; i++) {
|
|
res = cm->sendWithConnection(cm, connectionIds[i],
|
|
&UA_KEYVALUEMAP_NULL, NULL);
|
|
ck_assert(res == UA_STATUSCODE_GOOD);
|
|
}
|
|
ck_assert_uint_eq(messageCount, 0);
|
|
for(int i = 0; i < 20; i++) {
|
|
el->run(el,100);
|
|
}
|
|
ck_assert_uint_eq(messageCount, COUNT);
|
|
|
|
for(int i = 0; i < COUNT; i++) {
|
|
res = cm->closeConnection(cm, connectionIds[i]);
|
|
ck_assert(res == UA_STATUSCODE_GOOD);
|
|
}
|
|
|
|
for(int i = 0; i < 20; i++) {
|
|
el->run(el,100);
|
|
}
|
|
|
|
/* Stop the EventLoop */
|
|
int max_stop_iteration_count = 20;
|
|
int iteration = 0;
|
|
el->stop(el);
|
|
while(el->state != UA_EVENTLOOPSTATE_STOPPED && iteration < max_stop_iteration_count) {
|
|
UA_DateTime next = el->run(el, 1);
|
|
UA_fakeSleep((UA_UInt32)((next - UA_DateTime_now()) / UA_DATETIME_MSEC));
|
|
iteration++;
|
|
}
|
|
ck_assert(el->state == UA_EVENTLOOPSTATE_STOPPED);
|
|
el->free(el);
|
|
el = NULL;
|
|
} END_TEST
|
|
|
|
START_TEST(sendPostRequest) {
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("httpCM"));
|
|
UA_EventLoop *el = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
el->registerEventSource(el, &cm->eventSource);
|
|
el->start(el);
|
|
|
|
UA_String address = UA_STRING("127.0.0.1");
|
|
UA_UInt16 port = httpPort;
|
|
UA_Boolean useSSL = false;
|
|
|
|
size_t paramsSize = 3;
|
|
UA_KeyValuePair params[paramsSize];
|
|
params[0].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(¶ms[0].value, &address, &UA_TYPES[UA_TYPES_STRING]);
|
|
params[1].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶ms[1].value, &port, &UA_TYPES[UA_TYPES_UINT16]);
|
|
params[2].key = UA_QUALIFIEDNAME(0, "useSSL");
|
|
UA_Variant_setScalar(¶ms[2].value, &useSSL, &UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
UA_KeyValueMap kvm = {paramsSize, params};
|
|
|
|
UA_String path = UA_STRING("/post");
|
|
UA_String method = UA_STRING("POST");
|
|
|
|
size_t sendParamsSize = 3;
|
|
UA_KeyValuePair sendParams[sendParamsSize];
|
|
sendParams[0].key = UA_QUALIFIEDNAME(0, "path");
|
|
UA_Variant_setScalar(&sendParams[0].value, &path, &UA_TYPES[UA_TYPES_STRING]);
|
|
sendParams[1].key = UA_QUALIFIEDNAME(0, "method");
|
|
UA_Variant_setScalar(&sendParams[1].value, &method, &UA_TYPES[UA_TYPES_STRING]);
|
|
UA_UInt32 requestHandle = 0;
|
|
sendParams[2].key = UA_QUALIFIEDNAME(0, "handle");
|
|
UA_Variant_setScalar(&sendParams[2].value, &requestHandle,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
|
|
UA_KeyValueMap sendKvm = {sendParamsSize, sendParams};
|
|
|
|
uintptr_t connectionId = 0;
|
|
UA_StatusCode res = cm->openConnection(cm, &kvm, NULL, &connectionId,
|
|
connectionCallback);
|
|
ck_assert(res == UA_STATUSCODE_GOOD);
|
|
|
|
for(int i = 0; i < COUNT; i++) {
|
|
requestHandle = (UA_UInt32)i;
|
|
UA_ByteString msg = UA_BYTESTRING_ALLOC("text=hallo&send=data");
|
|
res = cm->sendWithConnection(cm, connectionId, &sendKvm, &msg);
|
|
ck_assert(res == UA_STATUSCODE_GOOD);
|
|
}
|
|
ck_assert_uint_eq(messageCount, 0);
|
|
for(int i = 0; i < 20; i++) {
|
|
el->run(el,100);
|
|
}
|
|
ck_assert_uint_eq(messageCount, COUNT);
|
|
|
|
res = cm->closeConnection(cm, connectionId);
|
|
ck_assert(res == UA_STATUSCODE_GOOD);
|
|
|
|
for(int i = 0; i < 20; i++) {
|
|
el->run(el,100);
|
|
}
|
|
|
|
/* Stop the EventLoop */
|
|
int max_stop_iteration_count = 20;
|
|
int iteration = 0;
|
|
el->stop(el);
|
|
while(el->state != UA_EVENTLOOPSTATE_STOPPED && iteration < max_stop_iteration_count) {
|
|
UA_DateTime next = el->run(el, 1);
|
|
UA_fakeSleep((UA_UInt32)((next - UA_DateTime_now()) / UA_DATETIME_MSEC));
|
|
iteration++;
|
|
}
|
|
ck_assert(el->state == UA_EVENTLOOPSTATE_STOPPED);
|
|
el->free(el);
|
|
el = NULL;
|
|
} END_TEST
|
|
|
|
typedef struct {
|
|
uintptr_t listenerId;
|
|
uintptr_t clientId;
|
|
uintptr_t acceptedConnectionId;
|
|
UA_UInt16 port;
|
|
UA_Boolean requestReceived;
|
|
UA_Boolean responseReceived;
|
|
UA_Boolean responseHeaderReceived;
|
|
UA_UInt16 responseStatus;
|
|
} HTTPServerTestContext;
|
|
|
|
static void
|
|
serverConnectionCallback(UA_ConnectionManager *cm, uintptr_t connectionId,
|
|
void *application, void **connectionContext,
|
|
UA_ConnectionState state,
|
|
const UA_KeyValueMap *params, UA_ByteString msg) {
|
|
(void)connectionContext;
|
|
HTTPServerTestContext *ctx = (HTTPServerTestContext*)application;
|
|
const UA_UInt16 *listenPort = (const UA_UInt16*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "listen-port"), &UA_TYPES[UA_TYPES_UINT16]);
|
|
if(listenPort) {
|
|
ctx->listenerId = connectionId;
|
|
ctx->port = *listenPort;
|
|
return;
|
|
}
|
|
|
|
if(state == UA_CONNECTIONSTATE_OPENING) {
|
|
ctx->clientId = connectionId;
|
|
return;
|
|
}
|
|
|
|
const UA_String *method = (const UA_String*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "method"), &UA_TYPES[UA_TYPES_STRING]);
|
|
const UA_String *path = (const UA_String*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "path"), &UA_TYPES[UA_TYPES_STRING]);
|
|
if(method && path && connectionId != ctx->clientId) {
|
|
const UA_Byte expected[] = {0x00, 0x01, 0x7f, 0xff};
|
|
const UA_String expectedMethod = UA_STRING_STATIC("POST");
|
|
const UA_String expectedPath = UA_STRING_STATIC("/binary");
|
|
ck_assert(UA_String_equal(method, &expectedMethod));
|
|
ck_assert(UA_String_equal(path, &expectedPath));
|
|
ck_assert_uint_eq(msg.length, sizeof(expected));
|
|
ck_assert_mem_eq(msg.data, expected, sizeof(expected));
|
|
const UA_Variant *headerValue = UA_KeyValueMap_get(
|
|
params, UA_QUALIFIEDNAME(0, "headers"));
|
|
ck_assert_ptr_nonnull(headerValue);
|
|
ck_assert_ptr_eq(headerValue->type, &UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
|
|
UA_KeyValuePair *requestHeaders = (UA_KeyValuePair*)headerValue->data;
|
|
UA_Boolean foundSecurityPolicy = false;
|
|
for(size_t i = 0; i < headerValue->arrayLength; i++) {
|
|
const UA_String expectedHeader =
|
|
UA_STRING_STATIC("opcua-securitypolicy");
|
|
if(!UA_String_equal(&requestHeaders[i].key.name, &expectedHeader))
|
|
continue;
|
|
ck_assert(UA_Variant_hasScalarType(
|
|
&requestHeaders[i].value, &UA_TYPES[UA_TYPES_STRING]));
|
|
const UA_String expectedValue = UA_STRING_STATIC(
|
|
"http://opcfoundation.org/UA/SecurityPolicy#None");
|
|
ck_assert(UA_String_equal(
|
|
(UA_String*)requestHeaders[i].value.data, &expectedValue));
|
|
foundSecurityPolicy = true;
|
|
}
|
|
ck_assert(foundSecurityPolicy);
|
|
ctx->acceptedConnectionId = connectionId;
|
|
ctx->requestReceived = true;
|
|
|
|
UA_String contentType = UA_STRING("application/octet-stream");
|
|
UA_KeyValuePair headers[1] = {0};
|
|
headers[0].key = UA_QUALIFIEDNAME(0, "content-type");
|
|
UA_Variant_setScalar(&headers[0].value, &contentType,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
UA_UInt16 statusCode = 201;
|
|
UA_KeyValuePair responseParameters[2] = {0};
|
|
responseParameters[0].key = UA_QUALIFIEDNAME(0, "status-code");
|
|
UA_Variant_setScalar(&responseParameters[0].value, &statusCode,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
responseParameters[1].key = UA_QUALIFIEDNAME(0, "headers");
|
|
UA_Variant_setArray(&responseParameters[1].value, headers, 1,
|
|
&UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
|
|
UA_KeyValueMap responseMap = {2, responseParameters};
|
|
UA_ByteString response = UA_BYTESTRING_ALLOC("binary-response");
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, connectionId, &responseMap,
|
|
&response), UA_STATUSCODE_GOOD);
|
|
return;
|
|
}
|
|
|
|
if(connectionId == ctx->clientId) {
|
|
const UA_UInt16 *statusCode =
|
|
(const UA_UInt16*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "status-code"),
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
if(statusCode)
|
|
ctx->responseStatus = *statusCode;
|
|
const UA_Variant *responseHeaders = UA_KeyValueMap_get(
|
|
params, UA_QUALIFIEDNAME(0, "headers"));
|
|
if(responseHeaders) {
|
|
UA_KeyValuePair *headers =
|
|
(UA_KeyValuePair*)responseHeaders->data;
|
|
for(size_t i = 0; i < responseHeaders->arrayLength; i++) {
|
|
const UA_String contentTypeName =
|
|
UA_STRING_STATIC("content-type");
|
|
if(UA_String_equal(&headers[i].key.name, &contentTypeName))
|
|
ctx->responseHeaderReceived = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(connectionId == ctx->clientId && msg.length > 0) {
|
|
ck_assert_uint_eq(msg.length, strlen("binary-response"));
|
|
ck_assert_mem_eq(msg.data, "binary-response", msg.length);
|
|
ctx->responseReceived = true;
|
|
}
|
|
}
|
|
|
|
START_TEST(serverBinaryRoundtrip) {
|
|
HTTPServerTestContext ctx = {0};
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("httpCM"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm);
|
|
ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
|
|
UA_String address = UA_STRING("127.0.0.1");
|
|
UA_UInt16 port = 0;
|
|
UA_Boolean listen = true;
|
|
UA_KeyValuePair listenerParameters[3] = {0};
|
|
listenerParameters[0].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(&listenerParameters[0].value, &address,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
listenerParameters[1].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(&listenerParameters[1].value, &port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
listenerParameters[2].key = UA_QUALIFIEDNAME(0, "listen");
|
|
UA_Variant_setScalar(&listenerParameters[2].value, &listen,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
UA_KeyValueMap listenerMap = {3, listenerParameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &listenerMap, &ctx, NULL,
|
|
serverConnectionCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_ne(ctx.listenerId, 0);
|
|
ck_assert_uint_ne(ctx.port, 0);
|
|
|
|
port = ctx.port;
|
|
UA_KeyValuePair clientParameters[2] = {0};
|
|
clientParameters[0].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(&clientParameters[0].value, &address,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
clientParameters[1].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(&clientParameters[1].value, &port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
UA_KeyValueMap clientMap = {2, clientParameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &clientMap, &ctx, NULL,
|
|
serverConnectionCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_ne(ctx.clientId, 0);
|
|
|
|
UA_String method = UA_STRING("POST");
|
|
UA_String path = UA_STRING("/binary");
|
|
UA_String securityPolicy = UA_STRING("http://opcfoundation.org/UA/SecurityPolicy#None");
|
|
UA_KeyValuePair headers[1] = {0};
|
|
headers[0].key = UA_QUALIFIEDNAME(0, "opcua-securitypolicy");
|
|
UA_Variant_setScalar(&headers[0].value, &securityPolicy,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
UA_KeyValuePair sendParameters[3] = {0};
|
|
sendParameters[0].key = UA_QUALIFIEDNAME(0, "method");
|
|
UA_Variant_setScalar(&sendParameters[0].value, &method,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
sendParameters[1].key = UA_QUALIFIEDNAME(0, "path");
|
|
UA_Variant_setScalar(&sendParameters[1].value, &path,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
sendParameters[2].key = UA_QUALIFIEDNAME(0, "headers");
|
|
UA_Variant_setArray(&sendParameters[2].value, headers, 1,
|
|
&UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
|
|
UA_KeyValueMap sendMap = {3, sendParameters};
|
|
const UA_Byte requestData[] = {0x00, 0x01, 0x7f, 0xff};
|
|
UA_ByteString request = UA_BYTESTRING_NULL;
|
|
ck_assert_uint_eq(UA_ByteString_allocBuffer(&request, sizeof(requestData)),
|
|
UA_STATUSCODE_GOOD);
|
|
memcpy(request.data, requestData, sizeof(requestData));
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx.clientId, &sendMap, &request),
|
|
UA_STATUSCODE_GOOD);
|
|
|
|
for(size_t i = 0; i < 200 && !ctx.responseReceived; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
ck_assert(ctx.requestReceived);
|
|
ck_assert(ctx.responseReceived);
|
|
ck_assert_uint_eq(ctx.responseStatus, 201);
|
|
ck_assert(ctx.responseHeaderReceived);
|
|
|
|
UA_StatusCode closeStatus = cm->closeConnection(cm, ctx.clientId);
|
|
ck_assert(closeStatus == UA_STATUSCODE_GOOD ||
|
|
closeStatus == UA_STATUSCODE_BADNOTFOUND);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
eventLoop->stop(eventLoop);
|
|
for(size_t i = 0; i < 100 &&
|
|
eventLoop->state != UA_EVENTLOOPSTATE_STOPPED; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
ck_assert_uint_eq(eventLoop->state, UA_EVENTLOOPSTATE_STOPPED);
|
|
eventLoop->free(eventLoop);
|
|
} END_TEST
|
|
|
|
typedef enum {
|
|
HTTP_TEST_ECHO,
|
|
HTTP_TEST_EMPTY_RESPONSE,
|
|
HTTP_TEST_NO_RESPONSE,
|
|
HTTP_TEST_DUPLICATE_RESPONSE,
|
|
HTTP_TEST_CLOSE_CONNECTION,
|
|
HTTP_TEST_CLOSE_LISTENER,
|
|
HTTP_TEST_FIXED_RESPONSE,
|
|
HTTP_TEST_ONE_REQUEST_FAILS,
|
|
HTTP_TEST_ONE_REQUEST_STALLS,
|
|
HTTP_TEST_DUPLICATE_ENCODING_RESPONSE
|
|
} HTTPTestMode;
|
|
|
|
typedef struct {
|
|
uintptr_t listenerId;
|
|
uintptr_t clientId;
|
|
uintptr_t acceptedConnectionId;
|
|
uintptr_t stalledConnectionId;
|
|
UA_UInt16 port;
|
|
HTTPTestMode mode;
|
|
size_t requestCount;
|
|
size_t acceptedConnectionCount;
|
|
size_t acceptedClosingCount;
|
|
size_t clientClosingCount;
|
|
size_t responseCount;
|
|
size_t responseCompleteCount;
|
|
size_t scalarHandleCallbacks;
|
|
size_t arrayHandleCallbacks;
|
|
size_t serverBodyBytes;
|
|
size_t clientBodyBytes;
|
|
size_t fixedResponseSize;
|
|
UA_Boolean bodyMismatch;
|
|
UA_Boolean responseHeaderReceived;
|
|
UA_StatusCode firstSendStatus;
|
|
UA_StatusCode secondSendStatus;
|
|
UA_StatusCode sendAfterCloseStatus;
|
|
UA_UInt16 responseStatus;
|
|
UA_StatusCode lastRequestStatus;
|
|
size_t goodRequestCompletions;
|
|
size_t badRequestCompletions;
|
|
} HTTPTestContext;
|
|
|
|
static void
|
|
fillPattern(UA_Byte *data, size_t length, size_t offset) {
|
|
for(size_t i = 0; i < length; i++)
|
|
data[i] = (UA_Byte)((offset + i) % 251);
|
|
}
|
|
|
|
static void
|
|
advancedHTTPCallback(UA_ConnectionManager *cm, uintptr_t connectionId,
|
|
void *application, void **connectionContext,
|
|
UA_ConnectionState state,
|
|
const UA_KeyValueMap *params, UA_ByteString msg) {
|
|
HTTPTestContext *ctx = (HTTPTestContext*)application;
|
|
const UA_UInt16 *listenPort = (const UA_UInt16*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "listen-port"), &UA_TYPES[UA_TYPES_UINT16]);
|
|
if(listenPort) {
|
|
ctx->listenerId = connectionId;
|
|
ctx->port = *listenPort;
|
|
return;
|
|
}
|
|
if(state == UA_CONNECTIONSTATE_OPENING) {
|
|
ctx->clientId = connectionId;
|
|
return;
|
|
}
|
|
if(state == UA_CONNECTIONSTATE_CLOSING) {
|
|
ck_assert_ptr_nonnull(connectionContext);
|
|
if(connectionId == ctx->clientId)
|
|
ctx->clientClosingCount++;
|
|
else if(connectionId != ctx->listenerId)
|
|
ctx->acceptedClosingCount++;
|
|
return;
|
|
}
|
|
|
|
const UA_String *method = (const UA_String*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "method"), &UA_TYPES[UA_TYPES_STRING]);
|
|
if(!method && connectionId != ctx->clientId) {
|
|
ck_assert_ptr_nonnull(connectionContext);
|
|
ck_assert_ptr_null(*connectionContext);
|
|
ck_assert_uint_ne(connectionId, ctx->listenerId);
|
|
*connectionContext = ctx;
|
|
ctx->acceptedConnectionCount++;
|
|
return;
|
|
}
|
|
if(method) {
|
|
ck_assert_ptr_nonnull(connectionContext);
|
|
ck_assert_ptr_eq(*connectionContext, ctx);
|
|
const UA_ByteString *requestRandom =
|
|
(const UA_ByteString*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "request-random"),
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
ck_assert_ptr_nonnull(requestRandom);
|
|
ck_assert_uint_eq(requestRandom->length, 32);
|
|
ctx->acceptedConnectionId = connectionId;
|
|
ctx->requestCount++;
|
|
for(size_t i = 0; i < msg.length; i++) {
|
|
if(msg.data[i] != (UA_Byte)((ctx->serverBodyBytes + i) % 251))
|
|
ctx->bodyMismatch = true;
|
|
}
|
|
ctx->serverBodyBytes += msg.length;
|
|
|
|
if(ctx->mode == HTTP_TEST_CLOSE_LISTENER) {
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx->listenerId),
|
|
UA_STATUSCODE_GOOD);
|
|
return;
|
|
}
|
|
|
|
const UA_String *path =
|
|
(const UA_String *)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "path"),
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
const UA_String failPath = UA_STRING_STATIC("/fail");
|
|
if(ctx->mode == HTTP_TEST_ONE_REQUEST_FAILS && path &&
|
|
UA_String_equal(path, &failPath)) {
|
|
ck_assert_uint_eq(cm->closeConnection(cm, connectionId),
|
|
UA_STATUSCODE_GOOD);
|
|
return;
|
|
}
|
|
|
|
const UA_String slowPath = UA_STRING_STATIC("/slow");
|
|
if(ctx->mode == HTTP_TEST_ONE_REQUEST_STALLS && path &&
|
|
UA_String_equal(path, &slowPath)) {
|
|
ctx->stalledConnectionId = connectionId;
|
|
return;
|
|
}
|
|
|
|
if(ctx->mode == HTTP_TEST_NO_RESPONSE)
|
|
return;
|
|
if(ctx->mode == HTTP_TEST_CLOSE_CONNECTION) {
|
|
ck_assert_uint_eq(cm->closeConnection(cm, connectionId),
|
|
UA_STATUSCODE_GOOD);
|
|
UA_ByteString late = UA_BYTESTRING_ALLOC("late");
|
|
ctx->sendAfterCloseStatus = cm->sendWithConnection(
|
|
cm, connectionId, &UA_KEYVALUEMAP_NULL, &late);
|
|
ck_assert_ptr_null(late.data);
|
|
return;
|
|
}
|
|
|
|
UA_ByteString response = UA_BYTESTRING_NULL;
|
|
size_t responseSize = msg.length;
|
|
if(ctx->mode == HTTP_TEST_EMPTY_RESPONSE)
|
|
responseSize = 0;
|
|
else if(ctx->mode == HTTP_TEST_FIXED_RESPONSE ||
|
|
ctx->mode == HTTP_TEST_ONE_REQUEST_FAILS)
|
|
responseSize = ctx->fixedResponseSize;
|
|
if(responseSize > 0) {
|
|
ck_assert_uint_eq(UA_ByteString_allocBuffer(&response, responseSize),
|
|
UA_STATUSCODE_GOOD);
|
|
fillPattern(response.data, response.length, 0);
|
|
}
|
|
|
|
UA_UInt16 statusCode = 202;
|
|
UA_String responseHeader = UA_STRING("present");
|
|
UA_String identityEncoding = UA_STRING("identity");
|
|
UA_KeyValuePair headers[3] = {0};
|
|
headers[0].key = UA_QUALIFIEDNAME(0, "x-response");
|
|
UA_Variant_setScalar(&headers[0].value, &responseHeader,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
size_t headersSize = 1;
|
|
if(ctx->mode == HTTP_TEST_DUPLICATE_ENCODING_RESPONSE) {
|
|
headers[1].key = UA_QUALIFIEDNAME(0, "content-encoding");
|
|
UA_Variant_setScalar(&headers[1].value, &identityEncoding,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
headers[2].key = UA_QUALIFIEDNAME(0, "Content-Encoding");
|
|
UA_Variant_setScalar(&headers[2].value, &identityEncoding,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
headersSize = 3;
|
|
}
|
|
UA_KeyValuePair responseParams[2] = {0};
|
|
responseParams[0].key = UA_QUALIFIEDNAME(0, "status-code");
|
|
UA_Variant_setScalar(&responseParams[0].value, &statusCode,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
responseParams[1].key = UA_QUALIFIEDNAME(0, "headers");
|
|
UA_Variant_setArray(&responseParams[1].value, headers, headersSize,
|
|
&UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
|
|
UA_KeyValueMap responseMap = {2, responseParams};
|
|
ctx->firstSendStatus = cm->sendWithConnection(
|
|
cm, connectionId, &responseMap, &response);
|
|
if(ctx->mode == HTTP_TEST_DUPLICATE_RESPONSE) {
|
|
UA_ByteString duplicate = UA_BYTESTRING_ALLOC("duplicate");
|
|
ctx->secondSendStatus = cm->sendWithConnection(
|
|
cm, connectionId, &responseMap, &duplicate);
|
|
ck_assert_ptr_null(duplicate.data);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if(connectionId != ctx->clientId)
|
|
return;
|
|
const UA_Variant *requestHandle = UA_KeyValueMap_get(
|
|
params, UA_QUALIFIEDNAME(0, "handle"));
|
|
if(requestHandle) {
|
|
if(UA_Variant_hasScalarType(requestHandle,
|
|
&UA_TYPES[UA_TYPES_UINT32])) {
|
|
ck_assert_uint_eq(*(UA_UInt32*)requestHandle->data, 17);
|
|
ctx->scalarHandleCallbacks++;
|
|
} else if(UA_Variant_hasArrayType(
|
|
requestHandle, &UA_TYPES[UA_TYPES_UINT16])) {
|
|
ck_assert(UA_Variant_hasArrayType(requestHandle,
|
|
&UA_TYPES[UA_TYPES_UINT16]));
|
|
ck_assert_uint_eq(requestHandle->arrayLength, 2);
|
|
const UA_UInt16 *values = (const UA_UInt16*)requestHandle->data;
|
|
ck_assert_uint_eq(values[0], 23);
|
|
ck_assert_uint_eq(values[1], 42);
|
|
ctx->arrayHandleCallbacks++;
|
|
} else
|
|
ck_assert(UA_Variant_hasScalarType(
|
|
requestHandle, &UA_TYPES[UA_TYPES_UINT64]));
|
|
}
|
|
const UA_Boolean *responseComplete =
|
|
(const UA_Boolean*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "response-complete"),
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
if(responseComplete) {
|
|
ck_assert(*responseComplete);
|
|
ctx->responseCompleteCount++;
|
|
const UA_StatusCode *requestStatus =
|
|
(const UA_StatusCode *)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "request-status"),
|
|
&UA_TYPES[UA_TYPES_STATUSCODE]);
|
|
ck_assert_ptr_nonnull(requestStatus);
|
|
ctx->lastRequestStatus = *requestStatus;
|
|
if(*requestStatus == UA_STATUSCODE_GOOD)
|
|
ctx->goodRequestCompletions++;
|
|
else
|
|
ctx->badRequestCompletions++;
|
|
}
|
|
const UA_UInt16 *statusCode = (const UA_UInt16*)UA_KeyValueMap_getScalar(
|
|
params, UA_QUALIFIEDNAME(0, "status-code"), &UA_TYPES[UA_TYPES_UINT16]);
|
|
if(statusCode) {
|
|
ctx->responseStatus = *statusCode;
|
|
if(msg.length == 0 && !responseComplete)
|
|
ctx->responseCount++;
|
|
}
|
|
const UA_Variant *responseHeaders = UA_KeyValueMap_get(
|
|
params, UA_QUALIFIEDNAME(0, "headers"));
|
|
if(responseHeaders) {
|
|
UA_KeyValuePair *headers = (UA_KeyValuePair*)responseHeaders->data;
|
|
const UA_String expectedName = UA_STRING_STATIC("x-response");
|
|
for(size_t i = 0; i < responseHeaders->arrayLength; i++) {
|
|
if(UA_String_equal(&headers[i].key.name, &expectedName))
|
|
ctx->responseHeaderReceived = true;
|
|
}
|
|
}
|
|
for(size_t i = 0; i < msg.length; i++) {
|
|
if(msg.data[i] != (UA_Byte)((ctx->clientBodyBytes + i) % 251))
|
|
ctx->bodyMismatch = true;
|
|
}
|
|
ctx->clientBodyBytes += msg.length;
|
|
}
|
|
|
|
static void
|
|
runUntilCount(UA_EventLoop *eventLoop, const size_t *value,
|
|
size_t expected, size_t iterations) {
|
|
for(size_t i = 0; i < iterations && *value < expected; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
ck_assert_uint_ge(*value, expected);
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
static int
|
|
connectHttpRaw(UA_UInt16 port) {
|
|
struct sockaddr_in address = {0};
|
|
address.sin_family = AF_INET;
|
|
address.sin_port = htons(port);
|
|
ck_assert_int_eq(inet_pton(AF_INET, "127.0.0.1", &address.sin_addr), 1);
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&address,
|
|
sizeof(address)), 0);
|
|
return fd;
|
|
}
|
|
|
|
static void
|
|
runUntilSocketReadable(UA_EventLoop *eventLoop, int fd, size_t iterations) {
|
|
for(size_t i = 0; i < iterations; i++) {
|
|
char byte;
|
|
ssize_t received = recv(fd, &byte, 1, MSG_PEEK | MSG_DONTWAIT);
|
|
if(received >= 0 || (errno != EAGAIN && errno != EWOULDBLOCK))
|
|
return;
|
|
eventLoop->run(eventLoop, 20);
|
|
}
|
|
ck_abort_msg("HTTP socket did not become readable");
|
|
}
|
|
|
|
static ssize_t
|
|
readSocketUntilClosed(UA_EventLoop *eventLoop, int fd, UA_Byte *buffer,
|
|
size_t capacity, size_t iterations) {
|
|
size_t total = 0;
|
|
for(size_t i = 0; i < iterations && total < capacity; i++) {
|
|
ssize_t received = recv(fd, &buffer[total], capacity - total,
|
|
MSG_DONTWAIT);
|
|
if(received > 0) {
|
|
total += (size_t)received;
|
|
continue;
|
|
}
|
|
if(received == 0)
|
|
return (ssize_t)total;
|
|
if(errno != EAGAIN && errno != EWOULDBLOCK)
|
|
return total > 0 ? (ssize_t)total : -1;
|
|
eventLoop->run(eventLoop, 20);
|
|
}
|
|
return (ssize_t)total;
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
closeHTTPConnectionIfPresent(UA_ConnectionManager *cm, uintptr_t connectionId) {
|
|
UA_StatusCode status = cm->closeConnection(cm, connectionId);
|
|
ck_assert(status == UA_STATUSCODE_GOOD ||
|
|
status == UA_STATUSCODE_BADNOTFOUND);
|
|
}
|
|
|
|
static void
|
|
openHTTPListener(UA_ConnectionManager *cm, HTTPTestContext *ctx,
|
|
UA_UInt16 timeout, UA_UInt32 recvMax, UA_UInt32 sendMax,
|
|
UA_Boolean useSSL, const UA_ByteString *certificate,
|
|
const UA_ByteString *privateKey,
|
|
const UA_ByteString *caCertificate) {
|
|
UA_UInt16 port = 0;
|
|
UA_Boolean listen = true;
|
|
UA_KeyValuePair parameters[8] = {0};
|
|
size_t size = 0;
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[size++].value, &port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "listen");
|
|
UA_Variant_setScalar(¶meters[size++].value, &listen,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "timeout");
|
|
UA_Variant_setScalar(¶meters[size++].value, &timeout,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
if(recvMax) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "recv-max-message-size");
|
|
UA_Variant_setScalar(¶meters[size++].value, &recvMax,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
}
|
|
if(sendMax) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "send-max-message-size");
|
|
UA_Variant_setScalar(¶meters[size++].value, &sendMax,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
}
|
|
if(useSSL) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "useSSL");
|
|
UA_Variant_setScalar(¶meters[size++].value, &useSSL,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "certificate");
|
|
UA_Variant_setScalar(¶meters[size++].value,
|
|
(void*)(uintptr_t)certificate,
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "private-key");
|
|
UA_Variant_setScalar(¶meters[size++].value,
|
|
(void*)(uintptr_t)privateKey,
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
}
|
|
if(caCertificate) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "ca-certificate");
|
|
UA_Variant_setScalar(¶meters[size++].value,
|
|
(void*)(uintptr_t)caCertificate,
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
}
|
|
UA_KeyValueMap map = {size, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &map, ctx, NULL,
|
|
advancedHTTPCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_ne(ctx->listenerId, 0);
|
|
ck_assert_uint_ne(ctx->port, 0);
|
|
}
|
|
|
|
static void
|
|
openHTTPClient(UA_ConnectionManager *cm, HTTPTestContext *ctx,
|
|
UA_UInt32 recvMax, UA_UInt32 sendMax, UA_Boolean useSSL,
|
|
const UA_ByteString *certificate,
|
|
const UA_ByteString *privateKey,
|
|
const UA_ByteString *caCertificate) {
|
|
UA_String address = UA_STRING("127.0.0.1");
|
|
UA_KeyValuePair parameters[8] = {0};
|
|
size_t size = 0;
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(¶meters[size++].value, &address,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[size++].value, &ctx->port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
if(recvMax) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "recv-max-message-size");
|
|
UA_Variant_setScalar(¶meters[size++].value, &recvMax,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
}
|
|
if(sendMax) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "send-max-message-size");
|
|
UA_Variant_setScalar(¶meters[size++].value, &sendMax,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
}
|
|
if(useSSL) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "useSSL");
|
|
UA_Variant_setScalar(¶meters[size++].value, &useSSL,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
if(certificate) {
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "certificate");
|
|
UA_Variant_setScalar(¶meters[size++].value,
|
|
(void*)(uintptr_t)certificate,
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "private-key");
|
|
UA_Variant_setScalar(¶meters[size++].value,
|
|
(void*)(uintptr_t)privateKey,
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
}
|
|
parameters[size].key = UA_QUALIFIEDNAME(0, "ca-certificate");
|
|
UA_Variant_setScalar(¶meters[size++].value,
|
|
(void*)(uintptr_t)caCertificate,
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
}
|
|
UA_KeyValueMap map = {size, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &map, ctx, NULL,
|
|
advancedHTTPCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_ne(ctx->clientId, 0);
|
|
}
|
|
|
|
static void
|
|
openHTTPClientWithTimeout(UA_ConnectionManager *cm, HTTPTestContext *ctx,
|
|
UA_UInt16 timeout) {
|
|
UA_String address = UA_STRING("127.0.0.1");
|
|
UA_KeyValuePair parameters[3] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(¶meters[0].value, &address,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[1].value, &ctx->port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[2].key = UA_QUALIFIEDNAME(0, "timeout");
|
|
UA_Variant_setScalar(¶meters[2].value, &timeout,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
UA_KeyValueMap map = {3, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &map, ctx, NULL,
|
|
advancedHTTPCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_ne(ctx->clientId, 0);
|
|
}
|
|
|
|
static void
|
|
openHTTPClientAt(UA_ConnectionManager *cm, HTTPTestContext *ctx,
|
|
UA_String *address, UA_UInt16 port) {
|
|
UA_KeyValuePair parameters[2] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(¶meters[0].value, address,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[1].value, &port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
UA_KeyValueMap map = {2, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &map, ctx, NULL,
|
|
advancedHTTPCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_ne(ctx->clientId, 0);
|
|
}
|
|
|
|
static void
|
|
sendPatternRequest(UA_ConnectionManager *cm, HTTPTestContext *ctx,
|
|
size_t size) {
|
|
UA_String method = UA_STRING("POST");
|
|
UA_String path = UA_STRING("/test");
|
|
UA_KeyValuePair parameters[2] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "method");
|
|
UA_Variant_setScalar(¶meters[0].value, &method,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "path");
|
|
UA_Variant_setScalar(¶meters[1].value, &path,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
UA_KeyValueMap map = {2, parameters};
|
|
UA_ByteString body = UA_BYTESTRING_NULL;
|
|
if(size > 0) {
|
|
ck_assert_uint_eq(UA_ByteString_allocBuffer(&body, size),
|
|
UA_STATUSCODE_GOOD);
|
|
fillPattern(body.data, body.length, 0);
|
|
}
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx->clientId, &map,
|
|
size ? &body : NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
}
|
|
|
|
static void
|
|
sendHandledRequestWithTimeout(UA_ConnectionManager *cm, HTTPTestContext *ctx,
|
|
const char *pathValue, UA_UInt16 timeout) {
|
|
UA_String method = UA_STRING("POST");
|
|
UA_String path = UA_STRING((char *)(uintptr_t)pathValue);
|
|
UA_UInt64 requestHandle = 5381;
|
|
for(const char *pos = pathValue; *pos; pos++)
|
|
requestHandle = requestHandle * 33u + (UA_Byte)*pos;
|
|
UA_KeyValuePair parameters[4] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "method");
|
|
UA_Variant_setScalar(¶meters[0].value, &method,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "path");
|
|
UA_Variant_setScalar(¶meters[1].value, &path,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
parameters[2].key = UA_QUALIFIEDNAME(0, "handle");
|
|
UA_Variant_setScalar(¶meters[2].value, &requestHandle,
|
|
&UA_TYPES[UA_TYPES_UINT64]);
|
|
size_t parametersSize = 3;
|
|
if(timeout > 0) {
|
|
parameters[parametersSize].key = UA_QUALIFIEDNAME(0, "timeout");
|
|
UA_Variant_setScalar(¶meters[parametersSize++].value, &timeout,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
}
|
|
UA_KeyValueMap map = {parametersSize, parameters};
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx->clientId, &map, NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
}
|
|
|
|
static void
|
|
sendHandledRequest(UA_ConnectionManager *cm, HTTPTestContext *ctx,
|
|
const char *pathValue) {
|
|
sendHandledRequestWithTimeout(cm, ctx, pathValue, 0);
|
|
}
|
|
|
|
START_TEST(serverLargeAndEmptyBodies) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_ECHO;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
const size_t largeSize = 128 * 1024 + 17;
|
|
sendPatternRequest(cm, &ctx, largeSize);
|
|
runUntilCount(eventLoop, &ctx.clientBodyBytes, largeSize, 500);
|
|
ck_assert_uint_eq(ctx.serverBodyBytes, largeSize);
|
|
ck_assert_uint_eq(ctx.responseStatus, 202);
|
|
ck_assert(ctx.responseHeaderReceived);
|
|
ck_assert(!ctx.bodyMismatch);
|
|
|
|
ctx.mode = HTTP_TEST_EMPTY_RESPONSE;
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.responseCount, 2, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 2);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(clientRequestHandles) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
ctx.fixedResponseSize = 2;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
UA_String method = UA_STRING("POST");
|
|
UA_String path = UA_STRING("/handles");
|
|
UA_UInt32 scalarHandle = 17;
|
|
UA_KeyValuePair scalarParams[3] = {0};
|
|
scalarParams[0].key = UA_QUALIFIEDNAME(0, "method");
|
|
UA_Variant_setScalar(&scalarParams[0].value, &method,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
scalarParams[1].key = UA_QUALIFIEDNAME(0, "path");
|
|
UA_Variant_setScalar(&scalarParams[1].value, &path,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
scalarParams[2].key = UA_QUALIFIEDNAME(0, "handle");
|
|
UA_Variant_setScalar(&scalarParams[2].value, &scalarHandle,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
UA_KeyValueMap scalarMap = {3, scalarParams};
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx.clientId, &scalarMap, NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx.clientId, &scalarMap, NULL),
|
|
UA_STATUSCODE_BADINVALIDSTATE);
|
|
scalarHandle = 99; /* The request retained a deep copy. */
|
|
|
|
UA_UInt16 arrayHandle[2] = {23, 42};
|
|
UA_KeyValuePair arrayParams[3] = {0};
|
|
arrayParams[0] = scalarParams[0];
|
|
arrayParams[1] = scalarParams[1];
|
|
arrayParams[2].key = UA_QUALIFIEDNAME(0, "handle");
|
|
UA_Variant_setArray(&arrayParams[2].value, arrayHandle, 2,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
UA_KeyValueMap arrayMap = {3, arrayParams};
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx.clientId, &arrayMap, NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
arrayHandle[0] = 100;
|
|
arrayHandle[1] = 101;
|
|
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 2, 300);
|
|
ck_assert_uint_eq(ctx.requestCount, 2);
|
|
ck_assert_uint_eq(ctx.clientBodyBytes, 4);
|
|
ck_assert_uint_ge(ctx.scalarHandleCallbacks, 3);
|
|
ck_assert_uint_ge(ctx.arrayHandleCallbacks, 3);
|
|
|
|
/* Legacy requests remain supported, but ambiguous concurrency is rejected. */
|
|
ctx.mode = HTTP_TEST_NO_RESPONSE;
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx.clientId,
|
|
&UA_KEYVALUEMAP_NULL, NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx.clientId,
|
|
&UA_KEYVALUEMAP_NULL, NULL),
|
|
UA_STATUSCODE_BADINVALIDSTATE);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(serverRequestLifecycle) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_DUPLICATE_RESPONSE;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
sendPatternRequest(cm, &ctx, 4);
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 200);
|
|
ck_assert_uint_eq(ctx.firstSendStatus, UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(ctx.secondSendStatus, UA_STATUSCODE_BADINVALIDSTATE);
|
|
ck_assert_uint_eq(ctx.acceptedClosingCount, 0);
|
|
|
|
ctx.mode = HTTP_TEST_CLOSE_CONNECTION;
|
|
sendPatternRequest(cm, &ctx, 4);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 200);
|
|
ck_assert(ctx.sendAfterCloseStatus == UA_STATUSCODE_BADINVALIDSTATE ||
|
|
ctx.sendAfterCloseStatus == UA_STATUSCODE_BADNOTFOUND);
|
|
ck_assert_uint_eq(ctx.acceptedClosingCount, 1);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(serverBrokenCarrierIsolation) {
|
|
HTTPTestContext first = {0};
|
|
first.mode = HTTP_TEST_NO_RESPONSE;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &first, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &first, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
/* Leave one request outstanding, then break only its client-side carrier. */
|
|
sendPatternRequest(cm, &first, 0);
|
|
runUntilCount(eventLoop, &first.requestCount, 1, 100);
|
|
|
|
HTTPTestContext sibling = {0};
|
|
sibling.port = first.port;
|
|
openHTTPClient(cm, &sibling, 0, 0, false, NULL, NULL, NULL);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, first.clientId),
|
|
UA_STATUSCODE_GOOD);
|
|
runUntilCount(eventLoop, &first.acceptedClosingCount, 1, 100);
|
|
runUntilCount(eventLoop, &first.clientClosingCount, 1, 100);
|
|
|
|
/* The sibling uses the same listener and must remain fully usable. */
|
|
first.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
first.fixedResponseSize = 2;
|
|
sendPatternRequest(cm, &sibling, 0);
|
|
runUntilCount(eventLoop, &sibling.responseCompleteCount, 1, 100);
|
|
ck_assert_uint_eq(first.requestCount, 2);
|
|
ck_assert_uint_eq(sibling.clientBodyBytes, 2);
|
|
ck_assert_uint_eq(first.acceptedClosingCount, 1);
|
|
|
|
closeHTTPConnectionIfPresent(cm, sibling.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, first.listenerId),
|
|
UA_STATUSCODE_GOOD);
|
|
runUntilCount(eventLoop, &first.acceptedClosingCount, 2, 100);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(clientRequestFailureIsolation) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_ONE_REQUEST_FAILS;
|
|
ctx.fixedResponseSize = 2;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
sendHandledRequest(cm, &ctx, "/fail");
|
|
sendHandledRequest(cm, &ctx, "/ok");
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 2, 200);
|
|
ck_assert_uint_eq(ctx.goodRequestCompletions, 1);
|
|
ck_assert_uint_eq(ctx.badRequestCompletions, 1);
|
|
ck_assert_uint_eq(ctx.clientBodyBytes, 2);
|
|
ck_assert_uint_eq(ctx.clientClosingCount, 0);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
closeHTTPConnectionIfPresent(cm, ctx.listenerId);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(clientSynchronousConnectionFailure) {
|
|
HTTPTestContext ctx = {0};
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
|
|
/* Some libwebsockets resolvers report a DNS failure from inside
|
|
* lws_client_connect_via_info. The callback must see an already-linked
|
|
* request and cleanup must defer freeing it until the connect call
|
|
* returns. */
|
|
UA_String address = UA_STRING("open62541.invalid");
|
|
openHTTPClientAt(cm, &ctx, &address, 80);
|
|
sendHandledRequest(cm, &ctx, "/dns-failure");
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 100);
|
|
ck_assert_uint_eq(ctx.badRequestCompletions, 1);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(clientRequestTimeoutIsolation) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_ONE_REQUEST_STALLS;
|
|
ctx.fixedResponseSize = 2;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
/* The slow request overrides the binding timeout. Its sibling must finish
|
|
* normally and the shared client binding must remain usable. */
|
|
sendHandledRequestWithTimeout(cm, &ctx, "/slow", 1);
|
|
sendHandledRequest(cm, &ctx, "/ok");
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 2, 200);
|
|
ck_assert_uint_eq(ctx.goodRequestCompletions, 1);
|
|
ck_assert_uint_eq(ctx.badRequestCompletions, 1);
|
|
ck_assert_uint_eq(ctx.clientClosingCount, 0);
|
|
ck_assert_uint_ne(ctx.stalledConnectionId, 0);
|
|
|
|
/* A response attempted after the timed-out carrier was destroyed cannot
|
|
* generate a second callback or become associated with another request. */
|
|
UA_UInt16 statusCode = 200;
|
|
UA_KeyValuePair responseParam = {
|
|
UA_QUALIFIEDNAME(0, "status-code"), {0}};
|
|
UA_Variant_setScalar(&responseParam.value, &statusCode,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
UA_KeyValueMap responseMap = {1, &responseParam};
|
|
UA_ByteString late = UA_BYTESTRING_ALLOC("late");
|
|
UA_StatusCode lateStatus = cm->sendWithConnection(
|
|
cm, ctx.stalledConnectionId, &responseMap, &late);
|
|
ck_assert(lateStatus == UA_STATUSCODE_GOOD ||
|
|
lateStatus == UA_STATUSCODE_BADNOTFOUND);
|
|
ck_assert_ptr_null(late.data);
|
|
for(size_t i = 0; i < 20; i++)
|
|
eventLoop->run(eventLoop, 10);
|
|
ck_assert_uint_eq(ctx.responseCompleteCount, 2);
|
|
|
|
sendHandledRequest(cm, &ctx, "/ok");
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 3, 100);
|
|
ck_assert_uint_eq(ctx.goodRequestCompletions, 2);
|
|
ck_assert_uint_eq(ctx.badRequestCompletions, 1);
|
|
ck_assert_uint_eq(ctx.clientClosingCount, 0);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
closeHTTPConnectionIfPresent(cm, ctx.listenerId);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(serverResponseFailureIsolation) {
|
|
#ifndef _WIN32
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
ctx.fixedResponseSize = 8 * 1024 * 1024;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
struct sockaddr_in serverAddress;
|
|
memset(&serverAddress, 0, sizeof(serverAddress));
|
|
serverAddress.sin_family = AF_INET;
|
|
serverAddress.sin_port = htons(ctx.port);
|
|
ck_assert_int_eq(inet_pton(AF_INET, "127.0.0.1",
|
|
&serverAddress.sin_addr), 1);
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char request[] =
|
|
"POST /large HTTP/1.1\r\nHost: localhost\r\n"
|
|
"Content-Length: 0\r\nConnection: keep-alive\r\n\r\n";
|
|
ck_assert_int_eq(send(fd, request, sizeof(request) - 1, 0),
|
|
(ssize_t)(sizeof(request) - 1));
|
|
runUntilCount(eventLoop, &ctx.requestCount, 1, 100);
|
|
struct linger reset = {1, 0};
|
|
ck_assert_int_eq(setsockopt(fd, SOL_SOCKET, SO_LINGER, &reset,
|
|
sizeof(reset)), 0);
|
|
close(fd);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 200);
|
|
|
|
/* A failed response carrier does not affect a new sibling carrier. */
|
|
ctx.fixedResponseSize = 2;
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 100);
|
|
ck_assert_uint_eq(ctx.requestCount, 2);
|
|
ck_assert_uint_eq(ctx.clientBodyBytes, 2);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId),
|
|
UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
#endif
|
|
} END_TEST
|
|
|
|
START_TEST(serverListenerCloseWithIdleCarrier) {
|
|
#ifndef _WIN32
|
|
HTTPTestContext ctx = {0};
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
struct sockaddr_in serverAddress;
|
|
memset(&serverAddress, 0, sizeof(serverAddress));
|
|
serverAddress.sin_family = AF_INET;
|
|
serverAddress.sin_port = htons(ctx.port);
|
|
ck_assert_int_eq(inet_pton(AF_INET, "127.0.0.1",
|
|
&serverAddress.sin_addr), 1);
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
for(size_t i = 0; i < 10; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
|
|
/* Shutdown must retain the listener until this idle carrier is detached. */
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId),
|
|
UA_STATUSCODE_GOOD);
|
|
for(size_t i = 0; i < 10; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
close(fd);
|
|
stopEventLoop(eventLoop);
|
|
#endif
|
|
} END_TEST
|
|
|
|
START_TEST(serverListenerCloseFromRequestCallback) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_CLOSE_LISTENER;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm);
|
|
ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop,
|
|
&cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
/* Listener teardown is requested from inside the accepted connection's
|
|
* request callback. Destruction is deferred until the callback has fully
|
|
* returned, then the accepted carrier and client request complete once. */
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 100);
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 100);
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
ck_assert_uint_eq(ctx.acceptedClosingCount, 1);
|
|
ck_assert_uint_eq(ctx.responseCompleteCount, 1);
|
|
ck_assert_uint_ne(ctx.lastRequestStatus, UA_STATUSCODE_GOOD);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
#ifdef UA_ENABLE_HTTP_COMPRESSION
|
|
static int
|
|
connectRawHttp(UA_UInt16 port) {
|
|
struct sockaddr_in serverAddress;
|
|
memset(&serverAddress, 0, sizeof(serverAddress));
|
|
serverAddress.sin_family = AF_INET;
|
|
serverAddress.sin_port = htons(port);
|
|
ck_assert_int_eq(inet_pton(AF_INET, "127.0.0.1",
|
|
&serverAddress.sin_addr), 1);
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
return fd;
|
|
}
|
|
|
|
static size_t
|
|
findHeaderEnd(const UA_Byte *response, size_t length) {
|
|
for(size_t i = 0; i + 3 < length; i++) {
|
|
if(memcmp(&response[i], "\r\n\r\n", 4) == 0)
|
|
return i + 4;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
START_TEST(serverContentEncodingAndExpansionLimit) {
|
|
#ifndef _WIN32
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_ECHO;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
|
|
UA_UInt16 port = 0;
|
|
UA_UInt16 timeout = 30;
|
|
UA_UInt32 wireLimit = 1024;
|
|
UA_UInt32 expandedLimit = 8192;
|
|
UA_Boolean listen = true;
|
|
UA_KeyValuePair parameters[5] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[0].value, &port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "listen");
|
|
UA_Variant_setScalar(¶meters[1].value, &listen,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
parameters[2].key = UA_QUALIFIEDNAME(0, "timeout");
|
|
UA_Variant_setScalar(¶meters[2].value, &timeout,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[3].key = UA_QUALIFIEDNAME(0, "recv-max-message-size");
|
|
UA_Variant_setScalar(¶meters[3].value, &wireLimit,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
parameters[4].key = UA_QUALIFIEDNAME(
|
|
0, "recv-max-decompressed-message-size");
|
|
UA_Variant_setScalar(¶meters[4].value, &expandedLimit,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
UA_KeyValueMap map = {5, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &map, &ctx, NULL,
|
|
advancedHTTPCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
|
|
UA_ByteString input = UA_BYTESTRING_NULL;
|
|
ck_assert_uint_eq(UA_ByteString_allocBuffer(&input, 4096),
|
|
UA_STATUSCODE_GOOD);
|
|
fillPattern(input.data, input.length, 0);
|
|
UA_ByteString compressed = UA_BYTESTRING_NULL;
|
|
ck_assert_uint_eq(UA_HTTP_compress(UA_HTTP_CONTENT_ENCODING_GZIP,
|
|
&input, &compressed),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_lt(compressed.length, wireLimit);
|
|
|
|
int fd = connectRawHttp(ctx.port);
|
|
char header[512];
|
|
int headerLength = snprintf(
|
|
header, sizeof(header),
|
|
"POST /compressed HTTP/1.1\r\nHost: localhost\r\n"
|
|
"Content-Encoding: gzip\r\nAccept-Encoding: gzip\r\n"
|
|
"Content-Length: %zu\r\nConnection: close\r\n\r\n",
|
|
compressed.length);
|
|
ck_assert_int_gt(headerLength, 0);
|
|
ck_assert_int_eq(send(fd, header, (size_t)headerLength, 0), headerLength);
|
|
ck_assert_int_eq(send(fd, compressed.data, compressed.length, 0),
|
|
(ssize_t)compressed.length);
|
|
runUntilCount(eventLoop, &ctx.requestCount, 1, 200);
|
|
runUntilSocketReadable(eventLoop, fd, 100);
|
|
UA_Byte response[16384];
|
|
ssize_t received = readSocketUntilClosed(
|
|
eventLoop, fd, response, sizeof(response) - 1, 100);
|
|
ck_assert_int_gt(received, 0);
|
|
close(fd);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 200);
|
|
response[received] = 0;
|
|
ck_assert_ptr_nonnull(strstr((char*)response, "HTTP/1.1 202"));
|
|
ck_assert_ptr_nonnull(strstr((char*)response, "Content-Encoding: gzip"));
|
|
size_t bodyOffset = findHeaderEnd(response, (size_t)received);
|
|
ck_assert_uint_gt(bodyOffset, 0);
|
|
UA_ByteString encodedResponse = {
|
|
(size_t)received - bodyOffset, &response[bodyOffset]
|
|
};
|
|
UA_ByteString decodedResponse = UA_BYTESTRING_NULL;
|
|
ck_assert_uint_eq(UA_HTTP_decompress(UA_HTTP_CONTENT_ENCODING_GZIP,
|
|
&encodedResponse, 8192,
|
|
&decodedResponse),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert(UA_ByteString_equal(&input, &decodedResponse));
|
|
UA_ByteString_clear(&decodedResponse);
|
|
UA_ByteString_clear(&compressed);
|
|
UA_ByteString_clear(&input);
|
|
|
|
/* A small wire body that expands past the independent limit gets 413 and
|
|
* is never delivered to the application. */
|
|
ck_assert_uint_eq(UA_ByteString_allocBuffer(&input, expandedLimit + 1),
|
|
UA_STATUSCODE_GOOD);
|
|
memset(input.data, 0, input.length);
|
|
ck_assert_uint_eq(UA_HTTP_compress(UA_HTTP_CONTENT_ENCODING_GZIP,
|
|
&input, &compressed),
|
|
UA_STATUSCODE_GOOD);
|
|
fd = connectRawHttp(ctx.port);
|
|
headerLength = snprintf(
|
|
header, sizeof(header),
|
|
"POST /bomb HTTP/1.1\r\nHost: localhost\r\n"
|
|
"Content-Encoding: gzip\r\nContent-Length: %zu\r\n"
|
|
"Connection: close\r\n\r\n", compressed.length);
|
|
ck_assert_int_eq(send(fd, header, (size_t)headerLength, 0), headerLength);
|
|
ck_assert_int_eq(send(fd, compressed.data, compressed.length, 0),
|
|
(ssize_t)compressed.length);
|
|
runUntilSocketReadable(eventLoop, fd, 100);
|
|
received = readSocketUntilClosed(
|
|
eventLoop, fd, response, sizeof(response) - 1, 100);
|
|
ck_assert_int_gt(received, 0);
|
|
close(fd);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 2, 200);
|
|
response[received] = 0;
|
|
ck_assert_ptr_nonnull(strstr((char*)response, "HTTP/1.1 413"));
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
UA_ByteString_clear(&compressed);
|
|
UA_ByteString_clear(&input);
|
|
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId),
|
|
UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
#endif
|
|
} END_TEST
|
|
|
|
START_TEST(clientCompressedResponse) {
|
|
#ifndef _WIN32
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
ctx.fixedResponseSize = 4096;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
UA_String address = UA_STRING("127.0.0.1");
|
|
UA_UInt32 wireLimit = 1024;
|
|
UA_UInt32 expandedLimit = 8192;
|
|
UA_KeyValuePair parameters[4] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "address");
|
|
UA_Variant_setScalar(¶meters[0].value, &address,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[1].value, &ctx.port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[2].key = UA_QUALIFIEDNAME(0, "recv-max-message-size");
|
|
UA_Variant_setScalar(¶meters[2].value, &wireLimit,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
parameters[3].key = UA_QUALIFIEDNAME(
|
|
0, "recv-max-decompressed-message-size");
|
|
UA_Variant_setScalar(¶meters[3].value, &expandedLimit,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
UA_KeyValueMap map = {4, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &map, &ctx, NULL,
|
|
advancedHTTPCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 200);
|
|
ck_assert_uint_eq(ctx.clientBodyBytes, ctx.fixedResponseSize);
|
|
ck_assert(!ctx.bodyMismatch);
|
|
ck_assert_uint_eq(ctx.responseStatus, 202);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
closeHTTPConnectionIfPresent(cm, ctx.listenerId);
|
|
stopEventLoop(eventLoop);
|
|
#endif
|
|
} END_TEST
|
|
#endif
|
|
|
|
START_TEST(clientRejectsDuplicateContentEncoding) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_DUPLICATE_ENCODING_RESPONSE;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClientWithTimeout(cm, &ctx, 1);
|
|
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 200);
|
|
ck_assert_uint_eq(ctx.responseCompleteCount, 1);
|
|
ck_assert_uint_ne(ctx.lastRequestStatus, UA_STATUSCODE_GOOD);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
closeHTTPConnectionIfPresent(cm, ctx.listenerId);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(serverTimeoutAndListenerClose) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_NO_RESPONSE;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 1, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 150);
|
|
ck_assert_uint_eq(ctx.acceptedClosingCount, 1);
|
|
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 100);
|
|
ck_assert_uint_ne(ctx.lastRequestStatus, UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(ctx.clientClosingCount, 0);
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.requestCount, 2, 100);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId),
|
|
UA_STATUSCODE_GOOD);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 2, 100);
|
|
ck_assert_uint_eq(ctx.acceptedClosingCount, 2);
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(serverReceiveAndKeepAliveTimeouts) {
|
|
#ifndef _WIN32
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
ctx.fixedResponseSize = 2;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 1, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
struct sockaddr_in serverAddress;
|
|
memset(&serverAddress, 0, sizeof(serverAddress));
|
|
serverAddress.sin_family = AF_INET;
|
|
serverAddress.sin_port = htons(ctx.port);
|
|
ck_assert_int_eq(inet_pton(AF_INET, "127.0.0.1",
|
|
&serverAddress.sin_addr), 1);
|
|
|
|
/* A client that never completes its headers cannot retain the raw WSI. */
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char partialHeader[] = "POST /slow HTTP/1.1\r\nHost:";
|
|
ck_assert_int_eq(send(fd, partialHeader, sizeof(partialHeader) - 1, 0),
|
|
(ssize_t)(sizeof(partialHeader) - 1));
|
|
for(size_t i = 0; i < 75; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
char byte;
|
|
ssize_t received = recv(fd, &byte, 1, MSG_DONTWAIT);
|
|
ck_assert(received == 0 ||
|
|
(received < 0 && errno != EAGAIN && errno != EWOULDBLOCK));
|
|
ck_assert_uint_eq(ctx.requestCount, 0);
|
|
close(fd);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 100);
|
|
|
|
/* An incomplete request body times out the accepted connection without
|
|
* delivering a request to the application. */
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char partialBody[] =
|
|
"POST /slow-body HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Content-Length: 4\r\n\r\n"
|
|
"x";
|
|
ck_assert_int_eq(send(fd, partialBody, sizeof(partialBody) - 1, 0),
|
|
(ssize_t)(sizeof(partialBody) - 1));
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 2, 100);
|
|
ck_assert_uint_eq(ctx.requestCount, 0);
|
|
close(fd);
|
|
|
|
/* Body progress within the absolute request deadline is accepted. The
|
|
* keep-alive socket is then bounded again after the response completes. */
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char progressHeader[] =
|
|
"POST /progress HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Content-Length: 3\r\n"
|
|
"Connection: keep-alive\r\n\r\n";
|
|
ck_assert_int_eq(send(fd, progressHeader, sizeof(progressHeader) - 1, 0),
|
|
(ssize_t)(sizeof(progressHeader) - 1));
|
|
for(UA_Byte value = 0; value < 3; value++) {
|
|
ck_assert_int_eq(send(fd, &value, 1, 0), 1);
|
|
if(value < 2) {
|
|
for(size_t i = 0; i < 15; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
}
|
|
}
|
|
runUntilCount(eventLoop, &ctx.requestCount, 1, 100);
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
char response[1024];
|
|
runUntilSocketReadable(eventLoop, fd, 100);
|
|
received = recv(fd, response, sizeof(response), 0);
|
|
ck_assert_int_gt(received, 0);
|
|
/* LWS can write the header and body in separate callbacks. Drain both
|
|
* before checking that the later keep-alive timeout closed the socket. */
|
|
for(size_t i = 0; i < 10; i++) {
|
|
eventLoop->run(eventLoop, 10);
|
|
while(recv(fd, response, sizeof(response), MSG_DONTWAIT) > 0) {}
|
|
}
|
|
for(size_t i = 0; i < 75; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
received = recv(fd, &byte, 1, MSG_DONTWAIT);
|
|
ck_assert(received == 0 ||
|
|
(received < 0 && errno != EAGAIN && errno != EWOULDBLOCK));
|
|
close(fd);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 3, 100);
|
|
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
#endif
|
|
} END_TEST
|
|
|
|
START_TEST(serverConnectionCapAndRequestDeadline) {
|
|
#ifndef _WIN32
|
|
HTTPTestContext ctx = {0};
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
|
|
UA_UInt16 port = 0, timeout = 1;
|
|
UA_UInt32 maxConnections = 1;
|
|
UA_Boolean listen = true;
|
|
UA_KeyValuePair parameters[4] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[0].value, &port, &UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "listen");
|
|
UA_Variant_setScalar(¶meters[1].value, &listen,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
parameters[2].key = UA_QUALIFIEDNAME(0, "timeout");
|
|
UA_Variant_setScalar(¶meters[2].value, &timeout,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[3].key = UA_QUALIFIEDNAME(0, "max-connections");
|
|
UA_Variant_setScalar(¶meters[3].value, &maxConnections,
|
|
&UA_TYPES[UA_TYPES_UINT32]);
|
|
UA_KeyValueMap map = {4, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &map, &ctx, NULL,
|
|
advancedHTTPCallback),
|
|
UA_STATUSCODE_GOOD);
|
|
|
|
int first = connectHttpRaw(ctx.port);
|
|
runUntilCount(eventLoop, &ctx.acceptedConnectionCount, 1, 30);
|
|
int second = connectHttpRaw(ctx.port);
|
|
for(size_t i = 0; i < 10; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 1);
|
|
|
|
/* Each fragment arrives before the inactivity timeout. The incomplete
|
|
* request must still close at the absolute one-second deadline. */
|
|
const char fragment[] = "GET /";
|
|
ck_assert_int_eq(send(first, fragment, sizeof(fragment) - 1, 0),
|
|
(ssize_t)(sizeof(fragment) - 1));
|
|
for(size_t i = 0; i < 20; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
ck_assert_int_eq(send(first, "slow", 4, 0), 4);
|
|
for(size_t i = 0; i < 20; i++)
|
|
eventLoop->run(eventLoop, 20);
|
|
const char finalFragment[] = " HTTP/1.1\r\nHost:";
|
|
ck_assert_int_eq(send(first, finalFragment,
|
|
sizeof(finalFragment) - 1, 0),
|
|
(ssize_t)(sizeof(finalFragment) - 1));
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 25);
|
|
ck_assert_uint_eq(ctx.requestCount, 0);
|
|
close(first);
|
|
close(second);
|
|
|
|
int third = connectHttpRaw(ctx.port);
|
|
runUntilCount(eventLoop, &ctx.acceptedConnectionCount, 2, 30);
|
|
close(third);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
#endif
|
|
} END_TEST
|
|
|
|
START_TEST(clientServerMutualTls) {
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_EMPTY_RESPONSE;
|
|
UA_ByteString certificate = loadFile("server_cert.der");
|
|
UA_ByteString privateKey = loadFile("server_key.der");
|
|
ck_assert_ptr_nonnull(certificate.data);
|
|
ck_assert_ptr_nonnull(privateKey.data);
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("https"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, true, &certificate, &privateKey,
|
|
&certificate);
|
|
openHTTPClient(cm, &ctx, 0, 0, true, &certificate, &privateKey,
|
|
&certificate);
|
|
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.responseCount, 1, 300);
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
ck_assert_uint_eq(ctx.responseStatus, 202);
|
|
ck_assert(ctx.responseHeaderReceived);
|
|
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
runUntilCount(eventLoop, &ctx.clientClosingCount, 1, 100);
|
|
|
|
/* The listener CA makes a client certificate mandatory. */
|
|
openHTTPClient(cm, &ctx, 0, 0, true, NULL, NULL, &certificate);
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 2, 200);
|
|
ck_assert_uint_ne(ctx.lastRequestStatus, UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
runUntilCount(eventLoop, &ctx.clientClosingCount, 2, 100);
|
|
|
|
UA_ByteString_clear(&certificate);
|
|
UA_ByteString_clear(&privateKey);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(messageSizeLimits) {
|
|
/* Client send limit. */
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_EMPTY_RESPONSE;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 4, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 0, 4, false, NULL, NULL, NULL);
|
|
UA_ByteString tooLarge = UA_BYTESTRING_ALLOC("12345");
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, ctx.clientId,
|
|
&UA_KEYVALUEMAP_NULL, &tooLarge),
|
|
UA_STATUSCODE_BADREQUESTTOOLARGE);
|
|
ck_assert_ptr_null(tooLarge.data);
|
|
|
|
/* Listener receive limit on a client without a send limit. */
|
|
sendPatternRequest(cm, &ctx, 4);
|
|
runUntilCount(eventLoop, &ctx.responseCount, 1, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
runUntilCount(eventLoop, &ctx.clientClosingCount, 1, 100);
|
|
openHTTPClient(cm, &ctx, 0, 0, false, NULL, NULL, NULL);
|
|
sendPatternRequest(cm, &ctx, 5);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 2, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
|
|
/* Listener response and client receive limits. */
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
ctx.fixedResponseSize = 5;
|
|
cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 4, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 4, 0, false, NULL, NULL, NULL);
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.requestCount, 1, 100);
|
|
ck_assert_uint_eq(ctx.firstSendStatus, UA_STATUSCODE_BADRESPONSETOOLARGE);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.acceptedConnectionId),
|
|
UA_STATUSCODE_GOOD);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 100);
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
ctx.fixedResponseSize = 5;
|
|
cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
openHTTPClient(cm, &ctx, 4, 0, false, NULL, NULL, NULL);
|
|
sendPatternRequest(cm, &ctx, 0);
|
|
runUntilCount(eventLoop, &ctx.responseCompleteCount, 1, 200);
|
|
ck_assert_uint_eq(ctx.lastRequestStatus,
|
|
UA_STATUSCODE_BADRESPONSETOOLARGE);
|
|
ck_assert_uint_eq(ctx.clientBodyBytes, 0);
|
|
closeHTTPConnectionIfPresent(cm, ctx.clientId);
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(parameterValidation) {
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
|
|
UA_UInt16 port = 0;
|
|
UA_Boolean listen = true;
|
|
UA_Boolean validate = true;
|
|
UA_KeyValuePair parameters[4] = {0};
|
|
parameters[0].key = UA_QUALIFIEDNAME(0, "port");
|
|
UA_Variant_setScalar(¶meters[0].value, &port,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
parameters[1].key = UA_QUALIFIEDNAME(0, "listen");
|
|
UA_Variant_setScalar(¶meters[1].value, &listen,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
parameters[2].key = UA_QUALIFIEDNAME(0, "validate");
|
|
UA_Variant_setScalar(¶meters[2].value, &validate,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
UA_KeyValueMap validMap = {3, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &validMap, NULL, NULL, NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
|
|
UA_Boolean useSSL = true;
|
|
parameters[3].key = UA_QUALIFIEDNAME(0, "useSSL");
|
|
UA_Variant_setScalar(¶meters[3].value, &useSSL,
|
|
&UA_TYPES[UA_TYPES_BOOLEAN]);
|
|
UA_KeyValueMap incompleteTlsMap = {4, parameters};
|
|
ck_assert_uint_eq(cm->openConnection(cm, &incompleteTlsMap,
|
|
NULL, NULL, NULL),
|
|
UA_STATUSCODE_BADINVALIDARGUMENT);
|
|
|
|
const char *oldStringParameters[] = {
|
|
"username", "password", "client-key-password"
|
|
};
|
|
UA_String oldStringValue = UA_STRING("unused");
|
|
for(size_t i = 0; i < 3; i++) {
|
|
parameters[3].key = UA_QUALIFIEDNAME(0, (char*)(uintptr_t)oldStringParameters[i]);
|
|
UA_Variant_setScalar(¶meters[3].value, &oldStringValue,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
UA_KeyValueMap oldMap = {4, parameters};
|
|
ck_assert_uint_ne(cm->openConnection(cm, &oldMap, NULL, NULL, NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
}
|
|
const char *oldByteStringParameters[] = {
|
|
"ca-cert", "client-cert", "client-key"
|
|
};
|
|
UA_ByteString oldByteStringValue = UA_BYTESTRING("unused");
|
|
for(size_t i = 0; i < 3; i++) {
|
|
parameters[3].key = UA_QUALIFIEDNAME(
|
|
0, (char*)(uintptr_t)oldByteStringParameters[i]);
|
|
UA_Variant_setScalar(¶meters[3].value, &oldByteStringValue,
|
|
&UA_TYPES[UA_TYPES_BYTESTRING]);
|
|
UA_KeyValueMap oldMap = {4, parameters};
|
|
ck_assert_uint_ne(cm->openConnection(cm, &oldMap, NULL, NULL, NULL),
|
|
UA_STATUSCODE_GOOD);
|
|
}
|
|
|
|
UA_String oldHeader = UA_STRING("x-test=value");
|
|
UA_KeyValuePair oldSendParameter = {0};
|
|
oldSendParameter.key = UA_QUALIFIEDNAME(0, "header");
|
|
UA_Variant_setScalar(&oldSendParameter.value, &oldHeader,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
UA_KeyValueMap oldSendMap = {1, &oldSendParameter};
|
|
UA_ByteString body = UA_BYTESTRING_ALLOC("consumed");
|
|
ck_assert_uint_ne(cm->sendWithConnection(cm, 1, &oldSendMap, &body),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_ptr_null(body.data);
|
|
|
|
UA_UInt16 invalidStatus = 99;
|
|
UA_KeyValuePair invalidStatusParameter = {0};
|
|
invalidStatusParameter.key = UA_QUALIFIEDNAME(0, "status-code");
|
|
UA_Variant_setScalar(&invalidStatusParameter.value, &invalidStatus,
|
|
&UA_TYPES[UA_TYPES_UINT16]);
|
|
UA_KeyValueMap invalidStatusMap = {1, &invalidStatusParameter};
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, 1, &invalidStatusMap, NULL),
|
|
UA_STATUSCODE_BADINVALIDARGUMENT);
|
|
|
|
UA_String contentLength = UA_STRING("5");
|
|
UA_KeyValuePair managedHeader = {0};
|
|
managedHeader.key = UA_QUALIFIEDNAME(0, "content-length");
|
|
UA_Variant_setScalar(&managedHeader.value, &contentLength,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
UA_KeyValuePair headersParameter = {0};
|
|
headersParameter.key = UA_QUALIFIEDNAME(0, "headers");
|
|
UA_Variant_setArray(&headersParameter.value, &managedHeader, 1,
|
|
&UA_TYPES[UA_TYPES_KEYVALUEPAIR]);
|
|
UA_KeyValueMap managedHeaderMap = {1, &headersParameter};
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, 1, &managedHeaderMap, NULL),
|
|
UA_STATUSCODE_BADINVALIDARGUMENT);
|
|
|
|
UA_String injectedValue = UA_STRING("ok\r\nx-injected: yes");
|
|
managedHeader.key = UA_QUALIFIEDNAME(0, "x-test");
|
|
UA_Variant_setScalar(&managedHeader.value, &injectedValue,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, 1, &managedHeaderMap, NULL),
|
|
UA_STATUSCODE_BADINVALIDARGUMENT);
|
|
|
|
UA_String validValue = UA_STRING("value");
|
|
managedHeader.key = UA_QUALIFIEDNAME(0, "x invalid");
|
|
UA_Variant_setScalar(&managedHeader.value, &validValue,
|
|
&UA_TYPES[UA_TYPES_STRING]);
|
|
ck_assert_uint_eq(cm->sendWithConnection(cm, 1, &managedHeaderMap, NULL),
|
|
UA_STATUSCODE_BADINVALIDARGUMENT);
|
|
|
|
stopEventLoop(eventLoop);
|
|
} END_TEST
|
|
|
|
START_TEST(chunkedRequestAndKeepAlive) {
|
|
#ifndef _WIN32
|
|
HTTPTestContext ctx = {0};
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
ctx.fixedResponseSize = 2;
|
|
UA_ConnectionManager *cm = UA_ConnectionManager_new_HTTP(UA_STRING("http"));
|
|
UA_EventLoop *eventLoop = UA_EventLoop_new_POSIX(UA_Log_Stdout);
|
|
ck_assert_ptr_nonnull(cm); ck_assert_ptr_nonnull(eventLoop);
|
|
ck_assert_uint_eq(eventLoop->registerEventSource(eventLoop, &cm->eventSource),
|
|
UA_STATUSCODE_GOOD);
|
|
ck_assert_uint_eq(eventLoop->start(eventLoop), UA_STATUSCODE_GOOD);
|
|
openHTTPListener(cm, &ctx, 30, 0, 0, false, NULL, NULL, NULL);
|
|
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
struct sockaddr_in serverAddress;
|
|
memset(&serverAddress, 0, sizeof(serverAddress));
|
|
serverAddress.sin_family = AF_INET;
|
|
serverAddress.sin_port = htons(ctx.port);
|
|
ck_assert_int_eq(inet_pton(AF_INET, "127.0.0.1",
|
|
&serverAddress.sin_addr), 1);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
|
|
int fd2 = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd2, 0);
|
|
ck_assert_int_eq(connect(fd2, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
|
|
const char chunkedHeaders[] =
|
|
"POST /chunked-a HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Transfer-Encoding: chunked\r\n"
|
|
"Connection: keep-alive\r\n\r\n";
|
|
const char chunkedHeaders2[] =
|
|
"POST /chunked-b HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Transfer-Encoding: chunked\r\n"
|
|
"Connection: keep-alive\r\n\r\n";
|
|
ck_assert_int_eq(send(fd, chunkedHeaders, sizeof(chunkedHeaders) - 1, 0),
|
|
(ssize_t)(sizeof(chunkedHeaders) - 1));
|
|
ck_assert_int_eq(send(fd2, chunkedHeaders2, sizeof(chunkedHeaders2) - 1, 0),
|
|
(ssize_t)(sizeof(chunkedHeaders2) - 1));
|
|
const UA_Byte firstChunk[] = {'2', '\r', '\n', 0, 1, '\r', '\n'};
|
|
const UA_Byte secondChunk[] = {
|
|
'3', '\r', '\n', 2, 3, 4, '\r', '\n', '0', '\r', '\n', '\r', '\n'
|
|
};
|
|
const UA_Byte firstChunk2[] = {'2', '\r', '\n', 5, 6, '\r', '\n'};
|
|
const UA_Byte secondChunk2[] = {
|
|
'3', ';', 'x', '=', 'y', '\r', '\n', 7, 8, 9, '\r', '\n',
|
|
'0', '\r', '\n', 'X', '-', 'T', 'r', 'a', 'i', 'l', 'e', 'r', ':',
|
|
' ', 'o', 'k', '\r', '\n', '\r', '\n'
|
|
};
|
|
/* Feed the first chunk across every possible callback boundary and
|
|
* interleave a second client's chunks. Each raw WSI owns its parser. */
|
|
for(size_t i = 0; i < sizeof(firstChunk); i++) {
|
|
ck_assert_int_eq(send(fd, &firstChunk[i], 1, 0), 1);
|
|
eventLoop->run(eventLoop, 0);
|
|
}
|
|
ck_assert_int_eq(send(fd2, firstChunk2, sizeof(firstChunk2), 0),
|
|
(ssize_t)sizeof(firstChunk2));
|
|
ck_assert_int_eq(send(fd, secondChunk, sizeof(secondChunk), 0),
|
|
(ssize_t)sizeof(secondChunk));
|
|
runUntilCount(eventLoop, &ctx.requestCount, 1, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 1);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 2);
|
|
ck_assert_uint_eq(ctx.serverBodyBytes, 5);
|
|
ck_assert(!ctx.bodyMismatch);
|
|
char response[2048];
|
|
runUntilSocketReadable(eventLoop, fd, 100);
|
|
ssize_t received = recv(fd, response, sizeof(response), 0);
|
|
ck_assert_int_gt(received, 0);
|
|
close(fd);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 1, 200);
|
|
|
|
ck_assert_int_eq(send(fd2, secondChunk2, sizeof(secondChunk2), 0),
|
|
(ssize_t)sizeof(secondChunk2));
|
|
runUntilCount(eventLoop, &ctx.requestCount, 2, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 2);
|
|
ck_assert_uint_eq(ctx.serverBodyBytes, 10);
|
|
ck_assert(!ctx.bodyMismatch);
|
|
runUntilSocketReadable(eventLoop, fd2, 100);
|
|
received = recv(fd2, response, sizeof(response), 0);
|
|
ck_assert_int_gt(received, 0);
|
|
close(fd2);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 2, 200);
|
|
|
|
/* Two requests over one HTTP/1.1 connection exercise keep-alive reuse. */
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char firstRequest[] =
|
|
"CUSTOM /first HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Connection: keep-alive\r\n\r\n";
|
|
ck_assert_int_eq(send(fd, firstRequest, sizeof(firstRequest) - 1, 0),
|
|
(ssize_t)(sizeof(firstRequest) - 1));
|
|
runUntilCount(eventLoop, &ctx.requestCount, 3, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 3);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 3);
|
|
uintptr_t keepAliveConnectionId = ctx.acceptedConnectionId;
|
|
runUntilSocketReadable(eventLoop, fd, 100);
|
|
received = recv(fd, response, sizeof(response), 0);
|
|
ck_assert_int_gt(received, 0);
|
|
|
|
const char secondRequest[] =
|
|
"GET /second HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Connection: close\r\n\r\n";
|
|
ck_assert_int_eq(send(fd, secondRequest, sizeof(secondRequest) - 1, 0),
|
|
(ssize_t)(sizeof(secondRequest) - 1));
|
|
runUntilCount(eventLoop, &ctx.requestCount, 4, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 4);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionId, keepAliveConnectionId);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 3);
|
|
runUntilSocketReadable(eventLoop, fd, 100);
|
|
received = recv(fd, response, sizeof(response), 0);
|
|
ck_assert_int_gt(received, 0);
|
|
close(fd);
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 3, 200);
|
|
|
|
/* Ambiguous framing is rejected before delivery. */
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char ambiguousRequest[] =
|
|
"POST /ambiguous HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Content-Length: 1\r\n"
|
|
"Transfer-Encoding: chunked\r\n\r\n";
|
|
ck_assert_int_eq(send(fd, ambiguousRequest, sizeof(ambiguousRequest) - 1, 0),
|
|
(ssize_t)(sizeof(ambiguousRequest) - 1));
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 4, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 4);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 4);
|
|
close(fd);
|
|
|
|
/* Malformed chunk framing is rejected by the vendored parser. */
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char malformedChunk[] =
|
|
"POST /malformed HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Transfer-Encoding: chunked\r\n\r\n"
|
|
"z\r\n";
|
|
ck_assert_int_eq(send(fd, malformedChunk, sizeof(malformedChunk) - 1, 0),
|
|
(ssize_t)(sizeof(malformedChunk) - 1));
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 5, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 4);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 5);
|
|
close(fd);
|
|
|
|
/* A second request must not enter while the first request on the same
|
|
* accepted connection is still waiting for its response. */
|
|
ctx.mode = HTTP_TEST_NO_RESPONSE;
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
ck_assert_int_eq(send(fd, firstRequest, sizeof(firstRequest) - 1, 0),
|
|
(ssize_t)(sizeof(firstRequest) - 1));
|
|
runUntilCount(eventLoop, &ctx.requestCount, 5, 200);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 6);
|
|
ck_assert_int_eq(send(fd, secondRequest, sizeof(secondRequest) - 1, 0),
|
|
(ssize_t)(sizeof(secondRequest) - 1));
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 6, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 5);
|
|
close(fd);
|
|
|
|
/* The same invariant applies when two requests arrive in one read. The
|
|
* parser delivers the first request only and rejects the pipelined tail. */
|
|
ctx.mode = HTTP_TEST_FIXED_RESPONSE;
|
|
fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
ck_assert_int_ge(fd, 0);
|
|
ck_assert_int_eq(connect(fd, (struct sockaddr*)&serverAddress,
|
|
sizeof(serverAddress)), 0);
|
|
const char pipelinedRequests[] =
|
|
"GET /pipeline-first HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Connection: keep-alive\r\n\r\n"
|
|
"GET /pipeline-second HTTP/1.1\r\n"
|
|
"Host: 127.0.0.1\r\n"
|
|
"Connection: close\r\n\r\n";
|
|
ck_assert_int_eq(send(fd, pipelinedRequests,
|
|
sizeof(pipelinedRequests) - 1, 0),
|
|
(ssize_t)(sizeof(pipelinedRequests) - 1));
|
|
runUntilCount(eventLoop, &ctx.acceptedClosingCount, 7, 200);
|
|
ck_assert_uint_eq(ctx.requestCount, 6);
|
|
ck_assert_uint_eq(ctx.acceptedConnectionCount, 7);
|
|
close(fd);
|
|
|
|
ck_assert_uint_eq(cm->closeConnection(cm, ctx.listenerId), UA_STATUSCODE_GOOD);
|
|
stopEventLoop(eventLoop);
|
|
#endif
|
|
} END_TEST
|
|
|
|
int main(void) {
|
|
Suite *s = suite_create("Test HTTP EventLoop");
|
|
TCase *tc = tcase_create("test cases");
|
|
tcase_add_checked_fixture(tc, setup, teardown);
|
|
tcase_add_test(tc, sendGetRequest);
|
|
tcase_add_test(tc, sendPostRequest);
|
|
tcase_add_test(tc, serverBinaryRoundtrip);
|
|
suite_add_tcase(s, tc);
|
|
|
|
TCase *largeBodies = tcase_create("large-and-empty-bodies");
|
|
tcase_add_test(largeBodies, serverLargeAndEmptyBodies);
|
|
suite_add_tcase(s, largeBodies);
|
|
TCase *lifecycle = tcase_create("request-lifecycle");
|
|
tcase_add_test(lifecycle, serverRequestLifecycle);
|
|
tcase_add_test(lifecycle, serverBrokenCarrierIsolation);
|
|
tcase_add_test(lifecycle, clientRequestFailureIsolation);
|
|
tcase_add_test(lifecycle, clientSynchronousConnectionFailure);
|
|
tcase_add_test(lifecycle, clientRequestTimeoutIsolation);
|
|
tcase_add_test(lifecycle, serverResponseFailureIsolation);
|
|
tcase_add_test(lifecycle, serverListenerCloseWithIdleCarrier);
|
|
tcase_add_test(lifecycle, serverListenerCloseFromRequestCallback);
|
|
suite_add_tcase(s, lifecycle);
|
|
TCase *timeouts = tcase_create("timeouts-and-listener-close");
|
|
tcase_add_test(timeouts, serverTimeoutAndListenerClose);
|
|
tcase_add_test(timeouts, serverReceiveAndKeepAliveTimeouts);
|
|
tcase_add_test(timeouts, serverConnectionCapAndRequestDeadline);
|
|
suite_add_tcase(s, timeouts);
|
|
TCase *tls = tcase_create("mutual-tls");
|
|
tcase_add_test(tls, clientServerMutualTls);
|
|
suite_add_tcase(s, tls);
|
|
TCase *limits = tcase_create("message-size-limits");
|
|
tcase_add_test(limits, messageSizeLimits);
|
|
suite_add_tcase(s, limits);
|
|
#ifdef UA_ENABLE_HTTP_COMPRESSION
|
|
TCase *compression = tcase_create("content-encoding");
|
|
tcase_add_test(compression, serverContentEncodingAndExpansionLimit);
|
|
tcase_add_test(compression, clientCompressedResponse);
|
|
suite_add_tcase(s, compression);
|
|
#endif
|
|
TCase *validation = tcase_create("parameter-validation");
|
|
tcase_add_test(validation, parameterValidation);
|
|
suite_add_tcase(s, validation);
|
|
TCase *handles = tcase_create("client-request-handles");
|
|
tcase_add_test(handles, clientRequestHandles);
|
|
tcase_add_test(handles, clientRejectsDuplicateContentEncoding);
|
|
suite_add_tcase(s, handles);
|
|
TCase *http11 = tcase_create("chunked-and-keep-alive");
|
|
tcase_add_test(http11, chunkedRequestAndKeepAlive);
|
|
suite_add_tcase(s, http11);
|
|
|
|
SRunner *sr = srunner_create(s);
|
|
srunner_set_fork_status(sr, CK_NOFORK);
|
|
srunner_run_all (sr, CK_NORMAL);
|
|
int number_failed = srunner_ntests_failed(sr);
|
|
srunner_free(sr);
|
|
|
|
return (number_failed == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
}
|