| <!doctype html> | |
| <meta charset=utf-8> | |
| <title>RTCDataChannel id attribute</title> | |
| <script src=/resources/testharness.js></script> | |
| <script src=/resources/testharnessreport.js></script> | |
| <script src="RTCPeerConnection-helper.js"></script> | |
| <script> | |
| 'use strict'; | |
| // Test is based on the following revision: | |
| // https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html | |
| // This is the maximum number of streams, NOT the maximum stream ID (which is 65534) | |
| // See: https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-6.2 | |
| const nStreams = 65535; | |
| /* | |
| 6.1. | |
| 21. If the [[DataChannelId]] slot is null (due to no ID being passed into | |
| createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP | |
| transport has already been negotiated, then initialize [[DataChannelId]] to a value | |
| generated by the user agent, according to [RTCWEB-DATA-PROTOCOL] [...] | |
| */ | |
| promise_test(async (t) => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc1 = pc.createDataChannel(''); | |
| const ids = new UniqueSet(); | |
| const offer = await pc.createOffer(); | |
| await pc.setLocalDescription(offer); | |
| // Turn our own offer SDP into valid answer SDP by setting the DTLS role to | |
| // "active". | |
| const answer = { | |
| type: 'answer', | |
| sdp: pc.localDescription.sdp.replace('actpass', 'active') | |
| }; | |
| await pc.setRemoteDescription(answer); | |
| // Since the remote description had an 'active' DTLS role, we're the server | |
| // and should use odd data channel IDs, according to rtcweb-data-channel. | |
| assert_equals(dc1.id % 2, 1, | |
| `Channel created by the DTLS server role must be odd (was ${dc1.id})`); | |
| const dc2 = pc.createDataChannel('another'); | |
| assert_equals(dc2.id % 2, 1, | |
| `Channel created by the DTLS server role must be odd (was ${dc2.id})`); | |
| // Ensure IDs are unique | |
| ids.add(dc1.id, `Channel ID ${dc1.id} should be unique`); | |
| ids.add(dc2.id, `Channel ID ${dc2.id} should be unique`); | |
| }, 'DTLS client uses odd data channel IDs'); | |
| promise_test(async (t) => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc1 = pc.createDataChannel(''); | |
| const ids = new UniqueSet(); | |
| const offer = await pc.createOffer(); | |
| await pc.setLocalDescription(offer); | |
| // Turn our own offer SDP into valid answer SDP by setting the DTLS role to | |
| // 'passive'. | |
| const answer = { | |
| type: 'answer', | |
| sdp: pc.localDescription.sdp.replace('actpass', 'passive') | |
| }; | |
| await pc.setRemoteDescription(answer); | |
| // Since the remote description had a 'passive' DTLS role, we're the client | |
| // and should use even data channel IDs, according to rtcweb-data-channel. | |
| assert_equals(dc1.id % 2, 0, | |
| `Channel created by the DTLS client role must be even (was ${dc1.id})`); | |
| const dc2 = pc.createDataChannel('another'); | |
| assert_equals(dc2.id % 2, 0, | |
| `Channel created by the DTLS client role must be even (was ${dc1.id})`); | |
| // Ensure IDs are unique | |
| ids.add(dc1.id, `Channel ID ${dc1.id} should be unique`); | |
| ids.add(dc2.id, `Channel ID ${dc2.id} should be unique`); | |
| }, 'DTLS server uses even data channel IDs'); | |
| /* | |
| Checks that the id is ignored if "negotiated" is false. | |
| See section 6.1, createDataChannel step 13. | |
| */ | |
| promise_test(async (t) => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const dc1 = pc1.createDataChannel('', { | |
| negotiated: false, | |
| id: 42 | |
| }); | |
| dc1.onopen = t.step_func(() => { | |
| dc1.send(':('); | |
| }); | |
| const dc2 = pc2.createDataChannel('', { | |
| negotiated: false, | |
| id: 42 | |
| }); | |
| // ID should be null prior to negotiation. | |
| assert_equals(dc1.id, null); | |
| assert_equals(dc2.id, null); | |
| exchangeIceCandidates(pc1, pc2); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| // We should now have 2 datachannels with different IDs. | |
| // At least one of the datachannels should not be 42. | |
| // If one has the value 42, it's an accident; if both have, | |
| // they are the same datachannel, and it's a bug. | |
| assert_false(dc1.id == 42 && dc2.id == 42); | |
| }, 'In-band negotiation with a specific ID should not work'); | |
| /* | |
| Check if the implementation still follows the odd/even role correctly if we annoy it with | |
| negotiated channels not following that rule. | |
| Note: This test assumes that the implementation can handle a minimum of 40 data channels. | |
| */ | |
| promise_test(async (t) => { | |
| // Takes the DTLS server role | |
| const pc1 = new RTCPeerConnection(); | |
| // Takes the DTLS client role | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| exchangeIceCandidates(pc1, pc2); | |
| const dcs = []; | |
| const negotiatedDcs = []; | |
| const ids = new UniqueSet(); | |
| // Create 10 DCEP-negotiated channels with pc1 | |
| // Note: These should not have any associated valid ID at this point | |
| for (let i = 0; i < 10; ++i) { | |
| const dc = pc1.createDataChannel('before-connection'); | |
| assert_equals(dc.id, null, 'Channel id must be null before DTLS role has been determined'); | |
| dcs.push(dc); | |
| } | |
| // Create 10 negotiated channels with pc1 violating the odd/even rule | |
| for (let id = 0; id < 20; id += 2) { | |
| const dc = pc1.createDataChannel(`negotiated-not-odd-${id}-before-connection`, { | |
| negotiated: true, | |
| id: id, | |
| }); | |
| assert_equals(dc.id, id, 'Channel id must be set before DTLS role has been determined when negotiated is true'); | |
| negotiatedDcs.push([dc, id]); | |
| ids.add(dc.id, `Channel ID ${dc.id} should be unique`); | |
| } | |
| await exchangeOfferAnswer(pc1, pc2, { | |
| offer: (offer) => { | |
| // Ensure pc1 takes the server role | |
| assert_true(offer.sdp.includes('actpass') || offer.sdp.includes('passive'), | |
| 'pc1 must take the DTLS server role'); | |
| return offer; | |
| }, | |
| answer: (answer) => { | |
| // Ensure pc2 takes the client role | |
| // Note: It very likely will choose 'active' itself | |
| answer.sdp = answer.sdp.replace('actpass', 'active'); | |
| assert_true(answer.sdp.includes('active'), 'pc2 must take the DTLS client role'); | |
| return answer; | |
| }, | |
| }); | |
| for (const dc of dcs) { | |
| assert_equals(dc.id % 2, 1, | |
| `Channel created by the DTLS server role must be odd (was ${dc.id})`); | |
| ids.add(dc.id, `Channel ID ${dc.id} should be unique`); | |
| } | |
| // Create 10 channels with pc1 | |
| for (let i = 0; i < 10; ++i) { | |
| const dc = pc1.createDataChannel('after-connection'); | |
| assert_equals(dc.id % 2, 1, | |
| `Channel created by the DTLS server role must be odd (was ${dc.id})`); | |
| dcs.push(dc); | |
| ids.add(dc.id, `Channel ID ${dc.id} should be unique`); | |
| } | |
| // Create 10 negotiated channels with pc1 violating the odd/even rule | |
| for (let i = 0; i < 10; ++i) { | |
| // Generate a valid even ID that has not been taken, yet. | |
| let id = 20; | |
| while (ids.has(id)) { | |
| id += 2; | |
| } | |
| const dc = pc1.createDataChannel(`negotiated-not-odd-${i}-after-connection`, { | |
| negotiated: true, | |
| id: id, | |
| }); | |
| negotiatedDcs.push([dc, id]); | |
| ids.add(dc.id, `Channel ID ${dc.id} should be unique`); | |
| } | |
| // Since we've added new channels, let's check again that the odd/even role is not violated | |
| for (const dc of dcs) { | |
| assert_equals(dc.id % 2, 1, | |
| `Channel created by the DTLS server role must be odd (was ${dc.id})`); | |
| } | |
| // Let's also make sure the negotiated channels have kept their ID | |
| for (const [dc, id] of negotiatedDcs) { | |
| assert_equals(dc.id, id, 'Negotiated channels should keep their assigned ID'); | |
| } | |
| }, 'Odd/even role should not be violated when mixing with negotiated channels'); | |
| /* | |
| Create 32768 (client), 32767 (server) channels to make sure all ids are exhausted AFTER | |
| establishing a peer connection. | |
| 6.1. createDataChannel | |
| 21. If the [[DataChannelId]] slot is null (due to no ID being passed into | |
| createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP | |
| transport has already been negotiated, then initialize [[DataChannelId]] to a value | |
| generated by the user agent, according to [RTCWEB-DATA-PROTOCOL], and skip | |
| to the next step. If no available ID could be generated, or if the value of the | |
| [[DataChannelId]] slot is being used by an existing RTCDataChannel, throw an | |
| OperationError exception. | |
| */ | |
| /* | |
| TODO: Improve test coverage for RTCSctpTransport.maxChannels. | |
| TODO: Improve test coverage for exhausting channel cases. | |
| */ | |
| /* | |
| Create 32768 (client), 32767 (server) channels to make sure all ids are exhausted BEFORE | |
| establishing a peer connection. | |
| Be aware that late channel id assignment can currently fail in many places not covered by the | |
| spec, see: https://github.com/w3c/webrtc-pc/issues/1818 | |
| 4.4.1.6. | |
| 2.2.6. If description negotiates the DTLS role of the SCTP transport, and there is an | |
| RTCDataChannel with a null id, then generate an ID according to [RTCWEB-DATA-PROTOCOL]. | |
| If no available ID could be generated, then run the following steps: | |
| 1. Let channel be the RTCDataChannel object for which an ID could not be generated. | |
| 2. Set channel's [[ReadyState]] slot to "closed". | |
| 3. Fire an event named error with an OperationError exception at channel. | |
| 4. Fire a simple event named close at channel. | |
| */ | |
| /* TEST DISABLED - it takes so long, it times out. | |
| promise_test(async (t) => { | |
| const resolver = new Resolver(); | |
| // Takes the DTLS server role | |
| const pc1 = new RTCPeerConnection(); | |
| // Takes the DTLS client role | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| exchangeIceCandidates(pc1, pc2); | |
| const dcs = []; | |
| const ids = new UniqueSet(); | |
| let nExpected = 0; | |
| let nActualCloses = 0; | |
| let nActualErrors = 0; | |
| const maybeDone = t.step_func(() => { | |
| if (nExpected === nActualCloses && nExpected === nActualErrors) { | |
| resolver.resolve(); | |
| } | |
| }); | |
| // Create 65535+2 channels (since 65535 streams is a SHOULD, we may have less than that.) | |
| // Create two extra channels to possibly trigger the steps in the description. | |
| // | |
| // Note: Following the spec strictly would assume that this cannot fail. But in reality it will | |
| // fail because the implementation knows how many streams it supports. What it doesn't | |
| // know is how many streams the other peer supports (e.g. what will be negotiated). | |
| for (let i = 0; i < (nStreams + 2); ++i) { | |
| let dc; | |
| try { | |
| const pc = i % 2 === 1 ? pc1 : pc2; | |
| dc = pc.createDataChannel('this is going to be fun'); | |
| dc.onclose = t.step_func(() => { | |
| ++nActualCloses; | |
| maybeDone(); | |
| }); | |
| dc.onerror = t.step_func((e) => { | |
| assert_true(e instanceof RTCError, 'Expect error object to be instance of RTCError'); | |
| assert_equals(e.error, 'sctp-failure', "Expect error to be of type 'sctp-failure'"); | |
| ++nActualErrors; | |
| maybeDone(); | |
| }); | |
| } catch (e) { | |
| assert_equals(e.name, 'OperationError', 'Fail on creation should throw OperationError'); | |
| break; | |
| } | |
| assert_equals(dc.id, null, 'Channel id must be null before DTLS role has been determined'); | |
| assert_not_equals(dc.readyState, 'closed', | |
| 'Channel may not be closed before connection establishment'); | |
| dcs.push([dc, i % 2 === 1]); | |
| } | |
| await exchangeOfferAnswer(pc1, pc2, { | |
| offer: (offer) => { | |
| // Ensure pc1 takes the server role | |
| assert_true(offer.sdp.includes('actpass') || offer.sdp.includes('passive'), | |
| 'pc1 must take the DTLS server role'); | |
| return offer; | |
| }, | |
| answer: (answer) => { | |
| // Ensure pc2 takes the client role | |
| // Note: It very likely will choose 'active' itself | |
| answer.sdp = answer.sdp.replace('actpass', 'active'); | |
| assert_true(answer.sdp.includes('active'), 'pc2 must take the DTLS client role'); | |
| return answer; | |
| }, | |
| }); | |
| // Since the spec does not define a specific order to which channels may fail if an ID could | |
| // not be generated, any of the channels may be affected by the steps of the description. | |
| for (const [dc, odd] of dcs) { | |
| if (dc.readyState !== 'closed') { | |
| assert_equals(dc.id % 2, odd ? 1 : 0, | |
| `Channels created by the DTLS ${odd ? 'server' : 'client'} role must be | |
| ${odd ? 'odd' : 'even'} (was ${dc.id})`); | |
| ids.add(dc.id, `Channel ID ${dc.id} should be unique`); | |
| } else { | |
| ++nExpected; | |
| } | |
| } | |
| // Try creating one further channel on both sides. The attempt should fail since all IDs are | |
| // taken. If one ID is available, the implementation probably miscounts (or I did in the test). | |
| assert_throws_dom('OperationError', () => | |
| pc1.createDataChannel('this is too exhausting!')); | |
| assert_throws_dom('OperationError', () => | |
| pc2.createDataChannel('this is too exhausting!')); | |
| maybeDone(); | |
| await resolver; | |
| }, 'Channel ID exhaustion handling (before and after connection establishment)'); | |
| END DISABLED TEST */ | |
| </script> |