diff --git a/iroh/examples/dump-blob-fsm.rs b/iroh/examples/dump-blob-fsm.rs index 93e270467e..d36cb79945 100644 --- a/iroh/examples/dump-blob-fsm.rs +++ b/iroh/examples/dump-blob-fsm.rs @@ -7,9 +7,8 @@ //! copy that ticket value (the long string after `--ticket`) & feed it to this example: //! $ cargo run --example dump-blob-fsm use std::env::args; -use std::str::FromStr; -use iroh::dial::Ticket; +use iroh::ticket::blob::Ticket; use iroh_bytes::get::fsm::{ConnectedNext, EndBlobNext}; use iroh_bytes::protocol::GetRequest; use iroh_io::ConcatenateSliceWriter; @@ -29,8 +28,7 @@ pub fn setup_logging() { async fn main() -> anyhow::Result<()> { setup_logging(); - let ticket = args().nth(1).expect("missing ticket"); - let ticket = Ticket::from_str(&ticket)?; + let ticket: Ticket = args().nth(1).expect("missing ticket").parse()?; // generate a transient secretkey for this connection // diff --git a/iroh/examples/dump-blob-stream.rs b/iroh/examples/dump-blob-stream.rs index 05ede6e538..51f5a8467a 100644 --- a/iroh/examples/dump-blob-stream.rs +++ b/iroh/examples/dump-blob-stream.rs @@ -8,14 +8,13 @@ //! $ cargo run --example dump-blob-stream use std::env::args; use std::io; -use std::str::FromStr; use bao_tree::io::fsm::BaoContentItem; use bytes::Bytes; use futures::{Stream, StreamExt}; use genawaiter::sync::Co; use genawaiter::sync::Gen; -use iroh::dial::Ticket; +use iroh::ticket::blob::Ticket; use iroh_bytes::get::fsm::{AtInitial, BlobContentNext, ConnectedNext, EndBlobNext}; use iroh_bytes::protocol::GetRequest; use iroh_net::key::SecretKey; @@ -166,8 +165,7 @@ fn stream_children(initial: AtInitial) -> impl Stream> async fn main() -> anyhow::Result<()> { setup_logging(); - let ticket = args().nth(1).expect("missing ticket"); - let ticket = Ticket::from_str(&ticket)?; + let ticket: Ticket = args().nth(1).expect("missing ticket").parse()?; // generate a transient secret key for this connection // diff --git a/iroh/src/commands.rs b/iroh/src/commands.rs index f3f4399db5..79a28d25e7 100644 --- a/iroh/src/commands.rs +++ b/iroh/src/commands.rs @@ -15,8 +15,8 @@ use indicatif::{ ProgressStyle, }; use iroh::client::quic::Iroh; -use iroh::dial::Ticket; use iroh::rpc_protocol::*; +use iroh::ticket::blob::Ticket; use iroh_bytes::{protocol::RequestToken, util::runtime, BlobFormat, Hash, Tag}; use iroh_net::magicsock::DirectAddrInfo; use iroh_net::PeerAddr; diff --git a/iroh/src/commands/add.rs b/iroh/src/commands/add.rs index d8bf070a61..12dbaa97b2 100644 --- a/iroh/src/commands/add.rs +++ b/iroh/src/commands/add.rs @@ -9,8 +9,8 @@ use futures::{Stream, StreamExt}; use indicatif::{HumanBytes, MultiProgress, ProgressBar, ProgressStyle}; use iroh::{ client::Iroh, - dial::Ticket, rpc_protocol::{ProviderService, SetTagOption, WrapOption}, + ticket::blob::Ticket, }; use iroh_bytes::{ protocol::RequestToken, diff --git a/iroh/src/commands/doctor.rs b/iroh/src/commands/doctor.rs index ba36c6990c..54853e4570 100644 --- a/iroh/src/commands/doctor.rs +++ b/iroh/src/commands/doctor.rs @@ -4,13 +4,14 @@ use std::{ collections::HashMap, net::SocketAddr, num::NonZeroU16, + str::FromStr, sync::Arc, time::{Duration, Instant}, }; use crate::config::{path_with_env, NodeConfig}; -use anyhow::Context; +use anyhow::{anyhow, Context}; use clap::Subcommand; use indicatif::{HumanBytes, MultiProgress, ProgressBar}; use iroh::util::{path::IrohPaths, progress::ProgressWriter}; @@ -148,6 +149,8 @@ pub enum Commands { #[clap(long, default_value_t = 5)] count: usize, }, + /// Inspect a ticket. + TicketInspect { ticket: String }, } #[derive(Debug, Serialize, Deserialize, MaxSize)] @@ -913,6 +916,28 @@ fn create_secret_key(secret_key: SecretKeyOption) -> anyhow::Result { }) } +fn inspect_ticket(ticket: &str) -> anyhow::Result<()> { + let (kind, _) = iroh::ticket::Kind::parse_prefix(ticket) + .ok_or_else(|| anyhow!("missing ticket prefix"))??; + match kind { + iroh::ticket::Kind::Blob => { + let ticket = iroh::ticket::blob::Ticket::from_str(ticket) + .context("failed parsing blob ticket")?; + println!("Blob ticket:\n{ticket:#?}"); + } + iroh::ticket::Kind::Doc => { + let ticket = + iroh::ticket::doc::Ticket::from_str(ticket).context("failed parsing doc ticket")?; + println!("Document ticket:\n{ticket:#?}"); + } + iroh::ticket::Kind::Node => { + println!("node tickets are yet to be implemented :)"); + } + } + + Ok(()) +} + pub async fn run(command: Commands, config: &NodeConfig) -> anyhow::Result<()> { match command { Commands::Report { @@ -971,5 +996,6 @@ pub async fn run(command: Commands, config: &NodeConfig) -> anyhow::Result<()> { let config = NodeConfig::from_env(None)?; derp_regions(count, config).await } + Commands::TicketInspect { ticket } => inspect_ticket(&ticket), } } diff --git a/iroh/src/dial.rs b/iroh/src/dial.rs index 925aee562d..75f1001b9d 100644 --- a/iroh/src/dial.rs +++ b/iroh/src/dial.rs @@ -1,18 +1,9 @@ -//! The ticket type for the provider. -//! -//! This is in it's own module to enforce the invariant that you can not construct a ticket -//! with an empty address list. +//! Utilities to dial a node. -use std::fmt::{self, Display}; -use std::str::FromStr; - -use anyhow::{ensure, Context, Result}; -use iroh_bytes::protocol::RequestToken; -use iroh_bytes::{BlobFormat, Hash}; +use anyhow::Context; use iroh_net::derp::{DerpMap, DerpMode}; use iroh_net::key::SecretKey; use iroh_net::PeerAddr; -use serde::{Deserialize, Serialize}; /// Options for the client #[derive(Clone, Debug)] @@ -47,206 +38,3 @@ pub async fn dial(opts: Options) -> anyhow::Result { .await .context("failed to connect to provider") } - -/// A token containing everything to get a file from the provider. -/// -/// It is a single item which can be easily serialized and deserialized. The [`Display`] -/// and [`FromStr`] implementations serialize to base32. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Ticket { - /// The provider to get a file from. - peer: PeerAddr, - /// The format of the blob. - format: BlobFormat, - /// The hash to retrieve. - hash: Hash, - /// Optional Request token. - token: Option, -} - -impl Ticket { - /// Creates a new ticket. - pub fn new( - peer: PeerAddr, - hash: Hash, - format: BlobFormat, - token: Option, - ) -> Result { - ensure!( - !peer.info.direct_addresses.is_empty(), - "addrs list can not be empty" - ); - Ok(Self { - hash, - format, - peer, - token, - }) - } - - /// Deserializes from bytes. - pub fn from_bytes(bytes: &[u8]) -> Result { - let slf: Ticket = postcard::from_bytes(bytes)?; - ensure!( - !slf.peer.info.direct_addresses.is_empty(), - "Invalid address list in ticket" - ); - Ok(slf) - } - - /// Serializes to bytes. - pub fn to_bytes(&self) -> Vec { - postcard::to_stdvec(self).expect("postcard::to_stdvec is infallible") - } - - /// The hash of the item this ticket can retrieve. - pub fn hash(&self) -> Hash { - self.hash - } - - /// The [`PeerAddr`] of the provider for this ticket. - pub fn node_addr(&self) -> &PeerAddr { - &self.peer - } - - /// The [`RequestToken`] for this ticket. - pub fn token(&self) -> Option<&RequestToken> { - self.token.as_ref() - } - - /// The [`BlobFormat`] for this ticket. - pub fn format(&self) -> BlobFormat { - self.format - } - - /// Set the [`RequestToken`] for this ticket. - pub fn with_token(self, token: Option) -> Self { - Self { token, ..self } - } - - /// True if the ticket is for a collection and should retrieve all blobs in it. - pub fn recursive(&self) -> bool { - self.format.is_hash_seq() - } - - /// Get the contents of the ticket, consuming it. - pub fn into_parts(self) -> (PeerAddr, Hash, BlobFormat, Option) { - let Ticket { - peer, - hash, - format, - token, - } = self; - (peer, hash, format, token) - } - - /// Convert this ticket into a [`Options`], adding the given secret key. - pub fn as_get_options(&self, secret_key: SecretKey, derp_map: Option) -> Options { - Options { - peer: self.peer.clone(), - secret_key, - keylog: true, - derp_map, - } - } -} - -/// Serializes to base32. -impl Display for Ticket { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let encoded = self.to_bytes(); - let mut text = data_encoding::BASE32_NOPAD.encode(&encoded); - text.make_ascii_lowercase(); - write!(f, "{text}") - } -} - -/// Deserializes from base32. -impl FromStr for Ticket { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let bytes = data_encoding::BASE32_NOPAD.decode(s.to_ascii_uppercase().as_bytes())?; - let slf = Self::from_bytes(&bytes)?; - Ok(slf) - } -} - -impl Serialize for Ticket { - fn serialize(&self, serializer: S) -> Result { - if serializer.is_human_readable() { - serializer.serialize_str(&self.to_string()) - } else { - let Ticket { - peer, - format, - hash, - token, - } = self; - (peer, format, hash, token).serialize(serializer) - } - } -} - -impl<'de> Deserialize<'de> for Ticket { - fn deserialize>(deserializer: D) -> Result { - if deserializer.is_human_readable() { - let s = String::deserialize(deserializer)?; - Self::from_str(&s).map_err(serde::de::Error::custom) - } else { - let (peer, format, hash, token) = Deserialize::deserialize(deserializer)?; - Self::new(peer, hash, format, token).map_err(serde::de::Error::custom) - } - } -} - -#[cfg(test)] -mod tests { - use std::net::SocketAddr; - - use bao_tree::blake3; - - use super::*; - - fn make_ticket() -> Ticket { - let hash = blake3::hash(b"hi there"); - let hash = Hash::from(hash); - let peer = SecretKey::generate().public(); - let addr = SocketAddr::from_str("127.0.0.1:1234").unwrap(); - let token = RequestToken::new(vec![1, 2, 3, 4, 5, 6]).unwrap(); - let derp_region = Some(0); - Ticket { - hash, - peer: PeerAddr::from_parts(peer, derp_region, vec![addr]), - token: Some(token), - format: BlobFormat::HashSeq, - } - } - - #[test] - fn test_ticket_postcard() { - let ticket = make_ticket(); - let bytes = postcard::to_stdvec(&ticket).unwrap(); - let ticket2: Ticket = postcard::from_bytes(&bytes).unwrap(); - assert_eq!(ticket2, ticket); - } - - #[test] - fn test_ticket_json() { - let ticket = make_ticket(); - let json = serde_json::to_string(&ticket).unwrap(); - let ticket2: Ticket = serde_json::from_str(&json).unwrap(); - assert_eq!(ticket2, ticket); - } - - #[test] - fn test_ticket_base32_roundtrip() { - let ticket = make_ticket(); - let base32 = ticket.to_string(); - println!("Ticket: {base32}"); - println!("{} bytes", base32.len()); - - let ticket2: Ticket = base32.parse().unwrap(); - assert_eq!(ticket2, ticket); - } -} diff --git a/iroh/src/lib.rs b/iroh/src/lib.rs index 4230c9e702..77a120e81e 100644 --- a/iroh/src/lib.rs +++ b/iroh/src/lib.rs @@ -13,6 +13,7 @@ pub mod get; pub mod node; pub mod rpc_protocol; pub mod sync_engine; +pub mod ticket; pub mod util; /// Expose metrics module diff --git a/iroh/src/node.rs b/iroh/src/node.rs index cb2313b849..c9c8789c52 100644 --- a/iroh/src/node.rs +++ b/iroh/src/node.rs @@ -54,7 +54,6 @@ use tokio::task::JoinError; use tokio_util::sync::CancellationToken; use tracing::{debug, error, error_span, info, trace, warn, Instrument}; -use crate::dial::Ticket; use crate::downloader::Downloader; use crate::rpc_protocol::{ BlobAddPathRequest, BlobAddPathResponse, BlobAddStreamRequest, BlobAddStreamResponse, @@ -68,6 +67,7 @@ use crate::rpc_protocol::{ ProviderResponse, ProviderService, SetTagOption, }; use crate::sync_engine::{SyncEngine, SYNC_ALPN}; +use crate::ticket::blob::Ticket; const MAX_CONNECTIONS: u32 = 1024; const MAX_STREAMS: u64 = 10; diff --git a/iroh/src/rpc_protocol.rs b/iroh/src/rpc_protocol.rs index 7bd4093ca1..d77bd855c4 100644 --- a/iroh/src/rpc_protocol.rs +++ b/iroh/src/rpc_protocol.rs @@ -7,13 +7,12 @@ //! response, while others like provide have a stream of responses. //! //! Note that this is subject to change. The RPC protocol is not yet stable. -use std::{collections::HashMap, fmt, net::SocketAddr, path::PathBuf, str::FromStr}; +use std::{collections::HashMap, net::SocketAddr, path::PathBuf}; use bytes::Bytes; use derive_more::{From, TryInto}; use iroh_bytes::util::{BlobFormat, Tag}; pub use iroh_bytes::{protocol::RequestToken, provider::DownloadProgress, Hash}; -use iroh_gossip::proto::util::base32; use iroh_net::{ key::PublicKey, magic_endpoint::{ConnectionInfo, PeerAddr}, @@ -34,6 +33,7 @@ use serde::{Deserialize, Serialize}; pub use iroh_bytes::{provider::AddProgress, store::ValidateProgress, util::RpcResult}; use crate::sync_engine::LiveEvent; +pub use crate::ticket::doc::Ticket as DocTicket; /// A 32-byte key or token pub type KeyBytes = [u8; 32]; @@ -495,46 +495,6 @@ pub struct DocCreateResponse { pub id: NamespaceId, } -/// Contains both a key (either secret or public) to a document, and a list of peers to join. -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct DocTicket { - /// either a public or private key - pub key: KeyBytes, - /// a list of peers - pub peers: Vec, -} -impl DocTicket { - /// Create a new doc ticket - pub fn new(key: KeyBytes, peers: Vec) -> Self { - Self { key, peers } - } - /// Serialize the ticket to a byte array. - pub fn to_bytes(&self) -> anyhow::Result> { - let bytes = postcard::to_stdvec(&self)?; - Ok(bytes) - } - /// Parse ticket from a byte array. - pub fn from_bytes(bytes: &[u8]) -> anyhow::Result { - let slf = postcard::from_bytes(bytes)?; - Ok(slf) - } -} -impl FromStr for DocTicket { - type Err = anyhow::Error; - fn from_str(s: &str) -> Result { - Self::from_bytes(&base32::parse_vec(s)?) - } -} -impl fmt::Display for DocTicket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - base32::fmt(self.to_bytes().expect("failed to serialize")) - ) - } -} - /// Import a document from a ticket. #[derive(Serialize, Deserialize, Debug)] pub struct DocImportRequest(pub DocTicket); diff --git a/iroh/src/sync_engine/rpc.rs b/iroh/src/sync_engine/rpc.rs index e7e647cf08..e8ef782908 100644 --- a/iroh/src/sync_engine/rpc.rs +++ b/iroh/src/sync_engine/rpc.rs @@ -115,7 +115,7 @@ impl SyncEngine { self.start_sync(req.doc_id, vec![]).await?; Ok(DocShareResponse(DocTicket { key, - peers: vec![me], + nodes: vec![me], })) } @@ -131,7 +131,7 @@ impl SyncEngine { } pub async fn doc_import(&self, req: DocImportRequest) -> RpcResult { - let DocImportRequest(DocTicket { key, peers }) = req; + let DocImportRequest(DocTicket { key, nodes: peers }) = req; let namespace = Namespace::from_bytes(&key); let doc_id = self.sync.import_namespace(namespace).await?; self.sync.open(doc_id, Default::default()).await?; diff --git a/iroh/src/ticket.rs b/iroh/src/ticket.rs new file mode 100644 index 0000000000..4784b530db --- /dev/null +++ b/iroh/src/ticket.rs @@ -0,0 +1,105 @@ +//! This module manages the different tickets Iroh has. + +pub mod blob; +pub mod doc; + +const PREFIX_SEPARATOR: char = ':'; + +/// Kind of ticket. +#[derive(Debug, strum::EnumString, strum::Display, PartialEq, Eq, Clone, Copy)] +#[strum(serialize_all = "snake_case")] +pub enum Kind { + /// A blob ticket. + Blob, + /// A document ticket. + Doc, + /// A ticket for an Iroh node. + Node, +} + +impl Kind { + /// Parse the ticket prefix to obtain the [`Kind`] and remainig string. + pub fn parse_prefix(s: &str) -> Option> { + let (prefix, rest) = s.split_once(PREFIX_SEPARATOR)?; + match prefix.parse() { + Ok(kind) => Some(Ok((kind, rest))), + Err(e) => Some(Err(e.into())), + } + } +} + +/// An error deserializing an iroh ticket. +#[derive(Debug, derive_more::Display, thiserror::Error)] +pub enum Error { + /// Found a ticket of the wrong [`Kind`]. + #[display("expected a {expected} ticket but found {found}")] + WrongKind { + /// Expected [`Kind`] of ticket. + expected: Kind, + /// Found [`Kind`] of ticket. + found: Kind, + }, + /// It appears to be a ticket but the prefix is not a known one. + #[display("unrecogized ticket prefix")] + UnrecognizedKind(#[from] strum::ParseError), + /// This does not appear to be a ticket. + #[display("not a {expected} ticket")] + MissingKind { + /// Prefix that is missing. + expected: Kind, + }, + /// This looks like a ticket, but postcard deserialization failed. + #[display("deserialization failed: {_0}")] + Postcard(#[from] postcard::Error), + /// This looks like a ticket, but base32 decoding failed. + #[display("decoding failed: {_0}")] + Encoding(#[from] data_encoding::DecodeError), + /// Verification of the deserialized bytes failed. + #[display("verification failed: {_0}")] + Verify(&'static str), +} + +trait IrohTicket: serde::Serialize + for<'de> serde::Deserialize<'de> { + /// Kind of Iroh ticket. + const KIND: Kind; + + /// Serialize to postcard bytes. + fn to_bytes(&self) -> Vec { + postcard::to_stdvec(&self).expect("postcard::to_stdvec is infallible") + } + + /// Deserialize from postcard bytes. + fn from_bytes(bytes: &[u8]) -> Result { + let ticket: Self = postcard::from_bytes(bytes)?; + ticket.verify().map_err(Error::Verify)?; + Ok(ticket) + } + + /// Verify this ticket. + fn verify(&self) -> Result<(), &'static str> { + Ok(()) + } + + /// Serialize to string. + fn serialize(&self) -> String { + let mut out = Self::KIND.to_string(); + out.push(PREFIX_SEPARATOR); + let bytes = self.to_bytes(); + data_encoding::BASE32_NOPAD.encode_append(&bytes, &mut out); + out.make_ascii_lowercase(); + out + } + + /// Deserialize from a string. + fn deserialize(str: &str) -> Result { + let expected = Self::KIND; + let (found, bytes) = Kind::parse_prefix(str).ok_or(Error::MissingKind { expected })??; + if expected != found { + return Err(Error::WrongKind { expected, found }); + } + let bytes = bytes.to_ascii_uppercase(); + let bytes = data_encoding::BASE32_NOPAD.decode(bytes.as_bytes())?; + let ticket = Self::from_bytes(&bytes)?; + Ok(ticket) + } +} diff --git a/iroh/src/ticket/blob.rs b/iroh/src/ticket/blob.rs new file mode 100644 index 0000000000..5cbde7f09e --- /dev/null +++ b/iroh/src/ticket/blob.rs @@ -0,0 +1,198 @@ +//! Tickets for blobs. + +use std::str::FromStr; + +use anyhow::{ensure, Result}; +use iroh_bytes::{protocol::RequestToken, BlobFormat, Hash}; +use iroh_net::{derp::DerpMap, key::SecretKey, PeerAddr}; +use serde::{Deserialize, Serialize}; + +use crate::dial::Options; + +use super::*; + +/// A token containing everything to get a file from the provider. +/// +/// It is a single item which can be easily serialized and deserialized. +#[derive(Debug, Clone, PartialEq, Eq, derive_more::Display)] +#[display("{}", IrohTicket::serialize(self))] +pub struct Ticket { + /// The provider to get a file from. + node: PeerAddr, + /// The format of the blob. + format: BlobFormat, + /// The hash to retrieve. + hash: Hash, + /// Optional Request token. + token: Option, +} + +impl IrohTicket for Ticket { + const KIND: Kind = Kind::Blob; + + fn verify(&self) -> std::result::Result<(), &'static str> { + if self.node.info.direct_addresses.is_empty() { + return Err("Invalid address list in ticket"); + } + Ok(()) + } +} + +impl FromStr for Ticket { + type Err = Error; + + fn from_str(s: &str) -> Result { + IrohTicket::deserialize(s) + } +} + +impl Ticket { + /// Creates a new ticket. + pub fn new( + peer: PeerAddr, + hash: Hash, + format: BlobFormat, + token: Option, + ) -> Result { + ensure!( + !peer.info.direct_addresses.is_empty(), + "addrs list can not be empty" + ); + Ok(Self { + hash, + format, + node: peer, + token, + }) + } + + /// The hash of the item this ticket can retrieve. + pub fn hash(&self) -> Hash { + self.hash + } + + /// The [`PeerAddr`] of the provider for this ticket. + pub fn node_addr(&self) -> &PeerAddr { + &self.node + } + + /// The [`RequestToken`] for this ticket. + pub fn token(&self) -> Option<&RequestToken> { + self.token.as_ref() + } + + /// The [`BlobFormat`] for this ticket. + pub fn format(&self) -> BlobFormat { + self.format + } + + /// Set the [`RequestToken`] for this ticket. + pub fn with_token(self, token: Option) -> Self { + Self { token, ..self } + } + + /// True if the ticket is for a collection and should retrieve all blobs in it. + pub fn recursive(&self) -> bool { + self.format.is_hash_seq() + } + + /// Get the contents of the ticket, consuming it. + pub fn into_parts(self) -> (PeerAddr, Hash, BlobFormat, Option) { + let Ticket { + node: peer, + hash, + format, + token, + } = self; + (peer, hash, format, token) + } + + /// Convert this ticket into a [`Options`], adding the given secret key. + pub fn as_get_options(&self, secret_key: SecretKey, derp_map: Option) -> Options { + Options { + peer: self.node.clone(), + secret_key, + keylog: true, + derp_map, + } + } +} + +impl Serialize for Ticket { + fn serialize(&self, serializer: S) -> Result { + if serializer.is_human_readable() { + serializer.serialize_str(&self.to_string()) + } else { + let Ticket { + node, + format, + hash, + token, + } = self; + (node, format, hash, token).serialize(serializer) + } + } +} + +impl<'de> Deserialize<'de> for Ticket { + fn deserialize>(deserializer: D) -> Result { + if deserializer.is_human_readable() { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(serde::de::Error::custom) + } else { + let (peer, format, hash, token) = Deserialize::deserialize(deserializer)?; + Self::new(peer, hash, format, token).map_err(serde::de::Error::custom) + } + } +} + +#[cfg(test)] +mod tests { + use std::net::SocketAddr; + + use bao_tree::blake3; + + use super::*; + + fn make_ticket() -> Ticket { + let hash = blake3::hash(b"hi there"); + let hash = Hash::from(hash); + let peer = SecretKey::generate().public(); + let addr = SocketAddr::from_str("127.0.0.1:1234").unwrap(); + let token = RequestToken::new(vec![1, 2, 3, 4, 5, 6]).unwrap(); + let derp_region = Some(0); + Ticket { + hash, + node: PeerAddr::from_parts(peer, derp_region, vec![addr]), + token: Some(token), + format: BlobFormat::HashSeq, + } + } + + #[test] + fn test_ticket_postcard() { + let ticket = make_ticket(); + let bytes = postcard::to_stdvec(&ticket).unwrap(); + let ticket2: Ticket = postcard::from_bytes(&bytes).unwrap(); + assert_eq!(ticket2, ticket); + } + + #[test] + fn test_ticket_json() { + let ticket = make_ticket(); + let json = serde_json::to_string(&ticket).unwrap(); + let ticket2: Ticket = serde_json::from_str(&json).unwrap(); + assert_eq!(ticket2, ticket); + } + + #[test] + fn test_ticket_base32_roundtrip() { + let ticket = make_ticket(); + let base32 = ticket.to_string(); + println!("Ticket: {base32}"); + println!("{} bytes", base32.len()); + + let ticket2: Ticket = base32.parse().unwrap(); + assert_eq!(ticket2, ticket); + } +} diff --git a/iroh/src/ticket/doc.rs b/iroh/src/ticket/doc.rs new file mode 100644 index 0000000000..85a8c8c643 --- /dev/null +++ b/iroh/src/ticket/doc.rs @@ -0,0 +1,36 @@ +//! Tickets for [`iroh-sync`] documents. + +use iroh_net::PeerAddr; +use serde::{Deserialize, Serialize}; + +use crate::rpc_protocol::KeyBytes; + +use super::*; + +/// Contains both a key (either secret or public) to a document, and a list of peers to join. +#[derive(Serialize, Deserialize, Clone, Debug, derive_more::Display)] +#[display("{}", IrohTicket::serialize(self))] +pub struct Ticket { + /// either a public or private key + pub key: KeyBytes, + /// A list of nodes to contact. + pub nodes: Vec, +} + +impl IrohTicket for Ticket { + const KIND: Kind = Kind::Doc; +} + +impl Ticket { + /// Create a new doc ticket + pub fn new(key: KeyBytes, peers: Vec) -> Self { + Self { key, nodes: peers } + } +} + +impl std::str::FromStr for Ticket { + type Err = Error; + fn from_str(s: &str) -> Result { + IrohTicket::deserialize(s) + } +} diff --git a/iroh/tests/cli.rs b/iroh/tests/cli.rs index 190d8c6e52..9dd7f94c4f 100644 --- a/iroh/tests/cli.rs +++ b/iroh/tests/cli.rs @@ -11,7 +11,7 @@ use anyhow::{Context, Result}; use bao_tree::blake3; use duct::{cmd, ReaderHandle}; use iroh::bytes::Hash; -use iroh::dial::Ticket; +use iroh::ticket::blob::Ticket; use rand::{Rng, RngCore, SeedableRng}; use regex::Regex; use testdir::testdir; @@ -748,7 +748,7 @@ fn match_provide_output(reader: T, num_blobs: usize) -> Result (r"Total: [_\w\d-]*", 1), (r"", 1), (r"Collection: [\da-z]{59}", 1), - (r"All-in-one ticket: ([_a-zA-Z\d-]*)", 1), + (r"All-in-one ticket: ([_a-zA-Z:\d-]*)", 1), ], ); diff --git a/iroh/tests/sync.rs b/iroh/tests/sync.rs index 8174f7993d..3553e91359 100644 --- a/iroh/tests/sync.rs +++ b/iroh/tests/sync.rs @@ -176,8 +176,8 @@ async fn sync_gossip_bulk() -> Result<()> { let doc0 = clients[0].docs.create().await?; let mut ticket = doc0.share(ShareMode::Write).await?; // unset peers to not yet start sync - let peers = ticket.peers.clone(); - ticket.peers = vec![]; + let peers = ticket.nodes.clone(); + ticket.nodes = vec![]; let doc1 = clients[1].docs.import(ticket).await?; let mut events = doc1.subscribe().await?; @@ -504,8 +504,8 @@ async fn sync_big() -> Result<()> { let doc0 = clients[0].docs.create().await?; let mut ticket = doc0.share(ShareMode::Write).await?; // do not join for now, just import without any peer info - let peer0 = ticket.peers[0].clone(); - ticket.peers = vec![]; + let peer0 = ticket.nodes[0].clone(); + ticket.nodes = vec![]; let mut docs = vec![]; docs.push(doc0);