diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..424f361 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: Cargo Build & Test + +on: + push: + pull_request: + +env: + CARGO_TERM_COLOR: always + +jobs: + build_and_test: + name: Rust project - latest + runs-on: ubuntu-latest + strategy: + matrix: + toolchain: + - stable + - beta + - nightly + steps: + - uses: actions/checkout@v3 + - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} + - run: cargo build --verbose + - run: cargo test --verbose \ No newline at end of file diff --git a/README.md b/README.md index f097528..c41edc3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # gday Work in progress. +TODO \ No newline at end of file diff --git a/gday/src/transfer.rs b/gday/src/transfer.rs index 389e34e..2cbdac1 100644 --- a/gday/src/transfer.rs +++ b/gday/src/transfer.rs @@ -7,7 +7,7 @@ use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write}; const FILE_BUFFER_SIZE: usize = 1_000_000; -/// Wrap a [`TcpStream`] in a [`gday_encryption::EncryptedStream`]. +/// Wrap a [`std::net::TcpStream`] in a [`gday_encryption::EncryptedStream`]. pub fn encrypt_connection( mut io_stream: T, shared_key: &[u8; 32], diff --git a/gday_contact_exchange_protocol/src/lib.rs b/gday_contact_exchange_protocol/src/lib.rs index 4c9153d..6e2db68 100644 --- a/gday_contact_exchange_protocol/src/lib.rs +++ b/gday_contact_exchange_protocol/src/lib.rs @@ -6,13 +6,13 @@ //! //! Using this protocol goes something like this: //! -//! 1. `peer A` connects to a server via the internet +//! 1. Peer A connects to a server via the internet //! and requests a new room with `room_code` using [`ClientMsg::CreateRoom`]. //! -//! 2. The server replies to `peer A` with [`ServerMsg::RoomCreated`] or [`ServerMsg::ErrorRoomTaken`] +//! 2. The server replies to peer A with [`ServerMsg::RoomCreated`] or [`ServerMsg::ErrorRoomTaken`] //! depending on if this `room_code` is in use. //! -//! 3. `peer A` externally tells `peer B` their `room_code` (by phone call, text message, carrier pigeon, etc.). +//! 3. Peer A externally tells peer B their `room_code` (by phone call, text message, carrier pigeon, etc.). //! //! 4. Both peers send this `room_code` and optionally their local/private socket addresses to the server //! via [`ClientMsg::SendAddr`] messages. The server determines their public addresses from the internet connections. @@ -29,7 +29,6 @@ //! 8. On their own, the peers use this info to connect directly to each other by using //! [hole punching](https://en.wikipedia.org/wiki/Hole_punching_(networking)). //! -//! # #![forbid(unsafe_code)] #![warn(clippy::all)] @@ -205,8 +204,11 @@ pub fn serialize_into(msg: impl Serialize, writer: &mut impl Write) -> Result<() Ok(()) } -/// Write `msg` to `writer` using [`postcard`]. -/// Prefixes the message with a byte that holds its length. +/// Read `msg` from `reader` using [`postcard`]. +/// Assumes the message is prefixed with a byte that holds its length. +/// +/// `buf` is a buffer that's used for desrialization. It's recommended to be +/// [`MAX_MSG_SIZE`] bytes long. pub fn deserialize_from<'a, T: Deserialize<'a>>( reader: &mut impl Read, buf: &'a mut [u8], @@ -233,8 +235,11 @@ pub async fn serialize_into_async( Ok(()) } -/// Asynchronously write `msg` to `writer` using [`postcard`]. -/// Prefixes the message with a byte that holds its length. +/// Asynchronously read `msg` from `reader` using [`postcard`]. +/// Assumes the message is prefixed with a byte that holds its length. +/// +/// `buf` is a buffer that's used for desrialization. It's recommended to be +/// [`MAX_MSG_SIZE`] bytes long. pub async fn deserialize_from_async<'a, T: Deserialize<'a>>( reader: &mut (impl AsyncRead + Unpin), buf: &'a mut [u8], @@ -246,7 +251,7 @@ pub async fn deserialize_from_async<'a, T: Deserialize<'a>>( Ok(from_bytes(&buf[0..len])?) } -/// Error from [`Messenger`]. +/// Message serialization/deserialization error #[derive(Error, Debug)] #[non_exhaustive] pub enum Error { diff --git a/gday_file_offer_protocol/src/lib.rs b/gday_file_offer_protocol/src/lib.rs index 7f1c1d8..243cb19 100644 --- a/gday_file_offer_protocol/src/lib.rs +++ b/gday_file_offer_protocol/src/lib.rs @@ -1,4 +1,19 @@ -//! TODO +//! This protocol lets one user offer to send some files, +//! and the other user respond with the files it wants to receive. +//! +//! On it's own, this crate doesn't do anything other than define a shared protocol, and functions to +//! send and receive messages of this protocol. +//! +//! # Process +//! +//! Using this protocol goes something like this: +//! +//! 1. Peer A sends [`FileOfferMsg`] to Peer B, containing a [`Vec`] of metadata about +//! files it offers to send. +//! +//! 2. Peer B sends [`FileResponseMsg`] to Peer A, containing a [`Vec`] of [`Option`] indicating +//! how much of each file to send. +//! #![forbid(unsafe_code)] #![warn(clippy::all)] @@ -12,7 +27,7 @@ use std::{ }; use thiserror::Error; -/// Information about an offered file +/// Information about an offered file. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileMeta { /// The file path offered @@ -28,11 +43,11 @@ impl FileMeta { Ok(std::env::current_dir()?.join(&self.short_path)) } - /// Return version of [`Self::get_save_path()`] + /// Returns a version of [`Self::get_save_path()`] /// that doesn't exist in the filesystem yet. /// /// If [`Self::get_save_path()`] already exists, suffixes its file stem with - /// `(1)`, `(2)`, ..., `(99)` until a free path is found. If all of + /// ` (1)`, ` (2)`, ..., ` (99)` until a free path is found. If all of /// these are occupied, returns [`Error::FilenameOccupied`]. pub fn get_unused_save_path(&self) -> Result { let plain_path = self.get_save_path()?; @@ -108,6 +123,7 @@ fn add_suffix_to_file_stem(path: &mut PathBuf, suffix: &OsStr) -> std::io::Resul } impl From for FileMeta { + /// Converts a [`FileMetaLocal`] into a [`FileMeta`]. fn from(other: FileMetaLocal) -> Self { Self { short_path: other.short_path, @@ -116,7 +132,7 @@ impl From for FileMeta { } } -/// Information about a local file +/// Information about a locally stored file #[derive(Debug, Clone)] pub struct FileMetaLocal { /// The path that will be offered to the peer @@ -143,21 +159,17 @@ impl std::hash::Hash for FileMetaLocal { } } -/// At the start of peer to peer communication, -/// the creator peer sends this message. -/// -/// Optinonally, they can offer to transmit files -/// by sending some Vec of their metadatas. In that case, -/// the other peer will reply with [`FileResponseMsg`]. +/// A list of file metadatas that this peer is offering +/// to send. The other peer should reply with [`FileResponseMsg`]. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileOfferMsg { pub files: Vec, } -/// This message responds to [`FileOfferMsg`]. +/// The receiving peer should reply with this message to [`FileOfferMsg`]. /// /// Specifies which of the offered files the other peer -/// should transmit. +/// should send. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileResponseMsg { /// The accepted files. `Some(start_byte)` element accepts the offered @@ -166,7 +178,8 @@ pub struct FileResponseMsg { pub accepted: Vec>, } -/// TODO +/// Write `msg` to `writer` using [`postcard`]. +/// Prefixes the message with 4 big-endian bytes that hold its length. pub fn serialize_into(msg: impl Serialize, writer: &mut impl Write) -> Result<(), Error> { // leave space for the message length let buf = vec![0_u8; 4]; @@ -184,7 +197,11 @@ pub fn serialize_into(msg: impl Serialize, writer: &mut impl Write) -> Result<() Ok(()) } -/// TODO +/// Read `msg` from `reader` using [`postcard`]. +/// Assumes the message is prefixed with a byte that holds its length. +/// +/// `buf` is a [`Vec`] that will be used as a buffer during deserialization. +/// It will be resized to match the length of the message. pub fn deserialize_from<'a, T: Deserialize<'a>>( reader: &mut impl Read, buf: &'a mut Vec, diff --git a/gday_hole_punch/src/contact_sharer.rs b/gday_hole_punch/src/contact_sharer.rs index 7d640ee..790aefb 100644 --- a/gday_hole_punch/src/contact_sharer.rs +++ b/gday_hole_punch/src/contact_sharer.rs @@ -3,7 +3,7 @@ use gday_contact_exchange_protocol::{ deserialize_from, serialize_into, ClientMsg, FullContact, ServerMsg, MAX_MSG_SIZE, }; -/// Used to exchange contact information with a peer via the `gday_server`. +/// Used to exchange socket addresses with a peer via a Gday server. pub struct ContactSharer { room_code: u64, is_creator: bool, @@ -11,14 +11,15 @@ pub struct ContactSharer { } impl ContactSharer { - /// Creates a new room in the `gday_server` that the given streams connect to. - /// Sends contact information to the server. + /// Creates a new room with `room_code` in the Gday server + /// that `server_connection` connects to. /// - /// Returns ( + /// Sends local socket addresses to the server + /// + /// Returns /// - The [`ContactSharer`] /// - The [`FullContact`] of this endpoint, as /// determined by the server - /// ) pub fn create_room( room_code: u64, mut server_connection: ServerConnection, @@ -47,16 +48,15 @@ impl ContactSharer { Ok((this, contact)) } - /// Joins a room in the `gday_server` that the given streams connect to. - /// `room_id` should be the code provided by the other peer who called `create_room`. - /// Sends contact information to the server. + /// Joins a room with `room_code` in the Gday server + /// that `server_connection` connects to. /// - /// Returns ( - /// - The `ContactSharer` - /// - The [`FullContact`] that the server returned. - /// Contains this user's public and private socket addresses. + /// Sends local socket addresses to the server /// - /// ) + /// Returns + /// - The [`ContactSharer`] + /// - The [`FullContact`] of this endpoint, as + /// determined by the server pub fn join_room( room_code: u64, server_connection: ServerConnection, @@ -111,10 +111,9 @@ impl ContactSharer { Ok(my_contact) } - /// Waits for the `gday_server` to send the contact information the - /// other peer submitted. - /// Then returns a [`HolePuncher`] which can be used to try establishing - /// an authenticated TCP connection with that peer. + /// Blocks until the Gday server sends the contact information the + /// other peer submitted. Returns the peer's [`FullContact`], as + /// determined by the server pub fn get_peer_contact(mut self) -> Result { let buf = &mut [0; MAX_MSG_SIZE]; let stream = &mut self.connection.streams()[0]; diff --git a/gday_hole_punch/src/hole_puncher.rs b/gday_hole_punch/src/hole_puncher.rs index e50e82f..93d1b3d 100644 --- a/gday_hole_punch/src/hole_puncher.rs +++ b/gday_hole_punch/src/hole_puncher.rs @@ -14,6 +14,17 @@ const RETRY_INTERVAL: Duration = Duration::from_millis(100); /// Tries to establish a TCP connection with the other peer by using /// [TCP hole punching](https://en.wikipedia.org/wiki/TCP_hole_punching). +/// +/// - `local_contact` should be the `private` field of your [`FullContact`] +/// that the [`crate::ContactSharer`] returned when you created or joined a room. +/// - `peer_contact` should be the [`FullContact`] returned by [`crate::ContactSharer::get_peer_contact()`]. +/// - `shared_secret` should be a secret that both peers know. It will be used to verify +/// the peer's identity, and derive a stronger shared key using [SPAKE2](https://docs.rs/spake2/latest/spake2/). +/// +/// Returns: +/// - A [`std::net::TcpStream`] to the other peer. +/// - A `[u8; 32]` shared key that was derived using +/// [SPAKE2](https://docs.rs/spake2/latest/spake2/) and the weaker `shared_secret`. pub fn try_connect_to_peer( local_contact: Contact, peer_contact: FullContact, @@ -23,25 +34,25 @@ pub fn try_connect_to_peer( let mut futs: Vec>>>> = Vec::with_capacity(6); - if let Some(local) = local_contact.v6 { + if let Some(local) = local_contact.v4 { futs.push(Box::pin(try_accept(local, p))); - if let Some(peer) = peer_contact.private.v6 { + if let Some(peer) = peer_contact.private.v4 { futs.push(Box::pin(try_connect(local, peer, p))); } - if let Some(peer) = peer_contact.public.v6 { + + if let Some(peer) = peer_contact.public.v4 { futs.push(Box::pin(try_connect(local, peer, p))); } } - if let Some(local) = local_contact.v4 { + if let Some(local) = local_contact.v6 { futs.push(Box::pin(try_accept(local, p))); - if let Some(peer) = peer_contact.private.v4 { + if let Some(peer) = peer_contact.private.v6 { futs.push(Box::pin(try_connect(local, peer, p))); } - - if let Some(peer) = peer_contact.public.v4 { + if let Some(peer) = peer_contact.public.v6 { futs.push(Box::pin(try_connect(local, peer, p))); } } @@ -91,7 +102,7 @@ async fn try_accept( } } -/// Uses [SPAKE 2](https://datatracker.ietf.org/doc/rfc9382/) +/// Uses [SPAKE 2](https://docs.rs/spake2/latest/spake2/) /// to derive a cryptographically secure secret from /// a potentially weak `shared_secret`. /// Verifies that the other peer derived the same secret. @@ -171,9 +182,9 @@ fn get_local_socket(local_addr: SocketAddr) -> std::io::Result { let _ = sock.set_reuse_port(true); let keepalive = TcpKeepalive::new() - .with_time(Duration::from_secs(10)) - .with_interval(Duration::from_secs(1)) - .with_retries(10); + .with_time(Duration::from_secs(5)) + .with_interval(Duration::from_secs(2)) + .with_retries(5); let _ = sock.set_tcp_keepalive(&keepalive); socket.bind(local_addr)?; diff --git a/gday_server/src/main.rs b/gday_server/src/main.rs index 0499dfc..c026bcd 100644 --- a/gday_server/src/main.rs +++ b/gday_server/src/main.rs @@ -1,4 +1,4 @@ -//! Runs a server for the `gday_contact_exchange_protocol`. +//! Runs a server for the [`gday_contact_exchange_protocol`]. //! Lets two users exchange their public and (optionally) private socket addresses. #![forbid(unsafe_code)] #![warn(clippy::all)]