use std::{collections::HashMap, time::Duration};
use http::{header, HeaderName, HeaderValue, Method, StatusCode};
use reqwest::redirect::Policy;
use serde::Serialize;
use tauri::{command, AppHandle, Runtime};
use crate::{Error, FetchRequest, HttpExt, RequestId};
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchResponse {
status: u16,
status_text: String,
headers: Vec<(String, String)>,
url: String,
}
#[command]
pub async fn fetch<R: Runtime>(
app: AppHandle<R>,
method: String,
url: url::Url,
headers: Vec<(String, String)>,
data: Option<Vec<u8>>,
connect_timeout: Option<u64>,
max_redirections: Option<usize>,
) -> crate::Result<RequestId> {
let scheme = url.scheme();
let method = Method::from_bytes(method.as_bytes())?;
let headers: HashMap<String, String> = HashMap::from_iter(headers);
match scheme {
"http" | "https" => {
if app.http().scope.is_allowed(&url) {
let mut builder = reqwest::ClientBuilder::new();
if let Some(timeout) = connect_timeout {
builder = builder.connect_timeout(Duration::from_millis(timeout));
}
if let Some(max_redirections) = max_redirections {
builder = builder.redirect(if max_redirections == 0 {
Policy::none()
} else {
Policy::limited(max_redirections)
});
}
let mut request = builder.build()?.request(method.clone(), url);
for (key, value) in &headers {
let name = HeaderName::from_bytes(key.as_bytes())?;
let v = HeaderValue::from_bytes(value.as_bytes())?;
if !matches!(name, header::HOST | header::CONTENT_LENGTH) {
request = request.header(name, v);
}
}
if data.is_none() && matches!(method, Method::POST | Method::PUT) {
request = request.header(header::CONTENT_LENGTH, HeaderValue::from(0));
}
if headers.contains_key(header::RANGE.as_str()) {
request = request.header(
header::ACCEPT_ENCODING,
HeaderValue::from_static("identity"),
);
}
if !headers.contains_key(header::USER_AGENT.as_str()) {
request = request.header(header::USER_AGENT, HeaderValue::from_static("tauri"));
}
if let Some(data) = data {
request = request.body(data);
}
let http_state = app.http();
let rid = http_state.next_id();
let fut = async move { Ok(request.send().await.map_err(Into::into)) };
let mut request_table = http_state.requests.lock().await;
request_table.insert(rid, FetchRequest::new(Box::pin(fut)));
Ok(rid)
} else {
Err(Error::UrlNotAllowed(url))
}
}
"data" => {
let data_url =
data_url::DataUrl::process(url.as_str()).map_err(|_| Error::DataUrlError)?;
let (body, _) = data_url
.decode_to_vec()
.map_err(|_| Error::DataUrlDecodeError)?;
let response = http::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, data_url.mime_type().to_string())
.body(reqwest::Body::from(body))?;
let http_state = app.http();
let rid = http_state.next_id();
let fut = async move { Ok(Ok(reqwest::Response::from(response))) };
let mut request_table = http_state.requests.lock().await;
request_table.insert(rid, FetchRequest::new(Box::pin(fut)));
Ok(rid)
}
_ => Err(Error::SchemeNotSupport(scheme.to_string())),
}
}
#[command]
pub async fn fetch_cancel<R: Runtime>(app: AppHandle<R>, rid: RequestId) -> crate::Result<()> {
let mut request_table = app.http().requests.lock().await;
let req = request_table
.get_mut(&rid)
.ok_or(Error::InvalidRequestId(rid))?;
*req = FetchRequest::new(Box::pin(async { Err(Error::RequestCanceled) }));
Ok(())
}
#[command]
pub async fn fetch_send<R: Runtime>(
app: AppHandle<R>,
rid: RequestId,
) -> crate::Result<FetchResponse> {
let mut request_table = app.http().requests.lock().await;
let req = request_table
.remove(&rid)
.ok_or(Error::InvalidRequestId(rid))?;
let res = match req.0.lock().await.as_mut().await {
Ok(Ok(res)) => res,
Ok(Err(e)) | Err(e) => return Err(e),
};
let status = res.status();
let url = res.url().to_string();
let mut headers = Vec::new();
for (key, val) in res.headers().iter() {
headers.push((
key.as_str().into(),
String::from_utf8(val.as_bytes().to_vec())?,
));
}
app.http().responses.lock().await.insert(rid, res);
Ok(FetchResponse {
status: status.as_u16(),
status_text: status.canonical_reason().unwrap_or_default().to_string(),
headers,
url,
})
}
#[command]
pub(crate) async fn fetch_read_body<R: Runtime>(
app: AppHandle<R>,
rid: RequestId,
) -> crate::Result<tauri::ipc::Response> {
let mut response_table = app.http().responses.lock().await;
let res = response_table
.remove(&rid)
.ok_or(Error::InvalidRequestId(rid))?;
Ok(tauri::ipc::Response::new(res.bytes().await?.to_vec()))
}