Compare commits

..

7 Commits

Author SHA1 Message Date
f1bc7bf418 fix: update all versions to -mac specific 2025-08-02 12:02:34 +10:00
cb2295dd7c feat: add macos signing args 2025-08-02 12:00:25 +10:00
cc5339a389 Reqwest optionally load certificates from disk (#94)
* feat: Add get_client function

Signed-off-by: quexeky <git@quexeky.dev>

* chore: Converted all instances of reqwest::blocking::Client::new() and reqwest::Client::new() to DROP_CLIENT_SYNC and DROP_CLIENT_ASYNC respectively

Signed-off-by: quexeky <git@quexeky.dev>

* fix: use_remote_logic not using certificates

Signed-off-by: quexeky <git@quexeky.dev>

* fix: add log statement to certificates

* chore: add more logging

* fix: clippy

* refactor: into single fetch_certificates func

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-08-02 11:59:50 +10:00
6104bfda72 Bump version to v0.3.1 (#85) 2025-08-01 14:13:13 +10:00
be688cb18f Version bump: v0.3.0 2025-08-01 14:09:16 +10:00
13cc69f10e Device code authorization (#83)
* feat: device code authorization

* Fix for setup executable unable to be launched (#81)

* Fix for redownload invalid chunks (#84)

* feat: Redownloading invalid chunks

Signed-off-by: quexeky <git@quexeky.dev>

* fix: clippy

* fix: clippy x2

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>

* chore: Run clippy fix pedantic

Signed-off-by: quexeky <git@quexeky.dev>

* feat: add better error handling

* fix: clippy

---------

Signed-off-by: quexeky <git@quexeky.dev>
Co-authored-by: quexeky <git@quexeky.dev>
2025-08-01 13:12:05 +10:00
574782f445 chore: Run clippy fix pedantic
Signed-off-by: quexeky <git@quexeky.dev>
2025-08-01 08:42:45 +10:00
33 changed files with 414 additions and 166 deletions

View File

@ -60,6 +60,9 @@ jobs:
- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ env.APPLE_SIGNING_IDENTITY }}
with:
tagName: v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version.
releaseName: 'Auto-release v__VERSION__'

View File

@ -13,11 +13,7 @@
<div class="max-w-lg">
<slot />
<div class="mt-10">
<button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="text-sm text-left font-semibold leading-7 text-blue-600"
>
<div>
<div v-if="loading" role="status">
<svg
aria-hidden="true"
@ -37,10 +33,19 @@
</svg>
<span class="sr-only">Loading...</span>
</div>
<span v-else>
Sign in with your browser <span aria-hidden="true">&rarr;</span>
<span class="inline-flex gap-x-8 items-center" v-else>
<button
@click="() => authWrapper_wrapper()"
:disabled="loading"
class="px-3 py-1 inline-flex items-center gap-x-2 bg-zinc-700 rounded text-sm text-left font-semibold leading-7 text-white"
>
Sign in with your browser <ArrowTopRightOnSquareIcon class="size-4" />
</button>
<NuxtLink href="/auth/code" class="text-zinc-100 text-sm hover:text-zinc-300">
Use a code &rarr;
</NuxtLink>
</span>
</button>
</div>
<div class="mt-5" v-if="offerManual">
<h1 class="text-zinc-100 font-semibold">Having trouble?</h1>
@ -121,6 +126,7 @@
<script setup lang="ts">
import { XCircleIcon } from "@heroicons/vue/16/solid";
import { ArrowTopRightOnSquareIcon } from "@heroicons/vue/20/solid";
import { invoke } from "@tauri-apps/api/core";
const loading = ref(false);

View File

@ -1,6 +1,6 @@
<template>
<div
class="h-10 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
class="h-16 cursor-pointer flex flex-row items-center justify-between bg-zinc-950"
>
<div class="px-5 py-3 grow" @mousedown="() => window.startDragging()">
<Wordmark class="mt-1" />

View File

@ -1,7 +1,7 @@
{
"name": "drop-app",
"private": true,
"version": "0.3.0",
"version": "0.3.1-mac",
"type": "module",
"scripts": {
"build": "nuxt build",

37
pages/auth/code.vue Normal file
View File

@ -0,0 +1,37 @@
<template>
<div class="min-h-full w-full flex items-center justify-center">
<div class="flex flex-col items-center">
<div class="text-center">
<h1 class="text-3xl font-semibold font-display leading-6 text-zinc-100">
Device authorization
</h1>
<div class="mt-4">
<p class="text-sm text-zinc-400 max-w-md mx-auto">
Open Drop on another one of your devices, and use your account
dropdown to "Authorize client", and enter the code below.
</p>
<div
class="mt-8 flex items-center justify-center gap-x-5 text-8xl font-bold text-zinc-100"
>
<span v-for="letter in code.split('')">{{ letter }}</span>
</div>
</div>
<div class="mt-10 flex items-center justify-center gap-x-6">
<NuxtLink href="/auth" class="text-sm font-semibold text-blue-600"
><span aria-hidden="true">&larr;</span> Use a different method
</NuxtLink>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { invoke } from "@tauri-apps/api/core";
const code = await invoke<string>("auth_initiate_code");
definePageMeta({
layout: "mini",
});
</script>

56
src-tauri/Cargo.lock generated
View File

@ -345,6 +345,22 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "async-tungstenite"
version = "0.29.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef0f7efedeac57d9b26170f72965ecfd31473ca52ca7a64e925b0b6f5f079886"
dependencies = [
"atomic-waker",
"futures-core",
"futures-io",
"futures-task",
"futures-util",
"log",
"pin-project-lite",
"tungstenite",
]
[[package]]
name = "atk"
version = "0.18.2"
@ -1268,7 +1284,7 @@ dependencies = [
[[package]]
name = "drop-app"
version = "0.3.0"
version = "0.3.1-mac"
dependencies = [
"atomic-instant-full",
"bitcode",
@ -1280,6 +1296,7 @@ dependencies = [
"droplet-rs",
"dynfmt",
"filetime",
"futures-lite",
"gethostname",
"hex 0.4.3",
"http 1.3.1",
@ -1296,6 +1313,7 @@ dependencies = [
"reqwest 0.12.16",
"reqwest-middleware 0.4.2",
"reqwest-middleware-cache",
"reqwest-websocket",
"rustbreak",
"rustix 0.38.44",
"schemars",
@ -4357,6 +4375,24 @@ dependencies = [
"url",
]
[[package]]
name = "reqwest-websocket"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f91a811daaa8b54faeaec9d507a336897a3d243834a4965254a17d39da8b5c9"
dependencies = [
"async-tungstenite",
"bytes",
"futures-util",
"reqwest 0.12.16",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
"tungstenite",
"web-sys",
]
[[package]]
name = "rfd"
version = "0.15.3"
@ -5831,6 +5867,7 @@ checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"pin-project-lite",
"tokio",
@ -6009,6 +6046,23 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http 1.3.1",
"httparse",
"log",
"rand 0.9.1",
"sha1",
"thiserror 2.0.12",
"utf-8",
]
[[package]]
name = "typeid"
version = "1.0.3"

View File

@ -1,6 +1,6 @@
[package]
name = "drop-app"
version = "0.3.0"
version = "0.3.1-mac"
description = "The client application for the open-source, self-hosted game distribution platform Drop"
authors = ["Drop OSS"]
edition = "2024"
@ -68,6 +68,8 @@ known-folders = "1.2.0"
native_model = { version = "0.6.1", features = ["rmp_serde_1_3"] }
tauri-plugin-opener = "2.4.0"
bitcode = "0.6.6"
reqwest-websocket = "0.5.0"
futures-lite = "2.6.0"
# tailscale = { path = "./tailscale" }
[dependencies.dynfmt]

View File

@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build();
}

View File

@ -13,8 +13,8 @@ pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mut
let download_manager = state.lock().unwrap().download_manager.clone();
match download_manager.ensure_terminated() {
Ok(res) => match res {
Ok(_) => debug!("download manager terminated correctly"),
Err(_) => error!("download manager failed to terminate correctly"),
Ok(()) => debug!("download manager terminated correctly"),
Err(()) => error!("download manager failed to terminate correctly"),
},
Err(e) => panic!("{e:?}"),
}

View File

@ -67,20 +67,17 @@ impl DatabaseImpls for DatabaseInterface {
let exists = fs::exists(db_path.clone()).unwrap();
match exists {
true => match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
},
false => {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default)
.expect("Database could not be created")
}
if exists { match PathDatabase::load_from_path(db_path.clone()) {
Ok(db) => db,
Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir),
} } else {
let default = Database::new(games_base_dir, None, cache_dir);
debug!(
"Creating database at path {}",
db_path.as_os_str().to_str().unwrap()
);
PathDatabase::create_at_path(db_path, default)
.expect("Database could not be created")
}
}
@ -137,19 +134,19 @@ impl<'a> Deref for DBRead<'a> {
&self.0
}
}
impl<'a> DerefMut for DBWrite<'a> {
impl DerefMut for DBWrite<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> Drop for DBWrite<'a> {
impl Drop for DBWrite<'_> {
fn drop(&mut self) {
unsafe {
ManuallyDrop::drop(&mut self.0);
}
match DB.save() {
Ok(_) => {}
Ok(()) => {}
Err(e) => {
error!("database failed to save with error {e}");
panic!("database failed to save with error {e}")

View File

@ -27,7 +27,7 @@ pub mod data {
use serde_with::serde_as;
use std::{collections::HashMap, path::PathBuf};
use super::*;
use super::{Serialize, Deserialize, native_model};
fn default_template() -> String {
"{}".to_owned()
@ -174,7 +174,7 @@ pub mod data {
use serde_with::serde_as;
use super::*;
use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, v1, GameVersion, DownloadableMetadata, ApplicationTransientStatus};
#[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
@ -283,7 +283,7 @@ pub mod data {
mod v3 {
use std::path::PathBuf;
use super::*;
use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, DatabaseApplications, DatabaseCompatInfo, v2};
#[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct Database {
@ -336,7 +336,7 @@ pub mod data {
transient_statuses: HashMap::new(),
},
prev_database,
base_url: "".to_owned(),
base_url: String::new(),
auth: None,
settings: Settings::default(),
cache_dir,

View File

@ -4,12 +4,12 @@ use crate::{database::models::data::DownloadableMetadata, AppState};
#[tauri::command]
pub fn pause_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.pause_downloads()
state.lock().unwrap().download_manager.pause_downloads();
}
#[tauri::command]
pub fn resume_downloads(state: tauri::State<'_, Mutex<AppState>>) {
state.lock().unwrap().download_manager.resume_downloads()
state.lock().unwrap().download_manager.resume_downloads();
}
#[tauri::command]
@ -22,10 +22,10 @@ pub fn move_download_in_queue(
.lock()
.unwrap()
.download_manager
.rearrange(old_index, new_index)
.rearrange(old_index, new_index);
}
#[tauri::command]
pub fn cancel_game(state: tauri::State<'_, Mutex<AppState>>, meta: DownloadableMetadata) {
state.lock().unwrap().download_manager.cancel(meta)
state.lock().unwrap().download_manager.cancel(meta);
}

View File

@ -176,7 +176,7 @@ impl DownloadManagerBuilder {
DownloadManagerSignal::Cancel(meta) => {
self.manage_cancel_signal(&meta);
}
};
}
}
}
fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {

View File

@ -23,13 +23,13 @@ use super::{
};
pub enum DownloadManagerSignal {
/// Resumes (or starts) the DownloadManager
/// Resumes (or starts) the `DownloadManager`
Go,
/// Pauses the DownloadManager
/// Pauses the `DownloadManager`
Stop,
/// Called when a DownloadAgent has fully completed a download.
/// Called when a `DownloadAgent` has fully completed a download.
Completed(DownloadableMetadata),
/// Generates and appends a DownloadAgent
/// Generates and appends a `DownloadAgent`
/// to the registry and queue
Queue(DownloadAgent),
/// Tells the Manager to stop the current
@ -70,14 +70,14 @@ pub enum DownloadStatus {
Error,
}
/// Accessible front-end for the DownloadManager
/// Accessible front-end for the `DownloadManager`
///
/// The system works entirely through signals, both internally and externally,
/// all of which are accessible through the DownloadManagerSignal type, but
/// all of which are accessible through the `DownloadManagerSignal` type, but
/// should not be used directly. Rather, signals are abstracted through this
/// interface.
///
/// The actual download queue may be accessed through the .edit() function,
/// The actual download queue may be accessed through the .`edit()` function,
/// which provides raw access to the underlying queue.
/// THIS EDITING IS BLOCKING!!!
pub struct DownloadManager {
@ -139,7 +139,7 @@ impl DownloadManager {
pub fn rearrange(&self, current_index: usize, new_index: usize) {
if current_index == new_index {
return;
};
}
let needs_pause = current_index == 0 || new_index == 0;
if needs_pause {
@ -183,7 +183,7 @@ impl DownloadManager {
}
}
/// Takes in the locked value from .edit() and attempts to
/// Takes in the locked value from .`edit()` and attempts to
/// get the index of whatever id is passed in
fn get_index_from_id(
queue: &mut MutexGuard<'_, VecDeque<DownloadableMetadata>>,

View File

@ -22,10 +22,7 @@ impl From<DownloadThreadControlFlag> for bool {
/// false => Stop
impl From<bool> for DownloadThreadControlFlag {
fn from(value: bool) -> Self {
match value {
true => DownloadThreadControlFlag::Go,
false => DownloadThreadControlFlag::Stop,
}
if value { DownloadThreadControlFlag::Go } else { DownloadThreadControlFlag::Stop }
}
}

View File

@ -11,7 +11,7 @@ pub struct RollingProgressWindow<const S: usize> {
impl<const S: usize> RollingProgressWindow<S> {
pub fn new() -> Self {
Self {
window: Arc::new([(); S].map(|_| AtomicUsize::new(0))),
window: Arc::new([(); S].map(|()| AtomicUsize::new(0))),
current: Arc::new(AtomicUsize::new(0)),
}
}

View File

@ -13,6 +13,7 @@ use super::drop_server_error::DropServerError;
#[derive(Debug, SerializeDisplay)]
pub enum RemoteAccessError {
FetchError(Arc<reqwest::Error>),
FetchErrorWS(Arc<reqwest_websocket::Error>),
ParsingError(ParseError),
InvalidEndpoint,
HandshakeFailed(String),
@ -29,7 +30,10 @@ impl Display for RemoteAccessError {
match self {
RemoteAccessError::FetchError(error) => {
if error.is_connect() {
return write!(f, "Failed to connect to Drop server. Check if you access Drop through a browser, and then try again.");
return write!(
f,
"Failed to connect to Drop server. Check if you access Drop through a browser, and then try again."
);
}
write!(
@ -38,24 +42,44 @@ impl Display for RemoteAccessError {
error,
error
.source()
.map(|e| e.to_string())
.map(std::string::ToString::to_string)
.or_else(|| Some("Unknown error".to_string()))
.unwrap()
)
},
}
RemoteAccessError::FetchErrorWS(error) => write!(
f,
"{}: {}",
error,
error
.source()
.map(|e| e.to_string())
.or_else(|| Some("Unknown error".to_string()))
.unwrap()
),
RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{parse_error}")
}
RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"),
RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {message}"),
RemoteAccessError::HandshakeFailed(message) => {
write!(f, "failed to complete handshake: {message}")
}
RemoteAccessError::GameNotFound(id) => write!(f, "could not find game on server: {id}"),
RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {}, {}", error.status_code, error.status_message),
RemoteAccessError::UnparseableResponse(error) => write!(f, "server returned an invalid response: {error}"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
RemoteAccessError::InvalidResponse(error) => write!(
f,
"failed to download game manifest: {status} {response}"
"server returned an invalid response: {}, {}",
error.status_code, error.status_message
),
RemoteAccessError::UnparseableResponse(error) => {
write!(f, "server returned an invalid response: {error}")
}
RemoteAccessError::ManifestDownloadFailed(status, response) => {
write!(f, "failed to download game manifest: {status} {response}")
}
RemoteAccessError::OutOfSync => write!(
f,
"server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"
),
RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"),
RemoteAccessError::Cache(error) => write!(f, "Cache Error: {error}"),
}
}
@ -66,6 +90,11 @@ impl From<reqwest::Error> for RemoteAccessError {
RemoteAccessError::FetchError(Arc::new(err))
}
}
impl From<reqwest_websocket::Error> for RemoteAccessError {
fn from(err: reqwest_websocket::Error) -> Self {
RemoteAccessError::FetchErrorWS(Arc::new(err))
}
}
impl From<ParseError> for RemoteAccessError {
fn from(err: ParseError) -> Self {
RemoteAccessError::ParsingError(err)

View File

@ -1,19 +1,18 @@
use reqwest::blocking::Client;
use serde_json::json;
use url::Url;
use crate::{
DB,
database::db::DatabaseImpls,
error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request},
DB,
remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC},
};
use super::collection::{Collection, Collections};
#[tauri::command]
pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
let client = Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(&client, &["/api/v1/client/collection"], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
@ -24,7 +23,7 @@ pub fn fetch_collections() -> Result<Collections, RemoteAccessError> {
#[tauri::command]
pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAccessError> {
let client = Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(
&client,
&["/api/v1/client/collection/", &collection_id],
@ -38,7 +37,7 @@ pub fn fetch_collection(collection_id: String) -> Result<Collection, RemoteAcces
#[tauri::command]
pub fn create_collection(name: String) -> Result<Collection, RemoteAccessError> {
let client = Client::new();
let client = DROP_CLIENT_SYNC.clone();
let base_url = DB.fetch_base_url();
let base_url = Url::parse(&format!("{base_url}api/v1/client/collection/"))?;
@ -57,7 +56,7 @@ pub fn add_game_to_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = Client::new();
let client = DROP_CLIENT_SYNC.clone();
let url = Url::parse(&format!(
"{}api/v1/client/collection/{}/entry/",
DB.fetch_base_url(),
@ -74,7 +73,7 @@ pub fn add_game_to_collection(
#[tauri::command]
pub fn delete_collection(collection_id: String) -> Result<bool, RemoteAccessError> {
let client = Client::new();
let client = DROP_CLIENT_SYNC.clone();
let base_url = Url::parse(&format!(
"{}api/v1/client/collection/{}",
DB.fetch_base_url(),
@ -93,7 +92,7 @@ pub fn delete_game_in_collection(
collection_id: String,
game_id: String,
) -> Result<(), RemoteAccessError> {
let client = Client::new();
let client = DROP_CLIENT_SYNC.clone();
let base_url = Url::parse(&format!(
"{}api/v1/client/collection/{}/entry",
DB.fetch_base_url(),

View File

@ -15,6 +15,7 @@ use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::downloads::validate::game_validate_logic;
use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update};
use crate::remote::requests::make_request;
use crate::remote::utils::DROP_CLIENT_SYNC;
use log::{debug, error, info};
use rayon::ThreadPoolBuilder;
use std::collections::HashMap;
@ -115,7 +116,7 @@ impl GameDownloadAgent {
);
let res = self
.run()
.map_err(|_| ApplicationDownloadError::DownloadError);
.map_err(|()| ApplicationDownloadError::DownloadError);
debug!(
"{} took {}ms to download",
@ -135,7 +136,7 @@ impl GameDownloadAgent {
fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(
&client,
&["/api/v1/client/game/manifest"],
@ -267,7 +268,7 @@ impl GameDownloadAgent {
let contexts = self.contexts.lock().unwrap();
debug!("{contexts:#?}");
pool.scope(|scope| {
let client = &reqwest::blocking::Client::new();
let client = &DROP_CLIENT_SYNC.clone();
let context_map = self.context_map.lock().unwrap();
for (index, context) in contexts.iter().enumerate() {
let client = client.clone();
@ -322,7 +323,7 @@ impl GameDownloadAgent {
}
});
let newly_completed = completed_contexts.to_owned();
let newly_completed = completed_contexts.clone();
let completed_lock_len = {
let mut context_map_lock = self.context_map.lock().unwrap();
@ -338,7 +339,7 @@ impl GameDownloadAgent {
.map(|x| {
(
x.checksum.clone(),
context_map_lock.get(&x.checksum).cloned().unwrap_or(false),
context_map_lock.get(&x.checksum).copied().unwrap_or(false),
)
})
.collect::<Vec<(String, bool)>>();

View File

@ -151,7 +151,7 @@ pub fn download_game_chunk(
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidResponse(err),
));
};
}
return Err(ApplicationDownloadError::Communication(
RemoteAccessError::UnparseableResponse(raw_res),
));
@ -187,7 +187,7 @@ pub fn download_game_chunk(
.map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
if !completed {
return Ok(false);
};
}
// If we complete the file, set the permissions (if on Linux)
#[cfg(unix)]

View File

@ -38,12 +38,9 @@ pub mod v1 {
impl DropData {
pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self {
let mut file = match File::open(base_path.join(DROP_DATA_PATH)) {
Ok(file) => file,
Err(_) => {
debug!("Generating new dropdata for game {game_id}");
return DropData::new(game_id, game_version, base_path);
}
let mut file = if let Ok(file) = File::open(base_path.join(DROP_DATA_PATH)) { file } else {
debug!("Generating new dropdata for game {game_id}");
return DropData::new(game_id, game_version, base_path);
};
let mut s = Vec::new();
@ -53,7 +50,7 @@ impl DropData {
error!("{e}");
return DropData::new(game_id, game_version, base_path);
}
};
}
match native_model::rmp_serde_1_3::RmpSerde::decode(s) {
Ok(manifest) => manifest,
@ -78,9 +75,9 @@ impl DropData {
};
match file.write_all(&manifest_raw) {
Ok(_) => {}
Ok(()) => {}
Err(e) => error!("{e}"),
};
}
}
pub fn set_contexts(&self, completed_contexts: &[(String, bool)]) {
*self.contexts.lock().unwrap() = completed_contexts.iter().map(|s| (s.0.clone(), s.1)).collect();

View File

@ -125,7 +125,7 @@ pub fn validate_game_chunk(
validate_copy(&mut source, &mut hasher, ctx.length, control_flag, progress).unwrap();
if !completed {
return Ok(false);
};
}
let res = hex::encode(hasher.compute().0);
if res != ctx.checksum {

View File

@ -18,6 +18,7 @@ use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::remote::auth::generate_authorization_header;
use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db};
use crate::remote::requests::make_request;
use crate::remote::utils::DROP_CLIENT_SYNC;
use crate::AppState;
use bitcode::{Encode, Decode};
@ -78,7 +79,7 @@ pub fn fetch_library_logic(
) -> Result<Vec<Game>, RemoteAccessError> {
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| {
f.header("Authorization", header)
})?
@ -96,7 +97,7 @@ pub fn fetch_library_logic(
let mut db_handle = borrow_db_mut_checked();
for game in games.iter() {
for game in &games {
handle.games.insert(game.id.clone(), game.clone());
if !db_handle.applications.game_statuses.contains_key(&game.id) {
db_handle
@ -177,7 +178,7 @@ pub fn fetch_game_logic(
return Ok(data);
}
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
@ -252,7 +253,7 @@ pub fn fetch_game_verion_options_logic(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<Vec<GameVersion>, RemoteAccessError> {
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(
&client,
@ -329,37 +330,34 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle)
drop(db_handle);
let app_handle = app_handle.clone();
spawn(move || match remove_dir_all(install_dir) {
Err(e) => {
error!("{e}");
}
Ok(_) => {
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
.installed_game_version
.remove(&meta.id);
db_handle
.applications
.game_statuses
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle);
spawn(move || if let Err(e) = remove_dir_all(install_dir) {
error!("{e}");
} else {
let mut db_handle = borrow_db_mut_checked();
db_handle.applications.transient_statuses.remove(&meta);
db_handle
.applications
.installed_game_version
.remove(&meta.id);
db_handle
.applications
.game_statuses
.entry(meta.id.clone())
.and_modify(|e| *e = GameDownloadStatus::Remote {});
drop(db_handle);
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap();
debug!("uninstalled game id {}", &meta.id);
app_handle.emit("update_library", ()).unwrap();
push_game_update(
&app_handle,
&meta.id,
None,
(Some(GameDownloadStatus::Remote {}), None),
);
}
push_game_update(
&app_handle,
&meta.id,
None,
(Some(GameDownloadStatus::Remote {}), None),
);
});
} else {
warn!("invalid previous state for uninstall, failing silently.")
warn!("invalid previous state for uninstall, failing silently.");
}
}
@ -381,7 +379,7 @@ pub fn on_game_incomplete(
return Err(RemoteAccessError::GameNotFound(meta.id.clone()));
}
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(
&client,
&["/api/v1/client/game/version"],
@ -443,7 +441,7 @@ pub fn on_game_complete(
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(
&client,
&["/api/v1/client/game/version"],

View File

@ -12,6 +12,7 @@ mod process;
mod remote;
use crate::process::commands::open_process_logs;
use crate::remote::commands::auth_initiate_code;
use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download};
use bitcode::{Decode, Encode};
use client::commands::fetch_state;
@ -163,7 +164,7 @@ fn setup(handle: AppHandle) -> AppState<'static> {
let mut missing_games = Vec::new();
let statuses = db_handle.applications.game_statuses.clone();
drop(db_handle);
for (game_id, status) in statuses.into_iter() {
for (game_id, status) in statuses {
match status {
GameDownloadStatus::Remote {} => {}
GameDownloadStatus::PartiallyInstalled { .. } => {}
@ -267,6 +268,7 @@ pub fn run() {
fetch_settings,
// Auth
auth_initiate,
auth_initiate_code,
retry_connect,
manual_recieve_handshake,
sign_out,
@ -344,7 +346,7 @@ pub fn run() {
let binding = event.urls();
let url = binding.first().unwrap();
if url.host_str().unwrap() == "handshake" {
recieve_handshake(handle.clone(), url.path().to_string())
recieve_handshake(handle.clone(), url.path().to_string());
}
});

View File

@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
drop_app_lib::run()
drop_app_lib::run();
}

View File

@ -17,9 +17,9 @@ pub fn launch_game(
//};
match process_manager_lock.launch_process(id) {
Ok(_) => {}
Ok(()) => {}
Err(e) => return Err(e),
};
}
drop(process_manager_lock);
drop(state_lock);

View File

@ -281,7 +281,7 @@ impl ProcessManager<'_> {
let launch_string = game_launcher.create_launch_process(
&meta,
launch.to_string(),
args.to_vec(),
args.clone(),
game_version,
install_dir,
);

View File

@ -12,9 +12,7 @@ use crate::{
database::{
db::{borrow_db_checked, borrow_db_mut_checked},
models::data::DatabaseAuth,
},
error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError},
AppState, AppStatus, User,
}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, remote::utils::DROP_CLIENT_SYNC, AppState, AppStatus, User
};
use super::{
@ -32,6 +30,7 @@ struct InitiateRequestBody {
name: String,
platform: String,
capabilities: HashMap<String, CapabilityConfiguration>,
mode: String,
}
#[derive(Serialize)]
@ -65,7 +64,7 @@ pub fn generate_authorization_header() -> String {
pub fn fetch_user() -> Result<User, RemoteAccessError> {
let header = generate_authorization_header();
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = make_request(&client, &["/api/v1/client/user"], &[], |f| {
f.header("Authorization", header)
})?
@ -81,11 +80,11 @@ pub fn fetch_user() -> Result<User, RemoteAccessError> {
return Err(RemoteAccessError::InvalidResponse(err));
}
response.json::<User>().map_err(|e| e.into())
response.json::<User>().map_err(std::convert::Into::into)
}
fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAccessError> {
let path_chunks: Vec<&str> = path.split("/").collect();
let path_chunks: Vec<&str> = path.split('/').collect();
if path_chunks.len() != 3 {
app.emit("auth/failed", ()).unwrap();
return Err(RemoteAccessError::HandshakeFailed(
@ -101,12 +100,12 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
let client_id = path_chunks.get(1).unwrap();
let token = path_chunks.get(2).unwrap();
let body = HandshakeRequestBody {
client_id: client_id.to_string(),
token: token.to_string(),
client_id: (*client_id).to_string(),
token: (*token).to_string(),
};
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = client.post(endpoint).json(&body).send()?;
debug!("handshake responsded with {}", response.status().as_u16());
if !response.status().is_success() {
@ -166,7 +165,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
app.emit("auth/finished", ()).unwrap();
}
pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
pub fn auth_initiate_logic(mode: String) -> Result<String, RemoteAccessError> {
let base_url = {
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
@ -182,9 +181,10 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
("peerAPI".to_owned(), CapabilityConfiguration {}),
("cloudSaves".to_owned(), CapabilityConfiguration {}),
]),
mode,
};
let client = reqwest::blocking::Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = client.post(endpoint.to_string()).json(&body).send()?;
if response.status() != 200 {
@ -194,13 +194,9 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> {
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
}
let redir_url = response.text()?;
let complete_redir_url = base_url.join(&redir_url)?;
let response = response.text()?;
debug!("opening web browser to continue authentication");
webbrowser::open(complete_redir_url.as_ref()).unwrap();
Ok(())
Ok(response)
}
pub fn setup() -> (AppStatus, Option<User>) {

View File

@ -1,15 +1,14 @@
use std::sync::Mutex;
use log::debug;
use reqwest::blocking::Client;
use futures_lite::StreamExt;
use log::{debug, warn};
use reqwest_websocket::{Message, RequestBuilderExt};
use serde::Deserialize;
use tauri::{AppHandle, Emitter, Manager};
use url::Url;
use crate::{
database::db::{borrow_db_checked, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError,
remote::{auth::generate_authorization_header, requests::make_request},
AppState, AppStatus,
database::db::{borrow_db_checked, borrow_db_mut_checked}, error::remote_access_error::RemoteAccessError, remote::{auth::generate_authorization_header, requests::make_request, utils::DROP_CLIENT_SYNC}, AppState, AppStatus
};
use super::{
@ -42,7 +41,7 @@ pub fn gen_drop_url(path: String) -> Result<String, RemoteAccessError> {
#[tauri::command]
pub fn fetch_drop_object(path: String) -> Result<Vec<u8>, RemoteAccessError> {
let _drop_url = gen_drop_url(path.clone())?;
let req = make_request(&Client::new(), &[&path], &[], |r| {
let req = make_request(&DROP_CLIENT_SYNC, &[&path], &[], |r| {
r.header("Authorization", generate_authorization_header())
})?
.send();
@ -91,7 +90,78 @@ pub fn retry_connect(state: tauri::State<'_, Mutex<AppState>>) {
#[tauri::command]
pub fn auth_initiate() -> Result<(), RemoteAccessError> {
auth_initiate_logic()
let base_url = {
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
};
let redir_url = auth_initiate_logic("callback".to_string())?;
let complete_redir_url = base_url.join(&redir_url)?;
debug!("opening web browser to continue authentication");
webbrowser::open(complete_redir_url.as_ref()).unwrap();
Ok(())
}
#[derive(Deserialize)]
struct CodeWebsocketResponse {
#[serde(rename = "type")]
response_type: String,
value: String,
}
#[tauri::command]
pub fn auth_initiate_code(app: AppHandle) -> Result<String, RemoteAccessError> {
let base_url = {
let db_lock = borrow_db_checked();
Url::parse(&db_lock.base_url.clone())?
};
let code = auth_initiate_logic("code".to_string())?;
let header_code = code.clone();
tauri::async_runtime::spawn(async move {
let load = async || -> Result<(), RemoteAccessError> {
let ws_url = base_url.join("/api/v1/client/auth/code/ws")?;
let response = reqwest::Client::default()
.get(ws_url)
.header("Authorization", header_code)
.upgrade()
.send()
.await?;
let mut websocket = response.into_websocket().await?;
while let Some(token) = websocket.try_next().await? {
if let Message::Text(response) = token {
let response = serde_json::from_str::<CodeWebsocketResponse>(&response)
.map_err(|e| RemoteAccessError::UnparseableResponse(e.to_string()))?;
match response.response_type.as_str() {
"token" => {
let recieve_app = app.clone();
tauri::async_runtime::spawn_blocking(move || {
manual_recieve_handshake(recieve_app, response.value);
});
return Ok(());
}
_ => return Err(RemoteAccessError::HandshakeFailed(response.value)),
}
}
}
Err(RemoteAccessError::HandshakeFailed(
"Failed to connect to websocket".to_string(),
))
};
let result = load().await;
if let Err(err) = result {
warn!("{err}");
app.emit("auth/failed", err.to_string()).unwrap();
}
});
Ok(code)
}
#[tauri::command]

View File

@ -2,7 +2,7 @@ use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
use log::warn;
use tauri::UriSchemeResponder;
use crate::{DB, database::db::DatabaseImpls};
use crate::{database::db::DatabaseImpls, remote::utils::DROP_CLIENT_ASYNC, DB};
use super::{
auth::generate_authorization_header,
@ -22,7 +22,7 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
}
let header = generate_authorization_header();
let client = reqwest::Client::new();
let client = DROP_CLIENT_ASYNC.clone();
let url = format!("{}api/v1/client/object/{object_id}", DB.fetch_base_url());
let response = client.get(url).header("Authorization", header).send().await;
@ -30,7 +30,7 @@ pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeR
match cache_result {
Ok(cache_result) => responder.respond(cache_result.into()),
Err(e) => {
warn!("{e}")
warn!("{e}");
}
}
return;

View File

@ -1,10 +1,9 @@
use std::str::FromStr;
use http::{uri::PathAndQuery, Request, Response, StatusCode, Uri};
use reqwest::blocking::Client;
use tauri::UriSchemeResponder;
use crate::database::db::borrow_db_checked;
use crate::{database::db::borrow_db_checked, remote::utils::DROP_CLIENT_SYNC};
pub fn handle_server_proto_offline(_request: Request<Vec<u8>>, responder: UriSchemeResponder) {
let four_oh_four = Response::builder()
@ -38,7 +37,7 @@ pub fn handle_server_proto(request: Request<Vec<u8>>, responder: UriSchemeRespon
return;
}
let client = Client::new();
let client = DROP_CLIENT_SYNC.clone();
let response = client
.request(request.method().clone(), new_uri.to_string())
.header("Authorization", format!("Bearer {web_token}"))

View File

@ -1,12 +1,18 @@
use std::sync::Mutex;
use std::{
fs::{self, File},
io::Read,
sync::{LazyLock, Mutex},
};
use log::{debug, warn};
use log::{debug, info, warn};
use reqwest::Certificate;
use serde::Deserialize;
use url::Url;
use crate::{
database::db::borrow_db_mut_checked, error::remote_access_error::RemoteAccessError, AppState,
AppStatus,
AppState, AppStatus,
database::db::{DATA_ROOT_DIR, borrow_db_mut_checked},
error::remote_access_error::RemoteAccessError,
};
#[derive(Deserialize)]
@ -15,6 +21,60 @@ struct DropHealthcheck {
app_name: String,
}
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);
fn fetch_certificates() -> Vec<Certificate> {
let certificate_dir = DATA_ROOT_DIR.join("certificates");
let mut certs = Vec::new();
match fs::read_dir(certificate_dir) {
Ok(c) => {
for entry in c {
match entry {
Ok(c) => {
let mut buf = Vec::new();
File::open(c.path()).unwrap().read_to_end(&mut buf).unwrap();
for cert in Certificate::from_pem_bundle(&buf).unwrap() {
certs.push(cert);
}
info!(
"added {} certificate(s) from {}",
certs.len(),
c.file_name().into_string().unwrap()
);
}
Err(_) => todo!(),
}
}
}
Err(e) => {
debug!("not loading certificates due to error: {e}");
}
};
certs
}
pub fn get_client_sync() -> reqwest::blocking::Client {
let mut client = reqwest::blocking::ClientBuilder::new();
let certs = fetch_certificates();
for cert in certs {
client = client.add_root_certificate(cert);
}
client.build().unwrap()
}
pub fn get_client_async() -> reqwest::Client {
let mut client = reqwest::ClientBuilder::new();
let certs = fetch_certificates();
for cert in certs {
client = client.add_root_certificate(cert);
}
client.build().unwrap()
}
pub fn use_remote_logic(
url: String,
state: tauri::State<'_, Mutex<AppState<'_>>>,
@ -24,7 +84,8 @@ pub fn use_remote_logic(
// Test Drop url
let test_endpoint = base_url.join("/api/v1")?;
let response = reqwest::blocking::get(test_endpoint.to_string())?;
let client = DROP_CLIENT_SYNC.clone();
let response = client.get(test_endpoint.to_string()).send()?;
let result: DropHealthcheck = response.json()?;

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2.0.0",
"productName": "Drop Desktop Client",
"version": "0.3.0",
"version": "0.3.1-mac",
"identifier": "dev.drop.app",
"build": {
"beforeDevCommand": "yarn dev --port 1432",