Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/selkies/webrtc/rtcicetransport.py: 33%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

187 statements  

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 https://mozilla.org/MPL/2.0/. 

4# 

5# This file incorporates work covered by the following copyright and 

6# permission notice: 

7# 

8# Copyright (c) Jeremy Lainé. 

9# All rights reserved. 

10# 

11# Redistribution and use in source and binary forms, with or without 

12# modification, are permitted provided that the following conditions are met: 

13# 

14# * Redistributions of source code must retain the above copyright notice, 

15# this list of conditions and the following disclaimer. 

16# * Redistributions in binary form must reproduce the above copyright notice, 

17# this list of conditions and the following disclaimer in the documentation 

18# and/or other materials provided with the distribution. 

19# * Neither the name of aiortc nor the names of its contributors may 

20# be used to endorse or promote products derived from this software without 

21# specific prior written permission. 

22# 

23# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 

24# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 

25# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 

26# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 

27# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 

28# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 

29# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 

30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 

31# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 

32# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 

33 

34import asyncio 

35import logging 

36import re 

37from dataclasses import dataclass 

38from typing import Any, Optional 

39 

40from ..ice import Candidate, Connection, ConnectionClosed 

41from pyee.asyncio import AsyncIOEventEmitter 

42 

43from .exceptions import InvalidStateError 

44from .rtcconfiguration import RTCIceServer 

45 

46# See https://datatracker.ietf.org/doc/html/rfc7064 

47# transport is not defined by RFC 7064 and rejected by browsers. 

48STUN_REGEX = re.compile( 

49 r"(?P<scheme>stun|stuns)\:(?P<host>[^?:]+)(\:(?P<port>[0-9]+?))?" 

50 r"(\?transport=(?P<transport>.*))?" 

51) 

52# See https://datatracker.ietf.org/doc/html/rfc7065 

53TURN_REGEX = re.compile( 

54 r"(?P<scheme>turn|turns)\:(?P<host>[^?:]+)(\:(?P<port>[0-9]+?))?" 

55 r"(\?transport=(?P<transport>.*))?" 

56) 

57 

58logger = logging.getLogger(__name__) 

59 

60 

61@dataclass 

62class RTCIceCandidate: 

63 """ 

64 The :class:`RTCIceCandidate` interface represents a candidate Interactive 

65 Connectivity Establishment (ICE) configuration which may be used to 

66 establish an RTCPeerConnection. 

67 """ 

68 

69 component: int 

70 foundation: str 

71 ip: str 

72 port: int 

73 priority: int 

74 protocol: str 

75 type: str 

76 relatedAddress: Optional[str] = None 

77 relatedPort: Optional[int] = None 

78 sdpMid: Optional[str] = None 

79 sdpMLineIndex: Optional[int] = None 

80 tcpType: Optional[str] = None 

81 

82 

83@dataclass 

84class RTCIceParameters: 

85 """ 

86 The :class:`RTCIceParameters` dictionary includes the ICE username 

87 fragment and password and other ICE-related parameters. 

88 """ 

89 

90 usernameFragment: Optional[str] = None 

91 "ICE username fragment." 

92 

93 password: Optional[str] = None 

94 "ICE password." 

95 

96 iceLite: bool = False 

97 

98 

99def candidate_from_aioice(x: Candidate) -> RTCIceCandidate: 

100 return RTCIceCandidate( 

101 component=x.component, 

102 foundation=x.foundation, 

103 ip=x.host, 

104 port=x.port, 

105 priority=x.priority, 

106 protocol=x.transport, 

107 relatedAddress=x.related_address, 

108 relatedPort=x.related_port, 

109 tcpType=x.tcptype, 

110 type=x.type, 

111 ) 

112 

113 

114def candidate_to_aioice(x: RTCIceCandidate) -> Candidate: 

115 return Candidate( 

116 component=x.component, 

117 foundation=x.foundation, 

118 host=x.ip, 

119 port=x.port, 

120 priority=x.priority, 

121 related_address=x.relatedAddress, 

122 related_port=x.relatedPort, 

123 transport=x.protocol, 

124 tcptype=x.tcpType, 

125 type=x.type, 

126 ) 

127 

128 

129def connection_kwargs(servers: list[RTCIceServer]) -> dict[str, Any]: 

130 kwargs: dict[str, Any] = {} 

131 

132 for server in servers: 

133 if isinstance(server.urls, list): 

134 uris = server.urls 

135 else: 

136 uris = [server.urls] 

137 

138 for uri in uris: 

139 parsed = parse_stun_turn_uri(uri) 

140 

141 if parsed["scheme"] == "stun": 

142 # only a single STUN server is supported 

143 if "stun_server" in kwargs: 

144 continue 

145 

146 kwargs["stun_server"] = (parsed["host"], parsed["port"]) 

147 elif parsed["scheme"] in ["turn", "turns"]: 

148 # only a single TURN server is supported 

149 if "turn_server" in kwargs: 

150 continue 

151 

152 # only 'udp' and 'tcp' transports are supported 

153 if parsed["scheme"] == "turn" and parsed["transport"] not in [ 

154 "udp", 

155 "tcp", 

156 ]: 

157 continue 

158 elif parsed["scheme"] == "turns" and parsed["transport"] != "tcp": 

159 continue 

160 

161 # only 'password' credentialType is supported 

162 if server.credentialType != "password": 

163 continue 

164 

165 kwargs["turn_server"] = (parsed["host"], parsed["port"]) 

166 kwargs["turn_ssl"] = parsed["scheme"] == "turns" 

167 kwargs["turn_transport"] = parsed["transport"] 

168 kwargs["turn_username"] = server.username 

169 kwargs["turn_password"] = server.credential 

170 

171 return kwargs 

172 

173 

174def parse_stun_turn_uri(uri: str) -> dict[str, Any]: 

175 if uri.startswith("stun"): 

176 match = STUN_REGEX.fullmatch(uri) 

177 elif uri.startswith("turn"): 

178 match = TURN_REGEX.fullmatch(uri) 

179 else: 

180 raise ValueError("malformed uri: invalid scheme") 

181 

182 if not match: 

183 raise ValueError("malformed uri") 

184 

185 # set port 

186 parsed: dict[str, Any] = match.groupdict() 

187 if parsed["port"]: 

188 parsed["port"] = int(parsed["port"]) 

189 elif parsed["scheme"] in ["stuns", "turns"]: 

190 parsed["port"] = 5349 

191 else: 

192 parsed["port"] = 3478 

193 

194 # set transport 

195 if parsed["scheme"] == "turn" and not parsed["transport"]: 

196 parsed["transport"] = "udp" 

197 elif parsed["scheme"] == "turns" and not parsed["transport"]: 

198 parsed["transport"] = "tcp" 

199 elif parsed["scheme"] in ["stun", "stuns"]: 

200 if parsed["transport"] is not None: 

201 raise ValueError( 

202 "malformed uri: " + parsed["scheme"] + " must not contain transport" 

203 ) 

204 del parsed["transport"] 

205 

206 return parsed 

207 

208 

209class RTCIceGatherer(AsyncIOEventEmitter): 

210 """ 

211 The :class:`RTCIceGatherer` interface gathers local host, server reflexive, 

212 and relay candidates, as well as enabling the retrieval of local 

213 Interactive Connectivity Establishment (ICE) parameters which can be 

214 exchanged in signaling. 

215 """ 

216 

217 def __init__( 

218 self, 

219 iceServers: Optional[list[RTCIceServer]] = None, 

220 local_username: Optional[str] = None, 

221 local_password: Optional[str] = None, 

222 ice_host_public_ips: Optional[list[str]] = None, 

223 ice_port_range: Optional[tuple[int, int]] = None, 

224 ice_udp_mux: Optional[Any] = None, 

225 ice_tcp_mux: Optional[Any] = None, 

226 ice_lite: bool = False, 

227 ) -> None: 

228 super().__init__() 

229 

230 if iceServers is None: 

231 iceServers = self.getDefaultIceServers() 

232 # An ICE-lite agent gathers host candidates only, so the servers stay 

233 # with the peer that still uses them. 

234 ice_kwargs = {} if ice_lite else connection_kwargs(iceServers) 

235 

236 self._connection = Connection( 

237 ice_controlling=False, 

238 local_username=local_username, 

239 local_password=local_password, 

240 nat1to1_ips=ice_host_public_ips, 

241 port_range=ice_port_range, 

242 udp_mux=ice_udp_mux, 

243 tcp_mux=ice_tcp_mux, 

244 ice_lite=ice_lite, 

245 **ice_kwargs, 

246 ) 

247 self._remote_candidates_end = False 

248 self.__state = "new" 

249 

250 @property 

251 def state(self) -> str: 

252 """ 

253 The current state of the ICE gatherer. 

254 """ 

255 return self.__state 

256 

257 async def gather(self) -> None: 

258 """ 

259 Gather ICE candidates. 

260 """ 

261 if self.__state == "new": 

262 self.__setState("gathering") 

263 await self._connection.gather_candidates() 

264 self.__setState("completed") 

265 

266 @classmethod 

267 def getDefaultIceServers(cls) -> list[RTCIceServer]: 

268 """ 

269 Return the list of default :class:`RTCIceServer`. 

270 """ 

271 return [RTCIceServer("stun:stun.l.google.com:19302")] 

272 

273 def getLocalCandidates(self) -> list[RTCIceCandidate]: 

274 """ 

275 Retrieve the list of valid local candidates associated with the ICE 

276 gatherer. 

277 """ 

278 return [candidate_from_aioice(x) for x in self._connection.local_candidates] 

279 

280 def getLocalParameters(self) -> RTCIceParameters: 

281 """ 

282 Retrieve the ICE parameters of the ICE gatherer. 

283 

284 :rtype: RTCIceParameters 

285 """ 

286 return RTCIceParameters( 

287 usernameFragment=self._connection.local_username, 

288 password=self._connection.local_password, 

289 iceLite=self._connection.ice_lite, 

290 ) 

291 

292 def __setState(self, state: str) -> None: 

293 self.__state = state 

294 self.emit("statechange") 

295 

296 

297class RTCIceTransport(AsyncIOEventEmitter): 

298 """ 

299 The :class:`RTCIceTransport` interface allows an application access to 

300 information about the Interactive Connectivity Establishment (ICE) 

301 transport over which packets are sent and received. 

302 

303 :param gatherer: An :class:`RTCIceGatherer`. 

304 """ 

305 

306 def __init__(self, gatherer: RTCIceGatherer) -> None: 

307 super().__init__() 

308 self.__iceGatherer = gatherer 

309 self.__monitor_task: Optional[asyncio.Future[None]] = None 

310 self.__start: Optional[asyncio.Event] = None 

311 self.__state = "new" 

312 self._connection = gatherer._connection 

313 self._role_set = False 

314 

315 # expose recv / send methods 

316 self._recv = self._connection.recv 

317 self._send = self._connection.send 

318 

319 @property 

320 def iceGatherer(self) -> RTCIceGatherer: 

321 """ 

322 The ICE gatherer passed in the constructor. 

323 """ 

324 return self.__iceGatherer 

325 

326 @property 

327 def role(self) -> str: 

328 """ 

329 The current role of the ICE transport. 

330 

331 Either `'controlling'` or `'controlled'`. 

332 """ 

333 if self._connection.ice_controlling: 

334 return "controlling" 

335 else: 

336 return "controlled" 

337 

338 @property 

339 def state(self) -> str: 

340 """ 

341 The current state of the ICE transport. 

342 """ 

343 return self.__state 

344 

345 async def addRemoteCandidate(self, candidate: Optional[RTCIceCandidate]) -> None: 

346 """ 

347 Add a remote candidate. 

348 

349 :param candidate: The new candidate or `None` to signal end of candidates. 

350 """ 

351 if not self.__iceGatherer._remote_candidates_end: 

352 if candidate is None: 

353 self.__iceGatherer._remote_candidates_end = True 

354 await self._connection.add_remote_candidate(None) 

355 else: 

356 await self._connection.add_remote_candidate( 

357 candidate_to_aioice(candidate) 

358 ) 

359 

360 def getRemoteCandidates(self) -> list[RTCIceCandidate]: 

361 """ 

362 Retrieve the list of candidates associated with the remote 

363 :class:`RTCIceTransport`. 

364 """ 

365 return [candidate_from_aioice(x) for x in self._connection.remote_candidates] 

366 

367 async def start(self, remoteParameters: RTCIceParameters) -> None: 

368 """ 

369 Initiate connectivity checks. 

370 

371 :param remoteParameters: The :class:`RTCIceParameters` associated with 

372 the remote :class:`RTCIceTransport`. 

373 """ 

374 if self.state == "closed": 

375 raise InvalidStateError("RTCIceTransport is closed") 

376 

377 # handle the case where start is already in progress 

378 if self.__start is not None: 

379 await self.__start.wait() 

380 return 

381 self.__start = asyncio.Event() 

382 self.__monitor_task = asyncio.ensure_future(self._monitor()) 

383 

384 self.__setState("checking") 

385 self._connection.remote_is_lite = remoteParameters.iceLite 

386 self._connection.remote_username = remoteParameters.usernameFragment 

387 self._connection.remote_password = remoteParameters.password 

388 try: 

389 await self._connection.connect() 

390 except ConnectionError: 

391 self.__setState("failed") 

392 else: 

393 self.__setState("completed") 

394 self.__start.set() 

395 

396 async def stop(self) -> None: 

397 """ 

398 Irreversibly stop the :class:`RTCIceTransport`. 

399 """ 

400 if self.state != "closed": 

401 self.__setState("closed") 

402 await self._connection.close() 

403 if self.__monitor_task is not None: 

404 await self.__monitor_task 

405 self.__monitor_task = None 

406 

407 async def _monitor(self) -> None: 

408 while True: 

409 event = await self._connection.get_event() 

410 if isinstance(event, ConnectionClosed): 

411 if self.state == "completed": 

412 self.__setState("failed") 

413 return 

414 

415 def __log_debug(self, msg: str, *args: object) -> None: 

416 logger.debug(f"RTCIceTransport(%s) {msg}", self.role, *args) 

417 

418 def __setState(self, state: str) -> None: 

419 if state != self.__state: 

420 self.__log_debug("- %s -> %s", self.__state, state) 

421 self.__state = state 

422 self.emit("statechange") 

423 

424 # no more events will be emitted, so remove all event listeners 

425 # to facilitate garbage collection. 

426 if state == "closed": 

427 self.iceGatherer.remove_all_listeners() 

428 self.remove_all_listeners()