Compare commits

...

28 Commits

Author SHA1 Message Date
ed
85a637af09 v1.8.2 2023-07-14 15:58:39 +00:00
ed
043e3c7dd6 fix traversal vulnerability GHSA-pxfv-7rr3-2qjg:
the /.cpr endpoint allowed full access to server filesystem,
unless mitigated by prisonparty
2023-07-14 15:55:49 +00:00
ed
8f59afb159 fix another race (unpost):
unposting could collide with most other database-related activities,
causing one or the other to fail.
luckily the unprotected query performed by the unpost API happens to be
very cheap, so also the most likely to fail, and would succeed upon a
manual reattempt from the UI.
even in the worst case scenario, there would be no unrecoverable damage
as the next rescan would auto-repair any resulting inconsistencies.
2023-07-14 15:21:14 +00:00
ed
77f1e51444 fix unlikely race (e2tsr):
if someone with admin rights refreshes the homepage exactly as the
directory indexer decides to `_drop_caches`, the indexer thread would
die and the up2k instance would become inoperable...
luckily the probability of hitting this by chance is absolutely minimal,
and the worst case scenario is having to restart copyparty if this
happens immediately after startup; there is no risk of database damage
2023-07-14 15:20:25 +00:00
ed
22fc4bb938 add event-hook for banning users 2023-07-13 22:29:32 +00:00
ed
50c7bba6ea volflag "nohtml" to never return html or rendered markdown from potentially unsafe volumes 2023-07-13 21:57:52 +00:00
ed
551d99b71b add permission "a" to show uploader IPs (#45) 2023-07-12 21:36:55 +00:00
ed
b54b7213a7 more thumbnailer configs available as volflags:
--th-convt = convt
--th-no-crop = nocrop
--th-size = thsize
2023-07-11 22:15:37 +00:00
ed
a14943c8de update pkgs to 1.8.1 2023-07-07 23:58:16 +00:00
ed
a10cad54fc v1.8.1 2023-07-07 22:20:01 +00:00
ed
8568b7702a add pillow10 support + improve text rendering 2023-07-07 22:13:04 +00:00
ed
5d8cb34885 404/403 can be handled with plugins 2023-07-07 21:33:40 +00:00
ed
8d248333e8 dont disable quickedit when hashing passwords interactively 2023-07-07 18:29:30 +00:00
ed
99e2ef7f33 ux: fix tabs clipping in fedora-ff, hackertheme up2k flags 2023-07-07 18:24:58 +00:00
ed
e767230383 very-bad-idea: prefer mpv / streamlink; closes #42 2023-06-28 21:25:40 +00:00
ed
90601314d6 better explain why very-bad-idea is a very bad idea 2023-06-27 22:30:14 +00:00
ed
9c5eac1274 add fedora package 2023-06-27 22:22:42 +00:00
ed
50905439e4 update pkgs to 1.8.0 2023-06-26 00:46:55 +00:00
ed
a0c1239246 v1.8.0 2023-06-26 00:05:12 +00:00
ed
b8e851c332 cloudflare update + cosmetics:
* toastb padding fixes scrollbar on norwegian 403 in firefox
* fix text aspect ratio in seekbaron compact toggle
* crashpage had link overlaps on homepage
2023-06-25 23:09:29 +00:00
ed
baaf2eb24d include mdns names in tls cert 2023-06-25 22:06:35 +00:00
ed
e197895c10 support hashed passwords; closes #39 2023-06-25 21:50:33 +00:00
ed
cb75efa05d md-editor: index file and trigger upload hooks 2023-06-20 18:11:35 +00:00
ed
8b0cf2c982 volflags to limit volume size / num files; closes #40 2023-06-19 00:42:45 +00:00
ed
fc7d9e1f9c update pkgs to 1.7.6 2023-06-11 09:13:58 +00:00
ed
10caafa34c v1.7.6 2023-06-11 08:14:45 +00:00
ed
22cc22225a v1.7.5 2023-06-11 01:32:56 +00:00
ed
22dff4b0e5 update pkgs to 1.7.4 2023-06-11 01:26:25 +00:00
46 changed files with 1298 additions and 220 deletions

View File

@@ -66,12 +66,14 @@ 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)
* [reverse-proxy](#reverse-proxy) - running copyparty next to other websites * [reverse-proxy](#reverse-proxy) - running copyparty next to other websites
* [packages](#packages) - the party might be closer than you think * [packages](#packages) - the party might be closer than you think
* [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes) * [arch package](#arch-package) - now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
* [fedora package](#fedora-package) - now [available on copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/)
* [nix package](#nix-package) - `nix profile install github:9001/copyparty` * [nix package](#nix-package) - `nix profile install github:9001/copyparty`
* [nixos module](#nixos-module) * [nixos module](#nixos-module)
* [browser support](#browser-support) - TLDR: yes * [browser support](#browser-support) - TLDR: yes
@@ -85,6 +87,7 @@ turn almost any device into a file server with resumable uploads/downloads using
* [security](#security) - some notes on hardening * [security](#security) - some notes on hardening
* [gotchas](#gotchas) - behavior that might be unexpected * [gotchas](#gotchas) - behavior that might be unexpected
* [cors](#cors) - cross-site request config * [cors](#cors) - cross-site request config
* [password hashing](#password-hashing) - you can hash passwords
* [https](#https) - both HTTP and HTTPS are accepted * [https](#https) - both HTTP and HTTPS are accepted
* [recovering from crashes](#recovering-from-crashes) * [recovering from crashes](#recovering-from-crashes)
* [client crashes](#client-crashes) * [client crashes](#client-crashes)
@@ -343,6 +346,7 @@ permissions:
* `d` (delete): delete files/folders * `d` (delete): delete files/folders
* `g` (get): only download files, cannot see folder contents or zip/tar * `g` (get): only download files, cannot see folder contents or zip/tar
* `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below) * `G` (upget): same as `g` except uploaders get to see their own filekeys (see `fk` in examples below)
* `a` (admin): can see uploader IPs
examples: examples:
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3` * add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
@@ -980,6 +984,8 @@ set upload rules using volflags, some examples:
* `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`) * `:c,sz=1k-3m` sets allowed filesize between 1 KiB and 3 MiB inclusive (suffixes: `b`, `k`, `m`, `g`)
* `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards * `:c,df=4g` block uploads if there would be less than 4 GiB free disk space afterwards
* `:c,vmaxb=1g` block uploads if total volume size would exceed 1 GiB afterwards
* `:c,vmaxn=4k` block uploads if volume would contain more than 4096 files afterwards
* `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`: * `:c,nosub` disallow uploading into subdirectories; goes well with `rotn` and `rotf`:
* `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1) * `:c,rotn=1000,2` moves uploads into subfolders, up to 1000 files in each folder before making a new one, two levels deep (must be at least 1)
* `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format * `:c,rotf=%Y/%m/%d/%H` enforces files to be uploaded into a structure of subfolders according to that date format
@@ -1123,6 +1129,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:
@@ -1225,6 +1238,19 @@ the party might be closer than you think
now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes) now [available on aur](https://aur.archlinux.org/packages/copyparty) maintained by [@icxes](https://github.com/icxes)
## fedora package
now [available on copr-pypi](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/) , maintained autonomously -- [track record](https://copr.fedorainfracloud.org/coprs/g/copr/PyPI/package/python-copyparty/) seems OK
```bash
dnf copr enable @copr/PyPI
dnf install python3-copyparty # just a minimal install, or...
dnf install python3-{copyparty,pillow,argon2-cffi,pyftpdlib,pyOpenSSL} ffmpeg-free # with recommended deps
```
this *may* also work on RHEL but [I'm not paying IBM to verify that](https://www.jeffgeerling.com/blog/2023/dear-red-hat-are-you-dumb)
## nix package ## nix package
`nix profile install github:9001/copyparty` `nix profile install github:9001/copyparty`
@@ -1511,6 +1537,7 @@ some notes on hardening
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy) * set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
* cors doesn't work right otherwise * cors doesn't work right otherwise
* if you allow anonymous uploads or otherwise don't trust the contents of a volume, you can prevent XSS with volflag `nohtml`
safety profiles: safety profiles:
@@ -1566,6 +1593,17 @@ by default, except for `GET` and `HEAD` operations, all requests must either:
cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf` cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`
## password hashing
you can hash passwords before putting them into config files / providing them as arguments; see `--help-pwhash` for all the details
`--ah-alg argon2` enables it, and if you have any plaintext passwords then it'll print the hashed versions on startup so you can replace them
optionally also specify `--ah-cli` to enter an interactive mode where it will hash passwords without ever writing the plaintext ones to disk
the default configs take about 0.4 sec and 256 MiB RAM to process a new password on a decent laptop
## https ## https
both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default) both HTTP and HTTPS are accepted by default, but letting a [reverse proxy](#reverse-proxy) handle the https/tls/ssl would be better (probably more secure by default)
@@ -1613,6 +1651,8 @@ mandatory deps:
install these to enable bonus features install these to enable bonus features
enable hashed passwords in config: `argon2-cffi`
enable ftp-server: enable ftp-server:
* for just plaintext FTP, `pyftpdlib` (is built into the SFX) * for just plaintext FTP, `pyftpdlib` (is built into the SFX)
* with TLS encryption, `pyftpdlib pyopenssl` * with TLS encryption, `pyftpdlib pyopenssl`

35
bin/handlers/README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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"))

View File

@@ -24,6 +24,15 @@ these do not have any problematic dependencies at all:
* also available as an [event hook](../hooks/wget.py) * also available as an [event hook](../hooks/wget.py)
## dangerous plugins
plugins in this section should only be used with appropriate precautions:
* [very-bad-idea.py](./very-bad-idea.py) combined with [meadup.js](https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js) converts copyparty into a janky yet extremely flexible chromecast clone
* also adds a virtual keyboard by @steinuil to the basic-upload tab for comfy couch crowd control
* anything uploaded through the [android app](https://github.com/9001/party-up) (files or links) are executed on the server, meaning anyone can infect your PC with malware... so protect this with a password and keep it on a LAN!
# dependencies # dependencies
run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos) run [`install-deps.sh`](install-deps.sh) to build/install most dependencies required by these programs (supports windows/linux/macos)

View File

@@ -1,6 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
WARNING -- DANGEROUS PLUGIN --
if someone is able to upload files to a copyparty which is
running this plugin, they can execute malware on your machine
so please keep this on a LAN and protect it with a password
use copyparty as a chromecast replacement: use copyparty as a chromecast replacement:
* post a URL and it will open in the default browser * post a URL and it will open in the default browser
* upload a file and it will open in the default application * upload a file and it will open in the default application
@@ -10,16 +15,17 @@ use copyparty as a chromecast replacement:
the android app makes it a breeze to post pics and links: the android app makes it a breeze to post pics and links:
https://github.com/9001/party-up/releases https://github.com/9001/party-up/releases
(iOS devices have to rely on the web-UI)
goes without saying, but this is HELLA DANGEROUS, iOS devices can use the web-UI or the shortcut instead:
GIVES RCE TO ANYONE WHO HAVE UPLOAD PERMISSIONS https://github.com/9001/copyparty#ios-shortcuts
example copyparty config to use this: example copyparty config to use this;
--urlform save,get -v.::w:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py lets the user "kevin" with password "hunter2" use this plugin:
-a kevin:hunter2 --urlform save,get -v.::w,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,c0,bin/mtag/very-bad-idea.py
recommended deps: recommended deps:
apt install xdotool libnotify-bin apt install xdotool libnotify-bin mpv
python3 -m pip install --user -U streamlink yt-dlp
https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js https://github.com/9001/copyparty/blob/hovudstraum/contrib/plugins/meadup.js
and you probably want `twitter-unmute.user.js` from the res folder and you probably want `twitter-unmute.user.js` from the res folder
@@ -63,8 +69,10 @@ set -e
EOF EOF
chmod 755 /usr/local/bin/chromium-browser chmod 755 /usr/local/bin/chromium-browser
# start the server (note: replace `-v.::rw:` with `-v.::w:` to disallow retrieving uploaded stuff) # start the server
cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py # note 1: replace hunter2 with a better password to access the server
# note 2: replace `-v.::rw` with `-v.::w` to disallow retrieving uploaded stuff
cd ~/Downloads; python3 copyparty-sfx.py -a kevin:hunter2 --urlform save,get -v.::rw,kevin:c,e2d,e2t,mte=+a1:c,mtp=a1=ad,kn,very-bad-idea.py
""" """
@@ -72,11 +80,23 @@ cd ~/Downloads; python3 copyparty-sfx.py --urlform save,get -v.::rw:c,e2d,e2t,mt
import os import os
import sys import sys
import time import time
import shutil
import subprocess as sp import subprocess as sp
from urllib.parse import unquote_to_bytes as unquote from urllib.parse import unquote_to_bytes as unquote
from urllib.parse import quote
have_mpv = shutil.which("mpv")
have_vlc = shutil.which("vlc")
def main(): def main():
if len(sys.argv) > 2 and sys.argv[1] == "x":
# invoked on commandline for testing;
# python3 very-bad-idea.py x msg=https://youtu.be/dQw4w9WgXcQ
txt = " ".join(sys.argv[2:])
txt = quote(txt.replace(" ", "+"))
return open_post(txt.encode("utf-8"))
fp = os.path.abspath(sys.argv[1]) fp = os.path.abspath(sys.argv[1])
with open(fp, "rb") as f: with open(fp, "rb") as f:
txt = f.read(4096) txt = f.read(4096)
@@ -92,7 +112,7 @@ def open_post(txt):
try: try:
k, v = txt.split(" ", 1) k, v = txt.split(" ", 1)
except: except:
open_url(txt) return open_url(txt)
if k == "key": if k == "key":
sp.call(["xdotool", "key"] + v.split(" ")) sp.call(["xdotool", "key"] + v.split(" "))
@@ -128,6 +148,17 @@ def open_url(txt):
# else: # else:
# sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo # sp.call(["xdotool", "getactivewindow", "windowminimize"]) # minimizes the focused windo
# mpv is probably smart enough to use streamlink automatically
if try_mpv(txt):
print("mpv got it")
return
# or maybe streamlink would be a good choice to open this
if try_streamlink(txt):
print("streamlink got it")
return
# nope,
# close any error messages: # close any error messages:
sp.call(["xdotool", "search", "--name", "Error", "windowclose"]) sp.call(["xdotool", "search", "--name", "Error", "windowclose"])
# sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all # sp.call(["xdotool", "key", "ctrl+alt+d"]) # doesnt work at all
@@ -136,4 +167,39 @@ def open_url(txt):
sp.call(["xdg-open", txt]) sp.call(["xdg-open", txt])
def try_mpv(url):
t0 = time.time()
try:
print("trying mpv...")
sp.check_call(["mpv", "--fs", url])
return True
except:
# if it ran for 15 sec it probably succeeded and terminated
t = time.time()
return t - t0 > 15
def try_streamlink(url):
t0 = time.time()
try:
import streamlink
print("trying streamlink...")
streamlink.Streamlink().resolve_url(url)
if have_mpv:
args = "-m streamlink -p mpv -a --fs"
else:
args = "-m streamlink"
cmd = [sys.executable] + args.split() + [url, "best"]
t0 = time.time()
sp.check_call(cmd)
return True
except:
# if it ran for 10 sec it probably succeeded and terminated
t = time.time()
return t - t0 > 10
main() main()

View File

@@ -138,6 +138,7 @@ in {
"d" (delete): permanently delete files and folders "d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents "g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads "G" (upget): "get", but can see filekeys of their own uploads
"a" (upget): can see uploader IPs
For example: "rwmd" For example: "rwmd"

View File

@@ -1,6 +1,6 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty pkgname=copyparty
pkgver="1.7.2" pkgver="1.8.1"
pkgrel=1 pkgrel=1
pkgdesc="Portable file sharing hub" pkgdesc="Portable file sharing hub"
arch=("any") arch=("any")
@@ -15,11 +15,12 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
"libkeyfinder-git: detection of musical keys" "libkeyfinder-git: detection of musical keys"
"qm-vamp-plugins: BPM detection" "qm-vamp-plugins: BPM detection"
"python-pyopenssl: ftps functionality" "python-pyopenssl: ftps functionality"
"python-argon2_cffi: hashed passwords in config"
"python-impacket-git: smb support (bad idea)" "python-impacket-git: smb support (bad idea)"
) )
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz") source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
backup=("etc/${pkgname}.d/init" ) backup=("etc/${pkgname}.d/init" )
sha256sums=("fb261d45ce7cf146a3f620d1e3109eb5c584f8950e61a872e2d92d7b7447bae0") sha256sums=("f43da11ba5d1d5adf99ad642bf068042c46c23d408e7ed17b025065121abab94")
build() { build() {
cd "${srcdir}/${pkgname}-${pkgver}" cd "${srcdir}/${pkgname}-${pkgver}"

View File

@@ -1,4 +1,7 @@
{ lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, pillow, pyvips, ffmpeg, mutagen, { lib, stdenv, makeWrapper, fetchurl, utillinux, python, jinja2, impacket, pyftpdlib, pyopenssl, argon2-cffi, pillow, pyvips, ffmpeg, mutagen,
# use argon2id-hashed passwords in config files (sha2 is always available)
withHashedPasswords ? true,
# create thumbnails with Pillow; faster than FFmpeg / MediaProcessing # create thumbnails with Pillow; faster than FFmpeg / MediaProcessing
withThumbnails ? true, withThumbnails ? true,
@@ -35,6 +38,7 @@ let
++ lib.optional withFastThumbnails pyvips ++ lib.optional withFastThumbnails pyvips
++ lib.optional withMediaProcessing ffmpeg ++ lib.optional withMediaProcessing ffmpeg
++ lib.optional withBasicAudioMetadata mutagen ++ lib.optional withBasicAudioMetadata mutagen
++ lib.optional withHashedPasswords argon2-cffi
); );
in stdenv.mkDerivation { in stdenv.mkDerivation {
pname = "copyparty"; pname = "copyparty";

View File

@@ -1,5 +1,5 @@
{ {
"url": "https://github.com/9001/copyparty/releases/download/v1.7.2/copyparty-sfx.py", "url": "https://github.com/9001/copyparty/releases/download/v1.8.1/copyparty-sfx.py",
"version": "1.7.2", "version": "1.8.1",
"hash": "sha256-h22sRRO/bpydgRVmSVD05ZLuzsUxBCWU3izt9Eg9bf0=" "hash": "sha256-0Lf5djrgGAM+wwZP66GtSXkmRnIp3tij8j7cANeoE7o="
} }

View File

@@ -247,13 +247,26 @@ def get_fk_salt(cert_path) -> str:
ret = f.read().strip() ret = f.read().strip()
except: except:
if os.path.exists(cert_path): if os.path.exists(cert_path):
print("salt from cert") zi = os.path.getmtime(cert_path)
return unicode(os.path.getmtime(cert_path)) ret = "{}".format(zi).encode("utf-8")
else: else:
print("salt from os.random")
ret = base64.b64encode(os.urandom(18)) ret = base64.b64encode(os.urandom(18))
with open(fp, "wb") as f:
f.write(ret + b"\n") with open(fp, "wb") as f:
f.write(ret + b"\n")
return ret.decode("utf-8")
def get_ah_salt() -> str:
fp = os.path.join(E.cfg, "ah-salt.txt")
try:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
ret = base64.b64encode(os.urandom(18))
with open(fp, "wb") as f:
f.write(ret + b"\n")
return ret.decode("utf-8") return ret.decode("utf-8")
@@ -479,6 +492,7 @@ def get_sects():
"d" (delete): permanently delete files and folders "d" (delete): permanently delete files and folders
"g" (get): download files, but cannot see folder contents "g" (get): download files, but cannot see folder contents
"G" (upget): "get", but can see filekeys of their own uploads "G" (upget): "get", but can see filekeys of their own uploads
"a" (admin): can see uploader IPs
too many volflags to list here, see --help-flags too many volflags to list here, see --help-flags
@@ -515,6 +529,50 @@ def get_sects():
).rstrip() ).rstrip()
+ build_flags_desc(), + build_flags_desc(),
], ],
[
"handlers",
"use plugins to handle certain events",
dedent(
"""
usually copyparty returns a \033[33m404\033[0m if a file does not exist, and
\033[33m403\033[0m 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 the function \033[35mmain(cli,vn,rem)\033[0m:
\033[35mcli\033[0m: the copyparty HttpCli instance
\033[35mvn\033[0m: the VFS which overlaps with the requested URL
\033[35mrem\033[0m: the remainder of the URL below the VFS mountpoint
`main` must return a string; one of the following:
> \033[32m"true"\033[0m: the plugin has responded to the request,
and the TCP connection should be kept open
> \033[32m"false"\033[0m: the plugin has responded to the request,
and the TCP connection should be terminated
> \033[32m"retry"\033[0m: 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
> \033[32m"allow"\033[0m: should ignore the insufficient permissions
and let the client continue anyways
> \033[32m""\033[0m: the plugin has not handled the request;
try the next plugin or return the usual 404 or 403
\033[1;35mPS!\033[0m 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",
@@ -529,6 +587,7 @@ def get_sects():
\033[36mxbd\033[35m executes CMD before a file delete \033[36mxbd\033[35m executes CMD before a file delete
\033[36mxad\033[35m executes CMD after a file delete \033[36mxad\033[35m executes CMD after a file delete
\033[36mxm\033[35m executes CMD on message \033[36mxm\033[35m executes CMD on message
\033[36mxban\033[35m executes CMD if someone gets banned
\033[0m \033[0m
can be defined as --args or volflags; for example \033[36m can be defined as --args or volflags; for example \033[36m
--xau notify-send --xau notify-send
@@ -564,6 +623,9 @@ def get_sects():
executed program on STDIN instead of as argv arguments, and executed program on STDIN instead of as argv arguments, and
it also includes the wark (file-id/hash) as a json property it also includes the wark (file-id/hash) as a json property
\033[36mxban\033[0m can be used to overrule / cancel a user ban event;
if the program returns 0 (true/OK) then the ban will NOT happen
except for \033[36mxm\033[0m, only one hook / one action can run at a time, except for \033[36mxm\033[0m, only one hook / one action can run at a time,
so it's recommended to use the \033[36mf\033[0m flag unless you really need so it's recommended to use the \033[36mf\033[0m flag unless you really need
to wait for the hook to finish before continuing (without \033[36mf\033[0m to wait for the hook to finish before continuing (without \033[36mf\033[0m
@@ -622,6 +684,38 @@ def get_sects():
""" """
), ),
], ],
[
"pwhash",
"password hashing",
dedent(
"""
when \033[36m--ah-alg\033[0m is not the default [\033[32mnone\033[0m], all account passwords must be hashed
passwords can be hashed on the commandline with \033[36m--ah-gen\033[0m, but copyparty will also hash and print any passwords that are non-hashed (password which do not start with '+') and then terminate afterwards
\033[36m--ah-alg\033[0m specifies the hashing algorithm and a list of optional comma-separated arguments:
\033[36m--ah-alg argon2\033[0m # which is the same as:
\033[36m--ah-alg argon2,3,256,4,19\033[0m
use argon2id with timecost 3, 256 MiB, 4 threads, version 19 (0x13/v1.3)
\033[36m--ah-alg scrypt\033[0m # which is the same as:
\033[36m--ah-alg scrypt,13,2,8,4\033[0m
use scrypt with cost 2**13, 2 iterations, blocksize 8, 4 threads
\033[36m--ah-alg sha2\033[0m # which is the same as:
\033[36m--ah-alg sha2,424242\033[0m
use sha2-512 with 424242 iterations
recommended: \033[32m--ah-alg argon2\033[0m
(takes about 0.4 sec and 256M RAM to process a new password)
argon2 needs python-package argon2-cffi,
scrypt needs openssl,
sha2 is always available
"""
),
],
] ]
@@ -730,6 +824,7 @@ def add_cert(ap, cert_path):
ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each --crt-ns") ap2.add_argument("--crt-exact", action="store_true", help="do not add wildcard entries for each --crt-ns")
ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert") ap2.add_argument("--crt-noip", action="store_true", help="do not add autodetected IP addresses into cert")
ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert") ap2.add_argument("--crt-nolo", action="store_true", help="do not add 127.0.0.1 / localhost into cert")
ap2.add_argument("--crt-nohn", action="store_true", help="do not add mDNS names / hostname into cert")
ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert") ap2.add_argument("--crt-dir", metavar="PATH", default=cert_dir, help="where to save the CA cert")
ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650, help="ca-certificate expiration time in days") ap2.add_argument("--crt-cdays", metavar="D", type=float, default=3650, help="ca-certificate expiration time in days")
ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365, help="server-cert expiration time in days") ap2.add_argument("--crt-sdays", metavar="D", type=float, default=365, help="server-cert expiration time in days")
@@ -812,6 +907,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")
@@ -822,6 +924,7 @@ def add_hooks(ap):
ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete") ap2.add_argument("--xbd", metavar="CMD", type=u, action="append", help="execute CMD before a file delete")
ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete") ap2.add_argument("--xad", metavar="CMD", type=u, action="append", help="execute CMD after a file delete")
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message") ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
ap2.add_argument("--xban", metavar="CMD", type=u, action="append", help="execute CMD if someone gets banned (pw/404)")
def add_yolo(ap): def add_yolo(ap):
@@ -844,7 +947,7 @@ def add_optouts(ap):
ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (as specified by the 'lifetime' volflag)") ap2.add_argument("--no-lifetime", action="store_true", help="disable automatic deletion of uploads after a certain time (as specified by the 'lifetime' volflag)")
def add_safety(ap, fk_salt): def add_safety(ap):
ap2 = ap.add_argument_group('safety options') ap2 = ap.add_argument_group('safety options')
ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js") ap2.add_argument("-s", action="count", default=0, help="increase safety: Disable thumbnails / potentially dangerous software (ffmpeg/pillow/vips), hide partial uploads, avoid crawlers.\n └─Alias of\033[32m --dotpart --no-thumb --no-mtag-ff --no-robots --force-js")
ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 --ban-404=50,60,1440 -nih") ap2.add_argument("-ss", action="store_true", help="further increase safety: Prevent js-injection, accidental move/delete, broken symlinks, webdav, 404 on 403, ban on excessive 404s.\n └─Alias of\033[32m -s --unpost=0 --no-del --no-mv --hardlink --vague-403 --ban-404=50,60,1440 -nih")
@@ -852,8 +955,6 @@ def add_safety(ap, fk_salt):
ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m; example [\033[32m**,*,ln,p,r\033[0m]") ap2.add_argument("--ls", metavar="U[,V[,F]]", type=u, help="do a sanity/safety check of all volumes on startup; arguments \033[33mUSER\033[0m,\033[33mVOL\033[0m,\033[33mFLAGS\033[0m; example [\033[32m**,*,ln,p,r\033[0m]")
ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)") ap2.add_argument("--xvol", action="store_true", help="never follow symlinks leaving the volume root, unless the link is into another volume where the user has similar access (volflag=xvol)")
ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)") ap2.add_argument("--xdev", action="store_true", help="stay within the filesystem of the volume root; do not descend into other devices (symlink or bind-mount to another HDD, ...) (volflag=xdev)")
ap2.add_argument("--salt", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files -- this one DOES matter")
ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles") ap2.add_argument("--no-dot-mv", action="store_true", help="disallow moving dotfiles; makes it impossible to move folders containing dotfiles")
ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile") ap2.add_argument("--no-dot-ren", action="store_true", help="disallow renaming dotfiles; makes it impossible to make something a dotfile")
ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings") ap2.add_argument("--no-logues", action="store_true", help="disable rendering .prologue/.epilogue.html into directory listings")
@@ -870,6 +971,16 @@ def add_safety(ap, fk_salt):
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like described in --acao)") ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like described in --acao)")
def add_salt(ap, fk_salt, ah_salt):
ap2 = ap.add_argument_group('salting options')
ap2.add_argument("--ah-alg", metavar="ALG", type=u, default="none", help="account-pw hashing algorithm; one of these, best to worst: argon2 scrypt sha2 none (each optionally followed by alg-specific comma-sep. config)")
ap2.add_argument("--ah-salt", metavar="SALT", type=u, default=ah_salt, help="account-pw salt; ignored if --ah-alg is none (default)")
ap2.add_argument("--ah-gen", metavar="PW", type=u, default="", help="generate hashed password for \033[33mPW\033[0m, or read passwords from STDIN if \033[33mPW\033[0m is [\033[32m-\033[0m]")
ap2.add_argument("--ah-cli", action="store_true", help="interactive shell which hashes passwords without ever storing or displaying the original passwords")
ap2.add_argument("--fk-salt", metavar="SALT", type=u, default=fk_salt, help="per-file accesskey salt; used to generate unpredictable URLs for hidden files")
ap2.add_argument("--warksalt", metavar="SALT", type=u, default="hunter2", help="up2k file-hash salt; serves no purpose, no reason to change this (but delete all databases if you do)")
def add_shutdown(ap): def add_shutdown(ap):
ap2 = ap.add_argument_group('shutdown options') ap2 = ap.add_argument_group('shutdown options')
ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints") ap2.add_argument("--ign-ebind", action="store_true", help="continue running even if it's impossible to listen on some of the requested endpoints")
@@ -902,10 +1013,10 @@ def add_thumbnail(ap):
ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)") ap2.add_argument("--no-thumb", action="store_true", help="disable all thumbnails (volflag=dthumb)")
ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)") ap2.add_argument("--no-vthumb", action="store_true", help="disable video thumbnails (volflag=dvthumb)")
ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)") ap2.add_argument("--no-athumb", action="store_true", help="disable audio thumbnails (spectrograms) (volflag=dathumb)")
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res") ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails") ap2.add_argument("--th-mt", metavar="CORES", type=int, default=CORES, help="num cpu cores to use for generating thumbnails")
ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds") ap2.add_argument("--th-convt", metavar="SEC", type=float, default=60, help="conversion timeout in seconds (volflag=convt)")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image") ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image (volflag=nocrop)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="image decoders, in order of preference")
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output") ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output") ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
@@ -968,7 +1079,7 @@ def add_db_metadata(ap):
ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/ffprobe parsers") ap2.add_argument("--mtag-vv", action="store_true", help="debug mtp settings and mutagen/ffprobe parsers")
ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping") ap2.add_argument("-mtm", metavar="M=t,t,t", type=u, action="append", help="add/replace metadata mapping")
ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)", ap2.add_argument("-mte", metavar="M,M,M", type=u, help="tags to index/display (comma-sep.)",
default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash") default="circle,album,.tn,artist,title,.bpm,key,.dur,.q,.vq,.aq,vc,ac,fmt,res,.fps,ahash,vhash,up_ip,.up_at")
ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)", ap2.add_argument("-mth", metavar="M,M,M", type=u, help="tags to hide by default (comma-sep.)",
default=".vq,.aq,vc,ac,fmt,res,.fps") default=".vq,.aq,vc,ac,fmt,res,.fps")
ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file") ap2.add_argument("-mtp", metavar="M=[f,]BIN", type=u, action="append", help="read tag M using program BIN to parse the file")
@@ -980,7 +1091,7 @@ def add_ui(ap, retry):
ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language") ap2.add_argument("--lang", metavar="LANG", type=u, default="eng", help="language")
ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use") ap2.add_argument("--theme", metavar="NUM", type=int, default=0, help="default theme to use")
ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed") ap2.add_argument("--themes", metavar="NUM", type=int, default=8, help="number of themes installed")
ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching REGEX in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\.(js|css)$\033[0m] (volflag=unlist)") ap2.add_argument("--unlist", metavar="REGEX", type=u, default="", help="don't show files matching REGEX in file list. Purely cosmetic! Does not affect API calls, just the browser. Example: [\033[32m\\.(js|css)$\033[0m] (volflag=unlist)")
ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable") ap2.add_argument("--favico", metavar="TXT", type=u, default="c 000 none" if retry else "🎉 000 none", help="\033[33mfavicon-text\033[0m [ \033[33mforeground\033[0m [ \033[33mbackground\033[0m ] ], set blank to disable")
ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])") ap2.add_argument("--mpmc", metavar="URL", type=u, default="", help="change the mediaplayer-toggle mouse cursor; URL to a folder with {2..5}.png inside (or disable with [\033[32m.\033[0m])")
ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include") ap2.add_argument("--js-browser", metavar="L", type=u, help="URL to additional JS to include")
@@ -1030,6 +1141,7 @@ def run_argparse(
cert_path = os.path.join(E.cfg, "cert.pem") cert_path = os.path.join(E.cfg, "cert.pem")
fk_salt = get_fk_salt(cert_path) fk_salt = get_fk_salt(cert_path)
ah_salt = get_ah_salt()
hcores = min(CORES, 4) # optimal on py3.11 @ r5-4500U hcores = min(CORES, 4) # optimal on py3.11 @ r5-4500U
@@ -1053,10 +1165,12 @@ def run_argparse(
add_ftp(ap) add_ftp(ap)
add_webdav(ap) add_webdav(ap)
add_smb(ap) add_smb(ap)
add_safety(ap, fk_salt) add_safety(ap)
add_salt(ap, fk_salt, ah_salt)
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)
@@ -1138,16 +1252,22 @@ def main(argv: Optional[list[str]] = None) -> None:
supp = args_from_cfg(v) supp = args_from_cfg(v)
argv.extend(supp) argv.extend(supp)
deprecated: list[tuple[str, str]] = [] deprecated: list[tuple[str, str]] = [("--salt", "--warksalt")]
for dk, nk in deprecated: for dk, nk in deprecated:
try: idx = -1
idx = argv.index(dk) ov = ""
except: for n, k in enumerate(argv):
if k == dk or k.startswith(dk + "="):
idx = n
if "=" in k:
ov = "=" + k.split("=", 1)[1]
if idx < 0:
continue continue
msg = "\033[1;31mWARNING:\033[0;1m\n {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m" msg = "\033[1;31mWARNING:\033[0;1m\n {} \033[0;33mwas replaced with\033[0;1m {} \033[0;33mand will be removed\n\033[0m"
lprint(msg.format(dk, nk)) lprint(msg.format(dk, nk))
argv[idx] = nk argv[idx] = nk + ov
time.sleep(2) time.sleep(2)
da = len(argv) == 1 da = len(argv) == 1
@@ -1198,7 +1318,7 @@ def main(argv: Optional[list[str]] = None) -> None:
elif not al.no_ansi: elif not al.no_ansi:
al.ansi = VT100 al.ansi = VT100
if WINDOWS and not al.keep_qem: if WINDOWS and not al.keep_qem and not al.ah_cli:
try: try:
disable_quickedit() disable_quickedit()
except: except:
@@ -1223,11 +1343,9 @@ def main(argv: Optional[list[str]] = None) -> None:
if re.match("c[^,]", opt): if re.match("c[^,]", opt):
mod = True mod = True
na.append("c," + opt[1:]) na.append("c," + opt[1:])
elif re.sub("^[rwmdgG]*", "", opt) and "," not in opt: elif re.sub("^[rwmdgGa]*", "", opt) and "," not in opt:
mod = True mod = True
perm = opt[0] perm = opt[0]
if perm == "a":
perm = "rw"
na.append(perm + "," + opt[1:]) na.append(perm + "," + opt[1:])
else: else:
na.append(opt) na.append(opt)

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 7, 4) VERSION = (1, 8, 2)
CODENAME = "unlinked" CODENAME = "argon"
BUILD_DT = (2023, 6, 11) BUILD_DT = (2023, 7, 14)
S_VERSION = ".".join(map(str, VERSION)) S_VERSION = ".".join(map(str, VERSION))
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT) S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)

View File

@@ -15,6 +15,7 @@ from datetime import datetime
from .__init__ import ANYWIN, TYPE_CHECKING, WINDOWS from .__init__ import ANYWIN, TYPE_CHECKING, WINDOWS
from .bos import bos from .bos import bos
from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap from .cfg import flagdescs, permdescs, vf_bmap, vf_cmap, vf_vmap
from .pwhash import PWHash
from .util import ( from .util import (
IMPLICATIONS, IMPLICATIONS,
META_NOBOTS, META_NOBOTS,
@@ -40,7 +41,10 @@ if True: # pylint: disable=using-constant-test
from .util import NamedLogger, RootLogger from .util import NamedLogger, RootLogger
if TYPE_CHECKING: if TYPE_CHECKING:
pass from .broker_mp import BrokerMp
from .broker_thr import BrokerThr
from .broker_util import BrokerCli
# Vflags: TypeAlias = dict[str, str | bool | float | list[str]] # Vflags: TypeAlias = dict[str, str | bool | float | list[str]]
# Vflags: TypeAlias = dict[str, Any] # Vflags: TypeAlias = dict[str, Any]
# Mflags: TypeAlias = dict[str, Vflags] # Mflags: TypeAlias = dict[str, Vflags]
@@ -58,6 +62,7 @@ class AXS(object):
udel: Optional[Union[list[str], set[str]]] = None, udel: Optional[Union[list[str], set[str]]] = None,
uget: Optional[Union[list[str], set[str]]] = None, uget: Optional[Union[list[str], set[str]]] = None,
upget: Optional[Union[list[str], set[str]]] = None, upget: Optional[Union[list[str], set[str]]] = None,
uadmin: Optional[Union[list[str], set[str]]] = None,
) -> None: ) -> None:
self.uread: set[str] = set(uread or []) self.uread: set[str] = set(uread or [])
self.uwrite: set[str] = set(uwrite or []) self.uwrite: set[str] = set(uwrite or [])
@@ -65,14 +70,11 @@ class AXS(object):
self.udel: set[str] = set(udel or []) self.udel: set[str] = set(udel or [])
self.uget: set[str] = set(uget or []) self.uget: set[str] = set(uget or [])
self.upget: set[str] = set(upget or []) self.upget: set[str] = set(upget or [])
self.uadmin: set[str] = set(uadmin or [])
def __repr__(self) -> str: def __repr__(self) -> str:
return "AXS(%s)" % ( ks = "uread uwrite umove udel uget upget uadmin".split()
", ".join( return "AXS(%s)" % (", ".join("%s=%r" % (k, self.__dict__[k]) for k in ks),)
"%s=%r" % (k, self.__dict__[k])
for k in "uread uwrite umove udel uget upget".split()
)
)
class Lim(object): class Lim(object):
@@ -90,6 +92,8 @@ class Lim(object):
self.dfl = 0 # free disk space limit self.dfl = 0 # free disk space limit
self.dft = 0 # last-measured time self.dft = 0 # last-measured time
self.dfv = 0 # currently free self.dfv = 0 # currently free
self.vbmax = 0 # volume bytes max
self.vnmax = 0 # volume max num files
self.smin = 0 # filesize min self.smin = 0 # filesize min
self.smax = 0 # filesize max self.smax = 0 # filesize max
@@ -119,8 +123,11 @@ class Lim(object):
ip: str, ip: str,
rem: str, rem: str,
sz: int, sz: int,
ptop: str,
abspath: str, abspath: str,
broker: Optional[Union["BrokerCli", "BrokerMp", "BrokerThr"]] = None,
reg: Optional[dict[str, dict[str, Any]]] = None, reg: Optional[dict[str, dict[str, Any]]] = None,
volgetter: str = "up2k.get_volsize",
) -> tuple[str, str]: ) -> tuple[str, str]:
if reg is not None and self.reg is None: if reg is not None and self.reg is None:
self.reg = reg self.reg = reg
@@ -131,6 +138,7 @@ class Lim(object):
self.chk_rem(rem) self.chk_rem(rem)
if sz != -1: if sz != -1:
self.chk_sz(sz) self.chk_sz(sz)
self.chk_vsz(broker, ptop, sz, volgetter)
self.chk_df(abspath, sz) # side effects; keep last-ish self.chk_df(abspath, sz) # side effects; keep last-ish
ap2, vp2 = self.rot(abspath) ap2, vp2 = self.rot(abspath)
@@ -146,6 +154,25 @@ class Lim(object):
if self.smax and sz > self.smax: if self.smax and sz > self.smax:
raise Pebkac(400, "file too big") raise Pebkac(400, "file too big")
def chk_vsz(
self,
broker: Optional[Union["BrokerCli", "BrokerMp", "BrokerThr"]],
ptop: str,
sz: int,
volgetter: str = "up2k.get_volsize",
) -> None:
if not broker or not self.vbmax + self.vnmax:
return
x = broker.ask(volgetter, ptop)
nbytes, nfiles = x.get()
if self.vbmax and self.vbmax < nbytes + sz:
raise Pebkac(400, "volume has exceeded max size")
if self.vnmax and self.vnmax < nfiles + 1:
raise Pebkac(400, "volume has exceeded max num.files")
def chk_df(self, abspath: str, sz: int, already_written: bool = False) -> None: def chk_df(self, abspath: str, sz: int, already_written: bool = False) -> None:
if not self.dfl: if not self.dfl:
return return
@@ -266,7 +293,7 @@ class Lim(object):
self.bupc[ip] = mark self.bupc[ip] = mark
if mark >= self.bmax: if mark >= self.bmax:
raise Pebkac(429, "ingress saturated") raise Pebkac(429, "upload size limit exceeded")
class VFS(object): class VFS(object):
@@ -406,8 +433,8 @@ class VFS(object):
def can_access( def can_access(
self, vpath: str, uname: str self, vpath: str, uname: str
) -> tuple[bool, bool, bool, bool, bool, bool]: ) -> tuple[bool, bool, bool, bool, bool, bool, bool]:
"""can Read,Write,Move,Delete,Get,Upget""" """can Read,Write,Move,Delete,Get,Upget,Admin"""
if vpath: if vpath:
vn, _ = self._find(undot(vpath)) vn, _ = self._find(undot(vpath))
else: else:
@@ -421,6 +448,7 @@ class VFS(object):
uname in c.udel or "*" in c.udel, uname in c.udel or "*" in c.udel,
uname in c.uget or "*" in c.uget, uname in c.uget or "*" in c.uget,
uname in c.upget or "*" in c.upget, uname in c.upget or "*" in c.upget,
uname in c.uadmin or "*" in c.uadmin,
) )
def get( def get(
@@ -729,6 +757,7 @@ class AuthSrv(object):
warn_anonwrite: bool = True, warn_anonwrite: bool = True,
dargs: Optional[argparse.Namespace] = None, dargs: Optional[argparse.Namespace] = None,
) -> None: ) -> None:
self.ah = PWHash(args)
self.args = args self.args = args
self.dargs = dargs or args self.dargs = dargs or args
self.log_func = log_func self.log_func = log_func
@@ -914,7 +943,7 @@ class AuthSrv(object):
try: try:
self._l(ln, 5, "volume access config:") self._l(ln, 5, "volume access config:")
sk, sv = ln.split(":") sk, sv = ln.split(":")
if re.sub("[rwmdgG]", "", sk) or not sk: if re.sub("[rwmdgGa]", "", sk) or not sk:
err = "invalid accs permissions list; " err = "invalid accs permissions list; "
raise Exception(err) raise Exception(err)
if " " in re.sub(", *", "", sv).strip(): if " " in re.sub(", *", "", sv).strip():
@@ -923,7 +952,7 @@ class AuthSrv(object):
self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp]) self._read_vol_str(sk, sv.replace(" ", ""), daxs[vp], mflags[vp])
continue continue
except: except:
err += "accs entries must be 'rwmdgG: user1, user2, ...'" err += "accs entries must be 'rwmdgGa: user1, user2, ...'"
raise Exception(err) raise Exception(err)
if cat == catf: if cat == catf:
@@ -959,7 +988,7 @@ class AuthSrv(object):
def _read_vol_str( def _read_vol_str(
self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any] self, lvl: str, uname: str, axs: AXS, flags: dict[str, Any]
) -> None: ) -> None:
if lvl.strip("crwmdgG"): if lvl.strip("crwmdgGa"):
raise Exception("invalid volflag: {},{}".format(lvl, uname)) raise Exception("invalid volflag: {},{}".format(lvl, uname))
if lvl == "c": if lvl == "c":
@@ -991,6 +1020,7 @@ class AuthSrv(object):
("g", axs.uget), ("g", axs.uget),
("G", axs.uget), ("G", axs.uget),
("G", axs.upget), ("G", axs.upget),
("a", axs.uadmin),
]: # b bb bbb ]: # b bb bbb
if ch in lvl: if ch in lvl:
if un == "*": if un == "*":
@@ -1017,7 +1047,8 @@ 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(): zs = "mtp on403 on404 xbu xau xiu xbr xar xbd xad xm xban"
if name not in zs.split():
if value is True: if value is True:
t = "└─add volflag [{}] = {} ({})" t = "└─add volflag [{}] = {} ({})"
else: else:
@@ -1062,7 +1093,7 @@ class AuthSrv(object):
if self.args.v: if self.args.v:
# list of src:dst:permset:permset:... # list of src:dst:permset:permset:...
# permset is <rwmdgG>[,username][,username] or <c>,<flag>[=args] # permset is <rwmdgGa>[,username][,username] or <c>,<flag>[=args]
for v_str in self.args.v: for v_str in self.args.v:
m = re_vol.match(v_str) m = re_vol.match(v_str)
if not m: if not m:
@@ -1106,6 +1137,8 @@ class AuthSrv(object):
self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns))) self.log("\n{0}\n{1}{0}".format(t, "\n".join(slns)))
raise raise
self.setup_pwhash(acct)
# case-insensitive; normalize # case-insensitive; normalize
if WINDOWS: if WINDOWS:
cased = {} cased = {}
@@ -1164,7 +1197,15 @@ class AuthSrv(object):
all_users = {} all_users = {}
missing_users = {} missing_users = {}
for axs in daxs.values(): for axs in daxs.values():
for d in [axs.uread, axs.uwrite, axs.umove, axs.udel, axs.uget, axs.upget]: for d in [
axs.uread,
axs.uwrite,
axs.umove,
axs.udel,
axs.uget,
axs.upget,
axs.uadmin,
]:
for usr in d: for usr in d:
all_users[usr] = 1 all_users[usr] = 1
if usr != "*" and usr not in acct: if usr != "*" and usr not in acct:
@@ -1290,6 +1331,16 @@ class AuthSrv(object):
use = True use = True
lim.bmax, lim.bwin = [unhumanize(x) for x in zs.split(",")] lim.bmax, lim.bwin = [unhumanize(x) for x in zs.split(",")]
zs = vol.flags.get("vmaxb")
if zs:
use = True
lim.vbmax = unhumanize(zs)
zs = vol.flags.get("vmaxn")
if zs:
use = True
lim.vnmax = unhumanize(zs)
if use: if use:
vol.lim = lim vol.lim = lim
@@ -1378,6 +1429,10 @@ class AuthSrv(object):
if k in vol.flags: if k in vol.flags:
vol.flags[k] = int(vol.flags[k]) vol.flags[k] = int(vol.flags[k])
for k in ("convt",):
if k in vol.flags:
vol.flags[k] = float(vol.flags[k])
for k1, k2 in IMPLICATIONS: for k1, k2 in IMPLICATIONS:
if k1 in vol.flags: if k1 in vol.flags:
vol.flags[k2] = True vol.flags[k2] = True
@@ -1403,8 +1458,8 @@ class AuthSrv(object):
vol.flags["mth"] = self.args.mth vol.flags["mth"] = self.args.mth
# 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 xban".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:
@@ -1426,6 +1481,10 @@ class AuthSrv(object):
hfs = [x for x in hfs if x != "f"] hfs = [x for x in hfs if x != "f"]
ocmd = ",".join(hfs + [cmd]) ocmd = ",".join(hfs + [cmd])
if "c" not in hfs and "f" not in hfs and hn == "xban":
hfs = ["c"] + hfs
ocmd = ",".join(hfs + [cmd])
ncmds.append(ocmd) ncmds.append(ocmd)
vol.flags[hn] = ncmds vol.flags[hn] = ncmds
@@ -1536,6 +1595,10 @@ class AuthSrv(object):
self.log(t, 1) self.log(t, 1)
errors = True errors = True
if self.args.smb and self.ah.on and acct:
self.log("--smb can only be used when --ah-alg is none", 1)
errors = True
for vol in vfs.all_vols.values(): for vol in vfs.all_vols.values():
for k in list(vol.flags.keys()): for k in list(vol.flags.keys()):
if re.match("^-[^-]+$", k): if re.match("^-[^-]+$", k):
@@ -1561,6 +1624,7 @@ class AuthSrv(object):
["delete", "udel"], ["delete", "udel"],
[" get", "uget"], [" get", "uget"],
[" upget", "upget"], [" upget", "upget"],
["uadmin", "uadmin"],
]: ]:
u = list(sorted(getattr(zv.axs, attr))) u = list(sorted(getattr(zv.axs, attr)))
u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u) u = ", ".join("\033[35meverybody\033[0m" if x == "*" else x for x in u)
@@ -1605,7 +1669,51 @@ class AuthSrv(object):
self.re_pwd = None self.re_pwd = None
pwds = [re.escape(x) for x in self.iacct.keys()] pwds = [re.escape(x) for x in self.iacct.keys()]
if pwds: if pwds:
self.re_pwd = re.compile("=(" + "|".join(pwds) + ")([]&; ]|$)") if self.ah.on:
zs = r"(\[H\] pw:.*|[?&]pw=)([^&]+)"
else:
zs = r"(\[H\] pw:.*|=)(" + "|".join(pwds) + r")([]&; ]|$)"
self.re_pwd = re.compile(zs)
def setup_pwhash(self, acct: dict[str, str]) -> None:
self.ah = PWHash(self.args)
if not self.ah.on:
return
if self.args.ah_cli:
self.ah.cli()
sys.exit()
elif self.args.ah_gen == "-":
self.ah.stdin()
sys.exit()
elif self.args.ah_gen:
print(self.ah.hash(self.args.ah_gen))
sys.exit()
if not acct:
return
changed = False
for uname, pw in list(acct.items())[:]:
if pw.startswith("+") and len(pw) == 33:
continue
changed = True
hpw = self.ah.hash(pw)
acct[uname] = hpw
t = "hashed password for account {}: {}"
self.log(t.format(uname, hpw), 3)
if not changed:
return
lns = []
for uname, pw in acct.items():
lns.append(" {}: {}".format(uname, pw))
t = "please use the following hashed passwords in your config:\n{}"
self.log(t.format("\n".join(lns)), 3)
def chk_sqlite_threadsafe(self) -> str: def chk_sqlite_threadsafe(self) -> str:
v = SQLITE_VER[-1:] v = SQLITE_VER[-1:]
@@ -1662,10 +1770,19 @@ class AuthSrv(object):
raise Exception("volume not found: " + zs) raise Exception("volume not found: " + zs)
self.log(str({"users": users, "vols": vols, "flags": flags})) self.log(str({"users": users, "vols": vols, "flags": flags}))
t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({})" t = "/{}: read({}) write({}) move({}) del({}) get({}) upget({}) uadmin({})"
for k, zv in self.vfs.all_vols.items(): for k, zv in self.vfs.all_vols.items():
vc = zv.axs vc = zv.axs
vs = [k, vc.uread, vc.uwrite, vc.umove, vc.udel, vc.uget, vc.upget] vs = [
k,
vc.uread,
vc.uwrite,
vc.umove,
vc.udel,
vc.uget,
vc.upget,
vc.uadmin,
]
self.log(t.format(*vs)) self.log(t.format(*vs))
flag_v = "v" in flags flag_v = "v" in flags
@@ -1745,7 +1862,8 @@ 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()) zs = "c ihead mtm mtp on403 on404 xad xar xau xiu xban xbd xbr xbu xm"
lst = set(zs.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
@@ -1804,6 +1922,7 @@ class AuthSrv(object):
"d": "udel", "d": "udel",
"g": "uget", "g": "uget",
"G": "upget", "G": "upget",
"a": "uadmin",
} }
users = {} users = {}
for pkey in perms.values(): for pkey in perms.values():
@@ -2000,7 +2119,7 @@ def upgrade_cfg_fmt(
else: else:
sn = sn.replace(",", ", ") sn = sn.replace(",", ", ")
ret.append(" " + sn) ret.append(" " + sn)
elif sn[:1] in "rwmdgG": elif sn[:1] in "rwmdgGa":
if cat != catx: if cat != catx:
cat = catx cat = catx
ret.append(cat) ret.append(cat)

View File

@@ -9,7 +9,7 @@ import queue
from .__init__ import CORES, TYPE_CHECKING from .__init__ import CORES, TYPE_CHECKING
from .broker_mpw import MpWorker from .broker_mpw import MpWorker
from .broker_util import try_exec from .broker_util import ExceptionalQueue, try_exec
from .util import Daemon, mp from .util import Daemon, mp
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -107,6 +107,19 @@ class BrokerMp(object):
if retq_id: if retq_id:
proc.q_pend.put((retq_id, "retq", rv)) proc.q_pend.put((retq_id, "retq", rv))
def ask(self, dest: str, *args: Any) -> ExceptionalQueue:
# new non-ipc invoking managed service in hub
obj = self.hub
for node in dest.split("."):
obj = getattr(obj, node)
rv = try_exec(True, obj, *args)
retq = ExceptionalQueue(1)
retq.put(rv)
return retq
def say(self, dest: str, *args: Any) -> None: def say(self, dest: str, *args: Any) -> None:
""" """
send message to non-hub component in other process, send message to non-hub component in other process,

View File

@@ -1,16 +1,18 @@
import os
import errno
import time
import json
import shutil
import filecmp
import calendar import calendar
import errno
import filecmp
import json
import os
import shutil
import time
from .util import runcmd, Netdev from .util import Netdev, runcmd
HAVE_CFSSL = True HAVE_CFSSL = True
if True: # pylint: disable=using-constant-test
from .util import RootLogger
def ensure_cert(log: "RootLogger", args) -> None: def ensure_cert(log: "RootLogger", args) -> None:
""" """
@@ -119,6 +121,9 @@ def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
names.append(ip.split("/")[0]) names.append(ip.split("/")[0])
if args.crt_nolo: if args.crt_nolo:
names = [x for x in names if x not in ("localhost", "127.0.0.1", "::1")] names = [x for x in names if x not in ("localhost", "127.0.0.1", "::1")]
if not args.crt_nohn:
names.append(args.name)
names.append(args.name + ".local")
if not names: if not names:
names = ["127.0.0.1"] names = ["127.0.0.1"]
if "127.0.0.1" in names or "::1" in names: if "127.0.0.1" in names or "::1" in names:

View File

@@ -13,6 +13,7 @@ def vf_bmap() -> dict[str, str]:
"no_dedup": "copydupes", "no_dedup": "copydupes",
"no_dupe": "nodupe", "no_dupe": "nodupe",
"no_forget": "noforget", "no_forget": "noforget",
"th_no_crop": "nocrop",
"dav_auth": "davauth", "dav_auth": "davauth",
"dav_rt": "davrt", "dav_rt": "davrt",
} }
@@ -40,8 +41,8 @@ def vf_bmap() -> dict[str, str]:
def vf_vmap() -> dict[str, str]: def vf_vmap() -> dict[str, str]:
"""argv-to-volflag: simple values""" """argv-to-volflag: simple values"""
ret = {} ret = {"th_convt": "convt", "th_size": "thsize"}
for k in ("lg_sbf", "md_sbf", "unlist"): for k in ("dbd", "lg_sbf", "md_sbf", "nrand", "unlist"):
ret[k] = k ret[k] = k
return ret return ret
@@ -49,7 +50,7 @@ def vf_vmap() -> dict[str, str]:
def vf_cmap() -> dict[str, str]: def vf_cmap() -> dict[str, str]:
"""argv-to-volflag: complex/lists""" """argv-to-volflag: complex/lists"""
ret = {} ret = {}
for k in ("dbd", "html_head", "mte", "mth", "nrand"): for k in ("html_head", "mte", "mth"):
ret[k] = k ret[k] = k
return ret return ret
@@ -78,7 +79,9 @@ flagcats = {
}, },
"upload rules": { "upload rules": {
"maxn=250,600": "max 250 uploads over 15min", "maxn=250,600": "max 250 uploads over 15min",
"maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g)", "maxb=1g,300": "max 1 GiB over 5min (suffixes: b, k, m, g, t)",
"vmaxb=1g": "total volume size max 1 GiB (suffixes: b, k, m, g, t)",
"vmaxn=4k": "max 4096 files in volume (suffixes: b, k, m, g, t)",
"rand": "force randomized filenames, 9 chars long by default", "rand": "force randomized filenames, 9 chars long by default",
"nrand=N": "randomized filenames are N chars long", "nrand=N": "randomized filenames are N chars long",
"sz=1k-3m": "allow filesizes between 1 KiB and 3MiB", "sz=1k-3m": "allow filesizes between 1 KiB and 3MiB",
@@ -122,6 +125,13 @@ flagcats = {
"dvthumb": "disables video thumbnails", "dvthumb": "disables video thumbnails",
"dathumb": "disables audio thumbnails (spectrograms)", "dathumb": "disables audio thumbnails (spectrograms)",
"dithumb": "disables image thumbnails", "dithumb": "disables image thumbnails",
"thsize": "thumbnail res; WxH",
"nocrop": "disable center-cropping",
"convt": "conversion timeout in seconds",
},
"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",
@@ -132,6 +142,7 @@ flagcats = {
"xbd=CMD": "execute CMD before a file delete", "xbd=CMD": "execute CMD before a file delete",
"xad=CMD": "execute CMD after a file delete", "xad=CMD": "execute CMD after a file delete",
"xm=CMD": "execute CMD on message", "xm=CMD": "execute CMD on message",
"xban=CMD": "execute CMD if someone gets banned",
}, },
"client and ux": { "client and ux": {
"grid": "show grid/thumbnails by default", "grid": "show grid/thumbnails by default",
@@ -145,6 +156,7 @@ flagcats = {
"sb_lg": "enable js sandbox for prologue/epilogue (default)", "sb_lg": "enable js sandbox for prologue/epilogue (default)",
"md_sbf": "list of markdown-sandbox safeguards to disable", "md_sbf": "list of markdown-sandbox safeguards to disable",
"lg_sbf": "list of *logue-sandbox safeguards to disable", "lg_sbf": "list of *logue-sandbox safeguards to disable",
"nohtml": "return html and markdown as text/html",
}, },
"others": { "others": {
"fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission', "fk=8": 'generates per-file accesskeys,\nwhich will then be required at the "g" permission',

View File

@@ -79,10 +79,13 @@ class FtpAuth(DummyAuthorizer):
raise AuthenticationFailed("banned") raise AuthenticationFailed("banned")
asrv = self.hub.asrv asrv = self.hub.asrv
if username == "anonymous": uname = "*"
uname = "*" if username != "anonymous":
else: for zs in (password, username):
uname = asrv.iacct.get(password, "") or asrv.iacct.get(username, "") or "*" zs = asrv.iacct.get(asrv.ah.hash(zs), "")
if zs:
uname = zs
break
if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)): if not uname or not (asrv.vfs.aread.get(uname) or asrv.vfs.awrite.get(uname)):
g = self.hub.gpwd g = self.hub.gpwd
@@ -131,6 +134,7 @@ class FtpFs(AbstractedFS):
self.can_read = self.can_write = self.can_move = False self.can_read = self.can_write = self.can_move = False
self.can_delete = self.can_get = self.can_upget = False self.can_delete = self.can_get = self.can_upget = False
self.can_admin = False
self.listdirinfo = self.listdir self.listdirinfo = self.listdir
self.chdir(".") self.chdir(".")
@@ -165,7 +169,7 @@ class FtpFs(AbstractedFS):
if not avfs: if not avfs:
raise FSE(t.format(vpath), 1) raise FSE(t.format(vpath), 1)
cr, cw, cm, cd, _, _ = avfs.can_access("", self.h.uname) cr, cw, cm, cd, _, _, _ = avfs.can_access("", self.h.uname)
if r and not cr or w and not cw or m and not cm or d and not cd: if r and not cr or w and not cw or m and not cm or d and not cd:
raise FSE(t.format(vpath), 1) raise FSE(t.format(vpath), 1)
@@ -240,6 +244,7 @@ class FtpFs(AbstractedFS):
self.can_delete, self.can_delete,
self.can_get, self.can_get,
self.can_upget, self.can_upget,
self.can_admin,
) = avfs.can_access("", self.h.uname) ) = avfs.can_access("", self.h.uname)
def mkdir(self, path: str) -> None: def mkdir(self, path: str) -> None:

View File

@@ -42,6 +42,7 @@ from .util import (
Pebkac, Pebkac,
UnrecvEOF, UnrecvEOF,
alltrace, alltrace,
absreal,
atomic_move, atomic_move,
exclude_dotfiles, exclude_dotfiles,
fsenc, fsenc,
@@ -58,6 +59,7 @@ from .util import (
html_escape, html_escape,
humansize, humansize,
ipnorm, ipnorm,
loadpy,
min_ex, min_ex,
quotep, quotep,
rand_name, rand_name,
@@ -136,6 +138,8 @@ class HttpCli(object):
self.uparam: dict[str, str] = {} self.uparam: dict[str, str] = {}
self.cookies: dict[str, str] = {} self.cookies: dict[str, str] = {}
self.avn: Optional[VFS] = None self.avn: Optional[VFS] = None
self.vn = self.asrv.vfs
self.rem = " "
self.vpath = " " self.vpath = " "
self.uname = " " self.uname = " "
self.pw = " " self.pw = " "
@@ -152,6 +156,7 @@ class HttpCli(object):
self.can_delete = False self.can_delete = False
self.can_get = False self.can_get = False
self.can_upget = False self.can_upget = False
self.can_admin = False
# post # post
self.parser: Optional[MultipartParser] = None self.parser: Optional[MultipartParser] = None
# end placeholders # end placeholders
@@ -173,13 +178,16 @@ class HttpCli(object):
def log(self, msg: str, c: Union[int, str] = 0) -> None: def log(self, msg: str, c: Union[int, str] = 0) -> None:
ptn = self.asrv.re_pwd ptn = self.asrv.re_pwd
if ptn and ptn.search(msg): if ptn and ptn.search(msg):
msg = ptn.sub(self.unpwd, msg) if self.asrv.ah.on:
msg = ptn.sub("\033[7m pw \033[27m", msg)
else:
msg = ptn.sub(self.unpwd, msg)
self.log_func(self.log_src, msg, c) self.log_func(self.log_src, msg, c)
def unpwd(self, m: Match[str]) -> str: def unpwd(self, m: Match[str]) -> str:
a, b = m.groups() a, b, c = m.groups()
return "=\033[7m {} \033[27m{}".format(self.asrv.iacct[a], b) return "{}\033[7m {} \033[27m{}".format(a, self.asrv.iacct[b], c)
def _check_nonfatal(self, ex: Pebkac, post: bool) -> bool: def _check_nonfatal(self, ex: Pebkac, post: bool) -> bool:
if post: if post:
@@ -383,13 +391,14 @@ class HttpCli(object):
zs = base64.b64decode(zb).decode("utf-8") zs = base64.b64decode(zb).decode("utf-8")
# try "pwd", "x:pwd", "pwd:x" # try "pwd", "x:pwd", "pwd:x"
for bauth in [zs] + zs.split(":", 1)[::-1]: for bauth in [zs] + zs.split(":", 1)[::-1]:
if self.asrv.iacct.get(bauth): hpw = self.asrv.ah.hash(bauth)
if self.asrv.iacct.get(hpw):
break break
except: except:
pass pass
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
self.uname = self.asrv.iacct.get(self.pw) or "*" self.uname = self.asrv.iacct.get(self.asrv.ah.hash(self.pw)) or "*"
self.rvol = self.asrv.vfs.aread[self.uname] self.rvol = self.asrv.vfs.aread[self.uname]
self.wvol = self.asrv.vfs.awrite[self.uname] self.wvol = self.asrv.vfs.awrite[self.uname]
self.mvol = self.asrv.vfs.amove[self.uname] self.mvol = self.asrv.vfs.amove[self.uname]
@@ -426,10 +435,13 @@ class HttpCli(object):
self.can_delete, self.can_delete,
self.can_get, self.can_get,
self.can_upget, self.can_upget,
self.can_admin,
) = ( ) = (
avn.can_access("", self.uname) if avn else [False] * 6 avn.can_access("", self.uname) if avn else [False] * 6
) )
self.avn = avn self.avn = avn
self.vn = vn
self.rem = rem
self.s.settimeout(self.args.s_tbody or None) self.s.settimeout(self.args.s_tbody or None)
@@ -565,9 +577,8 @@ class HttpCli(object):
# default to utf8 html if no content-type is set # default to utf8 html if no content-type is set
if not mime: if not mime:
mime = self.out_headers.get("Content-Type", "text/html; charset=utf-8") mime = self.out_headers.get("Content-Type") or "text/html; charset=utf-8"
assert mime
self.out_headers["Content-Type"] = mime self.out_headers["Content-Type"] = mime
for k, zs in list(self.out_headers.items()) + self.out_headerlist: for k, zs in list(self.out_headers.items()) + self.out_headerlist:
@@ -592,8 +603,22 @@ class HttpCli(object):
if g.lim: if g.lim:
bonk, ip = g.bonk(self.ip, self.vpath) bonk, ip = g.bonk(self.ip, self.vpath)
if bonk: if bonk:
self.log("client banned: 404s", 1) xban = self.vn.flags.get("xban")
self.conn.hsrv.bans[ip] = bonk if not xban or not runhook(
self.log,
xban,
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
"404",
):
self.log("client banned: 404s", 1)
self.conn.hsrv.bans[ip] = bonk
if volsan: if volsan:
vols = list(self.asrv.vfs.all_vols.values()) vols = list(self.asrv.vfs.all_vols.values())
@@ -755,7 +780,14 @@ class HttpCli(object):
self.reply(b"", 301, headers=h) self.reply(b"", 301, headers=h)
return True return True
static_path = os.path.join(self.E.mod, "web/", self.vpath[5:]) path_base = os.path.join(self.E.mod, "web")
static_path = absreal(os.path.join(path_base, self.vpath[5:]))
if not static_path.startswith(path_base):
t = "attempted path traversal [{}] => [{}]"
self.log(t.format(self.vpath, static_path), 1)
self.tx_404()
return False
return self.tx_file(static_path) return self.tx_file(static_path)
if "cf_challenge" in self.uparam: if "cf_challenge" in self.uparam:
@@ -763,11 +795,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 "on403" in self.vn.flags:
ret = self.on40x(self.vn.flags["on403"], self.vn, self.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
self.can_admin = 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()
@@ -1018,9 +1066,6 @@ class HttpCli(object):
from .dxml import mkenod, mktnod, parse_xml from .dxml import mkenod, mktnod, parse_xml
self.asrv.vfs.get(self.vpath, self.uname, False, False)
# abspath = vn.dcanonical(rem)
buf = b"" buf = b""
for rbuf in self.get_body_reader()[0]: for rbuf in self.get_body_reader()[0]:
buf += rbuf buf += rbuf
@@ -1077,8 +1122,7 @@ class HttpCli(object):
from .dxml import mkenod, mktnod, parse_xml from .dxml import mkenod, mktnod, parse_xml
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) abspath = self.vn.dcanonical(self.rem)
abspath = vn.dcanonical(rem)
buf = b"" buf = b""
for rbuf in self.get_body_reader()[0]: for rbuf in self.get_body_reader()[0]:
@@ -1292,20 +1336,17 @@ class HttpCli(object):
plain = zb.decode("utf-8", "replace") plain = zb.decode("utf-8", "replace")
if buf.startswith(b"msg="): if buf.startswith(b"msg="):
plain = plain[4:] plain = plain[4:]
vfs, rem = self.asrv.vfs.get( xm = self.vn.flags.get("xm")
self.vpath, self.uname, False, False
)
xm = vfs.flags.get("xm")
if xm: if xm:
runhook( runhook(
self.log, self.log,
xm, xm,
vfs.canonical(rem), self.vn.canonical(self.rem),
self.vpath, self.vpath,
self.host, self.host,
self.uname, self.uname,
time.time(), time.time(),
len(xm), len(buf),
self.ip, self.ip,
time.time(), time.time(),
plain, plain,
@@ -1358,7 +1399,9 @@ class HttpCli(object):
lim = vfs.get_dbv(rem)[0].lim lim = vfs.get_dbv(rem)[0].lim
fdir = vfs.canonical(rem) fdir = vfs.canonical(rem)
if lim: if lim:
fdir, rem = lim.all(self.ip, rem, remains, fdir) fdir, rem = lim.all(
self.ip, rem, remains, vfs.realpath, fdir, self.conn.hsrv.broker
)
fn = None fn = None
if rem and not self.trailing_slash and not bos.path.isdir(fdir): if rem and not self.trailing_slash and not bos.path.isdir(fdir):
@@ -1491,6 +1534,7 @@ class HttpCli(object):
lim.bup(self.ip, post_sz) lim.bup(self.ip, post_sz)
try: try:
lim.chk_sz(post_sz) lim.chk_sz(post_sz)
lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, post_sz)
except: except:
bos.unlink(path) bos.unlink(path)
raise raise
@@ -1965,7 +2009,7 @@ class HttpCli(object):
return True return True
def get_pwd_cookie(self, pwd: str) -> str: def get_pwd_cookie(self, pwd: str) -> str:
if pwd in self.asrv.iacct: if self.asrv.ah.hash(pwd) in self.asrv.iacct:
msg = "login ok" msg = "login ok"
dur = int(60 * 60 * self.args.logout) dur = int(60 * 60 * self.args.logout)
else: else:
@@ -1974,8 +2018,22 @@ class HttpCli(object):
if g.lim: if g.lim:
bonk, ip = g.bonk(self.ip, pwd) bonk, ip = g.bonk(self.ip, pwd)
if bonk: if bonk:
self.log("client banned: invalid passwords", 1) xban = self.vn.flags.get("xban")
self.conn.hsrv.bans[ip] = bonk if not xban or not runhook(
self.log,
xban,
self.vn.canonical(self.rem),
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
"pw",
):
self.log("client banned: invalid passwords", 1)
self.conn.hsrv.bans[ip] = bonk
msg = "naw dude" msg = "naw dude"
pwd = "x" # nosec pwd = "x" # nosec
@@ -2101,7 +2159,9 @@ class HttpCli(object):
lim = vfs.get_dbv(rem)[0].lim lim = vfs.get_dbv(rem)[0].lim
fdir_base = vfs.canonical(rem) fdir_base = vfs.canonical(rem)
if lim: if lim:
fdir_base, rem = lim.all(self.ip, rem, -1, fdir_base) fdir_base, rem = lim.all(
self.ip, rem, -1, vfs.realpath, fdir_base, self.conn.hsrv.broker
)
upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/") upload_vpath = "{}/{}".format(vfs.vpath, rem).strip("/")
if not nullwrite: if not nullwrite:
bos.makedirs(fdir_base) bos.makedirs(fdir_base)
@@ -2194,6 +2254,7 @@ class HttpCli(object):
try: try:
lim.chk_df(tabspath, sz, True) lim.chk_df(tabspath, sz, True)
lim.chk_sz(sz) lim.chk_sz(sz)
lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)
lim.chk_bup(self.ip) lim.chk_bup(self.ip)
lim.chk_nup(self.ip) lim.chk_nup(self.ip)
except: except:
@@ -2369,7 +2430,7 @@ class HttpCli(object):
fp = vfs.canonical(rp) fp = vfs.canonical(rp)
lim = vfs.get_dbv(rem)[0].lim lim = vfs.get_dbv(rem)[0].lim
if lim: if lim:
fp, rp = lim.all(self.ip, rp, clen, fp) fp, rp = lim.all(self.ip, rp, clen, vfs.realpath, fp, self.conn.hsrv.broker)
bos.makedirs(fp) bos.makedirs(fp)
fp = os.path.join(fp, fn) fp = os.path.join(fp, fn)
@@ -2440,6 +2501,25 @@ class HttpCli(object):
if p_field != "body": if p_field != "body":
raise Pebkac(400, "expected body, got {}".format(p_field)) raise Pebkac(400, "expected body, got {}".format(p_field))
xbu = vfs.flags.get("xbu")
if xbu:
if not runhook(
self.log,
xbu,
fp,
self.vpath,
self.host,
self.uname,
time.time(),
0,
self.ip,
time.time(),
"",
):
t = "save blocked by xbu server config"
self.log(t, 1)
raise Pebkac(403, t)
if bos.path.exists(fp): if bos.path.exists(fp):
bos.unlink(fp) bos.unlink(fp)
@@ -2451,6 +2531,7 @@ class HttpCli(object):
lim.bup(self.ip, sz) lim.bup(self.ip, sz)
try: try:
lim.chk_sz(sz) lim.chk_sz(sz)
lim.chk_vsz(self.conn.hsrv.broker, vfs.realpath, sz)
except: except:
bos.unlink(fp) bos.unlink(fp)
raise raise
@@ -2459,6 +2540,39 @@ class HttpCli(object):
new_lastmod3 = int(new_lastmod * 1000) new_lastmod3 = int(new_lastmod * 1000)
sha512 = sha512[:56] sha512 = sha512[:56]
xau = vfs.flags.get("xau")
if xau and not runhook(
self.log,
xau,
fp,
self.vpath,
self.host,
self.uname,
new_lastmod,
sz,
self.ip,
new_lastmod,
"",
):
t = "save blocked by xau server config"
self.log(t, 1)
os.unlink(fp)
raise Pebkac(403, t)
vfs, rem = vfs.get_dbv(rem)
self.conn.hsrv.broker.say(
"up2k.hash_file",
vfs.realpath,
vfs.vpath,
vfs.flags,
vsplit(rem)[0],
fn,
self.ip,
new_lastmod,
self.uname,
True,
)
response = json.dumps( response = json.dumps(
{"ok": True, "lastmod": new_lastmod3, "size": sz, "sha512": sha512} {"ok": True, "lastmod": new_lastmod3, "size": sz, "sha512": sha512}
) )
@@ -2648,6 +2762,9 @@ class HttpCli(object):
else: else:
mime = guess_mime(req_path) mime = guess_mime(req_path)
if "nohtml" in self.vn.flags and "html" in mime:
mime = "text/plain; charset=utf-8"
self.out_headers["Accept-Ranges"] = "bytes" self.out_headers["Accept-Ranges"] = "bytes"
self.send_headers(length=upper - lower, status=status, mime=mime) self.send_headers(length=upper - lower, status=status, mime=mime)
@@ -2993,6 +3110,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)
@@ -3303,14 +3434,29 @@ class HttpCli(object):
vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)]) vpnodes.append([quotep(vpath) + "/", html_escape(node, crlf=True)])
vn, rem = self.asrv.vfs.get(self.vpath, self.uname, False, False) vn = self.vn
rem = self.rem
abspath = vn.dcanonical(rem) abspath = vn.dcanonical(rem)
dbv, vrem = vn.get_dbv(rem) dbv, vrem = vn.get_dbv(rem)
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/")
@@ -3385,8 +3531,14 @@ class HttpCli(object):
self.log("wrong filekey, want {}, got {}".format(correct, got)) self.log("wrong filekey, want {}, got {}".format(correct, got))
return self.tx_404() return self.tx_404()
if abspath.endswith(".md") and ( if (
"v" in self.uparam or "edit" in self.uparam or "edit2" in self.uparam abspath.endswith(".md")
and "nohtml" not in vn.flags
and (
"v" in self.uparam
or "edit" in self.uparam
or "edit2" in self.uparam
)
): ):
return self.tx_md(abspath) return self.tx_md(abspath)
@@ -3427,6 +3579,8 @@ class HttpCli(object):
perms.append("get") perms.append("get")
if self.can_upget: if self.can_upget:
perms.append("upget") perms.append("upget")
if self.can_admin:
perms.append("admin")
url_suf = self.urlq({}, ["k"]) url_suf = self.urlq({}, ["k"])
is_ls = "ls" in self.uparam is_ls = "ls" in self.uparam
@@ -3678,22 +3832,33 @@ class HttpCli(object):
if vn != dbv: if vn != dbv:
_, rd = vn.get_dbv(rd) _, rd = vn.get_dbv(rd)
erd_efn = (rd, fn)
q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'" q = "select mt.k, mt.v from up inner join mt on mt.w = substr(up.w,1,16) where up.rd = ? and up.fn = ? and +mt.k != 'x'"
try: try:
r = icur.execute(q, (rd, fn)) r = icur.execute(q, erd_efn)
except Exception as ex: except Exception as ex:
if "database is locked" in str(ex): if "database is locked" in str(ex):
break break
try: try:
args = s3enc(idx.mem_cur, rd, fn) erd_efn = s3enc(idx.mem_cur, rd, fn)
r = icur.execute(q, args) r = icur.execute(q, erd_efn)
except: except:
t = "tag read error, {}/{}\n{}" t = "tag read error, {}/{}\n{}"
self.log(t.format(rd, fn, min_ex())) self.log(t.format(rd, fn, min_ex()))
break break
fe["tags"] = {k: v for k, v in r} fe["tags"] = {k: v for k, v in r}
if self.can_admin:
q = "select ip, at from up where rd=? and fn=?"
try:
zs1, zs2 = icur.execute(q, erd_efn).fetchone()
fe["tags"]["up_ip"] = zs1
fe["tags"][".up_at"] = zs2
except:
pass
_ = [tagset.add(k) for k in fe["tags"]] _ = [tagset.add(k) for k in fe["tags"]]
if icur: if icur:

View File

@@ -17,7 +17,9 @@ class Ico(object):
def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]: def get(self, ext: str, as_thumb: bool, chrome: bool) -> tuple[str, bytes]:
"""placeholder to make thumbnails not break""" """placeholder to make thumbnails not break"""
zb = hashlib.sha1(ext.encode("utf-8")).digest()[2:4] bext = ext.encode("ascii", "replace")
ext = bext.decode("utf-8")
zb = hashlib.sha1(bext).digest()[2:4]
if PY2: if PY2:
zb = [ord(x) for x in zb] zb = [ord(x) for x in zb]
@@ -33,7 +35,7 @@ class Ico(object):
h = int(100 / (float(sw) / float(sh))) h = int(100 / (float(sw) / float(sh)))
w = 100 w = 100
if chrome and as_thumb: if chrome:
# cannot handle more than ~2000 unique SVGs # cannot handle more than ~2000 unique SVGs
if HAVE_PIL: if HAVE_PIL:
# svg: 3s, cache: 6s, this: 8s # svg: 3s, cache: 6s, this: 8s
@@ -43,8 +45,19 @@ class Ico(object):
w = 64 w = 64
img = Image.new("RGB", (w, h), "#" + c[:6]) img = Image.new("RGB", (w, h), "#" + c[:6])
pb = ImageDraw.Draw(img) pb = ImageDraw.Draw(img)
tw, th = pb.textsize(ext) try:
pb.text(((w - tw) // 2, (h - th) // 2), ext, fill="#" + c[6:]) _, _, tw, th = pb.textbbox((0, 0), ext)
except:
tw, th = pb.textsize(ext)
tw += len(ext)
cw = tw // len(ext)
x = ((w - tw) // 2) - (cw * 2) // 3
fill = "#" + c[6:]
for ch in ext:
pb.text((x, (h - th) // 2), " %s " % (ch,), fill=fill)
x += cw
img = img.resize((w * 3, h * 3), Image.NEAREST) img = img.resize((w * 3, h * 3), Image.NEAREST)
buf = BytesIO() buf = BytesIO()

145
copyparty/pwhash.py Normal file
View File

@@ -0,0 +1,145 @@
# coding: utf-8
from __future__ import print_function, unicode_literals
import argparse
import base64
import hashlib
import sys
import threading
from .__init__ import unicode
class PWHash(object):
def __init__(self, args: argparse.Namespace):
self.args = args
try:
alg, ac = args.ah_alg.split(",")
except:
alg = args.ah_alg
ac = {}
if alg == "none":
alg = ""
self.alg = alg
self.ac = ac
if not alg:
self.on = False
self.hash = unicode
return
self.on = True
self.salt = args.ah_salt.encode("utf-8")
self.cache: dict[str, str] = {}
self.mutex = threading.Lock()
self.hash = self._cache_hash
if alg == "sha2":
self._hash = self._gen_sha2
elif alg == "scrypt":
self._hash = self._gen_scrypt
elif alg == "argon2":
self._hash = self._gen_argon2
else:
t = "unsupported password hashing algorithm [{}], must be one of these: argon2 scrypt sha2 none"
raise Exception(t.format(alg))
def _cache_hash(self, plain: str) -> str:
with self.mutex:
try:
return self.cache[plain]
except:
pass
if not plain:
return ""
if len(plain) > 255:
raise Exception("password too long")
if len(self.cache) > 9000:
self.cache = {}
ret = self._hash(plain)
self.cache[plain] = ret
return ret
def _gen_sha2(self, plain: str) -> str:
its = int(self.ac[0]) if self.ac else 424242
bplain = plain.encode("utf-8")
ret = b"\n"
for _ in range(its):
ret = hashlib.sha512(self.salt + bplain + ret).digest()
return "+" + base64.urlsafe_b64encode(ret[:24]).decode("utf-8")
def _gen_scrypt(self, plain: str) -> str:
cost = 2 << 13
its = 2
blksz = 8
para = 4
try:
cost = 2 << int(self.ac[0])
its = int(self.ac[1])
blksz = int(self.ac[2])
para = int(self.ac[3])
except:
pass
ret = plain.encode("utf-8")
for _ in range(its):
ret = hashlib.scrypt(ret, salt=self.salt, n=cost, r=blksz, p=para, dklen=24)
return "+" + base64.urlsafe_b64encode(ret).decode("utf-8")
def _gen_argon2(self, plain: str) -> str:
from argon2.low_level import Type as ArgonType
from argon2.low_level import hash_secret
time_cost = 3
mem_cost = 256
parallelism = 4
version = 19
try:
time_cost = int(self.ac[0])
mem_cost = int(self.ac[1])
parallelism = int(self.ac[2])
version = int(self.ac[3])
except:
pass
bplain = plain.encode("utf-8")
bret = hash_secret(
secret=bplain,
salt=self.salt,
time_cost=time_cost,
memory_cost=mem_cost * 1024,
parallelism=parallelism,
hash_len=24,
type=ArgonType.ID,
version=version,
)
ret = bret.split(b"$")[-1].decode("utf-8")
return "+" + ret.replace("/", "_").replace("+", "-")
def stdin(self) -> None:
while True:
ln = sys.stdin.readline().strip()
if not ln:
break
print(self.hash(ln))
def cli(self) -> None:
import getpass
while True:
p1 = getpass.getpass("password> ")
p2 = getpass.getpass("again or just hit ENTER> ")
if p2 and p1 != p2:
print("\033[31minputs don't match; try again\033[0m", file=sys.stderr)
continue
print(self.hash(p1))
print()

View File

@@ -30,6 +30,7 @@ if True: # pylint: disable=using-constant-test
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, EnvParams, unicode
from .authsrv import AuthSrv from .authsrv import AuthSrv
from .cert import ensure_cert
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
from .tcpsrv import TcpSrv from .tcpsrv import TcpSrv
from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv from .th_srv import HAVE_PIL, HAVE_VIPS, HAVE_WEBP, ThumbSrv
@@ -239,7 +240,8 @@ class SvcHub(object):
if args.ftp or args.ftps: if args.ftp or args.ftps:
from .ftpd import Ftpd from .ftpd import Ftpd
self.ftpd = Ftpd(self) self.ftpd: Optional[Ftpd] = None
Daemon(self.start_ftpd, "start_ftpd")
zms += "f" if args.ftp else "F" zms += "f" if args.ftp else "F"
if args.smb: if args.smb:
@@ -269,6 +271,28 @@ class SvcHub(object):
self.broker = Broker(self) self.broker = Broker(self)
def start_ftpd(self) -> None:
time.sleep(30)
if self.ftpd:
return
self.restart_ftpd()
def restart_ftpd(self) -> None:
if not hasattr(self, "ftpd"):
return
from .ftpd import Ftpd
if self.ftpd:
return # todo
if not os.path.exists(self.args.cert):
ensure_cert(self.log, self.args)
self.ftpd = Ftpd(self)
self.log("root", "started FTPd")
def thr_httpsrv_up(self) -> None: def thr_httpsrv_up(self) -> None:
time.sleep(1 if self.args.ign_ebind_all else 5) time.sleep(1 if self.args.ign_ebind_all else 5)
expected = self.broker.num_workers * self.tcpsrv.nsrv expected = self.broker.num_workers * self.tcpsrv.nsrv

View File

@@ -8,8 +8,8 @@ import sys
import time import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .stolen.qrcodegen import QrCode
from .cert import gencert from .cert import gencert
from .stolen.qrcodegen import QrCode
from .util import ( from .util import (
E_ACCESS, E_ACCESS,
E_ADDR_IN_USE, E_ADDR_IN_USE,
@@ -297,6 +297,7 @@ class TcpSrv(object):
self.hub.broker.say("set_netdevs", self.netdevs) self.hub.broker.say("set_netdevs", self.netdevs)
self.hub.start_zeroconf() self.hub.start_zeroconf()
gencert(self.log, self.args, self.netdevs) gencert(self.log, self.args, self.netdevs)
self.hub.restart_ftpd()
def shutdown(self) -> None: def shutdown(self) -> None:
self.stopping = True self.stopping = True

View File

@@ -13,6 +13,7 @@ import time
from queue import Queue from queue import Queue
from .__init__ import ANYWIN, TYPE_CHECKING from .__init__ import ANYWIN, TYPE_CHECKING
from .authsrv import VFS
from .bos import bos from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, ffprobe
from .util import ( from .util import (
@@ -110,8 +111,6 @@ class ThumbSrv(object):
self.args = hub.args self.args = hub.args
self.log_func = hub.log self.log_func = hub.log
res = hub.args.th_size.split("x")
self.res = tuple([int(x) for x in res])
self.poke_cd = Cooldown(self.args.th_poke) self.poke_cd = Cooldown(self.args.th_poke)
self.mutex = threading.Lock() self.mutex = threading.Lock()
@@ -119,7 +118,7 @@ class ThumbSrv(object):
self.stopping = False self.stopping = False
self.nthr = max(1, self.args.th_mt) self.nthr = max(1, self.args.th_mt)
self.q: Queue[Optional[tuple[str, str]]] = Queue(self.nthr * 4) self.q: Queue[Optional[tuple[str, str, VFS]]] = Queue(self.nthr * 4)
for n in range(self.nthr): for n in range(self.nthr):
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr)) Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
@@ -184,6 +183,10 @@ class ThumbSrv(object):
with self.mutex: with self.mutex:
return not self.nthr return not self.nthr
def getres(self, vn: VFS) -> tuple[int, int]:
w, h = vn.flags["thsize"].split("x")
return int(w), int(h)
def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]: def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]:
histpath = self.asrv.vfs.histtab.get(ptop) histpath = self.asrv.vfs.histtab.get(ptop)
if not histpath: if not histpath:
@@ -211,7 +214,13 @@ class ThumbSrv(object):
do_conv = True do_conv = True
if do_conv: if do_conv:
self.q.put((abspath, tpath)) allvols = list(self.asrv.vfs.all_vols.values())
vn = next((x for x in allvols if x.realpath == ptop), None)
if not vn:
self.log("ptop [{}] not in {}".format(ptop, allvols), 3)
vn = self.asrv.vfs.all_aps[0][1]
self.q.put((abspath, tpath, vn))
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6) self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
while not self.stopping: while not self.stopping:
@@ -248,7 +257,7 @@ class ThumbSrv(object):
if not task: if not task:
break break
abspath, tpath = task abspath, tpath, vn = task
ext = abspath.split(".")[-1].lower() ext = abspath.split(".")[-1].lower()
png_ok = False png_ok = False
funs = [] funs = []
@@ -281,7 +290,7 @@ class ThumbSrv(object):
for fun in funs: for fun in funs:
try: try:
fun(abspath, ttpath) fun(abspath, ttpath, vn)
break break
except Exception as ex: except Exception as ex:
msg = "{} could not create thumbnail of {}\n{}" msg = "{} could not create thumbnail of {}\n{}"
@@ -315,9 +324,10 @@ class ThumbSrv(object):
with self.mutex: with self.mutex:
self.nthr -= 1 self.nthr -= 1
def fancy_pillow(self, im: "Image.Image") -> "Image.Image": def fancy_pillow(self, im: "Image.Image", vn: VFS) -> "Image.Image":
# exif_transpose is expensive (loads full image + unconditional copy) # exif_transpose is expensive (loads full image + unconditional copy)
r = max(*self.res) * 2 res = self.getres(vn)
r = max(*res) * 2
im.thumbnail((r, r), resample=Image.LANCZOS) im.thumbnail((r, r), resample=Image.LANCZOS)
try: try:
k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation") k = next(k for k, v in ExifTags.TAGS.items() if v == "Orientation")
@@ -331,23 +341,23 @@ class ThumbSrv(object):
if rot in rots: if rot in rots:
im = im.transpose(rots[rot]) im = im.transpose(rots[rot])
if self.args.th_no_crop: if "nocrop" in vn.flags:
im.thumbnail(self.res, resample=Image.LANCZOS) im.thumbnail(res, resample=Image.LANCZOS)
else: else:
iw, ih = im.size iw, ih = im.size
dw, dh = self.res dw, dh = res
res = (min(iw, dw), min(ih, dh)) res = (min(iw, dw), min(ih, dh))
im = ImageOps.fit(im, res, method=Image.LANCZOS) im = ImageOps.fit(im, res, method=Image.LANCZOS)
return im return im
def conv_pil(self, abspath: str, tpath: str) -> None: def conv_pil(self, abspath: str, tpath: str, vn: VFS) -> None:
with Image.open(fsenc(abspath)) as im: with Image.open(fsenc(abspath)) as im:
try: try:
im = self.fancy_pillow(im) im = self.fancy_pillow(im, vn)
except Exception as ex: except Exception as ex:
self.log("fancy_pillow {}".format(ex), "90") self.log("fancy_pillow {}".format(ex), "90")
im.thumbnail(self.res) im.thumbnail(self.getres(vn))
fmts = ["RGB", "L"] fmts = ["RGB", "L"]
args = {"quality": 40} args = {"quality": 40}
@@ -370,12 +380,12 @@ class ThumbSrv(object):
im.save(tpath, **args) im.save(tpath, **args)
def conv_vips(self, abspath: str, tpath: str) -> None: def conv_vips(self, abspath: str, tpath: str, vn: VFS) -> None:
crops = ["centre", "none"] crops = ["centre", "none"]
if self.args.th_no_crop: if "nocrop" in vn.flags:
crops = ["none"] crops = ["none"]
w, h = self.res w, h = self.getres(vn)
kw = {"height": h, "size": "down", "intent": "relative"} kw = {"height": h, "size": "down", "intent": "relative"}
for c in crops: for c in crops:
@@ -389,8 +399,8 @@ class ThumbSrv(object):
img.write_to_file(tpath, Q=40) img.write_to_file(tpath, Q=40)
def conv_ffmpeg(self, abspath: str, tpath: str) -> None: def conv_ffmpeg(self, abspath: str, tpath: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret: if not ret:
return return
@@ -402,12 +412,13 @@ class ThumbSrv(object):
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
scale = "scale={0}:{1}:force_original_aspect_ratio=" scale = "scale={0}:{1}:force_original_aspect_ratio="
if self.args.th_no_crop: if "nocrop" in vn.flags:
scale += "decrease,setsar=1:1" scale += "decrease,setsar=1:1"
else: else:
scale += "increase,crop={0}:{1},setsar=1:1" scale += "increase,crop={0}:{1},setsar=1:1"
bscale = scale.format(*list(self.res)).encode("utf-8") res = self.getres(vn)
bscale = scale.format(*list(res)).encode("utf-8")
# fmt: off # fmt: off
cmd = [ cmd = [
b"ffmpeg", b"ffmpeg",
@@ -439,11 +450,11 @@ class ThumbSrv(object):
] ]
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd) self._run_ff(cmd, vn)
def _run_ff(self, cmd: list[bytes]) -> None: def _run_ff(self, cmd: list[bytes], vn: VFS) -> None:
# self.log((b" ".join(cmd)).decode("utf-8")) # self.log((b" ".join(cmd)).decode("utf-8"))
ret, _, serr = runcmd(cmd, timeout=self.args.th_convt) ret, _, serr = runcmd(cmd, timeout=vn.flags["convt"])
if not ret: if not ret:
return return
@@ -486,8 +497,8 @@ class ThumbSrv(object):
self.log(t + txt, c=c) self.log(t + txt, c=c)
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1])) raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_waves(self, abspath: str, tpath: str) -> None: def conv_waves(self, abspath: str, tpath: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
@@ -512,10 +523,10 @@ class ThumbSrv(object):
# fmt: on # fmt: on
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd) self._run_ff(cmd, vn)
def conv_spec(self, abspath: str, tpath: str) -> None: def conv_spec(self, abspath: str, tpath: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
@@ -555,13 +566,13 @@ class ThumbSrv(object):
] ]
cmd += [fsenc(tpath)] cmd += [fsenc(tpath)]
self._run_ff(cmd) self._run_ff(cmd, vn)
def conv_opus(self, abspath: str, tpath: str) -> None: def conv_opus(self, abspath: str, tpath: str, vn: VFS) -> None:
if self.args.no_acode: if self.args.no_acode:
raise Exception("disabled in server config") raise Exception("disabled in server config")
ret, _ = ffprobe(abspath, int(self.args.th_convt / 2)) ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret: if "ac" not in ret:
raise Exception("not audio") raise Exception("not audio")
@@ -597,7 +608,7 @@ class ThumbSrv(object):
fsenc(tmp_opus) fsenc(tmp_opus)
] ]
# fmt: on # fmt: on
self._run_ff(cmd) self._run_ff(cmd, vn)
# iOS fails to play some "insufficiently complex" files # iOS fails to play some "insufficiently complex" files
# (average file shorter than 8 seconds), so of course we # (average file shorter than 8 seconds), so of course we
@@ -621,7 +632,7 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd) self._run_ff(cmd, vn)
elif want_caf: elif want_caf:
# simple remux should be safe # simple remux should be safe
@@ -639,7 +650,7 @@ class ThumbSrv(object):
fsenc(tpath) fsenc(tpath)
] ]
# fmt: on # fmt: on
self._run_ff(cmd) self._run_ff(cmd, vn)
if tmp_opus != tpath: if tmp_opus != tpath:
try: try:

View File

@@ -69,7 +69,7 @@ class U2idx(object):
fsize = body["size"] fsize = body["size"]
fhash = body["hash"] fhash = body["hash"]
wark = up2k_wark_from_hashlist(self.args.salt, fsize, fhash) wark = up2k_wark_from_hashlist(self.args.warksalt, fsize, fhash)
uq = "substr(w,1,16) = ? and w = ?" uq = "substr(w,1,16) = ? and w = ?"
uv: list[Union[str, int]] = [wark[:16], wark] uv: list[Union[str, int]] = [wark[:16], wark]

View File

@@ -24,6 +24,7 @@ from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS from .__init__ import ANYWIN, PY2, TYPE_CHECKING, WINDOWS
from .authsrv import LEELOO_DALLAS, VFS, AuthSrv from .authsrv import LEELOO_DALLAS, VFS, AuthSrv
from .bos import bos from .bos import bos
from .cfg import vf_bmap, vf_vmap
from .fsutil import Fstab from .fsutil import Fstab
from .mtag import MParser, MTag from .mtag import MParser, MTag
from .util import ( from .util import (
@@ -41,6 +42,7 @@ from .util import (
gen_filekey, gen_filekey,
gen_filekey_dbg, gen_filekey_dbg,
hidedir, hidedir,
humansize,
min_ex, min_ex,
quotep, quotep,
rand_name, rand_name,
@@ -56,6 +58,7 @@ from .util import (
sfsenc, sfsenc,
spack, spack,
statdir, statdir,
unhumanize,
vjoin, vjoin,
vsplit, vsplit,
w8b64dec, w8b64dec,
@@ -110,7 +113,7 @@ class Up2k(object):
self.args = hub.args self.args = hub.args
self.log_func = hub.log self.log_func = hub.log
self.salt = self.args.salt self.salt = self.args.warksalt
self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$") self.r_hash = re.compile("^[0-9a-zA-Z_-]{44}$")
self.gid = 0 self.gid = 0
@@ -125,6 +128,8 @@ class Up2k(object):
self.registry: dict[str, dict[str, dict[str, Any]]] = {} self.registry: dict[str, dict[str, dict[str, Any]]] = {}
self.flags: dict[str, dict[str, Any]] = {} self.flags: dict[str, dict[str, Any]] = {}
self.droppable: dict[str, list[str]] = {} self.droppable: dict[str, list[str]] = {}
self.volnfiles: dict["sqlite3.Cursor", int] = {}
self.volsize: dict["sqlite3.Cursor", int] = {}
self.volstate: dict[str, str] = {} self.volstate: dict[str, str] = {}
self.vol_act: dict[str, float] = {} self.vol_act: dict[str, float] = {}
self.busy_aps: set[str] = set() self.busy_aps: set[str] = set()
@@ -195,7 +200,8 @@ class Up2k(object):
if self.stop: if self.stop:
# up-mt consistency not guaranteed if init is interrupted; # up-mt consistency not guaranteed if init is interrupted;
# drop caches for a full scan on next boot # drop caches for a full scan on next boot
self._drop_caches() with self.mutex:
self._drop_caches()
if self.pp: if self.pp:
self.pp.end = True self.pp.end = True
@@ -261,6 +267,20 @@ class Up2k(object):
} }
return json.dumps(ret, indent=4) return json.dumps(ret, indent=4)
def get_volsize(self, ptop: str) -> tuple[int, int]:
with self.mutex:
return self._get_volsize(ptop)
def _get_volsize(self, ptop: str) -> tuple[int, int]:
cur = self.cur[ptop]
nbytes = self.volsize[cur]
nfiles = self.volnfiles[cur]
for j in list(self.registry.get(ptop, {}).values()):
nbytes += j["size"]
nfiles += 1
return (nbytes, nfiles)
def rescan( def rescan(
self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool self, all_vols: dict[str, VFS], scan_vols: list[str], wait: bool, fscan: bool
) -> str: ) -> str:
@@ -575,7 +595,8 @@ class Up2k(object):
if self.args.re_dhash or [zv for zv in vols if "e2tsr" in zv.flags]: if self.args.re_dhash or [zv for zv in vols if "e2tsr" in zv.flags]:
self.args.re_dhash = False self.args.re_dhash = False
self._drop_caches() with self.mutex:
self._drop_caches()
for vol in vols: for vol in vols:
if self.stop: if self.stop:
@@ -739,8 +760,9 @@ class Up2k(object):
ff = "\033[0;35m{}{:.0}" ff = "\033[0;35m{}{:.0}"
fv = "\033[0;36m{}:\033[90m{}" fv = "\033[0;36m{}:\033[90m{}"
fx = set(("html_head",)) fx = set(("html_head",))
fdl = ("dbd", "lg_sbf", "md_sbf", "mte", "mth", "mtp", "nrand", "rand") fd = vf_bmap()
fd = {x: x for x in fdl} fd.update(vf_vmap())
fd = {v: k for k, v in fd.items()}
fl = { fl = {
k: v k: v
for k, v in flags.items() for k, v in flags.items()
@@ -751,6 +773,9 @@ class Up2k(object):
for k, v in fl.items() for k, v in fl.items()
if k not in fx if k not in fx
] ]
if not a:
a = ["\033[90mall-default"]
if a: if a:
vpath = "?" vpath = "?"
for k, v in self.asrv.vfs.all_vols.items(): for k, v in self.asrv.vfs.all_vols.items():
@@ -810,6 +835,8 @@ class Up2k(object):
try: try:
cur = self._open_db(db_path) cur = self._open_db(db_path)
self.cur[ptop] = cur self.cur[ptop] = cur
self.volsize[cur] = 0
self.volnfiles[cur] = 0
# speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5" 5400rpm 4kb) # speeds measured uploading 520 small files on a WD20SPZX (SMR 2.5" 5400rpm 4kb)
dbd = flags["dbd"] dbd = flags["dbd"]
@@ -917,6 +944,24 @@ class Up2k(object):
db.c.connection.commit() db.c.connection.commit()
if vol.flags.get("vmaxb") or vol.flags.get("vmaxn"):
zs = "select count(sz), sum(sz) from up"
vn, vb = db.c.execute(zs).fetchone()
vb = vb or 0
vb += vn * 2048
self.volsize[db.c] = vb
self.volnfiles[db.c] = vn
vmaxb = unhumanize(vol.flags.get("vmaxb") or "0")
vmaxn = unhumanize(vol.flags.get("vmaxn") or "0")
t = "{} / {} ( {} / {} files) in {}".format(
humansize(vb, True),
humansize(vmaxb, True),
humansize(vn, True).rstrip("B"),
humansize(vmaxn, True).rstrip("B"),
vol.realpath,
)
self.log(t)
return True, bool(n_add or n_rm or do_vac) return True, bool(n_add or n_rm or do_vac)
def _build_dir( def _build_dir(
@@ -1092,7 +1137,7 @@ class Up2k(object):
top, rp, dts, lmod, dsz, sz top, rp, dts, lmod, dsz, sz
) )
self.log(t) self.log(t)
self.db_rm(db.c, rd, fn) self.db_rm(db.c, rd, fn, 0)
ret += 1 ret += 1
db.n += 1 db.n += 1
in_db = [] in_db = []
@@ -1175,7 +1220,7 @@ class Up2k(object):
rm_files = [x for x in hits if x not in seen_files] rm_files = [x for x in hits if x not in seen_files]
n_rm = len(rm_files) n_rm = len(rm_files)
for fn in rm_files: for fn in rm_files:
self.db_rm(db.c, rd, fn) self.db_rm(db.c, rd, fn, 0)
if n_rm: if n_rm:
self.log("forgot {} deleted files".format(n_rm)) self.log("forgot {} deleted files".format(n_rm))
@@ -2284,7 +2329,9 @@ class Up2k(object):
if lost: if lost:
c2 = None c2 = None
for cur, dp_dir, dp_fn in lost: for cur, dp_dir, dp_fn in lost:
self.db_rm(cur, dp_dir, dp_fn) t = "forgetting deleted file: /{}"
self.log(t.format(vjoin(vjoin(vfs.vpath, dp_dir), dp_fn)))
self.db_rm(cur, dp_dir, dp_fn, cj["size"])
if c2 and c2 != cur: if c2 and c2 != cur:
c2.connection.commit() c2.connection.commit()
@@ -2418,7 +2465,14 @@ class Up2k(object):
if vfs.lim: if vfs.lim:
ap2, cj["prel"] = vfs.lim.all( ap2, cj["prel"] = vfs.lim.all(
cj["addr"], cj["prel"], cj["size"], ap1, reg cj["addr"],
cj["prel"],
cj["size"],
cj["ptop"],
ap1,
self.hub.broker,
reg,
"up2k._get_volsize",
) )
bos.makedirs(ap2) bos.makedirs(ap2)
vfs.lim.nup(cj["addr"]) vfs.lim.nup(cj["addr"])
@@ -2736,7 +2790,7 @@ class Up2k(object):
self._symlink(dst, d2, self.flags[ptop], lmod=lmod) self._symlink(dst, d2, self.flags[ptop], lmod=lmod)
if cur: if cur:
self.db_rm(cur, rd, fn) self.db_rm(cur, rd, fn, job["size"])
self.db_add(cur, vflags, rd, fn, lmod, *z2[3:]) self.db_add(cur, vflags, rd, fn, lmod, *z2[3:])
if cur: if cur:
@@ -2779,7 +2833,7 @@ class Up2k(object):
self.db_act = self.vol_act[ptop] = time.time() self.db_act = self.vol_act[ptop] = time.time()
try: try:
self.db_rm(cur, rd, fn) self.db_rm(cur, rd, fn, sz)
self.db_add( self.db_add(
cur, cur,
vflags, vflags,
@@ -2809,13 +2863,17 @@ class Up2k(object):
return True return True
def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str) -> None: def db_rm(self, db: "sqlite3.Cursor", rd: str, fn: str, sz: int) -> None:
sql = "delete from up where rd = ? and fn = ?" sql = "delete from up where rd = ? and fn = ?"
try: try:
db.execute(sql, (rd, fn)) r = db.execute(sql, (rd, fn))
except: except:
assert self.mem_cur assert self.mem_cur
db.execute(sql, s3enc(self.mem_cur, rd, fn)) r = db.execute(sql, s3enc(self.mem_cur, rd, fn))
if r.rowcount:
self.volsize[db] -= sz
self.volnfiles[db] -= 1
def db_add( def db_add(
self, self,
@@ -2844,6 +2902,9 @@ class Up2k(object):
v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0)) v = (wark, int(ts), sz, rd, fn, ip or "", int(at or 0))
db.execute(sql, v) db.execute(sql, v)
self.volsize[db] += sz
self.volnfiles[db] += 1
xau = False if skip_xau else vflags.get("xau") xau = False if skip_xau else vflags.get("xau")
dst = djoin(ptop, rd, fn) dst = djoin(ptop, rd, fn)
if xau and not runhook( if xau and not runhook(
@@ -2939,7 +3000,8 @@ class Up2k(object):
permsets = [[False, True]] permsets = [[False, True]]
vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0]) vn, rem = self.asrv.vfs.get(vpath, uname, *permsets[0])
vn, rem = vn.get_dbv(rem) vn, rem = vn.get_dbv(rem)
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem) with self.mutex:
_, _, _, _, dip, dat = self._find_from_vpath(vn.realpath, rem)
t = "you cannot delete this: " t = "you cannot delete this: "
if not dip: if not dip:
@@ -2991,12 +3053,12 @@ class Up2k(object):
break break
abspath = djoin(adir, fn) abspath = djoin(adir, fn)
st = bos.stat(abspath)
volpath = "{}/{}".format(vrem, fn).strip("/") volpath = "{}/{}".format(vrem, fn).strip("/")
vpath = "{}/{}".format(dbv.vpath, volpath).strip("/") vpath = "{}/{}".format(dbv.vpath, volpath).strip("/")
self.log("rm {}\n {}".format(vpath, abspath)) self.log("rm {}\n {}".format(vpath, abspath))
_ = dbv.get(volpath, uname, *permsets[0]) _ = dbv.get(volpath, uname, *permsets[0])
if xbd: if xbd:
st = bos.stat(abspath)
if not runhook( if not runhook(
self.log, self.log,
xbd, xbd,
@@ -3020,14 +3082,26 @@ class Up2k(object):
try: try:
ptop = dbv.realpath ptop = dbv.realpath
cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath) cur, wark, _, _, _, _ = self._find_from_vpath(ptop, volpath)
self._forget_file(ptop, volpath, cur, wark, True) self._forget_file(ptop, volpath, cur, wark, True, st.st_size)
finally: finally:
if cur: if cur:
cur.connection.commit() cur.connection.commit()
bos.unlink(abspath) bos.unlink(abspath)
if xad: if xad:
runhook(self.log, xad, abspath, vpath, "", uname, 0, 0, ip, 0, "") runhook(
self.log,
xad,
abspath,
vpath,
"",
uname,
st.st_mtime,
st.st_size,
ip,
0,
"",
)
if is_dir: if is_dir:
ok, ng = rmdirs(self.log_func, scandir, True, atop, 1) ok, ng = rmdirs(self.log_func, scandir, True, atop, 1)
@@ -3203,7 +3277,7 @@ class Up2k(object):
if c2 and c2 != c1: if c2 and c2 != c1:
self._copy_tags(c1, c2, w) self._copy_tags(c1, c2, w)
self._forget_file(svn.realpath, srem, c1, w, c1 != c2) self._forget_file(svn.realpath, srem, c1, w, c1 != c2, fsize)
self._relink(w, svn.realpath, srem, dabs) self._relink(w, svn.realpath, srem, dabs)
curs.add(c1) curs.add(c1)
@@ -3279,6 +3353,7 @@ class Up2k(object):
cur: Optional["sqlite3.Cursor"], cur: Optional["sqlite3.Cursor"],
wark: Optional[str], wark: Optional[str],
drop_tags: bool, drop_tags: bool,
sz: int,
) -> None: ) -> None:
"""forgets file in db, fixes symlinks, does not delete""" """forgets file in db, fixes symlinks, does not delete"""
srd, sfn = vsplit(vrem) srd, sfn = vsplit(vrem)
@@ -3293,7 +3368,7 @@ class Up2k(object):
q = "delete from mt where w=?" q = "delete from mt where w=?"
cur.execute(q, (wark[:16],)) cur.execute(q, (wark[:16],))
self.db_rm(cur, srd, sfn) self.db_rm(cur, srd, sfn, sz)
reg = self.registry.get(ptop) reg = self.registry.get(ptop)
if reg: if reg:

View File

@@ -1626,7 +1626,12 @@ def unhumanize(sz: str) -> int:
pass pass
mc = sz[-1:].lower() mc = sz[-1:].lower()
mi = {"k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}.get(mc, 1) mi = {
"k": 1024,
"m": 1024 * 1024,
"g": 1024 * 1024 * 1024,
"t": 1024 * 1024 * 1024 * 1024,
}.get(mc, 1)
return int(float(sz[:-1]) * mi) return int(float(sz[:-1]) * mi)
@@ -2422,7 +2427,7 @@ def killtree(root: int) -> None:
def runcmd( def runcmd(
argv: Union[list[bytes], list[str]], timeout: Optional[int] = None, **ka: Any argv: Union[list[bytes], list[str]], timeout: Optional[float] = None, **ka: Any
) -> tuple[int, str, str]: ) -> tuple[int, str, str]:
kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one kill = ka.pop("kill", "t") # [t]ree [m]ain [n]one
capture = ka.pop("capture", 3) # 0=none 1=stdout 2=stderr 3=both capture = ka.pop("capture", 3) # 0=none 1=stdout 2=stderr 3=both
@@ -2475,7 +2480,7 @@ def chkcmd(argv: Union[list[bytes], list[str]], **ka: Any) -> tuple[str, str]:
return sout, serr return sout, serr
def mchkcmd(argv: Union[list[bytes], list[str]], timeout: int = 10) -> None: def mchkcmd(argv: Union[list[bytes], list[str]], timeout: float = 10) -> None:
if PY2: if PY2:
with open(os.devnull, "wb") as f: with open(os.devnull, "wb") as f:
rv = sp.call(argv, stdout=f, stderr=f) rv = sp.call(argv, stdout=f, stderr=f)
@@ -2722,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)

View File

@@ -55,6 +55,7 @@
--u2-sbtn-b1: #999; --u2-sbtn-b1: #999;
--u2-txt-bg: var(--bg-u5); --u2-txt-bg: var(--bg-u5);
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1)); --u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-b1: rgba(128,128,128,0.8);
--u2-tab-1-fg: #fd7; --u2-tab-1-fg: #fd7;
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%); --u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
--u2-tab-1-b1: #7c5; --u2-tab-1-b1: #7c5;
@@ -270,6 +271,7 @@ html.bz {
--btn-1h-fg: #000; --btn-1h-fg: #000;
--txt-sh: a; --txt-sh: a;
--u2-tab-b1: var(--bg-u5);
--u2-tab-1-fg: var(--fg-max); --u2-tab-1-fg: var(--fg-max);
--u2-tab-1-bg: var(--bg); --u2-tab-1-bg: var(--bg);
@@ -329,6 +331,7 @@ html.c {
html.cz { html.cz {
--bgg: var(--bg-u2); --bgg: var(--bg-u2);
--srv-3: #fff; --srv-3: #fff;
--u2-tab-b1: var(--bg-d3);
} }
html.cy { html.cy {
--fg: #fff; --fg: #fff;
@@ -411,10 +414,11 @@ html.dz {
--op-aa-bg: var(--bg-d2); --op-aa-bg: var(--bg-d2);
--op-a-sh: rgba(0,0,0,0.5); --op-a-sh: rgba(0,0,0,0.5);
--u2-btn-b1: #999; --u2-btn-b1: var(--fg-weak);
--u2-sbtn-b1: #999; --u2-sbtn-b1: var(--fg-weak);
--u2-txt-bg: var(--bg-u5); --u2-txt-bg: var(--bg-u5);
--u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1)); --u2-tab-bg: linear-gradient(to bottom, var(--bg), var(--bg-u1));
--u2-tab-b1: var(--fg-weak);
--u2-tab-1-fg: #fff; --u2-tab-1-fg: #fff;
--u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%); --u2-tab-1-bg: linear-gradient(to bottom, var(#353), var(--bg) 80%);
--u2-tab-1-b1: #7c5; --u2-tab-1-b1: #7c5;
@@ -423,6 +427,12 @@ html.dz {
--u2-b-fg: #fff; --u2-b-fg: #fff;
--u2-b1-bg: #3a3; --u2-b1-bg: #3a3;
--u2-b2-bg: #3a3; --u2-b2-bg: #3a3;
--u2-o-bg: var(--btn-bg);
--u2-o-b1: var(--bg-u5);
--u2-o-h-bg: var(--fg-weak);
--u2-o-1-bg: var(--fg-weak);
--u2-o-1-b1: var(--a);
--u2-o-1h-bg: var(--a);
--u2-inf-bg: #07a; --u2-inf-bg: #07a;
--u2-inf-b1: #0be; --u2-inf-b1: #0be;
--u2-ok-bg: #380; --u2-ok-bg: #380;
@@ -2465,7 +2475,7 @@ html.y #bbox-overlay figcaption a {
width: 21em; width: 21em;
} }
#u2cards { #u2cards {
padding: 1em 1em .3em 1em; padding: 1em 1em .42em 1em;
margin: 0 auto; margin: 0 auto;
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: center;
@@ -2490,7 +2500,8 @@ html.y #bbox-overlay figcaption a {
#u2cards a { #u2cards a {
padding: .2em 1em; padding: .2em 1em;
background: var(--u2-tab-bg); background: var(--u2-tab-bg);
border: 1px solid rgba(128,128,128,0.8); border: 1px solid #999;
border-color: var(--u2-tab-b1);
border-width: 0 0 1px 0; border-width: 0 0 1px 0;
} }
#u2cards a:first-child { #u2cards a:first-child {

View File

@@ -3080,6 +3080,8 @@ function eval_hash() {
// compact media player // compact media player
function setacmp() { function setacmp() {
clmod(ebi('widget'), 'cmp', props.mcmp); clmod(ebi('widget'), 'cmp', props.mcmp);
pbar.onresize();
vbar.onresize();
} }
bcfg_bind(props, 'mcmp', 'au_compact', false, setacmp); bcfg_bind(props, 'mcmp', 'au_compact', false, setacmp);
setacmp(); setacmp();
@@ -5778,14 +5780,18 @@ var treectl = (function () {
for (var b = 0; b < res.taglist.length; b++) { for (var b = 0; b < res.taglist.length; b++) {
var k = res.taglist[b], var k = res.taglist[b],
v = (tn.tags || {})[k] || ""; v = (tn.tags || {})[k] || "",
sv = null;
if (k == ".dur") { if (k == ".dur")
var sv = v ? s2ms(v) : ""; sv = v ? s2ms(v) : "";
ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv; else if (k == ".up_at")
sv = v ? unix2iso(v) : "";
else {
ln.push(v);
continue; continue;
} }
ln.push(v); ln[ln.length - 1] += '</td><td sortv="' + v + '">' + sv;
} }
ln = ln.concat([tn.ext, unix2iso(tn.ts)]).join('</td><td>'); ln = ln.concat([tn.ext, unix2iso(tn.ts)]).join('</td><td>');
html.push(ln + '</td></tr>'); html.push(ln + '</td></tr>');
@@ -6064,7 +6070,7 @@ function apply_perms(res) {
var axs = [], var axs = [],
aclass = '>', aclass = '>',
chk = ['read', 'write', 'move', 'delete', 'get']; chk = ['read', 'write', 'move', 'delete', 'get', 'admin'];
for (var a = 0; a < chk.length; a++) for (var a = 0; a < chk.length; a++)
if (has(perms, chk[a])) if (has(perms, chk[a]))

View File

@@ -73,7 +73,7 @@ html {
#toastb { #toastb {
max-height: 70vh; max-height: 70vh;
overflow-y: auto; overflow-y: auto;
padding: 1px; padding: .1em;
} }
#toast.scroll #toastb { #toast.scroll #toastb {
overflow-y: scroll; overflow-y: scroll;

View File

@@ -159,8 +159,8 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
window.onerror = undefined; window.onerror = undefined;
var html = [ var html = [
'<h1>you hit a bug!</h1>', '<h1>you hit a bug!</h1>',
'<p style="font-size:1.3em;margin:0">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a> / <a href="?b=u">basic</a></p>', '<p style="font-size:1.3em;margin:0;line-height:2em">try to <a href="#" onclick="localStorage.clear();location.reload();">reset copyparty settings</a> if you are stuck here, or <a href="#" onclick="ignex();">ignore this</a> / <a href="#" onclick="ignex(true);">ignore all</a> / <a href="?b=u">basic</a></p>',
'<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">github issue</a></p>', '<p style="color:#fff">please send me a screenshot arigathanks gozaimuch: <a href="<ghi>" target="_blank">new github issue</a></p>',
'<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)).replace(/\n/g, '<br />') + '</p>', '<p class="b">' + esc(url + ' @' + lineNo + ':' + columnNo), '<br />' + esc(String(msg)).replace(/\n/g, '<br />') + '</p>',
'<p><b>UA:</b> ' + esc(navigator.userAgent + '') '<p><b>UA:</b> ' + esc(navigator.userAgent + '')
]; ];
@@ -225,7 +225,7 @@ function vis_exh(msg, url, lineNo, columnNo, error) {
'#exbox{background:#222;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' + '#exbox{background:#222;color:#ddd;font-family:sans-serif;font-size:0.8em;padding:0 1em 1em 1em;z-index:80386;position:fixed;top:0;left:0;right:0;bottom:0;width:100%;height:100%;overflow:auto;width:calc(100% - 2em)} ' +
'#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' + '#exbox,#exbox *{line-height:1.5em;overflow-wrap:break-word} ' +
'#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' + '#exbox code{color:#bf7;background:#222;padding:.1em;margin:.2em;font-size:1.1em;font-family:monospace,monospace} ' +
'#exbox a{text-decoration:underline;color:#fc0} ' + '#exbox a{text-decoration:underline;color:#fc0;background:#222;border:none} ' +
'#exbox h1{margin:.5em 1em 0 0;padding:0} ' + '#exbox h1{margin:.5em 1em 0 0;padding:0} ' +
'#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' + '#exbox p.b{border-top:1px solid #999;margin:1em 0 0 0;font-size:1em} ' +
'#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' + '#exbox ul, #exbox li {margin:0 0 0 .5em;padding:0} ' +
@@ -1786,16 +1786,17 @@ function xhrchk(xhr, prefix, e404, lvl, tag) {
if (xhr.status < 400 && xhr.status >= 200) if (xhr.status < 400 && xhr.status >= 200)
return true; return true;
if (xhr.status == 403) var errtxt = (xhr.response && xhr.response.err) || xhr.responseText,
fun = toast[lvl || 'err'],
is_cf = /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser|\/chall[e]nge-platform|"chall[e]nge-error|nable Ja[v]aScript and cook/.test(errtxt);
if (xhr.status == 403 && !is_cf)
return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag); return toast.err(0, prefix + (L && L.xhr403 || "403: access denied\n\ntry pressing F5, maybe you got logged out"), tag);
if (xhr.status == 404) if (xhr.status == 404)
return toast.err(0, prefix + e404, tag); return toast.err(0, prefix + e404, tag);
var errtxt = (xhr.response && xhr.response.err) || xhr.responseText, if (is_cf && (xhr.status == 403 || xhr.status == 503)) {
fun = toast[lvl || 'err'];
if (xhr.status == 503 && /[Cc]loud[f]lare|>Just a mo[m]ent|#cf-b[u]bbles|Chec[k]ing your br[o]wser/.test(errtxt)) {
var now = Date.now(), td = now - cf_cha_t; var now = Date.now(), td = now - cf_cha_t;
if (td < 15000) if (td < 15000)
return; return;

View File

@@ -1,3 +1,76 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0707-2220 `v1.8.1` in case of 404
## new features
* [handlers](https://github.com/9001/copyparty/tree/hovudstraum/bin/handlers); change the behavior of 404 / 403 with plugins
* makes it possible to use copyparty as a [caching proxy](https://github.com/9001/copyparty/blob/hovudstraum/bin/handlers/caching-proxy.py)
* #42 add mpv + streamlink support to [very-bad-idea](https://github.com/9001/copyparty/tree/hovudstraum/bin/mtag#dangerous-plugins)
* add support for Pillow 10
* also improved text rendering in icons
* mention the [fedora package](https://github.com/9001/copyparty#fedora-package) in the readme
## bugfixes
* theme 6 (hacker) didn't show the state of some toggle-switches
* windows: keep quickedit enabled when hashing passwords interactively
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0626-0005 `v1.8.0` argon
News: if you use rclone as a copyparty webdav client, upgrading to [rclone v1.63](https://github.com/rclone/rclone/releases/tag/v1.63.0) (just released) will give you [a huge speed boost](https://github.com/rclone/rclone/pull/6897) for small files
## new features
* #39 hashed passwords
* instead of keeping plaintext account passwords in config files, you can now store hashed ones instead
* `--ah-alg` specifies algorithm; best to worst: `argon2`, `scrypt`, `sha2`, or the default `none`
* the default settings of each algorithm takes `0.4 sec` to hash a password, and argon2 eats `256 MiB` RAM
* can be adjusted with optional comma-separated args after the algorithm name; see `--help-pwhash`
* `--ah-salt` is the [static salt](https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#hashed-passwords) for all passwords, and is autogenerated-and-persisted if not specified
* `--ah-cli` switches copyparty into a shell where you can hash passwords interactively
* but copyparty will also autoconvert any unhashed passwords on startup and give you the values to insert into the config anyways
* #40 volume size limit
* volflag `vmaxb` specifies max size of a volume
* volflag `vmaxn` specifies max number of files in a volume
* example: `-v [...]:c,vmaxb=900g:c,vmaxn=20k` blocks uploads if the volume reaches 900 GiB or a total of 20480 files
* good alternative to `--df` since it works per-volume
## bugfixes
* autogenerated TLS certs didn't include the mDNS name
## other changes
* improved cloudflare challenge detection
* markdown edits will now trigger upload hooks
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0611-0814 `v1.7.6` NO_COLOR
## new features
* #31 `--grid` shows thumbnails instead of file-list by default
* #28 `--unlist` regex-exclude files from browser listings
* for example `--unlist '\.(js|css)$'` hides all `.js` and `.css` files
* **purely cosmetic!** the files are still fully accessible, and still appear in API calls
* auto-generate TLS certificates on startup / network-change
* mostly good for LAN, requires [cfssl](https://github.com/cloudflare/cfssl/releases/latest), can be disabled with `--no-crt`
* creates a self-signed CA and certs with SANs of all detected server IPs
* so it's still recommended to use a reverse-proxy / letsencrypt for WAN servers
* the default `--fk-salt` is now much stronger
* all existing installations will keep the previously selected seed -- you can choose to upgrade by deleting `~/.config/copyparty/cert.pem` but this will change all filekeys / per-file passwords
* the `NO_COLOR` environment-variable is now supported, removing colors from stdout
* see https://no-color.org/ and more importantly https://youtu.be/biW5UVGkPMA?t=150
* `--ansi` and `--no-ansi` can also be used to force-enable/disable colored output
* #33 disable colors when stdout is redirected to a pipe/file -- by @clach04
* #32 simplify building sfx from source
* upgraded [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) to [python 3.11.4](https://pythoninsider.blogspot.com/2023/06/python-3114-31012-3917-3817-3717-and.html)
## bugfixes
* #30 `--ftps` didn't work without `--ftp`
* tiny css bug in light themes (opaque thumbnail controls)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0513-0000 `v1.7.2` hard resolve # 2023-0513-0000 `v1.7.2` hard resolve

View File

@@ -4,8 +4,9 @@
* [future plans](#future-plans) - some improvement ideas * [future plans](#future-plans) - some improvement ideas
* [design](#design) * [design](#design)
* [up2k](#up2k) - quick outline of the up2k protocol * [up2k](#up2k) - quick outline of the up2k protocol
* [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/) * [why not tus](#why-not-tus) - I didn't know about [tus](https://tus.io/)
* [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right? * [why chunk-hashes](#why-chunk-hashes) - a single sha512 would be better, right?
* [hashed passwords](#hashed-passwords) - regarding the curious decisions
* [http api](#http-api) * [http api](#http-api)
* [read](#read) * [read](#read)
* [write](#write) * [write](#write)
@@ -68,14 +69,14 @@ regarding the frequent server log message during uploads;
* on this http connection, `2.77 GiB` transferred, `102.9 MiB/s` average, `948` chunks handled * on this http connection, `2.77 GiB` transferred, `102.9 MiB/s` average, `948` chunks handled
* client says `4` uploads OK, `0` failed, `3` busy, `1` queued, `10042 MiB` total size, `7198 MiB` and `00:01:09` left * client says `4` uploads OK, `0` failed, `3` busy, `1` queued, `10042 MiB` total size, `7198 MiB` and `00:01:09` left
## why not tus ### why not tus
I didn't know about [tus](https://tus.io/) when I made this, but: I didn't know about [tus](https://tus.io/) when I made this, but:
* up2k has the advantage that it supports parallel uploading of non-contiguous chunks straight into the final file -- [tus does a merge at the end](https://tus.io/protocols/resumable-upload.html#concatenation) which is slow and taxing on the server HDD / filesystem (unless i'm misunderstanding) * up2k has the advantage that it supports parallel uploading of non-contiguous chunks straight into the final file -- [tus does a merge at the end](https://tus.io/protocols/resumable-upload.html#concatenation) which is slow and taxing on the server HDD / filesystem (unless i'm misunderstanding)
* up2k has the slight disadvantage of requiring the client to hash the entire file before an upload can begin, but this has the benefit of immediately skipping duplicate files * up2k has the slight disadvantage of requiring the client to hash the entire file before an upload can begin, but this has the benefit of immediately skipping duplicate files
* and the hashing happens in a separate thread anyways so it's usually not a bottleneck * and the hashing happens in a separate thread anyways so it's usually not a bottleneck
## why chunk-hashes ### why chunk-hashes
a single sha512 would be better, right? a single sha512 would be better, right?
@@ -92,6 +93,15 @@ hashwasm would solve the streaming issue but reduces hashing speed for sha512 (x
* blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids * blake2 might be a better choice since xxh is non-cryptographic, but that gets ~15 MiB/s on slower androids
# hashed passwords
regarding the curious decisions
there is a static salt for all passwords;
* because most copyparty APIs allow users to authenticate using only their password, making the username unknown, so impossible to do per-account salts
* the drawback of this is that an attacker can bruteforce all accounts in parallel, however most copyparty instances only have a handful of accounts in the first place, and it can be compensated by increasing the hashing cost anyways
# http api # http api
* table-column `params` = URL parameters; `?foo=bar&qux=...` * table-column `params` = URL parameters; `?foo=bar&qux=...`

View File

@@ -48,6 +48,7 @@ thumbnails2 = ["pyvips"]
audiotags = ["mutagen"] audiotags = ["mutagen"]
ftpd = ["pyftpdlib"] ftpd = ["pyftpdlib"]
ftps = ["pyftpdlib", "pyopenssl"] ftps = ["pyftpdlib", "pyopenssl"]
pwhash = ["argon2-cffi"]
[project.scripts] [project.scripts]
copyparty = "copyparty.__main__:main" copyparty = "copyparty.__main__:main"

View File

@@ -9,7 +9,7 @@ ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc \
wget \ wget \
py3-pillow \ py3-argon2-cffi py3-pillow \
ffmpeg \ ffmpeg \
&& rm -rf /tmp/pyc \ && rm -rf /tmp/pyc \
&& mkdir /cfg /w \ && mkdir /cfg /w \

View File

@@ -12,7 +12,7 @@ COPY i/bin/mtag/audio-bpm.py /mtag/
COPY i/bin/mtag/audio-key.py /mtag/ COPY i/bin/mtag/audio-key.py /mtag/
RUN apk add -U !pyc \ RUN apk add -U !pyc \
wget \ wget \
py3-pillow py3-pip py3-cffi \ py3-argon2-cffi py3-pillow py3-pip py3-cffi \
ffmpeg \ ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \ vips-jxl vips-heif vips-poppler vips-magick \
py3-numpy fftw libsndfile \ py3-numpy fftw libsndfile \

View File

@@ -9,7 +9,7 @@ ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add !pyc \ RUN apk --no-cache add !pyc \
wget \ wget \
py3-pillow py3-mutagen \ py3-argon2-cffi py3-pillow py3-mutagen \
&& rm -rf /tmp/pyc \ && rm -rf /tmp/pyc \
&& mkdir /cfg /w \ && mkdir /cfg /w \
&& chmod 777 /cfg /w \ && chmod 777 /cfg /w \

View File

@@ -9,7 +9,7 @@ ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk add -U !pyc \ RUN apk add -U !pyc \
wget \ wget \
py3-pillow py3-pip py3-cffi \ py3-argon2-cffi py3-pillow py3-pip py3-cffi \
ffmpeg \ ffmpeg \
vips-jxl vips-heif vips-poppler vips-magick \ vips-jxl vips-heif vips-poppler vips-magick \
&& apk add -t .bd \ && apk add -t .bd \

View File

@@ -26,5 +26,5 @@ ba91ab0518c61eff13e5612d9e6b532940813f6b56e6ed81ea6c7c4d45acee4d98136a383a250675
00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl 00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl
7f8f4daa4f4f2dbf24cdd534b2952ee3fba6334eb42b37465ccda3aa1cccc3d6204aa6bfffb8a83bf42ec59c702b5b5247d4c8ee0d4df906334ae53072ef8c4c MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl 7f8f4daa4f4f2dbf24cdd534b2952ee3fba6334eb42b37465ccda3aa1cccc3d6204aa6bfffb8a83bf42ec59c702b5b5247d4c8ee0d4df906334ae53072ef8c4c MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl
4a20aeb52d4fde6aabcba05ee261595eeb5482c72ee27332690f34dd6e7a49c0b3ba3813202ac15c9d21e29f1cd803f2e79ccc1c45ec314fcd0a937016bcbc56 mutagen-1.46.0-py3-none-any.whl 4a20aeb52d4fde6aabcba05ee261595eeb5482c72ee27332690f34dd6e7a49c0b3ba3813202ac15c9d21e29f1cd803f2e79ccc1c45ec314fcd0a937016bcbc56 mutagen-1.46.0-py3-none-any.whl
78414808cb9a5fa74e7b23360b8f46147952530e3cc78a3ad4b80be3e26598080537ac691a1be1f35b7428a22c1f65a6adf45986da2752fbe9d9819d77a58bf8 Pillow-9.5.0-cp311-cp311-win_amd64.whl 926d408a886059a75cf12706fa061146f9f042b27fb6e65be7d49f398ed23fb0227639d84804586ac014c6bcf7d08cd86a09c1a20793d341aa0802d3d32a546b Pillow-10.0.0-cp311-cp311-win_amd64.whl
a48ee8992eee60a0d620dced71b9f96596f5dd510e3024015aca55884cdb3f9e2405734bfc13f3f40b79106a77bc442cce02ac4c8f5d16207448052b368fd52a python-3.11.4-amd64.exe a48ee8992eee60a0d620dced71b9f96596f5dd510e3024015aca55884cdb3f9e2405734bfc13f3f40b79106a77bc442cce02ac4c8f5d16207448052b368fd52a python-3.11.4-amd64.exe

View File

@@ -23,6 +23,7 @@ copyparty/ico.py,
copyparty/mdns.py, copyparty/mdns.py,
copyparty/mtag.py, copyparty/mtag.py,
copyparty/multicast.py, copyparty/multicast.py,
copyparty/pwhash.py,
copyparty/res, copyparty/res,
copyparty/res/__init__.py, copyparty/res/__init__.py,
copyparty/res/COPYING.txt, copyparty/res/COPYING.txt,

View File

@@ -16,6 +16,8 @@ cat $f | awk '
h=0 h=0
}; };
}; };
/```/{o=!o}
o{next}
/^#/{s=1;rs=0;pr()} /^#/{s=1;rs=0;pr()}
/^#* *(nix package)/{rs=1} /^#* *(nix package)/{rs=1}
/^#* *(install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module)|`$/{s=rs} /^#* *(install on android|dev env setup|just the sfx|complete release|optional gpl stuff|nixos module)|`$/{s=rs}

View File

@@ -140,6 +140,7 @@ args = {
"audiotags": ["mutagen"], "audiotags": ["mutagen"],
"ftpd": ["pyftpdlib"], "ftpd": ["pyftpdlib"],
"ftps": ["pyftpdlib", "pyopenssl"], "ftps": ["pyftpdlib", "pyopenssl"],
"pwhash": ["argon2-cffi"],
}, },
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]}, "entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
"scripts": ["bin/partyfuse.py", "bin/u2c.py"], "scripts": ["bin/partyfuse.py", "bin/u2c.py"],

View File

@@ -178,9 +178,9 @@ class TestVFS(unittest.TestCase):
self.assertEqual(n.realpath, os.path.join(td, "a")) self.assertEqual(n.realpath, os.path.join(td, "a"))
self.assertAxs(n.axs.uread, ["*"]) self.assertAxs(n.axs.uread, ["*"])
self.assertAxs(n.axs.uwrite, []) self.assertAxs(n.axs.uwrite, [])
perm_na = (False, False, False, False, False, False) perm_na = (False, False, False, False, False, False, False)
perm_rw = (True, True, False, False, False, False) perm_rw = (True, True, False, False, False, False, False)
perm_ro = (True, False, False, False, False, False) perm_ro = (True, False, False, False, False, False, False)
self.assertEqual(vfs.can_access("/", "*"), perm_na) self.assertEqual(vfs.can_access("/", "*"), perm_na)
self.assertEqual(vfs.can_access("/", "k"), perm_rw) self.assertEqual(vfs.can_access("/", "k"), perm_rw)
self.assertEqual(vfs.can_access("/a", "*"), perm_ro) self.assertEqual(vfs.can_access("/a", "*"), perm_ro)

View File

@@ -98,7 +98,7 @@ class Cfg(Namespace):
def __init__(self, a=None, v=None, c=None): def __init__(self, a=None, v=None, c=None):
ka = {} ka = {}
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand vc xdev xlink xvol" ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop vc xdev xlink xvol"
ka.update(**{k: False for k in ex.split()}) ka.update(**{k: False for k in ex.split()})
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip" ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
@@ -107,16 +107,16 @@ class Cfg(Namespace):
ex = "css_browser hist js_browser no_forget no_hash no_idx" ex = "css_browser hist js_browser no_forget no_hash no_idx"
ka.update(**{k: None for k in ex.split()}) ka.update(**{k: None for k in ex.split()})
ex = "s_thead s_tbody" ex = "s_thead s_tbody th_convt"
ka.update(**{k: 9 for k in ex.split()}) ka.update(**{k: 9 for k in ex.split()})
ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo" ex = "df loris re_maxage rproxy rsp_jtr rsp_slp s_wr_slp theme themes turbo"
ka.update(**{k: 0 for k in ex.split()}) ka.update(**{k: 0 for k in ex.split()})
ex = "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 xban 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__(
@@ -126,6 +126,7 @@ class Cfg(Namespace):
E=E, E=E,
dbd="wal", dbd="wal",
s_wr_sz=512 * 1024, s_wr_sz=512 * 1024,
th_size="320x256",
unpost=600, unpost=600,
u2sort="s", u2sort="s",
mtp=[], mtp=[],