| /** @file |
| |
| Unit tests for HTTP2 |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include <catch2/catch_test_macros.hpp> |
| #include <catch2/matchers/catch_matchers_string.hpp> |
| #include <catch2/matchers/catch_matchers.hpp> |
| |
| #include "proxy/http2/HTTP2.h" |
| #include "iocore/eventsystem/IOBuffer.h" |
| |
| #include "tsutil/PostScript.h" |
| |
| #include <string> |
| |
| TEST_CASE("Convert HTTPHdr", "[HTTP2]") |
| { |
| HTTPParser parser; |
| ts::PostScript parser_defer([&]() -> void { http_parser_clear(&parser); }); |
| http_parser_init(&parser); |
| |
| SECTION("request") |
| { |
| const char request[] = "GET /index.html HTTP/1.1\r\n" |
| "Host: trafficserver.apache.org\r\n" |
| "User-Agent: foobar\r\n" |
| "\r\n"; |
| |
| HTTPHdr hdr_1; |
| ts::PostScript hdr_1_defer([&]() -> void { hdr_1.destroy(); }); |
| hdr_1.create(HTTPType::REQUEST, HTTP_2_0); |
| |
| // parse |
| const char *start = request; |
| const char *end = request + sizeof(request) - 1; |
| hdr_1.parse_req(&parser, &start, end, true); |
| |
| // convert to HTTP/2 |
| http2_convert_header_from_1_1_to_2(&hdr_1); |
| |
| // check pseudo headers |
| // :method |
| { |
| MIMEField *f = hdr_1.field_find(PSEUDO_HEADER_METHOD); |
| REQUIRE(f != nullptr); |
| std::string_view v = f->value_get(); |
| CHECK(v == "GET"); |
| } |
| |
| // :scheme |
| { |
| MIMEField *f = hdr_1.field_find(PSEUDO_HEADER_SCHEME); |
| REQUIRE(f != nullptr); |
| std::string_view v = f->value_get(); |
| CHECK(v == "https"); |
| } |
| |
| // :authority |
| { |
| MIMEField *f = hdr_1.field_find(PSEUDO_HEADER_AUTHORITY); |
| REQUIRE(f != nullptr); |
| std::string_view v = f->value_get(); |
| CHECK(v == "trafficserver.apache.org"); |
| } |
| |
| // :path |
| { |
| MIMEField *f = hdr_1.field_find(PSEUDO_HEADER_PATH); |
| REQUIRE(f != nullptr); |
| std::string_view v = f->value_get(); |
| CHECK(v == "/index.html"); |
| } |
| |
| // convert back to HTTP/1.1 |
| HTTPHdr hdr_2; |
| ts::PostScript hdr_2_defer([&]() -> void { hdr_2.destroy(); }); |
| hdr_2.create(HTTPType::REQUEST); |
| hdr_2.copy(&hdr_1); |
| |
| http2_convert_header_from_2_to_1_1(&hdr_2); |
| |
| // dump |
| char buf[1024] = {0}; |
| int bufindex = 0; |
| int dumpoffset = 0; |
| |
| hdr_2.print(buf, sizeof(buf), &bufindex, &dumpoffset); |
| |
| // check |
| CHECK_THAT(buf, Catch::Matchers::StartsWith("GET https://trafficserver.apache.org/index.html HTTP/1.1\r\n" |
| "Host: trafficserver.apache.org\r\n" |
| "User-Agent: foobar\r\n" |
| "\r\n")); |
| |
| // Verify that conversion from HTTP/2 to HTTP/1.1 works correctly when the |
| // HTTP/2 request contains a Host header. |
| HTTPHdr hdr_2_with_host; |
| ts::PostScript hdr_2_with_host_defer([&]() -> void { hdr_2_with_host.destroy(); }); |
| hdr_2_with_host.create(HTTPType::REQUEST); |
| hdr_2_with_host.copy(&hdr_1); |
| |
| MIMEField *host = hdr_2_with_host.field_create(static_cast<std::string_view>(MIME_FIELD_HOST)); |
| hdr_2_with_host.field_attach(host); |
| std::string_view host_value = "bogus.host.com"; |
| host->value_set(hdr_2_with_host.m_heap, hdr_2_with_host.m_mime, host_value); |
| |
| http2_convert_header_from_2_to_1_1(&hdr_2_with_host); |
| |
| // dump |
| memset(buf, 0, sizeof(buf)); |
| bufindex = 0; |
| dumpoffset = 0; |
| |
| hdr_2_with_host.print(buf, sizeof(buf), &bufindex, &dumpoffset); |
| |
| // check: Note that the Host will now be at the end of the Headers since we |
| // added it above and it will remain there, albeit with the updated value |
| // from the :authority header. |
| CHECK_THAT(buf, Catch::Matchers::StartsWith("GET https://trafficserver.apache.org/index.html HTTP/1.1\r\n" |
| "User-Agent: foobar\r\n" |
| "Host: trafficserver.apache.org\r\n" |
| "\r\n")); |
| } |
| |
| SECTION("reject CRLF in header value") |
| { |
| const char request[] = "GET /index.html HTTP/1.1\r\n" |
| "Host: trafficserver.apache.org\r\n" |
| "User-Agent: foobar\r\n" |
| "\r\n"; |
| |
| HTTPHdr hdr; |
| ts::PostScript hdr_defer([&]() -> void { hdr.destroy(); }); |
| hdr.create(HTTPType::REQUEST, HTTP_2_0); |
| |
| const char *start = request; |
| const char *end = request + sizeof(request) - 1; |
| hdr.parse_req(&parser, &start, end, true); |
| http2_convert_header_from_1_1_to_2(&hdr); |
| |
| MIMEField *evil = hdr.field_create("x-injected"); |
| hdr.field_attach(evil); |
| evil->value_set(hdr.m_heap, hdr.m_mime, std::string_view{"safe\r\ninjected: evil"}); |
| |
| HTTPHdr hdr_out; |
| ts::PostScript hdr_out_defer([&]() -> void { hdr_out.destroy(); }); |
| hdr_out.create(HTTPType::REQUEST); |
| hdr_out.copy(&hdr); |
| |
| CHECK(http2_convert_header_from_2_to_1_1(&hdr_out) == ParseResult::ERROR); |
| } |
| |
| SECTION("reject bare CR in header value") |
| { |
| const char request[] = "GET /index.html HTTP/1.1\r\n" |
| "Host: trafficserver.apache.org\r\n" |
| "\r\n"; |
| |
| HTTPHdr hdr; |
| ts::PostScript hdr_defer([&]() -> void { hdr.destroy(); }); |
| hdr.create(HTTPType::REQUEST, HTTP_2_0); |
| |
| const char *start = request; |
| const char *end = request + sizeof(request) - 1; |
| hdr.parse_req(&parser, &start, end, true); |
| http2_convert_header_from_1_1_to_2(&hdr); |
| |
| MIMEField *evil = hdr.field_create("x-injected"); |
| hdr.field_attach(evil); |
| evil->value_set(hdr.m_heap, hdr.m_mime, std::string_view{"before\rafter"}); |
| |
| HTTPHdr hdr_out; |
| ts::PostScript hdr_out_defer([&]() -> void { hdr_out.destroy(); }); |
| hdr_out.create(HTTPType::REQUEST); |
| hdr_out.copy(&hdr); |
| |
| CHECK(http2_convert_header_from_2_to_1_1(&hdr_out) == ParseResult::ERROR); |
| } |
| |
| SECTION("reject bare LF in header value") |
| { |
| const char request[] = "GET /index.html HTTP/1.1\r\n" |
| "Host: trafficserver.apache.org\r\n" |
| "\r\n"; |
| |
| HTTPHdr hdr; |
| ts::PostScript hdr_defer([&]() -> void { hdr.destroy(); }); |
| hdr.create(HTTPType::REQUEST, HTTP_2_0); |
| |
| const char *start = request; |
| const char *end = request + sizeof(request) - 1; |
| hdr.parse_req(&parser, &start, end, true); |
| http2_convert_header_from_1_1_to_2(&hdr); |
| |
| MIMEField *evil = hdr.field_create("x-injected"); |
| hdr.field_attach(evil); |
| evil->value_set(hdr.m_heap, hdr.m_mime, std::string_view{"before\nafter"}); |
| |
| HTTPHdr hdr_out; |
| ts::PostScript hdr_out_defer([&]() -> void { hdr_out.destroy(); }); |
| hdr_out.create(HTTPType::REQUEST); |
| hdr_out.copy(&hdr); |
| |
| CHECK(http2_convert_header_from_2_to_1_1(&hdr_out) == ParseResult::ERROR); |
| } |
| |
| SECTION("accept clean header value") |
| { |
| const char request[] = "GET /index.html HTTP/1.1\r\n" |
| "Host: trafficserver.apache.org\r\n" |
| "\r\n"; |
| |
| HTTPHdr hdr; |
| ts::PostScript hdr_defer([&]() -> void { hdr.destroy(); }); |
| hdr.create(HTTPType::REQUEST, HTTP_2_0); |
| |
| const char *start = request; |
| const char *end = request + sizeof(request) - 1; |
| hdr.parse_req(&parser, &start, end, true); |
| http2_convert_header_from_1_1_to_2(&hdr); |
| |
| MIMEField *clean = hdr.field_create("x-clean"); |
| hdr.field_attach(clean); |
| clean->value_set(hdr.m_heap, hdr.m_mime, std::string_view{"perfectly-fine-value"}); |
| |
| HTTPHdr hdr_out; |
| ts::PostScript hdr_out_defer([&]() -> void { hdr_out.destroy(); }); |
| hdr_out.create(HTTPType::REQUEST); |
| hdr_out.copy(&hdr); |
| |
| CHECK(http2_convert_header_from_2_to_1_1(&hdr_out) == ParseResult::DONE); |
| } |
| |
| SECTION("response") |
| { |
| const char response[] = "HTTP/1.1 200 OK\r\n" |
| "Connection: close\r\n" |
| "\r\n"; |
| |
| HTTPHdr hdr_1; |
| ts::PostScript hdr_1_defer([&]() -> void { hdr_1.destroy(); }); |
| hdr_1.create(HTTPType::RESPONSE, HTTP_2_0); |
| |
| // parse |
| const char *start = response; |
| const char *end = response + sizeof(response) - 1; |
| hdr_1.parse_resp(&parser, &start, end, true); |
| |
| // convert to HTTP/2 |
| http2_convert_header_from_1_1_to_2(&hdr_1); |
| |
| // check pseudo headers |
| // :status |
| { |
| MIMEField *f = hdr_1.field_find(PSEUDO_HEADER_STATUS); |
| REQUIRE(f != nullptr); |
| std::string_view v = f->value_get(); |
| CHECK(v == "200"); |
| } |
| |
| // no connection header |
| { |
| MIMEField *f = hdr_1.field_find(static_cast<std::string_view>(MIME_FIELD_CONNECTION)); |
| CHECK(f == nullptr); |
| } |
| |
| // convert to HTTP/1.1 |
| HTTPHdr hdr_2; |
| ts::PostScript hdr_2_defer([&]() -> void { hdr_2.destroy(); }); |
| hdr_2.create(HTTPType::REQUEST); |
| hdr_2.copy(&hdr_1); |
| |
| http2_convert_header_from_2_to_1_1(&hdr_2); |
| |
| // dump |
| char buf[1024] = {0}; |
| int bufindex = 0; |
| int dumpoffset = 0; |
| |
| hdr_2.print(buf, sizeof(buf), &bufindex, &dumpoffset); |
| |
| // check |
| REQUIRE(bufindex > 0); |
| CHECK_THAT(buf, Catch::Matchers::StartsWith("HTTP/1.1 200 OK\r\n\r\n")); |
| } |
| } |
| |
| // Regression: Http2ConnectionState::rcv_continuation_frame accumulates |
| // the size of every CONTINUATION payload into stream->header_blocks_length, a uint32_t. |
| // Before the fix, the increment was performed without overflow checking, so a crafted |
| // sequence of CONTINUATION frames whose payloads sum to more than UINT32_MAX would |
| // wrap the accumulator, and the subsequent ats_realloc would allocate a buffer smaller |
| // than the pre-wrap offset that memcpy then writes to. |
| TEST_CASE("CONTINUATION header_blocks_length overflow guard", "[HTTP2]") |
| { |
| SECTION("zero accumulator and zero payload do not overflow") |
| { |
| CHECK_FALSE(http2_continuation_length_would_overflow(0u, 0u)); |
| } |
| |
| SECTION("small additions do not overflow") |
| { |
| CHECK_FALSE(http2_continuation_length_would_overflow(0u, 16384u)); |
| CHECK_FALSE(http2_continuation_length_would_overflow(16384u, 16384u)); |
| CHECK_FALSE(http2_continuation_length_would_overflow(1u << 20, 1u << 20)); |
| } |
| |
| SECTION("sum that exactly fills uint32_t is allowed") |
| { |
| CHECK_FALSE(http2_continuation_length_would_overflow(UINT32_MAX, 0u)); |
| CHECK_FALSE(http2_continuation_length_would_overflow(0u, UINT32_MAX)); |
| CHECK_FALSE(http2_continuation_length_would_overflow(UINT32_MAX - 1u, 1u)); |
| CHECK_FALSE(http2_continuation_length_would_overflow(1u, UINT32_MAX - 1u)); |
| } |
| |
| SECTION("sum exceeding uint32_t by one wraps and must be rejected") |
| { |
| CHECK(http2_continuation_length_would_overflow(UINT32_MAX, 1u)); |
| CHECK(http2_continuation_length_would_overflow(1u, UINT32_MAX)); |
| } |
| |
| SECTION("realistic attack shape: prior bytes plus a max HTTP/2 frame payload") |
| { |
| constexpr uint32_t max_frame_payload = (1u << 24) - 1u; |
| CHECK(http2_continuation_length_would_overflow(UINT32_MAX - max_frame_payload + 1u, max_frame_payload)); |
| CHECK_FALSE(http2_continuation_length_would_overflow(UINT32_MAX - max_frame_payload, max_frame_payload)); |
| } |
| } |
| |
| namespace |
| { |
| // Print a header into a buffer of 4 KB blocks the same way Http2Stream::send_headers |
| // does, so that a header larger than one block is split across blocks. |
| void |
| print_into_blocks(HTTPHdr &hdr, MIOBuffer *buffer) |
| { |
| int bufindex; |
| int dumpoffset = 0; |
| int done, tmp; |
| |
| do { |
| bufindex = 0; |
| tmp = dumpoffset; |
| IOBufferBlock *block = buffer->get_current_block(); |
| if (!block) { |
| buffer->add_block(); |
| block = buffer->get_current_block(); |
| } |
| done = hdr.print(block->end(), block->write_avail(), &bufindex, &tmp); |
| dumpoffset += bufindex; |
| buffer->fill(bufindex); |
| if (!done) { |
| buffer->add_block(); |
| } |
| } while (!done); |
| } |
| } // namespace |
| |
| TEST_CASE("Parse a header split across IOBuffer blocks at EOF", "[HTTP2]") |
| { |
| // A bodyless HTTP/2 response is handed to the HttpSM along with EOS, so it is |
| // parsed with eof set. Every field must still parse when the printed header |
| // spans several blocks and a field is cut at a block boundary. |
| std::string const large_value(6000, 'v'); |
| std::string const pad_value(400, 'p'); |
| |
| HTTPParser parser; |
| ts::PostScript parser_defer([&]() -> void { http_parser_clear(&parser); }); |
| http_parser_init(&parser); |
| |
| MIOBuffer *buffer = new_MIOBuffer(BUFFER_SIZE_INDEX_4K); |
| ts::PostScript buffer_defer([&]() -> void { free_MIOBuffer(buffer); }); |
| IOBufferReader *reader = buffer->alloc_reader(); |
| |
| SECTION("response") |
| { |
| std::string text = "HTTP/1.1 302 Found\r\n" |
| "Location: https://example.com/landing\r\n"; |
| for (int i = 0; i < 3; ++i) { |
| text += "X-Pad-" + std::to_string(i) + ": " + pad_value + "\r\n"; |
| } |
| text += "Content-Security-Policy: " + large_value + "\r\n"; |
| for (int i = 3; i < 6; ++i) { |
| text += "X-Pad-" + std::to_string(i) + ": " + pad_value + "\r\n"; |
| } |
| text += "Content-Length: 0\r\n\r\n"; |
| |
| HTTPHdr origin; |
| ts::PostScript origin_defer([&]() -> void { origin.destroy(); }); |
| origin.create(HTTPType::RESPONSE); |
| char const *start = text.data(); |
| REQUIRE(origin.parse_resp(&parser, &start, text.data() + text.size(), true) == ParseResult::DONE); |
| http_parser_clear(&parser); |
| http_parser_init(&parser); |
| |
| print_into_blocks(origin, buffer); |
| REQUIRE(reader->read_avail() == static_cast<int64_t>(text.size())); |
| REQUIRE(reader->block_read_avail() < reader->read_avail()); |
| |
| HTTPHdr parsed; |
| ts::PostScript parsed_defer([&]() -> void { parsed.destroy(); }); |
| parsed.create(HTTPType::RESPONSE); |
| int bytes_used = 0; |
| REQUIRE(parsed.parse_resp(&parser, reader, &bytes_used, true) == ParseResult::DONE); |
| CHECK(bytes_used == static_cast<int>(text.size())); |
| CHECK(parsed.status_get() == HTTPStatus::MOVED_TEMPORARILY); |
| CHECK(parsed.fields_count() == 9); |
| CHECK(parsed.value_get("Content-Security-Policy"sv) == large_value); |
| CHECK(parsed.value_get("X-Pad-5"sv) == pad_value); |
| } |
| |
| SECTION("field ends at a block boundary") |
| { |
| std::string const prefix = "HTTP/1.1 204 No Content\r\nX-Pad: "; |
| std::string text = prefix + std::string(4096 - prefix.size() - 2, 'p') + "\r\n"; |
| REQUIRE(text.size() == 4096); |
| text += "X-Next: next\r\n\r\n"; |
| |
| HTTPHdr origin; |
| ts::PostScript origin_defer([&]() -> void { origin.destroy(); }); |
| origin.create(HTTPType::RESPONSE); |
| char const *start = text.data(); |
| REQUIRE(origin.parse_resp(&parser, &start, text.data() + text.size(), true) == ParseResult::DONE); |
| http_parser_clear(&parser); |
| http_parser_init(&parser); |
| |
| print_into_blocks(origin, buffer); |
| REQUIRE(reader->read_avail() == static_cast<int64_t>(text.size())); |
| REQUIRE(reader->block_read_avail() == 4096); |
| |
| HTTPHdr parsed; |
| ts::PostScript parsed_defer([&]() -> void { parsed.destroy(); }); |
| parsed.create(HTTPType::RESPONSE); |
| int bytes_used = 0; |
| REQUIRE(parsed.parse_resp(&parser, reader, &bytes_used, true) == ParseResult::DONE); |
| CHECK(bytes_used == static_cast<int>(text.size())); |
| CHECK(parsed.value_get("X-Next"sv) == "next"); |
| } |
| |
| SECTION("request") |
| { |
| std::string text = "GET /index.html HTTP/1.1\r\n" |
| "Host: example.com\r\n"; |
| text += "Cookie: " + large_value + "\r\n"; |
| text += "X-Pad: " + pad_value + "\r\n\r\n"; |
| |
| HTTPHdr client; |
| ts::PostScript client_defer([&]() -> void { client.destroy(); }); |
| client.create(HTTPType::REQUEST); |
| char const *start = text.data(); |
| REQUIRE(client.parse_req(&parser, &start, text.data() + text.size(), true) == ParseResult::DONE); |
| http_parser_clear(&parser); |
| http_parser_init(&parser); |
| |
| print_into_blocks(client, buffer); |
| REQUIRE(reader->read_avail() == static_cast<int64_t>(text.size())); |
| REQUIRE(reader->block_read_avail() < reader->read_avail()); |
| |
| HTTPHdr parsed; |
| ts::PostScript parsed_defer([&]() -> void { parsed.destroy(); }); |
| parsed.create(HTTPType::REQUEST); |
| int bytes_used = 0; |
| REQUIRE(parsed.parse_req(&parser, reader, &bytes_used, true) == ParseResult::DONE); |
| CHECK(bytes_used == static_cast<int>(text.size())); |
| CHECK(parsed.value_get("Cookie"sv) == large_value); |
| CHECK(parsed.value_get("X-Pad"sv) == pad_value); |
| } |
| } |