From 47aa5eb9d1fdbf816b76788e5379639c43e19836 Mon Sep 17 00:00:00 2001 From: Shelby Hagman Date: Thu, 18 Sep 2025 19:17:16 +0000 Subject: [PATCH] fix: properly handle IPv6 addresses in HTTP Host headers This commit fixes IPv6 address handling in HTTP client Host headers by adding bracket notation when required and improving URL parsing validation. Changes: - Add automatic bracket wrapping for unbracketed IPv6 addresses in Host headers for both standard and non-standard ports - Add IPv6 bracketing for HTTPS default port (443) to ensure RFC compliance even when port is omitted (e.g., Host: [::1]) - Fix off-by-one error in IPv6 bracket stripping (was removing one extra character) - Fix incorrect length calculation in flb_utils_copy_host_sds for bracketed IPv6 extraction (changed from absolute position to relative length to properly account for pos_init offset) - Strip IPv6 zone IDs (e.g., %eth0) from Host headers per RFC 3986 which prohibits zone IDs in URIs (e.g., fe80::1%eth0 becomes [fe80::1]:8080 in Host header) - Perform zone ID stripping before inet_pton() validation to ensure proper IPv6 address detection for link-local addresses - Add URI path prepending for URLs with query/fragment but no path (e.g., http://example.com?query=1 becomes /?query=1) per RFC 7230 - Constrain IPv6 bracket validation to host portion only, preventing false negatives when brackets appear in URL paths or query strings - Update validate_ipv6_brackets() to recognize '?' and '#' as host delimiters in addition to '/' - Refactor URL parsing logic to eliminate duplication - Use memchr with length limit for consistent and safe bracket detection in both IPv6 and non-IPv6 cases - Improve error handling in URL parsing with proper cleanup on failure - Update TLS flag checking to use flb_stream_get_flag_status() for more reliable detection Tests: - Add test for IPv6 with HTTPS on default port 443 - Add test cases for IPv6 addresses with zone IDs (verifying zone ID stripping behavior) - Add test cases for brackets in URL paths and query strings - Add test cases for malformed bracket scenarios Signed-off-by: Shelby Hagman --- src/flb_http_client.c | 56 +++++++- src/flb_network.c | 1 + src/flb_utils.c | 243 +++++++++++++++++++++++++++++------ tests/internal/http_client.c | 187 +++++++++++++++++++++++++++ tests/internal/utils.c | 43 +++++++ 5 files changed, 489 insertions(+), 41 deletions(-) diff --git a/src/flb_http_client.c b/src/flb_http_client.c index 810e172fe83..e1bd048a7ec 100644 --- a/src/flb_http_client.c +++ b/src/flb_http_client.c @@ -33,7 +33,13 @@ #define _GNU_SOURCE #include +#ifdef FLB_SYSTEM_WINDOWS +#include +#include +#endif + #include +#include #include #include #include @@ -617,11 +623,55 @@ static int add_host_and_content_length(struct flb_http_client *c) out_port = c->port; } - if (c->flags & FLB_IO_TLS && out_port == 443) { - tmp = flb_sds_copy(host, out_host, strlen(out_host)); + /* Check if out_host is an unbracketed IPv6 address */ + struct in6_addr addr; + char *zone_id; + char addr_buf[INET6_ADDRSTRLEN]; + int is_ipv6 = 0; + int is_https_default_port; + const char *host_for_header; + + if (out_host && out_host[0] != '[') { + /* Strip zone ID if present (e.g., fe80::1%eth0 -> fe80::1) */ + zone_id = strchr(out_host, '%'); + if (zone_id) { + len = zone_id - out_host; + if (len < INET6_ADDRSTRLEN) { + memcpy(addr_buf, out_host, len); + addr_buf[len] = '\0'; + is_ipv6 = (inet_pton(AF_INET6, addr_buf, &addr) == 1); + } + } + else { + is_ipv6 = (inet_pton(AF_INET6, out_host, &addr) == 1); + } + } + + /* Use stripped address (without zone ID) for Host header if zone ID was present */ + host_for_header = (is_ipv6 && zone_id) ? addr_buf : out_host; + + /* Check if connection uses TLS and port is 443 (HTTPS default) */ + is_https_default_port = flb_stream_get_flag_status(&u->base, FLB_IO_TLS) && out_port == 443; + + if (is_https_default_port) { + if (is_ipv6) { + /* IPv6 address needs brackets for RFC compliance */ + tmp = flb_sds_printf(&host, "[%s]", host_for_header); + } + else { + /* HTTPS on default port 443 - omit port from Host header */ + tmp = flb_sds_copy(host, out_host, strlen(out_host)); + } } else { - tmp = flb_sds_printf(&host, "%s:%i", out_host, out_port); + if (is_ipv6) { + /* IPv6 address needs brackets when combined with port */ + tmp = flb_sds_printf(&host, "[%s]:%i", host_for_header, out_port); + } + else { + /* IPv4 address, domain name, or already bracketed IPv6 */ + tmp = flb_sds_printf(&host, "%s:%i", out_host, out_port); + } } if (!tmp) { diff --git a/src/flb_network.c b/src/flb_network.c index 5d6937ca729..c5777d4fe25 100644 --- a/src/flb_network.c +++ b/src/flb_network.c @@ -30,6 +30,7 @@ #ifdef FLB_SYSTEM_WINDOWS #define poll WSAPoll #include +#include #else #include #endif diff --git a/src/flb_utils.c b/src/flb_utils.c index e816651fb4d..c160d293548 100644 --- a/src/flb_utils.c +++ b/src/flb_utils.c @@ -1431,13 +1431,103 @@ static char *flb_utils_copy_host_sds(const char *string, int pos_init, int pos_e if (string[pos_end-1] != ']') { return NULL; } - return flb_sds_create_len(string + pos_init + 1, pos_end - 1); + return flb_sds_create_len(string + pos_init + 1, pos_end - pos_init - 2); } else { - return flb_sds_create_len(string + pos_init, pos_end); + return flb_sds_create_len(string + pos_init, pos_end - pos_init); } } +/* Validate IPv6 bracket syntax in URL host part */ +static int validate_ipv6_brackets(const char *p, const char **out_bracket) +{ + const char *host_end; + const char *bracket = NULL; + const char *closing; + const char *query_or_fragment; + + /* Only inspect the host portion (up to the first '/', '?', or '#') */ + host_end = strchr(p, '/'); + query_or_fragment = strpbrk(p, "?#"); + + /* Use the earliest delimiter found */ + if (query_or_fragment && (!host_end || query_or_fragment < host_end)) { + host_end = query_or_fragment; + } + + if (!host_end) { + host_end = p + strlen(p); + } + + if (p[0] == '[') { + closing = memchr(p, ']', host_end - p); + if (!closing || closing == p + 1) { + /* Missing closing bracket or empty brackets [] */ + return -1; + } + bracket = closing; + } + else { + /* Non-bracketed hosts must not contain ']' before the first '/' */ + closing = memchr(p, ']', host_end - p); + if (closing) { + return -1; + } + } + + if (out_bracket) { + *out_bracket = bracket; + } + return 0; +} + +/* Helper to create URI with prepended '/' if it starts with '?' or '#' */ +static char *create_uri_with_slash(const char *uri_part) +{ + char *uri; + size_t uri_part_len; + + if (!uri_part || *uri_part == '\0') { + return flb_strdup("/"); + } + + /* If URI starts with '?' or '#', prepend '/' */ + if (*uri_part == '?' || *uri_part == '#') { + uri_part_len = strlen(uri_part); + /* Allocate space for '/' + uri_part + '\0' */ + uri = flb_malloc(uri_part_len + 2); + if (!uri) { + return NULL; + } + uri[0] = '/'; + /* +1 to include '\0' */ + memcpy(uri + 1, uri_part, uri_part_len + 1); + return uri; + } + + /* URI already starts with '/' or is a normal path */ + return flb_strdup(uri_part); +} + +/* SDS version: Helper to create URI with prepended '/' if it starts with '?' or '#' */ +static flb_sds_t create_uri_with_slash_sds(const char *uri_part) +{ + char *result; + flb_sds_t uri; + + /* Use the regular version to create the string */ + result = create_uri_with_slash(uri_part); + if (!result) { + return NULL; + } + + /* Convert to SDS */ + uri = flb_sds_create(result); + flb_free(result); + + return uri; +} + int flb_utils_url_split(const char *in_url, char **out_protocol, char **out_host, char **out_port, char **out_uri) { @@ -1448,6 +1538,7 @@ int flb_utils_url_split(const char *in_url, char **out_protocol, char *p; char *tmp; char *sep; + const char *bracket = NULL; /* Protocol */ p = strstr(in_url, "://"); @@ -1467,17 +1558,34 @@ int flb_utils_url_split(const char *in_url, char **out_protocol, /* Advance position after protocol */ p += 3; - /* Check for first '/' */ + /* Validate IPv6 brackets */ sep = strchr(p, '/'); - tmp = strchr(p, ':'); + if (validate_ipv6_brackets(p, &bracket) < 0) { + flb_errno(); + goto error; + } - /* Validate port separator is found before the first slash */ - if (sep && tmp) { - if (tmp > sep) { - tmp = NULL; - } + /* Compute end of host segment (before '/', '?', or '#') */ + const char *host_end = sep; + const char *qf = strpbrk(p, "?#"); + + if (!host_end || (qf && qf < host_end)) { + host_end = qf; + } + if (!host_end) { + host_end = p + strlen(p); + } + + if (bracket) { + /* For bracketed IPv6, only ports after ']' and before URI delimiters are valid */ + tmp = memchr(bracket, ':', host_end - bracket); + } + else { + /* Non-IPv6: limit ':' search to the host portion */ + tmp = memchr(p, ':', host_end - p); } + /* Extract host if port separator was found */ if (tmp) { host = flb_copy_host(p, 0, tmp - p); if (!host) { @@ -1485,30 +1593,46 @@ int flb_utils_url_split(const char *in_url, char **out_protocol, goto error; } p = tmp + 1; + } - /* Look for an optional URI */ - tmp = strchr(p, '/'); + /* Find URI delimiter (/, ?, or #) */ + tmp = strpbrk(p, "/?#"); + + if (!host) { + /* No port: extract host */ if (tmp) { - port = mk_string_copy_substr(p, 0, tmp - p); - uri = flb_strdup(tmp); + host = flb_copy_host(p, 0, tmp - p); } else { - port = flb_strdup(p); - uri = flb_strdup("/"); + host = flb_copy_host(p, 0, strlen(p)); + } + if (!host) { + flb_errno(); + goto error; } } else { - tmp = strchr(p, '/'); + /* Port exists: extract port */ if (tmp) { - host = flb_copy_host(p, 0, tmp - p); - uri = flb_strdup(tmp); + port = mk_string_copy_substr(p, 0, tmp - p); } else { - host = flb_copy_host(p, 0, strlen(p)); - uri = flb_strdup("/"); + port = flb_strdup(p); } } + /* Extract URI */ + if (tmp) { + uri = create_uri_with_slash(tmp); + if (!uri) { + flb_errno(); + goto error; + } + } + else { + uri = flb_strdup("/"); + } + if (!port) { if (strcmp(protocol, "http") == 0) { port = flb_strdup("80"); @@ -1529,6 +1653,15 @@ int flb_utils_url_split(const char *in_url, char **out_protocol, if (protocol) { flb_free(protocol); } + if (host) { + flb_free(host); + } + if (port) { + flb_free(port); + } + if (uri) { + flb_free(uri); + } return -1; } @@ -1544,6 +1677,7 @@ int flb_utils_url_split_sds(const flb_sds_t in_url, flb_sds_t *out_protocol, char *p = NULL; char *tmp = NULL; char *sep = NULL; + const char *bracket = NULL; /* Protocol */ p = strstr(in_url, "://"); @@ -1563,17 +1697,34 @@ int flb_utils_url_split_sds(const flb_sds_t in_url, flb_sds_t *out_protocol, /* Advance position after protocol */ p += 3; - /* Check for first '/' */ + /* Validate IPv6 brackets */ sep = strchr(p, '/'); - tmp = strchr(p, ':'); + if (validate_ipv6_brackets(p, &bracket) < 0) { + flb_errno(); + goto error; + } - /* Validate port separator is found before the first slash */ - if (sep && tmp) { - if (tmp > sep) { - tmp = NULL; - } + /* Compute end of host segment (before '/', '?', or '#') */ + const char *host_end = sep; + const char *qf = strpbrk(p, "?#"); + + if (!host_end || (qf && qf < host_end)) { + host_end = qf; + } + if (!host_end) { + host_end = p + strlen(p); } + if (bracket) { + /* For bracketed IPv6, only ports after ']' and before URI delimiters are valid */ + tmp = memchr(bracket, ':', host_end - bracket); + } + else { + /* Non-IPv6: limit ':' search to the host portion */ + tmp = memchr(p, ':', host_end - p); + } + + /* Extract host if port separator was found */ if (tmp) { host = flb_utils_copy_host_sds(p, 0, tmp - p); if (!host) { @@ -1581,29 +1732,45 @@ int flb_utils_url_split_sds(const flb_sds_t in_url, flb_sds_t *out_protocol, goto error; } p = tmp + 1; + } - /* Look for an optional URI */ - tmp = strchr(p, '/'); + /* Find URI delimiter (/, ?, or #) */ + tmp = strpbrk(p, "/?#"); + + if (!host) { + /* No port: extract host */ if (tmp) { - port = flb_sds_create_len(p, tmp - p); - uri = flb_sds_create(tmp); + host = flb_utils_copy_host_sds(p, 0, tmp - p); } else { - port = flb_sds_create_len(p, strlen(p)); - uri = flb_sds_create("/"); + host = flb_utils_copy_host_sds(p, 0, strlen(p)); + } + if (!host) { + flb_errno(); + goto error; } } else { - tmp = strchr(p, '/'); + /* Port exists: extract port */ if (tmp) { - host = flb_utils_copy_host_sds(p, 0, tmp - p); - uri = flb_sds_create(tmp); + port = flb_sds_create_len(p, tmp - p); } else { - host = flb_utils_copy_host_sds(p, 0, strlen(p)); - uri = flb_sds_create("/"); + port = flb_sds_create_len(p, strlen(p)); + } + } + + /* Extract URI */ + if (tmp) { + uri = create_uri_with_slash_sds(tmp); + if (!uri) { + flb_errno(); + goto error; } } + else { + uri = flb_sds_create("/"); + } if (!port) { if (strcmp(protocol, "http") == 0) { diff --git a/tests/internal/http_client.c b/tests/internal/http_client.c index 45e77e1b2ce..6e04150c201 100644 --- a/tests/internal/http_client.c +++ b/tests/internal/http_client.c @@ -466,6 +466,178 @@ void test_http_add_proxy_auth_header() test_ctx_destroy(ctx); } +/* Helper function to verify Host header value */ +static void check_host_header(struct flb_http_client *c, const char *expected) +{ + flb_sds_t ret_str = flb_http_get_header(c, "Host", 4); + if (!TEST_CHECK(ret_str != NULL)) { + TEST_MSG("flb_http_get_header failed"); + exit(EXIT_FAILURE); + } + + if (!TEST_CHECK(flb_sds_cmp(ret_str, expected, strlen(expected)) == 0)) { + TEST_MSG("strcmp failed. got=%s expect=%s", ret_str, expected); + } + + flb_sds_destroy(ret_str); +} + +/* Helper to test basic host header formatting */ +static void test_host_header_format(const char *host, int port, const char *expected) +{ + struct test_ctx *ctx = test_ctx_create(); + if (!TEST_CHECK(ctx != NULL)) { + exit(EXIT_FAILURE); + } + + struct flb_http_client *c = flb_http_client(ctx->u_conn, FLB_HTTP_GET, "/", + NULL, 0, host, port, NULL, 0); + if (!TEST_CHECK(c != NULL)) { + TEST_MSG("flb_http_client failed"); + test_ctx_destroy(ctx); + exit(EXIT_FAILURE); + } + + check_host_header(c, expected); + flb_http_client_destroy(c); + test_ctx_destroy(ctx); +} + +/* Helper to test TLS host header formatting */ +static void test_tls_host_header_format(const char *host, int port, const char *expected) +{ + struct test_ctx *ctx = test_ctx_create(); + if (!TEST_CHECK(ctx != NULL)) { + exit(EXIT_FAILURE); + } + + struct flb_upstream *u_tls = flb_upstream_create(ctx->config, host, port, FLB_IO_TLS, NULL); + if (!TEST_CHECK(u_tls != NULL)) { + TEST_MSG("flb_upstream_create failed"); + test_ctx_destroy(ctx); + exit(EXIT_FAILURE); + } + + struct flb_connection *u_conn_tls = flb_calloc(1, sizeof(struct flb_connection)); + if (!TEST_CHECK(u_conn_tls != NULL)) { + TEST_MSG("flb_calloc failed"); + flb_upstream_destroy(u_tls); + test_ctx_destroy(ctx); + exit(EXIT_FAILURE); + } + u_conn_tls->upstream = u_tls; + + struct flb_http_client *c = flb_http_client(u_conn_tls, FLB_HTTP_GET, "/", + NULL, 0, host, port, NULL, 0); + if (!TEST_CHECK(c != NULL)) { + TEST_MSG("flb_http_client failed"); + flb_free(u_conn_tls); + flb_upstream_destroy(u_tls); + test_ctx_destroy(ctx); + exit(EXIT_FAILURE); + } + + check_host_header(c, expected); + flb_http_client_destroy(c); + flb_free(u_conn_tls); + flb_upstream_destroy(u_tls); + test_ctx_destroy(ctx); +} + +void test_http_ipv6_host_header() +{ + test_host_header_format("::1", 8080, "[::1]:8080"); +} + +void test_http_ipv6_bracketed_host_header() +{ + test_host_header_format("[::1]", 8080, "[::1]:8080"); +} + +void test_http_ipv4_host_header() +{ + test_host_header_format("192.168.1.1", 8080, "192.168.1.1:8080"); +} + +void test_http_domain_host_header() +{ + test_host_header_format("example.com", 8080, "example.com:8080"); +} + +void test_https_default_port_host_header() +{ + test_tls_host_header_format("example.com", 443, "example.com"); +} + +/* Test various IPv6 address formats */ +void test_ipv6_formats_host_header() +{ + size_t index; + struct { + const char *input; + const char *expected; + } test_cases[] = { + {"2001:db8::1", "[2001:db8::1]:8080"}, + {"2001:0db8:0000:0000:0000:0000:0000:0001", "[2001:0db8:0000:0000:0000:0000:0000:0001]:8080"}, + {"::ffff:192.0.2.1", "[::ffff:192.0.2.1]:8080"}, + {"fe80::1", "[fe80::1]:8080"}, + {"::1", "[::1]:8080"}, + {"::", "[::]:8080"}, + {NULL, NULL} + }; + + for (index = 0; test_cases[index].input != NULL; index++) { + test_host_header_format(test_cases[index].input, 8080, test_cases[index].expected); + } +} + +void test_http_port_80_host_header() +{ + test_host_header_format("example.com", 80, "example.com:80"); +} + +void test_port_443_without_tls_host_header() +{ + test_host_header_format("example.com", 443, "example.com:443"); +} + +void test_ipv6_zone_id_host_header() +{ + test_host_header_format("fe80::1%eth0", 8080, "[fe80::1]:8080"); +} + +void test_https_non_standard_port_host_header() +{ + test_tls_host_header_format("example.com", 8443, "example.com:8443"); +} + +void test_ipv6_bracketed_zone_id_host_header() +{ + /* Already bracketed input - zone ID detection only works on unbracketed addresses, + * so this passes through as-is. In practice, bracketed input shouldn't have zone IDs. */ + test_host_header_format("[fe80::1%eth0]", 8080, "[fe80::1%eth0]:8080"); +} + +void test_https_ipv6_default_port_host_header() +{ + test_tls_host_header_format("::1", 443, "[::1]"); +} + +void test_https_ipv6_non_standard_port_host_header() +{ + test_tls_host_header_format("::1", 8443, "[::1]:8443"); +} + +void test_https_ipv6_zone_id_default_port_host_header() +{ + test_tls_host_header_format("fe80::1%eth0", 443, "[fe80::1]"); +} + +void test_https_ipv6_zone_id_non_standard_port_host_header() +{ + test_tls_host_header_format("fe80::1%eth0", 8443, "[fe80::1]:8443"); +} + TEST_LIST = { { "http_buffer_increase" , test_http_buffer_increase}, { "add_get_header" , test_http_add_get_header}, @@ -474,5 +646,20 @@ TEST_LIST = { { "encoding_gzip" , test_http_encoding_gzip}, { "add_basic_auth_header" , test_http_add_basic_auth_header}, { "add_proxy_auth_header" , test_http_add_proxy_auth_header}, + { "ipv6_host_header" , test_http_ipv6_host_header}, + { "ipv6_bracketed_host_header", test_http_ipv6_bracketed_host_header}, + { "ipv4_host_header" , test_http_ipv4_host_header}, + { "domain_host_header" , test_http_domain_host_header}, + { "https_default_port_host_header", test_https_default_port_host_header}, + { "ipv6_formats_host_header", test_ipv6_formats_host_header}, + { "http_port_80_host_header", test_http_port_80_host_header}, + { "port_443_without_tls_host_header", test_port_443_without_tls_host_header}, + { "ipv6_zone_id_host_header", test_ipv6_zone_id_host_header}, + { "https_non_standard_port_host_header", test_https_non_standard_port_host_header}, + { "ipv6_bracketed_zone_id_host_header", test_ipv6_bracketed_zone_id_host_header}, + { "https_ipv6_default_port_host_header", test_https_ipv6_default_port_host_header}, + { "https_ipv6_non_standard_port_host_header", test_https_ipv6_non_standard_port_host_header}, + { "https_ipv6_zone_id_default_port_host_header", test_https_ipv6_zone_id_default_port_host_header}, + { "https_ipv6_zone_id_non_standard_port_host_header", test_https_ipv6_zone_id_non_standard_port_host_header}, { 0 } }; diff --git a/tests/internal/utils.c b/tests/internal/utils.c index 82ca93dcb9c..680b093fbe8 100644 --- a/tests/internal/utils.c +++ b/tests/internal/utils.c @@ -36,6 +36,49 @@ struct url_check url_checks[] = { {0, "https://fluentbit.io:1234/", "https", "fluentbit.io", "1234", "/"}, {0, "https://fluentbit.io:1234/v", "https", "fluentbit.io", "1234", "/v"}, {-1, "://", NULL, NULL, NULL, NULL}, + // IPv6 tests + {0, "https://[::1]/something", "https", "::1", "443", "/something"}, + {0, "http://[::1]/something", "http", "::1", "80", "/something"}, + {0, "https://[::1]", "https", "::1", "443", "/"}, + {0, "https://[::1]:1234/something", "https", "::1", "1234", "/something"}, + {0, "http://[::1]:1234", "http", "::1", "1234", "/"}, + {0, "http://[::1]:1234/", "http", "::1", "1234", "/"}, + {0, "http://[::1]:1234/v", "http", "::1", "1234", "/v"}, + {0, "https://[2001:db8::1]", "https", "2001:db8::1", "443", "/"}, + {0, "https://[2001:db8::1]:1234/something", "https", "2001:db8::1", "1234", "/something"}, + {0, "http://[2001:0db8:0000:0000:0000:0000:0000:0001]:1234/something", "http", "2001:0db8:0000:0000:0000:0000:0000:0001", "1234", "/something"}, + {0, "https://[::192.9.5.5]:1234/v", "https", "::192.9.5.5", "1234", "/v"}, + {0, "https://[::1]/path?query=[value]", "https", "::1", "443", "/path?query=[value]"}, + /* Query string with brackets (no path) */ + {0, "https://example.com?query=[1]", "https", "example.com", "443", "/?query=[1]"}, + {0, "http://example.com?query=[value]&other=[2]", "http", "example.com", "80", "/?query=[value]&other=[2]"}, + {0, "https://[::1]?query=[value]", "https", "::1", "443", "/?query=[value]"}, + {0, "https://[2001:db8::1]:8080?query=[1]", "https", "2001:db8::1", "8080", "/?query=[1]"}, + /* Fragment with brackets */ + {0, "https://example.com#fragment=[1]", "https", "example.com", "443", "/#fragment=[1]"}, + {0, "https://[::1]#fragment=[value]", "https", "::1", "443", "/#fragment=[value]"}, + /* Query and fragment with brackets */ + {0, "https://example.com?query=[1]#fragment=[2]", "https", "example.com", "443", "/?query=[1]#fragment=[2]"}, + /* Port with query/fragment (non-IPv6) */ + {0, "https://example.com:8080?query=[1]", "https", "example.com", "8080", "/?query=[1]"}, + {0, "http://example.com:9000#fragment=[1]", "http", "example.com", "9000", "/#fragment=[1]"}, + /* Empty query/fragment */ + {0, "https://example.com?", "https", "example.com", "443", "/?"}, + {0, "https://example.com#", "https", "example.com", "443", "/#"}, + {0, "https://[::1]?", "https", "::1", "443", "/?"}, + /* IPv6 edge cases - malformed brackets */ + {-1, "http://[::1:8080/path", NULL, NULL, NULL, NULL}, /* missing closing bracket */ + {-1, "http://::1]:8080/path", NULL, NULL, NULL, NULL}, /* missing opening bracket in host */ + {-1, "http://[]:8080/path", NULL, NULL, NULL, NULL}, /* empty brackets */ + {-1, "http://host]name.com/path", NULL, NULL, NULL, NULL}, /* closing bracket in hostname without opening */ + {-1, "http://host]name.com?query=1", NULL, NULL, NULL, NULL}, /* closing bracket in hostname without opening (query) */ + /* Colons in query/fragment should not be treated as port separators */ + {0, "https://example.com?q=a:b", "https", "example.com", "443", "/?q=a:b"}, + {0, "https://example.com/path?time=12:30:45", "https", "example.com", "443", "/path?time=12:30:45"}, + {0, "http://example.com#section:subsection", "http", "example.com", "80", "/#section:subsection"}, + {0, "https://example.com?q=a:b#frag:ment", "https", "example.com", "443", "/?q=a:b#frag:ment"}, + {0, "https://[::1]?time=12:30", "https", "::1", "443", "/?time=12:30"}, + {0, "http://example.com:8080?q=a:b:c", "http", "example.com", "8080", "/?q=a:b:c"} }; void test_url_split_sds()