Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions quest/m1/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ existing lite-06 ALPN.

## Quests

- [Interop grants](/quest/m1/auth/interop.md) - the interop matrix asserts
each AUTH cell's grant and that a publish outside it fails loud
- [Unauthorized reset](/quest/m1/auth/unauthorized.md) - a subscription that
loses access resets with a dedicated UNAUTHORIZED stream code
- [Path patterns](/quest/m1/auth/patterns.md) - one matcher for every path
Expand Down
6 changes: 3 additions & 3 deletions quest/m1/auth/bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ mid-stream can be handed a new one without the plugin reconnecting.
async calls use, in `rs/libmoq/src/api.rs` and the session table;
regenerate `moq.h`, and update `cpp/obs/src` only if the plugin surfaces a
token field, otherwise leave it.
- Interop: the Python, Go, and C interop clients print their grant and join
the assertion [Interop grants](/quest/m1/auth/interop.md) adds for Rust
and JS.
- Interop: the Python, Go, and C interop clients print their grant as the
`auth granted publish=[...] subscribe=[...]` line and join `prints_grant` and
`enforces_grant` in `test/interop/interop.sh`, beside Rust and JS.
- Wrappers: `py/moq-rs/moq/session.py`, `swift/Sources/Moq`,
`kt/.../Flows.kt` (a `Flow` over `grant_changed`), `go/wrapper/moq/session.go`
(context-cancellable like the rest), and `dart/moq/lib/moq.dart`. Kotlin
Expand Down
21 changes: 0 additions & 21 deletions quest/m1/auth/interop.md

This file was deleted.

8 changes: 6 additions & 2 deletions rs/moq-ffi/src/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ impl MoqBroadcastProducer {
let _guard = crate::ffi::enter();
self.with_state(|state| {
let track = state.broadcast.create_track(name, None)?;
let producer = state.catalog.binary_snapshot(track, config.into())?;
let producer = state
.catalog
.binary_snapshot(track, moq_mux::binary::Config::from(config))?;
Ok(Arc::new(MoqBinarySnapshotProducer {
inner: std::sync::Mutex::new(Some(producer)),
}))
Expand All @@ -66,7 +68,9 @@ impl MoqBroadcastProducer {
let _guard = crate::ffi::enter();
self.with_state(|state| {
let track = state.broadcast.create_track(name, None)?;
let producer = state.catalog.binary_stream(track, config.into())?;
let producer = state
.catalog
.binary_stream(track, moq_mux::binary::Config::from(config))?;
Ok(Arc::new(MoqBinaryStreamProducer {
inner: std::sync::Mutex::new(Some(producer)),
}))
Expand Down
8 changes: 8 additions & 0 deletions rs/moq-net/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,14 @@ impl Handle {

/// Record an AUTH_OK for the token.
pub(crate) fn granted(&self, id: u64, grant: Grant) {
// One parseable line per AUTH_OK, so the grant the peer actually sent is observable
// without an API; the interop harness checks it against the token it minted.
let list = |patterns: &Patterns| format!("{:?}", patterns.iter().map(|p| p.to_string()).collect::<Vec<_>>());
tracing::debug!(
publish = %list(&grant.publish),
subscribe = %list(&grant.subscribe),
"auth granted"
);
let mut state = self.state.lock();
let Some(slot) = state.tokens.get_mut(&id) else {
return;
Expand Down
26 changes: 25 additions & 1 deletion test/interop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ than this checkout: it's a prebuilt NAPI QUIC/HTTP3 addon, not part of the moq
source tree. Everything else (`@moq/net`, `@moq/hang`, ...) resolves to the
workspace packages, because the JS clients here are bun workspace members.

## Auth

The relay verifies tokens through `moq auth serve` with a key generated for the
run; nothing is anonymous. Each publisher dials with a token for its broadcast's
subtree and each subscriber with one to read it, so every cell also covers the
`?jwt=` URL path in every client.

Clients that print the grant the relay sent back over AUTH, as an
`auth granted publish=[...] subscribe=[...]` line, must report exactly what
their token implies: the Rust CLI (a `moq_net::auth` debug log) and the native
JS subscribers. A cell whose grant is missing or wrong fails even when media
flowed. The binding clients (Python, Go, C, GStreamer) have no grant to print
until moq-ffi exposes one, and the browser's shared connection keeps its session
private, so their cells check media alone. Every client here negotiates
moq-lite-06, so a printing client that reports nothing never got its grant.

After the matrix, each publisher whose refusal the harness can read (Rust, the
browser) runs once more with a token that excludes its broadcast. It must fail
loud, logging Unauthorized and naming the path, and every subscriber must time
out.

Tokens grant subtrees (`name/**`) because moq-lite-06's AUTH_OK carries prefixes:
the relay withholds a literal grant it cannot encode, and the client sees none.

## Running locally

You need the workspace toolchain on `PATH` (cargo, ffmpeg, bun, uv, go,
Expand Down Expand Up @@ -161,7 +185,7 @@ contract](../README.md).

```text
interop.sh orchestrator: build clients, run the relay + matrix or media checks
interop.toml relay config (anonymous, self-signed localhost)
interop.toml relay config (token auth via `moq auth serve`, self-signed localhost)
clients/
python/interop.py publish/subscribe via py/moq-rs (import moq)
go/main.go publish/subscribe via go/wrapper (import moq-go/moq)
Expand Down
11 changes: 11 additions & 0 deletions test/interop/clients/js-native/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ if (role !== "subscribe" || !url || !broadcast || !Number.isFinite(timeoutMs) ||
async function run(): Promise<void> {
const origin = new Moq.Origin.Producer();
const connection = await Moq.Connection.connect({ url: new URL(url as string), consume: origin });
// The grant the relay sent, in the Rust client's `auth granted` shape, so the harness can
// check it against the token this cell minted.
const printGrant = (grant: Moq.Auth.Grant | undefined) => {
if (!grant) return;
const publish = JSON.stringify(grant.publish);
const subscribe = JSON.stringify(grant.subscribe);
console.error(`auth granted publish=${publish} subscribe=${subscribe}`);
};
printGrant(connection.auth.grant.peek());
const unwatch = connection.auth.grant.subscribe(printGrant);
let requested: Moq.Origin.Requesting | undefined;
try {
const path = Moq.Path.from(broadcast as string);
Expand Down Expand Up @@ -104,6 +114,7 @@ async function run(): Promise<void> {
}
throw new Error("no frame data received");
} finally {
unwatch();
requested?.close();
connection.close(); // returns void, not a promise
origin.close();
Expand Down
Loading
Loading