1use crate::seqstring::{global_bytes, global_string};
57use crate::stack::{Stack, pop, push};
58use crate::value::{MapKey, Value};
59
60use std::collections::HashMap;
61use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs};
62use std::sync::LazyLock;
63use std::time::Duration;
64
65const DEFAULT_TIMEOUT_SECS: u64 = 30;
67
68const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
70
71static HTTP_AGENT: LazyLock<ureq::Agent> = LazyLock::new(|| {
74 ureq::AgentBuilder::new()
75 .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
76 .build()
77});
78
79fn is_dangerous_ipv4(ip: Ipv4Addr) -> bool {
81 if ip.is_loopback() {
83 return true;
84 }
85 if ip.octets()[0] == 10 {
87 return true;
88 }
89 if ip.octets()[0] == 172 && (ip.octets()[1] >= 16 && ip.octets()[1] <= 31) {
91 return true;
92 }
93 if ip.octets()[0] == 192 && ip.octets()[1] == 168 {
95 return true;
96 }
97 if ip.octets()[0] == 169 && ip.octets()[1] == 254 {
99 return true;
100 }
101 if ip.is_broadcast() {
103 return true;
104 }
105 false
106}
107
108fn is_dangerous_ipv6(ip: Ipv6Addr) -> bool {
110 if ip.is_loopback() {
112 return true;
113 }
114 let segments = ip.segments();
116 if (segments[0] & 0xffc0) == 0xfe80 {
117 return true;
118 }
119 if (segments[0] & 0xfe00) == 0xfc00 {
121 return true;
122 }
123 if let Some(ipv4) = ip.to_ipv4_mapped() {
125 return is_dangerous_ipv4(ipv4);
126 }
127 false
128}
129
130fn is_dangerous_ip(ip: IpAddr) -> bool {
132 match ip {
133 IpAddr::V4(v4) => is_dangerous_ipv4(v4),
134 IpAddr::V6(v6) => is_dangerous_ipv6(v6),
135 }
136}
137
138fn validate_url_for_ssrf(url: &str) -> Result<(), String> {
141 let parsed = match url::Url::parse(url) {
143 Ok(u) => u,
144 Err(e) => return Err(format!("Invalid URL: {}", e)),
145 };
146
147 match parsed.scheme() {
149 "http" | "https" => {}
150 scheme => {
151 return Err(format!(
152 "Blocked scheme '{}': only http/https allowed",
153 scheme
154 ));
155 }
156 }
157
158 let host = match parsed.host_str() {
160 Some(h) => h,
161 None => return Err("URL has no host".to_string()),
162 };
163
164 let host_lower = host.to_lowercase();
166 if host_lower == "localhost"
167 || host_lower == "localhost.localdomain"
168 || host_lower.ends_with(".localhost")
169 {
170 return Err("Blocked: localhost access not allowed".to_string());
171 }
172
173 let port = parsed
175 .port()
176 .unwrap_or(if parsed.scheme() == "https" { 443 } else { 80 });
177
178 let addr_str = format!("{}:{}", host, port);
180 match addr_str.to_socket_addrs() {
181 Ok(addrs) => {
182 for addr in addrs {
183 if is_dangerous_ip(addr.ip()) {
184 return Err(format!(
185 "Blocked: {} resolves to private/internal IP {}",
186 host,
187 addr.ip()
188 ));
189 }
190 }
191 }
192 Err(_) => {
193 }
196 }
197
198 Ok(())
199}
200
201fn build_response_map(status: i64, body: Vec<u8>, ok: bool, error: Option<String>) -> Value {
209 let mut map: HashMap<MapKey, Value> = HashMap::new();
210
211 map.insert(
212 MapKey::String(global_string("status".to_string())),
213 Value::Int(status),
214 );
215 map.insert(
216 MapKey::String(global_string("body".to_string())),
217 Value::String(global_bytes(body)),
218 );
219 map.insert(
220 MapKey::String(global_string("ok".to_string())),
221 Value::Bool(ok),
222 );
223
224 if let Some(err) = error {
225 map.insert(
226 MapKey::String(global_string("error".to_string())),
227 Value::String(global_string(err)),
228 );
229 }
230
231 Value::Map(Box::new(map))
232}
233
234fn error_response(error: String) -> Value {
236 build_response_map(0, Vec::new(), false, Some(error))
237}
238
239#[unsafe(no_mangle)]
248pub unsafe extern "C" fn patch_seq_http_get(stack: Stack) -> Stack {
249 assert!(!stack.is_null(), "http.get: stack is empty");
250
251 let (stack, url_value) = unsafe { pop(stack) };
252
253 match url_value {
254 Value::String(url) => {
255 let response = perform_get(url.as_str_or_empty());
256 unsafe { push(stack, response) }
257 }
258 _ => panic!(
259 "http.get: expected String (URL) on stack, got {:?}",
260 url_value
261 ),
262 }
263}
264
265#[unsafe(no_mangle)]
274pub unsafe extern "C" fn patch_seq_http_post(stack: Stack) -> Stack {
275 assert!(!stack.is_null(), "http.post: stack is empty");
276
277 let (stack, content_type_value) = unsafe { pop(stack) };
278 let (stack, body_value) = unsafe { pop(stack) };
279 let (stack, url_value) = unsafe { pop(stack) };
280
281 match (url_value, body_value, content_type_value) {
282 (Value::String(url), Value::String(body), Value::String(content_type)) => {
283 let response = perform_post(
285 url.as_str_or_empty(),
286 body.as_bytes(),
287 content_type.as_str_or_empty(),
288 );
289 unsafe { push(stack, response) }
290 }
291 (url, body, ct) => panic!(
292 "http.post: expected (String, String, String) on stack, got ({:?}, {:?}, {:?})",
293 url, body, ct
294 ),
295 }
296}
297
298#[unsafe(no_mangle)]
307pub unsafe extern "C" fn patch_seq_http_put(stack: Stack) -> Stack {
308 assert!(!stack.is_null(), "http.put: stack is empty");
309
310 let (stack, content_type_value) = unsafe { pop(stack) };
311 let (stack, body_value) = unsafe { pop(stack) };
312 let (stack, url_value) = unsafe { pop(stack) };
313
314 match (url_value, body_value, content_type_value) {
315 (Value::String(url), Value::String(body), Value::String(content_type)) => {
316 let response = perform_put(
318 url.as_str_or_empty(),
319 body.as_bytes(),
320 content_type.as_str_or_empty(),
321 );
322 unsafe { push(stack, response) }
323 }
324 (url, body, ct) => panic!(
325 "http.put: expected (String, String, String) on stack, got ({:?}, {:?}, {:?})",
326 url, body, ct
327 ),
328 }
329}
330
331#[unsafe(no_mangle)]
340pub unsafe extern "C" fn patch_seq_http_delete(stack: Stack) -> Stack {
341 assert!(!stack.is_null(), "http.delete: stack is empty");
342
343 let (stack, url_value) = unsafe { pop(stack) };
344
345 match url_value {
346 Value::String(url) => {
347 let response = perform_delete(url.as_str_or_empty());
348 unsafe { push(stack, response) }
349 }
350 _ => panic!(
351 "http.delete: expected String (URL) on stack, got {:?}",
352 url_value
353 ),
354 }
355}
356
357fn read_response_bytes(response: ureq::Response) -> Result<Vec<u8>, std::io::Error> {
362 use std::io::Read;
363 let mut reader = response.into_reader().take((MAX_BODY_SIZE as u64) + 1);
364 let mut buf = Vec::new();
365 reader.read_to_end(&mut buf)?;
366 Ok(buf)
367}
368
369fn handle_response(result: Result<ureq::Response, ureq::Error>) -> Value {
371 match result {
372 Ok(response) => {
373 let status = response.status() as i64;
374 let ok = (200..300).contains(&response.status());
375
376 match read_response_bytes(response) {
377 Ok(body) => {
378 if body.len() > MAX_BODY_SIZE {
379 error_response(format!(
380 "Response body too large ({} bytes, max {})",
381 body.len(),
382 MAX_BODY_SIZE
383 ))
384 } else {
385 build_response_map(status, body, ok, None)
386 }
387 }
388 Err(e) => error_response(format!("Failed to read response body: {}", e)),
389 }
390 }
391 Err(ureq::Error::Status(code, response)) => {
392 let body = read_response_bytes(response).unwrap_or_default();
394 build_response_map(
395 code as i64,
396 body,
397 false,
398 Some(format!("HTTP error: {}", code)),
399 )
400 }
401 Err(ureq::Error::Transport(e)) => {
402 error_response(format!("Connection error: {}", e))
404 }
405 }
406}
407
408fn perform_get(url: &str) -> Value {
410 if let Err(msg) = validate_url_for_ssrf(url) {
412 return error_response(msg);
413 }
414 handle_response(HTTP_AGENT.get(url).call())
415}
416
417fn perform_post(url: &str, body: &[u8], content_type: &str) -> Value {
421 if let Err(msg) = validate_url_for_ssrf(url) {
423 return error_response(msg);
424 }
425 handle_response(
426 HTTP_AGENT
427 .post(url)
428 .set("Content-Type", content_type)
429 .send_bytes(body),
430 )
431}
432
433fn perform_put(url: &str, body: &[u8], content_type: &str) -> Value {
435 if let Err(msg) = validate_url_for_ssrf(url) {
437 return error_response(msg);
438 }
439 handle_response(
440 HTTP_AGENT
441 .put(url)
442 .set("Content-Type", content_type)
443 .send_bytes(body),
444 )
445}
446
447fn perform_delete(url: &str) -> Value {
449 if let Err(msg) = validate_url_for_ssrf(url) {
451 return error_response(msg);
452 }
453 handle_response(HTTP_AGENT.delete(url).call())
454}
455
456#[cfg(test)]
457mod tests;