Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement handling of non-actpass setup in offers #1090

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/aiortc/rtcpeerconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,8 @@ async def setRemoteDescription(
iceTransport._role_set = True

# set DTLS role
if description.type == "offer" and media.dtls.role == "client":
dtlsTransport._set_role(role="server")
if description.type == "answer":
dtlsTransport._set_role(
role="server" if media.dtls.role == "client" else "client"
Comment on lines +931 to 935
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw what role do we end up with when none of these conditionals match?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto which I hope means "determined by who wins ICE"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why we are only handling media.dtls.role == "client"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the handling of offers so the existing code assumes actpass. Lets see if I can prove that by adding more tests because who does not like more tests ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I suspected... the current code assumes role=auto in SRD. Which is not quite correct since auto means "determined by DTLS" whereas when we parse actpass we should be setting this to "client" explicitly. This is currently done in createAnswer which is a bit confusing but ok.

Expand Down Expand Up @@ -1273,8 +1275,6 @@ def __validate_description(
raise ValueError("ICE username fragment or password is missing")

# check DTLS role is allowed
fippo marked this conversation as resolved.
Show resolved Hide resolved
if description.type == "offer" and media.dtls.role != "auto":
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to also remove raising error in __validate_description when peer's role in peer's offer is not actpass?

raise ValueError("DTLS setup attribute must be 'actpass' for an offer")

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
214 changes: 190 additions & 24 deletions tests/test_rtcpeerconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4685,30 +4685,6 @@ async def test_setRemoteDescription_media_mismatch(self):
await pc1.close()
await pc2.close()

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

# apply offer
pc1.addTrack(AudioStreamTrack())
offer = await pc1.createOffer()
await pc1.setLocalDescription(offer)
mangled = RTCSessionDescription(
sdp=pc1.localDescription.sdp.replace("a=setup:actpass", "a=setup:active"),
type=pc1.localDescription.type,
)
with self.assertRaises(ValueError) as cm:
await pc2.setRemoteDescription(mangled)
self.assertEqual(
str(cm.exception),
"DTLS setup attribute must be 'actpass' for an offer",
)

# close
await pc1.close()
await pc2.close()

@asynctest
async def test_setRemoteDescription_with_invalid_dtls_setup_for_answer(self):
pc1 = RTCPeerConnection()
Expand Down Expand Up @@ -4976,3 +4952,193 @@ 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)

# 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)

# 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"]
)

@asynctest
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)

# 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"]
)