File size: 8,445 Bytes
d1ceb73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
"""Utilities for identifying local IP addresses."""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations

import os
import re
import socket
import subprocess
from subprocess import PIPE, Popen
from typing import Any, Callable, Iterable, Sequence
from warnings import warn

LOCAL_IPS: list = []
PUBLIC_IPS: list = []

LOCALHOST: str = ""


def _uniq_stable(elems: Iterable) -> list:
    """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.
    """
    seen = set()
    value = []
    for x in elems:
        if x not in seen:
            value.append(x)
            seen.add(x)
    return value


def _get_output(cmd: str | Sequence[str]) -> str:
    """Get output of a command, raising IOError if it fails"""
    startupinfo = None
    if os.name == "nt":
        startupinfo = subprocess.STARTUPINFO()  # type:ignore[attr-defined]
        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW  # type:ignore[attr-defined]
    p = Popen(cmd, stdout=PIPE, stderr=PIPE, startupinfo=startupinfo)  # noqa
    stdout, stderr = p.communicate()
    if p.returncode:
        msg = "Failed to run {}: {}".format(cmd, stderr.decode("utf8", "replace"))
        raise OSError(msg)
    return stdout.decode("utf8", "replace")


def _only_once(f: Callable) -> Callable:
    """decorator to only run a function once"""
    f.called = False  # type:ignore[attr-defined]

    def wrapped(**kwargs: Any) -> Any:
        if f.called:  # type:ignore[attr-defined]
            return
        ret = f(**kwargs)
        f.called = True  # type:ignore[attr-defined]
        return ret

    return wrapped


def _requires_ips(f: Callable) -> Callable:
    """decorator to ensure load_ips has been run before f"""

    def ips_loaded(*args: Any, **kwargs: Any) -> Any:
        _load_ips()
        return f(*args, **kwargs)

    return ips_loaded


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


def _populate_from_list(addrs: Sequence[str] | None) -> None:
    """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 or LOCALHOST == "127.0.0.1":
        LOCALHOST = "127.0.0.1"
        local_ips.insert(0, LOCALHOST)

    local_ips.extend(["0.0.0.0", ""])  # noqa

    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() -> None:
    """load ip addresses from `ifconfig` output (posix)"""

    try:
        out = _get_output("ifconfig")
    except 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() -> None:
    """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() -> None:
    """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() -> None:
    """load ip addresses with netifaces"""
    import netifaces  # type: ignore[import-not-found]

    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", ""])  # noqa
    LOCAL_IPS[:] = _uniq_stable(local_ips)
    PUBLIC_IPS[:] = _uniq_stable(public_ips)


def _load_ips_gethostbyname() -> None:
    """load ip addresses with socket.gethostbyname_ex

    This can be slow.
    """
    global LOCALHOST
    try:
        LOCAL_IPS[:] = socket.gethostbyname_ex("localhost")[2]
    except OSError:
        # 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 OSError:
        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", ""])  # noqa

    LOCAL_IPS[:] = _uniq_stable(LOCAL_IPS)

    LOCALHOST = LOCAL_IPS[0]


def _load_ips_dumb() -> None:
    """Fallback in case of unexpected failure"""
    global LOCALHOST
    LOCALHOST = "127.0.0.1"
    LOCAL_IPS[:] = [LOCALHOST, "0.0.0.0", ""]  # noqa
    PUBLIC_IPS[:] = []


@_only_once
def _load_ips(suppress_exceptions: bool = True) -> None:
    """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 (OSError, NoIPAddresses):
                pass
        else:
            try:
                return _load_ips_ip()
            except (OSError, NoIPAddresses):
                pass
            try:
                return _load_ips_ifconfig()
            except (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, stacklevel=2)
    _load_ips_dumb()


@_requires_ips
def local_ips() -> list[str]:
    """return the IP addresses that point to this machine"""
    return LOCAL_IPS


@_requires_ips
def public_ips() -> list[str]:
    """return the IP addresses for this machine that are visible to other machines"""
    return PUBLIC_IPS


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


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


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