tor-browser

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

RTCPeerConnection-perfect-negotiation-helper.js (6096B)


      1 'use strict'
      2 
      3 function peer(other, polite, fail = null) {
      4  const send = (tgt, msg) => tgt.postMessage(JSON.parse(JSON.stringify(msg)),
      5                                             "*");
      6  if (!fail) fail = e => send(window.parent, {error: `${e.name}: ${e.message}`});
      7  const pc = new RTCPeerConnection();
      8 
      9  if (!window.assert_equals) {
     10    window.assert_equals = (a, b, msg) => a === b ||
     11        fail(new Error(`${msg} expected ${b} but got ${a}`));
     12  }
     13 
     14  const commands = {
     15    async addTransceiver() {
     16      const transceiver = pc.addTransceiver("video");
     17      await new Promise(r => pc.addEventListener("negotiated", r, {once: true}));
     18      if (!transceiver.currentDirection) {
     19        // Might have just missed the negotiation train. Catch next one.
     20        await new Promise(r => pc.addEventListener("negotiated", r, {once: true}));
     21      }
     22      assert_equals(transceiver.currentDirection, "sendonly", "have direction");
     23      return pc.getTransceivers().length;
     24    },
     25    async simpleConnect() {
     26      const p = commands.addTransceiver();
     27      await new Promise(r => pc.oniceconnectionstatechange =
     28                        () => pc.iceConnectionState == "connected" && r());
     29      return await p;
     30    },
     31    async getNumTransceivers() {
     32      return pc.getTransceivers().length;
     33    },
     34  };
     35 
     36  try {
     37    pc.addEventListener("icecandidate", ({candidate}) => send(other,
     38                                                              {candidate}));
     39    let makingOffer = false, ignoreIceCandidateFailures = false;
     40    let srdAnswerPending = false;
     41    pc.addEventListener("negotiationneeded", async () => {
     42      try {
     43        assert_equals(pc.signalingState, "stable", "negotiationneeded always fires in stable state");
     44        assert_equals(makingOffer, false, "negotiationneeded not already in progress");
     45        makingOffer = true;
     46        await pc.setLocalDescription();
     47        assert_equals(pc.signalingState, "have-local-offer", "negotiationneeded not racing with onmessage");
     48        assert_equals(pc.localDescription.type, "offer", "negotiationneeded SLD worked");
     49        send(other, {description: pc.localDescription});
     50      } catch (e) {
     51        fail(e);
     52      } finally {
     53        makingOffer = false;
     54      }
     55    });
     56    window.onmessage = async ({data: {description, candidate, run}}) => {
     57      try {
     58        if (description) {
     59          // If we have a setRemoteDescription() answer operation pending, then
     60          // we will be "stable" by the time the next setRemoteDescription() is
     61          // executed, so we count this being stable when deciding whether to
     62          // ignore the offer.
     63          let isStable =
     64              pc.signalingState == "stable" ||
     65              (pc.signalingState == "have-local-offer" && srdAnswerPending);
     66          const ignoreOffer = description.type == "offer" && !polite &&
     67                         (makingOffer || !isStable);
     68          if (ignoreOffer) {
     69            ignoreIceCandidateFailures = true;
     70            return;
     71          }
     72          if (description.type == "answer")
     73            srdAnswerPending = true;
     74          await pc.setRemoteDescription(description);
     75          ignoreIceCandidateFailures = false;
     76          srdAnswerPending = false;
     77          if (description.type == "offer") {
     78            assert_equals(pc.signalingState, "have-remote-offer", "Remote offer");
     79            assert_equals(pc.remoteDescription.type, "offer", "SRD worked");
     80            await pc.setLocalDescription();
     81            assert_equals(pc.signalingState, "stable", "onmessage not racing with negotiationneeded");
     82            assert_equals(pc.localDescription.type, "answer", "onmessage SLD worked");
     83            send(other, {description: pc.localDescription});
     84          } else {
     85            assert_equals(pc.remoteDescription.type, "answer", "Answer was set");
     86            assert_equals(pc.signalingState, "stable", "answered");
     87            pc.dispatchEvent(new Event("negotiated"));
     88          }
     89        } else if (candidate) {
     90          try {
     91            await pc.addIceCandidate(candidate);
     92          } catch (e) {
     93            if (!ignoreIceCandidateFailures) throw e;
     94          }
     95        } else if (run) {
     96          send(window.parent, {[run.id]: await commands[run.cmd]() || 0});
     97        }
     98      } catch (e) {
     99        fail(e);
    100      }
    101    };
    102  } catch (e) {
    103    fail(e);
    104  }
    105  return pc;
    106 }
    107 
    108 async function setupPeerIframe(t, polite) {
    109  const iframe = document.createElement("iframe");
    110  t.add_cleanup(() => iframe.remove());
    111  iframe.srcdoc =
    112   `<html\><script\>(${peer.toString()})(window.parent, ${polite});</script\></html\>`;
    113  document.documentElement.appendChild(iframe);
    114 
    115  const failCatcher = t.step_func(({data}) =>
    116      ("error" in data) && assert_unreached(`Error in iframe: ${data.error}`));
    117  window.addEventListener("message", failCatcher);
    118  t.add_cleanup(() => window.removeEventListener("message", failCatcher));
    119  await new Promise(r => iframe.onload = r);
    120  return iframe;
    121 }
    122 
    123 function setupPeerTopLevel(t, other, polite) {
    124  const pc = peer(other, polite, t.step_func(e => { throw e; }));
    125  t.add_cleanup(() => { pc.close(); window.onmessage = null; });
    126 }
    127 
    128 let counter = 0;
    129 async function run(target, cmd) {
    130  const id = `result${counter++}`;
    131  target.postMessage({run: {cmd, id}}, "*");
    132  return new Promise(r => window.addEventListener("message",
    133                                                  function listen({data}) {
    134    if (!(id in data)) return;
    135    window.removeEventListener("message", listen);
    136    r(data[id]);
    137  }));
    138 }
    139 
    140 let iframe;
    141 async function setupAB(t, politeA, politeB) {
    142  iframe = await setupPeerIframe(t, politeB);
    143  return setupPeerTopLevel(t, iframe.contentWindow, politeA);
    144 }
    145 const runA = cmd => run(window, cmd);
    146 const runB = cmd => run(iframe.contentWindow, cmd);
    147 const runBoth = (cmdA, cmdB = cmdA) => Promise.all([runA(cmdA), runB(cmdB)]);
    148 
    149 async function promise_test_both_roles(f, name) {
    150  promise_test(async t => f(t, await setupAB(t, true, false)), name);
    151  promise_test(async t => f(t, await setupAB(t, false, true)),
    152               `${name} with roles reversed`);
    153 }