···4848 FHOST_USE_X_ACCEL_REDIRECT = True, # expect nginx by default
4949 FHOST_STORAGE_PATH = "up",
5050 FHOST_MAX_EXT_LENGTH = 9,
5151+ FHOST_SECRET_BYTES = 16,
5152 FHOST_EXT_OVERRIDE = {
5253 "audio/flac" : ".flac",
5354 "image/gif" : ".gif",
···129130 nsfw_score = db.Column(db.Float)
130131 expiration = db.Column(db.BigInteger)
131132 mgmt_token = db.Column(db.String)
133133+ secret = db.Column(db.String)
132134133135 def __init__(self, sha256, ext, mime, addr, expiration, mgmt_token):
134136 self.sha256 = sha256
···145147 n = self.getname()
146148147149 if self.nsfw_score and self.nsfw_score > app.config["NSFW_THRESHOLD"]:
148148- return url_for("get", path=n, _external=True, _anchor="nsfw") + "\n"
150150+ return url_for("get", path=n, secret=self.secret, _external=True, _anchor="nsfw") + "\n"
149151 else:
150150- return url_for("get", path=n, _external=True) + "\n"
152152+ return url_for("get", path=n, secret=self.secret, _external=True) + "\n"
151153152154 def getpath(self) -> Path:
153155 return Path(app.config["FHOST_STORAGE_PATH"]) / self.sha256
···195197 Any value greater that the longest allowed file lifespan will be rounded down to that
196198 value.
197199 """
198198- def store(file_, requested_expiration: typing.Optional[int], addr):
200200+ def store(file_, requested_expiration: typing.Optional[int], addr, secret: bool):
199201 data = file_.read()
200202 digest = sha256(data).hexdigest()
201203···260262261263 f.addr = addr
262264265265+ if isnew:
266266+ f.secret = None
267267+ if secret:
268268+ f.secret = secrets.token_urlsafe(app.config["FHOST_SECRET_BYTES"])
269269+263270 storage = Path(app.config["FHOST_STORAGE_PATH"])
264271 storage.mkdir(parents=True, exist_ok=True)
265272 p = storage / digest
···339346Any value greater that the longest allowed file lifespan will be rounded down to that
340347value.
341348"""
342342-def store_file(f, requested_expiration: typing.Optional[int], addr):
349349+def store_file(f, requested_expiration: typing.Optional[int], addr, secret: bool):
343350 if in_upload_bl(addr):
344351 return "Your host is blocked from uploading files.\n", 451
345352346346- sf, isnew = File.store(f, requested_expiration, addr)
353353+ sf, isnew = File.store(f, requested_expiration, addr, secret)
347354348355 response = make_response(sf.geturl())
349356 response.headers["X-Expires"] = sf.expiration
···353360354361 return response
355362356356-def store_url(url, addr):
363363+def store_url(url, addr, secret: bool):
357364 if is_fhost_url(url):
358365 abort(400)
359366···374381375382 f = urlfile(read=r.raw.read, content_type=r.headers["content-type"], filename="")
376383377377- return store_file(f, None, addr)
384384+ return store_file(f, None, addr, secret)
378385 else:
379386 abort(413)
380387 else:
···404411 abort(400)
405412406413@app.route("/<path:path>", methods=["GET", "POST"])
407407-def get(path):
408408- path = Path(path.split("/", 1)[0])
409409- sufs = "".join(path.suffixes[-2:])
410410- name = path.name[:-len(sufs) or None]
414414+@app.route("/s/<secret>/<path:path>", methods=["GET", "POST"])
415415+def get(path, secret=None):
416416+ p = Path(path.split("/", 1)[0])
417417+ sufs = "".join(p.suffixes[-2:])
418418+ name = p.name[:-len(sufs) or None]
411419412420 if "." in name:
413421 abort(404)
···416424417425 if sufs:
418426 f = File.query.get(id)
427427+ if f.secret != secret:
428428+ abort(404)
419429420430 if f and f.ext == sufs:
421431 if f.removed:
···443453 if request.method == "POST":
444454 abort(405)
445455456456+ if "/" in path:
457457+ abort(404)
458458+446459 u = URL.query.get(id)
447460448461 if u:
···454467def fhost():
455468 if request.method == "POST":
456469 sf = None
470470+ secret = "secret" in request.form
457471458472 if "file" in request.files:
459473 try:
···461475 return store_file(
462476 request.files["file"],
463477 int(request.form["expires"]),
464464- request.remote_addr
478478+ request.remote_addr,
479479+ secret
465480 )
466481 except ValueError:
467482 # The requested expiration date wasn't properly formed
···471486 return store_file(
472487 request.files["file"],
473488 None,
474474- request.remote_addr
489489+ request.remote_addr,
490490+ secret
475491 )
476492 elif "url" in request.form:
477477- return store_url(request.form["url"], request.remote_addr)
493493+ return store_url(
494494+ request.form["url"],
495495+ request.remote_addr,
496496+ secret
497497+ )
478498 elif "shorten" in request.form:
479499 return shorten(request.form["shorten"])
480500
+7
instance/config.example.py
···9494FHOST_MAX_EXT_LENGTH = 9
959596969797+# The number of bytes used for "secret" URLs
9898+#
9999+# When a user uploads a file with the "secret" option, 0x0 generates a string
100100+# from this many bytes of random data. It is base64-encoded, so on average
101101+# each byte results in approximately 1.3 characters.
102102+FHOST_SECRET_BYTES = 16
103103+97104# A list of filetypes to use when the uploader doesn't specify one
98105#
99106# When a user uploads a file with no file extension, we try to find an extension that
+26
migrations/versions/e2e816056589_.py
···11+"""add URL secret
22+33+Revision ID: e2e816056589
44+Revises: 0659d7b9eea8
55+Create Date: 2022-12-01 02:16:15.976864
66+77+"""
88+99+# revision identifiers, used by Alembic.
1010+revision = 'e2e816056589'
1111+down_revision = '0659d7b9eea8'
1212+1313+from alembic import op
1414+import sqlalchemy as sa
1515+1616+1717+def upgrade():
1818+ # ### commands auto generated by Alembic - please adjust! ###
1919+ op.add_column('file', sa.Column('secret', sa.String(), nullable=True))
2020+ # ### end Alembic commands ###
2121+2222+2323+def downgrade():
2424+ # ### commands auto generated by Alembic - please adjust! ###
2525+ op.drop_column('file', 'secret')
2626+ # ### end Alembic commands ###
+3
templates/index.html
···66 curl -F'file=@yourfile.png' {{ fhost_url }}
77You can also POST remote URLs:
88 curl -F'url=http://example.com/image.jpg' {{ fhost_url }}
99+If you don't want the resulting URL to be easy to guess:
1010+ curl -F'file=@yourfile.png' -Fsecret= {{ fhost_url }}
1111+ curl -F'url=http://example.com/image.jpg' -Fsecret= {{ fhost_url }}
912Or you can shorten URLs:
1013 curl -F'shorten=http://example.com/some/long/url' {{ fhost_url }}
1114