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,25 @@
1
+ [package]
2
+ name = "quietquic"
3
+ version = "0.0.1"
4
+ edition = "2021"
5
+ license = "0BSD"
6
+ publish = false
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ magnus = "0.8"
13
+ quietquic-core = { package = "quietquic", version = "=0.1.0-alpha.3" }
14
+ tokio = { version = "1", features = ["rt-multi-thread", "time"] }
15
+ # `FutureExt::catch_unwind` is used to catch a panic inside a spawned op so it
16
+ # surfaces as a `QuietQUIC::Error` rather than hanging the awaiting fiber (the
17
+ # result slot would otherwise never be populated and the wake byte never sent).
18
+ futures-util = "0.3"
19
+ libc = "0.2"
20
+ hex = "0.4"
21
+ toml = "0.8"
22
+ serde = { version = "1", features = ["derive"] }
23
+
24
+ # Own workspace so this crate does not join any containing workspace.
25
+ [workspace]
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: 0BSD
2
+ require "mkmf"
3
+ require "rb_sys/mkmf"
4
+
5
+ # Cargo resolves the published quietquic crate from crates.io; this gem does
6
+ # not vendor or path-depend on a local QuietQUIC checkout.
7
+ create_rust_makefile("quietquic/quietquic")
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ //! `QuietQUIC::Native::Client` — the FFI wrapper over the crate's cloaked QUIC
3
+ //! [`quietquic_core::client::Client`].
4
+ //!
5
+ //! This follows the exact native-handle-through-the-bridge pattern set up by
6
+ //! Task 5's `Server::bind` (see `server.rs`'s module doc for the full
7
+ //! rationale): `connect` spawns the crate's async `Client::connect` on the
8
+ //! shared tokio runtime via [`PendingOp::spawn_op`], and the op yields
9
+ //! **plain Rust data** (the crate's `Connection`) — the `to_ruby` converter
10
+ //! builds the Ruby [`crate::server::NativeConnection`] object ON THE RUBY
11
+ //! THREAD, never on a tokio worker thread.
12
+ //!
13
+ //! Unlike `Server`, `Client::connect` needs no long-lived shared handle: the
14
+ //! crate function is a one-shot `async fn` that either resolves to a
15
+ //! `Connection` or fails, so there is no `Arc<Mutex<..>>`, no shutdown
16
+ //! `Notify`, and no `close` method here — the connect future either completes
17
+ //! or the `PendingOp` is disposed (which aborts the tokio task).
18
+ //!
19
+ //! ## NativeConnection is reused, not redefined
20
+ //!
21
+ //! Task 7 extends one `Connection` type with streams for *both* accepted
22
+ //! (server-side) and dialed (client-side) connections. Task 5's `server.rs`
23
+ //! already defines `pub(crate) struct NativeConnection` (and its
24
+ //! `from_core` constructor) for exactly that purpose; this module imports and
25
+ //! reuses it rather than defining a second, parallel `NativeConnection` type.
26
+ //!
27
+ //! ## Error mapping
28
+ //!
29
+ //! The crate's `Client::connect` reports failure as a
30
+ //! [`quietquic_core::client::ClientError`]. Per the task interface, EVERY
31
+ //! variant — including the crate's own internal-timeout variant
32
+ //! `ClientError::TimedOut` — maps to `QuietQUIC::ConnectError`: from the
33
+ //! caller's point of view a timed-out dial and any other connect failure
34
+ //! (bad transport params, socket I/O, handshake lost before completion) are
35
+ //! all "the connect did not succeed," and `ConnectError` is the exception
36
+ //! class the brief and Task 5's `Server::bind` already use for that shape of
37
+ //! failure. Note the crate's `Client::connect` already has an internal
38
+ //! connect timeout (`~10s`, see `crate::client::DEFAULT_CONNECT_TIMEOUT` in
39
+ //! the core crate) — this bridge adds no timeout of its own.
40
+
41
+ use magnus::{function, prelude::*, Error, IntoValue, RModule, Ruby};
42
+
43
+ use quietquic_core::client::{Client as CoreClient, ClientError as CoreClientError};
44
+
45
+ use crate::config::ClientConfigHandle;
46
+ use crate::errors::{ErrorKind, MappedError};
47
+ use crate::pending::PendingOp;
48
+ use crate::conn::NativeConnection;
49
+
50
+ /// Map a crate `ClientError` to a `QuietQUIC::ConnectError`-classified
51
+ /// [`MappedError`]. Every variant (including `TimedOut`) maps to
52
+ /// `ErrorKind::Connect` — see the module doc for why a timeout and any other
53
+ /// connect failure are both surfaced identically to Ruby callers.
54
+ fn map_client_error(err: CoreClientError) -> MappedError {
55
+ match err {
56
+ CoreClientError::TimedOut => {
57
+ MappedError::new(ErrorKind::Connect, "connect timed out".to_string())
58
+ }
59
+ other => MappedError::new(ErrorKind::Connect, format!("connect failed: {other}")),
60
+ }
61
+ }
62
+
63
+ /// `QuietQUIC::Native::Client.connect(config) -> PendingOp`.
64
+ ///
65
+ /// Spawns the crate's `Client::connect(cfg)` on the shared runtime. The op
66
+ /// yields a `NativeConnection` handle — wrapped into the Ruby object by
67
+ /// `to_ruby` ON THE RUBY THREAD, exactly like `Server::bind`/`accept_op`. A
68
+ /// connect failure (including the crate's internal handshake timeout) maps to
69
+ /// `QuietQUIC::ConnectError`.
70
+ fn connect(config: &ClientConfigHandle) -> Result<PendingOp, Error> {
71
+ // Clone the parsed config out of the shared Ruby handle: `Client::connect`
72
+ // consumes it by value, and we only hold `&ClientConfigHandle`.
73
+ let cfg = config.0.clone();
74
+ PendingOp::spawn_op(
75
+ async move { CoreClient::connect(cfg).await.map_err(map_client_error) },
76
+ |conn| {
77
+ // Build the Ruby object on the Ruby thread (never off-thread).
78
+ let ruby = Ruby::get().expect("to_ruby runs on the Ruby thread");
79
+ NativeConnection::from_core(conn).into_value_with(&ruby)
80
+ },
81
+ )
82
+ }
83
+
84
+ /// Register `Client` and its methods on `QuietQUIC::Native`.
85
+ pub(crate) fn init(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
86
+ let client = native.define_class("Client", ruby.class_object())?;
87
+ client.define_singleton_method("connect", function!(connect, 1))?;
88
+ Ok(())
89
+ }
@@ -0,0 +1,292 @@
1
+ // SPDX-License-Identifier: 0BSD
2
+ //! Config builders: `QuietQUIC::Native.server_config_from_hash`,
3
+ //! `.server_config_from_toml`, `.client_config_from_parts`, and
4
+ //! `.client_config_from_toml`.
5
+ //!
6
+ //! These are synchronous — config parsing is fast and purely local, so unlike
7
+ //! the connect/stream operations added in later tasks, there is no
8
+ //! `PendingOp`/async bridge involved. Each builder either returns an opaque
9
+ //! handle (`ServerConfigHandle`/`ClientConfigHandle`) wrapping the crate's
10
+ //! parsed config, or raises `QuietQUIC::ConfigError` synchronously via
11
+ //! `MappedError::into_ruby_error`.
12
+ //!
13
+ //! The handles carry plain Rust data (no Ruby `Value`s), so they need no
14
+ //! custom `mark`/`free` — `#[magnus::wrap]` derives a `DataType` for them.
15
+
16
+ use std::net::SocketAddr;
17
+
18
+ use magnus::{prelude::*, r_hash::ForEach, Error, RHash, RString, Ruby, Symbol, Value};
19
+ use quietquic_core::config::{
20
+ ClientConfigFile, ConfigError as CoreConfigError, FileSource, SecretSource, ServerSecrets,
21
+ };
22
+
23
+ use crate::errors::{ErrorKind, MappedError};
24
+
25
+ /// Opaque handle wrapping a parsed server secrets file (listen addr + all
26
+ /// authorized clients). Produced by `server_config_from_hash`/`_toml`;
27
+ /// consumed by `QuietQUIC::Server.bind` in a later task. The wrapped
28
+ /// `ServerSecrets` is unread by this task on purpose — Task 4 only validates
29
+ /// and carries it; Task 5/6 will read it to actually bind.
30
+ #[magnus::wrap(class = "QuietQUIC::Native::ServerConfigHandle", free_immediately, size)]
31
+ #[allow(dead_code)]
32
+ pub(crate) struct ServerConfigHandle(pub(crate) ServerSecrets);
33
+
34
+ /// Opaque handle wrapping a parsed client config (identity, PSK, server
35
+ /// addr). Produced by `client_config_from_parts`/`_toml`; consumed by
36
+ /// `QuietQUIC::Client.connect` in a later task. See
37
+ /// [`ServerConfigHandle`] for why the wrapped value is unread here.
38
+ #[magnus::wrap(class = "QuietQUIC::Native::ClientConfigHandle", free_immediately, size)]
39
+ #[allow(dead_code)]
40
+ pub(crate) struct ClientConfigHandle(pub(crate) ClientConfigFile);
41
+
42
+ /// Build a `QuietQUIC::ConfigError` with `message`, ready to return from a
43
+ /// `Result`-returning native method.
44
+ fn config_error(ruby: &Ruby, message: impl Into<String>) -> Error {
45
+ MappedError::new(ErrorKind::Config, message).into_ruby_error(ruby)
46
+ }
47
+
48
+ /// Validate a PSK is 64 hex chars decoding to exactly 32 bytes. `Psk`'s inner
49
+ /// byte array is private outside the crate (by design — see its `Debug`
50
+ /// redaction), so there is no public constructor from raw bytes; the crate's
51
+ /// `serde::Deserialize` impl is the only construction path. We validate here
52
+ /// (for a precise, field-attributed error message) and let the TOML
53
+ /// round-trip in the callers actually build the `Psk`, via that same
54
+ /// `Deserialize` impl — so validation is never duplicated, only checked
55
+ /// early.
56
+ fn validate_psk(psk_hex: &str) -> Result<(), String> {
57
+ let bytes = hex::decode(psk_hex).map_err(|e| format!("invalid psk hex: {e}"))?;
58
+ if bytes.len() != 32 {
59
+ return Err("psk must be 32 bytes (64 hex chars)".to_string());
60
+ }
61
+ Ok(())
62
+ }
63
+
64
+ fn parse_addr(addr: &str, what: &str) -> Result<SocketAddr, String> {
65
+ addr.parse::<SocketAddr>()
66
+ .map_err(|e| format!("invalid {what} address {addr:?}: {e}"))
67
+ }
68
+
69
+ /// A minimal TOML-serializable mirror of a single client entry, used only to
70
+ /// hand `client_id`/`psk_hex` to `toml`/`serde` so the crate's own
71
+ /// `Psk: Deserialize` impl builds the real `Psk`. Never exposed to Ruby.
72
+ #[derive(serde::Serialize)]
73
+ struct ClientEntryToml<'a> {
74
+ client_id: &'a str,
75
+ psk: &'a str,
76
+ }
77
+
78
+ /// A minimal TOML-serializable mirror of `ServerSecrets`, for the same reason
79
+ /// as [`ClientEntryToml`].
80
+ #[derive(serde::Serialize)]
81
+ struct ServerSecretsToml<'a> {
82
+ listen: &'a str,
83
+ clients: Vec<ClientEntryToml<'a>>,
84
+ }
85
+
86
+ /// A minimal TOML-serializable mirror of `ClientConfigFile`.
87
+ #[derive(serde::Serialize)]
88
+ struct ClientConfigFileToml<'a> {
89
+ client_id: &'a str,
90
+ psk: &'a str,
91
+ server: &'a str,
92
+ }
93
+
94
+ /// Map the crate's `ConfigError` (io/parse) to a message for `ConfigError`.
95
+ fn core_config_error_message(err: CoreConfigError) -> String {
96
+ match err {
97
+ CoreConfigError::Io(e) => format!("io: {e}"),
98
+ CoreConfigError::Parse(e) => format!("parse: {e}"),
99
+ CoreConfigError::Invalid(e) => format!("invalid config: {e}"),
100
+ }
101
+ }
102
+
103
+ /// Convert a Ruby Hash key (a client id) to a `String`. Accepts a `String` or
104
+ /// a `Symbol` (coerced to its name) — both are idiomatic ways to write a
105
+ /// client-id key in Ruby (`{"alice" => psk}` / `{alice: psk}`). Anything else
106
+ /// (Integer, nil, ...) is a plausible user mistake, so it is reported via
107
+ /// `QuietQUIC::ConfigError` rather than letting magnus's own conversion
108
+ /// raise a bare Ruby `TypeError`.
109
+ fn client_id_from_key(ruby: &Ruby, key: Value) -> Result<String, Error> {
110
+ if let Some(s) = RString::from_value(key) {
111
+ return s
112
+ .to_string()
113
+ .map_err(|e| config_error(ruby, format!("invalid client id: {e}")));
114
+ }
115
+ if let Some(sym) = Symbol::from_value(key) {
116
+ return sym
117
+ .name()
118
+ .map(|n| n.into_owned())
119
+ .map_err(|e| config_error(ruby, format!("invalid client id symbol: {e}")));
120
+ }
121
+ Err(config_error(
122
+ ruby,
123
+ format!(
124
+ "client id must be a String or Symbol, got {}",
125
+ key.class().inspect()
126
+ ),
127
+ ))
128
+ }
129
+
130
+ /// Convert a Ruby Hash value (a PSK) to a `String`. Must be a Ruby `String`
131
+ /// (a PSK is a 64-hex-char string) — anything else is reported via
132
+ /// `QuietQUIC::ConfigError`, naming the offending client id.
133
+ fn psk_from_value(ruby: &Ruby, client_id: &str, value: Value) -> Result<String, Error> {
134
+ match RString::from_value(value) {
135
+ Some(s) => s.to_string().map_err(|e| {
136
+ config_error(ruby, format!("invalid psk for client {client_id:?}: {e}"))
137
+ }),
138
+ None => Err(config_error(
139
+ ruby,
140
+ format!(
141
+ "psk for client {client_id:?} must be a String (64 hex chars), got {}",
142
+ value.class().inspect()
143
+ ),
144
+ )),
145
+ }
146
+ }
147
+
148
+ /// `QuietQUIC::Native.server_config_from_hash(listen, clients) -> ServerConfigHandle`.
149
+ ///
150
+ /// `clients` is a `Hash` of `client_id => psk_hex` (64 hex chars each).
151
+ /// Raises `QuietQUIC::ConfigError` if `listen` doesn't parse as a
152
+ /// `SocketAddr`, or any PSK isn't valid 64-hex/32-bytes.
153
+ ///
154
+ /// `clients` is taken as a `magnus::RHash` (not `HashMap<String, String>`)
155
+ /// and iterated manually: magnus's blanket `TryConvert` for `HashMap<K, V>`
156
+ /// runs its per-entry conversion *before* this function body ever executes,
157
+ /// so a non-string key or value (e.g. Symbol keys, which are idiomatic Ruby)
158
+ /// would raise a bare Ruby `TypeError` that never reaches the `ConfigError`
159
+ /// mapping below. Iterating the `RHash` ourselves via `foreach::<Value,
160
+ /// Value>` (identity conversion, infallible) lets us apply our own
161
+ /// String/Symbol-friendly rules and always raise `QuietQUIC::ConfigError`
162
+ /// on a bad entry.
163
+ pub(crate) fn server_config_from_hash(
164
+ ruby: &Ruby,
165
+ listen: String,
166
+ clients: RHash,
167
+ ) -> Result<ServerConfigHandle, Error> {
168
+ // Validate the addr eagerly for a clean error, though it's re-parsed
169
+ // below by the crate's own `SocketAddr: Deserialize` via TOML.
170
+ parse_addr(&listen, "listen").map_err(|msg| config_error(ruby, msg))?;
171
+
172
+ let mut parsed: Vec<(String, String)> = Vec::with_capacity(clients.len());
173
+ let mut iter_err: Option<Error> = None;
174
+ clients.foreach(|key: Value, value: Value| {
175
+ let outcome = (|| -> Result<(String, String), Error> {
176
+ let client_id = client_id_from_key(ruby, key)?;
177
+ let psk_hex = psk_from_value(ruby, &client_id, value)?;
178
+ Ok((client_id, psk_hex))
179
+ })();
180
+ match outcome {
181
+ Ok(pair) => {
182
+ parsed.push(pair);
183
+ Ok(ForEach::Continue)
184
+ }
185
+ Err(e) => {
186
+ iter_err = Some(e);
187
+ Ok(ForEach::Stop)
188
+ }
189
+ }
190
+ })?;
191
+ if let Some(e) = iter_err {
192
+ return Err(e);
193
+ }
194
+
195
+ let mut entries = Vec::with_capacity(parsed.len());
196
+ for (client_id, psk_hex) in &parsed {
197
+ validate_psk(psk_hex)
198
+ .map_err(|msg| config_error(ruby, format!("client {client_id:?}: {msg}")))?;
199
+ entries.push(ClientEntryToml {
200
+ client_id,
201
+ psk: psk_hex,
202
+ });
203
+ }
204
+
205
+ let mirror = ServerSecretsToml {
206
+ listen: &listen,
207
+ clients: entries,
208
+ };
209
+ let toml_text = toml::to_string(&mirror)
210
+ .map_err(|e| config_error(ruby, format!("internal: failed to encode config: {e}")))?;
211
+ let secrets: ServerSecrets = toml::from_str(&toml_text)
212
+ .map_err(|e| config_error(ruby, format!("parse: {e}")))?;
213
+
214
+ Ok(ServerConfigHandle(secrets))
215
+ }
216
+
217
+ /// `QuietQUIC::Native.server_config_from_toml(path) -> ServerConfigHandle`.
218
+ ///
219
+ /// Delegates to the crate's `FileSource::new(path).load()`, which preserves
220
+ /// the crate's chmod-600 group/world-readable warning (logged via `tracing`,
221
+ /// not raised) and its io/parse error handling. Any failure is mapped to
222
+ /// `QuietQUIC::ConfigError`.
223
+ pub(crate) fn server_config_from_toml(
224
+ ruby: &Ruby,
225
+ path: String,
226
+ ) -> Result<ServerConfigHandle, Error> {
227
+ let secrets = FileSource::new(path)
228
+ .load()
229
+ .map_err(|e| config_error(ruby, core_config_error_message(e)))?;
230
+ Ok(ServerConfigHandle(secrets))
231
+ }
232
+
233
+ /// `QuietQUIC::Native.client_config_from_parts(client_id, psk_hex, server) -> ClientConfigHandle`.
234
+ pub(crate) fn client_config_from_parts(
235
+ ruby: &Ruby,
236
+ client_id: String,
237
+ psk_hex: String,
238
+ server: String,
239
+ ) -> Result<ClientConfigHandle, Error> {
240
+ validate_psk(&psk_hex).map_err(|msg| config_error(ruby, msg))?;
241
+ parse_addr(&server, "server").map_err(|msg| config_error(ruby, msg))?;
242
+
243
+ let mirror = ClientConfigFileToml {
244
+ client_id: &client_id,
245
+ psk: &psk_hex,
246
+ server: &server,
247
+ };
248
+ let toml_text = toml::to_string(&mirror)
249
+ .map_err(|e| config_error(ruby, format!("internal: failed to encode config: {e}")))?;
250
+ let parsed: ClientConfigFile = toml::from_str(&toml_text)
251
+ .map_err(|e| config_error(ruby, format!("parse: {e}")))?;
252
+
253
+ Ok(ClientConfigHandle(parsed))
254
+ }
255
+
256
+ /// `QuietQUIC::Native.client_config_from_toml(path) -> ClientConfigHandle`.
257
+ pub(crate) fn client_config_from_toml(
258
+ ruby: &Ruby,
259
+ path: String,
260
+ ) -> Result<ClientConfigHandle, Error> {
261
+ let text = std::fs::read_to_string(&path)
262
+ .map_err(|e| config_error(ruby, format!("io: {e}")))?;
263
+ let parsed: ClientConfigFile =
264
+ toml::from_str(&text).map_err(|e| config_error(ruby, format!("parse: {e}")))?;
265
+ Ok(ClientConfigHandle(parsed))
266
+ }
267
+
268
+ /// Register the config builders and handle classes on `QuietQUIC::Native`.
269
+ pub(crate) fn init(ruby: &Ruby, native: &magnus::RModule) -> Result<(), Error> {
270
+ use magnus::{function, prelude::*, Module};
271
+
272
+ native.define_class("ServerConfigHandle", ruby.class_object())?;
273
+ native.define_class("ClientConfigHandle", ruby.class_object())?;
274
+
275
+ native.define_singleton_method(
276
+ "server_config_from_hash",
277
+ function!(server_config_from_hash, 2),
278
+ )?;
279
+ native.define_singleton_method(
280
+ "server_config_from_toml",
281
+ function!(server_config_from_toml, 1),
282
+ )?;
283
+ native.define_singleton_method(
284
+ "client_config_from_parts",
285
+ function!(client_config_from_parts, 3),
286
+ )?;
287
+ native.define_singleton_method(
288
+ "client_config_from_toml",
289
+ function!(client_config_from_toml, 1),
290
+ )?;
291
+ Ok(())
292
+ }