1use crate::seqstring::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: String, ok: bool, error: Option<String>) -> Value {
203 let mut map: HashMap<MapKey, Value> = HashMap::new();
204
205 map.insert(
206 MapKey::String(global_string("status".to_string())),
207 Value::Int(status),
208 );
209 map.insert(
210 MapKey::String(global_string("body".to_string())),
211 Value::String(global_string(body)),
212 );
213 map.insert(
214 MapKey::String(global_string("ok".to_string())),
215 Value::Bool(ok),
216 );
217
218 if let Some(err) = error {
219 map.insert(
220 MapKey::String(global_string("error".to_string())),
221 Value::String(global_string(err)),
222 );
223 }
224
225 Value::Map(Box::new(map))
226}
227
228fn error_response(error: String) -> Value {
230 build_response_map(0, String::new(), false, Some(error))
231}
232
233#[unsafe(no_mangle)]
242pub unsafe extern "C" fn patch_seq_http_get(stack: Stack) -> Stack {
243 assert!(!stack.is_null(), "http.get: stack is empty");
244
245 let (stack, url_value) = unsafe { pop(stack) };
246
247 match url_value {
248 Value::String(url) => {
249 let response = perform_get(url.as_str());
250 unsafe { push(stack, response) }
251 }
252 _ => panic!(
253 "http.get: expected String (URL) on stack, got {:?}",
254 url_value
255 ),
256 }
257}
258
259#[unsafe(no_mangle)]
268pub unsafe extern "C" fn patch_seq_http_post(stack: Stack) -> Stack {
269 assert!(!stack.is_null(), "http.post: stack is empty");
270
271 let (stack, content_type_value) = unsafe { pop(stack) };
272 let (stack, body_value) = unsafe { pop(stack) };
273 let (stack, url_value) = unsafe { pop(stack) };
274
275 match (url_value, body_value, content_type_value) {
276 (Value::String(url), Value::String(body), Value::String(content_type)) => {
277 let response = perform_post(url.as_str(), body.as_str(), content_type.as_str());
278 unsafe { push(stack, response) }
279 }
280 (url, body, ct) => panic!(
281 "http.post: expected (String, String, String) on stack, got ({:?}, {:?}, {:?})",
282 url, body, ct
283 ),
284 }
285}
286
287#[unsafe(no_mangle)]
296pub unsafe extern "C" fn patch_seq_http_put(stack: Stack) -> Stack {
297 assert!(!stack.is_null(), "http.put: stack is empty");
298
299 let (stack, content_type_value) = unsafe { pop(stack) };
300 let (stack, body_value) = unsafe { pop(stack) };
301 let (stack, url_value) = unsafe { pop(stack) };
302
303 match (url_value, body_value, content_type_value) {
304 (Value::String(url), Value::String(body), Value::String(content_type)) => {
305 let response = perform_put(url.as_str(), body.as_str(), content_type.as_str());
306 unsafe { push(stack, response) }
307 }
308 (url, body, ct) => panic!(
309 "http.put: expected (String, String, String) on stack, got ({:?}, {:?}, {:?})",
310 url, body, ct
311 ),
312 }
313}
314
315#[unsafe(no_mangle)]
324pub unsafe extern "C" fn patch_seq_http_delete(stack: Stack) -> Stack {
325 assert!(!stack.is_null(), "http.delete: stack is empty");
326
327 let (stack, url_value) = unsafe { pop(stack) };
328
329 match url_value {
330 Value::String(url) => {
331 let response = perform_delete(url.as_str());
332 unsafe { push(stack, response) }
333 }
334 _ => panic!(
335 "http.delete: expected String (URL) on stack, got {:?}",
336 url_value
337 ),
338 }
339}
340
341fn handle_response(result: Result<ureq::Response, ureq::Error>) -> Value {
343 match result {
344 Ok(response) => {
345 let status = response.status() as i64;
346 let ok = (200..300).contains(&response.status());
347
348 match response.into_string() {
349 Ok(body) => {
350 if body.len() > MAX_BODY_SIZE {
351 error_response(format!(
352 "Response body too large ({} bytes, max {})",
353 body.len(),
354 MAX_BODY_SIZE
355 ))
356 } else {
357 build_response_map(status, body, ok, None)
358 }
359 }
360 Err(e) => error_response(format!("Failed to read response body: {}", e)),
361 }
362 }
363 Err(ureq::Error::Status(code, response)) => {
364 let body = response.into_string().unwrap_or_default();
366 build_response_map(
367 code as i64,
368 body,
369 false,
370 Some(format!("HTTP error: {}", code)),
371 )
372 }
373 Err(ureq::Error::Transport(e)) => {
374 error_response(format!("Connection error: {}", e))
376 }
377 }
378}
379
380fn perform_get(url: &str) -> Value {
382 if let Err(msg) = validate_url_for_ssrf(url) {
384 return error_response(msg);
385 }
386 handle_response(HTTP_AGENT.get(url).call())
387}
388
389fn perform_post(url: &str, body: &str, content_type: &str) -> Value {
391 if let Err(msg) = validate_url_for_ssrf(url) {
393 return error_response(msg);
394 }
395 handle_response(
396 HTTP_AGENT
397 .post(url)
398 .set("Content-Type", content_type)
399 .send_string(body),
400 )
401}
402
403fn perform_put(url: &str, body: &str, content_type: &str) -> Value {
405 if let Err(msg) = validate_url_for_ssrf(url) {
407 return error_response(msg);
408 }
409 handle_response(
410 HTTP_AGENT
411 .put(url)
412 .set("Content-Type", content_type)
413 .send_string(body),
414 )
415}
416
417fn perform_delete(url: &str) -> Value {
419 if let Err(msg) = validate_url_for_ssrf(url) {
421 return error_response(msg);
422 }
423 handle_response(HTTP_AGENT.delete(url).call())
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
434 fn test_build_response_map_success() {
435 let response = build_response_map(200, "Hello".to_string(), true, None);
436
437 match response {
438 Value::Map(map_data) => {
439 let map = map_data.as_ref();
440
441 let status_key = MapKey::String(global_string("status".to_string()));
443 assert!(matches!(map.get(&status_key), Some(Value::Int(200))));
444
445 let body_key = MapKey::String(global_string("body".to_string()));
447 if let Some(Value::String(s)) = map.get(&body_key) {
448 assert_eq!(s.as_str(), "Hello");
449 } else {
450 panic!("Expected body to be String");
451 }
452
453 let ok_key = MapKey::String(global_string("ok".to_string()));
455 assert!(matches!(map.get(&ok_key), Some(Value::Bool(true))));
456
457 let error_key = MapKey::String(global_string("error".to_string()));
459 assert!(map.get(&error_key).is_none());
460 }
461 _ => panic!("Expected Map"),
462 }
463 }
464
465 #[test]
466 fn test_build_response_map_error() {
467 let response = build_response_map(404, String::new(), false, Some("Not Found".to_string()));
468
469 match response {
470 Value::Map(map_data) => {
471 let map = map_data.as_ref();
472
473 let status_key = MapKey::String(global_string("status".to_string()));
475 assert!(matches!(map.get(&status_key), Some(Value::Int(404))));
476
477 let ok_key = MapKey::String(global_string("ok".to_string()));
479 assert!(matches!(map.get(&ok_key), Some(Value::Bool(false))));
480
481 let error_key = MapKey::String(global_string("error".to_string()));
483 if let Some(Value::String(s)) = map.get(&error_key) {
484 assert_eq!(s.as_str(), "Not Found");
485 } else {
486 panic!("Expected error to be String");
487 }
488 }
489 _ => panic!("Expected Map"),
490 }
491 }
492
493 #[test]
494 fn test_error_response() {
495 let response = error_response("Connection refused".to_string());
496
497 match response {
498 Value::Map(map_data) => {
499 let map = map_data.as_ref();
500
501 let status_key = MapKey::String(global_string("status".to_string()));
503 assert!(matches!(map.get(&status_key), Some(Value::Int(0))));
504
505 let ok_key = MapKey::String(global_string("ok".to_string()));
507 assert!(matches!(map.get(&ok_key), Some(Value::Bool(false))));
508
509 let error_key = MapKey::String(global_string("error".to_string()));
511 if let Some(Value::String(s)) = map.get(&error_key) {
512 assert_eq!(s.as_str(), "Connection refused");
513 } else {
514 panic!("Expected error to be String");
515 }
516 }
517 _ => panic!("Expected Map"),
518 }
519 }
520
521 #[test]
524 fn test_ssrf_blocks_localhost() {
525 assert!(validate_url_for_ssrf("http://localhost/").is_err());
526 assert!(validate_url_for_ssrf("http://localhost:8080/").is_err());
527 assert!(validate_url_for_ssrf("http://LOCALHOST/").is_err());
528 assert!(validate_url_for_ssrf("http://test.localhost/").is_err());
529 }
530
531 #[test]
532 fn test_ssrf_blocks_loopback_ip() {
533 assert!(validate_url_for_ssrf("http://127.0.0.1/").is_err());
534 assert!(validate_url_for_ssrf("http://127.0.0.1:8080/").is_err());
535 assert!(validate_url_for_ssrf("http://127.1.2.3/").is_err());
536 }
537
538 #[test]
539 fn test_ssrf_blocks_private_ranges() {
540 assert!(validate_url_for_ssrf("http://10.0.0.1/").is_err());
542 assert!(validate_url_for_ssrf("http://10.255.255.255/").is_err());
543
544 assert!(validate_url_for_ssrf("http://172.16.0.1/").is_err());
546 assert!(validate_url_for_ssrf("http://172.31.255.255/").is_err());
547
548 assert!(validate_url_for_ssrf("http://192.168.0.1/").is_err());
550 assert!(validate_url_for_ssrf("http://192.168.255.255/").is_err());
551 }
552
553 #[test]
554 fn test_ssrf_blocks_link_local() {
555 assert!(validate_url_for_ssrf("http://169.254.169.254/").is_err());
557 assert!(validate_url_for_ssrf("http://169.254.0.1/").is_err());
558 }
559
560 #[test]
561 fn test_ssrf_blocks_invalid_schemes() {
562 assert!(validate_url_for_ssrf("file:///etc/passwd").is_err());
563 assert!(validate_url_for_ssrf("ftp://example.com/").is_err());
564 assert!(validate_url_for_ssrf("gopher://example.com/").is_err());
565 }
566
567 #[test]
568 fn test_ssrf_allows_public_urls() {
569 assert!(validate_url_for_ssrf("https://example.com/").is_ok());
571 assert!(validate_url_for_ssrf("https://httpbin.org/get").is_ok());
572 assert!(validate_url_for_ssrf("http://8.8.8.8/").is_ok());
573 }
574
575 #[test]
576 fn test_dangerous_ipv4() {
577 use std::net::Ipv4Addr;
578
579 assert!(is_dangerous_ipv4(Ipv4Addr::new(127, 0, 0, 1)));
581 assert!(is_dangerous_ipv4(Ipv4Addr::new(127, 1, 2, 3)));
582
583 assert!(is_dangerous_ipv4(Ipv4Addr::new(10, 0, 0, 1)));
585 assert!(is_dangerous_ipv4(Ipv4Addr::new(10, 255, 255, 255)));
586
587 assert!(is_dangerous_ipv4(Ipv4Addr::new(172, 16, 0, 1)));
589 assert!(is_dangerous_ipv4(Ipv4Addr::new(172, 31, 255, 255)));
590 assert!(!is_dangerous_ipv4(Ipv4Addr::new(172, 15, 0, 1))); assert!(!is_dangerous_ipv4(Ipv4Addr::new(172, 32, 0, 1))); assert!(is_dangerous_ipv4(Ipv4Addr::new(192, 168, 0, 1)));
595 assert!(is_dangerous_ipv4(Ipv4Addr::new(192, 168, 255, 255)));
596
597 assert!(is_dangerous_ipv4(Ipv4Addr::new(169, 254, 169, 254)));
599
600 assert!(!is_dangerous_ipv4(Ipv4Addr::new(8, 8, 8, 8)));
602 assert!(!is_dangerous_ipv4(Ipv4Addr::new(1, 1, 1, 1)));
603 assert!(!is_dangerous_ipv4(Ipv4Addr::new(93, 184, 216, 34)));
604 }
605
606 #[test]
607 fn test_dangerous_ipv6() {
608 use std::net::Ipv6Addr;
609
610 assert!(is_dangerous_ipv6(Ipv6Addr::LOCALHOST));
612
613 assert!(is_dangerous_ipv6(Ipv6Addr::new(
615 0xfe80, 0, 0, 0, 0, 0, 0, 1
616 )));
617
618 assert!(is_dangerous_ipv6(Ipv6Addr::new(
620 0xfc00, 0, 0, 0, 0, 0, 0, 1
621 )));
622 assert!(is_dangerous_ipv6(Ipv6Addr::new(
623 0xfd00, 0, 0, 0, 0, 0, 0, 1
624 )));
625
626 assert!(!is_dangerous_ipv6(Ipv6Addr::new(
628 0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888
629 ))); }
631}