aboutsummaryrefslogtreecommitdiff
path: root/stackbin.py
diff options
context:
space:
mode:
Diffstat (limited to 'stackbin.py')
-rw-r--r--stackbin.py126
1 files changed, 90 insertions, 36 deletions
diff --git a/stackbin.py b/stackbin.py
index b5f774a..b245206 100644
--- a/stackbin.py
+++ b/stackbin.py
@@ -9,8 +9,10 @@
# 2014 ofshellohicy removed some features
# 2016 su27 added some features
# 2022 bgstack15 hard forked
+# 2022-03-17 add support for command | curl -d '@-'
# Reference:
# fuss.py
+# https://stackoverflow.com/questions/15974730/how-do-i-get-the-different-parts-of-a-flask-requests-url
# Improve:
# Dependencies:
# dep-devuan: python3-pytimeparse, python3-uwsgidecorators, python3-flask, python3:any, uwsgi-core, uwsgi-plugin-python3
@@ -41,6 +43,7 @@ from sqlalchemy.schema import Column
import uuid
class UUID(types.TypeDecorator):
impl = MSBinary
+ cache_ok = True
def __init__(self):
self.impl.length = 16
self.cache_ok = False # to shut up some useless warning
@@ -147,6 +150,32 @@ class User(db.Model):
db.session.add(self)
db.session.commit()
+def _calculate_expiration(config, request = None, default = 0):
+ """
+ Given the Flask request, default expiration value, and app.config, calculate this specific expiration timestamp for a paste. This is called by new_paste().
+ """
+ exp = default
+ if 'exp' in request.form and request.form['exp']:
+ exp_opt = request.form['exp']
+ if exp_opt not in config['EXPIRATION_OPTIONS']:
+ try:
+ exp = timeparse(f"+ {config['EXPIRATION_OPTIONS'][0]}")
+ except:
+ exp = 60 * 60 # failsafe, 1 hour
+ print(f"WARNING: requested expiration \"{exp_opt}\" is not in the list of options {config['EXPIRATION_OPTIONS']}, so will use {exp}")
+ else:
+ try:
+ exp = timeparse(f"+ {exp_opt}")
+ except:
+ exp = 0
+ else:
+ # so if the exp was not in the request form, i.e., this was from curl -d '@-', then use first option
+ try:
+ exp = timeparse(f"+ {config['EXPIRATION_OPTIONS'][0]}")
+ except:
+ exp = 60 * 60 # failsafe, 1 hour
+ return exp
+
@app.route('/', methods=['GET', 'POST'])
def new_paste():
parent = None
@@ -156,39 +185,60 @@ def new_paste():
parent = Paste.query.get(uuid.UUID(reply_to))
except:
parent = Paste.query.get(reply_to)
- if request.method == 'POST' and request.form['code']:
- is_private = bool(request.form.get('is_private') or False)
- title = "Untitled paste"
- if request.form['pastetitle'] and request.form['pastetitle'] != "Enter title here":
- title = request.form['pastetitle']
- relative_expiration_seconds = 0
- exp = 0 # start with an empty value
- if 'exp' in request.form and request.form['exp']:
- exp_opt = request.form['exp']
- if exp_opt not in app.config['EXPIRATION_OPTIONS']:
- try:
- exp = timeparse(f"+ {app.config['EXPIRATION_OPTIONS'][0]}")
- except:
- exp = 60 * 60 # failsafe, 1 hour
- print(f"WARNING: requested expiration \"{exp_opt}\" is not in the list of options {app.config['EXPIRATION_OPTIONS']}, so will use {exp}")
- else:
- try:
- exp = timeparse(f"+ {exp_opt}")
- except:
- exp = 0
- paste = Paste(
- g.user,
- request.form['code'],
- title,
- exp,
- parent=parent,
- is_private=is_private
- )
- db.session.add(paste)
- db.session.commit()
- sign = get_signed(paste.id, salt=app.config['SALT']) \
- if is_private else None
- return redirect(url_for('show_paste', paste_id=paste.id, s=sign))
+ if request.method == 'POST':
+ # This request.get_data() must be here in order for the else contents = to work.
+ # It is probably related to getting the data before checking for request.form or something.
+ request.get_data()
+ if 'code' in request.form and request.form['code']:
+ is_private = bool(request.form.get('is_private') or False)
+ title = "Untitled paste"
+ if request.form['pastetitle'] and request.form['pastetitle'] != "Enter title here":
+ title = request.form['pastetitle']
+ relative_expiration_seconds = 0
+ exp = _calculate_expiration(app.config, request, 0)
+ paste = Paste(
+ g.user,
+ request.form['code'],
+ title,
+ exp,
+ parent=parent,
+ is_private=is_private
+ )
+ db.session.add(paste)
+ db.session.commit()
+ sign = get_signed(paste.id, salt=app.config['SALT']) \
+ if is_private else None
+ return redirect(url_for('show_paste', paste_id=paste.id, s=sign))
+ else:
+ # form field 'code' was not present, so this is probably command line piped into curl
+ contents = request.get_data().decode('utf8')
+ first_line = contents.split('\n')[0]
+ # cli does not provide choices for expiration, so use the first option from the list in the app config.
+ exp = _calculate_expiration(app.config, request, 0)
+ paste = Paste(
+ g.user,
+ contents,
+ first_line,
+ exp,
+ parent = None,
+ is_private = False
+ )
+ # the url_root makes it a complete url
+ db.session.add(paste)
+ db.session.commit()
+ # We cannot rely on a user having visited /set already, so let us parse the proxied values right now.
+ prefix = ''
+ host = request.url_root
+ if 'HTTP_X_FORWARDED_PREFIX' in request.environ and 'PREFIX' not in app.config:
+ prefix = ''.join(request.environ['HTTP_X_FORWARDED_PREFIX'].split(", "))
+ if 'HTTP_X_FORWARDED_HOST' in request.environ:
+ #request.environ['wsgi.url_scheme'] tends to always be http
+ protocol = "https"
+ if 'CURL_RESPONSE_PROTOCOL' in app.config:
+ protocol = app.config['CURL_RESPONSE_PROTOCOL']
+ host = protocol + "://" + request.environ['HTTP_X_FORWARDED_HOST'].split(", ")[0]
+ return host.strip('/') + prefix.rstrip('/') + url_for('show_paste', paste_id = paste.id) + "\n"
+ # and now back to the GET
try:
exp_opts = app.config['EXPIRATION_OPTIONS']
except:
@@ -331,12 +381,16 @@ def favicon():
@app.route('/set')
def get_proxied_path():
prefix = "/"
+ print(f"DEBUG: request.environ: {request.environ}")
if 'HTTP_X_FORWARDED_PREFIX' in request.environ:
- pl = len(dict(request.headers)["X-Forwarded-Host"].split(", "))
+ hl = len(dict(request.headers)["X-Forwarded-Host"].split(", "))
+ pl = len(dict(request.headers)["X-Forwarded-Prefix"].split(", "))
prefix = request.environ['HTTP_X_FORWARDED_PREFIX']
- app.wsgi_app = ProxyFix(app.wsgi_app,x_for=pl,x_host=pl,x_port=pl,x_prefix=pl,x_proto=pl)
+ app.wsgi_app = ProxyFix(app.wsgi_app,x_for=pl,x_host=hl,x_port=hl,x_prefix=pl,x_proto=pl)
+ app.config['PROXYFIX'] = pl
+ app.config['PREFIX'] = prefix
# we can afford to use prefix because new_paste is the top-level endpoint of the whole app
- message = refresh_string(1, prefix) + "OK"
+ message = refresh_string(1, (prefix + "/").replace("//","/")) + "OK"
return message, 200
# stubs, to simplify any templates that ask url_for("login")
bgstack15