diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..6a8be8c0 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,42 @@ +[target.'cfg(all())'] +rustflags = [ + # Deny `unsafe`. + "-Dunsafe_code", + # Clippy groups. + "-Wclippy::pedantic", + "-Wclippy::cargo", + # Allowed Clippy lints. + "-Aclippy::tabs_in_doc_comments", + # Rustdoc group. + "-Wrustdoc::all", + # Rust groups. + "-Wfuture_incompatible", + "-Wrust_2018_compatibility", + "-Wrust_2018_idioms", + "-Wrust_2021_compatibility", + "-Wunused", + # Rust lints. + "-Wdeprecated_in_future", + "-Wffi_unwind_calls", + "-Winvalid-reference_casting", + "-Wmacro_use_extern_crate", + "-Wmeta_variable_misuse", + "-Wmissing_abi", + "-Wmissing_copy_implementations", + "-Wmissing_debug_implementations", + "-Wmissing_docs", + "-Wnon_ascii_idents", + "-Wnoop_method_call", + "-Wsingle_use_lifetimes", + "-Wtrivial_casts", + "-Wtrivial_numeric_casts", + "-Wunreachable_pub", + "-Wunsafe_op_in_unsafe_fn", + "-Wunused_crate_dependencies", + "-Wunused_import_braces", + "-Wunused_lifetimes", + "-Wunused_qualifications", + "-Wunused_tuple_struct_fields", + # Allowed Rust lints. + "-Aunused_crate_dependencies", +] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 501a252d..3533932e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -30,8 +30,7 @@ jobs: - name: Run doc tests run: | - rustup toolchain install nightly --profile minimal --allow-downgrade - cargo +nightly test --workspace --doc --all-features + cargo test --workspace --doc --all-features msrv: runs-on: ubuntu-latest @@ -63,6 +62,12 @@ jobs: run: | cargo clippy --workspace --all-targets --all-features -- -D warnings + - name: Run Rustdoc + run: | + cargo doc --no-deps --workspace --all-features + env: + RUSTDOCFLAGS: -D warnings + docs: runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' @@ -79,7 +84,7 @@ jobs: rustup install nightly --profile minimal cargo +nightly doc --no-deps --workspace --all-features env: - RUSTDOCFLAGS: -D warnings + RUSTDOCFLAGS: --cfg docsrs - name: Deploy Docs uses: JamesIves/github-pages-deploy-action@releases/v4 diff --git a/Cargo.toml b/Cargo.toml index b14c6f4b..3c094639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,10 @@ flume = { version = "0.11" } futures-channel = "0.3" futures-executor = "0.3" futures-util = "0.3" -if_chain = "1" parking_lot = { version = "0.12", features = ["send_guard"] } pin-project = "1" quinn = "0.10.1" rcgen = { version = "0.11.0", default-features = false, optional = true } -ring = "0.16" rustls = { version = "0.21.1", default-features = false, features = [ "dangerous_configuration", ] } @@ -65,4 +63,5 @@ lto = true [package.metadata.docs.rs] features = ["dangerous", "rcgen", "trust-dns"] +rustdoc-args = ["--cfg", "docsrs"] targets = [] diff --git a/examples/basic.rs b/examples/basic.rs index c22dea85..3f7dab13 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,3 +1,5 @@ +//! TODO + use anyhow::{Error, Result}; use fabruic::{Endpoint, KeyPair}; use futures_util::{future, StreamExt, TryFutureExt}; @@ -55,7 +57,7 @@ async fn main() -> Result<()> { ); // send message - sender.send(&format!("hello from client {}", index))?; + sender.send(&format!("hello from client {index}"))?; // start listening to new incoming messages // in this example we know there is only 1 incoming message, so we will diff --git a/examples/onestream.rs b/examples/onestream.rs new file mode 100644 index 00000000..b025784e --- /dev/null +++ b/examples/onestream.rs @@ -0,0 +1,98 @@ +//! TODO + +use anyhow::{Error, Result}; +use fabruic::{Endpoint, KeyPair}; +use futures_util::StreamExt; + +const SERVER_NAME: &str = "test"; +const SERVER_PORT: u16 = 5001; +const CLIENTS: usize = 100; + +#[tokio::main] +#[cfg_attr(test, test)] +async fn main() -> Result<()> { + // generate a certificate pair + let key_pair = KeyPair::new_self_signed(SERVER_NAME); + + // start the server + let server = Endpoint::new_server(SERVER_PORT, key_pair.clone())?; + let address = format!("quic://{}", server.local_address()?); + println!("[server] Listening on {address}"); + tokio::spawn(run_server(server)); + + // build a client + let client = Endpoint::new_client()?; + + let connection = client + .connect_pinned(address, key_pair.end_entity_certificate(), None) + .await? + .accept::<()>() + .await?; + connection.close_incoming().await?; + + // initiate a stream + let (sender, receiver) = connection.open_stream::(&()).await?; + + let tasks = (0..CLIENTS) + .map(|_| { + let sender = sender.clone(); + let mut receiver = receiver.clone(); + async move { + sender.send(&String::from("test"))?; + let value = receiver.next().await.expect("didn't get a response")?; + assert_eq!(value, "test"); + Ok(()) + } + }) + .collect::>(); + + futures_util::future::join_all(tasks) + .await + .into_iter() + .collect::, Error>>()?; + + // wait for client to finish cleanly + client.wait_idle().await; + + Ok(()) +} + +async fn run_server(mut server: Endpoint) -> Result<(), Error> { + // start listening to new incoming connections + // in this example we know there is `CLIENTS` number of clients, so we will not + // wait for more + let mut connection = server + .next() + .await + .expect("connection failed") + .accept::<()>() + .await?; + println!("[server] New Connection: {}", connection.remote_address()); + + // start listening to new incoming streams + // in this example we know there is only 1 incoming stream, so we will not wait + // for more + let incoming = connection.next().await.expect("no stream found")?; + connection.close_incoming().await?; + println!( + "[server] New incoming stream from: {}", + connection.remote_address() + ); + + // accept stream + let (sender, mut receiver) = incoming.accept::().await?; + + // start listening to new incoming messages + // in this example we know there is only 1 incoming message, so we will not wait + // for more + while let Some(message) = receiver.next().await { + let message = message?; + sender.send(&message)?; + } + + // wait for stream to finish + sender.finish().await?; + receiver.finish().await?; + + Ok(()) +} diff --git a/examples/twostream.rs b/examples/twostream.rs new file mode 100644 index 00000000..ddd5b5e3 --- /dev/null +++ b/examples/twostream.rs @@ -0,0 +1,113 @@ +//! TODO + +use anyhow::{Error, Result}; +use fabruic::{Endpoint, Incoming, KeyPair}; +use futures_util::StreamExt; + +const SERVER_NAME: &str = "test"; +const SERVER_PORT: u16 = 5002; +const REQUESTS_PER_STREAM: usize = 10; +const STREAMS: usize = 1000; + +#[tokio::main(worker_threads = 16)] +#[cfg_attr(test, test)] +async fn main() -> Result<()> { + // generate a certificate pair + let key_pair = KeyPair::new_self_signed(SERVER_NAME); + + // start the server + let server = Endpoint::new_server(SERVER_PORT, key_pair.clone())?; + let address = format!("quic://{}", server.local_address()?); + println!("[server] Listening on {address}"); + tokio::spawn(run_server(server)); + + // build a client + let client = Endpoint::new_client()?; + + let connection = client + .connect_pinned(address, key_pair.end_entity_certificate(), None) + .await? + .accept::<()>() + .await?; + connection.close_incoming().await?; + + // initiate a stream + + let tasks = (0..STREAMS) + .map(|_| async { + let (sender, receiver) = connection.open_stream::(&()).await.unwrap(); + (0..REQUESTS_PER_STREAM).for_each(|_| { + let sender = sender.clone(); + let mut receiver = receiver.clone(); + tokio::task::spawn(async move { + sender.send(&String::from("test"))?; + let value = receiver.next().await.expect("didn't get a response")?; + assert_eq!(value, "test"); + Result::<(), Error>::Ok(()) + }); + }); + Ok(()) + }) + .collect::>(); + + futures_util::future::join_all(tasks) + .await + .into_iter() + .collect::, Error>>() + .unwrap(); + + // wait for client to finish cleanly + client.wait_idle().await; + + Ok(()) +} + +async fn run_server(mut server: Endpoint) -> Result<(), Error> { + // start listening to new incoming connections + // in this example we know there is `CLIENTS` number of clients, so we will not + // wait for more + while let Some(connection) = server.next().await { + let connection = connection.accept::<()>().await?; + println!("[server] New Connection: {}", connection.remote_address()); + + // every new incoming connections is handled in it's own task + tokio::spawn(run_connection(connection)); + } + + Ok(()) +} + +async fn run_connection(mut connection: fabruic::Connection<()>) -> Result<(), Error> { + // start listening to new incoming streams + // in this example we know there is only 1 incoming stream, so we will not wait + // for more + while let Some(incoming) = connection.next().await { + // connection.close_incoming().await?; + /*println!( + "[server] New incoming stream from: {}", + connection.remote_address() + );*/ + + tokio::spawn(run_stream(incoming?)); + } + + Ok(()) +} + +async fn run_stream(incoming: Incoming<()>) -> Result<(), Error> { + let (sender, mut receiver) = incoming.accept::().await?; + + // start listening to new incoming messages + // in this example we know there is only 1 incoming message, so we will not wait + // for more + while let Some(message) = receiver.next().await { + let message = message?; + sender.send(&message)?; + } + + // wait for stream to finish + sender.finish().await?; + receiver.finish().await?; + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index c3668b76..7269e20b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,7 +17,6 @@ use quinn::ConnectionClose; pub use quinn::{ConnectError, ConnectionError, ReadError, WriteError}; use thiserror::Error; #[cfg(feature = "trust-dns")] -#[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub use trust_dns_resolver::error::ResolveError; pub use url::ParseError; pub use webpki::Error; @@ -161,7 +160,6 @@ pub enum Connect { ParseDomain(ParseError), /// Failed to resolve domain with [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] #[error("Error resolving domain with trust-dns: {0}")] TrustDns(#[from] Box), /// Failed to resolve domain with diff --git a/src/lib.rs b/src/lib.rs index cd57c40b..2c9af284 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,65 +1,11 @@ #![deny(unsafe_code)] -#![warn(clippy::cargo, clippy::pedantic, future_incompatible, rust_2018_idioms)] -#![warn( - macro_use_extern_crate, - meta_variable_misuse, - missing_copy_implementations, - missing_debug_implementations, - missing_docs, - non_ascii_idents, - single_use_lifetimes, - trivial_casts, - trivial_numeric_casts, - unreachable_pub, - unused_import_braces, - unused_lifetimes, - unused_qualifications, - unused_results, - variant_size_differences -)] -#![allow( - clippy::blanket_clippy_restriction_lints, - clippy::else_if_without_else, - clippy::exhaustive_enums, - clippy::expect_used, - clippy::future_not_send, - clippy::implicit_return, - clippy::missing_inline_in_public_items, - clippy::multiple_crate_versions, - clippy::non_ascii_literal, - clippy::pattern_type_mismatch, - clippy::redundant_pub_crate, - clippy::separated_literal_suffix, - clippy::shadow_reuse, - // Currently breaks async - clippy::shadow_same, - clippy::shadow_unrelated, - clippy::tabs_in_doc_comments, - clippy::unreachable, - clippy::wildcard_enum_match_arm, - // See: https://github.com/rust-lang/rust/issues/64762 - unreachable_pub, -)] -#![cfg_attr( - doc, - feature(doc_cfg), - warn(rustdoc::all), - allow(rustdoc::missing_doc_code_examples, rustdoc::private_doc_tests) -)] -#![cfg_attr( - test, - allow( - clippy::arithmetic_side_effects, - clippy::panic, - clippy::panic_in_result_fn - ) -)] +#![allow(clippy::multiple_crate_versions)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] //! TODO mod x509; #[cfg(feature = "dangerous")] -#[cfg_attr(doc, doc(cfg(feature = "dangerous")))] pub mod dangerous { //! Security-sensitive settings are hidden behind these traits. Be careful! diff --git a/src/quic/endpoint/builder/config.rs b/src/quic/endpoint/builder/config.rs index bdb1d634..25f17db7 100644 --- a/src/quic/endpoint/builder/config.rs +++ b/src/quic/endpoint/builder/config.rs @@ -33,15 +33,12 @@ pub(in crate::quic::endpoint) struct Config { max_idle_timeout: Option, /// Enable [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] trust_dns: bool, /// Enables DNSSEC validation for [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] dnssec: bool, /// Enables `/etc/hosts` file support for [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] hosts_file: bool, } @@ -96,7 +93,6 @@ impl Config { /// Controls the use of [`trust-dns`](trust_dns_resolver) for /// [`Endpoint::connect`](crate::Endpoint::connect). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub(super) fn set_trust_dns(&mut self, enable: bool) { self.trust_dns = enable; } @@ -126,14 +122,12 @@ impl Config { /// Controls DNSSEC validation for [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub(super) fn set_dnssec(&mut self, enable: bool) { self.dnssec = enable; } /// Returns if DNSSEC is enabled for [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub(in crate::quic::endpoint) const fn dnssec(&self) -> bool { self.dnssec } @@ -141,7 +135,6 @@ impl Config { /// Controls `/etc/hosts` file support for /// [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub(super) fn set_hosts_file(&mut self, enable: bool) { self.hosts_file = enable; } @@ -149,7 +142,6 @@ impl Config { /// Returns if `/etc/hosts` file support is enabled for /// [`trust-dns`](trust_dns_resolver). #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub(in crate::quic::endpoint) const fn hosts_file(&self) -> bool { self.hosts_file } diff --git a/src/quic/endpoint/builder/mod.rs b/src/quic/endpoint/builder/mod.rs index 3179f98d..991d4efb 100644 --- a/src/quic/endpoint/builder/mod.rs +++ b/src/quic/endpoint/builder/mod.rs @@ -272,7 +272,6 @@ impl Builder { /// builder.set_trust_dns(false); /// ``` #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub fn set_trust_dns(&mut self, enable: bool) { self.config.set_trust_dns(enable); } @@ -334,7 +333,6 @@ impl Builder { /// builder.set_dnssec(false); /// ``` #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub fn set_dnssec(&mut self, enable: bool) { self.config.set_dnssec(enable); } @@ -354,7 +352,6 @@ impl Builder { /// ``` #[must_use] #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub const fn dnssec(&self) -> bool { self.config.dnssec() } @@ -374,7 +371,6 @@ impl Builder { /// builder.set_hosts_file(false); /// ``` #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub fn set_hosts_file(&mut self, enable: bool) { self.config.set_hosts_file(enable); } @@ -395,7 +391,6 @@ impl Builder { /// ``` #[must_use] #[cfg(feature = "trust-dns")] - #[cfg_attr(doc, doc(cfg(feature = "trust-dns")))] pub const fn hosts_file(&self) -> bool { self.config.hosts_file() } @@ -1014,7 +1009,7 @@ mod test { // connection, for some reason IPv6 `[::]` doesn't work on GitHub Actions builder.set_address(([0, 0, 0, 0], 0).into()); // QUIC is comptaible with HTTP/3 to establish a connection only - builder.set_protocols([b"h3-29".to_vec()]); + builder.set_protocols([b"h3".to_vec()]); // `cloudflare-quic` doesn't support DNSSEC builder.set_dnssec(false); @@ -1044,7 +1039,7 @@ mod test { // connection, for some reason IPv6 `[::]` doesn't work on GitHub Actions builder.set_address(([0, 0, 0, 0], 0).into()); // QUIC is comptaible with HTTP/3 to establish a connection only - builder.set_protocols([b"h3-29".to_vec()]); + builder.set_protocols([b"h3".to_vec()]); // `cloudflare-quic` doesn't support DNSSEC builder.set_dnssec(false); @@ -1071,7 +1066,7 @@ mod test { // connection, for some reason IPv6 `[::]` doesn't work on GitHub Actions builder.set_address(([0, 0, 0, 0], 0).into()); // QUIC is comptaible with HTTP/3 to establish a connection only - builder.set_protocols([b"h3-29".to_vec()]); + builder.set_protocols([b"h3".to_vec()]); // `cloudflare-quic` doesn't support DNSSEC builder.set_dnssec(false); diff --git a/src/x509/mod.rs b/src/x509/mod.rs index 77ef9293..59f91f42 100644 --- a/src/x509/mod.rs +++ b/src/x509/mod.rs @@ -2,7 +2,7 @@ mod certificate; mod certificate_chain; -pub mod private_key; +pub(crate) mod private_key; pub use certificate::Certificate; pub use certificate_chain::CertificateChain; @@ -35,7 +35,6 @@ impl TryFrom<(CertificateChain, PrivateKey)> for KeyPair { impl KeyPair { /// Generate a self signed certificate. #[cfg(feature = "rcgen")] - #[cfg_attr(doc, doc(cfg(feature = "rcgen")))] #[allow(clippy::missing_panics_doc)] pub fn new_self_signed>(domain: S) -> Self { let key_pair = rcgen::generate_simple_self_signed([domain.into()])