use crate::seqstring::global_string;
use crate::stack::{Stack, pop, push};
use crate::value::{MapKey, Value};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, ToSocketAddrs};
use std::sync::LazyLock;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 30;
const MAX_BODY_SIZE: usize = 10 * 1024 * 1024;
static HTTP_AGENT: LazyLock<ureq::Agent> = LazyLock::new(|| {
ureq::AgentBuilder::new()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()
});
fn is_dangerous_ipv4(ip: Ipv4Addr) -> bool {
if ip.is_loopback() {
return true;
}
if ip.octets()[0] == 10 {
return true;
}
if ip.octets()[0] == 172 && (ip.octets()[1] >= 16 && ip.octets()[1] <= 31) {
return true;
}
if ip.octets()[0] == 192 && ip.octets()[1] == 168 {
return true;
}
if ip.octets()[0] == 169 && ip.octets()[1] == 254 {
return true;
}
if ip.is_broadcast() {
return true;
}
false
}
fn is_dangerous_ipv6(ip: Ipv6Addr) -> bool {
if ip.is_loopback() {
return true;
}
let segments = ip.segments();
if (segments[0] & 0xffc0) == 0xfe80 {
return true;
}
if (segments[0] & 0xfe00) == 0xfc00 {
return true;
}
if let Some(ipv4) = ip.to_ipv4_mapped() {
return is_dangerous_ipv4(ipv4);
}
false
}
fn is_dangerous_ip(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => is_dangerous_ipv4(v4),
IpAddr::V6(v6) => is_dangerous_ipv6(v6),
}
}
fn validate_url_for_ssrf(url: &str) -> Result<(), String> {
let parsed = match url::Url::parse(url) {
Ok(u) => u,
Err(e) => return Err(format!("Invalid URL: {}", e)),
};
match parsed.scheme() {
"http" | "https" => {}
scheme => {
return Err(format!(
"Blocked scheme '{}': only http/https allowed",
scheme
));
}
}
let host = match parsed.host_str() {
Some(h) => h,
None => return Err("URL has no host".to_string()),
};
let host_lower = host.to_lowercase();
if host_lower == "localhost"
|| host_lower == "localhost.localdomain"
|| host_lower.ends_with(".localhost")
{
return Err("Blocked: localhost access not allowed".to_string());
}
let port = parsed
.port()
.unwrap_or(if parsed.scheme() == "https" { 443 } else { 80 });
let addr_str = format!("{}:{}", host, port);
match addr_str.to_socket_addrs() {
Ok(addrs) => {
for addr in addrs {
if is_dangerous_ip(addr.ip()) {
return Err(format!(
"Blocked: {} resolves to private/internal IP {}",
host,
addr.ip()
));
}
}
}
Err(_) => {
}
}
Ok(())
}
fn build_response_map(status: i64, body: String, ok: bool, error: Option<String>) -> Value {
let mut map: HashMap<MapKey, Value> = HashMap::new();
map.insert(
MapKey::String(global_string("status".to_string())),
Value::Int(status),
);
map.insert(
MapKey::String(global_string("body".to_string())),
Value::String(global_string(body)),
);
map.insert(
MapKey::String(global_string("ok".to_string())),
Value::Bool(ok),
);
if let Some(err) = error {
map.insert(
MapKey::String(global_string("error".to_string())),
Value::String(global_string(err)),
);
}
Value::Map(Box::new(map))
}
fn error_response(error: String) -> Value {
build_response_map(0, String::new(), false, Some(error))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_http_get(stack: Stack) -> Stack {
assert!(!stack.is_null(), "http.get: stack is empty");
let (stack, url_value) = unsafe { pop(stack) };
match url_value {
Value::String(url) => {
let response = perform_get(url.as_str());
unsafe { push(stack, response) }
}
_ => panic!(
"http.get: expected String (URL) on stack, got {:?}",
url_value
),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_http_post(stack: Stack) -> Stack {
assert!(!stack.is_null(), "http.post: stack is empty");
let (stack, content_type_value) = unsafe { pop(stack) };
let (stack, body_value) = unsafe { pop(stack) };
let (stack, url_value) = unsafe { pop(stack) };
match (url_value, body_value, content_type_value) {
(Value::String(url), Value::String(body), Value::String(content_type)) => {
let response = perform_post(url.as_str(), body.as_str(), content_type.as_str());
unsafe { push(stack, response) }
}
(url, body, ct) => panic!(
"http.post: expected (String, String, String) on stack, got ({:?}, {:?}, {:?})",
url, body, ct
),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_http_put(stack: Stack) -> Stack {
assert!(!stack.is_null(), "http.put: stack is empty");
let (stack, content_type_value) = unsafe { pop(stack) };
let (stack, body_value) = unsafe { pop(stack) };
let (stack, url_value) = unsafe { pop(stack) };
match (url_value, body_value, content_type_value) {
(Value::String(url), Value::String(body), Value::String(content_type)) => {
let response = perform_put(url.as_str(), body.as_str(), content_type.as_str());
unsafe { push(stack, response) }
}
(url, body, ct) => panic!(
"http.put: expected (String, String, String) on stack, got ({:?}, {:?}, {:?})",
url, body, ct
),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn patch_seq_http_delete(stack: Stack) -> Stack {
assert!(!stack.is_null(), "http.delete: stack is empty");
let (stack, url_value) = unsafe { pop(stack) };
match url_value {
Value::String(url) => {
let response = perform_delete(url.as_str());
unsafe { push(stack, response) }
}
_ => panic!(
"http.delete: expected String (URL) on stack, got {:?}",
url_value
),
}
}
fn handle_response(result: Result<ureq::Response, ureq::Error>) -> Value {
match result {
Ok(response) => {
let status = response.status() as i64;
let ok = (200..300).contains(&response.status());
match response.into_string() {
Ok(body) => {
if body.len() > MAX_BODY_SIZE {
error_response(format!(
"Response body too large ({} bytes, max {})",
body.len(),
MAX_BODY_SIZE
))
} else {
build_response_map(status, body, ok, None)
}
}
Err(e) => error_response(format!("Failed to read response body: {}", e)),
}
}
Err(ureq::Error::Status(code, response)) => {
let body = response.into_string().unwrap_or_default();
build_response_map(
code as i64,
body,
false,
Some(format!("HTTP error: {}", code)),
)
}
Err(ureq::Error::Transport(e)) => {
error_response(format!("Connection error: {}", e))
}
}
}
fn perform_get(url: &str) -> Value {
if let Err(msg) = validate_url_for_ssrf(url) {
return error_response(msg);
}
handle_response(HTTP_AGENT.get(url).call())
}
fn perform_post(url: &str, body: &str, content_type: &str) -> Value {
if let Err(msg) = validate_url_for_ssrf(url) {
return error_response(msg);
}
handle_response(
HTTP_AGENT
.post(url)
.set("Content-Type", content_type)
.send_string(body),
)
}
fn perform_put(url: &str, body: &str, content_type: &str) -> Value {
if let Err(msg) = validate_url_for_ssrf(url) {
return error_response(msg);
}
handle_response(
HTTP_AGENT
.put(url)
.set("Content-Type", content_type)
.send_string(body),
)
}
fn perform_delete(url: &str) -> Value {
if let Err(msg) = validate_url_for_ssrf(url) {
return error_response(msg);
}
handle_response(HTTP_AGENT.delete(url).call())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_response_map_success() {
let response = build_response_map(200, "Hello".to_string(), true, None);
match response {
Value::Map(map_data) => {
let map = map_data.as_ref();
let status_key = MapKey::String(global_string("status".to_string()));
assert!(matches!(map.get(&status_key), Some(Value::Int(200))));
let body_key = MapKey::String(global_string("body".to_string()));
if let Some(Value::String(s)) = map.get(&body_key) {
assert_eq!(s.as_str(), "Hello");
} else {
panic!("Expected body to be String");
}
let ok_key = MapKey::String(global_string("ok".to_string()));
assert!(matches!(map.get(&ok_key), Some(Value::Bool(true))));
let error_key = MapKey::String(global_string("error".to_string()));
assert!(map.get(&error_key).is_none());
}
_ => panic!("Expected Map"),
}
}
#[test]
fn test_build_response_map_error() {
let response = build_response_map(404, String::new(), false, Some("Not Found".to_string()));
match response {
Value::Map(map_data) => {
let map = map_data.as_ref();
let status_key = MapKey::String(global_string("status".to_string()));
assert!(matches!(map.get(&status_key), Some(Value::Int(404))));
let ok_key = MapKey::String(global_string("ok".to_string()));
assert!(matches!(map.get(&ok_key), Some(Value::Bool(false))));
let error_key = MapKey::String(global_string("error".to_string()));
if let Some(Value::String(s)) = map.get(&error_key) {
assert_eq!(s.as_str(), "Not Found");
} else {
panic!("Expected error to be String");
}
}
_ => panic!("Expected Map"),
}
}
#[test]
fn test_error_response() {
let response = error_response("Connection refused".to_string());
match response {
Value::Map(map_data) => {
let map = map_data.as_ref();
let status_key = MapKey::String(global_string("status".to_string()));
assert!(matches!(map.get(&status_key), Some(Value::Int(0))));
let ok_key = MapKey::String(global_string("ok".to_string()));
assert!(matches!(map.get(&ok_key), Some(Value::Bool(false))));
let error_key = MapKey::String(global_string("error".to_string()));
if let Some(Value::String(s)) = map.get(&error_key) {
assert_eq!(s.as_str(), "Connection refused");
} else {
panic!("Expected error to be String");
}
}
_ => panic!("Expected Map"),
}
}
#[test]
fn test_ssrf_blocks_localhost() {
assert!(validate_url_for_ssrf("http://localhost/").is_err());
assert!(validate_url_for_ssrf("http://localhost:8080/").is_err());
assert!(validate_url_for_ssrf("http://LOCALHOST/").is_err());
assert!(validate_url_for_ssrf("http://test.localhost/").is_err());
}
#[test]
fn test_ssrf_blocks_loopback_ip() {
assert!(validate_url_for_ssrf("http://127.0.0.1/").is_err());
assert!(validate_url_for_ssrf("http://127.0.0.1:8080/").is_err());
assert!(validate_url_for_ssrf("http://127.1.2.3/").is_err());
}
#[test]
fn test_ssrf_blocks_private_ranges() {
assert!(validate_url_for_ssrf("http://10.0.0.1/").is_err());
assert!(validate_url_for_ssrf("http://10.255.255.255/").is_err());
assert!(validate_url_for_ssrf("http://172.16.0.1/").is_err());
assert!(validate_url_for_ssrf("http://172.31.255.255/").is_err());
assert!(validate_url_for_ssrf("http://192.168.0.1/").is_err());
assert!(validate_url_for_ssrf("http://192.168.255.255/").is_err());
}
#[test]
fn test_ssrf_blocks_link_local() {
assert!(validate_url_for_ssrf("http://169.254.169.254/").is_err());
assert!(validate_url_for_ssrf("http://169.254.0.1/").is_err());
}
#[test]
fn test_ssrf_blocks_invalid_schemes() {
assert!(validate_url_for_ssrf("file:///etc/passwd").is_err());
assert!(validate_url_for_ssrf("ftp://example.com/").is_err());
assert!(validate_url_for_ssrf("gopher://example.com/").is_err());
}
#[test]
fn test_ssrf_allows_public_urls() {
assert!(validate_url_for_ssrf("https://example.com/").is_ok());
assert!(validate_url_for_ssrf("https://httpbin.org/get").is_ok());
assert!(validate_url_for_ssrf("http://8.8.8.8/").is_ok());
}
#[test]
fn test_dangerous_ipv4() {
use std::net::Ipv4Addr;
assert!(is_dangerous_ipv4(Ipv4Addr::new(127, 0, 0, 1)));
assert!(is_dangerous_ipv4(Ipv4Addr::new(127, 1, 2, 3)));
assert!(is_dangerous_ipv4(Ipv4Addr::new(10, 0, 0, 1)));
assert!(is_dangerous_ipv4(Ipv4Addr::new(10, 255, 255, 255)));
assert!(is_dangerous_ipv4(Ipv4Addr::new(172, 16, 0, 1)));
assert!(is_dangerous_ipv4(Ipv4Addr::new(172, 31, 255, 255)));
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)));
assert!(is_dangerous_ipv4(Ipv4Addr::new(192, 168, 255, 255)));
assert!(is_dangerous_ipv4(Ipv4Addr::new(169, 254, 169, 254)));
assert!(!is_dangerous_ipv4(Ipv4Addr::new(8, 8, 8, 8)));
assert!(!is_dangerous_ipv4(Ipv4Addr::new(1, 1, 1, 1)));
assert!(!is_dangerous_ipv4(Ipv4Addr::new(93, 184, 216, 34)));
}
#[test]
fn test_dangerous_ipv6() {
use std::net::Ipv6Addr;
assert!(is_dangerous_ipv6(Ipv6Addr::LOCALHOST));
assert!(is_dangerous_ipv6(Ipv6Addr::new(
0xfe80, 0, 0, 0, 0, 0, 0, 1
)));
assert!(is_dangerous_ipv6(Ipv6Addr::new(
0xfc00, 0, 0, 0, 0, 0, 0, 1
)));
assert!(is_dangerous_ipv6(Ipv6Addr::new(
0xfd00, 0, 0, 0, 0, 0, 0, 1
)));
assert!(!is_dangerous_ipv6(Ipv6Addr::new(
0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888
))); }
}