tor-browser

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

RTCPeerConnection-operations.https.html (15650B)


      1 <!doctype html>
      2 <meta charset=utf-8>
      3 <title></title>
      4 <script src=/resources/testharness.js></script>
      5 <script src=/resources/testharnessreport.js></script>
      6 <script>
      7 'use strict';
      8 
      9 // Helpers to test APIs "return a promise rejected with a newly created" error.
     10 // Strictly speaking this means already-rejected upon return.
     11 function promiseState(p) {
     12  const t = {};
     13  return Promise.race([p, t])
     14    .then(v => (v === t)? "pending" : "fulfilled", () => "rejected");
     15 }
     16 
     17 // However, to allow promises to be used in implementations, this helper adds
     18 // some slack: returning a pending promise will pass, provided it is rejected
     19 // before the end of the current run of the event loop (i.e. on microtask queue
     20 // before next task).
     21 async function promiseStateFinal(p) {
     22  for (let i = 0; i < 20; i++) {
     23    await promiseState(p);
     24  }
     25  return promiseState(p);
     26 }
     27 
     28 [promiseState, promiseStateFinal].forEach(f => promise_test(async t => {
     29  assert_equals(await f(Promise.resolve()), "fulfilled");
     30  assert_equals(await f(Promise.reject()), "rejected");
     31  assert_equals(await f(new Promise(() => {})), "pending");
     32 }, `${f.name} helper works`));
     33 
     34 promise_test(async t => {
     35  const pc = new RTCPeerConnection();
     36  t.add_cleanup(() => pc.close());
     37  await pc.setRemoteDescription(await pc.createOffer());
     38  const p = pc.createOffer();
     39  const haveState = promiseStateFinal(p);
     40  try {
     41    await p;
     42    assert_unreached("Control. Must not succeed");
     43  } catch (e) {
     44    assert_equals(e.name, "InvalidStateError");
     45  }
     46  assert_equals(await haveState, "rejected", "promise rejected on same task");
     47 }, "createOffer must detect InvalidStateError synchronously when chain is empty (prerequisite)");
     48 
     49 promise_test(async t => {
     50  const pc = new RTCPeerConnection();
     51  t.add_cleanup(() => pc.close());
     52  const p = pc.createAnswer();
     53  const haveState = promiseStateFinal(p);
     54  try {
     55    await p;
     56    assert_unreached("Control. Must not succeed");
     57  } catch (e) {
     58    assert_equals(e.name, "InvalidStateError");
     59  }
     60  assert_equals(await haveState, "rejected", "promise rejected on same task");
     61 }, "createAnswer must detect InvalidStateError synchronously when chain is empty (prerequisite)");
     62 
     63 promise_test(async t => {
     64  const pc = new RTCPeerConnection();
     65  t.add_cleanup(() => pc.close());
     66  const p = pc.setLocalDescription({type: "rollback"});
     67  const haveState = promiseStateFinal(p);
     68  try {
     69    await p;
     70    assert_unreached("Control. Must not succeed");
     71  } catch (e) {
     72    assert_equals(e.name, "InvalidStateError");
     73  }
     74  assert_equals(await haveState, "rejected", "promise rejected on same task");
     75 }, "SLD(rollback) must detect InvalidStateError synchronously when chain is empty");
     76 
     77 promise_test(async t => {
     78  const pc = new RTCPeerConnection();
     79  t.add_cleanup(() => pc.close());
     80  const p = pc.addIceCandidate();
     81  const haveState = promiseStateFinal(p);
     82  try {
     83    await p;
     84    assert_unreached("Control. Must not succeed");
     85  } catch (e) {
     86    assert_equals(e.name, "InvalidStateError");
     87  }
     88  assert_equals(pc.remoteDescription, null, "no remote desciption");
     89  assert_equals(await haveState, "rejected", "promise rejected on same task");
     90 }, "addIceCandidate must detect InvalidStateError synchronously when chain is empty");
     91 
     92 promise_test(async t => {
     93  const pc = new RTCPeerConnection();
     94  t.add_cleanup(() => pc.close());
     95  const transceiver = pc.addTransceiver("audio");
     96  transceiver.stop();
     97  const p = transceiver.sender.replaceTrack(null);
     98  const haveState = promiseStateFinal(p);
     99  try {
    100    await p;
    101    assert_unreached("Control. Must not succeed");
    102  } catch (e) {
    103    assert_equals(e.name, "InvalidStateError");
    104  }
    105  assert_equals(await haveState, "rejected", "promise rejected on same task");
    106 }, "replaceTrack must detect InvalidStateError synchronously when chain is empty and transceiver is stopped");
    107 
    108 promise_test(async t => {
    109  const pc = new RTCPeerConnection();
    110  t.add_cleanup(() => pc.close());
    111  const transceiver = pc.addTransceiver("audio");
    112  transceiver.stop();
    113  const parameters = transceiver.sender.getParameters();
    114  const p = transceiver.sender.setParameters(parameters);
    115  const haveState = promiseStateFinal(p);
    116  try {
    117    await p;
    118    assert_unreached("Control. Must not succeed");
    119  } catch (e) {
    120    assert_equals(e.name, "InvalidStateError");
    121  }
    122  assert_equals(await haveState, "rejected", "promise rejected on same task");
    123 }, "setParameters must detect InvalidStateError synchronously always when transceiver is stopped");
    124 
    125 promise_test(async t => {
    126  const pc = new RTCPeerConnection();
    127  t.add_cleanup(() => pc.close());
    128  const {track} = new RTCPeerConnection().addTransceiver("audio").receiver;
    129  assert_not_equals(track, null);
    130  const p = pc.getStats(track);
    131  const haveState = promiseStateFinal(p);
    132  try {
    133    await p;
    134    assert_unreached("Control. Must not succeed");
    135  } catch (e) {
    136    assert_equals(e.name, "InvalidAccessError");
    137  }
    138  assert_equals(await haveState, "rejected", "promise rejected on same task");
    139 }, "pc.getStats must detect InvalidAccessError synchronously always");
    140 
    141 // Helper builds on above tests to check if operations queue is empty or not.
    142 //
    143 // Meaning of "empty": Because this helper uses the sloppy promiseStateFinal,
    144 // it may not detect operations on the chain unless they block the current run
    145 // of the event loop. In other words, it may not detect operations on the chain
    146 // that resolve on the emptying of the microtask queue at the end of this run of
    147 // the event loop.
    148 
    149 async function isOperationsChainEmpty(pc) {
    150  let p, error;
    151  const signalingState = pc.signalingState;
    152  if (signalingState == "have-remote-offer") {
    153    p = pc.createOffer();
    154  } else {
    155    p = pc.createAnswer();
    156  }
    157  const state = await promiseStateFinal(p);
    158  try {
    159    await p;
    160    // This helper tries to avoid side-effects by always failing,
    161    // but createAnswer above may succeed if chained after an SRD
    162    // that changes the signaling state on us. Ignore that success.
    163    if (signalingState == pc.signalingState) {
    164      assert_unreached("Control. Must not succeed");
    165    }
    166  } catch (e) {
    167    assert_equals(e.name, "InvalidStateError",
    168                  "isOperationsChainEmpty is working");
    169  }
    170  return state == "rejected";
    171 }
    172 
    173 promise_test(async t => {
    174  const pc = new RTCPeerConnection();
    175  t.add_cleanup(() => pc.close());
    176  assert_true(await isOperationsChainEmpty(pc), "Empty to start");
    177 }, "isOperationsChainEmpty detects empty in stable");
    178 
    179 promise_test(async t => {
    180  const pc = new RTCPeerConnection();
    181  t.add_cleanup(() => pc.close());
    182  await pc.setLocalDescription(await pc.createOffer());
    183  assert_true(await isOperationsChainEmpty(pc), "Empty to start");
    184 }, "isOperationsChainEmpty detects empty in have-local-offer");
    185 
    186 promise_test(async t => {
    187  const pc = new RTCPeerConnection();
    188  t.add_cleanup(() => pc.close());
    189  await pc.setRemoteDescription(await pc.createOffer());
    190  assert_true(await isOperationsChainEmpty(pc), "Empty to start");
    191 }, "isOperationsChainEmpty detects empty in have-remote-offer");
    192 
    193 promise_test(async t => {
    194  const pc = new RTCPeerConnection();
    195  t.add_cleanup(() => pc.close());
    196  const p = pc.createOffer();
    197  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
    198  await p;
    199 }, "createOffer uses operations chain");
    200 
    201 promise_test(async t => {
    202  const pc = new RTCPeerConnection();
    203  t.add_cleanup(() => pc.close());
    204  await pc.setRemoteDescription(await pc.createOffer());
    205  const p = pc.createAnswer();
    206  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
    207  await p;
    208 }, "createAnswer uses operations chain");
    209 
    210 promise_test(async t => {
    211  const pc = new RTCPeerConnection();
    212  t.add_cleanup(() => pc.close());
    213  const offer = await pc.createOffer();
    214  assert_true(await isOperationsChainEmpty(pc), "Empty before");
    215  const p = pc.setLocalDescription(offer);
    216  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
    217  await p;
    218 }, "setLocalDescription uses operations chain");
    219 
    220 promise_test(async t => {
    221  const pc = new RTCPeerConnection();
    222  t.add_cleanup(() => pc.close());
    223  const offer = await pc.createOffer();
    224  assert_true(await isOperationsChainEmpty(pc), "Empty before");
    225  const p = pc.setRemoteDescription(offer);
    226  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
    227  await p;
    228 }, "setRemoteDescription uses operations chain");
    229 
    230 promise_test(async t => {
    231  const pc1 = new RTCPeerConnection();
    232  t.add_cleanup(() => pc1.close());
    233  const pc2 = new RTCPeerConnection();
    234  t.add_cleanup(() => pc2.close());
    235 
    236  pc1.addTransceiver("video");
    237  const offer = await pc1.createOffer();
    238  await pc1.setLocalDescription(offer);
    239  const {candidate} = await new Promise(r => pc1.onicecandidate = r);
    240  await pc2.setRemoteDescription(offer);
    241  const p = pc2.addIceCandidate(candidate);
    242  assert_false(await isOperationsChainEmpty(pc2), "Non-empty chain");
    243  await p;
    244 }, "addIceCandidate uses operations chain");
    245 
    246 promise_test(async t => {
    247  const pc = new RTCPeerConnection();
    248  t.add_cleanup(() => pc.close());
    249  const transceiver = pc.addTransceiver("audio");
    250  await new Promise(r => pc.onnegotiationneeded = r);
    251  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    252  await new Promise(r => t.step_timeout(r, 0));
    253  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    254 }, "Firing of negotiationneeded does NOT use operations chain");
    255 
    256 promise_test(async t => {
    257  const pc1 = new RTCPeerConnection();
    258  t.add_cleanup(() => pc1.close());
    259  const pc2 = new RTCPeerConnection();
    260  t.add_cleanup(() => pc2.close());
    261 
    262  pc1.addTransceiver("audio");
    263  pc1.addTransceiver("video");
    264  const offer = await pc1.createOffer();
    265  await pc1.setLocalDescription(offer);
    266  const candidates = [];
    267  for (let c; (c = (await new Promise(r => pc1.onicecandidate = r)).candidate);) {
    268    candidates.push(c);
    269  }
    270  pc2.addTransceiver("video");
    271  let fired = false;
    272  const p = new Promise(r => pc2.onnegotiationneeded = () => r(fired = true));
    273  await Promise.all([
    274    pc2.setRemoteDescription(offer),
    275    ...candidates.map(candidate => pc2.addIceCandidate(candidate)),
    276    pc2.setLocalDescription()
    277  ]);
    278  assert_false(fired, "Negotiationneeded mustn't have fired yet.");
    279  await new Promise(r => t.step_timeout(r, 0));
    280  assert_true(fired, "Negotiationneeded must have fired by now.");
    281  await p;
    282 }, "Negotiationneeded only fires once operations chain is empty");
    283 
    284 promise_test(async t => {
    285  const pc = new RTCPeerConnection();
    286  t.add_cleanup(() => pc.close());
    287  const transceiver = pc.addTransceiver("audio");
    288  await new Promise(r => pc.onnegotiationneeded = r);
    289  // Note: since the negotiationneeded event is fired from a chained synchronous
    290  // function in the spec, queue a task before doing our precheck.
    291  await new Promise(r => t.step_timeout(r, 0));
    292  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    293  const p = transceiver.sender.replaceTrack(null);
    294  assert_false(await isOperationsChainEmpty(pc), "Non-empty chain");
    295  await p;
    296 }, "replaceTrack uses operations chain");
    297 
    298 promise_test(async t => {
    299  const pc = new RTCPeerConnection();
    300  t.add_cleanup(() => pc.close());
    301  const transceiver = pc.addTransceiver("audio");
    302  await new Promise(r => pc.onnegotiationneeded = r);
    303  await new Promise(r => t.step_timeout(r, 0));
    304  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    305  const parameters = transceiver.sender.getParameters();
    306  const p = transceiver.sender.setParameters(parameters);
    307  const haveState = promiseStateFinal(p);
    308  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    309  assert_equals(await haveState, "pending", "Method is async");
    310  await p;
    311 }, "setParameters does NOT use the operations chain");
    312 
    313 promise_test(async t => {
    314  const pc = new RTCPeerConnection();
    315  t.add_cleanup(() => pc.close());
    316  const p = pc.getStats();
    317  const haveState = promiseStateFinal(p);
    318  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    319  assert_equals(await haveState, "pending", "Method is async");
    320  await p;
    321 }, "pc.getStats does NOT use the operations chain");
    322 
    323 promise_test(async t => {
    324  const pc = new RTCPeerConnection();
    325  t.add_cleanup(() => pc.close());
    326  const {sender} = pc.addTransceiver("audio");
    327  await new Promise(r => pc.onnegotiationneeded = r);
    328  await new Promise(r => t.step_timeout(r, 0));
    329  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    330  const p = sender.getStats();
    331  const haveState = promiseStateFinal(p);
    332  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    333  assert_equals(await haveState, "pending", "Method is async");
    334  await p;
    335 }, "sender.getStats does NOT use the operations chain");
    336 
    337 promise_test(async t => {
    338  const pc = new RTCPeerConnection();
    339  t.add_cleanup(() => pc.close());
    340  const {receiver} = pc.addTransceiver("audio");
    341  await new Promise(r => pc.onnegotiationneeded = r);
    342  await new Promise(r => t.step_timeout(r, 0));
    343  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    344  const p = receiver.getStats();
    345  const haveState = promiseStateFinal(p);
    346  assert_true(await isOperationsChainEmpty(pc), "Empty chain");
    347  assert_equals(await haveState, "pending", "Method is async");
    348  await p;
    349 }, "receiver.getStats does NOT use the operations chain");
    350 
    351 promise_test(async t => {
    352  const pc1 = new RTCPeerConnection();
    353  t.add_cleanup(() => pc1.close());
    354  const pc2 = new RTCPeerConnection();
    355  t.add_cleanup(() => pc2.close());
    356 
    357  pc1.addTransceiver("video");
    358  const offer = await pc1.createOffer();
    359  await pc1.setLocalDescription(offer);
    360  const {candidate} = await new Promise(r => pc1.onicecandidate = r);
    361  try {
    362    await pc2.addIceCandidate(candidate);
    363    assert_unreached("Control. Must not succeed");
    364  } catch (e) {
    365    assert_equals(e.name, "InvalidStateError");
    366  }
    367  const p = pc2.setRemoteDescription(offer);
    368  await pc2.addIceCandidate(candidate);
    369  await p;
    370 }, "addIceCandidate chains onto SRD, fails before");
    371 
    372 promise_test(async t => {
    373  const pc = new RTCPeerConnection();
    374  t.add_cleanup(() => pc.close());
    375 
    376  const offer = await pc.createOffer();
    377  pc.addTransceiver("video");
    378  await new Promise(r => pc.onnegotiationneeded = r);
    379  const p = (async () => {
    380    await pc.setLocalDescription();
    381  })();
    382  await new Promise(r => t.step_timeout(r, 0));
    383  await pc.setRemoteDescription(offer);
    384  await p;
    385 }, "Operations queue not vulnerable to recursion by chained negotiationneeded");
    386 
    387 promise_test(async t => {
    388  const pc1 = new RTCPeerConnection();
    389  t.add_cleanup(() => pc1.close());
    390  const pc2 = new RTCPeerConnection();
    391  t.add_cleanup(() => pc2.close());
    392 
    393  pc1.addTransceiver("video");
    394  await Promise.all([
    395    pc1.createOffer(),
    396    pc1.setLocalDescription({type: "offer"})
    397  ]);
    398  await Promise.all([
    399    pc2.setRemoteDescription(pc1.localDescription),
    400    pc2.createAnswer(),
    401    pc2.setLocalDescription({type: "answer"})
    402  ]);
    403  await pc1.setRemoteDescription(pc2.localDescription);
    404 }, "Pack operations queue with implicit offer and answer");
    405 
    406 promise_test(async t => {
    407  const pc1 = new RTCPeerConnection();
    408  t.add_cleanup(() => pc1.close());
    409  const pc2 = new RTCPeerConnection();
    410  t.add_cleanup(() => pc2.close());
    411 
    412  const state = (pc, s) => new Promise(r => pc.onsignalingstatechange =
    413                                       () => pc.signalingState == s && r());
    414  pc1.addTransceiver("video");
    415  pc1.createOffer();
    416  pc1.setLocalDescription({type: "offer"});
    417  await state(pc1, "have-local-offer");
    418  pc2.setRemoteDescription(pc1.localDescription);
    419  pc2.createAnswer();
    420  pc2.setLocalDescription({type: "answer"});
    421  await state(pc2, "stable");
    422  await pc1.setRemoteDescription(pc2.localDescription);
    423 }, "Negotiate solely by operations queue and signaling state");
    424 
    425 </script>