Depot API & executor launch (#173)
* feat: depot api downloads * feat: frontend fixes and experimental webview store * feat: sync downloader * feat: cleanup and fixes * feat: encrypted database and fixed resuming * feat: launch option selector * fix: autostart when no options * fix: clippy * fix: clippy x2 * feat: executor launch * feat: executor launch * feat: not installed error handling * feat: better offline handling * feat: dependency popup * fix: cancelation and resuming issues * feat: dedup by platform * feat: new ui for additional components and fix dl manager clog * feat: auto-queue dependencies * feat: depot scanning and ranking * feat: new library fetching stack * In-app store page (Windows + macOS) (#176) * feat: async store loading * feat: fix overscroll behaviour * fix: query params in server protocol * fix: clippy
This commit is contained in:
@@ -21,7 +21,8 @@ pub struct DropServerError {
|
||||
|
||||
#[derive(Debug, SerializeDisplay)]
|
||||
pub enum RemoteAccessError {
|
||||
FetchError(Arc<reqwest::Error>),
|
||||
FetchErrorLegacy(Arc<reqwest::Error>),
|
||||
FetchError(Arc<reqwest_middleware::Error>),
|
||||
FetchErrorWS(Arc<reqwest_websocket::Error>),
|
||||
ParsingError(ParseError),
|
||||
InvalidEndpoint,
|
||||
@@ -33,6 +34,7 @@ pub enum RemoteAccessError {
|
||||
OutOfSync,
|
||||
Cache(std::io::Error),
|
||||
CorruptedState,
|
||||
NoDepots,
|
||||
}
|
||||
|
||||
impl Display for RemoteAccessError {
|
||||
@@ -56,6 +58,15 @@ impl Display for RemoteAccessError {
|
||||
.unwrap_or("Unknown error".to_string())
|
||||
)
|
||||
}
|
||||
RemoteAccessError::FetchErrorLegacy(error) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
error,
|
||||
error
|
||||
.source()
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or("Unknown error".to_string())
|
||||
),
|
||||
RemoteAccessError::FetchErrorWS(error) => write!(
|
||||
f,
|
||||
"{}: {}",
|
||||
@@ -93,13 +104,19 @@ impl Display for RemoteAccessError {
|
||||
f,
|
||||
"Drop encountered a corrupted internal state. Please report this to the developers, with details of reproduction."
|
||||
),
|
||||
}
|
||||
RemoteAccessError::NoDepots => write!(f, "There are no download depots configured on the server. Contact your server admin."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for RemoteAccessError {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
RemoteAccessError::FetchError(Arc::new(err))
|
||||
RemoteAccessError::FetchErrorLegacy(Arc::new(err))
|
||||
}
|
||||
}
|
||||
impl From<reqwest_middleware::Error> for RemoteAccessError {
|
||||
fn from(value: reqwest_middleware::Error) -> Self {
|
||||
RemoteAccessError::FetchError(Arc::new(value))
|
||||
}
|
||||
}
|
||||
impl From<reqwest_websocket::Error> for RemoteAccessError {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use database::{DB, interface::DatabaseImpls};
|
||||
use database::{DB};
|
||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||
use log::{debug, warn};
|
||||
use tauri::UriSchemeResponder;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
#![feature(slice_concat_trait)]
|
||||
#![feature(sync_nonpoison)]
|
||||
#![feature(nonpoison_mutex)]
|
||||
|
||||
pub mod auth;
|
||||
#[macro_use]
|
||||
pub mod cache;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
use database::{DB, interface::DatabaseImpls};
|
||||
use database::{DB};
|
||||
use reqwest_middleware::Error;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
auth::generate_authorization_header, error::RemoteAccessError, utils::DROP_CLIENT_ASYNC,
|
||||
};
|
||||
|
||||
pub fn generate_url<T: AsRef<str>>(
|
||||
path_components: &[T],
|
||||
query: &[(T, T)],
|
||||
pub fn generate_url(
|
||||
path_components: &[&str],
|
||||
query: &[(&str, &str)],
|
||||
) -> Result<Url, RemoteAccessError> {
|
||||
let mut base_url = DB.fetch_base_url();
|
||||
for endpoint in path_components {
|
||||
base_url = base_url.join(endpoint.as_ref())?;
|
||||
}
|
||||
let path_appended = path_components.join("/");
|
||||
let mut base_url = DB.fetch_base_url().join(&path_appended)?;
|
||||
{
|
||||
let mut queries = base_url.query_pairs_mut();
|
||||
for (param, val) in query {
|
||||
@@ -22,7 +21,7 @@ pub fn generate_url<T: AsRef<str>>(
|
||||
Ok(base_url)
|
||||
}
|
||||
|
||||
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, reqwest::Error> {
|
||||
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, Error> {
|
||||
DROP_CLIENT_ASYNC
|
||||
.get(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use database::borrow_db_checked;
|
||||
use http::{Request, Response, StatusCode, Uri, uri::PathAndQuery};
|
||||
use http::{
|
||||
HeaderMap, HeaderValue, Request, Response, StatusCode, Uri, header::USER_AGENT,
|
||||
};
|
||||
use log::{error, warn};
|
||||
use tauri::UriSchemeResponder;
|
||||
use utils::webbrowser_open::webbrowser_open;
|
||||
|
||||
use crate::utils::DROP_CLIENT_SYNC;
|
||||
use crate::utils::DROP_CLIENT_ASYNC;
|
||||
|
||||
pub async fn handle_server_proto_offline_wrapper(
|
||||
request: Request<Vec<u8>>,
|
||||
@@ -36,6 +35,7 @@ pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: U
|
||||
Response::builder()
|
||||
.status(e)
|
||||
.body(Vec::new())
|
||||
.inspect_err(|v| warn!("{:?}", v))
|
||||
.expect("Failed to build error response"),
|
||||
);
|
||||
}
|
||||
@@ -43,48 +43,49 @@ pub async fn handle_server_proto_wrapper(request: Request<Vec<u8>>, responder: U
|
||||
}
|
||||
|
||||
async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u8>>, StatusCode> {
|
||||
let db_handle = borrow_db_checked();
|
||||
let auth = match db_handle.auth.as_ref() {
|
||||
Some(auth) => auth,
|
||||
None => {
|
||||
error!("Could not find auth in database");
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
let (remote_uri, web_token) = {
|
||||
let db_handle = borrow_db_checked();
|
||||
let auth = match db_handle.auth.as_ref() {
|
||||
Some(auth) => auth,
|
||||
None => {
|
||||
error!("Could not find auth in database");
|
||||
return Err(StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
};
|
||||
let web_token = match &auth.web_token {
|
||||
Some(token) => token.clone(),
|
||||
None => return Err(StatusCode::UNAUTHORIZED),
|
||||
};
|
||||
let remote_uri = db_handle
|
||||
.base_url
|
||||
.parse::<Uri>()
|
||||
.inspect_err(|v| warn!("{:?}", v))
|
||||
.expect("Failed to parse base url");
|
||||
(remote_uri, web_token)
|
||||
};
|
||||
let web_token = match &auth.web_token {
|
||||
Some(token) => token,
|
||||
None => return Err(StatusCode::UNAUTHORIZED),
|
||||
};
|
||||
let remote_uri = db_handle
|
||||
.base_url
|
||||
.parse::<Uri>()
|
||||
.expect("Failed to parse base url");
|
||||
|
||||
let path = request.uri().path();
|
||||
|
||||
let mut new_uri = request.uri().clone().into_parts();
|
||||
new_uri.path_and_query = Some(
|
||||
PathAndQuery::from_str(&format!("{path}?noWrapper=true"))
|
||||
.expect("Failed to parse request path in proto"),
|
||||
);
|
||||
new_uri.authority = remote_uri.authority().cloned();
|
||||
new_uri.scheme = remote_uri.scheme().cloned();
|
||||
let err_msg = &format!("Failed to build new uri from parts {new_uri:?}");
|
||||
let new_uri = Uri::from_parts(new_uri).expect(err_msg);
|
||||
let new_uri = Uri::from_parts(new_uri)
|
||||
.inspect_err(|v| warn!("{:?}", v))
|
||||
.expect(err_msg);
|
||||
|
||||
let whitelist_prefix = ["/store", "/api", "/_", "/fonts"];
|
||||
let mut headers = HeaderMap::new();
|
||||
request.headers().clone_into(&mut headers);
|
||||
headers.remove(USER_AGENT);
|
||||
headers.append(USER_AGENT, HeaderValue::from_static("Drop Desktop Client"));
|
||||
headers.append(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Bearer {web_token}")).unwrap(),
|
||||
);
|
||||
|
||||
if whitelist_prefix.iter().all(|f| !path.starts_with(f)) {
|
||||
webbrowser_open(new_uri.to_string());
|
||||
return Ok(Response::new(Vec::new()));
|
||||
}
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = match client
|
||||
let response = match DROP_CLIENT_ASYNC
|
||||
.request(request.method().clone(), new_uri.to_string())
|
||||
.header("Authorization", format!("Bearer {web_token}"))
|
||||
.headers(request.headers().clone())
|
||||
.headers(headers)
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
@@ -94,15 +95,26 @@ async fn handle_server_proto(request: Request<Vec<u8>>) -> Result<Response<Vec<u
|
||||
};
|
||||
|
||||
let response_status = response.status();
|
||||
let response_body = match response.bytes() {
|
||||
let mut client_http_response = Response::builder()
|
||||
.status(response_status)
|
||||
.header("Access-Control-Allow-Origin", "*");
|
||||
|
||||
{
|
||||
let client_response_headers = client_http_response.headers_mut().unwrap();
|
||||
for (header, header_value) in response.headers() {
|
||||
client_response_headers.insert(header, header_value.clone());
|
||||
}
|
||||
};
|
||||
|
||||
let response_body = match response.bytes().await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => return Err(e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)),
|
||||
};
|
||||
|
||||
let http_response = Response::builder()
|
||||
.status(response_status)
|
||||
let client_http_response = client_http_response
|
||||
.body(response_body.to_vec())
|
||||
.inspect_err(|v| warn!("{:?}", v))
|
||||
.expect("Failed to build server proto response");
|
||||
|
||||
Ok(http_response)
|
||||
Ok(client_http_response)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,20 @@ use std::{
|
||||
fs::{self, File},
|
||||
io::Read,
|
||||
sync::LazyLock,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use client::{app_state::AppState, app_status::AppStatus};
|
||||
use database::db::DATA_ROOT_DIR;
|
||||
use http::Extensions;
|
||||
use log::{debug, info, warn};
|
||||
use reqwest::Certificate;
|
||||
use reqwest_middleware::{
|
||||
ClientBuilder, ClientWithMiddleware, Error, Middleware, Next, Result,
|
||||
reqwest::{Request, Response},
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use tauri::{AppHandle, Emitter, Manager, async_runtime::Mutex};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -21,9 +29,65 @@ impl DropHealthcheck {
|
||||
}
|
||||
static DROP_CERT_BUNDLE: LazyLock<Vec<Certificate>> = LazyLock::new(fetch_certificates);
|
||||
pub static DROP_CLIENT_SYNC: LazyLock<reqwest::blocking::Client> = LazyLock::new(get_client_sync);
|
||||
pub static DROP_CLIENT_ASYNC: LazyLock<reqwest::Client> = LazyLock::new(get_client_async);
|
||||
pub static DROP_CLIENT_ASYNC: LazyLock<ClientWithMiddleware> = LazyLock::new(get_client_async);
|
||||
pub static DROP_CLIENT_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
|
||||
|
||||
pub static DROP_APP_HANDLE: LazyLock<Mutex<Option<AppHandle>>> = LazyLock::new(|| Mutex::new(None));
|
||||
|
||||
struct AutoOfflineMiddleware;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Middleware for AutoOfflineMiddleware {
|
||||
async fn handle(
|
||||
&self,
|
||||
req: Request,
|
||||
extensions: &mut Extensions,
|
||||
next: Next<'_>,
|
||||
) -> Result<Response> {
|
||||
let res = next.run(req, extensions).await;
|
||||
match res {
|
||||
Ok(res) => {
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let lock = DROP_APP_HANDLE.lock().await;
|
||||
if let Some(app_handle) = &*lock {
|
||||
let state = app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
|
||||
let mut state_lock = state.lock();
|
||||
if state_lock.status == AppStatus::Offline {
|
||||
state_lock.status = AppStatus::SignedIn;
|
||||
app_handle
|
||||
.emit("update_state", &*state_lock)
|
||||
.expect("failed to emit state update");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
Err(err) => match err {
|
||||
Error::Middleware(error) => Err(Error::Middleware(error)),
|
||||
Error::Reqwest(error) => {
|
||||
if error.is_connect() {
|
||||
// Spawn to defer this action - the state will most likely be locked
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let lock = DROP_APP_HANDLE.lock().await;
|
||||
if let Some(app_handle) = &*lock {
|
||||
let state =
|
||||
app_handle.state::<std::sync::nonpoison::Mutex<AppState>>();
|
||||
let mut state_lock = state.lock();
|
||||
state_lock.status = AppStatus::Offline;
|
||||
app_handle
|
||||
.emit("update_state", &*state_lock)
|
||||
.expect("failed to emit state update");
|
||||
};
|
||||
});
|
||||
};
|
||||
Err(Error::Reqwest(error))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_certificates() -> Vec<Certificate> {
|
||||
let certificate_dir = DATA_ROOT_DIR.join("certificates");
|
||||
|
||||
@@ -91,19 +155,26 @@ pub fn get_client_sync() -> reqwest::blocking::Client {
|
||||
}
|
||||
client
|
||||
.use_rustls_tls()
|
||||
.user_agent("Drop Desktop Client")
|
||||
.connect_timeout(Duration::from_millis(1500))
|
||||
.build()
|
||||
.expect("Failed to build synchronous client")
|
||||
}
|
||||
pub fn get_client_async() -> reqwest::Client {
|
||||
pub fn get_client_async() -> ClientWithMiddleware {
|
||||
let mut client = reqwest::ClientBuilder::new();
|
||||
|
||||
for cert in DROP_CERT_BUNDLE.iter() {
|
||||
client = client.add_root_certificate(cert.clone());
|
||||
}
|
||||
client
|
||||
let normal_client = client
|
||||
.use_rustls_tls()
|
||||
.user_agent("Drop Desktop Client")
|
||||
.build()
|
||||
.expect("Failed to build asynchronous client");
|
||||
|
||||
ClientBuilder::new(normal_client)
|
||||
.with(AutoOfflineMiddleware)
|
||||
.build()
|
||||
.expect("Failed to build asynchronous client")
|
||||
}
|
||||
pub fn get_client_ws() -> reqwest::Client {
|
||||
let mut client = reqwest::ClientBuilder::new();
|
||||
@@ -113,6 +184,7 @@ pub fn get_client_ws() -> reqwest::Client {
|
||||
}
|
||||
client
|
||||
.use_rustls_tls()
|
||||
.user_agent("Drop Desktop Client")
|
||||
.http1_only()
|
||||
.build()
|
||||
.expect("Failed to build websocket client")
|
||||
|
||||
Reference in New Issue
Block a user