Merge remote-tracking branch 'origin/master'
Some checks failed
CI / checks (push) Failing after 1m51s
Some checks failed
CI / checks (push) Failing after 1m51s
This commit is contained in:
commit
1b60ccfe43
9 changed files with 385 additions and 107 deletions
|
|
@ -22,6 +22,7 @@ pub struct ClientConfig {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub tls: ClientTlsConfig,
|
pub tls: ClientTlsConfig,
|
||||||
pub client_id: u64,
|
pub client_id: u64,
|
||||||
|
pub description: Option<String>,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub auth_timeout: Duration,
|
pub auth_timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +39,7 @@ impl ClientConfig {
|
||||||
url: url.into(),
|
url: url.into(),
|
||||||
tls: ClientTlsConfig::SystemRoots,
|
tls: ClientTlsConfig::SystemRoots,
|
||||||
client_id: 0,
|
client_id: 0,
|
||||||
|
description: None,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
auth_timeout: Duration::from_secs(30),
|
auth_timeout: Duration::from_secs(30),
|
||||||
}
|
}
|
||||||
|
|
@ -57,6 +59,11 @@ impl ClientConfig {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_description(mut self, description: impl Into<String>) -> Self {
|
||||||
|
self.description = Some(description.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
|
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
|
||||||
self.auth_timeout = timeout;
|
self.auth_timeout = timeout;
|
||||||
|
|
@ -76,6 +83,7 @@ pub struct MTPConnection {
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
pub sender: Sender,
|
pub sender: Sender,
|
||||||
pub receiver: Receiver,
|
pub receiver: Receiver,
|
||||||
|
pub description: Option<String>,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub auth_state: AuthState,
|
pub auth_state: AuthState,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -150,12 +158,15 @@ impl MTPClient {
|
||||||
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
|
mtp_transport::connect(&config.url, config.server_cert(), Policy::default()).await?;
|
||||||
|
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
let mut ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::Id,
|
DataType::Id,
|
||||||
DataValue::UnsignedNumber(config.client_id.into()),
|
DataValue::UnsignedNumber(config.client_id.into()),
|
||||||
);
|
);
|
||||||
|
if let Some(desc) = &config.description {
|
||||||
|
ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
sender.send(&ident).await?;
|
sender.send(&ident).await?;
|
||||||
|
|
||||||
|
|
@ -163,6 +174,7 @@ impl MTPClient {
|
||||||
version: PROTOCOL_VERSION,
|
version: PROTOCOL_VERSION,
|
||||||
sender,
|
sender,
|
||||||
receiver,
|
receiver,
|
||||||
|
description: config.description,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
auth_state: AuthState::Unauthenticated,
|
auth_state: AuthState::Unauthenticated,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -379,12 +391,15 @@ impl MTPClient {
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
|
|
||||||
// 1. Send the unsigned Identification hello (version + claimed id).
|
// 1. Send the unsigned Identification hello (version + claimed id).
|
||||||
let ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
let mut ident = CommunicationValue::new(mtp_codec::CommunicationType::Identification)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::Id,
|
DataType::Id,
|
||||||
DataValue::UnsignedNumber(config.client_id as u128),
|
DataValue::UnsignedNumber(config.client_id as u128),
|
||||||
);
|
);
|
||||||
|
if let Some(desc) = &config.description {
|
||||||
|
ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||||
|
}
|
||||||
if let Err(e) = sender.send(&ident).await {
|
if let Err(e) = sender.send(&ident).await {
|
||||||
sender.close();
|
sender.close();
|
||||||
return Err(e);
|
return Err(e);
|
||||||
|
|
@ -464,6 +479,7 @@ impl MTPClient {
|
||||||
version: PROTOCOL_VERSION,
|
version: PROTOCOL_VERSION,
|
||||||
sender,
|
sender,
|
||||||
receiver,
|
receiver,
|
||||||
|
description: config.description,
|
||||||
auth_state: AuthState::Authenticated,
|
auth_state: AuthState::Authenticated,
|
||||||
client_id: config.client_id,
|
client_id: config.client_id,
|
||||||
})
|
})
|
||||||
|
|
@ -519,9 +535,12 @@ impl MTPClient {
|
||||||
let pk_bytes = pk_bundle.as_bytes();
|
let pk_bytes = pk_bundle.as_bytes();
|
||||||
|
|
||||||
// 1. Send the unsigned Register hello (version + public-key bundle).
|
// 1. Send the unsigned Register hello (version + public-key bundle).
|
||||||
let register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
|
let mut register = CommunicationValue::new(mtp_codec::CommunicationType::Register)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
||||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
|
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
|
||||||
|
if let Some(desc) = &config.description {
|
||||||
|
register = register.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||||
|
}
|
||||||
if let Err(e) = sender.send(®ister).await {
|
if let Err(e) = sender.send(®ister).await {
|
||||||
sender.close();
|
sender.close();
|
||||||
return Err(e);
|
return Err(e);
|
||||||
|
|
@ -607,6 +626,7 @@ impl MTPClient {
|
||||||
version: PROTOCOL_VERSION,
|
version: PROTOCOL_VERSION,
|
||||||
sender,
|
sender,
|
||||||
receiver,
|
receiver,
|
||||||
|
description: config.description,
|
||||||
auth_state: AuthState::Authenticated,
|
auth_state: AuthState::Authenticated,
|
||||||
client_id: assigned_id,
|
client_id: assigned_id,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@ The host's `accept()` method:
|
||||||
|
|
||||||
### Login/Register Handshake
|
### Login/Register Handshake
|
||||||
|
|
||||||
When `require_authentication` is set, the parties run a mutually-authenticated
|
When `authentication_policy` is `ForceAuthentication` or `AllowAuthentication`,
|
||||||
|
the parties run a mutually-authenticated
|
||||||
**challenge-response**. The client speaks first with an *unsigned* hello:
|
**challenge-response**. The client speaks first with an *unsigned* hello:
|
||||||
|
|
||||||
- **Login** (`CommunicationType::Identification`, reserved ID 0): version, client ID
|
- **Login** (`CommunicationType::Identification`, reserved ID 0): version, client ID
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ let config = ClientConfig::new("https://host.example.com:4433")
|
||||||
| `url` | `String` | `https://host:port` address of the MTP host |
|
| `url` | `String` | `https://host:port` address of the MTP host |
|
||||||
| `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` |
|
| `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` |
|
||||||
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
|
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
|
||||||
|
| `description` | `Option<String>` | Optional label sent during handshake (e.g. `"phone"`) |
|
||||||
| `auth_timeout` | `Duration` (crypto) | Authentication handshake timeout (default 30s) |
|
| `auth_timeout` | `Duration` (crypto) | Authentication handshake timeout (default 30s) |
|
||||||
|
|
||||||
### TLS Certificate Handling
|
### TLS Certificate Handling
|
||||||
|
|
@ -60,6 +61,7 @@ pub struct MTPConnection {
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
pub sender: Sender,
|
pub sender: Sender,
|
||||||
pub receiver: Receiver,
|
pub receiver: Receiver,
|
||||||
|
pub description: Option<String>,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub auth_state: AuthState,
|
pub auth_state: AuthState,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -69,6 +71,7 @@ pub struct MTPConnection {
|
||||||
|
|
||||||
- `version` -- the negotiated protocol version
|
- `version` -- the negotiated protocol version
|
||||||
- `sender` / `receiver` -- for message I/O
|
- `sender` / `receiver` -- for message I/O
|
||||||
|
- `description` -- the label sent during handshake (set via `ClientConfig::with_description`)
|
||||||
- `client_id` -- the confirmed/assigned client identifier (crypto only)
|
- `client_id` -- the confirmed/assigned client identifier (crypto only)
|
||||||
|
|
||||||
### Unauthenticated Connect
|
### Unauthenticated Connect
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,30 @@ let config = HostConfig::new(
|
||||||
| `port` | `u16` | Listen port |
|
| `port` | `u16` | Listen port |
|
||||||
| `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain |
|
| `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain |
|
||||||
| `tls_key` | `Vec<u8>` | PEM-encoded TLS private key |
|
| `tls_key` | `Vec<u8>` | PEM-encoded TLS private key |
|
||||||
| `require_authentication` | `bool` (crypto) | Enable login/register handshake |
|
| `authentication_policy` | `AuthenticationPolicy` (crypto) | `ForceAuthentication`, `AllowAuthentication`, or `Unauthenticated` |
|
||||||
| `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
|
| `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
|
||||||
| `get_existing_user` | `Fn(u64) -> Pin<Box<dyn Future<Output = Option<PublicKeyBundle>> + Send>> + Send + Sync` (crypto) | Async lookup callback for login |
|
| `get_existing_user` | `Fn(u64) -> Pin<Box<dyn Future<Output = Option<PublicKeyBundle>> + Send>> + Send + Sync` (crypto) | Async lookup callback for login |
|
||||||
| `complete_register` | `Fn(PublicKeyBundle) -> Pin<Box<dyn Future<Output = u64> + Send>> + Send + Sync` (crypto) | Async registration callback, returns new client ID |
|
| `complete_register` | `Fn(PublicKeyBundle) -> Pin<Box<dyn Future<Output = u64> + Send>> + Send + Sync` (crypto) | Async registration callback, returns new client ID |
|
||||||
|
|
||||||
|
### AuthenticationPolicy
|
||||||
|
|
||||||
|
`ForceAuthentication` requires every client to complete the login/register handshake. `AllowAuthentication` accepts both authenticated and unauthenticated connections — unauthenticated clients get a random ID and `AuthState::Unauthenticated`. `Unauthenticated` rejects any client that tries to authenticate and is the default.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use mtp::host::AuthenticationPolicy;
|
||||||
|
|
||||||
|
// Force authentication (default was `require_authentication: true`):
|
||||||
|
let config = HostConfig::new(ip, port, cert, key)
|
||||||
|
.with_authentication(host_keyring, get_user, register);
|
||||||
|
|
||||||
|
// Allow both authenticated and unauthenticated:
|
||||||
|
let config = HostConfig::new(ip, port, cert, key)
|
||||||
|
.with_allow_authentication(host_keyring, get_user, register);
|
||||||
|
|
||||||
|
// Unauthenticated only (default):
|
||||||
|
let config = HostConfig::new(ip, port, cert, key);
|
||||||
|
```
|
||||||
|
|
||||||
### TLS
|
### TLS
|
||||||
|
|
||||||
The host requires a TLS certificate. For development, generate a self-signed
|
The host requires a TLS certificate. For development, generate a self-signed
|
||||||
|
|
@ -82,6 +101,7 @@ pub struct MTPConnection {
|
||||||
pub codec: VersionedCodec,
|
pub codec: VersionedCodec,
|
||||||
pub sender: Sender,
|
pub sender: Sender,
|
||||||
pub receiver: Receiver,
|
pub receiver: Receiver,
|
||||||
|
pub description: Option<String>,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub auth_state: AuthState,
|
pub auth_state: AuthState,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -95,6 +115,7 @@ pub struct MTPConnection {
|
||||||
- `codec` -- a `VersionedCodec` scoped to the negotiated version (use for
|
- `codec` -- a `VersionedCodec` scoped to the negotiated version (use for
|
||||||
version-aware encode/decode)
|
version-aware encode/decode)
|
||||||
- `sender` / `receiver` -- for message I/O
|
- `sender` / `receiver` -- for message I/O
|
||||||
|
- `description` -- optional client-provided label (e.g. `"phone"`, `"desktop"`)
|
||||||
- `client_id` -- the authenticated client's ID
|
- `client_id` -- the authenticated client's ID
|
||||||
- `client_public_key` -- the client's public key bundle (for signature
|
- `client_public_key` -- the client's public key bundle (for signature
|
||||||
verification of subsequent messages)
|
verification of subsequent messages)
|
||||||
|
|
@ -130,7 +151,7 @@ let negotiated = registry.negotiate(&[Version(1, 0), Version(2, 0)]);
|
||||||
|
|
||||||
## Authentication Flow
|
## Authentication Flow
|
||||||
|
|
||||||
When `require_authentication` is `true`, `accept()` runs a mutually-authenticated
|
When `authentication_policy` is `ForceAuthentication`, `accept()` runs a mutually-authenticated
|
||||||
**challenge-response** handshake before returning the connection. The host issues
|
**challenge-response** handshake before returning the connection. The host issues
|
||||||
a fresh, random `server_challenge` that the client must sign, which is what makes
|
a fresh, random `server_challenge` that the client must sign, which is what makes
|
||||||
the client's proof unreplayable: a captured proof is bound to a one-time challenge
|
the client's proof unreplayable: a captured proof is bound to a one-time challenge
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
println!("Connecting to 127.0.0.1:8080 ...");
|
println!("Connecting to 127.0.0.1:8080 ...");
|
||||||
|
|
||||||
let config = ClientConfig::new("https://127.0.0.1:8080").with_pinned_pem(cert_pem);
|
let config = ClientConfig::new("https://127.0.0.1:8080")
|
||||||
|
.with_pinned_pem(cert_pem)
|
||||||
|
.with_description("MTP example client");
|
||||||
|
|
||||||
let server_bundle = host_public_key.clone();
|
let server_bundle = host_public_key.clone();
|
||||||
let (conn, keyring) =
|
let (conn, keyring) =
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Server listening on {}", host.local_addr());
|
println!("Server listening on {}", host.local_addr());
|
||||||
|
|
||||||
while let Some(conn) = host.accept().await? {
|
while let Some(conn) = host.accept().await? {
|
||||||
|
let desc = conn
|
||||||
|
.description
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("(no description)");
|
||||||
println!(
|
println!(
|
||||||
"\n--- New authenticated connection (version {}) ---",
|
"\n--- New connection (version {}, description: {desc}) ---",
|
||||||
conn.version
|
conn.version
|
||||||
);
|
);
|
||||||
println!("Client ID: {}", conn.client_id);
|
println!("Client ID: {}", conn.client_id);
|
||||||
|
|
|
||||||
388
host/src/lib.rs
388
host/src/lib.rs
|
|
@ -29,6 +29,14 @@ type CompleteRegister = Box<
|
||||||
+ Sync,
|
+ Sync,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum AuthenticationPolicy {
|
||||||
|
ForceAuthentication,
|
||||||
|
AllowAuthentication,
|
||||||
|
Unauthenticated,
|
||||||
|
}
|
||||||
|
|
||||||
/* Host configuration. */
|
/* Host configuration. */
|
||||||
pub struct HostConfig {
|
pub struct HostConfig {
|
||||||
pub ip: IpAddr,
|
pub ip: IpAddr,
|
||||||
|
|
@ -37,7 +45,7 @@ pub struct HostConfig {
|
||||||
pub tls_key: Vec<u8>,
|
pub tls_key: Vec<u8>,
|
||||||
|
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub require_authentication: bool,
|
pub authentication_policy: AuthenticationPolicy,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub auth_timeout: Duration,
|
pub auth_timeout: Duration,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -56,7 +64,7 @@ impl HostConfig {
|
||||||
tls_fullchain,
|
tls_fullchain,
|
||||||
tls_key,
|
tls_key,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
require_authentication: false,
|
authentication_policy: AuthenticationPolicy::Unauthenticated,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
auth_timeout: Duration::from_secs(30),
|
auth_timeout: Duration::from_secs(30),
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -93,13 +101,19 @@ impl HostConfig {
|
||||||
+ Sync
|
+ Sync
|
||||||
+ 'static,
|
+ 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.require_authentication = true;
|
self.authentication_policy = AuthenticationPolicy::ForceAuthentication;
|
||||||
self.host_keyring = host_keyring;
|
self.host_keyring = host_keyring;
|
||||||
self.get_existing_user = Box::new(get_existing_user);
|
self.get_existing_user = Box::new(get_existing_user);
|
||||||
self.complete_register = Box::new(complete_register);
|
self.complete_register = Box::new(complete_register);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
pub fn with_authentication_policy(mut self, policy: AuthenticationPolicy) -> Self {
|
||||||
|
self.authentication_policy = policy;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
|
pub fn with_auth_timeout(mut self, timeout: Duration) -> Self {
|
||||||
self.auth_timeout = timeout;
|
self.auth_timeout = timeout;
|
||||||
|
|
@ -152,6 +166,7 @@ pub struct MTPConnection {
|
||||||
pub codec: VersionedCodec,
|
pub codec: VersionedCodec,
|
||||||
pub sender: Sender,
|
pub sender: Sender,
|
||||||
pub receiver: Receiver,
|
pub receiver: Receiver,
|
||||||
|
pub description: Option<String>,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
pub auth_state: AuthState,
|
pub auth_state: AuthState,
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
|
|
@ -203,49 +218,94 @@ impl MTPHost {
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
if self.config.require_authentication {
|
match self.config.authentication_policy {
|
||||||
let timeout = self.config.auth_timeout;
|
AuthenticationPolicy::ForceAuthentication => {
|
||||||
return match tokio::time::timeout(timeout, self.accept_authenticated(sender, receiver))
|
let timeout = self.config.auth_timeout;
|
||||||
|
return match tokio::time::timeout(
|
||||||
|
timeout,
|
||||||
|
self.accept_authenticated(sender, receiver),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => result,
|
Ok(result) => result,
|
||||||
Err(_) => Err(AcceptError::AuthenticationTimedOut),
|
Err(_) => Err(AcceptError::AuthenticationTimedOut),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
AuthenticationPolicy::AllowAuthentication => {
|
||||||
|
return self.accept_allow_auth(sender, receiver).await;
|
||||||
|
}
|
||||||
|
AuthenticationPolicy::Unauthenticated => {
|
||||||
|
let first_msg = match receiver.receive().await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return Err(AcceptError::Receive(e)),
|
||||||
|
};
|
||||||
|
if first_msg.get_type()
|
||||||
|
== mtp_codec::CommunicationType::Register.to_id(&mtp_codec::TypeMap::latest())
|
||||||
|
{
|
||||||
|
sender.close();
|
||||||
|
return Err(AcceptError::AuthenticationFailed(
|
||||||
|
"authentication not allowed on this host".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let client_version = match extract_version(&first_msg) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Err(AcceptError::MissingVersion),
|
||||||
|
};
|
||||||
|
let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version))
|
||||||
|
{
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||||
|
};
|
||||||
|
let codec = VersionedCodec::new(self.registry.clone());
|
||||||
|
let description = match first_msg.get_data(DataType::Description) {
|
||||||
|
DataValue::Str(s) => Some(s.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
Ok(Some(MTPConnection {
|
||||||
|
version: negotiated,
|
||||||
|
codec,
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
description,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
auth_state: AuthState::Unauthenticated,
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
client_id: rand::random(),
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
client_public_key: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the first message (always encoded with reserved types).
|
// Non-crypto fallback: no authentication feature, just read and respond.
|
||||||
let first_msg = match receiver.receive().await {
|
#[cfg(not(feature = "crypto"))]
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => return Err(AcceptError::Receive(e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let client_version = match extract_version(&first_msg) {
|
|
||||||
Some(v) => v,
|
|
||||||
None => return Err(AcceptError::MissingVersion),
|
|
||||||
};
|
|
||||||
|
|
||||||
let negotiated = match self
|
|
||||||
.registry
|
|
||||||
.negotiate(std::slice::from_ref(&client_version))
|
|
||||||
{
|
{
|
||||||
Some(v) => v,
|
let first_msg = match receiver.receive().await {
|
||||||
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
Ok(m) => m,
|
||||||
};
|
Err(e) => return Err(AcceptError::Receive(e)),
|
||||||
|
};
|
||||||
let codec = VersionedCodec::new(self.registry.clone());
|
let client_version = match extract_version(&first_msg) {
|
||||||
|
Some(v) => v,
|
||||||
Ok(Some(MTPConnection {
|
None => return Err(AcceptError::MissingVersion),
|
||||||
version: negotiated,
|
};
|
||||||
codec,
|
let negotiated = match self.registry.negotiate(std::slice::from_ref(&client_version))
|
||||||
sender,
|
{
|
||||||
receiver,
|
Some(v) => v,
|
||||||
#[cfg(feature = "crypto")]
|
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||||
auth_state: AuthState::Unauthenticated,
|
};
|
||||||
#[cfg(feature = "crypto")]
|
let codec = VersionedCodec::new(self.registry.clone());
|
||||||
client_id: 0,
|
let description = match first_msg.get_data(DataType::Description) {
|
||||||
#[cfg(feature = "crypto")]
|
DataValue::Str(s) => Some(s.clone()),
|
||||||
client_public_key: None,
|
_ => None,
|
||||||
}))
|
};
|
||||||
|
return Ok(Some(MTPConnection {
|
||||||
|
version: negotiated,
|
||||||
|
codec,
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
description,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_addr(&self) -> std::net::SocketAddr {
|
pub fn local_addr(&self) -> std::net::SocketAddr {
|
||||||
|
|
@ -257,6 +317,18 @@ impl MTPHost {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "crypto")]
|
||||||
|
enum Flow {
|
||||||
|
Login {
|
||||||
|
id: u64,
|
||||||
|
bundle: mtp_crypto::PublicKeyBundle,
|
||||||
|
},
|
||||||
|
Register {
|
||||||
|
bundle: mtp_crypto::PublicKeyBundle,
|
||||||
|
pk_bytes: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "crypto")]
|
#[cfg(feature = "crypto")]
|
||||||
impl MTPHost {
|
impl MTPHost {
|
||||||
/*
|
/*
|
||||||
|
|
@ -278,51 +350,9 @@ impl MTPHost {
|
||||||
sender: Sender,
|
sender: Sender,
|
||||||
receiver: Receiver,
|
receiver: Receiver,
|
||||||
) -> Result<Option<MTPConnection>, AcceptError> {
|
) -> Result<Option<MTPConnection>, AcceptError> {
|
||||||
use mtp_crypto::{
|
use mtp_crypto::PublicKeyBundle;
|
||||||
Ed25519Signer, MlDsaSigner, PublicKeyBundle, SignatureScheme, auth, verify_ed25519,
|
|
||||||
verify_ml_dsa,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Flow-specific state resolved from the client's opening hello.
|
|
||||||
enum Flow {
|
|
||||||
Login {
|
|
||||||
id: u64,
|
|
||||||
bundle: PublicKeyBundle,
|
|
||||||
},
|
|
||||||
Register {
|
|
||||||
bundle: PublicKeyBundle,
|
|
||||||
pk_bytes: Vec<u8>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
let tm = mtp_codec::TypeMap::latest();
|
let tm = mtp_codec::TypeMap::latest();
|
||||||
let pq_enabled = !self
|
|
||||||
.config
|
|
||||||
.host_keyring
|
|
||||||
.sig_pq_secret_key
|
|
||||||
.as_bytes()
|
|
||||||
.is_empty();
|
|
||||||
|
|
||||||
// Sign `payload` with the host keys (Ed25519 always, ML-DSA when configured).
|
|
||||||
let host_sign = |payload: &[u8]| -> Result<(Vec<u8>, Vec<u8>), AcceptError> {
|
|
||||||
let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key)
|
|
||||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
|
||||||
let sig = signer
|
|
||||||
.sign(payload)
|
|
||||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
|
||||||
let pq_sig = if pq_enabled {
|
|
||||||
let pq = MlDsaSigner::new(
|
|
||||||
&self.config.host_keyring.sig_pq_secret_key,
|
|
||||||
&self.config.host_keyring.sig_pq_public_key,
|
|
||||||
)
|
|
||||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
|
||||||
pq.sign(payload)
|
|
||||||
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
Ok((sig, pq_sig))
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Step 1: receive the client's unsigned hello =====
|
// ===== Step 1: receive the client's unsigned hello =====
|
||||||
let hello = match receiver.receive().await {
|
let hello = match receiver.receive().await {
|
||||||
|
|
@ -347,6 +377,11 @@ impl MTPHost {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let description = match hello.get_data(DataType::Description) {
|
||||||
|
DataValue::Str(s) => Some(s.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
let (flow, response_type) =
|
let (flow, response_type) =
|
||||||
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
|
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
|
||||||
let cid = match hello.get_data(DataType::Id) {
|
let cid = match hello.get_data(DataType::Id) {
|
||||||
|
|
@ -400,6 +435,57 @@ impl MTPHost {
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
self.complete_auth_handshake(sender, receiver, flow, response_type, &version_str, client_version, description).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Steps 2-4 of the authenticated handshake, shared by both ForceAuthentication
|
||||||
|
* and AllowAuthentication. Takes the already-parsed hello (step 1) via `flow`,
|
||||||
|
* `version_str`, and `client_version`.
|
||||||
|
*/
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn complete_auth_handshake(
|
||||||
|
&self,
|
||||||
|
sender: Sender,
|
||||||
|
receiver: Receiver,
|
||||||
|
flow: Flow,
|
||||||
|
response_type: mtp_codec::CommunicationType,
|
||||||
|
version_str: &str,
|
||||||
|
client_version: Version,
|
||||||
|
description: Option<String>,
|
||||||
|
) -> Result<Option<MTPConnection>, AcceptError> {
|
||||||
|
use mtp_crypto::{
|
||||||
|
auth, verify_ed25519, verify_ml_dsa, Ed25519Signer, MlDsaSigner, SignatureScheme,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tm = mtp_codec::TypeMap::latest();
|
||||||
|
let pq_enabled = !self
|
||||||
|
.config
|
||||||
|
.host_keyring
|
||||||
|
.sig_pq_secret_key
|
||||||
|
.as_bytes()
|
||||||
|
.is_empty();
|
||||||
|
|
||||||
|
let host_sign = |payload: &[u8]| -> Result<(Vec<u8>, Vec<u8>), AcceptError> {
|
||||||
|
let signer = Ed25519Signer::new(&self.config.host_keyring.sig_cl_secret_key)
|
||||||
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||||
|
let sig = signer
|
||||||
|
.sign(payload)
|
||||||
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||||
|
let pq_sig = if pq_enabled {
|
||||||
|
let pq = MlDsaSigner::new(
|
||||||
|
&self.config.host_keyring.sig_pq_secret_key,
|
||||||
|
&self.config.host_keyring.sig_pq_public_key,
|
||||||
|
)
|
||||||
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?;
|
||||||
|
pq.sign(payload)
|
||||||
|
.map_err(|e| AcceptError::AuthenticationFailed(e.to_string()))?
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
Ok((sig, pq_sig))
|
||||||
|
};
|
||||||
|
|
||||||
let challenge_id = match &flow {
|
let challenge_id = match &flow {
|
||||||
Flow::Login { id, .. } => *id,
|
Flow::Login { id, .. } => *id,
|
||||||
Flow::Register { .. } => 0,
|
Flow::Register { .. } => 0,
|
||||||
|
|
@ -464,18 +550,13 @@ impl MTPHost {
|
||||||
|
|
||||||
let (proof_payload, bundle) = match &flow {
|
let (proof_payload, bundle) = match &flow {
|
||||||
Flow::Login { id, bundle } => (
|
Flow::Login { id, bundle } => (
|
||||||
auth::login_proof_payload(&version_str, *id, server_challenge, client_nonce),
|
auth::login_proof_payload(version_str, *id, server_challenge, client_nonce),
|
||||||
bundle,
|
bundle,
|
||||||
),
|
),
|
||||||
Flow::Register {
|
Flow::Register {
|
||||||
bundle, pk_bytes, ..
|
bundle, pk_bytes, ..
|
||||||
} => (
|
} => (
|
||||||
auth::register_proof_payload(
|
auth::register_proof_payload(version_str, pk_bytes, server_challenge, client_nonce),
|
||||||
&version_str,
|
|
||||||
pk_bytes,
|
|
||||||
server_challenge,
|
|
||||||
client_nonce,
|
|
||||||
),
|
|
||||||
bundle,
|
bundle,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
@ -551,11 +632,134 @@ impl MTPHost {
|
||||||
codec,
|
codec,
|
||||||
sender,
|
sender,
|
||||||
receiver,
|
receiver,
|
||||||
|
description,
|
||||||
auth_state: AuthState::Authenticated,
|
auth_state: AuthState::Authenticated,
|
||||||
client_id: assigned_id,
|
client_id: assigned_id,
|
||||||
client_public_key: Some(client_bundle),
|
client_public_key: Some(client_bundle),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Allow-authentication accept: clients may connect with or without
|
||||||
|
* authentication. Register messages always trigger the auth handshake.
|
||||||
|
* Identification with a known client-id triggers login; otherwise the
|
||||||
|
* client is treated as unauthenticated with a random id.
|
||||||
|
*/
|
||||||
|
async fn accept_allow_auth(
|
||||||
|
&mut self,
|
||||||
|
sender: Sender,
|
||||||
|
receiver: Receiver,
|
||||||
|
) -> Result<Option<MTPConnection>, AcceptError> {
|
||||||
|
use mtp_crypto::PublicKeyBundle;
|
||||||
|
|
||||||
|
let tm = mtp_codec::TypeMap::latest();
|
||||||
|
|
||||||
|
let hello = match receiver.receive().await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(e) => return Err(AcceptError::Receive(e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let version_str = match hello.get_data(DataType::Version) {
|
||||||
|
DataValue::Str(s) => s.clone(),
|
||||||
|
_ => {
|
||||||
|
sender.close();
|
||||||
|
return Err(AcceptError::MissingVersion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let client_version = match Version::parse(&version_str) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => {
|
||||||
|
sender.close();
|
||||||
|
return Err(AcceptError::MissingVersion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let description = match hello.get_data(DataType::Description) {
|
||||||
|
DataValue::Str(s) => Some(s.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register → authenticated registration.
|
||||||
|
if hello.get_type() == mtp_codec::CommunicationType::Register.to_id(&tm) {
|
||||||
|
let bundle = match hello.get_data(DataType::PublicKeys) {
|
||||||
|
DataValue::Bytes(b) => PublicKeyBundle::from_bytes(b).map_err(|_| {
|
||||||
|
sender.close();
|
||||||
|
AcceptError::AuthenticationFailed("invalid public key bundle".into())
|
||||||
|
})?,
|
||||||
|
_ => {
|
||||||
|
sender.close();
|
||||||
|
return Err(AcceptError::AuthenticationFailed("missing public keys".into()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let pk_bytes = bundle.as_bytes();
|
||||||
|
return self
|
||||||
|
.complete_auth_handshake(
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
Flow::Register {
|
||||||
|
bundle,
|
||||||
|
pk_bytes,
|
||||||
|
},
|
||||||
|
mtp_codec::CommunicationType::RegisterResponse,
|
||||||
|
&version_str,
|
||||||
|
client_version,
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identification with a known client-id → login.
|
||||||
|
if hello.get_type() == mtp_codec::CommunicationType::Identification.to_id(&tm) {
|
||||||
|
let cid = match hello.get_data(DataType::Id) {
|
||||||
|
DataValue::UnsignedNumber(n) => *n as u64,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
if cid > 0
|
||||||
|
&& let Some(bundle) = (self.config.get_existing_user)(cid).await
|
||||||
|
{
|
||||||
|
return self
|
||||||
|
.complete_auth_handshake(
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
Flow::Login {
|
||||||
|
id: cid,
|
||||||
|
bundle,
|
||||||
|
},
|
||||||
|
mtp_codec::CommunicationType::IdentificationResponse,
|
||||||
|
&version_str,
|
||||||
|
client_version,
|
||||||
|
description,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown (or zero) client id → unauthenticated connection.
|
||||||
|
let negotiated = match self
|
||||||
|
.registry
|
||||||
|
.negotiate(std::slice::from_ref(&client_version))
|
||||||
|
{
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Err(AcceptError::UnsupportedVersion(client_version)),
|
||||||
|
};
|
||||||
|
let codec = VersionedCodec::new(self.registry.clone());
|
||||||
|
return Ok(Some(MTPConnection {
|
||||||
|
version: negotiated,
|
||||||
|
codec,
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
description,
|
||||||
|
auth_state: AuthState::Unauthenticated,
|
||||||
|
client_id: rand::random(),
|
||||||
|
client_public_key: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.close();
|
||||||
|
Err(AcceptError::AuthenticationFailed(
|
||||||
|
"unexpected message type".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
||||||
|
|
@ -302,12 +302,15 @@ impl WasmClient {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let version_str = format!("{}", PROTOCOL_VERSION);
|
let version_str = format!("{}", PROTOCOL_VERSION);
|
||||||
let ident = CommunicationValue::new(CommunicationType::Identification)
|
let mut ident = CommunicationValue::new(CommunicationType::Identification)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str))
|
||||||
.add_typed_default(
|
.add_typed_default(
|
||||||
DataType::Id,
|
DataType::Id,
|
||||||
DataValue::UnsignedNumber(config.client_id as u128),
|
DataValue::UnsignedNumber(config.client_id as u128),
|
||||||
);
|
);
|
||||||
|
if let Some(desc) = &config.description {
|
||||||
|
ident = ident.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||||
|
}
|
||||||
let ident_bytes = ident
|
let ident_bytes = ident
|
||||||
.to_bytes()
|
.to_bytes()
|
||||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||||
|
|
@ -351,12 +354,16 @@ impl WasmClient {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 1. Send the unsigned Identification hello.
|
// 1. Send the unsigned Identification hello.
|
||||||
let hello = CommunicationValue::new(CommunicationType::Identification)
|
let mut hello = CommunicationValue::new(CommunicationType::Identification)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
||||||
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128))
|
.add_typed_default(DataType::Id, DataValue::UnsignedNumber(client_id as u128));
|
||||||
|
if let Some(desc) = &config.description {
|
||||||
|
hello = hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||||
|
}
|
||||||
|
let hello_bytes = hello
|
||||||
.to_bytes()
|
.to_bytes()
|
||||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||||
transport.send_frame(&hello).await?;
|
transport.send_frame(&hello_bytes).await?;
|
||||||
|
|
||||||
// 2. Receive and verify the host's challenge.
|
// 2. Receive and verify the host's challenge.
|
||||||
let server_challenge = self
|
let server_challenge = self
|
||||||
|
|
@ -463,12 +470,16 @@ impl WasmClient {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 1. Send the unsigned Register hello (version + public-key bundle).
|
// 1. Send the unsigned Register hello (version + public-key bundle).
|
||||||
let hello = CommunicationValue::new(CommunicationType::Register)
|
let mut hello = CommunicationValue::new(CommunicationType::Register)
|
||||||
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
.add_typed_default(DataType::Version, DataValue::Str(version_str.clone()))
|
||||||
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()))
|
.add_typed_default(DataType::PublicKeys, DataValue::Bytes(pk_bytes.clone()));
|
||||||
|
if let Some(desc) = &config.description {
|
||||||
|
hello = hello.add_typed_default(DataType::Description, DataValue::Str(desc.clone()));
|
||||||
|
}
|
||||||
|
let hello_bytes = hello
|
||||||
.to_bytes()
|
.to_bytes()
|
||||||
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
.map_err(|e| js_error(&format!("encode failed: {}", e)))?;
|
||||||
transport.send_frame(&hello).await?;
|
transport.send_frame(&hello_bytes).await?;
|
||||||
|
|
||||||
// 2. Receive and verify the host's challenge (register binds id = 0).
|
// 2. Receive and verify the host's challenge (register binds id = 0).
|
||||||
let server_challenge = self
|
let server_challenge = self
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ pub struct ConnectionConfig {
|
||||||
pub(crate) server_certificate_hashes: Option<Vec<String>>,
|
pub(crate) server_certificate_hashes: Option<Vec<String>>,
|
||||||
pub(crate) client_id: u64,
|
pub(crate) client_id: u64,
|
||||||
pub(crate) max_message_size: u32,
|
pub(crate) max_message_size: u32,
|
||||||
|
pub(crate) description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
@ -17,6 +18,7 @@ impl ConnectionConfig {
|
||||||
server_certificate_hashes: None,
|
server_certificate_hashes: None,
|
||||||
client_id: 0,
|
client_id: 0,
|
||||||
max_message_size: 1_000_000_000,
|
max_message_size: 1_000_000_000,
|
||||||
|
description: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,4 +51,14 @@ impl ConnectionConfig {
|
||||||
pub fn max_message_size(&self) -> u32 {
|
pub fn max_message_size(&self) -> u32 {
|
||||||
self.max_message_size
|
self.max_message_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(setter)]
|
||||||
|
pub fn set_description(&mut self, description: String) {
|
||||||
|
self.description = Some(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter)]
|
||||||
|
pub fn description(&self) -> Option<String> {
|
||||||
|
self.description.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue