mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
support PUT and ACAO
This commit is contained in:
parent
14899d3a7c
commit
b5fc537b89
10
README.md
10
README.md
|
@ -38,10 +38,20 @@ turn your phone or raspi into a portable file server with resumable uploads/down
|
||||||
* [x] accounts
|
* [x] accounts
|
||||||
* [x] markdown viewer
|
* [x] markdown viewer
|
||||||
* [x] markdown editor
|
* [x] markdown editor
|
||||||
|
* [x] FUSE client
|
||||||
|
|
||||||
summary: it works! you can use it! (but technically not even close to beta)
|
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
|
# dependencies
|
||||||
|
|
||||||
* `jinja2`
|
* `jinja2`
|
||||||
|
|
|
@ -135,9 +135,9 @@ class AuthSrv(object):
|
||||||
self.warn_anonwrite = True
|
self.warn_anonwrite = True
|
||||||
|
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):([^:]*)$")
|
self.re_vol = re.compile(r"^([a-zA-Z]:[\\/][^:]*|[^:]*):([^:]*):(.*)$")
|
||||||
else:
|
else:
|
||||||
self.re_vol = re.compile(r"^([^:]*):([^:]*):([^:]*)$")
|
self.re_vol = re.compile(r"^([^:]*):([^:]*):(.*)$")
|
||||||
|
|
||||||
self.mutex = threading.Lock()
|
self.mutex = threading.Lock()
|
||||||
self.reload()
|
self.reload()
|
||||||
|
@ -226,8 +226,7 @@ class AuthSrv(object):
|
||||||
raise Exception("invalid -v argument: [{}]".format(v_str))
|
raise Exception("invalid -v argument: [{}]".format(v_str))
|
||||||
|
|
||||||
src, dst, perms = m.groups()
|
src, dst, perms = m.groups()
|
||||||
print("\n".join([src, dst, perms]))
|
# print("\n".join([src, dst, perms]))
|
||||||
|
|
||||||
src = fsdec(os.path.abspath(fsenc(src)))
|
src = fsdec(os.path.abspath(fsenc(src)))
|
||||||
dst = dst.strip("/")
|
dst = dst.strip("/")
|
||||||
mount[dst] = src
|
mount[dst] = src
|
||||||
|
|
|
@ -36,13 +36,13 @@ class HttpCli(object):
|
||||||
|
|
||||||
self.bufsz = 1024 * 32
|
self.bufsz = 1024 * 32
|
||||||
self.absolute_urls = False
|
self.absolute_urls = False
|
||||||
self.out_headers = {}
|
self.out_headers = {"Access-Control-Allow-Origin": "*"}
|
||||||
|
|
||||||
def log(self, msg):
|
def log(self, msg):
|
||||||
self.log_func(self.log_src, msg)
|
self.log_func(self.log_src, msg)
|
||||||
|
|
||||||
def _check_nonfatal(self, ex):
|
def _check_nonfatal(self, ex):
|
||||||
return ex.code in [403, 404]
|
return ex.code in [404]
|
||||||
|
|
||||||
def _assert_safe_rem(self, rem):
|
def _assert_safe_rem(self, rem):
|
||||||
# sanity check to prevent any disasters
|
# sanity check to prevent any disasters
|
||||||
|
@ -128,6 +128,10 @@ class HttpCli(object):
|
||||||
return self.handle_get() and self.keepalive
|
return self.handle_get() and self.keepalive
|
||||||
elif self.mode == "POST":
|
elif self.mode == "POST":
|
||||||
return self.handle_post() and self.keepalive
|
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:
|
else:
|
||||||
raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
|
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={}):
|
def send_headers(self, length, status=200, mime=None, headers={}):
|
||||||
response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])]
|
response = ["HTTP/1.1 {} {}".format(status, HTTPCODE[status])]
|
||||||
|
|
||||||
if length is None:
|
if length is not None:
|
||||||
self.keepalive = False
|
|
||||||
else:
|
|
||||||
response.append("Content-Length: " + str(length))
|
response.append("Content-Length: " + str(length))
|
||||||
|
|
||||||
# close if unknown length, otherwise take client's preference
|
# close if unknown length, otherwise take client's preference
|
||||||
|
@ -230,6 +232,30 @@ class HttpCli(object):
|
||||||
|
|
||||||
return self.tx_browser()
|
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):
|
def handle_post(self):
|
||||||
self.log("POST " + self.req)
|
self.log("POST " + self.req)
|
||||||
|
|
||||||
|
@ -243,6 +269,9 @@ class HttpCli(object):
|
||||||
if not ctype:
|
if not ctype:
|
||||||
raise Pebkac(400, "you can't post without a content-type header")
|
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:
|
if "multipart/form-data" in ctype:
|
||||||
return self.handle_post_multipart()
|
return self.handle_post_multipart()
|
||||||
|
|
||||||
|
@ -255,6 +284,28 @@ class HttpCli(object):
|
||||||
|
|
||||||
raise Pebkac(405, "don't know how to handle {} POST".format(ctype))
|
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):
|
def handle_post_multipart(self):
|
||||||
self.parser = MultipartParser(self.log, self.sr, self.headers)
|
self.parser = MultipartParser(self.log, self.sr, self.headers)
|
||||||
self.parser.parse()
|
self.parser.parse()
|
||||||
|
|
|
@ -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"))
|
self.s.send(b"HTTP/1.1 400 Bad Request\r\n\r\n" + err.encode("utf-8"))
|
||||||
return
|
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:
|
if self.sr:
|
||||||
self.log("\033[1;31mTODO: cannot do https in jython\033[0m")
|
self.log("\033[1;31mTODO: cannot do https in jython\033[0m")
|
||||||
return
|
return
|
||||||
|
|
|
@ -42,6 +42,7 @@ if WINDOWS and PY2:
|
||||||
|
|
||||||
HTTPCODE = {
|
HTTPCODE = {
|
||||||
200: "OK",
|
200: "OK",
|
||||||
|
204: "No Content",
|
||||||
206: "Partial Content",
|
206: "Partial Content",
|
||||||
304: "Not Modified",
|
304: "Not Modified",
|
||||||
400: "Bad Request",
|
400: "Bad Request",
|
||||||
|
@ -445,6 +446,15 @@ def read_socket(sr, total_size):
|
||||||
yield buf
|
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):
|
def hashcopy(actor, fin, fout):
|
||||||
u32_lim = int((2 ** 31) * 0.9)
|
u32_lim = int((2 ** 31) * 0.9)
|
||||||
hashobj = hashlib.sha512()
|
hashobj = hashlib.sha512()
|
||||||
|
|
Loading…
Reference in a new issue