Skip to main content

lc_tools/extended/
http.rs

1//! HTTP tool with SSRF protection
2
3use 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
12/// HTTP request tool (GET/POST) with SSRF protection.
13pub 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    /// Allow requests to private/internal IP addresses (SSRF opt-in).
40    pub fn with_allow_private_ips(mut self, allow: bool) -> Self {
41        self.allow_private_ips = allow;
42        self
43    }
44
45    /// Check SSRF protection before making a request.
46    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}