tor-browser

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

iceTestUtils.js (14155B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 "use strict";
      6 
      7 // This is mostly so test_peerConnection_gatherWithStun300.html and
      8 // test_peerConnection_gatherWithStun300IPv6 can share this code. I would have
      9 // put the ipv6 test code in the same file, but our ipv6 tester support is
     10 // inconsistent enough that we need to be able to track the ipv6 test
     11 // separately.
     12 
     13 async function findStatsRelayCandidates(pc, protocol) {
     14  const stats = await pc.getStats();
     15  return [...stats.values()].filter(
     16    v =>
     17      v.type == "local-candidate" &&
     18      v.candidateType == "relay" &&
     19      v.relayProtocol == protocol
     20  );
     21 }
     22 
     23 // Trickles candidates if pcDst is set, and resolves the candidate list
     24 async function trickleIce(pc, pcDst) {
     25  const candidates = [],
     26    addCandidatePromises = [];
     27  while (true) {
     28    const { candidate } = await new Promise(r =>
     29      pc.addEventListener("icecandidate", r, { once: true })
     30    );
     31    if (!candidate) {
     32      break;
     33    }
     34    candidates.push(candidate);
     35    if (pcDst) {
     36      addCandidatePromises.push(pcDst.addIceCandidate(candidate));
     37    }
     38  }
     39  await Promise.all(addCandidatePromises);
     40  return candidates;
     41 }
     42 
     43 async function gather(pc) {
     44  if (pc.signalingState == "stable") {
     45    await pc.setLocalDescription(
     46      await pc.createOffer({ offerToReceiveAudio: true })
     47    );
     48  } else if (pc.signalingState == "have-remote-offer") {
     49    await pc.setLocalDescription();
     50  }
     51 
     52  return trickleIce(pc);
     53 }
     54 
     55 async function gatherWithTimeout(pc, timeout, context) {
     56  const throwOnTimeout = async () => {
     57    await wait(timeout);
     58    throw new Error(
     59      `Gathering did not complete within ${timeout} ms with ${context}`
     60    );
     61  };
     62 
     63  return Promise.race([gather(pc), throwOnTimeout()]);
     64 }
     65 
     66 async function iceConnected(pc) {
     67  return new Promise((resolve, reject) => {
     68    pc.addEventListener("iceconnectionstatechange", () => {
     69      if (["connected", "completed"].includes(pc.iceConnectionState)) {
     70        resolve();
     71      } else if (pc.iceConnectionState == "failed") {
     72        reject(new Error(`ICE failed`));
     73      }
     74    });
     75  });
     76 }
     77 
     78 async function dtlsConnected(pc) {
     79  return new Promise((resolve, reject) => {
     80    pc.addEventListener("connectionstatechange", () => {
     81      if (["connected", "completed"].includes(pc.connectionState)) {
     82        resolve();
     83      } else if (pc.connectionState == "failed") {
     84        reject(new Error(`Connection failed`));
     85      }
     86    });
     87  });
     88 }
     89 
     90 // Set up trickle, but does not wait for it to complete. Can be used by itself
     91 // in cases where we do not expect any new candidates, but want to still set up
     92 // the signal handling in case new candidates _do_ show up.
     93 async function connectNoTrickleWait(offerer, answerer, timeout, context) {
     94  return connect(offerer, answerer, timeout, context, true);
     95 }
     96 
     97 async function connect(
     98  offerer,
     99  answerer,
    100  timeout,
    101  context,
    102  noTrickleWait = false,
    103  waitForDtls = false
    104 ) {
    105  const trickle1 = trickleIce(offerer, answerer);
    106  const trickle2 = trickleIce(answerer, offerer);
    107  try {
    108    const offer = await offerer.createOffer({ offerToReceiveAudio: true });
    109    await offerer.setLocalDescription(offer);
    110    await answerer.setRemoteDescription(offer);
    111    const answer = await answerer.createAnswer();
    112    await Promise.all([
    113      offerer.setRemoteDescription(answer),
    114      answerer.setLocalDescription(answer),
    115    ]);
    116 
    117    const throwOnTimeout = async () => {
    118      if (timeout) {
    119        await wait(timeout);
    120        throw new Error(
    121          `ICE did not complete within ${timeout} ms with ${context}`
    122        );
    123      }
    124    };
    125 
    126    const connectionPromises = waitForDtls
    127      ? [dtlsConnected(offerer), dtlsConnected(answerer)]
    128      : [iceConnected(offerer), iceConnected(answerer)];
    129 
    130    await Promise.race([
    131      Promise.all(connectionPromises),
    132      throwOnTimeout(timeout, context),
    133    ]);
    134  } finally {
    135    if (!noTrickleWait) {
    136      // TODO(bug 1751509): For now, we need to let gathering finish before we
    137      // proceed, because there are races in ICE restart wrt gathering state.
    138      await Promise.all([trickle1, trickle2]);
    139    }
    140  }
    141 }
    142 
    143 function isV6HostCandidate(candidate) {
    144  const fields = candidate.candidate.split(" ");
    145  const type = fields[7];
    146  const ipAddress = fields[4];
    147  return type == "host" && ipAddress.includes(":");
    148 }
    149 
    150 async function ipv6Supported() {
    151  const pc = new RTCPeerConnection();
    152  const candidates = await gatherWithTimeout(pc, 8000);
    153  info(`baseline candidates: ${JSON.stringify(candidates)}`);
    154  pc.close();
    155  return candidates.some(isV6HostCandidate);
    156 }
    157 
    158 function makeContextString(iceServers) {
    159  const currentRedirectAddress = SpecialPowers.getCharPref(
    160    "media.peerconnection.nat_simulator.redirect_address",
    161    ""
    162  );
    163  const currentRedirectTargets = SpecialPowers.getCharPref(
    164    "media.peerconnection.nat_simulator.redirect_targets",
    165    ""
    166  );
    167  return `redirect rule: ${currentRedirectAddress}=>${currentRedirectTargets} iceServers: ${JSON.stringify(
    168    iceServers
    169  )}`;
    170 }
    171 
    172 async function checkSrflx(iceServers) {
    173  const context = makeContextString(iceServers);
    174  info(`checkSrflx ${context}`);
    175  const pc = new RTCPeerConnection({
    176    iceServers,
    177    bundlePolicy: "max-bundle", // Avoids extra candidates
    178  });
    179  const candidates = await gatherWithTimeout(pc, 8000, context);
    180  const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
    181  info(`candidates: ${JSON.stringify(srflxCandidates)}`);
    182  // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
    183  // result in a single srflx candidate
    184  is(
    185    srflxCandidates.length,
    186    2,
    187    `Should have two srflx candidates with ${context}`
    188  );
    189  pc.close();
    190 }
    191 
    192 async function checkNoSrflx(iceServers) {
    193  const context = makeContextString(iceServers);
    194  info(`checkNoSrflx ${context}`);
    195  const pc = new RTCPeerConnection({
    196    iceServers,
    197    bundlePolicy: "max-bundle", // Avoids extra candidates
    198  });
    199  const candidates = await gatherWithTimeout(pc, 8000, context);
    200  const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
    201  info(`candidates: ${JSON.stringify(srflxCandidates)}`);
    202  is(
    203    srflxCandidates.length,
    204    0,
    205    `Should have no srflx candidates with ${context}`
    206  );
    207  pc.close();
    208 }
    209 
    210 async function checkRelayUdp(iceServers) {
    211  const context = makeContextString(iceServers);
    212  info(`checkRelayUdp ${context}`);
    213  const pc = new RTCPeerConnection({
    214    iceServers,
    215    bundlePolicy: "max-bundle", // Avoids extra candidates
    216  });
    217  const candidates = await gatherWithTimeout(pc, 8000, context);
    218  const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
    219  info(`candidates: ${JSON.stringify(relayCandidates)}`);
    220  // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
    221  // result in a single relay candidate
    222  is(
    223    relayCandidates.length,
    224    2,
    225    `Should have two relay candidates with ${context}`
    226  );
    227  // It would be nice if RTCIceCandidate had a field telling us what the
    228  // "related protocol" is (similar to relatedAddress and relatedPort).
    229  // Because there is no such thing, we need to go through the stats API,
    230  // which _does_ have that information.
    231  is(
    232    (await findStatsRelayCandidates(pc, "tcp")).length,
    233    0,
    234    `No TCP relay candidates should be present with ${context}`
    235  );
    236  pc.close();
    237 }
    238 
    239 async function checkRelayTcp(iceServers) {
    240  const context = makeContextString(iceServers);
    241  info(`checkRelayTcp ${context}`);
    242  const pc = new RTCPeerConnection({
    243    iceServers,
    244    bundlePolicy: "max-bundle", // Avoids extra candidates
    245  });
    246  const candidates = await gatherWithTimeout(pc, 8000, context);
    247  const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
    248  info(`candidates: ${JSON.stringify(relayCandidates)}`);
    249  // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
    250  // result in a single relay candidate
    251  is(
    252    relayCandidates.length,
    253    2,
    254    `Should have two relay candidates with ${context}`
    255  );
    256  // It would be nice if RTCIceCandidate had a field telling us what the
    257  // "related protocol" is (similar to relatedAddress and relatedPort).
    258  // Because there is no such thing, we need to go through the stats API,
    259  // which _does_ have that information.
    260  is(
    261    (await findStatsRelayCandidates(pc, "udp")).length,
    262    0,
    263    `No UDP relay candidates should be present with ${context}`
    264  );
    265  pc.close();
    266 }
    267 
    268 async function checkRelayUdpTcp(iceServers) {
    269  const context = makeContextString(iceServers);
    270  info(`checkRelayUdpTcp ${context}`);
    271  const pc = new RTCPeerConnection({
    272    iceServers,
    273    bundlePolicy: "max-bundle", // Avoids extra candidates
    274  });
    275  const candidates = await gatherWithTimeout(pc, 8000, context);
    276  const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
    277  info(`candidates: ${JSON.stringify(relayCandidates)}`);
    278  // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
    279  // result in a single relay candidate each for UDP and TCP
    280  is(
    281    relayCandidates.length,
    282    4,
    283    `Should have two relay candidates for each protocol with ${context}`
    284  );
    285  // It would be nice if RTCIceCandidate had a field telling us what the
    286  // "related protocol" is (similar to relatedAddress and relatedPort).
    287  // Because there is no such thing, we need to go through the stats API,
    288  // which _does_ have that information.
    289  is(
    290    (await findStatsRelayCandidates(pc, "udp")).length,
    291    2,
    292    `Two UDP relay candidates should be present with ${context}`
    293  );
    294  // TODO(bug 1705563): This is 1 because of bug 1705563
    295  is(
    296    (await findStatsRelayCandidates(pc, "tcp")).length,
    297    1,
    298    `One TCP relay candidates should be present with ${context}`
    299  );
    300  pc.close();
    301 }
    302 
    303 async function checkNoRelay(iceServers) {
    304  const context = makeContextString(iceServers);
    305  info(`checkNoRelay ${context}`);
    306  const pc = new RTCPeerConnection({
    307    iceServers,
    308    bundlePolicy: "max-bundle", // Avoids extra candidates
    309  });
    310  const candidates = await gatherWithTimeout(pc, 8000, context);
    311  const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
    312  info(`candidates: ${JSON.stringify(relayCandidates)}`);
    313  is(
    314    relayCandidates.length,
    315    0,
    316    `Should have no relay candidates with ${context}`
    317  );
    318  pc.close();
    319 }
    320 
    321 function gatheringStateReached(object, state) {
    322  if (object instanceof RTCIceTransport) {
    323    return new Promise(r =>
    324      object.addEventListener("gatheringstatechange", function listener() {
    325        if (object.gatheringState == state) {
    326          object.removeEventListener("gatheringstatechange", listener);
    327          r(state);
    328        }
    329      })
    330    );
    331  } else if (object instanceof RTCPeerConnection) {
    332    return new Promise(r =>
    333      object.addEventListener("icegatheringstatechange", function listener() {
    334        if (object.iceGatheringState == state) {
    335          object.removeEventListener("icegatheringstatechange", listener);
    336          r(state);
    337        }
    338      })
    339    );
    340  } else {
    341    throw "First parameter is neither an RTCIceTransport nor an RTCPeerConnection";
    342  }
    343 }
    344 
    345 function nextGatheringState(object) {
    346  if (object instanceof RTCIceTransport) {
    347    return new Promise(resolve =>
    348      object.addEventListener(
    349        "gatheringstatechange",
    350        () => resolve(object.gatheringState),
    351        { once: true }
    352      )
    353    );
    354  } else if (object instanceof RTCPeerConnection) {
    355    return new Promise(resolve =>
    356      object.addEventListener(
    357        "icegatheringstatechange",
    358        () => resolve(object.iceGatheringState),
    359        { once: true }
    360      )
    361    );
    362  } else {
    363    throw "First parameter is neither an RTCIceTransport nor an RTCPeerConnection";
    364  }
    365 }
    366 
    367 function emptyCandidate(pc) {
    368  return new Promise(r =>
    369    pc.addEventListener("icecandidate", function listener(e) {
    370      if (e.candidate && e.candidate.candidate == "") {
    371        pc.removeEventListener("icecandidate", listener);
    372        r(e);
    373      }
    374    })
    375  );
    376 }
    377 
    378 function nullCandidate(pc) {
    379  return new Promise(r =>
    380    pc.addEventListener("icecandidate", function listener(e) {
    381      if (!e.candidate) {
    382        pc.removeEventListener("icecandidate", listener);
    383        r(e);
    384      }
    385    })
    386  );
    387 }
    388 
    389 function connectionStateReached(object, state) {
    390  if (object instanceof RTCIceTransport || object instanceof RTCDtlsTransport) {
    391    return new Promise(resolve =>
    392      object.addEventListener("statechange", function listener() {
    393        if (object.state == state) {
    394          object.removeEventListener("statechange", listener);
    395          resolve(state);
    396        }
    397      })
    398    );
    399  } else if (object instanceof RTCPeerConnection) {
    400    return new Promise(resolve =>
    401      object.addEventListener("connectionstatechange", function listener() {
    402        if (object.connectionState == state) {
    403          object.removeEventListener("connectionstatechange", listener);
    404          resolve(state);
    405        }
    406      })
    407    );
    408  } else {
    409    throw "First parameter is neither an RTCIceTransport, an RTCDtlsTransport, nor an RTCPeerConnection";
    410  }
    411 }
    412 
    413 function nextConnectionState(object) {
    414  if (object instanceof RTCIceTransport || object instanceof RTCDtlsTransport) {
    415    return new Promise(resolve =>
    416      object.addEventListener("statechange", () => resolve(object.state), {
    417        once: true,
    418      })
    419    );
    420  } else if (object instanceof RTCPeerConnection) {
    421    return new Promise(resolve =>
    422      object.addEventListener(
    423        "connectionstatechange",
    424        () => resolve(object.connectionState),
    425        { once: true }
    426      )
    427    );
    428  } else {
    429    throw "First parameter is neither an RTCIceTransport, an RTCDtlsTransport, nor an RTCPeerConnection";
    430  }
    431 }
    432 
    433 function nextIceConnectionState(pc) {
    434  if (pc instanceof RTCPeerConnection) {
    435    return new Promise(resolve =>
    436      pc.addEventListener(
    437        "iceconnectionstatechange",
    438        () => resolve(pc.iceConnectionState),
    439        { once: true }
    440      )
    441    );
    442  } else {
    443    throw "First parameter is not an RTCPeerConnection";
    444  }
    445 }