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::{guarded_get, 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    /// Creates an HTTP tool with a 30s timeout and SSRF protection enabled.
20    pub fn new() -> Self {
21        Self {
22            client: reqwest::Client::builder()
23                .timeout(Duration::from_secs(30))
24                // SSRF: 禁用自动重定向,由 guarded_get 逐跳重查
25                .redirect(reqwest::redirect::Policy::none())
26                .build()
27                .unwrap_or_else(|_| reqwest::Client::new()),
28            allow_private_ips: false,
29        }
30    }
31
32    /// Creates an HTTP tool with a custom timeout (SSRF protection enabled).
33    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    /// Allow requests to private/internal IP addresses (SSRF opt-in).
45    pub fn with_allow_private_ips(mut self, allow: bool) -> Self {
46        self.allow_private_ips = allow;
47        self
48    }
49
50    /// Check SSRF protection before making a request.
51    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    /// Sends a GET request, following redirects with SSRF checks per hop.
66    pub async fn get(&self, url: &str) -> Result<String, ToolError> {
67        // SSRF: guarded_get 逐跳检查并手动跟随重定向
68        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    /// Sends a POST request with a JSON body (single-hop SSRF check).
76    pub async fn post(&self, url: &str, body: Value) -> Result<String, ToolError> {
77        // POST 已禁用自动重定向(3xx 原样返回),单跳 SSRF 检查即可
78        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}