From 65116e21030bc2b8ee7caeb1125c2c3a42f14575 Mon Sep 17 00:00:00 2001 From: "B. Stack" Date: Thu, 17 Mar 2022 11:36:16 -0400 Subject: v0.0.2: add curl support --- stackbin.py | 126 +++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 36 deletions(-) (limited to 'stackbin.py') 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") -- cgit