@ -0,0 +1,87 @@
|
||||
from . import config
|
||||
|
||||
import ipaddress
|
||||
from os import path
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
########## Common functions ##########
|
||||
|
||||
# Check request IP is in config whitelist
|
||||
def verify_whitelist(ip):
|
||||
address = ipaddress.ip_address(ip)
|
||||
|
||||
# Check blacklist
|
||||
for network in config.blacklist_cidr:
|
||||
if address in ipaddress.IPv4Network(network):
|
||||
return False
|
||||
|
||||
if not config.use_whitelist:
|
||||
return True
|
||||
|
||||
# Check whitelist
|
||||
for network in config.whitelist_cidr:
|
||||
if address in ipaddress.IPv4Network(network):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# Solve anomalies in icon naming
|
||||
def get_icon(language):
|
||||
if language == "C#":
|
||||
return "csharp"
|
||||
elif language == "C++":
|
||||
return "cplusplus"
|
||||
elif language == "Jupyter Notebook":
|
||||
return "jupyter"
|
||||
else:
|
||||
return language.lower()
|
||||
|
||||
# For handling reverse proxy configurations
|
||||
# Note that these are HTTP headers and are generally untrustworthy
|
||||
# Make sure your proxy configuration is either setting or clearing these
|
||||
def get_source_ip(request):
|
||||
if config.behind_proxy:
|
||||
if 'X-Real-IP' in request.headers:
|
||||
return request.headers['X-Real-IP']
|
||||
elif 'X-Forwarded-For' in request.headers:
|
||||
return request.headers['X-Forwarded-For']
|
||||
|
||||
return request.remote_addr
|
||||
|
||||
# Determine theme by checking for cookie, or returning default
|
||||
def set_theme(request):
|
||||
if 'pastey_theme' in request.cookies:
|
||||
return request.cookies['pastey_theme']
|
||||
return config.default_theme
|
||||
|
||||
# Get a sorted list of all themes in the theme dir
|
||||
def get_themes():
|
||||
themes = []
|
||||
for path in Path("./static/themes/").iterdir():
|
||||
themes.append(str(path).split('/')[-1].split('.')[0])
|
||||
return sorted(themes, key=str.casefold)
|
||||
|
||||
# Get file path from unique id
|
||||
# This is a wrapper to check for files with the .expires extension
|
||||
def determine_file(unique_id):
|
||||
attempt = config.data_directory + "/" + unique_id
|
||||
if path.exists(attempt):
|
||||
return attempt
|
||||
|
||||
# Check for expiration format
|
||||
attempt = attempt + ".expires"
|
||||
if path.exists(attempt):
|
||||
return attempt
|
||||
|
||||
return None
|
||||
|
||||
# Take a paste object and check if it is expired
|
||||
def is_expired(paste):
|
||||
if 'expiration' in paste and paste['expiration'] != "":
|
||||
expires = datetime.strptime(paste['expiration'], "%a, %d %b %Y at %H:%M:%S")
|
||||
|
||||
if expires < datetime.now():
|
||||
return True
|
||||
|
||||
return False
|
@ -0,0 +1,52 @@
|
||||
# Data directory
|
||||
data_directory = "./data"
|
||||
|
||||
# Listen address
|
||||
listen_address = "0.0.0.0"
|
||||
|
||||
# Listen port
|
||||
listen_port = 5000
|
||||
|
||||
# Use whitelisting
|
||||
# Whitelisted IPs can view recent pastes on the home page, as well as delete pastes
|
||||
# For limiting pasting to whitelisted users, enable the "restrict_pasting" option below
|
||||
use_whitelist = True
|
||||
|
||||
# Whitelist CIDR
|
||||
whitelist_cidr = ['127.0.0.1/32', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
|
||||
|
||||
# Blacklist CIDR
|
||||
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"
|
||||
|
||||
# Guess threshold for automatic language detection
|
||||
guess_threshold = 0.20
|
||||
|
||||
# Number of recent pastes to show on the home page
|
||||
recent_pastes = 10
|
||||
|
||||
# Try to use X-Real-IP or X-Forwarded-For HTTP headers
|
||||
behind_proxy = False
|
||||
|
||||
# Default theme to display to users
|
||||
default_theme = "Light"
|
||||
|
||||
# Purge interval (in seconds) for checking expired pastes
|
||||
purge_interval = 3600
|
||||
|
||||
# Show recent pastes, even to non-whitelisted users (without a delete button)
|
||||
force_show_recent = False
|
||||
|
||||
# Ignore these classifications for language detection
|
||||
ignore_guess = ['TeX', 'SQL']
|
||||
|
||||
# Show CLI button on home page
|
||||
show_cli_button = True
|
@ -0,0 +1,143 @@
|
||||
from __main__ import guess, app
|
||||
from . import config, common
|
||||
|
||||
from os import path, remove
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timedelta
|
||||
from cryptography.fernet import Fernet
|
||||
import time
|
||||
import uuid
|
||||
import json
|
||||
|
||||
########## Paste functions ##########
|
||||
|
||||
# Get recent n pastes, defined in config by recent_pastes
|
||||
def get_recent(limit=config.recent_pastes):
|
||||
paths = sorted(Path(config.data_directory).iterdir(), key=path.getmtime, reverse=True)
|
||||
|
||||
recent_pastes = []
|
||||
i = 0
|
||||
while i < limit and i < len(paths):
|
||||
with open(paths[i]) as fp:
|
||||
paste = json.loads(fp.read())
|
||||
|
||||
basename = path.basename(paths[i])
|
||||
paste['unique_id'] = basename[:-8] if basename.endswith(".expires") else basename
|
||||
paste['content'] = '\n'.join(paste['content'].splitlines()[0:10])
|
||||
paste['icon'] = common.get_icon(paste['language'])
|
||||
|
||||
if paste['encrypted']:
|
||||
paste['content'] = "[Encrypted]"
|
||||
|
||||
recent_pastes.append(paste)
|
||||
i += 1
|
||||
|
||||
return recent_pastes
|
||||
|
||||
# Get paste by ID
|
||||
def get_paste(unique_id, key=""):
|
||||
file_path = common.determine_file(unique_id)
|
||||
|
||||
if file_path is not None:
|
||||
with open(file_path, "r") as fp:
|
||||
paste = json.loads(fp.read())
|
||||
|
||||
# Check if paste is expired
|
||||
if common.is_expired(paste):
|
||||
delete_paste(unique_id)
|
||||
return None
|
||||
|
||||
# Check remaining uses, and decrement
|
||||
# -1 = unlimited uses
|
||||
if paste['uses'] != -1:
|
||||
paste['uses'] -= 1
|
||||
if paste['uses'] == 0:
|
||||
delete_paste(unique_id)
|
||||
else:
|
||||
with open(file_path, "w") as fp:
|
||||
fp.write(json.dumps(paste))
|
||||
|
||||
# Decrypt content, if necessary
|
||||
try:
|
||||
if key != "":
|
||||
cipher_suite = Fernet(key.encode('utf-8'))
|
||||
paste['content'] = cipher_suite.decrypt(paste['content'].encode('utf-8')).decode('utf-8')
|
||||
except Exception as e:
|
||||
return 401
|
||||
|
||||
return paste
|
||||
else:
|
||||
return None
|
||||
|
||||
# Delete paste by ID
|
||||
def delete_paste(unique_id):
|
||||
paste = common.determine_file(unique_id)
|
||||
if paste is not None:
|
||||
remove(paste)
|
||||
|
||||
# Create new paste
|
||||
def new_paste(title, content, source_ip, expires=0, single=False, encrypt=False):
|
||||
unique_id = str(uuid.uuid4())
|
||||
output_file = config.data_directory + "/" + unique_id
|
||||
|
||||
# Check for existing paste id (unlikely)
|
||||
while path.exists(output_file) or path.exists(output_file + ".expires"):
|
||||
unique_id = str(uuid.uuid4())
|
||||
output_file = config.data_directory + "/" + unique_id
|
||||
|
||||
# Attempt to guess programming language
|
||||
guesses = guess.probabilities(content)
|
||||
language = guesses[0][0] if guesses[0][1] > config.guess_threshold and guesses[0][0] not in config.ignore_guess else "Plaintext"
|
||||
|
||||
# Check if encryption is necessary
|
||||
key = ""
|
||||
if encrypt:
|
||||
init_key = Fernet.generate_key()
|
||||
cipher_suite = Fernet(init_key)
|
||||
content = cipher_suite.encrypt(content.encode('utf-8')).decode('utf-8')
|
||||
key = init_key.decode('utf-8')
|
||||
|
||||
# Check if single use is set
|
||||
uses = 2 if single else -1
|
||||
|
||||
# Check for expiration
|
||||
now = datetime.now()
|
||||
expiration = ""
|
||||
if expires > 0:
|
||||
expiration = (now + timedelta(hours=expires)).strftime("%a, %d %b %Y at %H:%M:%S")
|
||||
output_file = output_file + ".expires"
|
||||
|
||||
output = {
|
||||
"timestamp": now.strftime("%a, %d %b %Y at %H:%M:%S"),
|
||||
"language": language,
|
||||
"source_ip": source_ip,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"encrypted": encrypt,
|
||||
"uses": uses,
|
||||
"expiration": expiration
|
||||
}
|
||||
|
||||
# Write to output file
|
||||
with open(output_file, "w+") as fp:
|
||||
fp.write(json.dumps(output))
|
||||
|
||||
return unique_id, key
|
||||
|
||||
# Purge expired pastes
|
||||
def purge_expired_pastes():
|
||||
print("Starting purge thread, with interval {0} seconds...".format(config.purge_interval))
|
||||
while True:
|
||||
for paste in Path(config.data_directory).iterdir():
|
||||
if str(paste).endswith(".expires"):
|
||||
unique_id = path.basename(paste)[:-8]
|
||||
|
||||
with open(paste, "r") as fp:
|
||||
content = json.loads(fp.read())
|
||||
|
||||
# Check if paste is expired
|
||||
if common.is_expired(content):
|
||||
delete_paste(unique_id)
|
||||
|
||||
# Sleep for specified interval
|
||||
time.sleep(config.purge_interval)
|
@ -0,0 +1,163 @@
|
||||
from __main__ import app, limiter, loaded_config
|
||||
from . import config, common, functions
|
||||
|
||||
from flask import Flask, render_template, request, redirect, abort
|
||||
from urllib.parse import quote
|
||||
from datetime import datetime
|
||||
|
||||
# Load themes
|
||||
loaded_themes = common.get_themes()
|
||||
|
||||
########## Routes ##########
|
||||
|
||||
# Home page
|
||||
@app.route("/")
|
||||
def home():
|
||||
whitelisted = common.verify_whitelist(common.get_source_ip(request))
|
||||
pastes = []
|
||||
|
||||
if whitelisted or config.force_show_recent:
|
||||
pastes = functions.get_recent()
|
||||
|
||||
return render_template("index.html",
|
||||
pastes=pastes,
|
||||
whitelisted=whitelisted,
|
||||
active_theme=common.set_theme(request),
|
||||
themes=loaded_themes,
|
||||
force_show_recent=config.force_show_recent,
|
||||
show_cli_button=config.show_cli_button,
|
||||
script_url=request.url.rsplit('/', 1)[0] + "/pastey")
|
||||
|
||||
# New paste page
|
||||
@app.route("/new")
|
||||
def new():
|
||||
whitelisted = common.verify_whitelist(common.get_source_ip(request))
|
||||
return render_template("new.html",
|
||||
whitelisted=whitelisted,
|
||||
active_theme=common.set_theme(request),
|
||||
themes=loaded_themes)
|
||||
|
||||
# Config page
|
||||
@app.route("/config")
|
||||
def config_page():
|
||||
whitelisted = common.verify_whitelist(common.get_source_ip(request))
|
||||
if not whitelisted:
|
||||
abort(401)
|
||||
|
||||
return render_template("config.html",
|
||||
config_items=loaded_config,
|
||||
script_url=request.url.rsplit('/', 1)[0] + "/pastey",
|
||||
whitelisted=whitelisted,
|
||||
active_theme=common.set_theme(request),
|
||||
themes=loaded_themes)
|
||||
|
||||
# View paste page
|
||||
@app.route("/view/<unique_id>")
|
||||
def view(unique_id):
|
||||
content = functions.get_paste(unique_id)
|
||||
|
||||
if content is not None:
|
||||
return render_template("view.html",
|
||||
paste=content,
|
||||
url=request.url,
|
||||
whitelisted=common.verify_whitelist(common.get_source_ip(request)),
|
||||
active_theme=common.set_theme(request),
|
||||
themes=loaded_themes)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
# View paste page (encrypted)
|
||||
@app.route("/view/<unique_id>/<key>")
|
||||
def view_key(unique_id, key):
|
||||
content = functions.get_paste(unique_id, key=key)
|
||||
|
||||
if content == 401:
|
||||
abort(401)
|
||||
elif content is not None:
|
||||
return render_template("view.html",
|
||||
paste=content,
|
||||
url=request.url,
|
||||
whitelisted=common.verify_whitelist(common.get_source_ip(request)),
|
||||
active_theme=common.set_theme(request),
|
||||
themes=loaded_themes)
|
||||
else:
|
||||
abort(404)
|
||||
|
||||
|
||||
# Delete paste
|
||||
@app.route("/delete/<unique_id>")
|
||||
def delete(unique_id):
|
||||
if not common.verify_whitelist(common.get_source_ip(request)):
|
||||
abort(401)
|
||||
|
||||
functions.delete_paste(unique_id)
|
||||
return redirect("/")
|
||||
|
||||
# Script download
|
||||
@app.route("/pastey")
|
||||
def pastey_script():
|
||||
return render_template('pastey.sh', endpoint=request.url.rsplit('/', 1)[0] + "/raw"), 200, {
|
||||
'Content-Disposition': 'attachment; filename="pastey"',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
# POST new paste
|
||||
@app.route('/paste', methods = ['POST'])
|
||||
@limiter.limit(config.rate_limit, exempt_when=lambda: common.verify_whitelist(common.get_source_ip(request)))
|
||||
def paste():
|
||||
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)
|
||||
|
||||
content = request.form['content']
|
||||
|
||||
# Check if content is empty
|
||||
if request.form['content'].strip() == "":
|
||||
return redirect("/new")
|
||||
else:
|
||||
|
||||
# Verify form options
|
||||
title = request.form['title'] if request.form['title'].strip() != "" else "Untitled"
|
||||
single = True if 'single' in request.form else False
|
||||
encrypt = True if 'encrypt' in request.form else False
|
||||
|
||||
# Create paste
|
||||
unique_id, key = functions.new_paste(title, content, source_ip, expires=int(request.form['expiration']), single=single, encrypt=encrypt)
|
||||
if encrypt:
|
||||
return redirect("/view/" + unique_id + "/" + quote(key))
|
||||
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():
|
||||
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):
|
||||
abort(401)
|
||||
|
||||
# Create paste
|
||||
unique_id, key = new_paste("Untitled", request.data.decode('utf-8'), source_ip, single=False, encrypt=False)
|
||||
link = request.url.rsplit('/', 1)[0] + "/view/" + unique_id
|
||||
|
||||
return link, 200
|
||||
|
||||
# Custom 404 handler
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(e):
|
||||
return render_template('404.html',
|
||||
whitelisted=common.verify_whitelist(common.get_source_ip(request)),
|
||||
active_theme=common.set_theme(request),
|
||||
themes=loaded_themes), 404
|
||||
|
||||
# Custom 401 handler
|
||||
@app.errorhandler(401)
|
||||
def unauthorized(e):
|
||||
return render_template('401.html',
|
||||
whitelisted=common.verify_whitelist(common.get_source_ip(request)),
|
||||
active_theme=common.set_theme(request),
|
||||
themes=loaded_themes), 401
|
Before Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 125 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 129 KiB |
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,105 @@
|
||||
body, tr {
|
||||
background-color: #262626 !important;
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
|
||||
.pastey-logo {
|
||||
background-image: url('/static/img/pastey-dark.png');
|
||||
}
|
||||
|
||||
.pastey-navbar {
|
||||
background-color: #262626 !important;
|
||||
}
|
||||
|
||||
.pastey-link {
|
||||
color: rgba(255, 255, 255, 0.80);
|
||||
}
|
||||
|
||||
.pastey-link:hover {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.pastey-header {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.pastey-input {
|
||||
color: rgba(255, 255, 255) !important;
|
||||
background-color: #2f2f2f !important;
|
||||
}
|
||||
|
||||
.pastey-input-title {
|
||||
color: rgba(255, 255, 255) !important;
|
||||
}
|
||||
|
||||
.pastey-preview-image {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
.pastey-select {
|
||||
background-color: #2f2f2f;
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
|
||||
.nocode {
|
||||
color: rgb(255,255,255) !important;
|
||||
}
|
||||
|
||||
/* Google prettify elements */
|
||||
|
||||
/* Alternating line colors */
|
||||
li.L1,li.L3,li.L5,li.L7,li.L9 {
|
||||
background:#2a2a2a;
|
||||
}
|
||||
|
||||
li.L0,li.L2,li.L4,li.L6,li.L8 {
|
||||
background:#222222;
|
||||
}
|
||||
|
||||
/*
|
||||
Syntax highlighting
|
||||
See Google prettify for more details
|
||||
*/
|
||||
.pln, .pun {
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
|
||||
.kwd {
|
||||
color: rgb(152, 209, 255);
|
||||
}
|
||||
|
||||
.com {
|
||||
color: rgb(255, 51, 51);
|
||||
}
|
||||
|
||||
.str {
|
||||
color: rgb(0, 195, 0);
|
||||
}
|
||||
|
||||
.lit {
|
||||
color: rgb(0, 161, 161);
|
||||
}
|
||||
|
||||
.tag {
|
||||
color: rgb(0, 167, 218);
|
||||
}
|
||||
|
||||
.atn {
|
||||
color: rgb(248, 1, 248);
|
||||
}
|
||||
|
||||
.atv {
|
||||
color: rgb(0, 195, 0);
|
||||
}
|
||||
|
||||
.dec, .var {
|
||||
color: rgb(255, 2, 129);
|
||||
}
|
||||
|
||||
.typ {
|
||||
color: rgb(197, 0, 197);
|
||||
}
|
||||
|
||||
.opn, .clo {
|
||||
color: rgb(218, 218, 0);
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
.pastey-logo {
|
||||
background-image: url('/static/img/pastey.png');
|
||||
}
|
||||
|
||||
.pastey-link {
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.pastey-link:hover {
|
||||
color: rgba(0, 0, 0, 0.70);;
|
||||
}
|
||||
|
||||
/*
|
||||
Syntax highlighting
|
||||
See Google prettify for more details
|
||||
*/
|
||||
.pun {
|
||||
color: rgba(0, 0, 0);
|
||||
}
|