├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .gitlab-ci.yml
├── Cargo.lock
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── deny.toml
├── examples
└── base
│ ├── Cargo.toml
│ └── src
│ └── main.rs
├── watermelon-mini
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
│ ├── lib.rs
│ ├── non_standard_zstd.rs
│ ├── proto
│ ├── authenticator.rs
│ ├── connection
│ │ ├── compression.rs
│ │ ├── mod.rs
│ │ └── security.rs
│ ├── connector.rs
│ └── mod.rs
│ └── util.rs
├── watermelon-net
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
│ ├── connection
│ ├── mod.rs
│ ├── streaming.rs
│ └── websocket.rs
│ ├── future.rs
│ ├── happy_eyeballs.rs
│ └── lib.rs
├── watermelon-nkeys
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
│ ├── crc.rs
│ ├── lib.rs
│ └── seed.rs
├── watermelon-proto
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
│ ├── connect.rs
│ ├── headers
│ ├── map.rs
│ ├── mod.rs
│ ├── name.rs
│ └── value.rs
│ ├── lib.rs
│ ├── message.rs
│ ├── proto
│ ├── client.rs
│ ├── decoder
│ │ ├── framed.rs
│ │ ├── mod.rs
│ │ └── stream.rs
│ ├── encoder
│ │ ├── framed.rs
│ │ ├── mod.rs
│ │ └── stream.rs
│ ├── mod.rs
│ └── server.rs
│ ├── queue_group.rs
│ ├── server_addr.rs
│ ├── server_error.rs
│ ├── server_info.rs
│ ├── status_code.rs
│ ├── subject.rs
│ ├── subscription_id.rs
│ ├── tests.rs
│ └── util
│ ├── buf_list.rs
│ ├── crlf.rs
│ ├── lines_iter.rs
│ ├── mod.rs
│ ├── split_spaces.rs
│ └── uint.rs
└── watermelon
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
├── client
├── builder.rs
├── commands
│ ├── mod.rs
│ ├── publish.rs
│ └── request.rs
├── from_env.rs
├── jetstream
│ ├── commands
│ │ ├── consumer_batch.rs
│ │ ├── consumer_list.rs
│ │ ├── consumer_stream.rs
│ │ ├── mod.rs
│ │ └── stream_list.rs
│ ├── mod.rs
│ └── resources
│ │ ├── consumer.rs
│ │ ├── mod.rs
│ │ └── stream.rs
├── mod.rs
├── quick_info.rs
└── tests.rs
├── handler
├── delayed.rs
├── mod.rs
└── pinger.rs
├── lib.rs
├── multiplexed_subscription.rs
├── subscription.rs
├── tests.rs
└── util
├── atomic.rs
├── future.rs
└── mod.rs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | cargo-deny:
10 | name: cargo-deny
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v4
14 |
15 | - uses: EmbarkStudios/cargo-deny-action@v2
16 |
17 | fmt:
18 | name: rustfmt / 1.90.0
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 |
24 | - uses: dtolnay/rust-toolchain@1.90.0
25 | with:
26 | components: rustfmt
27 |
28 | - name: Rust rustfmt
29 | run: cargo fmt --all -- --check
30 |
31 | clippy:
32 | name: clippy / 1.90.0
33 | runs-on: ubuntu-latest
34 |
35 | steps:
36 | - uses: actions/checkout@v4
37 |
38 | - uses: dtolnay/rust-toolchain@1.90.0
39 | with:
40 | components: clippy
41 |
42 | - name: Run clippy
43 | run: cargo clippy --all-features -- -D warnings
44 |
45 | cargo-hack:
46 | name: cargo-hack / 1.90.0
47 | runs-on: ubuntu-latest
48 | steps:
49 | - uses: actions/checkout@v4
50 |
51 | - uses: dtolnay/rust-toolchain@1.90.0
52 |
53 | - uses: taiki-e/install-action@v2
54 | with:
55 | tool: cargo-hack@0.6.37
56 |
57 | - name: Run cargo-hack
58 | run: cargo hack check --feature-powerset --no-dev-deps --at-least-one-of aws-lc-rs,ring --at-least-one-of rand,getrandom
59 |
60 | test:
61 | name: test / ${{ matrix.name }}
62 | runs-on: ubuntu-latest
63 |
64 | strategy:
65 | matrix:
66 | include:
67 | - name: stable
68 | rust: stable
69 | - name: beta
70 | rust: beta
71 | - name: nightly
72 | rust: nightly
73 | - name: 1.85.0
74 | rust: 1.85.0
75 |
76 | steps:
77 | - uses: actions/checkout@v4
78 |
79 | - uses: dtolnay/rust-toolchain@master
80 | with:
81 | toolchain: ${{ matrix.rust }}
82 |
83 | - name: Run tests
84 | run: cargo test
85 |
86 | - name: Run tests (--features websocket,portable-atomic)
87 | run: cargo test --features websocket,portable-atomic
88 |
89 | - name: Run tests (--no-default-features --features ring,getrandom)
90 | run: cargo test --no-default-features --features ring,getrandom
91 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | stages:
2 | - test
3 |
4 | rust:deny:
5 | stage: test
6 | image: rust:1.90-alpine3.22
7 | before_script:
8 | - apk add cargo-deny
9 | script:
10 | - cargo deny check
11 |
12 | rust:fmt:
13 | stage: test
14 | image: rust:1.90-alpine3.22
15 | before_script:
16 | - rustup component add rustfmt
17 | script:
18 | - cargo fmt -- --check
19 |
20 | rust:clippy:
21 | stage: test
22 | image: rust:1.90-alpine3.20
23 | before_script:
24 | - apk add build-base musl-dev linux-headers cmake perl go
25 | - rustup component add clippy
26 | script:
27 | - cargo clippy --all-features -- -D warnings
28 |
29 | rust:hack:
30 | stage: test
31 | image: rust:1.90-alpine3.20
32 | before_script:
33 | - apk add build-base musl-dev linux-headers cmake perl go cargo-hack
34 | script:
35 | - cargo hack check --feature-powerset --no-dev-deps --at-least-one-of aws-lc-rs,ring --at-least-one-of rand,getrandom
36 |
37 | rust:test:
38 | stage: test
39 | image: rust:1.90-alpine3.22
40 | before_script:
41 | - apk add musl-dev cmake perl go
42 | script:
43 | - cargo test
44 | - cargo test --features websocket,portable-atomic
45 | - cargo test --no-default-features --features ring,getrandom
46 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "watermelon",
4 | "watermelon-mini",
5 | "watermelon-net",
6 | "watermelon-proto",
7 | "watermelon-nkeys",
8 | "examples/base",
9 | ]
10 | resolver = "2"
11 |
12 | [workspace.package]
13 | edition = "2024"
14 | license = "MIT OR Apache-2.0"
15 | repository = "https://github.com/M4SS-Code/watermelon"
16 | rust-version = "1.85"
17 |
18 | [workspace.lints.rust]
19 | unsafe_code = "deny"
20 | unreachable_pub = "deny"
21 |
22 | [workspace.lints.clippy]
23 | pedantic = { level = "warn", priority = -1 }
24 | module_name_repetitions = "allow"
25 | await_holding_refcell_ref = "deny"
26 | map_unwrap_or = "warn"
27 | needless_lifetimes = "warn"
28 | needless_raw_string_hashes = "warn"
29 | redundant_closure_for_method_calls = "warn"
30 | semicolon_if_nothing_returned = "warn"
31 | str_to_string = "warn"
32 | clone_on_ref_ptr = "warn"
33 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024-2025 M4SS Srl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
watermelon
2 |
3 |
4 | Pure Rust NATS client implementation
5 |
6 |
7 |
8 | `watermelon` is an independent and opinionated implementation of the NATS
9 | client protocol and the NATS client API for Rust. The goal of the project
10 | is to produce a more secure, composable and idiomatic implementation compared
11 | to the official one.
12 |
13 | Most users of this project will depend on the `watermelon` crate directly and on
14 | `watermelon-proto` and `watermelon-nkeys` via the re-exports in `watermelon`.
15 |
16 | Watermelon is divided into multiple crates, all hosted in the same monorepo.
17 |
18 | | Crate name | Crates.io release | Docs | Description |
19 | | ------------------ | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
20 | | `watermelon` | [](https://crates.io/crates/watermelon) | [](https://docs.rs/watermelon) | High level actor based NATS Core and NATS Jetstream client implementation |
21 | | `watermelon-mini` | [](https://crates.io/crates/watermelon-mini) | [](https://docs.rs/watermelon-mini) | Bare bones NATS Core client implementation |
22 | | `watermelon-net` | [](https://crates.io/crates/watermelon-net) | [](https://docs.rs/watermelon-net) | Low-level NATS Core network implementation |
23 | | `watermelon-proto` | [](https://crates.io/crates/watermelon-proto) | [](https://docs.rs/watermelon-proto) | `#[no_std]` NATS Core Sans-IO protocol implementation |
24 | | `watermelon-nkeys` | [](https://crates.io/crates/watermelon-nkeys) | [](https://docs.rs/watermelon-nkeys) | Minimal NKeys implementation for NATS client authentication |
25 |
26 | # Philosophy and Design
27 |
28 | 1. **Security by design**: this library uses type-safe and checked APIs, such as `Subject`, to prevent entire classes of errors and security vulnerabilities.
29 | 2. **Layering and composability**: the library is split into layers. You can get a high-level, batteries included implementation via `watermelon`, or depend directly on the lower-level crates for maximum flexibility.
30 | 3. **Opinionated, Rusty take**: we adapt the Go-style API of nats-server and apply different trade-offs to make NATS feel more Rusty. We sacrifice a bit of performance by enabling server verbose mode, and get better errors in return.
31 | 4. **Legacy is in the past**: we only support `nats-server >= 2.10` and avoid legacy versions compatibility code like the STARTTLS-style TLS upgrade path or fallbacks for older JetStream APIs. We also prefer pull consumers over push consumers given the robust flow control, easier compatibility with multi-account environments and stronger permissions handling.
32 | 5. **Permissive licensing**: dual licensed under MIT and APACHE-2.0.
33 |
34 | ## License
35 |
36 | Licensed under either of
37 |
38 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or )
39 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or )
40 |
41 | at your option.
42 |
43 | ### Contribution
44 |
45 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
46 |
--------------------------------------------------------------------------------
/deny.toml:
--------------------------------------------------------------------------------
1 | [advisories]
2 | ignore = [
3 | ]
4 |
5 | [licenses]
6 | allow = [
7 | "MIT",
8 | "Apache-2.0",
9 | "BSD-3-Clause",
10 | "ISC",
11 | "Unicode-3.0",
12 | "0BSD",
13 | "OpenSSL",
14 | "CDLA-Permissive-2.0",
15 | ]
16 |
17 | [licenses.private]
18 | ignore = true
19 |
20 | [bans]
21 | multiple-versions = "warn"
22 | wildcards = "deny"
23 | deny = [
24 | ]
25 |
26 | [sources]
27 | unknown-registry = "deny"
28 | unknown-git = "deny"
29 |
30 | [sources.allow-org]
31 | #github = ["M4SS-Code"]
32 |
--------------------------------------------------------------------------------
/examples/base/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "watermelon-base-example"
3 | version = "0.1.0"
4 | edition.workspace = true
5 | license.workspace = true
6 | repository.workspace = true
7 | rust-version.workspace = true
8 | publish = false
9 |
10 | [dependencies]
11 | tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "time", "signal"] }
12 | futures-util = { version = "0.3.31", default-features = false }
13 | watermelon = { path = "../../watermelon", version = "0.4" }
14 | bytes = "1.10.1"
15 | jiff = { version = "0.2.1", default-features = false, features = ["std", "tz-system", "tzdb-zoneinfo"] }
16 |
17 | [lints]
18 | workspace = true
19 |
--------------------------------------------------------------------------------
/examples/base/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | hash::{DefaultHasher, Hasher as _},
3 | time::Duration,
4 | };
5 |
6 | use bytes::Bytes;
7 | use futures_util::TryStreamExt as _;
8 | use jiff::Zoned;
9 | use tokio::{
10 | signal::ctrl_c,
11 | task::JoinSet,
12 | time::{MissedTickBehavior, interval},
13 | };
14 | use watermelon::{
15 | core::Client,
16 | proto::{
17 | Subject,
18 | headers::{HeaderName, HeaderValue},
19 | },
20 | };
21 |
22 | type BoxError = Box;
23 |
24 | #[tokio::main]
25 | async fn main() -> Result<(), BoxError> {
26 | let client = Client::builder()
27 | .connect("nats://demo.nats.io".parse()?)
28 | .await?;
29 | println!("Quick Info: {:?}", client.quick_info());
30 |
31 | let mut set = JoinSet::new();
32 |
33 | // Subscribe to `watermelon.>`, print every message we get and reply if possible
34 | set.spawn({
35 | let client = client.clone();
36 |
37 | async move {
38 | let mut subscription = client
39 | .subscribe(Subject::from_static("watermelon.>"), None)
40 | .await?;
41 | while let Some(msg) = subscription.try_next().await? {
42 | println!(
43 | "Received new message subject={:?} headers={:?} payload={:?}",
44 | msg.base.subject, msg.base.headers, msg.base.payload
45 | );
46 |
47 | if let Some(reply_subject) = msg.base.reply_subject {
48 | client
49 | .publish(reply_subject)
50 | .header(HeaderName::from_static("Local-Time"), local_time())
51 | .payload(Bytes::from_static("Welcome from Watermelon!".as_bytes()))
52 | .await?;
53 | }
54 | }
55 |
56 | Ok::<_, BoxError>(())
57 | }
58 | });
59 |
60 | // Publish to `watermelon.[random number]` every 20 seconds and await the response
61 | set.spawn({
62 | let client = client.clone();
63 |
64 | async move {
65 | let mut interval = interval(Duration::from_secs(20));
66 | interval.set_missed_tick_behavior(MissedTickBehavior::Delay);
67 |
68 | loop {
69 | interval.tick().await;
70 |
71 | let subject = format!("watermelon.{}", rng()).try_into()?;
72 | println!("Sending new request...");
73 | let response_fut = client
74 | .request(subject)
75 | .header(HeaderName::from_static("Local-Time"), local_time())
76 | .payload(Bytes::from_static(b"Hello from Watermelon!"))
77 | .await?;
78 | println!("Awaiting response...");
79 | match response_fut.await {
80 | Ok(resp) => {
81 | println!(
82 | "Received response subject={:?} headers={:?} payload={:?}",
83 | resp.base.subject, resp.base.headers, resp.base.payload
84 | );
85 | }
86 | Err(err) => {
87 | eprintln!("Received error err={err:?}");
88 | }
89 | }
90 | }
91 | }
92 | });
93 |
94 | // Wait for the user to CTRL+C the program and gracefully shutdown the client
95 | set.spawn(async move {
96 | ctrl_c().await?;
97 | println!("Starting graceful shutdown...");
98 | client.close().await;
99 | Ok::<_, BoxError>(())
100 | });
101 |
102 | while let Some(next) = set.join_next().await {
103 | println!("Task exited with: {next:?}");
104 | }
105 |
106 | Ok(())
107 | }
108 |
109 | /// Get the local time and timezone
110 | fn local_time() -> HeaderValue {
111 | Zoned::now()
112 | .to_string()
113 | .try_into()
114 | .expect("local DateTime can always be encoded into `HeaderValue`")
115 | }
116 |
117 | /// A poor man's RNG
118 | fn rng() -> u64 {
119 | DefaultHasher::new().finish()
120 | }
121 |
--------------------------------------------------------------------------------
/watermelon-mini/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "watermelon-mini"
3 | version = "0.3.2"
4 | description = "Minimal NATS Core client implementation"
5 | categories = ["api-bindings", "network-programming"]
6 | keywords = ["nats", "client"]
7 | edition.workspace = true
8 | license.workspace = true
9 | repository.workspace = true
10 | rust-version.workspace = true
11 |
12 | [package.metadata.docs.rs]
13 | features = ["websocket", "non-standard-zstd"]
14 |
15 | [dependencies]
16 | tokio = { version = "1.36", features = ["net"] }
17 | tokio-rustls = { version = "0.26", default-features = false }
18 | rustls-platform-verifier = "0.6"
19 |
20 | watermelon-net = { version = "0.2", path = "../watermelon-net", default-features = false }
21 | watermelon-proto = { version = "0.1.3", path = "../watermelon-proto" }
22 | watermelon-nkeys = { version = "0.1", path = "../watermelon-nkeys", default-features = false }
23 |
24 | thiserror = "2"
25 |
26 | # non-standard-zstd
27 | async-compression = { version = "0.4", features = ["tokio"], optional = true }
28 |
29 | [features]
30 | default = ["aws-lc-rs", "rand"]
31 | websocket = ["watermelon-net/websocket"]
32 | aws-lc-rs = ["tokio-rustls/aws-lc-rs", "watermelon-net/aws-lc-rs", "watermelon-nkeys/aws-lc-rs"]
33 | ring = ["tokio-rustls/ring", "watermelon-net/ring", "watermelon-nkeys/ring"]
34 | fips = ["tokio-rustls/fips", "watermelon-net/fips", "watermelon-nkeys/fips"]
35 | rand = ["watermelon-net/rand"]
36 | getrandom = ["watermelon-net/getrandom"]
37 | non-standard-zstd = ["watermelon-net/non-standard-zstd", "watermelon-proto/non-standard-zstd", "dep:async-compression", "async-compression/zstd"]
38 |
39 | [lints]
40 | workspace = true
41 |
--------------------------------------------------------------------------------
/watermelon-mini/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/watermelon-mini/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/watermelon-mini/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/watermelon-mini/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![forbid(unsafe_code)]
2 |
3 | use std::sync::Arc;
4 |
5 | use rustls_platform_verifier::Verifier;
6 | use tokio::net::TcpStream;
7 | use tokio_rustls::{
8 | TlsConnector,
9 | rustls::{self, ClientConfig, crypto::CryptoProvider, version::TLS13},
10 | };
11 | use watermelon_net::Connection;
12 | use watermelon_proto::{ServerAddr, ServerInfo};
13 |
14 | #[cfg(feature = "non-standard-zstd")]
15 | pub use self::non_standard_zstd::ZstdStream;
16 | use self::proto::connect;
17 | pub use self::proto::{
18 | AuthenticationMethod, ConnectError, ConnectionCompression, ConnectionSecurity,
19 | };
20 |
21 | #[cfg(feature = "non-standard-zstd")]
22 | pub(crate) mod non_standard_zstd;
23 | mod proto;
24 | mod util;
25 |
26 | #[derive(Debug, Clone)]
27 | #[non_exhaustive]
28 | pub struct ConnectFlags {
29 | pub tcp_nodelay: bool,
30 | pub echo: bool,
31 | #[cfg(feature = "non-standard-zstd")]
32 | pub zstd_compression_level: Option,
33 | }
34 |
35 | impl Default for ConnectFlags {
36 | fn default() -> Self {
37 | Self {
38 | tcp_nodelay: true,
39 | echo: false,
40 | #[cfg(feature = "non-standard-zstd")]
41 | zstd_compression_level: Some(3),
42 | }
43 | }
44 | }
45 |
46 | /// Connect to a given address with some reasonable presets.
47 | ///
48 | /// The function is going to establish a TLS 1.3 connection, without the support of the client
49 | /// authorization.
50 | ///
51 | /// # Errors
52 | ///
53 | /// This returns an error in case the connection fails.
54 | #[expect(
55 | clippy::missing_panics_doc,
56 | reason = "the crypto_provider function always returns a provider that supports TLS 1.3"
57 | )]
58 | pub async fn easy_connect(
59 | addr: &ServerAddr,
60 | auth: Option<&AuthenticationMethod>,
61 | flags: ConnectFlags,
62 | ) -> Result<
63 | (
64 | Connection<
65 | ConnectionCompression>,
66 | ConnectionSecurity,
67 | >,
68 | Box,
69 | ),
70 | ConnectError,
71 | > {
72 | let provider = Arc::new(crypto_provider());
73 | let connector = TlsConnector::from(Arc::new(
74 | ClientConfig::builder_with_provider(Arc::clone(&provider))
75 | .with_protocol_versions(&[&TLS13])
76 | .unwrap()
77 | .dangerous()
78 | .with_custom_certificate_verifier(Arc::new(
79 | Verifier::new(provider).map_err(ConnectError::Tls)?,
80 | ))
81 | .with_no_client_auth(),
82 | ));
83 |
84 | let (conn, info) = connect(&connector, addr, "watermelon".to_owned(), auth, flags).await?;
85 | Ok((conn, info))
86 | }
87 |
88 | fn crypto_provider() -> CryptoProvider {
89 | #[cfg(feature = "aws-lc-rs")]
90 | return rustls::crypto::aws_lc_rs::default_provider();
91 | #[cfg(all(not(feature = "aws-lc-rs"), feature = "ring"))]
92 | return rustls::crypto::ring::default_provider();
93 | #[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
94 | compile_error!("Please enable the `aws-lc-rs` or the `ring` feature")
95 | }
96 |
--------------------------------------------------------------------------------
/watermelon-mini/src/non_standard_zstd.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::{self, Debug, Formatter},
3 | io,
4 | pin::Pin,
5 | task::{Context, Poll},
6 | };
7 |
8 | use async_compression::{
9 | Level,
10 | tokio::{bufread::ZstdDecoder, write::ZstdEncoder},
11 | };
12 | use tokio::io::{AsyncRead, AsyncWrite, BufReader, ReadBuf};
13 |
14 | use crate::util::MaybeConnection;
15 |
16 | pub struct ZstdStream {
17 | decoder: ZstdDecoder>>,
18 | encoder: ZstdEncoder>,
19 | }
20 |
21 | impl ZstdStream
22 | where
23 | S: AsyncRead + AsyncWrite + Unpin,
24 | {
25 | #[must_use]
26 | pub fn new(stream: S, compression_level: u8) -> Self {
27 | Self {
28 | decoder: ZstdDecoder::new(BufReader::new(MaybeConnection(Some(stream)))),
29 | encoder: ZstdEncoder::with_quality(
30 | MaybeConnection(None),
31 | Level::Precise(compression_level.into()),
32 | ),
33 | }
34 | }
35 | }
36 |
37 | impl AsyncRead for ZstdStream
38 | where
39 | S: AsyncRead + AsyncWrite + Unpin,
40 | {
41 | fn poll_read(
42 | mut self: Pin<&mut Self>,
43 | cx: &mut Context<'_>,
44 | buf: &mut ReadBuf<'_>,
45 | ) -> Poll> {
46 | if let Some(stream) = self.encoder.get_mut().0.take() {
47 | self.decoder.get_mut().get_mut().0 = Some(stream);
48 | }
49 |
50 | Pin::new(&mut self.decoder).poll_read(cx, buf)
51 | }
52 | }
53 |
54 | impl AsyncWrite for ZstdStream
55 | where
56 | S: AsyncRead + AsyncWrite + Unpin,
57 | {
58 | fn poll_write(
59 | mut self: Pin<&mut Self>,
60 | cx: &mut Context<'_>,
61 | buf: &[u8],
62 | ) -> Poll> {
63 | if let Some(stream) = self.decoder.get_mut().get_mut().0.take() {
64 | self.encoder.get_mut().0 = Some(stream);
65 | }
66 |
67 | Pin::new(&mut self.encoder).poll_write(cx, buf)
68 | }
69 |
70 | fn poll_write_vectored(
71 | mut self: Pin<&mut Self>,
72 | cx: &mut Context<'_>,
73 | bufs: &[io::IoSlice<'_>],
74 | ) -> Poll> {
75 | if let Some(stream) = self.decoder.get_mut().get_mut().0.take() {
76 | self.encoder.get_mut().0 = Some(stream);
77 | }
78 |
79 | Pin::new(&mut self.encoder).poll_write_vectored(cx, bufs)
80 | }
81 |
82 | fn is_write_vectored(&self) -> bool {
83 | if let Some(stream) = &self.encoder.get_ref().0 {
84 | stream.is_write_vectored()
85 | } else if let Some(stream) = &self.decoder.get_ref().get_ref().0 {
86 | stream.is_write_vectored()
87 | } else {
88 | unreachable!()
89 | }
90 | }
91 |
92 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
93 | if let Some(stream) = self.decoder.get_mut().get_mut().0.take() {
94 | self.encoder.get_mut().0 = Some(stream);
95 | }
96 |
97 | Pin::new(&mut self.encoder).poll_flush(cx)
98 | }
99 |
100 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
101 | if let Some(stream) = self.decoder.get_mut().get_mut().0.take() {
102 | self.encoder.get_mut().0 = Some(stream);
103 | }
104 |
105 | Pin::new(&mut self.encoder).poll_shutdown(cx)
106 | }
107 | }
108 |
109 | impl Debug for ZstdStream {
110 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
111 | f.debug_struct("ZstdStream").finish_non_exhaustive()
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/watermelon-mini/src/proto/authenticator.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{self, Debug, Formatter};
2 |
3 | use watermelon_nkeys::{KeyPair, KeyPairFromSeedError};
4 | use watermelon_proto::{Connect, ServerAddr, ServerInfo};
5 |
6 | pub enum AuthenticationMethod {
7 | UserAndPassword { username: String, password: String },
8 | Creds { jwt: String, nkey: KeyPair },
9 | }
10 |
11 | #[derive(Debug, thiserror::Error)]
12 | pub enum AuthenticationError {
13 | #[error("missing nonce")]
14 | MissingNonce,
15 | }
16 |
17 | #[derive(Debug, thiserror::Error)]
18 | pub enum CredsParseError {
19 | #[error("contents are truncated")]
20 | Truncated,
21 | #[error("missing closing for JWT")]
22 | MissingJwtClosing,
23 | #[error("missing closing for nkey")]
24 | MissingNkeyClosing,
25 | #[error("missing JWT")]
26 | MissingJwt,
27 | #[error("missing nkey")]
28 | MissingNkey,
29 | #[error("invalid nkey")]
30 | InvalidKey(#[source] KeyPairFromSeedError),
31 | }
32 |
33 | impl AuthenticationMethod {
34 | pub(crate) fn try_from_addr(addr: &ServerAddr) -> Option {
35 | if let (Some(username), Some(password)) = (addr.username(), addr.password()) {
36 | Some(Self::UserAndPassword {
37 | username: username.to_owned(),
38 | password: password.to_owned(),
39 | })
40 | } else {
41 | None
42 | }
43 | }
44 |
45 | pub(crate) fn prepare_for_auth(
46 | &self,
47 | info: &ServerInfo,
48 | connect: &mut Connect,
49 | ) -> Result<(), AuthenticationError> {
50 | match self {
51 | Self::UserAndPassword { username, password } => {
52 | connect.username = Some(username.clone());
53 | connect.password = Some(password.clone());
54 | }
55 | Self::Creds { jwt, nkey } => {
56 | let nonce = info
57 | .nonce
58 | .as_deref()
59 | .ok_or(AuthenticationError::MissingNonce)?;
60 | let signature = nkey.sign(nonce.as_bytes()).to_string();
61 |
62 | connect.jwt = Some(jwt.clone());
63 | connect.nkey = Some(nkey.public_key().to_string());
64 | connect.signature = Some(signature);
65 | }
66 | }
67 |
68 | Ok(())
69 | }
70 |
71 | /// Creates an `AuthenticationMethod` from the content of a credentials file.
72 | ///
73 | /// # Errors
74 | ///
75 | /// It returns an error if the content is not valid.
76 | pub fn from_creds(contents: &str) -> Result {
77 | let mut jtw = None;
78 | let mut secret = None;
79 |
80 | let mut lines = contents.lines();
81 | while let Some(line) = lines.next() {
82 | if line == "-----BEGIN NATS USER JWT-----" {
83 | jtw = Some(lines.next().ok_or(CredsParseError::Truncated)?);
84 |
85 | let line = lines.next().ok_or(CredsParseError::Truncated)?;
86 | if line != "------END NATS USER JWT------" {
87 | return Err(CredsParseError::MissingJwtClosing);
88 | }
89 | } else if line == "-----BEGIN USER NKEY SEED-----" {
90 | secret = Some(lines.next().ok_or(CredsParseError::Truncated)?);
91 |
92 | let line = lines.next().ok_or(CredsParseError::Truncated)?;
93 | if line != "------END USER NKEY SEED------" {
94 | return Err(CredsParseError::MissingNkeyClosing);
95 | }
96 | }
97 | }
98 |
99 | let jtw = jtw.ok_or(CredsParseError::MissingJwt)?;
100 | let nkey = secret.ok_or(CredsParseError::MissingNkey)?;
101 | let nkey = KeyPair::from_encoded_seed(nkey).map_err(CredsParseError::InvalidKey)?;
102 |
103 | Ok(Self::Creds {
104 | jwt: jtw.to_owned(),
105 | nkey,
106 | })
107 | }
108 | }
109 |
110 | impl Debug for AuthenticationMethod {
111 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
112 | f.debug_struct("AuthenticationMethod")
113 | .finish_non_exhaustive()
114 | }
115 | }
116 |
117 | #[cfg(test)]
118 | mod tests {
119 | use super::AuthenticationMethod;
120 |
121 | #[test]
122 | fn parse_creds() {
123 | let creds = r"-----BEGIN NATS USER JWT-----
124 | eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJUVlNNTEtTWkJBN01VWDNYQUxNUVQzTjRISUw1UkZGQU9YNUtaUFhEU0oyWlAzNkVMNVJBIiwiaWF0IjoxNTU4MDQ1NTYyLCJpc3MiOiJBQlZTQk0zVTQ1REdZRVVFQ0tYUVM3QkVOSFdHN0tGUVVEUlRFSEFKQVNPUlBWV0JaNEhPSUtDSCIsIm5hbWUiOiJvbWVnYSIsInN1YiI6IlVEWEIyVk1MWFBBU0FKN1pEVEtZTlE3UU9DRldTR0I0Rk9NWVFRMjVIUVdTQUY3WlFKRUJTUVNXIiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.6TQ2ilCDb6m2ZDiJuj_D_OePGXFyN3Ap2DEm3ipcU5AhrWrNvneJryWrpgi_yuVWKo1UoD5s8bxlmwypWVGFAA
125 | ------END NATS USER JWT------
126 |
127 | ************************* IMPORTANT *************************
128 | NKEY Seed printed below can be used to sign and prove identity.
129 | NKEYs are sensitive and should be treated as secrets.
130 |
131 | -----BEGIN USER NKEY SEED-----
132 | SUAOY5JZ2WJKVR4UO2KJ2P3SW6FZFNWEOIMAXF4WZEUNVQXXUOKGM55CYE
133 | ------END USER NKEY SEED------
134 |
135 | *************************************************************";
136 |
137 | let AuthenticationMethod::Creds { jwt, nkey } =
138 | AuthenticationMethod::from_creds(creds).unwrap()
139 | else {
140 | panic!("invalid auth method");
141 | };
142 | assert_eq!(
143 | jwt,
144 | "eyJ0eXAiOiJqd3QiLCJhbGciOiJlZDI1NTE5In0.eyJqdGkiOiJUVlNNTEtTWkJBN01VWDNYQUxNUVQzTjRISUw1UkZGQU9YNUtaUFhEU0oyWlAzNkVMNVJBIiwiaWF0IjoxNTU4MDQ1NTYyLCJpc3MiOiJBQlZTQk0zVTQ1REdZRVVFQ0tYUVM3QkVOSFdHN0tGUVVEUlRFSEFKQVNPUlBWV0JaNEhPSUtDSCIsIm5hbWUiOiJvbWVnYSIsInN1YiI6IlVEWEIyVk1MWFBBU0FKN1pEVEtZTlE3UU9DRldTR0I0Rk9NWVFRMjVIUVdTQUY3WlFKRUJTUVNXIiwidHlwZSI6InVzZXIiLCJuYXRzIjp7InB1YiI6e30sInN1YiI6e319fQ.6TQ2ilCDb6m2ZDiJuj_D_OePGXFyN3Ap2DEm3ipcU5AhrWrNvneJryWrpgi_yuVWKo1UoD5s8bxlmwypWVGFAA"
145 | );
146 | assert_eq!(
147 | nkey.public_key().to_string(),
148 | "SAAO4HKVRO54CIBH7EONLBWD6BYIW2IYHQVZTCCDLU6C2IAX7GBEQGJDYE"
149 | );
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/watermelon-mini/src/proto/connection/compression.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | io,
3 | pin::Pin,
4 | task::{Context, Poll},
5 | };
6 |
7 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
8 |
9 | #[cfg(feature = "non-standard-zstd")]
10 | use crate::non_standard_zstd::ZstdStream;
11 |
12 | #[derive(Debug)]
13 | pub enum ConnectionCompression {
14 | Plain(S),
15 | #[cfg(feature = "non-standard-zstd")]
16 | Zstd(ZstdStream),
17 | }
18 |
19 | impl ConnectionCompression
20 | where
21 | S: AsyncRead + AsyncWrite + Unpin,
22 | {
23 | #[cfg(feature = "non-standard-zstd")]
24 | pub(crate) fn upgrade_zstd(self, compression_level: u8) -> Self {
25 | let Self::Plain(socket) = self else {
26 | unreachable!()
27 | };
28 |
29 | Self::Zstd(ZstdStream::new(socket, compression_level))
30 | }
31 |
32 | #[cfg(feature = "non-standard-zstd")]
33 | pub fn is_zstd_compressed(&self) -> bool {
34 | matches!(self, Self::Zstd(_))
35 | }
36 | }
37 |
38 | impl AsyncRead for ConnectionCompression
39 | where
40 | S: AsyncRead + AsyncWrite + Unpin,
41 | {
42 | fn poll_read(
43 | self: Pin<&mut Self>,
44 | cx: &mut Context<'_>,
45 | buf: &mut ReadBuf<'_>,
46 | ) -> Poll> {
47 | match self.get_mut() {
48 | Self::Plain(conn) => Pin::new(conn).poll_read(cx, buf),
49 | #[cfg(feature = "non-standard-zstd")]
50 | Self::Zstd(conn) => Pin::new(conn).poll_read(cx, buf),
51 | }
52 | }
53 | }
54 |
55 | impl AsyncWrite for ConnectionCompression
56 | where
57 | S: AsyncRead + AsyncWrite + Unpin,
58 | {
59 | fn poll_write(
60 | self: Pin<&mut Self>,
61 | cx: &mut Context<'_>,
62 | buf: &[u8],
63 | ) -> Poll> {
64 | match self.get_mut() {
65 | Self::Plain(conn) => Pin::new(conn).poll_write(cx, buf),
66 | #[cfg(feature = "non-standard-zstd")]
67 | Self::Zstd(conn) => Pin::new(conn).poll_write(cx, buf),
68 | }
69 | }
70 |
71 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
72 | match self.get_mut() {
73 | Self::Plain(conn) => Pin::new(conn).poll_flush(cx),
74 | #[cfg(feature = "non-standard-zstd")]
75 | Self::Zstd(conn) => Pin::new(conn).poll_flush(cx),
76 | }
77 | }
78 |
79 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
80 | match self.get_mut() {
81 | Self::Plain(conn) => Pin::new(conn).poll_shutdown(cx),
82 | #[cfg(feature = "non-standard-zstd")]
83 | Self::Zstd(conn) => Pin::new(conn).poll_shutdown(cx),
84 | }
85 | }
86 |
87 | fn poll_write_vectored(
88 | self: Pin<&mut Self>,
89 | cx: &mut Context<'_>,
90 | bufs: &[io::IoSlice<'_>],
91 | ) -> Poll> {
92 | match self.get_mut() {
93 | Self::Plain(conn) => Pin::new(conn).poll_write_vectored(cx, bufs),
94 | #[cfg(feature = "non-standard-zstd")]
95 | Self::Zstd(conn) => Pin::new(conn).poll_write_vectored(cx, bufs),
96 | }
97 | }
98 |
99 | fn is_write_vectored(&self) -> bool {
100 | match self {
101 | Self::Plain(conn) => conn.is_write_vectored(),
102 | #[cfg(feature = "non-standard-zstd")]
103 | Self::Zstd(conn) => conn.is_write_vectored(),
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/watermelon-mini/src/proto/connection/mod.rs:
--------------------------------------------------------------------------------
1 | pub use self::compression::ConnectionCompression;
2 | pub use self::security::ConnectionSecurity;
3 |
4 | mod compression;
5 | mod security;
6 |
--------------------------------------------------------------------------------
/watermelon-mini/src/proto/connection/security.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | io,
3 | pin::Pin,
4 | task::{Context, Poll},
5 | };
6 |
7 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
8 | use tokio_rustls::{TlsConnector, client::TlsStream, rustls::pki_types::ServerName};
9 |
10 | #[derive(Debug)]
11 | #[expect(
12 | clippy::large_enum_variant,
13 | reason = "using TLS is the recommended thing, we do not want to affect it"
14 | )]
15 | pub enum ConnectionSecurity {
16 | Plain(S),
17 | Tls(TlsStream),
18 | }
19 |
20 | impl ConnectionSecurity
21 | where
22 | S: AsyncRead + AsyncWrite + Unpin,
23 | {
24 | pub(crate) async fn upgrade_tls(
25 | self,
26 | connector: &TlsConnector,
27 | domain: ServerName<'static>,
28 | ) -> io::Result {
29 | let conn = match self {
30 | Self::Plain(conn) => conn,
31 | Self::Tls(_) => unreachable!("trying to upgrade to Tls a Tls connection"),
32 | };
33 |
34 | let conn = connector.connect(domain, conn).await?;
35 | Ok(Self::Tls(conn))
36 | }
37 | }
38 |
39 | impl AsyncRead for ConnectionSecurity
40 | where
41 | S: AsyncRead + AsyncWrite + Unpin,
42 | {
43 | fn poll_read(
44 | self: Pin<&mut Self>,
45 | cx: &mut Context<'_>,
46 | buf: &mut ReadBuf<'_>,
47 | ) -> Poll> {
48 | match self.get_mut() {
49 | Self::Plain(conn) => Pin::new(conn).poll_read(cx, buf),
50 | Self::Tls(conn) => Pin::new(conn).poll_read(cx, buf),
51 | }
52 | }
53 | }
54 |
55 | impl AsyncWrite for ConnectionSecurity
56 | where
57 | S: AsyncRead + AsyncWrite + Unpin,
58 | {
59 | fn poll_write(
60 | self: Pin<&mut Self>,
61 | cx: &mut Context<'_>,
62 | buf: &[u8],
63 | ) -> Poll> {
64 | match self.get_mut() {
65 | Self::Plain(conn) => Pin::new(conn).poll_write(cx, buf),
66 | Self::Tls(conn) => Pin::new(conn).poll_write(cx, buf),
67 | }
68 | }
69 |
70 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
71 | match self.get_mut() {
72 | Self::Plain(conn) => Pin::new(conn).poll_flush(cx),
73 | Self::Tls(conn) => Pin::new(conn).poll_flush(cx),
74 | }
75 | }
76 |
77 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
78 | match self.get_mut() {
79 | Self::Plain(conn) => Pin::new(conn).poll_shutdown(cx),
80 | Self::Tls(conn) => Pin::new(conn).poll_shutdown(cx),
81 | }
82 | }
83 |
84 | fn poll_write_vectored(
85 | self: Pin<&mut Self>,
86 | cx: &mut Context<'_>,
87 | bufs: &[io::IoSlice<'_>],
88 | ) -> Poll> {
89 | match self.get_mut() {
90 | Self::Plain(conn) => Pin::new(conn).poll_write_vectored(cx, bufs),
91 | Self::Tls(conn) => Pin::new(conn).poll_write_vectored(cx, bufs),
92 | }
93 | }
94 |
95 | fn is_write_vectored(&self) -> bool {
96 | match self {
97 | Self::Plain(conn) => conn.is_write_vectored(),
98 | Self::Tls(conn) => conn.is_write_vectored(),
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/watermelon-mini/src/proto/connector.rs:
--------------------------------------------------------------------------------
1 | use std::io;
2 |
3 | use tokio::net::TcpStream;
4 | use tokio_rustls::{
5 | TlsConnector,
6 | rustls::{
7 | self,
8 | pki_types::{InvalidDnsNameError, ServerName},
9 | },
10 | };
11 | use watermelon_net::{
12 | Connection, StreamingConnection, connect_tcp,
13 | error::{ConnectionReadError, StreamingReadError},
14 | proto_connect,
15 | };
16 | #[cfg(feature = "websocket")]
17 | use watermelon_net::{WebsocketConnection, error::WebsocketReadError};
18 | #[cfg(feature = "websocket")]
19 | use watermelon_proto::proto::error::FrameDecoderError;
20 | use watermelon_proto::{
21 | Connect, Host, NonStandardConnect, Protocol, ServerAddr, ServerInfo, Transport,
22 | proto::{ServerOp, error::DecoderError},
23 | };
24 |
25 | use crate::{ConnectFlags, ConnectionCompression, util::MaybeConnection};
26 |
27 | use super::{
28 | authenticator::{AuthenticationError, AuthenticationMethod},
29 | connection::ConnectionSecurity,
30 | };
31 |
32 | #[derive(Debug, thiserror::Error)]
33 | pub enum ConnectError {
34 | #[error("io error")]
35 | Io(#[source] io::Error),
36 | #[error("TLS error")]
37 | Tls(rustls::Error),
38 | #[error("invalid DNS name")]
39 | InvalidDnsName(#[source] InvalidDnsNameError),
40 | #[error("websocket not supported")]
41 | WebsocketUnsupported,
42 | #[error("unexpected ServerOp")]
43 | UnexpectedServerOp,
44 | #[error("decoder error")]
45 | Decoder(#[source] DecoderError),
46 | #[error("authentication error")]
47 | Authentication(#[source] AuthenticationError),
48 | #[error("connect")]
49 | Connect(#[source] watermelon_net::error::ConnectError),
50 | }
51 |
52 | #[expect(clippy::too_many_lines)]
53 | pub(crate) async fn connect(
54 | connector: &TlsConnector,
55 | addr: &ServerAddr,
56 | client_name: String,
57 | auth_method: Option<&AuthenticationMethod>,
58 | flags: ConnectFlags,
59 | ) -> Result<
60 | (
61 | Connection<
62 | ConnectionCompression>,
63 | ConnectionSecurity,
64 | >,
65 | Box,
66 | ),
67 | ConnectError,
68 | > {
69 | let conn = connect_tcp(addr).await.map_err(ConnectError::Io)?;
70 | conn.set_nodelay(flags.tcp_nodelay)
71 | .map_err(ConnectError::Io)?;
72 | let mut conn = ConnectionSecurity::Plain(conn);
73 |
74 | if matches!(addr.protocol(), Protocol::TLS) {
75 | let domain = rustls_server_name_from_addr(addr).map_err(ConnectError::InvalidDnsName)?;
76 | conn = conn
77 | .upgrade_tls(connector, domain.to_owned())
78 | .await
79 | .map_err(ConnectError::Io)?;
80 | }
81 |
82 | let mut conn = match addr.transport() {
83 | Transport::TCP => Connection::Streaming(StreamingConnection::new(conn)),
84 | #[cfg(feature = "websocket")]
85 | Transport::Websocket => {
86 | let uri = addr.to_string().parse().unwrap();
87 | Connection::Websocket(
88 | WebsocketConnection::new(uri, conn)
89 | .await
90 | .map_err(ConnectError::Io)?,
91 | )
92 | }
93 | #[cfg(not(feature = "websocket"))]
94 | Transport::Websocket => return Err(ConnectError::WebsocketUnsupported),
95 | };
96 | let info = match conn.read_next().await {
97 | Ok(ServerOp::Info { info }) => info,
98 | Ok(_) => return Err(ConnectError::UnexpectedServerOp),
99 | Err(ConnectionReadError::Streaming(StreamingReadError::Io(err))) => {
100 | return Err(ConnectError::Io(err));
101 | }
102 | Err(ConnectionReadError::Streaming(StreamingReadError::Decoder(err))) => {
103 | return Err(ConnectError::Decoder(err));
104 | }
105 | #[cfg(feature = "websocket")]
106 | Err(ConnectionReadError::Websocket(WebsocketReadError::Io(err))) => {
107 | return Err(ConnectError::Io(err));
108 | }
109 | #[cfg(feature = "websocket")]
110 | Err(ConnectionReadError::Websocket(WebsocketReadError::Decoder(
111 | FrameDecoderError::Decoder(err),
112 | ))) => return Err(ConnectError::Decoder(err)),
113 | #[cfg(feature = "websocket")]
114 | Err(ConnectionReadError::Websocket(WebsocketReadError::Decoder(
115 | FrameDecoderError::IncompleteFrame,
116 | ))) => todo!(),
117 | #[cfg(feature = "websocket")]
118 | Err(ConnectionReadError::Websocket(WebsocketReadError::Closed)) => todo!(),
119 | };
120 |
121 | let conn = match conn {
122 | Connection::Streaming(streaming) => Connection::Streaming(
123 | if matches!(
124 | (addr.protocol(), info.tls_required),
125 | (Protocol::PossiblyPlain, true)
126 | ) {
127 | let domain =
128 | rustls_server_name_from_addr(addr).map_err(ConnectError::InvalidDnsName)?;
129 | StreamingConnection::new(
130 | streaming
131 | .into_inner()
132 | .upgrade_tls(connector, domain.to_owned())
133 | .await
134 | .map_err(ConnectError::Io)?,
135 | )
136 | } else {
137 | streaming
138 | },
139 | ),
140 | Connection::Websocket(websocket) => Connection::Websocket(websocket),
141 | };
142 |
143 | let auth;
144 | let auth_method = if let Some(auth_method) = auth_method {
145 | Some(auth_method)
146 | } else if let Some(auth_method) = AuthenticationMethod::try_from_addr(addr) {
147 | auth = auth_method;
148 | Some(&auth)
149 | } else {
150 | None
151 | };
152 |
153 | #[allow(unused_mut)]
154 | let mut non_standard = NonStandardConnect::default();
155 | #[cfg(feature = "non-standard-zstd")]
156 | if matches!(conn, Connection::Streaming(_)) {
157 | non_standard.zstd = flags.zstd_compression_level.is_some() && info.non_standard.zstd;
158 | }
159 |
160 | let mut connect = Connect {
161 | verbose: true,
162 | pedantic: false,
163 | require_tls: false,
164 | auth_token: None,
165 | username: None,
166 | password: None,
167 | client_name: Some(client_name),
168 | client_lang: "rust-watermelon",
169 | client_version: env!("CARGO_PKG_VERSION"),
170 | protocol: 1,
171 | echo: flags.echo,
172 | signature: None,
173 | jwt: None,
174 | supports_no_responders: true,
175 | supports_headers: true,
176 | nkey: None,
177 | non_standard,
178 | };
179 | if let Some(auth_method) = auth_method {
180 | auth_method
181 | .prepare_for_auth(&info, &mut connect)
182 | .map_err(ConnectError::Authentication)?;
183 | }
184 |
185 | let mut conn = match conn {
186 | Connection::Streaming(streaming) => {
187 | Connection::Streaming(streaming.replace_socket(|stream| {
188 | MaybeConnection(Some(ConnectionCompression::Plain(stream)))
189 | }))
190 | }
191 | Connection::Websocket(websocket) => Connection::Websocket(websocket),
192 | };
193 |
194 | #[cfg(feature = "non-standard-zstd")]
195 | let zstd = connect.non_standard.zstd;
196 |
197 | proto_connect(&mut conn, connect, |conn| {
198 | #[cfg(feature = "non-standard-zstd")]
199 | match conn {
200 | Connection::Streaming(streaming) => {
201 | if zstd {
202 | if let Some(zstd_compression_level) = flags.zstd_compression_level {
203 | let stream = streaming.socket_mut().0.take().unwrap();
204 | streaming.socket_mut().0 =
205 | Some(stream.upgrade_zstd(zstd_compression_level));
206 | }
207 | }
208 | }
209 | Connection::Websocket(_websocket) => {}
210 | }
211 |
212 | let _ = conn;
213 | })
214 | .await
215 | .map_err(ConnectError::Connect)?;
216 |
217 | let conn = match conn {
218 | Connection::Streaming(streaming) => {
219 | Connection::Streaming(streaming.replace_socket(|stream| stream.0.unwrap()))
220 | }
221 | Connection::Websocket(websocket) => Connection::Websocket(websocket),
222 | };
223 |
224 | Ok((conn, info))
225 | }
226 |
227 | fn rustls_server_name_from_addr(addr: &ServerAddr) -> Result, InvalidDnsNameError> {
228 | match addr.host() {
229 | Host::Ip(addr) => Ok(ServerName::IpAddress((*addr).into())),
230 | Host::Dns(name) => <_ as AsRef>::as_ref(name).try_into(),
231 | }
232 | }
233 |
--------------------------------------------------------------------------------
/watermelon-mini/src/proto/mod.rs:
--------------------------------------------------------------------------------
1 | pub use self::authenticator::AuthenticationMethod;
2 | pub use self::connection::{ConnectionCompression, ConnectionSecurity};
3 | pub use self::connector::ConnectError;
4 | pub(crate) use self::connector::connect;
5 |
6 | mod authenticator;
7 | mod connection;
8 | mod connector;
9 |
--------------------------------------------------------------------------------
/watermelon-mini/src/util.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | io,
3 | pin::Pin,
4 | task::{Context, Poll},
5 | };
6 |
7 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
8 |
9 | #[derive(Debug)]
10 | pub(crate) struct MaybeConnection(pub(crate) Option);
11 |
12 | impl AsyncRead for MaybeConnection
13 | where
14 | S: AsyncRead + Unpin,
15 | {
16 | fn poll_read(
17 | mut self: Pin<&mut Self>,
18 | cx: &mut Context<'_>,
19 | buf: &mut ReadBuf<'_>,
20 | ) -> Poll> {
21 | Pin::new(self.0.as_mut().unwrap()).poll_read(cx, buf)
22 | }
23 | }
24 |
25 | impl AsyncWrite for MaybeConnection
26 | where
27 | S: AsyncWrite + Unpin,
28 | {
29 | fn poll_write(
30 | mut self: Pin<&mut Self>,
31 | cx: &mut Context<'_>,
32 | buf: &[u8],
33 | ) -> Poll> {
34 | Pin::new(self.0.as_mut().unwrap()).poll_write(cx, buf)
35 | }
36 |
37 | fn poll_write_vectored(
38 | mut self: Pin<&mut Self>,
39 | cx: &mut Context<'_>,
40 | bufs: &[io::IoSlice<'_>],
41 | ) -> Poll> {
42 | Pin::new(self.0.as_mut().unwrap()).poll_write_vectored(cx, bufs)
43 | }
44 |
45 | fn is_write_vectored(&self) -> bool {
46 | self.0.as_ref().unwrap().is_write_vectored()
47 | }
48 |
49 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
50 | Pin::new(self.0.as_mut().unwrap()).poll_flush(cx)
51 | }
52 |
53 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> {
54 | Pin::new(self.0.as_mut().unwrap()).poll_shutdown(cx)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/watermelon-net/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "watermelon-net"
3 | version = "0.2.3"
4 | description = "Low-level NATS Core network implementation"
5 | categories = ["api-bindings", "network-programming"]
6 | keywords = ["nats", "client"]
7 | edition.workspace = true
8 | license.workspace = true
9 | repository.workspace = true
10 | rust-version.workspace = true
11 |
12 | [package.metadata.docs.rs]
13 | features = ["websocket", "non-standard-zstd"]
14 |
15 | [dependencies]
16 | tokio = { version = "1.36", features = ["rt", "net", "time", "io-util"] }
17 | futures-core = "0.3.14"
18 | bytes = "1"
19 |
20 | tokio-websockets = { version = "0.12", features = ["client"], optional = true }
21 | futures-sink = { version = "0.3.14", default-features = false, optional = true }
22 | http = { version = "1", optional = true }
23 |
24 | watermelon-proto = { version = "0.1.3", path = "../watermelon-proto" }
25 |
26 | thiserror = "2"
27 | pin-project-lite = "0.2.15"
28 |
29 | [dev-dependencies]
30 | tokio = { version = "1", features = ["macros"] }
31 | futures-util = { version = "0.3.14", default-features = false }
32 | claims = "0.8"
33 |
34 | [features]
35 | default = ["aws-lc-rs", "rand"]
36 | websocket = ["dep:tokio-websockets", "dep:futures-sink", "dep:http"]
37 | ring = ["tokio-websockets?/ring"]
38 | aws-lc-rs = ["tokio-websockets?/aws_lc_rs"]
39 | fips = []
40 | rand = ["tokio-websockets?/rand"]
41 | getrandom = ["tokio-websockets?/getrandom"]
42 | non-standard-zstd = ["watermelon-proto/non-standard-zstd"]
43 |
44 | [lints]
45 | workspace = true
46 |
--------------------------------------------------------------------------------
/watermelon-net/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/watermelon-net/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/watermelon-net/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/watermelon-net/src/connection/streaming.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | future::{self, Future},
3 | io,
4 | pin::{Pin, pin},
5 | task::{Context, Poll},
6 | };
7 |
8 | use bytes::Buf;
9 | use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite};
10 | use watermelon_proto::proto::{
11 | ClientOp, ServerOp, StreamDecoder, StreamEncoder, error::DecoderError,
12 | };
13 |
14 | #[derive(Debug)]
15 | pub struct StreamingConnection {
16 | socket: S,
17 | encoder: StreamEncoder,
18 | decoder: StreamDecoder,
19 | may_flush: bool,
20 | }
21 |
22 | impl StreamingConnection
23 | where
24 | S: AsyncRead + AsyncWrite + Unpin,
25 | {
26 | #[must_use]
27 | pub fn new(socket: S) -> Self {
28 | Self {
29 | socket,
30 | encoder: StreamEncoder::new(),
31 | decoder: StreamDecoder::new(),
32 | may_flush: false,
33 | }
34 | }
35 |
36 | pub fn poll_read_next(
37 | &mut self,
38 | cx: &mut Context<'_>,
39 | ) -> Poll> {
40 | loop {
41 | match self.decoder.decode() {
42 | Ok(Some(server_op)) => return Poll::Ready(Ok(server_op)),
43 | Ok(None) => {}
44 | Err(err) => return Poll::Ready(Err(StreamingReadError::Decoder(err))),
45 | }
46 |
47 | let read_buf_fut = pin!(self.socket.read_buf(self.decoder.read_buf()));
48 | match read_buf_fut.poll(cx) {
49 | Poll::Pending => return Poll::Pending,
50 | Poll::Ready(Ok(1..)) => {}
51 | Poll::Ready(Ok(0)) => {
52 | return Poll::Ready(Err(StreamingReadError::Io(
53 | io::ErrorKind::UnexpectedEof.into(),
54 | )));
55 | }
56 | Poll::Ready(Err(err)) => return Poll::Ready(Err(StreamingReadError::Io(err))),
57 | }
58 | }
59 | }
60 |
61 | /// Reads the next [`ServerOp`].
62 | ///
63 | /// # Errors
64 | ///
65 | /// It returns an error if the content cannot be decoded or if an I/O error occurs.
66 | pub async fn read_next(&mut self) -> Result {
67 | future::poll_fn(|cx| self.poll_read_next(cx)).await
68 | }
69 |
70 | pub fn may_write(&self) -> bool {
71 | self.encoder.has_remaining()
72 | }
73 |
74 | pub fn may_flush(&self) -> bool {
75 | self.may_flush
76 | }
77 |
78 | pub fn may_enqueue_more_ops(&self) -> bool {
79 | self.encoder.remaining() < 8_290_304
80 | }
81 |
82 | pub fn enqueue_write_op(&mut self, item: &ClientOp) {
83 | self.encoder.enqueue_write_op(item);
84 | }
85 |
86 | pub fn poll_write_next(&mut self, cx: &mut Context<'_>) -> Poll> {
87 | let remaining = self.encoder.remaining();
88 | if remaining == 0 {
89 | return Poll::Ready(Ok(0));
90 | }
91 |
92 | let chunk = self.encoder.chunk();
93 | let write_outcome = if chunk.len() < remaining && self.socket.is_write_vectored() {
94 | let mut bufs = [io::IoSlice::new(&[]); 64];
95 | let n = self.encoder.chunks_vectored(&mut bufs);
96 | debug_assert!(
97 | n >= 2,
98 | "perf: chunks_vectored yielded less than 2 chunks despite the apparently fragmented internal encoder representation"
99 | );
100 |
101 | Pin::new(&mut self.socket).poll_write_vectored(cx, &bufs[..n])
102 | } else {
103 | debug_assert!(
104 | !chunk.is_empty(),
105 | "perf: chunk shouldn't be empty given that `remaining > 0`"
106 | );
107 | Pin::new(&mut self.socket).poll_write(cx, chunk)
108 | };
109 |
110 | match write_outcome {
111 | Poll::Pending => {
112 | self.may_flush = false;
113 | Poll::Pending
114 | }
115 | Poll::Ready(Ok(n)) => {
116 | self.encoder.advance(n);
117 | self.may_flush = true;
118 | Poll::Ready(Ok(n))
119 | }
120 | Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
121 | }
122 | }
123 |
124 | /// Writes the next chunk of data to the socket.
125 | ///
126 | /// It returns the number of bytes that have been written.
127 | ///
128 | /// # Errors
129 | ///
130 | /// An I/O error is returned if it is not possible to write to the socket.
131 | pub async fn write_next(&mut self) -> io::Result {
132 | future::poll_fn(|cx| self.poll_write_next(cx)).await
133 | }
134 |
135 | pub fn poll_flush(&mut self, cx: &mut Context<'_>) -> Poll> {
136 | match Pin::new(&mut self.socket).poll_flush(cx) {
137 | Poll::Pending => Poll::Pending,
138 | Poll::Ready(Ok(())) => {
139 | self.may_flush = false;
140 | Poll::Ready(Ok(()))
141 | }
142 | Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
143 | }
144 | }
145 |
146 | /// Flush any buffered writes to the connection
147 | ///
148 | /// # Errors
149 | ///
150 | /// Returns an error if flushing fails
151 | pub async fn flush(&mut self) -> io::Result<()> {
152 | future::poll_fn(|cx| self.poll_flush(cx)).await
153 | }
154 |
155 | /// Shutdown the connection
156 | ///
157 | /// # Errors
158 | ///
159 | /// Returns an error if shutting down the connection fails.
160 | /// Implementations usually ignore this error.
161 | pub async fn shutdown(&mut self) -> io::Result<()> {
162 | future::poll_fn(|cx| Pin::new(&mut self.socket).poll_shutdown(cx)).await
163 | }
164 |
165 | pub fn socket(&self) -> &S {
166 | &self.socket
167 | }
168 |
169 | pub fn socket_mut(&mut self) -> &mut S {
170 | &mut self.socket
171 | }
172 |
173 | pub fn replace_socket(self, replacer: F) -> StreamingConnection
174 | where
175 | F: FnOnce(S) -> S2,
176 | {
177 | StreamingConnection {
178 | socket: replacer(self.socket),
179 | encoder: self.encoder,
180 | decoder: self.decoder,
181 | may_flush: self.may_flush,
182 | }
183 | }
184 |
185 | pub fn into_inner(self) -> S {
186 | self.socket
187 | }
188 | }
189 |
190 | #[derive(Debug, thiserror::Error)]
191 | pub enum StreamingReadError {
192 | #[error("decoder")]
193 | Decoder(#[source] DecoderError),
194 | #[error("io")]
195 | Io(#[source] io::Error),
196 | }
197 |
198 | #[cfg(test)]
199 | mod tests {
200 | use std::{
201 | pin::Pin,
202 | task::{Context, Poll},
203 | };
204 |
205 | use claims::assert_matches;
206 | use futures_util::task;
207 | use tokio::io::{self, AsyncRead, AsyncWrite, ReadBuf};
208 | use watermelon_proto::proto::{ClientOp, ServerOp};
209 |
210 | use super::StreamingConnection;
211 |
212 | #[test]
213 | fn ping_pong() {
214 | let waker = task::noop_waker();
215 | let mut cx = Context::from_waker(&waker);
216 |
217 | let (socket, mut conn) = io::duplex(1024);
218 |
219 | let mut client = StreamingConnection::new(socket);
220 |
221 | // Initial state is ok
222 | assert!(client.poll_read_next(&mut cx).is_pending());
223 | assert_matches!(client.poll_write_next(&mut cx), Poll::Ready(Ok(0)));
224 |
225 | let mut buf = [0; 1024];
226 | let mut read_buf = ReadBuf::new(&mut buf);
227 | assert!(
228 | Pin::new(&mut conn)
229 | .poll_read(&mut cx, &mut read_buf)
230 | .is_pending()
231 | );
232 |
233 | // Write PING and verify it was received
234 | client.enqueue_write_op(&ClientOp::Ping);
235 | assert_matches!(client.poll_write_next(&mut cx), Poll::Ready(Ok(6)));
236 | assert_matches!(
237 | Pin::new(&mut conn).poll_read(&mut cx, &mut read_buf),
238 | Poll::Ready(Ok(()))
239 | );
240 | assert_eq!(read_buf.filled(), b"PING\r\n");
241 |
242 | // Receive PONG
243 | assert_matches!(
244 | Pin::new(&mut conn).poll_write(&mut cx, b"PONG\r\n"),
245 | Poll::Ready(Ok(6))
246 | );
247 | assert_matches!(
248 | client.poll_read_next(&mut cx),
249 | Poll::Ready(Ok(ServerOp::Pong))
250 | );
251 | assert!(client.poll_read_next(&mut cx).is_pending());
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/watermelon-net/src/connection/websocket.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | future, io,
3 | pin::Pin,
4 | task::{Context, Poll, Waker},
5 | };
6 |
7 | use bytes::Bytes;
8 | use futures_core::Stream as _;
9 | use futures_sink::Sink;
10 | use http::Uri;
11 | use tokio::io::{AsyncRead, AsyncWrite};
12 | use tokio_websockets::{ClientBuilder, Message, WebSocketStream};
13 | use watermelon_proto::proto::{
14 | ClientOp, FramedEncoder, ServerOp, decode_frame, error::FrameDecoderError,
15 | };
16 |
17 | #[derive(Debug)]
18 | pub struct WebsocketConnection {
19 | socket: WebSocketStream,
20 | encoder: FramedEncoder,
21 | residual_frame: Bytes,
22 | should_flush: bool,
23 | }
24 |
25 | impl WebsocketConnection
26 | where
27 | S: AsyncRead + AsyncWrite + Unpin,
28 | {
29 | /// Construct a websocket stream to a pre-established connection `socket`.
30 | ///
31 | /// # Errors
32 | ///
33 | /// Returns an error if the websocket handshake fails.
34 | pub async fn new(uri: Uri, socket: S) -> io::Result {
35 | let (socket, _resp) = ClientBuilder::from_uri(uri)
36 | .connect_on(socket)
37 | .await
38 | .map_err(websockets_error_to_io)?;
39 | Ok(Self {
40 | socket,
41 | encoder: FramedEncoder::new(),
42 | residual_frame: Bytes::new(),
43 | should_flush: false,
44 | })
45 | }
46 |
47 | pub fn poll_read_next(
48 | &mut self,
49 | cx: &mut Context<'_>,
50 | ) -> Poll> {
51 | loop {
52 | if !self.residual_frame.is_empty() {
53 | return Poll::Ready(
54 | decode_frame(&mut self.residual_frame).map_err(WebsocketReadError::Decoder),
55 | );
56 | }
57 |
58 | match Pin::new(&mut self.socket).poll_next(cx) {
59 | Poll::Pending => return Poll::Pending,
60 | Poll::Ready(Some(Ok(message))) if message.is_binary() => {
61 | self.residual_frame = message.into_payload().into();
62 | }
63 | Poll::Ready(Some(Ok(_message))) => {}
64 | Poll::Ready(Some(Err(err))) => {
65 | return Poll::Ready(Err(WebsocketReadError::Io(websockets_error_to_io(err))));
66 | }
67 | Poll::Ready(None) => return Poll::Ready(Err(WebsocketReadError::Closed)),
68 | }
69 | }
70 | }
71 |
72 | /// Reads the next [`ServerOp`].
73 | ///
74 | /// # Errors
75 | ///
76 | /// It returns an error if the content cannot be decoded or if an I/O error occurs.
77 | pub async fn read_next(&mut self) -> Result {
78 | future::poll_fn(|cx| self.poll_read_next(cx)).await
79 | }
80 |
81 | pub fn should_flush(&self) -> bool {
82 | self.should_flush
83 | }
84 |
85 | pub fn may_enqueue_more_ops(&mut self) -> bool {
86 | let mut cx = Context::from_waker(Waker::noop());
87 | Pin::new(&mut self.socket).poll_ready(&mut cx).is_ready()
88 | }
89 |
90 | /// Enqueue `item` to be written.
91 | #[expect(clippy::missing_panics_doc)]
92 | pub fn enqueue_write_op(&mut self, item: &ClientOp) {
93 | let payload = self.encoder.encode(item);
94 | Pin::new(&mut self.socket)
95 | .start_send(Message::binary(payload))
96 | .unwrap();
97 | self.should_flush = true;
98 | }
99 |
100 | pub fn poll_flush(&mut self, cx: &mut Context<'_>) -> Poll> {
101 | Pin::new(&mut self.socket)
102 | .poll_flush(cx)
103 | .map_err(websockets_error_to_io)
104 | }
105 |
106 | /// Flush any buffered writes to the connection
107 | ///
108 | /// # Errors
109 | ///
110 | /// Returns an error if flushing fails
111 | pub async fn flush(&mut self) -> io::Result<()> {
112 | future::poll_fn(|cx| self.poll_flush(cx)).await
113 | }
114 |
115 | /// Shutdown the connection
116 | ///
117 | /// # Errors
118 | ///
119 | /// Returns an error if shutting down the connection fails.
120 | /// Implementations usually ignore this error.
121 | pub async fn shutdown(&mut self) -> io::Result<()> {
122 | future::poll_fn(|cx| Pin::new(&mut self.socket).poll_close(cx))
123 | .await
124 | .map_err(websockets_error_to_io)
125 | }
126 | }
127 |
128 | #[derive(Debug, thiserror::Error)]
129 | pub enum WebsocketReadError {
130 | #[error("decoder")]
131 | Decoder(#[source] FrameDecoderError),
132 | #[error("io")]
133 | Io(#[source] io::Error),
134 | #[error("closed")]
135 | Closed,
136 | }
137 |
138 | fn websockets_error_to_io(err: tokio_websockets::Error) -> io::Error {
139 | match err {
140 | tokio_websockets::Error::Io(err) => err,
141 | err => io::Error::other(err),
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/watermelon-net/src/future.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | pin::Pin,
3 | task::{Context, Poll},
4 | };
5 |
6 | use futures_core::Stream;
7 |
8 | #[derive(Debug, Clone)]
9 | pub(crate) struct IterToStream {
10 | pub(crate) iter: I,
11 | }
12 |
13 | impl Unpin for IterToStream {}
14 |
15 | impl Stream for IterToStream {
16 | type Item = I::Item;
17 |
18 | fn poll_next(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll