From e82c13ba03defcee10f981ac964f4d570b21a251 Mon Sep 17 00:00:00 2001 From: Patrick Griffis Date: Wed, 12 Aug 2026 14:21:52 -0500 Subject: [PATCH] message-headers: Fix Range parsing overflows and coalescing cost A suffix range longer than the body drove the range start negative, which then reached a g_assert() and aborted the process. Clamp it to select the whole body instead, as RFC 9110 section 14.1.2 requires. Range starts and ends which overflow the goffset they are parsed into are now rejected and clamped respectively, rather than wrapping negative and reaching the same assertion. Ranges are no longer coalesced by removing each merged element in turn, which was quadratic in their number, and a Range header listing more than MAX_RANGES ranges is now answered with 416 instead of being served; RFC 9110 section 15.5.17 names an excessive number of ranges as a reason for that status. Finally, sort_ranges() returned a goffset difference truncated to int, so it reported the wrong order for ranges more than G_MAXINT apart, and the merge below it then dropped ranges from responses over 2GB. Fixes #516 Fixes #519 Fixes #535 Fixes #538 Fixes #544 Fixes #547 Fixes #548 --- libsoup/soup-message-headers-private.h | 2 + libsoup/soup-message-headers.c | 89 +++++-- tests/range-test.c | 347 +++++++++++++++++++++++++ tests/server-mem-limit-test.c | 61 +++-- 4 files changed, 464 insertions(+), 35 deletions(-) diff --git a/libsoup/soup-message-headers-private.h b/libsoup/soup-message-headers-private.h index 708afe98..2e96af34 100644 --- a/libsoup/soup-message-headers-private.h +++ b/libsoup/soup-message-headers-private.h @@ -12,6 +12,8 @@ G_BEGIN_DECLS #define MAX_HEADERS_BUFFER_SIZE 256 * 1024 /* 256K */ +#define MAX_RANGES 200 + typedef enum { SOUP_HEADER_VALUE_UNTRUSTED, SOUP_HEADER_VALUE_TRUSTED diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c index b2ec036c..7e2e6583 100644 --- a/libsoup/soup-message-headers.c +++ b/libsoup/soup-message-headers.c @@ -1226,7 +1226,12 @@ sort_ranges (gconstpointer a, gconstpointer b) SoupRange *ra = (SoupRange *)a; SoupRange *rb = (SoupRange *)b; - return ra->start - rb->start; + if (ra->start < rb->start) + return -1; + else if (ra->start > rb->start) + return 1; + else + return 0; } /* like soup_message_headers_get_ranges(), except it returns: @@ -1270,6 +1275,17 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs, if (!range_list) return SOUP_STATUS_OK; /* invalid list */ + /* Reject the header outright if it asks for more ranges than we are + * willing to serve, rather than answering with the whole body: a client + * asking for this many ranges wants to be told so, and RFC 9110 §14.2 + * allows rejecting such a header for exactly this reason. + */ + if (g_slist_length (range_list) > MAX_RANGES) { + soup_header_free_list (range_list); + return check_satisfiable ? SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE + : SOUP_STATUS_OK; + } + /* Loop through the ranges and modify the status accordingly. Default to * status 200 (OK, ignoring the ranges). Switch to status 206 (Partial * Content) if there is at least one partially valid range. Switch to @@ -1281,15 +1297,48 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs, spec = r->data; if (*spec == '-') { - cur.start = g_ascii_strtoll (spec, &end, 10) + total_length; + gint64 suffix_length; + + errno = 0; + suffix_length = g_ascii_strtoll (spec, &end, 10); + + /* A suffix range asks for the last -suffix_length bytes + * of the body. If the body is shorter than that then the + * whole body is used, per RFC 9110 §14.1.2; without + * clamping, the start would go negative and reach the + * assertion below. + */ + if (errno == ERANGE || suffix_length <= -total_length) + cur.start = 0; + else + cur.start = total_length + suffix_length; + cur.end = total_length - 1; } else { - cur.start = g_ascii_strtoull (spec, &end, 10); + guint64 value; + + errno = 0; + value = g_ascii_strtoull (spec, &end, 10); + if (errno == ERANGE || value > G_MAXINT64) { + is_all_valid = FALSE; + continue; + } + cur.start = (goffset) value; + if (*end == '-') end++; - if (*end) - cur.end = g_ascii_strtoull (end, &end, 10); - else + if (*end) { + errno = 0; + value = g_ascii_strtoull (end, &end, 10); + + /* An end this large is clamped to the end of the + * body below, like any other end past it. + */ + if (errno == ERANGE || value > G_MAXINT64) + cur.end = G_MAXINT64; + else + cur.end = (goffset) value; + } else cur.end = total_length - 1; } @@ -1330,19 +1379,24 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs, } if (total_length) { - guint i; + SoupRange *data; + guint i, last = 0; g_array_sort (array, sort_ranges); - for (i = 1; i < array->len; i++) { - SoupRange *cur = &((SoupRange *)array->data)[i]; - SoupRange *prev = &((SoupRange *)array->data)[i - 1]; - if (cur->start <= prev->end) { - prev->end = MAX (prev->end, cur->end); - g_array_remove_index (array, i); - i--; - } + /* Merge overlapping ranges into the run being built at @last. + * Removing the merged elements one at a time instead made this + * quadratic in the number of ranges. + */ + data = (SoupRange *)array->data; + for (i = 1; i < array->len; i++) { + if (data[i].start <= data[last].end) + data[last].end = MAX (data[last].end, data[i].end); + else + data[++last] = data[i]; } + + g_array_set_size (array, last + 1); } *ranges = (SoupRange *)array->data; @@ -1375,6 +1429,11 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders *hdrs, * Beware that even if given a @total_length, this function does not * check that the ranges are satisfiable. * + * A Range header requesting more than 200 ranges is rejected, since serving + * that many ranges costs far more than the request asking for them. + * [class@Server] answers such a request with + * %SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE. + * * [class@Server] has built-in handling for range requests. If your * server handler returns a %SOUP_STATUS_OK response containing the * complete response body (rather than pausing the message and diff --git a/tests/range-test.c b/tests/range-test.c index cfd5f613..9e4ed2c9 100644 --- a/tests/range-test.c +++ b/tests/range-test.c @@ -3,6 +3,8 @@ #include "config.h" #include "test-utils.h" +#include "soup-message-headers-private.h" +#include "soup-misc.h" GBytes *full_response; int total_length; @@ -161,6 +163,21 @@ request_single_range_by_string (SoupSession *session, const char *uri, g_object_unref (msg); } +/* Like request_single_range_by_string(), but able to check the ranges of a + * successful 206 as well. */ +static void +request_single_range_by_string_full (SoupSession *session, const char *uri, + const char *range, SoupStatus expected_status, + int expected_start, int expected_end) +{ + SoupMessage *msg; + + msg = soup_message_new ("GET", uri); + soup_message_headers_replace (soup_message_get_request_headers (msg), "Range", range); + + do_single_range (session, msg, 0, 0, expected_status, expected_start, expected_end); +} + static void do_multi_range (SoupSession *session, SoupMessage *msg, int expected_return_ranges) @@ -445,6 +462,290 @@ do_range_test (SoupSession *session, const char *uri, SOUP_STATUS_OK); } +/* Tests for the Range parser itself. Unlike the tests above, these don't need + * a server, so they can use total lengths which would be impractical to + * actually serve, and they can check the exact status which the server would + * use rather than only the ones a client can distinguish. + */ +typedef struct { + const char *description; + const char *bugref; + const char *range; + goffset total_length; + guint expected_status; + int expected_n_ranges; + SoupRange expected_ranges[3]; +} RangeParsingTest; + +static const RangeParsingTest range_parsing_tests[] = { + /* Valid ranges against a ten byte body, as a baseline. */ + { "simple range", NULL, + "bytes=0-4", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 4 } } }, + { "whole body", NULL, + "bytes=0-9", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "open ended range", NULL, + "bytes=5-", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 5, 9 } } }, + { "final byte", NULL, + "bytes=9-", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 9, 9 } } }, + { "end past the body is clamped", NULL, + "bytes=1-100", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 1, 9 } } }, + { "suffix range", NULL, + "bytes=-5", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 5, 9 } } }, + { "single byte suffix range", NULL, + "bytes=-1", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 9, 9 } } }, + { "whitespace around the ranges", NULL, + "bytes \t = \t 0-4", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 4 } } }, + + /* Unsatisfiable and invalid ranges. */ + { "start past the body", NULL, + "bytes=10-20", 10, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0, { } }, + { "end before start", NULL, + "bytes=10-1", 10, SOUP_STATUS_OK, 0, { } }, + { "zero length suffix range", NULL, + "bytes=-0", 10, SOUP_STATUS_OK, 0, { } }, + { "trailing garbage", NULL, + "bytes=0-10 but with weird trailing content", 10, SOUP_STATUS_OK, 0, { } }, + { "invalid range dash", NULL, + "bytes=0a10", 10, SOUP_STATUS_OK, 0, { } }, + { "unknown range unit", NULL, + "horses=0-10", 10, SOUP_STATUS_OK, 0, { } }, + { "missing equals", NULL, + "bytes 0-10", 10, SOUP_STATUS_OK, 0, { } }, + { "delimiters but no ranges", NULL, + "bytes=, ,,\t, ", 10, SOUP_STATUS_OK, 0, { } }, + + /* A suffix length at least as long as the body selects the whole body, + * per RFC 9110 §14.1.2. These used to drive the range start negative, + * which aborted the process at the g_assert() below the parse. + */ + { "suffix range the length of the body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/516", + "bytes=-10", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "suffix range one longer than the body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/516", + "bytes=-11", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "suffix range much longer than the body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/548", + "bytes=-999999", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "suffix range of G_MININT", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547", + "bytes=-2147483648", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "suffix range larger than a guint32", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547", + "bytes=-4294967296", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "suffix range of G_MAXINT64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547", + "bytes=-9223372036854775807", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "suffix range of G_MININT64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/547", + "bytes=-9223372036854775808", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "suffix range overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=-99999999999999999999", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "oversized suffix range merged with a valid range", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/548", + "bytes=-999999,4-5", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + + /* Range starts and ends which overflow the signed goffset they are + * parsed into. An overflowing start is treated like any other start + * beyond the end of the body, and an overflowing end is clamped like + * any other end beyond the end of the body. + */ + { "start overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=9888888888888019900-", 10, SOUP_STATUS_OK, 0, { } }, + { "start overflowing gint64 with no dash", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=9888888888888019900", 10, SOUP_STATUS_OK, 0, { } }, + { "start of G_MAXINT64 + 1", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=9223372036854775808-", 10, SOUP_STATUS_OK, 0, { } }, + { "start overflowing guint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=18446744073709551616-", 10, SOUP_STATUS_OK, 0, { } }, + { "start and end overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=9888888888888019900-9888888888888019901", 10, SOUP_STATUS_OK, 0, { } }, + { "end overflowing gint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=0-9888888888888019900", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + { "end overflowing guint64", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/535", + "bytes=0-18446744073709551616", 10, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 9 } } }, + + /* Zero length bodies. soup_message_headers_get_ranges() is public API, + * so it can be called with one even though the server never does. + */ + { "range against an empty body", NULL, + "bytes=0-9", 0, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0, { } }, + { "suffix range against an empty body", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/548", + "bytes=-5", 0, SOUP_STATUS_OK, 0, { } }, + + /* Merging. */ + { "overlapping ranges are merged", NULL, + "bytes=0-10,5-20", 100, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 20 } } }, + { "contained ranges are merged", NULL, + "bytes=0-20,5-10", 100, SOUP_STATUS_PARTIAL_CONTENT, 1, { { 0, 20 } } }, + { "touching ranges are not merged", NULL, + "bytes=0-4,5-9", 100, SOUP_STATUS_PARTIAL_CONTENT, 2, { { 0, 4 }, { 5, 9 } } }, + { "ranges are sorted", NULL, + "bytes=20-29,0-9", 100, SOUP_STATUS_PARTIAL_CONTENT, 2, { { 0, 9 }, { 20, 29 } } }, + { "invalid ranges do not prevent valid ones", NULL, + "bytes=0-9,50-40,20-29", 100, SOUP_STATUS_PARTIAL_CONTENT, 2, { { 0, 9 }, { 20, 29 } } }, + + /* The comparison function used to sort the ranges before merging them + * used to truncate a goffset difference to int, which flips its sign + * for bodies over 2GB and silently dropped ranges from the response. + */ + { "ranges more than G_MAXINT apart", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/519", + "bytes=0-100, 2500000000-2500000100, 4999999000-4999999100", 5000000000, + SOUP_STATUS_PARTIAL_CONTENT, 3, + { { 0, 100 }, { 2500000000, 2500000100 }, { 4999999000, 4999999100 } } }, + { "unsorted ranges more than G_MAXINT apart", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/519", + "bytes=4999999000-4999999100, 0-100, 2500000000-2500000100", 5000000000, + SOUP_STATUS_PARTIAL_CONTENT, 3, + { { 0, 100 }, { 2500000000, 2500000100 }, { 4999999000, 4999999100 } } }, +}; + +static void +check_parsed_ranges (const char *range, + goffset total_length, + guint expected_status, + int expected_n_ranges, + const SoupRange *expected_ranges) +{ + SoupMessageHeaders *hdrs; + SoupRange *ranges = NULL; + int n_ranges = 0; + guint status; + int i; + + hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST); + soup_message_headers_replace (hdrs, "Range", range); + + status = soup_message_headers_get_ranges_internal (hdrs, total_length, TRUE, + &ranges, &n_ranges); + + g_assert_cmpuint (status, ==, expected_status); + + if (status == SOUP_STATUS_PARTIAL_CONTENT) { + g_assert_nonnull (ranges); + g_assert_cmpint (n_ranges, ==, expected_n_ranges); + + for (i = 0; i < n_ranges; i++) { + debug_printf (2, " [%d]: %" G_GINT64_FORMAT "-%" G_GINT64_FORMAT "\n", + i, ranges[i].start, ranges[i].end); + + g_assert_cmpint (ranges[i].start, ==, expected_ranges[i].start); + g_assert_cmpint (ranges[i].end, ==, expected_ranges[i].end); + + /* Whatever the input, the parsed ranges must be usable + * as offsets into a buffer of total_length bytes. + */ + g_assert_cmpint (ranges[i].start, >=, 0); + g_assert_cmpint (ranges[i].end, >=, ranges[i].start); + g_assert_cmpint (ranges[i].end, <, total_length); + } + } + + soup_message_headers_free_ranges (hdrs, ranges); + soup_message_headers_unref (hdrs); +} + +static void +do_range_parsing_test (void) +{ + guint i; + + for (i = 0; i < G_N_ELEMENTS (range_parsing_tests); i++) { + const RangeParsingTest *test = &range_parsing_tests[i]; + + debug_printf (1, "%2u. %s: '%s' against %" G_GOFFSET_FORMAT " bytes\n", + i + 1, test->description, test->range, test->total_length); + + if (test->bugref) + g_test_message ("Bug reference: %s", test->bugref); + + check_parsed_ranges (test->range, test->total_length, + test->expected_status, test->expected_n_ranges, + test->expected_ranges); + } +} + +/* A single Range header can list far more ranges than are reasonable to serve: + * the only limit on the wire is the maximum request header size. */ +static void +do_range_count_test (void) +{ + struct { + int n_ranges; + gboolean identical; + guint expected_status; + int expected_n_ranges; + } tests[] = { + /* Distinct ranges, up to and then past the limit. Going past it + * is rejected rather than ignored. + */ + { 100, FALSE, SOUP_STATUS_PARTIAL_CONTENT, 100 }, + { MAX_RANGES, FALSE, SOUP_STATUS_PARTIAL_CONTENT, MAX_RANGES }, + { MAX_RANGES + 1, FALSE, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0 }, + + /* Identical ranges, which all merge into one. This is the + * shape which used to be quadratic. + */ + { MAX_RANGES, TRUE, SOUP_STATUS_PARTIAL_CONTENT, 1 }, + { MAX_RANGES + 1, TRUE, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0 }, + { 25585, TRUE, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE, 0 }, + }; + guint i; + int j; + + for (i = 0; i < G_N_ELEMENTS (tests); i++) { + SoupMessageHeaders *hdrs; + SoupRange *ranges = NULL; + int n_ranges = 0; + guint status; + GString *range; + + debug_printf (1, "%2u. %d %s ranges\n", i + 1, tests[i].n_ranges, + tests[i].identical ? "identical" : "distinct"); + + range = g_string_new ("bytes="); + for (j = 0; j < tests[i].n_ranges; j++) { + int start = tests[i].identical ? 0 : j * 2; + + if (j > 0) + g_string_append_c (range, ','); + g_string_append_printf (range, "%d-%d", start, start); + } + + hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST); + soup_message_headers_replace (hdrs, "Range", range->str); + g_string_free (range, TRUE); + + status = soup_message_headers_get_ranges_internal (hdrs, 1000000, TRUE, + &ranges, &n_ranges); + + g_assert_cmpuint (status, ==, tests[i].expected_status); + if (status == SOUP_STATUS_PARTIAL_CONTENT) + g_assert_cmpint (n_ranges, ==, tests[i].expected_n_ranges); + + soup_message_headers_free_ranges (hdrs, ranges); + soup_message_headers_unref (hdrs); + } + + /* Callers which don't ask about satisfiability, such as the public + * soup_message_headers_get_ranges(), can't be told 416, so an + * over-limit header just reports no ranges to them. + */ + { + SoupMessageHeaders *hdrs; + SoupRange *ranges = NULL; + int n_ranges = 0; + GString *range; + + range = g_string_new ("bytes=0-0"); + for (j = 0; j < MAX_RANGES; j++) + g_string_append (range, ",0-0"); + + hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_REQUEST); + soup_message_headers_replace (hdrs, "Range", range->str); + g_string_free (range, TRUE); + + g_assert_cmpuint (soup_message_headers_get_ranges_internal (hdrs, 1000000, FALSE, + &ranges, &n_ranges), + ==, SOUP_STATUS_OK); + g_assert_false (soup_message_headers_get_ranges (hdrs, 1000000, &ranges, &n_ranges)); + + soup_message_headers_free_ranges (hdrs, ranges); + soup_message_headers_unref (hdrs); + } +} + #ifdef HAVE_APACHE static void do_apache_range_test (void) @@ -473,6 +774,49 @@ server_handler (SoupServer *server, full_response); } +static void +do_libsoup_only_range_test (SoupSession *session, const char *uri) +{ + gsize full_response_length = g_bytes_get_size (full_response); + GString *range; + int i; + + /* A suffix length at least as long as the body selects the whole body. */ + debug_printf (1, "Requesting (suffix range the length of the body) -%d\n", + (int) full_response_length); + request_single_range (session, uri, + -((int) full_response_length), -1, + SOUP_STATUS_PARTIAL_CONTENT, 0, -1); + + debug_printf (1, "Requesting (suffix range longer than the body) -999999\n"); + request_single_range_by_string_full (session, uri, "bytes=-999999", + SOUP_STATUS_PARTIAL_CONTENT, 0, -1); + + debug_printf (1, "Requesting (suffix range overflowing gint64) -99999999999999999999\n"); + request_single_range_by_string_full (session, uri, "bytes=-99999999999999999999", + SOUP_STATUS_PARTIAL_CONTENT, 0, -1); + + /* A start which overflows gint64 is treated like any other start past + * the end of the body. + * https://gitlab.gnome.org/GNOME/libsoup/-/issues/535 + */ + debug_printf (1, "Requesting (start overflowing gint64) 9888888888888019900-\n"); + request_single_range_by_string (session, uri, "bytes=9888888888888019900-", + SOUP_STATUS_OK); + + /* More ranges than the server is willing to coalesce, which is + * rejected rather than answered with the whole body. + * https://gitlab.gnome.org/GNOME/libsoup/-/issues/538 + */ + debug_printf (1, "Requesting (more ranges than the limit)\n"); + range = g_string_new ("bytes="); + for (i = 0; i < MAX_RANGES + 1; i++) + g_string_append (range, i > 0 ? ",0-0" : "0-0"); + request_single_range_by_string (session, uri, range->str, + SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE); + g_string_free (range, TRUE); +} + static void do_libsoup_range_test (void) { @@ -488,6 +832,7 @@ do_libsoup_range_test (void) base_uri = soup_test_server_get_uri (server, "http", NULL); base_uri_str = g_uri_to_string (base_uri); do_range_test (session, base_uri_str, TRUE, TRUE); + do_libsoup_only_range_test (session, base_uri_str); g_uri_unref (base_uri); g_free (base_uri_str); soup_test_server_quit_unref (server); @@ -512,6 +857,8 @@ main (int argc, char **argv) g_test_add_func ("/ranges/apache", do_apache_range_test); #endif g_test_add_func ("/ranges/libsoup", do_libsoup_range_test); + g_test_add_func ("/ranges/parsing", do_range_parsing_test); + g_test_add_func ("/ranges/count", do_range_count_test); ret = g_test_run (); diff --git a/tests/server-mem-limit-test.c b/tests/server-mem-limit-test.c index 65dc875e..ee20ba9b 100644 --- a/tests/server-mem-limit-test.c +++ b/tests/server-mem-limit-test.c @@ -4,6 +4,7 @@ */ #include "test-utils.h" +#include "soup-message-headers-private.h" #include @@ -81,46 +82,66 @@ server_file_callback (SoupServer *server, } static void -do_ranges_overlaps_test (ServerData *sd, gconstpointer test_data) +request_ranges (ServerData *sd, const char *range, SoupStatus expected_status) { SoupSession *session; SoupMessage *msg; - GString *range; GUri *uri; - const char *chunk = ",0,0,0,0,0,0,0,0,0,0,0"; - - g_test_bug ("428"); - - #ifdef G_OS_WIN32 - g_test_skip ("Cannot run under windows"); - return; - #endif - - range = g_string_sized_new (99 * 1024); - g_string_append (range, "bytes=1024"); - while (range->len < 99 * 1024) - g_string_append (range, chunk); session = soup_test_session_new (NULL); - server_add_handler (sd, "/file", server_file_callback, NULL, NULL); uri = g_uri_parse_relative (sd->base_uri, "/file", SOUP_HTTP_URI_FLAGS, NULL); msg = soup_message_new_from_uri ("GET", uri); - soup_message_headers_append (soup_message_get_request_headers (msg), "Range", range->str); + soup_message_headers_append (soup_message_get_request_headers (msg), "Range", range); soup_test_session_send_message (session, msg); - soup_test_assert_message_status (msg, SOUP_STATUS_PARTIAL_CONTENT); + soup_test_assert_message_status (msg, expected_status); g_object_unref (msg); - - g_string_free (range, TRUE); g_uri_unref (uri); soup_test_session_abort_unref (session); } +static void +do_ranges_overlaps_test (ServerData *sd, gconstpointer test_data) +{ + GString *range; + const char *chunk = ",0,0,0,0,0,0,0,0,0,0,0"; + int i; + + g_test_bug ("428"); + + #ifdef G_OS_WIN32 + g_test_skip ("Cannot run under windows"); + return; + #endif + + server_add_handler (sd, "/file", server_file_callback, NULL, NULL); + + /* Requesting the same range many times over used to make the server + * allocate a response proportional to the number of ranges instead of + * coalescing them into one. Each "0" here is an open ended range + * covering the whole body, so they all collapse into a single range. + */ + range = g_string_new ("bytes=1024"); + for (i = 1; i < MAX_RANGES; i++) + g_string_append (range, ",0"); + request_ranges (sd, range->str, SOUP_STATUS_PARTIAL_CONTENT); + g_string_free (range, TRUE); + + /* A header listing more ranges than the server is willing to coalesce + * is rejected outright. */ + range = g_string_sized_new ((gsize)99 * 1024); + g_string_append (range, "bytes=1024"); + while (range->len < (gssize)99 * 1024) + g_string_append (range, chunk); + request_ranges (sd, range->str, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE); + g_string_free (range, TRUE); +} + int main (int argc, char **argv) { -- GitLab