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    pub fn new() -> Self {
20        Self {
21            client: reqwest::Client::builder()
22                .timeout(Duration::from_secs(30))
23                // SSRF: 禁用自动重定向,由 guarded_get 逐跳重查
24                .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    /// Allow requests to private/internal IP addresses (SSRF opt-in).
43    pub fn with_allow_private_ips(mut self, allow: bool) -> Self {
44        self.allow_private_ips = allow;
45        self
46    }
47
48    /// Check SSRF protection before making a request.
49    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        // SSRF: guarded_get 逐跳检查并手动跟随重定向
65        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        // POST 已禁用自动重定向(3xx 原样返回),单跳 SSRF 检查即可
74        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}