From 3950f6e8ebf264ff5fb7c4302f11667e41c0ce21 Mon Sep 17 00:00:00 2001 From: jonas-w Date: Sun, 15 Jan 2023 19:28:21 +0100 Subject: [PATCH 1/6] fix 500 error when file extension could not be guessed when a file without an extension was uploaded and the mimetypes.guess_extension returned None because there is no official file extension for that mimetype a NoneType was subscripted which yielded a 500 http error --- fhost.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fhost.py b/fhost.py index a59e22e..4880f08 100755 --- a/fhost.py +++ b/fhost.py @@ -247,8 +247,10 @@ class File(db.Model): if not ext: if gmime in app.config["FHOST_EXT_OVERRIDE"]: ext = app.config["FHOST_EXT_OVERRIDE"][gmime] + elif guess: + ext = guess else: - ext = guess_extension(gmime) + ext = "" return ext[:app.config["FHOST_MAX_EXT_LENGTH"]] or ".bin" From e00866f5e4a66c18690f0bcdd274d68f3c3d9d0d Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Wed, 29 Mar 2023 07:19:47 +0200 Subject: [PATCH 2/6] URL: Explicitly set upper-case table name Looks like recent SQLAlchemy/Alembic chose to lower-case it by default. Try not to break existing schemas. --- fhost.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fhost.py b/fhost.py index 4880f08..0a04256 100755 --- a/fhost.py +++ b/fhost.py @@ -106,6 +106,7 @@ db = SQLAlchemy(app) migrate = Migrate(app, db) class URL(db.Model): + __tablename__ = "URL" id = db.Column(db.Integer, primary_key = True) url = db.Column(db.UnicodeText, unique = True) From 3d1facaec306d453d5749fc49ad6772ebbafff7e Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Wed, 29 Mar 2023 07:21:36 +0200 Subject: [PATCH 3/6] Store user agent with files Needed for moderation. --- fhost.py | 20 ++++++++----- ...b7d2_store_user_agent_string_with_files.py | 30 +++++++++++++++++++ mod.py | 4 +++ 3 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 migrations/versions/dd0766afb7d2_store_user_agent_string_with_files.py diff --git a/fhost.py b/fhost.py index 0a04256..3d45c4d 100755 --- a/fhost.py +++ b/fhost.py @@ -135,6 +135,7 @@ class File(db.Model): ext = db.Column(db.UnicodeText) mime = db.Column(db.UnicodeText) addr = db.Column(db.UnicodeText) + ua = db.Column(db.UnicodeText) removed = db.Column(db.Boolean, default=False) nsfw_score = db.Column(db.Float) expiration = db.Column(db.BigInteger) @@ -143,11 +144,12 @@ class File(db.Model): last_vscan = db.Column(db.DateTime) size = db.Column(db.BigInteger) - def __init__(self, sha256, ext, mime, addr, expiration, mgmt_token): + def __init__(self, sha256, ext, mime, addr, ua, expiration, mgmt_token): self.sha256 = sha256 self.ext = ext self.mime = mime self.addr = addr + self.ua = ua self.expiration = expiration self.mgmt_token = mgmt_token @@ -212,7 +214,7 @@ class File(db.Model): Any value greater that the longest allowed file lifespan will be rounded down to that value. """ - def store(file_, requested_expiration: typing.Optional[int], addr, secret: bool): + def store(file_, requested_expiration: typing.Optional[int], addr, ua, secret: bool): data = file_.read() digest = sha256(data).hexdigest() @@ -278,9 +280,10 @@ class File(db.Model): mime = get_mime() ext = get_ext(mime) mgmt_token = secrets.token_urlsafe() - f = File(digest, ext, mime, addr, expiration, mgmt_token) + f = File(digest, ext, mime, addr, ua, expiration, mgmt_token) f.addr = addr + f.ua = ua if isnew: f.secret = None @@ -368,11 +371,11 @@ requested_expiration can be: Any value greater that the longest allowed file lifespan will be rounded down to that value. """ -def store_file(f, requested_expiration: typing.Optional[int], addr, secret: bool): +def store_file(f, requested_expiration: typing.Optional[int], addr, ua, secret: bool): if in_upload_bl(addr): return "Your host is blocked from uploading files.\n", 451 - sf, isnew = File.store(f, requested_expiration, addr, secret) + sf, isnew = File.store(f, requested_expiration, addr, ua, secret) response = make_response(sf.geturl()) response.headers["X-Expires"] = sf.expiration @@ -382,7 +385,7 @@ def store_file(f, requested_expiration: typing.Optional[int], addr, secret: boo return response -def store_url(url, addr, secret: bool): +def store_url(url, addr, ua, secret: bool): if is_fhost_url(url): abort(400) @@ -403,7 +406,7 @@ def store_url(url, addr, secret: bool): f = urlfile(read=r.raw.read, content_type=r.headers["content-type"], filename="") - return store_file(f, None, addr, secret) + return store_file(f, None, addr, ua, secret) else: abort(413) else: @@ -498,6 +501,7 @@ def fhost(): request.files["file"], int(request.form["expires"]), request.remote_addr, + request.user_agent.string, secret ) except ValueError: @@ -509,12 +513,14 @@ def fhost(): request.files["file"], None, request.remote_addr, + request.user_agent.string, secret ) elif "url" in request.form: return store_url( request.form["url"], request.remote_addr, + request.user_agent.string, secret ) elif "shorten" in request.form: diff --git a/migrations/versions/dd0766afb7d2_store_user_agent_string_with_files.py b/migrations/versions/dd0766afb7d2_store_user_agent_string_with_files.py new file mode 100644 index 0000000..4af7680 --- /dev/null +++ b/migrations/versions/dd0766afb7d2_store_user_agent_string_with_files.py @@ -0,0 +1,30 @@ +"""Store user agent string with files + +Revision ID: dd0766afb7d2 +Revises: 30bfe33aa328 +Create Date: 2023-03-29 07:18:49.113200 + +""" + +# revision identifiers, used by Alembic. +revision = 'dd0766afb7d2' +down_revision = '30bfe33aa328' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.add_column(sa.Column('ua', sa.UnicodeText(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('file', schema=None) as batch_op: + batch_op.drop_column('ua') + + # ### end Alembic commands ### diff --git a/mod.py b/mod.py index 9a68e79..559fee8 100755 --- a/mod.py +++ b/mod.py @@ -26,6 +26,7 @@ class NullptrMod(Screen): ("f4", "filter(4, 'Filter extension:')", "Filter Ext."), ("f5", "refresh", "Refresh"), ("f6", "filter_clear", "Clear filter"), + ("f7", "filter(5, 'Filter user agent:')", "Filter UA"), ("r", "remove_file(False)", "Remove file"), ("ctrl+r", "remove_file(True)", "Ban file"), ("p", "ban_ip(False)", "Ban IP"), @@ -60,6 +61,7 @@ class NullptrMod(Screen): case 2: finput.value = self.current_file.addr case 3: finput.value = self.current_file.mime case 4: finput.value = self.current_file.ext + case 5: finput.value = self.current_file.ua def on_input_submitted(self, message: Input.Submitted) -> None: self.query_one("#filter_container").display = False @@ -74,6 +76,7 @@ class NullptrMod(Screen): case 2: ftable.query = ftable.base_query.filter(File.addr == message.value) case 3: ftable.query = ftable.base_query.filter(File.mime.like(message.value)) case 4: ftable.query = ftable.base_query.filter(File.ext.like(message.value)) + case 5: ftable.query = ftable.base_query.filter(File.ua.like(message.value)) else: ftable.query = ftable.base_query @@ -249,6 +252,7 @@ class NullptrMod(Screen): ("MIME type:", f.mime), ("SHA256 checksum:", f.sha256), ("Uploaded by:", Text(f.addr)), + ("User agent:", Text(f.ua or "")), ("Management token:", f.mgmt_token), ("Secret:", f.secret), ("Is NSFW:", ("Yes" if f.is_nsfw else "No") + (f" (Score: {f.nsfw_score:0.4f})" if f.nsfw_score else " (Not scanned)")), From c189c47306682bff9188156b022316472f8609ae Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Wed, 29 Mar 2023 07:38:36 +0200 Subject: [PATCH 4/6] ModUI: Allow LIKE matching for address filtering --- mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod.py b/mod.py index 559fee8..4dc7107 100755 --- a/mod.py +++ b/mod.py @@ -73,7 +73,7 @@ class NullptrMod(Screen): case 1: try: ftable.query = ftable.base_query.filter(File.id == su.debase(message.value)) except ValueError: pass - case 2: ftable.query = ftable.base_query.filter(File.addr == message.value) + case 2: ftable.query = ftable.base_query.filter(File.addr.like(message.value)) case 3: ftable.query = ftable.base_query.filter(File.mime.like(message.value)) case 4: ftable.query = ftable.base_query.filter(File.ext.like(message.value)) case 5: ftable.query = ftable.base_query.filter(File.ua.like(message.value)) From c2b5e95903053d084ce9208a2e96226e9f5feebd Mon Sep 17 00:00:00 2001 From: Mia Herkt Date: Wed, 29 Mar 2023 07:49:56 +0200 Subject: [PATCH 5/6] ModUI: Handle opening filter panel with NULL user agent --- mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod.py b/mod.py index 4dc7107..0748a42 100755 --- a/mod.py +++ b/mod.py @@ -61,7 +61,7 @@ class NullptrMod(Screen): case 2: finput.value = self.current_file.addr case 3: finput.value = self.current_file.mime case 4: finput.value = self.current_file.ext - case 5: finput.value = self.current_file.ua + case 5: finput.value = self.current_file.ua or "" def on_input_submitted(self, message: Input.Submitted) -> None: self.query_one("#filter_container").display = False From 8a912e87449bd6e56ac8a83f25d0ca58982a81d3 Mon Sep 17 00:00:00 2001 From: polina4096 Date: Sun, 4 Jun 2023 03:00:12 +0300 Subject: [PATCH 6/6] Fix remote URL content length check off-by-one Fixes #85 --- fhost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fhost.py b/fhost.py index 3d45c4d..8201954 100755 --- a/fhost.py +++ b/fhost.py @@ -400,7 +400,7 @@ def store_url(url, addr, ua, secret: bool): if "content-length" in r.headers: l = int(r.headers["content-length"]) - if l < app.config["MAX_CONTENT_LENGTH"]: + if l <= app.config["MAX_CONTENT_LENGTH"]: def urlfile(**kwargs): return type('',(),kwargs)()