Skip to content

Commit

Permalink
shares: add revival and expiration extension
Browse files Browse the repository at this point in the history
  • Loading branch information
9001 committed Aug 30, 2024
1 parent c4e2b0f commit ad2371f
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 19 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,8 @@ specify `--shr /foobar` to enable this feature; a toplevel virtual folder named
users can delete their own shares in the controlpanel, and a list of privileged users (`--shr-adm`) are allowed to see and/or delet any share on the server
after a share has expired, it remains visible in the controlpanel for `--shr-rt` minutes (default is 1 day), and the owner can revive it by extending the expiration time there
**security note:** using this feature does not mean that you can skip the [accounts and volumes](#accounts-and-volumes) section -- you still need to restrict access to volumes that you do not intend to share with unauthenticated users! it is not sufficient to use rules in the reverseproxy to restrict access to just the `/share` folder.
Expand Down
7 changes: 4 additions & 3 deletions copyparty/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -975,9 +975,10 @@ def add_fs(ap):
def add_share(ap):
db_path = os.path.join(E.cfg, "shares.db")
ap2 = ap.add_argument_group('share-url options')
ap2.add_argument("--shr", metavar="DIR", default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
ap2.add_argument("--shr-db", metavar="FILE", default=db_path, help="database to store shares in")
ap2.add_argument("--shr-adm", metavar="U,U", default="", help="comma-separated list of users allowed to view/delete any share")
ap2.add_argument("--shr", metavar="DIR", type=u, default="", help="toplevel virtual folder for shared files/folders, for example [\033[32m/share\033[0m]")
ap2.add_argument("--shr-db", metavar="FILE", type=u, default=db_path, help="database to store shares in")
ap2.add_argument("--shr-adm", metavar="U,U", type=u, default="", help="comma-separated list of users allowed to view/delete any share")
ap2.add_argument("--shr-rt", metavar="MIN", type=int, default=1440, help="shares can be revived by their owner if they expired less than MIN minutes ago; [\033[32m60\033[0m]=hour, [\033[32m1440\033[0m]=day, [\033[32m10080\033[0m]=week")
ap2.add_argument("--shr-v", action="store_true", help="debug")


Expand Down
32 changes: 25 additions & 7 deletions copyparty/httpcli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1611,8 +1611,8 @@ def handle_post(self) -> bool:
if "delete" in self.uparam:
return self.handle_rm([])

if "unshare" in self.uparam:
return self.handle_unshare()
if "eshare" in self.uparam:
return self.handle_eshare()

if "application/octet-stream" in ctype:
return self.handle_post_binary()
Expand Down Expand Up @@ -4304,34 +4304,52 @@ def tx_shares(self) -> bool:
self.reply(html.encode("utf-8"), status=200)
return True

def handle_unshare(self) -> bool:
def handle_eshare(self) -> bool:
idx = self.conn.get_u2idx()
if not idx or not hasattr(idx, "p_end"):
if not HAVE_SQLITE3:
raise Pebkac(500, "sqlite3 not found on server; sharing is disabled")
raise Pebkac(500, "server busy, cannot create share; please retry in a bit")

if self.args.shr_v:
self.log("handle_unshare: " + self.req)
self.log("handle_eshare: " + self.req)

cur = idx.get_shr()
if not cur:
raise Pebkac(400, "huh, sharing must be disabled in the server config...")

skey = self.vpath.split("/")[-1]

uns = cur.execute("select un from sh where k = ?", (skey,)).fetchall()
un = uns[0][0] if uns and uns[0] else ""
rows = cur.execute("select un, t1 from sh where k = ?", (skey,)).fetchall()
un = rows[0][0] if rows and rows[0] else ""

if not un:
raise Pebkac(400, "that sharekey didn't match anything")

expiry = rows[0][1]

if un != self.uname and self.uname != self.args.shr_adm:
t = "your username (%r) does not match the sharekey's owner (%r) and you're not admin"
raise Pebkac(400, t % (self.uname, un))

cur.execute("delete from sh where k = ?", (skey,))
reload = False
act = self.uparam["eshare"]
if act == "rm":
cur.execute("delete from sh where k = ?", (skey,))
if skey in self.asrv.vfs.nodes[self.args.shr.strip("/")].nodes:
reload = True
else:
now = time.time()
if expiry < now:
expiry = now
reload = True
expiry += int(act) * 60
cur.execute("update sh set t1 = ? where k = ?", (expiry, skey))

cur.connection.commit()
if reload:
self.conn.hsrv.broker.ask("_reload_blocking", False, False).get()
self.conn.hsrv.broker.ask("up2k.wake_rescanner").get()

self.redirect(self.args.SRS + "?shares")
return True
Expand Down
40 changes: 35 additions & 5 deletions copyparty/up2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -435,14 +435,17 @@ def _rescan(
def _sched_rescan(self) -> None:
volage = {}
cooldown = timeout = time.time() + 3.0
while True:
while not self.stop:
now = time.time()
timeout = max(timeout, cooldown)
wait = timeout - time.time()
# self.log("SR in {:.2f}".format(wait), 5)
with self.rescan_cond:
self.rescan_cond.wait(wait)

if self.stop:
return

now = time.time()
if now < cooldown:
# self.log("SR: cd - now = {:.2f}".format(cooldown - now), 5)
Expand All @@ -466,6 +469,7 @@ def _sched_rescan(self) -> None:
if self.args.shr:
timeout = min(self._check_shares(), timeout)
except Exception as ex:
timeout = min(timeout, now + 60)
t = "could not check for expiring shares: %r"
self.log(t % (ex,), 1)

Expand Down Expand Up @@ -575,27 +579,53 @@ def _check_shares(self) -> float:

now = time.time()
timeout = now + 9001
maxage = self.args.shr_rt * 60
low = now - maxage

vn = self.asrv.vfs.nodes.get(self.args.shr.strip("/"))
active = vn and vn.nodes

db = sqlite3.connect(self.args.shr_db, timeout=2)
cur = db.cursor()

q = "select k from sh where t1 and t1 <= ?"
rm = [x[0] for x in cur.execute(q, (now,))]
rm = [x[0] for x in cur.execute(q, (now,))] if active else []
if rm:
assert vn and vn.nodes # type: ignore
# self.log("chk_shr: %d" % (len(rm),))
zss = set(rm)
rm = [zs for zs in vn.nodes if zs in zss]
reload = bool(rm)
if reload:
self.log("disabling expired shares %s" % (rm,))

rm = [x[0] for x in cur.execute(q, (low,))]
if rm:
self.log("forgetting expired shares %s" % (rm,))
cur.executemany("delete from sh where k=?", [(x,) for x in rm])
cur.executemany("delete from sf where k=?", [(x,) for x in rm])
db.commit()

if reload:
Daemon(self.hub._reload_blocking, "sharedrop", (False, False))

q = "select min(t1) from sh where t1 > 1"
(earliest,) = cur.execute(q).fetchone()
q = "select min(t1) from sh where t1 > ?"
(earliest,) = cur.execute(q, (1,)).fetchone()
if earliest:
timeout = earliest - now
# deadline for revoking regular access
timeout = min(timeout, earliest + maxage)

(earliest,) = cur.execute(q, (now - 2,)).fetchone()
if earliest:
# deadline for revival; drop entirely
timeout = min(timeout, earliest)

cur.close()
db.close()

if self.args.shr_v:
self.log("next shr_chk = %d (%d)" % (timeout, timeout - time.time()))

return timeout

def _check_xiu(self) -> float:
Expand Down
6 changes: 4 additions & 2 deletions copyparty/web/shares.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<th>expires</th>
<th>min</th>
<th>hrs</th>
<th>add time</th>
</tr></thead><tbody>
{% for k, pw, vp, pr, st, un, t0, t1 in rows %}
<tr>
Expand All @@ -45,8 +46,9 @@
<td>{{ un|e }}</td>
<td>{{ t0 }}</td>
<td>{{ t1 }}</td>
<td>{{ ((t1 - now) / 60) | round(1) if t1 else "inf" }}</td>
<td>{{ ((t1 - now) / 3600) | round(1) if t1 else "inf" }}</td>
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 60) | round(1) }}</td>
<td>{{ "inf" if not t1 else "dead" if t1 < now else ((t1 - now) / 3600) | round(1) }}</td>
<td></td>
</tr>
{% endfor %}
</tbody></table>
Expand Down
21 changes: 20 additions & 1 deletion copyparty/web/shares.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ for (var a = 0; a < t.length; a++)
t[a].onclick = rm;

function rm() {
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?unshare',
var u = SR + shr + uricom_enc(this.getAttribute('k')) + '?eshare=rm',
xhr = new XHR();

xhr.open('POST', u, true);
xhr.onload = xhr.onerror = cb;
xhr.send();
}

function bump() {
var k = this.closest('tr').getElementsByTagName('a')[0].getAttribute('k'),
u = SR + shr + uricom_enc(k) + '?eshare=' + this.value,
xhr = new XHR();

xhr.open('POST', u, true);
Expand Down Expand Up @@ -34,4 +44,13 @@ function cb() {
tr[a].cells[b].innerHTML =
v ? unix2iso(v).replace(' ', ',&nbsp;') : 'never';
}

for (var a = 0; a < tr.length; a++)
tr[a].cells[11].innerHTML =
'<button value="1">1min</button> ' +
'<button value="60">1h</button>';

var btns = QSA('td button'), aa = btns.length;
for (var a = 0; a < aa; a++)
btns[a].onclick = bump;
})();
3 changes: 2 additions & 1 deletion docs/devnotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| mPOST | `?media` | `f=FILE` | ...and return medialink (not hotlink) |
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
| POST | `?delete` | | delete URL recursively |
| POST | `?unshare` | | stop sharing a file/folder |
| POST | `?eshare=rm` | | stop sharing a file/folder |
| POST | `?eshare=3` | | set expiration to 3 minutes |
| jPOST | `?share` | (complicated) | create temp URL for file/folder |
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |
| uPOST | | `msg=foo` | send message `foo` into server log |
Expand Down

0 comments on commit ad2371f

Please sign in to comment.