1#![allow(deprecated)]
12
13use futures::{AsyncRead, AsyncWrite};
14use serde_derive::{Deserialize, Serialize};
15#[cfg(test)]
16use serde_json::json;
17use std::sync::Arc;
18
19use super::{core::WormholeError, transit, AppID, Wormhole};
20use futures::Future;
21use std::{borrow::Cow, collections::BTreeMap};
22
23#[cfg(not(target_family = "wasm"))]
24use std::path::{Path, PathBuf};
25
26use transit::{
27 Abilities as TransitAbilities, Transit, TransitConnectError, TransitConnector, TransitError,
28};
29
30mod cancel;
31#[doc(hidden)]
32pub mod offer;
33mod v1;
34#[cfg(feature = "experimental-transfer-v2")]
35#[allow(missing_docs)]
36mod v2;
37
38#[doc(hidden)]
39pub use v1::ReceiveRequest as ReceiveRequestV1;
40
41#[cfg(not(feature = "experimental-transfer-v2"))]
42pub use v1::ReceiveRequest;
43
44#[cfg(feature = "experimental-transfer-v2")]
45pub use v2::ReceiveRequest as ReceiveRequestV2;
46
47const APPID_RAW: &str = "lothar.com/wormhole/text-or-file-xfer";
48
49pub const APPID: AppID = AppID(Cow::Borrowed(APPID_RAW));
51
52pub const APP_CONFIG: crate::AppConfig<AppVersion> = crate::AppConfig::<AppVersion> {
57 id: AppID(Cow::Borrowed(APPID_RAW)),
58 rendezvous_url: Cow::Borrowed(crate::rendezvous::DEFAULT_RENDEZVOUS_SERVER),
59 app_version: AppVersion::new(),
60};
61
62#[derive(Debug, thiserror::Error)]
65#[non_exhaustive]
66pub enum TransferError {
68 #[error("Transfer was not acknowledged by peer")]
70 AckError,
71
72 #[error("Receive checksum error")]
74 Checksum,
75
76 #[error("The file contained a different amount of bytes than advertized! Sent {} bytes, but should have been {}", sent_size, file_size)]
78 FileSize {
79 sent_size: u64,
81 file_size: u64,
83 },
84
85 #[error("The file(s) to send got modified during the transfer, and thus corrupted")]
87 FilesystemSkew,
88
89 #[error("Unsupported offer type")]
92 UnsupportedOffer,
93
94 #[error("Something went wrong on the other side: {}", _0)]
96 PeerError(String),
97
98 #[error("Corrupt JSON message received")]
100 ProtocolJson(
101 #[from]
102 #[source]
103 serde_json::Error,
104 ),
105
106 #[error("Corrupt Msgpack message received")]
108 ProtocolMsgpack(
109 #[from]
110 #[source]
111 rmp_serde::decode::Error,
112 ),
113
114 #[error("Protocol error: {}", _0)]
117 Protocol(Box<str>),
118
119 #[error(
121 "Unexpected message (protocol error): Expected '{}', but got: '{}'",
122 _0,
123 _1
124 )]
125 ProtocolUnexpectedMessage(Box<str>, Box<str>),
126
127 #[error("Wormhole connection error")]
129 Wormhole(
130 #[from]
131 #[source]
132 WormholeError,
133 ),
134
135 #[error("Error while establishing transit connection")]
137 TransitConnect(
138 #[from]
139 #[source]
140 TransitConnectError,
141 ),
142
143 #[error("Transit error")]
145 Transit(
146 #[from]
147 #[source]
148 TransitError,
149 ),
150
151 #[error("I/O error")]
153 IO(
154 #[from]
155 #[source]
156 std::io::Error,
157 ),
158}
159
160impl TransferError {
161 pub(self) fn unexpected_message(
162 expected: impl Into<Box<str>>,
163 got: impl std::fmt::Display,
164 ) -> Self {
165 Self::ProtocolUnexpectedMessage(expected.into(), got.to_string().into())
166 }
167}
168
169#[derive(Clone, Serialize, Deserialize)]
173#[serde(rename_all = "kebab-case")]
174pub struct AppVersion {
175 #[serde(default)]
176 abilities: Cow<'static, [Cow<'static, str>]>,
177 #[serde(default)]
178 #[cfg(feature = "experimental-transfer-v2")]
179 transfer_v2: Option<AppVersionTransferV2Hint>,
180}
181
182impl AppVersion {
184 const fn new() -> Self {
185 Self {
186 abilities: Cow::Borrowed(&[
188 Cow::Borrowed("transfer-v1"), ]),
190 #[cfg(feature = "experimental-transfer-v2")]
191 transfer_v2: Some(AppVersionTransferV2Hint::new()),
192 }
193 }
194
195 #[allow(dead_code)]
196 fn supports_v2(&self) -> bool {
197 self.abilities.contains(&"transfer-v2".into())
198 }
199}
200
201impl Default for AppVersion {
202 fn default() -> Self {
203 Self::new()
204 }
205}
206
207#[cfg(feature = "experimental-transfer-v2")]
209#[derive(Clone, Debug, Serialize, Deserialize)]
210#[serde(rename_all = "kebab-case")]
211pub struct AppVersionTransferV2Hint {
212 supported_formats: Cow<'static, [Cow<'static, str>]>,
213 transit_abilities: transit::Abilities,
214}
215
216#[cfg(feature = "experimental-transfer-v2")]
217impl AppVersionTransferV2Hint {
218 const fn new() -> Self {
219 Self {
220 supported_formats: Cow::Borrowed(&[Cow::Borrowed("plain"), Cow::Borrowed("tar")]),
221 transit_abilities: transit::Abilities::ALL_ABILITIES,
222 }
223 }
224}
225
226#[cfg(feature = "experimental-transfer-v2")]
227impl Default for AppVersionTransferV2Hint {
228 fn default() -> Self {
229 Self::new()
230 }
231}
232
233#[derive(Deserialize, Serialize, derive_more::Display, Debug, Clone)]
237#[serde(rename_all = "kebab-case")]
238#[non_exhaustive]
239#[deprecated(
240 since = "0.7.0",
241 note = "This will be a private type in the future. Open an issue if you require access to protocol intrinsics in the future"
242)]
243pub enum PeerMessage {
244 #[display("transit")]
247 Transit(v1::TransitV1),
248
249 #[display("offer")]
251 Offer(v1::OfferMessage),
252
253 #[display("answer")]
255 Answer(v1::AnswerMessage),
256 #[cfg(feature = "experimental-transfer-v2")]
259 #[display("transit-v2")]
260 TransitV2(v2::TransitV2),
261
262 #[display("error")]
264 Error(String),
265
266 #[display("unknown")]
268 #[serde(other)]
269 Unknown,
270}
271
272impl PeerMessage {
273 #[allow(unused)]
274 fn offer_message_v1(msg: impl Into<String>) -> Self {
275 PeerMessage::Offer(v1::OfferMessage::Message(msg.into()))
276 }
277
278 fn offer_file_v1(name: impl Into<String>, size: u64) -> Self {
279 PeerMessage::Offer(v1::OfferMessage::File {
280 filename: name.into(),
281 filesize: size,
282 })
283 }
284
285 #[allow(dead_code)]
286 fn offer_directory_v1(
287 name: impl Into<String>,
288 mode: impl Into<String>,
289 compressed_size: u64,
290 numbytes: u64,
291 numfiles: u64,
292 ) -> Self {
293 PeerMessage::Offer(v1::OfferMessage::Directory {
294 dirname: name.into(),
295 mode: mode.into(),
296 zipsize: compressed_size,
297 numbytes,
298 numfiles,
299 })
300 }
301
302 #[allow(dead_code)]
303 fn message_ack_v1(msg: impl Into<String>) -> Self {
304 PeerMessage::Answer(v1::AnswerMessage::MessageAck(msg.into()))
305 }
306
307 fn file_ack_v1(msg: impl Into<String>) -> Self {
308 PeerMessage::Answer(v1::AnswerMessage::FileAck(msg.into()))
309 }
310
311 fn error_message(msg: impl Into<String>) -> Self {
312 PeerMessage::Error(msg.into())
313 }
314
315 fn transit_v1(abilities: TransitAbilities, hints: transit::Hints) -> Self {
316 PeerMessage::Transit(v1::TransitV1 {
317 abilities_v1: abilities,
318 hints_v1: hints,
319 })
320 }
321
322 #[cfg(feature = "experimental-transfer-v2")]
323 fn transit_v2(hints_v2: transit::Hints) -> Self {
324 PeerMessage::TransitV2(v2::TransitV2 { hints_v2 })
325 }
326
327 fn check_err(&self) -> Result<Self, TransferError> {
328 match self {
329 Self::Error(err) => Err(TransferError::PeerError(err.clone())),
330 other => Ok(other.clone()),
331 }
332 }
333
334 #[allow(dead_code)]
335 fn ser_json(&self) -> Vec<u8> {
336 serde_json::to_vec(self).unwrap()
337 }
338}
339
340#[cfg_attr(not(feature = "experimental-transfer-v2"), doc(hidden))]
345pub async fn send(
346 wormhole: Wormhole,
347 relay_hints: Vec<transit::RelayHint>,
348 transit_abilities: transit::Abilities,
349 offer: offer::OfferSend,
350 transit_handler: impl FnOnce(transit::TransitInfo),
351 progress_handler: impl FnMut(u64, u64) + 'static,
352 cancel: impl Future<Output = ()>,
353) -> Result<(), TransferError> {
354 let peer_version: AppVersion = serde_json::from_value(wormhole.peer_version().clone())?;
355
356 #[cfg(feature = "experimental-transfer-v2")]
357 {
358 if peer_version.supports_v2() {
359 return v2::send(
360 wormhole,
361 relay_hints,
362 transit_abilities,
363 offer,
364 progress_handler,
365 peer_version,
366 cancel,
367 )
368 .await;
369 }
370 }
371
372 v1::send(
373 wormhole,
374 relay_hints,
375 transit_abilities,
376 offer,
377 progress_handler,
378 transit_handler,
379 peer_version,
380 cancel,
381 )
382 .await
383}
384
385#[cfg(feature = "experimental-transfer-v2")]
397pub async fn request(
398 wormhole: Wormhole,
399 relay_hints: Vec<transit::RelayHint>,
400 transit_abilities: transit::Abilities,
401 cancel: impl Future<Output = ()>,
402) -> Result<Option<ReceiveRequest>, TransferError> {
403 #[cfg(feature = "experimental-transfer-v2")]
404 {
405 let peer_version: AppVersion = serde_json::from_value(wormhole.peer_version().clone())?;
406 if peer_version.supports_v2() {
407 v2::request(
408 wormhole,
409 relay_hints,
410 peer_version,
411 transit_abilities,
412 cancel,
413 )
414 .await
415 .map(|req| req.map(ReceiveRequest::V2))
416 } else {
417 v1::request(wormhole, relay_hints, transit_abilities, cancel)
418 .await
419 .map(|req| req.map(ReceiveRequest::V1))
420 }
421 }
422}
423
424#[cfg_attr(
430 feature = "experimental-transfer-v2",
431 deprecated(
432 since = "0.7.0",
433 note = "transfer::request_file does not support file transfer protocol version 2.
434 To continue only supporting version 1, use transfer::v1::request. To support both protocol versions, use transfer::request"
435 )
436)]
437pub async fn request_file(
438 wormhole: Wormhole,
439 relay_hints: Vec<transit::RelayHint>,
440 transit_abilities: transit::Abilities,
441 cancel: impl Future<Output = ()>,
442) -> Result<Option<v1::ReceiveRequest>, TransferError> {
443 v1::request(wormhole, relay_hints, transit_abilities, cancel).await
444}
445
446#[cfg_attr(
450 feature = "experimental-transfer-v2",
451 deprecated(
452 since = "0.7.0",
453 note = "transfer::send_file does not support file transfer protocol version 2, use transfer::send"
454 )
455)]
456pub async fn send_file<F, N, G, H>(
457 wormhole: Wormhole,
458 relay_hints: Vec<transit::RelayHint>,
459 file: &mut F,
460 file_name: N,
461 file_size: u64,
462 transit_abilities: transit::Abilities,
463 transit_handler: G,
464 progress_handler: H,
465 cancel: impl Future<Output = ()>,
466) -> Result<(), TransferError>
467where
468 F: AsyncRead + Unpin + Send,
469 N: Into<String>,
470 G: FnOnce(transit::TransitInfo),
471 H: FnMut(u64, u64) + 'static,
472{
473 v1::send_file(
474 wormhole,
475 relay_hints,
476 file,
477 file_name,
478 file_size,
479 transit_abilities,
480 transit_handler,
481 progress_handler,
482 cancel,
483 )
484 .await
485}
486
487#[cfg_attr(
489 feature = "experimental-transfer-v2",
490 deprecated(
491 since = "0.7.0",
492 note = "transfer::send_file_or_folder does not support file transfer protocol version 2, use transfer::send"
493 )
494)]
495#[allow(deprecated)]
496#[cfg(not(target_family = "wasm"))]
497pub async fn send_file_or_folder<N, M, G, H>(
498 wormhole: Wormhole,
499 relay_hints: Vec<transit::RelayHint>,
500 file_path: N,
501 file_name: M,
502 transit_abilities: transit::Abilities,
503 transit_handler: G,
504 progress_handler: H,
505 cancel: impl Future<Output = ()>,
506) -> Result<(), TransferError>
507where
508 N: AsRef<Path>,
509 M: Into<String>,
510 G: FnOnce(transit::TransitInfo),
511 H: FnMut(u64, u64) + 'static,
512{
513 use async_std::fs::File;
514 let file_path = file_path.as_ref();
515 let file_name = file_name.into();
516
517 let mut file = File::open(file_path).await?;
518 let metadata = file.metadata().await?;
519 if metadata.is_dir() {
520 send_folder(
521 wormhole,
522 relay_hints,
523 file_path,
524 file_name,
525 transit_abilities,
526 transit_handler,
527 progress_handler,
528 cancel,
529 )
530 .await?;
531 } else {
532 let file_size = metadata.len();
533 send_file(
534 wormhole,
535 relay_hints,
536 &mut file,
537 file_name,
538 file_size,
539 transit_abilities,
540 transit_handler,
541 progress_handler,
542 cancel,
543 )
544 .await?;
545 }
546 Ok(())
547}
548
549#[cfg_attr(
553 feature = "experimental-transfer-v2",
554 deprecated(
555 since = "0.7.0",
556 note = "transfer::send_folder does not support file transfer protocol version 2, use transfer::send"
557 )
558)]
559#[cfg(not(target_family = "wasm"))]
560pub async fn send_folder<N, M, G, H>(
561 wormhole: Wormhole,
562 relay_hints: Vec<transit::RelayHint>,
563 folder_path: N,
564 folder_name: M,
565 transit_abilities: transit::Abilities,
566 transit_handler: G,
567 progress_handler: H,
568 cancel: impl Future<Output = ()>,
569) -> Result<(), TransferError>
570where
571 N: Into<PathBuf>,
572 M: Into<String>,
573 G: FnOnce(transit::TransitInfo),
574 H: FnMut(u64, u64) + 'static,
575{
576 let offer = offer::OfferSendEntry::new(folder_path.into()).await?;
577
578 v1::send_folder(
579 wormhole,
580 relay_hints,
581 folder_name.into(),
582 offer,
583 transit_abilities,
584 transit_handler,
585 progress_handler,
586 cancel,
587 )
588 .await
589}
590
591#[must_use]
597#[cfg(feature = "experimental-transfer-v2")]
598pub enum ReceiveRequest {
599 V1(ReceiveRequestV1),
601 V2(ReceiveRequestV2),
603}
604
605#[cfg(feature = "experimental-transfer-v2")]
606impl ReceiveRequest {
607 pub async fn accept<F, G, W>(
609 self,
610 transit_handler: G,
611 progress_handler: F,
612 mut answer: offer::OfferAccept,
613 cancel: impl Future<Output = ()>,
614 ) -> Result<(), TransferError>
615 where
616 F: FnMut(u64, u64) + 'static,
617 G: FnOnce(transit::TransitInfo),
618 W: AsyncWrite + Unpin,
619 {
620 match self {
621 ReceiveRequest::V1(request) => {
622 let (_name, entry) = answer.content.pop_first().expect(
624 "must call accept(..) with an offer that contains at least one element",
625 );
626
627 let mut acceptor = match entry {
628 offer::OfferEntry::RegularFile { content, .. } => {
629 (content.content)(true).await?
630 },
631 _ => panic!(
632 "when using transfer v1 you must call accept(..) with file offers only",
633 ),
634 };
635
636 request
637 .accept(transit_handler, progress_handler, &mut acceptor, cancel)
638 .await
639 },
640 ReceiveRequest::V2(request) => {
641 request
642 .accept(transit_handler, answer, progress_handler, cancel)
643 .await
644 },
645 }
646 }
647
648 pub async fn reject(self) -> Result<(), TransferError> {
654 match self {
655 ReceiveRequest::V1(request) => request.reject().await,
656 ReceiveRequest::V2(request) => request.reject().await,
657 }
658 }
659
660 pub fn offer(&self) -> Arc<offer::Offer> {
662 match self {
663 ReceiveRequest::V1(req) => req.offer(),
664 ReceiveRequest::V2(req) => req.offer(),
665 }
666 }
667}
668
669#[cfg(test)]
670mod test {
671 use super::*;
672 use transit::{Abilities, DirectHint, RelayHint};
673
674 #[test]
675 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
676 fn test_transit() {
677 let abilities = Abilities::ALL_ABILITIES;
678 let hints = transit::Hints::new(
679 [DirectHint::new("192.168.1.8", 46295)],
680 [RelayHint::new(
681 None,
682 [DirectHint::new("magic-wormhole-transit.debian.net", 4001)],
683 [],
684 )],
685 );
686 assert_eq!(
687 serde_json::json!(crate::transfer::PeerMessage::transit_v1(abilities, hints)),
688 serde_json::json!({
689 "transit": {
690 "abilities-v1": [{"type":"direct-tcp-v1"},{"type":"relay-v1"}],
691 "hints-v1": [
692 {"hostname":"192.168.1.8","port":46295,"type":"direct-tcp-v1"},
693 {
694 "type": "relay-v1",
695 "hints": [
696 {"type": "direct-tcp-v1", "hostname": "magic-wormhole-transit.debian.net", "port": 4001}
697 ],
698 "name": null
699 }
700 ],
701 }
702 })
703 );
704 }
705
706 #[test]
707 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
708 fn test_message() {
709 let m1 = PeerMessage::offer_message_v1("hello from rust");
710 assert_eq!(
711 serde_json::json!(m1).to_string(),
712 "{\"offer\":{\"message\":\"hello from rust\"}}"
713 );
714 }
715
716 #[test]
717 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
718 fn test_offer_file() {
719 let f1 = PeerMessage::offer_file_v1("somefile.txt", 34556);
720 assert_eq!(
721 serde_json::json!(f1).to_string(),
722 "{\"offer\":{\"file\":{\"filename\":\"somefile.txt\",\"filesize\":34556}}}"
723 );
724 }
725
726 #[test]
727 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
728 fn test_offer_directory() {
729 let d1 = PeerMessage::offer_directory_v1("somedirectory", "zipped", 45, 1234, 10);
730 assert_eq!(
731 serde_json::json!(d1).to_string(),
732 "{\"offer\":{\"directory\":{\"dirname\":\"somedirectory\",\"mode\":\"zipped\",\"numbytes\":1234,\"numfiles\":10,\"zipsize\":45}}}"
733 );
734 }
735
736 #[test]
737 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
738 fn test_message_ack() {
739 let m1 = PeerMessage::message_ack_v1("ok");
740 assert_eq!(
741 serde_json::json!(m1).to_string(),
742 "{\"answer\":{\"message_ack\":\"ok\"}}"
743 );
744 }
745
746 #[test]
747 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
748 fn test_file_ack() {
749 let f1 = PeerMessage::file_ack_v1("ok");
750 assert_eq!(
751 serde_json::json!(f1).to_string(),
752 "{\"answer\":{\"file_ack\":\"ok\"}}"
753 );
754 }
755}