diff --git a/copyparty/__main__.py b/copyparty/__main__.py index c5b5e52c..cff92ab0 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -847,6 +847,7 @@ def run_argparse( ap2.add_argument("--no-scandir", action="store_true", help="disable scandir; instead using listdir + stat on each file") ap2.add_argument("--no-fastboot", action="store_true", help="wait for up2k indexing before starting the httpd") ap2.add_argument("--no-htp", action="store_true", help="disable httpserver threadpool, create threads as-needed instead") + ap2.add_argument("--rclone-mdns", action="store_true", help="use mdns-domain instead of server-ip on /?hc") ap2.add_argument("--stackmon", metavar="P,S", type=u, help="write stacktrace to Path every S second, for example --stackmon=\033[32m./st/%%Y-%%m/%%d/%%H%%M.xz,60") ap2.add_argument("--log-thrs", metavar="SEC", type=float, help="list active threads every SEC") ap2.add_argument("--log-fk", metavar="REGEX", type=u, default="", help="log filekey params for files where path matches REGEX; [\033[32m.\033[0m] (a single dot) = all files") diff --git a/copyparty/broker_mp.py b/copyparty/broker_mp.py index b0bf9883..cb8204ad 100644 --- a/copyparty/broker_mp.py +++ b/copyparty/broker_mp.py @@ -113,6 +113,10 @@ class BrokerMp(object): for p in self.procs: p.q_pend.put((0, dest, [args[0], len(self.procs)])) + elif dest == "set_netdevs": + for p in self.procs: + p.q_pend.put((0, dest, list(args))) + elif dest == "cb_httpsrv_up": self.hub.cb_httpsrv_up() diff --git a/copyparty/broker_mpw.py b/copyparty/broker_mpw.py index 41c6ca7c..1a899e87 100644 --- a/copyparty/broker_mpw.py +++ b/copyparty/broker_mpw.py @@ -97,6 +97,9 @@ class MpWorker(BrokerCli): elif dest == "listen": self.httpsrv.listen(args[0], args[1]) + elif dest == "set_netdevs": + self.httpsrv.set_netdevs(args[0]) + elif dest == "retq": # response from previous ipc call with self.retpend_mutex: diff --git a/copyparty/broker_thr.py b/copyparty/broker_thr.py index 515671c6..e40cd38d 100644 --- a/copyparty/broker_thr.py +++ b/copyparty/broker_thr.py @@ -61,6 +61,10 @@ class BrokerThr(BrokerCli): self.httpsrv.listen(args[0], 1) return + if dest == "set_netdevs": + self.httpsrv.set_netdevs(args[0]) + return + # new ipc invoking managed service in hub obj = self.hub for node in dest.split("."): diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 26bc9809..459bcecf 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -691,6 +691,9 @@ class HttpCli(object): if "reset" in self.uparam: return self.set_cfg_reset() + if "hc" in self.uparam: + return self.tx_svcs() + if "h" in self.uparam: return self.tx_mounts() @@ -2562,6 +2565,32 @@ class HttpCli(object): return True + def tx_svcs(self) -> bool: + aname = re.sub("[^0-9a-zA-Z]+", "", self.args.name) or "a" + ep = self.headers["host"] + host = ep.split(":")[0] + hport = ep[ep.find(":") :] if ":" in ep else "" + rip = ( + host + if self.args.rclone_mdns or not self.args.zm + else self.conn.hsrv.nm.map(self.ip) or host + ) + html = self.j2s( + "svcs", + args=self.args, + accs=bool(self.asrv.acct), + s="s" if self.is_https else "", + rip=rip, + ep=ep, + vp=(self.uparam["hc"] or "").lstrip("/"), + host=host, + hport=hport, + aname=aname, + pw=self.pw or "pw", + ) + self.reply(html.encode("utf-8")) + return True + def tx_mounts(self) -> bool: suf = self.urlq({}, ["h"]) avol = [x for x in self.wvol if x in self.rvol] diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index fb3eb0c8..60835d6e 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -37,6 +37,7 @@ from .util import ( Daemon, Garda, Magician, + NetMap, ipnorm, min_ex, shut_socket, @@ -72,6 +73,7 @@ class HttpSrv(object): nsuf = "-n{}-i{:x}".format(nid, os.getpid()) if nid else "" self.magician = Magician() + self.nm = NetMap([], {}) self.ssdp: Optional["SSDPr"] = None self.gpwd = Garda(self.args.ban_pw) self.g404 = Garda(self.args.ban_404) @@ -102,10 +104,8 @@ class HttpSrv(object): env = jinja2.Environment() env.loader = jinja2.FileSystemLoader(os.path.join(self.E.mod, "web")) - self.j2 = { - x: env.get_template(x + ".html") - for x in ["splash", "browser", "browser2", "msg", "md", "mde", "cf"] - } + jn = ["splash", "svcs", "browser", "browser2", "msg", "md", "mde", "cf"] + self.j2 = {x: env.get_template(x + ".html") for x in jn} zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz") self.prism = os.path.exists(zs) @@ -140,6 +140,9 @@ class HttpSrv(object): except: pass + def set_netdevs(self, netdevs: dict[str, str]) -> None: + self.nm = NetMap([self.ip], netdevs) + def start_threads(self, n: int) -> None: self.tp_nthr += n if self.args.log_htp: diff --git a/copyparty/mdns.py b/copyparty/mdns.py index 64dd97e6..15b50c28 100644 --- a/copyparty/mdns.py +++ b/copyparty/mdns.py @@ -257,7 +257,12 @@ class MDNS(MCast): self.log("sendto failed: {} ({})".format(srv.ip, ex), "90") def run(self) -> None: - bound = self.create_servers() + try: + bound = self.create_servers() + except: + self.log("no server IP matches the mdns config", 1) + bound = [] + if not bound: self.log("failed to announce copyparty services on the network", 3) return diff --git a/copyparty/multicast.py b/copyparty/multicast.py index 2d121d65..f61a211b 100644 --- a/copyparty/multicast.py +++ b/copyparty/multicast.py @@ -20,6 +20,10 @@ if not hasattr(socket, "IPPROTO_IPV6"): setattr(socket, "IPPROTO_IPV6", 41) +class NoIPs(Exception): + pass + + class MC_Sck(object): """there is one socket for each server ip""" @@ -114,7 +118,7 @@ class MCast(object): ips = [x for x in ips if ":" not in x or x.startswith("fe80")] if not ips: - raise Exception("no server IP matches the mdns config") + raise NoIPs() for ip in ips: v6 = ":" in ip @@ -302,8 +306,8 @@ class MCast(object): t = "could not map client {} to known subnet; maybe forwarded from another network?" self.log(t.format(cip), 3) - self.cscache[cip] = ret if len(self.cscache) > 9000: self.cscache = {} + self.cscache[cip] = ret return ret diff --git a/copyparty/ssdp.py b/copyparty/ssdp.py index c24a218c..0fa7511f 100644 --- a/copyparty/ssdp.py +++ b/copyparty/ssdp.py @@ -99,7 +99,12 @@ class SSDPd(MCast): self.log_func("SSDP", msg, c) def run(self) -> None: - bound = self.create_servers() + try: + bound = self.create_servers() + except: + self.log("no server IP matches the ssdp config", 1) + bound = [] + if not bound: self.log("failed to announce copyparty services on the network", 3) return diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index 77d50767..1188e9aa 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -252,6 +252,7 @@ class TcpSrv(object): self.srv = srvs self.bound = bound self.nsrv = len(srvs) + self.hub.broker.say("set_netdevs", self.netdevs) def shutdown(self) -> None: self.stopping = True diff --git a/copyparty/util.py b/copyparty/util.py index 6ec54fe7..eafd3e12 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -26,10 +26,10 @@ from collections import Counter from datetime import datetime from email.utils import formatdate -from ipaddress import IPv6Address +from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network from queue import Queue -from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS, unicode +from .__init__ import ANYWIN, MACOS, PY2, TYPE_CHECKING, VT100, WINDOWS from .__version__ import S_BUILD_DT, S_VERSION from .stolen import surrogateescape @@ -95,6 +95,8 @@ if TYPE_CHECKING: from .authsrv import VFS +FAKE_MP = False + try: import multiprocessing as mp @@ -429,6 +431,52 @@ class HLog(logging.Handler): self.log_func(record.name[-21:], msg, c) +class NetMap(object): + def __init__(self, ips: list[str], netdevs: dict[str, str]) -> None: + if "::" in ips: + ips = [x for x in ips if x != "::"] + list( + [x.split("/")[0] for x in netdevs if ":" in x] + ) + ips.append("0.0.0.0") + + if "0.0.0.0" in ips: + ips = [x for x in ips if x != "0.0.0.0"] + list( + [x.split("/")[0] for x in netdevs if ":" not in x] + ) + + ips = [x for x in ips if x not in ("::1", "127.0.0.1")] + ips = [[x for x in netdevs if x.startswith(y + "/")][0] for y in ips] + + self.cache: dict[str, str] = {} + self.b2sip: dict[bytes, str] = {} + self.b2net: dict[bytes, Union[IPv4Network, IPv6Network]] = {} + self.bip: list[bytes] = [] + for ip in ips: + v6 = ":" in ip + fam = socket.AF_INET6 if v6 else socket.AF_INET + bip = socket.inet_pton(fam, ip.split("/")[0]) + self.bip.append(bip) + self.b2sip[bip] = ip.split('/')[0] + self.b2net[bip] = (IPv6Network if v6 else IPv4Network)(ip, False) + + self.bip.sort(reverse=True) + + def map(self, ip: str) -> str: + try: + return self.cache[ip] + except: + pass + + v6 = ":" in ip + ci = IPv6Address(ip) if v6 else IPv4Address(ip) + bip = next((x for x in self.bip if ci in self.b2net[x]), None) + ret = self.b2sip[bip] if bip else "" + if len(self.cache) > 9000: + self.cache = {} + self.cache[ip] = ret + return ret + + class UnrecvEOF(OSError): pass diff --git a/copyparty/web/a/up2k.py b/copyparty/web/a/up2k.py new file mode 120000 index 00000000..90ad442e --- /dev/null +++ b/copyparty/web/a/up2k.py @@ -0,0 +1 @@ +../../../bin/up2k.py \ No newline at end of file diff --git a/copyparty/web/a/webdav-cfg.bat b/copyparty/web/a/webdav-cfg.bat new file mode 120000 index 00000000..63c3b9c6 --- /dev/null +++ b/copyparty/web/a/webdav-cfg.bat @@ -0,0 +1 @@ +../../../contrib/webdav-cfg.bat \ No newline at end of file diff --git a/copyparty/web/splash.css b/copyparty/web/splash.css index 9794bf4d..bbd557c2 100644 --- a/copyparty/web/splash.css +++ b/copyparty/web/splash.css @@ -10,6 +10,9 @@ html { padding: 0 1em 3em 1em; line-height: 1.3em; } +#wrap.w { + max-width: 96%; +} h1 { border-bottom: 1px solid #ccc; margin: 2em 0 .4em 0; @@ -25,15 +28,16 @@ a { text-decoration: none; border-bottom: 1px solid #8ab; border-radius: .2em; - padding: .2em .8em; + padding: .2em .6em; + margin: 0 .3em; } -a+a { - margin-left: .5em; +td a { + margin: 0; } -.refresh, +.af, .logout { float: right; - margin: -.2em 0 0 .5em; + margin: -.2em 0 0 .8em; } .logout, a.r { @@ -64,9 +68,15 @@ table { .num td:first-child { text-align: right; } +.c { + text-align: center; +} .btns { margin: 1em 0; } +.btns>a:first-child { + margin-left: 0; +} #msg { margin: 3em 0; } @@ -83,6 +93,41 @@ blockquote { border-left: .3em solid rgba(128,128,128,0.5); border-radius: 0 0 0 .25em; } +pre, code { + color: #480; + background: #fff; + font-family: 'scp', monospace, monospace; + border: 1px solid rgba(128,128,128,0.3); + border-radius: .2em; + padding: .15em .2em; +} +html.z pre, +html.z code { + color: #9e0; + background: rgba(0,16,0,0.2); +} +.ossel a.r { + box-shadow: 0 .3em 1em #c04; +} +.os { + line-height: 1.5em; +} +.sph { + margin-top: 4em; +} +.sph code { + margin-left: .3em; +} +pre b, +code b { + color: #000; + font-weight: normal; + text-shadow: 0 0 .2em #0f0; +} +html.z pre b, +html.z code b { + color: #fff; +} html.z { @@ -102,6 +147,9 @@ html.z a.r { background: #804; border-color: #c28; } +html.z .ossel a.r { + box-shadow: 0 .3em 1em #704, 0 .3em 1em #704; +} html.z input { color: #fff; background: #626; diff --git a/copyparty/web/splash.html b/copyparty/web/splash.html index b003b454..67205b2c 100644 --- a/copyparty/web/splash.html +++ b/copyparty/web/splash.html @@ -13,7 +13,8 @@
howdy stranger (you're not logged in)
diff --git a/copyparty/web/splash.js b/copyparty/web/splash.js index fad3292c..7bb9037c 100644 --- a/copyparty/web/splash.js +++ b/copyparty/web/splash.js @@ -24,11 +24,14 @@ var Ls = { ".s1": "kartlegg", "t1": "handling", "u2": "tid siden noen sist skrev til serveren$N( opplastning / navneendring / ... )$N$N17d = 17 dager$N1h23 = 1 time 23 minutter$N4m56 = 4 minuter 56 sekunder", + "v1": "koble til", + "v2": "bruk denne serveren som en lokal harddisk$N$NADVARSEL: kommer til å vise passordet ditt!" }, "eng": { "d2": "shows the state of all active threads", "e2": "reload config files (accounts/volumes/volflags),$Nand rescan all e2ds volumes", "u2": "time since the last server write$N( upload / rename / ... )$N$N17d = 17 days$N1h23 = 1 hour 23 minutes$N4m56 = 4 minutes 56 seconds", + "v2": "use this server as a local HDD$N$NWARNING: this will show your password!", } }, d = Ls[sread("lang") || lang]; diff --git a/copyparty/web/svcs.html b/copyparty/web/svcs.html new file mode 100644 index 00000000..02ec73bc --- /dev/null +++ b/copyparty/web/svcs.html @@ -0,0 +1,199 @@ + + + + + +or choose your OS for cooler alternatives:
+ +
+ make this server appear on your computer as a regular HDD!
+ pick your favorite below (sorted by performance, best first) and lets 🎉
+
+ placeholders:
+
+ {% if accs %}{{ pw }}
=password, {% endif %}W:
=mountpoint
+
+
+ {% if accs %}{{ pw }}
=password, {% endif %}mp
=mountpoint
+
+
note: rclone-FTP is a bit faster, so {% if args.ftp or args.ftps %}try that first{% else %}consider enabling FTP in server settings{% endif %}
+if you can, install winfsp+rclone and then paste this in cmd:
++ rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=other{% if accs %} user=k pass={{ pw }}{% endif %} + rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ vp }} W: ++ {% if s %} +
note: if you are on LAN (or just dont have valid certificates), add --no-check-certificate
to the mount command
---
if you want to use the native WebDAV client in windows instead (slow and buggy), first run webdav-cfg.bat to remove the 47 MiB filesize limit (also fixes password login), then connect:
++ net use w: http{{ s }}://{{ ep }}/{{ vp }}{% if accs %} k /user:{{ pw }}{% endif %} ++
+ yum install davfs2 + {% if accs %}printf '%s\n' {{ pw }} k | {% endif %}mount -t davfs -ouid=1000 http{{ s }}://{{ ep }}/{{ vp }} mp ++
or you can use rclone instead, which is much slower but doesn't require root:
++ rclone config create {{ aname }}-dav webdav url=http{{ s }}://{{ rip }}{{ hport }} vendor=other{% if accs %} user=k pass={{ pw }}{% endif %} + rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-dav:{{ vp }} mp ++ {% if s %} +
note: if you are on LAN (or just dont have valid certificates), add --no-check-certificate
to the mount command
---
or the emergency alternative (gnome/gui-only):
+ ++ {%- if accs %} + echo {{ pw }} | gio mount dav{{ s }}://k@{{ ep }}/{{ vp }} + {%- else %} + gio mount -a dav{{ s }}://{{ ep }}/{{ vp }} + {%- endif %} ++
+ osascript -e ' mount volume "http{{ s }}://k:{{ pw }}@{{ ep }}/{{ vp }}" ' ++
or you can open up a Finder, press command-K and paste this instead:
++ http{{ s }}://k:{{ pw }}@{{ ep }}/{{ vp }} ++ + {% if s %} +
replace https
with http
if it doesn't work
if you can, install winfsp+rclone and then paste this in cmd:
++ rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp or args.ftps }} pass=k user={% if accs %}{{ pw }}{% else %}anonymous{% endif %} tls={{ "false" if args.ftp else "true" }} + rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ vp }} W: ++
if you want to use the native FTP client in windows instead (please dont), press win+R
and run this command:
+ explorer {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}{{ pw }}:k@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }} ++
+ rclone config create {{ aname }}-ftp ftp host={{ rip }} port={{ args.ftp or args.ftps }} pass=k user={% if accs %}{{ pw }}{% else %}anonymous{% endif %} tls={{ "false" if args.ftp else "true" }} + rclone mount --vfs-cache-mode writes --dir-cache-time 5s {{ aname }}-ftp:{{ vp }} mp ++
emergency alternative (gnome/gui-only):
+ ++ {%- if accs %} + echo {{ pw }} | gio mount ftp{{ "" if args.ftp else "s" }}://k@{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }} + {%- else %} + gio mount -a ftp{{ "" if args.ftp else "s" }}://{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }} + {%- endif %} ++
note: FTP is read-only on macos; please use WebDAV instead
++ open {{ "ftp" if args.ftp else "ftps" }}://{% if accs %}k:{{ pw }}@{% else %}anonymous:@{% endif %}{{ host }}:{{ args.ftp or args.ftps }}/{{ vp }} ++
+ partyfuse.py -- fast, read-only, + needs winfsp + doesn't need root +
++ partyfuse.py{% if accs %} -a {{ pw }}{% endif %} http{{ s }}://{{ ep }}/{{ vp }} W:mp ++ {% if s %} +
note: if you are on LAN (or just dont have valid certificates), add -td
+ you can use up2k.py to upload (sometimes faster than web-browsers) +
+ + + {% if args.smb %} ++ net use w: \\{{ host }}\a{% if accs %} k /user:{{ pw }}{% endif %} ++ +
+ mount -t cifs -o{% if accs %}user={{ pw }},pass=k,{% endif %}vers={{ 1 if args.smb1 else 2 }}.0,port={{ args.smb_port }},uid=1000 //{{ host }}/a/ mp ++ +
+ @@@ net use w: \\{{ host }}\a k /user:{{ pw }} ++ {% endif %} + + + +