From af553c422d2b2e9dea6f8e7ac9d2623e687f4c74 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 29 Apr 2021 18:18:18 +0200 Subject: [PATCH] implement file size upload limit --- htdocs/lib/settings/ImageUpload.js | 14 ++++++++- owrx/controllers/__init__.py | 8 ++++- owrx/controllers/imageupload.py | 50 ++++++++++++++++++++++-------- owrx/form/input/gfx.py | 20 ++++++++++-- 4 files changed, 75 insertions(+), 17 deletions(-) diff --git a/htdocs/lib/settings/ImageUpload.js b/htdocs/lib/settings/ImageUpload.js index 02edbce..b5be1cd 100644 --- a/htdocs/lib/settings/ImageUpload.js +++ b/htdocs/lib/settings/ImageUpload.js @@ -6,6 +6,7 @@ $.fn.imageUpload = function() { var originalUrl = $img.prop('src'); var $input = $(this).find('input'); var id = $input.prop('id'); + var maxSize = $(this).data('max-size'); $uploadButton.click(function(){ $uploadButton.prop('disabled', true); var input = document.createElement('input'); @@ -14,9 +15,20 @@ $.fn.imageUpload = function() { input.onchange = function(e) { var reader = new FileReader() - // TODO: implement file size check reader.readAsArrayBuffer(e.target.files[0]); + reader.onprogress = function(e) { + if (e.loaded > maxSize) { + console.error('maximum file size exceeded, aborting file upload'); + $uploadButton.prop('disabled', false); + reader.abort(); + } + }; reader.onload = function(e) { + if (e.loaded > maxSize) { + console.error('maximum file size exceeded, aborting file upload'); + $uploadButton.prop('disabled', false); + return; + } $.ajax({ url: '/imageupload?id=' + id, type: 'POST', diff --git a/owrx/controllers/__init__.py b/owrx/controllers/__init__.py index e1b5fbd..f0ad9a7 100644 --- a/owrx/controllers/__init__.py +++ b/owrx/controllers/__init__.py @@ -1,6 +1,10 @@ from datetime import datetime, timezone +class BodySizeError(Exception): + pass + + class Controller(object): def __init__(self, handler, request, options): self.handler = handler @@ -33,10 +37,12 @@ class Controller(object): self.handler.send_header("Location", location) self.handler.end_headers() - def get_body(self): + def get_body(self, max_size=None): if "Content-Length" not in self.handler.headers: return None length = int(self.handler.headers["Content-Length"]) + if max_size is not None and length > max_size: + raise BodySizeError("HTTP body exceeds maximum allowed size") return self.handler.rfile.read(length) def handle_request(self): diff --git a/owrx/controllers/imageupload.py b/owrx/controllers/imageupload.py index b3f0100..a65a822 100644 --- a/owrx/controllers/imageupload.py +++ b/owrx/controllers/imageupload.py @@ -1,11 +1,20 @@ +from owrx.controllers import BodySizeError from owrx.controllers.assets import AssetsController from owrx.controllers.admin import AuthorizationMixin from owrx.config.core import CoreConfig +from owrx.form.input.gfx import AvatarInput, TopPhotoInput import uuid import json class ImageUploadController(AuthorizationMixin, AssetsController): + # max upload filesizes + max_sizes = { + # not the best idea to instantiate inputs, but i didn't want to duplicate the sizes here + "receiver_avatar": AvatarInput("id", "label").getMaxSize(), + "receiver_top_photo": TopPhotoInput("id", "label").getMaxSize(), + } + def __init__(self, handler, request, options): super().__init__(handler, request, options) self.file = request.query["file"][0] if "file" in request.query else None @@ -29,22 +38,37 @@ class ImageUploadController(AuthorizationMixin, AssetsController): def processImage(self): if "id" not in self.request.query: - self.send_response("{}", content_type="application/json", code=400) - # TODO: limit file size - contents = self.get_body() + self.send_json_response({"error": "missing id"}, code=400) + return + file_id = self.request.query["id"][0] + + if file_id not in ImageUploadController.max_sizes: + self.send_json_response({"error": "unexpected image id"}, code=400) + return + + try: + contents = self.get_body(ImageUploadController.max_sizes[file_id]) + except BodySizeError: + self.send_json_response({"error": "file size too large"}, code=400) + return + filetype = None if self._is_png(contents): filetype = "png" if self._is_jpg(contents): filetype = "jpg" if filetype is None: - self.send_response("{}", content_type="application/json", code=400) - else: - self.file = "{id}-{uuid}.{ext}".format( - id=self.request.query["id"][0], - uuid=uuid.uuid4().hex, - ext=filetype, - ) - with open(self.getFilePath(), "wb") as f: - f.write(contents) - self.send_response(json.dumps({"file": self.file}), content_type="application/json") + self.send_json_response({"error": "unsupported file type"}, code=400) + return + + self.file = "{id}-{uuid}.{ext}".format( + id=file_id, + uuid=uuid.uuid4().hex, + ext=filetype, + ) + with open(self.getFilePath(), "wb") as f: + f.write(contents) + self.send_json_response({"file": self.file}, code=200) + + def send_json_response(self, obj, code): + self.send_response(json.dumps(obj), code=code, content_type="application/json") diff --git a/owrx/form/input/gfx.py b/owrx/form/input/gfx.py index eb9f181..24516b4 100644 --- a/owrx/form/input/gfx.py +++ b/owrx/form/input/gfx.py @@ -7,7 +7,7 @@ class ImageInput(Input, metaclass=ABCMeta): def render_input(self, value, errors): # TODO display errors return """ -
+
{label} @@ -16,7 +16,11 @@ class ImageInput(Input, metaclass=ABCMeta):
""".format( - id=self.id, label=self.label, url=self.cachebuster(self.getUrl()), classes=" ".join(self.getImgClasses()) + id=self.id, + label=self.label, + url=self.cachebuster(self.getUrl()), + classes=" ".join(self.getImgClasses()), + maxsize=self.getMaxSize(), ) def cachebuster(self, url: str): @@ -34,6 +38,10 @@ class ImageInput(Input, metaclass=ABCMeta): def getImgClasses(self) -> list: pass + @abstractmethod + def getMaxSize(self) -> int: + pass + class AvatarInput(ImageInput): def getUrl(self) -> str: @@ -42,6 +50,10 @@ class AvatarInput(ImageInput): def getImgClasses(self) -> list: return ["webrx-rx-avatar"] + def getMaxSize(self) -> int: + # 256 kB + return 250 * 1024 + class TopPhotoInput(ImageInput): def getUrl(self) -> str: @@ -49,3 +61,7 @@ class TopPhotoInput(ImageInput): def getImgClasses(self) -> list: return ["webrx-top-photo"] + + def getMaxSize(self) -> int: + # 2 MB + return 2 * 1024 * 1024