mirror of
https://github.com/Drop-OSS/drop-app.git
synced 2025-11-24 05:31:41 +10:00
refactor: into rust workspaces
This commit is contained in:
21
src-tauri/drop-remote/Cargo.toml
Normal file
21
src-tauri/drop-remote/Cargo.toml
Normal file
@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "drop-remote"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bitcode = "0.6.7"
|
||||
chrono = "0.4.42"
|
||||
drop-database = { path = "../drop-database" }
|
||||
drop-errors = { path = "../drop-errors" }
|
||||
droplet-rs = "0.7.3"
|
||||
gethostname = "1.0.2"
|
||||
hex = "0.4.3"
|
||||
http = "1.3.1"
|
||||
log = "0.4.28"
|
||||
md5 = "0.8.0"
|
||||
reqwest = "0.12.23"
|
||||
reqwest-websocket = "0.5.1"
|
||||
serde = { version = "1.0.220", features = ["derive"] }
|
||||
tauri = "2.8.5"
|
||||
url = "2.5.7"
|
||||
167
src-tauri/drop-remote/src/auth.rs
Normal file
167
src-tauri/drop-remote/src/auth.rs
Normal file
@ -0,0 +1,167 @@
|
||||
use std::{collections::HashMap, env, sync::Mutex};
|
||||
|
||||
use chrono::Utc;
|
||||
use drop_database::{borrow_db_checked, borrow_db_mut_checked, models::data::DatabaseAuth, runtime_models::User};
|
||||
use drop_errors::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError};
|
||||
use droplet_rs::ssl::sign_nonce;
|
||||
use gethostname::gethostname;
|
||||
use log::{debug, error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{requests::make_authenticated_get, utils::{DROP_CLIENT_ASYNC, DROP_CLIENT_SYNC}};
|
||||
|
||||
use super::requests::generate_url;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct CapabilityConfiguration {}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct InitiateRequestBody {
|
||||
name: String,
|
||||
platform: String,
|
||||
capabilities: HashMap<String, CapabilityConfiguration>,
|
||||
mode: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HandshakeRequestBody {
|
||||
client_id: String,
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HandshakeResponse {
|
||||
private: String,
|
||||
certificate: String,
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub fn generate_authorization_header() -> String {
|
||||
let certs = {
|
||||
let db = borrow_db_checked();
|
||||
db.auth.clone().unwrap()
|
||||
};
|
||||
|
||||
let nonce = Utc::now().timestamp_millis().to_string();
|
||||
|
||||
let signature = sign_nonce(certs.private, nonce.clone()).unwrap();
|
||||
|
||||
format!("Nonce {} {} {}", certs.client_id, nonce, signature)
|
||||
}
|
||||
|
||||
pub async fn fetch_user() -> Result<User, RemoteAccessError> {
|
||||
let response = make_authenticated_get(generate_url(&["/api/v1/client/user"], &[])?).await?;
|
||||
if response.status() != 200 {
|
||||
let err: DropServerError = response.json().await?;
|
||||
warn!("{err:?}");
|
||||
|
||||
if err.status_message == "Nonce expired" {
|
||||
return Err(RemoteAccessError::OutOfSync);
|
||||
}
|
||||
|
||||
return Err(RemoteAccessError::InvalidResponse(err));
|
||||
}
|
||||
|
||||
response
|
||||
.json::<User>()
|
||||
.await
|
||||
.map_err(std::convert::Into::into)
|
||||
}
|
||||
|
||||
pub async fn recieve_handshake_logic(path: String) -> Result<(), RemoteAccessError> {
|
||||
let path_chunks: Vec<&str> = path.split('/').collect();
|
||||
if path_chunks.len() != 3 {
|
||||
// app.emit("auth/failed", ()).unwrap();
|
||||
return Err(RemoteAccessError::HandshakeFailed(
|
||||
"failed to parse token".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let base_url = {
|
||||
let handle = borrow_db_checked();
|
||||
Url::parse(handle.base_url.as_str())?
|
||||
};
|
||||
|
||||
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(),
|
||||
};
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/auth/handshake")?;
|
||||
let client = DROP_CLIENT_ASYNC.clone();
|
||||
let response = client.post(endpoint).json(&body).send().await?;
|
||||
debug!("handshake responsded with {}", response.status().as_u16());
|
||||
if !response.status().is_success() {
|
||||
return Err(RemoteAccessError::InvalidResponse(response.json().await?));
|
||||
}
|
||||
let response_struct: HandshakeResponse = response.json().await?;
|
||||
|
||||
{
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
handle.auth = Some(DatabaseAuth {
|
||||
private: response_struct.private,
|
||||
cert: response_struct.certificate,
|
||||
client_id: response_struct.id,
|
||||
web_token: None, // gets created later
|
||||
});
|
||||
}
|
||||
|
||||
let web_token = {
|
||||
let header = generate_authorization_header();
|
||||
let token = client
|
||||
.post(base_url.join("/api/v1/client/user/webtoken").unwrap())
|
||||
.header("Authorization", header)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
token.text().await.unwrap()
|
||||
};
|
||||
|
||||
let mut handle = borrow_db_mut_checked();
|
||||
let mut_auth = handle.auth.as_mut().unwrap();
|
||||
mut_auth.web_token = Some(web_token);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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())?
|
||||
};
|
||||
|
||||
let hostname = gethostname();
|
||||
|
||||
let endpoint = base_url.join("/api/v1/client/auth/initiate")?;
|
||||
let body = InitiateRequestBody {
|
||||
name: format!("{} (Desktop)", hostname.into_string().unwrap()),
|
||||
platform: env::consts::OS.to_string(),
|
||||
capabilities: HashMap::from([
|
||||
("peerAPI".to_owned(), CapabilityConfiguration {}),
|
||||
("cloudSaves".to_owned(), CapabilityConfiguration {}),
|
||||
]),
|
||||
mode,
|
||||
};
|
||||
|
||||
let client = DROP_CLIENT_SYNC.clone();
|
||||
let response = client.post(endpoint.to_string()).json(&body).send()?;
|
||||
|
||||
if response.status() != 200 {
|
||||
let data: DropServerError = response.json()?;
|
||||
error!("could not start handshake: {}", data.status_message);
|
||||
|
||||
return Err(RemoteAccessError::HandshakeFailed(data.status_message));
|
||||
}
|
||||
|
||||
let response = response.text()?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
133
src-tauri/drop-remote/src/cache.rs
Normal file
133
src-tauri/drop-remote/src/cache.rs
Normal file
@ -0,0 +1,133 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use bitcode::{Decode, DecodeOwned, Encode};
|
||||
use drop_database::{borrow_db_checked, models::data::Database};
|
||||
use drop_errors::remote_access_error::RemoteAccessError;
|
||||
use http::{Response, header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! offline {
|
||||
($var:expr, $func1:expr, $func2:expr, $( $arg:expr ),* ) => {
|
||||
|
||||
// TODO add offline mode back
|
||||
// || $var.lock().unwrap().status == AppStatus::Offline
|
||||
async move { if drop_database::borrow_db_checked().settings.force_offline {
|
||||
$func2( $( $arg ), *).await
|
||||
} else {
|
||||
$func1( $( $arg ), *).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sys_time_in_secs() -> u64 {
|
||||
match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
|
||||
Ok(n) => n.as_secs(),
|
||||
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cache_path(base: &Path, key: &str) -> PathBuf {
|
||||
let key_hash = hex::encode(md5::compute(key.as_bytes()).0);
|
||||
base.join(key_hash)
|
||||
}
|
||||
|
||||
fn write_sync(base: &Path, key: &str, data: Vec<u8>) -> io::Result<()> {
|
||||
let cache_path = get_cache_path(base, key);
|
||||
let mut file = File::create(cache_path)?;
|
||||
file.write_all(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_sync(base: &Path, key: &str) -> io::Result<Vec<u8>> {
|
||||
let cache_path = get_cache_path(base, key);
|
||||
let file = std::fs::read(cache_path)?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
fn delete_sync(base: &Path, key: &str) -> io::Result<()> {
|
||||
let cache_path = get_cache_path(base, key);
|
||||
std::fs::remove_file(cache_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cache_object<D: Encode>(key: &str, data: &D) -> Result<(), RemoteAccessError> {
|
||||
cache_object_db(key, data, &borrow_db_checked())
|
||||
}
|
||||
pub fn cache_object_db<D: Encode>(
|
||||
key: &str,
|
||||
data: &D,
|
||||
database: &Database,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
let bytes = bitcode::encode(data);
|
||||
write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache)
|
||||
}
|
||||
pub fn get_cached_object<D: Encode + DecodeOwned>(key: &str) -> Result<D, RemoteAccessError> {
|
||||
get_cached_object_db::<D>(key, &borrow_db_checked())
|
||||
}
|
||||
pub fn get_cached_object_db<D: DecodeOwned>(
|
||||
key: &str,
|
||||
db: &Database,
|
||||
) -> Result<D, RemoteAccessError> {
|
||||
let bytes = read_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
|
||||
let data =
|
||||
bitcode::decode::<D>(&bytes).map_err(|e| RemoteAccessError::Cache(io::Error::other(e)))?;
|
||||
Ok(data)
|
||||
}
|
||||
pub fn clear_cached_object(key: &str) -> Result<(), RemoteAccessError> {
|
||||
clear_cached_object_db(key, &borrow_db_checked())
|
||||
}
|
||||
pub fn clear_cached_object_db(
|
||||
key: &str,
|
||||
db: &Database,
|
||||
) -> Result<(), RemoteAccessError> {
|
||||
delete_sync(&db.cache_dir, key).map_err(RemoteAccessError::Cache)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct ObjectCache {
|
||||
content_type: String,
|
||||
body: Vec<u8>,
|
||||
expiry: u64,
|
||||
}
|
||||
|
||||
impl ObjectCache {
|
||||
pub fn has_expired(&self) -> bool {
|
||||
let current = get_sys_time_in_secs();
|
||||
self.expiry < current
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response<Vec<u8>>> for ObjectCache {
|
||||
fn from(value: Response<Vec<u8>>) -> Self {
|
||||
ObjectCache {
|
||||
content_type: value
|
||||
.headers()
|
||||
.get(CONTENT_TYPE)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned(),
|
||||
body: value.body().clone(),
|
||||
expiry: get_sys_time_in_secs() + 60 * 60 * 24,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<ObjectCache> for Response<Vec<u8>> {
|
||||
fn from(value: ObjectCache) -> Self {
|
||||
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type);
|
||||
resp_builder.body(value.body).unwrap()
|
||||
}
|
||||
}
|
||||
impl From<&ObjectCache> for Response<Vec<u8>> {
|
||||
fn from(value: &ObjectCache) -> Self {
|
||||
let resp_builder = ResponseBuilder::new().header(CONTENT_TYPE, value.content_type.clone());
|
||||
resp_builder.body(value.body.clone()).unwrap()
|
||||
}
|
||||
}
|
||||
53
src-tauri/drop-remote/src/fetch_object.rs
Normal file
53
src-tauri/drop-remote/src/fetch_object.rs
Normal file
@ -0,0 +1,53 @@
|
||||
use drop_database::{db::DatabaseImpls as _, DB};
|
||||
use http::{header::CONTENT_TYPE, response::Builder as ResponseBuilder};
|
||||
use log::warn;
|
||||
use tauri::UriSchemeResponder;
|
||||
|
||||
|
||||
use crate::utils::DROP_CLIENT_ASYNC;
|
||||
|
||||
use super::{
|
||||
auth::generate_authorization_header,
|
||||
cache::{ObjectCache, cache_object, get_cached_object},
|
||||
};
|
||||
|
||||
pub async fn fetch_object(request: http::Request<Vec<u8>>, responder: UriSchemeResponder) {
|
||||
// Drop leading /
|
||||
let object_id = &request.uri().path()[1..];
|
||||
|
||||
let cache_result = get_cached_object::<ObjectCache>(object_id);
|
||||
if let Ok(cache_result) = &cache_result
|
||||
&& !cache_result.has_expired()
|
||||
{
|
||||
responder.respond(cache_result.into());
|
||||
return;
|
||||
}
|
||||
|
||||
let header = generate_authorization_header();
|
||||
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;
|
||||
|
||||
if response.is_err() {
|
||||
match cache_result {
|
||||
Ok(cache_result) => responder.respond(cache_result.into()),
|
||||
Err(e) => {
|
||||
warn!("{e}");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let response = response.unwrap();
|
||||
|
||||
let resp_builder = ResponseBuilder::new().header(
|
||||
CONTENT_TYPE,
|
||||
response.headers().get("Content-Type").unwrap(),
|
||||
);
|
||||
let data = Vec::from(response.bytes().await.unwrap());
|
||||
let resp = resp_builder.body(data).unwrap();
|
||||
if cache_result.is_err() || cache_result.unwrap().has_expired() {
|
||||
cache_object::<ObjectCache>(object_id, &resp.clone().into()).unwrap();
|
||||
}
|
||||
|
||||
responder.respond(resp);
|
||||
}
|
||||
5
src-tauri/drop-remote/src/lib.rs
Normal file
5
src-tauri/drop-remote/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod auth;
|
||||
pub mod cache;
|
||||
pub mod fetch_object;
|
||||
pub mod requests;
|
||||
pub mod utils;
|
||||
30
src-tauri/drop-remote/src/requests.rs
Normal file
30
src-tauri/drop-remote/src/requests.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use drop_database::{db::DatabaseImpls as _, DB};
|
||||
use drop_errors::remote_access_error::RemoteAccessError;
|
||||
use url::Url;
|
||||
|
||||
use crate::{auth::generate_authorization_header, utils::DROP_CLIENT_ASYNC};
|
||||
|
||||
pub fn generate_url<T: AsRef<str>>(
|
||||
path_components: &[T],
|
||||
query: &[(T, T)],
|
||||
) -> 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 mut queries = base_url.query_pairs_mut();
|
||||
for (param, val) in query {
|
||||
queries.append_pair(param.as_ref(), val.as_ref());
|
||||
}
|
||||
}
|
||||
Ok(base_url)
|
||||
}
|
||||
|
||||
pub async fn make_authenticated_get(url: Url) -> Result<reqwest::Response, reqwest::Error> {
|
||||
DROP_CLIENT_ASYNC
|
||||
.get(url)
|
||||
.header("Authorization", generate_authorization_header())
|
||||
.send()
|
||||
.await
|
||||
}
|
||||
72
src-tauri/drop-remote/src/utils.rs
Normal file
72
src-tauri/drop-remote/src/utils.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::Read,
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use drop_database::db::DATA_ROOT_DIR;
|
||||
use log::{debug, info};
|
||||
use reqwest::Certificate;
|
||||
use serde::Deserialize;
|
||||
|
||||
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_WS_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(get_client_ws);
|
||||
|
||||
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();
|
||||
|
||||
for cert in DROP_CERT_BUNDLE.iter() {
|
||||
client = client.add_root_certificate(cert.clone());
|
||||
}
|
||||
client.use_rustls_tls().build().unwrap()
|
||||
}
|
||||
pub fn get_client_async() -> reqwest::Client {
|
||||
let mut client = reqwest::ClientBuilder::new();
|
||||
|
||||
for cert in DROP_CERT_BUNDLE.iter() {
|
||||
client = client.add_root_certificate(cert.clone());
|
||||
}
|
||||
client.use_rustls_tls().build().unwrap()
|
||||
}
|
||||
pub fn get_client_ws() -> reqwest::Client {
|
||||
let mut client = reqwest::ClientBuilder::new();
|
||||
|
||||
for cert in DROP_CERT_BUNDLE.iter() {
|
||||
client = client.add_root_certificate(cert.clone());
|
||||
}
|
||||
client.use_rustls_tls().http1_only().build().unwrap()
|
||||
}
|
||||
Reference in New Issue
Block a user