mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 00:52:16 -06:00
404/403 can be handled with plugins
This commit is contained in:
parent
8d248333e8
commit
5d8cb34885
|
@ -66,6 +66,7 @@ turn almost any device into a file server with resumable uploads/downloads using
|
||||||
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
* [file parser plugins](#file-parser-plugins) - provide custom parsers to index additional tags
|
||||||
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
|
* [event hooks](#event-hooks) - trigger a program on uploads, renames etc ([examples](./bin/hooks/))
|
||||||
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
* [upload events](#upload-events) - the older, more powerful approach ([examples](./bin/mtag/))
|
||||||
|
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
|
||||||
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
|
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
|
||||||
* [themes](#themes)
|
* [themes](#themes)
|
||||||
* [complete examples](#complete-examples)
|
* [complete examples](#complete-examples)
|
||||||
|
@ -1127,6 +1128,13 @@ note that this is way more complicated than the new [event hooks](#event-hooks)
|
||||||
note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1`
|
note that it will occupy the parsing threads, so fork anything expensive (or set `kn` to have copyparty fork it for you) -- otoh if you want to intentionally queue/singlethread you can combine it with `--mtag-mt 1`
|
||||||
|
|
||||||
|
|
||||||
|
## handlers
|
||||||
|
|
||||||
|
redefine behavior with plugins ([examples](./bin/handlers/))
|
||||||
|
|
||||||
|
replace 404 and 403 errors with something completely different (that's it for now)
|
||||||
|
|
||||||
|
|
||||||
## hiding from google
|
## hiding from google
|
||||||
|
|
||||||
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
|
tell search engines you dont wanna be indexed, either using the good old [robots.txt](https://www.robotstxt.org/robotstxt.html) or through copyparty settings:
|
||||||
|
|
35
bin/handlers/README.md
Normal file
35
bin/handlers/README.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
replace the standard 404 / 403 responses with plugins
|
||||||
|
|
||||||
|
|
||||||
|
# usage
|
||||||
|
|
||||||
|
load plugins either globally with `--on404 ~/dev/copyparty/bin/handlers/sorry.py` or for a specific volume with `:c,on404=~/handlers/sorry.py`
|
||||||
|
|
||||||
|
|
||||||
|
# api
|
||||||
|
|
||||||
|
each plugin must define a `main()` which takes 3 arguments;
|
||||||
|
|
||||||
|
* `cli` is an instance of [copyparty/httpcli.py](https://github.com/9001/copyparty/blob/hovudstraum/copyparty/httpcli.py) (the monstrosity itself)
|
||||||
|
* `vn` is the VFS which overlaps with the requested URL, and
|
||||||
|
* `rem` is the URL remainder below the VFS mountpoint
|
||||||
|
* so `vn.vpath + rem` == `cli.vpath` == original request
|
||||||
|
|
||||||
|
|
||||||
|
# examples
|
||||||
|
|
||||||
|
## on404
|
||||||
|
|
||||||
|
* [sorry.py](answer.py) replies with a custom message instead of the usual 404
|
||||||
|
* [nooo.py](nooo.py) replies with an endless noooooooooooooo
|
||||||
|
* [never404.py](never404.py) 100% guarantee that 404 will never be a thing again as it automatically creates dummy files whenever necessary
|
||||||
|
* [caching-proxy.py](caching-proxy.py) transforms copyparty into a squid/varnish knockoff
|
||||||
|
|
||||||
|
## on403
|
||||||
|
|
||||||
|
* [ip-ok.py](ip-ok.py) disables security checks if client-ip is 1.2.3.4
|
||||||
|
|
||||||
|
|
||||||
|
# notes
|
||||||
|
|
||||||
|
* on403 only works for trivial stuff (basic http access) since I haven't been able to think of any good usecases for it (was just easy to add while doing on404)
|
36
bin/handlers/caching-proxy.py
Executable file
36
bin/handlers/caching-proxy.py
Executable file
|
@ -0,0 +1,36 @@
|
||||||
|
# assume each requested file exists on another webserver and
|
||||||
|
# download + mirror them as they're requested
|
||||||
|
# (basically pretend we're warnish)
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from copyparty.httpcli import HttpCli
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli: "HttpCli", vn, rem):
|
||||||
|
url = "https://mirrors.edge.kernel.org/alpine/" + rem
|
||||||
|
abspath = os.path.join(vn.realpath, rem)
|
||||||
|
|
||||||
|
# sneaky trick to preserve a requests-session between downloads
|
||||||
|
# so it doesn't have to spend ages reopening https connections;
|
||||||
|
# luckily we can stash it inside the copyparty client session,
|
||||||
|
# name just has to be definitely unused so "hacapo_req_s" it is
|
||||||
|
req_s = getattr(cli.conn, "hacapo_req_s", None) or requests.Session()
|
||||||
|
setattr(cli.conn, "hacapo_req_s", req_s)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(abspath), exist_ok=True)
|
||||||
|
with req_s.get(url, stream=True, timeout=69) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
with open(abspath, "wb", 64 * 1024) as f:
|
||||||
|
for buf in r.iter_content(chunk_size=64 * 1024):
|
||||||
|
f.write(buf)
|
||||||
|
except:
|
||||||
|
os.unlink(abspath)
|
||||||
|
return "false"
|
||||||
|
|
||||||
|
return "retry"
|
6
bin/handlers/ip-ok.py
Executable file
6
bin/handlers/ip-ok.py
Executable file
|
@ -0,0 +1,6 @@
|
||||||
|
# disable permission checks and allow access if client-ip is 1.2.3.4
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
if cli.ip == "1.2.3.4":
|
||||||
|
return "allow"
|
11
bin/handlers/never404.py
Executable file
11
bin/handlers/never404.py
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
# create a dummy file and let copyparty return it
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
print("hello", cli.ip)
|
||||||
|
|
||||||
|
abspath = vn.canonical(rem)
|
||||||
|
with open(abspath, "wb") as f:
|
||||||
|
f.write(b"404? not on MY watch!")
|
||||||
|
|
||||||
|
return "retry"
|
16
bin/handlers/nooo.py
Executable file
16
bin/handlers/nooo.py
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
# reply with an endless "noooooooooooooooooooooooo"
|
||||||
|
|
||||||
|
|
||||||
|
def say_no():
|
||||||
|
yield b"n"
|
||||||
|
while True:
|
||||||
|
yield b"o" * 4096
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
cli.send_headers(None, 404, "text/plain")
|
||||||
|
|
||||||
|
for chunk in say_no():
|
||||||
|
cli.s.sendall(chunk)
|
||||||
|
|
||||||
|
return "false"
|
7
bin/handlers/sorry.py
Executable file
7
bin/handlers/sorry.py
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
# sends a custom response instead of the usual 404
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli, vn, rem):
|
||||||
|
msg = f"sorry {cli.ip} but {cli.vpath} doesn't exist"
|
||||||
|
|
||||||
|
return str(cli.reply(msg.encode("utf-8"), 404, "text/plain"))
|
|
@ -528,6 +528,51 @@ def get_sects():
|
||||||
).rstrip()
|
).rstrip()
|
||||||
+ build_flags_desc(),
|
+ build_flags_desc(),
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"handlers",
|
||||||
|
"use plugins to handle certain events",
|
||||||
|
dedent(
|
||||||
|
"""
|
||||||
|
usually copyparty returns a 404 if a file does not exist, and
|
||||||
|
403 if a user tries to access a file they don't have access to
|
||||||
|
|
||||||
|
you can load a plugin which will be invoked right before this
|
||||||
|
happens, and the plugin can choose to override this behavior
|
||||||
|
|
||||||
|
load the plugin using --args or volflags; for example \033[36m
|
||||||
|
--on404 ~/partyhandlers/not404.py
|
||||||
|
-v .::r:c,on404=~/partyhandlers/not404.py
|
||||||
|
\033[0m
|
||||||
|
|
||||||
|
the file must define a `main(cli,vn,rem)` function:
|
||||||
|
cli: the HttpCli instance
|
||||||
|
vn: the VFS which overlaps with the requested URL
|
||||||
|
rem: the remainder of the URL below the VFS mountpoint
|
||||||
|
|
||||||
|
`main` must return a string; one of the following:
|
||||||
|
|
||||||
|
> "true": the plugin has responded to the request,
|
||||||
|
and the TCP connection should be kept open
|
||||||
|
|
||||||
|
> "false": the plugin has responded to the request,
|
||||||
|
and the TCP connection should be terminated
|
||||||
|
|
||||||
|
> "retry": the plugin has done something to resolve the 404
|
||||||
|
situation, and copyparty should reattempt reading the file.
|
||||||
|
if it still fails, a regular 404 will be returned
|
||||||
|
|
||||||
|
> "allow": should ignore the insufficient permissions
|
||||||
|
and let the client continue anyways
|
||||||
|
|
||||||
|
> "": the plugin has not handled the request;
|
||||||
|
try the next plugin or return the usual 404 or 403
|
||||||
|
|
||||||
|
PS! the folder that contains the python file should ideally
|
||||||
|
not contain many other python files, and especially nothing
|
||||||
|
with filenames that overlap with modules used by copyparty
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"hooks",
|
"hooks",
|
||||||
"execute commands before/after various events",
|
"execute commands before/after various events",
|
||||||
|
@ -858,6 +903,13 @@ def add_smb(ap):
|
||||||
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
ap2.add_argument("--smbvvv", action="store_true", help="verbosest")
|
||||||
|
|
||||||
|
|
||||||
|
def add_handlers(ap):
|
||||||
|
ap2 = ap.add_argument_group('handlers (see --help-handlers)')
|
||||||
|
ap2.add_argument("--on404", metavar="PY", type=u, action="append", help="handle 404s by executing PY file")
|
||||||
|
ap2.add_argument("--on403", metavar="PY", type=u, action="append", help="handle 403s by executing PY file")
|
||||||
|
ap2.add_argument("--hot-handlers", action="store_true", help="reload handlers on each request -- expensive but convenient when hacking on stuff")
|
||||||
|
|
||||||
|
|
||||||
def add_hooks(ap):
|
def add_hooks(ap):
|
||||||
ap2 = ap.add_argument_group('event hooks (see --help-hooks)')
|
ap2 = ap.add_argument_group('event hooks (see --help-hooks)')
|
||||||
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute CMD before a file upload starts")
|
ap2.add_argument("--xbu", metavar="CMD", type=u, action="append", help="execute CMD before a file upload starts")
|
||||||
|
@ -1113,6 +1165,7 @@ def run_argparse(
|
||||||
add_optouts(ap)
|
add_optouts(ap)
|
||||||
add_shutdown(ap)
|
add_shutdown(ap)
|
||||||
add_yolo(ap)
|
add_yolo(ap)
|
||||||
|
add_handlers(ap)
|
||||||
add_hooks(ap)
|
add_hooks(ap)
|
||||||
add_ui(ap, retry)
|
add_ui(ap, retry)
|
||||||
add_admin(ap)
|
add_admin(ap)
|
||||||
|
|
|
@ -1047,7 +1047,7 @@ class AuthSrv(object):
|
||||||
flags[name] = True
|
flags[name] = True
|
||||||
return
|
return
|
||||||
|
|
||||||
if name not in "mtp xbu xau xiu xbr xar xbd xad xm".split():
|
if name not in "mtp xbu xau xiu xbr xar xbd xad xm on404 on403".split():
|
||||||
if value is True:
|
if value is True:
|
||||||
t = "└─add volflag [{}] = {} ({})"
|
t = "└─add volflag [{}] = {} ({})"
|
||||||
else:
|
else:
|
||||||
|
@ -1446,7 +1446,7 @@ class AuthSrv(object):
|
||||||
|
|
||||||
# append additive args from argv to volflags
|
# append additive args from argv to volflags
|
||||||
hooks = "xbu xau xiu xbr xar xbd xad xm".split()
|
hooks = "xbu xau xiu xbr xar xbd xad xm".split()
|
||||||
for name in ["mtp"] + hooks:
|
for name in "mtp on404 on403".split() + hooks:
|
||||||
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
|
self._read_volflag(vol.flags, name, getattr(self.args, name), True)
|
||||||
|
|
||||||
for hn in hooks:
|
for hn in hooks:
|
||||||
|
@ -1835,7 +1835,7 @@ class AuthSrv(object):
|
||||||
]
|
]
|
||||||
|
|
||||||
csv = set("i p".split())
|
csv = set("i p".split())
|
||||||
lst = set("c ihead mtm mtp xad xar xau xiu xbd xbr xbu xm".split())
|
lst = set("c ihead mtm mtp xad xar xau xiu xbd xbr xbu xm on404 on403".split())
|
||||||
askip = set("a v c vc cgen theme".split())
|
askip = set("a v c vc cgen theme".split())
|
||||||
|
|
||||||
# keymap from argv to vflag
|
# keymap from argv to vflag
|
||||||
|
|
|
@ -125,6 +125,10 @@ flagcats = {
|
||||||
"dathumb": "disables audio thumbnails (spectrograms)",
|
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||||
"dithumb": "disables image thumbnails",
|
"dithumb": "disables image thumbnails",
|
||||||
},
|
},
|
||||||
|
"handlers\n(better explained in --help-handlers)": {
|
||||||
|
"on404=PY": "handle 404s by executing PY file",
|
||||||
|
"on403=PY": "handle 403s by executing PY file",
|
||||||
|
},
|
||||||
"event hooks\n(better explained in --help-hooks)": {
|
"event hooks\n(better explained in --help-hooks)": {
|
||||||
"xbu=CMD": "execute CMD before a file upload starts",
|
"xbu=CMD": "execute CMD before a file upload starts",
|
||||||
"xau=CMD": "execute CMD after a file upload finishes",
|
"xau=CMD": "execute CMD after a file upload finishes",
|
||||||
|
|
|
@ -58,6 +58,7 @@ from .util import (
|
||||||
html_escape,
|
html_escape,
|
||||||
humansize,
|
humansize,
|
||||||
ipnorm,
|
ipnorm,
|
||||||
|
loadpy,
|
||||||
min_ex,
|
min_ex,
|
||||||
quotep,
|
quotep,
|
||||||
rand_name,
|
rand_name,
|
||||||
|
@ -767,11 +768,27 @@ class HttpCli(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if not self.can_read and not self.can_write and not self.can_get:
|
if not self.can_read and not self.can_write and not self.can_get:
|
||||||
if self.vpath:
|
t = "@{} has no access to [{}]"
|
||||||
self.log("inaccessible: [{}]".format(self.vpath))
|
self.log(t.format(self.uname, self.vpath))
|
||||||
return self.tx_404(True)
|
|
||||||
|
|
||||||
self.uparam["h"] = ""
|
if self.avn and "on403" in self.avn.flags:
|
||||||
|
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False)
|
||||||
|
ret = self.on40x(vn.flags["on403"], vn, rem)
|
||||||
|
if ret == "true":
|
||||||
|
return True
|
||||||
|
elif ret == "false":
|
||||||
|
return False
|
||||||
|
elif ret == "allow":
|
||||||
|
self.log("plugin override; access permitted")
|
||||||
|
self.can_read = self.can_write = self.can_move = True
|
||||||
|
self.can_delete = self.can_get = self.can_upget = True
|
||||||
|
else:
|
||||||
|
return self.tx_404(True)
|
||||||
|
else:
|
||||||
|
if self.vpath:
|
||||||
|
return self.tx_404(True)
|
||||||
|
|
||||||
|
self.uparam["h"] = ""
|
||||||
|
|
||||||
if "tree" in self.uparam:
|
if "tree" in self.uparam:
|
||||||
return self.tx_tree()
|
return self.tx_tree()
|
||||||
|
@ -3056,6 +3073,20 @@ class HttpCli(object):
|
||||||
self.reply(html.encode("utf-8"), status=rc)
|
self.reply(html.encode("utf-8"), status=rc)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def on40x(self, mods: list[str], vn: VFS, rem: str) -> str:
|
||||||
|
for mpath in mods:
|
||||||
|
try:
|
||||||
|
mod = loadpy(mpath, self.args.hot_handlers)
|
||||||
|
except Exception as ex:
|
||||||
|
self.log("import failed: {!r}".format(ex))
|
||||||
|
continue
|
||||||
|
|
||||||
|
ret = mod.main(self, vn, rem)
|
||||||
|
if ret:
|
||||||
|
return ret.lower()
|
||||||
|
|
||||||
|
return "" # unhandled / fallthrough
|
||||||
|
|
||||||
def scanvol(self) -> bool:
|
def scanvol(self) -> bool:
|
||||||
if not self.can_read or not self.can_write:
|
if not self.can_read or not self.can_write:
|
||||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||||
|
@ -3373,7 +3404,21 @@ class HttpCli(object):
|
||||||
try:
|
try:
|
||||||
st = bos.stat(abspath)
|
st = bos.stat(abspath)
|
||||||
except:
|
except:
|
||||||
return self.tx_404()
|
if "on404" not in vn.flags:
|
||||||
|
return self.tx_404()
|
||||||
|
|
||||||
|
ret = self.on40x(vn.flags["on404"], vn, rem)
|
||||||
|
if ret == "true":
|
||||||
|
return True
|
||||||
|
elif ret == "false":
|
||||||
|
return False
|
||||||
|
elif ret == "retry":
|
||||||
|
try:
|
||||||
|
st = bos.stat(abspath)
|
||||||
|
except:
|
||||||
|
return self.tx_404()
|
||||||
|
else:
|
||||||
|
return self.tx_404()
|
||||||
|
|
||||||
if rem.startswith(".hist/up2k.") or (
|
if rem.startswith(".hist/up2k.") or (
|
||||||
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
||||||
|
|
|
@ -2727,6 +2727,34 @@ def runhook(
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def loadpy(ap: str, hot: bool) -> Any:
|
||||||
|
"""
|
||||||
|
a nice can of worms capable of causing all sorts of bugs
|
||||||
|
depending on what other inconveniently named files happen
|
||||||
|
to be in the same folder
|
||||||
|
"""
|
||||||
|
if ap.startswith("~"):
|
||||||
|
ap = os.path.expanduser(ap)
|
||||||
|
|
||||||
|
mdir, mfile = os.path.split(absreal(ap))
|
||||||
|
mname = mfile.rsplit(".", 1)[0]
|
||||||
|
sys.path.insert(0, mdir)
|
||||||
|
|
||||||
|
if PY2:
|
||||||
|
mod = __import__(mname)
|
||||||
|
if hot:
|
||||||
|
reload(mod)
|
||||||
|
else:
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
mod = importlib.import_module(mname)
|
||||||
|
if hot:
|
||||||
|
importlib.reload(mod)
|
||||||
|
|
||||||
|
sys.path.remove(mdir)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
def gzip_orig_sz(fn: str) -> int:
|
def gzip_orig_sz(fn: str) -> int:
|
||||||
with open(fsenc(fn), "rb") as f:
|
with open(fsenc(fn), "rb") as f:
|
||||||
f.seek(-4, 2)
|
f.seek(-4, 2)
|
||||||
|
|
|
@ -116,7 +116,7 @@ class Cfg(Namespace):
|
||||||
ex = "ah_alg doctitle favico html_head lg_sbf log_fk md_sbf mth textfiles unlist R RS SR"
|
ex = "ah_alg doctitle favico html_head lg_sbf log_fk md_sbf mth textfiles unlist R RS SR"
|
||||||
ka.update(**{k: "" for k in ex.split()})
|
ka.update(**{k: "" for k in ex.split()})
|
||||||
|
|
||||||
ex = "xad xar xau xbd xbr xbu xiu xm"
|
ex = "on403 on404 xad xar xau xbd xbr xbu xiu xm"
|
||||||
ka.update(**{k: [] for k in ex.split()})
|
ka.update(**{k: [] for k in ex.split()})
|
||||||
|
|
||||||
super(Cfg, self).__init__(
|
super(Cfg, self).__init__(
|
||||||
|
|
Loading…
Reference in a new issue