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) 2018-2019 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 aioice 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 hashlib
35import ipaddress
36from typing import Optional
37
38
39def candidate_foundation(
40 candidate_type: str, candidate_transport: str, base_address: str
41) -> str:
42 """
43 See RFC 5245 - 4.1.1.3. Computing Foundations
44 """
45 key = "%s|%s|%s" % (candidate_type, candidate_transport, base_address)
46 return hashlib.md5(key.encode("ascii"), usedforsecurity=False).hexdigest()
47
48
49def candidate_priority(
50 candidate_component: int, candidate_type: str, local_pref: int = 65535
51) -> int:
52 """
53 See RFC 5245 - 4.1.2.1. Recommended Formula
54 """
55 if candidate_type == "host":
56 type_pref = 126
57 elif candidate_type == "prflx":
58 type_pref = 110
59 elif candidate_type == "srflx":
60 type_pref = 100
61 else:
62 type_pref = 0
63
64 return (1 << 24) * type_pref + (1 << 8) * local_pref + (256 - candidate_component)
65
66
67class Candidate:
68 """
69 An ICE candidate.
70 """
71
72 def __init__(
73 self,
74 foundation: str,
75 component: int,
76 transport: str,
77 priority: int,
78 host: str,
79 port: int,
80 type: str,
81 related_address: Optional[str] = None,
82 related_port: Optional[int] = None,
83 tcptype: Optional[str] = None,
84 generation: Optional[int] = None,
85 ) -> None:
86 self.foundation = foundation
87 self.component = component
88 self.transport = transport
89 self.priority = priority
90 self.host = host
91 self.port = port
92 self.type = type
93 self.related_address = related_address
94 self.related_port = related_port
95 self.tcptype = tcptype
96 self.generation = generation
97
98 @classmethod
99 def from_sdp(cls, sdp: str) -> "Candidate":
100 """
101 Parse a :class:`Candidate` from SDP.
102
103 .. code-block:: python
104
105 Candidate.from_sdp(
106 '6815297761 1 udp 659136 1.2.3.4 31102 typ host generation 0')
107 """
108 bits = sdp.split()
109 if len(bits) < 8:
110 raise ValueError("SDP does not have enough properties")
111
112 related_address: Optional[str] = None
113 related_port: Optional[int] = None
114 tcptype: Optional[str] = None
115 generation: Optional[int] = None
116 for i in range(8, len(bits) - 1, 2):
117 if bits[i] == "raddr":
118 related_address = bits[i + 1]
119 elif bits[i] == "rport":
120 related_port = int(bits[i + 1])
121 elif bits[i] == "tcptype":
122 tcptype = bits[i + 1]
123 elif bits[i] == "generation":
124 generation = int(bits[i + 1])
125
126 return Candidate(
127 foundation=bits[0],
128 component=int(bits[1]),
129 transport=bits[2],
130 priority=int(bits[3]),
131 host=bits[4],
132 port=int(bits[5]),
133 type=bits[7],
134 related_address=related_address,
135 related_port=related_port,
136 tcptype=tcptype,
137 generation=generation,
138 )
139
140 def to_sdp(self) -> str:
141 """
142 Return a string representation suitable for SDP.
143 """
144 sdp = "%s %d %s %d %s %d typ %s" % (
145 self.foundation,
146 self.component,
147 self.transport,
148 self.priority,
149 self.host,
150 self.port,
151 self.type,
152 )
153 if self.related_address is not None:
154 sdp += " raddr %s" % self.related_address
155 if self.related_port is not None:
156 sdp += " rport %s" % self.related_port
157 if self.tcptype is not None:
158 sdp += " tcptype %s" % self.tcptype
159 if self.generation is not None:
160 sdp += " generation %d" % self.generation
161 return sdp
162
163 def can_pair_with(self, other: "Candidate") -> bool:
164 """
165 A local candidate is paired with a remote candidate if and only if
166 the two candidates have the same component ID and have the same IP
167 address version.
168 """
169 a = ipaddress.ip_address(self.host)
170 b = ipaddress.ip_address(other.host)
171 return (
172 self.component == other.component
173 and self.transport.lower() == other.transport.lower()
174 and a.version == b.version
175 )
176
177 def __repr__(self) -> str:
178 return "Candidate(%s)" % self.to_sdp()