| <!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 doSignalingHandshake(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 doSignalingHandshake(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 doSignalingHandshake(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('OperationError', () => |
| pc1.createDataChannel('this is too exhausting!')); |
| assert_throws('OperationError', () => |
| pc2.createDataChannel('this is too exhausting!')); |
| |
| maybeDone(); |
| await resolver; |
| }, 'Channel ID exhaustion handling (before and after connection establishment)'); |
| |
| END DISABLED TEST */ |
| |
| </script> |