aws_sigv4: merge repeated headers in canonical request

When multiple headers share the same name, AWS SigV4 expects them to be
merged into a single header line, with values comma-delimited in the
order they appeared.

Add libtest 1978 to verify.

Closes #16743
This commit is contained in:
Austin Moore 2025-03-18 23:58:56 -04:00 committed by Daniel Stenberg
parent fb4dbbac4a
commit 3978bd4498
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
5 changed files with 238 additions and 1 deletions

View file

@ -154,6 +154,57 @@ static int compare_header_names(const char *a, const char *b)
return cmp;
}
/* Merge duplicate header definitions by comma delimiting their values
in the order defined the headers are defined, expecting headers to
be alpha-sorted and use ':' at this point */
static CURLcode merge_duplicate_headers(struct curl_slist *head)
{
struct curl_slist *curr = head;
CURLcode result = CURLE_OK;
while(curr) {
struct curl_slist *next = curr->next;
if(!next)
break;
if(compare_header_names(curr->data, next->data) == 0) {
struct dynbuf buf;
char *colon_next;
char *val_next;
Curl_dyn_init(&buf, CURL_MAX_HTTP_HEADER);
result = Curl_dyn_add(&buf, curr->data);
if(result)
return result;
colon_next = strchr(next->data, ':');
DEBUGASSERT(colon_next);
val_next = colon_next + 1;
result = Curl_dyn_addn(&buf, ",", 1);
if(result)
return result;
result = Curl_dyn_add(&buf, val_next);
if(result)
return result;
free(curr->data);
curr->data = Curl_dyn_ptr(&buf);
curr->next = next->next;
free(next->data);
free(next);
}
else {
curr = curr->next;
}
}
return CURLE_OK;
}
/* timestamp should point to a buffer of at last TIMESTAMP_SIZE bytes */
static CURLcode make_headers(struct Curl_easy *data,
const char *hostname,
@ -299,6 +350,10 @@ static CURLcode make_headers(struct Curl_easy *data,
}
} while(again);
ret = merge_duplicate_headers(head);
if(ret)
goto fail;
for(l = head; l; l = l->next) {
char *tmp;

View file

@ -239,6 +239,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 \
\
test2000 test2001 test2002 test2003 test2004 test2005 \
\

73
tests/data/test1978 Normal file
View file

@ -0,0 +1,73 @@
<testcase>
<info>
<keywords>
HTTP
CURLOPT_AWS_SIGV4
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Thu, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
SSL
Debug
crypto
</features>
<name>
HTTP AWS_SIGV4 canonical request duplicate header test
</name>
<tool>
lib%TESTNUMBER
</tool>
<command>
http://xxx:yyy@127.0.0.1:9000/%TESTNUMBER/testapi/test 127.0.0.1:9000:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strip>
^User-Agent:.*
^Content-Length:.*
^Accept:.*
</strip>
<protocol crlf="yes">
PUT /%TESTNUMBER/testapi/test HTTP/1.1
Host: 127.0.0.1:9000
Authorization: AWS4-HMAC-SHA256 Credential=xxx/19700101/us-east-1/s3/aws4_request, SignedHeaders=curr-header-no-colon;duplicate-header;header-no-value;header-some-no-value;host;next-header-no-colon;some-other-header;x-amz-content-sha256;x-amz-date;x-amz-meta-blah;x-amz-meta-test;x-amz-meta-test2, Signature=b1e2ac88fd1307f0b9031140c3c99cc20d6263f3fe64b132dc433c95fe2c6316
X-Amz-Date: 19700101T000000Z
x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-meta-test: test2
some-other-header: value
x-amz-meta-test: test1
duplicate-header: duplicate
x-amz-meta-test: test3
X-amz-meta-test2: test2
x-amz-meta-blah: blah
x-Amz-meta-test2: test1
x-amz-Meta-test2: test3
curr-header-no-colon: value
next-header-no-colon: value
duplicate-header: duplicate
header-no-value:
header-no-value:
header-some-no-value:
header-some-no-value: value
</protocol>
</verify>
</testcase>

View file

@ -72,7 +72,7 @@ LIBTESTPROGS = libauthretry libntlmconnect libprereq \
lib1933 lib1934 lib1935 lib1936 lib1937 lib1938 lib1939 lib1940 \
lib1945 lib1946 lib1947 lib1948 lib1955 lib1956 lib1957 lib1958 lib1959 \
lib1960 lib1964 \
lib1970 lib1971 lib1972 lib1973 lib1974 lib1975 lib1977 \
lib1970 lib1971 lib1972 lib1973 lib1974 lib1975 lib1977 lib1978 \
lib2301 lib2302 lib2304 lib2305 lib2306 lib2308 lib2309 lib2310 \
lib2311 \
lib2402 lib2404 lib2405 \
@ -680,6 +680,9 @@ lib1975_LDADD = $(TESTUTIL_LIBS)
lib1977_SOURCES = lib1977.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
lib1977_LDADD = $(TESTUTIL_LIBS)
lib1978_SOURCES = lib1978.c $(SUPPORTFILES) $(TESTUTIL) $(WARNLESS)
lib1978_LDADD = $(TESTUTIL_LIBS)
lib2301_SOURCES = lib2301.c $(SUPPORTFILES)
lib2301_LDADD = $(TESTUTIL_LIBS)

105
tests/libtest/lib1978.c Normal file
View file

@ -0,0 +1,105 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, 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.haxx.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 "test.h"
#include "memdebug.h"
CURLcode test(char *URL)
{
CURL *curl;
CURLcode res = TEST_ERR_MAJOR_BAD;
struct curl_slist *connect_to = NULL;
struct curl_slist *list = NULL;
if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
fprintf(stderr, "curl_global_init() failed\n");
return TEST_ERR_MAJOR_BAD;
}
curl = curl_easy_init();
if(!curl) {
fprintf(stderr, "curl_easy_init() failed\n");
curl_global_cleanup();
return TEST_ERR_MAJOR_BAD;
}
test_setopt(curl, CURLOPT_UPLOAD, 1L);
test_setopt(curl, CURLOPT_INFILESIZE, 0L);
test_setopt(curl, CURLOPT_VERBOSE, 1L);
test_setopt(curl, CURLOPT_AWS_SIGV4, "aws:amz:us-east-1:s3");
test_setopt(curl, CURLOPT_USERPWD, "xxx");
test_setopt(curl, CURLOPT_HEADER, 0L);
test_setopt(curl, CURLOPT_URL, URL);
/* We want to test a couple assumptions here.
1. the merging works with non-adjacent headers
2. the merging works across multiple duplicate headers
3. the merging works if a duplicate header has no colon
4. the merging works if the headers are cased differently
5. the merging works across multiple duplicate headers
6. the merging works across multiple duplicate headers with the
same value
7. merging works for headers all with no values
8. merging works for headers some with no values
*/
list = curl_slist_append(list, "x-amz-meta-test: test2");
if(!list)
goto test_cleanup;
curl_slist_append(list, "some-other-header: value");
curl_slist_append(list, "x-amz-meta-test: test1");
curl_slist_append(list, "duplicate-header: duplicate");
curl_slist_append(list, "header-no-value");
curl_slist_append(list, "x-amz-meta-test: test3");
curl_slist_append(list, "X-amz-meta-test2: test2");
curl_slist_append(list, "x-amz-meta-blah: blah");
curl_slist_append(list, "x-Amz-meta-test2: test1");
curl_slist_append(list, "x-amz-Meta-test2: test3");
curl_slist_append(list, "curr-header-no-colon");
curl_slist_append(list, "curr-header-no-colon: value");
curl_slist_append(list, "next-header-no-colon: value");
curl_slist_append(list, "next-header-no-colon");
curl_slist_append(list, "duplicate-header: duplicate");
curl_slist_append(list, "header-no-value;");
curl_slist_append(list, "header-no-value;");
curl_slist_append(list, "header-some-no-value;");
curl_slist_append(list, "header-some-no-value: value");
test_setopt(curl, CURLOPT_HTTPHEADER, list);
if(libtest_arg2) {
connect_to = curl_slist_append(connect_to, libtest_arg2);
}
test_setopt(curl, CURLOPT_CONNECT_TO, connect_to);
res = curl_easy_perform(curl);
test_cleanup:
curl_slist_free_all(connect_to);
curl_slist_free_all(list);
curl_easy_cleanup(curl);
curl_global_cleanup();
return res;
}