use std::collections::HashMap; use napi::{ Env, Error, Result, Status, Task, bindgen_prelude::{AsyncTask, Buffer}, }; use napi_derive::napi; #[napi(string_enum = "snake_case")] #[derive(Clone, Copy, Debug)] pub enum SafeFetchMethod { Get, Head, Post, Put, Propfind, Report, } #[napi(object)] pub struct SafeFetchRequest { pub url: String, pub method: Option, pub headers: Option>, pub body: Option, pub timeout_ms: Option, pub max_redirects: Option, pub max_bytes: Option, pub allowed_headers: Option>, pub allowed_hosts: Option>, pub allow_http: Option, pub allow_private_target_origin: Option, pub enable_ech: Option, pub ech_config_list: Option, } #[napi(object)] #[derive(Clone, Debug)] pub struct AssertSafeUrlRequest { pub url: String, } #[napi(object)] pub struct SafeFetchResponse { pub status: u16, pub final_url: String, pub headers: HashMap, pub body: Buffer, } #[napi(object)] #[derive(Clone, Debug)] pub struct ImageInspectionOptions { pub max_width: Option, pub max_height: Option, pub max_pixels: Option, } #[napi(object)] pub struct ImageInspection { pub mime_type: String, pub width: u32, pub height: u32, } #[napi(object)] #[derive(Clone, Debug)] pub struct RemoteAttachmentFetchRequest { pub url: String, pub timeout_ms: Option, pub max_bytes: u32, pub allow_private_target_origin: Option, pub expected_content_type_prefix: Option, pub max_image_width: Option, pub max_image_height: Option, pub max_image_pixels: Option, } #[napi(object)] pub struct RemoteAttachmentFetchResponse { pub final_url: String, pub mime_type: String, pub body: Buffer, } #[napi(object)] #[derive(Clone, Debug)] pub struct RemoteMimeTypeRequest { pub url: String, pub timeout_ms: Option, } pub struct AsyncSafeFetchTask { request: SafeFetchRequest, } pub struct AsyncRemoteAttachmentFetchTask { request: RemoteAttachmentFetchRequest, } pub struct AsyncRemoteMimeTypeTask { request: RemoteMimeTypeRequest, } impl From for safefetch::SafeFetchMethod { fn from(method: SafeFetchMethod) -> Self { match method { SafeFetchMethod::Get => safefetch::SafeFetchMethod::Get, SafeFetchMethod::Head => safefetch::SafeFetchMethod::Head, SafeFetchMethod::Post => safefetch::SafeFetchMethod::Post, SafeFetchMethod::Put => safefetch::SafeFetchMethod::Put, SafeFetchMethod::Propfind => safefetch::SafeFetchMethod::Propfind, SafeFetchMethod::Report => safefetch::SafeFetchMethod::Report, } } } #[napi] impl Task for AsyncSafeFetchTask { type Output = safefetch::SafeFetchResponse; type JsValue = SafeFetchResponse; fn compute(&mut self) -> Result { let request = safe_fetch_request(&self.request).map_err(invalid_arg)?; safefetch::safe_fetch(&request).map_err(invalid_arg) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { Ok(SafeFetchResponse { status: output.status, final_url: output.final_url, headers: output.headers, body: output.body.into(), }) } } #[napi] pub fn safe_fetch(request: SafeFetchRequest) -> AsyncTask { AsyncTask::new(AsyncSafeFetchTask { request }) } #[napi] pub fn assert_safe_url(request: AssertSafeUrlRequest) -> Result<()> { safefetch::assert_safe_url(&request.url).map_err(invalid_arg) } #[napi] pub fn inspect_image_for_proxy(input: Buffer, options: Option) -> Result { let output = safefetch::inspect_image( &input, image_inspection_options(options.unwrap_or(ImageInspectionOptions { max_width: None, max_height: None, max_pixels: None, })), ) .map_err(invalid_arg)?; Ok(ImageInspection { mime_type: output.mime_type, width: output.width, height: output.height, }) } #[napi] impl Task for AsyncRemoteAttachmentFetchTask { type Output = safefetch::RemoteAttachmentFetchResponse; type JsValue = RemoteAttachmentFetchResponse; fn compute(&mut self) -> Result { safefetch::fetch_remote_attachment(&safefetch::RemoteAttachmentFetchRequest { url: self.request.url.clone(), timeout_ms: self.request.timeout_ms, max_bytes: self.request.max_bytes, allow_private_target_origin: self.request.allow_private_target_origin, expected_content_type_prefix: self.request.expected_content_type_prefix.clone(), max_image_width: self.request.max_image_width, max_image_height: self.request.max_image_height, max_image_pixels: self.request.max_image_pixels, }) .map_err(invalid_arg) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { Ok(RemoteAttachmentFetchResponse { final_url: output.final_url, mime_type: output.mime_type, body: output.body.into(), }) } } #[napi] pub fn fetch_remote_attachment(request: RemoteAttachmentFetchRequest) -> AsyncTask { AsyncTask::new(AsyncRemoteAttachmentFetchTask { request }) } #[napi] impl Task for AsyncRemoteMimeTypeTask { type Output = String; type JsValue = String; fn compute(&mut self) -> Result { Ok(safefetch::infer_remote_mime_type(&safefetch::RemoteMimeTypeRequest { url: self.request.url.clone(), timeout_ms: self.request.timeout_ms, })) } fn resolve(&mut self, _: Env, output: Self::Output) -> Result { Ok(output) } } #[napi] pub fn infer_remote_mime_type(request: RemoteMimeTypeRequest) -> AsyncTask { AsyncTask::new(AsyncRemoteMimeTypeTask { request }) } pub(crate) fn safe_fetch_request(request: &SafeFetchRequest) -> anyhow::Result { Ok(safefetch::SafeFetchRequest { url: request.url.clone(), method: request.method.map(Into::into), headers: request.headers.clone(), body: request.body.as_ref().map(|body| body.to_vec()), timeout_ms: request.timeout_ms, max_redirects: request.max_redirects, max_bytes: request.max_bytes, allowed_headers: request.allowed_headers.clone(), allowed_hosts: request.allowed_hosts.clone(), allow_http: request.allow_http, allow_private_target_origin: request.allow_private_target_origin, ech_config_list: ech_config_list(request)?, }) } fn image_inspection_options(options: ImageInspectionOptions) -> safefetch::ImageInspectionOptions { safefetch::ImageInspectionOptions { max_width: options.max_width, max_height: options.max_height, max_pixels: options.max_pixels, } } fn ech_config_list(request: &SafeFetchRequest) -> anyhow::Result>> { if !request.enable_ech.unwrap_or(false) { return Ok(None); } let Some(config_list) = request.ech_config_list.as_ref() else { anyhow::bail!("ech_config_required"); }; Ok(Some(config_list.to_vec())) } fn invalid_arg(error: impl ToString) -> Error { Error::new(Status::InvalidArg, error.to_string()) }