diff --git a/common/Makefile.am b/common/Makefile.am index 56ecb1b3..d7568b9c 100644 --- a/common/Makefile.am +++ b/common/Makefile.am @@ -49,6 +49,8 @@ libcommon_la_SOURCES = \ base64.c \ channel_defs.h \ defines.h \ + dechunker.c \ + dechunker.h \ fifo.c \ fifo.h \ file.c \ diff --git a/common/dechunker.c b/common/dechunker.c new file mode 100644 index 00000000..7ead6d0c --- /dev/null +++ b/common/dechunker.c @@ -0,0 +1,371 @@ +/** + * xrdp: A Remote Desktop Protocol server. + * + * Copyright (C) Jay Sorg 2006-2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * @file dechunker.c + * @brief Dechunker functions for chunks on virtual channels - definitions + * @author Matt Burt + * + */ + +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include "dechunker.h" +#include "parse.h" +#include "os_calls.h" +#include "string_calls.h" + +enum vc_dechunker_state +{ + E_NO_DATA = 0, + E_READING, + E_DATA, + E_SKIPPING +}; + +enum vc_chunk_type +{ + CT_INTERMEDIATE = 0, + CT_FIRST = 1, // CHANNEL_FLAG_FIRST + CT_LAST = 2, // CHANNEL_FLAG_LAST + CT_FIRST_LAST = 3 // CHANNEL_FLAG_FIRST | CHANNEL_FLAG_LAST +}; + +struct vc_dechunker +{ + char name[64]; + int max_chunk_size; + enum vc_dechunker_state state; + struct stream *reassembly_s; +}; + +enum +{ + // This is a rather arbitrary figure, but one we are unlikely to + // go below. It's a sanity check for vc_dechunker_init() + E_MAX_CHUNK_SIZE_LOWER_LIMIT = 50 +}; +/*****************************************************************************/ +struct vc_dechunker * +vc_dechunker_init(const char *chan_name, int max_chunk_size) +{ + struct vc_dechunker *self = NULL; + if (chan_name == NULL) + { + LOG(LOG_LEVEL_ERROR, "vc_dechunker_init() called with no channel name"); + } + else if (max_chunk_size < E_MAX_CHUNK_SIZE_LOWER_LIMIT) + { + LOG(LOG_LEVEL_ERROR, "Dechunker: Max chunk size for %s is too small", + chan_name); + } + else if ((self = g_new(struct vc_dechunker, 1)) == NULL) + { + LOG(LOG_LEVEL_ERROR, "Dechunker: no memory for %s", chan_name); + } + else + { + strlcpy(self->name, chan_name, sizeof(self->name)); + self->max_chunk_size = max_chunk_size; + self->state = E_NO_DATA; + self->reassembly_s = NULL; + } + + return self; +} + +/*****************************************************************************/ +void +vc_dechunker_free(struct vc_dechunker *self) +{ + if (self != NULL) + { + free_stream(self->reassembly_s); + free(self); + } +} + +/*****************************************************************************/ +static void +vc_dechunker_reset(struct vc_dechunker *self) +{ + if (self != NULL) + { + free_stream(self->reassembly_s); + self->reassembly_s = NULL; + self->state = E_NO_DATA; + } +} + +/*****************************************************************************/ +static void +log_unexpected_vc_chunk_type(struct vc_dechunker *self, + enum vc_chunk_type ct) +{ + static const char *chunk_type_str[4] = + { + "CT_INTERMEDIATE", + "CT_FIRST", + "CT_LAST", + "CT_FIRST_LAST" + }; + + int index = (int)ct & 3; // Guarantee to be 0..3 + LOG (LOG_LEVEL_ERROR, + "Dechunker: unexpected chunk type %s received on %s", + chunk_type_str[index], self->name); + self->state = E_SKIPPING; // Look for the next PDU +} + +/*****************************************************************************/ +static enum vc_dechunker_status +handle_no_data_state(struct vc_dechunker *self, + struct stream *s, + int chunk_size, + int total_size, + enum vc_chunk_type ct) +{ + enum vc_dechunker_status rv = E_VC_ERROR; + switch (ct) + { + case CT_FIRST: + // See [MS-RDPBCGR] 3.1.5.2.1 Sending of Virtual Channel PDU + if (total_size <= self->max_chunk_size) + { + LOG (LOG_LEVEL_ERROR, + "Dechunker: short first chunk received on %s", + self->name); + self->state = E_SKIPPING; + } + else if (chunk_size >= total_size) + { + LOG (LOG_LEVEL_ERROR, + "Dechunker: malformed first chunk received on %s", + self->name); + self->state = E_SKIPPING; + } + else + { + make_stream(self->reassembly_s); + if (self->reassembly_s) + { + init_stream(self->reassembly_s, total_size); + } + if (self->reassembly_s == NULL || + self->reassembly_s->data == NULL) + { + LOG (LOG_LEVEL_ERROR, + "Dechunker: out-of-memory on %s", + self->name); + self->state = E_SKIPPING; + } + else + { + out_uint8p(self->reassembly_s, s->p, chunk_size); + in_uint8s(s, chunk_size); + self->state = E_READING; + rv = E_VC_IN_PROGRESS; + } + } + break; + + case CT_FIRST_LAST: + rv = E_VC_INLINE_CHUNK; + break; + + default: + log_unexpected_vc_chunk_type(self, ct); + self->state = E_SKIPPING; + } + + return rv; +} + +/*****************************************************************************/ +static enum vc_dechunker_status +handle_reading_state(struct vc_dechunker *self, + struct stream *s, + int chunk_size, + int total_size, + enum vc_chunk_type ct) +{ + enum vc_dechunker_status rv = E_VC_ERROR; + if (ct == CT_INTERMEDIATE || ct == CT_LAST) + { + /* Data to add to the reassembly stream + * + * [MS-RDPBCGR] 3.1.5.2.2.1 imposes no requirement to check + * the total_size field is consistent between chunks. Only + * the total_size value for CT_FIRST is important + */ + if (chunk_size > s_rem_out(self->reassembly_s)) + { + LOG (LOG_LEVEL_ERROR, + "Dechunker: oversized chunk received on %s", + self->name); + self->state = E_SKIPPING; + } + else + { + out_uint8p(self->reassembly_s, s->p, chunk_size); + in_uint8s(s, chunk_size); + if (ct == CT_LAST) + { + if (s_rem_out(self->reassembly_s) == 0) + { + // Make the stream ready for reading + s_mark_end(self->reassembly_s); + self->reassembly_s->p = self->reassembly_s->data; + // Tell the caller the stream is available. + self->state = E_DATA; + rv = E_VC_READY; + } + else + { + LOG (LOG_LEVEL_ERROR, + "Dechunker: undersized last chunk received on %s", + self->name); + self->state = E_SKIPPING; + } + } + else + { + rv = E_VC_IN_PROGRESS; + } + } + } + else + { + log_unexpected_vc_chunk_type(self, ct); + } + + return rv; +} + +/*****************************************************************************/ +enum vc_dechunker_status +vc_dechunker_process_chunk(struct vc_dechunker *self, + struct stream *s, int flags, + int total_size) +{ + enum vc_dechunker_status rv = E_VC_ERROR; + enum vc_chunk_type ct = (enum vc_chunk_type)(flags & 3); + int chunk_size = s ? s_rem(s) : 0; // Chunk is remainder of stream + + if (self == NULL || s == NULL) + { + ; // Nothing to do + } + else if (total_size < 0) + { + LOG (LOG_LEVEL_ERROR, + "Dechunker: out-of-range length received on %s", + self->name); + self->state = E_SKIPPING; + } + else if (chunk_size > self->max_chunk_size) + { + // [MS-RDPBCGR] 2.2.6.1 + // > ... This field MUST NOT be larger than CHANNEL_CHUNK_size + // > (1600) bytes in size unless the maximum virtual channel + // > chunk size is specified in the optional VCChunkSize field + // > of the Virtual Channel Capability Set (section 2.2.7.1.10). + LOG (LOG_LEVEL_ERROR, + "Dechunker: oversize chunk received on %s (%d octets)", + self->name, chunk_size); + self->state = E_SKIPPING; + } + else + { + // Check for a restart after an error + if (self->state == E_SKIPPING) + { + switch (ct) + { + case CT_FIRST: + case CT_FIRST_LAST: + // Clean up the dechunker and start again + vc_dechunker_reset(self); + break; + default: + break; + } + } + + switch (self->state) + { + case E_NO_DATA: + rv = handle_no_data_state(self, s, chunk_size, + total_size, ct); + break; + + case E_READING: + rv = handle_reading_state(self, s, chunk_size, + total_size, ct); + break; + + case E_DATA: + // If we get here, we've not cleared the existing buffer. + // This is a serious problem and we continue returning + // a error until the buffer is cleared. + LOG(LOG_LEVEL_ALWAYS, + "Dechunker: unprocessed PDU on %s", self->name); + break; + + case E_SKIPPING: + rv = E_VC_IN_PROGRESS; // Ignore this chunk + break; + + default: + // Shouldn't get here. + LOG (LOG_LEVEL_ERROR, + "Dechunker: called when %s has an unknown state %d", + self->name, (int)self->state); + self->state = E_SKIPPING; + } + } + + return rv; +} + +/*****************************************************************************/ +struct stream * +vc_dechunker_get_stream(struct vc_dechunker *self) +{ + struct stream *s; + const char *name = (self != NULL) ? self->name : ""; + if (self == NULL || self->state != E_DATA) + { + LOG (LOG_LEVEL_ERROR, + "Dechunker: get stream called for %s with no data available", + name); + s = NULL; + } + else + { + // Pass ownership of the stream to the caller + s = self->reassembly_s; + self->reassembly_s = NULL; // So we don't free it ourselves! + vc_dechunker_reset(self); + } + + return s; +} diff --git a/common/dechunker.h b/common/dechunker.h new file mode 100644 index 00000000..f3dcdbda --- /dev/null +++ b/common/dechunker.h @@ -0,0 +1,100 @@ +/** + * xrdp: A Remote Desktop Protocol server. + * + * Copyright (C) Jay Sorg 2004-2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * @file dechunker.h + * @brief Dechunker functions for chunks on virtual channels - declarations + * + * Many places in xrdp need to dechunk data received from virtual channels. + * This process is described in [MS-RDPBCGR] 3.1.5.2.1 (sending) and + * 3.1.5.2.2.1 (reassembly). + * + * + * @author Matt Burt + */ + +#if !defined(DECHUNKER_H) +#define DECHUNKER_H + +struct stream; + +/* Private type */ +struct vc_dechunker; + +/** + * Returned from dechunker_process_vc_chunk() + */ +enum vc_dechunker_status +{ + E_VC_INLINE_CHUNK = 0, ///< This chunk is complete in itself + E_VC_IN_PROGRESS, ///< The dechunker is processing chunks + E_VC_READY, ///< A dechunked stream is now complete + E_VC_ERROR ///< An error occurred (logged) +}; + +/** + * Initialise a virtual channel dechunker + * + * @param chan_name - Name of channel + * @param max_chunk_size - Max size of chunks allowed on channel + * @return vc_dechunker + */ +struct vc_dechunker * +vc_dechunker_init(const char *chan_name, int max_chunk_size); + +/** + * Free a virtual channel dechunker + * @param self vc dechunker to free + */ +void +vc_dechunker_free(struct vc_dechunker *self); + +/** + * Process a virtual channel chunk + * + * @param self dechunker + * @param s Stream for chunk, positioned at start of chunk + * @param flags from CHANNEL_PDU_HEADER + * @param total_size length from CHANNEL_PDU_HEADER + * @return status of dechunker + * + * If E_VC_ERROR is returned, the dechunker will ignore further PDUs + * until the start of the next one is detected. This can be used to + * recover from chunking errors without losing the channel entirely. It + * is up to the caller whether to treat a dechunking error as fatal for + * the channel or not. + */ +enum vc_dechunker_status +vc_dechunker_process_chunk(struct vc_dechunker *self, + struct stream *s, int flags, int total_size); +/** + * Get the stream from a ready dechunker + * + * @param self virtual channel dechunker + * @return input stream containing completed chunk + * + * Ownership of the stream passes to the caller + * + * Resets the dechunker state so that further chunks can be processed. + */ +struct stream * +vc_dechunker_get_stream(struct vc_dechunker *self); + + +#endif // DECHUNKER_H diff --git a/libxrdp/libxrdp.h b/libxrdp/libxrdp.h index 7182f886..5aa6075c 100644 --- a/libxrdp/libxrdp.h +++ b/libxrdp/libxrdp.h @@ -151,6 +151,8 @@ struct xrdp_drdynvc int (*data)(struct xrdp_process *id, int chan_id, char *data, int bytes); }; +struct vc_dechunker; // Forward declaration + /* channel */ struct xrdp_channel { @@ -158,7 +160,7 @@ struct xrdp_channel struct xrdp_mcs *mcs_layer; int drdynvc_channel_id; int drdynvc_state; - struct stream *s; + struct vc_dechunker *drdynvc_dc; struct xrdp_drdynvc drdynvcs[256]; }; diff --git a/libxrdp/xrdp_channel.c b/libxrdp/xrdp_channel.c index b8b43aea..1925b0d1 100644 --- a/libxrdp/xrdp_channel.c +++ b/libxrdp/xrdp_channel.c @@ -23,7 +23,7 @@ #endif #include "libxrdp.h" -#include "parse.h" +#include "dechunker.h" #include "string_calls.h" #include "xrdp_channel.h" @@ -78,7 +78,7 @@ xrdp_channel_delete(struct xrdp_channel *self) { return; } - free_stream(self->s); + vc_dechunker_free(self->drdynvc_dc); g_memset(self, 0, sizeof(struct xrdp_channel)); g_free(self); } @@ -550,11 +550,12 @@ xrdp_channel_process_drdynvc(struct xrdp_channel *self, struct stream *s) { int total_length; - int length; int flags; int cmd; int rv; - struct stream *ls; + struct stream *ls = NULL; + int free_ls = 0; + enum vc_dechunker_status dechunker_status; if (!s_check_rem_and_log(s, 8, "Parsing [MS-RDPBCGR] CHANNEL_PDU_HEADER")) { @@ -564,65 +565,32 @@ xrdp_channel_process_drdynvc(struct xrdp_channel *self, in_uint32_le(s, flags); /* flags */ LOG_DEVEL(LOG_LEVEL_TRACE, "Received header [MS-RDPBCGR] CHANNEL_PDU_HEADER " "length %d, flags 0x%8.8x", total_length, flags); - ls = NULL; - switch (flags & 3) + dechunker_status = vc_dechunker_process_chunk( + self->drdynvc_dc, + s, flags, total_length); + switch (dechunker_status) { - case 0: /* not first chunk and not last chunk */ - length = (int) (s->end - s->p); - LOG_DEVEL(LOG_LEVEL_TRACE, "Received [MS-RDPBCGR] data chunk (middle) " - "length %d", length); - if (length > s_rem_out(self->s)) - { - LOG(LOG_LEVEL_ERROR, "[MS-RDPBCGR] Data chunk length is bigger than " - "the remaining chunk buffer size. length %d, remaining %d", - length, s_rem_out(self->s)); - return 1; - } - out_uint8a(self->s, s->p, length); /* append data to chunk buffer */ - in_uint8s(s, length); /* virtualChannelData */ - return 0; - case 1: /* CHANNEL_FLAG_FIRST */ - free_stream(self->s); - make_stream(self->s); - init_stream(self->s, total_length); - length = (int) (s->end - s->p); - LOG_DEVEL(LOG_LEVEL_TRACE, "Received [MS-RDPBCGR] data chunk (first) " - "length %d", length); - if (length > s_rem_out(self->s)) - { - LOG(LOG_LEVEL_ERROR, "[MS-RDPBCGR] Data chunk length is bigger than " - "the remaining chunk buffer size. length %d, remaining %d", - length, s_rem_out(self->s)); - return 1; - } - out_uint8a(self->s, s->p, length); /* append data to chunk buffer */ - in_uint8s(s, length); /* virtualChannelData */ - return 0; - case 2: /* CHANNEL_FLAG_LAST */ - length = (int) (s->end - s->p); - LOG_DEVEL(LOG_LEVEL_TRACE, "Received [MS-RDPBCGR] data chunk (last) " - "length %d", length); - if (length > s_rem_out(self->s)) - { - LOG(LOG_LEVEL_ERROR, "[MS-RDPBCGR] Data chunk length is bigger than " - "the remaining chunk buffer size. length %d, remaining %d", - length, s_rem_out(self->s)); - return 1; - } - out_uint8a(self->s, s->p, length); /* append data to chunk buffer */ - in_uint8s(s, length); /* virtualChannelData */ - s_mark_end(self->s); - self->s->p = self->s->data; - ls = self->s; - break; - case 3: /* CHANNEL_FLAG_FIRST and CHANNEL_FLAG_LAST */ - LOG_DEVEL(LOG_LEVEL_TRACE, "Received [MS-RDPBCGR] data chunk (first and last) " - "length %d", total_length); + case E_VC_INLINE_CHUNK: ls = s; break; + + case E_VC_IN_PROGRESS: + return 0; + break; + + case E_VC_READY: + ls = vc_dechunker_get_stream(self->drdynvc_dc); + // We now own the stream, so must delete it + free_ls = 1; + break; + + case E_VC_ERROR: + // Error has been logged + return 1; + default: - LOG(LOG_LEVEL_ERROR, "Received [MS-RDPBCGR] data chunk with " - "unknown flag 0x%8.8x", (int) (flags & 3)); + LOG(LOG_LEVEL_ERROR, "Dechunker returned unknown error %d", + (int)dechunker_status); return 1; } if (ls == NULL) @@ -657,6 +625,10 @@ xrdp_channel_process_drdynvc(struct xrdp_channel *self, "unknown command 0x%2.2x", cmd); break; } + if (free_ls) + { + free_stream(ls); + } return rv; } @@ -806,6 +778,13 @@ xrdp_channel_drdynvc_start(struct xrdp_channel *self) DRDYNVC_SVC_CHANNEL_NAME); rv = -1; } + else if ((self->drdynvc_dc = + vc_dechunker_init(DRDYNVC_SVC_CHANNEL_NAME, + CHANNEL_CHUNK_LENGTH)) == NULL) + { + LOG(LOG_LEVEL_ERROR, "No memory"); + rv = -1; + } else { self->drdynvc_channel_id = (dci->chanid - MCS_GLOBAL_CHANNEL) - 1; diff --git a/tests/common/Makefile.am b/tests/common/Makefile.am index a4bb1325..5b0f7162 100644 --- a/tests/common/Makefile.am +++ b/tests/common/Makefile.am @@ -14,6 +14,7 @@ check_PROGRAMS = test_common test_common_SOURCES = \ test_common.h \ test_common_main.c \ + test_dechunker.c \ test_fifo_calls.c \ test_list_calls.c \ test_list16_calls.c \ diff --git a/tests/common/test_common.h b/tests/common/test_common.h index 9d9e1376..6a76d92e 100644 --- a/tests/common/test_common.h +++ b/tests/common/test_common.h @@ -7,6 +7,7 @@ char * bin_to_hex(const char *input, int length); +Suite *make_suite_test_dechunker(void); Suite *make_suite_test_fifo(void); Suite *make_suite_test_list(void); Suite *make_suite_test_list16(void); diff --git a/tests/common/test_common_main.c b/tests/common/test_common_main.c index bcf88c65..973b46be 100644 --- a/tests/common/test_common_main.c +++ b/tests/common/test_common_main.c @@ -41,24 +41,75 @@ bin_to_hex(const char *input, int length) return result; } +static int +run_suite(const char *test_name) +{ + const char *env = getenv("TEST_NAME"); + return (env == NULL || strcmp(env, test_name) == 0); +} + int main (void) { int number_failed; SRunner *sr; - sr = srunner_create (make_suite_test_fifo()); - srunner_add_suite(sr, make_suite_test_list()); - srunner_add_suite(sr, make_suite_test_list16()); - srunner_add_suite(sr, make_suite_test_parse()); - srunner_add_suite(sr, make_suite_test_set_int()); - srunner_add_suite(sr, make_suite_test_string()); - srunner_add_suite(sr, make_suite_test_string_unicode()); - srunner_add_suite(sr, make_suite_test_os_calls()); - srunner_add_suite(sr, make_suite_test_ssl_calls()); - srunner_add_suite(sr, make_suite_test_base64()); - srunner_add_suite(sr, make_suite_test_guid()); - srunner_add_suite(sr, make_suite_test_scancode()); - srunner_add_suite(sr, make_suite_test_timers()); + sr = srunner_create (NULL); + if (run_suite("dechunker")) + { + srunner_add_suite(sr, make_suite_test_dechunker()); + } + if (run_suite("fifo")) + { + srunner_add_suite(sr, make_suite_test_fifo()); + } + if (run_suite("list")) + { + srunner_add_suite(sr, make_suite_test_list()); + } + if (run_suite("list16")) + { + srunner_add_suite(sr, make_suite_test_list16()); + } + if (run_suite("parse")) + { + srunner_add_suite(sr, make_suite_test_parse()); + } + if (run_suite("set_int")) + { + srunner_add_suite(sr, make_suite_test_set_int()); + } + if (run_suite("string")) + { + srunner_add_suite(sr, make_suite_test_string()); + } + if (run_suite("unicode")) + { + srunner_add_suite(sr, make_suite_test_string_unicode()); + } + if (run_suite("os_calls")) + { + srunner_add_suite(sr, make_suite_test_os_calls()); + } + if (run_suite("ssl_calls")) + { + srunner_add_suite(sr, make_suite_test_ssl_calls()); + } + if (run_suite("base64")) + { + srunner_add_suite(sr, make_suite_test_base64()); + } + if (run_suite("guid")) + { + srunner_add_suite(sr, make_suite_test_guid()); + } + if (run_suite("scancode")) + { + srunner_add_suite(sr, make_suite_test_scancode()); + } + if (run_suite("timers")) + { + srunner_add_suite(sr, make_suite_test_timers()); + } srunner_set_tap(sr, "-"); /* diff --git a/tests/common/test_dechunker.c b/tests/common/test_dechunker.c new file mode 100644 index 00000000..c2d917dd --- /dev/null +++ b/tests/common/test_dechunker.c @@ -0,0 +1,624 @@ +/** + * xrdp: A Remote Desktop Protocol server. + * + * Copyright (C) Jay Sorg 2006-2026 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * @file test_dechunker.c + * @brief Test functiond for dechunker code + * @author Matt Burt + * + */ + + +#if defined(HAVE_CONFIG_H) +#include "config_ac.h" +#endif + +#include "dechunker.h" +#include "ms-rdpbcgr.h" +#include "os_calls.h" +#include "parse.h" + +#include "test_common.h" + +// Chapter 1 of Mary Shelley's Frankenstein (thanks to Project Gutenberg) +static const char frankenstein[] = + "Letter 1\n\n" + "To Mrs. Saville, England.\n\n" + "St. Petersburgh, Dec. 11th, 17—.\n" + "You will rejoice to hear that no disaster has accompanied the " + "commencement of an enterprise which you have regarded with such evil " + "forebodings. I arrived here yesterday, and my first task is to assure " + "my dear sister of my welfare and increasing confidence in the success " + "of my undertaking.\n" + "I am already far north of London, and as I walk in the streets of " + "Petersburgh, I feel a cold northern breeze play upon my cheeks, which " + "braces my nerves and fills me with delight. Do you understand this " + "feeling? This breeze, which has travelled from the regions towards which " + "I am advancing, gives me a foretaste of those icy climes. Inspirited " + "by this wind of promise, my daydreams become more fervent and vivid. I " + "try in vain to be persuaded that the pole is the seat of frost and " + "desolation; it ever presents itself to my imagination as the region " + "of beauty and delight. There, Margaret, the sun is for ever visible, " + "its broad disk just skirting the horizon and diffusing a perpetual " + "splendour. There—for with your leave, my sister, I will put some trust " + "in preceding navigators—there snow and frost are banished; and, sailing " + "over a calm sea, we may be wafted to a land surpassing in wonders and " + "in beauty every region hitherto discovered on the habitable globe. Its " + "productions and features may be without example, as the phenomena of the " + "heavenly bodies undoubtedly are in those undiscovered solitudes. What " + "may not be expected in a country of eternal light? I may there discover " + "the wondrous power which attracts the needle and may regulate a thousand " + "celestial observations that require only this voyage to render their " + "seeming eccentricities consistent for ever. I shall satiate my ardent " + "curiosity with the sight of a part of the world never before visited, " + "and may tread a land never before imprinted by the foot of man. These " + "are my enticements, and they are sufficient to conquer all fear of danger " + "or death and to induce me to commence this laborious voyage with the joy " + "a child feels when he embarks in a little boat, with his holiday mates, " + "on an expedition of discovery up his native river. But supposing all " + "these conjectures to be false, you cannot contest the inestimable benefit " + "which I shall confer on all mankind, to the last generation, by " + "discovering a passage near the pole to those countries, to reach which at " + "present so many months are requisite; or by ascertaining the secret of " + "the magnet, which, if at all possible, can only be effected by an " + " undertaking such as mine.\n" + "These reflections have dispelled the agitation with which I began my " + "letter, and I feel my heart glow with an enthusiasm which elevates me " + "to heaven, for nothing contributes so much to tranquillise the mind as " + "a steady purpose—a point on which the soul may fix its intellectual " + "eye. This expedition has been the favourite dream of my early years. I " + "have read with ardour the accounts of the various voyages which have " + "been made in the prospect of arriving at the North Pacific Ocean through " + "the seas which surround the pole. You may remember that a history of " + "all the voyages made for purposes of discovery composed the whole of " + "our good Uncle Thomas’ library. My education was neglected, yet I was " + "passionately fond of reading. These volumes were my study day and night, " + "and my familiarity with them increased that regret which I had felt, as " + "a child, on learning that my father’s dying injunction had forbidden " + "my uncle to allow me to embark in a seafaring life.\n" + "These visions faded when I perused, for the first time, those poets whose " + "effusions entranced my soul and lifted it to heaven. I also became a poet " + "and for one year lived in a paradise of my own creation; I imagined that " + "I also might obtain a niche in the temple where the names of Homer and " + "Shakespeare are consecrated. You are well acquainted with my failure and " + "how heavily I bore the disappointment. But just at that time I inherited " + "the fortune of my cousin, and my thoughts were turned into the channel " + "of their earlier bent.\n" + "Six years have passed since I resolved on my present undertaking. I can, " + "even now, remember the hour from which I dedicated myself to this great " + "enterprise. I commenced by inuring my body to hardship. I accompanied " + "the whale-fishers on several expeditions to the North Sea; I voluntarily " + "endured cold, famine, thirst, and want of sleep; I often worked harder " + "than the common sailors during the day and devoted my nights to the " + "study of mathematics, the theory of medicine, and those branches of " + "physical science from which a naval adventurer might derive the greatest " + "practical advantage. Twice I actually hired myself as an under-mate in " + "a Greenland whaler, and acquitted myself to admiration. I must own I " + "felt a little proud when my captain offered me the second dignity in " + "the vessel and entreated me to remain with the greatest earnestness, " + "so valuable did he consider my services.\n" + "And now, dear Margaret, do I not deserve to accomplish some great " + "purpose? My life might have been passed in ease and luxury, but I " + "preferred glory to every enticement that wealth placed in my path. Oh, " + "that some encouraging voice would answer in the affirmative! My courage " + "and my resolution is firm; but my hopes fluctuate, and my spirits are " + "often depressed. I am about to proceed on a long and difficult voyage, " + "the emergencies of which will demand all my fortitude: I am required not " + "only to raise the spirits of others, but sometimes to sustain my own, " + "when theirs are failing.\n" + "This is the most favourable period for travelling in Russia. They fly " + "quickly over the snow in their sledges; the motion is pleasant, and, in " + "my opinion, far more agreeable than that of an English stagecoach. The " + "cold is not excessive, if you are wrapped in furs—a dress which I have " + "already adopted, for there is a great difference between walking the deck " + "and remaining seated motionless for hours, when no exercise prevents " + "the blood from actually freezing in your veins. I have no ambition to " + "lose my life on the post-road between St. Petersburgh and Archangel.\n" + "I shall depart for the latter town in a fortnight or three weeks; and my " + "intention is to hire a ship there, which can easily be done by paying " + "the insurance for the owner, and to engage as many sailors as I think " + "necessary among those who are accustomed to the whale-fishing. I do not " + "intend to sail until the month of June; and when shall I return? Ah, " + "dear sister, how can I answer this question? If I succeed, many, many " + "months, perhaps years, will pass before you and I may meet. If I fail, " + "you will see me again soon, or never.\n" + "Farewell, my dear, excellent Margaret. Heaven shower down blessings on " + "you, and save me, that I may again and again testify my gratitude for " + "all your love and kindness.\n\n" + "Your affectionate brother,\nR. Walton "; + +// Number of CHANNEL_CHUNK_LENGTH chunks required to send the +// above text into the dechunker +#define FRANKENSTEIN_CHUNK_COUNT \ + ((sizeof(frankenstein) + (CHANNEL_CHUNK_LENGTH -1)) \ + / CHANNEL_CHUNK_LENGTH) + +// See the private E_MAX_CHUNK_SIZE_LOWER_LIMIT in dechunker.c +#define PAD50 " " + +/******************************************************************************/ +/* + * Constructs a stream from static data + * + * The returned stream is suitable for reading. + */ +static struct stream * +make_stream_from_data(const char *data, int data_len) +{ + struct stream *s; + make_stream(s); + init_stream(s, data_len); + s_push_layer(s, iso_hdr, 0); + out_uint8p(s, data, data_len); + s_mark_end(s); + s_pop_layer(s, iso_hdr); + return s; +} + +/******************************************************************************/ +/* + * Check bad parameters passed to the dechunker functions + */ +START_TEST(test_dechunker_bad_params) +{ + struct vc_dechunker *dc; + const char data[] = "Some stream data"; + struct stream *s = make_stream_from_data(data, sizeof(data)); + enum vc_dechunker_status stat; + + // vc_dechunker_init + dc = vc_dechunker_init(NULL, 1000); // No channel name + ck_assert_ptr_eq(dc, NULL); + dc = vc_dechunker_init("test", 1); // max chunk size too small + ck_assert_ptr_eq(dc, NULL); + dc = vc_dechunker_init("test", 1000); // Should be OK + ck_assert_ptr_ne(dc, NULL); + + // vc_dechunker_free + vc_dechunker_free(NULL); // Musn't crash! + + // vc_dechunker_get_stream + vc_dechunker_get_stream(NULL); // Musn't crash! + + // vc_dechunker_process_chunk + stat = vc_dechunker_process_chunk(NULL, s, 0, 1600); // No dechunker + ck_assert_int_eq(stat, E_VC_ERROR); + stat = vc_dechunker_process_chunk(dc, NULL, 0, 1600); // No stream + ck_assert_int_eq(stat, E_VC_ERROR); + stat = vc_dechunker_process_chunk(dc, s, 0, -1); // bad total_size + ck_assert_int_eq(stat, E_VC_ERROR); + + free_stream(s); + vc_dechunker_free(dc); +} + +/******************************************************************************/ +/* + * Check passthrough chunks (i.e. those with FIRST and LAST bits set) + * + * When the dechunker is in normal operation, these chunks are + * immediately returned to the caller with E_VC_INLINE_CHUNK. If + * however, we are currently dechunking, a passthrough chunk is not allowed + */ +START_TEST(test_dechunker_passthrough) +{ + const char data[] = "Some data to dechunk"; + struct stream *s = make_stream_from_data(data, sizeof(data)); + enum vc_dechunker_status stat; + + struct vc_dechunker *dc = vc_dechunker_init("test", 1000); + ck_assert_ptr_ne(dc, NULL); + + // Save the stream pointer so we can reset the stream in between calls + s_push_layer(s, iso_hdr, 0); + + // Check a passthrough chunk is normally recognised immediately + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST | XR_CHANNEL_FLAG_LAST, + s->size); + ck_assert_int_eq(stat, E_VC_INLINE_CHUNK); + + // Check a passthrough chunk after a first chunk generates an error + // The total size passed for the first chunk must be bigger than the + // dechunker chunking_size + s_pop_layer(s, iso_hdr); // Restore the stream pointer + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST, + 2000); + ck_assert_int_eq(stat, E_VC_IN_PROGRESS); + + s_pop_layer(s, iso_hdr); + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST | XR_CHANNEL_FLAG_LAST, + s->size); + ck_assert_int_eq(stat, E_VC_ERROR); + + // Check a passthrough chunk is accepted after the error + // (i.e. the dechunker can still be used if required) + s_pop_layer(s, iso_hdr); + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST | XR_CHANNEL_FLAG_LAST, + s->size); + ck_assert_int_eq(stat, E_VC_INLINE_CHUNK); + + vc_dechunker_free(dc); + free_stream(s); +} +END_TEST + + +/******************************************************************************/ +/* + * An intermediate chunk (neither first of last) must be rejected if we + * are not dechunking + */ +START_TEST(test_dechunker_intermediate) +{ + const char data[] = PAD50 "Some data to dechunk"; + struct stream *s = make_stream_from_data(data, sizeof(data)); + enum vc_dechunker_status stat; + + struct vc_dechunker *dc = vc_dechunker_init("test", sizeof(data)); + ck_assert_ptr_ne(dc, NULL); + + // Check an intermediate chunk is immediately rejected + stat = vc_dechunker_process_chunk( + dc, s, + 0, + s->size + 1); + ck_assert_int_eq(stat, E_VC_ERROR); + + vc_dechunker_free(dc); + free_stream(s); +} +END_TEST + +/******************************************************************************/ +/* + * A LAST chunk must be rejected if we are not dechunking + */ +START_TEST(test_dechunker_last) +{ + const char data[] = PAD50 "Some data to dechunk"; + struct stream *s = make_stream_from_data(data, sizeof(data)); + enum vc_dechunker_status stat; + + struct vc_dechunker *dc = vc_dechunker_init("test", sizeof(data)); + ck_assert_ptr_ne(dc, NULL); + + // Check a LAST chunk is immediately rejected + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_LAST, + s->size + 1); + ck_assert_int_eq(stat, E_VC_ERROR); + + vc_dechunker_free(dc); + free_stream(s); +} +END_TEST + +/******************************************************************************/ +/** + * Two consecutive FIRST chunks are not allowed + */ +START_TEST(test_dechunker_first_first) +{ + const char data[] = PAD50 "Some data to dechunk"; + struct stream *s = make_stream_from_data(data, sizeof(data)); + enum vc_dechunker_status stat; + + struct vc_dechunker *dc = vc_dechunker_init("test", sizeof(data)); + ck_assert_ptr_ne(dc, NULL); + + // Save the stream pointer so we can reset the stream in between calls + s_push_layer(s, iso_hdr, 0); + + // Check a FIRST chunk is accepted... + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST, + s->size + 1); + ck_assert_int_eq(stat, E_VC_IN_PROGRESS); + + // ... and another FIRST chunk is an error + s_pop_layer(s, iso_hdr); + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST, + s->size + 1); + ck_assert_int_eq(stat, E_VC_ERROR); + + vc_dechunker_free(dc); + free_stream(s); +} +END_TEST + + +/******************************************************************************/ +/** + * Checks that a FIRST chunk cannot be the total_size. This follows from + * [MS-RDPBCGR] 3.1.5.2.1: + * + * > If the total size of the virtual channel data is larger than + * > the chunk size, then each chunk MUST be sent in a separate Virtual + * > Channel PDU. +* +* > Virtual channel data that fits in a single Virtual Channel PDU MUST +* > specify both flags + */ +START_TEST(test_dechunker_chunk_overflow) +{ + const char data[] = PAD50 "Some data to dechunk"; + struct stream *s = make_stream_from_data(data, sizeof(data)); + enum vc_dechunker_status stat; + + struct vc_dechunker *dc = vc_dechunker_init("test", sizeof(data)); + ck_assert_ptr_ne(dc, NULL); + + // Save the stream pointer so we can reset the stream in between calls + s_push_layer(s, iso_hdr, 0); + + // Check a FIRST chunk the same size as the total size is rejected + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST, + s->size); + ck_assert_int_eq(stat, E_VC_ERROR); + + // Check a FIRST chunk bigger than the total size is rejected + // [MS-RDPBCGR] 3.1.5.2.1 + vc_dechunker_free(dc); + dc = vc_dechunker_init("test", sizeof(data)); + ck_assert_ptr_ne(dc, NULL); + s_pop_layer(s, iso_hdr); + stat = vc_dechunker_process_chunk( + dc, s, + XR_CHANNEL_FLAG_FIRST, + s->size - 1); + ck_assert_int_eq(stat, E_VC_ERROR); + + vc_dechunker_free(dc); + free_stream(s); +} +END_TEST + +/******************************************************************************/ +// Returns a stream with some random data, then a chunk of +// up to CHANNEL_CHUNK_LENGTH bytes from Frankenstein chapter 1 +// The stream pointer will be positioned at the start of the text. +static struct stream * +make_bigtest_chunk(unsigned int chunk_num, int *flags) +{ + struct stream *s = NULL; + + if (chunk_num < FRANKENSTEIN_CHUNK_COUNT) + { + int chunk_size; + // Work out the size of this chunk + if (chunk_num == (FRANKENSTEIN_CHUNK_COUNT - 1)) + { + chunk_size = sizeof(frankenstein) % CHANNEL_CHUNK_LENGTH; + if (chunk_size == 0) + { + chunk_size = CHANNEL_CHUNK_LENGTH; + } + } + else + { + chunk_size = CHANNEL_CHUNK_LENGTH; + } + + // Add some random data at the start of the stream, so we + // can check the dechunker works for non-zero positioned + // streams + int rand_size = 4 * 4 * chunk_num; + + make_stream(s); + init_stream(s, rand_size + chunk_size); + + // Write the random data + int i; + for (i = 0 ; i < rand_size; ++i) + { + out_uint8(s, rand() & 255); + } + s_push_layer(s, iso_hdr, 0); + + // Copy the chapter data + out_uint8a(s, &frankenstein[chunk_num * CHANNEL_CHUNK_LENGTH], + chunk_size); + + // Get the stream ready for reading from the Frankenstein text + s_mark_end(s); + s_pop_layer(s, iso_hdr); + + // Sort out the flags + *flags = + (chunk_num == 0) ? XR_CHANNEL_FLAG_FIRST : + (chunk_num == (FRANKENSTEIN_CHUNK_COUNT - 1)) ? XR_CHANNEL_FLAG_LAST : + 0; + } + + return s; +} + +/******************************************************************************/ +/* + * Streams a lot of data through the dechunker and checks it's all + * assembled correctly at the end + */ +START_TEST(test_dechunker_big_test) +{ + enum vc_dechunker_status stat; + struct vc_dechunker *dc = vc_dechunker_init("test", CHANNEL_CHUNK_LENGTH); + ck_assert_ptr_ne(dc, NULL); + struct stream *s; + + int i; + for (i = 0 ; i < FRANKENSTEIN_CHUNK_COUNT; ++i) + { + int flags; + s = make_bigtest_chunk(i, &flags); + stat = vc_dechunker_process_chunk( + dc, s, + flags, + sizeof(frankenstein)); + if (i < FRANKENSTEIN_CHUNK_COUNT - 1) + { + ck_assert_int_eq(stat, E_VC_IN_PROGRESS); + } + else + { + ck_assert_int_eq(stat, E_VC_READY); + } + free_stream(s); + } + + // Check we have a result + s = vc_dechunker_get_stream(dc); + ck_assert_ptr_ne(s, NULL); + + // Is it the right size? + int stream_size = s_rem(s); + ck_assert_int_eq(stream_size, sizeof(frankenstein)); + + // Check the data + const char *p; + in_uint8p(s, p, stream_size); + ck_assert_mem_eq(frankenstein, p, stream_size); + + free_stream(s); + vc_dechunker_free(dc); +} + +/******************************************************************************/ +// Like test_dechunker_big_test, but the last chunk is oversized +START_TEST(test_dechunker_big_test_oversize_fail) +{ + enum vc_dechunker_status stat; + struct vc_dechunker *dc = vc_dechunker_init("test", CHANNEL_CHUNK_LENGTH); + ck_assert_ptr_ne(dc, NULL); + struct stream *s; + + int i; + for (i = 0 ; i < FRANKENSTEIN_CHUNK_COUNT; ++i) + { + int flags; + s = make_bigtest_chunk(i, &flags); + if (i == (FRANKENSTEIN_CHUNK_COUNT - 1)) + { + // Add a byte to the end of the text in the chunk + struct stream *s2; + make_stream(s2); + init_stream(s2, s_rem(s) + 1); + s_push_layer(s2, iso_hdr, 0); + out_uint8p(s2, s->p, s_rem(s)); + out_uint8(s2, 'x'); + s_mark_end(s2); + s_pop_layer(s2, iso_hdr); // Rewind for reading + // Swap s2 and s and delete the original stream + struct stream *tmp = s; + s = s2; + free_stream(tmp); + } + stat = vc_dechunker_process_chunk( + dc, s, + flags, + sizeof(frankenstein)); + if (i < FRANKENSTEIN_CHUNK_COUNT - 1) + { + ck_assert_int_eq(stat, E_VC_IN_PROGRESS); + } + else + { + ck_assert_int_eq(stat, E_VC_ERROR); + } + free_stream(s); + } + + vc_dechunker_free(dc); +} + +/******************************************************************************/ +// Like test_dechunker_big_test, but the last chunk is undersized +START_TEST(test_dechunker_big_test_undersize_fail) +{ + enum vc_dechunker_status stat; + struct vc_dechunker *dc = vc_dechunker_init("test", CHANNEL_CHUNK_LENGTH); + ck_assert_ptr_ne(dc, NULL); + struct stream *s; + + int i; + for (i = 0 ; i < FRANKENSTEIN_CHUNK_COUNT; ++i) + { + int flags; + s = make_bigtest_chunk(i, &flags); + if (i == (FRANKENSTEIN_CHUNK_COUNT - 1)) + { + // Skip a byte in the stream, so there is one + // less byte than expected + in_uint8s(s, 1); + } + stat = vc_dechunker_process_chunk( + dc, s, + flags, + sizeof(frankenstein)); + if (i < FRANKENSTEIN_CHUNK_COUNT - 1) + { + ck_assert_int_eq(stat, E_VC_IN_PROGRESS); + } + else + { + ck_assert_int_eq(stat, E_VC_ERROR); + } + free_stream(s); + } + + vc_dechunker_free(dc); +} + +/******************************************************************************/ + +Suite * +make_suite_test_dechunker(void) +{ + Suite *s; + TCase *rc_dechunker; + + s = suite_create("dechunker"); + + rc_dechunker = tcase_create("dechunker_basic"); + suite_add_tcase(s, rc_dechunker); + tcase_add_test(rc_dechunker, test_dechunker_bad_params); + tcase_add_test(rc_dechunker, test_dechunker_passthrough); + tcase_add_test(rc_dechunker, test_dechunker_intermediate); + tcase_add_test(rc_dechunker, test_dechunker_last); + tcase_add_test(rc_dechunker, test_dechunker_first_first); + tcase_add_test(rc_dechunker, test_dechunker_chunk_overflow); + tcase_add_test(rc_dechunker, test_dechunker_big_test); + tcase_add_test(rc_dechunker, test_dechunker_big_test_oversize_fail); + tcase_add_test(rc_dechunker, test_dechunker_big_test_undersize_fail); + + return s; +}