forked from modelcontextprotocol/rust-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhttp_header.rs
More file actions
126 lines (111 loc) · 4.08 KB
/
http_header.rs
File metadata and controls
126 lines (111 loc) · 4.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
pub const HEADER_SESSION_ID: &str = "Mcp-Session-Id";
pub const HEADER_LAST_EVENT_ID: &str = "Last-Event-Id";
pub const HEADER_MCP_PROTOCOL_VERSION: &str = "MCP-Protocol-Version";
pub const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream";
pub const JSON_MIME_TYPE: &str = "application/json";
/// Reserved headers that must not be overridden by user-supplied custom headers.
/// `MCP-Protocol-Version` is in this list but is allowed through because the worker
/// injects it after initialization.
#[allow(dead_code)]
pub(crate) const RESERVED_HEADERS: &[&str] = &[
"accept",
HEADER_SESSION_ID,
HEADER_MCP_PROTOCOL_VERSION, // allowed through by validate_custom_header; worker injects it post-init
HEADER_LAST_EVENT_ID,
];
/// Checks whether a custom header name is allowed.
/// Returns `Ok(())` if allowed, `Err(name)` if rejected as reserved.
/// `MCP-Protocol-Version` is reserved but allowed through (the worker injects it post-init).
#[cfg(feature = "client-side-sse")]
pub(crate) fn validate_custom_header(name: &http::HeaderName) -> Result<(), String> {
if RESERVED_HEADERS
.iter()
.any(|&r| name.as_str().eq_ignore_ascii_case(r))
{
if name
.as_str()
.eq_ignore_ascii_case(HEADER_MCP_PROTOCOL_VERSION)
{
return Ok(());
}
return Err(name.to_string());
}
Ok(())
}
/// Extracts the `scope=` parameter from a `WWW-Authenticate` header value.
/// Handles both quoted (`scope="files:read files:write"`) and unquoted (`scope=read:data`) forms.
#[cfg(feature = "client-side-sse")]
pub(crate) fn extract_scope_from_header(header: &str) -> Option<String> {
let header_lowercase = header.to_ascii_lowercase();
let scope_key = "scope=";
if let Some(pos) = header_lowercase.find(scope_key) {
let start = pos + scope_key.len();
let value_slice = &header[start..];
if let Some(stripped) = value_slice.strip_prefix('"') {
if let Some(end_quote) = stripped.find('"') {
return Some(stripped[..end_quote].to_string());
}
} else {
let end = value_slice
.find(|c: char| c == ',' || c == ';' || c.is_whitespace())
.unwrap_or(value_slice.len());
if end > 0 {
return Some(value_slice[..end].to_string());
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_scope_quoted() {
let header = r#"Bearer error="insufficient_scope", scope="files:read files:write""#;
assert_eq!(
extract_scope_from_header(header),
Some("files:read files:write".to_string())
);
}
#[test]
fn extract_scope_unquoted() {
let header = r#"Bearer scope=read:data, error="insufficient_scope""#;
assert_eq!(
extract_scope_from_header(header),
Some("read:data".to_string())
);
}
#[test]
fn extract_scope_missing() {
let header = r#"Bearer error="invalid_token""#;
assert_eq!(extract_scope_from_header(header), None);
}
#[test]
fn extract_scope_empty_header() {
assert_eq!(extract_scope_from_header("Bearer"), None);
}
#[cfg(feature = "client-side-sse")]
#[test]
fn validate_rejects_reserved_accept() {
let name = http::HeaderName::from_static("accept");
assert!(validate_custom_header(&name).is_err());
}
#[cfg(feature = "client-side-sse")]
#[test]
fn validate_rejects_reserved_session_id() {
let name = http::HeaderName::from_static("mcp-session-id");
assert!(validate_custom_header(&name).is_err());
}
#[cfg(feature = "client-side-sse")]
#[test]
fn validate_allows_mcp_protocol_version() {
let name = http::HeaderName::from_static("mcp-protocol-version");
assert!(validate_custom_header(&name).is_ok());
}
#[cfg(feature = "client-side-sse")]
#[test]
fn validate_allows_custom_header() {
let name = http::HeaderName::from_static("x-custom");
assert!(validate_custom_header(&name).is_ok());
}
}