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);
+}