Skip to content

Commit 59185f5

Browse files
authored
livekit_api: Fix LiveKit token revocation timestamps (#60157)
LiveKit Cloud rejects room-join tokens as revoked when their `nbf` predates a participant revocation. Zed generated those LiveKit JWTs with `nbf: 0`, so a fresh participant token minted after stale connection cleanup could still appear older than the cleanup and leave a user joined at the collab layer without audio or screen sharing. This sets `nbf` to the issuance time for room-join tokens while leaving admin/API tokens unchanged, and adds regression coverage at the token, mock LiveKit, and channel rejoin layers. Closes FR-83 Release Notes: - Fixed calls getting stuck without audio or screen sharing after restarting Zed and rejoining a channel.
1 parent 98ddc3a commit 59185f5

7 files changed

Lines changed: 570 additions & 35 deletions

File tree

crates/collab/tests/integration/channel_tests.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,95 @@ async fn test_channel_room(
589589
);
590590
}
591591

592+
#[gpui::test]
593+
async fn test_rejoining_channel_after_stale_connection_cleanup_connects_livekit(
594+
executor: BackgroundExecutor,
595+
cx_a: &mut TestAppContext,
596+
cx_a2: &mut TestAppContext,
597+
cx_b: &mut TestAppContext,
598+
) {
599+
let mut server = TestServer::start(executor.clone()).await;
600+
let client_a = server.create_client(cx_a, "user_a").await;
601+
let client_b = server.create_client(cx_b, "user_b").await;
602+
603+
let channel_id = server
604+
.make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)])
605+
.await;
606+
607+
let active_call_a = cx_a.read(ActiveCall::global);
608+
active_call_a
609+
.update(cx_a, |active_call, cx| {
610+
active_call.join_channel(channel_id, cx)
611+
})
612+
.await
613+
.unwrap();
614+
615+
let active_call_b = cx_b.read(ActiveCall::global);
616+
active_call_b
617+
.update(cx_b, |active_call, cx| {
618+
active_call.join_channel(channel_id, cx)
619+
})
620+
.await
621+
.unwrap();
622+
623+
executor.run_until_parked();
624+
625+
let old_room_a =
626+
cx_a.read(|cx| active_call_a.read_with(cx, |call, _| call.room().unwrap().clone()));
627+
cx_a.read(|cx| old_room_a.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
628+
629+
server.disconnect_client(client_a.peer_id().unwrap());
630+
executor.run_until_parked();
631+
server.advance_livekit_timestamp();
632+
633+
let client_a2 = server.create_client(cx_a2, "user_a").await;
634+
let active_call_a2 = cx_a2.read(ActiveCall::global);
635+
active_call_a2
636+
.update(cx_a2, |active_call, cx| {
637+
active_call.join_channel(channel_id, cx)
638+
})
639+
.await
640+
.unwrap();
641+
642+
executor.run_until_parked();
643+
644+
let room_a2 =
645+
cx_a2.read(|cx| active_call_a2.read_with(cx, |call, _| call.room().unwrap().clone()));
646+
cx_a2.read(|cx| room_a2.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
647+
assert_eq!(
648+
room_participants(&room_a2, cx_a2),
649+
RoomParticipants {
650+
remote: vec!["user_b".to_string()],
651+
pending: vec![]
652+
}
653+
);
654+
655+
let room_b =
656+
cx_b.read(|cx| active_call_b.read_with(cx, |call, _| call.room().unwrap().clone()));
657+
cx_b.read(|cx| room_b.read_with(cx, |room, cx| assert!(room.is_connected(cx))));
658+
assert_eq!(
659+
room_participants(&room_b, cx_b),
660+
RoomParticipants {
661+
remote: vec!["user_a".to_string()],
662+
pending: vec![]
663+
}
664+
);
665+
666+
cx_a2.read(|cx| {
667+
client_a2.channel_store().read_with(cx, |channels, _| {
668+
let mut participant_ids = channels
669+
.channel_participants(channel_id)
670+
.iter()
671+
.map(|participant| participant.legacy_id)
672+
.collect::<Vec<_>>();
673+
participant_ids.sort_unstable();
674+
let mut expected_ids = vec![client_a2.user_id().unwrap(), client_b.user_id().unwrap()];
675+
expected_ids.sort_unstable();
676+
assert_eq!(participant_ids, expected_ids);
677+
})
678+
});
679+
}
680+
592681
#[gpui::test]
593682
async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppContext) {
594683
let mut server = TestServer::start(executor.clone()).await;

crates/collab/tests/integration/test_server.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ use std::{
4848
use util::path;
4949
use workspace::{MultiWorkspace, Workspace, WorkspaceStore};
5050

51-
use livekit_client::test::TestServer as LivekitTestServer;
51+
use livekit_client::test::{ManualUnixTimestampSource, TestServer as LivekitTestServer};
5252

5353
use crate::db_tests::TestDb;
5454

5555
pub struct TestServer {
5656
pub app_state: Arc<AppState>,
5757
pub test_livekit_server: Arc<LivekitTestServer>,
5858
pub test_db: TestDb,
59+
livekit_timestamp_source: Arc<ManualUnixTimestampSource>,
5960
server: Arc<Server>,
6061
next_github_user_id: i32,
6162
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
@@ -96,11 +97,13 @@ impl TestServer {
9697
TestDb::sqlite(deterministic.clone())
9798
};
9899
let livekit_server_id = NEXT_LIVEKIT_SERVER_ID.fetch_add(1, SeqCst);
99-
let livekit_server = LivekitTestServer::create(
100+
let livekit_timestamp_source = Arc::new(ManualUnixTimestampSource::new(1_234_567));
101+
let livekit_server = LivekitTestServer::create_with_timestamp_source(
100102
format!("http://livekit.{}.test", livekit_server_id),
101103
format!("devkey-{}", livekit_server_id),
102104
format!("secret-{}", livekit_server_id),
103105
deterministic.clone(),
106+
livekit_timestamp_source.clone(),
104107
)
105108
.unwrap();
106109
let executor = Executor::Deterministic(deterministic.clone());
@@ -121,10 +124,15 @@ impl TestServer {
121124
forbid_connections: Default::default(),
122125
next_github_user_id: 0,
123126
test_db,
127+
livekit_timestamp_source,
124128
test_livekit_server: livekit_server,
125129
}
126130
}
127131

132+
pub fn advance_livekit_timestamp(&self) {
133+
self.livekit_timestamp_source.advance();
134+
}
135+
128136
pub async fn start2(
129137
cx_a: &mut TestAppContext,
130138
cx_b: &mut TestAppContext,

crates/livekit_api/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ workspace = true
1313
path = "src/livekit_api.rs"
1414
doctest = false
1515

16+
[features]
17+
test-support = []
18+
1619
[dependencies]
1720
anyhow.workspace = true
1821
async-trait.workspace = true

crates/livekit_api/src/livekit_api.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,25 @@ pub struct LiveKitClient {
3131
url: Arc<str>,
3232
key: Arc<str>,
3333
secret: Arc<str>,
34+
timestamp_source: Arc<dyn token::UnixTimestampSource>,
3435
}
3536

3637
impl LiveKitClient {
37-
pub fn new(mut url: String, key: String, secret: String) -> Self {
38+
pub fn new(url: String, key: String, secret: String) -> Self {
39+
Self::new_with_timestamp_source(
40+
url,
41+
key,
42+
secret,
43+
Arc::new(token::SystemUnixTimestampSource),
44+
)
45+
}
46+
47+
pub(crate) fn new_with_timestamp_source(
48+
mut url: String,
49+
key: String,
50+
secret: String,
51+
timestamp_source: Arc<dyn token::UnixTimestampSource>,
52+
) -> Self {
3853
if url.ends_with('/') {
3954
url.pop();
4055
}
@@ -47,6 +62,7 @@ impl LiveKitClient {
4762
url: url.into(),
4863
key: key.into(),
4964
secret: secret.into(),
65+
timestamp_source,
5066
}
5167
}
5268

@@ -61,7 +77,13 @@ impl LiveKitClient {
6177
Res: Default + Message,
6278
{
6379
let client = self.http.clone();
64-
let token = token::create(&self.key, &self.secret, None, grant);
80+
let token = token::create_with_timestamp_source(
81+
&self.key,
82+
&self.secret,
83+
None,
84+
grant,
85+
self.timestamp_source.as_ref(),
86+
);
6587
let url = format!("{}/{}", self.url, path);
6688
log::info!("Request {}: {:?}", url, body);
6789
async move {
@@ -163,20 +185,22 @@ impl Client for LiveKitClient {
163185
}
164186

165187
fn room_token(&self, room: &str, identity: &str) -> Result<String> {
166-
token::create(
188+
token::create_with_timestamp_source(
167189
&self.key,
168190
&self.secret,
169191
Some(identity),
170192
token::VideoGrant::to_join(room),
193+
self.timestamp_source.as_ref(),
171194
)
172195
}
173196

174197
fn guest_token(&self, room: &str, identity: &str) -> Result<String> {
175-
token::create(
198+
token::create_with_timestamp_source(
176199
&self.key,
177200
&self.secret,
178201
Some(identity),
179202
token::VideoGrant::for_guest(room),
203+
self.timestamp_source.as_ref(),
180204
)
181205
}
182206
}

0 commit comments

Comments
 (0)