1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #include "async_resp.hpp" 4 #include "http/http2_connection.hpp" 5 #include "http/http_request.hpp" 6 #include "http/http_response.hpp" 7 #include "http_connect_types.hpp" 8 #include "nghttp2_adapters.hpp" 9 #include "test_stream.hpp" 10 11 #include <nghttp2/nghttp2.h> 12 #include <unistd.h> 13 14 #include <boost/asio/buffer.hpp> 15 #include <boost/asio/io_context.hpp> 16 #include <boost/asio/ssl/context.hpp> 17 #include <boost/asio/ssl/stream.hpp> 18 #include <boost/asio/steady_timer.hpp> 19 #include <boost/asio/write.hpp> 20 #include <boost/beast/http/field.hpp> 21 22 #include <bit> 23 #include <cstddef> 24 #include <cstdint> 25 #include <functional> 26 #include <memory> 27 #include <string> 28 #include <string_view> 29 #include <utility> 30 #include <vector> 31 32 #include "gmock/gmock.h" 33 #include "gtest/gtest.h" 34 namespace crow 35 { 36 37 namespace 38 { 39 40 using ::testing::Pair; 41 using ::testing::UnorderedElementsAre; 42 43 struct FakeHandler 44 { 45 bool called = false; 46 void handle(const std::shared_ptr<Request>& req, 47 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 48 { 49 called = true; 50 EXPECT_EQ(req->url().buffer(), "/redfish/v1/"); 51 EXPECT_EQ(req->methodString(), "GET"); 52 EXPECT_EQ(req->getHeaderValue(boost::beast::http::field::user_agent), 53 "curl/8.5.0"); 54 EXPECT_EQ(req->getHeaderValue(boost::beast::http::field::accept), 55 "*/*"); 56 EXPECT_EQ(req->getHeaderValue(":authority"), "localhost:18080"); 57 asyncResp->res.write("StringOutput"); 58 } 59 }; 60 61 std::string getDateStr() 62 { 63 return "TestTime"; 64 } 65 66 void unpackHeaders(std::string_view dataField, 67 std::vector<std::pair<std::string, std::string>>& headers) 68 { 69 nghttp2_hd_inflater_ex inflater; 70 71 while (!dataField.empty()) 72 { 73 nghttp2_nv nv; 74 int inflateFlags = 0; 75 const uint8_t* data = std::bit_cast<const uint8_t*>(dataField.data()); 76 ssize_t parsed = 77 inflater.hd2(&nv, &inflateFlags, data, dataField.size(), 1); 78 79 ASSERT_GT(parsed, 0); 80 dataField.remove_prefix(static_cast<size_t>(parsed)); 81 if ((inflateFlags & NGHTTP2_HD_INFLATE_EMIT) > 0) 82 { 83 const char* namePtr = std::bit_cast<const char*>(nv.name); 84 std::string key(namePtr, nv.namelen); 85 const char* valPtr = std::bit_cast<const char*>(nv.value); 86 std::string value(valPtr, nv.valuelen); 87 headers.emplace_back(key, value); 88 } 89 if ((inflateFlags & NGHTTP2_HD_INFLATE_FINAL) > 0) 90 { 91 EXPECT_EQ(inflater.endHeaders(), 0); 92 break; 93 } 94 } 95 EXPECT_TRUE(dataField.empty()); 96 } 97 98 TEST(http_connection, RequestPropogates) 99 { 100 using namespace std::literals; 101 boost::asio::io_context io; 102 TestStream stream(io); 103 TestStream out(io); 104 stream.connect(out); 105 // This is a binary pre-encrypted stream captured from curl for a request to 106 // curl https://localhost:18080/redfish/v1/ 107 std::string_view toSend = 108 // Hello 109 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" 110 // 18 byte settings frame 111 "\x00\x00\x12\x04\x00\x00\x00\x00\x00" 112 // Settings 113 "\x00\x03\x00\x00\x00\x64\x00\x04\x00\xa0\x00\x00\x00\x02\x00\x00\x00\x00" 114 // Window update frame 115 "\x00\x00\x04\x08\x00\x00\x00\x00\x00" 116 // Window update 117 "\x3e\x7f\x00\x01" 118 // Header frame END_STREAM, END_HEADERS set 119 "\x00\x00\x29\x01\x05\x00\x00\x00" 120 // Header payload 121 "\x01\x82\x87\x41\x8b\xa0\xe4\x1d\x13\x9d\x09\xb8\x17\x80\xf0\x3f" 122 "\x04\x89\x62\xc2\xc9\x29\x91\x3b\x1d\xc2\xc7\x7a\x88\x25\xb6\x50" 123 "\xc3\xcb\xb6\xb8\x3f\x53\x03\x2a\x2f\x2a"sv; 124 125 boost::asio::write(out, boost::asio::buffer(toSend)); 126 127 FakeHandler handler; 128 boost::asio::steady_timer timer(io); 129 std::function<std::string()> date(getDateStr); 130 boost::asio::ssl::context sslCtx(boost::asio::ssl::context::tls_server); 131 auto conn = std::make_shared<HTTP2Connection<TestStream, FakeHandler>>( 132 boost::asio::ssl::stream<TestStream>(std::move(stream), sslCtx), 133 &handler, date, HttpType::HTTP); 134 conn->start(); 135 136 std::string_view expectedPrefix = 137 // Settings frame size 13 138 "\x00\x00\x0c\x04\x00\x00\x00\x00\x00" 139 // 4 max concurrent streams 140 "\x00\x03\x00\x00\x00\x04" 141 // Enable push = false 142 "\x00\x02\x00\x00\x00\x00" 143 // Settings ACK from server to client 144 "\x00\x00\x00\x04\x01\x00\x00\x00\x00" 145 146 // Start Headers frame stream 1, size 0x005f 147 "\x00\x00\x5f\x01\x04\x00\x00\x00\x01"sv; 148 149 std::string_view expectedPostfix = 150 // Data Frame, Length 12, Stream 1, End Stream flag set 151 "\x00\x00\x0c\x00\x01\x00\x00\x00\x01" 152 // The body expected 153 "StringOutput"sv; 154 155 std::string_view outStr; 156 constexpr size_t headerSize = 0x05f; 157 158 // Run until we receive the expected amount of data 159 while (outStr.size() < 160 expectedPrefix.size() + headerSize + expectedPostfix.size()) 161 { 162 io.run_one(); 163 outStr = out.str(); 164 } 165 EXPECT_TRUE(handler.called); 166 167 // check the stream output against expected 168 EXPECT_EQ(outStr.substr(0, expectedPrefix.size()), expectedPrefix); 169 outStr.remove_prefix(expectedPrefix.size()); 170 std::vector<std::pair<std::string, std::string>> headers; 171 unpackHeaders(outStr.substr(0, headerSize), headers); 172 outStr.remove_prefix(headerSize); 173 174 EXPECT_THAT(headers, 175 UnorderedElementsAre( 176 Pair(":status", "200"), Pair("content-length", "12"), 177 Pair("strict-transport-security", 178 "max-age=31536000; includeSubdomains"), 179 Pair("cache-control", "no-store, max-age=0"), 180 Pair("x-content-type-options", "nosniff"), 181 Pair("pragma", "no-cache"), Pair("date", "TestTime"))); 182 183 EXPECT_EQ(outStr, expectedPostfix); 184 } 185 186 } // namespace 187 } // namespace crow 188