diff --git a/include/aws/common/host_utils.h b/include/aws/common/host_utils.h new file mode 100644 index 000000000..0b8285d7a --- /dev/null +++ b/include/aws/common/host_utils.h @@ -0,0 +1,28 @@ +#ifndef AWS_COMMON_HOST_UTILS_H +#define AWS_COMMON_HOST_UTILS_H +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include + +struct aws_byte_cursor; + +AWS_PUSH_SANE_WARNING_LEVEL +AWS_EXTERN_C_BEGIN + +/* + * Determine whether host cursor is IPv4 string. + */ +AWS_COMMON_API bool aws_host_utils_is_ipv4(struct aws_byte_cursor host); + +/* + * Determine whether host cursor is IPv6 string. + * Supports checking for uri encoded strings and scoped literals. + */ +AWS_COMMON_API bool aws_host_utils_is_ipv6(struct aws_byte_cursor host, bool is_uri_encoded); + +AWS_EXTERN_C_END +AWS_POP_SANE_WARNING_LEVEL + +#endif /* AWS_COMMON_HOST_UTILS_H */ diff --git a/source/host_utils.c b/source/host_utils.c new file mode 100644 index 000000000..6cd26ba88 --- /dev/null +++ b/source/host_utils.c @@ -0,0 +1,127 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ +#include +#include +#include + +#ifdef _MSC_VER /* Disable sscanf warnings on windows. */ +# pragma warning(disable : 4204) +# pragma warning(disable : 4706) +# pragma warning(disable : 4996) +#endif + +/* 4 octets of 3 chars max + 3 separators + null terminator */ +#define AWS_IPV4_STR_LEN 16 +#define IP_CHAR_FMT "%03" SCNu16 + +static bool s_is_ipv6_char(uint8_t value) { + return aws_isxdigit(value) || value == ':'; +} + +static bool s_starts_with(struct aws_byte_cursor cur, uint8_t ch) { + return cur.len > 0 && cur.ptr[0] == ch; +} + +static bool s_ends_with(struct aws_byte_cursor cur, uint8_t ch) { + return cur.len > 0 && cur.ptr[cur.len - 1] == ch; +} + +bool aws_host_utils_is_ipv4(struct aws_byte_cursor host) { + if (host.len > AWS_IPV4_STR_LEN - 1) { + return false; + } + + char copy[AWS_IPV4_STR_LEN] = {0}; + memcpy(copy, host.ptr, host.len); + + uint16_t octet[4] = {0}; + char remainder[2] = {0}; + if (4 != sscanf( + copy, + IP_CHAR_FMT "." IP_CHAR_FMT "." IP_CHAR_FMT "." IP_CHAR_FMT "%1s", + &octet[0], + &octet[1], + &octet[2], + &octet[3], + remainder)) { + return false; + } + + for (size_t i = 0; i < 4; ++i) { + if (octet[i] > 255) { + return false; + } + } + + return true; +} + +/* actual encoding is %25, but % is omitted for simplicity, since split removes it */ +static struct aws_byte_cursor s_percent_uri_enc = AWS_BYTE_CUR_INIT_FROM_STRING_LITERAL("25"); +/* + * IPv6 format: + * 8 groups of 4 hex chars separated by colons (:) + * leading 0s in each group can be skipped + * 2 or more consecutive zero groups can be replaced by double colon (::), + * but only once. + * ipv6 literal can be scoped by to zone by appending % followed by zone name + * ( does not look like there is length reqs on zone name length. this + * implementation enforces that its > 1 ) + * ipv6 can be embedded in url, in which case it must be wrapped inside [] + * and % be uri encoded as %25. + * Implementation is fairly trivial and just iterates through the string + * keeping track of the spec above. + */ +bool aws_host_utils_is_ipv6(struct aws_byte_cursor host, bool is_uri_encoded) { + if (host.len == 0) { + return false; + } + + if (is_uri_encoded) { + if (!s_starts_with(host, '[') || !s_ends_with(host, ']')) { + return false; + } + aws_byte_cursor_advance(&host, 1); + --host.len; + } + + struct aws_byte_cursor substr = {0}; + /* first split is required ipv6 part */ + bool is_split = aws_byte_cursor_next_split(&host, '%', &substr); + AWS_ASSERT(is_split); /* function is guaranteed to return at least one split */ + + if (!is_split || substr.len == 0 || s_ends_with(substr, ':') || + !aws_byte_cursor_satisfies_pred(&substr, s_is_ipv6_char)) { + return false; + } + + uint8_t group_count = 0; + bool has_double_colon = false; + struct aws_byte_cursor group = {0}; + while (aws_byte_cursor_next_split(&substr, ':', &group)) { + ++group_count; + + if (group_count > 8 || /* too many groups */ + group.len > 4 || /* too many chars in group */ + (has_double_colon && group.len == 0 && group_count > 2)) { /* only one double colon allowed */ + return false; + } + + has_double_colon = has_double_colon || group.len == 0; + } + + /* second split is optional zone part */ + if (aws_byte_cursor_next_split(&host, '%', &substr)) { + if ((is_uri_encoded && + (substr.len < 3 || + !aws_byte_cursor_starts_with(&substr, &s_percent_uri_enc))) || /* encoding for % + 1 extra char */ + (!is_uri_encoded && substr.len == 0) || /* at least 1 char */ + !aws_byte_cursor_satisfies_pred(&substr, aws_isalnum)) { + return false; + } + } + + return has_double_colon ? group_count < 7 : group_count == 8; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 35be1133d..6f224e4f2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -528,6 +528,8 @@ add_test_case(test_cross_process_lock_works_cross_proc) add_test_case(cross_process_lock_mp_test_runner) add_test_case(test_cross_process_lock_invalid_nonce_fails) +add_test_case(host_util_is_ipv4) +add_test_case(host_util_is_ipv6) generate_test_driver(${PROJECT_NAME}-tests) diff --git a/tests/host_util_test.c b/tests/host_util_test.c new file mode 100644 index 000000000..bf0192610 --- /dev/null +++ b/tests/host_util_test.c @@ -0,0 +1,74 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0. + */ + +#include +#include +#include + +AWS_TEST_CASE(host_util_is_ipv4, s_test_is_ipv4) +static int s_test_is_ipv4(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + + ASSERT_TRUE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("0.0.0.0"))); + ASSERT_TRUE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("127.0.0.1"))); + ASSERT_TRUE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("255.255.255.255"))); + ASSERT_TRUE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("192.168.1.1"))); + + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("256.0.0.1"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("127.0.0"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("127.0"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("127"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str(""))); + + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("foo.com"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("a.b.c.d"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("a127.0.0.1"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("127.0.0.1a"))); + ASSERT_FALSE(aws_host_utils_is_ipv4(aws_byte_cursor_from_c_str("127.0.0.1011"))); + + return AWS_OP_SUCCESS; +} + +AWS_TEST_CASE(host_util_is_ipv6, s_test_is_ipv6) +static int s_test_is_ipv6(struct aws_allocator *allocator, void *ctx) { + (void)allocator; + (void)ctx; + + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("0:0:0000:0000:0000:0:0:0"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("2001:0db8:0000:0000:0000:8a2e:0370:7334"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("2001:0DB8:0000:0000:0000:8a2e:0370:7334"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("fe80::1"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("fe80::1%en0"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("::1"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("0:0:0:0:0:0:0:1"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("fd00:ec2::23"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("fd00:ec2:0:0:0:0:0:23"), false)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[2001:0db8:0000:0000:0000:8a2e:0370:7334]"), true)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[fe80::1]"), true)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[fe80::1%25en0]"), true)); + ASSERT_TRUE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[2001:db8:85a3:8d3:1319:8a2e:370:7348]"), true)); + + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("2001:0db8:0000:0000:0000:8a2e:0370"), false)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("2001:0db8:0000:0000:0000:8a2e:0370:"), false)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("2001::"), false)); + ASSERT_FALSE( + aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("2001:0db8:0000:0000:0000:8a2e:0370:7334:8745"), false)); + ASSERT_FALSE( + aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str(":2001:0db8:0000:0000:0000:8a2e:0370:7334:8745"), false)); + ASSERT_FALSE( + aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("z001:0db8:0000:0000:0000:8a2e:0370:7334:8745"), false)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("z001::8a2e::8745"), false)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("::2001:0db8:0000:0000:8a2e:0370:7334"), false)); + + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("fe80::1%25en0"), true)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[fe80::1%en0]"), true)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[fe80::1%24en0]"), true)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[fe80::1%25en0"), true)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("fe80::1%25en0]"), true)); + ASSERT_FALSE(aws_host_utils_is_ipv6(aws_byte_cursor_from_c_str("[fe80::1%25]"), true)); + + return AWS_OP_SUCCESS; +}