[mirror of https://git.0x0.st/mia/0x0] No-bullshit file hosting and URL shortening service https://0x0.st
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add support for “secret” file URLs

Closes #47

+70 -14
+34 -14
fhost.py
··· 48 48 FHOST_USE_X_ACCEL_REDIRECT = True, # expect nginx by default 49 49 FHOST_STORAGE_PATH = "up", 50 50 FHOST_MAX_EXT_LENGTH = 9, 51 + FHOST_SECRET_BYTES = 16, 51 52 FHOST_EXT_OVERRIDE = { 52 53 "audio/flac" : ".flac", 53 54 "image/gif" : ".gif", ··· 129 130 nsfw_score = db.Column(db.Float) 130 131 expiration = db.Column(db.BigInteger) 131 132 mgmt_token = db.Column(db.String) 133 + secret = db.Column(db.String) 132 134 133 135 def __init__(self, sha256, ext, mime, addr, expiration, mgmt_token): 134 136 self.sha256 = sha256 ··· 145 147 n = self.getname() 146 148 147 149 if self.nsfw_score and self.nsfw_score > app.config["NSFW_THRESHOLD"]: 148 - return url_for("get", path=n, _external=True, _anchor="nsfw") + "\n" 150 + return url_for("get", path=n, secret=self.secret, _external=True, _anchor="nsfw") + "\n" 149 151 else: 150 - return url_for("get", path=n, _external=True) + "\n" 152 + return url_for("get", path=n, secret=self.secret, _external=True) + "\n" 151 153 152 154 def getpath(self) -> Path: 153 155 return Path(app.config["FHOST_STORAGE_PATH"]) / self.sha256 ··· 195 197 Any value greater that the longest allowed file lifespan will be rounded down to that 196 198 value. 197 199 """ 198 - def store(file_, requested_expiration: typing.Optional[int], addr): 200 + def store(file_, requested_expiration: typing.Optional[int], addr, secret: bool): 199 201 data = file_.read() 200 202 digest = sha256(data).hexdigest() 201 203 ··· 260 262 261 263 f.addr = addr 262 264 265 + if isnew: 266 + f.secret = None 267 + if secret: 268 + f.secret = secrets.token_urlsafe(app.config["FHOST_SECRET_BYTES"]) 269 + 263 270 storage = Path(app.config["FHOST_STORAGE_PATH"]) 264 271 storage.mkdir(parents=True, exist_ok=True) 265 272 p = storage / digest ··· 339 346 Any value greater that the longest allowed file lifespan will be rounded down to that 340 347 value. 341 348 """ 342 - def store_file(f, requested_expiration: typing.Optional[int], addr): 349 + def store_file(f, requested_expiration: typing.Optional[int], addr, secret: bool): 343 350 if in_upload_bl(addr): 344 351 return "Your host is blocked from uploading files.\n", 451 345 352 346 - sf, isnew = File.store(f, requested_expiration, addr) 353 + sf, isnew = File.store(f, requested_expiration, addr, secret) 347 354 348 355 response = make_response(sf.geturl()) 349 356 response.headers["X-Expires"] = sf.expiration ··· 353 360 354 361 return response 355 362 356 - def store_url(url, addr): 363 + def store_url(url, addr, secret: bool): 357 364 if is_fhost_url(url): 358 365 abort(400) 359 366 ··· 374 381 375 382 f = urlfile(read=r.raw.read, content_type=r.headers["content-type"], filename="") 376 383 377 - return store_file(f, None, addr) 384 + return store_file(f, None, addr, secret) 378 385 else: 379 386 abort(413) 380 387 else: ··· 404 411 abort(400) 405 412 406 413 @app.route("/<path:path>", methods=["GET", "POST"]) 407 - def get(path): 408 - path = Path(path.split("/", 1)[0]) 409 - sufs = "".join(path.suffixes[-2:]) 410 - name = path.name[:-len(sufs) or None] 414 + @app.route("/s/<secret>/<path:path>", methods=["GET", "POST"]) 415 + def get(path, secret=None): 416 + p = Path(path.split("/", 1)[0]) 417 + sufs = "".join(p.suffixes[-2:]) 418 + name = p.name[:-len(sufs) or None] 411 419 412 420 if "." in name: 413 421 abort(404) ··· 416 424 417 425 if sufs: 418 426 f = File.query.get(id) 427 + if f.secret != secret: 428 + abort(404) 419 429 420 430 if f and f.ext == sufs: 421 431 if f.removed: ··· 443 453 if request.method == "POST": 444 454 abort(405) 445 455 456 + if "/" in path: 457 + abort(404) 458 + 446 459 u = URL.query.get(id) 447 460 448 461 if u: ··· 454 467 def fhost(): 455 468 if request.method == "POST": 456 469 sf = None 470 + secret = "secret" in request.form 457 471 458 472 if "file" in request.files: 459 473 try: ··· 461 475 return store_file( 462 476 request.files["file"], 463 477 int(request.form["expires"]), 464 - request.remote_addr 478 + request.remote_addr, 479 + secret 465 480 ) 466 481 except ValueError: 467 482 # The requested expiration date wasn't properly formed ··· 471 486 return store_file( 472 487 request.files["file"], 473 488 None, 474 - request.remote_addr 489 + request.remote_addr, 490 + secret 475 491 ) 476 492 elif "url" in request.form: 477 - return store_url(request.form["url"], request.remote_addr) 493 + return store_url( 494 + request.form["url"], 495 + request.remote_addr, 496 + secret 497 + ) 478 498 elif "shorten" in request.form: 479 499 return shorten(request.form["shorten"]) 480 500
+7
instance/config.example.py
··· 94 94 FHOST_MAX_EXT_LENGTH = 9 95 95 96 96 97 + # The number of bytes used for "secret" URLs 98 + # 99 + # When a user uploads a file with the "secret" option, 0x0 generates a string 100 + # from this many bytes of random data. It is base64-encoded, so on average 101 + # each byte results in approximately 1.3 characters. 102 + FHOST_SECRET_BYTES = 16 103 + 97 104 # A list of filetypes to use when the uploader doesn't specify one 98 105 # 99 106 # When a user uploads a file with no file extension, we try to find an extension that
+26
migrations/versions/e2e816056589_.py
··· 1 + """add URL secret 2 + 3 + Revision ID: e2e816056589 4 + Revises: 0659d7b9eea8 5 + Create Date: 2022-12-01 02:16:15.976864 6 + 7 + """ 8 + 9 + # revision identifiers, used by Alembic. 10 + revision = 'e2e816056589' 11 + down_revision = '0659d7b9eea8' 12 + 13 + from alembic import op 14 + import sqlalchemy as sa 15 + 16 + 17 + def upgrade(): 18 + # ### commands auto generated by Alembic - please adjust! ### 19 + op.add_column('file', sa.Column('secret', sa.String(), nullable=True)) 20 + # ### end Alembic commands ### 21 + 22 + 23 + def downgrade(): 24 + # ### commands auto generated by Alembic - please adjust! ### 25 + op.drop_column('file', 'secret') 26 + # ### end Alembic commands ###
+3
templates/index.html
··· 6 6 curl -F'file=@yourfile.png' {{ fhost_url }} 7 7 You can also POST remote URLs: 8 8 curl -F'url=http://example.com/image.jpg' {{ fhost_url }} 9 + If you don't want the resulting URL to be easy to guess: 10 + curl -F'file=@yourfile.png' -Fsecret= {{ fhost_url }} 11 + curl -F'url=http://example.com/image.jpg' -Fsecret= {{ fhost_url }} 9 12 Or you can shorten URLs: 10 13 curl -F'shorten=http://example.com/some/long/url' {{ fhost_url }} 11 14