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()