diff --git a/core_lib/Cargo.lock b/core_lib/Cargo.lock index 53c67d6..98e4beb 100644 --- a/core_lib/Cargo.lock +++ b/core_lib/Cargo.lock @@ -217,6 +217,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + [[package]] name = "cc" version = "1.0.83" @@ -407,7 +413,7 @@ dependencies = [ "futures-util", "libc", "libdbus-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -671,6 +677,12 @@ dependencies = [ "slab", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "generic-array" version = "0.14.7" @@ -682,6 +694,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -1059,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1432,6 +1466,7 @@ dependencies = [ "btleplug", "directories", "futures", + "get_if_addrs", "hex", "hkdf", "hmac", @@ -1596,7 +1631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1968,6 +2003,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -1990,7 +2031,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] diff --git a/core_lib/Cargo.toml b/core_lib/Cargo.toml index 4b574bd..cffb17f 100644 --- a/core_lib/Cargo.toml +++ b/core_lib/Cargo.toml @@ -34,6 +34,7 @@ ts-rs = { version = "7.1", features = ["serde-compat", "uuid-impl", "chrono-impl uuid = "1.7.0" serde = { version = "1.0", features = ["derive"] } mime_guess = "2.0.4" +get_if_addrs = "0.5.3" [build-dependencies] prost-build = "0.12" diff --git a/core_lib/bindings/ChannelMessage.ts b/core_lib/bindings/ChannelMessage.ts index 9eacfde..6a50b55 100644 --- a/core_lib/bindings/ChannelMessage.ts +++ b/core_lib/bindings/ChannelMessage.ts @@ -3,5 +3,6 @@ import type { ChannelAction } from "./ChannelAction"; import type { ChannelDirection } from "./ChannelDirection"; import type { State } from "./State"; import type { TransferMetadata } from "./TransferMetadata"; +import type { TransferType } from "./TransferType"; -export interface ChannelMessage { id: string, direction: ChannelDirection, action: ChannelAction | null, state: State | null, meta: TransferMetadata | null, } \ No newline at end of file +export interface ChannelMessage { id: string, direction: ChannelDirection, action: ChannelAction | null, rtype: TransferType | null, state: State | null, meta: TransferMetadata | null, } \ No newline at end of file diff --git a/core_lib/bindings/EndpointInfo.ts b/core_lib/bindings/EndpointInfo.ts new file mode 100644 index 0000000..d802b21 --- /dev/null +++ b/core_lib/bindings/EndpointInfo.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DeviceType } from "./DeviceType"; + +export interface EndpointInfo { id: string, name: string | null, ip: string | null, port: string | null, rtype: DeviceType | null, present: boolean | null, } \ No newline at end of file diff --git a/core_lib/bindings/OutboundPayload.ts b/core_lib/bindings/OutboundPayload.ts new file mode 100644 index 0000000..3370805 --- /dev/null +++ b/core_lib/bindings/OutboundPayload.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type OutboundPayload = { "Files": Array }; \ No newline at end of file diff --git a/core_lib/bindings/SendInfo.ts b/core_lib/bindings/SendInfo.ts new file mode 100644 index 0000000..3bb6dce --- /dev/null +++ b/core_lib/bindings/SendInfo.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { OutboundPayload } from "./OutboundPayload"; + +export interface SendInfo { id: string, name: string, addr: string, ob: OutboundPayload, } \ No newline at end of file diff --git a/core_lib/bindings/State.ts b/core_lib/bindings/State.ts index faaa82c..6ad3ef0 100644 --- a/core_lib/bindings/State.ts +++ b/core_lib/bindings/State.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type State = "Initial" | "ReceivedConnectionRequest" | "SentUkeyServerInit" | "ReceivedUkeyClientFinish" | "SentConnectionResponse" | "SentPairedKeyResult" | "ReceivedPairedKeyResult" | "WaitingForUserConsent" | "ReceivingFiles" | "Disconnected" | "Finished"; \ No newline at end of file +export type State = "Initial" | "ReceivedConnectionRequest" | "SentUkeyServerInit" | "SentUkeyClientInit" | "SentUkeyClientFinish" | "SentPairedKeyEncryption" | "ReceivedUkeyClientFinish" | "SentConnectionResponse" | "SentPairedKeyResult" | "SentIntroduction" | "ReceivedPairedKeyResult" | "WaitingForUserConsent" | "ReceivingFiles" | "SendingFiles" | "Disconnected" | "Finished"; \ No newline at end of file diff --git a/core_lib/bindings/TransferType.ts b/core_lib/bindings/TransferType.ts new file mode 100644 index 0000000..f4c2e3c --- /dev/null +++ b/core_lib/bindings/TransferType.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type TransferType = "Inbound" | "Outbound"; \ No newline at end of file diff --git a/core_lib/bindings/index.ts b/core_lib/bindings/index.ts index f657b3c..f519648 100644 --- a/core_lib/bindings/index.ts +++ b/core_lib/bindings/index.ts @@ -1,7 +1,11 @@ export * from "./ChannelDirection" export * from "./DeviceType" export * from "./ChannelMessage" +export * from "./TransferType" +export * from "./OutboundPayload" export * from "./TransferMetadata" +export * from "./EndpointInfo" export * from "./ChannelAction" export * from "./RemoteDeviceInfo" -export * from "./State" \ No newline at end of file +export * from "./State" +export * from "./SendInfo" \ No newline at end of file diff --git a/core_lib/dist/ChannelMessage.d.ts b/core_lib/dist/ChannelMessage.d.ts index ca5d462..bf1b938 100644 --- a/core_lib/dist/ChannelMessage.d.ts +++ b/core_lib/dist/ChannelMessage.d.ts @@ -2,10 +2,12 @@ import type { ChannelAction } from "./ChannelAction"; import type { ChannelDirection } from "./ChannelDirection"; import type { State } from "./State"; import type { TransferMetadata } from "./TransferMetadata"; +import type { TransferType } from "./TransferType"; export interface ChannelMessage { id: string; direction: ChannelDirection; action: ChannelAction | null; + rtype: TransferType | null; state: State | null; meta: TransferMetadata | null; } diff --git a/core_lib/dist/EndpointInfo.d.ts b/core_lib/dist/EndpointInfo.d.ts new file mode 100644 index 0000000..8d4a59d --- /dev/null +++ b/core_lib/dist/EndpointInfo.d.ts @@ -0,0 +1,9 @@ +import type { DeviceType } from "./DeviceType"; +export interface EndpointInfo { + id: string; + name: string | null; + ip: string | null; + port: string | null; + rtype: DeviceType | null; + present: boolean | null; +} diff --git a/core_lib/dist/EndpointInfo.js b/core_lib/dist/EndpointInfo.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/core_lib/dist/EndpointInfo.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/core_lib/dist/State.d.ts b/core_lib/dist/State.d.ts index 962b4ee..04bf7ee 100644 --- a/core_lib/dist/State.d.ts +++ b/core_lib/dist/State.d.ts @@ -1 +1 @@ -export type State = "Initial" | "ReceivedConnectionRequest" | "SentUkeyServerInit" | "ReceivedUkeyClientFinish" | "SentConnectionResponse" | "SentPairedKeyResult" | "ReceivedPairedKeyResult" | "WaitingForUserConsent" | "ReceivingFiles" | "Disconnected" | "Finished"; +export type State = "Initial" | "ReceivedConnectionRequest" | "SentUkeyServerInit" | "SentUkeyClientInit" | "SentUkeyClientFinish" | "SentPairedKeyEncryption" | "ReceivedUkeyClientFinish" | "SentConnectionResponse" | "SentPairedKeyResult" | "SentIntroduction" | "ReceivedPairedKeyResult" | "WaitingForUserConsent" | "ReceivingFiles" | "SendingFiles" | "Disconnected" | "Finished"; diff --git a/core_lib/dist/TransferType.d.ts b/core_lib/dist/TransferType.d.ts new file mode 100644 index 0000000..5024d1d --- /dev/null +++ b/core_lib/dist/TransferType.d.ts @@ -0,0 +1 @@ +export type TransferType = "Inbound" | "Outbound"; diff --git a/core_lib/dist/TransferType.js b/core_lib/dist/TransferType.js new file mode 100644 index 0000000..ca914b0 --- /dev/null +++ b/core_lib/dist/TransferType.js @@ -0,0 +1,3 @@ +"use strict"; +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/core_lib/dist/index.d.ts b/core_lib/dist/index.d.ts index 50a5d23..9d7cab1 100644 --- a/core_lib/dist/index.d.ts +++ b/core_lib/dist/index.d.ts @@ -1,7 +1,9 @@ export * from "./ChannelDirection"; export * from "./DeviceType"; export * from "./ChannelMessage"; +export * from "./TransferType"; export * from "./TransferMetadata"; +export * from "./EndpointInfo"; export * from "./ChannelAction"; export * from "./RemoteDeviceInfo"; export * from "./State"; diff --git a/core_lib/dist/index.js b/core_lib/dist/index.js index cc7a67e..860f4eb 100644 --- a/core_lib/dist/index.js +++ b/core_lib/dist/index.js @@ -17,7 +17,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); __exportStar(require("./ChannelDirection"), exports); __exportStar(require("./DeviceType"), exports); __exportStar(require("./ChannelMessage"), exports); +__exportStar(require("./TransferType"), exports); __exportStar(require("./TransferMetadata"), exports); +__exportStar(require("./EndpointInfo"), exports); __exportStar(require("./ChannelAction"), exports); __exportStar(require("./RemoteDeviceInfo"), exports); __exportStar(require("./State"), exports); diff --git a/core_lib/esm/ChannelMessage.d.ts b/core_lib/esm/ChannelMessage.d.ts index ca5d462..bf1b938 100644 --- a/core_lib/esm/ChannelMessage.d.ts +++ b/core_lib/esm/ChannelMessage.d.ts @@ -2,10 +2,12 @@ import type { ChannelAction } from "./ChannelAction"; import type { ChannelDirection } from "./ChannelDirection"; import type { State } from "./State"; import type { TransferMetadata } from "./TransferMetadata"; +import type { TransferType } from "./TransferType"; export interface ChannelMessage { id: string; direction: ChannelDirection; action: ChannelAction | null; + rtype: TransferType | null; state: State | null; meta: TransferMetadata | null; } diff --git a/core_lib/esm/EndpointInfo.d.ts b/core_lib/esm/EndpointInfo.d.ts new file mode 100644 index 0000000..8d4a59d --- /dev/null +++ b/core_lib/esm/EndpointInfo.d.ts @@ -0,0 +1,9 @@ +import type { DeviceType } from "./DeviceType"; +export interface EndpointInfo { + id: string; + name: string | null; + ip: string | null; + port: string | null; + rtype: DeviceType | null; + present: boolean | null; +} diff --git a/core_lib/esm/EndpointInfo.js b/core_lib/esm/EndpointInfo.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/core_lib/esm/EndpointInfo.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/core_lib/esm/State.d.ts b/core_lib/esm/State.d.ts index 962b4ee..04bf7ee 100644 --- a/core_lib/esm/State.d.ts +++ b/core_lib/esm/State.d.ts @@ -1 +1 @@ -export type State = "Initial" | "ReceivedConnectionRequest" | "SentUkeyServerInit" | "ReceivedUkeyClientFinish" | "SentConnectionResponse" | "SentPairedKeyResult" | "ReceivedPairedKeyResult" | "WaitingForUserConsent" | "ReceivingFiles" | "Disconnected" | "Finished"; +export type State = "Initial" | "ReceivedConnectionRequest" | "SentUkeyServerInit" | "SentUkeyClientInit" | "SentUkeyClientFinish" | "SentPairedKeyEncryption" | "ReceivedUkeyClientFinish" | "SentConnectionResponse" | "SentPairedKeyResult" | "SentIntroduction" | "ReceivedPairedKeyResult" | "WaitingForUserConsent" | "ReceivingFiles" | "SendingFiles" | "Disconnected" | "Finished"; diff --git a/core_lib/esm/TransferType.d.ts b/core_lib/esm/TransferType.d.ts new file mode 100644 index 0000000..5024d1d --- /dev/null +++ b/core_lib/esm/TransferType.d.ts @@ -0,0 +1 @@ +export type TransferType = "Inbound" | "Outbound"; diff --git a/core_lib/esm/TransferType.js b/core_lib/esm/TransferType.js new file mode 100644 index 0000000..ca914b0 --- /dev/null +++ b/core_lib/esm/TransferType.js @@ -0,0 +1,3 @@ +"use strict"; +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/core_lib/esm/index.d.ts b/core_lib/esm/index.d.ts index 50a5d23..9d7cab1 100644 --- a/core_lib/esm/index.d.ts +++ b/core_lib/esm/index.d.ts @@ -1,7 +1,9 @@ export * from "./ChannelDirection"; export * from "./DeviceType"; export * from "./ChannelMessage"; +export * from "./TransferType"; export * from "./TransferMetadata"; +export * from "./EndpointInfo"; export * from "./ChannelAction"; export * from "./RemoteDeviceInfo"; export * from "./State"; diff --git a/core_lib/esm/index.js b/core_lib/esm/index.js index cc7a67e..860f4eb 100644 --- a/core_lib/esm/index.js +++ b/core_lib/esm/index.js @@ -17,7 +17,9 @@ Object.defineProperty(exports, "__esModule", { value: true }); __exportStar(require("./ChannelDirection"), exports); __exportStar(require("./DeviceType"), exports); __exportStar(require("./ChannelMessage"), exports); +__exportStar(require("./TransferType"), exports); __exportStar(require("./TransferMetadata"), exports); +__exportStar(require("./EndpointInfo"), exports); __exportStar(require("./ChannelAction"), exports); __exportStar(require("./RemoteDeviceInfo"), exports); __exportStar(require("./State"), exports); diff --git a/core_lib/src/bin.rs b/core_lib/src/bin.rs index 74051dd..7a7a247 100644 --- a/core_lib/src/bin.rs +++ b/core_lib/src/bin.rs @@ -2,6 +2,7 @@ extern crate log; use rquickshare::RQS; +use tokio::sync::mpsc; #[tokio::main] async fn main() -> Result<(), anyhow::Error> { @@ -17,9 +18,13 @@ async fn main() -> Result<(), anyhow::Error> { tracing_subscriber::fmt::init(); // Start the RQuickShare service - let rqs = RQS::default(); + let mut rqs = RQS::default(); rqs.run().await?; + let discovery_channel = mpsc::channel(10); + + rqs.discovery(discovery_channel.0)?; + // Wait for CTRL+C and then stop RQS let _ = tokio::signal::ctrl_c().await; info!("Stopping service."); diff --git a/core_lib/src/channel.rs b/core_lib/src/channel.rs index aabc7f6..c941387 100644 --- a/core_lib/src/channel.rs +++ b/core_lib/src/channel.rs @@ -20,6 +20,13 @@ pub enum ChannelAction { CancelTransfer, } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, TS)] +#[ts(export)] +pub enum TransferType { + Inbound, + Outbound, +} + #[derive(Debug, Clone, Default, Deserialize, Serialize, TS)] #[ts(export)] pub struct ChannelMessage { @@ -30,6 +37,7 @@ pub struct ChannelMessage { pub action: Option, // Only present when channelDirection is libToFront + pub rtype: Option, pub state: Option, pub meta: Option, } diff --git a/core_lib/src/hdl/inbound.rs b/core_lib/src/hdl/inbound.rs index 9b34a58..9e712e8 100644 --- a/core_lib/src/hdl/inbound.rs +++ b/core_lib/src/hdl/inbound.rs @@ -44,7 +44,7 @@ const SANE_FRAME_LENGTH: i32 = 5 * 1024 * 1024; #[derive(Debug)] pub struct InboundRequest { socket: TcpStream, - state: InnerState, + pub state: InnerState, sender: Sender, receiver: Receiver, } @@ -572,9 +572,9 @@ impl InboundRequest { current_offset )); } - if current_offset + chunk.body().len() as i64 > file_internal.meta.size() { + if current_offset + chunk.body().len() as i64 > file_internal.total_size { return Err(anyhow!( - "Transferred file size exceeds previously specified value" + "Transferred file size exceeds previously specified value: {} vs {}", current_offset + chunk.body().len() as i64, file_internal.total_size )); } @@ -753,10 +753,10 @@ impl InboundRequest { } let info = InternalFileInfo { - meta: file.clone(), payload_id: file.payload_id(), - destination_url: dest, + file_url: dest, bytes_transferred: 0, + total_size: file.size(), file: None, }; self.state.transferred_files.insert(file.payload_id(), info); @@ -864,7 +864,7 @@ impl InboundRequest { for id in ids { let mfi = self.state.transferred_files.get_mut(&id).unwrap(); - let file = File::create(&mfi.destination_url)?; + let file = File::create(&mfi.file_url)?; info!("Created file: {:?}", &file); mfi.file = Some(file); } @@ -1190,6 +1190,7 @@ impl InboundRequest { let _ = self.sender.send(ChannelMessage { id: self.state.id.clone(), direction: ChannelDirection::LibToFront, + rtype: Some(crate::channel::TransferType::Inbound), state: Some(self.state.state.clone()), meta: self.state.transfer_metadata.clone(), ..Default::default() diff --git a/core_lib/src/hdl/info.rs b/core_lib/src/hdl/info.rs index 171a030..aa3a5f4 100644 --- a/core_lib/src/hdl/info.rs +++ b/core_lib/src/hdl/info.rs @@ -4,19 +4,18 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; use ts_rs::TS; -use crate::sharing_nearby::FileMetadata; use crate::utils::RemoteDeviceInfo; #[derive(Debug)] pub struct InternalFileInfo { - pub meta: FileMetadata, pub payload_id: i64, - pub destination_url: PathBuf, + pub file_url: PathBuf, pub bytes_transferred: i64, + pub total_size: i64, pub file: Option, } -#[derive(Debug, Clone, Deserialize, Serialize, TS)] +#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)] #[ts(export)] pub struct TransferMetadata { pub id: String, diff --git a/core_lib/src/hdl/mod.rs b/core_lib/src/hdl/mod.rs index 65086d8..144cbaa 100644 --- a/core_lib/src/hdl/mod.rs +++ b/core_lib/src/hdl/mod.rs @@ -19,7 +19,7 @@ mod outbound; pub use outbound::*; #[allow(dead_code)] -#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS, PartialEq)] #[ts(export)] pub enum State { #[default] diff --git a/core_lib/src/hdl/outbound.rs b/core_lib/src/hdl/outbound.rs index 413acd8..46157e3 100644 --- a/core_lib/src/hdl/outbound.rs +++ b/core_lib/src/hdl/outbound.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use std::fs::File; +use std::io::Read; use std::os::unix::fs::MetadataExt; use std::path::Path; @@ -10,11 +12,14 @@ use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; use p256::{EncodedPoint, PublicKey}; use prost::Message; use rand::Rng; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256, Sha512}; use tokio::io::AsyncWriteExt; use tokio::net::TcpStream; use tokio::sync::broadcast::{Receiver, Sender}; +use ts_rs::TS; +use super::info::{InternalFileInfo, TransferMetadata}; use super::{InnerState, State}; use crate::channel::{ChannelAction, ChannelDirection, ChannelMessage}; use crate::location_nearby_connections::bandwidth_upgrade_negotiation_frame::upgrade_path_info::Medium; @@ -46,7 +51,8 @@ type HmacSha256 = Hmac; const SANE_FRAME_LENGTH: i32 = 5 * 1024 * 1024; -#[derive(Debug)] +#[derive(Debug, Deserialize, Serialize, TS)] +#[ts(export)] pub enum OutboundPayload { Files(Vec), } @@ -55,7 +61,7 @@ pub enum OutboundPayload { pub struct OutboundRequest { endpoint_id: [u8; 4], socket: TcpStream, - state: InnerState, + pub state: InnerState, sender: Sender, receiver: Receiver, payload: OutboundPayload, @@ -68,8 +74,12 @@ impl OutboundRequest { id: String, sender: Sender, payload: OutboundPayload, + rdi: RemoteDeviceInfo, ) -> Self { let receiver = sender.subscribe(); + let files = match &payload { + OutboundPayload::Files(f) => f, + }; Self { endpoint_id, @@ -81,6 +91,12 @@ impl OutboundRequest { state: State::Initial, encryption_done: true, text_payload_id: -1, + transfer_metadata: Some(TransferMetadata { + id: String::from(""), + source: Some(rdi), + files: Some(files.to_owned()), + ..Default::default() + }), ..Default::default() }, sender, @@ -149,13 +165,18 @@ impl OutboundRequest { State::SentUkeyClientInit => { debug!("Handling State::SentUkeyClientInit frame"); let msg = Ukey2Message::decode(&*frame_data)?; + self.update_state( + |e| { + e.server_init_data = Some(frame_data); + }, + false, + ); self.process_ukey2_server_init(&msg).await?; // Advance current state self.update_state( |e: &mut InnerState| { e.state = State::SentUkeyClientFinish; - e.server_init_data = Some(frame_data); e.encryption_done = true; }, false, @@ -194,9 +215,7 @@ impl OutboundRequest { location_nearby_connections::v1_frame::FrameType::ConnectionRequest.into(), ), connection_request: Some(location_nearby_connections::ConnectionRequestFrame { - endpoint_id: Some(unsafe { - String::from_utf8_unchecked(self.endpoint_id.to_vec()) - }), + endpoint_id: Some(String::from_utf8_lossy(&self.endpoint_id).to_string()), endpoint_name: Some(sys_metrics::host::get_hostname()?), endpoint_info: Some( RemoteDeviceInfo { @@ -553,12 +572,12 @@ impl OutboundRequest { |e| { e.state = State::SentIntroduction; }, - false, + true, ); } State::SentIntroduction => { debug!("Processing State::SentIntroduction"); - // self.process_introduction(v1_frame).await?; + self.process_consent(v1_frame).await?; } State::SendingFiles => {} _ => { @@ -605,6 +624,7 @@ impl OutboundRequest { } let mut file_metadata: Vec = vec![]; + let mut transferred_files: HashMap = HashMap::new(); let mut total_to_send = 0; // TODO - Handle sending Text match &self.payload { @@ -612,6 +632,7 @@ impl OutboundRequest { for f in files { let path = Path::new(f); if !path.is_file() { + warn!("Path is not a file: {}", f); continue; } @@ -646,6 +667,7 @@ impl OutboundRequest { file_metadata::Type::Unknown }; + info!("File type to send: {}", ftype); let fname = path .file_name() .ok_or_else(|| anyhow!("Failed to get file_name for {f}"))?; @@ -657,7 +679,16 @@ impl OutboundRequest { r#type: Some(meta_type.into()), ..Default::default() }; - // TODO - See how to handle those + transferred_files.insert( + fmeta.payload_id(), + InternalFileInfo { + payload_id: fmeta.payload_id(), + file_url: path.to_path_buf(), + bytes_transferred: 0, + total_size: fmeta.size(), + file: Some(file), + }, + ); file_metadata.push(fmeta); total_to_send += fmetadata.size(); } @@ -667,6 +698,7 @@ impl OutboundRequest { self.update_state( |e| { e.bytes_to_send = total_to_send; + e.transferred_files = transferred_files; }, false, ); @@ -688,6 +720,191 @@ impl OutboundRequest { Ok(()) } + async fn process_consent( + &mut self, + v1_frame: &sharing_nearby::V1Frame, + ) -> Result<(), anyhow::Error> { + if v1_frame.r#type() != sharing_nearby::v1_frame::FrameType::Response + || v1_frame.connection_response.is_none() + { + return Err(anyhow!("Missing required fields")); + } + + match v1_frame.connection_response.as_ref().unwrap().status() { + sharing_nearby::connection_response_frame::Status::Accept => { + info!("State is now State::SendingFiles"); + self.update_state( + |e| { + e.state = State::SendingFiles; + }, + true, + ); + + // TODO - Handle sending Text + let ids: Vec = self.state.transferred_files.keys().cloned().collect(); + info!("We are sending: {:?}", ids); + let mut ids_iter = ids.into_iter(); + // Loop through all files + loop { + let current = match ids_iter.next() { + Some(i) => i, + None => { + info!("All files have been transferred"); + self.disconnection().await?; + self.update_state( + |e| { + e.state = State::Finished; + }, + true, + ); + break; + } + }; + + // Loop until we reached end of file + loop { + // Workaround to limit scope of the immutable borrow on self + let (curr_state, buffer, bytes_read) = { + let curr_state = match self.state.transferred_files.get(¤t) { + Some(s) => s, + None => break, + }; + + info!("> Currently sending {:?}", curr_state.file_url); + if curr_state.bytes_transferred == curr_state.total_size { + debug!("File {current} finished"); + self.update_state( + |e| { + e.transferred_files.remove(¤t); + }, + false, + ); + break; + } + + if curr_state.file.is_none() { + warn!("File {current} is none"); + break; + } + + let mut buffer = vec![0u8; 512 * 1024]; + let bytes_read = curr_state.file.as_ref().unwrap().read(&mut buffer)?; + + ( + InternalFileInfo { + payload_id: curr_state.payload_id, + file_url: curr_state.file_url.clone(), + bytes_transferred: curr_state.bytes_transferred, + total_size: curr_state.total_size, + file: None, + }, + buffer, + bytes_read, + ) + }; + + let sending_buffer = buffer[..bytes_read].to_vec(); + info!( + "> File ready: {bytes_read} bytes && {} && left to send: {} with current offset: {}", + sending_buffer.len(), + curr_state.total_size - curr_state.bytes_transferred, + curr_state.bytes_transferred + ); + + let payload_header = PayloadHeader { + id: Some(current), + r#type: Some(payload_header::PayloadType::File.into()), + total_size: Some(curr_state.total_size), + is_sensitive: Some(false), + ..Default::default() + }; + + let wrapper = location_nearby_connections::OfflineFrame { + version: Some(location_nearby_connections::offline_frame::Version::V1.into()), + v1: Some(location_nearby_connections::V1Frame { + r#type: Some( + location_nearby_connections::v1_frame::FrameType::PayloadTransfer.into(), + ), + payload_transfer: Some(PayloadTransferFrame { + packet_type: Some(PacketType::Data.into()), + payload_chunk: Some(PayloadChunk { + offset: Some(curr_state.bytes_transferred), + flags: Some(0), + body: Some(buffer[..bytes_read].to_vec()), + }), + payload_header: Some(payload_header.clone()), + ..Default::default() + }), + ..Default::default() + }), + }; + + self.encrypt_and_send(&wrapper).await?; + self.update_state( + |e| { + if let Some(mu) = e.transferred_files.get_mut(¤t) { + mu.bytes_transferred += bytes_read as i64; + } + }, + false, + ); + + // If we just sent the last bytes of the file, mark it as finished + if curr_state.bytes_transferred + bytes_read as i64 == curr_state.total_size + { + debug!( + "File {current} finished, curr offset: {} over total: {}", + curr_state.bytes_transferred + bytes_read as i64, + curr_state.total_size + ); + + let wrapper = location_nearby_connections::OfflineFrame { + version: Some(location_nearby_connections::offline_frame::Version::V1.into()), + v1: Some(location_nearby_connections::V1Frame { + r#type: Some( + location_nearby_connections::v1_frame::FrameType::PayloadTransfer.into(), + ), + payload_transfer: Some(PayloadTransferFrame { + packet_type: Some(PacketType::Data.into()), + payload_chunk: Some(PayloadChunk { + offset: Some(curr_state.total_size), + flags: Some(1), // lastChunk + body: Some(vec![]), + }), + payload_header: Some(payload_header), + ..Default::default() + }), + ..Default::default() + }), + }; + + self.encrypt_and_send(&wrapper).await?; + break; + } + } + } + } + sharing_nearby::connection_response_frame::Status::Reject + | sharing_nearby::connection_response_frame::Status::NotEnoughSpace + | sharing_nearby::connection_response_frame::Status::UnsupportedAttachmentType + | sharing_nearby::connection_response_frame::Status::TimedOut => { + warn!( + "Cannot process: consent denied: {:?}", + v1_frame.connection_response.as_ref().unwrap().status() + ); + self.disconnection().await?; + return Err(anyhow!(crate::errors::AppError::NotAnError)); + } + sharing_nearby::connection_response_frame::Status::Unknown => { + error!("Unknown consent type: aborting"); + self.disconnection().await?; + return Err(anyhow!(crate::errors::AppError::NotAnError)); + } + } + + Ok(()) + } + async fn disconnection(&mut self) -> Result<(), anyhow::Error> { let frame = location_nearby_connections::OfflineFrame { version: Some(location_nearby_connections::offline_frame::Version::V1.into()), @@ -771,8 +988,12 @@ impl OutboundRequest { e.send_hmac_key = Some(client_hmac_key); e.pin_code = Some(to_four_digit_string(&auth_string)); e.encryption_done = true; + + if let Some(ref mut tm) = e.transfer_metadata { + tm.pin_code = Some(to_four_digit_string(&auth_string)); + } }, - false, + true, ); info!("Pin code: {:?}", self.state.pin_code); @@ -981,6 +1202,7 @@ impl OutboundRequest { let _ = self.sender.send(ChannelMessage { id: self.state.id.clone(), direction: ChannelDirection::LibToFront, + rtype: Some(crate::channel::TransferType::Outbound), state: Some(self.state.state.clone()), meta: self.state.transfer_metadata.clone(), ..Default::default() diff --git a/core_lib/src/lib.rs b/core_lib/src/lib.rs index e83d71f..fafd687 100644 --- a/core_lib/src/lib.rs +++ b/core_lib/src/lib.rs @@ -2,16 +2,19 @@ extern crate log; use channel::ChannelMessage; -use manager::SendInfo; -use rand::Rng; -use tokio::net::TcpListener; +use mdns_sd::{ServiceDaemon, ServiceEvent}; +use rand::{distributions, Rng}; +use serde::{Deserialize, Serialize}; +use tokio::net::{TcpListener, TcpStream}; use tokio::sync::broadcast::{self, Receiver}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; +use ts_rs::TS; use crate::hdl::{BleListener, MDnsServer}; use crate::manager::TcpServer; +use crate::utils::{is_not_self_ip, parse_mdns_endpoint_info}; pub mod channel; mod errors; @@ -20,6 +23,7 @@ mod manager; mod utils; pub use hdl::OutboundPayload; +pub use manager::SendInfo; pub use utils::DeviceType; pub mod sharing_nearby { @@ -42,6 +46,7 @@ pub mod location_nearby_connections { pub struct RQS { tracker: TaskTracker, ctoken: CancellationToken, + discovery_ctk: Option, pub channel: (broadcast::Sender, Receiver), } @@ -60,12 +65,17 @@ impl RQS { Self { tracker, ctoken, + discovery_ctk: None, channel, } } pub async fn run(&self) -> Result, anyhow::Error> { - let endpoint_id = rand::thread_rng().gen::<[u8; 4]>(); + let endpoint_id: Vec = rand::thread_rng() + .sample_iter(distributions::Alphanumeric) + .take(4) + .map(u8::from) + .collect(); let tcp_listener = TcpListener::bind("0.0.0.0:0").await?; let binded_addr = tcp_listener.local_addr()?; info!("TcpListener on: {}", binded_addr); @@ -76,7 +86,12 @@ impl RQS { let send_channel = mpsc::channel(10); // Start TcpServer in own "task" - let mut server = TcpServer::new(endpoint_id, tcp_listener, sender, send_channel.1)?; + let mut server = TcpServer::new( + endpoint_id[..4].try_into()?, + tcp_listener, + sender, + send_channel.1, + )?; let ctk = self.ctoken.clone(); self.tracker.spawn(async move { server.run(ctk).await }); @@ -90,7 +105,7 @@ impl RQS { // Start MDnsServer in own "task" let mdns = MDnsServer::new( - endpoint_id, + endpoint_id[..4].try_into()?, binded_addr.port(), DeviceType::Laptop, ble_channel.1, @@ -103,8 +118,123 @@ impl RQS { Ok(send_channel.0) } - pub async fn stop(&self) { + pub fn discovery(&mut self, sender: mpsc::Sender) -> Result<(), anyhow::Error> { + let ctk = CancellationToken::new(); + self.discovery_ctk = Some(ctk.clone()); + + let discovery = MDnsDiscovery::new(sender)?; + self.tracker.spawn(async move { discovery.run(ctk).await }); + + Ok(()) + } + + pub fn stop_discovery(&mut self) { + if let Some(discovert_ctk) = &self.discovery_ctk { + discovert_ctk.cancel(); + self.discovery_ctk = None; + } + } + + pub async fn stop(&mut self) { + self.stop_discovery(); self.ctoken.cancel(); self.tracker.wait().await; } } + +#[derive(Debug, Clone, Default, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct EndpointInfo { + pub id: String, + pub name: Option, + pub ip: Option, + pub port: Option, + pub rtype: Option, + pub present: Option, +} + +pub struct MDnsDiscovery { + daemon: ServiceDaemon, + sender: mpsc::Sender, +} + +impl MDnsDiscovery { + pub fn new(sender: mpsc::Sender) -> Result { + let daemon = ServiceDaemon::new()?; + + Ok(Self { daemon, sender }) + } + + pub async fn run(self, ctk: CancellationToken) -> Result<(), anyhow::Error> { + info!("MDnsDiscovery: service starting"); + + let service_type = "_FC9F5ED42C8A._tcp.local."; + let receiver = self.daemon.browse(service_type)?; + + loop { + tokio::select! { + _ = ctk.cancelled() => { + info!("MDnsDiscovery: tracker cancelled, breaking"); + break; + } + r = receiver.recv_async() => { + match r { + Ok(event) => { + match event { + ServiceEvent::ServiceResolved(info) => { + let port = info.get_port(); + let ip_hash = info.get_addresses_v4(); + let ip = if !ip_hash.is_empty() { + ip_hash.iter().next().unwrap() + } else { + continue; + }; + + // Check that the IP is not a "self IP" + if !is_not_self_ip(ip) { + continue; + } + + // Decode the "n" text properties + let n = info.get_property("n"); + if n.is_none() { + continue; + } + let (dt, dn) = match parse_mdns_endpoint_info(n.unwrap().val_str()) { + Ok(r) => r, + Err(_) => continue + }; + + if TcpStream::connect(format!("{ip}:{port}")).await.is_ok() { + let ei = EndpointInfo { + id: info.get_fullname().to_string(), + name: Some(dn), + ip: Some(ip.to_string()), + port: Some(port.to_string()), + rtype: Some(dt), + present: Some(true), + }; + info!("Resolved a new service: {:?}", ei); + let _ = self.sender.send(ei).await; + } + } + ServiceEvent::ServiceRemoved(_, fullname) => { + info!("Remove a previous service: {}", fullname); + let _ = self.sender.send(EndpointInfo { + id: fullname, + ..Default::default() + }).await; + } + ServiceEvent::SearchStarted(_) | ServiceEvent::SearchStopped(_) => {} + _ => {} + } + }, + Err(err) => error!("MDnsDiscovery: error: {}", err), + } + } + } + } + + Ok(()) + } +} diff --git a/core_lib/src/manager.rs b/core_lib/src/manager.rs index ebe7611..49faa59 100644 --- a/core_lib/src/manager.rs +++ b/core_lib/src/manager.rs @@ -1,17 +1,25 @@ +use anyhow::anyhow; +use serde::{Deserialize, Serialize}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::broadcast::Sender; use tokio::sync::mpsc::Receiver; use tokio_util::sync::CancellationToken; +use ts_rs::TS; use crate::channel::{ChannelDirection, ChannelMessage}; use crate::errors::AppError; use crate::hdl::{InboundRequest, OutboundPayload, OutboundRequest, State}; +use crate::utils::RemoteDeviceInfo; const INNER_NAME: &str = "TcpServer"; +#[derive(Debug, Deserialize, Serialize, TS)] +#[ts(export)] pub struct SendInfo { - addr: String, - ob: OutboundPayload, + pub id: String, + pub name: String, + pub addr: String, + pub ob: OutboundPayload, } pub struct TcpServer { @@ -48,7 +56,8 @@ impl TcpServer { break; } Some(i) = self.connect_receiver.recv() => { - if let Err(e) = self.connect(cctk, i.addr, i.ob).await { + info!("{INNER_NAME}: connect_receiver: got {:?}", i); + if let Err(e) = self.connect(cctk, i).await { error!("{INNER_NAME}: error sending: {:?}", e); } } @@ -68,13 +77,19 @@ impl TcpServer { Err(e) => match e.downcast_ref() { Some(AppError::NotAnError) => break, None => { - let _ = esender.send(ChannelMessage { - id: remote_addr.to_string(), - direction: ChannelDirection::LibToFront, - state: Some(State::Disconnected), - ..Default::default() - }); - error!("{INNER_NAME}: error while handling client: {e}"); + if ir.state.state == State::Initial { + break; + } + + if ir.state.state != State::Finished { + let _ = esender.send(ChannelMessage { + id: remote_addr.to_string(), + direction: ChannelDirection::LibToFront, + state: Some(State::Disconnected), + ..Default::default() + }); + } + error!("{INNER_NAME}: error while handling client: {e} ({:?})", ir.state.state); break; } }, @@ -95,19 +110,26 @@ impl TcpServer { } /// To be called inside a separate task if we want to handle concurrency - pub async fn connect( - &self, - ctk: CancellationToken, - addr: String, - ob: OutboundPayload, - ) -> Result<(), anyhow::Error> { - let socket = TcpStream::connect(addr.clone()).await?; + pub async fn connect(&self, ctk: CancellationToken, si: SendInfo) -> Result<(), anyhow::Error> { + debug!("{INNER_NAME}: Connecting to: {}", si.addr); + let socket = match TcpStream::connect(si.addr.clone()).await { + Ok(r) => r, + Err(e) => { + warn!("Couldn't connect to {}: {}", si.addr, e); + return Err(anyhow!("failed to connect to {}", si.addr)); + } + }; + let mut or = OutboundRequest::new( self.endpoint_id, socket, - addr.clone(), + si.id, self.sender.clone(), - ob, + si.ob, + RemoteDeviceInfo { + device_type: crate::DeviceType::Unknown, + name: si.name, + }, ); // Send connection request @@ -126,13 +148,19 @@ impl TcpServer { match e.downcast_ref() { Some(AppError::NotAnError) => break, None => { - let _ = self.sender.clone().send(ChannelMessage { - id: addr, - direction: ChannelDirection::LibToFront, - state: Some(State::Disconnected), - ..Default::default() - }); - error!("{INNER_NAME}: error while handling client: {e}"); + if or.state.state == State::Initial { + break; + } + + if or.state.state != State::Finished { + let _ = self.sender.clone().send(ChannelMessage { + id: si.addr, + direction: ChannelDirection::LibToFront, + state: Some(State::Disconnected), + ..Default::default() + }); + } + error!("{INNER_NAME}: error while handling client: {e} ({:?})", or.state.state); break; } } diff --git a/core_lib/src/utils.rs b/core_lib/src/utils.rs index 03f1d26..33c96ad 100644 --- a/core_lib/src/utils.rs +++ b/core_lib/src/utils.rs @@ -1,11 +1,13 @@ +use std::net::Ipv4Addr; use std::path::{Path, PathBuf}; use anyhow::anyhow; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; +use get_if_addrs::get_if_addrs; use hkdf::Hkdf; use p256::{PublicKey, SecretKey}; -use rand::{thread_rng, Rng, RngCore}; +use rand::{Rng, RngCore}; use serde::{Deserialize, Serialize}; use sha2::digest::generic_array::GenericArray; use sha2::Sha256; @@ -45,27 +47,19 @@ pub struct RemoteDeviceInfo { impl RemoteDeviceInfo { pub fn serialize(&self) -> Vec { - // Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bit) - // Assuming Version = 1, Visibility = 1 for demonstration - let version_and_visibility: u8 = 0b1000_0000; // Version 1 and visible - let device_type_bits: u8 = (self.device_type.clone() as u8) << 1; - let first_byte = version_and_visibility | device_type_bits; - - let mut endpoint_info = Vec::new(); - endpoint_info.push(first_byte); - - // Append 16 random bytes - let mut rng = thread_rng(); - let random_bytes: Vec = (0..16).map(|_| rng.gen()).collect(); - endpoint_info.extend(random_bytes); - - // Appending device name in UTF-8 prefixed with 1-byte length - let mut name_bytes = self.name.as_bytes().to_vec(); - if name_bytes.len() > 255 { - name_bytes.truncate(255); + // 1 byte: Version(3 bits)|Visibility(1 bit)|Device Type(3 bits)|Reserved(1 bit) + let mut endpoint_info: Vec = vec![((self.device_type.clone() as u8) << 1) & 0b111]; + + // 16 bytes: unknown random bytes + endpoint_info.extend((0..16).map(|_| rand::thread_rng().gen_range(0..=255))); + + // Device name in UTF-8 prefixed with 1-byte length + let mut name_chars = self.name.as_bytes().to_vec(); + if name_chars.len() > 255 { + name_chars.truncate(255); } - endpoint_info.push(name_bytes.len() as u8); - endpoint_info.extend(name_bytes); + endpoint_info.push(name_chars.len() as u8); + endpoint_info.extend(name_chars); endpoint_info } @@ -106,6 +100,25 @@ pub fn gen_mdns_endpoint_info(device_type: u8, device_name: &str) -> String { URL_SAFE_NO_PAD.encode(&record) } +pub fn parse_mdns_endpoint_info(encoded_str: &str) -> Result<(DeviceType, String), anyhow::Error> { + let decoded_bytes = URL_SAFE_NO_PAD.decode(encoded_str)?; + if decoded_bytes.len() < 19 { + return Err(anyhow!("Invalid data length")); + } + + let device_type_byte = decoded_bytes[0]; + let device_type = (device_type_byte & 0b0111) >> 4; + let name_length = decoded_bytes[17] as usize; + if 18 + name_length > decoded_bytes.len() { + return Err(anyhow!("Invalid name length")); + } + + let device_name_bytes = &decoded_bytes[18..18 + name_length]; + let device_name = String::from_utf8(device_name_bytes.to_vec())?; + + Ok((DeviceType::from_raw_value(device_type), device_name)) +} + pub async fn stream_read_exact( socket: &mut TcpStream, buf: &mut [u8], @@ -179,3 +192,15 @@ pub fn get_download_dir() -> PathBuf { Path::new("/").to_path_buf() } + +pub fn is_not_self_ip(ip_address: &Ipv4Addr) -> bool { + if let Ok(if_addrs) = get_if_addrs() { + for if_addr in if_addrs { + if if_addr.ip() == *ip_address { + return false; + } + } + } + + true +} diff --git a/frontend/.eslintrc-auto-import.json b/frontend/.eslintrc-auto-import.json index 0be846a..b2abc8d 100644 --- a/frontend/.eslintrc-auto-import.json +++ b/frontend/.eslintrc-auto-import.json @@ -4,10 +4,14 @@ "ComponentPublicInstance": true, "ComputedRef": true, "EffectScope": true, + "ExtractDefaultPropTypes": true, + "ExtractPropTypes": true, + "ExtractPublicPropTypes": true, "InjectionKey": true, "PropType": true, "Ref": true, "VNode": true, + "WritableComputedRef": true, "computed": true, "createApp": true, "customRef": true, @@ -48,6 +52,7 @@ "toRaw": true, "toRef": true, "toRefs": true, + "toValue": true, "triggerRef": true, "unref": true, "useAttrs": true, @@ -57,11 +62,6 @@ "watch": true, "watchEffect": true, "watchPostEffect": true, - "watchSyncEffect": true, - "toValue": true, - "ExtractDefaultPropTypes": true, - "ExtractPropTypes": true, - "ExtractPublicPropTypes": true, - "WritableComputedRef": true + "watchSyncEffect": true } } diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 905d0d3..c8e037c 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -509,6 +509,12 @@ dependencies = [ "serde", ] +[[package]] +name = "c_linked_list" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" + [[package]] name = "cairo-rs" version = "0.15.12" @@ -923,7 +929,7 @@ dependencies = [ "futures-util", "libc", "libdbus-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1033,7 +1039,7 @@ checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -1511,6 +1517,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "gdk" version = "0.15.4" @@ -1621,6 +1633,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "get_if_addrs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abddb55a898d32925f3148bd281174a68eeb68bbfd9a5938a57b18f506ee4ef7" +dependencies = [ + "c_linked_list", + "get_if_addrs-sys", + "libc", + "winapi 0.2.8", +] + +[[package]] +name = "get_if_addrs-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d04f9fb746cf36b191c00f3ede8bde9c8e64f9f4b05ae2694a9ccf5e3f5ab48" +dependencies = [ + "gcc", + "libc", +] + [[package]] name = "gethostname" version = "0.4.3" @@ -1686,7 +1720,7 @@ dependencies = [ "gobject-sys", "libc", "system-deps 6.2.0", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2385,7 +2419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2686,7 +2720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2878,7 +2912,7 @@ checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" dependencies = [ "log", "serde", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3627,6 +3661,7 @@ dependencies = [ "btleplug", "directories", "futures", + "get_if_addrs", "hex", "hkdf", "hmac", @@ -4007,7 +4042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4063,7 +4098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4949,7 +4984,7 @@ checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset 0.9.0", "tempfile", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5294,6 +5329,12 @@ dependencies = [ "rustix 0.38.31", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -5316,7 +5357,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5845,7 +5886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5888,7 +5929,7 @@ dependencies = [ "static_assertions", "tracing", "uds_windows", - "winapi", + "winapi 0.3.9", "xdg-home", "zbus_macros", "zbus_names", diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index dab6c3a..a1c52a0 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -6,15 +6,21 @@ #[macro_use] extern crate log; +use std::sync::Mutex; + use rquickshare::channel::{ChannelDirection, ChannelMessage}; -use rquickshare::RQS; +use rquickshare::{EndpointInfo, SendInfo, RQS}; use tauri::{ CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, }; -use tokio::sync::broadcast::Sender; +use tokio::sync::broadcast; +use tokio::sync::mpsc; pub struct AppState { - pub sender: Sender, + pub sender: broadcast::Sender, + pub sender_file: mpsc::Sender, + pub rqs: Mutex, + pub dch_sender: mpsc::Sender, } #[tokio::main] @@ -35,33 +41,59 @@ async fn main() -> Result<(), anyhow::Error> { // Start the RQuickShare service let rqs = RQS::default(); - rqs.run().await?; + let sender_file = rqs.run().await?; - let (sender, mut receiver) = rqs.channel; + let (dch_sender, mut dch_receiver) = mpsc::channel(10); + let (sender, mut receiver) = (rqs.channel.0.clone(), rqs.channel.1.resubscribe()); // Configure System Tray + let test = CustomMenuItem::new("test".to_string(), "Test"); let show = CustomMenuItem::new("show".to_string(), "Show"); let quit = CustomMenuItem::new("quit".to_string(), "Quit"); let tray_menu = SystemTrayMenu::new() .add_item(show) .add_native_item(SystemTrayMenuItem::Separator) - .add_item(quit); + .add_item(quit) + .add_native_item(SystemTrayMenuItem::Separator) + .add_item(test); let tray = SystemTray::new().with_menu(tray_menu); // Build and run Tauri app tauri::Builder::default() - .manage(AppState { sender }) - .invoke_handler(tauri::generate_handler![js2rs, open]) + .manage(AppState { + sender: sender, + sender_file: sender_file, + rqs: Mutex::new(rqs), + dch_sender: dch_sender, + }) + .invoke_handler(tauri::generate_handler![ + js2rs, + open, + send_payload, + start_discovery, + stop_discovery + ]) .setup(|app| { let app_handle = app.handle(); tauri::async_runtime::spawn(async move { loop { - if let Ok(info) = receiver.recv().await { - rs2js(info, &app_handle); - } else { - error!("Error getting receiver message"); - // TODO - Handle error gracefully - std::process::exit(0); + match receiver.recv().await { + Ok(info) => rs2js(info, &app_handle), + Err(e) => { + error!("Error getting receiver message: {}", e); + } + } + } + }); + + let capp_handle = app.handle(); + tauri::async_runtime::spawn(async move { + loop { + match dch_receiver.recv().await { + Some(info) => rs2js_discovery(info, &capp_handle), + None => { + error!("Error getting dch_receiver message"); + } } } }); @@ -113,9 +145,25 @@ fn rs2js(message: ChannelMessage, manager: &impl Manager) manager.emit_all("rs2js", &message).unwrap(); } +fn rs2js_discovery(message: EndpointInfo, manager: &impl Manager) { + info!("rs2js_discovery: {:?}", &message); + manager.emit_all("rs2js_discovery", &message).unwrap(); +} + +#[tauri::command] +async fn send_payload(message: SendInfo, state: tauri::State<'_, AppState>) -> Result<(), String> { + info!("send_payload: {:?}", &message); + + state + .sender_file + .send(message) + .await + .map_err(|e| format!("couldn't send payload: {e}")) +} + #[tauri::command] fn open(message: String) -> Result<(), String> { - info!("js2rs: {:?}", &message); + info!("open: {:?}", &message); match open::that(message) { Ok(_) => Ok(()), @@ -132,3 +180,22 @@ fn js2rs(message: ChannelMessage, state: tauri::State<'_, AppState>) -> Result<( Err(e) => return Err(format!("Coudln't perform: {}", e)), } } + +#[tauri::command] +async fn start_discovery(state: tauri::State<'_, AppState>) -> Result<(), String> { + info!("start_discovery"); + + state + .rqs + .lock() + .unwrap() + .discovery(state.dch_sender.clone()) + .map_err(|e| format!("unable to start discovery: {}", e)) +} + +#[tauri::command] +fn stop_discovery(state: tauri::State<'_, AppState>) { + info!("stop_discovery"); + + state.rqs.lock().unwrap().stop_discovery(); +} diff --git a/frontend/src/components/HomePage.vue b/frontend/src/components/HomePage.vue index 7aa05fe..045d90e 100644 --- a/frontend/src/components/HomePage.vue +++ b/frontend/src/components/HomePage.vue @@ -2,11 +2,16 @@ import { ref } from 'vue' import { listen } from '@tauri-apps/api/event' import { invoke } from '@tauri-apps/api/tauri' +import { appWindow } from "@tauri-apps/api/window"; import { isPermissionGranted, requestPermission, sendNotification } from '@tauri-apps/api/notification' import { ChannelMessage } from '../../../core_lib/bindings/ChannelMessage'; import { ChannelAction } from '../../../core_lib/bindings/ChannelAction'; +import { EndpointInfo } from '../../../core_lib/dist/EndpointInfo'; +import { OutboundPayload } from '../../../core_lib/bindings/OutboundPayload'; +import { SendInfo } from '../../../core_lib/bindings/SendInfo'; +let discoveryRunning = false; let isAppInForeground = false; // Do you have permission to send a notification? let permissionGranted = await isPermissionGranted(); @@ -17,15 +22,18 @@ if (!permissionGranted) { permissionGranted = permission === 'granted'; } -const _stateToDisplay = ["ReceivedPairedKeyResult", "WaitingForUserConsent", "ReceivingFiles", "Disconnected", "Finished"] +const _stateToDisplay = ["ReceivedPairedKeyResult", "WaitingForUserConsent", "ReceivingFiles", "Disconnected", "Finished", "SentIntroduction", "SendingFiles"] interface ToDelete { id: string, triggered: number } +const isDragHovering = ref(false); const requests = ref([]); +const endpointsInfo = ref([]); const toDelete = ref([]); +const outboundPayload = ref(); const requestIsEmpty = computed(() => { return requests.value.filter((el) => _stateToDisplay.includes(el.state ?? 'Initial')).length == 0 }); @@ -37,12 +45,26 @@ async function sendCmd(id: string, action: ChannelAction) { action: action, meta: null, state: null, + rtype: null, }; console.log("js2rs:", cm); await invoke('js2rs', { message: cm }); } +async function sendInfo(ei: EndpointInfo) { + if (outboundPayload.value === undefined) return; + + const msg: SendInfo = { + id: ei.id, + name: ei.name ?? 'Unknown', + addr: ei.ip + ":" + ei.port, + ob: outboundPayload.value, + }; + + await invoke('send_payload', { message: msg }); +} + function removeRequest(id: string) { const idx = requests.value.findIndex((el) => el.id === id); @@ -51,6 +73,13 @@ function removeRequest(id: string) { } } +async function clearSending() { + outboundPayload.value = undefined; + await invoke('stop_discovery'); + discoveryRunning = false; + endpointsInfo.value = []; +} + await listen('rs2js', (event) => { const cm = event.payload as ChannelMessage; console.log("rs2js:", cm); @@ -64,7 +93,7 @@ await listen('rs2js', (event) => { }); } - if (idx != -1) { + if (idx !== -1) { const prev = requests.value.at(idx); // Update the existing message at index 'idx' requests.value.splice(idx, 1, { @@ -82,6 +111,42 @@ await listen('rs2js', (event) => { } }) +await listen('rs2js_discovery', (event) => { + const ei = event.payload as EndpointInfo; + console.log("rs2js:", ei); + + const idx = endpointsInfo.value.findIndex((el) => el.id === ei.id); + if (!ei.present) { + if (idx !== -1) { + endpointsInfo.value.splice(idx, 1); + } + + return; + } + + if (idx !== -1) { + endpointsInfo.value.splice(idx, 1, ei); + } else { + endpointsInfo.value.push(ei); + } +}); + +await appWindow.onFileDropEvent(async (event) => { + if (event.payload.type === 'hover') { + isDragHovering.value = true; + } else if (event.payload.type === 'drop') { + console.log("Dropped"); + isDragHovering.value = false; + outboundPayload.value = { + Files: event.payload.paths + } as OutboundPayload; + if (!discoveryRunning) await invoke('start_discovery'); + discoveryRunning = true; + } else { + isDragHovering.value = false; + } +}); + setInterval(() => { toDelete.value.forEach((itemToDelete) => { const now = new Date(); @@ -134,7 +199,7 @@ window.addEventListener('blur', () => { -
+

Currently

@@ -145,6 +210,36 @@ window.addEventListener('blur', () => { Everyone can share with you (you still need to approve each transfer).

+
+
+

+ Sharing {{ outboundPayload.Files.length }} file{{ outboundPayload.Files.length > 1 ? 's' : '' }} +

+
+ + + + +
+

+ {{ f.split('/').pop() }} +

+ +

+ Make sure both devices are unlocked, close together, and have bluetooth turned on. Device you're sharing with need + Quick Share turned on and visible to you. +

+
+ +

+ Cancel +

+
@@ -153,16 +248,71 @@ window.addEventListener('blur', () => { Nearby devices -
+
+
+ + + + +

+ Drop files to send +

+
+ +
+
+
+ + + + + + + + + + + + + + + + +
+
+
+

+ {{ ei.name ?? 'Unknown' }} +

+
+
+
@@ -197,7 +347,9 @@ window.addEventListener('blur', () => {
-

+

@@ -218,18 +370,26 @@ window.addEventListener('blur', () => {

Accept

Decline

+
+

+ Sending +

+

+ {{ f }} +

+

Receiving... @@ -249,13 +409,13 @@ window.addEventListener('blur', () => {

Open

Clear

@@ -268,7 +428,7 @@ window.addEventListener('blur', () => {

Clear

@@ -276,7 +436,7 @@ window.addEventListener('blur', () => {
-
+