From 7e68faf27ff76e60ad3b80317732684abb164197 Mon Sep 17 00:00:00 2001 From: Cesura Date: Fri, 16 Apr 2021 18:26:51 +0300 Subject: [PATCH] Add /json endpoint and fix pasting via curl to /paste --- app.py | 3 +- pastey/common.py | 11 +++++-- pastey/config.py | 5 ++- pastey/routes.py | 73 +++++++++++++++++++++++++++++++++++++------ templates/config.html | 29 +++++++++++++++-- 5 files changed, 103 insertions(+), 18 deletions(-) diff --git a/app.py b/app.py index a765eff..80005ae 100644 --- a/app.py +++ b/app.py @@ -6,7 +6,7 @@ from os import environ from distutils.util import strtobool from threading import Thread -pastey_version = "0.4" +pastey_version = "0.4.1" loaded_config = {} loaded_themes = [] @@ -25,7 +25,6 @@ config.listen_address = environ["PASTEY_LISTEN_ADDRESS"] if "PASTEY_LISTEN_ADDRE config.listen_port = environ["PASTEY_LISTEN_PORT"] if "PASTEY_LISTEN_PORT" in environ else config.listen_port config.use_whitelist = bool(strtobool(environ["PASTEY_USE_WHITELIST"])) if "PASTEY_USE_WHITELIST" in environ else config.use_whitelist config.restrict_pasting = bool(strtobool(environ["PASTEY_RESTRICT_PASTING"])) if "PASTEY_RESTRICT_PASTING" in environ else config.restrict_pasting -config.restrict_raw_pasting = bool(strtobool(environ["PASTEY_RESTRICT_RAW_PASTING"])) if "PASTEY_RESTRICT_RAW_PASTING" in environ else config.restrict_raw_pasting config.guess_threshold = float(environ["PASTEY_GUESS_THRESHOLD"]) if "PASTEY_GUESS_THRESHOLD" in environ else config.guess_threshold config.recent_pastes = int(environ["PASTEY_RECENT_PASTES"]) if "PASTEY_RECENT_PASTES" in environ else config.recent_pastes config.whitelist_cidr = environ["PASTEY_WHITELIST_CIDR"].split(",") if "PASTEY_WHITELIST_CIDR" in environ else config.whitelist_cidr diff --git a/pastey/common.py b/pastey/common.py index 5278fe7..75bae7e 100644 --- a/pastey/common.py +++ b/pastey/common.py @@ -88,7 +88,14 @@ def is_expired(paste): # Build a URL, accounting for config options def build_url(request, path="/"): - protocol = request.url.split('//')[0] if not config.force_https_links else "https:" domain = request.url.split('//')[1].split("/")[0] if config.override_domain == "" else config.override_domain - + + # Check for HTTPS headers + if 'X-Forwarded-Proto' in request.headers and request.headers['X-Forwarded-Proto'] == "https": + protocol = "https:" + elif 'X-Forwarded-Port' in request.headers and request.headers['X-Forwarded-Port'] == "443": + protocol = "https:" + else: + protocol = request.url.split('//')[0] if not config.force_https_links else "https:" + return protocol + "//" + domain + path \ No newline at end of file diff --git a/pastey/config.py b/pastey/config.py index 689f5eb..1c384e0 100644 --- a/pastey/config.py +++ b/pastey/config.py @@ -21,9 +21,6 @@ blacklist_cidr = [] # Restrict pasting functionality to whitelisted IPs restrict_pasting = False -# Restrict raw pasting to whitelisted IPs -restrict_raw_pasting = True - # Rate limit for pasting (ignored for whitelisted users) rate_limit = "5/hour" @@ -54,6 +51,8 @@ show_cli_button = True # Include https in the generated links instead of http # This assumes you are behind something else doing the SSL # termination, but want the users to see https links +# +# This is normally handled by the HTTP headers now force_https_links = False # This can be used to specify a different domain for generated links diff --git a/pastey/routes.py b/pastey/routes.py index d1d1f99..dc3945b 100644 --- a/pastey/routes.py +++ b/pastey/routes.py @@ -5,6 +5,7 @@ from flask import Flask, render_template, request, redirect, abort from urllib.parse import quote from datetime import datetime from os import environ +import json # Load themes loaded_themes = common.get_themes() @@ -116,33 +117,49 @@ def paste(): if config.restrict_pasting and not common.verify_whitelist(source_ip): abort(401) + # Content field is necessary + if 'content' not in request.form: + abort(400) + content = request.form['content'] # Check if content is empty - if request.form['content'].strip() == "": - return redirect("/new") + if content.strip() == "": + if 'cli' in request.form: + abort(400) + else: + return redirect("/new") else: # Verify form options - title = request.form['title'] if request.form['title'].strip() != "" else "Untitled" + title = request.form['title'] if ('title' in request.form and request.form['title'].strip() != "") else "Untitled" single = True if 'single' in request.form else False encrypt = True if 'encrypt' in request.form else False - + expiration = int(request.form['expiration']) if 'expiration' in request.form else -1 + # Create paste - unique_id, key = functions.new_paste(title, content, source_ip, expires=int(request.form['expiration']), single=single, encrypt=encrypt) + unique_id, key = functions.new_paste(title, content, source_ip, expires=expiration, single=single, encrypt=encrypt) if encrypt: - return redirect("/view/" + unique_id + "/" + quote(key)) + + # Return link if cli form option was set + if 'cli' in request.form: + return common.build_url(request, "/view/" + unique_id + "/" + quote(key)), 200 + else: + return redirect("/view/" + unique_id + "/" + quote(key)) else: - return redirect("/view/" + unique_id) + if 'cli' in request.form: + return common.build_url(request, "/view/" + unique_id), 200 + else: + return redirect("/view/" + unique_id) # POST new raw paste @app.route('/raw', methods = ['POST']) @limiter.limit(config.rate_limit, exempt_when=lambda: common.verify_whitelist(common.get_source_ip(request))) -def raw(): +def paste_raw(): source_ip = common.get_source_ip(request) # Check if restrict pasting to whitelist CIDRs is enabled - if config.restrict_raw_pasting and not common.verify_whitelist(source_ip): + if config.restrict_pasting and not common.verify_whitelist(source_ip): abort(401) # Create paste @@ -151,6 +168,44 @@ def raw(): return link, 200 +# POST new json paste +@app.route('/json', methods = ['POST']) +@limiter.limit(config.rate_limit, exempt_when=lambda: common.verify_whitelist(common.get_source_ip(request))) +def paste_json(): + source_ip = common.get_source_ip(request) + + # Check if restrict pasting to whitelist CIDRs is enabled + if config.restrict_pasting and not common.verify_whitelist(source_ip): + abort(401) + + # Check json integrity + try: + paste = json.loads(request.data) + except json.JSONDecodeError: + abort(400) + + # Content field is mandatory + if 'content' not in paste or paste['content'].strip() == "": + abort(400) + content = paste['content'] + + # Optional fields + title = paste['title'] if ('title' in paste and paste['title'].strip() != "") else "Untitled" + single = paste['single'] if ('single' in paste and type(paste['single']) == bool) else False + encrypt = paste['encrypt'] if ('encrypt' in paste and type(paste['encrypt']) == bool) else False + expiration = paste['expiration'] if ('expiration' in paste and type(paste['expiration']) == int) else -1 + + # Create paste + unique_id, key = functions.new_paste(title, content, source_ip, expires=expiration, single=single, encrypt=encrypt) + if encrypt: + return { + "link": common.build_url(request, "/view/" + unique_id + "/" + quote(key)) + }, 200 + else: + return { + "link": common.build_url(request, "/view/" + unique_id) + }, 200 + # Custom 404 handler @app.errorhandler(404) def page_not_found(e): diff --git a/templates/config.html b/templates/config.html index e5f1f52..589de05 100644 --- a/templates/config.html +++ b/templates/config.html @@ -129,15 +129,40 @@
- +
-
+
+
+
+

JSON

+

You can also send a properly-formatted json POST request to /json:

+
+
+ +
+
+
{
+  "content": "This is a paste",
+  "title": "Interesting title",
+  "expiration": -1,
+  "encrypt": true,
+  "single": false
+}
+
+
+ +
+
+

Note that expiration is some value in hours, and that -1 = no expiration date. + Only the content field is mandatory.

+
+