1"""Utilities for identifying local IP addresses."""
2
3# Copyright (c) Jupyter Development Team.
4# Distributed under the terms of the Modified BSD License.
5
6import os
7import re
8import socket
9import subprocess
10from subprocess import Popen, PIPE
11
12from warnings import warn
13
14
15LOCAL_IPS = []
16PUBLIC_IPS = []
17
18LOCALHOST = ''
19
20
21def _uniq_stable(elems):
22    """uniq_stable(elems) -> list
23
24    Return from an iterable, a list of all the unique elements in the input,
25    maintaining the order in which they first appear.
26    """
27    seen = set()
28    return [x for x in elems if x not in seen and not seen.add(x)]
29
30def _get_output(cmd):
31    """Get output of a command, raising IOError if it fails"""
32    startupinfo = None
33    if os.name == 'nt':
34        startupinfo = subprocess.STARTUPINFO()
35        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
36    p = Popen(cmd, stdout=PIPE, stderr=PIPE, startupinfo=startupinfo)
37    stdout, stderr = p.communicate()
38    if p.returncode:
39        raise IOError("Failed to run %s: %s" % (cmd, stderr.decode('utf8', 'replace')))
40    return stdout.decode('utf8', 'replace')
41
42def _only_once(f):
43    """decorator to only run a function once"""
44    f.called = False
45    def wrapped(**kwargs):
46        if f.called:
47            return
48        ret = f(**kwargs)
49        f.called = True
50        return ret
51    return wrapped
52
53def _requires_ips(f):
54    """decorator to ensure load_ips has been run before f"""
55    def ips_loaded(*args, **kwargs):
56        _load_ips()
57        return f(*args, **kwargs)
58    return ips_loaded
59
60# subprocess-parsing ip finders
61class NoIPAddresses(Exception):
62    pass
63
64def _populate_from_list(addrs):
65    """populate local and public IPs from flat list of all IPs"""
66    if not addrs:
67        raise NoIPAddresses
68
69    global LOCALHOST
70    public_ips = []
71    local_ips = []
72
73    for ip in addrs:
74        local_ips.append(ip)
75        if not ip.startswith('127.'):
76            public_ips.append(ip)
77        elif not LOCALHOST:
78            LOCALHOST = ip
79
80    if not LOCALHOST or LOCALHOST == '127.0.0.1':
81        LOCALHOST = '127.0.0.1'
82        local_ips.insert(0, LOCALHOST)
83
84    local_ips.extend(['0.0.0.0', ''])
85
86    LOCAL_IPS[:] = _uniq_stable(local_ips)
87    PUBLIC_IPS[:] = _uniq_stable(public_ips)
88
89_ifconfig_ipv4_pat = re.compile(r'inet\b.*?(\d+\.\d+\.\d+\.\d+)', re.IGNORECASE)
90
91def _load_ips_ifconfig():
92    """load ip addresses from `ifconfig` output (posix)"""
93
94    try:
95        out = _get_output('ifconfig')
96    except (IOError, OSError):
97        # no ifconfig, it's usually in /sbin and /sbin is not on everyone's PATH
98        out = _get_output('/sbin/ifconfig')
99
100    lines = out.splitlines()
101    addrs = []
102    for line in lines:
103        m = _ifconfig_ipv4_pat.match(line.strip())
104        if m:
105            addrs.append(m.group(1))
106    _populate_from_list(addrs)
107
108
109def _load_ips_ip():
110    """load ip addresses from `ip addr` output (Linux)"""
111    out = _get_output(['ip', '-f', 'inet', 'addr'])
112
113    lines = out.splitlines()
114    addrs = []
115    for line in lines:
116        blocks = line.lower().split()
117        if (len(blocks) >= 2) and (blocks[0] == 'inet'):
118            addrs.append(blocks[1].split('/')[0])
119    _populate_from_list(addrs)
120
121_ipconfig_ipv4_pat = re.compile(r'ipv4.*?(\d+\.\d+\.\d+\.\d+)$', re.IGNORECASE)
122
123def _load_ips_ipconfig():
124    """load ip addresses from `ipconfig` output (Windows)"""
125    out = _get_output('ipconfig')
126
127    lines = out.splitlines()
128    addrs = []
129    for line in lines:
130        m = _ipconfig_ipv4_pat.match(line.strip())
131        if m:
132            addrs.append(m.group(1))
133    _populate_from_list(addrs)
134
135
136def _load_ips_netifaces():
137    """load ip addresses with netifaces"""
138    import netifaces
139    global LOCALHOST
140    local_ips = []
141    public_ips = []
142
143    # list of iface names, 'lo0', 'eth0', etc.
144    for iface in netifaces.interfaces():
145        # list of ipv4 addrinfo dicts
146        ipv4s = netifaces.ifaddresses(iface).get(netifaces.AF_INET, [])
147        for entry in ipv4s:
148            addr = entry.get('addr')
149            if not addr:
150                continue
151            if not (iface.startswith('lo') or addr.startswith('127.')):
152                public_ips.append(addr)
153            elif not LOCALHOST:
154                LOCALHOST = addr
155            local_ips.append(addr)
156    if not LOCALHOST:
157        # we never found a loopback interface (can this ever happen?), assume common default
158        LOCALHOST = '127.0.0.1'
159        local_ips.insert(0, LOCALHOST)
160    local_ips.extend(['0.0.0.0', ''])
161    LOCAL_IPS[:] = _uniq_stable(local_ips)
162    PUBLIC_IPS[:] = _uniq_stable(public_ips)
163
164
165def _load_ips_gethostbyname():
166    """load ip addresses with socket.gethostbyname_ex
167
168    This can be slow.
169    """
170    global LOCALHOST
171    try:
172        LOCAL_IPS[:] = socket.gethostbyname_ex('localhost')[2]
173    except socket.error:
174        # assume common default
175        LOCAL_IPS[:] = ['127.0.0.1']
176
177    try:
178        hostname = socket.gethostname()
179        PUBLIC_IPS[:] = socket.gethostbyname_ex(hostname)[2]
180        # try hostname.local, in case hostname has been short-circuited to loopback
181        if not hostname.endswith('.local') and all(ip.startswith('127') for ip in PUBLIC_IPS):
182            PUBLIC_IPS[:] = socket.gethostbyname_ex(socket.gethostname() + '.local')[2]
183    except socket.error:
184        pass
185    finally:
186        PUBLIC_IPS[:] = _uniq_stable(PUBLIC_IPS)
187        LOCAL_IPS.extend(PUBLIC_IPS)
188
189    # include all-interface aliases: 0.0.0.0 and ''
190    LOCAL_IPS.extend(['0.0.0.0', ''])
191
192    LOCAL_IPS[:] = _uniq_stable(LOCAL_IPS)
193
194    LOCALHOST = LOCAL_IPS[0]
195
196def _load_ips_dumb():
197    """Fallback in case of unexpected failure"""
198    global LOCALHOST
199    LOCALHOST = '127.0.0.1'
200    LOCAL_IPS[:] = [LOCALHOST, '0.0.0.0', '']
201    PUBLIC_IPS[:] = []
202
203@_only_once
204def _load_ips(suppress_exceptions=True):
205    """load the IPs that point to this machine
206
207    This function will only ever be called once.
208
209    It will use netifaces to do it quickly if available.
210    Then it will fallback on parsing the output of ifconfig / ip addr / ipconfig, as appropriate.
211    Finally, it will fallback on socket.gethostbyname_ex, which can be slow.
212    """
213
214    try:
215        # first priority, use netifaces
216        try:
217            return _load_ips_netifaces()
218        except ImportError:
219            pass
220
221        # second priority, parse subprocess output (how reliable is this?)
222
223        if os.name == 'nt':
224            try:
225                return _load_ips_ipconfig()
226            except (IOError, NoIPAddresses):
227                pass
228        else:
229            try:
230                return _load_ips_ip()
231            except (IOError, OSError, NoIPAddresses):
232                pass
233            try:
234                return _load_ips_ifconfig()
235            except (IOError, OSError, NoIPAddresses):
236                pass
237
238        # lowest priority, use gethostbyname
239
240        return _load_ips_gethostbyname()
241    except Exception as e:
242        if not suppress_exceptions:
243            raise
244        # unexpected error shouldn't crash, load dumb default values instead.
245        warn("Unexpected error discovering local network interfaces: %s" % e)
246    _load_ips_dumb()
247
248
249@_requires_ips
250def local_ips():
251    """return the IP addresses that point to this machine"""
252    return LOCAL_IPS
253
254@_requires_ips
255def public_ips():
256    """return the IP addresses for this machine that are visible to other machines"""
257    return PUBLIC_IPS
258
259@_requires_ips
260def localhost():
261    """return ip for localhost (almost always 127.0.0.1)"""
262    return LOCALHOST
263
264@_requires_ips
265def is_local_ip(ip):
266    """does `ip` point to this machine?"""
267    return ip in LOCAL_IPS
268
269@_requires_ips
270def is_public_ip(ip):
271    """is `ip` a publicly visible address?"""
272    return ip in PUBLIC_IPS
273