From 52825c2eb7043698d7a0668cfe6bb5a23da87cf5 Mon Sep 17 00:00:00 2001 From: Milan Crha Date: Thu, 15 May 2025 07:59:14 +0200 Subject: [PATCH 1/2] soup-date-utils: Add value checks for date/time parsing Reject date/time when it does not represent a valid value. Closes https://gitlab.gnome.org/GNOME/libsoup/-/issues/448 --- libsoup/soup-date.c | 23 ++- tests/cookies-test.c | 10 + tests/date-test.c | 434 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 459 insertions(+), 8 deletions(-) create mode 100644 tests/date-test.c diff --git a/libsoup/soup-date.c b/libsoup/soup-date.c index dabae9d4..8b2beea4 100644 --- a/libsoup/soup-date.c +++ b/libsoup/soup-date.c @@ -284,7 +284,7 @@ parse_day (SoupDate *date, const char **date_string) while (*end == ' ' || *end == '-') end++; *date_string = end; - return TRUE; + return date->day >= 1 && date->day <= 31; } static inline gboolean @@ -324,7 +324,7 @@ parse_year (SoupDate *date, const char **date_string) while (*end == ' ' || *end == '-') end++; *date_string = end; - return TRUE; + return date->year > 0 && date->year < 9999; } static inline gboolean @@ -348,7 +348,7 @@ parse_time (SoupDate *date, const char **date_string) while (*p == ' ') p++; *date_string = p; - return TRUE; + return date->hour >= 0 && date->hour < 24 && date->minute >= 0 && date->minute < 60 && date->second >= 0 && date->second < 60; } static inline gboolean @@ -361,9 +361,14 @@ parse_timezone (SoupDate *date, const char **date_string) gulong val; int sign = (**date_string == '+') ? -1 : 1; val = strtoul (*date_string + 1, (char **)date_string, 10); - if (**date_string == ':') - val = 60 * val + strtoul (*date_string + 1, (char **)date_string, 10); - else + if (val > 9999) + return FALSE; + if (**date_string == ':') { + gulong val2 = strtoul (*date_string + 1, (char **)date_string, 10); + if (val > 99 || val2 > 99) + return FALSE; + val = 60 * val + val2; + } else val = 60 * (val / 100) + (val % 100); date->offset = sign * val; date->utc = (sign == -1) && !val; @@ -407,7 +412,8 @@ parse_textual_date (SoupDate *date, const char *date_string) if (!parse_month (date, &date_string) || !parse_day (date, &date_string) || !parse_time (date, &date_string) || - !parse_year (date, &date_string)) + !parse_year (date, &date_string) || + !g_date_valid_dmy (date->day, date->month, date->year)) return FALSE; /* There shouldn't be a timezone, but check anyway */ @@ -419,7 +425,8 @@ parse_textual_date (SoupDate *date, const char *date_string) if (!parse_day (date, &date_string) || !parse_month (date, &date_string) || !parse_year (date, &date_string) || - !parse_time (date, &date_string)) + !parse_time (date, &date_string) || + !g_date_valid_dmy (date->day, date->month, date->year)) return FALSE; /* This time there *should* be a timezone, but we diff --git a/tests/cookies-test.c b/tests/cookies-test.c index 17b77f29..61adfac5 100644 --- a/tests/cookies-test.c +++ b/tests/cookies-test.c @@ -288,6 +288,15 @@ do_cookies_parsing_nopath_nullorigin (void) soup_cookie_free (cookie); } +static void +do_cookies_parsing_int32_overflow (void) +{ + SoupCookie *cookie = soup_cookie_parse ("Age=1;expires=3Mar9 999:9:9+ 999999999-age=main=gne=", NULL); + g_assert_nonnull (cookie); + g_assert_null (soup_cookie_get_expires (cookie)); + soup_cookie_free (cookie); +} + int main (int argc, char **argv) { @@ -309,6 +318,7 @@ main (int argc, char **argv) g_test_add_func ("/cookies/accept-policy-subdomains", do_cookies_subdomain_policy_test); g_test_add_func ("/cookies/parsing", do_cookies_parsing_test); g_test_add_func ("/cookies/parsing/no-path-null-origin", do_cookies_parsing_nopath_nullorigin); + g_test_add_func ("/cookies/parsing/int32-overflow", do_cookies_parsing_int32_overflow); ret = g_test_run (); diff --git a/tests/date-test.c b/tests/date-test.c new file mode 100644 index 00000000..38f27335 --- /dev/null +++ b/tests/date-test.c @@ -0,0 +1,434 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ +/* + * Copyright (C) 2005 Novell, Inc. + */ + +#include "test-utils.h" + +static void check_ok (gconstpointer data); + +static SoupDate * +make_date (const char *strdate) +{ + char *dup; + SoupDate *date; + + /* We do it this way so that if soup_date_new_from_string() + * reads off the end of the string, it will trigger an error + * when valgrinding, rather than just reading the start of the + * next const string. + */ + dup = g_strdup (strdate); + date = soup_date_new_from_string (dup); + g_free (dup); + return date; +} + +static SoupDate * +check_correct_date (const char *strdate) +{ + SoupDate *date; + + date = make_date (strdate); + if (!date) { + g_assert_nonnull (date); + return NULL; + } + + g_assert_cmpint (date->year, ==, 2004); + g_assert_cmpint (date->month, ==, 11); + g_assert_cmpint (date->day, ==, 6); + g_assert_cmpint (date->hour, ==, 8); + g_assert_cmpint (date->minute, ==, 9); + g_assert_cmpint (date->second, ==, 7); + + return date; +} + +typedef struct { + SoupDateFormat format; + const char *date; + const char *bugref; +} GoodDate; + +static const GoodDate good_dates[] = { + { SOUP_DATE_HTTP, "Sat, 06 Nov 2004 08:09:07 GMT", NULL }, + { SOUP_DATE_COOKIE, "Sat, 06-Nov-2004 08:09:07 GMT", NULL }, + { SOUP_DATE_RFC2822, "Sat, 6 Nov 2004 08:09:07 -0430", "579055" }, + { SOUP_DATE_ISO8601_COMPACT, "20041106T080907", NULL }, + { SOUP_DATE_ISO8601_FULL, "2004-11-06T08:09:07", NULL }, + { SOUP_DATE_ISO8601_XMLRPC, "20041106T08:09:07", NULL } +}; + +static void +check_good (gconstpointer data) +{ + GoodDate *good = (GoodDate *)data; + SoupDate *date; + char *strdate2; + + if (good->bugref) + g_test_bug (good->bugref); + + date = check_correct_date (good->date); + if (!date) + return; + + strdate2 = soup_date_to_string (date, good->format); + soup_date_free (date); + + soup_test_assert (strcmp (good->date, strdate2) == 0, + "restringification failed: '%s' -> '%s'\n", + good->date, strdate2); + g_free (strdate2); +} + +typedef struct { + const char *date; + const char *bugref; +} OkDate; + +static const OkDate ok_dates[] = { + /* rfc1123-date, and broken variants */ + { "Sat, 06 Nov 2004 08:09:07 GMT", NULL }, + { "Sat, 6 Nov 2004 08:09:07 GMT", NULL }, + { "Sat, 6 Nov 2004 08:09:07 GMT", NULL }, + { "Sat, 06 Nov 2004 08:09:07", NULL }, + { "06 Nov 2004 08:09:07 GMT", NULL }, + { "SAT, 06 NOV 2004 08:09:07 +1000", "644048" }, + { "Sat, 06-Nov-2004 08:09:07 -10000", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" }, + { "Sat, 06-Nov-2004 08:09:07 +01:30", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" }, + { "Sat, 06-Nov-2004 08:09:07 +0:180", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" }, + { "Sat, 06-Nov-2004 08:09:07 +100:100", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" }, + { "Sat, 06-Nov-2004 08:09:07 Z", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" }, + + /* rfc850-date, and broken variants */ + { "Saturday, 06-Nov-04 08:09:07 GMT", NULL }, + { "Saturday, 6-Nov-04 08:09:07 GMT", NULL }, + { "Saturday, 6-Nov-04 08:09:07 GMT", NULL }, + { "Saturday, 06-Nov-104 08:09:07 GMT", NULL }, + { "Saturday, 06-Nov-2004 08:09:07 GMT", NULL }, + { "Saturday, 6-Nov-2004 08:09:07 GMT", NULL }, + { "Saturday, 6-Nov-2004 08:09:07 GMT", NULL }, + { "Saturday, 06-Nov-04 08:09:07", NULL }, + { "06-Nov-04 08:09:07 GMT", NULL }, + + /* asctime-date, and broken variants */ + { "Sat Nov 6 08:09:07 2004", NULL }, + { "Sat Nov 06 08:09:07 2004", NULL }, + { "Sat Nov 6 08:09:07 2004", NULL }, + { "Sat Nov 6 08:09:07 2004 GMT", NULL }, + { "Sat Nov 6 08:09:07 2004 NoZone", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" }, + { "Sat Nov 6 08:09:07 2004 UTC", "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" }, + + /* ISO 8601 */ + { "2004-11-06T08:09:07Z", NULL }, + { "20041106T08:09:07Z", NULL }, + { "20041106T08:09:07+00:00", NULL }, + { "20041106T080907+00:00", NULL }, + + /* Netscape cookie spec date, and broken variants */ + { "Sat, 06-Nov-2004 08:09:07 GMT", NULL }, + { "Sat, 6-Nov-2004 08:09:07 GMT", NULL }, + { "Sat, 6-Nov-2004 08:09:07 GMT", NULL }, + { "Sat, 06-Nov-2004 08:09:07", NULL }, + + /* Original version of Netscape cookie spec, and broken variants */ + { "Sat, 06-Nov-04 08:09:07 GMT", NULL }, + { "Sat, 6-Nov-04 08:09:07 GMT", NULL }, + { "Sat, 6-Nov-04 08:09:07 GMT", NULL }, + { "Sat, 06-Nov-104 08:09:07 GMT", NULL }, + { "Sat, 06-Nov-04 08:09:07", NULL }, + + /* Miscellaneous broken formats seen on the web */ + { "Sat 06-Nov-2004 08:9:07", NULL }, + { "Saturday, 06-Nov-04 8:9:07 GMT", NULL }, + { "Sat, 06 Nov 2004 08:09:7 GMT", NULL } +}; + +static void +check_ok (gconstpointer data) +{ + OkDate *ok = (OkDate *)data; + SoupDate *date; + + if (ok->bugref) + g_test_bug (ok->bugref); + + date = check_correct_date (ok->date); + if (!date) + return; + soup_date_free (date); +} + +#define TIME_T 1099728547L +#define TIME_T_STRING "1099728547" + +static void +check_ok_time_t (void) +{ + SoupDate *date; + + date = soup_date_new_from_time_t (TIME_T); + + g_assert_cmpint (date->year, ==, 2004); + g_assert_cmpint (date->month, ==, 11); + g_assert_cmpint (date->day, ==, 6); + g_assert_cmpint (date->hour, ==, 8); + g_assert_cmpint (date->minute, ==, 9); + g_assert_cmpint (date->second, ==, 7); + + g_assert_cmpuint (TIME_T, ==, soup_date_to_time_t (date)); + + soup_date_free (date); +} + +typedef struct { + const char *date; + const char *bugref; +} BadDate; + +static const BadDate bad_dates[] = { + /* broken rfc1123-date */ + { ", 06 Nov 2004 08:09:07 GMT", NULL }, + { "Sat, Nov 2004 08:09:07 GMT", NULL }, + { "Sat, 06 2004 08:09:07 GMT", NULL }, + { "Sat, 06 Nov 08:09:07 GMT", NULL }, + { "Sat, 06 Nov 2004 :09:07 GMT", NULL }, + { "Sat, 06 Nov 2004 09:07 GMT", NULL }, + { "Sat, 06 Nov 2004 08::07 GMT", NULL }, + { "Sat, 06 Nov 2004 08:09: GMT", NULL }, + + /* broken rfc850-date */ + { ", 06-Nov-04 08:09:07 GMT", NULL }, + { "Saturday, -Nov-04 08:09:07 GMT", NULL }, + { "Saturday, Nov-04 08:09:07 GMT", NULL }, + { "Saturday, 06-04 08:09:07 GMT", NULL }, + { "Saturday, 06--04 08:09:07 GMT", NULL }, + { "Saturday, 06-Nov- 08:09:07 GMT", NULL }, + { "Saturday, 06-Nov 08:09:07 GMT", NULL }, + { "Saturday, 06-Nov-04 :09:07 GMT", NULL }, + { "Saturday, 06-Nov-04 09:07 GMT", NULL }, + { "Saturday, 06-Nov-04 08::07 GMT", NULL }, + { "Saturday, 06-Nov-04 08:09: GMT", NULL }, + + /* broken asctime-date */ + { "Nov 6 08:09:07 2004", NULL }, + { "Sat 6 08:09:07 2004", NULL }, + { "Sat Nov 08:09:07 2004", NULL }, + { "Sat Nov 6 :09:07 2004", NULL }, + { "Sat Nov 6 09:07 2004", NULL }, + { "Sat Nov 6 08::07 2004", NULL }, + { "Sat Nov 6 08:09: 2004", NULL }, + { "Sat Nov 6 08:09:07", NULL }, + { "Sat Nov 6 08:09:07 GMT 2004", NULL }, + + /* range constraints added "https://gitlab.gnome.org/GNOME/libsoup/-/issues/448" */ + { "Sat, 00-Nov-2004 08:09:07 GMT", NULL }, + { "Sat, 32-Nov-2004 08:09:07 GMT", NULL }, + { "Sat, 06-Nov-0 08:09:07 GMT", NULL }, + { "Sat, 06-Nov-9999 08:09:07 GMT", NULL }, + { "Sat, 06-Nov-2004 0-1:09:07 GMT", NULL }, + { "(Sat), Nov 6 -1:09:07 2004", NULL }, + { "Sat, 06-Nov-2004 24:09:07 GMT", NULL }, + { "Sat, 06-Nov-2004 08:-1:07 GMT", NULL }, + { "Sat, 06-Nov-2004 08:60:07 GMT", NULL }, + { "Sat, 06-Nov-2004 08:09:-10 GMT", NULL }, + { "Sat, 06-Nov-2004 08:09:60 GMT", NULL }, + { "Sat, 06-Nov-71 08:09:99 UTC", NULL }, + { "Sat, 31-Feb-2004 08:09:07 UTC", NULL }, +}; + +static void +check_bad (gconstpointer data) +{ + BadDate *bad = (BadDate *)data; + SoupDate *date; + + if (bad->bugref) + g_test_bug (bad->bugref); + + date = make_date (bad->date); + soup_test_assert (date == NULL, + "date parsing succeeded for '%s': %d %d %d - %d %d %d", + bad->date, + date->year, date->month, date->day, + date->hour, date->minute, date->second); + g_clear_pointer (&date, soup_date_free); +} + +typedef struct { + const char *source; + const char *http, *cookie, *rfc2822, *compact, *full, *xmlrpc; +} DateConversion; + +static const DateConversion conversions[] = { + /* SOUP_DATE_HTTP */ + { "Sat, 06 Nov 2004 08:09:07 GMT", + + "Sat, 06 Nov 2004 08:09:07 GMT", + "Sat, 06-Nov-2004 08:09:07 GMT", + "Sat, 6 Nov 2004 08:09:07 +0000", + "20041106T080907Z", + "2004-11-06T08:09:07Z", + "20041106T08:09:07" }, + + /* RFC2822 GMT */ + { "Sat, 6 Nov 2004 08:09:07 +0000", + + "Sat, 06 Nov 2004 08:09:07 GMT", + "Sat, 06-Nov-2004 08:09:07 GMT", + "Sat, 6 Nov 2004 08:09:07 +0000", + "20041106T080907Z", + "2004-11-06T08:09:07Z", + "20041106T08:09:07" }, + + /* RFC2822 with positive offset */ + { "Sat, 6 Nov 2004 08:09:07 +0430", + + "Sat, 06 Nov 2004 04:39:07 GMT", + "Sat, 06-Nov-2004 04:39:07 GMT", + "Sat, 6 Nov 2004 08:09:07 +0430", + "20041106T080907+0430", + "2004-11-06T08:09:07+04:30", + "20041106T08:09:07" }, + + /* RFC2822 with negative offset */ + { "Sat, 6 Nov 2004 08:09:07 -0430", + + "Sat, 06 Nov 2004 12:39:07 GMT", + "Sat, 06-Nov-2004 12:39:07 GMT", + "Sat, 6 Nov 2004 08:09:07 -0430", + "20041106T080907-0430", + "2004-11-06T08:09:07-04:30", + "20041106T08:09:07" }, + + /* RFC2822 floating */ + { "Sat, 6 Nov 2004 08:09:07 -0000", + + "Sat, 06 Nov 2004 08:09:07 GMT", + "Sat, 06-Nov-2004 08:09:07 GMT", + "Sat, 6 Nov 2004 08:09:07 -0000", + "20041106T080907", + "2004-11-06T08:09:07", + "20041106T08:09:07" }, + + /* ISO GMT */ + { "2004-11-06T08:09:07Z", + + "Sat, 06 Nov 2004 08:09:07 GMT", + "Sat, 06-Nov-2004 08:09:07 GMT", + "Sat, 6 Nov 2004 08:09:07 +0000", + "20041106T080907Z", + "2004-11-06T08:09:07Z", + "20041106T08:09:07" }, + + /* ISO with positive offset */ + { "2004-11-06T08:09:07+04:30", + + "Sat, 06 Nov 2004 04:39:07 GMT", + "Sat, 06-Nov-2004 04:39:07 GMT", + "Sat, 6 Nov 2004 08:09:07 +0430", + "20041106T080907+0430", + "2004-11-06T08:09:07+04:30", + "20041106T08:09:07" }, + + /* ISO with negative offset */ + { "2004-11-06T08:09:07-04:30", + + "Sat, 06 Nov 2004 12:39:07 GMT", + "Sat, 06-Nov-2004 12:39:07 GMT", + "Sat, 6 Nov 2004 08:09:07 -0430", + "20041106T080907-0430", + "2004-11-06T08:09:07-04:30", + "20041106T08:09:07" }, + + /* ISO floating */ + { "2004-11-06T08:09:07", + + "Sat, 06 Nov 2004 08:09:07 GMT", + "Sat, 06-Nov-2004 08:09:07 GMT", + "Sat, 6 Nov 2004 08:09:07 -0000", + "20041106T080907", + "2004-11-06T08:09:07", + "20041106T08:09:07" } +}; + +static void +check_conversion (gconstpointer data) +{ + const DateConversion *conv = data; + SoupDate *date; + char *str; + + date = make_date (conv->source); + if (!date) { + soup_test_assert (FALSE, "date parsing failed for '%s'.", conv->source); + return; + } + + str = soup_date_to_string (date, SOUP_DATE_HTTP); + g_assert_cmpstr (str, ==, conv->http); + g_free (str); + + str = soup_date_to_string (date, SOUP_DATE_COOKIE); + g_assert_cmpstr (str, ==, conv->cookie); + g_free (str); + + str = soup_date_to_string (date, SOUP_DATE_RFC2822); + g_assert_cmpstr (str, ==, conv->rfc2822); + g_free (str); + + str = soup_date_to_string (date, SOUP_DATE_ISO8601_COMPACT); + g_assert_cmpstr (str, ==, conv->compact); + g_free (str); + + str = soup_date_to_string (date, SOUP_DATE_ISO8601_FULL); + g_assert_cmpstr (str, ==, conv->full); + g_free (str); + + str = soup_date_to_string (date, SOUP_DATE_ISO8601_XMLRPC); + g_assert_cmpstr (str, ==, conv->xmlrpc); + g_free (str); + + soup_date_free (date); +} + +int +main (int argc, char **argv) +{ + int i, ret; + char *path; + + test_init (argc, argv, NULL); + + for (i = 0; i < G_N_ELEMENTS (good_dates); i++) { + path = g_strdup_printf ("/date/good/%s", good_dates[i].date); + g_test_add_data_func (path, &good_dates[i], check_good); + g_free (path); + } + + for (i = 0; i < G_N_ELEMENTS (ok_dates); i++) { + path = g_strdup_printf ("/date/ok/%s", ok_dates[i].date); + g_test_add_data_func (path, &ok_dates[i], check_ok); + g_free (path); + } + g_test_add_func ("/date/ok/" TIME_T_STRING, check_ok_time_t); + + for (i = 0; i < G_N_ELEMENTS (bad_dates); i++) { + path = g_strdup_printf ("/date/bad/%s", bad_dates[i].date); + g_test_add_data_func (path, &bad_dates[i], check_bad); + g_free (path); + } + + for (i = 0; i < G_N_ELEMENTS (conversions); i++) { + path = g_strdup_printf ("/date/conversions/%s", conversions[i].source); + g_test_add_data_func (path, &conversions[i], check_conversion); + g_free (path); + } + + ret = g_test_run (); + + test_cleanup (); + return ret; +} -- 2.51.0 From 724efae52bcf2c0b8a247f736103285ad68d1b15 Mon Sep 17 00:00:00 2001 From: Brian Yurko <155515-byurko@users.noreply.gitlab.gnome.org> Date: Wed, 11 Jun 2025 11:00:56 -0400 Subject: [PATCH 2/2] tests: Add tests for date-time including timezone validation work These tests are built on top of earlier work in a related pull request. Closes #448 --- libsoup/soup-date-utils.c | 339 ++++++++++++++++++++++++++++++++++++++ tests/cookies-test.c | 1 + 2 files changed, 340 insertions(+) create mode 100644 libsoup/soup-date-utils.c diff --git a/libsoup/soup-date-utils.c b/libsoup/soup-date-utils.c new file mode 100644 index 00000000..73f80ab6 --- /dev/null +++ b/libsoup/soup-date-utils.c @@ -0,0 +1,339 @@ +/* + * Copyright (C) 2020 Igalia, S.L. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "soup-date-utils.h" +#include "soup-date-utils-private.h" + +/** + * soup_date_time_is_past: + * @date: a #GDateTime + * + * Determines if @date is in the past. + * + * Returns: %TRUE if @date is in the past + */ +gboolean +soup_date_time_is_past (GDateTime *date) +{ + g_return_val_if_fail (date != NULL, TRUE); + + /* optimization */ + if (g_date_time_get_year (date) < 2025) + return TRUE; + + return g_date_time_to_unix (date) < time (NULL); +} + +/** + * SoupDateFormat: + * @SOUP_DATE_HTTP: RFC 1123 format, used by the HTTP "Date" header. Eg + * "Sun, 06 Nov 1994 08:49:37 GMT". + * @SOUP_DATE_COOKIE: The format for the "Expires" timestamp in the + * Netscape cookie specification. Eg, "Sun, 06-Nov-1994 08:49:37 GMT". + * + * Date formats that [func@date_time_to_string] can use. + * + * @SOUP_DATE_HTTP and @SOUP_DATE_COOKIE always coerce the time to + * UTC. + * + * This enum may be extended with more values in future releases. + **/ + +/* Do not internationalize */ +static const char *const months[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; + +/* Do not internationalize */ +static const char *const days[] = { + "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" +}; + +/** + * soup_date_time_to_string: + * @date: a #GDateTime + * @format: the format to generate the date in + * + * Converts @date to a string in the format described by @format. + * + * Returns: (transfer full): @date as a string or %NULL + **/ +char * +soup_date_time_to_string (GDateTime *date, + SoupDateFormat format) +{ + g_return_val_if_fail (date != NULL, NULL); + + if (format == SOUP_DATE_HTTP || format == SOUP_DATE_COOKIE) { + /* HTTP and COOKIE formats require UTC timestamp, so coerce + * @date if it's non-UTC. + */ + GDateTime *utcdate = g_date_time_to_utc (date); + char *date_format; + char *formatted_date; + + // We insert days/months ourselves to avoid locale specific formatting + if (format == SOUP_DATE_HTTP) { + /* "Sun, 06 Nov 1994 08:49:37 GMT" */ + date_format = g_strdup_printf ("%s, %%d %s %%Y %%T GMT", + days[g_date_time_get_day_of_week (utcdate) - 1], + months[g_date_time_get_month (utcdate) - 1]); + } else { + /* "Sun, 06-Nov-1994 08:49:37 GMT" */ + date_format = g_strdup_printf ("%s, %%d-%s-%%Y %%T GMT", + days[g_date_time_get_day_of_week (utcdate) - 1], + months[g_date_time_get_month (utcdate) - 1]); + } + + formatted_date = g_date_time_format (utcdate, (const char*)date_format); + g_date_time_unref (utcdate); + g_free (date_format); + return formatted_date; + } + + g_return_val_if_reached (NULL); +} + +static inline gboolean +parse_day (int *day, const char **date_string) +{ + char *end; + + *day = strtoul (*date_string, &end, 10); + if (end == (char *)*date_string) + return FALSE; + + while (*end == ' ' || *end == '-') + end++; + *date_string = end; + return *day >= 1 && *day <= 31; +} + +static inline gboolean +parse_month (int *month, const char **date_string) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (months); i++) { + if (!g_ascii_strncasecmp (*date_string, months[i], 3)) { + *month = i + 1; + *date_string += 3; + while (**date_string == ' ' || **date_string == '-') + (*date_string)++; + return TRUE; + } + } + return FALSE; +} + +static inline gboolean +parse_year (int *year, const char **date_string) +{ + char *end; + + *year = strtoul (*date_string, &end, 10); + if (end == (char *)*date_string) + return FALSE; + + if (end == (char *)*date_string + 2) { + if (*year < 70) + *year += 2000; + else + *year += 1900; + } else if (end == (char *)*date_string + 3) + *year += 1900; + + while (*end == ' ' || *end == '-') + end++; + *date_string = end; + return *year > 0 && *year < 9999; +} + +static inline gboolean +parse_time (int *hour, int *minute, int *second, const char **date_string) +{ + char *p, *end; + + *hour = strtoul (*date_string, &end, 10); + if (end == (char *)*date_string || *end++ != ':') + return FALSE; + p = end; + *minute = strtoul (p, &end, 10); + if (end == p || *end++ != ':') + return FALSE; + p = end; + *second = strtoul (p, &end, 10); + if (end == p) + return FALSE; + p = end; + + while (*p == ' ') + p++; + *date_string = p; + return *hour >= 0 && *hour < 24 && *minute >= 0 && *minute < 60 && *second >= 0 && *second < 60; +} + +static inline gboolean +parse_timezone (GTimeZone **timezone, const char **date_string) +{ + gint32 offset_minutes; + gboolean utc; + + if (!**date_string) { + utc = FALSE; + offset_minutes = 0; + } else if (**date_string == '+' || **date_string == '-') { + gulong val; + int sign = (**date_string == '+') ? 1 : -1; + val = strtoul (*date_string + 1, (char **)date_string, 10); + if (val > 9999) + return FALSE; + if (**date_string == ':') { + gulong val2 = strtoul (*date_string + 1, (char **)date_string, 10); + if (val > 99 || val2 > 99) + return FALSE; + val = 60 * val + val2; + } else + val = 60 * (val / 100) + (val % 100); + offset_minutes = sign * val; + utc = (sign == -1) && !val; + } else if (**date_string == 'Z') { + offset_minutes = 0; + utc = TRUE; + (*date_string)++; + } else if (!strcmp (*date_string, "GMT") || + !strcmp (*date_string, "UTC")) { + offset_minutes = 0; + utc = TRUE; + (*date_string) += 3; + } else if (strchr ("ECMP", **date_string) && + ((*date_string)[1] == 'D' || (*date_string)[1] == 'S') && + (*date_string)[2] == 'T') { + offset_minutes = -60 * (5 * strcspn ("ECMP", *date_string)); + if ((*date_string)[1] == 'D') + offset_minutes += 60; + utc = FALSE; + } else + return FALSE; + + if (utc) + *timezone = g_time_zone_new_utc (); + else + *timezone = g_time_zone_new_offset (offset_minutes * 60); + return TRUE; +} + +static GDateTime * +parse_textual_date (const char *date_string) +{ + int month, day, year, hour, minute, second; + GTimeZone *tz = NULL; + GDateTime *date; + + /* If it starts with a word, it must be a weekday, which we skip */ + if (g_ascii_isalpha (*date_string)) { + while (g_ascii_isalpha (*date_string)) + date_string++; + if (*date_string == ',') + date_string++; + while (g_ascii_isspace (*date_string)) + date_string++; + } + + /* If there's now another word, this must be an asctime-date */ + if (g_ascii_isalpha (*date_string)) { + /* (Sun) Nov 6 08:49:37 1994 */ + if (!parse_month (&month, &date_string) || + !parse_day (&day, &date_string) || + !parse_time (&hour, &minute, &second, &date_string) || + !parse_year (&year, &date_string) || + !g_date_valid_dmy (day, month, year)) + return NULL; + + /* There shouldn't be a timezone, but check anyway */ + parse_timezone (&tz, &date_string); + } else { + /* Non-asctime date, so some variation of + * (Sun,) 06 Nov 1994 08:49:37 GMT + */ + if (!parse_day (&day, &date_string) || + !parse_month (&month, &date_string) || + !parse_year (&year, &date_string) || + !parse_time (&hour, &minute, &second, &date_string) || + !g_date_valid_dmy (day, month, year)) + return NULL; + + /* This time there *should* be a timezone, but we + * survive if there isn't. + */ + parse_timezone (&tz, &date_string); + } + + if (!tz) + tz = g_time_zone_new_utc (); + + date = g_date_time_new (tz, year, month, day, hour, minute, second); + g_time_zone_unref (tz); + + return date; +} + +/** + * soup_date_time_new_from_http_string: + * @date_string: The date as a string + * + * Parses @date_string and tries to extract a date from it. + * + * This recognizes all of the "HTTP-date" formats from RFC 2616, RFC 2822 dates, + * and reasonable approximations thereof. (Eg, it is lenient about whitespace, + * leading "0"s, etc.) + * + * Returns: (nullable): a new #GDateTime, or %NULL if @date_string + * could not be parsed. + **/ +GDateTime * +soup_date_time_new_from_http_string (const char *date_string) +{ + g_return_val_if_fail (date_string != NULL, NULL); + + while (g_ascii_isspace (*date_string)) + date_string++; + + /* If it starts with a digit, it's either an ISO 8601 date, or + * an RFC2822 date without the optional weekday; in the later + * case, there will be a month name later on, so look for one + * of the month-start letters. + * Previous versions of this library supported parsing iso8601 strings + * however g_date_time_new_from_iso8601() should be used now. Just + * catch those in case for testing. + */ + if (G_UNLIKELY (g_ascii_isdigit (*date_string) && !strpbrk (date_string, "JFMASOND"))) { + g_debug ("Unsupported format passed to soup_date_time_new_from_http_string(): %s", date_string); + return NULL; + } + + return parse_textual_date (date_string); +} diff --git a/tests/cookies-test.c b/tests/cookies-test.c index 61adfac5..aa719cdc 100644 --- a/tests/cookies-test.c +++ b/tests/cookies-test.c @@ -292,6 +292,7 @@ static void do_cookies_parsing_int32_overflow (void) { SoupCookie *cookie = soup_cookie_parse ("Age=1;expires=3Mar9 999:9:9+ 999999999-age=main=gne=", NULL); + g_test_bug ("https://gitlab.gnome.org/GNOME/libsoup/-/issues/448"); g_assert_nonnull (cookie); g_assert_null (soup_cookie_get_expires (cookie)); soup_cookie_free (cookie); -- 2.51.0