diff --git a/app.py b/app.py index 8a71216..b0ea61e 100644 --- a/app.py +++ b/app.py @@ -2,16 +2,13 @@ from flask import Flask, render_template, request, redirect, abort from flask_limiter import Limiter from flask_limiter.util import get_remote_address from guesslang import Guess -from datetime import datetime -from urllib.parse import quote -from cryptography.fernet import Fernet -import ipaddress -import uuid -import json -from os import path, remove, environ -from pathlib import Path -from config import config +from os import environ from distutils.util import strtobool +from threading import Thread + +pastey_version = "0.3" +loaded_config = {} +loaded_themes = [] app = Flask(__name__) limiter = Limiter( @@ -20,9 +17,7 @@ limiter = Limiter( ) guess = Guess() -# Pastey version -pastey_version = "0.2" -loaded_config = {} +from pastey import config, common, routes, functions # Check environment variable overrides config.data_directory = environ["PASTEY_DATA_DIRECTORY"] if "PASTEY_DATA_DIRECTORY" in environ else config.data_directory @@ -37,270 +32,11 @@ config.recent_pastes = int(environ["PASTEY_RECENT_PASTES"]) if "PASTEY_RECENT_PA config.whitelist_cidr = environ["PASTEY_WHITELIST_CIDR"].split(",") if "PASTEY_WHITELIST_CIDR" in environ else config.whitelist_cidr config.blacklist_cidr = environ["PASTEY_BLACKLIST_CIDR"].split(",") if "PASTEY_BLACKLIST_CIDR" in environ else config.blacklist_cidr config.behind_proxy = bool(strtobool(environ["PASTEY_BEHIND_PROXY"])) if "PASTEY_BEHIND_PROXY" in environ else config.behind_proxy - -# 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 - -########## 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()) - paste['unique_id'] = path.basename(paths[i]) - paste['content'] = '\n'.join(paste['content'].splitlines()[0:10]) - paste['icon'] = 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=""): - if path.exists(config.data_directory + "/" + unique_id): - with open(config.data_directory + "/" + unique_id, "r") as fp: - paste = json.loads(fp.read()) - - # 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(config.data_directory + "/" + unique_id, "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 = config.data_directory + "/" + unique_id - if path.exists(paste): - remove(paste) - -# Create new paste -def new_paste(title, content, source_ip, single=False, encrypt=False): - unique_id = str(uuid.uuid4()) - while path.exists(config.data_directory + "/" + unique_id): - unique_id = str(uuid.uuid4()) - - # Attempt to guess programming language - guesses = guess.probabilities(content) - language = guesses[0][0] if guesses[0][1] > config.guess_threshold and guesses[0][0] != "SQL" 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 - - output = { - "timestamp": datetime.now().strftime("%a, %d %b %Y at %H:%M:%S"), - "language": language, - "source_ip": source_ip, - "title": title, - "content": content, - "encrypted": encrypt, - "uses": uses - } - - # Write to output file - with open(config.data_directory + "/" + unique_id, "w+") as fp: - fp.write(json.dumps(output)) - - return unique_id, key - -########## Routes ########## - -# Home page -@app.route("/") -def home(): - whitelisted = verify_whitelist(get_source_ip(request)) - pastes = [] - - if whitelisted: - pastes=get_recent() - - return render_template("index.html", pastes=pastes, whitelisted=whitelisted) - -# New paste page -@app.route("/new") -def new(): - whitelisted = verify_whitelist(get_source_ip(request)) - return render_template("new.html", whitelisted=whitelisted) - -# Config page -@app.route("/config") -def config_page(): - whitelisted = verify_whitelist(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) - -# View paste page -@app.route("/view/") -def view(unique_id): - whitelisted = verify_whitelist(get_source_ip(request)) - - content = get_paste(unique_id) - if content is not None: - return render_template("view.html", paste=content, url=request.url, whitelisted=whitelisted) - else: - abort(404) - -# View paste page (encrypted) -@app.route("/view//") -def view_key(unique_id, key): - whitelisted = verify_whitelist(get_source_ip(request)) - content = 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=whitelisted) - else: - abort(404) - - -# Delete paste -@app.route("/delete/") -def delete(unique_id): - if not verify_whitelist(get_source_ip(request)): - abort(401) - - delete_paste(unique_id) - return redirect("/") - -@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: verify_whitelist(get_source_ip(request))) -def paste(): - source_ip = get_source_ip(request) - - # Check if restrict pasting to whitelist CIDRs is enabled - if config.restrict_pasting and not 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 = new_paste(title, content, source_ip, 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: verify_whitelist(get_source_ip(request))) -def raw(): - source_ip = get_source_ip(request) - - # Check if restrict pasting to whitelist CIDRs is enabled - if config.restrict_raw_pasting and not 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): - whitelisted = verify_whitelist(get_source_ip(request)) - return render_template('404.html', whitelisted=whitelisted), 404 - -# Custom 401 handler -@app.errorhandler(401) -def unauthorized(e): - whitelisted = verify_whitelist(get_source_ip(request)) - return render_template('401.html', whitelisted=whitelisted), 401 +config.default_theme = environ["PASTEY_DEFAULT_THEME"] if "PASTEY_DEFAULT_THEME" in environ else config.default_theme +config.purge_interval = int(environ["PASTEY_PURGE_INTERVAL"]) if "PASTEY_PURGE_INTERVAL" in environ else config.purge_interval +config.force_show_recent = bool(strtobool(environ["PASTEY_FORCE_SHOW_RECENT"])) if "PASTEY_FORCE_SHOW_RECENT" in environ else config.force_show_recent +config.ignore_guess = environ["PASTEY_IGNORE_GUESS"].split(",") if "PASTEY_IGNORE_GUESS" in environ else config.ignore_guess +config.show_cli_button = bool(strtobool(environ["PASTEY_SHOW_CLI_BUTTON"])) if "PASTEY_SHOW_CLI_BUTTON" in environ else config.show_cli_button # Main loop if __name__ == "__main__": @@ -317,7 +53,11 @@ if __name__ == "__main__": print("=====================================") # Register error handlers - app.register_error_handler(404, page_not_found) - app.register_error_handler(401, unauthorized) + app.register_error_handler(404, routes.page_not_found) + app.register_error_handler(401, routes.unauthorized) - app.run(host=config.listen_address, port=config.listen_port) \ No newline at end of file + # Start purging expired pastes thread + purge_thread = Thread(target=functions.purge_expired_pastes, daemon=True) + purge_thread.start() + + app.run(host=config.listen_address, port=config.listen_port) diff --git a/pastey/__init__.py b/pastey/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pastey/common.py b/pastey/common.py new file mode 100644 index 0000000..4fb0bbd --- /dev/null +++ b/pastey/common.py @@ -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 \ No newline at end of file diff --git a/pastey/config.py b/pastey/config.py new file mode 100644 index 0000000..6c830e8 --- /dev/null +++ b/pastey/config.py @@ -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 \ No newline at end of file diff --git a/pastey/functions.py b/pastey/functions.py new file mode 100644 index 0000000..edc207d --- /dev/null +++ b/pastey/functions.py @@ -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) diff --git a/pastey/routes.py b/pastey/routes.py new file mode 100644 index 0000000..979ac9d --- /dev/null +++ b/pastey/routes.py @@ -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/") +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//") +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/") +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 diff --git a/screenshots/cli.png b/screenshots/cli.png deleted file mode 100644 index 4c5e852..0000000 Binary files a/screenshots/cli.png and /dev/null differ diff --git a/screenshots/dark.png b/screenshots/dark.png new file mode 100644 index 0000000..1482860 Binary files /dev/null and b/screenshots/dark.png differ diff --git a/screenshots/home.png b/screenshots/home.png index 15c25e1..b5f0b0c 100644 Binary files a/screenshots/home.png and b/screenshots/home.png differ diff --git a/screenshots/new.png b/screenshots/new.png index 11f8e9b..82ed1a2 100644 Binary files a/screenshots/new.png and b/screenshots/new.png differ diff --git a/screenshots/view.png b/screenshots/view.png index ef5ef81..a058e79 100644 Binary files a/screenshots/view.png and b/screenshots/view.png differ diff --git a/static/css/style.css b/static/css/style.css index 804efbc..01f4352 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -7,12 +7,34 @@ #intro { margin-top: 45px; } + .pastey-checkbox { + margin-bottom:20px; + } +} + +.tooltip { + pointer-events: none; } -.paste-checkbox { - padding-bottom: 20px; +.prettyprint { + overflow-x: auto; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; } .error { font-size: 60px; -} \ No newline at end of file +} + +.pastey-logo { + width:160px; + height:26px; + background-size: 160px 26px; +} + +.pastey-checkbox { + padding-top:10px; +} diff --git a/static/img/pastey-dark.png b/static/img/pastey-dark.png new file mode 100644 index 0000000..11caaea Binary files /dev/null and b/static/img/pastey-dark.png differ diff --git a/static/js/common.js b/static/js/common.js index 68e5e50..8bf9f72 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -3,4 +3,14 @@ function copyToClipboard() { copyText.select(); copyText.setSelectionRange(0, 99999); document.execCommand("copy"); -} \ No newline at end of file +} + +function setTheme(theme) { + document.cookie = "pastey_theme=" + theme + "; Expires=Thu, 01 Jan 2100 00:00:01 GMT; path=/; SameSite=Lax;"; + location.reload(); +} + +function resetTheme() { + document.cookie = "pastey_theme=; Expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; SameSite=Lax;"; + location.reload(); +} diff --git a/static/themes/Dark.css b/static/themes/Dark.css new file mode 100644 index 0000000..8f2d3ff --- /dev/null +++ b/static/themes/Dark.css @@ -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); +} \ No newline at end of file diff --git a/static/themes/Light.css b/static/themes/Light.css new file mode 100644 index 0000000..dc3dd9d --- /dev/null +++ b/static/themes/Light.css @@ -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); +} \ No newline at end of file diff --git a/templates/401.html b/templates/401.html index 89a7c3a..2000fc0 100644 --- a/templates/401.html +++ b/templates/401.html @@ -9,38 +9,61 @@ +
-