# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import argparse import array import re import socket import struct import subprocess import sys import mozinfo import mozlog if mozinfo.isLinux: import fcntl if mozinfo.isWin: import os class NetworkError(Exception): """Exception thrown when unable to obtain interface or IP.""" def _get_logger(): logger = mozlog.get_default_logger(component="moznetwork") if not logger: logger = mozlog.unstructured.getLogger("moznetwork") return logger def _get_interface_list(): """Provides a list of available network interfaces as a list of tuples (name, ip)""" logger = _get_logger() logger.debug("Gathering interface list") max_iface = 32 # Maximum number of interfaces(arbitrary) bytes = max_iface * 32 is_32bit = (8 * struct.calcsize("P")) == 32 # Set Architecture struct_size = 32 if is_32bit else 40 try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) names = array.array("B", b"\0" * bytes) outbytes = struct.unpack( "iL", fcntl.ioctl( s.fileno(), 0x8912, # SIOCGIFCONF struct.pack("iL", bytes, names.buffer_info()[0]), ), )[0] namestr = names.tobytes() return [ ( namestr[i : i + 32].split(b"\0", 1)[0].decode(), socket.inet_ntoa(namestr[i + 20 : i + 24]), ) for i in range(0, outbytes, struct_size) ] except OSError: raise NetworkError("Unable to call ioctl with SIOCGIFCONF") def _proc_matches(args, regex): """Helper returns the matches of regex in the output of a process created with the given arguments""" output = subprocess.Popen( args=args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True, ).stdout.read() return re.findall(regex, output) def _parse_ifconfig(): """Parse the output of running ifconfig on mac in cases other methods have failed""" logger = _get_logger() logger.debug("Parsing ifconfig") # Attempt to determine the default interface in use. default_iface = _proc_matches( ["route", "-n", "get", "default"], r"interface: (\w+)" ) if default_iface: addr_list = _proc_matches( ["ifconfig", default_iface[0]], r"inet (\d+.\d+.\d+.\d+)" ) if addr_list: logger.debug( "Default interface: [%s] %s" % (default_iface[0], addr_list[0]) ) if not addr_list[0].startswith("127."): return addr_list[0] # Iterate over plausible interfaces if we didn't find a suitable default. for iface in ["en%s" % i for i in range(10)]: addr_list = _proc_matches(["ifconfig", iface], r"inet (\d+.\d+.\d+.\d+)") if addr_list: logger.debug("Interface: [%s] %s" % (iface, addr_list[0])) if not addr_list[0].startswith("127."): return addr_list[0] # Just return any that isn't localhost. If we can't find one, we have # failed. addrs = _proc_matches(["ifconfig"], r"inet (\d+.\d+.\d+.\d+)") try: return [addr for addr in addrs if not addr.startswith("127.")][0] except IndexError: return None def _parse_powershell(): logger = _get_logger() logger.debug("Parsing Get-NetIPAdress output via PowerShell") try: cmd = os.path.join( os.environ.get("SystemRoot", "C:\\WINDOWS"), "system32", "windowspowershell", "v1.0", "powershell.exe", ) output = subprocess.check_output([ cmd, "(Get-NetIPAddress -AddressFamily IPv4 -AddressState Preferred | Format-List -Property IPAddress)", ]).decode("ascii") ips = re.findall(r"IPAddress : (\d+.\d+.\d+.\d+)", output) for ip in ips: logger.debug("IPAddress: %s" % ip) if not ip.startswith("127."): return ip return None except FileNotFoundError: return None def get_ip(): """Provides an available network interface address, for example "192.168.1.3". A `NetworkError` exception is raised in case of failure.""" logger = _get_logger() try: hostname = socket.gethostname() try: logger.debug("Retrieving IP for %s" % hostname) ips = socket.gethostbyname_ex(hostname)[2] except socket.gaierror: # for Mac OS X hostname += ".local" logger.debug("Retrieving IP for %s" % hostname) ips = socket.gethostbyname_ex(hostname)[2] if len(ips) == 1: ip = ips[0] elif len(ips) > 1: logger.debug("Multiple addresses found: %s" % ips) ip = None else: ip = None except socket.gaierror: # sometimes the hostname doesn't resolve to an ip address, in which # case this will always fail ip = None if ip is None or ip.startswith("127."): if mozinfo.isLinux: interfaces = _get_interface_list() for ifconfig in interfaces: logger.debug("Interface: [%s] %s" % (ifconfig[0], ifconfig[1])) if ifconfig[0] == "lo": continue else: return ifconfig[1] elif mozinfo.isMac: ip = _parse_ifconfig() elif mozinfo.isWin: ip = _parse_powershell() if ip is None: raise NetworkError("Unable to obtain network address") return ip def get_lan_ip(): """Deprecated. Please use get_ip() instead.""" return get_ip() def cli(args=sys.argv[1:]): parser = argparse.ArgumentParser(description="Retrieve IP address") mozlog.commandline.add_logging_group( parser, include_formatters=mozlog.commandline.TEXT_FORMATTERS ) args = parser.parse_args() mozlog.commandline.setup_logging("mozversion", args, {"mach": sys.stdout}) _get_logger().info("IP address: %s" % get_ip()) if __name__ == "__main__": cli()