1use async_trait::async_trait;
6use rucora_core::{
7 error::ToolError,
8 tool::{Tool, ToolCategory},
9};
10use serde_json::{Value, json};
11use std::time::Duration;
12use tracing::{info, warn};
13
14use super::security::validate_public_http_url;
15
16const DEFAULT_TIMEOUT_SECS: u64 = 30;
18
19const MAX_RESPONSE_SIZE: usize = 5 * 1024 * 1024;
21
22pub struct HttpRequestTool {
47 allowed_domains: Option<Vec<String>>,
49 blocked_domains: Option<Vec<String>>,
51 max_redirects: u32,
53}
54
55impl HttpRequestTool {
56 pub fn new() -> Self {
58 Self {
59 allowed_domains: None,
60 blocked_domains: None,
61 max_redirects: 3,
62 }
63 }
64
65 pub fn with_allowed_domains(mut self, domains: Vec<String>) -> Self {
67 self.allowed_domains = Some(domains);
68 self
69 }
70
71 pub fn with_blocked_domains(mut self, domains: Vec<String>) -> Self {
73 self.blocked_domains = Some(domains);
74 self
75 }
76
77 pub fn with_max_redirects(mut self, max: u32) -> Self {
79 self.max_redirects = max;
80 self
81 }
82}
83
84impl Default for HttpRequestTool {
85 fn default() -> Self {
86 Self::new()
87 }
88}
89
90#[async_trait]
91impl Tool for HttpRequestTool {
92 fn name(&self) -> &str {
93 "http_request"
94 }
95
96 fn description(&self) -> Option<&str> {
97 Some("发送 HTTP 请求(有安全限制:禁止内网访问,支持域名白名单/黑名单)")
98 }
99
100 fn categories(&self) -> &'static [ToolCategory] {
101 &[ToolCategory::Network]
102 }
103
104 fn input_schema(&self) -> Value {
105 json!({
106 "type": "object",
107 "properties": {
108 "method": {
109 "type": "string",
110 "description": "HTTP 方法",
111 "enum": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
112 },
113 "url": {
114 "type": "string",
115 "description": "请求 URL(必须是 http/https 协议)"
116 },
117 "headers": {
118 "type": "object",
119 "additionalProperties": {
120 "type": "string"
121 },
122 "description": "请求头"
123 },
124 "body": {
125 "type": "string",
126 "description": "请求体"
127 },
128 "timeout": {
129 "type": "integer",
130 "description": "超时时间(秒),默认 30 秒",
131 "default": 30
132 }
133 },
134 "required": ["url"]
135 })
136 }
137
138 async fn call(&self, input: Value) -> Result<Value, ToolError> {
139 let url = input
140 .get("url")
141 .and_then(|v| v.as_str())
142 .ok_or_else(|| ToolError::Message("缺少必需的 'url' 字段".to_string()))?;
143
144 validate_public_http_url(
146 url,
147 self.allowed_domains.as_deref(),
148 self.blocked_domains.as_deref(),
149 )
150 .await?;
151
152 let method_str = input
153 .get("method")
154 .and_then(|v| v.as_str())
155 .unwrap_or("GET")
156 .to_uppercase();
157
158 let timeout_secs = input
159 .get("timeout")
160 .and_then(|v| v.as_u64())
161 .unwrap_or(DEFAULT_TIMEOUT_SECS);
162
163 info!(
164 tool.name = "http_request",
165 http.method = %method_str,
166 http.url = %url,
167 http.timeout_secs = timeout_secs,
168 "http_request.start"
169 );
170
171 let start = std::time::Instant::now();
172
173 let method = match method_str.as_str() {
175 "GET" => reqwest::Method::GET,
176 "POST" => reqwest::Method::POST,
177 "PUT" => reqwest::Method::PUT,
178 "DELETE" => reqwest::Method::DELETE,
179 "PATCH" => reqwest::Method::PATCH,
180 "HEAD" => reqwest::Method::HEAD,
181 "OPTIONS" => reqwest::Method::OPTIONS,
182 _ => {
183 return Err(ToolError::Message(format!(
184 "不支持的 HTTP 方法:{method_str}"
185 )));
186 }
187 };
188
189 let _configured_redirect_limit = self.max_redirects;
191 let redirect_policy = reqwest::redirect::Policy::none();
193 let client = reqwest::Client::builder()
194 .timeout(Duration::from_secs(timeout_secs))
195 .redirect(redirect_policy)
196 .user_agent("Mozilla/5.0 (compatible; rucora/0.1)")
197 .build()
198 .map_err(|e| ToolError::Message(format!("HTTP 客户端创建失败:{e}")))?;
199
200 let mut request = client.request(method, url);
202
203 if let Some(headers_map) = input.get("headers").and_then(|v| v.as_object()) {
205 for (key, value) in headers_map {
206 if let Some(val_str) = value.as_str() {
207 request = request.header(key, val_str);
208 }
209 }
210 }
211
212 if let Some(body_str) = input.get("body").and_then(|v| v.as_str()) {
214 request = request.body(body_str.to_string());
215 }
216
217 let response = request.send().await.map_err(|e| {
219 warn!(
220 tool.name = "http_request",
221 http.method = %method_str,
222 http.url = %url,
223 error = %e,
224 "http_request.error"
225 );
226 ToolError::Message(format!("HTTP 请求失败:{e}"))
227 })?;
228
229 let status = response.status().as_u16();
230
231 let body_bytes = response
233 .bytes()
234 .await
235 .map_err(|e| ToolError::Message(format!("读取响应体失败:{e}")))?;
236
237 if body_bytes.len() > MAX_RESPONSE_SIZE {
238 return Err(ToolError::Message(format!(
239 "响应体过大({} 字节),超过限制({} 字节)",
240 body_bytes.len(),
241 MAX_RESPONSE_SIZE
242 )));
243 }
244
245 let body = String::from_utf8_lossy(&body_bytes).to_string();
246
247 let elapsed_ms = start.elapsed().as_millis() as u64;
248 let body_len = body.len();
249
250 info!(
251 tool.name = "http_request",
252 http.method = %method_str,
253 http.url = %url,
254 http.status = status,
255 http.success = (200..300).contains(&status),
256 http.body_len = body_len,
257 http.elapsed_ms = elapsed_ms,
258 "http_request.done"
259 );
260
261 Ok(json!({
262 "url": url,
263 "status": status,
264 "body": body,
265 "body_len": body_len,
266 "success": (200..300).contains(&status),
267 "elapsed_ms": elapsed_ms
268 }))
269 }
270}