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..ea8d97c1b 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" @@ -15,6 +16,20 @@ 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); + +/** + * 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 * @@ -175,6 +190,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 +325,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..071e6ad22 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 @@ -76,15 +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 = ""); + 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. @@ -184,9 +189,11 @@ 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 - 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::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") std::string external_url_; // External URL for absolute SSE callback URLs mutable bool enable_metrics_ = true; // Enable metrics by default @@ -218,4 +225,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..c20665f26 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,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); // ListenerCallbacks interface void onAccept(network::ConnectionSocketPtr&& socket) override; @@ -254,6 +262,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..fb9e98c17 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,10 +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]() { - *request_future_ptr = - sendRequest("tools/list", mcp::make_optional(*params_ptr)); - }); + 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!) @@ -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,10 +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]() { - *request_future_ptr = - sendRequest("tools/call", mcp::make_optional(*params_ptr)); - }); + 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 996721546..ddfea4105 100644 --- a/src/filter/http_codec_filter.cc +++ b/src/filter/http_codec_filter.cc @@ -25,6 +25,42 @@ 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); +} + +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 == "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) { + if (!isValidClientHeader(header.first, header.second) || + isGeneratedClientHeader(header.first)) { + continue; + } + request << header.first << ": " << header.second << "\r\n"; + } +} +} // namespace + // HttpFilterChainBridge implementation HttpCodecFilter::HttpFilterChainBridge::HttpFilterChainBridge( @@ -385,6 +421,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 +458,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..0572f69de 100644 --- a/src/mcp_connection_manager.cc +++ b/src/mcp_connection_manager.cc @@ -22,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" @@ -83,6 +84,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 +687,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 +703,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 +977,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 +1047,19 @@ 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 (!filter::isValidClientHeader(header.first, header.second) || + filter::isGeneratedClientHeader(header.first)) { + continue; + } + request << header.first << ": " << header.second << "\r\n"; + } request << "\r\n"; request << json_body; @@ -1260,7 +1292,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 +1305,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 +1321,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 +1352,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 +1364,16 @@ 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 OwnedBuffer buffer; buffer.add(json_str); @@ -1334,10 +1385,14 @@ 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"); }); 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..2f2a57fab 100644 --- a/tests/filter/test_http_headers_compatibility.cc +++ b/tests/filter/test_http_headers_compatibility.cc @@ -220,6 +220,188 @@ 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"}, + {"Transfer-Encoding", "chunked"}, + {"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("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) + << 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, 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"); + 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; +} + +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 // ============================================================================= diff --git a/tests/network/test_mcp_connection_manager.cc b/tests/network/test_mcp_connection_manager.cc index 89eb80601..da712e323 100644 --- a/tests/network/test_mcp_connection_manager.cc +++ b/tests/network/test_mcp_connection_manager.cc @@ -1,7 +1,13 @@ +#include #include #include +#include +#include +#include #include +#include +#include #include "mcp/event/event_loop.h" #include "mcp/mcp_connection_manager.h" @@ -10,6 +16,80 @@ 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 +524,59 @@ 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 +614,4 @@ TEST_F(McpConnectionManagerTest, DISABLED_UsageExample) { } } // namespace -} // namespace mcp \ No newline at end of file +} // namespace mcp