From b50d09094610cea122682892a111c6fe048b7677 Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 1 May 2022 22:12:25 +0200 Subject: [PATCH] add logout on inactivity + related errorhandling --- copyparty/__main__.py | 1 + copyparty/httpcli.py | 27 +++++++--- copyparty/httpconn.py | 1 + copyparty/web/browser.js | 104 +++++++++++++++++++++++---------------- copyparty/web/md2.js | 12 ++--- copyparty/web/mde.js | 8 +-- copyparty/web/util.js | 17 ++++++- 7 files changed, 110 insertions(+), 60 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index e44bf73c..6ea6288e 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -487,6 +487,7 @@ def run_argparse(argv, formatter): ap2.add_argument("--vague-403", action="store_true", help="send 404 instead of 403 (security through ambiguity, very enterprise)") ap2.add_argument("--force-js", action="store_true", help="don't send folder listings as HTML, force clients to use the embedded json instead -- slight protection against misbehaving search engines which ignore --no-robots") ap2.add_argument("--no-robots", action="store_true", help="adds http and html headers asking search engines to not index anything") + ap2.add_argument("--logout", metavar="H", type=float, default="8086", help="logout clients after H hours of inactivity (0.0028=10sec, 0.1=6min, 24=day, 168=week, 720=month, 8760=year)") ap2 = ap.add_argument_group('yolo 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") diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 1a177538..91eeebb1 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -143,6 +143,10 @@ class HttpCli(object): if self.args.rsp_slp: time.sleep(self.args.rsp_slp) + self.ua = self.headers.get("user-agent", "") + self.is_rclone = self.ua.startswith("rclone/") + self.is_ancient = self.ua.startswith("Mozilla/4.") + v = self.headers.get("connection", "").lower() self.keepalive = not v.startswith("close") and self.http_ver != "HTTP/1.0" self.is_https = (self.headers.get("x-forwarded-proto", "").lower() == "https" or self.tls) @@ -241,11 +245,12 @@ class HttpCli(object): self.dvol = self.asrv.vfs.adel[self.uname] self.gvol = self.asrv.vfs.aget[self.uname] - if pwd and "pw" in self.ouparam and pwd != cookies.get("cppwd"): + # resend auth cookie if more than 1/3 of the lifetime has passed + # (rate-limited to prevent thrashing browser state, not for performance) + if pwd and self.conn.pwd_cookie_upd < self.t0 - 20 * 60 * self.args.logout: self.out_headerlist.append(("Set-Cookie", self.get_pwd_cookie(pwd)[0])) + self.conn.pwd_cookie_upd = self.t0 - self.ua = self.headers.get("user-agent", "") - self.is_rclone = self.ua.startswith("rclone/") if self.is_rclone: uparam["raw"] = False uparam["dots"] = False @@ -1007,9 +1012,15 @@ class HttpCli(object): pwd = self.parser.require("cppwd", 64) self.parser.drop() + self.out_headerlist = [ + x + for x in self.out_headerlist + if x[0] != "Set-Cookie" or "cppwd=" not in x[1] + ] + dst = "/" if self.vpath: - dst = "/" + quotep(self.vpath) + dst += quotep(self.vpath) ck, msg = self.get_pwd_cookie(pwd) html = self.j2("msg", h1=msg, h2='ack', redir=dst) @@ -1019,14 +1030,14 @@ class HttpCli(object): def get_pwd_cookie(self, pwd): if pwd in self.asrv.iacct: msg = "login ok" - dur = 60 * 60 * 24 * 365 + dur = int(60 * 60 * self.args.logout) else: msg = "naw dude" pwd = "x" # nosec dur = None r = gencookie("cppwd", pwd, dur) - if self.headers.get("user-agent", "").startswith("Mozilla/4."): + if self.is_ancient: r = r.rsplit(" ", 1)[0] return [r, msg] @@ -1818,15 +1829,17 @@ class HttpCli(object): self.redirect("", "?h#cc") def tx_404(self, is_403=False): + rc = 404 if self.args.vague_403: m = '

404 not found  ┐( ´ -`)┌

or maybe you don\'t have access -- try logging in or go home

' elif is_403: m = '

403 forbiddena  ~┻━┻

you\'ll have to log in or go home

' + rc = 403 else: m = '

404 not found  ┐( ´ -`)┌

go home

' html = self.j2("splash", this=self, qvpath=quotep(self.vpath), msg=m) - self.reply(html.encode("utf-8"), status=404) + self.reply(html.encode("utf-8"), status=rc) return True def scanvol(self): diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index ae25070c..b0093af4 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -46,6 +46,7 @@ class HttpConn(object): self.stopping = False self.nreq = 0 self.nbyte = 0 + self.pwd_cookie_upd = 0 self.u2idx = None self.log_func = hsrv.log self.lf_url = re.compile(self.args.lf_url) if self.args.lf_url else None diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 9af71828..2bce9f4c 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -3,6 +3,7 @@ function dbg(msg) { ebi('path').innerHTML = msg; } +var XHR = XMLHttpRequest; // toolbar @@ -1562,12 +1563,37 @@ function evau_error(e) { err = 'Unknown Errol'; break; } - if (eplaya.error.message) - err += '\n\n' + eplaya.error.message; + var em = '' + eplaya.error.message, + mfile = '\n\nFile: «' + uricom_dec(eplaya.src.split('/').pop())[0] + '»', + e404 = 'Could not play audio; error 404: File not found.', + e403 = 'Could not play audio; error 403: Access denied.\n\nTry pressing F5 to reload, maybe you got logged out'; - err += '\n\nFile: «' + uricom_dec(eplaya.src.split('/').pop())[0] + '»'; + if (em) + err += '\n\n' + em; - toast.warn(15, esc(basenames(err))); + if (em.startsWith('403: ')) + err = e403; + + if (em.startsWith('404: ')) + err = e404; + + toast.warn(15, esc(basenames(err + mfile))); + + if (em.startsWith('MEDIA_ELEMENT_ERROR:')) { + // chromish for 40x + var xhr = new XHR(); + xhr.open('HEAD', eplaya.src, true); + xhr.onreadystatechange = function () { + if (this.readyState != XHR.DONE || this.status < 400) + return; + + err = this.status == 403 ? e403 : this.status == 404 ? e404 : + 'Could not play audio; server error ' + this.status; + + toast.warn(15, esc(basenames(err + mfile))); + }; + xhr.send(); + } } @@ -2147,7 +2173,7 @@ var fileman = (function () { var dst = base + uricom_enc(f[0].inew.value, false); function rename_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) { @@ -2160,7 +2186,7 @@ var fileman = (function () { return rn_apply(); } - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('GET', f[0].src + '?move=' + dst, true); xhr.onreadystatechange = rename_cb; xhr.send(); @@ -2182,7 +2208,7 @@ var fileman = (function () { return toast.err(3, 'select at least 1 item to delete'); function deleter() { - var xhr = new XMLHttpRequest(), + var xhr = new XHR(), vp = vps.shift(); if (!vp) { @@ -2197,7 +2223,7 @@ var fileman = (function () { xhr.send(); } function delete_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) { @@ -2290,7 +2316,7 @@ var fileman = (function () { return; function paster() { - var xhr = new XMLHttpRequest(), + var xhr = new XHR(), vp = req.shift(); if (!vp) { @@ -2308,7 +2334,7 @@ var fileman = (function () { xhr.send(); } function paste_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) { @@ -2461,7 +2487,7 @@ var showfile = (function () { }; r.show = function (url, no_push) { - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.url = url; xhr.no_push = no_push; xhr.ts = Date.now(); @@ -2471,13 +2497,11 @@ var showfile = (function () { }; function load_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; - if (this.status !== 200) { - toast.err(0, "recvtree, http " + this.status + ": " + this.responseText); + if (!xhrchk(this, "could not load textfile:\n\nerror ", "404, file not found")) return; - } render([this.url, '', this.responseText], this.no_push); } @@ -3464,7 +3488,7 @@ document.onkeydown = function (e) { srch_msg(false, "searching..."); clearTimeout(search_timeout); - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('POST', '/?srch', true); xhr.setRequestHeader('Content-Type', 'text/plain'); xhr.onreadystatechange = xhr_search_results; @@ -3474,7 +3498,7 @@ document.onkeydown = function (e) { } function xhr_search_results() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) { @@ -3802,7 +3826,7 @@ var treectl = (function () { }; function get_tree(top, dst, rst) { - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.top = top; xhr.dst = dst; xhr.rst = rst; @@ -3814,13 +3838,11 @@ var treectl = (function () { } function recvtree() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; - if (this.status !== 200) { - toast.err(0, "recvtree, http " + this.status + ": " + this.responseText); + if (!xhrchk(this, "could not list subfolders:\n\nerror ", "404, folder not found")) return; - } var cur = ebi('treeul').getAttribute('ts'); if (cur && parseInt(cur) > this.ts) { @@ -3973,7 +3995,7 @@ var treectl = (function () { } r.reqls = function (url, hpush, no_tree) { - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.top = url; xhr.hpush = hpush; xhr.ts = Date.now(); @@ -4002,13 +4024,11 @@ var treectl = (function () { } function recvls() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; - if (this.status !== 200) { - toast.err(0, "recvls, http " + this.status + ": " + this.responseText); + if (!xhrchk(this, "could not list files in folder:\n\nerror ", "404, folder not found")) return; - } var cur = ebi('files').getAttribute('ts'); if (cur && parseInt(cur) > this.ts) { @@ -4137,7 +4157,7 @@ var treectl = (function () { r.hydrate = function () { qsr('#bbsw'); if (ls0 === null) { - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('GET', '/?am_js', true); xhr.send(); @@ -4898,7 +4918,7 @@ var msel = (function () { fd.append("act", "mkdir"); fd.append("name", tb.value); - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.vp = get_evpath(); xhr.dn = tb.value; xhr.open('POST', xhr.vp, true); @@ -4910,7 +4930,7 @@ var msel = (function () { }; function cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.vp !== get_evpath()) { @@ -4918,6 +4938,8 @@ var msel = (function () { return; } + xhrchk(this, "could not create subfolder:\n\nerror ", "404, parent folder not found"); + if (this.status !== 200) { sf.textContent = 'error: ' + this.responseText; return; @@ -4947,7 +4969,7 @@ var msel = (function () { clmod(sf, 'vis', 1); sf.textContent = 'sending...'; - var xhr = new XMLHttpRequest(), + var xhr = new XHR(), ct = 'application/x-www-form-urlencoded;charset=UTF-8'; xhr.msg = tb.value; @@ -4963,9 +4985,11 @@ var msel = (function () { }; function cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; + xhrchk(this, "could not send message:\n\nerror ", "404, parent folder not found"); + if (this.status !== 200) { sf.textContent = 'error: ' + this.responseText; return; @@ -5080,15 +5104,11 @@ var unpost = (function () { html = []; function unpost_load_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; - if (this.status !== 200) { - var msg = this.responseText; - toast.err(9, 'unpost-load failed:\n' + msg); - ebi('op_unpost').innerHTML = html.join('\n'); - return; - } + if (!xhrchk(this, "unpost-load failed:\n\nerror ", "404, file not found??")) + return ebi('op_unpost').innerHTML = 'failed to load unpost list from server'; var res = JSON.parse(this.responseText); if (res.length) { @@ -5128,7 +5148,7 @@ var unpost = (function () { if (filt.value) q += '&filter=' + uricom_enc(filt.value, true); - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('GET', q, true); xhr.onreadystatechange = unpost_load_cb; xhr.send(); @@ -5137,7 +5157,7 @@ var unpost = (function () { }; function unpost_delete_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) { @@ -5188,7 +5208,7 @@ var unpost = (function () { toast.inf(0, "deleting " + req.length + " files..."); - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.n = n; xhr.n2 = n2; xhr.open('POST', '/?delete', true); diff --git a/copyparty/web/md2.js b/copyparty/web/md2.js index d584a30f..27cc3149 100644 --- a/copyparty/web/md2.js +++ b/copyparty/web/md2.js @@ -255,7 +255,7 @@ function Modpoll() { console.log('modpoll...'); var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now(); - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('GET', url, true); xhr.responseType = 'text'; xhr.onreadystatechange = r.cb; @@ -268,7 +268,7 @@ function Modpoll() { return; } - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) { @@ -336,7 +336,7 @@ function save(e) { fd.append("body", txt); var url = (document.location + '').split('?')[0]; - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('POST', url, true); xhr.responseType = 'text'; xhr.onreadystatechange = save_cb; @@ -356,7 +356,7 @@ function save(e) { } function save_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) @@ -397,7 +397,7 @@ function save_cb() { function run_savechk(lastmod, txt, btn, ntry) { // download the saved doc from the server and compare var url = (document.location + '').split('?')[0] + '?raw&_=' + Date.now(); - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('GET', url, true); xhr.responseType = 'text'; xhr.onreadystatechange = savechk_cb; @@ -409,7 +409,7 @@ function run_savechk(lastmod, txt, btn, ntry) { } function savechk_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) diff --git a/copyparty/web/mde.js b/copyparty/web/mde.js index 12577c46..e81a98c1 100644 --- a/copyparty/web/mde.js +++ b/copyparty/web/mde.js @@ -114,7 +114,7 @@ function save(mde) { fd.append("body", txt); var url = (document.location + '').split('?')[0]; - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('POST', url, true); xhr.responseType = 'text'; xhr.onreadystatechange = save_cb; @@ -133,7 +133,7 @@ function save(mde) { } function save_cb() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) @@ -170,7 +170,7 @@ function save_cb() { // download the saved doc from the server and compare var url = (document.location + '').split('?')[0] + '?raw'; - var xhr = new XMLHttpRequest(); + var xhr = new XHR(); xhr.open('GET', url, true); xhr.responseType = 'text'; xhr.onreadystatechange = save_chk; @@ -182,7 +182,7 @@ function save_cb() { } function save_chk() { - if (this.readyState != XMLHttpRequest.DONE) + if (this.readyState != XHR.DONE) return; if (this.status !== 200) diff --git a/copyparty/web/util.js b/copyparty/web/util.js index 3ad297e5..0366699c 100644 --- a/copyparty/web/util.js +++ b/copyparty/web/util.js @@ -14,7 +14,8 @@ var is_touch = 'ontouchstart' in window, var ebi = document.getElementById.bind(document), QS = document.querySelector.bind(document), QSA = document.querySelectorAll.bind(document), - mknod = document.createElement.bind(document); + mknod = document.createElement.bind(document), + XHR = XMLHttpRequest; function qsr(sel) { @@ -1386,3 +1387,17 @@ var favico = (function () { r.to = setTimeout(r.init, 100); return r; })(); + + +function xhrchk(xhr, prefix, e404) { + if (xhr.status < 400 && xhr.status >= 200) + return true; + + if (xhr.status == 403) + return toast.err(0, prefix + "403, access denied\n\ntry pressing F5, maybe you got logged out"); + + if (xhr.status == 404) + return toast.err(0, prefix + e404); + + return toast.err(0, prefix + xhr.status + ": " + xhr.responseText); +}