use std::sync::Arc;
use std::time::Duration;
use ferridriver::http_client::{HttpClient, HttpResponse, NetGuard, RequestOptions};
use rquickjs::function::Opt;
use rquickjs::{Ctx, JsLifetime, Value, class::Trace};
use serde::Deserialize;
use crate::bindings::convert::{FerriResultExt, serde_from_js};
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct JsRequestOptions {
headers: Option<Vec<(String, String)>>,
data: Option<Vec<u8>>,
json: Option<serde_json::Value>,
form: Option<Vec<(String, String)>>,
params: Option<Vec<(String, String)>>,
timeout_ms: Option<u64>,
fail_on_status_code: Option<bool>,
max_redirects: Option<u32>,
}
impl JsRequestOptions {
fn into_core(self) -> RequestOptions {
RequestOptions {
method: None,
headers: self.headers,
data: self.data,
json_data: self.json,
form: self.form,
params: self.params,
timeout: self.timeout_ms.map(Duration::from_millis),
fail_on_status_code: self.fail_on_status_code,
max_redirects: self.max_redirects,
net_guard: None,
}
}
}
fn parse_options<'js>(ctx: &Ctx<'js>, value: Opt<Value<'js>>) -> rquickjs::Result<Option<RequestOptions>> {
match value.0 {
Some(v) if !v.is_undefined() && !v.is_null() => {
let parsed: JsRequestOptions = serde_from_js(ctx, v)?;
Ok(Some(parsed.into_core()))
},
_ => Ok(None),
}
}
#[derive(JsLifetime, Trace)]
#[rquickjs::class(rename = "HttpClient")]
pub struct HttpClientJs {
#[qjs(skip_trace)]
inner: Arc<HttpClient>,
#[qjs(skip_trace)]
net: Arc<[String]>,
}
impl HttpClientJs {
#[must_use]
pub fn new(inner: Arc<HttpClient>) -> Self {
Self {
inner,
net: Arc::from([]),
}
}
#[must_use]
pub fn with_net(inner: Arc<HttpClient>, net: Arc<[String]>) -> Self {
Self { inner, net }
}
#[must_use]
pub fn inner_arc(&self) -> Arc<HttpClient> {
self.inner.clone()
}
pub(crate) fn net_guard(&self) -> NetGuard {
NetGuard {
allowlist: (!self.net.is_empty()).then(|| self.net.clone()),
block_metadata: true,
block_private: false,
}
}
fn guard(&self, url: &str) -> rquickjs::Result<()> {
net_check(&self.net, url).map_err(|m| rquickjs::Error::new_from_js_message("request", "Error", m))
}
}
fn with_guard(opts: Option<RequestOptions>, g: NetGuard) -> RequestOptions {
let mut o = opts.unwrap_or_default();
o.net_guard = Some(g);
o
}
pub(crate) fn net_check(net: &[String], url: &str) -> Result<(), String> {
if net.is_empty() {
return Ok(());
}
let host = ferridriver::http_client::host_of(url)
.ok_or_else(|| format!("request to invalid/relative URL \"{url}\" is not permitted by allow.net"))?;
if ferridriver::http_client::host_allowed(&host, net) {
Ok(())
} else {
Err(format!("request host \"{host}\" is not in allow.net {net:?}"))
}
}
#[rquickjs::methods]
impl HttpClientJs {
#[qjs(rename = "get")]
pub async fn get<'js>(
&self,
ctx: Ctx<'js>,
url: String,
options: Opt<Value<'js>>,
) -> rquickjs::Result<HttpResponseJs> {
self.guard(&url)?;
let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
let resp = self.inner.get(&url, opts).await.into_js()?;
Ok(HttpResponseJs::new(resp))
}
#[qjs(rename = "post")]
pub async fn post<'js>(
&self,
ctx: Ctx<'js>,
url: String,
options: Opt<Value<'js>>,
) -> rquickjs::Result<HttpResponseJs> {
self.guard(&url)?;
let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
let resp = self.inner.post(&url, opts).await.into_js()?;
Ok(HttpResponseJs::new(resp))
}
#[qjs(rename = "put")]
pub async fn put<'js>(
&self,
ctx: Ctx<'js>,
url: String,
options: Opt<Value<'js>>,
) -> rquickjs::Result<HttpResponseJs> {
self.guard(&url)?;
let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
let resp = self.inner.put(&url, opts).await.into_js()?;
Ok(HttpResponseJs::new(resp))
}
#[qjs(rename = "delete")]
pub async fn delete<'js>(
&self,
ctx: Ctx<'js>,
url: String,
options: Opt<Value<'js>>,
) -> rquickjs::Result<HttpResponseJs> {
self.guard(&url)?;
let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
let resp = self.inner.delete(&url, opts).await.into_js()?;
Ok(HttpResponseJs::new(resp))
}
#[qjs(rename = "patch")]
pub async fn patch<'js>(
&self,
ctx: Ctx<'js>,
url: String,
options: Opt<Value<'js>>,
) -> rquickjs::Result<HttpResponseJs> {
self.guard(&url)?;
let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
let resp = self.inner.patch(&url, opts).await.into_js()?;
Ok(HttpResponseJs::new(resp))
}
#[qjs(rename = "head")]
pub async fn head<'js>(
&self,
ctx: Ctx<'js>,
url: String,
options: Opt<Value<'js>>,
) -> rquickjs::Result<HttpResponseJs> {
self.guard(&url)?;
let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
let resp = self.inner.head(&url, opts).await.into_js()?;
Ok(HttpResponseJs::new(resp))
}
#[qjs(rename = "fetch")]
pub async fn fetch<'js>(
&self,
ctx: Ctx<'js>,
url: String,
options: Opt<Value<'js>>,
) -> rquickjs::Result<HttpResponseJs> {
self.guard(&url)?;
let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
let resp = self.inner.fetch(&url, opts).await.into_js()?;
Ok(HttpResponseJs::new(resp))
}
}
#[derive(JsLifetime, Trace)]
#[rquickjs::class(rename = "HttpResponse")]
pub struct HttpResponseJs {
#[qjs(skip_trace)]
inner: HttpResponse,
}
impl HttpResponseJs {
#[must_use]
pub fn new(inner: HttpResponse) -> Self {
Self { inner }
}
#[must_use]
pub fn inner_clone(&self) -> HttpResponse {
self.inner.clone()
}
}
#[rquickjs::methods]
impl HttpResponseJs {
#[qjs(rename = "status")]
pub fn status(&self) -> i32 {
i32::from(self.inner.status())
}
#[qjs(rename = "statusText")]
pub fn status_text(&self) -> String {
self.inner.status_text().to_string()
}
#[qjs(rename = "url")]
pub fn url(&self) -> String {
self.inner.url().to_string()
}
#[qjs(rename = "ok")]
pub fn ok(&self) -> bool {
self.inner.ok()
}
#[qjs(rename = "headersArray")]
pub fn headers_array<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let h = self.inner.headers();
let pairs: Vec<(&str, &str)> = h.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
crate::bindings::convert::name_value_array_to_js(&ctx, &pairs)
}
#[qjs(rename = "header")]
pub fn header(&self, name: String) -> Option<String> {
self.inner.header(&name).map(str::to_string)
}
#[qjs(rename = "text")]
pub fn text(&self) -> rquickjs::Result<String> {
self.inner.text().into_js()
}
#[qjs(rename = "json")]
pub fn json<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
let text = self.inner.text().into_js()?;
ctx.json_parse(text)
}
}