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