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 }