accept file POSTs without specifying the act field;

primarily to support uploading from Igloo IRC but also generally useful
(not actually tested with Igloo IRC yet because it's a paid feature
so just gonna wait for spiky to wake up and tell me it didn't work)
This commit is contained in:
ed 2024-01-08 19:09:53 +00:00
parent dc8e621d7c
commit 9bc09ce949
4 changed files with 43 additions and 13 deletions

View file

@ -1569,10 +1569,12 @@ interact with copyparty using non-browser clients
* `var xhr = new XMLHttpRequest(); xhr.open('POST', '//127.0.0.1:3923/msgs?raw'); xhr.send('foo');` * `var xhr = new XMLHttpRequest(); xhr.open('POST', '//127.0.0.1:3923/msgs?raw'); xhr.send('foo');`
* curl/wget: upload some files (post=file, chunk=stdin) * curl/wget: upload some files (post=file, chunk=stdin)
* `post(){ curl -F act=bput -F f=@"$1" http://127.0.0.1:3923/?pw=wark;}` * `post(){ curl -F f=@"$1" http://127.0.0.1:3923/?pw=wark;}`
`post movie.mkv` `post movie.mkv` (gives HTML in return)
* `post(){ curl -F f=@"$1" 'http://127.0.0.1:3923/?want=url&pw=wark';}`
`post movie.mkv` (gives hotlink in return)
* `post(){ curl -H pw:wark -H rand:8 -T "$1" http://127.0.0.1:3923/;}` * `post(){ curl -H pw:wark -H rand:8 -T "$1" http://127.0.0.1:3923/;}`
`post movie.mkv` `post movie.mkv` (randomized filename)
* `post(){ wget --header='pw: wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}` * `post(){ wget --header='pw: wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
`post movie.mkv` `post movie.mkv`
* `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}` * `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}`

View file

@ -45,6 +45,7 @@ from .util import (
ODict, ODict,
Pebkac, Pebkac,
UnrecvEOF, UnrecvEOF,
WrongPostKey,
absreal, absreal,
alltrace, alltrace,
atomic_move, atomic_move,
@ -1862,7 +1863,16 @@ class HttpCli(object):
self.parser = MultipartParser(self.log, self.sr, self.headers) self.parser = MultipartParser(self.log, self.sr, self.headers)
self.parser.parse() self.parser.parse()
file0: list[tuple[str, Optional[str], Generator[bytes, None, None]]] = []
try:
act = self.parser.require("act", 64) act = self.parser.require("act", 64)
except WrongPostKey as ex:
if ex.got == "f" and ex.fname:
self.log("missing 'act', but looks like an upload so assuming that")
file0 = [(ex.got, ex.fname, ex.datagen)]
act = "bput"
else:
raise
if act == "login": if act == "login":
return self.handle_login() return self.handle_login()
@ -1875,7 +1885,7 @@ class HttpCli(object):
return self.handle_new_md() return self.handle_new_md()
if act == "bput": if act == "bput":
return self.handle_plain_upload() return self.handle_plain_upload(file0)
if act == "tput": if act == "tput":
return self.handle_text_upload() return self.handle_text_upload()
@ -2314,7 +2324,9 @@ class HttpCli(object):
vfs.flags.get("xau") or [], vfs.flags.get("xau") or [],
) )
def handle_plain_upload(self) -> bool: def handle_plain_upload(
self, file0: list[tuple[str, Optional[str], Generator[bytes, None, None]]]
) -> bool:
assert self.parser assert self.parser
nullwrite = self.args.nw nullwrite = self.args.nw
vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True) vfs, rem = self.asrv.vfs.get(self.vpath, self.uname, False, True)
@ -2340,7 +2352,8 @@ class HttpCli(object):
t0 = time.time() t0 = time.time()
try: try:
assert self.parser.gen assert self.parser.gen
for nfile, (p_field, p_file, p_data) in enumerate(self.parser.gen): gens = itertools.chain(file0, self.parser.gen)
for nfile, (p_field, p_file, p_data) in enumerate(gens):
if not p_file: if not p_file:
self.log("discarding incoming file without filename") self.log("discarding incoming file without filename")
# fallthrough # fallthrough

View file

@ -1537,11 +1537,9 @@ class MultipartParser(object):
raises if the field name is not as expected raises if the field name is not as expected
""" """
assert self.gen assert self.gen
p_field, _, p_data = next(self.gen) p_field, p_fname, p_data = next(self.gen)
if p_field != field_name: if p_field != field_name:
raise Pebkac( raise WrongPostKey(field_name, p_field, p_fname, p_data)
422, 'expected field "{}", got "{}"'.format(field_name, p_field)
)
return self._read_value(p_data, max_len).decode("utf-8", "surrogateescape") return self._read_value(p_data, max_len).decode("utf-8", "surrogateescape")
@ -3058,3 +3056,20 @@ class Pebkac(Exception):
def __repr__(self) -> str: def __repr__(self) -> str:
return "Pebkac({}, {})".format(self.code, repr(self.args)) return "Pebkac({}, {})".format(self.code, repr(self.args))
class WrongPostKey(Pebkac):
def __init__(
self,
expected: str,
got: str,
fname: Optional[str],
datagen: Generator[bytes, None, None],
) -> None:
msg = 'expected field "{}", got "{}"'.format(expected, got)
super(WrongPostKey, self).__init__(422, msg)
self.expected = expected
self.got = got
self.fname = fname
self.datagen = datagen

View file

@ -162,8 +162,8 @@ authenticate using header `Cookie: cppwd=foo` or url param `&pw=foo`
| PUT | | (binary data) | upload into file at URL | | PUT | | (binary data) | upload into file at URL |
| PUT | `?gz` | (binary data) | compress with gzip and write into file at URL | | PUT | `?gz` | (binary data) | compress with gzip and write into file at URL |
| PUT | `?xz` | (binary data) | compress with xz and write into file at URL | | PUT | `?xz` | (binary data) | compress with xz and write into file at URL |
| mPOST | | `act=bput`, `f=FILE` | upload `FILE` into the folder at URL | | mPOST | | `f=FILE` | upload `FILE` into the folder at URL |
| mPOST | `?j` | `act=bput`, `f=FILE` | ...and reply with json | | mPOST | `?j` | `f=FILE` | ...and reply with json |
| mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL | | mPOST | | `act=mkdir`, `name=foo` | create directory `foo` at URL |
| POST | `?delete` | | delete URL recursively | | POST | `?delete` | | delete URL recursively |
| jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively | | jPOST | `?delete` | `["/foo","/bar"]` | delete `/foo` and `/bar` recursively |