Skip to content

Commit

Permalink
Implement handling of non-actpass setup in offers
Browse files Browse the repository at this point in the history
RFC 8842 updated RFC 5763 to allow a differt setup attribute than
  actpass
in offers which allows for more control of the DTLS role.

See https://issues.webrtc.org/issues/42223106 for additional details

Fixes #1087
  • Loading branch information
fippo committed May 4, 2024
1 parent e9c13ea commit b57992b
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 3 deletions.
7 changes: 4 additions & 3 deletions src/aiortc/rtcpeerconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,10 @@ async def setRemoteDescription(
iceTransport._role_set = True

# set DTLS role
if description.type == "offer":
dtlsTransport._set_role(
role="server" if media.dtls.role == "client" else "client"
)
if description.type == "answer":
dtlsTransport._set_role(
role="server" if media.dtls.role == "client" else "client"
Expand Down Expand Up @@ -1272,9 +1276,6 @@ def __validate_description(
if not media.ice.usernameFragment or not media.ice.password:
raise ValueError("ICE username fragment or password is missing")

# check DTLS role is allowed
if description.type == "offer" and media.dtls.role != "auto":
raise ValueError("DTLS setup attribute must be 'actpass' for an offer")
if description.type in ["answer", "pranswer"] and media.dtls.role not in [
"client",
"server",
Expand Down
201 changes: 201 additions & 0 deletions tests/test_rtcpeerconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4976,3 +4976,204 @@ async def test_setRemoteDescription_media_datachannel_bundled(self):
"closed",
],
)

@asynctest
async def test_dtls_role_offer_actpass(self):
pc1 = RTCPeerConnection()
pc2 = RTCPeerConnection()

pc1_states = track_states(pc1)
pc2_states = track_states(pc2)

self.assertEqual(pc1.iceConnectionState, "new")
self.assertEqual(pc1.iceGatheringState, "new")
self.assertIsNone(pc1.localDescription)
self.assertIsNone(pc1.remoteDescription)

self.assertEqual(pc2.iceConnectionState, "new")
self.assertEqual(pc2.iceGatheringState, "new")
self.assertIsNone(pc2.localDescription)
self.assertIsNone(pc2.remoteDescription)

"""
initial negotiation
"""

# create offer
pc1.createDataChannel("chat", protocol="")
offer = await pc1.createOffer()
self.assertEqual(offer.type, "offer")

await pc1.setLocalDescription(offer)
self.assertEqual(pc1.iceConnectionState, "new")
self.assertEqual(pc1.iceGatheringState, "complete")

# set remote description
await pc2.setRemoteDescription(pc1.localDescription)

# create answer
answer = await pc2.createAnswer()
self.assertHasDtls(answer, "active")

await pc2.setLocalDescription(answer)
await self.assertIceChecking(pc2)

# handle answer
await pc1.setRemoteDescription(pc2.localDescription)
self.assertEqual(pc1.remoteDescription, pc2.localDescription)

# check outcome
await self.assertIceCompleted(pc1, pc2)

self.assertEqual(pc1.sctp.transport._role, "server")
self.assertEqual(pc2.sctp.transport._role, "client")
# close
await pc1.close()
await pc2.close()
self.assertClosed(pc1)
self.assertClosed(pc2)

# check state changes
self.assertEqual(
pc1_states["connectionState"], ["new", "connecting", "connected", "closed"]
)
self.assertEqual(
pc2_states["connectionState"], ["new", "connecting", "connected", "closed"]
)

@asynctest
async def test_dtls_role_offer_passive(self):
pc1 = RTCPeerConnection()
pc2 = RTCPeerConnection()

pc1_states = track_states(pc1)
pc2_states = track_states(pc2)

self.assertEqual(pc1.iceConnectionState, "new")
self.assertEqual(pc1.iceGatheringState, "new")
self.assertIsNone(pc1.localDescription)
self.assertIsNone(pc1.remoteDescription)

self.assertEqual(pc2.iceConnectionState, "new")
self.assertEqual(pc2.iceGatheringState, "new")
self.assertIsNone(pc2.localDescription)
self.assertIsNone(pc2.remoteDescription)

"""
initial negotiation
"""

# create offer
pc1.createDataChannel("chat", protocol="")
offer = await pc1.createOffer()
self.assertEqual(offer.type, "offer")

await pc1.setLocalDescription(offer)
self.assertEqual(pc1.iceConnectionState, "new")
self.assertEqual(pc1.iceGatheringState, "complete")

# handle offer with replaced DTLS role
await pc2.setRemoteDescription(
RTCSessionDescription(
type="offer", sdp=pc1.localDescription.sdp.replace("actpass", "passive")
)
)

# create answer
answer = await pc2.createAnswer()
self.assertHasDtls(answer, "active")

await pc2.setLocalDescription(answer)
await self.assertIceChecking(pc2)

# handle answer
await pc1.setRemoteDescription(pc2.localDescription)
self.assertEqual(pc1.remoteDescription, pc2.localDescription)

# check outcome
await self.assertIceCompleted(pc1, pc2)

# pc1 is explicity passive so server.
self.assertEqual(pc1.sctp.transport._role, "server")
self.assertEqual(pc2.sctp.transport._role, "client")
# close
await pc1.close()
await pc2.close()
self.assertClosed(pc1)
self.assertClosed(pc2)

# check state changes
self.assertEqual(
pc1_states["connectionState"], ["new", "connecting", "connected", "closed"]
)
self.assertEqual(
pc2_states["connectionState"], ["new", "connecting", "connected", "closed"]
)

async def test_dtls_role_offer_active(self):
pc1 = RTCPeerConnection()
pc2 = RTCPeerConnection()

pc1_states = track_states(pc1)
pc2_states = track_states(pc2)

self.assertEqual(pc1.iceConnectionState, "new")
self.assertEqual(pc1.iceGatheringState, "new")
self.assertIsNone(pc1.localDescription)
self.assertIsNone(pc1.remoteDescription)

self.assertEqual(pc2.iceConnectionState, "new")
self.assertEqual(pc2.iceGatheringState, "new")
self.assertIsNone(pc2.localDescription)
self.assertIsNone(pc2.remoteDescription)

"""
initial negotiation
"""

# create offer
pc1.createDataChannel("chat", protocol="")
offer = await pc1.createOffer()
self.assertEqual(offer.type, "offer")

await pc1.setLocalDescription(offer)
self.assertEqual(pc1.iceConnectionState, "new")
self.assertEqual(pc1.iceGatheringState, "complete")

# handle offer with replaced DTLS role
await pc2.setRemoteDescription(
RTCSessionDescription(
type="offer", sdp=pc1.localDescription.sdp.replace("actpass", "active")
)
)

# create answer
answer = await pc2.createAnswer()
self.assertHasDtls(answer, "passive")

await pc2.setLocalDescription(answer)
await self.assertIceChecking(pc2)

# handle answer
await pc1.setRemoteDescription(pc2.localDescription)
self.assertEqual(pc1.remoteDescription, pc2.localDescription)

# check outcome
await self.assertIceCompleted(pc1, pc2)

# pc1 is explicity active so client.
self.assertEqual(pc1.sctp.transport._role, "client")
self.assertEqual(pc2.sctp.transport._role, "server")
# close
await pc1.close()
await pc2.close()
self.assertClosed(pc1)
self.assertClosed(pc2)

# check state changes
self.assertEqual(
pc1_states["connectionState"], ["new", "connecting", "connected", "closed"]
)
self.assertEqual(
pc2_states["connectionState"], ["new", "connecting", "connected", "closed"]
)

0 comments on commit b57992b

Please sign in to comment.