localinterfaces.py 7.69 KB
Newer Older
Stelios Karozis's avatar
Stelios Karozis committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
"""Utilities for identifying local IP addresses."""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

import os
import re
import socket
import subprocess
from subprocess import Popen, PIPE

from warnings import warn


LOCAL_IPS = []
PUBLIC_IPS = []

LOCALHOST = ''


def _uniq_stable(elems):
    """uniq_stable(elems) -> list

    Return from an iterable, a list of all the unique elements in the input,
    maintaining the order in which they first appear.
    
    From ipython_genutils.data
    """
    seen = set()
    return [x for x in elems if x not in seen and not seen.add(x)]

def _get_output(cmd):
    """Get output of a command, raising IOError if it fails"""
    startupinfo = None
    if os.name == 'nt':
        startupinfo = subprocess.STARTUPINFO()
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
    p = Popen(cmd, stdout=PIPE, stderr=PIPE, startupinfo=startupinfo)
    stdout, stderr = p.communicate()
    if p.returncode:
        raise IOError("Failed to run %s: %s" % (cmd, stderr.decode('utf8', 'replace')))
    return stdout.decode('utf8', 'replace')

def _only_once(f):
    """decorator to only run a function once"""
    f.called = False
    def wrapped(**kwargs):
        if f.called:
            return
        ret = f(**kwargs)
        f.called = True
        return ret
    return wrapped

def _requires_ips(f):
    """decorator to ensure load_ips has been run before f"""
    def ips_loaded(*args, **kwargs):
        _load_ips()
        return f(*args, **kwargs)
    return ips_loaded

# subprocess-parsing ip finders
class NoIPAddresses(Exception):
    pass

def _populate_from_list(addrs):
    """populate local and public IPs from flat list of all IPs"""
    if not addrs:
        raise NoIPAddresses
    
    global LOCALHOST
    public_ips = []
    local_ips = []
    
    for ip in addrs:
        local_ips.append(ip)
        if not ip.startswith('127.'):
            public_ips.append(ip)
        elif not LOCALHOST:
            LOCALHOST = ip
    
    if not LOCALHOST:
        LOCALHOST = '127.0.0.1'
        local_ips.insert(0, LOCALHOST)
        
    local_ips.extend(['0.0.0.0', ''])
    
    LOCAL_IPS[:] = _uniq_stable(local_ips)
    PUBLIC_IPS[:] = _uniq_stable(public_ips)

_ifconfig_ipv4_pat = re.compile(r'inet\b.*?(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)

def _load_ips_ifconfig():
    """load ip addresses from `ifconfig` output (posix)"""
    
    try:
        out = _get_output('ifconfig')
    except (IOError, OSError):
        # no ifconfig, it's usually in /sbin and /sbin is not on everyone's PATH
        out = _get_output('/sbin/ifconfig')
    
    lines = out.splitlines()
    addrs = []
    for line in lines:
        m = _ifconfig_ipv4_pat.match(line.strip())
        if m:
            addrs.append(m.group(1))
    _populate_from_list(addrs)


def _load_ips_ip():
    """load ip addresses from `ip addr` output (Linux)"""
    out = _get_output(['ip', '-f', 'inet', 'addr'])
    
    lines = out.splitlines()
    addrs = []
    for line in lines:
        blocks = line.lower().split()
        if (len(blocks) >= 2) and (blocks[0] == 'inet'):
            addrs.append(blocks[1].split('/')[0])
    _populate_from_list(addrs)

_ipconfig_ipv4_pat = re.compile(r'ipv4.*?(\d+\.\d+\.\d+\.\d+)$', re.IGNORECASE)

def _load_ips_ipconfig():
    """load ip addresses from `ipconfig` output (Windows)"""
    out = _get_output('ipconfig')
    
    lines = out.splitlines()
    addrs = []
    for line in lines:
        m = _ipconfig_ipv4_pat.match(line.strip())
        if m:
            addrs.append(m.group(1))
    _populate_from_list(addrs)


def _load_ips_netifaces():
    """load ip addresses with netifaces"""
    import netifaces
    global LOCALHOST
    local_ips = []
    public_ips = []
    
    # list of iface names, 'lo0', 'eth0', etc.
    for iface in netifaces.interfaces():
        # list of ipv4 addrinfo dicts
        ipv4s = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
        for entry in ipv4s:
            addr = entry.get('addr')
            if not addr:
                continue
            if not (iface.startswith('lo') or addr.startswith('127.')):
                public_ips.append(addr)
            elif not LOCALHOST:
                LOCALHOST = addr
            local_ips.append(addr)
    if not LOCALHOST:
        # we never found a loopback interface (can this ever happen?), assume common default
        LOCALHOST = '127.0.0.1'
        local_ips.insert(0, LOCALHOST)
    local_ips.extend(['0.0.0.0', ''])
    LOCAL_IPS[:] = _uniq_stable(local_ips)
    PUBLIC_IPS[:] = _uniq_stable(public_ips)


def _load_ips_gethostbyname():
    """load ip addresses with socket.gethostbyname_ex
    
    This can be slow.
    """
    global LOCALHOST
    try:
        LOCAL_IPS[:] = socket.gethostbyname_ex('localhost')[2]
    except socket.error:
        # assume common default
        LOCAL_IPS[:] = ['127.0.0.1']
    
    try:
        hostname = socket.gethostname()
        PUBLIC_IPS[:] = socket.gethostbyname_ex(hostname)[2]
        # try hostname.local, in case hostname has been short-circuited to loopback
        if not hostname.endswith('.local') and all(ip.startswith('127') for ip in PUBLIC_IPS):
            PUBLIC_IPS[:] = socket.gethostbyname_ex(socket.gethostname() + '.local')[2]
    except socket.error:
        pass
    finally:
        PUBLIC_IPS[:] = _uniq_stable(PUBLIC_IPS)
        LOCAL_IPS.extend(PUBLIC_IPS)
    
    # include all-interface aliases: 0.0.0.0 and ''
    LOCAL_IPS.extend(['0.0.0.0', ''])

    LOCAL_IPS[:] = _uniq_stable(LOCAL_IPS)

    LOCALHOST = LOCAL_IPS[0]

def _load_ips_dumb():
    """Fallback in case of unexpected failure"""
    global LOCALHOST
    LOCALHOST = '127.0.0.1'
    LOCAL_IPS[:] = [LOCALHOST, '0.0.0.0', '']
    PUBLIC_IPS[:] = []

@_only_once
def _load_ips(suppress_exceptions=True):
    """load the IPs that point to this machine
    
    This function will only ever be called once.
    
    It will use netifaces to do it quickly if available.
    Then it will fallback on parsing the output of ifconfig / ip addr / ipconfig, as appropriate.
    Finally, it will fallback on socket.gethostbyname_ex, which can be slow.
    """
    
    try:
        # first priority, use netifaces
        try:
            return _load_ips_netifaces()
        except ImportError:
            pass
        
        # second priority, parse subprocess output (how reliable is this?)
        
        if os.name == 'nt':
            try:
                return _load_ips_ipconfig()
            except (IOError, NoIPAddresses):
                pass
        else:
            try:
                return _load_ips_ip()
            except (IOError, OSError, NoIPAddresses):
                pass
            try:
                return _load_ips_ifconfig()
            except (IOError, OSError, NoIPAddresses):
                pass
        
        # lowest priority, use gethostbyname
        
        return _load_ips_gethostbyname()
    except Exception as e:
        if not suppress_exceptions:
            raise
        # unexpected error shouldn't crash, load dumb default values instead.
        warn("Unexpected error discovering local network interfaces: %s" % e)
    _load_ips_dumb()


@_requires_ips
def local_ips():
    """return the IP addresses that point to this machine"""
    return LOCAL_IPS

@_requires_ips
def public_ips():
    """return the IP addresses for this machine that are visible to other machines"""
    return PUBLIC_IPS

@_requires_ips
def localhost():
    """return ip for localhost (almost always 127.0.0.1)"""
    return LOCALHOST

@_requires_ips
def is_local_ip(ip):
    """does `ip` point to this machine?"""
    return ip in LOCAL_IPS

@_requires_ips
def is_public_ip(ip):
    """is `ip` a publicly visible address?"""
    return ip in PUBLIC_IPS