Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/selkies/ice/mdns.py: 25%
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
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
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.
34import asyncio
35import re
36import socket
37import uuid
38from typing import Optional, Union, cast
40import dns.exception
41import dns.flags
42import dns.message
43import dns.name
44import dns.rdata
45import dns.rdataclass
46import dns.rdataset
47import dns.rdatatype
48import dns.zone
50MDNS_ADDRESS = "224.0.0.251"
51MDNS_PORT = 5353
52MDNS_HOSTNAME_RE = re.compile(r"^[a-zA-Z0-9-]{1,63}\.local$")
54MDNS_RDCLASS = dns.rdataclass.IN | 0x8000
57def create_mdns_hostname() -> str:
58 return str(uuid.uuid4()) + ".local"
61def is_mdns_hostname(name: str) -> bool:
62 return MDNS_HOSTNAME_RE.match(name) is not None
65class MDnsProtocol(asyncio.DatagramProtocol):
66 def __init__(self, tx_transport: asyncio.DatagramTransport) -> None:
67 self.__closed: asyncio.Future[bool] = asyncio.Future()
68 self.zone = dns.zone.Zone("", relativize=False, rdclass=MDNS_RDCLASS)
69 self.queries: dict[dns.name.Name, set[asyncio.Future[str]]] = {}
71 self.rx_transport: Optional[asyncio.DatagramTransport] = None
72 self.tx_transport = tx_transport
74 def connection_lost(self, exc: Exception) -> None: # type: ignore
75 # abort any outstanding queries
76 for name, futures in list(self.queries.items()):
77 for future in futures:
78 future.set_exception(asyncio.TimeoutError)
79 self.__closed.set_result(True)
81 def connection_made(self, transport: asyncio.BaseTransport) -> None:
82 self.rx_transport = cast(asyncio.DatagramTransport, transport)
84 def datagram_received(self, data: Union[bytes, str], addr: tuple) -> None:
85 # parse message
86 try:
87 message = dns.message.from_wire(cast(bytes, data))
88 except dns.exception.FormError:
89 return
91 if isinstance(message, dns.message.QueryMessage):
92 # answer question
93 for question in message.question:
94 rdtypes: list[int] = []
95 if question.rdtype in (
96 dns.rdatatype.ANY,
97 dns.rdatatype.A,
98 dns.rdatatype.AAAA,
99 ):
100 rdtypes += [dns.rdatatype.A, dns.rdatatype.AAAA]
102 response = dns.message.QueryMessage(id=0)
103 response.flags |= dns.flags.QR
104 response.flags |= dns.flags.AA
106 for rdtype in rdtypes:
107 try:
108 response.answer.append(
109 self.zone.find_rrset(name=question.name, rdtype=rdtype)
110 )
111 except KeyError:
112 continue
113 if response.answer:
114 self.tx_transport.sendto(
115 response.to_wire(), (MDNS_ADDRESS, MDNS_PORT)
116 )
118 # handle answer
119 for answer in message.answer:
120 for item in answer:
121 item = item.to_generic()
122 if (
123 isinstance(item, dns.rdata.GenericRdata)
124 and item.rdclass == MDNS_RDCLASS
125 and item.rdtype in (dns.rdatatype.A, dns.rdatatype.AAAA)
126 ):
127 if item.rdtype == dns.rdatatype.A:
128 result = socket.inet_ntop(socket.AF_INET, item.data)
129 else:
130 result = socket.inet_ntop(socket.AF_INET6, item.data)
131 for future in self.queries.pop(answer.name, []):
132 future.set_result(result)
134 # custom
136 async def close(self) -> None:
137 self.rx_transport.close()
138 self.tx_transport.close()
139 await self.__closed
141 async def publish(self, hostname: str, addr: str) -> None:
142 name = dns.name.from_text(hostname)
143 try:
144 data = socket.inet_pton(socket.AF_INET, addr)
145 rdtype = dns.rdatatype.A
146 except OSError:
147 data = socket.inet_pton(socket.AF_INET6, addr)
148 rdtype = dns.rdatatype.AAAA
150 rdata = dns.rdata.GenericRdata(rdclass=MDNS_RDCLASS, rdtype=rdtype, data=data)
151 self.zone.replace_rdataset(name, dns.rdataset.from_rdata(120, rdata))
153 async def resolve(
154 self, hostname: str, timeout: Optional[float] = 1.0
155 ) -> Optional[str]:
156 name = dns.name.from_text(hostname)
157 future: asyncio.Future[str] = asyncio.Future()
159 if name in self.queries:
160 # a query for this name is already pending
161 self.queries[name].add(future)
162 else:
163 # no query for this name is pending, send a request
164 self.queries[name] = set([future])
165 message = dns.message.make_query(name, rdtype=dns.rdatatype.A)
166 message.id = 0
167 message.flags = 0
168 self.tx_transport.sendto(message.to_wire(), (MDNS_ADDRESS, MDNS_PORT))
170 try:
171 return await asyncio.wait_for(future, timeout=timeout)
172 except asyncio.TimeoutError:
173 return None
174 finally:
175 if name in self.queries:
176 self.queries[name].discard(future)
177 if not self.queries[name]:
178 del self.queries[name]
181async def create_mdns_protocol() -> MDnsProtocol:
182 """
183 Using a single socket works fine on Linux, but on OS X we need to use
184 separate sockets for sending and receiving.
185 """
186 loop = asyncio.get_running_loop()
188 # sender
190 tx_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
191 tx_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
192 if hasattr(socket, "SO_REUSEPORT"):
193 tx_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
194 tx_sock.bind(("", MDNS_PORT))
196 tx_transport, _ = await loop.create_datagram_endpoint(
197 lambda: asyncio.DatagramProtocol(),
198 sock=tx_sock,
199 )
201 # receiver
203 rx_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
204 rx_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
205 if hasattr(socket, "SO_REUSEPORT"):
206 rx_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
207 rx_sock.setsockopt(
208 socket.IPPROTO_IP,
209 socket.IP_ADD_MEMBERSHIP,
210 socket.inet_aton(MDNS_ADDRESS) + b"\x00\x00\x00\x00",
211 )
212 rx_sock.bind((MDNS_ADDRESS, MDNS_PORT))
214 _, protocol = await loop.create_datagram_endpoint(
215 lambda: MDnsProtocol(tx_transport=tx_transport),
216 sock=rx_sock,
217 )
219 return protocol