From 5b1e73ff714daa52ffdeeda01333739dc779a96a Mon Sep 17 00:00:00 2001 From: ed Date: Tue, 18 Jun 2019 19:27:17 +0000 Subject: [PATCH] less todo (handle client/network errors) --- copyparty/authsrv.py | 2 +- copyparty/httpcli.py | 116 +++++++++++++++++++++++++----------------- copyparty/httpconn.py | 1 - copyparty/util.py | 12 +++-- 4 files changed, 79 insertions(+), 52 deletions(-) diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index 59dc2ef0..1497429f 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -269,7 +269,7 @@ class AuthSrv(object): if self.warn_anonwrite: self.warn_anonwrite = False self.log( - "\033[31manyone can write to the current directory: {}\033[0m".format( + "\033[31manyone can read/write the current directory: {}\033[0m".format( os.getcwd() ) ) diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index be2d0a11..1f1af0f6 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -31,7 +31,6 @@ class HttpCli(object): self.log_func = conn.log_func self.log_src = conn.log_src - self.ok = True self.bufsz = 1024 * 32 self.absolute_urls = False self.out_headers = {} @@ -42,13 +41,17 @@ class HttpCli(object): def run(self): try: headerlines = read_header(self.sr) - except: - return False + if not headerlines: + return False - try: - mode, self.req, _ = headerlines[0].split(" ") - except: - raise Pebkac("bad headers:\n" + "\n".join(headerlines)) + try: + mode, self.req, _ = headerlines[0].split(" ") + except: + raise Pebkac("bad headers:\n" + "\n".join(headerlines)) + + except Pebkac as ex: + self.loud_reply(str(ex)) + return False self.headers = {} for header_line in headerlines[1:]: @@ -97,17 +100,19 @@ class HttpCli(object): try: if mode == "GET": - self.handle_get() + return self.handle_get() elif mode == "POST": - self.handle_post() + return self.handle_post() else: - self.loud_reply('invalid HTTP mode "{0}"'.format(mode)) + raise Pebkac('invalid HTTP mode "{0}"'.format(mode)) except Pebkac as ex: - self.loud_reply(str(ex)) - return False + try: + self.loud_reply(str(ex)) + except Pebkac: + pass - return self.ok + return False def reply(self, body, status="200 OK", mime="text/html", headers=[]): # TODO something to reply with user-supplied values safely @@ -122,8 +127,10 @@ class HttpCli(object): response.extend(headers) response_str = "\r\n".join(response).encode("utf-8") - if self.ok: + try: self.s.send(response_str + b"\r\n\r\n" + body) + except: + raise Pebkac("client disconnected before http response") return body @@ -218,40 +225,53 @@ class HttpCli(object): raise Exception("that was close") files = [] + errmsg = "" t0 = time.time() - for nfile, (p_field, p_file, p_data) in enumerate(self.parser.gen): - if not p_file: - self.log("discarding incoming file without filename") + try: + for nfile, (p_field, p_file, p_data) in enumerate(self.parser.gen): + if not p_file: + self.log("discarding incoming file without filename") + # fallthrough - fn = os.devnull - if p_file and not nullwrite: - fn = os.path.join(vfs.realpath, rem, sanitize_fn(p_file)) + fn = os.devnull + if p_file and not nullwrite: + fdir = os.path.join(vfs.realpath, rem) + fn = os.path.join(fdir, sanitize_fn(p_file)) - # TODO broker which avoid this race - # and provides a new filename if taken - if os.path.exists(fsenc(fn)): - fn += ".{:.6f}".format(time.time()) + if not os.path.isdir(fsenc(fdir)): + raise Pebkac("that folder does not exist") - try: - with open(fn, "wb") as f: - self.log("writing to {0}".format(fn)) - sz, sha512 = hashcopy(self.conn, p_data, f) - if sz == 0: - break + # TODO broker which avoid this race + # and provides a new filename if taken + if os.path.exists(fsenc(fn)): + fn += ".{:.6f}".format(time.time()) - files.append([sz, sha512]) + try: + with open(fsenc(fn), "wb") as f: + self.log("writing to {0}".format(fn)) + sz, sha512 = hashcopy(self.conn, p_data, f) + if sz == 0: + raise Pebkac("empty files in post") - except FileNotFoundError: - raise Pebkac("create that folder before uploading to it") + files.append([sz, sha512]) - self.parser.drop() + except Pebkac: + if not nullwrite: + os.rename(fsenc(fn), fsenc(fn + ".PARTIAL")) + + raise + + except Pebkac as ex: + errmsg = str(ex) td = time.time() - t0 sz_total = sum(x[0] for x in files) spd = (sz_total / td) / (1024 * 1024) status = "OK" - if not self.ok: + if errmsg: + self.log(errmsg) + errmsg = "ERROR: " + errmsg status = "ERROR" msg = "{0} // {1} bytes // {2:.3f} MiB/s\n".format(status, sz_total, spd) @@ -261,15 +281,7 @@ class HttpCli(object): # truncated SHA-512 prevents length extension attacks; # using SHA-512/224, optionally SHA-512/256 = :64 - html = self.conn.tpl_msg.render( - h2='return to /{}'.format( - quotep(self.vpath), cgi.escape(self.vpath, quote=True) - ), - pre=msg, - ) self.log(msg) - self.reply(html.encode("utf-8")) - if not nullwrite: # TODO this is bad log_fn = "up.{:.6f}.txt".format(t0) @@ -284,9 +296,21 @@ class HttpCli(object): ] ) + "\n" + + errmsg + + "\n" ).encode("utf-8") ) + html = self.conn.tpl_msg.render( + h2='return to /{}'.format( + quotep(self.vpath), cgi.escape(self.vpath, quote=True) + ), + pre=msg, + ) + self.reply(html.encode("utf-8")) + + self.parser.drop() + def tx_file(self, path): sz = os.path.getsize(fsenc(path)) mime = mimetypes.guess_type(path)[0] @@ -296,11 +320,10 @@ class HttpCli(object): "utf-8" ) - if self.ok: - self.s.send(header) + self.s.send(header) with open(fsenc(path), "rb") as f: - while self.ok: + while True: buf = f.read(4096) if not buf: break @@ -309,7 +332,6 @@ class HttpCli(object): self.s.send(buf) except ConnectionResetError: return False - # TODO propagate (self.ok or return) def tx_mounts(self): html = self.conn.tpl_mounts.render(this=self) diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index 27744c72..bc56e8d9 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -24,7 +24,6 @@ class HttpConn(object): self.sr = Unrecv(sck) self.workload = 0 - self.ok = True self.log_func = log_func self.log_src = "{} \033[36m{}".format(addr[0], addr[1]).ljust(26) diff --git a/copyparty/util.py b/copyparty/util.py index df7fb9cc..c6d49385 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -159,7 +159,7 @@ class MultipartParser(object): buf = self.sr.recv(bufsz) if not buf: # abort: client disconnected - raise Exception("client disconnected during post") + raise Pebkac("client disconnected during post") while True: ofs = buf.find(self.boundary) @@ -193,7 +193,7 @@ class MultipartParser(object): buf2 = self.sr.recv(bufsz) if not buf2: # abort: client disconnected - raise Exception("client disconnected during post") + raise Pebkac("client disconnected during post") buf += buf2 @@ -288,7 +288,13 @@ def read_header(sr): buf = sr.recv(n) if not buf: - raise Exception("failed to read headers") + if not ret: + return None + + raise Pebkac( + "protocol error while reading headers:\n" + + ret.decode("utf-8", "replace") + ) ret += buf