Compare commits

..

57 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
ed
a00ff2b086 v1.7.4 2023-06-11 00:07:38 +00:00
ed
e4acddc23b v1.7.3 2023-06-11 00:03:03 +00:00
ed
2b2d8e4e02 tls / gencert fixes 2023-06-10 23:34:34 +00:00
ed
5501d49032 prefer urandom for fk-salt unless cert.pem exists 2023-06-10 22:47:39 +00:00
ed
fa54b2eec4 generate tls certs 2023-06-10 22:46:24 +00:00
ed
cb0160021f upgrade pyinstaller env/deps 2023-06-10 11:58:58 +00:00
ed
93a723d588 add --ansi to systemd, fix grid controls bg,
mention folder thumbs dependency on -e2d,
improve make-sfx warnings,
update changelog
2023-06-06 22:04:39 +00:00
ed
8ebe1fb5e8 mention cfssl.sh in the default-certificate warning,
and improve documentation inside cfssl.sh
2023-06-06 21:41:19 +00:00
clach04
2acdf685b1 Fix issue #33 - no color output expected when redirecting stdout 2023-06-05 01:58:49 +02:00
ed
9f122ccd16 make-sfx: option to auto-obtain webdeps 2023-06-04 23:46:38 +00:00
ed
03be26fafc improve check for type-hint support 2023-06-04 22:59:25 +00:00
ed
df5d309d6e document the make-sfx.sh fast option 2023-06-04 14:13:35 +00:00
ed
c355f9bd91 catch common environment issues (#32):
* error-message which explains how to run on py2 / older py3
   when trying to run from source
* check compatibility between jinja2 and cpython on startup
* verify that webdeps are present on startup
* verify that webdeps are present when building sfx
* make-sfx.sh grabs the strip-hints dependency
2023-06-04 13:13:36 +00:00
ed
9c28ba417e option to regex-exclude files in browser listings 2023-06-02 21:54:25 +00:00
ed
705b58c741 support the NO_COLOR environment variable
https://no-color.org/ and more importantly
https://youtu.be/biW5UVGkPMA?t=150
2023-06-02 20:22:57 +00:00
ed
510302d667 support ftps-only; closes #30 2023-06-02 19:02:50 +00:00
ed
025a537413 add option to show thumbs by default; closes #31 2023-06-02 18:41:21 +00:00
ed
60a1ff0fc0 macos: mute select() noise on wake from suspend 2023-05-19 16:37:52 +02:00
ed
f94a0b1bff update pkgs to 1.7.2 2023-05-13 00:49:46 +00:00
ed
4ccfeeb2cd v1.7.2 2023-05-13 00:00:07 +00:00
ed
2646f6a4f2 oh nice, looks like 3.18 fixed whatever broke in 3.17 2023-05-12 23:38:10 +00:00
ed
b286ab539e readme: add more examples 2023-05-12 22:41:06 +00:00
ed
2cca6e0922 warn when sharing certain system locations 2023-05-12 21:38:16 +00:00
ed
db51f1b063 cfg: allow trailing colon on category headers 2023-05-12 21:01:34 +00:00
ed
d979c47f50 optimize clearTimeout + always shrink upload panes after completion + fix GET alignment 2023-05-12 20:46:45 +00:00
ed
e64b87b99b dont hardlink symlinks (they could be relative) 2023-05-12 20:41:09 +00:00
ed
b985011a00 upgrade docker to alpine 3.18:
* enables chiptune player
* smaller containers (generate pycache at runtime)
2023-05-11 06:56:21 +00:00
ed
c2ed2314c8 pkg/arch: add setuptools 2023-05-08 22:24:46 +00:00
ed
cd496658c3 update pkgs to 1.7.1 2023-05-07 19:51:59 +00:00
62 changed files with 1904 additions and 334 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)
@@ -126,7 +129,7 @@ enable thumbnails (images/audio/video), media indexing, and audio transcoding by
running copyparty without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes) running copyparty without arguments (for example doubleclicking it on Windows) will give everyone read/write access to the current folder; you may want [accounts and volumes](#accounts-and-volumes)
or see [complete windows example](./docs/examples/windows.md) or see [some usage examples](#complete-examples) for inspiration, or the [complete windows example](./docs/examples/windows.md)
some recommended options: some recommended options:
* `-e2dsa` enables general [file indexing](#file-indexing) * `-e2dsa` enables general [file indexing](#file-indexing)
@@ -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`
@@ -471,6 +475,7 @@ click the `🌲` or pressing the `B` hotkey to toggle between breadcrumbs path (
## thumbnails ## thumbnails
press `g` or `田` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails press `g` or `田` to toggle grid-view instead of the file listing and `t` toggles icons / thumbnails
* can be made default globally with `--grid` or per-volume with volflag `grid`
![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png) ![copyparty-thumbs-fs8](https://user-images.githubusercontent.com/241032/129636211-abd20fa2-a953-4366-9423-1c88ebb96ba9.png)
@@ -481,6 +486,7 @@ it does static images with Pillow / pyvips / FFmpeg, and uses FFmpeg for video f
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`) audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg` images with the following names (see `--th-covers`) become the thumbnail of the folder they're in: `folder.png`, `folder.jpg`, `cover.png`, `cover.jpg`
* and, if you enable [file indexing](#file-indexing), all remaining folders will also get thumbnails (as long as they contain any pics at all)
in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked in the grid/thumbnail view, if the audio player panel is open, songs will start playing when clicked
* indicated by the audio files having the ▶ icon instead of 💾 * indicated by the audio files having the ▶ icon instead of 💾
@@ -929,14 +935,13 @@ through arguments:
* `--xlink` enables deduplication across volumes * `--xlink` enables deduplication across volumes
the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling: the same arguments can be set as volflags, in addition to `d2d`, `d2ds`, `d2t`, `d2ts`, `d2v` for disabling:
* `-v ~/music::r:c,e2dsa,e2tsr` does a full reindex of everything on startup * `-v ~/music::r:c,e2ds,e2tsr` does a full reindex of everything on startup
* `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on * `-v ~/music::r:c,d2d` disables **all** indexing, even if any `-e2*` are on
* `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*` * `-v ~/music::r:c,d2t` disables all `-e2t*` (tags), does not affect `-e2d*`
* `-v ~/music::r:c,d2ds` disables on-boot scans; only index new uploads * `-v ~/music::r:c,d2ds` disables on-boot scans; only index new uploads
* `-v ~/music::r:c,d2ts` same except only affecting tags * `-v ~/music::r:c,d2ts` same except only affecting tags
note: note:
* the parser can finally handle `c,e2dsa,e2tsr` so you no longer have to `c,e2dsa:c,e2tsr`
* `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise * `e2tsr` is probably always overkill, since `e2ds`/`e2dsa` would pick up any file modifications and `e2ts` would then reindex those, unless there is a new copyparty version with new parsers and the release note says otherwise
* the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher * the rescan button in the admin panel has no effect unless the volume has `-e2ds` or higher
* deduplication is possible on windows if you run copyparty as administrator (not saying you should!) * deduplication is possible on windows if you run copyparty as administrator (not saying you should!)
@@ -979,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
@@ -1122,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:
@@ -1158,9 +1172,33 @@ see the top of [./copyparty/web/browser.css](./copyparty/web/browser.css) where
## complete examples ## complete examples
* [running on windows](./docs/examples/windows.md) * see [running on windows](./docs/examples/windows.md) for a fancy windows setup
* read-only music server * or use any of the examples below, just replace `python copyparty-sfx.py` with `copyparty.exe` if you're using the exe edition
* allow anyone to download or upload files into the current folder:
`python copyparty-sfx.py`
* enable searching and music indexing with `-e2dsa -e2ts`
* start an FTP server on port 3921 with `--ftp 3921`
* announce it on your LAN with `-z` so it appears in windows/Linux file managers
* anyone can upload, but nobody can see any files (even the uploader):
`python copyparty-sfx.py -e2dsa -v .::w`
* block uploads if there's less than 4 GiB free disk space with `--df 4`
* show a popup on new uploads with `--xau bin/hooks/notify.py`
* anyone can upload, and receive "secret" links for each upload they do:
`python copyparty-sfx.py -e2dsa -v .::wG:c,fk=8`
* anyone can browse, only `kevin` (password `okgo`) can upload/move/delete files:
`python copyparty-sfx.py -e2dsa -a kevin:okgo -v .::r:rwmd,kevin`
* read-only music server:
`python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2` `python copyparty-sfx.py -v /mnt/nas/music:/music:r -e2dsa -e2ts --no-robots --force-js --theme 2`
* ...with bpm and key scanning * ...with bpm and key scanning
@@ -1200,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`
@@ -1486,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:
@@ -1541,12 +1593,28 @@ 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)
copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well copyparty doesn't speak HTTP/2 or QUIC, so using a reverse proxy would solve that as well
if [cfssl](https://github.com/cloudflare/cfssl/releases/latest) is installed, copyparty will automatically create a CA and server-cert on startup
* the certs are written to `--crt-dir` for distribution, see `--help` for the other `--crt` options
* this will be a self-signed certificate so you must install your `ca.pem` into all your browsers/devices
* if you want to avoid the hassle of distributing certs manually, please consider using a reverse proxy
# recovering from crashes # recovering from crashes
@@ -1583,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

@@ -1,14 +1,44 @@
#!/bin/bash #!/bin/bash
set -e set -e
cat >/dev/null <<'EOF'
NOTE: copyparty is now able to do this automatically;
however you may wish to use this script instead if
you have specific needs (or if copyparty breaks)
this script generates a new self-signed TLS certificate and
replaces the default insecure one that comes with copyparty
as it is trivial to impersonate a copyparty server using the
default certificate, it is highly recommended to do this
this will create a self-signed CA, and a Server certificate
which gets signed by that CA -- you can run it multiple times
with different server-FQDNs / IPs to create additional certs
for all your different servers / (non-)copyparty services
EOF
# ca-name and server-fqdn # ca-name and server-fqdn
ca_name="$1" ca_name="$1"
srv_fqdn="$2" srv_fqdn="$2"
[ -z "$srv_fqdn" ] && { [ -z "$srv_fqdn" ] && { cat <<'EOF'
echo "need arg 1: ca name" need arg 1: ca name
echo "need arg 2: server fqdn and/or IPs, comma-separated" need arg 2: server fqdn and/or IPs, comma-separated
echo "optional arg 3: if set, write cert into copyparty cfg" optional arg 3: if set, write cert into copyparty cfg
example:
./cfssl.sh PartyCo partybox.local y
EOF
exit 1
}
command -v cfssljson 2>/dev/null || {
echo please install cfssl and try again
exit 1 exit 1
} }
@@ -59,12 +89,14 @@ show() {
} }
show ca.pem show ca.pem
show "$srv_fqdn.pem" show "$srv_fqdn.pem"
echo
echo "successfully generated new certificates"
# write cert into copyparty config # write cert into copyparty config
[ -z "$3" ] || { [ -z "$3" ] || {
mkdir -p ~/.config/copyparty mkdir -p ~/.config/copyparty
cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem cat "$srv_fqdn".{key,pem} ca.pem >~/.config/copyparty/cert.pem
echo "successfully replaced copyparty certificate"
} }

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,13 +1,13 @@
# Maintainer: icxes <dev.null@need.moe> # Maintainer: icxes <dev.null@need.moe>
pkgname=copyparty pkgname=copyparty
pkgver="1.7.0" pkgver="1.8.1"
pkgrel=1 pkgrel=1
pkgdesc="Portable file sharing hub" pkgdesc="Portable file sharing hub"
arch=("any") arch=("any")
url="https://github.com/9001/${pkgname}" url="https://github.com/9001/${pkgname}"
license=('MIT') license=('MIT')
depends=("python" "lsof" "python-jinja") depends=("python" "lsof" "python-jinja")
makedepends=("python-wheel" "python-build" "python-installer" "make" "pigz") makedepends=("python-wheel" "python-setuptools" "python-build" "python-installer" "make" "pigz")
optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags" optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tags"
"python-mutagen: music tags (alternative)" "python-mutagen: music tags (alternative)"
"python-pillow: thumbnails for images" "python-pillow: thumbnails for images"
@@ -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=("782e62eb1378e8d9d50af3fa1c18b95d11bb4721df93b4525beba80f14d55661") 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.0/copyparty-sfx.py", "url": "https://github.com/9001/copyparty/releases/download/v1.8.1/copyparty-sfx.py",
"version": "1.7.0", "version": "1.8.1",
"hash": "sha256-CW2+Bzt5bJyilCUCY0W0htV7J1LejDttMv2Mk5bFtgI=" "hash": "sha256-0Lf5djrgGAM+wwZP66GtSXkmRnIp3tij8j7cANeoE7o="
} }

View File

@@ -1,3 +1,6 @@
# NOTE: this is now a built-in feature in copyparty
# but you may still want this if you have specific needs
#
# systemd service which generates a new TLS certificate on each boot, # systemd service which generates a new TLS certificate on each boot,
# that way the one-year expiry time won't cause any issues -- # that way the one-year expiry time won't cause any issues --
# just have everyone trust the ca.pem once every 10 years # just have everyone trust the ca.pem once every 10 years

View File

@@ -22,6 +22,7 @@
# add '-i 127.0.0.1' to only allow local connections # add '-i 127.0.0.1' to only allow local connections
# add '-e2dsa' to enable filesystem scanning + indexing # add '-e2dsa' to enable filesystem scanning + indexing
# add '-e2ts' to enable metadata indexing # add '-e2ts' to enable metadata indexing
# remove '--ansi' to disable colored logs
# #
# with `Type=notify`, copyparty will signal systemd when it is ready to # with `Type=notify`, copyparty will signal systemd when it is ready to
# accept connections; correctly delaying units depending on copyparty. # accept connections; correctly delaying units depending on copyparty.
@@ -59,7 +60,7 @@ ExecStartPre=+nft add rule ip nat prerouting tcp dport 443 redirect to :3923
ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf' ExecStartPre=+/bin/bash -c 'mkdir -p /run/tmpfiles.d/ && echo "x /tmp/pe-copyparty*" > /run/tmpfiles.d/copyparty.conf'
# copyparty settings # copyparty settings
ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py -e2d -v /mnt::rw ExecStart=/usr/bin/python3 /usr/local/bin/copyparty-sfx.py --ansi -e2d -v /mnt::rw
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -6,6 +6,10 @@ import platform
import sys import sys
import time import time
# fmt: off
_:tuple[int,int]=(0,0) # _____________________________________________________________________ hey there! if you are reading this, your python is too old to run copyparty without some help. Please use https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py or the pypi package instead, or see https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building if you want to build it yourself :-) ************************************************************************************************************************************************
# fmt: on
try: try:
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
except: except:
@@ -27,7 +31,12 @@ WINDOWS: Any = (
else False else False
) )
VT100 = not WINDOWS or WINDOWS >= [10, 0, 14393] VT100 = "--ansi" in sys.argv or (
os.environ.get("NO_COLOR", "").lower() in ("", "0", "false")
and sys.stdout.isatty()
and "--no-ansi" not in sys.argv
and (not WINDOWS or WINDOWS >= [10, 0, 14393])
)
# introduced in anniversary update # introduced in anniversary update
ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"] ANYWIN = WINDOWS or sys.platform in ["msys", "cygwin"]

View File

@@ -10,11 +10,9 @@ __url__ = "https://github.com/9001/copyparty/"
import argparse import argparse
import base64 import base64
import filecmp
import locale import locale
import os import os
import re import re
import shutil
import socket import socket
import sys import sys
import threading import threading
@@ -242,6 +240,37 @@ def get_srvname() -> str:
return ret return ret
def get_fk_salt(cert_path) -> str:
fp = os.path.join(E.cfg, "fk-salt.txt")
try:
with open(fp, "rb") as f:
ret = f.read().strip()
except:
if os.path.exists(cert_path):
zi = os.path.getmtime(cert_path)
ret = "{}".format(zi).encode("utf-8")
else:
ret = base64.b64encode(os.urandom(18))
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")
def ensure_locale() -> None: def ensure_locale() -> None:
safe = "en_US.UTF-8" safe = "en_US.UTF-8"
for x in [ for x in [
@@ -261,45 +290,22 @@ def ensure_locale() -> None:
warn(t.format(safe)) warn(t.format(safe))
def ensure_cert(al: argparse.Namespace) -> None: def ensure_webdeps() -> None:
ap = os.path.join(E.mod, "web/deps/mini-fa.woff")
if os.path.exists(ap):
return
warn(
"""could not find webdeps;
if you are running the sfx, or exe, or pypi package, or docker image,
then this is a bug! Please let me know so I can fix it, thanks :-)
https://github.com/9001/copyparty/issues/new?labels=bug&template=bug_report.md
however, if you are a dev, or running copyparty from source, and you want
full client functionality, you will need to build or obtain the webdeps:
https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building
""" """
the default cert (and the entire TLS support) is only here to enable the )
crypto.subtle javascript API, which is necessary due to the webkit guys
being massive memers (https://www.chromium.org/blink/webcrypto)
i feel awful about this and so should they
"""
cert_insec = os.path.join(E.mod, "res/insecure.pem")
cert_appdata = os.path.join(E.cfg, "cert.pem")
if not os.path.isfile(al.cert):
if cert_appdata != al.cert:
raise Exception("certificate file does not exist: " + al.cert)
shutil.copy(cert_insec, al.cert)
with open(al.cert, "rb") as f:
buf = f.read()
o1 = buf.find(b" PRIVATE KEY-")
o2 = buf.find(b" CERTIFICATE-")
m = "unsupported certificate format: "
if o1 < 0:
raise Exception(m + "no private key inside pem")
if o2 < 0:
raise Exception(m + "no server certificate inside pem")
if o1 > o2:
raise Exception(m + "private key must appear before server certificate")
try:
if filecmp.cmp(al.cert, cert_insec):
lprint(
"\033[33musing default TLS certificate; https will be insecure."
+ "\033[36m\ncertificate location: {}\033[0m\n".format(al.cert)
)
except:
pass
# speaking of the default cert,
# printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout
def configure_ssl_ver(al: argparse.Namespace) -> None: def configure_ssl_ver(al: argparse.Namespace) -> None:
@@ -486,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
@@ -522,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",
@@ -536,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
@@ -571,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
@@ -629,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
"""
),
],
] ]
@@ -729,6 +816,25 @@ def add_tls(ap, cert_path):
ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets for later decryption in wireshark") ap2.add_argument("--ssl-log", metavar="PATH", type=u, help="log master secrets for later decryption in wireshark")
def add_cert(ap, cert_path):
cert_dir = os.path.dirname(cert_path)
ap2 = ap.add_argument_group('TLS certificate generator options')
ap2.add_argument("--no-crt", action="store_true", help="disable automatic certificate creation")
ap2.add_argument("--crt-ns", metavar="N,N", type=u, default="", help="comma-separated list of FQDNs (domains) to add into the certificate")
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-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-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-cn", metavar="TXT", type=u, default="partyco", help="CA/server-cert common-name")
ap2.add_argument("--crt-cnc", metavar="TXT", type=u, default="--crt-cn", help="override CA name")
ap2.add_argument("--crt-cns", metavar="TXT", type=u, default="--crt-cn cpp", help="override server-cert name")
ap2.add_argument("--crt-back", metavar="HRS", type=float, default=72, help="backdate in hours")
ap2.add_argument("--crt-alg", metavar="S-N", type=u, default="ecdsa-256", help="algorithm and keysize; one of these: ecdsa-256 rsa-4096 rsa-2048")
def add_zeroconf(ap): def add_zeroconf(ap):
ap2 = ap.add_argument_group("Zeroconf options") ap2 = ap.add_argument_group("Zeroconf options")
ap2.add_argument("-z", action="store_true", help="enable all zeroconf backends (mdns, ssdp)") ap2.add_argument("-z", action="store_true", help="enable all zeroconf backends (mdns, ssdp)")
@@ -801,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")
@@ -811,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):
@@ -833,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")
@@ -841,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")
@@ -859,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")
@@ -870,6 +992,8 @@ def add_logging(ap):
ap2 = ap.add_argument_group('logging options') ap2 = ap.add_argument_group('logging options')
ap2.add_argument("-q", action="store_true", help="quiet") ap2.add_argument("-q", action="store_true", help="quiet")
ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz") ap2.add_argument("-lo", metavar="PATH", type=u, help="logfile, example: \033[32mcpp-%%Y-%%m%%d-%%H%%M%%S.txt.xz")
ap2.add_argument("--no-ansi", action="store_true", default=not VT100, help="disable colors; same as environment-variable NO_COLOR")
ap2.add_argument("--ansi", action="store_true", help="force colors; overrides environment-variable NO_COLOR")
ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup") ap2.add_argument("--no-voldump", action="store_true", help="do not list volumes and permissions on startup")
ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs") ap2.add_argument("--log-conn", action="store_true", help="debug: print tcp-server msgs")
ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling") ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling")
@@ -889,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")
@@ -901,7 +1025,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than SEC seconds") ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than SEC seconds")
ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled") ap2.add_argument("--th-clean", metavar="SEC", type=int, default=43200, help="cleanup interval; 0=disabled")
ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than --th-poke seconds will get deleted every --th-clean seconds") ap2.add_argument("--th-maxage", metavar="SEC", type=int, default=604800, help="max folder age -- folders which haven't been poked for longer than --th-poke seconds will get deleted every --th-clean seconds")
ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; case-insensitive if -e2d") ap2.add_argument("--th-covers", metavar="N,N", type=u, default="folder.png,folder.jpg,cover.png,cover.jpg", help="folder thumbnails to stat/look for; enabling -e2d will make these case-insensitive, and also automatically select thumbnails for all folders that contain pics, even if none match this pattern")
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html # https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
# https://github.com/libvips/libvips # https://github.com/libvips/libvips
# ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:' # ffmpeg -hide_banner -demuxers | awk '/^ D /{print$2}' | while IFS= read -r x; do ffmpeg -hide_banner -h demuxer=$x; done | grep -E '^Demuxer |extensions:'
@@ -955,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")
@@ -963,9 +1087,11 @@ def add_db_metadata(ap):
def add_ui(ap, retry): def add_ui(ap, retry):
ap2 = ap.add_argument_group('ui options') ap2 = ap.add_argument_group('ui options')
ap2.add_argument("--grid", action="store_true", help="show grid/thumbnails by default (volflag=grid)")
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("--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")
@@ -1014,10 +1140,8 @@ def run_argparse(
cert_path = os.path.join(E.cfg, "cert.pem") cert_path = os.path.join(E.cfg, "cert.pem")
try: fk_salt = get_fk_salt(cert_path)
fk_salt = unicode(os.path.getmtime(cert_path)) ah_salt = get_ah_salt()
except:
fk_salt = "hunter2"
hcores = min(CORES, 4) # optimal on py3.11 @ r5-4500U hcores = min(CORES, 4) # optimal on py3.11 @ r5-4500U
@@ -1028,6 +1152,7 @@ def run_argparse(
add_general(ap, nc, srvname) add_general(ap, nc, srvname)
add_network(ap) add_network(ap)
add_tls(ap, cert_path) add_tls(ap, cert_path)
add_cert(ap, cert_path)
add_qr(ap, tty) add_qr(ap, tty)
add_zeroconf(ap) add_zeroconf(ap)
add_zc_mdns(ap) add_zc_mdns(ap)
@@ -1040,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)
@@ -1112,6 +1239,8 @@ def main(argv: Optional[list[str]] = None) -> None:
ensure_locale() ensure_locale()
ensure_webdeps()
for k, v in zip(argv[1:], argv[2:]): for k, v in zip(argv[1:], argv[2:]):
if k == "-c" and os.path.isfile(v): if k == "-c" and os.path.isfile(v):
supp = args_from_cfg(v) supp = args_from_cfg(v)
@@ -1123,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
@@ -1178,16 +1313,18 @@ def main(argv: Optional[list[str]] = None) -> None:
except: except:
sys.exit(1) sys.exit(1)
if HAVE_SSL: if al.ansi:
ensure_cert(al) al.no_ansi = False
elif not al.no_ansi:
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:
lprint("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n") lprint("\nfailed to disable quick-edit-mode:\n" + min_ex() + "\n")
if not VT100: if al.ansi:
al.wintitle = "" al.wintitle = ""
nstrs: list[str] = [] nstrs: list[str] = []
@@ -1206,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)
@@ -1266,6 +1401,7 @@ def main(argv: Optional[list[str]] = None) -> None:
configure_ssl_ciphers(al) configure_ssl_ciphers(al)
else: else:
warn("ssl module does not exist; cannot enable https") warn("ssl module does not exist; cannot enable https")
al.http_only = True
if PY2 and WINDOWS and al.e2d: if PY2 and WINDOWS and al.e2d:
warn( warn(

View File

@@ -1,8 +1,8 @@
# coding: utf-8 # coding: utf-8
VERSION = (1, 7, 1) VERSION = (1, 8, 2)
CODENAME = "unlinked" CODENAME = "argon"
BUILD_DT = (2023, 5, 7) 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
@@ -841,6 +870,9 @@ class AuthSrv(object):
if not ln.split("#")[0].strip(): if not ln.split("#")[0].strip():
continue continue
if re.match(r"^\[.*\]:$", ln):
ln = ln[:-1]
subsection = ln in (catx, catf) subsection = ln in (catx, catf)
if ln.startswith("[") or subsection: if ln.startswith("[") or subsection:
self._e() self._e()
@@ -911,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():
@@ -920,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:
@@ -956,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":
@@ -988,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 == "*":
@@ -1014,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:
@@ -1059,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:
@@ -1103,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 = {}
@@ -1161,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:
@@ -1287,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
@@ -1375,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
@@ -1400,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:
@@ -1423,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
@@ -1533,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):
@@ -1558,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)
@@ -1578,6 +1645,12 @@ class AuthSrv(object):
if t: if t:
self.log("\n\033[{}\033[0m\n".format(t)) self.log("\n\033[{}\033[0m\n".format(t))
zv, _ = vfs.get("/", "*", False, False)
zs = zv.realpath.lower()
if zs in ("/", "c:\\") or zs.startswith(r"c:\windows"):
t = "you are sharing a system directory: {}\n"
self.log(t.format(zv.realpath), c=1)
try: try:
zv, _ = vfs.get("/", "*", False, True) zv, _ = vfs.get("/", "*", False, True)
if self.warn_anonwrite and os.getcwd() == zv.realpath: if self.warn_anonwrite and os.getcwd() == zv.realpath:
@@ -1596,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:]
@@ -1653,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
@@ -1736,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
@@ -1795,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():
@@ -1991,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,

222
copyparty/cert.py Normal file
View File

@@ -0,0 +1,222 @@
import calendar
import errno
import filecmp
import json
import os
import shutil
import time
from .util import Netdev, runcmd
HAVE_CFSSL = True
if True: # pylint: disable=using-constant-test
from .util import RootLogger
def ensure_cert(log: "RootLogger", args) -> None:
"""
the default cert (and the entire TLS support) is only here to enable the
crypto.subtle javascript API, which is necessary due to the webkit guys
being massive memers (https://www.chromium.org/blink/webcrypto)
i feel awful about this and so should they
"""
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
cert_appdata = os.path.join(args.E.cfg, "cert.pem")
if not os.path.isfile(args.cert):
if cert_appdata != args.cert:
raise Exception("certificate file does not exist: " + args.cert)
shutil.copy(cert_insec, args.cert)
with open(args.cert, "rb") as f:
buf = f.read()
o1 = buf.find(b" PRIVATE KEY-")
o2 = buf.find(b" CERTIFICATE-")
m = "unsupported certificate format: "
if o1 < 0:
raise Exception(m + "no private key inside pem")
if o2 < 0:
raise Exception(m + "no server certificate inside pem")
if o1 > o2:
raise Exception(m + "private key must appear before server certificate")
try:
if filecmp.cmp(args.cert, cert_insec):
t = "using default TLS certificate; https will be insecure:\033[36m {}"
log("cert", t.format(args.cert), 3)
except:
pass
# speaking of the default cert,
# printf 'NO\n.\n.\n.\n.\ncopyparty-insecure\n.\n' | faketime '2000-01-01 00:00:00' openssl req -x509 -sha256 -newkey rsa:2048 -keyout insecure.pem -out insecure.pem -days $((($(printf %d 0x7fffffff)-$(date +%s --date=2000-01-01T00:00:00Z))/(60*60*24))) -nodes && ls -al insecure.pem && openssl x509 -in insecure.pem -text -noout
def _read_crt(args, fn):
try:
if not os.path.exists(os.path.join(args.crt_dir, fn)):
return 0, {}
acmd = ["cfssl-certinfo", "-cert", fn]
rc, so, se = runcmd(acmd, cwd=args.crt_dir)
if rc:
return 0, {}
inf = json.loads(so)
zs = inf["not_after"]
expiry = calendar.timegm(time.strptime(zs, "%Y-%m-%dT%H:%M:%SZ"))
return expiry, inf
except OSError as ex:
if ex.errno == errno.ENOENT:
raise
return 0, {}
except:
return 0, {}
def _gen_ca(log: "RootLogger", args):
expiry = _read_crt(args, "ca.pem")[0]
if time.time() + args.crt_cdays * 60 * 60 * 24 * 0.1 < expiry:
return
backdate = "{}m".format(int(args.crt_back * 60))
expiry = "{}m".format(int(args.crt_cdays * 60 * 24))
cn = args.crt_cnc.replace("--crt-cn", args.crt_cn)
algo, ksz = args.crt_alg.split("-")
req = {
"CN": cn,
"CA": {"backdate": backdate, "expiry": expiry, "pathlen": 0},
"key": {"algo": algo, "size": int(ksz)},
"names": [{"O": cn}],
}
sin = json.dumps(req).encode("utf-8")
log("cert", "creating new ca ...", 6)
cmd = "cfssl gencert -initca -"
rc, so, se = runcmd(cmd.split(), 30, sin=sin)
if rc:
raise Exception("failed to create ca-cert: {}, {}".format(rc, se), 3)
cmd = "cfssljson -bare ca"
sin = so.encode("utf-8")
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
if rc:
raise Exception("failed to translate ca-cert: {}, {}".format(rc, se), 3)
bname = os.path.join(args.crt_dir, "ca")
os.rename(bname + "-key.pem", bname + ".key")
os.unlink(bname + ".csr")
log("cert", "new ca OK", 2)
def _gen_srv(log: "RootLogger", args, netdevs: dict[str, Netdev]):
names = args.crt_ns.split(",") if args.crt_ns else []
if not args.crt_exact:
for n in names[:]:
names.append("*.{}".format(n))
if not args.crt_noip:
for ip in netdevs.keys():
names.append(ip.split("/")[0])
if args.crt_nolo:
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:
names = ["127.0.0.1"]
if "127.0.0.1" in names or "::1" in names:
names.append("localhost")
names = list({x: 1 for x in names}.keys())
try:
expiry, inf = _read_crt(args, "srv.pem")
expired = time.time() + args.crt_sdays * 60 * 60 * 24 * 0.1 > expiry
cert_insec = os.path.join(args.E.mod, "res/insecure.pem")
for n in names:
if n not in inf["sans"]:
raise Exception("does not have {}".format(n))
if expired:
raise Exception("old server-cert has expired")
if not filecmp.cmp(args.cert, cert_insec):
return
except Exception as ex:
log("cert", "will create new server-cert; {}".format(ex))
log("cert", "creating server-cert ...", 6)
backdate = "{}m".format(int(args.crt_back * 60))
expiry = "{}m".format(int(args.crt_sdays * 60 * 24))
cfg = {
"signing": {
"default": {
"backdate": backdate,
"expiry": expiry,
"usages": ["signing", "key encipherment", "server auth"],
}
}
}
with open(os.path.join(args.crt_dir, "cfssl.json"), "wb") as f:
f.write(json.dumps(cfg).encode("utf-8"))
cn = args.crt_cns.replace("--crt-cn", args.crt_cn)
algo, ksz = args.crt_alg.split("-")
req = {
"key": {"algo": algo, "size": int(ksz)},
"names": [{"O": cn}],
}
sin = json.dumps(req).encode("utf-8")
cmd = "cfssl gencert -config=cfssl.json -ca ca.pem -ca-key ca.key -profile=www"
acmd = cmd.split() + ["-hostname=" + ",".join(names), "-"]
rc, so, se = runcmd(acmd, 30, sin=sin, cwd=args.crt_dir)
if rc:
raise Exception("failed to create cert: {}, {}".format(rc, se))
cmd = "cfssljson -bare srv"
sin = so.encode("utf-8")
rc, so, se = runcmd(cmd.split(), 10, sin=sin, cwd=args.crt_dir)
if rc:
raise Exception("failed to translate cert: {}, {}".format(rc, se))
bname = os.path.join(args.crt_dir, "srv")
os.rename(bname + "-key.pem", bname + ".key")
os.unlink(bname + ".csr")
with open(os.path.join(args.crt_dir, "ca.pem"), "rb") as f:
ca = f.read()
with open(bname + ".key", "rb") as f:
skey = f.read()
with open(bname + ".pem", "rb") as f:
scrt = f.read()
with open(args.cert, "wb") as f:
f.write(skey + scrt + ca)
log("cert", "new server-cert OK", 2)
def gencert(log: "RootLogger", args, netdevs: dict[str, Netdev]):
global HAVE_CFSSL
if args.http_only:
return
if args.no_crt or not HAVE_CFSSL:
ensure_cert(log, args)
return
try:
_gen_ca(log, args)
_gen_srv(log, args, netdevs)
except Exception as ex:
HAVE_CFSSL = False
log("cert", "could not create TLS certificates: {}".format(ex), 3)
if getattr(ex, "errno", 0) == errno.ENOENT:
t = "install cfssl if you want to fix this; https://github.com/cloudflare/cfssl/releases/latest"
log("cert", t, 6)
ensure_cert(log, args)

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",
} }
@@ -24,6 +25,7 @@ def vf_bmap() -> dict[str, str]:
"e2v", "e2v",
"e2vu", "e2vu",
"e2vp", "e2vp",
"grid",
"hardlink", "hardlink",
"magic", "magic",
"no_sb_md", "no_sb_md",
@@ -39,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"): for k in ("dbd", "lg_sbf", "md_sbf", "nrand", "unlist"):
ret[k] = k ret[k] = k
return ret return ret
@@ -48,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
@@ -77,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",
@@ -121,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",
@@ -131,8 +142,11 @@ 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",
"unlist": "dont list files matching REGEX",
"html_head=TXT": "includes TXT in the <head>", "html_head=TXT": "includes TXT in the <head>",
"robots": "allows indexing by search engines (default)", "robots": "allows indexing by search engines (default)",
"norobots": "kindly asks search engines to leave", "norobots": "kindly asks search engines to leave",
@@ -142,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:
@@ -502,9 +507,9 @@ class Ftpd(object):
for h_lp in hs: for h_lp in hs:
h2, lp = h_lp h2, lp = h_lp
h2.hub = hub FtpHandler.hub = h2.hub = hub
h2.args = hub.args FtpHandler.args = h2.args = hub.args
h2.authorizer = FtpAuth(hub) FtpHandler.authorizer = h2.authorizer = FtpAuth(hub)
if self.args.ftp_pr: if self.args.ftp_pr:
p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")] p1, p2 = [int(x) for x in self.args.ftp_pr.split("-")]

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())
@@ -726,7 +751,7 @@ class HttpCli(object):
def handle_get(self) -> bool: def handle_get(self) -> bool:
if self.do_log: if self.do_log:
logmsg = "%4s %s @%s" % (self.mode, self.req, self.uname) logmsg = "%-4s %s @%s" % (self.mode, self.req, self.uname)
if "range" in self.headers: if "range" in self.headers:
try: try:
@@ -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
@@ -3459,6 +3613,7 @@ class HttpCli(object):
break break
vf = vn.flags vf = vn.flags
unlist = vf.get("unlist", "")
ls_ret = { ls_ret = {
"dirs": [], "dirs": [],
"files": [], "files": [],
@@ -3469,6 +3624,7 @@ class HttpCli(object):
"itag": e2t, "itag": e2t,
"lifetime": vn.flags.get("lifetime") or 0, "lifetime": vn.flags.get("lifetime") or 0,
"frand": bool(vn.flags.get("rand")), "frand": bool(vn.flags.get("rand")),
"unlist": unlist,
"perms": perms, "perms": perms,
"logues": logues, "logues": logues,
"readme": readme, "readme": readme,
@@ -3500,6 +3656,8 @@ class HttpCli(object):
"readme": readme, "readme": readme,
"title": html_escape(self.vpath, crlf=True) or "💾🎉", "title": html_escape(self.vpath, crlf=True) or "💾🎉",
"srv_info": srv_infot, "srv_info": srv_infot,
"dgrid": "grid" in vf,
"unlist": unlist,
"dtheme": self.args.theme, "dtheme": self.args.theme,
"themes": self.args.themes, "themes": self.args.themes,
"turbolvl": self.args.turbo, "turbolvl": self.args.turbo,
@@ -3674,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:
@@ -3729,7 +3898,12 @@ class HttpCli(object):
dirs.sort(key=itemgetter("name")) dirs.sort(key=itemgetter("name"))
if is_js: if is_js:
j2a["ls0"] = {"dirs": dirs, "files": files, "taglist": taglist} j2a["ls0"] = {
"dirs": dirs,
"files": files,
"taglist": taglist,
"unlist": unlist,
}
j2a["files"] = [] j2a["files"] = []
else: else:
j2a["files"] = dirs + files j2a["files"] = dirs + files

View File

@@ -54,7 +54,6 @@ class HttpConn(object):
self.args: argparse.Namespace = hsrv.args # mypy404 self.args: argparse.Namespace = hsrv.args # mypy404
self.E: EnvParams = self.args.E self.E: EnvParams = self.args.E
self.asrv: AuthSrv = hsrv.asrv # mypy404 self.asrv: AuthSrv = hsrv.asrv # mypy404
self.cert_path = hsrv.cert_path
self.u2fh: Util.FHC = hsrv.u2fh # mypy404 self.u2fh: Util.FHC = hsrv.u2fh # mypy404
self.iphash: HMaccas = hsrv.broker.iphash self.iphash: HMaccas = hsrv.broker.iphash
self.bans: dict[str, int] = hsrv.bans self.bans: dict[str, int] = hsrv.bans
@@ -114,7 +113,7 @@ class HttpConn(object):
def _detect_https(self) -> bool: def _detect_https(self) -> bool:
method = None method = None
if self.cert_path: if True:
try: try:
method = self.s.recv(4, socket.MSG_PEEK) method = self.s.recv(4, socket.MSG_PEEK)
except socket.timeout: except socket.timeout:
@@ -148,7 +147,7 @@ class HttpConn(object):
self.sr = None self.sr = None
if self.args.https_only: if self.args.https_only:
is_https = True is_https = True
elif self.args.http_only or not HAVE_SSL: elif self.args.http_only:
is_https = False is_https = False
else: else:
# raise Exception("asdf") # raise Exception("asdf")
@@ -162,7 +161,7 @@ class HttpConn(object):
self.log_src = self.log_src.replace("[36m", "[35m") self.log_src = self.log_src.replace("[36m", "[35m")
try: try:
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(self.cert_path) ctx.load_cert_chain(self.args.cert)
if self.args.ssl_ver: if self.args.ssl_ver:
ctx.options &= ~self.args.ssl_flags_en ctx.options &= ~self.args.ssl_flags_en
ctx.options |= self.args.ssl_flags_de ctx.options |= self.args.ssl_flags_de

View File

@@ -33,7 +33,23 @@ except MNFE:
* (try another python version, if you have one) * (try another python version, if you have one)
* (try copyparty.sfx instead) * (try copyparty.sfx instead)
""".format( """.format(
os.path.basename(sys.executable) sys.executable
)
)
sys.exit(1)
except SyntaxError:
if EXE:
raise
print(
"""\033[1;31m
your jinja2 version is incompatible with your python version;\033[33m
please try to replace it with an older version:\033[0m
* {} -m pip install --user jinja2==2.11.3
* (try another python version, if you have one)
* (try copyparty.sfx instead)
""".format(
sys.executable
) )
) )
sys.exit(1) sys.exit(1)
@@ -132,12 +148,6 @@ class HttpSrv(object):
self.ssdp = SSDPr(broker) self.ssdp = SSDPr(broker)
cert_path = self.args.cert
if bos.path.exists(cert_path):
self.cert_path = cert_path
else:
self.cert_path = ""
if self.tp_q: if self.tp_q:
self.start_threads(4) self.start_threads(4)

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()

View File

@@ -1,6 +1,7 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import errno
import random import random
import select import select
import socket import socket
@@ -277,6 +278,18 @@ class MDNS(MCast):
zf = time.time() + 2 zf = time.time() + 2
self.probing = zf # cant unicast so give everyone an extra sec self.probing = zf # cant unicast so give everyone an extra sec
self.unsolicited = [zf, zf + 1, zf + 3, zf + 7] # rfc-8.3 self.unsolicited = [zf, zf + 1, zf + 3, zf + 7] # rfc-8.3
try:
self.run2()
except OSError as ex:
if ex.errno != errno.EBADF:
raise
self.log("stopping due to {}".format(ex), "90")
self.log("stopped", 2)
def run2(self) -> None:
last_hop = time.time() last_hop = time.time()
ihop = self.args.mc_hop ihop = self.args.mc_hop
while self.running: while self.running:
@@ -314,8 +327,6 @@ class MDNS(MCast):
self.log(t.format(self.hn[:-1]), 2) self.log(t.format(self.hn[:-1]), 2)
self.probing = 0 self.probing = 0
self.log("stopped", 2)
def stop(self, panic=False) -> None: def stop(self, panic=False) -> None:
self.running = False self.running = False
for srv in self.srv.values(): for srv in self.srv.values():

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

@@ -1,6 +1,7 @@
# coding: utf-8 # coding: utf-8
from __future__ import print_function, unicode_literals from __future__ import print_function, unicode_literals
import errno
import re import re
import select import select
import socket import socket
@@ -129,6 +130,17 @@ class SSDPd(MCast):
srv.hport = hp srv.hport = hp
self.log("listening") self.log("listening")
try:
self.run2()
except OSError as ex:
if ex.errno != errno.EBADF:
raise
self.log("stopping due to {}".format(ex), "90")
self.log("stopped", 2)
def run2(self) -> None:
while self.running: while self.running:
rdy = select.select(self.srv, [], [], self.args.z_chk or 180) rdy = select.select(self.srv, [], [], self.args.z_chk or 180)
rx: list[socket.socket] = rdy[0] # type: ignore rx: list[socket.socket] = rdy[0] # type: ignore
@@ -148,8 +160,6 @@ class SSDPd(MCast):
) )
self.log(t, 6) self.log(t, 6)
self.log("stopped", 2)
def stop(self) -> None: def stop(self) -> None:
self.running = False self.running = False
for srv in self.srv.values(): for srv in self.srv.values():

View File

@@ -28,8 +28,9 @@ if True: # pylint: disable=using-constant-test
import typing import typing
from typing import Any, Optional, Union from typing import Any, Optional, Union
from .__init__ import ANYWIN, EXE, MACOS, TYPE_CHECKING, VT100, 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
@@ -80,6 +81,7 @@ class SvcHub(object):
self.dargs = dargs self.dargs = dargs
self.argv = argv self.argv = argv
self.E: EnvParams = args.E self.E: EnvParams = args.E
self.no_ansi = args.no_ansi
self.logf: Optional[typing.TextIO] = None self.logf: Optional[typing.TextIO] = None
self.logf_base_fn = "" self.logf_base_fn = ""
self.stop_req = False self.stop_req = False
@@ -238,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:
@@ -268,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
@@ -681,11 +706,15 @@ class SvcHub(object):
now = time.time() now = time.time()
if now >= self.next_day: if now >= self.next_day:
dt = datetime.utcfromtimestamp(now) dt = datetime.utcfromtimestamp(now)
print("\033[36m{}\033[0m\n".format(dt.strftime("%Y-%m-%d")), end="") zs = "{}\n" if self.no_ansi else "\033[36m{}\033[0m\n"
zs = zs.format(dt.strftime("%Y-%m-%d"))
print(zs, end="")
self._set_next_day() self._set_next_day()
if self.logf:
self.logf.write(zs)
fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n" fmt = "\033[36m%s \033[33m%-21s \033[0m%s\n"
if not VT100: if self.no_ansi:
fmt = "%s %-21s %s\n" fmt = "%s %-21s %s\n"
if "\033" in msg: if "\033" in msg:
msg = ansi_re.sub("", msg) msg = ansi_re.sub("", msg)

View File

@@ -7,7 +7,8 @@ import socket
import sys import sys
import time import time
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, VT100, unicode from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .cert import gencert
from .stolen.qrcodegen import QrCode from .stolen.qrcodegen import QrCode
from .util import ( from .util import (
E_ACCESS, E_ACCESS,
@@ -295,6 +296,8 @@ class TcpSrv(object):
def _distribute_netdevs(self): def _distribute_netdevs(self):
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)
self.hub.restart_ftpd()
def shutdown(self) -> None: def shutdown(self) -> None:
self.stopping = True self.stopping = True
@@ -501,7 +504,7 @@ class TcpSrv(object):
zoom = 1 zoom = 1
qr = qrc.render(zoom, pad) qr = qrc.render(zoom, pad)
if not VT100: if self.args.no_ansi:
return "{}\n{}".format(txt, qr) return "{}\n{}".format(txt, qr)
halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m" halfc = "\033[40;48;5;{0}m{1}\033[47;48;5;{2}m"

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"])
@@ -2564,7 +2618,7 @@ class Up2k(object):
try: try:
if "hardlink" in flags: if "hardlink" in flags:
os.link(fsenc(src), fsenc(dst)) os.link(fsenc(absreal(src)), fsenc(dst))
linked = True linked = True
except Exception as ex: except Exception as ex:
self.log("cannot hardlink: " + repr(ex)) self.log("cannot hardlink: " + repr(ex))
@@ -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)
@@ -2019,6 +2024,8 @@ def shut_socket(log: "NamedLogger", sck: socket.socket, timeout: int = 3) -> Non
sck.shutdown(socket.SHUT_RDWR) sck.shutdown(socket.SHUT_RDWR)
except: except:
pass pass
except Exception as ex:
log("shut({}): {}".format(fd, ex), "90")
finally: finally:
td = time.time() - t0 td = time.time() - t0
if td >= 1: if td >= 1:
@@ -2420,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
@@ -2473,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)
@@ -2720,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;
@@ -1751,6 +1761,7 @@ html.y #tree.nowrap .ntree a+a:hover {
display: none; display: none;
} }
.ghead { .ghead {
background: var(--bg-u2);
border-radius: .3em; border-radius: .3em;
padding: .2em .5em; padding: .2em .5em;
line-height: 2.3em; line-height: 2.3em;
@@ -2464,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;
@@ -2489,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 {
@@ -2947,6 +2959,7 @@ html.b #treepar {
html.b #wrap { html.b #wrap {
margin-top: 2em; margin-top: 2em;
} }
html.by .ghead,
html.bz .ghead { html.bz .ghead {
background: var(--bg); background: var(--bg);
padding: .2em 0; padding: .2em 0;

View File

@@ -138,6 +138,7 @@
TS = "{{ ts }}", TS = "{{ ts }}",
acct = "{{ acct }}", acct = "{{ acct }}",
perms = {{ perms }}, perms = {{ perms }},
dgrid = {{ dgrid|tojson }},
themes = {{ themes }}, themes = {{ themes }},
dtheme = "{{ dtheme }}", dtheme = "{{ dtheme }}",
srvinf = "{{ srv_info }}", srvinf = "{{ srv_info }}",

View File

@@ -1974,7 +1974,10 @@ var pbar = (function () {
w = 8, w = 8,
apos, adur; apos, adur;
clearTimeout(t_redraw); if (t_redraw) {
clearTimeout(t_redraw);
t_redraw = 0;
}
pctx.clearRect(0, 0, pc.w, pc.h); pctx.clearRect(0, 0, pc.w, pc.h);
if (!mp || !mp.au || !isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos) if (!mp || !mp.au || !isNum(adur = mp.au.duration) || !isNum(apos = mp.au.currentTime) || apos < 0 || adur < apos)
@@ -3077,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();
@@ -4523,7 +4528,7 @@ var thegrid = (function () {
bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty); bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty);
bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel); bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel);
bcfg_bind(r, 'en', 'griden', false, function (v) { bcfg_bind(r, 'en', 'griden', dgrid, function (v) {
v ? loadgrid() : r.setvis(true); v ? loadgrid() : r.setvis(true);
pbar.onresize(); pbar.onresize();
vbar.onresize(); vbar.onresize();
@@ -5723,6 +5728,12 @@ var treectl = (function () {
seen = {}; seen = {};
r.lsc = res; r.lsc = res;
if (res.unlist) {
var ptn = new RegExp(res.unlist);
for (var a = nodes.length - 1; a >= 0; a--)
if (ptn.exec(nodes[a].href.split('?')[0]))
nodes.splice(a, 1);
}
nodes = sortfiles(nodes); nodes = sortfiles(nodes);
window.removeEventListener('scroll', r.tscroll); window.removeEventListener('scroll', r.tscroll);
r.trunc = nodes.length > r.nvis && location.hash.length < 2; r.trunc = nodes.length > r.nvis && location.hash.length < 2;
@@ -5769,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>');
@@ -6055,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

@@ -1826,6 +1826,7 @@ function up2k_init(subtle) {
timer.rm(etafun); timer.rm(etafun);
timer.rm(donut.do); timer.rm(donut.do);
ebi('u2tabw').style.minHeight = '0px';
utw_minh = 0; utw_minh = 0;
} }

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> or <code>ed#2644</code></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,123 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 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
## new features
* print a warning if `c:\`, `c:\windows*`, or all of `/` are shared
* upgraded the docker image to v3.18 which enables the [chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)
* in config files, allow trailing `:` in section headers
## bugfixes
* when `--hardlink` (or the volflag) is set, resolve symlinks before hardlinking
* uploads could fail due to relative symlinks
* really minor ux fixes
* left-align `GET` in access logs
* the upload panel didn't always shrink back down after uploads completed
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0507-1834 `v1.7.1` CräzY;PWDs
## new features
* webdav:
* support write-only folders
* option `--dav-auth` / volflag `davauth` forces clients to always auth
* helps clients such as `davfs2` see all folders if the root is anon-readable but some subfolders are not
* alternatively you could configure your client to always send the password in the `PW` header
* include usernames in http request logs
* audio player:
* consumes less power on phones when the screen is off
* smoother playback cursor on short songs
## bugfixes
* the characters `;` and `%` can now be used in passwords
* but non-ascii characters (such as the ä in the release title) can, in fact, not
* verify that all accounts have unique passwords on startup (#25)
## other changes
* ftpd: log incorrect passwords only, not correct ones
* `up2k.py` (the upload, folder-sync, and file-search client) has been renamed to [u2c.py](https://github.com/9001/copyparty/tree/hovudstraum/bin#u2cpy)
* `u2c` as in `up2k client`, or `up2k CLI`, or `upload-to-copyparty` -- good name
* now the only things named "up2k" are the web-ui and the server backend which is way less confusing
* upgrade packaging from [setup.py](https://github.com/9001/copyparty/blob/hovudstraum/setup.py) to [pyproject.toml](https://github.com/9001/copyparty/blob/hovudstraum/pyproject.toml)
* no practical consequences aside from a warm fuzzy feeling of being in the future
* the docker images ~~will be~~ got rebuilt 2023-05-11 ~~in a few days (when [alpine](https://alpinelinux.org/) 3.18 is released)~~ enabling [the chiptune player](https://a.ocv.me/pub/demo/music/chiptunes/#af-f6fb2e5f)
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
# 2023-0429-2114 `v1.7.0` unlinked # 2023-0429-2114 `v1.7.0` unlinked

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=...`
@@ -236,25 +246,13 @@ pip install black==21.12b0 click==8.0.2 bandit pylint flake8 isort mypy # vscod
## just the sfx ## just the sfx
first grab the web-dependencies from a previous sfx (assuming you don't need to modify something in those): if you just want to modify the copyparty source code (py/html/css/js) then this is the easiest approach
```sh build the sfx using any of the following examples:
rm -rf copyparty/web/deps
curl -L https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py >x.py
python3 x.py --version
rm x.py
cp -R /tmp/pe-copyparty.$(id -u)/copyparty/web/deps copyparty/web/
```
or you could build the web-dependencies from source instead (NB: does not include prismjs, need to grab that manually):
```sh
make -C scripts/deps-docker
```
then build the sfx using any of the following examples:
```sh ```sh
./scripts/make-sfx.sh # regular edition ./scripts/make-sfx.sh # regular edition
./scripts/make-sfx.sh fast # build faster (worse js/css compression)
./scripts/make-sfx.sh gz no-cm # gzip-compressed + no fancy markdown editor ./scripts/make-sfx.sh gz no-cm # gzip-compressed + no fancy markdown editor
``` ```
@@ -285,6 +283,8 @@ python3 setup.py install --skip-build --prefix=/usr --root=$HOME/pe/copyparty
also builds the sfx so skip the sfx section above also builds the sfx so skip the sfx section above
*WARNING: `rls.sh` has not yet been updated with the docker-images and arch/nix packaging*
does everything completely from scratch, straight from your local repo does everything completely from scratch, straight from your local repo
in the `scripts` folder: in the `scripts` folder:

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"
@@ -95,7 +96,6 @@ target-version = ['py27']
[tool.isort] [tool.isort]
profile = "black" profile = "black"
include_trailing_comma = true include_trailing_comma = true
force_sort_within_sections = true
[tool.bandit] [tool.bandit]
skips = ["B104", "B110", "B112"] skips = ["B104", "B110", "B112"]

View File

@@ -1,5 +1,4 @@
# TODO easymde embeds codemirror on 3.17 due to new npm probably FROM alpine:3.18
FROM alpine:3.16
WORKDIR /z WORKDIR /z
ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \ ENV ver_asmcrypto=c72492f4a66e17a0e5dd8ad7874de354f3ccdaa5 \
ver_hashwasm=4.9.0 \ ver_hashwasm=4.9.0 \

View File

@@ -5,11 +5,13 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-ac" \ org.opencontainers.image.title="copyparty-ac" \
org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)" org.opencontainers.image.description="copyparty with Pillow and FFmpeg (image/audio/video thumbnails, audio transcoding, media tags)"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \ RUN apk --no-cache add !pyc \
wget \ wget \
py3-pillow \ py3-argon2-cffi py3-pillow \
ffmpeg \ ffmpeg \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \ && mkdir /cfg /w \
&& chmod 777 /cfg /w \ && chmod 777 /cfg /w \
&& echo % /cfg > initcfg && echo % /cfg > initcfg

View File

@@ -5,26 +5,27 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-dj" \ org.opencontainers.image.title="copyparty-dj" \
org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection" org.opencontainers.image.description="copyparty with all optional dependencies, including musical key / bpm detection"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
COPY i/bin/mtag/install-deps.sh ./ COPY i/bin/mtag/install-deps.sh ./
COPY i/bin/mtag/audio-bpm.py /mtag/ 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 \ 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 \
vamp-sdk vamp-sdk-libs \ vamp-sdk vamp-sdk-libs \
&& python3 -m pip install pyvips \ && apk add -t .bd \
&& apk --no-cache add -t .bd \
bash wget gcc g++ make cmake patchelf \ bash wget gcc g++ make cmake patchelf \
python3-dev ffmpeg-dev fftw-dev libsndfile-dev \ python3-dev ffmpeg-dev fftw-dev libsndfile-dev \
py3-wheel py3-numpy-dev \ py3-wheel py3-numpy-dev \
vamp-sdk-dev \ vamp-sdk-dev \
&& python3 -m pip install pyvips \
&& bash install-deps.sh \ && bash install-deps.sh \
&& apk del py3-pip .bd \ && apk del py3-pip .bd \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* /tmp/pyc \
&& chmod 777 /root \ && chmod 777 /root \
&& ln -s /root/vamp /root/.local / \ && ln -s /root/vamp /root/.local / \
&& mkdir /cfg /w \ && mkdir /cfg /w \

View File

@@ -5,10 +5,12 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-im" \ org.opencontainers.image.title="copyparty-im" \
org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)" org.opencontainers.image.description="copyparty with Pillow and Mutagen (image thumbnails, media tags)"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \ RUN apk --no-cache add !pyc \
wget \ wget \
py3-pillow py3-mutagen \ py3-argon2-cffi py3-pillow py3-mutagen \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \ && mkdir /cfg /w \
&& chmod 777 /cfg /w \ && chmod 777 /cfg /w \
&& echo % /cfg > initcfg && echo % /cfg > initcfg

View File

@@ -5,14 +5,19 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-iv" \ org.opencontainers.image.title="copyparty-iv" \
org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)" org.opencontainers.image.description="copyparty with Pillow, FFmpeg, libvips (image/audio/video thumbnails, audio transcoding, media tags)"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \ 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 \
bash wget gcc g++ make cmake patchelf \
python3-dev py3-wheel \
&& python3 -m pip install pyvips \ && python3 -m pip install pyvips \
&& apk del py3-pip \ && apk del py3-pip .bd \
&& rm -rf /var/cache/apk/* /tmp/pyc \
&& mkdir /cfg /w \ && mkdir /cfg /w \
&& chmod 777 /cfg /w \ && chmod 777 /cfg /w \
&& echo % /cfg > initcfg && echo % /cfg > initcfg

View File

@@ -5,9 +5,11 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-min" \ org.opencontainers.image.title="copyparty-min" \
org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding" org.opencontainers.image.description="just copyparty, no thumbnails / media tags / audio transcoding"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add \ RUN apk --no-cache add !pyc \
python3 \ python3 \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \ && mkdir /cfg /w \
&& chmod 777 /cfg /w \ && chmod 777 /cfg /w \
&& echo % /cfg > initcfg && echo % /cfg > initcfg

View File

@@ -5,10 +5,12 @@ LABEL org.opencontainers.image.url="https://github.com/9001/copyparty" \
org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.title="copyparty-min-pip" \ org.opencontainers.image.title="copyparty-min-pip" \
org.opencontainers.image.description="just copyparty, no thumbnails, no media tags, no audio transcoding" org.opencontainers.image.description="just copyparty, no thumbnails, no media tags, no audio transcoding"
ENV PYTHONPYCACHEPREFIX=/tmp/pyc
RUN apk --no-cache add python3 py3-pip \ RUN apk --no-cache add python3 py3-pip !pyc \
&& python3 -m pip install copyparty \ && python3 -m pip install copyparty \
&& apk del py3-pip \ && apk del py3-pip \
&& rm -rf /tmp/pyc \
&& mkdir /cfg /w \ && mkdir /cfg /w \
&& chmod 777 /cfg /w \ && chmod 777 /cfg /w \
&& echo % /cfg > initcfg && echo % /cfg > initcfg

View File

@@ -95,7 +95,7 @@ filt=
[ $(jobs -p | wc -l) -lt $(nproc) ] && break [ $(jobs -p | wc -l) -lt $(nproc) ] && break
while [ -e .blk ]; do sleep 0.2; done while [ -e .blk ]; do sleep 0.2; done
done done
aa="$(printf '%7s' $a)" aa="$(printf '%11s' $a-$i)"
# arm takes forever so make it top priority # arm takes forever so make it top priority
[ ${a::3} == arm ] && nice= || nice=nice [ ${a::3} == arm ] && nice= || nice=nice

View File

@@ -2,10 +2,14 @@
set -e set -e
echo echo
berr() { p=$(head -c 72 </dev/zero | tr '\0' =); printf '\n%s\n\n' $p; cat; printf '\n%s\n\n' $p; }
help() { exec cat <<'EOF' help() { exec cat <<'EOF'
# optional args: # optional args:
# #
# `fast` builds faster, with cheaper js/css compression
#
# `clean` uses files from git (everything except web/deps), # `clean` uses files from git (everything except web/deps),
# so local changes won't affect the produced sfx # so local changes won't affect the produced sfx
# #
@@ -42,6 +46,13 @@ help() { exec cat <<'EOF'
# #
# `no-dd` saves ~2k by removing the mouse cursor # `no-dd` saves ~2k by removing the mouse cursor
# #
# _____________________________________________________________________
# build behavior:
#
# `dl-wd` automatically downloads webdeps if necessary
#
# `ign-wd` allows building an sfx without webdeps
#
# --------------------------------------------------------------------- # ---------------------------------------------------------------------
# #
# if you are on windows, you can use msys2: # if you are on windows, you can use msys2:
@@ -109,6 +120,8 @@ while [ ! -z "$1" ]; do
no-hl) no_hl=1 ; ;; no-hl) no_hl=1 ; ;;
no-dd) no_dd=1 ; ;; no-dd) no_dd=1 ; ;;
no-cm) no_cm=1 ; ;; no-cm) no_cm=1 ; ;;
dl-wd) dl_wd=1 ; ;;
ign-wd) ign_wd=1 ; ;;
fast) zopf= ; ;; fast) zopf= ; ;;
ultra) ultra=1 ; ;; ultra) ultra=1 ; ;;
lang) shift;langs="$1"; ;; lang) shift;langs="$1"; ;;
@@ -223,7 +236,7 @@ necho() {
# enable this to dynamically remove type hints at startup, # enable this to dynamically remove type hints at startup,
# in case a future python version can use them for performance # in case a future python version can use them for performance
true || ( true && (
necho collecting strip-hints necho collecting strip-hints
f=../build/strip-hints-0.1.10.tar.gz f=../build/strip-hints-0.1.10.tar.gz
[ -e $f ] || [ -e $f ] ||
@@ -283,12 +296,56 @@ necho() {
rm -f copyparty/stolen/*/README.md rm -f copyparty/stolen/*/README.md
# remove type hints before build instead # remove type hints before build instead
(cd copyparty; "$pybin" ../../scripts/strip_hints/a.py; rm uh) (cd copyparty; PYTHONPATH="..:$PYTHONPATH" "$pybin" ../../scripts/strip_hints/a.py; rm uh)
licfile=$(realpath copyparty/res/COPYING.txt) licfile=$(realpath copyparty/res/COPYING.txt)
(cd ../scripts; ./genlic.sh "$licfile") (cd ../scripts; ./genlic.sh "$licfile")
} }
[ ! -e copyparty/web/deps/mini-fa.woff ] && [ $dl_wd ] && {
echo "could not find webdeps; downloading..."
url=https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py
wget -Ox.py "$url" || curl -L "$url" >x.py
echo "extracting webdeps..."
wdsrc="$("$pybin" x.py --version 2>&1 | tee /dev/stderr | awk '/sfxdir:/{sub(/.*: /,"");print;exit}')"
[ "$wdsrc" ] || {
echo failed to discover tempdir of reference copyparty-sfx.py
exit 1
}
rm -rf copyparty/web/deps
cp -pvR "$wdsrc/copyparty/web/deps" copyparty/web/
# also copy it out into the source-tree for next time
rm -rf ../copyparty/web/deps
cp -pR copyparty/web/deps ../copyparty/web
rm x.py
}
[ -e copyparty/web/deps/mini-fa.woff ] || [ $ign_wd ] || { berr <<'EOF'
ERROR:
could not find webdeps; the front-end will not be fully functional
please choose one of the following:
A) add the argument "dl-wd" to fix it automatically; this will
download copyparty-sfx.py and extract the webdeps from there
B) build the webdeps from source: make -C scripts/deps-docker
C) add the argument "ign-wd" to continue building the sfx without webdeps
alternative A is a good choice if you are only intending to
modify the copyparty source code (py/html/css/js) and do not
plan to make any changes to the mostly-third-party webdeps
there may be additional hints in the devnotes:
https://github.com/9001/copyparty/blob/hovudstraum/docs/devnotes.md#building
EOF
exit 1
}
ver= ver=
[ -z "$repack" ] && [ -z "$repack" ] &&
git describe --tags >/dev/null 2>/dev/null && { git describe --tags >/dev/null 2>/dev/null && {
@@ -421,7 +478,7 @@ while IFS= read -r f; do
done done
# up2k goes from 28k to 22k laff # up2k goes from 28k to 22k laff
awk 'BEGIN{gensub(//,"",1)}' </dev/null && awk 'BEGIN{gensub(//,"",1)}' </dev/null 2>/dev/null &&
echo entabbening && echo entabbening &&
find | grep -E '\.css$' | while IFS= read -r f; do find | grep -E '\.css$' | while IFS= read -r f; do
awk '{ awk '{
@@ -435,7 +492,9 @@ find | grep -E '\.css$' | while IFS= read -r f; do
1 1
' <$f | sed -r 's/;\}$/}/; /\{\}$/d' >t ' <$f | sed -r 's/;\}$/}/; /\{\}$/d' >t
tmv "$f" tmv "$f"
done done ||
echo "WARNING: your awk does not have gensub, so the sfx will not have optimal compression"
unexpand -h 2>/dev/null && unexpand -h 2>/dev/null &&
find | grep -E '\.(js|html)$' | while IFS= read -r f; do find | grep -E '\.(js|html)$' | while IFS= read -r f; do
unexpand -t 4 --first-only <"$f" >t unexpand -t 4 --first-only <"$f" >t
@@ -529,7 +588,7 @@ sed -r 's/([^ ]*) (.*)/\2.\1/' | grep -vE '/list1?$' > list1
for n in {1..50}; do for n in {1..50}; do
(grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | (shuf||gshuf) ) >list || true (grep -vE '\.(gz|br)$' list1; grep -E '\.(gz|br)$' list1 | (shuf||gshuf) ) >list || true
s=$( (sha1sum||shasum) < list | cut -c-16) s=$( (sha1sum||shasum) < list | cut -c-16)
grep -q $s "$zdir/h" && continue grep -q $s "$zdir/h" 2>/dev/null && continue
echo $s >> "$zdir/h" echo $s >> "$zdir/h"
break break
done done

View File

@@ -1,9 +1,9 @@
d5510a24cb5e15d6d30677335bbc7624c319b371c0513981843dc51d9b3a1e027661096dfcfc540634222bb2634be6db55bf95185b30133cb884f1e47652cf53 altgraph-0.17.3-py2.py3-none-any.whl d5510a24cb5e15d6d30677335bbc7624c319b371c0513981843dc51d9b3a1e027661096dfcfc540634222bb2634be6db55bf95185b30133cb884f1e47652cf53 altgraph-0.17.3-py2.py3-none-any.whl
eda6c38fc4d813fee897e969ff9ecc5acc613df755ae63df0392217bbd67408b5c1f6c676f2bf5497b772a3eb4e1a360e1245e1c16ee83f0af555f1ab82c3977 Git-2.39.1-32-bit.exe eda6c38fc4d813fee897e969ff9ecc5acc613df755ae63df0392217bbd67408b5c1f6c676f2bf5497b772a3eb4e1a360e1245e1c16ee83f0af555f1ab82c3977 Git-2.39.1-32-bit.exe
17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl 17ce52ba50692a9d964f57a23ac163fb74c77fdeb2ca988a6d439ae1fe91955ff43730c073af97a7b3223093ffea3479a996b9b50ee7fba0869247a56f74baa6 pefile-2023.2.7-py3-none-any.whl
d68c78bc83f4f48c604912b2d1ca4772b0e6ed676cd2eb439411e0a74d63fe215aac93dd9dab04ed341909a4a6a1efc13ec982516e3cb0fc7c355055e63d9178 pyinstaller-5.10.1-py3-none-win32.whl 2410f79f25b55829169fdd45611c04f51932f7701c0601df64ade0eb545c96ba950b7be186eb082482506bc689fcde5fe09c1f6f7cd77c2107028959b7e0d06f pyinstaller-5.12.0-py3-none-win32.whl
fe62705893c86eeb2d5b841da8debe05dedda98364dec190b487e718caad8a8735503bf93739a7a27ea793a835bf976fb919ceec1424b8fc550b936bae4a54e9 pyinstaller-5.10.1-py3-none-win_amd64.whl 62f4f3dda0526ea88cfc5af1806c7b53094672f4237d64c088626c226ad2fbc7549f6c9c6bbe5b228b1f87faf1e5c343ec468c485e4c17fe6d79c6b1f570153a pyinstaller-5.12.0-py3-none-win_amd64.whl
61c543983ff67e2bdff94d2d6198023679437363db8c660fa81683aff87c5928cd800720488e18d09be89fe45d6ab99be3ccb912cb2e03e2bca385b4338e1e42 pyinstaller_hooks_contrib-2023.2-py2.py3-none-any.whl 2612c263f73a02eab41404ba96e0c7cf8be4475104668b47dfbae50fadf977b3621dd4102682b301264d82b6e130d95ea84a28bf2106a626a1a2845dac16df47 pyinstaller_hooks_contrib-2023.3-py2.py3-none-any.whl
132a5380f33a245f2e744413a0e1090bc42b7356376de5121397cec5976b04b79f7c9ebe28af222c9c7b01461f7d7920810d220e337694727e0d7cd9e91fa667 pywin32_ctypes-0.2.0-py2.py3-none-any.whl 132a5380f33a245f2e744413a0e1090bc42b7356376de5121397cec5976b04b79f7c9ebe28af222c9c7b01461f7d7920810d220e337694727e0d7cd9e91fa667 pywin32_ctypes-0.2.0-py2.py3-none-any.whl
3c5adf0a36516d284a2ede363051edc1bcc9df925c5a8a9fa2e03cab579dd8d847fdad42f7fd5ba35992e08234c97d2dbfec40a9d12eec61c8dc03758f2bd88e typing_extensions-4.4.0-py3-none-any.whl 3c5adf0a36516d284a2ede363051edc1bcc9df925c5a8a9fa2e03cab579dd8d847fdad42f7fd5ba35992e08234c97d2dbfec40a9d12eec61c8dc03758f2bd88e typing_extensions-4.4.0-py3-none-any.whl
4b6e9ae967a769fe32be8cf0bc0d5a213b138d1e0344e97656d08a3d15578d81c06c45b334c872009db2db8f39db0c77c94ff6c35168d5e13801917667c08678 upx-4.0.2-win32.zip 4b6e9ae967a769fe32be8cf0bc0d5a213b138d1e0344e97656d08a3d15578d81c06c45b334c872009db2db8f39db0c77c94ff6c35168d5e13801917667c08678 upx-4.0.2-win32.zip
@@ -24,7 +24,7 @@ c06b3295d1d0b0f0a6f9a6cd0be861b9b643b4a5ea37857f0bd41c45deaf27bb927b71922dab74e6
ba91ab0518c61eff13e5612d9e6b532940813f6b56e6ed81ea6c7c4d45acee4d98136a383a25067512b8f75538c67c987cf3944bfa0229e3cb677e2fb81e763e zipp-3.10.0-py3-none-any.whl ba91ab0518c61eff13e5612d9e6b532940813f6b56e6ed81ea6c7c4d45acee4d98136a383a25067512b8f75538c67c987cf3944bfa0229e3cb677e2fb81e763e zipp-3.10.0-py3-none-any.whl
# win10 # win10
00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl 00558cca2e0ac813d404252f6e5aeacb50546822ecb5d0570228b8ddd29d94e059fbeb6b90393dee5abcddaca1370aca784dc9b095cbb74e980b3c024767fb24 Jinja2-3.1.2-py3-none-any.whl
b1db6f5a79fc15391547643e5973cf5946c0acfa6febb68bc90fc3f66369681100cc100f32dd04256dcefa510e7864c718515a436a4af3a10fe205c413c7e693 MarkupSafe-2.1.2-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
4b7711b950858f459d47145b88ccde659279c6af47144d58a1c54ea2ce4b80ec43eb7f69c68f12f8f6bc54c86a44e77441993257f7ad43aab364655de5c51bb1 python-3.11.2-amd64.exe a48ee8992eee60a0d620dced71b9f96596f5dd510e3024015aca55884cdb3f9e2405734bfc13f3f40b79106a77bc442cce02ac4c8f5d16207448052b368fd52a python-3.11.4-amd64.exe

View File

@@ -49,7 +49,7 @@ a = Analysis(
# this is the only change to the autogenerated specfile: # this is the only change to the autogenerated specfile:
xdll = ["libcrypto-1_1.dll"] xdll = ["libcrypto-1_1.dll"]
a.binaries = TOC([x for x in a.binaries if x[0] not in xdll]) a.binaries = [x for x in a.binaries if x[0] not in xdll]
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

View File

@@ -11,6 +11,7 @@ copyparty/broker_mp.py,
copyparty/broker_mpw.py, copyparty/broker_mpw.py,
copyparty/broker_thr.py, copyparty/broker_thr.py,
copyparty/broker_util.py, copyparty/broker_util.py,
copyparty/cert.py,
copyparty/cfg.py, copyparty/cfg.py,
copyparty/dxml.py, copyparty/dxml.py,
copyparty/fsutil.py, copyparty/fsutil.py,
@@ -22,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 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 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=[],