core/unittest/ebpf/ProtocolParserUnittest.cpp (113 lines of code) (raw):

// Copyright 2024 iLogtail Authors // // Licensed 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 <json/json.h> #include <algorithm> #include <iostream> #include <random> #include "ebpf/protocol/ProtocolParser.h" #include "ebpf/protocol/http/HttpParser.h" #include "logger/Logger.h" #include "unittest/Unittest.h" DECLARE_FLAG_BOOL(logtail_mode); namespace logtail { namespace ebpf { class ProtocolParserUnittest : public testing::Test { public: void TestParseHttp(); void TestParseHttpResponse(); void TestParseHttpHeaders(); void TestParseChunkedEncoding(); void TestParseInvalidRequests(); void TestParsePartialRequests(); void TestProtocolParserManager(); void TestHttpParserEdgeCases(); void RequestBenchmark(); void RequestWithoutBodyBenchmark(); void ResponseBenchmark(); void ResponseWithoutBodyBenchmark(); void ChunkedResponseBenchmark(); protected: void SetUp() override {} void TearDown() override {} private: bool IsValidHttpHeader(const std::string& name, const std::string& value) { return !name.empty() && name.find_first_of("()<>@,;:\\\"/[]?={}t") == std::string::npos; } }; void ProtocolParserUnittest::TestParseHttp() { const std::string input = "GET /index.html HTTP/1.1\r\nHost: www.cmonitor.ai\r\nAccept: image/gif, image/jpeg, " "*/*\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64)\r\n\r\n"; std::string_view buf(input); std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); ParseState state = http::ParseRequest(buf, result, true); APSARA_TEST_EQUAL(state, ParseState::kSuccess); APSARA_TEST_EQUAL(result->GetProtocolVersion(), "http1.1"); APSARA_TEST_EQUAL(result->GetRealPath(), "/index.html"); APSARA_TEST_EQUAL(result->GetPath(), "/index.html"); APSARA_TEST_EQUAL(result->GetReqBody(), ""); APSARA_TEST_EQUAL(result->GetReqBodySize(), 0UL); APSARA_TEST_EQUAL(result->GetReqHeaderMap().size(), 3UL); // APSARA_TEST_EQUAL(result->GetReqHeaderMap()_byte_size, input.size()); // APSARA_TEST_EQUAL(result.body, ""); // APSARA_TEST_EQUAL(result.body_size, result.body.size()); // // 检查头部信息 // APSARA_TEST_EQUAL(result->GetReqHeaderMap().size(), 3); const std::string input2 = "GET /path HTTP/1.1\r\nHost: example.com"; // Incomplete header std::string_view buf2(input2); result = std::make_shared<HttpRecord>(nullptr); state = http::ParseRequest(buf2, result, true); APSARA_TEST_EQUAL(state, ParseState::kNeedsMoreData); } void ProtocolParserUnittest::TestParseHttpResponse() { const std::string input = "HTTP/1.1 200 OK\r\n" "Content-Type: text/html\r\n" "Content-Length: 13\r\n" "\r\n" "Hello, World!"; std::string_view buf(input); std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); ParseState state = http::ParseResponse(buf, result, false, true); APSARA_TEST_EQUAL(state, ParseState::kSuccess); APSARA_TEST_EQUAL(result->GetStatusCode(), 200); APSARA_TEST_EQUAL(result->GetRespMsg(), "OK"); APSARA_TEST_EQUAL(result->GetRespHeaderMap().size(), 2UL); APSARA_TEST_EQUAL(result->GetRespBody(), "Hello, World!"); // 测试404响应 const std::string notFound = "HTTP/1.1 404 Not Found\r\n" "Content-Type: text/plain\r\n" "Content-Length: 9\r\n" "\r\n" "Not Found"; std::string_view buf2(notFound); result = std::make_shared<HttpRecord>(nullptr); state = http::ParseResponse(buf2, result, false, true); APSARA_TEST_EQUAL(state, ParseState::kSuccess); APSARA_TEST_EQUAL(result->GetStatusCode(), 404); APSARA_TEST_EQUAL(result->GetRespMsg(), "Not Found"); } void ProtocolParserUnittest::TestParseHttpHeaders() { const std::string input = "GET /test HTTP/1.1\r\n" "Host: example.com\r\n" "Content-Type: application/json\r\n" "X-Custom-Header: value1, value2\r\n" "Cookie: session=abc123; user=john\r\n" "\r\n"; std::string_view buf(input); std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); ParseState state = http::ParseRequest(buf, result, true); APSARA_TEST_EQUAL(state, ParseState::kSuccess); APSARA_TEST_EQUAL(result->GetReqHeaderMap().size(), 4UL); // 验证特定头部 APSARA_TEST_TRUE(result->GetReqHeaderMap().find("host") != result->GetReqHeaderMap().end()); APSARA_TEST_TRUE(result->GetReqHeaderMap().find("content-type") != result->GetReqHeaderMap().end()); APSARA_TEST_TRUE(result->GetReqHeaderMap().find("x-custom-header") != result->GetReqHeaderMap().end()); APSARA_TEST_TRUE(result->GetReqHeaderMap().find("cookie") != result->GetReqHeaderMap().end()); // 验证头部值 auto host = result->GetReqHeaderMap().find("host"); APSARA_TEST_NOT_EQUAL(host, result->GetReqHeaderMap().end()); APSARA_TEST_EQUAL(host->second, "example.com"); auto contentType = result->GetReqHeaderMap().find("content-type"); APSARA_TEST_NOT_EQUAL(contentType, result->GetReqHeaderMap().end()); APSARA_TEST_EQUAL(contentType->second, "application/json"); } void ProtocolParserUnittest::TestParseChunkedEncoding() { const std::string input = "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "7\r\n" "Mozilla\r\n" "9\r\n" "Developer\r\n" "7\r\n" "Network\r\n" "0\r\n" "\r\n"; std::string_view buf(input); std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); ParseState state = http::ParseResponse(buf, result, false, true); APSARA_TEST_EQUAL(state, ParseState::kSuccess); // 验证分块解码后的完整消息 std::string expected = "MozillaDeveloperNetwork"; APSARA_TEST_EQUAL(result->GetRespBody(), expected); } void ProtocolParserUnittest::TestParseInvalidRequests() { const std::string invalidMethod = "INVALID /test HTTP/1.1\r\n\r\n"; std::string_view buf1(invalidMethod); std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); ParseState state = http::ParseRequest(buf1, result, true); APSARA_TEST_EQUAL(state, ParseState::kSuccess); const std::string invalidVersion = "GET /test HTTP/2.0\r\n\r\n"; std::string_view buf2(invalidVersion); result = std::make_shared<HttpRecord>(nullptr); state = http::ParseRequest(buf2, result, true); APSARA_TEST_EQUAL(state, ParseState::kInvalid); const std::string invalidHeader = "GET /test HTTP/1.1\r\nInvalid Header\r\n\r\n"; std::string_view buf3(invalidHeader); result = std::make_shared<HttpRecord>(nullptr); state = http::ParseRequest(buf3, result, true); APSARA_TEST_EQUAL(state, ParseState::kInvalid); } void ProtocolParserUnittest::TestParsePartialRequests() { // 测试不完整的请求行 const std::string partialRequestLine = "GET /test"; std::string_view buf1(partialRequestLine); std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); ParseState state = http::ParseRequest(buf1, result, true); APSARA_TEST_EQUAL(state, ParseState::kNeedsMoreData); // 测试不完整的头部 const std::string partialHeaders = "GET /test HTTP/1.1\r\nHost: example.com\r\n"; std::string_view buf2(partialHeaders); result = std::make_shared<HttpRecord>(nullptr); state = http::ParseRequest(buf2, result, true); APSARA_TEST_EQUAL(state, ParseState::kNeedsMoreData); const std::string partialBody = "POST /test HTTP/1.1\r\n" "Content-Length: 10\r\n" "\r\n" "Part"; std::string_view buf3(partialBody); state = http::ParseRequest(buf3, result, true); APSARA_TEST_EQUAL(state, ParseState::kNeedsMoreData); } void ProtocolParserUnittest::TestProtocolParserManager() { auto& manager = ProtocolParserManager::GetInstance(); APSARA_TEST_TRUE(manager.AddParser(support_proto_e::ProtoHTTP)); APSARA_TEST_TRUE(manager.AddParser(support_proto_e::ProtoHTTP)); APSARA_TEST_TRUE(manager.RemoveParser(support_proto_e::ProtoHTTP)); APSARA_TEST_TRUE(manager.RemoveParser(support_proto_e::ProtoHTTP)); } void ProtocolParserUnittest::TestHttpParserEdgeCases() { // 测试空请求 const std::string emptyRequest; std::string_view buf1(emptyRequest); std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); ParseState state = http::ParseRequest(buf1, result, true); APSARA_TEST_EQUAL(state, ParseState::kNeedsMoreData); std::string longUrl = "GET /"; longUrl.append(2048, 'a'); longUrl += " HTTP/1.1\r\n\r\n"; std::string_view buf2(longUrl); result = std::make_shared<HttpRecord>(nullptr); state = http::ParseRequest(buf2, result, true); APSARA_TEST_EQUAL(state, ParseState::kSuccess); std::string manyHeaders = "GET /test HTTP/1.1\r\n"; for (int i = 0; i < 200; i++) { manyHeaders += "X-Custom-Header-" + std::to_string(i) + ": value\r\n"; } manyHeaders += "\r\n"; std::string_view buf3(manyHeaders); result = std::make_shared<HttpRecord>(nullptr); state = http::ParseRequest(buf3, result, true); APSARA_TEST_EQUAL(state, ParseState::kInvalid); } const std::string REQ = "GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n" "Host: www.kittyhell.com\r\n" "User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 " "Pathtraq/0.9\r\n" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" "Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n" "Accept-Encoding: gzip,deflate\r\n" "Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n" "Keep-Alive: 115\r\n" "Connection: keep-alive\r\n" "Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; " "__utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; " "__utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral\r\n" "\r\n"; const std::string CHUNKED_RESP_MSG = "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "9\r\n" "pixielabs\r\n" "C\r\n" " is awesome!\r\n" "100\r\n" "0000000000000000000000000000000000000000000000000000000000000000" "1111111111111111111111111111111111111111111111111111111111111111" "2222222222222222222222222222222222222222222222222222222222222222" "3333333333333333333333333333333333333333333333333333333333333333" "\r\n" "0\r\n" "\r\n"; const std::string RESP_MSG = "HTTP/1.1 200 OK\r\n" "Content-Length: 320\r\n" "\r\n" "0000000000000000000000000000000000000000000000000000000000000000" "1111111111111111111111111111111111111111111111111111111111111111" "2222222222222222222222222222222222222222222222222222222222222222" "3333333333333333333333333333333333333333333333333333333333333333" "4444444444444444444444444444444444444444444444444444444444444444\r\n\r\n"; void ProtocolParserUnittest::RequestBenchmark() { std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 1000000; i++) { std::string_view reqBuf(REQ); http::ParseRequest(reqBuf, result, true); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> elapsed = end - start; std::cout << "[request] elapsed: " << elapsed.count() << " seconds" << std::endl; } void ProtocolParserUnittest::RequestWithoutBodyBenchmark() { HTTPRequest result; auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 10000000; i++) { std::string_view reqBuf(REQ); http::ParseHttpRequest(reqBuf, result); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> elapsed = end - start; std::cout << "[request] elapsed: " << elapsed.count() << " seconds" << std::endl; } void ProtocolParserUnittest::ResponseBenchmark() { std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 1000000; i++) { std::string_view respBuf(RESP_MSG); http::ParseResponse(respBuf, result, false, true); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> elapsed = end - start; std::cout << "[response] elapsed: " << elapsed.count() << " seconds" << std::endl; } void ProtocolParserUnittest::ChunkedResponseBenchmark() { std::shared_ptr<HttpRecord> result = std::make_shared<HttpRecord>(nullptr); auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 1000000; i++) { std::string_view respBuf(CHUNKED_RESP_MSG); http::ParseResponse(respBuf, result, false, true); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> elapsed = end - start; std::cout << "[response][chunked] elapsed: " << elapsed.count() << " seconds" << std::endl; } UNIT_TEST_CASE(ProtocolParserUnittest, TestParseHttp); UNIT_TEST_CASE(ProtocolParserUnittest, TestParseHttpResponse); UNIT_TEST_CASE(ProtocolParserUnittest, TestParseHttpHeaders); UNIT_TEST_CASE(ProtocolParserUnittest, TestParseChunkedEncoding); UNIT_TEST_CASE(ProtocolParserUnittest, TestParseInvalidRequests); UNIT_TEST_CASE(ProtocolParserUnittest, TestParsePartialRequests); UNIT_TEST_CASE(ProtocolParserUnittest, TestProtocolParserManager); UNIT_TEST_CASE(ProtocolParserUnittest, TestHttpParserEdgeCases); UNIT_TEST_CASE(ProtocolParserUnittest, RequestBenchmark); UNIT_TEST_CASE(ProtocolParserUnittest, RequestWithoutBodyBenchmark); UNIT_TEST_CASE(ProtocolParserUnittest, ResponseBenchmark); UNIT_TEST_CASE(ProtocolParserUnittest, ChunkedResponseBenchmark); } // namespace ebpf } // namespace logtail UNIT_TEST_MAIN