From b57992b6b167df9e450090c6bd3d5e3790aa443b Mon Sep 17 00:00:00 2001 From: Philipp Hancke Date: Fri, 3 May 2024 17:44:22 -0700 Subject: [PATCH] Implement handling of non-actpass setup in offers 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 --- src/aiortc/rtcpeerconnection.py | 7 +- tests/test_rtcpeerconnection.py | 201 ++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+), 3 deletions(-) diff --git a/src/aiortc/rtcpeerconnection.py b/src/aiortc/rtcpeerconnection.py index 17e06cb0a..9af680845 100644 --- a/src/aiortc/rtcpeerconnection.py +++ b/src/aiortc/rtcpeerconnection.py @@ -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" @@ -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", diff --git a/tests/test_rtcpeerconnection.py b/tests/test_rtcpeerconnection.py index ba51b4af1..5e1b6e9d8 100644 --- a/tests/test_rtcpeerconnection.py +++ b/tests/test_rtcpeerconnection.py @@ -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"] + )