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