diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 7bfaa7a6..479d569d 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -673,6 +673,7 @@ def run_argparse( ap2.add_argument("-p", metavar="PORT", type=u, default="3923", help="ports to bind (comma/range)") ap2.add_argument("--ll", action="store_true", help="include link-local IPv4/IPv6 even if the NIC has routable IPs (breaks some mdns clients)") 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("--webroot", metavar="PATH", type=u, default="", help="if reverse-proxying on a location instead of a dedicated subdomain, provide the location here") if ANYWIN: ap2.add_argument("--reuseaddr", action="store_true", help="set reuseaddr on listening sockets on windows; allows rapid restart of copyparty at the expense of being able to accidentally start multiple instances") 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 40da9a2c..9083277e 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -119,6 +119,8 @@ class HttpCli(object): # placeholders; assigned by run() self.keepalive = False self.is_https = False + self.is_proxied = False + self.is_vproxied = False self.in_hdr_recv = True self.headers: dict[str, str] = {} self.mode = " " @@ -191,6 +193,7 @@ class HttpCli(object): def j2s(self, name: str, **ka: Any) -> str: tpl = self.conn.hsrv.j2[name] + ka["r"] = self.args.SR if self.is_vproxied else "" ka["ts"] = self.conn.hsrv.cachebuster() ka["lang"] = self.args.lang ka["favico"] = self.args.favico @@ -276,6 +279,8 @@ class HttpCli(object): self.log(t.format(self.args.rproxy, zso), c=3) self.log_src = self.conn.set_rproxy(self.ip) + self.is_vproxied = bool(self.args.R) + self.is_proxied = True if self.is_banned(): return False @@ -320,6 +325,13 @@ class HttpCli(object): else: uparam[k.lower()] = "" + if self.is_vproxied: + if vpath.startswith(self.args.R): + vpath = vpath[len(self.args.R) + 1 :] + else: + t = "incorrect --webroot or webserver config; expected vpath starting with [{}] but got [{}]" + self.log(t.format(self.args.R, vpath), 1) + self.ouparam = {k: zs for k, zs in uparam.items()} if self.args.rsp_slp: @@ -604,10 +616,11 @@ class HttpCli(object): status: int = 200, use302: bool = False, ) -> bool: + vp = self.args.RS + vpath html = self.j2s( "msg", h2='{} /{}'.format( - quotep(vpath) + suf, flavor, html_escape(vpath, crlf=True) + suf + quotep(vp) + suf, flavor, html_escape(vp, crlf=True) + suf ), pre=msg, click=click, @@ -1375,7 +1388,7 @@ class HttpCli(object): url = "{}://{}/{}".format( "https" if self.is_https else "http", self.headers.get("host") or "{}:{}".format(*list(self.s.getsockname()[:2])), - vpath + vsuf, + self.args.RS + vpath + vsuf, ) return post_sz, sha_hex, sha_b64, remains, path, url @@ -1576,6 +1589,10 @@ class HttpCli(object): x = self.conn.hsrv.broker.ask("up2k.handle_json", body) ret = x.get() + if self.is_vproxied: + if "purl" in ret: + ret["purl"] = self.args.SR + ret["purl"] + ret = json.dumps(ret) self.log(ret) self.reply(ret.encode("utf-8"), mime="application/json") @@ -1634,6 +1651,10 @@ class HttpCli(object): if t not in order: order.append(t) + if self.is_vproxied: + for hit in hits: + hit["rp"] = self.args.RS + hit["rp"] + r = json.dumps({"hits": hits, "tag_order": order}).encode("utf-8") self.reply(r, mime="application/json") return True @@ -2032,7 +2053,7 @@ class HttpCli(object): )[: vfs.flags["fk"]] vpath = "{}/{}".format(upload_vpath, lfn).strip("/") - rel_url = quotep(vpath) + vsuf + rel_url = quotep(self.args.RS + vpath) + vsuf msg += 'sha512: {} // {} // {} bytes // {} {}\n'.format( sha_hex[:56], sha_b64, @@ -2537,6 +2558,7 @@ class HttpCli(object): boundary = "\roll\tide" targs = { + "r": self.args.SR if self.is_vproxied else "", "ts": self.conn.hsrv.cachebuster(), "svcname": self.args.doctitle, "html_head": self.html_head, @@ -2765,6 +2787,11 @@ class HttpCli(object): dst = dst[len(top) + 1 :] ret = self.gen_tree(top, dst) + if self.is_vproxied: + parents = self.args.R.split("/") + for parent in parents[::-1]: + ret = {"k{}".format(parent): ret, "a": []} + zs = json.dumps(ret) self.reply(zs.encode("utf-8"), mime="application/json") return True @@ -2875,6 +2902,11 @@ class HttpCli(object): break ret = ret[:2000] + + if self.is_vproxied: + for v in ret: + v["vp"] = self.args.SR + v["vp"] + jtxt = json.dumps(ret, indent=2, sort_keys=True).encode("utf-8", "replace") self.log("{} #{} {:.2f}sec".format(lm, len(ret), time.time() - t0)) self.reply(jtxt, mime="application/json") @@ -2889,6 +2921,8 @@ class HttpCli(object): if not req: req = [self.vpath] + elif self.is_vproxied: + req = [x[len(self.args.SR) :] for x in req] nlim = int(self.uparam.get("lim") or 0) lim = [nlim, nlim] if nlim else [] @@ -2900,6 +2934,10 @@ class HttpCli(object): def handle_mv(self) -> bool: # full path of new loc (incl filename) dst = self.uparam.get("move") + + if self.is_vproxied and dst and dst.startswith(self.args.SR): + dst = dst[len(self.args.RS) :] + if not dst: raise Pebkac(400, "need dst vpath") diff --git a/copyparty/httpsrv.py b/copyparty/httpsrv.py index 75a8b9af..9106b532 100644 --- a/copyparty/httpsrv.py +++ b/copyparty/httpsrv.py @@ -178,11 +178,6 @@ class HttpSrv(object): def listen(self, sck: socket.socket, nlisteners: int) -> None: if self.args.j != 1: # lost in the pickle; redefine - try: - sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except: - pass - if not ANYWIN or self.args.reuseaddr: sck.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) diff --git a/copyparty/star.py b/copyparty/star.py index 1bb4618b..dc4fc120 100644 --- a/copyparty/star.py +++ b/copyparty/star.py @@ -1,6 +1,7 @@ # coding: utf-8 from __future__ import print_function, unicode_literals +import stat import tarfile from queue import Queue @@ -79,6 +80,9 @@ class StreamTar(StreamArc): src = f["ap"] fsi = f["st"] + if stat.S_ISDIR(fsi.st_mode): + return + inf = tarfile.TarInfo(name=name) inf.mode = fsi.st_mode inf.size = fsi.st_size diff --git a/copyparty/svchub.py b/copyparty/svchub.py index a031e7a3..b2663925 100644 --- a/copyparty/svchub.py +++ b/copyparty/svchub.py @@ -301,6 +301,17 @@ class SvcHub(object): vs = [x for x in vs if x] setattr(al, n, vs) + R = al.webroot + if "//" in R or ":" in R: + t = "found URL in --webroot; it should be just the location, for example /foo/bar" + raise Exception(t) + + R = R.strip("/") + if R: + al.R = R + al.SR = "/" + R + al.RS = R + "/" + return True def _setlimits(self) -> None: diff --git a/copyparty/szip.py b/copyparty/szip.py index 4207d682..86615cf7 100644 --- a/copyparty/szip.py +++ b/copyparty/szip.py @@ -3,6 +3,7 @@ from __future__ import print_function, unicode_literals import calendar import time +import stat import zlib from .bos import bos @@ -238,6 +239,9 @@ class StreamZip(StreamArc): src = f["ap"] st = f["st"] + if stat.S_ISDIR(st.st_mode): + return + sz = st.st_size ts = st.st_mtime diff --git a/copyparty/tcpsrv.py b/copyparty/tcpsrv.py index f813eb0b..122ee6a8 100644 --- a/copyparty/tcpsrv.py +++ b/copyparty/tcpsrv.py @@ -209,10 +209,6 @@ class TcpSrv(object): def _listen(self, ip: str, port: int) -> None: ipv = socket.AF_INET6 if ":" in ip else socket.AF_INET srv = socket.socket(ipv, socket.SOCK_STREAM) - try: - srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) - except: - pass if not ANYWIN or self.args.reuseaddr: srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index 029ce581..b77395a2 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1075,18 +1075,18 @@ html.y #widget.open { top: -.12em; } #wtico { - cursor: url(/.cpr/dd/4.png), pointer; + cursor: url(dd/4.png), pointer; animation: cursor 500ms; } #wtico:hover { animation: cursor 500ms infinite; } @keyframes cursor { - 0% {cursor: url(/.cpr/dd/2.png), pointer} - 30% {cursor: url(/.cpr/dd/3.png), pointer} - 50% {cursor: url(/.cpr/dd/4.png), pointer} - 75% {cursor: url(/.cpr/dd/5.png), pointer} - 85% {cursor: url(/.cpr/dd/4.png), pointer} + 0% {cursor: url(dd/2.png), pointer} + 30% {cursor: url(dd/3.png), pointer} + 50% {cursor: url(dd/4.png), pointer} + 75% {cursor: url(dd/5.png), pointer} + 85% {cursor: url(dd/4.png), pointer} } @keyframes spin { 100% {transform: rotate(360deg)} diff --git a/copyparty/web/browser.html b/copyparty/web/browser.html index b5662e45..7c2148c6 100644 --- a/copyparty/web/browser.html +++ b/copyparty/web/browser.html @@ -8,8 +8,8 @@ {{ html_head }} - - + + {%- if css %} {%- endif %} @@ -71,7 +71,7 @@