lc_tools/extended/
http.rs1use std::time::Duration;
4
5use async_trait::async_trait;
6use serde_json::Value;
7
8use crate::ssrf::url_points_to_private_ip;
9use lc_core::tools::ToolError;
10use lc_core::BaseTool;
11
12pub struct HTTPTool {
14 client: reqwest::Client,
15 allow_private_ips: bool,
16}
17
18impl HTTPTool {
19 pub fn new() -> Self {
20 Self {
21 client: reqwest::Client::builder()
22 .timeout(Duration::from_secs(30))
23 .build()
24 .unwrap_or_else(|_| reqwest::Client::new()),
25 allow_private_ips: false,
26 }
27 }
28
29 pub fn with_timeout(timeout: Duration) -> Self {
30 Self {
31 client: reqwest::Client::builder()
32 .timeout(timeout)
33 .build()
34 .unwrap_or_else(|_| reqwest::Client::new()),
35 allow_private_ips: false,
36 }
37 }
38
39 pub fn with_allow_private_ips(mut self, allow: bool) -> Self {
41 self.allow_private_ips = allow;
42 self
43 }
44
45 async fn check_ssrf(&self, url: &str) -> Result<(), ToolError> {
47 if self.allow_private_ips {
48 return Ok(());
49 }
50 if url_points_to_private_ip(url).await? {
51 return Err(ToolError::ExecutionFailed(
52 "Request to private/internal IP address is blocked by SSRF protection. \
53 Call .with_allow_private_ips(true) to allow."
54 .to_string(),
55 ));
56 }
57 Ok(())
58 }
59
60 pub async fn get(&self, url: &str) -> Result<String, ToolError> {
61 self.check_ssrf(url).await?;
62 self.client
63 .get(url)
64 .send()
65 .await
66 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?
67 .text()
68 .await
69 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))
70 }
71
72 pub async fn post(&self, url: &str, body: Value) -> Result<String, ToolError> {
73 self.check_ssrf(url).await?;
74 self.client
75 .post(url)
76 .json(&body)
77 .send()
78 .await
79 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))?
80 .text()
81 .await
82 .map_err(|e| ToolError::ExecutionFailed(e.to_string()))
83 }
84}
85
86impl Default for HTTPTool {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92#[async_trait]
93impl BaseTool for HTTPTool {
94 fn name(&self) -> &str {
95 "http_request"
96 }
97
98 fn description(&self) -> &str {
99 "Make HTTP requests. Input JSON: {\"url\": \"...\", \"method\": \"get|post\", \"body\": {...}}. \
100 SSRF protection enabled by default (blocks private IPs)."
101 }
102
103 async fn run(&self, input: String) -> Result<String, ToolError> {
104 let v: Value =
105 serde_json::from_str(&input).map_err(|e| ToolError::InvalidInput(e.to_string()))?;
106 let url = v
107 .get("url")
108 .and_then(|x| x.as_str())
109 .ok_or_else(|| ToolError::InvalidInput("Missing 'url' field".to_string()))?;
110 let method = v.get("method").and_then(|x| x.as_str()).unwrap_or("get");
111 match method {
112 "get" => self.get(url).await,
113 "post" => {
114 self.post(url, v.get("body").cloned().unwrap_or(Value::Null))
115 .await
116 }
117 other => Err(ToolError::InvalidInput(format!(
118 "Unknown method: {}. Supported: get, post",
119 other
120 ))),
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::ssrf::is_private_ip;
129 use std::net::IpAddr;
130
131 #[test]
132 fn test_name_description() {
133 let t = HTTPTool::new();
134 assert_eq!(t.name(), "http_request");
135 assert!(t.description().contains("HTTP"));
136 }
137
138 #[test]
139 fn test_private_ip_detection() {
140 assert!(is_private_ip(&IpAddr::from([127, 0, 0, 1])));
141 assert!(is_private_ip(&IpAddr::from([10, 0, 0, 1])));
142 assert!(is_private_ip(&IpAddr::from([172, 16, 0, 1])));
143 assert!(is_private_ip(&IpAddr::from([172, 31, 255, 255])));
144 assert!(is_private_ip(&IpAddr::from([192, 168, 1, 1])));
145 assert!(is_private_ip(&IpAddr::from([169, 254, 169, 254])));
146 assert!(is_private_ip(&IpAddr::from([0, 0, 0, 0])));
147
148 assert!(!is_private_ip(&IpAddr::from([8, 8, 8, 8])));
149 assert!(!is_private_ip(&IpAddr::from([1, 1, 1, 1])));
150 assert!(!is_private_ip(&IpAddr::from([172, 15, 0, 1])));
151 assert!(!is_private_ip(&IpAddr::from([172, 32, 0, 1])));
152 }
153
154 #[tokio::test]
155 async fn test_ssrf_blocks_localhost() {
156 let tool = HTTPTool::new();
157 let result = tool.check_ssrf("http://127.0.0.1:6379/").await;
158 assert!(result.is_err());
159 assert!(result.unwrap_err().to_string().contains("SSRF"));
160 }
161
162 #[tokio::test]
163 async fn test_ssrf_blocks_cloud_metadata() {
164 let tool = HTTPTool::new();
165 let result = tool
166 .check_ssrf("http://169.254.169.254/latest/meta-data/")
167 .await;
168 assert!(result.is_err());
169 }
170
171 #[tokio::test]
172 async fn test_ssrf_allows_when_opt_in() {
173 let tool = HTTPTool::new().with_allow_private_ips(true);
174 let result = tool.check_ssrf("http://127.0.0.1:6379/").await;
175 assert!(result.is_ok());
176 }
177
178 #[tokio::test]
179 async fn test_run_invalid_json() {
180 let t = HTTPTool::new();
181 assert!(t.run("not json".to_string()).await.is_err());
182 }
183
184 #[tokio::test]
185 async fn test_run_missing_url() {
186 let t = HTTPTool::new();
187 assert!(t.run(r#"{"method":"get"}"#.to_string()).await.is_err());
188 }
189
190 #[tokio::test]
191 async fn test_run_unknown_method() {
192 let t = HTTPTool::new();
193 let r = t
194 .run(r#"{"url":"http://x","method":"put"}"#.to_string())
195 .await;
196 assert!(r.is_err());
197 }
198}