diff --git a/copyparty/__init__.py b/copyparty/__init__.py index 9adc6dd6..aeda2bb7 100644 --- a/copyparty/__init__.py +++ b/copyparty/__init__.py @@ -12,11 +12,11 @@ except: TYPE_CHECKING = False if True: - from typing import Any, Callable, Union + from typing import Any, Callable PY2 = sys.version_info < (3,) if not PY2: - unicode: Callable[[Union[str, int, float]], str] = str + unicode: Callable[[Any], str] = str else: sys.dont_write_bytecode = True unicode = unicode # noqa: F821 # pylint: disable=undefined-variable,self-assigning-variable diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3c7cd044..572292be 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -666,15 +666,15 @@ def run_argparse( ap2 = ap.add_argument_group("Zeroconf options") ap2.add_argument("-z", action="store_true", help="enable all zeroconf backends (mdns, ssdp)") - ap2.add_argument("--z-on", metavar="NICS/NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names\n └─example: \033[32meth0, wlo1, virhost0, 192.168.123.0/24, fd00:fda::/96\033[0m") - ap2.add_argument("--z-off", metavar="NICS/NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names") + ap2.add_argument("--z-on", metavar="NICS/NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes\n └─example: \033[32meth0, wlo1, virhost0, 192.168.123.0/24, fd00:fda::/96\033[0m") + ap2.add_argument("--z-off", metavar="NICS/NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("-zv", action="store_true", help="verbose all zeroconf backends") ap2.add_argument("--mc-hop", metavar="SEC", type=int, default=0, help="rejoin multicast groups every SEC seconds (workaround for some switches/routers which cause mDNS to suddenly stop working after some time); try [\033[32m300\033[0m] or [\033[32m180\033[0m]") ap2 = ap.add_argument_group("Zeroconf-mDNS options:") ap2.add_argument("--zm", action="store_true", help="announce the enabled protocols over mDNS (multicast DNS-SD) -- compatible with KDE, gnome, macOS, ...") - ap2.add_argument("--zm-on", metavar="NICS/NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names") - ap2.add_argument("--zm-off", metavar="NICS/NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names") + ap2.add_argument("--zm-on", metavar="NICS/NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes") + ap2.add_argument("--zm-off", metavar="NICS/NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("--zm4", action="store_true", help="IPv4 only -- try this if some clients can't connect") ap2.add_argument("--zm6", action="store_true", help="IPv6 only") ap2.add_argument("--zmv", action="store_true", help="verbose mdns") @@ -690,8 +690,8 @@ def run_argparse( ap2 = ap.add_argument_group("Zeroconf-SSDP options:") ap2.add_argument("--zs", action="store_true", help="announce the enabled protocols over SSDP -- compatible with Windows") - ap2.add_argument("--zs-on", metavar="NICS/NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names") - ap2.add_argument("--zs-off", metavar="NICS/NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names") + ap2.add_argument("--zs-on", metavar="NICS/NETS", type=u, default="", help="enable zeroconf ONLY on the comma-separated list of subnets and/or interface names/indexes") + ap2.add_argument("--zs-off", metavar="NICS/NETS", type=u, default="", help="disable zeroconf on the comma-separated list of subnets and/or interface names/indexes") ap2.add_argument("--zsv", action="store_true", help="verbose SSDP") ap2.add_argument("--zsl", metavar="PATH", type=u, default="/?hc", help="location to include in the url (or a complete external URL), for example [\033[32mpriv/?pw=hunter2\033[0m] or [\033[32mpriv/?pw=hunter2\033[0m]") ap2.add_argument("--zsid", metavar="UUID", type=u, default=uuid.uuid4().urn[4:], help="USN (device identifier) to announce") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index b5af2ed5..e339b489 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -34,7 +34,6 @@ from .util import ( if True: # pylint: disable=using-constant-test from collections.abc import Iterable - import typing from typing import Any, Generator, Optional, Union from .util import RootLogger diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index af10334d..04cf3790 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -38,6 +38,7 @@ from .util import ( Garda, Magician, NetMap, + Netdev, ipnorm, min_ex, shut_socket, @@ -140,7 +141,7 @@ class HttpSrv(object): except: pass - def set_netdevs(self, netdevs: dict[str, str]) -> None: + def set_netdevs(self, netdevs: dict[str, Netdev]) -> None: self.nm = NetMap([self.ip], netdevs) def start_threads(self, n: int) -> None: diff --git a/copyparty/mdns.py b/copyparty/mdns.py index fcd2e7f5..fd0ef574 100644 --- a/copyparty/mdns.py +++ b/copyparty/mdns.py @@ -25,7 +25,7 @@ from .stolen.dnslib import ( DNSQuestion, DNSRecord, ) -from .util import CachedSet, Daemon, min_ex +from .util import CachedSet, Daemon, Netdev, min_ex if TYPE_CHECKING: from .svchub import SvcHub @@ -42,13 +42,12 @@ class MDNS_Sck(MC_Sck): def __init__( self, sck: socket.socket, - idx: int, - name: str, + nd: Netdev, grp: str, ip: str, net: Union[IPv4Network, IPv6Network], ): - super(MDNS_Sck, self).__init__(sck, idx, name, grp, ip, net) + super(MDNS_Sck, self).__init__(sck, nd, grp, ip, net) self.bp_probe = b"" self.bp_ip = b"" @@ -263,7 +262,8 @@ class MDNS(MCast): try: bound = self.create_servers() except: - self.log("no server IP matches the mdns config", 1) + t = "no server IP matches the mdns config\n{}" + self.log(t.format(min_ex()), 1) bound = [] if not bound: diff --git a/copyparty/multicast.py b/copyparty/multicast.py index 46fc9d20..3d4e4fe4 100644 --- a/copyparty/multicast.py +++ b/copyparty/multicast.py @@ -7,8 +7,8 @@ import time import ipaddress from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network -from .__init__ import MACOS, TYPE_CHECKING -from .util import min_ex, spack +from .__init__ import TYPE_CHECKING +from .util import Netdev, min_ex, spack if TYPE_CHECKING: from .svchub import SvcHub @@ -30,15 +30,14 @@ class MC_Sck(object): def __init__( self, sck: socket.socket, - idx: int, - name: str, + nd: Netdev, grp: str, ip: str, net: Union[IPv4Network, IPv6Network], ): self.sck = sck - self.idx = idx - self.name = name + self.idx = nd.idx + self.name = nd.name self.grp = grp self.mreq = b"" self.ip = ip @@ -112,7 +111,7 @@ class MCast(object): for lst in (on, off): for av in list(lst): for sk, sv in netdevs.items(): - if av == sv.split(",")[0] and sk not in lst: + if (av == str(sv.idx) or av == sv.name) and sk not in lst: lst.append(sk) if on: @@ -137,12 +136,8 @@ class MCast(object): for ip in ips: v6 = ":" in ip - netdev = "?" - try: - netdev = netdevs[ip].split(",")[0] - idx = socket.if_nametoindex(netdev) - except: - idx = socket.INADDR_ANY + netdev = netdevs[ip] + if not netdev.idx: t = "using INADDR_ANY for ip [{}], netdev [{}]" if not self.srv and ip not in ["::", "0.0.0.0"]: self.log(t.format(ip, netdev), 3) @@ -159,20 +154,14 @@ class MCast(object): # most ipv6 clients expect multicast on linklocal ip only; # add a/aaaa records for the other nic IPs other_ips: set[str] = set() - if v6 and netdev not in ("?", ""): - for oip, onic in netdevs.items(): - if ( - onic.split(",")[0] == netdev - and oip in all_selected - and ":" in oip - ): - other_ips.add(oip) + if v6: + for nd in netdevs.values(): + if nd.idx == netdev.idx and nd.ip in all_selected and ":" in nd.ip: + other_ips.add(nd.ip) net = ipaddress.ip_network(ip, False) ip = ip.split("/")[0] - srv = self.Srv( - sck, idx, netdev, self.grp6 if ":" in ip else self.grp4, ip, net - ) + srv = self.Srv(sck, netdev, self.grp6 if ":" in ip else self.grp4, ip, net) for oth_ip in other_ips: srv.ips[oth_ip.split("/")[0]] = ipaddress.ip_network(oth_ip, False) @@ -225,8 +214,11 @@ class MCast(object): self.b2srv[bip] = srv self.b6.append(bip) - grp = self.grp6 if srv.idx and not MACOS else "" - sck.bind((grp, self.port, 0, srv.idx)) + grp = self.grp6 if srv.idx else "" + try: + sck.bind((grp, self.port, 0, srv.idx)) + except: + sck.bind(("", self.port, 0, srv.idx)) bgrp = socket.inet_pton(socket.AF_INET6, self.grp6) dev = spack(b"@I", srv.idx) @@ -249,8 +241,12 @@ class MCast(object): self.b2srv[bip] = srv self.b4.append(bip) - grp = self.grp4 if srv.idx and not MACOS else "" - sck.bind((grp, self.port)) + grp = self.grp4 if srv.idx else "" + try: + sck.bind((grp, self.port)) + except: + sck.bind(("", self.port)) + bgrp = socket.inet_aton(self.grp4) dev = ( spack(b"=I", socket.INADDR_ANY) diff --git a/copyparty/ssdp.py b/copyparty/ssdp.py index 3f536c07..40ae5626 100644 --- a/copyparty/ssdp.py +++ b/copyparty/ssdp.py @@ -105,7 +105,8 @@ class SSDPd(MCast): try: bound = self.create_servers() except: - self.log("no server IP matches the ssdp config", 1) + t = "no server IP matches the ssdp config\n{}" + self.log(t.format(min_ex()), 1) bound = [] if not bound: @@ -130,7 +131,7 @@ class SSDPd(MCast): for sck in rx: buf, addr = sck.recvfrom(4096) try: - self.eat(buf, addr, sck) + self.eat(buf, addr) except: if not self.running: return @@ -144,7 +145,7 @@ class SSDPd(MCast): self.running = False self.srv = {} - def eat(self, buf: bytes, addr: tuple[str, int], sck: socket.socket) -> None: + def eat(self, buf: bytes, addr: tuple[str, int]) -> None: cip = addr[0] if cip.startswith("169.254"): return diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index d3b1b026..655fcacf 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -6,14 +6,14 @@ import re import socket import sys -from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, VT100, unicode +from .__init__ import ANYWIN, PY2, TYPE_CHECKING, VT100, unicode from .stolen.qrcodegen import QrCode from .util import ( E_ACCESS, E_ADDR_IN_USE, E_ADDR_NOT_AVAIL, E_UNREACH, - chkcmd, + Netdev, min_ex, sunpack, termsize, @@ -101,7 +101,10 @@ class TcpSrv(object): if pad: self.log("tcpsrv", "") - eps = {"127.0.0.1": "local only", "::1": "local only"} + eps = { + "127.0.0.1": Netdev("127.0.0.1", 0, "", "local only"), + "::1": Netdev("::1", 0, "", "local only"), + } nonlocals = [x for x in self.args.i if x not in [k.split("/")[0] for k in eps]] if nonlocals: try: @@ -114,7 +117,7 @@ class TcpSrv(object): eps.update({k.split("/")[0]: v for k, v in self.netdevs.items()}) if not eps: for x in nonlocals: - eps[x] = "external" + eps[x] = Netdev(x, 0, "", "external") else: self.netdevs = {} @@ -264,143 +267,11 @@ class TcpSrv(object): self.log("tcpsrv", "ok bye") - def ips_linux_ifconfig(self) -> dict[str, str]: - # for termux - try: - txt, _ = chkcmd(["ifconfig"]) - except: - return {} - - eps: dict[str, str] = {} - dev = None - ip = None - up = None - for ln in (txt + "\n").split("\n"): - if not ln.strip() and dev and ip: - eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN") - dev = ip = up = None - continue - - if ln == ln.lstrip(): - dev = re.split(r"[: ]", ln)[0] - - if "UP" in re.split(r"[<>, \t]", ln): - up = True - - m = re.match(r"^\s+inet\s+([^ ]+)", ln) - if m: - ip = m.group(1) - - return eps - - def ips_linux(self) -> dict[str, str]: - try: - txt, _ = chkcmd(["ip", "addr"]) - except: - return self.ips_linux_ifconfig() - - r = re.compile(r"^\s+inet6? ([^ ]+)/") - ri = re.compile(r"^[0-9]+: ([^:]+): ") - dev = "" - up = False - eps: dict[str, str] = {} - for ln in txt.split("\n"): - m = ri.match(ln) - if m: - dev = m.group(1) - up = "UP" in re.split("[>,< ]", ln) - - m = r.match(ln.rstrip()) - if not m or not dev or " scope link" in ln: - continue - - ip = m.group(1) - eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN") - - return eps - - def ips_macos(self) -> dict[str, str]: - eps: dict[str, str] = {} - try: - txt, _ = chkcmd(["ifconfig"]) - except: - return eps - - rdev = re.compile(r"^([^ ]+):") - rip = re.compile(r"^\tinet ([0-9\.]+) ") - dev = "UNKNOWN" - for ln in txt.split("\n"): - m = rdev.match(ln) - if m: - dev = m.group(1) - - m = rip.match(ln) - if m: - eps[m.group(1)] = dev - dev = "UNKNOWN" - - return eps - - def ips_windows_ipconfig(self) -> tuple[dict[str, str], set[str]]: - eps: dict[str, str] = {} - offs: set[str] = set() - try: - txt, _ = chkcmd(["ipconfig"]) - except: - return eps, offs - - rdev = re.compile(r"(^[^ ].*):$") - rip = re.compile(r"^ +IPv?4? [^:]+: *([0-9\.]{7,15})$") - roff = re.compile(r".*: Media disconnected$") - dev = None - for ln in txt.replace("\r", "").split("\n"): - m = rdev.match(ln) - if m: - if dev and dev not in eps.values(): - offs.add(dev) - - dev = m.group(1).split(" adapter ", 1)[-1] - - if dev and roff.match(ln): - offs.add(dev) - dev = None - - m = rip.match(ln) - if m and dev: - eps[m.group(1)] = dev - dev = None - - if dev and dev not in eps.values(): - offs.add(dev) - - return eps, offs - - def ips_windows_netsh(self) -> dict[str, str]: - eps: dict[str, str] = {} - try: - txt, _ = chkcmd("netsh interface ip show address".split()) - except: - return eps - - rdev = re.compile(r'.* "([^"]+)"$') - rip = re.compile(r".* IP\b.*: +([0-9\.]{7,15})$") - dev = None - for ln in txt.replace("\r", "").split("\n"): - m = rdev.match(ln) - if m: - dev = m.group(1) - - m = rip.match(ln) - if m and dev: - eps[m.group(1)] = dev - - return eps - - def detect_interfaces(self, listen_ips: list[str]) -> dict[str, str]: + def detect_interfaces(self, listen_ips: list[str]) -> dict[str, Netdev]: from .stolen.ifaddr import get_adapters nics = get_adapters(True) - eps = {} + eps: dict[str, Netdev] = {} for nic in nics: for nip in nic.ips: ipa = nip.ip[0] if ":" in str(nip.ip) else nip.ip @@ -409,14 +280,15 @@ class TcpSrv(object): # browsers dont impl linklocal continue - eps[sip] = nic.nice_name + eps[sip] = Netdev(sip, nic.index or 0, nic.nice_name, "") if "0.0.0.0" not in listen_ips and "::" not in listen_ips: eps = {k: v for k, v in eps.items() if k.split("/")[0] in listen_ips} try: ext_devs = list(self._extdevs_nix()) - ext_ips = [k for k, v in eps.items() if v.split(",")[0] in ext_devs] + ext_ips = [k for k, v in eps.items() if v.name in ext_devs] + ext_ips = [x.split("/")[0] for x in ext_ips] if not ext_ips: raise Exception() except: @@ -430,11 +302,9 @@ class TcpSrv(object): desc = "\033[32mexternal" ips = ext_ips if lip in ["0.0.0.0", "::"] else [lip] for ip in ips: - try: - if "external" not in eps[ip]: - eps[ip] += ", " + desc - except: - eps[ip] = desc + ip = next((x for x in eps if x.startswith(ip + "/")), "") + if ip and "external" not in eps[ip].desc: + eps[ip].desc += ", " + desc return eps diff --git a/copyparty/util.py b/copyparty/util.py index f209c119..e4fd4dd3 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -188,10 +188,14 @@ IMPLICATIONS = [ ["z", "zm"], ["z", "zs"], ["zmvv", "zmv"], + ["zm4", "zm"], + ["zm6", "zm"], ["zmv", "zm"], ["zms", "zm"], ["zsv", "zs"], ] +if ANYWIN: + IMPLICATIONS.extend([["z", "zm4"]]) UNPLICATIONS = [["no_dav", "daw"]] @@ -363,6 +367,23 @@ class Daemon(threading.Thread): self.start() +class Netdev(object): + def __init__(self, ip: str, idx: int, name: str, desc: str): + self.ip = ip + self.idx = idx + self.name = name + self.desc = desc + + def __str__(self): + return "{}-{}{}".format(self.idx, self.name, self.desc) + + def __lt__(self, rhs): + return str(self) < str(rhs) + + def __eq__(self, rhs): + return str(self) == str(rhs) + + class Cooldown(object): def __init__(self, maxage: float) -> None: self.maxage = maxage @@ -434,7 +455,7 @@ class HLog(logging.Handler): class NetMap(object): - def __init__(self, ips: list[str], netdevs: dict[str, str]) -> None: + def __init__(self, ips: list[str], netdevs: dict[str, Netdev]) -> None: if "::" in ips: ips = [x for x in ips if x != "::"] + list( [x.split("/")[0] for x in netdevs if ":" in x] @@ -1791,11 +1812,14 @@ if not PY2 or not WINDOWS: else: # moonrunes become \x3f with bytestrings, # losing mojibake support is worth - def _not_actually_mbcs(txt: str) -> str: + def _not_actually_mbcs_enc(txt: str) -> bytes: return txt - fsenc = _not_actually_mbcs - fsdec = _not_actually_mbcs + def _not_actually_mbcs_dec(txt: bytes) -> str: + return txt + + fsenc = _not_actually_mbcs_enc + fsdec = _not_actually_mbcs_dec def s3enc(mem_cur: "sqlite3.Cursor", rd: str, fn: str) -> tuple[str, str]: diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css index 1f6ec451..a9563afe 100644 --- a/copyparty/web/splash.css +++ b/copyparty/web/splash.css @@ -109,6 +109,7 @@ pre, code { html.z pre, html.z code { color: #9e0; + background: #000; background: rgba(0,16,0,0.2); } .os { diff --git a/scripts/make-sfx.sh b/scripts/make-sfx.sh index 4d042d35..3e11e3fe 100755 --- a/scripts/make-sfx.sh +++ b/scripts/make-sfx.sh @@ -138,7 +138,7 @@ tmpdir="$( )" necho() { - printf '\033[G%s\033[K' "$*" + printf '\033[G%s ... \033[K' "$*" } [ $repack ] && { @@ -331,7 +331,6 @@ find -name py.typed -delete find -type f \( -name .DS_Store -or -name ._.DS_Store \) -delete find -type f -name ._\* | while IFS= read -r f; do cmp <(printf '\x00\x05\x16') <(head -c 3 -- "$f") && rm -f -- "$f"; done -echo use smol web deps rm -f copyparty/web/deps/*.full.* copyparty/web/dbg-* copyparty/web/Makefile find copyparty | LC_ALL=C sort | sed 's/\.gz$//;s/$/,/' > have @@ -466,7 +465,7 @@ zdir="$tmpdir/cpp-mk$CSN" [ -e "$zdir/$stamp" ] || rm -rf "$zdir" mkdir -p "$zdir" echo a > "$zdir/$stamp" -nf=$(ls -1 "$zdir"/arc.* | wc -l) +nf=$(ls -1 "$zdir"/arc.* 2>/dev/null | wc -l) [ $nf -ge 2 ] && [ ! $repack ] && use_zdir=1 || use_zdir= [ $use_zdir ] || { diff --git a/scripts/pyinstaller/build.sh b/scripts/pyinstaller/build.sh index 09e45450..df8cc4d8 100644 --- a/scripts/pyinstaller/build.sh +++ b/scripts/pyinstaller/build.sh @@ -3,6 +3,7 @@ set -e curl -k https://192.168.123.1:3923/cpp/scripts/pyinstaller/build.sh | tee build2.sh | cmp build.sh && rm build2.sh || { + [ -s build2.sh ] || exit 1 echo "new build script; upgrade y/n:" while true; do read -u1 -n1 -r r; [[ $r =~ [yYnN] ]] && break; done [[ $r =~ [yY] ]] && mv build{2,}.sh && exec ./build.sh