From 1ccd7b3d724738ad0b321e5ad044c536a775d9c2 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Fri, 26 Jun 2026 08:19:35 +0800 Subject: [PATCH 1/7] Add MCP HTTP header passthrough support (#250) Summary: - Add default HTTP header storage to MCP client and connection configuration so SDK-created clients can send runtime Authorization headers. - Add per-request HTTP header overloads for sendRequest, listTools, and callTool so gateway callers can pass the client bearer token through to backend MCP servers. - Propagate configured and per-request headers into HTTP/SSE and streamable HTTP request formatting, with per-request headers overriding static defaults. - Preserve generated transport headers such as Host, Content-Type, Content-Length, Accept, Connection, User-Agent, and Cache-Control. --- include/mcp/client/mcp_client.h | 17 +++ include/mcp/filter/http_codec_filter.h | 15 +++ .../filter/http_sse_filter_chain_factory.h | 12 +- include/mcp/mcp_connection_manager.h | 12 ++ src/client/mcp_client.cc | 42 ++++++- src/filter/http_codec_filter.cc | 34 ++++++ src/filter/http_sse_filter_chain_factory.cc | 19 ++- src/mcp_connection_manager.cc | 69 ++++++++++- .../filter/test_http_headers_compatibility.cc | 109 ++++++++++++++++++ 9 files changed, 315 insertions(+), 14 deletions(-) diff --git a/include/mcp/client/mcp_client.h b/include/mcp/client/mcp_client.h index 6b2156c2d..969d4052e 100644 --- a/include/mcp/client/mcp_client.h +++ b/include/mcp/client/mcp_client.h @@ -68,6 +68,7 @@ struct McpClientConfig : public application::ApplicationBase::Config { // Transport configuration TransportType preferred_transport = TransportType::Stdio; bool auto_negotiate_transport = true; + std::map http_headers; // Connection pool settings size_t connection_pool_size = 10; @@ -145,6 +146,7 @@ struct RequestContext { RequestId id; std::string method; optional params; + std::map http_headers; std::chrono::steady_clock::time_point start_time; std::promise promise; size_t retry_count{0}; @@ -444,6 +446,10 @@ class McpClient : public application::ApplicationBase { // Request methods with future-based async API std::future sendRequest(const std::string& method, const optional& params = nullopt); + std::future sendRequest( + const std::string& method, + const optional& params, + const std::map& http_headers); // Batch processing - sends multiple requests efficiently std::vector> sendBatch( @@ -463,8 +469,15 @@ class McpClient : public application::ApplicationBase { // Tool operations std::future listTools( const optional& cursor = nullopt); + std::future listTools( + const optional& cursor, + const std::map& http_headers); std::future callTool( const std::string& name, const optional& arguments = nullopt); + std::future callTool( + const std::string& name, + const optional& arguments, + const std::map& http_headers); // Prompt operations std::future listPrompts( @@ -544,6 +557,10 @@ class McpClient : public application::ApplicationBase { RequestId generateRequestId(); std::shared_ptr createRequestContext( const std::string& method, const optional& params); + std::shared_ptr createRequestContext( + const std::string& method, + const optional& params, + const std::map& http_headers); void sendRequestInternal(std::shared_ptr context); void handleTimeout(std::shared_ptr context); void retryRequest(std::shared_ptr context); diff --git a/include/mcp/filter/http_codec_filter.h b/include/mcp/filter/http_codec_filter.h index a5a296a01..970164733 100644 --- a/include/mcp/filter/http_codec_filter.h +++ b/include/mcp/filter/http_codec_filter.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "mcp/buffer.h" @@ -175,6 +176,18 @@ class HttpCodecFilter : public network::Filter { client_host_ = host; } + /** + * Set extra client request headers for generated HTTP requests. + */ + void setClientHeaders(const std::map& headers) { + client_headers_ = headers; + } + + void setClientHeaderSource( + const std::shared_ptr>& headers) { + client_header_source_ = headers; + } + /** * Set the message endpoint for POST requests (client mode only) * Called after receiving endpoint event from SSE stream @@ -298,6 +311,8 @@ class HttpCodecFilter : public network::Filter { bool is_server_; std::string client_path_{"/rpc"}; // HTTP request path for client mode std::string client_host_{"localhost"}; // HTTP Host header for client mode + std::map client_headers_; + std::shared_ptr> client_header_source_; std::string message_endpoint_; // Endpoint for POST requests (from SSE // endpoint event) bool has_message_endpoint_{ diff --git a/include/mcp/filter/http_sse_filter_chain_factory.h b/include/mcp/filter/http_sse_filter_chain_factory.h index b4fbeeb55..6887b398a 100644 --- a/include/mcp/filter/http_sse_filter_chain_factory.h +++ b/include/mcp/filter/http_sse_filter_chain_factory.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -84,7 +85,12 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory { bool use_sse = true, const std::string& sse_path = "/sse", const std::string& rpc_path = "/mcp", - const std::string& external_url = ""); + const std::string& external_url = "", + const std::map& + client_headers = {}, + const std::shared_ptr< + std::map>& + client_header_source = nullptr); // Destructor defined out-of-line so the unique_ptr // member can use the incomplete forward-declared type in this header. @@ -184,6 +190,8 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory { bool is_server_; std::string http_path_; // HTTP request path for client mode std::string http_host_; // HTTP Host header for client mode + std::map client_headers_; + std::shared_ptr> client_header_source_; bool use_sse_; // True for SSE mode, false for Streamable HTTP std::string sse_path_; // Server-side SSE endpoint path (e.g., "/sse") std::string rpc_path_; // Server-side JSON-RPC endpoint path (e.g., "/mcp") @@ -218,4 +226,4 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory { }; } // namespace filter -} // namespace mcp \ No newline at end of file +} // namespace mcp diff --git a/include/mcp/mcp_connection_manager.h b/include/mcp/mcp_connection_manager.h index d3ec128e0..436753e60 100644 --- a/include/mcp/mcp_connection_manager.h +++ b/include/mcp/mcp_connection_manager.h @@ -2,6 +2,7 @@ #define MCP_MCP_CONNECTION_MANAGER_H #include +#include #include #include "mcp/core/result.h" @@ -51,6 +52,8 @@ struct McpConnectionConfig { std::string http_path{"/rpc"}; // Request path (e.g., /sse, /mcp) std::string http_host; // Host header value (auto-set from server_address if empty) + std::map http_headers; + std::shared_ptr> current_http_headers; }; /** @@ -161,6 +164,9 @@ class McpConnectionManager : public McpProtocolCallbacks, * Send a request */ VoidResult sendRequest(const jsonrpc::Request& request); + VoidResult sendRequest( + const jsonrpc::Request& request, + const std::map& http_headers); /** * Send a notification @@ -232,6 +238,9 @@ class McpConnectionManager : public McpProtocolCallbacks, void onError(const Error& error) override; void onMessageEndpoint(const std::string& endpoint) override; bool sendHttpPost(const std::string& json_body) override; + bool sendHttpPost( + const std::string& json_body, + const std::map& http_headers); // ListenerCallbacks interface void onAccept(network::ConnectionSocketPtr&& socket) override; @@ -254,6 +263,9 @@ class McpConnectionManager : public McpProtocolCallbacks, // Send JSON message VoidResult sendJsonMessage(const json::JsonValue& message); + VoidResult sendJsonMessage( + const json::JsonValue& message, + const std::map& http_headers); event::Dispatcher& dispatcher_; network::SocketInterface& socket_interface_; diff --git a/src/client/mcp_client.cc b/src/client/mcp_client.cc index 9e154a949..d11a63da5 100644 --- a/src/client/mcp_client.cc +++ b/src/client/mcp_client.cc @@ -651,6 +651,13 @@ InitializeResult McpClient::parseInitializeResponse( // Send request with future-based async API std::future McpClient::sendRequest(const std::string& method, const optional& params) { + return sendRequest(method, params, {}); +} + +std::future McpClient::sendRequest( + const std::string& method, + const optional& params, + const std::map& http_headers) { // Check if circuit breaker allows request if (!circuit_breaker_->allowRequest()) { client_stats_.circuit_breaker_opens++; @@ -666,6 +673,7 @@ std::future McpClient::sendRequest(const std::string& method, // Create request context auto context = std::make_shared(id, method); context->params = params; + context->http_headers = http_headers; context->start_time = std::chrono::steady_clock::now(); // Track request @@ -805,7 +813,8 @@ void McpClient::sendRequestInternal(std::shared_ptr context) { last_activity_time_ = std::chrono::steady_clock::now(); // Send through connection manager - auto send_result = connection_manager_->sendRequest(request); + auto send_result = connection_manager_->sendRequest(request, + context->http_headers); GOPHER_LOG_DEBUG("sendRequest result: is_error={}", is_error(send_result)); @@ -1023,6 +1032,10 @@ McpConnectionConfig McpClient::createConnectionConfig(TransportType transport) { http_config.server_address = server_addr; config.http_path = http_path; config.http_host = server_addr; + config.http_headers = config_.http_headers; + config.current_http_headers = + std::make_shared>( + config_.http_headers); // Set SSL transport for HTTPS URLs if (is_https) { @@ -1074,6 +1087,10 @@ McpConnectionConfig McpClient::createConnectionConfig(TransportType transport) { http_config.server_address = server_addr; config.http_path = http_path; config.http_host = server_addr; + config.http_headers = config_.http_headers; + config.current_http_headers = + std::make_shared>( + config_.http_headers); // Set SSL transport for HTTPS URLs if (is_https) { @@ -1354,6 +1371,12 @@ std::future McpClient::unsubscribeResource(const std::string& uri) { // List available tools std::future McpClient::listTools( const optional& cursor) { + return listTools(cursor, {}); +} + +std::future McpClient::listTools( + const optional& cursor, + const std::map& http_headers) { auto result_promise = std::make_shared>(); if (!main_dispatcher_) { @@ -1380,9 +1403,10 @@ std::future McpClient::listTools( cursor.has_value() ? cursor.value() : ""); // Step 1: Post to dispatcher to send the request (non-blocking) - main_dispatcher_->post([this, request_future_ptr, params_ptr]() { + main_dispatcher_->post([this, request_future_ptr, params_ptr, http_headers]() { *request_future_ptr = - sendRequest("tools/list", mcp::make_optional(*params_ptr)); + sendRequest("tools/list", mcp::make_optional(*params_ptr), + http_headers); }); // Step 2: Use std::thread to wait for response on a worker thread (not @@ -1430,6 +1454,13 @@ std::future McpClient::listTools( // Call a tool std::future McpClient::callTool( const std::string& name, const optional& arguments) { + return callTool(name, arguments, {}); +} + +std::future McpClient::callTool( + const std::string& name, + const optional& arguments, + const std::map& http_headers) { auto result_promise = std::make_shared>(); if (!main_dispatcher_) { @@ -1464,9 +1495,10 @@ std::future McpClient::callTool( : ""); // Step 1: Post to dispatcher to send the request (non-blocking) - main_dispatcher_->post([this, request_future_ptr, params_ptr]() { + main_dispatcher_->post([this, request_future_ptr, params_ptr, http_headers]() { *request_future_ptr = - sendRequest("tools/call", mcp::make_optional(*params_ptr)); + sendRequest("tools/call", mcp::make_optional(*params_ptr), + http_headers); }); // Step 2: Use std::thread to wait for response on a worker thread (not diff --git a/src/filter/http_codec_filter.cc b/src/filter/http_codec_filter.cc index 996721546..c8b53e603 100644 --- a/src/filter/http_codec_filter.cc +++ b/src/filter/http_codec_filter.cc @@ -25,6 +25,34 @@ namespace mcp { namespace filter { +namespace { +std::string toLowerHeaderName(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return value; +} + +bool isGeneratedClientHeader(const std::string& name) { + const std::string lower = toLowerHeaderName(name); + return lower == "host" || lower == "content-length" || + lower == "connection" || lower == "accept" || + lower == "content-type" || lower == "user-agent" || + lower == "cache-control"; +} + +void appendClientHeaders(std::ostringstream& request, + const std::map& headers) { + for (const auto& header : headers) { + if (header.first.empty() || header.second.empty() || + isGeneratedClientHeader(header.first)) { + continue; + } + request << header.first << ": " << header.second << "\r\n"; + } +} +} // namespace + // HttpFilterChainBridge implementation HttpCodecFilter::HttpFilterChainBridge::HttpFilterChainBridge( @@ -385,6 +413,9 @@ network::FilterStatus HttpCodecFilter::onWrite(Buffer& data, bool end_stream) { request << "Cache-Control: no-cache\r\n"; request << "Connection: keep-alive\r\n"; request << "User-Agent: gopher-mcp/1.0\r\n"; + appendClientHeaders(request, client_header_source_ + ? *client_header_source_ + : client_headers_); request << "\r\n"; sse_get_sent_ = true; @@ -419,6 +450,9 @@ network::FilterStatus HttpCodecFilter::onWrite(Buffer& data, bool end_stream) { request << "Accept: application/json, text/event-stream\r\n"; request << "Connection: keep-alive\r\n"; request << "User-Agent: gopher-mcp/1.0\r\n"; + appendClientHeaders(request, client_header_source_ + ? *client_header_source_ + : client_headers_); request << "\r\n"; request << body_data; } diff --git a/src/filter/http_sse_filter_chain_factory.cc b/src/filter/http_sse_filter_chain_factory.cc index 3fc0052f6..60c398a9f 100644 --- a/src/filter/http_sse_filter_chain_factory.cc +++ b/src/filter/http_sse_filter_chain_factory.cc @@ -126,12 +126,17 @@ class HttpSseJsonRpcProtocolFilter const std::string& configured_sse_path = "/sse", const std::string& configured_rpc_path = "/mcp", const std::string& configured_external_url = "", + const std::map& client_headers = {}, + const std::shared_ptr>& + client_header_source = nullptr, SseSessionRegistry* sse_registry = nullptr) : dispatcher_(dispatcher), mcp_callbacks_(mcp_callbacks), is_server_(is_server), http_path_(http_path), http_host_(http_host), + client_headers_(client_headers), + client_header_source_(client_header_source), configured_sse_path_(configured_sse_path), configured_rpc_path_(configured_rpc_path), configured_external_url_(configured_external_url), @@ -157,6 +162,8 @@ class HttpSseJsonRpcProtocolFilter // Set client endpoint for HTTP requests if (!is_server) { http_filter_->setClientEndpoint(http_path, http_host); + http_filter_->setClientHeaders(client_headers_); + http_filter_->setClientHeaderSource(client_header_source_); // Only enable SSE GET mode if use_sse is true // For Streamable HTTP, we send POST requests directly if (use_sse) { @@ -1233,6 +1240,8 @@ class HttpSseJsonRpcProtocolFilter // SSE client endpoint configuration std::string http_path_{"/rpc"}; // Default HTTP path for requests std::string http_host_{"localhost"}; // Default HTTP host for requests + std::map client_headers_; + std::shared_ptr> client_header_source_; // SSE server transport (only meaningful when is_server_ == true). std::string configured_sse_path_{"/sse"}; @@ -1302,12 +1311,17 @@ HttpSseFilterChainFactory::HttpSseFilterChainFactory( bool use_sse, const std::string& sse_path, const std::string& rpc_path, - const std::string& external_url) + const std::string& external_url, + const std::map& client_headers, + const std::shared_ptr>& + client_header_source) : dispatcher_(dispatcher), message_callbacks_(message_callbacks), is_server_(is_server), http_path_(http_path), http_host_(http_host), + client_headers_(client_headers), + client_header_source_(client_header_source), use_sse_(use_sse), sse_path_(sse_path), rpc_path_(rpc_path), @@ -1392,7 +1406,8 @@ bool HttpSseFilterChainFactory::createFilterChain( auto combined_filter = std::make_shared( dispatcher_, message_callbacks_, is_server_, http_path_, http_host_, use_sse_, route_registration_callback_, sse_path_, rpc_path_, - external_url_, sse_registry_.get()); + external_url_, client_headers_, client_header_source_, + sse_registry_.get()); // Add as both read and write filter. The FilterManager owns the filter // for the connection's lifetime (per-connection filter ownership): when diff --git a/src/mcp_connection_manager.cc b/src/mcp_connection_manager.cc index 15df01e5f..a0a1c4fef 100644 --- a/src/mcp_connection_manager.cc +++ b/src/mcp_connection_manager.cc @@ -1,6 +1,8 @@ #include "mcp/mcp_connection_manager.h" +#include #include +#include #include #include #include @@ -83,6 +85,12 @@ McpConnectionManager::McpConnectionManager( : dispatcher_(dispatcher), socket_interface_(socket_interface), config_(config) { + if (!config_.current_http_headers) { + config_.current_http_headers = + std::make_shared>( + config_.http_headers); + } + // Create connection manager network::ConnectionManagerConfig conn_config; conn_config.per_connection_buffer_limit = config.buffer_limit; @@ -680,6 +688,12 @@ VoidResult McpConnectionManager::listen( } VoidResult McpConnectionManager::sendRequest(const jsonrpc::Request& request) { + return sendRequest(request, {}); +} + +VoidResult McpConnectionManager::sendRequest( + const jsonrpc::Request& request, + const std::map& http_headers) { if (!connected_ || !active_connection_) { Error err; err.code = -1; @@ -690,7 +704,7 @@ VoidResult McpConnectionManager::sendRequest(const jsonrpc::Request& request) { // Convert to JSON using the bridge auto json_val = json::to_json(request); - return sendJsonMessage(json_val); + return sendJsonMessage(json_val, http_headers); } VoidResult McpConnectionManager::sendNotification( @@ -964,6 +978,12 @@ void McpConnectionManager::onMessageEndpoint(const std::string& endpoint) { } bool McpConnectionManager::sendHttpPost(const std::string& json_body) { + return sendHttpPost(json_body, {}); +} + +bool McpConnectionManager::sendHttpPost( + const std::string& json_body, + const std::map& http_headers) { GOPHER_LOG_DEBUG( "McpConnectionManager::sendHttpPost endpoint={}, body_len={}", message_endpoint_, json_body.length()); @@ -1028,6 +1048,28 @@ bool McpConnectionManager::sendHttpPost(const std::string& json_body) { request << "Content-Type: application/json\r\n"; request << "Content-Length: " << json_body.length() << "\r\n"; request << "Connection: close\r\n"; // One-shot connection + std::map merged_headers = + config_.current_http_headers ? *config_.current_http_headers + : config_.http_headers; + for (const auto& header : http_headers) { + merged_headers[header.first] = header.second; + } + for (const auto& header : merged_headers) { + if (header.first.empty() || header.second.empty()) { + continue; + } + const std::string name = header.first; + std::string lower = name; + std::transform(lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + if (lower == "host" || lower == "content-type" || + lower == "content-length" || lower == "connection") { + continue; + } + request << header.first << ": " << header.second << "\r\n"; + } request << "\r\n"; request << json_body; @@ -1260,7 +1302,9 @@ McpConnectionManager::createFilterChainFactory() { // - JSON-RPC for message protocol return std::make_shared( - dispatcher_, *this, is_server_, config_.http_path, config_.http_host); + dispatcher_, *this, is_server_, config_.http_path, config_.http_host, + true /* use_sse */, "/sse", "/mcp", "", config_.http_headers, + config_.current_http_headers); } else if (config_.transport_type == TransportType::StreamableHttp) { // Streamable HTTP: Simple POST request/response pattern @@ -1271,7 +1315,8 @@ McpConnectionManager::createFilterChainFactory() { return std::make_shared( dispatcher_, *this, is_server_, config_.http_path, config_.http_host, - false /* use_sse */); + false /* use_sse */, "/sse", "/mcp", "", config_.http_headers, + config_.current_http_headers); } else { // Simple direct transport (stdio, websocket): @@ -1286,6 +1331,12 @@ McpConnectionManager::createFilterChainFactory() { VoidResult McpConnectionManager::sendJsonMessage( const json::JsonValue& message) { + return sendJsonMessage(message, {}); +} + +VoidResult McpConnectionManager::sendJsonMessage( + const json::JsonValue& message, + const std::map& http_headers) { GOPHER_LOG_DEBUG( "McpConnectionManager::sendJsonMessage called, connected={}, conn={}", connected_, (void*)active_connection_.get()); @@ -1311,7 +1362,7 @@ VoidResult McpConnectionManager::sendJsonMessage( // Post write to dispatcher thread to ensure thread safety // The write() call must happen on the dispatcher thread // We capture `this` to check if connection is still valid when callback runs - dispatcher_.post([this, json_str = std::move(json_str)]() { + dispatcher_.post([this, json_str = std::move(json_str), http_headers]() { // Check if connection is still valid - it may have been closed if (!active_connection_) { GOPHER_LOG_DEBUG( @@ -1323,6 +1374,14 @@ VoidResult McpConnectionManager::sendJsonMessage( "McpConnectionManager write callback executing, conn={}, msg_len={}", (void*)active_connection_.get(), json_str.length()); + if (config_.current_http_headers) { + auto merged_headers = config_.http_headers; + for (const auto& header : http_headers) { + merged_headers[header.first] = header.second; + } + *config_.current_http_headers = std::move(merged_headers); + } + // Create buffer with JSON payload OwnedBuffer buffer; buffer.add(json_str); @@ -1340,4 +1399,4 @@ VoidResult McpConnectionManager::sendJsonMessage( return makeVoidSuccess(); } -} // namespace mcp \ No newline at end of file +} // namespace mcp diff --git a/tests/filter/test_http_headers_compatibility.cc b/tests/filter/test_http_headers_compatibility.cc index 93547d8fa..50f1a765e 100644 --- a/tests/filter/test_http_headers_compatibility.cc +++ b/tests/filter/test_http_headers_compatibility.cc @@ -220,6 +220,115 @@ TEST_F(HttpHeadersCompatibilityTest, SseGetRequestHasAllRequiredHeaders) { << "Should have User-Agent header"; } +// ============================================================================= +// Header Passthrough Tests +// ============================================================================= + +TEST_F(HttpHeadersCompatibilityTest, PostRequestIncludesClientHeaders) { + HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); + filter.setClientEndpoint("/mcp", "backend.example.com"); + filter.setClientHeaders({{"Authorization", "Bearer caller-token"}, + {"X-Request-ID", "req-123"}}); + + OwnedBuffer write_buffer; + std::string json_data = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; + write_buffer.add(json_data.c_str(), json_data.length()); + + filter.onWrite(write_buffer, false); + + std::string request = write_buffer.toString(); + + EXPECT_NE(request.find("Authorization: Bearer caller-token\r\n"), + std::string::npos) + << request; + EXPECT_NE(request.find("X-Request-ID: req-123\r\n"), std::string::npos) + << request; +} + +TEST_F(HttpHeadersCompatibilityTest, ClientHeadersCannotOverrideGenerated) { + HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); + filter.setClientEndpoint("/mcp", "backend.example.com"); + filter.setClientHeaders({{"Host", "attacker.example.com"}, + {"Content-Type", "text/plain"}, + {"Content-Length", "999999"}, + {"Accept", "text/plain"}, + {"Connection", "close"}, + {"User-Agent", "custom-agent"}, + {"Cache-Control", "max-age=3600"}, + {"Authorization", "Bearer caller-token"}}); + + OwnedBuffer write_buffer; + std::string json_data = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; + write_buffer.add(json_data.c_str(), json_data.length()); + + filter.onWrite(write_buffer, false); + + std::string request = write_buffer.toString(); + + EXPECT_NE(request.find("Host: backend.example.com\r\n"), std::string::npos) + << request; + EXPECT_NE(request.find("Content-Type: application/json\r\n"), + std::string::npos) + << request; + EXPECT_NE(request.find("Content-Length: " + std::to_string(json_data.size()) + + "\r\n"), + std::string::npos) + << request; + EXPECT_NE(request.find("Accept: application/json, text/event-stream\r\n"), + std::string::npos) + << request; + EXPECT_NE(request.find("Connection: keep-alive\r\n"), std::string::npos) + << request; + EXPECT_NE(request.find("User-Agent: gopher-mcp/1.0\r\n"), std::string::npos) + << request; + EXPECT_EQ(request.find("Host: attacker.example.com"), std::string::npos) + << request; + EXPECT_EQ(request.find("Content-Type: text/plain"), std::string::npos) + << request; + EXPECT_EQ(request.find("Content-Length: 999999"), std::string::npos) + << request; + EXPECT_EQ(request.find("Accept: text/plain"), std::string::npos) << request; + EXPECT_EQ(request.find("Connection: close"), std::string::npos) << request; + EXPECT_EQ(request.find("User-Agent: custom-agent"), std::string::npos) + << request; + EXPECT_EQ(request.find("Cache-Control: max-age=3600"), std::string::npos) + << request; + EXPECT_NE(request.find("Authorization: Bearer caller-token\r\n"), + std::string::npos) + << request; +} + +TEST_F(HttpHeadersCompatibilityTest, ClientHeaderSourceOverridesStaticHeaders) { + HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); + filter.setClientEndpoint("/mcp", "backend.example.com"); + filter.setClientHeaders({{"Authorization", "Bearer static-token"}, + {"X-Static", "yes"}}); + + auto current_headers = + std::make_shared>(); + (*current_headers)["Authorization"] = "Bearer per-request-token"; + (*current_headers)["X-Request-ID"] = "req-456"; + filter.setClientHeaderSource(current_headers); + + OwnedBuffer write_buffer; + std::string json_data = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"id\":2}"; + write_buffer.add(json_data.c_str(), json_data.length()); + + filter.onWrite(write_buffer, false); + + std::string request = write_buffer.toString(); + + EXPECT_NE(request.find("Authorization: Bearer per-request-token\r\n"), + std::string::npos) + << request; + EXPECT_NE(request.find("X-Request-ID: req-456\r\n"), std::string::npos) + << request; + EXPECT_EQ(request.find("Authorization: Bearer static-token"), + std::string::npos) + << request; + EXPECT_EQ(request.find("X-Static: yes"), std::string::npos) << request; +} + // ============================================================================= // Edge Cases // ============================================================================= From 9c49972b44bc91a8b6d44797dfbb0de3d932177a Mon Sep 17 00:00:00 2001 From: RahulHere Date: Tue, 30 Jun 2026 16:20:36 +0800 Subject: [PATCH 2/7] make format --- .../filter/http_sse_filter_chain_factory.h | 33 +++++++++---------- include/mcp/mcp_connection_manager.h | 5 ++- src/client/mcp_client.cc | 24 +++++++------- src/filter/http_codec_filter.cc | 6 ++-- src/mcp_connection_manager.cc | 7 ++-- .../filter/test_http_headers_compatibility.cc | 20 ++++++----- 6 files changed, 47 insertions(+), 48 deletions(-) diff --git a/include/mcp/filter/http_sse_filter_chain_factory.h b/include/mcp/filter/http_sse_filter_chain_factory.h index 6887b398a..071e6ad22 100644 --- a/include/mcp/filter/http_sse_filter_chain_factory.h +++ b/include/mcp/filter/http_sse_filter_chain_factory.h @@ -77,20 +77,19 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory { * callback URL advertised on GET /sse. Leave empty to * derive the URL from the incoming Host header. */ - HttpSseFilterChainFactory(event::Dispatcher& dispatcher, - McpProtocolCallbacks& message_callbacks, - bool is_server = true, - const std::string& http_path = "/rpc", - const std::string& http_host = "localhost", - bool use_sse = true, - const std::string& sse_path = "/sse", - const std::string& rpc_path = "/mcp", - const std::string& external_url = "", - const std::map& - client_headers = {}, - const std::shared_ptr< - std::map>& - client_header_source = nullptr); + HttpSseFilterChainFactory( + event::Dispatcher& dispatcher, + McpProtocolCallbacks& message_callbacks, + bool is_server = true, + const std::string& http_path = "/rpc", + const std::string& http_host = "localhost", + bool use_sse = true, + const std::string& sse_path = "/sse", + const std::string& rpc_path = "/mcp", + const std::string& external_url = "", + const std::map& client_headers = {}, + const std::shared_ptr>& + client_header_source = nullptr); // Destructor defined out-of-line so the unique_ptr // member can use the incomplete forward-declared type in this header. @@ -192,9 +191,9 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory { std::string http_host_; // HTTP Host header for client mode std::map client_headers_; std::shared_ptr> client_header_source_; - bool use_sse_; // True for SSE mode, false for Streamable HTTP - std::string sse_path_; // Server-side SSE endpoint path (e.g., "/sse") - std::string rpc_path_; // Server-side JSON-RPC endpoint path (e.g., "/mcp") + bool use_sse_; // True for SSE mode, false for Streamable HTTP + std::string sse_path_; // Server-side SSE endpoint path (e.g., "/sse") + std::string rpc_path_; // Server-side JSON-RPC endpoint path (e.g., "/mcp") std::string external_url_; // External URL for absolute SSE callback URLs mutable bool enable_metrics_ = true; // Enable metrics by default diff --git a/include/mcp/mcp_connection_manager.h b/include/mcp/mcp_connection_manager.h index 436753e60..c20665f26 100644 --- a/include/mcp/mcp_connection_manager.h +++ b/include/mcp/mcp_connection_manager.h @@ -238,9 +238,8 @@ class McpConnectionManager : public McpProtocolCallbacks, void onError(const Error& error) override; void onMessageEndpoint(const std::string& endpoint) override; bool sendHttpPost(const std::string& json_body) override; - bool sendHttpPost( - const std::string& json_body, - const std::map& http_headers); + bool sendHttpPost(const std::string& json_body, + const std::map& http_headers); // ListenerCallbacks interface void onAccept(network::ConnectionSocketPtr&& socket) override; diff --git a/src/client/mcp_client.cc b/src/client/mcp_client.cc index d11a63da5..fb9e98c17 100644 --- a/src/client/mcp_client.cc +++ b/src/client/mcp_client.cc @@ -813,8 +813,8 @@ void McpClient::sendRequestInternal(std::shared_ptr context) { last_activity_time_ = std::chrono::steady_clock::now(); // Send through connection manager - auto send_result = connection_manager_->sendRequest(request, - context->http_headers); + auto send_result = + connection_manager_->sendRequest(request, context->http_headers); GOPHER_LOG_DEBUG("sendRequest result: is_error={}", is_error(send_result)); @@ -1403,11 +1403,11 @@ std::future McpClient::listTools( cursor.has_value() ? cursor.value() : ""); // Step 1: Post to dispatcher to send the request (non-blocking) - main_dispatcher_->post([this, request_future_ptr, params_ptr, http_headers]() { - *request_future_ptr = - sendRequest("tools/list", mcp::make_optional(*params_ptr), - http_headers); - }); + main_dispatcher_->post( + [this, request_future_ptr, params_ptr, http_headers]() { + *request_future_ptr = sendRequest( + "tools/list", mcp::make_optional(*params_ptr), http_headers); + }); // Step 2: Use std::thread to wait for response on a worker thread (not // dispatcher!) @@ -1495,11 +1495,11 @@ std::future McpClient::callTool( : ""); // Step 1: Post to dispatcher to send the request (non-blocking) - main_dispatcher_->post([this, request_future_ptr, params_ptr, http_headers]() { - *request_future_ptr = - sendRequest("tools/call", mcp::make_optional(*params_ptr), - http_headers); - }); + main_dispatcher_->post( + [this, request_future_ptr, params_ptr, http_headers]() { + *request_future_ptr = sendRequest( + "tools/call", mcp::make_optional(*params_ptr), http_headers); + }); // Step 2: Use std::thread to wait for response on a worker thread (not // dispatcher!) diff --git a/src/filter/http_codec_filter.cc b/src/filter/http_codec_filter.cc index c8b53e603..48fce6fb6 100644 --- a/src/filter/http_codec_filter.cc +++ b/src/filter/http_codec_filter.cc @@ -27,9 +27,9 @@ namespace filter { namespace { std::string toLowerHeaderName(std::string value) { - std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { - return static_cast(std::tolower(c)); - }); + std::transform( + value.begin(), value.end(), value.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); return value; } diff --git a/src/mcp_connection_manager.cc b/src/mcp_connection_manager.cc index a0a1c4fef..5d3490c1a 100644 --- a/src/mcp_connection_manager.cc +++ b/src/mcp_connection_manager.cc @@ -1060,10 +1060,9 @@ bool McpConnectionManager::sendHttpPost( } const std::string name = header.first; std::string lower = name; - std::transform(lower.begin(), lower.end(), lower.begin(), - [](unsigned char c) { - return static_cast(std::tolower(c)); - }); + std::transform( + lower.begin(), lower.end(), lower.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); if (lower == "host" || lower == "content-type" || lower == "content-length" || lower == "connection") { continue; diff --git a/tests/filter/test_http_headers_compatibility.cc b/tests/filter/test_http_headers_compatibility.cc index 50f1a765e..3393de306 100644 --- a/tests/filter/test_http_headers_compatibility.cc +++ b/tests/filter/test_http_headers_compatibility.cc @@ -227,11 +227,12 @@ TEST_F(HttpHeadersCompatibilityTest, SseGetRequestHasAllRequiredHeaders) { TEST_F(HttpHeadersCompatibilityTest, PostRequestIncludesClientHeaders) { HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); filter.setClientEndpoint("/mcp", "backend.example.com"); - filter.setClientHeaders({{"Authorization", "Bearer caller-token"}, - {"X-Request-ID", "req-123"}}); + filter.setClientHeaders( + {{"Authorization", "Bearer caller-token"}, {"X-Request-ID", "req-123"}}); OwnedBuffer write_buffer; - std::string json_data = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; + std::string json_data = + "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; write_buffer.add(json_data.c_str(), json_data.length()); filter.onWrite(write_buffer, false); @@ -258,7 +259,8 @@ TEST_F(HttpHeadersCompatibilityTest, ClientHeadersCannotOverrideGenerated) { {"Authorization", "Bearer caller-token"}}); OwnedBuffer write_buffer; - std::string json_data = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; + std::string json_data = + "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; write_buffer.add(json_data.c_str(), json_data.length()); filter.onWrite(write_buffer, false); @@ -301,17 +303,17 @@ TEST_F(HttpHeadersCompatibilityTest, ClientHeadersCannotOverrideGenerated) { TEST_F(HttpHeadersCompatibilityTest, ClientHeaderSourceOverridesStaticHeaders) { HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); filter.setClientEndpoint("/mcp", "backend.example.com"); - filter.setClientHeaders({{"Authorization", "Bearer static-token"}, - {"X-Static", "yes"}}); + filter.setClientHeaders( + {{"Authorization", "Bearer static-token"}, {"X-Static", "yes"}}); - auto current_headers = - std::make_shared>(); + auto current_headers = std::make_shared>(); (*current_headers)["Authorization"] = "Bearer per-request-token"; (*current_headers)["X-Request-ID"] = "req-456"; filter.setClientHeaderSource(current_headers); OwnedBuffer write_buffer; - std::string json_data = "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"id\":2}"; + std::string json_data = + "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"id\":2}"; write_buffer.add(json_data.c_str(), json_data.length()); filter.onWrite(write_buffer, false); From ebe6250070b1d78df4aeb23df5c42f8723cf1626 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Tue, 7 Jul 2026 20:28:24 +0800 Subject: [PATCH 3/7] Reject unsafe client HTTP headers Summary: Validate client-supplied HTTP headers before the codec serializes them so names or values containing CR, LF, or NUL cannot inject extra header lines or smuggle another request. Add regression coverage for CRLF and NUL header injection attempts while preserving valid passthrough headers. --- include/mcp/filter/http_codec_filter.h | 7 ++++ src/filter/http_codec_filter.cc | 10 +++++- .../filter/test_http_headers_compatibility.cc | 35 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/include/mcp/filter/http_codec_filter.h b/include/mcp/filter/http_codec_filter.h index 970164733..40d66ed1d 100644 --- a/include/mcp/filter/http_codec_filter.h +++ b/include/mcp/filter/http_codec_filter.h @@ -16,6 +16,13 @@ namespace mcp { namespace filter { +/** + * Validate client-supplied HTTP header names and values before serialization. + * Generated headers are added separately; this only checks that passthrough + * headers cannot inject extra lines or terminate the request. + */ +bool isValidClientHeader(const std::string& name, const std::string& value); + /** * HttpCodecFilter - HTTP/1.1 codec supporting both client and server modes * diff --git a/src/filter/http_codec_filter.cc b/src/filter/http_codec_filter.cc index 48fce6fb6..abd5cea4a 100644 --- a/src/filter/http_codec_filter.cc +++ b/src/filter/http_codec_filter.cc @@ -25,6 +25,14 @@ namespace mcp { namespace filter { +bool isValidClientHeader(const std::string& name, const std::string& value) { + auto has_invalid_byte = [](const std::string& text) { + return text.find_first_of("\r\n\0", 0, 3) != std::string::npos; + }; + return !name.empty() && !value.empty() && !has_invalid_byte(name) && + !has_invalid_byte(value); +} + namespace { std::string toLowerHeaderName(std::string value) { std::transform( @@ -44,7 +52,7 @@ bool isGeneratedClientHeader(const std::string& name) { void appendClientHeaders(std::ostringstream& request, const std::map& headers) { for (const auto& header : headers) { - if (header.first.empty() || header.second.empty() || + if (!isValidClientHeader(header.first, header.second) || isGeneratedClientHeader(header.first)) { continue; } diff --git a/tests/filter/test_http_headers_compatibility.cc b/tests/filter/test_http_headers_compatibility.cc index 3393de306..ef9717def 100644 --- a/tests/filter/test_http_headers_compatibility.cc +++ b/tests/filter/test_http_headers_compatibility.cc @@ -300,6 +300,41 @@ TEST_F(HttpHeadersCompatibilityTest, ClientHeadersCannotOverrideGenerated) { << request; } +TEST_F(HttpHeadersCompatibilityTest, ClientHeadersRejectLineInjection) { + HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); + filter.setClientEndpoint("/mcp", "backend.example.com"); + + std::string nul_name = "X-Bad"; + nul_name.push_back('\0'); + nul_name += "Name"; + std::string nul_value = "bad"; + nul_value.push_back('\0'); + nul_value += "value"; + + filter.setClientHeaders({{"Authorization", "Bearer caller-token"}, + {"X-Injected-Value", "ok\r\nX-Smuggled: yes"}, + {"X-Bad\nName", "value"}, + {nul_name, "value"}, + {"X-Nul-Value", nul_value}}); + + OwnedBuffer write_buffer; + std::string json_data = + "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; + write_buffer.add(json_data.c_str(), json_data.length()); + + filter.onWrite(write_buffer, false); + + std::string request = write_buffer.toString(); + + EXPECT_NE(request.find("Authorization: Bearer caller-token\r\n"), + std::string::npos) + << request; + EXPECT_EQ(request.find("X-Smuggled: yes"), std::string::npos) << request; + EXPECT_EQ(request.find("X-Injected-Value:"), std::string::npos) << request; + EXPECT_EQ(request.find("X-Bad\nName"), std::string::npos) << request; + EXPECT_EQ(request.find("X-Nul-Value:"), std::string::npos) << request; +} + TEST_F(HttpHeadersCompatibilityTest, ClientHeaderSourceOverridesStaticHeaders) { HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); filter.setClientEndpoint("/mcp", "backend.example.com"); From 4038b413dc38e729c4b9d0c69b3a988848164adf Mon Sep 17 00:00:00 2001 From: RahulHere Date: Tue, 7 Jul 2026 20:29:53 +0800 Subject: [PATCH 4/7] Block client transfer encoding headers Summary: Treat Transfer-Encoding as a generated HTTP client header so caller-supplied values cannot be serialized alongside the codec's generated Content-Length. Expose the generated-header predicate for reuse and extend header compatibility coverage for the chunked smuggling case. --- include/mcp/filter/http_codec_filter.h | 7 +++++++ src/filter/http_codec_filter.cc | 6 +++--- tests/filter/test_http_headers_compatibility.cc | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/include/mcp/filter/http_codec_filter.h b/include/mcp/filter/http_codec_filter.h index 40d66ed1d..ea8d97c1b 100644 --- a/include/mcp/filter/http_codec_filter.h +++ b/include/mcp/filter/http_codec_filter.h @@ -23,6 +23,13 @@ namespace filter { */ bool isValidClientHeader(const std::string& name, const std::string& value); +/** + * Returns true for headers generated by the HTTP client codec itself. + * Client-supplied values for these names are ignored to keep framing + * deterministic and avoid conflicting Content-Length/Transfer-Encoding pairs. + */ +bool isGeneratedClientHeader(const std::string& name); + /** * HttpCodecFilter - HTTP/1.1 codec supporting both client and server modes * diff --git a/src/filter/http_codec_filter.cc b/src/filter/http_codec_filter.cc index abd5cea4a..49a5326f2 100644 --- a/src/filter/http_codec_filter.cc +++ b/src/filter/http_codec_filter.cc @@ -33,7 +33,6 @@ bool isValidClientHeader(const std::string& name, const std::string& value) { !has_invalid_byte(value); } -namespace { std::string toLowerHeaderName(std::string value) { std::transform( value.begin(), value.end(), value.begin(), @@ -44,11 +43,12 @@ std::string toLowerHeaderName(std::string value) { bool isGeneratedClientHeader(const std::string& name) { const std::string lower = toLowerHeaderName(name); return lower == "host" || lower == "content-length" || - lower == "connection" || lower == "accept" || - lower == "content-type" || lower == "user-agent" || + lower == "transfer-encoding" || lower == "connection" || + lower == "accept" || lower == "content-type" || lower == "user-agent" || lower == "cache-control"; } +namespace { void appendClientHeaders(std::ostringstream& request, const std::map& headers) { for (const auto& header : headers) { diff --git a/tests/filter/test_http_headers_compatibility.cc b/tests/filter/test_http_headers_compatibility.cc index ef9717def..436a7f6ce 100644 --- a/tests/filter/test_http_headers_compatibility.cc +++ b/tests/filter/test_http_headers_compatibility.cc @@ -252,6 +252,7 @@ TEST_F(HttpHeadersCompatibilityTest, ClientHeadersCannotOverrideGenerated) { filter.setClientHeaders({{"Host", "attacker.example.com"}, {"Content-Type", "text/plain"}, {"Content-Length", "999999"}, + {"Transfer-Encoding", "chunked"}, {"Accept", "text/plain"}, {"Connection", "close"}, {"User-Agent", "custom-agent"}, @@ -289,6 +290,8 @@ TEST_F(HttpHeadersCompatibilityTest, ClientHeadersCannotOverrideGenerated) { << request; EXPECT_EQ(request.find("Content-Length: 999999"), std::string::npos) << request; + EXPECT_EQ(request.find("Transfer-Encoding: chunked"), std::string::npos) + << request; EXPECT_EQ(request.find("Accept: text/plain"), std::string::npos) << request; EXPECT_EQ(request.find("Connection: close"), std::string::npos) << request; EXPECT_EQ(request.find("User-Agent: custom-agent"), std::string::npos) From 41900554f26916068a4f04a4aa8688806fbe6290 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Tue, 7 Jul 2026 20:33:54 +0800 Subject: [PATCH 5/7] Share HTTP client header filtering Summary: Make McpConnectionManager's direct HTTP POST path use the same client header validation and generated-header blocklist as HttpCodecFilter, including Transfer-Encoding filtering. Add a loopback regression that verifies valid headers pass while CRLF, NUL, Content-Length, and Transfer-Encoding headers are omitted on the wire. --- src/mcp_connection_manager.cc | 15 +- tests/network/test_mcp_connection_manager.cc | 139 ++++++++++++++++++- 2 files changed, 141 insertions(+), 13 deletions(-) diff --git a/src/mcp_connection_manager.cc b/src/mcp_connection_manager.cc index 5d3490c1a..1397c430d 100644 --- a/src/mcp_connection_manager.cc +++ b/src/mcp_connection_manager.cc @@ -1,8 +1,6 @@ #include "mcp/mcp_connection_manager.h" -#include #include -#include #include #include #include @@ -24,6 +22,7 @@ #endif #include "mcp/core/result.h" +#include "mcp/filter/http_codec_filter.h" #include "mcp/filter/http_sse_filter_chain_factory.h" #include "mcp/filter/protocol_detection_filter_chain_factory.h" #include "mcp/filter/stdio_filter_chain_factory.h" @@ -1055,16 +1054,8 @@ bool McpConnectionManager::sendHttpPost( merged_headers[header.first] = header.second; } for (const auto& header : merged_headers) { - if (header.first.empty() || header.second.empty()) { - continue; - } - const std::string name = header.first; - std::string lower = name; - std::transform( - lower.begin(), lower.end(), lower.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - if (lower == "host" || lower == "content-type" || - lower == "content-length" || lower == "connection") { + if (!filter::isValidClientHeader(header.first, header.second) || + filter::isGeneratedClientHeader(header.first)) { continue; } request << header.first << ": " << header.second << "\r\n"; diff --git a/tests/network/test_mcp_connection_manager.cc b/tests/network/test_mcp_connection_manager.cc index 89eb80601..10e17c8d5 100644 --- a/tests/network/test_mcp_connection_manager.cc +++ b/tests/network/test_mcp_connection_manager.cc @@ -1,8 +1,16 @@ #include +#include #include +#include #include +#include + +#include +#include +#include + #include "mcp/event/event_loop.h" #include "mcp/mcp_connection_manager.h" #include "mcp/network/socket_impl.h" @@ -10,6 +18,81 @@ namespace mcp { namespace { +class LoopbackHttpCapture { + public: + LoopbackHttpCapture() { + listen_fd_ = ::socket(AF_INET, SOCK_STREAM, 0); + EXPECT_GE(listen_fd_, 0); + + int opt = 1; + EXPECT_EQ(::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, + sizeof(opt)), + 0); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + EXPECT_EQ(::bind(listen_fd_, reinterpret_cast(&addr), + sizeof(addr)), + 0); + EXPECT_EQ(::listen(listen_fd_, 1), 0); + + socklen_t len = sizeof(addr); + EXPECT_EQ(::getsockname(listen_fd_, reinterpret_cast(&addr), + &len), + 0); + port_ = ntohs(addr.sin_port); + + request_future_ = request_promise_.get_future(); + server_thread_ = std::thread([this]() { acceptOne(); }); + } + + ~LoopbackHttpCapture() { + if (listen_fd_ >= 0) { + ::close(listen_fd_); + } + if (server_thread_.joinable()) { + server_thread_.join(); + } + } + + uint16_t port() const { return port_; } + + std::future& requestFuture() { return request_future_; } + + private: + void acceptOne() { + int fd = ::accept(listen_fd_, nullptr, nullptr); + if (fd < 0) { + request_promise_.set_value(""); + return; + } + + std::string request; + char buf[512]; + while (request.find("\r\n\r\n") == std::string::npos) { + ssize_t n = ::recv(fd, buf, sizeof(buf), 0); + if (n <= 0) { + break; + } + request.append(buf, static_cast(n)); + } + + const char response[] = + "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + (void)::send(fd, response, sizeof(response) - 1, 0); + ::close(fd); + request_promise_.set_value(request); + } + + int listen_fd_{-1}; + uint16_t port_{0}; + std::promise request_promise_; + std::future request_future_; + std::thread server_thread_; +}; + // Mock MCP message callbacks class MockMcpProtocolCallbacks : public McpProtocolCallbacks { public: @@ -444,6 +527,60 @@ TEST_F(McpConnectionManagerTest, HttpSseConfig) { // TODO: Add integration test with real dispatcher for HTTP/SSE connections } +TEST_F(McpConnectionManagerTest, HttpPostFiltersUnsafeAndGeneratedHeaders) { + LoopbackHttpCapture capture; + + McpConnectionConfig http_config; + http_config.transport_type = TransportType::HttpSse; + http_config.http_headers = {{"Authorization", "Bearer base-token"}, + {"Transfer-Encoding", "chunked"}, + {"X-Bad-Base", "ok\r\nX-Smuggled: yes"}}; + + McpConnectionManager http_manager(*dispatcher_, *socket_interface_, + http_config); + http_manager.onMessageEndpoint("http://127.0.0.1:" + + std::to_string(capture.port()) + "/mcp"); + + std::string nul_value = "bad"; + nul_value.push_back('\0'); + nul_value += "value"; + + ASSERT_TRUE(http_manager.sendHttpPost( + "{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}", + {{"X-Request-ID", "req-1"}, + {"Content-Length", "9999"}, + {"X-Injected", "ok\r\nX-Injected-Header: yes"}, + {"X-Nul", nul_value}})); + + auto& request_future = capture.requestFuture(); + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::milliseconds(2000); + while (request_future.wait_for(std::chrono::milliseconds(0)) != + std::future_status::ready && + std::chrono::steady_clock::now() < deadline) { + dispatcher_->run(event::RunType::NonBlock); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + ASSERT_EQ(request_future.wait_for(std::chrono::milliseconds(0)), + std::future_status::ready); + const std::string request = request_future.get(); + + EXPECT_NE(request.find("Authorization: Bearer base-token\r\n"), + std::string::npos) + << request; + EXPECT_NE(request.find("X-Request-ID: req-1\r\n"), std::string::npos) + << request; + EXPECT_EQ(request.find("Transfer-Encoding: chunked"), std::string::npos) + << request; + EXPECT_EQ(request.find("Content-Length: 9999"), std::string::npos) + << request; + EXPECT_EQ(request.find("X-Smuggled: yes"), std::string::npos) << request; + EXPECT_EQ(request.find("X-Injected-Header: yes"), std::string::npos) + << request; + EXPECT_EQ(request.find("X-Nul:"), std::string::npos) << request; +} + TEST_F(McpConnectionManagerTest, FactoryFunction) { // Test factory function auto manager = createMcpConnectionManager(*dispatcher_); @@ -481,4 +618,4 @@ TEST_F(McpConnectionManagerTest, DISABLED_UsageExample) { } } // namespace -} // namespace mcp \ No newline at end of file +} // namespace mcp From a3cbe115df8025f67f1b875facb2ac980346e008 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Tue, 7 Jul 2026 20:36:22 +0800 Subject: [PATCH 6/7] Scope HTTP headers to one write Summary: Reset the shared HTTP client header source back to base headers immediately after each connection-manager write so per-request headers such as Authorization cannot leak into later requests. Add regression coverage that a second codec write with a cleared header source does not reuse the previous request's Authorization value. --- src/mcp_connection_manager.cc | 6 ++++ .../filter/test_http_headers_compatibility.cc | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/mcp_connection_manager.cc b/src/mcp_connection_manager.cc index 1397c430d..0572f69de 100644 --- a/src/mcp_connection_manager.cc +++ b/src/mcp_connection_manager.cc @@ -1364,12 +1364,14 @@ VoidResult McpConnectionManager::sendJsonMessage( "McpConnectionManager write callback executing, conn={}, msg_len={}", (void*)active_connection_.get(), json_str.length()); + bool reset_current_http_headers = false; if (config_.current_http_headers) { auto merged_headers = config_.http_headers; for (const auto& header : http_headers) { merged_headers[header.first] = header.second; } *config_.current_http_headers = std::move(merged_headers); + reset_current_http_headers = true; } // Create buffer with JSON payload @@ -1383,6 +1385,10 @@ VoidResult McpConnectionManager::sendJsonMessage( // - Transport socket: raw I/O only active_connection_->write(buffer, false); + if (reset_current_http_headers && config_.current_http_headers) { + *config_.current_http_headers = config_.http_headers; + } + GOPHER_LOG_DEBUG("McpConnectionManager write completed"); }); diff --git a/tests/filter/test_http_headers_compatibility.cc b/tests/filter/test_http_headers_compatibility.cc index 436a7f6ce..2f2a57fab 100644 --- a/tests/filter/test_http_headers_compatibility.cc +++ b/tests/filter/test_http_headers_compatibility.cc @@ -369,6 +369,39 @@ TEST_F(HttpHeadersCompatibilityTest, ClientHeaderSourceOverridesStaticHeaders) { EXPECT_EQ(request.find("X-Static: yes"), std::string::npos) << request; } +TEST_F(HttpHeadersCompatibilityTest, ClientHeaderSourceDoesNotReusePrevious) { + HttpCodecFilter filter(callbacks_, *dispatcher_, false /* is_server */); + filter.setClientEndpoint("/mcp", "backend.example.com"); + + auto current_headers = std::make_shared>(); + filter.setClientHeaderSource(current_headers); + + (*current_headers)["Authorization"] = "Bearer first-token"; + OwnedBuffer first_write; + std::string first_json = + "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":1}"; + first_write.add(first_json.c_str(), first_json.length()); + filter.onWrite(first_write, false); + + const std::string first_request = first_write.toString(); + EXPECT_NE(first_request.find("Authorization: Bearer first-token\r\n"), + std::string::npos) + << first_request; + + current_headers->clear(); + OwnedBuffer second_write; + std::string second_json = + "{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"id\":2}"; + second_write.add(second_json.c_str(), second_json.length()); + filter.onWrite(second_write, false); + + const std::string second_request = second_write.toString(); + EXPECT_EQ(second_request.find("Authorization:"), std::string::npos) + << second_request; + EXPECT_EQ(second_request.find("Bearer first-token"), std::string::npos) + << second_request; +} + // ============================================================================= // Edge Cases // ============================================================================= From 0f850c177aee15b4e230331326ff3da6d1bcfb57 Mon Sep 17 00:00:00 2001 From: RahulHere Date: Tue, 7 Jul 2026 20:45:00 +0800 Subject: [PATCH 7/7] make format Summary: Apply formatting updates to the HTTP codec header filtering code and connection manager header regression test after the header safety fixes. --- src/filter/http_codec_filter.cc | 4 +- tests/network/test_mcp_connection_manager.cc | 46 +++++++++----------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/filter/http_codec_filter.cc b/src/filter/http_codec_filter.cc index 49a5326f2..ddfea4105 100644 --- a/src/filter/http_codec_filter.cc +++ b/src/filter/http_codec_filter.cc @@ -44,8 +44,8 @@ bool isGeneratedClientHeader(const std::string& name) { const std::string lower = toLowerHeaderName(name); return lower == "host" || lower == "content-length" || lower == "transfer-encoding" || lower == "connection" || - lower == "accept" || lower == "content-type" || lower == "user-agent" || - lower == "cache-control"; + lower == "accept" || lower == "content-type" || + lower == "user-agent" || lower == "cache-control"; } namespace { diff --git a/tests/network/test_mcp_connection_manager.cc b/tests/network/test_mcp_connection_manager.cc index 10e17c8d5..da712e323 100644 --- a/tests/network/test_mcp_connection_manager.cc +++ b/tests/network/test_mcp_connection_manager.cc @@ -1,13 +1,11 @@ -#include #include +#include #include #include - -#include - #include #include +#include #include #include @@ -25,23 +23,22 @@ class LoopbackHttpCapture { EXPECT_GE(listen_fd_, 0); int opt = 1; - EXPECT_EQ(::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, - sizeof(opt)), - 0); + EXPECT_EQ( + ::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)), + 0); sockaddr_in addr{}; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); addr.sin_port = 0; - EXPECT_EQ(::bind(listen_fd_, reinterpret_cast(&addr), - sizeof(addr)), - 0); + EXPECT_EQ( + ::bind(listen_fd_, reinterpret_cast(&addr), sizeof(addr)), + 0); EXPECT_EQ(::listen(listen_fd_, 1), 0); socklen_t len = sizeof(addr); - EXPECT_EQ(::getsockname(listen_fd_, reinterpret_cast(&addr), - &len), - 0); + EXPECT_EQ( + ::getsockname(listen_fd_, reinterpret_cast(&addr), &len), 0); port_ = ntohs(addr.sin_port); request_future_ = request_promise_.get_future(); @@ -538,23 +535,23 @@ TEST_F(McpConnectionManagerTest, HttpPostFiltersUnsafeAndGeneratedHeaders) { McpConnectionManager http_manager(*dispatcher_, *socket_interface_, http_config); - http_manager.onMessageEndpoint("http://127.0.0.1:" + - std::to_string(capture.port()) + "/mcp"); + http_manager.onMessageEndpoint( + "http://127.0.0.1:" + std::to_string(capture.port()) + "/mcp"); std::string nul_value = "bad"; nul_value.push_back('\0'); nul_value += "value"; - ASSERT_TRUE(http_manager.sendHttpPost( - "{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}", - {{"X-Request-ID", "req-1"}, - {"Content-Length", "9999"}, - {"X-Injected", "ok\r\nX-Injected-Header: yes"}, - {"X-Nul", nul_value}})); + ASSERT_TRUE( + http_manager.sendHttpPost("{\"jsonrpc\":\"2.0\",\"method\":\"ping\"}", + {{"X-Request-ID", "req-1"}, + {"Content-Length", "9999"}, + {"X-Injected", "ok\r\nX-Injected-Header: yes"}, + {"X-Nul", nul_value}})); auto& request_future = capture.requestFuture(); - const auto deadline = std::chrono::steady_clock::now() + - std::chrono::milliseconds(2000); + const auto deadline = + std::chrono::steady_clock::now() + std::chrono::milliseconds(2000); while (request_future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready && std::chrono::steady_clock::now() < deadline) { @@ -573,8 +570,7 @@ TEST_F(McpConnectionManagerTest, HttpPostFiltersUnsafeAndGeneratedHeaders) { << request; EXPECT_EQ(request.find("Transfer-Encoding: chunked"), std::string::npos) << request; - EXPECT_EQ(request.find("Content-Length: 9999"), std::string::npos) - << request; + EXPECT_EQ(request.find("Content-Length: 9999"), std::string::npos) << request; EXPECT_EQ(request.find("X-Smuggled: yes"), std::string::npos) << request; EXPECT_EQ(request.find("X-Injected-Header: yes"), std::string::npos) << request;