def publish(self): import zeroconf # zeroconf doesn't do this for us # .. pick one at random? Ideally, zeroconf would publish all valid # addresses as the A record.. but doesn't seem to do so addrs = zeroconf.get_all_addresses(socket.AF_INET) address = None if addrs: for addr in addrs: if addr != '127.0.0.1': address = socket.inet_aton(addrs[0]) type_ = self.stype + ".local." self.info = zeroconf.ServiceInfo( type_, self.name + "." + type_, address=address, port=self.port, properties=self.text, server=self.host if self.host else None ) self.zc = zeroconf.Zeroconf() self.zc.register_service(self.info)
def _register_zeroconf(self): if self.labthing: host = f"{self.labthing.safe_title}._labthing._tcp.local." if len(host) > 63: host = ( f"{hashlib.sha1(host.encode()).hexdigest()}._labthing._tcp.local." ) print(f"Registering zeroconf {host}") # Get list of host addresses mdns_addresses = { socket.inet_aton(i) for i in get_all_addresses() if i not in ("127.0.0.1", "0.0.0.0") } # LabThing service self.service_infos.append( ServiceInfo( "_labthing._tcp.local.", host, port=self.port, properties={ "path": self.labthing.url_prefix, "id": self.labthing.id, }, addresses=mdns_addresses, )) self.zeroconf_server = Zeroconf(ip_version=IPVersion.V4Only) for service in self.service_infos: self.zeroconf_server.register_service(service)
def add_service(self, zeroconf, type, name): timeout = 10000 info = zeroconf.get_service_info(type, name, timeout=timeout) if info is None: logger.warn( "Zeroconf network service information could not be retrieved within {} seconds" .format(str(timeout / 1000.0))) return id = _id_from_name(name) ip = socket.inet_ntoa(info.address) base_url = "http://{ip}:{port}/".format(ip=ip, port=info.port) zeroconf_service = ZEROCONF_STATE.get("service") is_self = zeroconf_service and zeroconf_service.id == id instance = { "id": id, "ip": ip, "local": ip in get_all_addresses(), "port": info.port, "host": info.server.strip("."), "base_url": base_url, "self": is_self, } device_info = parse_device_info(info) instance.update(device_info) self.instances[id] = instance if not is_self: try: DynamicNetworkLocation.objects.update_or_create(dict( base_url=base_url, **device_info), id=id) logger.info( "Kolibri instance '%s' joined zeroconf network; service info: %s" % (id, self.instances[id])) except ValidationError: import traceback logger.warn( """ A new Kolibri instance '%s' was seen on the zeroconf network, but we had trouble getting the information we needed about it. Service info: %s The following exception was raised: %s """ % (id, self.instances[id], traceback.format_exc(limit=1))) finally: connection.close()
def IfacesMonitor(self, event): NewState = get_all_addresses(socket.AF_INET) if self.IfacesMonitorState != NewState: if self.IfacesMonitorState is not None: # refresh only if a new address appeared for addr in NewState: if addr not in self.IfacesMonitorState: self.RefreshList() break self.IfacesMonitorState = NewState event.Skip()
def _get_sockets(self): sockets = [] for addr in zeroconf.get_all_addresses(): try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Set the time-to-live for messages for local network sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, self.SSDP_MX) sock.bind((addr, 0)) sockets.append(sock) except socket.error: pass return sockets
def get_local_ip(): """ Get local ip address of the localhost. Example: "10.0.0.2" :return: local ip address :rtype: str """ ranges = ["10.", "172.", "192."] all_addresses = zeroconf.get_all_addresses(netifaces.AF_INET) for address in all_addresses: for ran in ranges: if address.startswith(ran): return address return None
def add_service(self, zeroconf, type, name): info = zeroconf.get_service_info(type, name) id = _id_from_name(name) ip = socket.inet_ntoa(info.address) self.instances[id] = { "id": id, "ip": ip, "local": ip in get_all_addresses(), "port": info.port, "host": info.server.strip("."), "data": {key: json.loads(val) for (key, val) in info.properties.items()}, "base_url": "http://{ip}:{port}/".format(ip=ip, port=info.port), } logger.info( "Kolibri instance '%s' joined zeroconf network; service info: %s\n" % (id, self.instances[id]))
def get_urls(listen_port=None): """ :param listen_port: if set, will not try to determine the listen port from other running instances. """ try: if listen_port: port = listen_port else: __, __, port = get_status() urls = [] if port: try: for ip in get_all_addresses(): urls.append("http://{}:{}/".format(ip, port)) except RuntimeError: logger.error("Error retrieving network interface list!") return STATUS_RUNNING, urls except NotRunning as e: return e.status_code, []
def _get_interface_ip(interface: str) -> List[str]: if interface is None: return get_all_addresses() # check if the interface was passed as an IP try: IPv4Address(interface) return [interface] except AddressValueError: pass # pyzeroconf only supports ipv4 so limit to that ipv4s = list({ addr.ip for iface in ifaddr.get_adapters() for addr in iface.ips # Host only netmask 255.255.255.255 if addr.is_IPv4 and iface.nice_name == interface and addr.network_prefix != 32 }) if len(ipv4s) == 0: raise Exception("unable to find an ipv4 address for interface") return ipv4s
def scan(timeout=DISCOVER_TIMEOUT): """Send a message over the network to discover uPnP devices. Inspired by Crimsdings https://github.com/crimsdings/ChromeCast/blob/master/cc_discovery.py Protocol explanation: https://embeddedinn.wordpress.com/tutorials/upnp-device-architecture/ """ ssdp_requests = ssdp_request(ST_ALL), ssdp_request(ST_ROOTDEVICE) stop_wait = datetime.now() + timedelta(seconds=timeout) sockets = [] for addr in zeroconf.get_all_addresses(): try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # Set the time-to-live for messages for local network sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, SSDP_MX) sock.bind((addr, 0)) sockets.append(sock) except socket.error: pass entries = {} for sock in [s for s in sockets]: try: for req in ssdp_requests: sock.sendto(req, SSDP_TARGET) sock.setblocking(False) except socket.error: sockets.remove(sock) sock.close() try: while sockets: time_diff = stop_wait - datetime.now() seconds_left = time_diff.total_seconds() if seconds_left <= 0: break ready = select.select(sockets, [], [], seconds_left)[0] for sock in ready: try: data, address = sock.recvfrom(1024) response = data.decode("utf-8") except UnicodeDecodeError: logging.getLogger(__name__).debug( 'Ignoring invalid unicode response from %s', address) continue except socket.error: logging.getLogger(__name__).exception( "Socket error while discovering SSDP devices") sockets.remove(sock) sock.close() continue entry = UPNPEntry.from_response(response) entries[(entry.st, entry.location)] = entry finally: for s in sockets: s.close() return sorted(entries.values(), key=lambda entry: entry.location or '')
def publish(self, daap_server, preferred_database=None): """ Publish a given `DAAPServer` instance. The given instances should be fully configured, including the provider. By default Zeroconf only advertises the first database, but the DAAP protocol has support for multiple databases. Therefore, the parameter `preferred_database` can be set to choose which database ID will be served. If the provider is not fully configured (in other words, if the preferred database cannot be found), this method will not publish this server. In this case, simply call this method again when the provider is ready. If the server was already published, it will be unpublished first. :param DAAPServer daap_server: DAAP Server instance to publish. :param int preferred_database: ID of the database to advertise. """ if daap_server in self.daap_servers: self.unpublish(daap_server) # Zeroconf can advertise the information for one database only. Since # the protocol supports multiple database, let the user decide which # database to advertise. If none is specified, take the first one. provider = daap_server.provider try: if preferred_database is not None: database = provider.server.databases[preferred_database] else: database = provider.server.databases.values()[0] except LookupError: # The server may not have any databases (yet). return # The IP 0.0.0.0 tells this server to bind to all interfaces. However, # Bonjour advertises itself to others, so others need an actual IP. # There is definately a better way, but it works. if daap_server.ip == "0.0.0.0": addresses = [] for address in zeroconf.get_all_addresses(socket.AF_INET): if not address == "127.0.0.1": addresses.append(socket.inet_aton(address)) else: addresses = [socket.inet_aton(daap_server.ip)] # Determine machine ID and database ID, depending on the provider. If # the provider has no support for persistent IDs, generate a random # ID. if provider.supports_persistent_id: machine_id = hex(provider.server.persistent_id) database_id = hex(database.persistent_id) else: machine_id = hex(generate_persistent_id()) database_id = hex(generate_persistent_id()) # iTunes 11+ uses more properties, but this seems to be sufficient. description = { "txtvers": "1", "Password": str(int(bool(daap_server.password))), "Machine Name": provider.server.name, "Machine ID": machine_id.upper(), "Database ID": database_id.upper() } # Test is zeroconf supports multiple addresses or not. For # compatibility with zeroconf 0.17.3 or less. if not hasattr(zeroconf.ServiceInfo("", ""), "addresses"): addresses = addresses[0] self.daap_servers[daap_server] = zeroconf.ServiceInfo( type="_daap._tcp.local.", name=provider.server.name + "._daap._tcp.local.", address=addresses, port=daap_server.port, properties=description) self.zeroconf.register_service(self.daap_servers[daap_server])
def publish(self, daap_server, preferred_database=None): """ Publish a given `DAAPServer` instance. The given instances should be fully configured, including the provider. By default Zeroconf only advertises the first database, but the DAAP protocol has support for multiple databases. Therefore, the parameter `preferred_database` can be set to choose which database ID will be served. If the server was already published, it will be unpublished first. :param DAAPServer daap_server: DAAP Server instance to publish. :param int preferred_database: ID of the database to advertise. """ if daap_server in self.daap_servers: self.unpublish(daap_server) # The IP 0.0.0.0 tells SubDaap to bind to all interfaces. However, # Bonjour advertises itself to others, so others need an actual IP. # There is definately a better way, but it works. address = daap_server.ip if daap_server.ip == "0.0.0.0": for ip in zeroconf.get_all_addresses(socket.AF_INET): if ip != "127.0.0.1": address = ip break # Zeroconf can advertise the information for one database only. Since # the protocol supports multiple database, let the user decide which # database to advertise. If none is specified, take the first one. provider = daap_server.provider if preferred_database is not None: database = provider.server.databases[preferred_database] else: database = provider.server.databases.values()[0] # Determine machine ID and database ID, depending on the provider. If # the provider has no support for persistent IDs, generate a random # ID. if provider.supports_persistent_id: machine_id = hex(provider.server.persistent_id) database_id = hex(database.persistent_id) else: machine_id = hex(generate_persistent_id()) database_id = hex(generate_persistent_id()) # iTunes 11+ uses more properties, but this seems to be sufficient. description = { "txtvers": "1", "Password": str(int(bool(daap_server.password))), "Machine Name": provider.server.name, "Machine ID": machine_id.upper(), "Database ID": database_id.upper() } self.daap_servers[daap_server] = zeroconf.ServiceInfo( type="_daap._tcp.local.", name=provider.server.name + "._daap._tcp.local.", address=socket.inet_aton(address), port=daap_server.port, properties=description) self.zeroconf.register_service(self.daap_servers[daap_server])