http error codes

This commit is contained in:
ed 2019-06-26 23:32:34 +00:00
parent f110d1254d
commit 96c6be0ea1
3 changed files with 57 additions and 37 deletions

View file

@ -80,10 +80,10 @@ class VFS(object):
vn, rem = self._find(vpath) vn, rem = self._find(vpath)
if will_read and (uname not in vn.uread and "*" not in vn.uread): if will_read and (uname not in vn.uread and "*" not in vn.uread):
raise Pebkac("you don't have read-access for this location") raise Pebkac(403, "you don't have read-access for this location")
if will_write and (uname not in vn.uwrite and "*" not in vn.uwrite): if will_write and (uname not in vn.uwrite and "*" not in vn.uwrite):
raise Pebkac("you don't have write-access for this location") raise Pebkac(403, "you don't have write-access for this location")
return vn, rem return vn, rem

View file

@ -50,10 +50,10 @@ class HttpCli(object):
try: try:
self.mode, self.req, _ = headerlines[0].split(" ") self.mode, self.req, _ = headerlines[0].split(" ")
except: except:
raise Pebkac("bad headers:\n" + "\n".join(headerlines)) raise Pebkac(400, "bad headers:\n" + "\n".join(headerlines))
except Pebkac as ex: except Pebkac as ex:
self.loud_reply(str(ex)) self.loud_reply(str(ex), status=ex.code)
return False return False
self.headers = {} self.headers = {}
@ -107,20 +107,20 @@ class HttpCli(object):
elif self.mode == "POST": elif self.mode == "POST":
return self.handle_post() return self.handle_post()
else: else:
raise Pebkac('invalid HTTP mode "{0}"'.format(self.mode)) raise Pebkac(400, 'invalid HTTP mode "{0}"'.format(self.mode))
except Pebkac as ex: except Pebkac as ex:
try: try:
self.loud_reply(str(ex)) self.loud_reply(str(ex), status=ex.code)
except Pebkac: except Pebkac:
pass pass
return False return False
def reply(self, body, status="200 OK", mime="text/html", headers=[]): def reply(self, body, status=200, mime="text/html", headers=[]):
# TODO something to reply with user-supplied values safely # TODO something to reply with user-supplied values safely
response = [ response = [
"HTTP/1.1 " + status, "HTTP/1.1 {} {}".format(status, HTTPCODE[status]),
"Connection: Keep-Alive", "Connection: Keep-Alive",
"Content-Type: " + mime, "Content-Type: " + mime,
"Content-Length: " + str(len(body)), "Content-Length: " + str(len(body)),
@ -133,7 +133,7 @@ class HttpCli(object):
try: try:
self.s.sendall(response_str + b"\r\n\r\n" + body) self.s.sendall(response_str + b"\r\n\r\n" + body)
except: except:
raise Pebkac("client disconnected before http response") raise Pebkac(400, "client disconnected before http response")
return body return body
@ -148,7 +148,7 @@ class HttpCli(object):
try: try:
rval = self.headers["range"].split("=", 1)[1] rval = self.headers["range"].split("=", 1)[1]
except: except:
rval += self.headers["range"] rval = self.headers["range"]
logmsg += " [\033[36m" + rval + "\033[0m]" logmsg += " [\033[36m" + rval + "\033[0m]"
@ -196,7 +196,7 @@ class HttpCli(object):
ctype = self.headers.get("content-type", "").lower() ctype = self.headers.get("content-type", "").lower()
if not ctype: if not ctype:
raise Pebkac("you can't post without a content-type header") raise Pebkac(400, "you can't post without a content-type header")
if "multipart/form-data" in ctype: if "multipart/form-data" in ctype:
return self.handle_post_multipart() return self.handle_post_multipart()
@ -207,7 +207,7 @@ class HttpCli(object):
if "application/octet-stream" in ctype: if "application/octet-stream" in ctype:
return self.handle_post_binary() return self.handle_post_binary()
raise Pebkac("don't know how to handle {} POST".format(ctype)) raise Pebkac(405, "don't know how to handle {} POST".format(ctype))
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)
@ -221,16 +221,16 @@ class HttpCli(object):
if act == "login": if act == "login":
return self.handle_login() return self.handle_login()
raise Pebkac('invalid action "{}"'.format(act)) raise Pebkac(422, 'invalid action "{}"'.format(act))
def handle_post_json(self): def handle_post_json(self):
try: try:
remains = int(self.headers["content-length"]) remains = int(self.headers["content-length"])
except: except:
raise Pebkac("you must supply a content-length for JSON POST") raise Pebkac(400, "you must supply a content-length for JSON POST")
if remains > 1024 * 1024: if remains > 1024 * 1024:
raise Pebkac("json 2big") raise Pebkac(413, "json 2big")
enc = "utf-8" enc = "utf-8"
ctype = self.headers.get("content-type", "").lower() ctype = self.headers.get("content-type", "").lower()
@ -245,7 +245,7 @@ class HttpCli(object):
try: try:
body = json.loads(json_buf.decode(enc, "replace")) body = json.loads(json_buf.decode(enc, "replace"))
except: except:
raise Pebkac("you POSTed invalid json") raise Pebkac(422, "you POSTed invalid json")
print(body) print(body)
@ -291,7 +291,7 @@ class HttpCli(object):
fn = os.path.join(fdir, sanitize_fn(p_file)) fn = os.path.join(fdir, sanitize_fn(p_file))
if not os.path.isdir(fsenc(fdir)): if not os.path.isdir(fsenc(fdir)):
raise Pebkac("that folder does not exist") raise Pebkac(404, "that folder does not exist")
# TODO broker which avoid this race # TODO broker which avoid this race
# and provides a new filename if taken # and provides a new filename if taken
@ -303,7 +303,7 @@ class HttpCli(object):
self.log("writing to {0}".format(fn)) self.log("writing to {0}".format(fn))
sz, sha512 = hashcopy(self.conn, p_data, f) sz, sha512 = hashcopy(self.conn, p_data, f)
if sz == 0: if sz == 0:
raise Pebkac("empty files in post") raise Pebkac(400, "empty files in post")
files.append([sz, sha512]) files.append([sz, sha512])
@ -365,9 +365,9 @@ class HttpCli(object):
def tx_file(self, req_path): def tx_file(self, req_path):
do_send = True do_send = True
status = "200 OK" status = 200
extra_headers = [] extra_headers = []
logmsg = "{:4} {} {}".format("", self.req, status) logmsg = "{:4} {} {}".format("", self.req, "200 OK")
# #
# if request is for foo.js, check if we have foo.js.gz # if request is for foo.js, check if we have foo.js.gz
@ -382,7 +382,7 @@ class HttpCli(object):
try: try:
file_sz = os.path.getsize(fsenc(fs_path)) file_sz = os.path.getsize(fsenc(fs_path))
except: except:
raise Pebkac("404 Not Found") raise Pebkac(404)
# #
# if-modified # if-modified
@ -402,7 +402,7 @@ class HttpCli(object):
do_send = file_lastmod != cli_lastmod do_send = file_lastmod != cli_lastmod
if not do_send: if not do_send:
status = "304 Not Modified" status = 304
# #
# partial # partial
@ -426,12 +426,12 @@ class HttpCli(object):
upper = file_sz upper = file_sz
if lower < 0 or lower >= file_sz or upper < 0 or upper > file_sz: if lower < 0 or lower >= file_sz or upper < 0 or upper > file_sz:
raise Pebkac("na") raise Exception()
except: except:
self.loud_reply("invalid range requested: " + hrange) raise Pebkac(400, "invalid range requested: " + hrange)
status = "206 Partial Content" status = 206
extra_headers.append( extra_headers.append(
"Content-Range: bytes {}-{}/{}".format(lower, upper - 1, file_sz) "Content-Range: bytes {}-{}/{}".format(lower, upper - 1, file_sz)
) )
@ -469,7 +469,7 @@ class HttpCli(object):
mime = mimetypes.guess_type(req_path)[0] or "application/octet-stream" mime = mimetypes.guess_type(req_path)[0] or "application/octet-stream"
headers = [ headers = [
"HTTP/1.1 " + status, "HTTP/1.1 {} {}".format(status, HTTPCODE[status]),
"Connection: Keep-Alive", "Connection: Keep-Alive",
"Content-Type: " + mime, "Content-Type: " + mime,
"Content-Length: " + str(upper - lower), "Content-Length: " + str(upper - lower),
@ -535,7 +535,7 @@ class HttpCli(object):
if not os.path.exists(fsenc(abspath)): if not os.path.exists(fsenc(abspath)):
print(abspath) print(abspath)
raise Pebkac("404 not found") raise Pebkac(404)
if not os.path.isdir(fsenc(abspath)): if not os.path.isdir(fsenc(abspath)):
return self.tx_file(abspath) return self.tx_file(abspath)

View file

@ -24,6 +24,21 @@ surrogateescape.register_surrogateescape()
FS_ENCODING = sys.getfilesystemencoding() FS_ENCODING = sys.getfilesystemencoding()
HTTPCODE = {
200: "OK",
206: "Partial Content",
304: "Not Modified",
400: "Bad Request",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
413: "Payload Too Large",
422: "Unprocessable Entity",
500: "Internal Server Error",
501: "Not Implemented",
}
class Counter(object): class Counter(object):
def __init__(self, v=0): def __init__(self, v=0):
self.v = v self.v = v
@ -104,12 +119,12 @@ class MultipartParser(object):
continue continue
if m.group(1).lower() != "form-data": if m.group(1).lower() != "form-data":
raise Pebkac("not form-data: {}".format(ln)) raise Pebkac(400, "not form-data: {}".format(ln))
try: try:
field = self.re_cdisp_field.match(ln).group(1) field = self.re_cdisp_field.match(ln).group(1)
except: except:
raise Pebkac("missing field name: {}".format(ln)) raise Pebkac(400, "missing field name: {}".format(ln))
try: try:
fn = self.re_cdisp_file.match(ln).group(1) fn = self.re_cdisp_file.match(ln).group(1)
@ -160,7 +175,7 @@ class MultipartParser(object):
buf = self.sr.recv(bufsz) buf = self.sr.recv(bufsz)
if not buf: if not buf:
# abort: client disconnected # abort: client disconnected
raise Pebkac("client disconnected during post") raise Pebkac(400, "client disconnected during post")
while True: while True:
ofs = buf.find(self.boundary) ofs = buf.find(self.boundary)
@ -194,7 +209,7 @@ class MultipartParser(object):
buf2 = self.sr.recv(bufsz) buf2 = self.sr.recv(bufsz)
if not buf2: if not buf2:
# abort: client disconnected # abort: client disconnected
raise Pebkac("client disconnected during post") raise Pebkac(400, "client disconnected during post")
buf += buf2 buf += buf2
@ -217,14 +232,14 @@ class MultipartParser(object):
return return
if tail != b"\r\n": if tail != b"\r\n":
raise Pebkac("protocol error after field value") raise Pebkac(400, "protocol error after field value")
def _read_value(self, iterator, max_len): def _read_value(self, iterator, max_len):
ret = b"" ret = b""
for buf in iterator: for buf in iterator:
ret += buf ret += buf
if len(ret) > max_len: if len(ret) > max_len:
raise Pebkac("field length is too long") raise Pebkac(400, "field length is too long")
return ret return ret
@ -250,7 +265,9 @@ class MultipartParser(object):
""" """
p_field, _, p_data = next(self.gen) p_field, _, p_data = next(self.gen)
if p_field != field_name: if p_field != field_name:
raise Pebkac('expected field "{}", got "{}"'.format(field_name, p_field)) raise Pebkac(
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")
@ -268,7 +285,7 @@ def get_boundary(headers):
ct = headers["content-type"] ct = headers["content-type"]
m = re.match(ptn, ct, re.IGNORECASE) m = re.match(ptn, ct, re.IGNORECASE)
if not m: if not m:
raise Pebkac("invalid content-type for a multipart post: {}".format(ct)) raise Pebkac(400, "invalid content-type for a multipart post: {}".format(ct))
return m.group(2) return m.group(2)
@ -293,8 +310,9 @@ def read_header(sr):
return None return None
raise Pebkac( raise Pebkac(
400,
"protocol error while reading headers:\n" "protocol error while reading headers:\n"
+ ret.decode("utf-8", "replace") + ret.decode("utf-8", "replace"),
) )
ret += buf ret += buf
@ -424,4 +442,6 @@ def gzip_orig_sz(fn):
class Pebkac(Exception): class Pebkac(Exception):
pass def __init__(self, code, msg=None):
super(Pebkac, self).__init__(msg or HTTPCODE[code])
self.code = code