diff --git a/README.md b/README.md index 5b4affa0..c0a3febd 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,7 @@ recommended additional steps on debian which enable audio metadata and thumbnai ## features * backend stuff + * ☑ IPv6 * ☑ [multiprocessing](#performance) (actual multithreading) * ☑ volumes (mountpoints) * ☑ [accounts](#accounts-and-volumes) @@ -223,6 +224,9 @@ browser-specific: * Desktop-Firefox: ~~may use gigabytes of RAM if your files are massive~~ *seems to be OK now* * Desktop-Firefox: may stop you from deleting files you've uploaded until you visit `about:memory` and click `Minimize memory usage` +server-os-specific: +* RHEL8 / Rocky8: you can run copyparty using `/usr/libexec/platform-python` + # bugs @@ -756,7 +760,7 @@ some **BIG WARNINGS** specific to SMB/CIFS, in decreasing importance: and some minor issues, * clients only see the first ~400 files in big folders; [impacket#1433](https://github.com/SecureAuthCorp/impacket/issues/1433) * hot-reload of server config (`/?reload=cfg`) only works for volumes, not account passwords -* listens on the first `-i` interface only (default = 0.0.0.0 = all) +* listens on the first IPv4 `-i` interface only (default = :: = 0.0.0.0 = all) * login doesn't work on winxp, but anonymous access is ok -- remove all accounts from copyparty config for that to work * win10 onwards does not allow connecting anonymously / without accounts * on windows, creating a new file through rightclick --> new --> textfile throws an error due to impacket limitations -- hit OK and F5 to get your file diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 3a3c2e00..91562cef 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -613,7 +613,7 @@ def run_argparse( ap2.add_argument("--write-uplog", action="store_true", help="write POST reports to textfiles in working-directory") ap2 = ap.add_argument_group('network options') - ap2.add_argument("-i", metavar="IP", type=u, default="0.0.0.0", help="ip to bind (comma-sep.)") + ap2.add_argument("-i", metavar="IP", type=u, default="::", help="ip to bind (comma-sep.), default: all IPv4 and IPv6") ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)") ap2.add_argument("--rproxy", metavar="DEPTH", type=int, default=1, help="which ip to keep; [\033[32m0\033[0m]=tcp, [\033[32m1\033[0m]=origin (first x-fwd), [\033[32m2\033[0m]=cloudflare, [\033[32m3\033[0m]=nginx, [\033[32m-1\033[0m]=closest proxy") ap2.add_argument("--s-wr-sz", metavar="B", type=int, default=256*1024, help="socket write size in bytes") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index c0c386d4..b3af9a12 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -1366,7 +1366,7 @@ class HttpCli(object): url = "{}://{}/{}".format( "https" if self.is_https else "http", - self.headers.get("host") or "{}:{}".format(*list(self.s.getsockname())), + self.headers.get("host") or "{}:{}".format(*list(self.s.getsockname()[:2])), vpath + vsuf, ) @@ -1386,7 +1386,7 @@ class HttpCli(object): else: t = "{}\n{}\n{}\n{}\n".format(post_sz, sha_b64, sha_hex[:56], url) - h = {"Location": url} if is_put else {} + h = {"Location": url} if is_put and url else {} self.reply(t.encode("utf-8"), 201, headers=h) return True @@ -2037,7 +2037,7 @@ class HttpCli(object): "url": "{}://{}/{}".format( "https" if self.is_https else "http", self.headers.get("host") - or "{}:{}".format(*list(self.s.getsockname())), + or "{}:{}".format(*list(self.s.getsockname()[:2])), rel_url, ), "sha512": sha_hex[:56], diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index 14bf26b5..ffb8c7d6 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -174,7 +174,7 @@ class HttpSrv(object): sck.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sck.settimeout(None) # < does not inherit, ^ does - ip, port = sck.getsockname() + ip, port = sck.getsockname()[:2] self.srvs.append(sck) self.nclimax = math.ceil(self.args.nc * 1.0 / nlisteners) Daemon( @@ -185,9 +185,10 @@ class HttpSrv(object): def thr_listen(self, srv_sck: socket.socket) -> None: """listens on a shared tcp server""" - ip, port = srv_sck.getsockname() + ip, port = srv_sck.getsockname()[:2] fno = srv_sck.fileno() - msg = "subscribed @ {}:{} f{} p{}".format(ip, port, fno, os.getpid()) + hip = "[{}]".format(ip) if ":" in ip else ip + msg = "subscribed @ {}:{} f{} p{}".format(hip, port, fno, os.getpid()) self.log(self.name, msg) def fun() -> None: @@ -261,7 +262,12 @@ class HttpSrv(object): self.log(self.name, "|%sC-acc1" % ("-" * 2,), c="90") try: - sck, addr = srv_sck.accept() + sck, saddr = srv_sck.accept() + cip, cport = saddr[:2] + if cip.startswith("::ffff:"): + cip = cip[7:] + + addr = (cip, cport) except (OSError, socket.error) as ex: self.log(self.name, "accept({}): {}".format(fno, ex), c=6) time.sleep(0.02) diff --git a/copyparty/smbd.py b/copyparty/smbd.py index 4e6f8746..2a07ed85 100644 --- a/copyparty/smbd.py +++ b/copyparty/smbd.py @@ -75,7 +75,11 @@ class SMB(object): smbserver.isInFileJail = self._is_in_file_jail self._disarm() - ip = self.args.i[0] + ip = next((x for x in self.args.i if ":" not in x), None) + if not ip: + self.log("smb", "IPv6 not supported for SMB; listening on 0.0.0.0", 3) + ip = "0.0.0.0" + port = int(self.args.smb_port) srv = smbserver.SimpleSMBServer(listenAddress=ip, listenPort=port) diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index e121226c..ae85e4d1 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -14,6 +14,7 @@ from .util import ( E_ADDR_NOT_AVAIL, E_UNREACH, chkcmd, + min_ex, sunpack, termsize, ) @@ -43,25 +44,57 @@ class TcpSrv(object): self.srv: list[socket.socket] = [] self.nsrv = 0 self.qr = "" + pad = False ok: dict[str, list[int]] = {} for ip in self.args.i: - ok[ip] = [] + if ip == "::": + if socket.has_ipv6: + ips = ["::", "0.0.0.0"] + dual = True + else: + ips = ["0.0.0.0"] + dual = False + else: + ips = [ip] + dual = False + + for ipa in ips: + ok[ipa] = [] + for port in self.args.p: - self.nsrv += 1 + successful_binds = 0 try: - self._listen(ip, port) - ok[ip].append(port) + for ipa in ips: + try: + self._listen(ipa, port) + ok[ipa].append(port) + successful_binds += 1 + except: + if dual and ":" in ipa: + t = "listen on IPv6 [{}] failed; trying IPv4 {}...\n{}" + self.log("tcpsrv", t.format(ipa, ips[1], min_ex()), 3) + pad = True + continue + + # binding 0.0.0.0 after :: fails on dualstack + # but is necessary on non-dualstakc + if successful_binds: + continue + + raise + except Exception as ex: if self.args.ign_ebind or self.args.ign_ebind_all: t = "could not listen on {}:{}: {}" self.log("tcpsrv", t.format(ip, port, ex), c=3) + pad = True else: raise if not self.srv and not self.args.ign_ebind_all: raise Exception("could not listen on any of the given interfaces") - if self.nsrv != len(self.srv): + if pad: self.log("tcpsrv", "") ip = "127.0.0.1" @@ -81,7 +114,11 @@ class TcpSrv(object): t = "available @ {}://{}:{}/ (\033[33m{}\033[0m)" for ip, desc in sorted(eps.items(), key=lambda x: x[1]): for port in sorted(self.args.p): - if port not in ok.get(ip, ok.get("0.0.0.0", [])): + if ( + port not in ok.get(ip, []) + and port not in ok.get("::", []) + and port not in ok.get("0.0.0.0", []) + ): continue proto = " http" @@ -90,7 +127,8 @@ class TcpSrv(object): elif self.args.https_only or port == 443: proto = "https" - msgs.append(t.format(proto, ip, port, desc)) + hip = "[{}]".format(ip) if ":" in ip else ip + msgs.append(t.format(proto, hip, port, desc)) is_ext = "external" in unicode(desc) qrt = qr1 if is_ext else qr2 @@ -125,18 +163,20 @@ class TcpSrv(object): title_tab[tk] = {tv: 1} if msgs: - msgs[-1] += "\n" for t in msgs: self.log("tcpsrv", t) if self.args.wintitle: self._set_wintitle(title_tab) + else: + print("\n", end="") if self.args.qr or self.args.qrs: self.qr = self._qr(qr1, qr2) def _listen(self, ip: str, port: int) -> None: - srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ipv = socket.AF_INET6 if ":" in ip else socket.AF_INET + srv = socket.socket(ipv, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) srv.settimeout(None) # < does not inherit, ^ does @@ -153,17 +193,40 @@ class TcpSrv(object): raise Exception(e) def run(self) -> None: + all_eps = [x.getsockname()[:2] for x in self.srv] + bound = [] + srvs = [] for srv in self.srv: - srv.listen(self.args.nc) - ip, port = srv.getsockname() + ip, port = srv.getsockname()[:2] + try: + srv.listen(self.args.nc) + except: + if ip == "0.0.0.0" and ("::", port) in bound: + # dualstack + srv.close() + continue + + if ip == "::" and ("0.0.0.0", port) in all_eps: + # no ipv6 + srv.close() + continue + + raise + + bound.append((ip, port)) + srvs.append(srv) fno = srv.fileno() - msg = "listening @ {}:{} f{} p{}".format(ip, port, fno, os.getpid()) + hip = "[{}]".format(ip) if ":" in ip else ip + msg = "listening @ {}:{} f{} p{}".format(hip, port, fno, os.getpid()) self.log("tcpsrv", msg) if self.args.q: print(msg) self.hub.broker.say("listen", srv) + self.srv = srvs + self.nsrv = len(srvs) + def shutdown(self) -> None: self.stopping = True try: @@ -209,19 +272,23 @@ class TcpSrv(object): except: return self.ips_linux_ifconfig() - r = re.compile(r"^\s+inet ([^ ]+)/.* (.*)") - ri = re.compile(r"^\s*[0-9]+\s*:.*") + 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"): - if ri.match(ln): + m = ri.match(ln) + if m: + dev = m.group(1) up = "UP" in re.split("[>,< ]", ln) - try: - ip, dev = r.match(ln.rstrip()).groups() # type: ignore - eps[ip] = dev + ("" if up else ", \033[31mLINK-DOWN") - except: - pass + 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 @@ -314,7 +381,7 @@ class TcpSrv(object): else: eps = self.ips_linux() - if "0.0.0.0" not in listen_ips: + 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 in listen_ips} try: @@ -323,14 +390,15 @@ class TcpSrv(object): if not ext_ips: raise Exception() except: - ext_ips = [self._defroute()] + rt = self._defroute() + ext_ips = [rt] if rt else [] for lip in listen_ips: - if not ext_ips or lip not in ["0.0.0.0"] + ext_ips: + if not ext_ips or lip not in ["0.0.0.0", "::"] + ext_ips: continue desc = "\033[32mexternal" - ips = ext_ips if lip == "0.0.0.0" else [lip] + ips = ext_ips if lip in ["0.0.0.0", "::"] else [lip] for ip in ips: try: if "external" not in eps[ip]: @@ -422,6 +490,9 @@ class TcpSrv(object): if not ip: return "" + if ":" in ip: + ip = "[{}]".format(ip) + if self.args.http_only: https = "" elif self.args.https_only: