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.
- checksums.yaml +7 -0
- data/HISTORY.md +79 -0
- data/LICENSE +13 -0
- data/README.md +318 -0
- data/examples/cooperative_async.rb +91 -0
- data/examples/echo_client.rb +40 -0
- data/examples/echo_server.rb +48 -0
- data/ext/quietquic/Cargo.lock +1377 -0
- data/ext/quietquic/Cargo.toml +25 -0
- data/ext/quietquic/extconf.rb +7 -0
- data/ext/quietquic/src/client.rs +89 -0
- data/ext/quietquic/src/config.rs +292 -0
- data/ext/quietquic/src/conn.rs +283 -0
- data/ext/quietquic/src/errors.rs +140 -0
- data/ext/quietquic/src/lib.rs +59 -0
- data/ext/quietquic/src/pending.rs +386 -0
- data/ext/quietquic/src/runtime.rs +72 -0
- data/ext/quietquic/src/server.rs +259 -0
- data/lib/quietquic/await.rb +22 -0
- data/lib/quietquic/client.rb +40 -0
- data/lib/quietquic/connection.rb +55 -0
- data/lib/quietquic/server.rb +70 -0
- data/lib/quietquic/stream.rb +79 -0
- data/lib/quietquic/version.rb +4 -0
- data/lib/quietquic.rb +8 -0
- metadata +125 -0
|
@@ -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,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
|
+
}
|