mirror of
https://github.com/9001/copyparty.git
synced 2025-08-16 08:32:13 -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
|
||||
* [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/))
|
||||
* [handlers](#handlers) - redefine behavior with plugins ([examples](./bin/handlers/))
|
||||
* [hiding from google](#hiding-from-google) - tell search engines you dont wanna be indexed
|
||||
* [themes](#themes)
|
||||
* [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`
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
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()
|
||||
+ 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",
|
||||
"execute commands before/after various events",
|
||||
|
@ -858,6 +903,13 @@ def add_smb(ap):
|
|||
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):
|
||||
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")
|
||||
|
@ -1113,6 +1165,7 @@ def run_argparse(
|
|||
add_optouts(ap)
|
||||
add_shutdown(ap)
|
||||
add_yolo(ap)
|
||||
add_handlers(ap)
|
||||
add_hooks(ap)
|
||||
add_ui(ap, retry)
|
||||
add_admin(ap)
|
||||
|
|
|
@ -1047,7 +1047,7 @@ class AuthSrv(object):
|
|||
flags[name] = True
|
||||
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:
|
||||
t = "└─add volflag [{}] = {} ({})"
|
||||
else:
|
||||
|
@ -1446,7 +1446,7 @@ class AuthSrv(object):
|
|||
|
||||
# append additive args from argv to volflags
|
||||
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)
|
||||
|
||||
for hn in hooks:
|
||||
|
@ -1835,7 +1835,7 @@ class AuthSrv(object):
|
|||
]
|
||||
|
||||
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())
|
||||
|
||||
# keymap from argv to vflag
|
||||
|
|
|
@ -125,6 +125,10 @@ flagcats = {
|
|||
"dathumb": "disables audio thumbnails (spectrograms)",
|
||||
"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)": {
|
||||
"xbu=CMD": "execute CMD before a file upload starts",
|
||||
"xau=CMD": "execute CMD after a file upload finishes",
|
||||
|
|
|
@ -58,6 +58,7 @@ from .util import (
|
|||
html_escape,
|
||||
humansize,
|
||||
ipnorm,
|
||||
loadpy,
|
||||
min_ex,
|
||||
quotep,
|
||||
rand_name,
|
||||
|
@ -767,11 +768,27 @@ class HttpCli(object):
|
|||
return True
|
||||
|
||||
if not self.can_read and not self.can_write and not self.can_get:
|
||||
if self.vpath:
|
||||
self.log("inaccessible: [{}]".format(self.vpath))
|
||||
return self.tx_404(True)
|
||||
t = "@{} has no access to [{}]"
|
||||
self.log(t.format(self.uname, self.vpath))
|
||||
|
||||
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:
|
||||
return self.tx_tree()
|
||||
|
@ -3056,6 +3073,20 @@ class HttpCli(object):
|
|||
self.reply(html.encode("utf-8"), status=rc)
|
||||
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:
|
||||
if not self.can_read or not self.can_write:
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
@ -3373,7 +3404,21 @@ class HttpCli(object):
|
|||
try:
|
||||
st = bos.stat(abspath)
|
||||
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 (
|
||||
rem.endswith("/dir.txt") and rem.startswith(".hist/th/")
|
||||
|
|
|
@ -2727,6 +2727,34 @@ def runhook(
|
|||
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:
|
||||
with open(fsenc(fn), "rb") as f:
|
||||
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"
|
||||
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()})
|
||||
|
||||
super(Cfg, self).__init__(
|
||||
|
|
Loading…
Reference in a new issue