|
1 | 1 | # -*- coding: utf-8 -*- |
| 2 | +"""HTTP Header Passthrough Utilities. |
| 3 | +
|
| 4 | +Copyright 2025 |
| 5 | +SPDX-License-Identifier: Apache-2.0 |
| 6 | +Authors: Mihai Criveti |
| 7 | +
|
| 8 | +This module provides utilities for handling HTTP header passthrough functionality |
| 9 | +in the MCP Gateway. It enables forwarding of specific headers from incoming |
| 10 | +client requests to backing MCP servers while preventing conflicts with |
| 11 | +existing authentication mechanisms. |
| 12 | +
|
| 13 | +Key Features: |
| 14 | +- Global configuration support via environment variables and database |
| 15 | +- Per-gateway header configuration overrides |
| 16 | +- Intelligent conflict detection with existing authentication headers |
| 17 | +- Security-first approach with explicit allowlist handling |
| 18 | +- Comprehensive logging for debugging and monitoring |
| 19 | +
|
| 20 | +The header passthrough system follows a priority hierarchy: |
| 21 | +1. Gateway-specific headers (highest priority) |
| 22 | +2. Global database configuration |
| 23 | +3. Environment variable defaults (lowest priority) |
| 24 | +
|
| 25 | +Example Usage: |
| 26 | + Basic header passthrough with global configuration: |
| 27 | + >>> from unittest.mock import Mock |
| 28 | + >>> mock_db = Mock() |
| 29 | + >>> mock_global_config = Mock() |
| 30 | + >>> mock_global_config.passthrough_headers = ["X-Tenant-Id"] |
| 31 | + >>> mock_db.query.return_value.first.return_value = mock_global_config |
| 32 | + >>> headers = get_passthrough_headers( |
| 33 | + ... request_headers={"x-tenant-id": "123"}, |
| 34 | + ... base_headers={"Content-Type": "application/json"}, |
| 35 | + ... db=mock_db |
| 36 | + ... ) |
| 37 | + >>> sorted(headers.items()) |
| 38 | + [('Content-Type', 'application/json'), ('X-Tenant-Id', '123')] |
| 39 | +""" |
| 40 | + |
2 | 41 | # Standard |
3 | 42 | import logging |
4 | 43 | from typing import Dict, Optional |
|
17 | 56 | def get_passthrough_headers(request_headers: Dict[str, str], base_headers: Dict[str, str], db: Session, gateway: Optional[DbGateway] = None) -> Dict[str, str]: |
18 | 57 | """Get headers that should be passed through to the target gateway. |
19 | 58 |
|
| 59 | + This function implements the core logic for HTTP header passthrough in the MCP Gateway. |
| 60 | + It determines which headers from incoming client requests should be forwarded to |
| 61 | + backing MCP servers based on configuration settings and security policies. |
| 62 | +
|
| 63 | + Configuration Priority (highest to lowest): |
| 64 | + 1. Gateway-specific passthrough_headers setting |
| 65 | + 2. Global database configuration (GlobalConfig.passthrough_headers) |
| 66 | + 3. Environment variable DEFAULT_PASSTHROUGH_HEADERS |
| 67 | +
|
| 68 | + Security Features: |
| 69 | + - Prevents conflicts with existing base headers (e.g., Content-Type) |
| 70 | + - Blocks Authorization header conflicts with gateway authentication |
| 71 | + - Logs all conflicts and skipped headers for debugging |
| 72 | + - Uses case-insensitive header matching for robustness |
| 73 | +
|
20 | 74 | Args: |
21 | | - request_headers: Headers from the incoming request |
22 | | - base_headers: Base headers that should always be included |
23 | | - gateway: Target gateway (optional) |
24 | | - db: Database session for global config lookup |
| 75 | + request_headers (Dict[str, str]): Headers from the incoming HTTP request. |
| 76 | + Keys should be header names, values should be header values. |
| 77 | + Example: {"Authorization": "Bearer token123", "X-Tenant-Id": "acme"} |
| 78 | + base_headers (Dict[str, str]): Base headers that should always be included |
| 79 | + in the final result. These take precedence over passthrough headers. |
| 80 | + Example: {"Content-Type": "application/json", "User-Agent": "MCPGateway/1.0"} |
| 81 | + db (Session): SQLAlchemy database session for querying global configuration. |
| 82 | + Used to retrieve GlobalConfig.passthrough_headers setting. |
| 83 | + gateway (Optional[DbGateway]): Target gateway instance. If provided, uses |
| 84 | + gateway.passthrough_headers to override global settings. Also checks |
| 85 | + gateway.auth_type to prevent Authorization header conflicts. |
25 | 86 |
|
26 | 87 | Returns: |
27 | | - Dict of headers that should be passed through |
| 88 | + Dict[str, str]: Combined dictionary of base headers plus allowed passthrough |
| 89 | + headers from the request. Base headers are preserved, and passthrough |
| 90 | + headers are added only if they don't conflict with security policies. |
| 91 | +
|
| 92 | + Raises: |
| 93 | + No exceptions are raised. Errors are logged as warnings and processing continues. |
| 94 | + Database connection issues may propagate from the db.query() call. |
| 95 | +
|
| 96 | + Examples: |
| 97 | + Basic usage with global configuration: |
| 98 | + >>> # Mock database and settings for doctest |
| 99 | + >>> from unittest.mock import Mock, MagicMock |
| 100 | + >>> mock_db = Mock() |
| 101 | + >>> mock_global_config = Mock() |
| 102 | + >>> mock_global_config.passthrough_headers = ["X-Tenant-Id", "X-Trace-Id"] |
| 103 | + >>> mock_db.query.return_value.first.return_value = mock_global_config |
| 104 | + >>> |
| 105 | + >>> request_headers = { |
| 106 | + ... "authorization": "Bearer token123", |
| 107 | + ... "x-tenant-id": "acme-corp", |
| 108 | + ... "x-trace-id": "trace-456", |
| 109 | + ... "user-agent": "TestClient/1.0" |
| 110 | + ... } |
| 111 | + >>> base_headers = {"Content-Type": "application/json"} |
| 112 | + >>> |
| 113 | + >>> result = get_passthrough_headers(request_headers, base_headers, mock_db) |
| 114 | + >>> sorted(result.items()) |
| 115 | + [('Content-Type', 'application/json'), ('X-Tenant-Id', 'acme-corp'), ('X-Trace-Id', 'trace-456')] |
| 116 | +
|
| 117 | + Gateway-specific configuration override: |
| 118 | + >>> mock_gateway = Mock() |
| 119 | + >>> mock_gateway.passthrough_headers = ["X-Custom-Header"] |
| 120 | + >>> mock_gateway.auth_type = None |
| 121 | + >>> request_headers = { |
| 122 | + ... "x-custom-header": "custom-value", |
| 123 | + ... "x-tenant-id": "should-be-ignored" |
| 124 | + ... } |
| 125 | + >>> |
| 126 | + >>> result = get_passthrough_headers(request_headers, base_headers, mock_db, mock_gateway) |
| 127 | + >>> sorted(result.items()) |
| 128 | + [('Content-Type', 'application/json'), ('X-Custom-Header', 'custom-value')] |
| 129 | +
|
| 130 | + Authorization header conflict with basic auth: |
| 131 | + >>> mock_gateway.auth_type = "basic" |
| 132 | + >>> mock_gateway.passthrough_headers = ["Authorization", "X-Tenant-Id"] |
| 133 | + >>> request_headers = { |
| 134 | + ... "authorization": "Bearer should-be-blocked", |
| 135 | + ... "x-tenant-id": "acme-corp" |
| 136 | + ... } |
| 137 | + >>> |
| 138 | + >>> result = get_passthrough_headers(request_headers, base_headers, mock_db, mock_gateway) |
| 139 | + >>> sorted(result.items()) # Authorization blocked due to basic auth conflict |
| 140 | + [('Content-Type', 'application/json'), ('X-Tenant-Id', 'acme-corp')] |
| 141 | +
|
| 142 | + Base header conflict prevention: |
| 143 | + >>> base_headers_with_conflict = {"Content-Type": "application/json", "x-tenant-id": "from-base"} |
| 144 | + >>> request_headers = {"x-tenant-id": "from-request"} |
| 145 | + >>> mock_gateway.auth_type = None |
| 146 | + >>> mock_gateway.passthrough_headers = ["X-Tenant-Id"] |
| 147 | + >>> |
| 148 | + >>> result = get_passthrough_headers(request_headers, base_headers_with_conflict, mock_db, mock_gateway) |
| 149 | + >>> result["x-tenant-id"] # Base header preserved, request header blocked |
| 150 | + 'from-base' |
| 151 | +
|
| 152 | + Empty allowed headers (no passthrough): |
| 153 | + >>> empty_global_config = Mock() |
| 154 | + >>> empty_global_config.passthrough_headers = [] |
| 155 | + >>> mock_db.query.return_value.first.return_value = empty_global_config |
| 156 | + >>> |
| 157 | + >>> request_headers = {"x-tenant-id": "should-be-ignored"} |
| 158 | + >>> result = get_passthrough_headers(request_headers, {"Content-Type": "application/json"}, mock_db) |
| 159 | + >>> result |
| 160 | + {'Content-Type': 'application/json'} |
| 161 | +
|
| 162 | + Note: |
| 163 | + Header names are matched case-insensitively but preserved in their original |
| 164 | + case from the allowed_headers configuration. Request header values are |
| 165 | + matched case-insensitively against the request_headers dictionary. |
28 | 166 | """ |
29 | 167 | passthrough_headers = base_headers.copy() |
30 | 168 |
|
|
0 commit comments