summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMia Herkt <mia@0x0.st>2022-11-30 01:42:49 +0100
committerMia Herkt <mia@0x0.st>2022-11-30 02:19:29 +0100
commita182b6199b7f55eeb573dccd6ce7926c40043680 (patch)
tree3c4faa600e40ab15befd731a085630c5b579dcf0
parenteb0b1d2f6992740e3b09bf383d8c0fa442faca55 (diff)
Allow management operations like deleting files
This introduces the X-Token header field in the response of newly uploaded files as a simple way for users to manage their own files. It does not need to be particularly secure.
-rwxr-xr-xfhost.py60
-rw-r--r--migrations/versions/0659d7b9eea8_.py26
-rw-r--r--templates/401.html2
-rw-r--r--templates/index.html5
4 files changed, 84 insertions, 9 deletions
diff --git a/fhost.py b/fhost.py
index bda680f..c771d3e 100755
--- a/fhost.py
+++ b/fhost.py
@@ -34,6 +34,7 @@ import sys
import time
import typing
import requests
+import secrets
from validators import url as url_valid
from pathlib import Path
@@ -127,13 +128,15 @@ class File(db.Model):
removed = db.Column(db.Boolean, default=False)
nsfw_score = db.Column(db.Float)
expiration = db.Column(db.BigInteger)
+ mgmt_token = db.Column(db.String)
- def __init__(self, sha256, ext, mime, addr, expiration):
+ def __init__(self, sha256, ext, mime, addr, expiration, mgmt_token):
self.sha256 = sha256
self.ext = ext
self.mime = mime
self.addr = addr
self.expiration = expiration
+ self.mgmt_token = mgmt_token
def getname(self):
return u"{0}{1}".format(su.enbase(self.id), self.ext)
@@ -146,6 +149,15 @@ class File(db.Model):
else:
return url_for("get", path=n, _external=True) + "\n"
+ def getpath(self) -> Path:
+ return Path(app.config["FHOST_STORAGE_PATH"]) / self.sha256
+
+ def delete(self, permanent=False):
+ self.expiration = None
+ self.mgmt_token = None
+ self.removed = permanent
+ self.getpath().unlink(missing_ok=True)
+
"""
requested_expiration can be:
- None, to use the longest allowed file lifespan
@@ -218,6 +230,7 @@ class File(db.Model):
else:
# Treat the requested expiration time as a timestamp in epoch millis
return min(this_files_max_expiration, requested_expiration);
+ isnew = True
f = File.query.filter_by(sha256=digest).first()
if f:
@@ -228,14 +241,19 @@ class File(db.Model):
if f.expiration is None:
# The file has expired, so give it a new expiration date
f.expiration = get_expiration()
+
+ # Also generate a new management token
+ f.mgmt_token = secrets.token_urlsafe()
else:
# The file already exists, update the expiration if needed
f.expiration = max(f.expiration, get_expiration())
+ isnew = False
else:
mime = get_mime()
ext = get_ext(mime)
expiration = get_expiration()
- f = File(digest, ext, mime, addr, expiration)
+ mgmt_token = secrets.token_urlsafe()
+ f = File(digest, ext, mime, addr, expiration, mgmt_token)
f.addr = addr
@@ -252,8 +270,7 @@ class File(db.Model):
db.session.add(f)
db.session.commit()
- return f
-
+ return f, isnew
class UrlEncoder(object):
@@ -323,9 +340,14 @@ def store_file(f, requested_expiration: typing.Optional[int], addr):
if in_upload_bl(addr):
return "Your host is blocked from uploading files.\n", 451
- sf = File.store(f, requested_expiration, addr)
+ sf, isnew = File.store(f, requested_expiration, addr)
- return sf.geturl()
+ response = make_response(sf.geturl())
+
+ if isnew:
+ response.headers["X-Token"] = sf.mgmt_token
+
+ return response
def store_url(url, addr):
if is_fhost_url(url):
@@ -354,7 +376,20 @@ def store_url(url, addr):
else:
abort(411)
-@app.route("/<path:path>")
+def manage_file(f):
+ try:
+ assert(request.form["token"] == f.mgmt_token)
+ except:
+ abort(401)
+
+ if "delete" in request.form:
+ f.delete()
+ db.session.commit()
+ return ""
+
+ abort(400)
+
+@app.route("/<path:path>", methods=["GET", "POST"])
def get(path):
path = Path(path.split("/", 1)[0])
sufs = "".join(path.suffixes[-2:])
@@ -368,11 +403,14 @@ def get(path):
if f.removed:
abort(451)
- fpath = Path(app.config["FHOST_STORAGE_PATH"]) / f.sha256
+ fpath = f.getpath()
if not fpath.is_file():
abort(404)
+ if request.method == "POST":
+ return manage_file(f)
+
if app.config["FHOST_USE_X_ACCEL_REDIRECT"]:
response = make_response()
response.headers["Content-Type"] = f.mime
@@ -382,6 +420,9 @@ def get(path):
else:
return send_from_directory(app.config["FHOST_STORAGE_PATH"], f.sha256, mimetype = f.mime)
else:
+ if request.method == "POST":
+ abort(405)
+
u = URL.query.get(id)
if u:
@@ -428,6 +469,7 @@ Disallow: /
"""
@app.errorhandler(400)
+@app.errorhandler(401)
@app.errorhandler(404)
@app.errorhandler(411)
@app.errorhandler(413)
@@ -436,7 +478,7 @@ Disallow: /
@app.errorhandler(451)
def ehandler(e):
try:
- return render_template(f"{e.code}.html", id=id), e.code
+ return render_template(f"{e.code}.html", id=id, request=request), e.code
except TemplateNotFound:
return "Segmentation fault\n", e.code
diff --git a/migrations/versions/0659d7b9eea8_.py b/migrations/versions/0659d7b9eea8_.py
new file mode 100644
index 0000000..2ef2151
--- /dev/null
+++ b/migrations/versions/0659d7b9eea8_.py
@@ -0,0 +1,26 @@
+"""add file management token
+
+Revision ID: 0659d7b9eea8
+Revises: 939a08e1d6e5
+Create Date: 2022-11-30 01:06:53.362973
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '0659d7b9eea8'
+down_revision = '939a08e1d6e5'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.add_column('file', sa.Column('mgmt_token', sa.String(), nullable=True))
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_column('file', 'mgmt_token')
+ # ### end Alembic commands ###
diff --git a/templates/401.html b/templates/401.html
new file mode 100644
index 0000000..672c7e4
--- /dev/null
+++ b/templates/401.html
@@ -0,0 +1,2 @@
+rm: cannot remove '{{ request.path.split("/")[1] }}': Permission denied
+
diff --git a/templates/index.html b/templates/index.html
index 6f84f59..f315daf 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -20,6 +20,11 @@ OR by setting "expires" to a timestamp in epoch milliseconds
Expired files won't be removed immediately, but will be removed as part of
the next purge.
+Whenever a file that does not already exist or has expired is uploaded,
+the HTTP response header includes an X-Token field. You can use this
+to delete the file immediately:
+ curl -Ftoken=token_here -Fdelete= {{ fhost_url }}/abc.txt
+
{% set max_size = config["MAX_CONTENT_LENGTH"]|filesizeformat(True) %}
Maximum file size: {{ max_size }}
Not allowed: {{ config["FHOST_MIME_BLACKLIST"]|join(", ") }}