parent
ed84d3752c
commit
0b80a62f80
48
fhost.py
48
fhost.py
@ -48,6 +48,7 @@ app.config.update(
|
|||||||
FHOST_USE_X_ACCEL_REDIRECT = True, # expect nginx by default
|
FHOST_USE_X_ACCEL_REDIRECT = True, # expect nginx by default
|
||||||
FHOST_STORAGE_PATH = "up",
|
FHOST_STORAGE_PATH = "up",
|
||||||
FHOST_MAX_EXT_LENGTH = 9,
|
FHOST_MAX_EXT_LENGTH = 9,
|
||||||
|
FHOST_SECRET_BYTES = 16,
|
||||||
FHOST_EXT_OVERRIDE = {
|
FHOST_EXT_OVERRIDE = {
|
||||||
"audio/flac" : ".flac",
|
"audio/flac" : ".flac",
|
||||||
"image/gif" : ".gif",
|
"image/gif" : ".gif",
|
||||||
@ -129,6 +130,7 @@ class File(db.Model):
|
|||||||
nsfw_score = db.Column(db.Float)
|
nsfw_score = db.Column(db.Float)
|
||||||
expiration = db.Column(db.BigInteger)
|
expiration = db.Column(db.BigInteger)
|
||||||
mgmt_token = db.Column(db.String)
|
mgmt_token = db.Column(db.String)
|
||||||
|
secret = db.Column(db.String)
|
||||||
|
|
||||||
def __init__(self, sha256, ext, mime, addr, expiration, mgmt_token):
|
def __init__(self, sha256, ext, mime, addr, expiration, mgmt_token):
|
||||||
self.sha256 = sha256
|
self.sha256 = sha256
|
||||||
@ -145,9 +147,9 @@ class File(db.Model):
|
|||||||
n = self.getname()
|
n = self.getname()
|
||||||
|
|
||||||
if self.nsfw_score and self.nsfw_score > app.config["NSFW_THRESHOLD"]:
|
if self.nsfw_score and self.nsfw_score > app.config["NSFW_THRESHOLD"]:
|
||||||
return url_for("get", path=n, _external=True, _anchor="nsfw") + "\n"
|
return url_for("get", path=n, secret=self.secret, _external=True, _anchor="nsfw") + "\n"
|
||||||
else:
|
else:
|
||||||
return url_for("get", path=n, _external=True) + "\n"
|
return url_for("get", path=n, secret=self.secret, _external=True) + "\n"
|
||||||
|
|
||||||
def getpath(self) -> Path:
|
def getpath(self) -> Path:
|
||||||
return Path(app.config["FHOST_STORAGE_PATH"]) / self.sha256
|
return Path(app.config["FHOST_STORAGE_PATH"]) / self.sha256
|
||||||
@ -195,7 +197,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):
|
def store(file_, requested_expiration: typing.Optional[int], addr, secret: bool):
|
||||||
data = file_.read()
|
data = file_.read()
|
||||||
digest = sha256(data).hexdigest()
|
digest = sha256(data).hexdigest()
|
||||||
|
|
||||||
@ -260,6 +262,11 @@ class File(db.Model):
|
|||||||
|
|
||||||
f.addr = addr
|
f.addr = addr
|
||||||
|
|
||||||
|
if isnew:
|
||||||
|
f.secret = None
|
||||||
|
if secret:
|
||||||
|
f.secret = secrets.token_urlsafe(app.config["FHOST_SECRET_BYTES"])
|
||||||
|
|
||||||
storage = Path(app.config["FHOST_STORAGE_PATH"])
|
storage = Path(app.config["FHOST_STORAGE_PATH"])
|
||||||
storage.mkdir(parents=True, exist_ok=True)
|
storage.mkdir(parents=True, exist_ok=True)
|
||||||
p = storage / digest
|
p = storage / digest
|
||||||
@ -339,11 +346,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):
|
def store_file(f, requested_expiration: typing.Optional[int], addr, 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)
|
sf, isnew = File.store(f, requested_expiration, addr, secret)
|
||||||
|
|
||||||
response = make_response(sf.geturl())
|
response = make_response(sf.geturl())
|
||||||
response.headers["X-Expires"] = sf.expiration
|
response.headers["X-Expires"] = sf.expiration
|
||||||
@ -353,7 +360,7 @@ def store_file(f, requested_expiration: typing.Optional[int], addr):
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def store_url(url, addr):
|
def store_url(url, addr, secret: bool):
|
||||||
if is_fhost_url(url):
|
if is_fhost_url(url):
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
@ -374,7 +381,7 @@ def store_url(url, addr):
|
|||||||
|
|
||||||
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)
|
return store_file(f, None, addr, secret)
|
||||||
else:
|
else:
|
||||||
abort(413)
|
abort(413)
|
||||||
else:
|
else:
|
||||||
@ -404,10 +411,11 @@ def manage_file(f):
|
|||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
@app.route("/<path:path>", methods=["GET", "POST"])
|
@app.route("/<path:path>", methods=["GET", "POST"])
|
||||||
def get(path):
|
@app.route("/s/<secret>/<path:path>", methods=["GET", "POST"])
|
||||||
path = Path(path.split("/", 1)[0])
|
def get(path, secret=None):
|
||||||
sufs = "".join(path.suffixes[-2:])
|
p = Path(path.split("/", 1)[0])
|
||||||
name = path.name[:-len(sufs) or None]
|
sufs = "".join(p.suffixes[-2:])
|
||||||
|
name = p.name[:-len(sufs) or None]
|
||||||
|
|
||||||
if "." in name:
|
if "." in name:
|
||||||
abort(404)
|
abort(404)
|
||||||
@ -416,6 +424,8 @@ def get(path):
|
|||||||
|
|
||||||
if sufs:
|
if sufs:
|
||||||
f = File.query.get(id)
|
f = File.query.get(id)
|
||||||
|
if f.secret != secret:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
if f and f.ext == sufs:
|
if f and f.ext == sufs:
|
||||||
if f.removed:
|
if f.removed:
|
||||||
@ -443,6 +453,9 @@ def get(path):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
abort(405)
|
abort(405)
|
||||||
|
|
||||||
|
if "/" in path:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
u = URL.query.get(id)
|
u = URL.query.get(id)
|
||||||
|
|
||||||
if u:
|
if u:
|
||||||
@ -454,6 +467,7 @@ def get(path):
|
|||||||
def fhost():
|
def fhost():
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
sf = None
|
sf = None
|
||||||
|
secret = "secret" in request.form
|
||||||
|
|
||||||
if "file" in request.files:
|
if "file" in request.files:
|
||||||
try:
|
try:
|
||||||
@ -461,7 +475,8 @@ def fhost():
|
|||||||
return store_file(
|
return store_file(
|
||||||
request.files["file"],
|
request.files["file"],
|
||||||
int(request.form["expires"]),
|
int(request.form["expires"]),
|
||||||
request.remote_addr
|
request.remote_addr,
|
||||||
|
secret
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# The requested expiration date wasn't properly formed
|
# The requested expiration date wasn't properly formed
|
||||||
@ -471,10 +486,15 @@ def fhost():
|
|||||||
return store_file(
|
return store_file(
|
||||||
request.files["file"],
|
request.files["file"],
|
||||||
None,
|
None,
|
||||||
request.remote_addr
|
request.remote_addr,
|
||||||
|
secret
|
||||||
)
|
)
|
||||||
elif "url" in request.form:
|
elif "url" in request.form:
|
||||||
return store_url(request.form["url"], request.remote_addr)
|
return store_url(
|
||||||
|
request.form["url"],
|
||||||
|
request.remote_addr,
|
||||||
|
secret
|
||||||
|
)
|
||||||
elif "shorten" in request.form:
|
elif "shorten" in request.form:
|
||||||
return shorten(request.form["shorten"])
|
return shorten(request.form["shorten"])
|
||||||
|
|
||||||
|
@ -94,6 +94,13 @@ FHOST_STORAGE_PATH = "up"
|
|||||||
FHOST_MAX_EXT_LENGTH = 9
|
FHOST_MAX_EXT_LENGTH = 9
|
||||||
|
|
||||||
|
|
||||||
|
# The number of bytes used for "secret" URLs
|
||||||
|
#
|
||||||
|
# When a user uploads a file with the "secret" option, 0x0 generates a string
|
||||||
|
# from this many bytes of random data. It is base64-encoded, so on average
|
||||||
|
# each byte results in approximately 1.3 characters.
|
||||||
|
FHOST_SECRET_BYTES = 16
|
||||||
|
|
||||||
# A list of filetypes to use when the uploader doesn't specify one
|
# A list of filetypes to use when the uploader doesn't specify one
|
||||||
#
|
#
|
||||||
# When a user uploads a file with no file extension, we try to find an extension that
|
# When a user uploads a file with no file extension, we try to find an extension that
|
||||||
|
26
migrations/versions/e2e816056589_.py
Normal file
26
migrations/versions/e2e816056589_.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""add URL secret
|
||||||
|
|
||||||
|
Revision ID: e2e816056589
|
||||||
|
Revises: 0659d7b9eea8
|
||||||
|
Create Date: 2022-12-01 02:16:15.976864
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'e2e816056589'
|
||||||
|
down_revision = '0659d7b9eea8'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('file', sa.Column('secret', sa.String(), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('file', 'secret')
|
||||||
|
# ### end Alembic commands ###
|
@ -6,6 +6,9 @@ HTTP POST files here:
|
|||||||
curl -F'file=@yourfile.png' {{ fhost_url }}
|
curl -F'file=@yourfile.png' {{ fhost_url }}
|
||||||
You can also POST remote URLs:
|
You can also POST remote URLs:
|
||||||
curl -F'url=http://example.com/image.jpg' {{ fhost_url }}
|
curl -F'url=http://example.com/image.jpg' {{ fhost_url }}
|
||||||
|
If you don't want the resulting URL to be easy to guess:
|
||||||
|
curl -F'file=@yourfile.png' -Fsecret= {{ fhost_url }}
|
||||||
|
curl -F'url=http://example.com/image.jpg' -Fsecret= {{ fhost_url }}
|
||||||
Or you can shorten URLs:
|
Or you can shorten URLs:
|
||||||
curl -F'shorten=http://example.com/some/long/url' {{ fhost_url }}
|
curl -F'shorten=http://example.com/some/long/url' {{ fhost_url }}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user