Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1441ccee4f | ||
|
|
491803d8b7 | ||
|
|
3dcc386b6f | ||
|
|
5aa54d1217 | ||
|
|
88b876027c | ||
|
|
fcc3aa98fd | ||
|
|
f2f5e266b4 | ||
|
|
e17bf8f325 | ||
|
|
d19cb32bf3 |
10
README.md
10
README.md
@@ -327,7 +327,7 @@ upgrade notes
|
||||
# accounts and volumes
|
||||
|
||||
per-folder, per-user permissions - if your setup is getting complex, consider making a [config file](./docs/example.conf) instead of using arguments
|
||||
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if logged in as admin)
|
||||
* much easier to manage, and you can modify the config at runtime with `systemctl reload copyparty` or more conveniently using the `[reload cfg]` button in the control-panel (if the user has `a`/admin in any volume)
|
||||
* changes to the `[global]` config section requires a restart to take effect
|
||||
|
||||
a quick summary can be seen using `--help-accounts`
|
||||
@@ -346,7 +346,7 @@ permissions:
|
||||
* `d` (delete): delete files/folders
|
||||
* `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)
|
||||
* `a` (admin): can see uploader IPs
|
||||
* `a` (admin): can see uploader IPs, config-reload
|
||||
|
||||
examples:
|
||||
* add accounts named u1, u2, u3 with passwords p1, p2, p3: `-a u1:p1 -a u2:p2 -a u3:p3`
|
||||
@@ -491,6 +491,9 @@ images with the following names (see `--th-covers`) become the thumbnail of the
|
||||
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 💾
|
||||
|
||||
enabling `multiselect` lets you click files to select them, and then shift-click another file for range-select
|
||||
* `multiselect` is mostly intended for phones/tablets, but the `sel` option in the `[⚙️] settings` tab is better suited for desktop use, allowing selection by CTRL-clicking and range-selection with SHIFT-click, all without affecting regular clicking
|
||||
|
||||
|
||||
## zip downloads
|
||||
|
||||
@@ -613,6 +616,7 @@ file selection: click somewhere on the line (not the link itsef), then:
|
||||
* `up/down` to move
|
||||
* `shift-up/down` to move-and-select
|
||||
* `ctrl-shift-up/down` to also scroll
|
||||
* shift-click another line for range-select
|
||||
|
||||
* cut: select some files and `ctrl-x`
|
||||
* paste: `ctrl-v` in another folder
|
||||
@@ -774,7 +778,7 @@ for the above example to work, add the commandline argument `-e2ts` to also scan
|
||||
using arguments or config files, or a mix of both:
|
||||
* config files (`-c some.conf`) can set additional commandline arguments; see [./docs/example.conf](docs/example.conf) and [./docs/example2.conf](docs/example2.conf)
|
||||
* `kill -s USR1` (same as `systemctl reload copyparty`) to reload accounts and volumes from config files without restarting
|
||||
* or click the `[reload cfg]` button in the control-panel when logged in as admin
|
||||
* or click the `[reload cfg]` button in the control-panel if the user has `a`/admin in any volume
|
||||
* changes to the `[global]` config section requires a restart to take effect
|
||||
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ in {
|
||||
"d" (delete): permanently delete files and folders
|
||||
"g" (get): download files, but cannot see folder contents
|
||||
"G" (upget): "get", but can see filekeys of their own uploads
|
||||
"a" (upget): can see uploader IPs
|
||||
"a" (upget): can see uploader IPs, config-reload
|
||||
|
||||
For example: "rwmd"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Maintainer: icxes <dev.null@need.moe>
|
||||
pkgname=copyparty
|
||||
pkgver="1.8.1"
|
||||
pkgver="1.8.3"
|
||||
pkgrel=1
|
||||
pkgdesc="Portable file sharing hub"
|
||||
arch=("any")
|
||||
@@ -20,7 +20,7 @@ optdepends=("ffmpeg: thumbnails for videos, images (slower) and audio, music tag
|
||||
)
|
||||
source=("https://github.com/9001/${pkgname}/releases/download/v${pkgver}/${pkgname}-${pkgver}.tar.gz")
|
||||
backup=("etc/${pkgname}.d/init" )
|
||||
sha256sums=("f43da11ba5d1d5adf99ad642bf068042c46c23d408e7ed17b025065121abab94")
|
||||
sha256sums=("6903106cab52536e5273f385813884b9c6dc734ee971ddddacfef8af6b7fec9b")
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.8.1/copyparty-sfx.py",
|
||||
"version": "1.8.1",
|
||||
"hash": "sha256-0Lf5djrgGAM+wwZP66GtSXkmRnIp3tij8j7cANeoE7o="
|
||||
"url": "https://github.com/9001/copyparty/releases/download/v1.8.3/copyparty-sfx.py",
|
||||
"version": "1.8.3",
|
||||
"hash": "sha256-jV9DUp2+lxhLP4QlIYtMoE0Woum9W4i6U/oLDyYyoRE="
|
||||
}
|
||||
@@ -492,7 +492,7 @@ def get_sects():
|
||||
"d" (delete): permanently delete files and folders
|
||||
"g" (get): download files, but cannot see folder contents
|
||||
"G" (upget): "get", but can see filekeys of their own uploads
|
||||
"a" (admin): can see uploader IPs
|
||||
"a" (admin): can see uploader IPs, config-reload
|
||||
|
||||
too many volflags to list here, see --help-flags
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# coding: utf-8
|
||||
|
||||
VERSION = (1, 8, 2)
|
||||
VERSION = (1, 8, 4)
|
||||
CODENAME = "argon"
|
||||
BUILD_DT = (2023, 7, 14)
|
||||
BUILD_DT = (2023, 7, 18)
|
||||
|
||||
S_VERSION = ".".join(map(str, VERSION))
|
||||
S_BUILD_DT = "{0:04d}-{1:02d}-{2:02d}".format(*BUILD_DT)
|
||||
|
||||
@@ -324,6 +324,7 @@ class VFS(object):
|
||||
self.adel: dict[str, list[str]] = {}
|
||||
self.aget: dict[str, list[str]] = {}
|
||||
self.apget: dict[str, list[str]] = {}
|
||||
self.aadmin: dict[str, list[str]] = {}
|
||||
|
||||
if realpath:
|
||||
rp = realpath + ("" if realpath.endswith(os.sep) else os.sep)
|
||||
@@ -1182,7 +1183,7 @@ class AuthSrv(object):
|
||||
vol.all_vps.sort(key=lambda x: len(x[0]), reverse=True)
|
||||
vol.root = vfs
|
||||
|
||||
for perm in "read write move del get pget".split():
|
||||
for perm in "read write move del get pget admin".split():
|
||||
axs_key = "u" + perm
|
||||
unames = ["*"] + list(acct.keys())
|
||||
umap: dict[str, list[str]] = {x: [] for x in unames}
|
||||
|
||||
@@ -109,6 +109,7 @@ flagcats = {
|
||||
"nohash=\\.iso$": "skips hashing file contents if path matches *.iso",
|
||||
"noidx=\\.iso$": "fully ignores the contents at paths matching *.iso",
|
||||
"noforget": "don't forget files when deleted from disk",
|
||||
"fat32": "avoid excessive reindexing on android sdcardfs",
|
||||
"dbd=[acid|swal|wal|yolo]": "database speed-durability tradeoff",
|
||||
"xlink": "cross-volume dupe detection / linking",
|
||||
"xdev": "do not descend into other filesystems",
|
||||
|
||||
@@ -149,6 +149,7 @@ class HttpCli(object):
|
||||
self.dvol = [" "]
|
||||
self.gvol = [" "]
|
||||
self.upvol = [" "]
|
||||
self.avol = [" "]
|
||||
self.do_log = True
|
||||
self.can_read = False
|
||||
self.can_write = False
|
||||
@@ -405,6 +406,7 @@ class HttpCli(object):
|
||||
self.dvol = self.asrv.vfs.adel[self.uname]
|
||||
self.gvol = self.asrv.vfs.aget[self.uname]
|
||||
self.upvol = self.asrv.vfs.apget[self.uname]
|
||||
self.avol = self.asrv.vfs.aadmin[self.uname]
|
||||
|
||||
if self.pw and (
|
||||
self.pw != cookie_pw or self.conn.freshen_pwd + 30 < time.time()
|
||||
@@ -3003,13 +3005,12 @@ class HttpCli(object):
|
||||
|
||||
def tx_mounts(self) -> bool:
|
||||
suf = self.urlq({}, ["h"])
|
||||
avol = [x for x in self.wvol if x in self.rvol]
|
||||
rvol, wvol, avol = [
|
||||
[("/" + x).rstrip("/") + "/" for x in y]
|
||||
for y in [self.rvol, self.wvol, avol]
|
||||
for y in [self.rvol, self.wvol, self.avol]
|
||||
]
|
||||
|
||||
if avol and not self.args.no_rescan:
|
||||
if self.avol and not self.args.no_rescan:
|
||||
x = self.conn.hsrv.broker.ask("up2k.get_state")
|
||||
vs = json.loads(x.get())
|
||||
vstate = {("/" + k).rstrip("/") + "/": v for k, v in vs["volstate"].items()}
|
||||
@@ -3125,7 +3126,7 @@ class HttpCli(object):
|
||||
return "" # unhandled / fallthrough
|
||||
|
||||
def scanvol(self) -> bool:
|
||||
if not self.can_read or not self.can_write:
|
||||
if not self.can_admin:
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_rescan:
|
||||
@@ -3148,7 +3149,7 @@ class HttpCli(object):
|
||||
if act != "cfg":
|
||||
raise Pebkac(400, "only config files ('cfg') can be reloaded rn")
|
||||
|
||||
if not [x for x in self.wvol if x in self.rvol]:
|
||||
if not self.avol:
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_reload:
|
||||
@@ -3158,7 +3159,7 @@ class HttpCli(object):
|
||||
return self.redirect("", "?h", x.get(), "return to", False)
|
||||
|
||||
def tx_stack(self) -> bool:
|
||||
if not [x for x in self.wvol if x in self.rvol]:
|
||||
if not self.avol and not [x for x in self.wvol if x in self.rvol]:
|
||||
raise Pebkac(403, "not allowed for user " + self.uname)
|
||||
|
||||
if self.args.no_stack:
|
||||
@@ -3862,7 +3863,8 @@ class HttpCli(object):
|
||||
_ = [tagset.add(k) for k in fe["tags"]]
|
||||
|
||||
if icur:
|
||||
taglist = [k for k in vn.flags.get("mte", "").split(",") if k in tagset]
|
||||
mte = vn.flags.get("mte") or "up_ip,.up_at"
|
||||
taglist = [k for k in mte.split(",") if k in tagset]
|
||||
for fe in dirs:
|
||||
fe["tags"] = {}
|
||||
else:
|
||||
|
||||
@@ -883,6 +883,7 @@ class Up2k(object):
|
||||
rei = vol.flags.get("noidx")
|
||||
reh = vol.flags.get("nohash")
|
||||
n4g = bool(vol.flags.get("noforget"))
|
||||
ffat = "fat32" in vol.flags
|
||||
cst = bos.stat(top)
|
||||
dev = cst.st_dev if vol.flags.get("xdev") else 0
|
||||
|
||||
@@ -919,6 +920,7 @@ class Up2k(object):
|
||||
rei,
|
||||
reh,
|
||||
n4g,
|
||||
ffat,
|
||||
[],
|
||||
cst,
|
||||
dev,
|
||||
@@ -974,6 +976,7 @@ class Up2k(object):
|
||||
rei: Optional[Pattern[str]],
|
||||
reh: Optional[Pattern[str]],
|
||||
n4g: bool,
|
||||
ffat: bool,
|
||||
seen: list[str],
|
||||
cst: os.stat_result,
|
||||
dev: int,
|
||||
@@ -1018,7 +1021,7 @@ class Up2k(object):
|
||||
|
||||
lmod = int(inf.st_mtime)
|
||||
sz = inf.st_size
|
||||
if fat32 and inf.st_mtime % 2:
|
||||
if fat32 and not ffat and inf.st_mtime % 2:
|
||||
fat32 = False
|
||||
|
||||
if stat.S_ISDIR(inf.st_mode):
|
||||
@@ -1035,7 +1038,19 @@ class Up2k(object):
|
||||
# self.log(" dir: {}".format(abspath))
|
||||
try:
|
||||
ret += self._build_dir(
|
||||
db, top, excl, abspath, rap, rei, reh, n4g, seen, inf, dev, xvol
|
||||
db,
|
||||
top,
|
||||
excl,
|
||||
abspath,
|
||||
rap,
|
||||
rei,
|
||||
reh,
|
||||
n4g,
|
||||
fat32,
|
||||
seen,
|
||||
inf,
|
||||
dev,
|
||||
xvol,
|
||||
)
|
||||
except:
|
||||
t = "failed to index subdir [{}]:\n{}"
|
||||
|
||||
@@ -127,7 +127,7 @@ window.baguetteBox = (function () {
|
||||
var gallery = [];
|
||||
[].forEach.call(tagsNodeList, function (imageElement, imageIndex) {
|
||||
var imageElementClickHandler = function (e) {
|
||||
if (ctrl(e))
|
||||
if (ctrl(e) || e && e.shiftKey)
|
||||
return true;
|
||||
|
||||
e.preventDefault ? e.preventDefault() : e.returnValue = false;
|
||||
|
||||
@@ -1230,7 +1230,8 @@ html.y #widget.open {
|
||||
#wfm a.hide {
|
||||
display: none;
|
||||
}
|
||||
#files tbody tr.fcut td {
|
||||
#files tbody tr.fcut td,
|
||||
#ggrid>a.fcut {
|
||||
animation: fcut .5s ease-out;
|
||||
}
|
||||
@keyframes fcut {
|
||||
|
||||
@@ -189,7 +189,8 @@ var Ls = {
|
||||
"cl_hpick": "click one column header to hide in the table below",
|
||||
"cl_hcancel": "column hiding aborted",
|
||||
|
||||
"ct_thumb": "in icon view, toggle icons or thumbnails$NHotkey: T",
|
||||
"ct_thumb": "in grid-view, toggle icons or thumbnails$NHotkey: T",
|
||||
"ct_csel": "use CTRL and SHIFT for file selection in grid-view",
|
||||
"ct_dots": "show hidden files (if server permits)",
|
||||
"ct_dir1st": "sort folders before files",
|
||||
"ct_readme": "show README.md in folder listings",
|
||||
@@ -651,6 +652,7 @@ var Ls = {
|
||||
"cl_hcancel": "kolonne-skjuling avbrutt",
|
||||
|
||||
"ct_thumb": "vis miniatyrbilder istedenfor ikoner$NSnarvei: T",
|
||||
"ct_csel": "bruk tastene CTRL og SHIFT for markering av filer i ikonvisning",
|
||||
"ct_dots": "vis skjulte filer (gitt at serveren tillater det)",
|
||||
"ct_dir1st": "sorter slik at mapper kommer foran filer",
|
||||
"ct_readme": "vis README.md nedenfor filene",
|
||||
@@ -1096,6 +1098,7 @@ ebi('op_cfg').innerHTML = (
|
||||
' <a id="tooltips" class="tgl btn" href="#" tt="◔ ◡ ◔">ℹ️ tooltips</a>\n' +
|
||||
' <a id="griden" class="tgl btn" href="#" tt="' + L.wt_grid + '">田 the grid</a>\n' +
|
||||
' <a id="thumbs" class="tgl btn" href="#" tt="' + L.ct_thumb + '">🖼️ thumbs</a>\n' +
|
||||
' <a id="csel" class="tgl btn" href="#" tt="' + L.ct_csel + '">sel</a>\n' +
|
||||
' <a id="dotfiles" class="tgl btn" href="#" tt="' + L.ct_dots + '">dotfiles</a>\n' +
|
||||
' <a id="dir1st" class="tgl btn" href="#" tt="' + L.ct_dir1st + '">📁 first</a>\n' +
|
||||
' <a id="ireadme" class="tgl btn" href="#" tt="' + L.ct_readme + '">📜 readme</a>\n' +
|
||||
@@ -3689,18 +3692,27 @@ var fileman = (function () {
|
||||
if (!sel.length)
|
||||
toast.err(3, L.fc_emore);
|
||||
|
||||
var els = [];
|
||||
var els = [], griden = thegrid.en;
|
||||
for (var a = 0; a < sel.length; a++) {
|
||||
vps.push(sel[a].vp);
|
||||
if (sel.length < 100) {
|
||||
els.push(ebi(sel[a].id).closest('tr'));
|
||||
clmod(els[a], 'fcut');
|
||||
}
|
||||
if (sel.length < 100)
|
||||
try {
|
||||
if (griden)
|
||||
els.push(QS('#ggrid>a[ref="' + sel[a].id + '"]'));
|
||||
else
|
||||
els.push(ebi(sel[a].id).closest('tr'));
|
||||
|
||||
clmod(els[a], 'fcut');
|
||||
}
|
||||
catch (ex) { }
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
for (var a = 0; a < els.length; a++)
|
||||
clmod(els[a], 'fcut', 1);
|
||||
try {
|
||||
for (var a = 0; a < els.length; a++)
|
||||
clmod(els[a], 'fcut', 1);
|
||||
}
|
||||
catch (ex) { }
|
||||
}, 1);
|
||||
|
||||
try {
|
||||
@@ -4288,7 +4300,7 @@ var thegrid = (function () {
|
||||
setsz();
|
||||
|
||||
function gclick1(e) {
|
||||
if (ctrl(e))
|
||||
if (ctrl(e) && !treectl.csel && !r.sel)
|
||||
return true;
|
||||
|
||||
return gclick.bind(this)(e, false);
|
||||
@@ -4312,8 +4324,10 @@ var thegrid = (function () {
|
||||
td = oth.closest('td').nextSibling,
|
||||
tr = td.parentNode;
|
||||
|
||||
if (r.sel && !dbl) {
|
||||
td.click();
|
||||
if ((r.sel && !dbl && !ctrl(e)) || (treectl.csel && (e.shiftKey || ctrl(e)))) {
|
||||
td.onclick.bind(td)(e);
|
||||
if (e.shiftKey)
|
||||
return r.loadsel();
|
||||
clmod(this, 'sel', clgot(tr, 'sel'));
|
||||
}
|
||||
else if (widget.is_open && aplay)
|
||||
@@ -4706,6 +4720,7 @@ document.onkeydown = function (e) {
|
||||
|
||||
if (e.shiftKey) {
|
||||
clmod(el, 'sel', 't');
|
||||
msel.origin_tr(el);
|
||||
msel.selui();
|
||||
}
|
||||
|
||||
@@ -4714,6 +4729,7 @@ document.onkeydown = function (e) {
|
||||
}
|
||||
if (k == 'Space') {
|
||||
clmod(ae, 'sel', 't');
|
||||
msel.origin_tr(ae);
|
||||
msel.selui();
|
||||
return ev(e);
|
||||
}
|
||||
@@ -4722,6 +4738,7 @@ document.onkeydown = function (e) {
|
||||
all = msel.getall();
|
||||
|
||||
msel.evsel(e, sel.length < all.length);
|
||||
msel.origin_id(null);
|
||||
return ev(e);
|
||||
}
|
||||
}
|
||||
@@ -5198,6 +5215,7 @@ var treectl = (function () {
|
||||
bcfg_bind(r, 'ireadme', 'ireadme', true);
|
||||
bcfg_bind(r, 'idxh', 'idxh', idxh, setidxh);
|
||||
bcfg_bind(r, 'dyn', 'dyntree', true, onresize);
|
||||
bcfg_bind(r, 'csel', 'csel', false);
|
||||
bcfg_bind(r, 'dots', 'dotfiles', false, function (v) {
|
||||
r.goto(get_evpath());
|
||||
});
|
||||
@@ -6139,6 +6157,16 @@ function apply_perms(res) {
|
||||
}
|
||||
|
||||
|
||||
function tr2id(tr) {
|
||||
try {
|
||||
return tr.cells[1].querySelector('a[id]').getAttribute('id');
|
||||
}
|
||||
catch (ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function find_file_col(txt) {
|
||||
var i = -1,
|
||||
min = false,
|
||||
@@ -6641,9 +6669,11 @@ var msel = (function () {
|
||||
var r = {};
|
||||
r.sel = null;
|
||||
r.all = null;
|
||||
r.so = null; // selection origin
|
||||
r.pr = null; // previous range
|
||||
|
||||
r.load = function () {
|
||||
if (r.sel)
|
||||
r.load = function (reset) {
|
||||
if (r.sel && !reset)
|
||||
return;
|
||||
|
||||
r.sel = [];
|
||||
@@ -6654,7 +6684,8 @@ var msel = (function () {
|
||||
if (ao.sel)
|
||||
r.sel.push(ao);
|
||||
}
|
||||
return;
|
||||
if (!reset)
|
||||
return;
|
||||
}
|
||||
|
||||
r.all = [];
|
||||
@@ -6680,6 +6711,7 @@ var msel = (function () {
|
||||
};
|
||||
|
||||
r.loadsel = function (sel) {
|
||||
r.so = r.pr = null;
|
||||
r.sel = [];
|
||||
r.load();
|
||||
|
||||
@@ -6711,15 +6743,60 @@ var msel = (function () {
|
||||
thegrid.loadsel();
|
||||
fileman.render();
|
||||
showfile.updtree();
|
||||
}
|
||||
};
|
||||
r.seltgl = function (e) {
|
||||
ev(e);
|
||||
var tr = this.parentNode;
|
||||
clmod(tr, 'sel', 't');
|
||||
var tr = this.parentNode,
|
||||
id = tr2id(tr);
|
||||
|
||||
if ((treectl.csel || !thegrid.en || thegrid.sel) && e.shiftKey && r.so && id && r.so != id) {
|
||||
var o1 = -1, o2 = -1;
|
||||
for (a = 0; a < r.all.length; a++) {
|
||||
var ai = r.all[a].id;
|
||||
if (ai == r.so)
|
||||
o1 = a;
|
||||
if (ai == id)
|
||||
o2 = a;
|
||||
}
|
||||
var st = r.all[o1].sel;
|
||||
if (o1 > o2)
|
||||
o2 = [o1, o1 = o2][0];
|
||||
|
||||
if (r.pr) {
|
||||
// invert previous range, in case it was narrowed
|
||||
for (var a = r.pr[0]; a <= r.pr[1]; a++)
|
||||
clmod(ebi(r.all[a].id).closest('tr'), 'sel', !st);
|
||||
|
||||
// and invert current selection if repeated
|
||||
if (r.pr[0] === o1 && r.pr[1] === o2)
|
||||
st = !st;
|
||||
}
|
||||
|
||||
for (var a = o1; a <= o2; a++)
|
||||
clmod(ebi(r.all[a].id).closest('tr'), 'sel', st);
|
||||
|
||||
r.pr = [o1, o2];
|
||||
|
||||
if (window.getSelection)
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
else {
|
||||
clmod(tr, 'sel', 't');
|
||||
r.origin_tr(tr);
|
||||
}
|
||||
r.selui();
|
||||
}
|
||||
};
|
||||
r.origin_tr = function (tr) {
|
||||
r.so = tr2id(tr);
|
||||
r.pr = null;
|
||||
};
|
||||
r.origin_id = function (id) {
|
||||
r.so = id;
|
||||
r.pr = null;
|
||||
};
|
||||
r.evsel = function (e, fun) {
|
||||
ev(e);
|
||||
r.so = r.pr = null;
|
||||
var trs = QSA('#files tbody tr');
|
||||
for (var a = 0, aa = trs.length; a < aa; a++)
|
||||
clmod(trs[a], 'sel', fun);
|
||||
@@ -7344,7 +7421,7 @@ ebi('path').onclick = function (e) {
|
||||
|
||||
|
||||
ebi('files').onclick = ebi('docul').onclick = function (e) {
|
||||
if (ctrl(e))
|
||||
if (!treectl.csel && e && (ctrl(e) || e.shiftKey))
|
||||
return true;
|
||||
|
||||
var tgt = e.target.closest('a[id]');
|
||||
@@ -7441,6 +7518,8 @@ function reload_browser() {
|
||||
reload_mp();
|
||||
try { showsort(ftab); } catch (ex) { }
|
||||
makeSortable(ftab, function () {
|
||||
msel.origin_id(null);
|
||||
msel.load(true);
|
||||
thegrid.setdirty();
|
||||
mp.read_order();
|
||||
});
|
||||
|
||||
@@ -1,3 +1,69 @@
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0714-1558 `v1.8.2` URGENT: fix path traversal vulnerability
|
||||
|
||||
* read-only demo server at https://a.ocv.me/pub/demo/
|
||||
* [docker image](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) ╱ [similar software](https://github.com/9001/copyparty/blob/hovudstraum/docs/versus.md) ╱ [client testbed](https://cd.ocv.me/b/)
|
||||
|
||||
Starting with the bad and important news; this release fixes https://github.com/9001/copyparty/security/advisories/GHSA-pxfv-7rr3-2qjg / [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474) -- so please upgrade!
|
||||
|
||||
Every version until now had a [path traversal vulnerability](https://owasp.org/www-community/attacks/Path_Traversal) which allowed read-access to any file on the server's filesystem. To summarize,
|
||||
* Every file that the copyparty process had the OS-level permissions to read, could be retrieved over HTTP without password authentication
|
||||
* However, an attacker would need to know the full (or copyparty-module-relative) path to the file; it was luckily impossible to list directory contents to discover files on the server
|
||||
* You may have been running copyparty with some mitigations against this:
|
||||
* [prisonparty](https://github.com/9001/copyparty/tree/hovudstraum/bin#prisonpartysh) limited the scope of access to files which were intentionally given to copyparty for sharing; meaning all volumes, as well as the following read-only filesystem locations: `/bin`, `/lib`, `/lib32`, `/lib64`, `/sbin`, `/usr`, `/etc/alternatives`
|
||||
* the [nix package](https://github.com/9001/copyparty#nix-package) has a similar mitigation implemented using systemd concepts
|
||||
* [docker containers](https://github.com/9001/copyparty/tree/hovudstraum/scripts/docker) would only expose the files which were intentionally mounted into the container, so even better
|
||||
* More conventional setups, such as just running the sfx (python or exe editions), would unfortunately expose all files readable by the current user
|
||||
* The following configurations would have made the impact much worse:
|
||||
* running copyparty as root
|
||||
|
||||
So, three years, and finally a CVE -- which has been there since day one... Not great huh. There is a list of all the copyparty alternatives that I know of in the `similar software` link above.
|
||||
|
||||
Thanks for flying copyparty! And especially if you decide to continue doing so :-)
|
||||
|
||||
## new features
|
||||
* #43 volflags to specify thumbnailer behavior per-volume;
|
||||
* `--th-no-crop` / volflag `nocrop` to specify whether autocrop should be disabled
|
||||
* `--th-size` / volflag `thsize` to set a custom thumbnail resolution
|
||||
* `--th-convt` / volflag `convt` to specify conversion timeout
|
||||
* #45 resulted in a handful of opportunities to tighten security in intentionally-dangerous setups (public folders with anonymous uploads enabled):
|
||||
* a new permission, `a` (in addition to the existing `rwmdgG`), to show the uploader-IP and upload-time for each file in the file listing
|
||||
* accidentally incompatible with the `d2t` volflag (will be fixed in the next ver)
|
||||
* volflag `nohtml` is a good defense against (un)intentional XSS; it returns HTML-files and markdown-files as plaintext instead of rendering them, meaning any malicious `<script>` won't run -- bad idea for regular use since it breaks fundamental functionality, but good when you really need it
|
||||
* the README-previews below the file-listing still renders as usual, as this is fine thanks to the sandbox
|
||||
* a new eventhook `--xban` to run a plugin when copyparty decides to ban someone (for password bruteforcing or excessive 404's), for example to blackhole the IP using fail2ban or similar
|
||||
|
||||
## bugfixes
|
||||
* **fixes a path traversal vulnerability,** https://github.com/9001/copyparty/security/advisories/GHSA-pxfv-7rr3-2qjg / [CVE-2023-37474](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-37474)
|
||||
* HUGE thanks to @TheHackyDog for reporting this !!
|
||||
* if you use a reverse proxy, you can check if you have been exploited like so:
|
||||
* nginx: grep your logs for URLs containing both `.cpr/` and `%2[^0]`, for example using the following command:
|
||||
```bash
|
||||
(gzip -dc access.log.*.gz; cat access.log) | sed -r 's/" [0-9]+ .*//' | grep -E 'cpr/.*%2[^0]' | grep -vF data:image/svg
|
||||
```
|
||||
* 77f1e5144455eb946db7368792ea11c934f0f6da fixes an extremely unlikely race-condition (see the commit for details)
|
||||
* 8f59afb1593a75b8ce8c91ceee304097a07aea6e fixes another race-condition which is a bit worse:
|
||||
* the unpost feature could collide with other database activity, with the worst-case outcome being aborted batch operations, for example a directory move or a batch-rename which stops halfways
|
||||
|
||||
----
|
||||
|
||||
# 💾 what to download?
|
||||
| download link | is it good? | description |
|
||||
| -- | -- | -- |
|
||||
| **[copyparty-sfx.py](https://github.com/9001/copyparty/releases/latest/download/copyparty-sfx.py)** | ✅ the best 👍 | runs anywhere! only needs python |
|
||||
| [a docker image](https://github.com/9001/copyparty/blob/hovudstraum/scripts/docker/README.md) | it's ok | good if you prefer docker 🐋 |
|
||||
| [copyparty.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty.exe) | ⚠️ [acceptable](https://github.com/9001/copyparty#copypartyexe) | for [win8](https://user-images.githubusercontent.com/241032/221445946-1e328e56-8c5b-44a9-8b9f-dee84d942535.png) or later; built-in thumbnailer |
|
||||
| [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.7.1/u2c.exe) | ⚠️ acceptable | [CLI uploader](https://github.com/9001/copyparty/blob/hovudstraum/bin/u2c.py) as a win7+ exe ([video](https://a.ocv.me/pub/demo/pics-vids/u2cli.webm)) |
|
||||
| [copyparty32.exe](https://github.com/9001/copyparty/releases/latest/download/copyparty32.exe) | ⛔️ [dangerous](https://github.com/9001/copyparty#copypartyexe) | for [win7](https://user-images.githubusercontent.com/241032/221445944-ae85d1f4-d351-4837-b130-82cab57d6cca.png) -- never expose to the internet! |
|
||||
| [cpp-winpe64.exe](https://github.com/9001/copyparty/releases/download/v1.8.2/copyparty-winpe64.exe) | ⛔️ dangerous | runs on [64bit WinPE](https://user-images.githubusercontent.com/241032/205454984-e6b550df-3c49-486d-9267-1614078dd0dd.png), otherwise useless |
|
||||
|
||||
* except for [u2c.exe](https://github.com/9001/copyparty/releases/download/v1.7.1/u2c.exe), all of the options above are equivalent
|
||||
* the zip and tar.gz files below are just source code
|
||||
* python packages are available at [PyPI](https://pypi.org/project/copyparty/#files)
|
||||
|
||||
|
||||
|
||||
|
||||
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
|
||||
# 2023-0707-2220 `v1.8.1` in case of 404
|
||||
|
||||
|
||||
@@ -77,8 +77,3 @@ or using commandline arguments,
|
||||
# build the images yourself
|
||||
|
||||
basically `./make.sh hclean pull img push` but see [devnotes.md](./devnotes.md)
|
||||
|
||||
|
||||
# notes
|
||||
|
||||
* currently unable to play [tracker music](https://en.wikipedia.org/wiki/Module_file) (mod/s3m/xm/it/...) -- will be fixed in june 2023 (Alpine 3.18)
|
||||
|
||||
73
scripts/test/ptrav.py
Executable file
73
scripts/test/ptrav.py
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import itertools
|
||||
import requests
|
||||
|
||||
atlas = ["%", "25", "2e", "2f", ".", "/"]
|
||||
|
||||
|
||||
def genlen(ubase, port, ntot, nth, wlen):
|
||||
n = 0
|
||||
t0 = time.time()
|
||||
print("genlen %s nth %s port %s" % (wlen, nth, port))
|
||||
rsession = requests.Session()
|
||||
ptn = re.compile(r"2.2.2.2|\.\.\.|///|%%%|\.2|/2./|%\.|/%/")
|
||||
for path in itertools.product(atlas, repeat=wlen):
|
||||
if "%" not in path:
|
||||
continue
|
||||
path = "".join(path)
|
||||
if ptn.search(path):
|
||||
continue
|
||||
n += 1
|
||||
if n % ntot != nth:
|
||||
continue
|
||||
url = ubase % (port, path)
|
||||
if n % 500 == nth:
|
||||
spd = n / (time.time() - t0)
|
||||
print(wlen, n, int(spd), url)
|
||||
|
||||
try:
|
||||
r = rsession.get(url)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
print("\n[=== RETRY ===]", url)
|
||||
try:
|
||||
r = rsession.get(url)
|
||||
except:
|
||||
r = rsession.get(url)
|
||||
|
||||
if "fgsfds" in r.text:
|
||||
with open("hit-%s.txt" % (time.time()), "w", encoding="utf-8") as f:
|
||||
f.write(url)
|
||||
raise Exception("HIT! {}".format(url))
|
||||
|
||||
|
||||
def main():
|
||||
ubase = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
ntot = int(sys.argv[3])
|
||||
nth = int(sys.argv[4])
|
||||
for wlen in range(20):
|
||||
genlen(ubase, port, ntot, nth, wlen)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
"""
|
||||
python3 -m copyparty -v srv::r -p 3931 -q -j4
|
||||
nice python3 ./ptrav.py "http://127.0.0.1:%s/%sfa" 3931 3 0
|
||||
nice python3 ./ptrav.py "http://127.0.0.1:%s/%sfa" 3931 3 1
|
||||
nice python3 ./ptrav.py "http://127.0.0.1:%s/%sfa" 3931 3 2
|
||||
nice python3 ./ptrav2.py "http://127.0.0.1:%s/.cpr/%sfa" 3931 3 0
|
||||
nice python3 ./ptrav2.py "http://127.0.0.1:%s/.cpr/%sfa" 3931 3 1
|
||||
nice python3 ./ptrav2.py "http://127.0.0.1:%s/.cpr/%sfa" 3931 3 2
|
||||
(13x slower than /tests/ptrav.py)
|
||||
"""
|
||||
@@ -62,7 +62,16 @@ class Cpp(object):
|
||||
|
||||
def tc1(vflags):
|
||||
ub = "http://127.0.0.1:4321/"
|
||||
td = os.path.join("srv", "smoketest")
|
||||
try:
|
||||
if not os.path.exists("/dev/shm"):
|
||||
raise Exception()
|
||||
|
||||
td = "/dev/shm/cppsmoketst"
|
||||
ntd = 4
|
||||
except:
|
||||
td = os.path.join("srv", "smoketest")
|
||||
ntd = 2
|
||||
|
||||
try:
|
||||
shutil.rmtree(td)
|
||||
except:
|
||||
@@ -91,6 +100,7 @@ def tc1(vflags):
|
||||
"-p4321",
|
||||
"-e2dsa",
|
||||
"-e2tsr",
|
||||
"--dbd=yolo",
|
||||
"--no-mutagen",
|
||||
"--th-ff-jpg",
|
||||
"--hist",
|
||||
@@ -99,38 +109,38 @@ def tc1(vflags):
|
||||
pdirs = []
|
||||
hpaths = {}
|
||||
|
||||
for d1 in ["r", "w", "a"]:
|
||||
for d1 in ["r", "w", "rw"]:
|
||||
pdirs.append("{}/{}".format(td, d1))
|
||||
pdirs.append("{}/{}/j".format(td, d1))
|
||||
for d2 in ["r", "w", "a", "c"]:
|
||||
for d2 in ["r", "w", "rw", "c"]:
|
||||
d = os.path.join(td, d1, "j", d2)
|
||||
pdirs.append(d)
|
||||
os.makedirs(d)
|
||||
|
||||
pdirs = [x.replace("\\", "/") for x in pdirs]
|
||||
udirs = [x.split("/", 2)[2] for x in pdirs]
|
||||
udirs = [x.split("/", ntd)[ntd] for x in pdirs]
|
||||
perms = [x.rstrip("cj/")[-1] for x in pdirs]
|
||||
perms = ["rw" if x == "a" else x for x in perms]
|
||||
for pd, ud, p in zip(pdirs, udirs, perms):
|
||||
if ud[-1] == "j" or ud[-1] == "c":
|
||||
continue
|
||||
|
||||
hp = None
|
||||
if pd.endswith("st/a"):
|
||||
if pd.endswith("st/rw"):
|
||||
hp = hpaths[ud] = os.path.join(td, "db1")
|
||||
elif pd[:-1].endswith("a/j/"):
|
||||
elif pd[:-1].endswith("rw/j/"):
|
||||
hpaths[ud] = os.path.join(td, "dbm")
|
||||
hp = None
|
||||
else:
|
||||
hp = "-"
|
||||
hpaths[ud] = os.path.join(pd, ".hist")
|
||||
|
||||
arg = "{}:{}:{}".format(pd, ud, p)
|
||||
arg = "{}:{}:a{}".format(pd, ud, p)
|
||||
if hp:
|
||||
arg += ":c,hist=" + hp
|
||||
|
||||
args += ["-v", arg + vflags]
|
||||
|
||||
# print("\n".join(args))
|
||||
# return
|
||||
cpp = Cpp(args)
|
||||
CPP.append(cpp)
|
||||
@@ -163,7 +173,7 @@ def tc1(vflags):
|
||||
|
||||
# stat filesystem
|
||||
for d, p in zip(pdirs, perms):
|
||||
u = "{}/{}.h264".format(d, d.split("test/")[-1].replace("/", ""))
|
||||
u = "{}/{}.h264".format(d, d[len(td) :].replace("/", ""))
|
||||
ok = os.path.exists(u)
|
||||
if ok != (p in ["rw", "w"]):
|
||||
raise Exception("stat {} with perm {} at {}".format(ok, p, u))
|
||||
|
||||
87
tests/ptrav.py
Normal file
87
tests/ptrav.py
Normal file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import itertools
|
||||
|
||||
from . import util as tu
|
||||
from .util import Cfg
|
||||
|
||||
from copyparty.authsrv import AuthSrv
|
||||
from copyparty.httpcli import HttpCli
|
||||
|
||||
atlas = ["%", "25", "2e", "2f", ".", "/"]
|
||||
|
||||
|
||||
def nolog(*a, **ka):
|
||||
pass
|
||||
|
||||
|
||||
def hdr(query):
|
||||
h = "GET /{} HTTP/1.1\r\nCookie: cppwd=o\r\nConnection: close\r\n\r\n"
|
||||
return h.format(query).encode("utf-8")
|
||||
|
||||
|
||||
def curl(args, asrv, url, binary=False):
|
||||
conn = tu.VHttpConn(args, asrv, nolog, hdr(url))
|
||||
HttpCli(conn).run()
|
||||
if binary:
|
||||
h, b = conn.s._reply.split(b"\r\n\r\n", 1)
|
||||
return [h.decode("utf-8"), b]
|
||||
|
||||
return conn.s._reply.decode("utf-8").split("\r\n\r\n", 1)
|
||||
|
||||
|
||||
def genlen(ubase, ntot, nth, wlen):
|
||||
args = Cfg(v=["s2::r"], a=["o:o", "x:x"])
|
||||
asrv = AuthSrv(args, print)
|
||||
# h, ret = curl(args, asrv, "hey")
|
||||
|
||||
n = 0
|
||||
t0 = time.time()
|
||||
print("genlen %s nth %s" % (wlen, nth))
|
||||
ptn = re.compile(r"2.2.2.2|\.\.\.|///|%%%|\.2|/2./|%\.|/%/")
|
||||
for path in itertools.product(atlas, repeat=wlen):
|
||||
if "%" not in path:
|
||||
continue
|
||||
path = "".join(path)
|
||||
if ptn.search(path):
|
||||
continue
|
||||
n += 1
|
||||
if n % ntot != nth:
|
||||
continue
|
||||
url = ubase + path + "fa"
|
||||
if n % 500 == nth:
|
||||
spd = n / (time.time() - t0)
|
||||
print(wlen, n, int(spd), url)
|
||||
|
||||
hdr, r = curl(args, asrv, url)
|
||||
if "fgsfds" in r:
|
||||
with open("hit-%s.txt" % (time.time()), "w", encoding="utf-8") as f:
|
||||
f.write(url)
|
||||
raise Exception("HIT! {}".format(url))
|
||||
|
||||
|
||||
def main():
|
||||
ubase = sys.argv[1]
|
||||
ntot = int(sys.argv[2])
|
||||
nth = int(sys.argv[3])
|
||||
for wlen in range(20):
|
||||
genlen(ubase, ntot, nth, wlen)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
nice pypy3 -m tests.ptrav "" 2 0
|
||||
nice pypy3 -m tests.ptrav "" 2 1
|
||||
nice pypy3 -m tests.ptrav .cpr 2 0
|
||||
nice pypy3 -m tests.ptrav .cpr 2 1
|
||||
(13x faster than /scripts/test/ptrav.py)
|
||||
"""
|
||||
@@ -32,7 +32,7 @@ if MACOS:
|
||||
|
||||
from copyparty.__init__ import E
|
||||
from copyparty.__main__ import init_E
|
||||
from copyparty.util import Unrecv, FHC
|
||||
from copyparty.util import Unrecv, FHC, Garda
|
||||
|
||||
init_E(E)
|
||||
|
||||
@@ -98,7 +98,7 @@ class Cfg(Namespace):
|
||||
def __init__(self, a=None, v=None, c=None):
|
||||
ka = {}
|
||||
|
||||
ex = "daw dav_auth dav_inf dav_mac dav_rt dotsrch e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_thumb no_vthumb no_zip nrand nw rand smb th_no_crop 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 vague_403 vc ver xdev xlink xvol"
|
||||
ka.update(**{k: False for k in ex.split()})
|
||||
|
||||
ex = "dotpart no_rescan no_sendfile no_voldump plain_ip"
|
||||
@@ -113,7 +113,7 @@ class Cfg(Namespace):
|
||||
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()})
|
||||
|
||||
ex = "ah_alg doctitle favico html_head lg_sbf log_fk md_sbf mth textfiles unlist R RS SR"
|
||||
ex = "ah_alg doctitle favico html_head lg_sbf log_fk md_sbf mth name textfiles unlist R RS SR"
|
||||
ka.update(**{k: "" for k in ex.split()})
|
||||
|
||||
ex = "on403 on404 xad xar xau xban xbd xbr xbu xiu xm"
|
||||
@@ -176,6 +176,9 @@ class VHttpSrv(object):
|
||||
aliases = ["splash", "browser", "browser2", "msg", "md", "mde"]
|
||||
self.j2 = {x: J2_FILES for x in aliases}
|
||||
|
||||
self.gpwd = Garda("")
|
||||
self.g404 = Garda("")
|
||||
|
||||
def cachebuster(self):
|
||||
return "a"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user