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.
This commit is contained in:
parent
eb0b1d2f69
commit
a182b6199b
4 changed files with 84 additions and 9 deletions
60
fhost.py
60
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
|
||||
|
||||
|
|
26
migrations/versions/0659d7b9eea8_.py
Normal file
26
migrations/versions/0659d7b9eea8_.py
Normal file
|
@ -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 ###
|
2
templates/401.html
Normal file
2
templates/401.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
rm: cannot remove '{{ request.path.split("/")[1] }}': Permission denied
|
||||
|
|
@ -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(", ") }}
|
||||
|
|
Loading…
Reference in a new issue