Compare commits

...

7 commits

Author SHA1 Message Date
7d7bcd045e Merge branch 'master' into boop 2023-06-04 09:02:13 +03:00
polina4096
8a912e8744
Fix remote URL content length check off-by-one
Fixes #85
2023-06-04 06:35:11 +02:00
Mia Herkt
c2b5e95903
ModUI: Handle opening filter panel with NULL user agent 2023-03-29 07:49:56 +02:00
Mia Herkt
c189c47306
ModUI: Allow LIKE matching for address filtering 2023-03-29 07:38:36 +02:00
Mia Herkt
3d1facaec3
Store user agent with files
Needed for moderation.
2023-03-29 07:36:49 +02:00
Mia Herkt
e00866f5e4
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.
2023-03-29 07:19:47 +02:00
jonas-w
3950f6e8eb
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
2023-01-15 20:36:39 +01:00
3 changed files with 53 additions and 10 deletions

View file

@ -107,6 +107,7 @@ db = SQLAlchemy(app)
migrate = Migrate(app, db) migrate = Migrate(app, db)
class URL(db.Model): class URL(db.Model):
__tablename__ = "URL"
id = db.Column(db.Integer, primary_key = True) id = db.Column(db.Integer, primary_key = True)
url = db.Column(db.UnicodeText, unique = True) url = db.Column(db.UnicodeText, unique = True)
@ -135,6 +136,7 @@ class File(db.Model):
ext = db.Column(db.UnicodeText) ext = db.Column(db.UnicodeText)
mime = db.Column(db.UnicodeText) mime = db.Column(db.UnicodeText)
addr = db.Column(db.UnicodeText) addr = db.Column(db.UnicodeText)
ua = db.Column(db.UnicodeText)
removed = db.Column(db.Boolean, default=False) removed = db.Column(db.Boolean, default=False)
nsfw_score = db.Column(db.Float) nsfw_score = db.Column(db.Float)
expiration = db.Column(db.BigInteger) expiration = db.Column(db.BigInteger)
@ -143,11 +145,12 @@ class File(db.Model):
last_vscan = db.Column(db.DateTime) last_vscan = db.Column(db.DateTime)
size = db.Column(db.BigInteger) 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.sha256 = sha256
self.ext = ext self.ext = ext
self.mime = mime self.mime = mime
self.addr = addr self.addr = addr
self.ua = ua
self.expiration = expiration self.expiration = expiration
self.mgmt_token = mgmt_token self.mgmt_token = mgmt_token
@ -212,7 +215,7 @@ class File(db.Model):
Any value greater that the longest allowed file lifespan will be rounded down to that Any value greater that the longest allowed file lifespan will be rounded down to that
value. 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() data = file_.read()
digest = sha256(data).hexdigest() digest = sha256(data).hexdigest()
@ -248,8 +251,10 @@ class File(db.Model):
if not ext: if not ext:
if gmime in app.config["FHOST_EXT_OVERRIDE"]: if gmime in app.config["FHOST_EXT_OVERRIDE"]:
ext = app.config["FHOST_EXT_OVERRIDE"][gmime] ext = app.config["FHOST_EXT_OVERRIDE"][gmime]
elif guess:
ext = guess
else: else:
ext = guess_extension(gmime) ext = ""
return ext[:app.config["FHOST_MAX_EXT_LENGTH"]] or ".bin" return ext[:app.config["FHOST_MAX_EXT_LENGTH"]] or ".bin"
@ -276,9 +281,10 @@ class File(db.Model):
mime = get_mime() mime = get_mime()
ext = get_ext(mime) ext = get_ext(mime)
mgmt_token = secrets.token_urlsafe() 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.addr = addr
f.ua = ua
if isnew: if isnew:
f.secret = None f.secret = None
@ -366,11 +372,11 @@ requested_expiration can be:
Any value greater that the longest allowed file lifespan will be rounded down to that Any value greater that the longest allowed file lifespan will be rounded down to that
value. 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): if in_upload_bl(addr):
return "Your host is blocked from uploading files.\n", 451 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 = make_response(sf.geturl())
response.headers["X-Expires"] = sf.expiration response.headers["X-Expires"] = sf.expiration
@ -380,7 +386,7 @@ def store_file(f, requested_expiration: typing.Optional[int], addr, secret: boo
return response return response
def store_url(url, addr, secret: bool): def store_url(url, addr, ua, secret: bool):
if is_fhost_url(url): if is_fhost_url(url):
abort(400) abort(400)
@ -395,13 +401,13 @@ def store_url(url, addr, secret: bool):
if "content-length" in r.headers: if "content-length" in r.headers:
l = int(r.headers["content-length"]) l = int(r.headers["content-length"])
if l < app.config["MAX_CONTENT_LENGTH"]: if l <= app.config["MAX_CONTENT_LENGTH"]:
def urlfile(**kwargs): def urlfile(**kwargs):
return type('',(),kwargs)() return type('',(),kwargs)()
f = urlfile(read=r.raw.read, content_type=r.headers["content-type"], filename="") 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: else:
abort(413) abort(413)
else: else:
@ -496,6 +502,7 @@ def fhost():
request.files["file"], request.files["file"],
int(request.form["expires"]), int(request.form["expires"]),
request.remote_addr, request.remote_addr,
request.user_agent.string,
secret secret
) )
except ValueError: except ValueError:
@ -507,12 +514,14 @@ def fhost():
request.files["file"], request.files["file"],
None, None,
request.remote_addr, request.remote_addr,
request.user_agent.string,
secret secret
) )
elif "url" in request.form: elif "url" in request.form:
return store_url( return store_url(
request.form["url"], request.form["url"],
request.remote_addr, request.remote_addr,
request.user_agent.string,
secret secret
) )
elif "shorten" in request.form: elif "shorten" in request.form:

View file

@ -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 ###

6
mod.py
View file

@ -26,6 +26,7 @@ class NullptrMod(Screen):
("f4", "filter(4, 'Filter extension:')", "Filter Ext."), ("f4", "filter(4, 'Filter extension:')", "Filter Ext."),
("f5", "refresh", "Refresh"), ("f5", "refresh", "Refresh"),
("f6", "filter_clear", "Clear filter"), ("f6", "filter_clear", "Clear filter"),
("f7", "filter(5, 'Filter user agent:')", "Filter UA"),
("r", "remove_file(False)", "Remove file"), ("r", "remove_file(False)", "Remove file"),
("ctrl+r", "remove_file(True)", "Ban file"), ("ctrl+r", "remove_file(True)", "Ban file"),
("p", "ban_ip(False)", "Ban IP"), ("p", "ban_ip(False)", "Ban IP"),
@ -60,6 +61,7 @@ class NullptrMod(Screen):
case 2: finput.value = self.current_file.addr case 2: finput.value = self.current_file.addr
case 3: finput.value = self.current_file.mime case 3: finput.value = self.current_file.mime
case 4: finput.value = self.current_file.ext case 4: finput.value = self.current_file.ext
case 5: finput.value = self.current_file.ua or ""
def on_input_submitted(self, message: Input.Submitted) -> None: def on_input_submitted(self, message: Input.Submitted) -> None:
self.query_one("#filter_container").display = False self.query_one("#filter_container").display = False
@ -71,9 +73,10 @@ class NullptrMod(Screen):
case 1: case 1:
try: ftable.query = ftable.base_query.filter(File.id == su.debase(message.value)) try: ftable.query = ftable.base_query.filter(File.id == su.debase(message.value))
except ValueError: pass 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 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 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: else:
ftable.query = ftable.base_query ftable.query = ftable.base_query
@ -249,6 +252,7 @@ class NullptrMod(Screen):
("MIME type:", f.mime), ("MIME type:", f.mime),
("SHA256 checksum:", f.sha256), ("SHA256 checksum:", f.sha256),
("Uploaded by:", Text(f.addr)), ("Uploaded by:", Text(f.addr)),
("User agent:", Text(f.ua or "")),
("Management token:", f.mgmt_token), ("Management token:", f.mgmt_token),
("Secret:", f.secret), ("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)")), ("Is NSFW:", ("Yes" if f.is_nsfw else "No") + (f" (Score: {f.nsfw_score:0.4f})" if f.nsfw_score else " (Not scanned)")),