From: Nigel Brittain Date: Sun, 27 Apr 2025 00:22:23 +0000 (+0000) Subject: http_aws_sigv4: improve sigv4 url encoding and canonicalization X-Git-Tag: curl-8_14_0~41 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c19465ca556d2d05a058b4690c27eb228d05f2e6;p=thirdparty%2Fcurl.git http_aws_sigv4: improve sigv4 url encoding and canonicalization Closes #17129 --- diff --git a/docs/KNOWN_BUGS b/docs/KNOWN_BUGS index 191d47a979..7ed5ee39db 100644 --- a/docs/KNOWN_BUGS +++ b/docs/KNOWN_BUGS @@ -99,8 +99,6 @@ problems may have been fixed or changed somewhat since this was written. 16. aws-sigv4 16.2 aws-sigv4 does not handle multipart/form-data correctly - 16.3 aws-sigv4 has problems with particular URLs - 16.6 aws-sigv4 does not behave well with AWS VPC Lattice 17. HTTP/2 17.1 HTTP/2 prior knowledge over proxy @@ -607,14 +605,6 @@ problems may have been fixed or changed somewhat since this was written. https://github.com/curl/curl/issues/13351 -16.3 aws-sigv4 has problems with particular URLs - - https://github.com/curl/curl/issues/13058 - -16.6 aws-sigv4 does not behave well with AWS VPC Lattice - - https://github.com/curl/curl/issues/11007 - 17. HTTP/2 17.1 HTTP/2 prior knowledge over proxy diff --git a/lib/http_aws_sigv4.c b/lib/http_aws_sigv4.c index 1b8354fdff..4d09013851 100644 --- a/lib/http_aws_sigv4.c +++ b/lib/http_aws_sigv4.c @@ -63,6 +63,26 @@ /* hex-encoded with trailing null */ #define SHA256_HEX_LENGTH (2 * CURL_SHA256_DIGEST_LENGTH + 1) +#define MAX_QUERY_COMPONENTS 128 + +struct pair { + struct dynbuf key; + struct dynbuf value; +}; + +static void dyn_array_free(struct dynbuf *db, size_t num_elements); +static void pair_array_free(struct pair *pair_array, size_t num_elements); +static CURLcode split_to_dyn_array(const char *source, char split_by, + struct dynbuf db[MAX_QUERY_COMPONENTS], size_t *num_splits); +static bool is_reserved_char(const char c); +static CURLcode uri_encode_path(struct Curl_str *original_path, + struct dynbuf *new_path); +static CURLcode encode_query_component(char *component, size_t len, + struct dynbuf *db); +static CURLcode http_aws_decode_encode(const char *in, size_t in_len, + struct dynbuf *out); +static bool should_urlencode(struct Curl_str *service_name); + static void sha256_to_hex(char *dst, unsigned char *sha) { Curl_hexencode(sha, CURL_SHA256_DIGEST_LENGTH, @@ -390,8 +410,7 @@ fail: static const char *parse_content_sha_hdr(struct Curl_easy *data, const char *provider1, size_t plen, - size_t *value_len) -{ + size_t *value_len) { char key[CONTENT_SHA256_KEY_LEN]; size_t key_len; const char *value; @@ -478,150 +497,187 @@ fail: return ret; } -struct pair { - const char *p; - size_t len; -}; - static int compare_func(const void *a, const void *b) { + const struct pair *aa = a; const struct pair *bb = b; + const size_t aa_key_len = curlx_dyn_len(&aa->key); + const size_t bb_key_len = curlx_dyn_len(&bb->key); + const size_t aa_value_len = curlx_dyn_len(&aa->value); + const size_t bb_value_len = curlx_dyn_len(&bb->value); + int compare; + /* If one element is empty, the other is always sorted higher */ - if(aa->len == 0 && bb->len == 0) + + /* Compare keys */ + if((aa_key_len == 0) && (bb_key_len == 0)) return 0; - if(aa->len == 0) + if(aa_key_len == 0) return -1; - if(bb->len == 0) + if(bb_key_len == 0) return 1; - return strncmp(aa->p, bb->p, aa->len < bb->len ? aa->len : bb->len); -} + compare = strcmp(curlx_dyn_ptr(&aa->key), curlx_dyn_ptr(&bb->key)); + if(compare) { + return compare; + } -#define MAX_QUERYPAIRS 64 + /* Compare values */ + if((aa_value_len == 0) && (bb_value_len == 0)) + return 0; + if(aa_value_len == 0) + return -1; + if(bb_value_len == 0) + return 1; + compare = strcmp(curlx_dyn_ptr(&aa->value), curlx_dyn_ptr(&bb->value)); + + return compare; -/** - * found_equals have a double meaning, - * detect if an equal have been found when called from canon_query, - * and mark that this function is called to compute the path, - * if found_equals is NULL. - */ -static CURLcode canon_string(const char *q, size_t len, - struct dynbuf *dq, bool *found_equals) +} + +UNITTEST CURLcode canon_path(const char *q, size_t len, + struct dynbuf *new_path, + bool do_uri_encode) { CURLcode result = CURLE_OK; - for(; len && !result; q++, len--) { - if(ISALNUM(*q)) - result = curlx_dyn_addn(dq, q, 1); - else { - switch(*q) { - case '-': - case '.': - case '_': - case '~': - /* allowed as-is */ - result = curlx_dyn_addn(dq, q, 1); - break; - case '%': - /* uppercase the following if hexadecimal */ - if(ISXDIGIT(q[1]) && ISXDIGIT(q[2])) { - char tmp[3]="%"; - tmp[1] = Curl_raw_toupper(q[1]); - tmp[2] = Curl_raw_toupper(q[2]); - result = curlx_dyn_addn(dq, tmp, 3); - q += 2; - len -= 2; - } - else - /* '%' without a following two-digit hex, encode it */ - result = curlx_dyn_addn(dq, "%25", 3); - break; - default: { - unsigned char out[3]={'%'}; - - if(!found_equals) { - /* if found_equals is NULL assuming, been in path */ - if(*q == '/') { - /* allowed as if */ - result = curlx_dyn_addn(dq, q, 1); - break; - } - } - else { - /* allowed as-is */ - if(*q == '=') { - result = curlx_dyn_addn(dq, q, 1); - *found_equals = TRUE; - break; - } - } - /* URL encode */ - Curl_hexbyte(&out[1], *q, FALSE); - result = curlx_dyn_addn(dq, out, 3); - break; - } - } + struct Curl_str original_path; + + curlx_str_assign(&original_path, q, len); + + /* Normalized path will be either the same or shorter than the original + * path, plus trailing slash */ + + if(do_uri_encode) { + result = uri_encode_path(&original_path, new_path); + if(result) { + goto fail; } } + else { + result = curlx_dyn_addn(new_path, q, len); + if(result) { + goto fail; + } + } + + if(curlx_dyn_len(new_path) == 0) { + result = curlx_dyn_add(new_path, "/"); + } +fail: return result; } - -static CURLcode canon_query(struct Curl_easy *data, - const char *query, struct dynbuf *dq) +UNITTEST CURLcode canon_query(const char *query, struct dynbuf *dq) { CURLcode result = CURLE_OK; - int entry = 0; - int i; - const char *p = query; - struct pair array[MAX_QUERYPAIRS]; - struct pair *ap = &array[0]; + + struct dynbuf query_array[MAX_QUERY_COMPONENTS]; + struct pair encoded_query_array[MAX_QUERY_COMPONENTS]; + size_t num_query_components; + size_t counted_query_components = 0; + size_t index; + size_t in_key_len; + size_t in_value_len; + size_t query_part_len; + const char *in_key; + char *in_value; + char *offset; + char *key_ptr; + char *value_ptr; + const char *query_part; + if(!query) return result; - /* sort the name=value pairs first */ - do { - char *amp; - entry++; - ap->p = p; - amp = strchr(p, '&'); - if(amp) - ap->len = amp - p; /* excluding the ampersand */ + result = split_to_dyn_array(query, '&', &query_array[0], + &num_query_components); + if(result) { + goto fail; + } + + /* Create list of pairs, each pair containing an encoded query + * component */ + + for(index = 0; index < num_query_components; + index++) { + + query_part_len = curlx_dyn_len(&query_array[index]); + query_part = curlx_dyn_ptr(&query_array[index]); + + in_key = query_part; + + offset = strchr(query_part, '='); + /* If there is no equals, this key has no value */ + if(!offset) { + in_key_len = strlen(in_key); + } else { - ap->len = strlen(p); - break; + in_key_len = offset - in_key; + } + + curlx_dyn_init(&encoded_query_array[index].key, query_part_len*3 + 1); + curlx_dyn_init(&encoded_query_array[index].value, query_part_len*3 + 1); + counted_query_components++; + + /* Decode/encode the key */ + result = http_aws_decode_encode(in_key, in_key_len, + &encoded_query_array[index].key); + if(result) { + goto fail; + } + + /* Decode/encode the value if it exists */ + if(offset && offset != (query_part + query_part_len - 1)) { + in_value = offset + 1; + in_value_len = query_part + query_part_len - (offset + 1); + result = http_aws_decode_encode(in_value, in_value_len, + &encoded_query_array[index].value); + if(result) { + goto fail; + } + } + else { + /* If there is no value, the value is an empty string */ + curlx_dyn_init(&encoded_query_array[index].value, 2); + result = curlx_dyn_addn(&encoded_query_array[index].value, "", 1); + } + + if(result) { + goto fail; } - ap++; - p = amp + 1; - } while(entry < MAX_QUERYPAIRS); - if(entry == MAX_QUERYPAIRS) { - /* too many query pairs for us */ - failf(data, "aws-sigv4: too many query pairs in URL"); - return CURLE_URL_MALFORMAT; } - qsort(&array[0], entry, sizeof(struct pair), compare_func); + /* Sort the encoded query components by key and value */ + qsort(&encoded_query_array, num_query_components, + sizeof(struct pair), compare_func); - ap = &array[0]; - for(i = 0; !result && (i < entry); i++, ap++) { - const char *q = ap->p; - bool found_equals = FALSE; - if(!ap->len) - continue; - result = canon_string(q, ap->len, dq, &found_equals); - if(!result && !found_equals) { - /* queries without value still need an equals */ - result = curlx_dyn_addn(dq, "=", 1); + /* Append the query components together to make a full query string */ + for(index = 0; index < num_query_components; index++) { + + key_ptr = curlx_dyn_ptr(&encoded_query_array[index].key); + value_ptr = curlx_dyn_ptr(&encoded_query_array[index].value); + + if(value_ptr && strlen(value_ptr)) { + result = curlx_dyn_addf(dq, "%s=%s&", key_ptr, value_ptr); } - if(!result && i < entry - 1) { - /* insert ampersands between query pairs */ - result = curlx_dyn_addn(dq, "&", 1); + else { + /* Empty value is always encoded to key= */ + result = curlx_dyn_addf(dq, "%s=&", key_ptr); + } + if(result) { + goto fail; } } + /* Remove trailing & */ + result = curlx_dyn_setlen(dq, curlx_dyn_len(dq)-1); + +fail: + pair_array_free(&encoded_query_array[0], counted_query_components); + dyn_array_free(&query_array[0], num_query_components); return result; } - CURLcode Curl_output_aws_sigv4(struct Curl_easy *data) { CURLcode result = CURLE_OUT_OF_MEMORY; @@ -658,6 +714,11 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data) unsigned char sign1[CURL_SHA256_DIGEST_LENGTH] = {0}; char *auth_headers = NULL; + if(data->set.path_as_is) { + failf(data, "Cannot use sigv4 authentication with path-as-is flag"); + return CURLE_BAD_FUNCTION_ARGUMENT; + } + if(Curl_checkheaders(data, STRCONST("Authorization"))) { /* Authorization already present, Bailing out */ return CURLE_OK; @@ -787,12 +848,13 @@ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data) memcpy(date, timestamp, sizeof(date)); date[sizeof(date) - 1] = 0; - result = canon_query(data, data->state.up.query, &canonical_query); + result = canon_query(data->state.up.query, &canonical_query); if(result) goto fail; - result = canon_string(data->state.up.path, strlen(data->state.up.path), - &canonical_path, NULL); + result = canon_path(data->state.up.path, strlen(data->state.up.path), + &canonical_path, + should_urlencode(&service)); if(result) goto fail; result = CURLE_OUT_OF_MEMORY; @@ -925,4 +987,200 @@ fail: return result; } +/* +* Frees all allocated strings in a dynbuf pair array, and the dynbuf itself +*/ + +static void pair_array_free(struct pair *pair_array, size_t num_elements) +{ + size_t index; + + for(index = 0; index != num_elements; index++) { + curlx_dyn_free(&pair_array[index].key); + curlx_dyn_free(&pair_array[index].value); + } + +} + +/* +* Frees all allocated strings in a split dynbuf, and the dynbuf itself +*/ + +static void dyn_array_free(struct dynbuf *db, size_t num_elements) +{ + size_t index; + + for(index = 0; index < num_elements; index++) { + curlx_dyn_free((&db[index])); + } + +} + +/* +* Splits source string by split_by, and creates an array of dynbuf in db +* db is initialized by this function +* Caller is responsible for freeing the array elements with dyn_array_free +*/ + +static CURLcode split_to_dyn_array(const char *source, char split_by, + struct dynbuf db[MAX_QUERY_COMPONENTS], size_t *num_splits_out) +{ + + CURLcode result = CURLE_OK; + + size_t len = strlen(source); + + size_t pos = 0; /* Position in result buffer */ + size_t start = 0; /* Start of current segment */ + size_t segment_length = 0; + size_t index = 0; + size_t num_splits; + + /* Split source_ptr on split_by and store the segment offsets and + * length in array */ + num_splits = 0; + for(pos = 0; pos < len; pos++) { + if(source[pos] == split_by) { + if(segment_length) { + curlx_dyn_init(&db[index], segment_length + 1); + result = curlx_dyn_addn(&db[index], &source[start], + segment_length); + if(result) { + goto fail; + } + segment_length = 0; + index++; + if(++num_splits == MAX_QUERY_COMPONENTS) { + goto fail; + } + } + start = pos + 1; + } + else { + segment_length++; + } + } + + if(segment_length) { + curlx_dyn_init(&db[index], segment_length + 1); + result = curlx_dyn_addn(&db[index], &source[start], + segment_length); + if(result) { + goto fail; + } + if(++num_splits == MAX_QUERY_COMPONENTS) { + goto fail; + } +} +fail: + *num_splits_out = num_splits; + return result; +} + + +static bool is_reserved_char(const char c) +{ + return (ISALNUM(c) || ISURLPUNTCS(c)); +} + +static CURLcode uri_encode_path(struct Curl_str *original_path, +struct dynbuf *new_path) +{ + + const char *p = curlx_str(original_path); + CURLcode result = CURLE_OK; + size_t index; + + for(index = 0; index < curlx_strlen(original_path); index++) { + /* Do not encode slashes or unreserved chars from RFC 3986 */ + unsigned char c = p[index]; + if(is_reserved_char(c) || c == '/') { + result = curlx_dyn_addn(new_path, &c, 1); + if(result) { + goto fail; + } + } + else { + result = curlx_dyn_addf(new_path, "%%%02X", c); + if(result) { + goto fail; + } + } + } +fail: + return result; +} + + +static CURLcode encode_query_component(char *component, size_t len, + struct dynbuf *db) +{ + + size_t index; + CURLcode result = CURLE_OK; + unsigned char this_char; + + for(index = 0; index < len; index++) { + + this_char = component[index]; + + if(is_reserved_char(this_char)) { + /* Escape unreserved chars from RFC 3986 */ + result = curlx_dyn_addn(db, &this_char, 1); + } + else if(this_char == '+') { + /* Encode '+' as space */ + result = curlx_dyn_add(db, "%20"); + } + else { + result = curlx_dyn_addf(db, "%%%02X", this_char); + } + if(result) { + goto fail; + } + + } +fail: + return result; +} + +/* +* Populates a dynbuf containing url_encode(url_decode(in)) +*/ + +static CURLcode http_aws_decode_encode(const char *in, size_t in_len, +struct dynbuf *out) +{ + CURLcode result = CURLE_OK; + char *out_s; + size_t out_s_len; + + result = Curl_urldecode(in, in_len, &out_s, &out_s_len, REJECT_NADA); + + if(result) { + goto fail; + } + result = encode_query_component(out_s, out_s_len, out); + Curl_safefree(out_s); +fail: + return result; +} + +static bool should_urlencode(struct Curl_str *service_name) +{ + /* + * These services require unmodified (not additionally url encoded) URL + * paths. + * should_urlencode == true is equivalent to should_urlencode_uri_path + * from the AWS SDK. Urls are already normalized by the curl url parser + */ + + if(curlx_str_cmp(service_name, "s3") || + curlx_str_cmp(service_name, "s3-express") || + curlx_str_cmp(service_name, "s3-outposts")) { + return false; + } + return true; +} + #endif /* !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_AWS) */ diff --git a/lib/http_aws_sigv4.h b/lib/http_aws_sigv4.h index 8928f4f717..7d3a3d94f4 100644 --- a/lib/http_aws_sigv4.h +++ b/lib/http_aws_sigv4.h @@ -24,8 +24,18 @@ * ***************************************************************************/ #include "curl_setup.h" +#include "curlx/dynbuf.h" +#include "urldata.h" +#include "curlx/strparse.h" /* this is for creating aws_sigv4 header output */ CURLcode Curl_output_aws_sigv4(struct Curl_easy *data); +#ifdef UNITTESTS +UNITTEST CURLcode canon_path(const char *q, size_t len, + struct dynbuf *new_path, + bool normalize); +UNITTEST CURLcode canon_query(const char *query, struct dynbuf *dq); +#endif + #endif /* HEADER_CURL_HTTP_AWS_SIGV4_H */ diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am index c718b44615..e8f9e12be7 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -240,7 +240,7 @@ test1933 test1934 test1935 test1936 test1937 test1938 test1939 test1940 \ test1941 test1942 test1943 test1944 test1945 test1946 test1947 test1948 \ test1955 test1956 test1957 test1958 test1959 test1960 test1964 \ test1970 test1971 test1972 test1973 test1974 test1975 test1976 test1977 \ -test1978 \ +test1978 test1979 test1980 \ \ test2000 test2001 test2002 test2003 test2004 test2005 \ \ diff --git a/tests/data/test1979 b/tests/data/test1979 new file mode 100644 index 0000000000..d89b097a07 --- /dev/null +++ b/tests/data/test1979 @@ -0,0 +1,22 @@ + + + +unittest +canon_string + + + +# +# Client-side + + +none + + +unittest + + +sigv4 canon_string unit tests + + + diff --git a/tests/data/test1980 b/tests/data/test1980 new file mode 100644 index 0000000000..6e801fe061 --- /dev/null +++ b/tests/data/test1980 @@ -0,0 +1,22 @@ + + + +unittest +canon_query + + + +# +# Client-side + + +none + + +unittest + + +sigv4 canon_query unit tests + + + diff --git a/tests/data/test439 b/tests/data/test439 index 7bf6c6520b..dc1ae43f9e 100644 --- a/tests/data/test439 +++ b/tests/data/test439 @@ -39,7 +39,7 @@ aws aws-sigv4 with query -"http://fake.fake.fake:8000/%TESTNUMBER/?name=me%&noval&aim=b%aad&&&weirdo=*.//-" -u user:secret --aws-sigv4 "aws:amz:us-east-2:es" --connect-to fake.fake.fake:8000:%HOSTIP:%HTTPPORT +"http://fake.fake.fake:8000/%TESTNUMBER/?name=me&noval&aim=b%aad&&&weirdo=*.//-" -u user:secret --aws-sigv4 "aws:amz:us-east-2:es" --connect-to fake.fake.fake:8000:%HOSTIP:%HTTPPORT @@ -47,9 +47,9 @@ aws-sigv4 with query # Verify data after the test has been "shot" -GET /439/?name=me%&noval&aim=b%aad&&&weirdo=*.//- HTTP/1.1 +GET /439/?name=me&noval&aim=b%aad&&&weirdo=*.//- HTTP/1.1 Host: fake.fake.fake:8000 -Authorization: AWS4-HMAC-SHA256 Credential=user/19700101/us-east-2/es/aws4_request, SignedHeaders=host;x-amz-date, Signature=cbbf4a72764e27e396730f5e56cea046d4ce862a2d91db4856fb086b92f49270 +Authorization: AWS4-HMAC-SHA256 Credential=user/19700101/us-east-2/es/aws4_request, SignedHeaders=host;x-amz-date, Signature=9dd8592929306832a6673d10063491391e486e5f50de4647ea7c2c797277e0a6 X-Amz-Date: 19700101T000000Z User-Agent: curl/%VERSION Accept: */* diff --git a/tests/data/test472 b/tests/data/test472 index 88b8fba39d..3b4cf74764 100644 --- a/tests/data/test472 +++ b/tests/data/test472 @@ -50,7 +50,7 @@ aws-sigv4 with query GET /472/a=%e3%81%82 HTTP/1.1 Host: fake.fake.fake:8000 -Authorization: AWS4-HMAC-SHA256 Credential=user/19700101/us-east-2/es/aws4_request, SignedHeaders=host;x-amz-date, Signature=c63315c199922f7ee00141869a250389405d19e205057249fb74726d940b1fc3 +Authorization: AWS4-HMAC-SHA256 Credential=user/19700101/us-east-2/es/aws4_request, SignedHeaders=host;x-amz-date, Signature=d2f4797c813fc51d729ac555a23ac682be908fdbfae2042ba98d214c9298201b X-Amz-Date: 19700101T000000Z User-Agent: curl/%VERSION Accept: */* diff --git a/tests/unit/Makefile.inc b/tests/unit/Makefile.inc index b44b1c5589..fb68911941 100644 --- a/tests/unit/Makefile.inc +++ b/tests/unit/Makefile.inc @@ -41,6 +41,7 @@ UNITPROGS = unit1300 unit1302 unit1303 unit1304 unit1305 unit1307 \ unit1650 unit1651 unit1652 unit1653 unit1654 unit1655 unit1656 unit1657 \ unit1658 \ unit1660 unit1661 unit1663 unit1664 \ + unit1979 unit1980 \ unit2600 unit2601 unit2602 unit2603 unit2604 \ unit3200 \ unit3205 \ @@ -132,6 +133,10 @@ unit1663_SOURCES = unit1663.c $(UNITFILES) unit1664_SOURCES = unit1664.c $(UNITFILES) +unit1979_SOURCES = unit1979.c $(UNITFILES) + +unit1980_SOURCES = unit1980.c $(UNITFILES) + unit2600_SOURCES = unit2600.c $(UNITFILES) unit2601_SOURCES = unit2601.c $(UNITFILES) diff --git a/tests/unit/unit1979.c b/tests/unit/unit1979.c new file mode 100644 index 0000000000..e5f044dd63 --- /dev/null +++ b/tests/unit/unit1979.c @@ -0,0 +1,145 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ +#include "curlcheck.h" + + +#include "http_aws_sigv4.h" +#include "dynbuf.h" + +static CURLcode unit_setup(void) +{ + return CURLE_OK; +} + +static void unit_stop(void) +{ +} + +UNITTEST_START + +#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_AWS) + +struct testcase +{ + const char *testname; + const bool normalize; + const char *url_part; + const char *canonical_url; +}; + +static const struct testcase testcases[] = { + { + "test-equals-encode", + true, + "/a=b", + "/a%3Db", + }, + { + "test-equals-noencode", + false, + "/a=b", + "/a=b", + }, + { + "test-s3-tables", + true, + "/tables/arn%3Aaws%3As3tables%3Aus-east-1%3A022954301426%3Abucket%2Fja" + "soehartablebucket/jasoeharnamespace/jasoehartable/encryption", + "/tables/arn%253Aaws%253As3tables%253Aus-east-1%253A022954301426%253Ab" + "ucket%252Fjasoehartablebucket/jasoeharnamespace/jasoehartable/encrypt" + "ion", + }, + { + "get-vanilla", + true, + "/", + "/", + }, + { + "get-unreserved", + true, + "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + }, + { + "get-slashes-unnormalized", + false, + "//example//", + "//example//", + }, + { + "get-space-normalized", + true, + "/example space/", + "/example%20space/", + }, + { + "get-slash-dot-slash-unnormalized", + false, + "/./", + "/./", + }, + { + "get-slash-unnormalized", + false, + "//", + "//", + }, + { + "get-relative-relative-unnormalized", + false, + "/example1/example2/../..", + "/example1/example2/../..", + } +}; + + struct dynbuf canonical_path; + + char buffer[1024]; + char *canonical_path_string; + size_t i; + int result; + int msnprintf_result; + + for(i = 0; i < CURL_ARRAYSIZE(testcases); i++) { + curlx_dyn_init(&canonical_path, CURL_MAX_HTTP_HEADER); + + result = canon_path(testcases[i].url_part, strlen(testcases[i].url_part), + &canonical_path, + testcases[i].normalize); + canonical_path_string = curlx_dyn_ptr(&canonical_path); + msnprintf_result = curl_msnprintf(buffer, sizeof(buffer), + "%s: Received \"%s\" and should be \"%s\", normalize (%d)", + testcases[i].testname, curlx_dyn_ptr(&canonical_path), + testcases[i].canonical_url, testcases[i].normalize); + fail_unless(msnprintf_result >= 0, "curl_msnprintf fails"); + fail_unless(!result && canonical_path_string && + !strcmp(canonical_path_string, + testcases[i].canonical_url), buffer); + curlx_dyn_free(&canonical_path); + } + +#endif /* !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_AWS) */ + +UNITTEST_STOP diff --git a/tests/unit/unit1980.c b/tests/unit/unit1980.c new file mode 100644 index 0000000000..69cd2dd8c0 --- /dev/null +++ b/tests/unit/unit1980.c @@ -0,0 +1,114 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ +#include "curlcheck.h" + + +#include "http_aws_sigv4.h" +#include "dynbuf.h" + + +static CURLcode unit_setup(void) +{ + return CURLE_OK; +} + +static void unit_stop(void) +{ +} + +UNITTEST_START +#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_AWS) + +struct testcase { + const char *testname; + const char *query_part; + const char *canonical_query; +}; + +static const struct testcase testcases[] = { + { + "no-value", + "Param1=", + "Param1=" + }, + { + "test-439", + "name=me&noval&aim=b%aad&weirdo=*.//-", + "aim=b%AAd&name=me&noval=&weirdo=%2A.%2F%2F-" + }, + { + "blank-query-params", + "hello=a&b&c=&d", + "b=&c=&d=&hello=a" + }, + { + "get-vanilla-query-order-key-case", + "Param2=value2&Param1=value1", + "Param1=value1&Param2=value2" + }, + { + "get-vanilla-query-unreserved", + "-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=" + "-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz=" + "-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + }, + { + "get-vanilla-empty-query-key", + "Param1=value1", + "Param1=value1" + }, + { + "get-vanilla-query-order-encoded", + "Param-3=Value3&Param=Value2&%E1%88%B4=Value1", + "%E1%88%B4=Value1&Param=Value2&Param-3=Value3" + }, +}; + + struct dynbuf canonical_query; + + char buffer[1024]; + char *canonical_query_ptr; + size_t i; + int result; + int msnprintf_result; + + for(i = 0; i < CURL_ARRAYSIZE(testcases); i++) { + curlx_dyn_init(&canonical_query, CURL_MAX_HTTP_HEADER); + + result = canon_query(testcases[i].query_part, &canonical_query); + canonical_query_ptr = curlx_dyn_ptr(&canonical_query); + msnprintf_result = curl_msnprintf(buffer, sizeof(buffer), + "%s: Received \"%s\" and should be \"%s\"", + testcases[i].testname, canonical_query_ptr, + testcases[i].canonical_query); + fail_unless(msnprintf_result >= 0, "curl_msnprintf fails"); + fail_unless(!result && canonical_query_ptr && !strcmp(canonical_query_ptr, + testcases[i].canonical_query), + buffer); + curlx_dyn_free(&canonical_query); + } + + #endif /* !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_AWS) */ + UNITTEST_STOP