From b5fc537b89752345636fc858c13382cef5fb9ba1 Mon Sep 17 00:00:00 2001 From: ed Date: Sat, 8 Aug 2020 00:47:54 +0000 Subject: [PATCH] support PUT and ACAO --- README.md | 10 +++++++ copyparty/authsrv.py | 7 +++-- copyparty/httpcli.py | 61 +++++++++++++++++++++++++++++++++++++++---- copyparty/httpconn.py | 2 +- copyparty/util.py | 10 +++++++ 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 145b8c9e..fe71366d 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,20 @@ turn your phone or raspi into a portable file server with resumable uploads/down * [x] accounts * [x] markdown viewer * [x] markdown editor +* [x] FUSE client summary: it works! you can use it! (but technically not even close to beta) +# client examples + +* javascript: dump some state into a file (two separate examples) + `await fetch('https://127.0.0.1:3923/', {method:"PUT", body: JSON.stringify(foo)});` + `var xhr = new XMLHttpRequest(); xhr.open('POST', 'https://127.0.0.1:3923/msgs?raw'); xhr.send('foo');` + +* FUSE: mount a copyparty server as a local filesystem (see [./bin/](bin/)) + + # dependencies * `jinja2` diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index a391b976..2cd0c749 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -135,9 +135,9 @@ class AuthSrv(object): self.warn_anonwrite = True if WINDOWS: - self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):([^:]*)$") + self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$") else: - self.re_vol = re.compile(r"^([^:]*):([^:]*):([^:]*)$") + self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$") self.mutex = threading.Lock() self.reload() @@ -226,8 +226,7 @@ class AuthSrv(object): raise Exception("invalid -v argument: [{}]".format(v_str)) src, dst, perms = m.groups() - print("\n".join([src, dst, perms])) - + # print("\n".join([src, dst, perms])) src = fsdec(os.path.abspath(fsenc(src))) dst = dst.strip("/") mount[dst] = src diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 35bd3428..0533ed1f 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -36,13 +36,13 @@ class HttpCli(object): self.bufsz = 1024 * 32 self.absolute_urls = False - self.out_headers = {} + self.out_headers = {"Access-Control-Allow-Origin": "*"} def log(self, msg): self.log_func(self.log_src, msg) def _check_nonfatal(self, ex): - return ex.code in [403, 404] + return ex.code in [404] def _assert_safe_rem(self, rem): # sanity check to prevent any disasters @@ -128,6 +128,10 @@ class HttpCli(object): return self.handle_get() and self.keepalive elif self.mode == "POST": return self.handle_post() and self.keepalive + elif self.mode == "PUT": + return self.handle_put() and self.keepalive + elif self.mode == "OPTIONS": + return self.handle_options() and self.keepalive else: raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode)) @@ -143,9 +147,7 @@ class HttpCli(object): def send_headers(self, length, status=200, mime=None, headers={}): response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])] - if length is None: - self.keepalive = False - else: + if length is not None: response.append("Content-Length: " + str(length)) # close if unknown length, otherwise take client's preference @@ -230,6 +232,30 @@ class HttpCli(object): return self.tx_browser() + def handle_options(self): + self.log("OPTIONS " + self.req) + self.send_headers( + None, + 204, + headers={ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Headers": "*", + }, + ) + return True + + def handle_put(self): + self.log("PUT " + self.req) + + if self.headers.get("expect", "").lower() == "100-continue": + try: + self.s.sendall(b"HTTP/1.1 100 Continue\r\n\r\n") + except: + raise Pebkac(400, "client d/c before 100 continue") + + return self.handle_stash() + def handle_post(self): self.log("POST " + self.req) @@ -243,6 +269,9 @@ class HttpCli(object): if not ctype: raise Pebkac(400, "you can't post without a content-type header") + if "raw" in self.uparam: + return self.handle_stash() + if "multipart/form-data" in ctype: return self.handle_post_multipart() @@ -255,6 +284,28 @@ class HttpCli(object): raise Pebkac(405, "don't know how to handle {} POST".format(ctype)) + def handle_stash(self): + remains = int(self.headers.get("content-length", None)) + if remains is None: + reader = read_socket_unbounded(self.sr) + self.keepalive = False + else: + reader = read_socket(self.sr, remains) + + vfs, rem = self.conn.auth.vfs.get(self.vpath, self.uname, False, True) + fdir = os.path.join(vfs.realpath, rem) + + addr = self.conn.addr[0].replace(":", ".") + fn = "put-{:.6f}-{}.bin".format(time.time(), addr) + path = os.path.join(fdir, fn) + + with open(path, "wb", 512 * 1024) as f: + post_sz, _, sha_b64 = hashcopy(self.conn, reader, f) + + self.log("wrote {}/{} bytes to {}".format(post_sz, remains, path)) + self.reply("{}\n{}\n".format(post_sz, sha_b64).encode("utf-8")) + return True + def handle_post_multipart(self): self.parser = MultipartParser(self.log, self.sr, self.headers) self.parser.parse() diff --git a/copyparty/httpconn.py b/copyparty/httpconn.py index 6e4135a8..8be3c49a 100644 --- a/copyparty/httpconn.py +++ b/copyparty/httpconn.py @@ -86,7 +86,7 @@ class HttpConn(object): self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8")) return - if method not in [None, b"GET ", b"HEAD", b"POST"]: + if method not in [None, b"GET ", b"HEAD", b"POST", b"PUT ", b"OPTI"]: if self.sr: self.log("\033[1;31mTODO: cannot do https in jython\033[0m") return diff --git a/copyparty/util.py b/copyparty/util.py index 097fcbf8..0474d96f 100644 --- a/copyparty/util.py +++ b/copyparty/util.py @@ -42,6 +42,7 @@ if WINDOWS and PY2: HTTPCODE = { 200: "OK", + 204: "No Content", 206: "Partial Content", 304: "Not Modified", 400: "Bad Request", @@ -445,6 +446,15 @@ def read_socket(sr, total_size): yield buf +def read_socket_unbounded(sr): + while True: + buf = sr.recv(32 * 1024) + if not buf: + return + + yield buf + + def hashcopy(actor, fin, fout): u32_lim = int((2 ** 31) * 0.9) hashobj = hashlib.sha512()