diff --git a/requirements.txt b/requirements.txt index 524bed4..27a07ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ bottle==0.12.18 Beaker==1.11.0 Paste==3.4.3 appdirs==1.4.4 +bleach==3.1.5 diff --git a/zerobin/paste.py b/zerobin/paste.py index 33aecd9..0bf3c5f 100644 --- a/zerobin/paste.py +++ b/zerobin/paste.py @@ -6,9 +6,12 @@ import os import hashlib import base64 import lockfile +import json from datetime import datetime, timedelta +import bleach + from zerobin.utils import settings, to_ascii, as_unicode, safe_open as open @@ -26,10 +29,13 @@ class Paste(object): "never": 365 * 24 * 3600 * 100, } - def __init__(self, uuid=None, uuid_length=None, content=None, expiration=None): + def __init__( + self, uuid=None, uuid_length=None, content=None, expiration=None, title="" + ): self.content = content self.expiration = self.get_expiration(expiration) + self.title = bleach.clean(title, strip=True)[:60] if not uuid: # generate the uuid from the decoded content by hashing it @@ -97,6 +103,10 @@ class Paste(object): uuid = os.path.basename(path) expiration = next(paste).strip() content = next(paste).strip() + try: + metadata = json.loads(next(paste).strip()) + except (StopIteration, json.decoder.JSONDecodeError): + metadata = {} if "burn_after_reading" not in expiration: expiration = datetime.strptime(expiration, "%Y-%m-%d %H:%M:%S.%f") @@ -105,7 +115,12 @@ class Paste(object): except (IOError, OSError): raise ValueError("Can not open paste from file %s" % path) - return Paste(uuid=uuid, expiration=expiration, content=content) + return Paste( + uuid=uuid, + expiration=expiration, + content=content, + title=" ".join(metadata.get("title", "").split()), + ) @classmethod def load(cls, uuid): @@ -123,9 +138,8 @@ class Paste(object): """ path = settings.PASTE_FILES_ROOT counter_file = os.path.join(path, "counter") - lock = lockfile.LockFile(counter_file) - with lock: + with lockfile.LockFile(counter_file): # Read the value from the counter try: with open(counter_file, "r") as fcounter: @@ -176,6 +190,8 @@ class Paste(object): with open(self.path, "w") as f: f.write(expiration + "\n") f.write(self.content + "\n") + if self.title: + f.write(json.dumps({"title": self.title}) + "\n") return self diff --git a/zerobin/routes.py b/zerobin/routes.py index df83018..53d9a74 100644 --- a/zerobin/routes.py +++ b/zerobin/routes.py @@ -3,6 +3,7 @@ """ import os +import pdb import sys import _thread as thread @@ -87,7 +88,7 @@ def admin(): return {"status": "ok", "message": "Paste deleted", **GLOBAL_CONTEXT} - return {"status": "ok", "message": "" ** GLOBAL_CONTEXT} + return {"status": "ok", "message": "", **GLOBAL_CONTEXT} @app.get(ADMIN_LOGIN_URL) @@ -121,55 +122,37 @@ def logout(): @app.post("/paste/create") def create_paste(): - try: - body = parse_qs(request.body.read(int(settings.MAX_SIZE * 1.1))) - except ValueError: + # Reject what is too small, too big, or what does not seem encrypted to + # limit a abuses + content = request.forms.get("content", "") + if '{"iv":' not in content or not (0 < len(content) < settings.MAX_SIZE): return {"status": "error", "message": "Wrong data payload."} - try: - content = "".join(x.decode("utf8") for x in body[b"content"]) - except (UnicodeDecodeError, KeyError): - return { - "status": "error", - "message": "Encoding error: the paste couldn't be saved.", - } + expiration = request.forms.get("expiration", "burn_after_reading") + title = request.forms.get("title", "") - if '{"iv":' not in content: # reject silently non encrypted content - return {"status": "error", "message": "Wrong data payload."} + paste = Paste( + expiration=expiration, + content=content, + uuid_length=settings.PASTE_ID_LENGTH, + title=title, + ) + paste.save() - # check size of the paste. if more than settings return error - # without saving paste. prevent from unusual use of the - # system. need to be improved - if 0 < len(content) < settings.MAX_SIZE: - expiration = body.get(b"expiration", [b"burn_after_reading"])[0] - paste = Paste( - expiration=expiration.decode("utf8"), - content=content, - uuid_length=settings.PASTE_ID_LENGTH, + # If refresh time elapsed pick up, update the counter + if settings.DISPLAY_COUNTER: + + paste.increment_counter() + + now = datetime.now() + timeout = GLOBAL_CONTEXT["refresh_counter"] + timedelta( + seconds=settings.REFRESH_COUNTER ) - paste.save() + if timeout < now: + GLOBAL_CONTEXT["pastes_count"] = Paste.get_pastes_count() + GLOBAL_CONTEXT["refresh_counter"] = now - # display counter - if settings.DISPLAY_COUNTER: - - # increment paste counter - paste.increment_counter() - - # if refresh time elapsed pick up new counter value - now = datetime.now() - timeout = GLOBAL_CONTEXT["refresh_counter"] + timedelta( - seconds=settings.REFRESH_COUNTER - ) - if timeout < now: - GLOBAL_CONTEXT["pastes_count"] = Paste.get_pastes_count() - GLOBAL_CONTEXT["refresh_counter"] = now - - return {"status": "ok", "paste": paste.uuid, "owner_key": paste.owner_key} - - return { - "status": "error", - "message": "Serveur error: the paste couldn't be saved. " "Please try later.", - } + return {"status": "ok", "paste": paste.uuid, "owner_key": paste.owner_key} @app.get("/paste/:paste_id") diff --git a/zerobin/static/js/behavior.js b/zerobin/static/js/behavior.js index 19559a2..ec7614d 100644 --- a/zerobin/static/js/behavior.js +++ b/zerobin/static/js/behavior.js @@ -33,10 +33,12 @@ const app = new Vue({ type: '', content: '', downloadLink: {}, + title: '', }, newPaste: { expiration: '1_day', content: '', + title: '', }, messages: [], /** Check for browser support of the named featured. Store the result @@ -112,18 +114,21 @@ const app = new Vue({ window.location.reload(); }, - handleClone: () => { + handleClone: function () { document.querySelector('.submit-form').style.display = "inherit"; document.querySelector('.paste-form').style.display = "none"; + document.querySelector('h1').style.display = "none"; let content = document.getElementById('content'); content.value = zerobin.getPasteContent(); content.dispatchEvent(new Event('change')); + this.newPaste.title = this.currentPaste.title; }, handleCancelClone: () => { document.querySelector('.submit-form').style.display = "none"; document.querySelector('.paste-form').style.display = "inherit"; + document.querySelector('h1').style.display = "inherit"; }, handleUpload: (files) => { @@ -200,7 +205,7 @@ const app = new Vue({ newly created paste, adding the key in the hash. */ - encryptAndSendPaste: (e) => { + encryptAndSendPaste: () => { var paste = document.querySelector('textarea').value; @@ -235,7 +240,8 @@ const app = new Vue({ bar.set('Sending...', '95%'); var data = { content: content, - expiration: app.newPaste.expiration + expiration: app.newPaste.expiration, + title: app.newPaste.title }; var sizebytes = zerobin.count(JSON.stringify(data)); var oversized = sizebytes > zerobin.max_size; // 100kb - the others header information @@ -856,6 +862,11 @@ window.onload = function () { }) } + let title = document.querySelector('h1'); + if (title) { + app.currentPaste.title = title.innerText; + } + } /* Display previous pastes */ diff --git a/zerobin/utils.py b/zerobin/utils.py index 0f431d9..02c3bb2 100644 --- a/zerobin/utils.py +++ b/zerobin/utils.py @@ -1,7 +1,3 @@ -import time -import os -import glob -import tempfile import codecs import unicodedata import hashlib @@ -135,6 +131,8 @@ def ensure_app_context(data_dir=None, config_dir=None): bottle.TEMPLATE_PATH.insert(0, CUSTOM_VIEWS_DIR) + bottle.BaseRequest.MEMFILE_MAX = settings.MAX_SIZE + (1024 * 100) + secret_key_file = settings.CONFIG_DIR / "secret_key" if not secret_key_file.is_file(): secret_key_file.write_text(secrets.token_urlsafe(64)) diff --git a/zerobin/views/base.tpl b/zerobin/views/base.tpl index 0d73490..69894cb 100644 --- a/zerobin/views/base.tpl +++ b/zerobin/views/base.tpl @@ -67,6 +67,11 @@
× diff --git a/zerobin/views/home.tpl b/zerobin/views/home.tpl index c944d88..bd2b911 100644 --- a/zerobin/views/home.tpl +++ b/zerobin/views/home.tpl @@ -32,6 +32,9 @@