use lingshu_security::url_validation::{UrlValidationError, validate_outbound_url};
use lingshu_types::ToolError;
use super::backends::ddgs::{ImpersonateProfile, build_wreq_client, pick_random_profile};
use super::error::SearchError;
pub fn validate_url_for_tool(url: &str, tool: &str) -> Result<(), ToolError> {
validate_outbound_url(url).map_err(|err| map_url_validation_error(url, tool, err))
}
fn map_url_validation_error(url: &str, tool: &str, err: UrlValidationError) -> ToolError {
match err {
UrlValidationError::SsrfBlocked(_) => ToolError::PermissionDenied(format!(
"URL blocked by SSRF policy for tool '{tool}': {url}"
)),
UrlValidationError::WebsitePolicyBlocked(msg) => ToolError::PermissionDenied(msg),
UrlValidationError::Invalid(e) => {
ToolError::PermissionDenied(format!("URL validation error in '{tool}': {e}"))
}
}
}
pub fn validate_search_url(url: &str) -> Result<(), SearchError> {
validate_outbound_url(url).map_err(|err| match err {
UrlValidationError::SsrfBlocked(msg) | UrlValidationError::WebsitePolicyBlocked(msg) => {
SearchError::hard("web_search", msg)
}
UrlValidationError::Invalid(e) => SearchError::hard("web_search", e),
})
}
pub fn build_api_client(timeout_secs: u64) -> Result<reqwest::Client, SearchError> {
Ok(lingshu_security::url_safety::build_ssrf_safe_client(
std::time::Duration::from_secs(timeout_secs.max(1)),
))
}
pub fn urlencoding_encode(s: &str) -> String {
s.chars()
.map(|c| match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
' ' => "+".to_string(),
other => {
let bytes = other.to_string().into_bytes();
bytes.iter().map(|b| format!("%{:02X}", b)).collect()
}
})
.collect()
}
pub fn build_chrome_client(timeout_secs: u64) -> Result<wreq::Client, SearchError> {
build_chrome_client_with_headers(timeout_secs, None, None, None)
}
pub fn build_chrome_client_with_headers(
timeout_secs: u64,
referer: Option<&str>,
user_agent: Option<&str>,
proxy_url: Option<String>,
) -> Result<wreq::Client, SearchError> {
let profile = if user_agent.is_some() {
ImpersonateProfile::Chrome131Mac
} else {
pick_random_profile()
};
build_wreq_client(timeout_secs, profile, referer, user_agent, proxy_url)
.map_err(|e| SearchError::hard("web_search", format!("Failed to build Chrome client: {e}")))
}
pub fn map_reqwest_error(backend: &str, err: reqwest::Error) -> SearchError {
if err.is_timeout() {
SearchError::timeout(backend, format!("Request timed out: {err}"))
} else if err.is_connect() || err.is_request() {
SearchError::network(backend, format!("Network error: {err}"))
} else {
SearchError::network(backend, format!("HTTP client error: {err}"))
}
}
pub fn redact_secrets(text: &str, secrets: &[&str]) -> String {
let mut out = text.to_string();
for secret in secrets {
if !secret.is_empty() {
out = out.replace(secret, "[REDACTED]");
}
}
out
}
pub fn validate_url_legacy(url: &str, tool: &str) -> Result<(), ToolError> {
validate_url_for_tool(url, tool)
}