quietquic 0.1.0.alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,259 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ //! `QuietQUIC::Native::Server` — the FFI wrapper over the crate's cloaked QUIC
3
+ //! [`quietquic_core::server::Server`], plus a minimal [`NativeConnection`] that
4
+ //! Task 7 extends into the friendly stream-carrying `Connection`.
5
+ //!
6
+ //! ## Native-handle-through-the-bridge pattern (the template for Tasks 6/7)
7
+ //!
8
+ //! The crate's `Server` is a Rust value whose UDP/endpoint driver runs on the
9
+ //! shared tokio runtime (see [`crate::runtime`]). We must (a) hand a handle onto
10
+ //! it back to Ruby, (b) let an async `accept` drive it, and (c) let a sync
11
+ //! `close` stop it — all while the Ruby object is freely shared/`dup`ed.
12
+ //!
13
+ //! Sharing model:
14
+ //! - `NativeServer` wraps `Arc<tokio::sync::Mutex<Option<Server>>>`. The `Arc`
15
+ //! lets the same underlying server be driven by an `accept` future while the
16
+ //! Ruby object is shared; the `Option` lets `close` (and GC `free`) *take* and
17
+ //! drop the crate `Server`, which drops its `JoinHandle` and stops the driver.
18
+ //! - `accept` takes `&mut Server`, and the future must hold the server locked
19
+ //! across the `.await`. We therefore use a **`tokio::sync::Mutex`**, never a
20
+ //! `std::sync::Mutex` — holding a std mutex across `.await` is forbidden (it
21
+ //! would block a runtime worker and can deadlock). Only one `accept` can be
22
+ //! in flight at a time, which matches the crate's single-consumer `accept`.
23
+ //! - `local_addr` is cached as a plain `SocketAddr` at wrap time, so
24
+ //! `local_address` is lock-free and still answers after `close`.
25
+ //! - A parked `accept` holds the async mutex across `server.accept().await`,
26
+ //! which never resolves while no client connects — the normal idle state of an
27
+ //! accept loop. `close` must take that same mutex to drop the server, so it
28
+ //! would block forever (and, under a single-threaded `Async` reactor,
29
+ //! `blocking_lock` freezes the whole Ruby VM). To break this, `NativeServer`
30
+ //! carries a shared `shutdown: Arc<Notify>`: the accept future `select!`s on
31
+ //! it, so `close` can fire `notify_waiters()` FIRST to unpark a parked accept.
32
+ //! The woken accept drops its guard on the tokio worker thread, so `close`'s
33
+ //! subsequent `blocking_lock` acquires promptly instead of deadlocking.
34
+ //! - BUT `notify_waiters()` is EDGE-triggered: it wakes only accepts already
35
+ //! registered on the `Notify` at that instant. With CONCURRENT accepts, one
36
+ //! accept can still be queued on the FIFO async mutex (not yet in `select!`,
37
+ //! not yet registered) when `close` fires; it acquires the guard AFTER the
38
+ //! woken accept releases it, reaches `select!`, registers on the `Notify` too
39
+ //! late (the edge already passed), and parks forever — re-deadlocking `close`.
40
+ //! To close this window, `NativeServer` also carries a LATCHING
41
+ //! `closed: Arc<AtomicBool>`: `close` sets it `true` (Release) BEFORE firing
42
+ //! the notify, and every accept, right after acquiring the async-mutex guard
43
+ //! and BEFORE entering `select!`, checks it (Acquire) and returns
44
+ //! `ConnectionLost` immediately if set — never parking, releasing the guard at
45
+ //! once. The `Notify` still promptly unparks the already-parked accept; the
46
+ //! latch covers any accept that reaches the guard after the edge passed. This
47
+ //! is deadlock-free for 0, 1, or N concurrent parked accepts plus a `close`.
48
+ //!
49
+ //! FFI thread-safety: `spawn_op` runs the future on a tokio worker and yields
50
+ //! **plain Rust data** (`Server` / `Connection`); the Ruby `NativeServer` /
51
+ //! `NativeConnection` object is built by the `to_ruby` converter on the Ruby
52
+ //! thread. No magnus `Value` is ever constructed on a tokio worker thread.
53
+
54
+ use std::net::SocketAddr;
55
+ use std::sync::atomic::{AtomicBool, Ordering};
56
+ use std::sync::Arc;
57
+
58
+ use magnus::{function, method, prelude::*, Error, IntoValue, RModule, Ruby};
59
+ use tokio::sync::Mutex as AsyncMutex;
60
+ use tokio::sync::Notify;
61
+
62
+ use quietquic_core::conn::Connection as CoreConnection;
63
+ use quietquic_core::server::Server as CoreServer;
64
+
65
+ use crate::config::ServerConfigHandle;
66
+ use crate::conn::NativeConnection;
67
+ use crate::errors::{ErrorKind, MappedError};
68
+ use crate::pending::PendingOp;
69
+
70
+ /// Shared, drop-on-close handle onto the crate's running `Server`. `None` once
71
+ /// closed (or never bound). Cloned into the `accept` future so it can drive the
72
+ /// server; taken by `close`/`free` to stop the driver.
73
+ type SharedServer = Arc<AsyncMutex<Option<CoreServer>>>;
74
+
75
+ /// `QuietQUIC::Native::Server` — TypedData wrapper over the crate's `Server`.
76
+ ///
77
+ /// `Send` (required for the GC free path and for moving the shared handle into
78
+ /// `accept` futures) because `Arc<tokio::sync::Mutex<Option<Server>>>` is `Send`
79
+ /// (the crate `Server` is `Send`) and `SocketAddr` is `Copy + Send`.
80
+ #[magnus::wrap(class = "QuietQUIC::Native::Server", free_immediately, size)]
81
+ pub(crate) struct NativeServer {
82
+ server: SharedServer,
83
+ /// Cached at bind time so `local_address` needs no lock and survives `close`.
84
+ local_addr: SocketAddr,
85
+ /// Fired by `close` to unpark a parked `accept`. The accept future `select!`s
86
+ /// on `notified()`, so a `close` racing an idle-waiting `accept` wakes it; the
87
+ /// woken accept then releases the async mutex on the tokio worker thread,
88
+ /// letting `close`'s `blocking_lock` proceed instead of deadlocking. Cloned
89
+ /// (as an `Arc`) into the accept future so both sides share one `Notify`.
90
+ shutdown: Arc<Notify>,
91
+ /// Latching shutdown flag. Set `true` by `close` (Release) BEFORE firing
92
+ /// `shutdown.notify_waiters()`; read by every accept (Acquire) after it
93
+ /// acquires the async-mutex guard and BEFORE it enters `select!`. Because
94
+ /// `notify_waiters()` is edge-triggered, an accept that only reaches the
95
+ /// guard AFTER `close`'s notify fired would otherwise register on the
96
+ /// `Notify` too late and park forever; this latch lets such a late accept
97
+ /// see closure and return `ConnectionLost` at once, releasing the guard so
98
+ /// `close`'s `blocking_lock` proceeds. Lock-free, so it is read before (not
99
+ /// across) the `.await` — no `std` lock is ever held across an await.
100
+ closed: Arc<AtomicBool>,
101
+ }
102
+
103
+ impl NativeServer {
104
+ /// `QuietQUIC::Native::Server.bind(config) -> PendingOp`.
105
+ ///
106
+ /// Spawns the crate's `Server::bind(secrets)` on the shared runtime. The op
107
+ /// yields a `NativeServer` handle — wrapped into the Ruby object by `to_ruby`
108
+ /// ON THE RUBY THREAD. A bind failure (socket bind / TLS setup, an
109
+ /// `io::Error`) maps to `QuietQUIC::ConnectError`.
110
+ fn bind(config: &ServerConfigHandle) -> Result<PendingOp, Error> {
111
+ // Clone the parsed secrets out of the shared Ruby handle: `Server::bind`
112
+ // consumes them by value, and we only hold `&ServerConfigHandle`.
113
+ let secrets = config.0.clone();
114
+ PendingOp::spawn_op(
115
+ async move {
116
+ CoreServer::bind(secrets)
117
+ .await
118
+ .map_err(|e| MappedError::new(ErrorKind::Connect, format!("bind failed: {e}")))
119
+ },
120
+ |server: CoreServer| {
121
+ // Build the Ruby object on the Ruby thread (never off-thread).
122
+ let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
123
+ let local_addr = server.local_addr();
124
+ let native = NativeServer {
125
+ server: Arc::new(AsyncMutex::new(Some(server))),
126
+ local_addr,
127
+ shutdown: Arc::new(Notify::new()),
128
+ closed: Arc::new(AtomicBool::new(false)),
129
+ };
130
+ native.into_value_with(&ruby)
131
+ },
132
+ )
133
+ }
134
+
135
+ /// `NativeServer#accept_op -> PendingOp`.
136
+ ///
137
+ /// Spawns `server.accept()`; yields a `NativeConnection`. Holds the server's
138
+ /// `tokio::sync::Mutex` across the `.await` (safe — async mutex), so exactly
139
+ /// one accept drives the server at a time. Maps a closed/lost server (accept
140
+ /// yielding `None`, or the server already closed) to `QuietQUIC::ConnectionLost`.
141
+ ///
142
+ /// The accept `.await` parks indefinitely while no client connects — the
143
+ /// normal idle state of an accept loop — and holds the async mutex the whole
144
+ /// time. To let a concurrent `close` shut down an idle server, the future
145
+ /// `select!`s the accept against `shutdown.notified()`: when `close` fires
146
+ /// `notify_waiters()`, the shutdown arm wins, the future returns a mapped
147
+ /// `ConnectionLost`, and the guard is dropped HERE on the tokio worker
148
+ /// thread — unblocking `close`'s lock acquisition.
149
+ fn accept_op(&self) -> Result<PendingOp, Error> {
150
+ let shared = Arc::clone(&self.server);
151
+ let shutdown = Arc::clone(&self.shutdown);
152
+ let closed = Arc::clone(&self.closed);
153
+ PendingOp::spawn_op(
154
+ async move {
155
+ // Lock across the await: this is a tokio async mutex, so parking
156
+ // here yields the worker rather than blocking it.
157
+ let mut guard = shared.lock().await;
158
+ // Latching closed-check, BEFORE the select!. `close` sets `closed`
159
+ // Release-ordered before firing the edge-triggered notify, so any
160
+ // accept that reaches this guard after that edge sees `true` here
161
+ // and returns WITHOUT parking — releasing the guard at once so
162
+ // `close`'s `blocking_lock` proceeds. This is the window that a
163
+ // bare `notify_waiters()` leaves open for a concurrent accept.
164
+ // The AtomicBool is lock-free, so this read is not a lock held
165
+ // across an await.
166
+ if closed.load(Ordering::Acquire) {
167
+ return Err(MappedError::new(ErrorKind::ConnectionLost, "server is closed"));
168
+ }
169
+ let server = guard.as_mut().ok_or_else(|| {
170
+ MappedError::new(ErrorKind::ConnectionLost, "server is closed")
171
+ })?;
172
+ tokio::select! {
173
+ // Bias the accept arm so a connection that is already ready is
174
+ // preferred over a simultaneous shutdown signal; a purely idle
175
+ // accept still parks and yields to the shutdown arm.
176
+ biased;
177
+ res = server.accept() => match res {
178
+ Some(conn) => Ok(conn),
179
+ None => Err(MappedError::new(
180
+ ErrorKind::ConnectionLost,
181
+ "server stopped accepting (driver shut down)",
182
+ )),
183
+ },
184
+ () = shutdown.notified() => Err(MappedError::new(
185
+ ErrorKind::ConnectionLost,
186
+ "server closed while accepting",
187
+ )),
188
+ }
189
+ // `guard` is dropped here, on the tokio worker thread, releasing
190
+ // the async mutex so a waiting `close` can `take()` the server.
191
+ },
192
+ |conn: CoreConnection| {
193
+ let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
194
+ NativeConnection::from_core(conn).into_value_with(&ruby)
195
+ },
196
+ )
197
+ }
198
+
199
+ /// `NativeServer#local_address -> String`. Synchronous, lock-free.
200
+ fn local_address(&self) -> String {
201
+ self.local_addr.to_string()
202
+ }
203
+
204
+ /// `NativeServer#close`. Synchronous; drops the crate `Server`, stopping its
205
+ /// driver (which prunes CIDs and releases the socket). Idempotent: a second
206
+ /// close finds `None` and is a no-op.
207
+ ///
208
+ /// Order matters, and there are THREE steps.
209
+ ///
210
+ /// 1. Set the latching `closed` flag (Release). This must happen BEFORE the
211
+ /// notify so any accept that later acquires the async-mutex guard — even
212
+ /// one still queued FIFO on that mutex when `close` runs — observes
213
+ /// closure (Acquire) and returns immediately instead of parking. This is
214
+ /// the fix for the CONCURRENT-accept deadlock: `notify_waiters()` is
215
+ /// edge-triggered and cannot wake an accept that has not yet registered.
216
+ /// 2. Fire `shutdown.notify_waiters()`. This promptly unparks any accept
217
+ /// ALREADY parked in `select!` (registered on the `Notify`), so it does
218
+ /// not have to wait for step 1's guarantee — it returns and drops its
219
+ /// guard on the tokio worker thread right away.
220
+ /// 3. `blocking_lock` to `take()` + drop the server. If an `accept` is parked
221
+ /// on `server.accept().await` (idle-waiting for a client), it holds the
222
+ /// async mutex, and a bare `blocking_lock` here would block forever — and
223
+ /// under a single-threaded `Async` reactor `blocking_lock` does not
224
+ /// release the GVL, freezing the whole VM. Steps 1+2 guarantee every
225
+ /// parked or about-to-park accept releases the guard promptly (via the
226
+ /// notify if already parked, via the latch if it reaches the guard later),
227
+ /// so this lock acquisition always completes. `blocking_lock` is otherwise
228
+ /// safe: `close` runs on the Ruby thread, never on a runtime worker.
229
+ ///
230
+ /// Idempotent + drop-safe: a second `close` re-stores `true` (harmless),
231
+ /// finds `None` (harmless `notify_waiters` on a `Notify` with no waiters is a
232
+ /// no-op, and `take()` yields `None`). Racing a still-in-flight accept cannot
233
+ /// double-drop: whoever holds the guard owns the `Some`, and `take()`
234
+ /// replaces it with `None`, so exactly one path drops the crate `Server`.
235
+ fn close(&self) {
236
+ // 1. Latch closure FIRST so a late/queued accept sees it and never parks.
237
+ self.closed.store(true, Ordering::Release);
238
+ // 2. Unpark any already-parked accept so it releases the async mutex.
239
+ self.shutdown.notify_waiters();
240
+ // 3. Take + drop the server. The lock now resolves promptly.
241
+ let taken = {
242
+ let mut guard = self.server.blocking_lock();
243
+ guard.take()
244
+ };
245
+ drop(taken);
246
+ }
247
+ }
248
+
249
+ /// Register `Server` and its methods on `QuietQUIC::Native`. The
250
+ /// `Connection`/`Stream` classes are registered by [`crate::conn`].
251
+ pub(crate) fn init(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
252
+ let server = native.define_class("Server", ruby.class_object())?;
253
+ server.define_singleton_method("bind", function!(NativeServer::bind, 1))?;
254
+ server.define_method("accept_op", method!(NativeServer::accept_op, 0))?;
255
+ server.define_method("local_address", method!(NativeServer::local_address, 0))?;
256
+ server.define_method("close", method!(NativeServer::close, 0))?;
257
+
258
+ Ok(())
259
+ }
@@ -0,0 +1,22 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ module QuietQUIC
3
+ # Wait for an in-flight native operation (a +PendingOp+) to complete and
4
+ # return its result, raising the mapped +QuietQUIC::*+ exception on failure.
5
+ #
6
+ # Waiting goes through +IO#wait_readable+ on the op's self-pipe fd, which is
7
+ # fiber-scheduler aware (Ruby >= 3.0): under +Async+ it parks the fiber and
8
+ # lets the reactor run other tasks; with no scheduler it blocks while
9
+ # releasing the GVL.
10
+ #
11
+ # +autoclose: false+ is a hard invariant: Rust owns the fd and closes it on
12
+ # disposal, so the Ruby +IO+ must never autoclose it (that would double-close).
13
+ #
14
+ # The op is explicitly disposed in an +ensure+ so its pipe fds are freed at
15
+ # await-completion rather than whenever the GC eventually runs the finalizer.
16
+ def self.await(op)
17
+ IO.for_fd(op.fd, autoclose: false).wait_readable
18
+ op.take_result
19
+ ensure
20
+ op.close
21
+ end
22
+ end
@@ -0,0 +1,40 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ module QuietQUIC
3
+ # Dial a cloaked QUIC server.
4
+ #
5
+ # Build a connection with {.connect} (from a client id + PSK + server
6
+ # address) or {.connect_toml} (from a client config file). Both parse and
7
+ # validate the config synchronously (raising {QuietQUIC::ConfigError} on a
8
+ # bad address or PSK), then +await+ the native connect on the shared
9
+ # runtime.
10
+ #
11
+ # The underlying crate connect has its own internal handshake timeout
12
+ # (about 10 seconds); a server that never responds surfaces as
13
+ # {QuietQUIC::ConnectError}, same as any other failed dial.
14
+ class Client
15
+ # Dial a server from this client's identity, PSK, and the server address.
16
+ #
17
+ # @param client_id [String] this client's id (must be authorized by the
18
+ # server).
19
+ # @param psk [String] the shared PSK, 64 hex chars (32 bytes).
20
+ # @param server [String] the server's +"ip:port"+ to dial.
21
+ # @return [Connection]
22
+ # @raise [QuietQUIC::ConfigError] on a bad address or PSK.
23
+ # @raise [QuietQUIC::ConnectError] if the connect fails or times out.
24
+ def self.connect(client_id:, psk:, server:)
25
+ handle = Native.client_config_from_parts(client_id, psk, server)
26
+ Connection.new(QuietQUIC.await(Native::Client.connect(handle)))
27
+ end
28
+
29
+ # Dial a server using a client config TOML file.
30
+ #
31
+ # @param path [String] path to the client config TOML.
32
+ # @return [Connection]
33
+ # @raise [QuietQUIC::ConfigError] if the file is missing or invalid.
34
+ # @raise [QuietQUIC::ConnectError] if the connect fails or times out.
35
+ def self.connect_toml(path)
36
+ handle = Native.client_config_from_toml(path)
37
+ Connection.new(QuietQUIC.await(Native::Client.connect(handle)))
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ module QuietQUIC
3
+ # A single post-handshake, PSK-authenticated QUIC connection.
4
+ #
5
+ # Produced identically by {Server#accept} and {Client.connect} (both sides
6
+ # surface the same type). Carries bidirectional {Stream}s: {#open_stream}
7
+ # dials a new stream, {#accept_stream} awaits the next peer-opened one. Both
8
+ # park the fiber (under an +Async+ reactor) until they resolve.
9
+ #
10
+ # Dropping a +Connection+ does not tear the connection down; call {#close} to
11
+ # send a CONNECTION_CLOSE so the peer tears down promptly rather than idling
12
+ # out.
13
+ class Connection
14
+ # @param native [Native::Connection] the wrapped crate connection.
15
+ def initialize(native)
16
+ @native = native
17
+ end
18
+
19
+ # Open a new bidirectional {Stream}.
20
+ #
21
+ # @return [Stream]
22
+ # @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
23
+ # @raise [QuietQUIC::StreamError] if the transport refuses the stream.
24
+ def open_stream
25
+ Stream.new(QuietQUIC.await(@native.open_stream_op))
26
+ end
27
+
28
+ # Await and return the next bidirectional {Stream} the peer opens.
29
+ #
30
+ # Blocks until the peer initiates a stream. Under an +Async+ reactor it
31
+ # parks the fiber; otherwise it blocks with the GVL released.
32
+ #
33
+ # @return [Stream]
34
+ # @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost
35
+ # while awaiting.
36
+ def accept_stream
37
+ Stream.new(QuietQUIC.await(@native.accept_stream_op))
38
+ end
39
+
40
+ # The remote peer's address (e.g. +"127.0.0.1:54321"+).
41
+ #
42
+ # @return [String]
43
+ def remote_address
44
+ @native.remote_address
45
+ end
46
+
47
+ # Close the connection, sending a CONNECTION_CLOSE frame so the peer tears
48
+ # down promptly. Best-effort and idempotent.
49
+ #
50
+ # @return [void]
51
+ def close
52
+ @native.close
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ module QuietQUIC
3
+ # A running cloaked QUIC server.
4
+ #
5
+ # Build one with {.bind} (from a listen address + a +{client_id => psk}+ map)
6
+ # or {.bind_toml} (from a secrets file). Both parse and validate the config
7
+ # synchronously (raising {QuietQUIC::ConfigError} on a bad address or PSK),
8
+ # then +await+ the native bind on the shared runtime.
9
+ #
10
+ # {#accept} yields the next authenticated {Connection}; it blocks (parking the
11
+ # fiber under an +Async+ reactor) until a peer completes the cloaked handshake.
12
+ class Server
13
+ # Bind a server from a listen address and an authorized-clients map.
14
+ #
15
+ # @param listen [String] the +"ip:port"+ to bind (use port +0+ for an
16
+ # ephemeral port, then read {#local_address}).
17
+ # @param clients [Hash{String=>String}] +client_id => psk_hex+ (64 hex chars).
18
+ # @return [Server]
19
+ # @raise [QuietQUIC::ConfigError] on a bad address or PSK.
20
+ # @raise [QuietQUIC::ConnectError] if the socket cannot be bound.
21
+ def self.bind(listen:, clients:)
22
+ handle = Native.server_config_from_hash(listen, clients)
23
+ new(QuietQUIC.await(Native::Server.bind(handle)))
24
+ end
25
+
26
+ # Bind a server from a TOML secrets file.
27
+ #
28
+ # @param path [String] path to the server secrets TOML.
29
+ # @return [Server]
30
+ # @raise [QuietQUIC::ConfigError] if the file is missing or invalid.
31
+ # @raise [QuietQUIC::ConnectError] if the socket cannot be bound.
32
+ def self.bind_toml(path)
33
+ handle = Native.server_config_from_toml(path)
34
+ new(QuietQUIC.await(Native::Server.bind(handle)))
35
+ end
36
+
37
+ # @param native [Native::Server] the wrapped crate server.
38
+ def initialize(native)
39
+ @native = native
40
+ end
41
+
42
+ # Await and return the next authenticated {Connection}.
43
+ #
44
+ # Blocks until a peer completes the cloaked handshake. Under an +Async+
45
+ # reactor it parks the fiber; otherwise it blocks with the GVL released.
46
+ #
47
+ # @return [Connection]
48
+ # @raise [QuietQUIC::ConnectionLost] if the server has been closed or its
49
+ # driver has shut down.
50
+ def accept
51
+ Connection.new(QuietQUIC.await(@native.accept_op))
52
+ end
53
+
54
+ # The address the server is actually listening on (e.g. +"127.0.0.1:54321"+).
55
+ # After binding to port +0+ this reports the kernel-chosen port.
56
+ #
57
+ # @return [String]
58
+ def local_address
59
+ @native.local_address
60
+ end
61
+
62
+ # Stop the server: drop the crate server, halting its driver and releasing
63
+ # the socket. Idempotent.
64
+ #
65
+ # @return [void]
66
+ def close
67
+ @native.close
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,79 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ module QuietQUIC
3
+ # A bidirectional QUIC stream on a {Connection}.
4
+ #
5
+ # Obtained from {Connection#open_stream} or {Connection#accept_stream}. Write
6
+ # bytes with {#write_all}, signal end-of-data with {#finish}, and read the
7
+ # peer's whole reply with {#read_to_end}. Each operation parks the fiber
8
+ # (under an +Async+ reactor) until it resolves.
9
+ #
10
+ # @note The native alpha.3 API has split send/receive halves. This Ruby class
11
+ # keeps one facade around those halves: send operations serialize with send
12
+ # operations, receive operations serialize with receive operations, and both
13
+ # still park through the same scheduler-aware bridge.
14
+ class Stream
15
+ # Default upper bound for {#read_to_end}, chosen to keep the convenience
16
+ # method safe against an authenticated peer sending an unbounded response.
17
+ DEFAULT_READ_LIMIT = 1024 * 1024
18
+ DEFAULT_FINISH_TIMEOUT = 10
19
+
20
+ # @param native [Native::Stream] the wrapped crate stream.
21
+ def initialize(native)
22
+ @native = native
23
+ end
24
+
25
+ # Write all of +bytes+ to the stream, waiting out flow-control
26
+ # back-pressure. The bytes are sent verbatim: +bytes+ is read as a raw byte
27
+ # sequence regardless of its String encoding.
28
+ #
29
+ # @param bytes [String] the bytes to send.
30
+ # @return [void]
31
+ # @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
32
+ # @raise [QuietQUIC::StreamError] if the stream is reset or stopped.
33
+ def write_all(bytes)
34
+ QuietQUIC.await(@native.write_all_op(bytes))
35
+ end
36
+
37
+ # Finish (send FIN on) the stream, signalling end-of-data to the peer.
38
+ #
39
+ # @return [void]
40
+ # @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
41
+ # @raise [QuietQUIC::StreamError] if the stream is reset or stopped.
42
+ def finish
43
+ QuietQUIC.await(@native.finish_op)
44
+ end
45
+
46
+ # Finish the stream and wait for the peer to acknowledge its FIN.
47
+ #
48
+ # Use this before immediately closing the connection when delivery of the
49
+ # final stream matters. A plain {#finish} only queues FIN locally.
50
+ #
51
+ # @param timeout [Numeric] maximum seconds to wait.
52
+ # @return [void]
53
+ def finish_and_wait(timeout: DEFAULT_FINISH_TIMEOUT)
54
+ unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
55
+ raise ArgumentError, "timeout must be a positive finite number"
56
+ end
57
+
58
+ QuietQUIC.await(@native.finish_and_wait_op(timeout.to_f))
59
+ end
60
+
61
+ # Read the stream to end-of-stream, returning up to +limit+ bytes.
62
+ #
63
+ # Parks until the peer sends FIN. The result is a +String+ with
64
+ # +Encoding::BINARY+ (ASCII-8BIT): exact bytes, never UTF-8-validated.
65
+ #
66
+ # @note This parks until the peer sends FIN. Avoid issuing multiple
67
+ # concurrent reads on the same Stream; they share one receive half.
68
+ # @param limit [Integer] maximum response size in bytes.
69
+ # @return [String] the received bytes, BINARY-encoded.
70
+ # @raise [QuietQUIC::ConnectionLost] if the connection is closed or lost.
71
+ # @raise [QuietQUIC::StreamError] if the stream is reset before FIN or
72
+ # exceeds +limit+.
73
+ def read_to_end(limit: DEFAULT_READ_LIMIT)
74
+ raise ArgumentError, "limit must be a non-negative Integer" unless limit.is_a?(Integer) && limit >= 0
75
+
76
+ QuietQUIC.await(@native.read_to_end_op(limit))
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,4 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ module QuietQUIC
3
+ VERSION = "0.1.0.alpha.3"
4
+ end
data/lib/quietquic.rb ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ require_relative "quietquic/version"
3
+ require "quietquic/quietquic" # the compiled extension
4
+ require_relative "quietquic/await"
5
+ require_relative "quietquic/stream"
6
+ require_relative "quietquic/connection"
7
+ require_relative "quietquic/server"
8
+ require_relative "quietquic/client"
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: quietquic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.alpha.3
5
+ platform: ruby
6
+ authors:
7
+ - quietquic contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rake-compiler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.2'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rspec
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.13'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.13'
54
+ - !ruby/object:Gem::Dependency
55
+ name: async
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ description: Experimental alpha Ruby bindings (native extension) for the quietquic
69
+ Rust crate.
70
+ executables: []
71
+ extensions:
72
+ - ext/quietquic/extconf.rb
73
+ extra_rdoc_files: []
74
+ files:
75
+ - HISTORY.md
76
+ - LICENSE
77
+ - README.md
78
+ - examples/cooperative_async.rb
79
+ - examples/echo_client.rb
80
+ - examples/echo_server.rb
81
+ - ext/quietquic/Cargo.lock
82
+ - ext/quietquic/Cargo.toml
83
+ - ext/quietquic/extconf.rb
84
+ - ext/quietquic/src/client.rs
85
+ - ext/quietquic/src/config.rs
86
+ - ext/quietquic/src/conn.rs
87
+ - ext/quietquic/src/errors.rs
88
+ - ext/quietquic/src/lib.rs
89
+ - ext/quietquic/src/pending.rs
90
+ - ext/quietquic/src/runtime.rs
91
+ - ext/quietquic/src/server.rs
92
+ - lib/quietquic.rb
93
+ - lib/quietquic/await.rb
94
+ - lib/quietquic/client.rb
95
+ - lib/quietquic/connection.rb
96
+ - lib/quietquic/server.rb
97
+ - lib/quietquic/stream.rb
98
+ - lib/quietquic/version.rb
99
+ homepage: https://github.com/astounding/quietquicrb
100
+ licenses:
101
+ - 0BSD
102
+ metadata:
103
+ source_code_uri: https://github.com/astounding/quietquicrb
104
+ changelog_uri: https://github.com/astounding/quietquicrb/blob/main/HISTORY.md
105
+ documentation_uri: https://github.com/astounding/quietquicrb#readme
106
+ rubygems_mfa_required: 'true'
107
+ rdoc_options: []
108
+ require_paths:
109
+ - lib
110
+ required_ruby_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '3.1'
115
+ required_rubygems_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubygems_version: 4.0.15
122
+ specification_version: 4
123
+ summary: 'Cloaked QUIC transport: scanner-invisible, PSK-authenticated, camouflaged
124
+ as vanilla QUIC.'
125
+ test_files: []