turbomcp_auth/oauth2/
validation.rs1use turbomcp_protocol::{Error as McpError, Result as McpResult};
9
10pub fn validate_canonical_resource_uri(uri: &str) -> McpResult<()> {
29 use url::Url;
30
31 let scheme_end = uri
34 .find("://")
35 .ok_or_else(|| McpError::validation("Resource URI must have a valid scheme".to_string()))?;
36
37 let scheme = &uri[..scheme_end];
38 if scheme != scheme.to_lowercase() {
39 return Err(McpError::validation(
40 "Resource URI must use canonical form (lowercase scheme and host)".to_string(),
41 ));
42 }
43
44 let parsed =
45 Url::parse(uri).map_err(|e| McpError::validation(format!("Invalid resource URI: {e}")))?;
46
47 if parsed.scheme() != "https" && parsed.scheme() != "http" {
49 return Err(McpError::validation(
50 "Resource URI must use http or https scheme".to_string(),
51 ));
52 }
53
54 if parsed.fragment().is_some() {
55 return Err(McpError::validation(
56 "Resource URI must not contain fragment".to_string(),
57 ));
58 }
59
60 if parsed.host_str().is_none() {
62 return Err(McpError::validation(
63 "Resource URI must include host".to_string(),
64 ));
65 }
66
67 let _host = parsed.host_str().expect("host validated above");
69
70 let host_start = uri.find("://").expect("scheme checked above") + 3;
73 let host_in_uri = &uri[host_start..];
74 let host_end = host_in_uri
75 .find(['/', ':', '?', '#'])
76 .unwrap_or(host_in_uri.len());
77 let original_host = &host_in_uri[..host_end];
78
79 if original_host != original_host.to_lowercase() {
80 return Err(McpError::validation(
81 "Resource URI must use canonical form (lowercase scheme and host)".to_string(),
82 ));
83 }
84
85 Ok(())
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn test_valid_https_uri() {
94 assert!(validate_canonical_resource_uri("https://example.com/resource").is_ok());
95 }
96
97 #[test]
98 fn test_valid_http_uri() {
99 assert!(validate_canonical_resource_uri("http://example.com/resource").is_ok());
100 }
101
102 #[test]
103 fn test_non_canonical_uppercase_host() {
104 let result = validate_canonical_resource_uri("https://Example.COM/resource");
105 assert!(result.is_err());
106 assert!(result.unwrap_err().to_string().contains("canonical form"));
107 }
108
109 #[test]
110 fn test_non_canonical_uppercase_scheme() {
111 let result = validate_canonical_resource_uri("HTTPS://example.com/resource");
112 assert!(result.is_err());
113 assert!(result.unwrap_err().to_string().contains("canonical form"));
114 }
115
116 #[test]
117 fn test_missing_host() {
118 let result = validate_canonical_resource_uri("file:///etc/passwd");
120 assert!(result.is_err());
121 assert!(
122 result
123 .unwrap_err()
124 .to_string()
125 .contains("http or https scheme")
126 );
127
128 }
131
132 #[test]
133 fn test_fragment_not_allowed() {
134 let result = validate_canonical_resource_uri("https://example.com/resource#fragment");
135 assert!(result.is_err());
136 assert!(result.unwrap_err().to_string().contains("fragment"));
137 }
138
139 #[test]
140 fn test_invalid_scheme() {
141 let result = validate_canonical_resource_uri("ftp://example.com/resource");
142 assert!(result.is_err());
143 assert!(
144 result
145 .unwrap_err()
146 .to_string()
147 .contains("http or https scheme")
148 );
149 }
150}