tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

RTCDataChannel-send.html (18128B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <meta name="timeout" content="long">
      4 <title>RTCDataChannel.prototype.send</title>
      5 <script src="/resources/testharness.js"></script>
      6 <script src="/resources/testharnessreport.js"></script>
      7 <script src="RTCPeerConnection-helper.js"></script>
      8 <script src="RTCDataChannel-helper.js"></script>
      9 <script src="third_party/sdp/sdp.js"></script>
     10 <script>
     11 'use strict';
     12 
     13 // Test is based on the following editor draft:
     14 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html
     15 
     16 // The following helper functions are called from RTCPeerConnection-helper.js:
     17 //  createDataChannelPair
     18 //  awaitMessage
     19 //  blobToArrayBuffer
     20 //  assert_equals_typed_array
     21 
     22 /*
     23  6.2.  RTCDataChannel
     24    interface RTCDataChannel : EventTarget {
     25      ...
     26      readonly  attribute RTCDataChannelState readyState;
     27      readonly  attribute unsigned long       bufferedAmount;
     28                attribute EventHandler        onmessage;
     29                attribute DOMString           binaryType;
     30 
     31      void send(USVString data);
     32      void send(Blob data);
     33      void send(ArrayBuffer data);
     34      void send(ArrayBufferView data);
     35    };
     36 */
     37 
     38 // Simple ASCII encoded string
     39 const helloString = 'hello';
     40 const emptyString = '';
     41 // ASCII encoded buffer representation of the string
     42 const helloBuffer = Uint8Array.of(0x68, 0x65, 0x6c, 0x6c, 0x6f);
     43 const emptyBuffer = new Uint8Array();
     44 const helloBlob = new Blob([helloBuffer]);
     45 
     46 // Unicode string with multiple code units
     47 const unicodeString = '世界你好';
     48 // UTF-8 encoded buffer representation of the string
     49 const unicodeBuffer = Uint8Array.of(
     50  0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c,
     51  0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd);
     52 
     53 /*
     54    6.2.  send()
     55      2.  If channel's readyState attribute is connecting, throw an InvalidStateError.
     56 */
     57 test(t => {
     58  const pc = new RTCPeerConnection();
     59  t.add_cleanup(() => pc.close());
     60  const channel = pc.createDataChannel('test');
     61  assert_equals(channel.readyState, 'connecting');
     62  assert_throws_dom('InvalidStateError', () => channel.send(helloString));
     63 }, 'Calling send() when data channel is in connecting state should throw InvalidStateError');
     64 
     65 for (const options of [{}, {negotiated: true, id: 0}]) {
     66  const mode = `${options.negotiated? "Negotiated d" : "D"}atachannel`;
     67 
     68  /*
     69    6.2.  send()
     70      3.  Execute the sub step that corresponds to the type of the methods argument:
     71 
     72          string object
     73            Let data be the object and increase the bufferedAmount attribute
     74            by the number of bytes needed to express data as UTF-8.
     75 
     76    [WebSocket]
     77    5.  Feedback from the protocol
     78      When a WebSocket message has been received
     79        4.  If type indicates that the data is Text, then initialize event's data
     80            attribute to data.
     81   */
     82  promise_test(t => {
     83    return createDataChannelPairWithLabel(t, 'string', options)
     84    .then(([channel1, channel2]) => {
     85      channel1.send(helloString);
     86      return awaitMessage(channel2)
     87    }).then(message => {
     88      assert_equals(typeof message, 'string',
     89        'Expect message to be a string');
     90 
     91      assert_equals(message, helloString);
     92    });
     93  }, `${mode} should be able to send simple string and receive as string`);
     94 
     95  promise_test(t => {
     96    return createDataChannelPairWithLabel(t, 'unicode', options)
     97    .then(([channel1, channel2]) => {
     98      channel1.send(unicodeString);
     99      return awaitMessage(channel2)
    100    }).then(message => {
    101      assert_equals(typeof message, 'string',
    102        'Expect message to be a string');
    103 
    104      assert_equals(message, unicodeString);
    105    });
    106  }, `${mode} should be able to send unicode string and receive as unicode string`);
    107  promise_test(t => {
    108    return createDataChannelPairWithLabel(t, 'recv_string', options)
    109    .then(([channel1, channel2]) => {
    110      channel2.binaryType = 'arraybuffer';
    111      channel1.send(helloString);
    112      return awaitMessage(channel2);
    113    }).then(message => {
    114      assert_equals(typeof message, 'string',
    115        'Expect message to be a string');
    116 
    117      assert_equals(message, helloString);
    118    });
    119  }, `${mode} should ignore binaryType and always receive string message as string`);
    120  promise_test(t => {
    121    return createDataChannelPairWithLabel(t, 'emptystring', options)
    122    .then(([channel1, channel2]) => {
    123      channel1.send(emptyString);
    124      // Send a non-empty string in case the implementation ignores empty messages
    125      channel1.send(helloString);
    126      return awaitMessage(channel2)
    127    }).then(message => {
    128      assert_equals(typeof message, 'string',
    129        'Expect message to be a string');
    130 
    131      assert_equals(message, emptyString);
    132    });
    133  }, `${mode} should be able to send an empty string and receive an empty string`);
    134 
    135  /*
    136    6.2. send()
    137      3.  Execute the sub step that corresponds to the type of the methods argument:
    138          ArrayBufferView object
    139            Let data be the data stored in the section of the buffer described
    140            by the ArrayBuffer object that the ArrayBufferView object references
    141            and increase the bufferedAmount attribute by the length of the
    142            ArrayBufferView in bytes.
    143 
    144    [WebSocket]
    145    5.  Feedback from the protocol
    146      When a WebSocket message has been received
    147        4.  If binaryType is set to "arraybuffer", then initialize event's data
    148            attribute to a new read-only ArrayBuffer object whose contents are data.
    149 
    150    [WebIDL]
    151    4.1.  ArrayBufferView
    152      typedef (Int8Array or Int16Array or Int32Array or
    153        Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or
    154        Float32Array or Float64Array or DataView) ArrayBufferView;
    155   */
    156  promise_test(t => {
    157    return createDataChannelPairWithLabel(t, 'array_to_arraybuffer', options)
    158    .then(([channel1, channel2]) => {
    159      channel2.binaryType = 'arraybuffer';
    160      channel1.send(helloBuffer);
    161      return awaitMessage(channel2)
    162    }).then(messageBuffer => {
    163      assert_true(messageBuffer instanceof ArrayBuffer,
    164        'Expect messageBuffer to be an ArrayBuffer');
    165 
    166      assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
    167    });
    168  }, `${mode} should be able to send Uint8Array message and receive as ArrayBuffer`);
    169 
    170  /*
    171    6.2. send()
    172      3.  Execute the sub step that corresponds to the type of the methods argument:
    173          ArrayBuffer object
    174            Let data be the data stored in the buffer described by the ArrayBuffer
    175            object and increase the bufferedAmount attribute by the length of the
    176            ArrayBuffer in bytes.
    177   */
    178  promise_test(t => {
    179    return createDataChannelPairWithLabel(t, 'arraybuffer', options)
    180    .then(([channel1, channel2]) => {
    181      channel2.binaryType = 'arraybuffer';
    182      channel1.send(helloBuffer.buffer);
    183      return awaitMessage(channel2)
    184    }).then(messageBuffer => {
    185      assert_true(messageBuffer instanceof ArrayBuffer,
    186        'Expect messageBuffer to be an ArrayBuffer');
    187 
    188      assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
    189    });
    190  }, `${mode} should be able to send ArrayBuffer message and receive as ArrayBuffer`);
    191 
    192  promise_test(t => {
    193    return createDataChannelPairWithLabel(t, 'empty_arraybuffer', options)
    194    .then(([channel1, channel2]) => {
    195      channel2.binaryType = 'arraybuffer';
    196      channel1.send(emptyBuffer.buffer);
    197      // Send a non-empty buffer in case the implementation ignores empty messages
    198      channel1.send(helloBuffer.buffer);
    199      return awaitMessage(channel2)
    200    }).then(messageBuffer => {
    201      assert_true(messageBuffer instanceof ArrayBuffer,
    202        'Expect messageBuffer to be an ArrayBuffer');
    203 
    204      assert_equals_typed_array(messageBuffer, emptyBuffer.buffer);
    205    });
    206  }, `${mode} should be able to send an empty ArrayBuffer message and receive as ArrayBuffer`);
    207 
    208  /*
    209    6.2. send()
    210      3.  Execute the sub step that corresponds to the type of the methods argument:
    211          Blob object
    212            Let data be the raw data represented by the Blob object and increase
    213            the bufferedAmount attribute by the size of data, in bytes.
    214   */
    215  promise_test(t => {
    216    return createDataChannelPairWithLabel(t, 'blob_to_arraybuffer', options)
    217    .then(([channel1, channel2]) => {
    218      channel2.binaryType = 'arraybuffer';
    219      channel1.send(helloBlob);
    220      return awaitMessage(channel2);
    221    }).then(messageBuffer => {
    222      assert_true(messageBuffer instanceof ArrayBuffer,
    223        'Expect messageBuffer to be an ArrayBuffer');
    224 
    225      assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
    226    });
    227  }, `${mode} should be able to send Blob message and receive as ArrayBuffer`);
    228 
    229  /*
    230    [WebSocket]
    231    5.  Feedback from the protocol
    232      When a WebSocket message has been received
    233        4.  If binaryType is set to "blob", then initialize event's data attribute
    234            to a new Blob object that represents data as its raw data.
    235   */
    236  promise_test(t => {
    237    return createDataChannelPairWithLabel(t, 'arraybuffer_to_blob', options)
    238    .then(([channel1, channel2]) => {
    239      channel2.binaryType = 'blob';
    240      channel1.send(helloBuffer);
    241      return awaitMessage(channel2);
    242    })
    243    .then(messageBlob => {
    244      assert_true(messageBlob instanceof Blob,
    245        'Expect received messageBlob to be a Blob');
    246 
    247      return blobToArrayBuffer(messageBlob);
    248    }).then(messageBuffer => {
    249      assert_true(messageBuffer instanceof ArrayBuffer,
    250        'Expect messageBuffer to be an ArrayBuffer');
    251 
    252      assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
    253    });
    254  }, `${mode} should be able to send ArrayBuffer message and receive as Blob`);
    255 
    256  /*
    257    6.2.  RTCDataChannel
    258      binaryType
    259        The binaryType attribute must, on getting, return the value to which it was
    260        last set. On setting, the user agent must set the IDL attribute to the new
    261        value. When a RTCDataChannel object is created, the binaryType attribute must
    262        be initialized to the string "blob".
    263   */
    264  promise_test(t => {
    265    return createDataChannelPairWithLabel(t, 'arraybuffer_recv', options)
    266    .then(([channel1, channel2]) => {
    267      assert_equals(channel2.binaryType, 'arraybuffer',
    268        'Expect initial binaryType value to be arraybuffer');
    269 
    270      channel1.send(helloBuffer);
    271      return awaitMessage(channel2);
    272    }).then(messageBuffer => {
    273      assert_true(messageBuffer instanceof ArrayBuffer,
    274        'Expect messageBuffer to be an ArrayBuffer');
    275 
    276      assert_equals_typed_array(messageBuffer, helloBuffer.buffer);
    277    });
    278  }, `${mode} binaryType should receive message as ArrayBuffer by default`);
    279 
    280  // Test sending 3 messages: helloBuffer, unicodeString, helloBlob
    281  promise_test(t => {
    282    const receivedMessages = [];
    283 
    284    const onMessage = t.step_func(event => {
    285      const { data } = event;
    286      receivedMessages.push(data);
    287 
    288      if(receivedMessages.length === 3) {
    289        assert_equals_typed_array(receivedMessages[0], helloBuffer.buffer);
    290        assert_equals(receivedMessages[1], unicodeString);
    291        assert_equals_typed_array(receivedMessages[2], helloBuffer.buffer);
    292 
    293        t.done();
    294      }
    295    });
    296 
    297    return createDataChannelPairWithLabel(t, 'multitype', options)
    298    .then(([channel1, channel2]) => {
    299      channel2.binaryType = 'arraybuffer';
    300      channel2.addEventListener('message', onMessage);
    301 
    302      channel1.send(helloBuffer);
    303      channel1.send(unicodeString);
    304      channel1.send(helloBlob);
    305 
    306    }).catch(t.step_func(err =>
    307      assert_unreached(`Unexpected promise rejection: ${err}`)));
    308  }, `${mode} sending multiple messages with different types should succeed and be received`);
    309 
    310  /*
    311    [Deferred]
    312    6.2.  RTCDataChannel
    313      The send() method is being amended in w3c/webrtc-pc#1209 to throw error instead
    314      of closing data channel when buffer is full
    315 
    316      send()
    317        4.  If channel's underlying data transport is not established yet, or if the
    318            closing procedure has started, then abort these steps.
    319        5.  Attempt to send data on channel's underlying data transport; if the data
    320            cannot be sent, e.g. because it would need to be buffered but the buffer
    321            is full, the user agent must abruptly close channel's underlying data
    322            transport with an error.
    323 
    324    test(t => {
    325      const pc = new RTCPeerConnection();
    326      const channel = pc.createDataChannel('test');
    327      channel.close();
    328      assert_equals(channel.readyState, 'closing');
    329      channel.send(helloString);
    330    }, 'Calling send() when data channel is in closing state should succeed');
    331  */
    332 }
    333 
    334  promise_test(async t => {
    335    // This test uses some bundle shenanigans to cause packet loss
    336    // The intent is to create a situation where a small message is received
    337    // when there is a partially received large message, and check whether
    338    // that small message is misinterpreted as part of the large message
    339    // instead of its own thing.
    340    const pc1 = new RTCPeerConnection({bundlePolicy: 'balanced'});
    341    const pc2 = new RTCPeerConnection();
    342    t.add_cleanup(() => pc1.close());
    343    t.add_cleanup(() => pc2.close());
    344    pc1.addTransceiver('audio');
    345    pc1.addTransceiver('video');
    346    const channel1 = pc1.createDataChannel('foo1', {id: 1, ordered: false, negotiated: true});
    347    const channel2 = pc2.createDataChannel('foo2', {id: 1, ordered: false, negotiated: true});
    348    const channel1Open = new Promise(r => channel1.onopen = r);
    349    const channel2Open = new Promise(r => channel2.onopen = r);
    350    exchangeIceCandidates(pc1, pc2);
    351 
    352    const size = 1024*1024*5;
    353    const bigMessage = 'a'.repeat(size);
    354    const smallMessage = 'boom';
    355    const numLittle = 1000;
    356 
    357    const receivedMessages = new Promise((resolve, reject) => {
    358      let littleReceived = 0;
    359      let bigReceived = false;
    360      channel2.onmessage = ({data}) => {
    361        try {
    362          if (data.length == size) {
    363            assert_false(bigReceived, 'Only received big message once');
    364            bigReceived = true;
    365            assert_equals(data, bigMessage, 'Big message has correct contents');
    366          } else if (data.length == smallMessage.length) {
    367            littleReceived++;
    368            assert_equals(data, smallMessage, 'Small message has correct contents');
    369            assert_less_than_equal(littleReceived, numLittle, 'Got no more than expected small messages');
    370          } else {
    371            assert_true(false, `Message was an expected size (got ${data.length}, last bytes are ${data.slice(-20)})`);
    372          }
    373 
    374          if (littleReceived == 1000 && bigReceived) {
    375            resolve();
    376          }
    377        } catch (e) {
    378          reject(e);
    379        }
    380      };
    381    });
    382 
    383    function rebundleMids(offer, mids) {
    384      // Disable any existing groups
    385      let sdp = offer.sdp.replaceAll('a=group:BUNDLE','a=moop:BUNGLE');
    386      sdp = sdp.replaceAll('a=msid-semantic:', 'a=msid-pedantic:');
    387      // Create group attrs for the mids we want
    388      const midsString = mids.join(' ');
    389      sdp = sdp.replace('\r\nm=', `\r\na=group:BUNDLE ${midsString}\r\na=msid-semantic:WMS *\r\nm=`);
    390      return {type: 'offer', sdp};
    391    }
    392 
    393    await pc1.setLocalDescription();
    394    let offer = pc1.localDescription;
    395 
    396    // offer should have three separate transports, one for each mid
    397    const sections = SDPUtils.splitSections(offer.sdp);
    398    assert_equals(sections.length, 4);
    399    const mids = sections.slice(1).map(section => SDPUtils.getMid(section));
    400    assert_equals(mids.length, 3);
    401 
    402    // Cause the DataChannel msection to be bundled with the audio msection
    403    await pc2.setRemoteDescription(rebundleMids(offer, [mids[0], mids[2]]));
    404    await pc2.setLocalDescription();
    405    await pc1.setRemoteDescription(pc2.localDescription);
    406    await channel1Open;
    407    await channel2Open;
    408 
    409    channel1.send(bigMessage);
    410 
    411    // Send until we're almost done with the first (big) message
    412    channel1.bufferedAmountLowThreshold = 20000;
    413    await new Promise(r => channel1.onbufferedamountlow = r);
    414 
    415    // Ok, now for the shenanigans
    416    let reoffer = await pc1.createOffer();
    417    // Make pc2 _think_ the datachannel is now bundled with the video msection,
    418    // so it will discard the packets
    419    await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[1], mids[2]]));
    420    await pc2.setLocalDescription();
    421 
    422    for (let i = 0; i < numLittle; i++) {
    423      channel1.send('boom');
    424    }
    425 
    426    // Now, put it back the way it is supposed to be
    427    await pc1.setLocalDescription();
    428    reoffer = pc1.localDescription;
    429    await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[0], mids[2]]));
    430    await pc2.setLocalDescription();
    431    await pc1.setRemoteDescription(pc2.localDescription);
    432    await receivedMessages;
    433  }, `Sending multiple messages simultaneously in unordered mode works reliably`);
    434 
    435 promise_test(async t => {
    436  const offerer = new RTCPeerConnection();
    437  const answerer = new RTCPeerConnection();
    438  t.add_cleanup(() => offerer.close());
    439  t.add_cleanup(() => answerer.close());
    440  const channel1 = offerer.createDataChannel('foo');
    441 
    442  const toSend = [];
    443  for (let i = 0; i < 100; i++) {
    444    toSend.push(`So anyway I started blastin' ${i}`);
    445  }
    446 
    447  function blastMessages(channel) {
    448    assert_equals(channel.readyState, 'open', 'channel should already be open');
    449    for (const message of toSend) {
    450      channel.send(message);
    451    }
    452  }
    453 
    454  // Set up both channels to send messages as soon as they are open
    455  channel1.onopen = () => blastMessages(channel1);
    456 
    457  const channel2Created = new Promise(r => {
    458    answerer.ondatachannel = ({channel}) => {
    459      blastMessages(channel);
    460      r(channel);
    461    }
    462  });
    463 
    464  async function receiveMessages(channel) {
    465    const received = [];
    466    while (received.length < toSend.length) {
    467      const {data} = await new Promise(r => channel.onmessage = r);
    468      received.push(data);
    469    }
    470    return received;
    471  }
    472 
    473  const received1 = receiveMessages(channel1);
    474 
    475  await negotiate(offerer, answerer);
    476 
    477  const channel2 = await channel2Created;
    478  assert_array_equals(await receiveMessages(channel2), toSend);
    479  assert_array_equals(await received1, toSend);
    480 }, 'Sending before the other side is open should work');
    481 
    482 
    483 </script>