aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md7
-rw-r--r--debian/changelog7
-rw-r--r--debian/stackbin.dsc6
-rw-r--r--extra/stackbin.spec5
-rw-r--r--stackbin.conf.dev28
-rw-r--r--stackbin.conf.example3
-rw-r--r--stackbin.py126
-rw-r--r--stackbin.wsgi.ini.dev23
9 files changed, 166 insertions, 40 deletions
diff --git a/.gitignore b/.gitignore
index 83a7c37..10dd0d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@ debian/*.substvars
debian/fuss/
debian/stackbin/
.pc
+var/
diff --git a/README.md b/README.md
index 1257abe..5139c48 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,13 @@ This means that if your app is behind `http://example.com/stackbin/` then you wo
http://example.com/stackbin/set
+### Command line
+
+The application is designed primarily to be used with the web form, however, users can send data from the command line with curl.
+
+ $ apt-cache show putty | curl https://example.com/stackbin/ -X POST --data-raw '@-'
+ https://example.com/stackbin/a2ee1780-f4c3-4672-bd47-e6ff0334b215
+
## Dependencies
For a production stack on CentOS 7:
diff --git a/debian/changelog b/debian/changelog
index 67536b3..c37c16a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+stackbin (0.0.2-1) obs; urgency=low
+
+ * New upstream release
+ * Add support to upload from command line
+
+ -- B. Stack <bgstack15@gmail.com> Thu, 17 Mar 2022 11:31:40 -0400
+
stackbin (0.0.1-1) obs; urgency=low
* Initial release. Closes: packages-want#005
diff --git a/debian/stackbin.dsc b/debian/stackbin.dsc
index 4a3f24a..75c2267 100644
--- a/debian/stackbin.dsc
+++ b/debian/stackbin.dsc
@@ -2,7 +2,7 @@ Format: 3.0 (quilt)
Source: stackbin
Binary: stackbin
Architecture: all
-Version: 0.0.1-1
+Version: 0.0.2-1
Maintainer: B. Stack <bgstack15@gmail.com>
Homepage: https://bgstack15.ddns.net/
Standards-Version: 4.5.0
@@ -10,5 +10,5 @@ Build-Depends: debhelper-compat (= 12)
Package-List:
stackbin deb web optional arch=all
Files:
- 00000000000000000000000000000000 1 stackbin_0.0.1.orig.tar.gz
- 00000000000000000000000000000000 1 stackbin_0.0.1-1.debian.tar.xz
+ 00000000000000000000000000000000 1 stackbin.orig.tar.gz
+ 00000000000000000000000000000000 1 stackbin.debian.tar.xz
diff --git a/extra/stackbin.spec b/extra/stackbin.spec
index 9ae510e..cd0d115 100644
--- a/extra/stackbin.spec
+++ b/extra/stackbin.spec
@@ -34,7 +34,7 @@
Summary: Pastebin implementation in flask
Name: stackbin
-Version: 0.0.1
+Version: 0.0.2
Release: 1
License: GPL 3.0
Source0: https://gitlab.com/bgstack15/%{name}/-/archive/master/%{name}-master.tar.gz
@@ -154,5 +154,8 @@ exit 0
%{_defaultdocdir}/%{name}
%changelog
+* Thu Mar 17 2022 B. Stack <bgstack15@gmail.com> - 0.0.1-1
+- New upstream release
+
* Tue Feb 15 2022 B. Stack <bgstack15@gmail.com> - 0.0.1-1
- Initial release
diff --git a/stackbin.conf.dev b/stackbin.conf.dev
new file mode 100644
index 0000000..e2fa21f
--- /dev/null
+++ b/stackbin.conf.dev
@@ -0,0 +1,28 @@
+DEBUG=True
+SQLALCHEMY_DATABASE_URI='sqlite:///stackbin.db'
+SQLALCHEMY_TRACK_MODIFICATIONS = False
+SECRET_KEY='development-key'
+SALT='jackson'
+DELETESALT='differentstring'
+APPNAME='stackbin'
+# LOOP_DELAY in seconds is how many seconds between running the expiration cleanup task
+LOOP_DELAY = 60 * 5
+
+# Disable this variable entirely, to disable any choices for expiration
+# Any very simple expression for relative time can be used here. This will be processed by pyparsedate.
+EXPIRATION_OPTIONS = [
+ "never",
+ "1 day",
+ "1 hour",
+ "15 minutes"
+]
+
+#TEMPLATE_FOLDER = "/usr/share/stackbin/templates"
+#STATIC_FOLDER = "/usr/share/stackbin/static"
+
+# If you turn these off, the admin panel is entirely disabled.
+ADMIN_USERNAME = 'admin'
+ADMIN_PASSWORD = 'fluffycat10'
+
+# Because the app generates the url entirely from scratch for a curl POST, the protocol can get lost, so you can force https if you define this variable.
+CURL_RESPONSE_PROTOCOL = "https"
diff --git a/stackbin.conf.example b/stackbin.conf.example
index 481a448..518af7c 100644
--- a/stackbin.conf.example
+++ b/stackbin.conf.example
@@ -23,3 +23,6 @@ STATIC_FOLDER = "/usr/share/stackbin/static"
# If you turn these off, the admin panel is entirely disabled.
ADMIN_USERNAME = 'admin'
ADMIN_PASSWORD = 'fluffycat10'
+
+# Because the app generates the url entirely from scratch for a curl POST, the protocol can get lost, so you can force https if you define this variable.
+CURL_RESPONSE_PROTOCOL = "https"
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")
diff --git a/stackbin.wsgi.ini.dev b/stackbin.wsgi.ini.dev
new file mode 100644
index 0000000..7ed3421
--- /dev/null
+++ b/stackbin.wsgi.ini.dev
@@ -0,0 +1,23 @@
+[uwsgi]
+# CentOS 7 uwsgi needs "python36" added to this list.
+#plugins = logfile, python36
+# Devuan Ceres does not.
+plugins = logfile
+http-socket = 0.0.0.0:4680
+wsgi-file = stackbin.py
+callable = app
+touch-reload = stackbin.py
+touch-reload = stackbin.conf
+touch-reload = stackbin.wsgi.ini
+touch-reload = templates/admin.html
+touch-reload = templates/layout.html
+touch-reload = templates/login_form.html
+touch-reload = templates/new_paste.html
+touch-reload = templates/_pagination.html
+touch-reload = templates/show_paste.html
+req-logger = file:log/req.log
+# to get strftime format fields, you need double percent signs
+logdate = "%%FT%%T"
+logger = file:log/stackbin.log
+# the init script uses a different pidfile owned by root.
+pidfile = var/stackbin-wsgi.pid
bgstack15