ferridriver_script/bindings/
http_client.rs1use std::sync::Arc;
5use std::time::Duration;
6
7use ferridriver::http_client::{HttpClient, HttpResponse, NetGuard, RequestOptions};
8use rquickjs::function::Opt;
9use rquickjs::{Ctx, JsLifetime, Value, class::Trace};
10use serde::Deserialize;
11
12use crate::bindings::convert::{FerriResultExt, serde_from_js};
13
14#[derive(Debug, Default, Deserialize)]
20#[serde(default)]
21struct JsRequestOptions {
22 headers: Option<Vec<(String, String)>>,
23 data: Option<Vec<u8>>,
24 json: Option<serde_json::Value>,
25 form: Option<Vec<(String, String)>>,
26 params: Option<Vec<(String, String)>>,
27 timeout_ms: Option<u64>,
29 fail_on_status_code: Option<bool>,
30 max_redirects: Option<u32>,
31}
32
33impl JsRequestOptions {
34 fn into_core(self) -> RequestOptions {
35 RequestOptions {
36 method: None,
37 headers: self.headers,
38 data: self.data,
39 json_data: self.json,
40 form: self.form,
41 params: self.params,
42 timeout: self.timeout_ms.map(Duration::from_millis),
43 fail_on_status_code: self.fail_on_status_code,
44 max_redirects: self.max_redirects,
45 net_guard: None,
47 }
48 }
49}
50
51fn parse_options<'js>(ctx: &Ctx<'js>, value: Opt<Value<'js>>) -> rquickjs::Result<Option<RequestOptions>> {
52 match value.0 {
53 Some(v) if !v.is_undefined() && !v.is_null() => {
54 let parsed: JsRequestOptions = serde_from_js(ctx, v)?;
55 Ok(Some(parsed.into_core()))
56 },
57 _ => Ok(None),
58 }
59}
60
61#[derive(JsLifetime, Trace)]
64#[rquickjs::class(rename = "HttpClient")]
65pub struct HttpClientJs {
66 #[qjs(skip_trace)]
67 inner: Arc<HttpClient>,
68 #[qjs(skip_trace)]
74 net: Arc<[String]>,
75}
76
77impl HttpClientJs {
78 #[must_use]
79 pub fn new(inner: Arc<HttpClient>) -> Self {
80 Self {
81 inner,
82 net: Arc::from([]),
83 }
84 }
85
86 #[must_use]
90 pub fn with_net(inner: Arc<HttpClient>, net: Arc<[String]>) -> Self {
91 Self { inner, net }
92 }
93
94 #[must_use]
97 pub fn inner_arc(&self) -> Arc<HttpClient> {
98 self.inner.clone()
99 }
100
101 pub(crate) fn net_guard(&self) -> NetGuard {
107 NetGuard {
108 allowlist: (!self.net.is_empty()).then(|| self.net.clone()),
109 block_metadata: true,
110 block_private: false,
111 }
112 }
113
114 fn guard(&self, url: &str) -> rquickjs::Result<()> {
119 net_check(&self.net, url).map_err(|m| rquickjs::Error::new_from_js_message("request", "Error", m))
120 }
121}
122
123fn with_guard(opts: Option<RequestOptions>, g: NetGuard) -> RequestOptions {
126 let mut o = opts.unwrap_or_default();
127 o.net_guard = Some(g);
128 o
129}
130
131pub(crate) fn net_check(net: &[String], url: &str) -> Result<(), String> {
138 if net.is_empty() {
139 return Ok(());
140 }
141 let host = ferridriver::http_client::host_of(url)
142 .ok_or_else(|| format!("request to invalid/relative URL \"{url}\" is not permitted by allow.net"))?;
143 if ferridriver::http_client::host_allowed(&host, net) {
144 Ok(())
145 } else {
146 Err(format!("request host \"{host}\" is not in allow.net {net:?}"))
147 }
148}
149
150#[rquickjs::methods]
151impl HttpClientJs {
152 #[qjs(rename = "get")]
153 pub async fn get<'js>(
154 &self,
155 ctx: Ctx<'js>,
156 url: String,
157 options: Opt<Value<'js>>,
158 ) -> rquickjs::Result<HttpResponseJs> {
159 self.guard(&url)?;
160 let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
161 let resp = self.inner.get(&url, opts).await.into_js()?;
162 Ok(HttpResponseJs::new(resp))
163 }
164
165 #[qjs(rename = "post")]
166 pub async fn post<'js>(
167 &self,
168 ctx: Ctx<'js>,
169 url: String,
170 options: Opt<Value<'js>>,
171 ) -> rquickjs::Result<HttpResponseJs> {
172 self.guard(&url)?;
173 let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
174 let resp = self.inner.post(&url, opts).await.into_js()?;
175 Ok(HttpResponseJs::new(resp))
176 }
177
178 #[qjs(rename = "put")]
179 pub async fn put<'js>(
180 &self,
181 ctx: Ctx<'js>,
182 url: String,
183 options: Opt<Value<'js>>,
184 ) -> rquickjs::Result<HttpResponseJs> {
185 self.guard(&url)?;
186 let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
187 let resp = self.inner.put(&url, opts).await.into_js()?;
188 Ok(HttpResponseJs::new(resp))
189 }
190
191 #[qjs(rename = "delete")]
192 pub async fn delete<'js>(
193 &self,
194 ctx: Ctx<'js>,
195 url: String,
196 options: Opt<Value<'js>>,
197 ) -> rquickjs::Result<HttpResponseJs> {
198 self.guard(&url)?;
199 let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
200 let resp = self.inner.delete(&url, opts).await.into_js()?;
201 Ok(HttpResponseJs::new(resp))
202 }
203
204 #[qjs(rename = "patch")]
205 pub async fn patch<'js>(
206 &self,
207 ctx: Ctx<'js>,
208 url: String,
209 options: Opt<Value<'js>>,
210 ) -> rquickjs::Result<HttpResponseJs> {
211 self.guard(&url)?;
212 let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
213 let resp = self.inner.patch(&url, opts).await.into_js()?;
214 Ok(HttpResponseJs::new(resp))
215 }
216
217 #[qjs(rename = "head")]
218 pub async fn head<'js>(
219 &self,
220 ctx: Ctx<'js>,
221 url: String,
222 options: Opt<Value<'js>>,
223 ) -> rquickjs::Result<HttpResponseJs> {
224 self.guard(&url)?;
225 let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
226 let resp = self.inner.head(&url, opts).await.into_js()?;
227 Ok(HttpResponseJs::new(resp))
228 }
229
230 #[qjs(rename = "fetch")]
234 pub async fn fetch<'js>(
235 &self,
236 ctx: Ctx<'js>,
237 url: String,
238 options: Opt<Value<'js>>,
239 ) -> rquickjs::Result<HttpResponseJs> {
240 self.guard(&url)?;
241 let opts = Some(with_guard(parse_options(&ctx, options)?, self.net_guard()));
242 let resp = self.inner.fetch(&url, opts).await.into_js()?;
243 Ok(HttpResponseJs::new(resp))
244 }
245}
246
247#[derive(JsLifetime, Trace)]
250#[rquickjs::class(rename = "HttpResponse")]
251pub struct HttpResponseJs {
252 #[qjs(skip_trace)]
253 inner: HttpResponse,
254}
255
256impl HttpResponseJs {
257 #[must_use]
258 pub fn new(inner: HttpResponse) -> Self {
259 Self { inner }
260 }
261
262 #[must_use]
266 pub fn inner_clone(&self) -> HttpResponse {
267 self.inner.clone()
268 }
269}
270
271#[rquickjs::methods]
272impl HttpResponseJs {
273 #[qjs(rename = "status")]
274 pub fn status(&self) -> i32 {
275 i32::from(self.inner.status())
276 }
277
278 #[qjs(rename = "statusText")]
279 pub fn status_text(&self) -> String {
280 self.inner.status_text().to_string()
281 }
282
283 #[qjs(rename = "url")]
284 pub fn url(&self) -> String {
285 self.inner.url().to_string()
286 }
287
288 #[qjs(rename = "ok")]
289 pub fn ok(&self) -> bool {
290 self.inner.ok()
291 }
292
293 #[qjs(rename = "headersArray")]
296 pub fn headers_array<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
297 let h = self.inner.headers();
298 let pairs: Vec<(&str, &str)> = h.iter().map(|(n, v)| (n.as_str(), v.as_str())).collect();
299 crate::bindings::convert::name_value_array_to_js(&ctx, &pairs)
300 }
301
302 #[qjs(rename = "header")]
304 pub fn header(&self, name: String) -> Option<String> {
305 self.inner.header(&name).map(str::to_string)
306 }
307
308 #[qjs(rename = "text")]
310 pub fn text(&self) -> rquickjs::Result<String> {
311 self.inner.text().into_js()
312 }
313
314 #[qjs(rename = "json")]
316 pub fn json<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
317 let text = self.inner.text().into_js()?;
322 ctx.json_parse(text)
323 }
324}