Add: Finalize unit and integration tests. README for test usage.
This commit is contained in:
parent
15dfec8695
commit
29c0d8c0cf
127
Cargo.lock
generated
127
Cargo.lock
generated
@ -26,6 +26,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anstyle"
|
||||||
|
version = "1.0.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.100"
|
version = "1.0.100"
|
||||||
@ -167,12 +173,40 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "downcast"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
|
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fragile"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
@ -266,6 +300,12 @@ version = "0.2.176"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
@ -323,6 +363,33 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"downcast",
|
||||||
|
"fragile",
|
||||||
|
"lazy_static",
|
||||||
|
"mockall_derive",
|
||||||
|
"predicates",
|
||||||
|
"predicates-tree",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mockall_derive"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "niom-turn"
|
name = "niom-turn"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -334,12 +401,14 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"md5",
|
"md5",
|
||||||
|
"mockall",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
"rustls 0.21.12",
|
"rustls 0.21.12",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha1",
|
||||||
|
"tempfile",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
@ -422,6 +491,32 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates"
|
||||||
|
version = "3.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||||
|
dependencies = [
|
||||||
|
"anstyle",
|
||||||
|
"predicates-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-core"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "predicates-tree"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||||
|
dependencies = [
|
||||||
|
"predicates-core",
|
||||||
|
"termtree",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.101"
|
version = "1.0.101"
|
||||||
@ -519,6 +614,19 @@ version = "0.1.26"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.20.9"
|
version = "0.20.9"
|
||||||
@ -713,6 +821,25 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
"once_cell",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "termtree"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
|
|||||||
@ -34,3 +34,7 @@ thiserror = "1.0"
|
|||||||
crc32fast = "1.3"
|
crc32fast = "1.3"
|
||||||
md5 = "0.7"
|
md5 = "0.7"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
mockall = "0.12"
|
||||||
|
tempfile = "3"
|
||||||
|
|
||||||
|
|||||||
74
tests/README.md
Normal file
74
tests/README.md
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Test Suite Overview
|
||||||
|
|
||||||
|
The test tree is split by concern so it is easy to find the relevant scenarios. Each folder owns its
|
||||||
|
own helpers while all reusable mocks live in `support/mocks.rs`.
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── README.md # This file
|
||||||
|
├── support/ # Shared tooling (stun builders, TLS utils, mocks)
|
||||||
|
│ ├── mocks.rs # mockall definitions grouped by domain
|
||||||
|
│ ├── mod.rs # re-exports used by integration crates
|
||||||
|
│ ├── stun_builders.rs
|
||||||
|
│ └── tls.rs
|
||||||
|
├── udp_turn.rs # Legacy UDP end-to-end happy-path integration
|
||||||
|
├── tls_turn.rs # Legacy TLS allocate/refresh integration
|
||||||
|
├── auth/
|
||||||
|
│ ├── helpers.rs # Realm/nonce utilities + TURN bootstrapping
|
||||||
|
│ ├── unit.rs # Realm mismatch + unknown user unit tests
|
||||||
|
│ ├── integration_udp.rs # UDP auth failures (unknown user)
|
||||||
|
│ └── integration_tls.rs # TLS auth failure path (bad credentials)
|
||||||
|
├── alloc/
|
||||||
|
│ ├── helpers.rs # Lifetime/nonce helpers and UDP harness
|
||||||
|
│ ├── unit.rs # Lifetime clamp + zero release
|
||||||
|
│ └── integration_udp.rs # Refresh request clamping (server path)
|
||||||
|
├── channel/
|
||||||
|
│ ├── helpers.rs # Channel/peer fixtures + harness
|
||||||
|
│ ├── unit.rs # ChannelData parsing coverage
|
||||||
|
│ ├── integration_udp.rs # ChannelBind without allocation → 437
|
||||||
|
│ └── integration_tls.rs # TLS ChannelBind mismatch handling
|
||||||
|
├── config/
|
||||||
|
│ ├── helpers.rs # JSON builders + temp-file writer
|
||||||
|
│ ├── unit.rs # Minimal/malformed parse tests
|
||||||
|
│ └── integration.rs # Config::from_file → AuthManager wiring
|
||||||
|
└── errors/
|
||||||
|
├── helpers.rs # Malformed frame + harness
|
||||||
|
├── unit.rs # Frame stream mock + parse errors
|
||||||
|
├── integration_udp.rs # Malformed packet dropped silently
|
||||||
|
└── integration_tls.rs # TLS reader ignores garbage frames
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mocks
|
||||||
|
|
||||||
|
- `support/mocks.rs` centralises every `mockall` mock. Sections are grouped by domain (Auth,
|
||||||
|
Allocation, Channel, Config, Error) so it is obvious which tests should use which mock.
|
||||||
|
- Tests import mocks via `#[path = "../support/mod.rs"] mod support;` and reach the relevant mock at
|
||||||
|
`support::mocks::MockFoo`.
|
||||||
|
- Additional domain-specific helper traits can be added to `mocks.rs` without touching the main
|
||||||
|
crate.
|
||||||
|
|
||||||
|
## Helper Policy
|
||||||
|
|
||||||
|
- Each folder keeps tiny `helpers.rs` files for fixtures that are only meaningful within that domain
|
||||||
|
(e.g. auth peers, config JSON blobs). This keeps intent local and avoids a mega helper file.
|
||||||
|
- When scenarios need runtime bootstrapping they should add small helper modules in their folder
|
||||||
|
rather than extending `tests/support`.
|
||||||
|
|
||||||
|
## Domain Coverage Highlights
|
||||||
|
|
||||||
|
- **Auth**: Validates realm mismatch and unknown users at unit level plus UDP/TLS rejection flows.
|
||||||
|
- **Allocation**: Exercises lifetime clamping in isolation and via refresh STUN requests.
|
||||||
|
- **Channel**: Covers ChannelData parsing plus UDP/TLS ChannelBind error paths when allocations
|
||||||
|
are missing.
|
||||||
|
- **Config**: Confirms JSON defaults, malformed detection, and `Config::from_file` wiring into an
|
||||||
|
`AuthManager` via a temp file.
|
||||||
|
- **Errors**: Ensures malformed frames trigger parser errors and are ignored by UDP/TLS loops
|
||||||
|
without producing responses.
|
||||||
|
- **End-to-end baselines**: `udp_turn.rs` and `tls_turn.rs` remain as the canonical happy-path
|
||||||
|
integration tests for Allocate/Refresh/Permission flows over both transports.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
- Full suite: `cargo test` (runs unit + integration crates, TLS fixtures included).
|
||||||
|
- Per-domain focus: `cargo test --test auth_integration_udp`, `cargo test --test channel_unit`, etc.
|
||||||
|
- Include ignored tests: none remain; every scenario runs as part of the default suite.
|
||||||
50
tests/alloc/helpers.rs
Normal file
50
tests/alloc/helpers.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//! Allocation test helpers.
|
||||||
|
use crate::support::{default_test_credentials, test_auth_manager};
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use niom_turn::auth::{AuthManager, InMemoryStore};
|
||||||
|
use niom_turn::constants::ATTR_NONCE;
|
||||||
|
use niom_turn::models::stun::StunMessage;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
pub fn sample_client() -> SocketAddr {
|
||||||
|
"127.0.0.1:41000".parse().expect("client addr")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_peer() -> SocketAddr {
|
||||||
|
"127.0.0.1:42000".parse().expect("peer addr")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lifetime_secs(secs: u64) -> Duration {
|
||||||
|
Duration::from_secs(secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_auth_manager() -> AuthManager<InMemoryStore> {
|
||||||
|
let (user, password) = default_test_credentials();
|
||||||
|
test_auth_manager(user, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn_udp_server(
|
||||||
|
auth: AuthManager<InMemoryStore>,
|
||||||
|
allocs: AllocationManager,
|
||||||
|
) -> SocketAddr {
|
||||||
|
let server = UdpSocket::bind("127.0.0.1:0").await.expect("udp bind");
|
||||||
|
let addr = server.local_addr().expect("udp addr");
|
||||||
|
let arc = Arc::new(server);
|
||||||
|
let reader = arc.clone();
|
||||||
|
let auth_clone = auth.clone();
|
||||||
|
let alloc_clone = allocs.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = niom_turn::server::udp_reader_loop(reader, auth_clone, alloc_clone).await;
|
||||||
|
});
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_nonce(msg: &StunMessage) -> Option<String> {
|
||||||
|
msg.attributes
|
||||||
|
.iter()
|
||||||
|
.find(|attr| attr.typ == ATTR_NONCE)
|
||||||
|
.and_then(|attr| String::from_utf8(attr.value.clone()).ok())
|
||||||
|
}
|
||||||
70
tests/alloc/integration_udp.rs
Normal file
70
tests/alloc/integration_udp.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
//! UDP allocation lifecycle integration tests.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use crate::support::stun_builders::{
|
||||||
|
build_allocate_request, build_refresh_request, extract_lifetime, new_transaction_id, parse,
|
||||||
|
};
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use niom_turn::auth;
|
||||||
|
use support::{default_test_credentials, init_tracing, test_auth_manager};
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn refresh_request_is_clamped_to_maximum_lifetime() {
|
||||||
|
init_tracing();
|
||||||
|
let (username, password) = default_test_credentials();
|
||||||
|
let auth_manager = test_auth_manager(username, password);
|
||||||
|
let allocs = AllocationManager::new();
|
||||||
|
let server_addr = spawn_udp_server(auth_manager.clone(), allocs.clone()).await;
|
||||||
|
|
||||||
|
let client = UdpSocket::bind("127.0.0.1:0").await.expect("client bind");
|
||||||
|
let mut buf = [0u8; 1500];
|
||||||
|
|
||||||
|
// Challenge for nonce
|
||||||
|
let challenge = build_allocate_request(None, None, None, None, None);
|
||||||
|
client
|
||||||
|
.send_to(&challenge, server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send challenge");
|
||||||
|
let (len, _) = client.recv_from(&mut buf).await.expect("recv nonce");
|
||||||
|
let resp = parse(&buf[..len]);
|
||||||
|
let nonce = helpers::extract_nonce(&resp).expect("nonce attr");
|
||||||
|
|
||||||
|
// Successful allocation
|
||||||
|
let key = auth::compute_a1_md5(username, auth_manager.realm(), password);
|
||||||
|
let allocate = build_allocate_request(
|
||||||
|
Some(username),
|
||||||
|
Some(auth_manager.realm()),
|
||||||
|
Some(&nonce),
|
||||||
|
Some(&key),
|
||||||
|
Some(600),
|
||||||
|
);
|
||||||
|
client
|
||||||
|
.send_to(&allocate, server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send auth allocate");
|
||||||
|
client.recv_from(&mut buf).await.expect("recv alloc success");
|
||||||
|
|
||||||
|
// Request refresh with value exceeding MAX (7200s) and assert server clamps to 3600
|
||||||
|
let refresh = build_refresh_request(
|
||||||
|
new_transaction_id(),
|
||||||
|
username,
|
||||||
|
auth_manager.realm(),
|
||||||
|
&nonce,
|
||||||
|
&key,
|
||||||
|
7200,
|
||||||
|
);
|
||||||
|
client
|
||||||
|
.send_to(&refresh, server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send refresh");
|
||||||
|
let (len, _) = client.recv_from(&mut buf).await.expect("recv refresh");
|
||||||
|
let resp = parse(&buf[..len]);
|
||||||
|
let lifetime = extract_lifetime(&resp).expect("lifetime attr");
|
||||||
|
assert_eq!(lifetime, 3600);
|
||||||
|
}
|
||||||
44
tests/alloc/unit.rs
Normal file
44
tests/alloc/unit.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//! Allocation lifecycle unit tests covering clamping and removal.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
async fn allocate_sample(manager: &AllocationManager) -> SocketAddr {
|
||||||
|
let server = Arc::new(UdpSocket::bind("127.0.0.1:0").await.expect("udp bind"));
|
||||||
|
let client = sample_client();
|
||||||
|
manager
|
||||||
|
.allocate_for(client, server)
|
||||||
|
.await
|
||||||
|
.expect("allocate relay");
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn refresh_clamps_to_minimum_lifetime() {
|
||||||
|
let manager = AllocationManager::new();
|
||||||
|
let client = allocate_sample(&manager).await;
|
||||||
|
let applied = manager
|
||||||
|
.refresh_allocation(client, Some(Duration::from_secs(30)))
|
||||||
|
.expect("refresh");
|
||||||
|
assert_eq!(applied, Duration::from_secs(60));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn zero_lifetime_removes_allocation() {
|
||||||
|
let manager = AllocationManager::new();
|
||||||
|
let client = allocate_sample(&manager).await;
|
||||||
|
let applied = manager
|
||||||
|
.refresh_allocation(client, Some(Duration::from_secs(0)))
|
||||||
|
.expect("refresh zero");
|
||||||
|
assert_eq!(applied, Duration::from_secs(0));
|
||||||
|
assert!(manager.get_allocation(&client).is_none());
|
||||||
|
}
|
||||||
50
tests/auth/helpers.rs
Normal file
50
tests/auth/helpers.rs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
//! Helpers dedicated to auth-focused tests.
|
||||||
|
use crate::support::{default_test_credentials, test_auth_manager};
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use niom_turn::auth::{AuthManager, InMemoryStore};
|
||||||
|
use niom_turn::config::AuthOptions;
|
||||||
|
use niom_turn::constants::ATTR_NONCE;
|
||||||
|
use niom_turn::models::stun::StunMessage;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
pub fn default_options() -> AuthOptions {
|
||||||
|
AuthOptions::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn loopback_peer() -> SocketAddr {
|
||||||
|
"127.0.0.1:55000".parse().expect("loopback socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_auth_manager() -> AuthManager<InMemoryStore> {
|
||||||
|
let (user, password) = default_test_credentials();
|
||||||
|
test_auth_manager(user, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_credentials() -> (&'static str, &'static str) {
|
||||||
|
default_test_credentials()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn_udp_server(
|
||||||
|
auth: AuthManager<InMemoryStore>,
|
||||||
|
allocs: AllocationManager,
|
||||||
|
) -> SocketAddr {
|
||||||
|
let server = UdpSocket::bind("127.0.0.1:0").await.expect("udp bind");
|
||||||
|
let addr = server.local_addr().expect("udp addr");
|
||||||
|
let server_arc = Arc::new(server);
|
||||||
|
let reader = server_arc.clone();
|
||||||
|
let auth_clone = auth.clone();
|
||||||
|
let alloc_clone = allocs.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = niom_turn::server::udp_reader_loop(reader, auth_clone, alloc_clone).await;
|
||||||
|
});
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_nonce(msg: &StunMessage) -> Option<String> {
|
||||||
|
msg.attributes
|
||||||
|
.iter()
|
||||||
|
.find(|attr| attr.typ == ATTR_NONCE)
|
||||||
|
.and_then(|attr| String::from_utf8(attr.value.clone()).ok())
|
||||||
|
}
|
||||||
116
tests/auth/integration_tls.rs
Normal file
116
tests/auth/integration_tls.rs
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
//! TLS-focused authentication integration tests.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use crate::support::stun_builders::{build_allocate_request, extract_error_code, parse};
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use support::{default_test_credentials, init_tracing, test_auth_manager};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpListener, UdpSocket};
|
||||||
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn tls_rejects_invalid_credentials() {
|
||||||
|
init_tracing();
|
||||||
|
let udp = UdpSocket::bind("127.0.0.1:0").await.expect("udp bind");
|
||||||
|
let udp_arc = Arc::new(udp);
|
||||||
|
let (username, password) = default_test_credentials();
|
||||||
|
let auth = test_auth_manager(username, password);
|
||||||
|
let allocs = AllocationManager::new();
|
||||||
|
|
||||||
|
let (cert, key) = support::tls::generate_self_signed_cert();
|
||||||
|
let mut cfg = tokio_rustls::rustls::ServerConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(vec![cert.clone()], key)
|
||||||
|
.expect("server config");
|
||||||
|
cfg.alpn_protocols.push(b"turn".to_vec());
|
||||||
|
let acceptor = TlsAcceptor::from(Arc::new(cfg));
|
||||||
|
|
||||||
|
let tcp_listener = TcpListener::bind("127.0.0.1:0").await.expect("tcp bind");
|
||||||
|
let tcp_addr = tcp_listener.local_addr().expect("tcp addr");
|
||||||
|
let udp_clone = udp_arc.clone();
|
||||||
|
let auth_clone = auth.clone();
|
||||||
|
let alloc_clone = allocs.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let (stream, peer) = match tcp_listener.accept().await {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
let acceptor = acceptor.clone();
|
||||||
|
let udp_clone = udp_clone.clone();
|
||||||
|
let auth_clone = auth_clone.clone();
|
||||||
|
let alloc_clone = alloc_clone.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match acceptor.accept(stream).await {
|
||||||
|
Ok(mut tls_stream) => {
|
||||||
|
if let Err(e) = niom_turn::tls::handle_tls_connection(
|
||||||
|
&mut tls_stream,
|
||||||
|
peer,
|
||||||
|
udp_clone,
|
||||||
|
auth_clone,
|
||||||
|
alloc_clone,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("tls connection error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("tls accept failed: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||||
|
root_store.add(&cert).expect("add root");
|
||||||
|
let client_cfg = tokio_rustls::rustls::ClientConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_root_certificates(root_store)
|
||||||
|
.with_no_client_auth();
|
||||||
|
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_cfg));
|
||||||
|
|
||||||
|
let tcp_stream = tokio::net::TcpStream::connect(tcp_addr)
|
||||||
|
.await
|
||||||
|
.expect("tcp connect");
|
||||||
|
let domain = tokio_rustls::rustls::ServerName::try_from("localhost").unwrap();
|
||||||
|
let mut tls_stream = connector
|
||||||
|
.connect(domain, tcp_stream)
|
||||||
|
.await
|
||||||
|
.expect("tls connect");
|
||||||
|
|
||||||
|
let allocate = build_allocate_request(None, None, None, None, None);
|
||||||
|
tls_stream
|
||||||
|
.write_all(&allocate)
|
||||||
|
.await
|
||||||
|
.expect("write challenge");
|
||||||
|
let mut buf = vec![0u8; 1500];
|
||||||
|
let n = tls_stream.read(&mut buf).await.expect("read nonce");
|
||||||
|
let resp = parse(&buf[..n]);
|
||||||
|
let nonce = extract_nonce(&resp).expect("nonce attr");
|
||||||
|
|
||||||
|
let key = niom_turn::auth::compute_a1_md5(username, auth.realm(), "wrongpass");
|
||||||
|
let request = build_allocate_request(
|
||||||
|
Some(username),
|
||||||
|
Some(auth.realm()),
|
||||||
|
Some(&nonce),
|
||||||
|
Some(&key),
|
||||||
|
Some(600),
|
||||||
|
);
|
||||||
|
tls_stream
|
||||||
|
.write_all(&request)
|
||||||
|
.await
|
||||||
|
.expect("write invalid alloc");
|
||||||
|
let n = tls_stream.read(&mut buf).await.expect("read reject");
|
||||||
|
let resp = parse(&buf[..n]);
|
||||||
|
let code = extract_error_code(&resp).expect("error attr");
|
||||||
|
assert_eq!(code, 401);
|
||||||
|
}
|
||||||
53
tests/auth/integration_udp.rs
Normal file
53
tests/auth/integration_udp.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
//! UDP-focused authentication integration tests.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use crate::support::stun_builders::{build_allocate_request, extract_error_code, parse};
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use support::{default_test_credentials, init_tracing, test_auth_manager};
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn udp_rejects_unknown_user_after_nonce() {
|
||||||
|
init_tracing();
|
||||||
|
let (username, password) = default_test_credentials();
|
||||||
|
let auth = test_auth_manager(username, password);
|
||||||
|
let allocs = AllocationManager::new();
|
||||||
|
let server_addr = spawn_udp_server(auth.clone(), allocs.clone()).await;
|
||||||
|
|
||||||
|
let client = UdpSocket::bind("127.0.0.1:0").await.expect("client bind");
|
||||||
|
|
||||||
|
// Trigger initial challenge to receive nonce
|
||||||
|
let request = build_allocate_request(None, None, None, None, None);
|
||||||
|
client
|
||||||
|
.send_to(&request, server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send challenge");
|
||||||
|
let mut buf = [0u8; 1500];
|
||||||
|
let (len, _) = client.recv_from(&mut buf).await.expect("recv nonce");
|
||||||
|
let resp = parse(&buf[..len]);
|
||||||
|
let nonce = extract_nonce(&resp).expect("nonce attr");
|
||||||
|
|
||||||
|
// Attempt to authenticate with an unknown username
|
||||||
|
let intruder = "intruder";
|
||||||
|
let key = niom_turn::auth::compute_a1_md5(intruder, auth.realm(), "wrongpass");
|
||||||
|
let request = build_allocate_request(
|
||||||
|
Some(intruder),
|
||||||
|
Some(auth.realm()),
|
||||||
|
Some(&nonce),
|
||||||
|
Some(&key),
|
||||||
|
Some(600),
|
||||||
|
);
|
||||||
|
client
|
||||||
|
.send_to(&request, server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send invalid auth allocate");
|
||||||
|
let (len, _) = client.recv_from(&mut buf).await.expect("recv reject");
|
||||||
|
let resp = parse(&buf[..len]);
|
||||||
|
let code = extract_error_code(&resp).expect("error code attr");
|
||||||
|
assert_eq!(code, 401);
|
||||||
|
}
|
||||||
80
tests/auth/unit.rs
Normal file
80
tests/auth/unit.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
//! Auth-specific unit tests driven by mock credential stores.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::auth::{self, AuthStatus};
|
||||||
|
use niom_turn::traits::CredentialStore;
|
||||||
|
use support::mocks;
|
||||||
|
use crate::support::stun_builders::{build_allocate_request, parse};
|
||||||
|
|
||||||
|
fn realm_options(realm: &str) -> niom_turn::config::AuthOptions {
|
||||||
|
let mut opts = default_options();
|
||||||
|
opts.realm = realm.to_string();
|
||||||
|
opts.nonce_secret = Some("static-secret".into());
|
||||||
|
opts
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn credential_store_mock_allows_lookup() {
|
||||||
|
let mut store = mocks::MockCredentialStore::new();
|
||||||
|
store
|
||||||
|
.expect_get_password()
|
||||||
|
.with(mocks::predicates::eq("alice"))
|
||||||
|
.returning(|_| Box::pin(async { Some("s3cret".to_string()) }));
|
||||||
|
|
||||||
|
let password = CredentialStore::get_password(&store, "alice").await;
|
||||||
|
assert_eq!(password.as_deref(), Some("s3cret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn rejects_mismatched_realm_requests() {
|
||||||
|
let peer = loopback_peer();
|
||||||
|
let mut store = niom_turn::auth::InMemoryStore::new();
|
||||||
|
store.insert("alice", "secret");
|
||||||
|
let auth = niom_turn::auth::AuthManager::new(store, &realm_options("expected.realm"));
|
||||||
|
let nonce = auth.mint_nonce(&peer);
|
||||||
|
let wrong_realm = "other.realm";
|
||||||
|
let key = auth::compute_a1_md5("alice", wrong_realm, "secret");
|
||||||
|
let buf = build_allocate_request(
|
||||||
|
Some("alice"),
|
||||||
|
Some(wrong_realm),
|
||||||
|
Some(&nonce),
|
||||||
|
Some(&key),
|
||||||
|
Some(600),
|
||||||
|
);
|
||||||
|
let msg = parse(&buf);
|
||||||
|
match auth.authenticate(&msg, &peer).await {
|
||||||
|
AuthStatus::Reject { code, reason } => {
|
||||||
|
assert_eq!(code, 400);
|
||||||
|
assert_eq!(reason, "Realm Mismatch");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected auth result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn rejects_unknown_user() {
|
||||||
|
let peer = loopback_peer();
|
||||||
|
let auth = build_auth_manager();
|
||||||
|
let nonce = auth.mint_nonce(&peer);
|
||||||
|
let key = auth::compute_a1_md5("intruder", auth.realm(), "badpass");
|
||||||
|
let buf = build_allocate_request(
|
||||||
|
Some("intruder"),
|
||||||
|
Some(auth.realm()),
|
||||||
|
Some(&nonce),
|
||||||
|
Some(&key),
|
||||||
|
Some(600),
|
||||||
|
);
|
||||||
|
let msg = parse(&buf);
|
||||||
|
match auth.authenticate(&msg, &peer).await {
|
||||||
|
AuthStatus::Reject { code, reason } => {
|
||||||
|
assert_eq!(code, 401);
|
||||||
|
assert_eq!(reason, "Unknown User");
|
||||||
|
}
|
||||||
|
other => panic!("unexpected auth result: {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
49
tests/channel/helpers.rs
Normal file
49
tests/channel/helpers.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//! Helpers for channel-oriented tests.
|
||||||
|
use crate::support::{default_test_credentials, test_auth_manager};
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use niom_turn::auth::{AuthManager, InMemoryStore};
|
||||||
|
use niom_turn::constants::ATTR_NONCE;
|
||||||
|
use niom_turn::models::stun::StunMessage;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
pub fn sample_channel_number() -> u16 {
|
||||||
|
0x4001
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_peer() -> SocketAddr {
|
||||||
|
"127.0.0.1:43000".parse().expect("peer addr")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample_payload() -> Vec<u8> {
|
||||||
|
b"some-channel-payload".to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_auth_manager() -> AuthManager<InMemoryStore> {
|
||||||
|
let (user, password) = default_test_credentials();
|
||||||
|
test_auth_manager(user, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn_udp_server(
|
||||||
|
auth: AuthManager<InMemoryStore>,
|
||||||
|
allocs: AllocationManager,
|
||||||
|
) -> SocketAddr {
|
||||||
|
let server = UdpSocket::bind("127.0.0.1:0").await.expect("udp bind");
|
||||||
|
let addr = server.local_addr().expect("udp addr");
|
||||||
|
let arc = Arc::new(server);
|
||||||
|
let reader = arc.clone();
|
||||||
|
let auth_clone = auth.clone();
|
||||||
|
let alloc_clone = allocs.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = niom_turn::server::udp_reader_loop(reader, auth_clone, alloc_clone).await;
|
||||||
|
});
|
||||||
|
addr
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn extract_nonce(msg: &StunMessage) -> Option<String> {
|
||||||
|
msg.attributes
|
||||||
|
.iter()
|
||||||
|
.find(|attr| attr.typ == ATTR_NONCE)
|
||||||
|
.and_then(|attr| String::from_utf8(attr.value.clone()).ok())
|
||||||
|
}
|
||||||
117
tests/channel/integration_tls.rs
Normal file
117
tests/channel/integration_tls.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
//! TLS channel bind integration tests.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use crate::support::stun_builders::{build_allocate_request, build_channel_bind_request, extract_error_code, parse};
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use niom_turn::auth;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use support::{default_test_credentials, init_tracing, test_auth_manager};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpListener, UdpSocket};
|
||||||
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn tls_channel_bind_without_allocation_returns_mismatch() {
|
||||||
|
init_tracing();
|
||||||
|
let udp = UdpSocket::bind("127.0.0.1:0").await.expect("udp bind");
|
||||||
|
let udp_arc = Arc::new(udp);
|
||||||
|
let (username, password) = default_test_credentials();
|
||||||
|
let auth_manager = test_auth_manager(username, password);
|
||||||
|
let allocs = AllocationManager::new();
|
||||||
|
|
||||||
|
let (cert, key) = support::tls::generate_self_signed_cert();
|
||||||
|
let mut cfg = tokio_rustls::rustls::ServerConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(vec![cert.clone()], key)
|
||||||
|
.expect("server config");
|
||||||
|
cfg.alpn_protocols.push(b"turn".to_vec());
|
||||||
|
let acceptor = TlsAcceptor::from(Arc::new(cfg));
|
||||||
|
|
||||||
|
let tcp_listener = TcpListener::bind("127.0.0.1:0").await.expect("tcp bind");
|
||||||
|
let tcp_addr = tcp_listener.local_addr().expect("tcp addr");
|
||||||
|
let udp_clone = udp_arc.clone();
|
||||||
|
let auth_clone = auth_manager.clone();
|
||||||
|
let alloc_clone = allocs.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let (stream, peer) = match tcp_listener.accept().await {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
let acceptor = acceptor.clone();
|
||||||
|
let udp_clone = udp_clone.clone();
|
||||||
|
let auth_clone = auth_clone.clone();
|
||||||
|
let alloc_clone = alloc_clone.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match acceptor.accept(stream).await {
|
||||||
|
Ok(mut tls_stream) => {
|
||||||
|
if let Err(e) = niom_turn::tls::handle_tls_connection(
|
||||||
|
&mut tls_stream,
|
||||||
|
peer,
|
||||||
|
udp_clone,
|
||||||
|
auth_clone,
|
||||||
|
alloc_clone,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("tls connection error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("tls accept failed: {:?}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||||
|
root_store.add(&cert).expect("add root");
|
||||||
|
let client_cfg = tokio_rustls::rustls::ClientConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_root_certificates(root_store)
|
||||||
|
.with_no_client_auth();
|
||||||
|
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_cfg));
|
||||||
|
|
||||||
|
let tcp_stream = tokio::net::TcpStream::connect(tcp_addr)
|
||||||
|
.await
|
||||||
|
.expect("tcp connect");
|
||||||
|
let domain = tokio_rustls::rustls::ServerName::try_from("localhost").unwrap();
|
||||||
|
let mut tls_stream = connector
|
||||||
|
.connect(domain, tcp_stream)
|
||||||
|
.await
|
||||||
|
.expect("tls connect");
|
||||||
|
|
||||||
|
// Obtain nonce via challenge
|
||||||
|
let allocate = build_allocate_request(None, None, None, None, None);
|
||||||
|
tls_stream
|
||||||
|
.write_all(&allocate)
|
||||||
|
.await
|
||||||
|
.expect("write challenge");
|
||||||
|
let mut buf = vec![0u8; 1500];
|
||||||
|
let n = tls_stream.read(&mut buf).await.expect("read nonce");
|
||||||
|
let resp = parse(&buf[..n]);
|
||||||
|
let nonce = extract_nonce(&resp).expect("nonce attr");
|
||||||
|
|
||||||
|
let key = auth::compute_a1_md5(username, auth_manager.realm(), password);
|
||||||
|
let channel_req = build_channel_bind_request(
|
||||||
|
username,
|
||||||
|
auth_manager.realm(),
|
||||||
|
&nonce,
|
||||||
|
&key,
|
||||||
|
sample_channel_number(),
|
||||||
|
&sample_peer(),
|
||||||
|
);
|
||||||
|
tls_stream
|
||||||
|
.write_all(&channel_req)
|
||||||
|
.await
|
||||||
|
.expect("write channel bind");
|
||||||
|
let n = tls_stream.read(&mut buf).await.expect("read response");
|
||||||
|
let resp = parse(&buf[..n]);
|
||||||
|
let code = extract_error_code(&resp).expect("error attr");
|
||||||
|
assert_eq!(code, 437);
|
||||||
|
}
|
||||||
55
tests/channel/integration_udp.rs
Normal file
55
tests/channel/integration_udp.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
//! UDP channel bind integration coverage.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use crate::support::stun_builders::{
|
||||||
|
build_allocate_request, build_channel_bind_request, extract_error_code, parse,
|
||||||
|
};
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use niom_turn::auth;
|
||||||
|
use support::{default_test_credentials, init_tracing, test_auth_manager};
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn channel_bind_without_allocation_returns_mismatch() {
|
||||||
|
init_tracing();
|
||||||
|
let (username, password) = default_test_credentials();
|
||||||
|
let auth_manager = test_auth_manager(username, password);
|
||||||
|
let allocs = AllocationManager::new();
|
||||||
|
let server_addr = spawn_udp_server(auth_manager.clone(), allocs.clone()).await;
|
||||||
|
|
||||||
|
let client = UdpSocket::bind("127.0.0.1:0").await.expect("client bind");
|
||||||
|
let mut buf = [0u8; 1500];
|
||||||
|
|
||||||
|
// Challenge to obtain nonce (no allocation performed yet)
|
||||||
|
let challenge = build_allocate_request(None, None, None, None, None);
|
||||||
|
client
|
||||||
|
.send_to(&challenge, server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send challenge");
|
||||||
|
let (len, _) = client.recv_from(&mut buf).await.expect("recv nonce");
|
||||||
|
let resp = parse(&buf[..len]);
|
||||||
|
let nonce = extract_nonce(&resp).expect("nonce attr");
|
||||||
|
|
||||||
|
let key = auth::compute_a1_md5(username, auth_manager.realm(), password);
|
||||||
|
let channel_req = build_channel_bind_request(
|
||||||
|
username,
|
||||||
|
auth_manager.realm(),
|
||||||
|
&nonce,
|
||||||
|
&key,
|
||||||
|
sample_channel_number(),
|
||||||
|
&sample_peer(),
|
||||||
|
);
|
||||||
|
client
|
||||||
|
.send_to(&channel_req, server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send channel bind");
|
||||||
|
let (len, _) = client.recv_from(&mut buf).await.expect("recv error");
|
||||||
|
let resp = parse(&buf[..len]);
|
||||||
|
let code = extract_error_code(&resp).expect("error attr");
|
||||||
|
assert_eq!(code, 437);
|
||||||
|
}
|
||||||
40
tests/channel/unit.rs
Normal file
40
tests/channel/unit.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//! Channel-centric unit scaffolding validating mock sinks.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::stun::{build_channel_data, parse_channel_data};
|
||||||
|
use support::mocks;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn channel_sink_mock_records_payload() {
|
||||||
|
let mut sink = mocks::MockChannelSink::new();
|
||||||
|
sink
|
||||||
|
.expect_send_channel_data()
|
||||||
|
.withf(|channel, payload| *channel == sample_channel_number() && payload == &sample_payload())
|
||||||
|
.returning(|_, _| Box::pin(async { Ok(()) }));
|
||||||
|
|
||||||
|
sink
|
||||||
|
.send_channel_data(sample_channel_number(), sample_payload())
|
||||||
|
.await
|
||||||
|
.expect("channel data to send");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_channel_data_round_trip() {
|
||||||
|
let payload = sample_payload();
|
||||||
|
let frame = build_channel_data(sample_channel_number(), &payload);
|
||||||
|
let (channel, body) = parse_channel_data(&frame).expect("parse channel frame");
|
||||||
|
assert_eq!(channel, sample_channel_number());
|
||||||
|
assert_eq!(body, payload.as_slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_channel_data_rejects_invalid_channel_range() {
|
||||||
|
let mut frame = build_channel_data(sample_channel_number(), &sample_payload());
|
||||||
|
frame[0] = 0x20; // invalid prefix (must be 0x40)
|
||||||
|
assert!(parse_channel_data(&frame).is_none());
|
||||||
|
}
|
||||||
24
tests/config/helpers.rs
Normal file
24
tests/config/helpers.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
//! Helpers for config parsing tests.
|
||||||
|
use serde_json::json;
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
pub fn minimal_config_json() -> String {
|
||||||
|
json!({
|
||||||
|
"server": { "bind": "127.0.0.1:0", "tls_cert": null, "tls_key": null },
|
||||||
|
"credentials": [],
|
||||||
|
"auth": { "realm": "niom-turn.test", "nonce_ttl_seconds": 300 }
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn malformed_config_json() -> String {
|
||||||
|
"{ server: }".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_temp_config(body: &str) -> NamedTempFile {
|
||||||
|
let mut file = NamedTempFile::new().expect("temp file");
|
||||||
|
file.write_all(body.as_bytes()).expect("write temp config");
|
||||||
|
file.flush().expect("flush temp config");
|
||||||
|
file
|
||||||
|
}
|
||||||
29
tests/config/integration.rs
Normal file
29
tests/config/integration.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
//! Config-driven integration scaffolding testing startup paths.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn config_file_round_trip_populates_auth_manager() {
|
||||||
|
let cfg_json = json!({
|
||||||
|
"server": { "bind": "127.0.0.1:3478", "tls_cert": null, "tls_key": null },
|
||||||
|
"credentials": [ { "username": "alice", "password": "secret" } ],
|
||||||
|
"auth": { "realm": "niom-turn.integration", "nonce_ttl_seconds": 120 }
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
let temp = helpers::write_temp_config(&cfg_json);
|
||||||
|
let cfg = niom_turn::config::Config::from_file(temp.path()).expect("config load");
|
||||||
|
assert_eq!(cfg.server.bind, "127.0.0.1:3478");
|
||||||
|
assert_eq!(cfg.credentials.len(), 1);
|
||||||
|
|
||||||
|
let store = niom_turn::auth::InMemoryStore::new();
|
||||||
|
for cred in &cfg.credentials {
|
||||||
|
store.insert(&cred.username, &cred.password);
|
||||||
|
}
|
||||||
|
let auth = niom_turn::auth::AuthManager::new(store, &cfg.auth);
|
||||||
|
assert_eq!(auth.realm(), "niom-turn.integration");
|
||||||
|
}
|
||||||
37
tests/config/unit.rs
Normal file
37
tests/config/unit.rs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
//! Config parsing unit scaffolding using mock sources.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::config::Config;
|
||||||
|
use support::mocks;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn config_source_mock_serves_payload() {
|
||||||
|
let mut source = mocks::MockConfigSource::new();
|
||||||
|
source
|
||||||
|
.expect_load()
|
||||||
|
.returning(|| Ok(minimal_config_json()));
|
||||||
|
|
||||||
|
let body = source.load().expect("config payload");
|
||||||
|
assert!(body.contains("niom-turn.test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parsing_minimal_config_populates_defaults() {
|
||||||
|
let json = minimal_config_json();
|
||||||
|
let cfg: Config = serde_json::from_str(&json).expect("config parse");
|
||||||
|
assert_eq!(cfg.server.bind, "127.0.0.1:0");
|
||||||
|
assert_eq!(cfg.auth.realm, "niom-turn.test");
|
||||||
|
assert_eq!(cfg.auth.nonce_ttl_seconds, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_config_fails_to_parse() {
|
||||||
|
let json = malformed_config_json();
|
||||||
|
let parsed: Result<Config, _> = serde_json::from_str(&json);
|
||||||
|
assert!(parsed.is_err());
|
||||||
|
}
|
||||||
36
tests/errors/helpers.rs
Normal file
36
tests/errors/helpers.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//! Helpers for error-path tests.
|
||||||
|
use crate::support::{default_test_credentials, test_auth_manager};
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use niom_turn::auth::{AuthManager, InMemoryStore};
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
|
||||||
|
pub fn malformed_stun_frame() -> Vec<u8> {
|
||||||
|
vec![0x00, 0x01, 0x02] // too short for STUN header
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn oversized_payload() -> Vec<u8> {
|
||||||
|
vec![0u8; 4096]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_auth_manager() -> AuthManager<InMemoryStore> {
|
||||||
|
let (user, password) = default_test_credentials();
|
||||||
|
test_auth_manager(user, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn spawn_udp_server(
|
||||||
|
auth: AuthManager<InMemoryStore>,
|
||||||
|
allocs: AllocationManager,
|
||||||
|
) -> SocketAddr {
|
||||||
|
let server = UdpSocket::bind("127.0.0.1:0").await.expect("udp bind");
|
||||||
|
let addr = server.local_addr().expect("udp addr");
|
||||||
|
let arc = Arc::new(server);
|
||||||
|
let reader = arc.clone();
|
||||||
|
let auth_clone = auth.clone();
|
||||||
|
let alloc_clone = allocs.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = niom_turn::server::udp_reader_loop(reader, auth_clone, alloc_clone).await;
|
||||||
|
});
|
||||||
|
addr
|
||||||
|
}
|
||||||
92
tests/errors/integration_tls.rs
Normal file
92
tests/errors/integration_tls.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
//! TLS error-path integration tests.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use support::{init_tracing, test_auth_manager, default_test_credentials};
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::net::{TcpListener, UdpSocket};
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn malformed_tls_frame_is_ignored() {
|
||||||
|
init_tracing();
|
||||||
|
let udp = UdpSocket::bind("127.0.0.1:0").await.expect("udp bind");
|
||||||
|
let udp_arc = Arc::new(udp);
|
||||||
|
let (username, password) = default_test_credentials();
|
||||||
|
let auth = test_auth_manager(username, password);
|
||||||
|
let allocs = AllocationManager::new();
|
||||||
|
|
||||||
|
let (cert, key) = support::tls::generate_self_signed_cert();
|
||||||
|
let mut cfg = tokio_rustls::rustls::ServerConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(vec![cert.clone()], key)
|
||||||
|
.expect("server config");
|
||||||
|
cfg.alpn_protocols.push(b"turn".to_vec());
|
||||||
|
let acceptor = TlsAcceptor::from(Arc::new(cfg));
|
||||||
|
|
||||||
|
let tcp_listener = TcpListener::bind("127.0.0.1:0").await.expect("tcp bind");
|
||||||
|
let tcp_addr = tcp_listener.local_addr().expect("tcp addr");
|
||||||
|
let udp_clone = udp_arc.clone();
|
||||||
|
let auth_clone = auth.clone();
|
||||||
|
let alloc_clone = allocs.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let (stream, peer) = match tcp_listener.accept().await {
|
||||||
|
Ok(conn) => conn,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
let acceptor = acceptor.clone();
|
||||||
|
let udp_clone = udp_clone.clone();
|
||||||
|
let auth_clone = auth_clone.clone();
|
||||||
|
let alloc_clone = alloc_clone.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match acceptor.accept(stream).await {
|
||||||
|
Ok(mut tls_stream) => {
|
||||||
|
let _ = niom_turn::tls::handle_tls_connection(
|
||||||
|
&mut tls_stream,
|
||||||
|
peer,
|
||||||
|
udp_clone,
|
||||||
|
auth_clone,
|
||||||
|
alloc_clone,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(e) => tracing::error!("tls accept failed: {:?}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||||
|
root_store.add(&cert).expect("add root");
|
||||||
|
let client_cfg = tokio_rustls::rustls::ClientConfig::builder()
|
||||||
|
.with_safe_defaults()
|
||||||
|
.with_root_certificates(root_store)
|
||||||
|
.with_no_client_auth();
|
||||||
|
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_cfg));
|
||||||
|
|
||||||
|
let tcp_stream = tokio::net::TcpStream::connect(tcp_addr)
|
||||||
|
.await
|
||||||
|
.expect("tcp connect");
|
||||||
|
let domain = tokio_rustls::rustls::ServerName::try_from("localhost").unwrap();
|
||||||
|
let mut tls_stream = connector
|
||||||
|
.connect(domain, tcp_stream)
|
||||||
|
.await
|
||||||
|
.expect("tls connect");
|
||||||
|
|
||||||
|
tls_stream
|
||||||
|
.write_all(&malformed_stun_frame())
|
||||||
|
.await
|
||||||
|
.expect("write malformed");
|
||||||
|
let mut buf = vec![0u8; 512];
|
||||||
|
let result = timeout(Duration::from_millis(200), tls_stream.read(&mut buf)).await;
|
||||||
|
assert!(result.is_err(), "server should not respond to malformed frame");
|
||||||
|
}
|
||||||
30
tests/errors/integration_udp.rs
Normal file
30
tests/errors/integration_udp.rs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//! UDP error-path integration tests.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::alloc::AllocationManager;
|
||||||
|
use support::init_tracing;
|
||||||
|
use tokio::net::UdpSocket;
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn malformed_packet_is_dropped_without_response() {
|
||||||
|
init_tracing();
|
||||||
|
let auth = build_auth_manager();
|
||||||
|
let allocs = AllocationManager::new();
|
||||||
|
let server_addr = spawn_udp_server(auth, allocs).await;
|
||||||
|
|
||||||
|
let client = UdpSocket::bind("127.0.0.1:0").await.expect("client bind");
|
||||||
|
client
|
||||||
|
.send_to(&malformed_stun_frame(), server_addr)
|
||||||
|
.await
|
||||||
|
.expect("send malformed");
|
||||||
|
|
||||||
|
let mut buf = [0u8; 1500];
|
||||||
|
let recv = timeout(Duration::from_millis(200), client.recv_from(&mut buf)).await;
|
||||||
|
assert!(recv.is_err(), "server should not respond to malformed frame");
|
||||||
|
}
|
||||||
28
tests/errors/unit.rs
Normal file
28
tests/errors/unit.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
//! Error-path unit scaffolding leveraging mock frame streams.
|
||||||
|
|
||||||
|
#[path = "../support/mod.rs"]
|
||||||
|
mod support;
|
||||||
|
|
||||||
|
mod helpers;
|
||||||
|
|
||||||
|
use helpers::*;
|
||||||
|
use niom_turn::stun::{parse_message, ParseError};
|
||||||
|
use support::mocks;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn frame_stream_mock_yields_sequence() {
|
||||||
|
let mut stream = mocks::MockFrameStream::new();
|
||||||
|
stream
|
||||||
|
.expect_next_frame()
|
||||||
|
.times(1)
|
||||||
|
.returning(|| Box::pin(async { Some(malformed_stun_frame()) }));
|
||||||
|
|
||||||
|
let first = stream.next_frame().await;
|
||||||
|
assert_eq!(first.as_ref().map(|f| f.len()), Some(3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_message_rejects_short_frame() {
|
||||||
|
let err = parse_message(&malformed_stun_frame()).unwrap_err();
|
||||||
|
assert!(matches!(err, ParseError::TooShort));
|
||||||
|
}
|
||||||
94
tests/support/mocks.rs
Normal file
94
tests/support/mocks.rs
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
//! Centralised mock definitions for test crates.
|
||||||
|
//! Sections are grouped by domain to keep the mapping between mocks and
|
||||||
|
//! their intended test areas obvious.
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use mockall::mock;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
pub mod predicates {
|
||||||
|
#![allow(unused_imports)]
|
||||||
|
pub use mockall::predicate::*;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auth domain -----------------------------------------------------------
|
||||||
|
mock! {
|
||||||
|
pub CredentialStore {}
|
||||||
|
#[async_trait]
|
||||||
|
impl niom_turn::traits::CredentialStore for CredentialStore {
|
||||||
|
async fn get_password(&self, username: &str) -> Option<String>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Allocation domain -----------------------------------------------------
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RelayIo: Send + Sync {
|
||||||
|
async fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, SocketAddr)>;
|
||||||
|
async fn send_to(&self, buf: &[u8], target: SocketAddr) -> std::io::Result<usize>;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub RelayIo {}
|
||||||
|
#[async_trait]
|
||||||
|
impl RelayIo for RelayIo {
|
||||||
|
async fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, SocketAddr)>;
|
||||||
|
async fn send_to(&self, buf: &[u8], target: SocketAddr) -> std::io::Result<usize>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AllocationClock: Send + Sync {
|
||||||
|
fn now(&self) -> Instant;
|
||||||
|
fn advance(&self, delta: Duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub AllocationClock {}
|
||||||
|
impl AllocationClock for AllocationClock {
|
||||||
|
fn now(&self) -> Instant;
|
||||||
|
fn advance(&self, delta: Duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Channel domain --------------------------------------------------------
|
||||||
|
#[async_trait]
|
||||||
|
pub trait ChannelSink: Send + Sync {
|
||||||
|
async fn send_channel_data(&self, channel: u16, payload: Vec<u8>) -> anyhow::Result<()>;
|
||||||
|
async fn send_data_indication(&self, peer: SocketAddr, payload: Vec<u8>) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub ChannelSink {}
|
||||||
|
#[async_trait]
|
||||||
|
impl ChannelSink for ChannelSink {
|
||||||
|
async fn send_channel_data(&self, channel: u16, payload: Vec<u8>) -> anyhow::Result<()>;
|
||||||
|
async fn send_data_indication(&self, peer: SocketAddr, payload: Vec<u8>) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Config domain ---------------------------------------------------------
|
||||||
|
pub trait ConfigSource: Send + Sync {
|
||||||
|
fn load(&self) -> Result<String, std::io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub ConfigSource {}
|
||||||
|
impl ConfigSource for ConfigSource {
|
||||||
|
fn load(&self) -> Result<String, std::io::Error>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Error/Parser domain ---------------------------------------------------
|
||||||
|
#[async_trait]
|
||||||
|
pub trait FrameStream: Send + Sync {
|
||||||
|
async fn next_frame(&self) -> Option<Vec<u8>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock! {
|
||||||
|
pub FrameStream {}
|
||||||
|
#[async_trait]
|
||||||
|
impl FrameStream for FrameStream {
|
||||||
|
async fn next_frame(&self) -> Option<Vec<u8>>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
pub mod mocks;
|
||||||
pub mod stun_builders;
|
pub mod stun_builders;
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
|
|
||||||
|
|||||||
@ -98,6 +98,45 @@ pub fn build_send_request(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a ChannelBind request binding `channel` to `peer`.
|
||||||
|
pub fn build_channel_bind_request(
|
||||||
|
username: &str,
|
||||||
|
realm: &str,
|
||||||
|
nonce: &str,
|
||||||
|
key: &[u8],
|
||||||
|
channel: u16,
|
||||||
|
peer: &std::net::SocketAddr,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
buf.extend_from_slice(&METHOD_CHANNEL_BIND.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&0u16.to_be_bytes());
|
||||||
|
buf.extend_from_slice(&MAGIC_COOKIE_BYTES);
|
||||||
|
let trans = new_transaction_id();
|
||||||
|
buf.extend_from_slice(&trans);
|
||||||
|
|
||||||
|
push_string_attr(&mut buf, ATTR_USERNAME, username);
|
||||||
|
push_string_attr(&mut buf, ATTR_REALM, realm);
|
||||||
|
push_string_attr(&mut buf, ATTR_NONCE, nonce);
|
||||||
|
|
||||||
|
let mut channel_value = vec![0u8; 4];
|
||||||
|
channel_value[0] = (channel >> 8) as u8;
|
||||||
|
channel_value[1] = channel as u8;
|
||||||
|
push_bytes_attr(&mut buf, ATTR_CHANNEL_NUMBER, &channel_value);
|
||||||
|
|
||||||
|
let encoded = niom_turn::stun::encode_xor_peer_address(peer, &trans);
|
||||||
|
push_bytes_attr(&mut buf, ATTR_XOR_PEER_ADDRESS, &encoded);
|
||||||
|
|
||||||
|
append_message_integrity(&mut buf, key);
|
||||||
|
|
||||||
|
// update length
|
||||||
|
let total_len = (buf.len() - 20) as u16;
|
||||||
|
let len_bytes = total_len.to_be_bytes();
|
||||||
|
buf[2] = len_bytes[0];
|
||||||
|
buf[3] = len_bytes[1];
|
||||||
|
|
||||||
|
buf.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
fn build_authenticated_request(
|
fn build_authenticated_request(
|
||||||
method: u16,
|
method: u16,
|
||||||
username: Option<&str>,
|
username: Option<&str>,
|
||||||
@ -208,3 +247,38 @@ fn append_message_integrity(buf: &mut BytesMut, key: &[u8]) {
|
|||||||
pub fn parse(buf: &[u8]) -> niom_turn::models::stun::StunMessage {
|
pub fn parse(buf: &[u8]) -> niom_turn::models::stun::StunMessage {
|
||||||
parse_message(buf).expect("valid stun message")
|
parse_message(buf).expect("valid stun message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extract ERROR-CODE attribute value (e.g. 401, 437) if present.
|
||||||
|
pub fn extract_error_code(msg: &niom_turn::models::stun::StunMessage) -> Option<u16> {
|
||||||
|
msg.attributes
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.typ == ATTR_ERROR_CODE)
|
||||||
|
.and_then(|attr| {
|
||||||
|
if attr.value.len() >= 4 {
|
||||||
|
let class = attr.value[2] as u16;
|
||||||
|
let number = attr.value[3] as u16;
|
||||||
|
Some(class * 100 + number)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract lifetime (seconds) from responses when present.
|
||||||
|
pub fn extract_lifetime(msg: &niom_turn::models::stun::StunMessage) -> Option<u32> {
|
||||||
|
msg.attributes
|
||||||
|
.iter()
|
||||||
|
.find(|a| a.typ == ATTR_LIFETIME)
|
||||||
|
.and_then(|attr| {
|
||||||
|
if attr.value.len() >= 4 {
|
||||||
|
Some(u32::from_be_bytes([
|
||||||
|
attr.value[0],
|
||||||
|
attr.value[1],
|
||||||
|
attr.value[2],
|
||||||
|
attr.value[3],
|
||||||
|
]))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user