fix(watch-together): re-host an abandoned room code instead of joining it
A room whose peers have all left is a code nobody is using, but the relay kept it bound to the creator's reconnect capability and rejected every other create with room_exists. The app compounded it: enterRoom only promoted to host on room_not_found, so tapping a recent code landed the user in the retained room as a guest of a host that was never coming back, until the cleanup sweep finally dropped the room. Create now replaces a room with no connected peers, and enterRoom hosts the code when its probe join finds an empty room. An occupied room still rejects create, including from its previous owner, and a host that is merely disconnected still reclaims its peer ID through join with the matching token.
This commit is contained in:
+71
-17
@@ -2273,12 +2273,13 @@ func TestIdempotentCreateReannouncesPreviouslyAbsentHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCannotReclaimReservedEmptyRoom(t *testing.T) {
|
||||
func TestCreateReclaimsAbandonedEmptyRoom(t *testing.T) {
|
||||
h := newRelayHarness(t)
|
||||
hostToken, hostVerifier := mustReconnectToken(t)
|
||||
original := &Room{
|
||||
SessionID: "STALE",
|
||||
HostPeerID: "old-host",
|
||||
ProtocolVersion: relayProtocolVersion,
|
||||
hostVerifier: hostVerifier,
|
||||
peerReservations: make(map[string]peerReservation),
|
||||
Peers: map[string]*Client{},
|
||||
@@ -2289,29 +2290,82 @@ func TestCreateCannotReclaimReservedEmptyRoom(t *testing.T) {
|
||||
h.srv.rooms["STALE"] = original
|
||||
h.srv.mu.Unlock()
|
||||
|
||||
creatorToken, _ := mustReconnectToken(t)
|
||||
creator := h.dial(t, "1.1.1.6")
|
||||
creator.send(clientMsg{Type: relayTypeCreate, SessionID: "STALE", PeerID: "new-host"})
|
||||
creator.expectError(relayErrorRoomExists)
|
||||
|
||||
unproved := h.dial(t, "1.1.1.60")
|
||||
unproved.send(clientMsg{Type: relayTypeJoin, SessionID: "STALE", PeerID: "old-host"})
|
||||
unproved.expectError(relayErrorPeerIdUnavailable)
|
||||
|
||||
reconnected := h.dial(t, "1.1.1.61")
|
||||
reconnected.send(clientMsg{
|
||||
Type: relayTypeJoin,
|
||||
SessionID: "STALE",
|
||||
PeerID: "old-host",
|
||||
ReconnectToken: hostToken,
|
||||
creator.send(clientMsg{
|
||||
Type: relayTypeCreate,
|
||||
SessionID: "STALE",
|
||||
PeerID: "new-host",
|
||||
ReconnectToken: creatorToken,
|
||||
ProtocolVersion: relayProtocolVersion,
|
||||
})
|
||||
reconnected.expectAuthority(relayTypeJoined, "old-host")
|
||||
creator.expectAuthority(relayTypeCreated, "new-host")
|
||||
|
||||
h.srv.mu.RLock()
|
||||
current := h.srv.rooms["STALE"]
|
||||
h.srv.mu.RUnlock()
|
||||
if current != original {
|
||||
t.Fatal("reserved room identity was replaced")
|
||||
if current == original {
|
||||
t.Fatal("abandoned room identity survived the reclaim")
|
||||
}
|
||||
|
||||
// The previous owner's capability died with the room it belonged to, and
|
||||
// the live replacement is not reclaimable by anyone, owner included.
|
||||
former := h.dial(t, "1.1.1.60")
|
||||
former.send(clientMsg{
|
||||
Type: relayTypeCreate,
|
||||
SessionID: "STALE",
|
||||
PeerID: "old-host",
|
||||
ReconnectToken: hostToken,
|
||||
ProtocolVersion: relayProtocolVersion,
|
||||
})
|
||||
former.expectError(relayErrorRoomExists)
|
||||
}
|
||||
|
||||
// The recent-rooms flow: a host restarts its app, so it presents a fresh
|
||||
// reconnect capability for a code the relay still holds. The abandoned code
|
||||
// must come back as a hosted room instead of a ghost room with no host.
|
||||
func TestAbandonedCodeIsRecreatableByARestartedHost(t *testing.T) {
|
||||
h := newRelayHarness(t)
|
||||
firstToken, _ := mustReconnectToken(t)
|
||||
host := h.dial(t, "6.4.0.1")
|
||||
host.send(clientMsg{
|
||||
Type: relayTypeCreate,
|
||||
SessionID: "REUSE",
|
||||
PeerID: "H",
|
||||
ReconnectToken: firstToken,
|
||||
ProtocolVersion: relayProtocolVersion,
|
||||
})
|
||||
host.expectAuthority(relayTypeCreated, "H")
|
||||
host.conn.Close()
|
||||
h.waitRoomPeers(t, "REUSE", 0)
|
||||
|
||||
// A restarted app mints a new capability, so it cannot prove the previous
|
||||
// ownership even when it reuses its own peer ID.
|
||||
restartToken, _ := mustReconnectToken(t)
|
||||
restarted := h.dial(t, "6.4.0.2")
|
||||
restarted.send(clientMsg{
|
||||
Type: relayTypeCreate,
|
||||
SessionID: "REUSE",
|
||||
PeerID: "H",
|
||||
ReconnectToken: restartToken,
|
||||
ProtocolVersion: relayProtocolVersion,
|
||||
})
|
||||
recreated := restarted.expectAuthority(relayTypeCreated, "H")
|
||||
if recreated.ReconnectToken != restartToken {
|
||||
t.Fatalf("recreated room token=%q, want the presented capability", recreated.ReconnectToken)
|
||||
}
|
||||
|
||||
guestToken, _ := mustReconnectToken(t)
|
||||
guest := h.dial(t, "6.4.0.3")
|
||||
guest.send(clientMsg{
|
||||
Type: relayTypeJoin,
|
||||
SessionID: "REUSE",
|
||||
PeerID: "G",
|
||||
ReconnectToken: guestToken,
|
||||
ProtocolVersion: relayProtocolVersion,
|
||||
})
|
||||
guest.expectAuthority(relayTypeJoined, "H")
|
||||
restarted.expect(relayTypePeerJoined)
|
||||
}
|
||||
|
||||
func TestCreateReclaimsOwnedEmptyRoomWithoutDoubleCharging(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user