diff --git a/zerobin.py b/zerobin.py deleted file mode 100644 index 271fcac..0000000 --- a/zerobin.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 - -from zerobin.cmd import main - -main() diff --git a/zerobin/__main__.py b/zerobin/__main__.py new file mode 100644 index 0000000..eadae7c --- /dev/null +++ b/zerobin/__main__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +from zerobin.cli import main + +main() diff --git a/zerobin/cmd.py b/zerobin/cli.py similarity index 74% rename from zerobin/cmd.py rename to zerobin/cli.py index 28f9827..c879752 100644 --- a/zerobin/cmd.py +++ b/zerobin/cli.py @@ -7,16 +7,20 @@ import sys import re +import os + +import zerobin from zerobin.utils import ( settings, SettingsValidationError, - ensure_var_env, + ensure_app_context, hash_password, ) from zerobin.routes import get_app from zerobin.paste import Paste + from bottle import run import clize @@ -26,11 +30,9 @@ def runserver( *, host="", port="", + config_dir="", + data_dir="", debug=None, - user="", - group="", - settings_file="", - compressed_static=None, version=False, paste_id_length=None, server="paste", @@ -39,23 +41,18 @@ def runserver( print("0bin V%s" % settings.VERSION) sys.exit(0) - settings.HOST = host or settings.HOST - settings.PORT = port or settings.PORT - settings.USER = user or settings.USER - settings.GROUP = group or settings.GROUP - settings.PASTE_ID_LENGTH = paste_id_length or settings.PASTE_ID_LENGTH - settings.DEBUG = bool(debug) if debug is not None else settings.DEBUG - - ensure_var_env() - try: - _, app = get_app(debug, settings_file, compressed_static, settings=settings) + settings, app = get_app(debug=debug, config_dir=config_dir, data_dir=data_dir,) except SettingsValidationError as err: print("Configuration error: %s" % err.message, file=sys.stderr) sys.exit(1) + settings.HOST = host or os.environ.get("ZEROBIN_HOST", settings.HOST) + settings.PORT = port or os.environ.get("ZEROBIN_PORT", settings.PORT) + if settings.DEBUG: - print(f"Admin URL: http://{settings.HOST}:{settings.PORT}{settings.ADMIN_URL}") + print(f"Admin URL: {settings.ADMIN_URL}") + print() run( app, host=settings.HOST, port=settings.PORT, reloader=True, server=server, ) @@ -100,7 +97,7 @@ def delete_paste(*pastes, quiet=False): print("Paste {} doesn't exist".format(paste_uuid)) -def print_admin_url(): +def infos(): """ Print the route to the 0bin admin. The admin route is generated by zerobin so that bots won't easily @@ -123,8 +120,16 @@ def print_admin_url(): """ - ensure_var_env() - print(settings.ADMIN_URL) + ensure_app_context() + print(f"Zerobin version: {zerobin.__version__}") + print(f"Admin URL (to moderate pastes): {settings.ADMIN_URL}") + print(f"Data dir (pastes and counter): {settings.DATA_DIR}") + print( + f"Config dir (config file, secret key, admin password and custom views): {settings.CONFIG_DIR}" + ) + print( + f"Static files dir (to configure apache, nging, etc.): {settings.STATIC_FILES_ROOT}" + ) def set_admin_password(password): @@ -134,17 +139,17 @@ def set_admin_password(password): """ - ensure_var_env() + ensure_app_context() settings.ADMIN_PASSWORD_FILE.write_bytes(hash_password(password)) def main(): - subcommands = [runserver, delete_paste, print_admin_url, set_admin_password] + subcommands = [runserver, delete_paste, infos, set_admin_password] subcommand_names = [ clize.util.name_py2cli(name) for name in clize.util.dict_from_names(subcommands).keys() ] if len(sys.argv) < 2 or sys.argv[1] not in subcommand_names: sys.argv.insert(1, subcommand_names[0]) - clize.run(runserver, delete_paste, print_admin_url, set_admin_password) + clize.run(runserver, delete_paste, infos, set_admin_password) diff --git a/zerobin/default_settings.py b/zerobin/default_settings.py index ce8c248..afefa84 100644 --- a/zerobin/default_settings.py +++ b/zerobin/default_settings.py @@ -1,57 +1,19 @@ -from zerobin import ROOT_DIR - -# Path to the directory that will contains all variable content, such -# as pastes, the secret key, etc -VAR_DIR = ROOT_DIR.parent / "var" - -# debug will get you error messages and auto reload -# don't set this to True in production +# Get error messages and auto reload. +# Don't set this to True in production DEBUG = False -# Should the application serve static files on it's own ? -# If yes, set the absolute path to the static files. -# If no, set it to None -# In dev this is handy, in prod you probably want the HTTP servers -# to serve it, but it's OK for small traffic to set it to True in prod too. -STATIC_FILES_ROOT = ROOT_DIR / "static" - -# If True, will link the compressed verion of the js and css files, -# otherwise, will use the ordinary files -COMPRESSED_STATIC_FILES = False - -# A tuple of absolute paths of directory where to look the template for -# the first one will be the first to be looked into -# if you want to override, it needs to be it a directory at the begining of -# this tuple. By default, custom_views is meant for that purpose. -TEMPLATE_DIRS = ( - VAR_DIR / "custom_views", - ROOT_DIR / "views", -) - -# Port and host the embeded python server should be using -# You can also specify them using the --host and --port script options -# which have priority on these settings +# Port and host the embedded python server should be using HOST = "127.0.0.1" -PORT = "8000" - -# User and group the server should run as. Set to None if it should be the -# current user. Some OS don't support it and if so, it will be ignored. -USER = None -GROUP = None +PORT = "3255" # Display a tiny counter for pastes created. -# Be carreful if your site have to many pastes this can hurt your hard drive performances. -# Refresh counter interval. Default to every minute after a paste. DISPLAY_COUNTER = True -REFRESH_COUNTER = 60 * 1 # Fill this if you want to -ADMIN_CREDENTIALS = { - "username": None, - "password": None, -} +# Refresh counter interval. +REFRESH_COUNTER = 60 # in seconds - -# Names/links to insert in the menu bar. -# Any link with "mailto:" will be escaped to prevent spam +# Names/links to insert in the footer. +# Any link with "mailto:" will be escaped to limit spam, but displayed +# correctly to the user using JS. MENU = ( ("Create paste", "/"), # internal link. First link will be highlited ("Github", "https://github.com/Tygs/0bin"), # external link @@ -60,11 +22,11 @@ MENU = ( ("Zerobin Pastebin", "https://www.0bin.net/"), # Thanks the authors :) ) -# limit size of pasted text in bytes. Be careful allowing too much size can -# slow down user's browser +# Size limit of the paste content in bytes. Be careful, allowing a size too big can +# slow down the user's browser MAX_SIZE = 1024 * 500 -# length of base64-like paste-id string in the url, int from 4 to 27 (length of sha1 digest) +# Length of the paste-id string in the url, int from 4 to 27 (length of sha1 digest) # total number of unique pastes can be calculated as 2^(6*PASTE_ID_LENGTH) # for PASTE_ID_LENGTH=8, for example, it's 2^(6*8) = 281 474 976 710 656 PASTE_ID_LENGTH = 8 diff --git a/zerobin/routes.py b/zerobin/routes.py index 272cc07..df83018 100644 --- a/zerobin/routes.py +++ b/zerobin/routes.py @@ -27,15 +27,15 @@ from beaker.middleware import SessionMiddleware from zerobin import __version__ from zerobin.utils import ( - settings, SettingsValidationError, - ensure_var_env, + ensure_app_context, check_password, + settings, ) from zerobin.paste import Paste -ensure_var_env() +ensure_app_context() GLOBAL_CONTEXT = { @@ -235,33 +235,33 @@ def server_static(filename): return static_file(filename, root=settings.STATIC_FILES_ROOT) -def get_app(debug=None, settings_file="", compressed_static=None, settings=settings): +def get_app(debug=None, config_dir="", data_dir=""): """ Return a tuple (settings, app) configured using passed parameters and/or a setting file. """ - settings_file = settings_file or os.environ.get("ZEROBIN_SETTINGS_FILE") + data_dir = data_dir or os.environ.get("ZEROBIN_DATA_DIR") + config_dir = config_dir or os.environ.get("ZEROBIN_CONFIG_DIR") - if settings_file: - settings.update_with_file(os.path.realpath(settings_file)) + ensure_app_context(config_dir=config_dir, data_dir=data_dir) + + settings.DEBUG = bool(debug or os.environ.get("ZEROBIN_DEBUG", settings.DEBUG)) + + settings.DISPLAY_COUNTER = bool( + os.environ.get("ZEROBIN_DISPLAY_COUNTER", settings.DISPLAY_COUNTER) + ) + settings.REFRESH_COUNTER = int( + os.environ.get("ZEROBIN_REFRESH_COUNTER", settings.REFRESH_COUNTER) + ) + settings.MAX_SIZE = int(os.environ.get("ZEROBIN_MAX_SIZE", settings.MAX_SIZE)) + settings.PASTE_ID_LENGTH = int( + os.environ.get("ZEROBIN_PASTE_ID_LENGTH", settings.PASTE_ID_LENGTH) + ) if settings.PASTE_ID_LENGTH < 4: raise SettingsValidationError("PASTE_ID_LENGTH cannot be lower than 4") - if compressed_static is not None: - settings.COMPRESSED_STATIC_FILES = compressed_static - - if debug is not None: - settings.DEBUG = debug - - # make sure the templates can be loaded - for d in reversed(settings.TEMPLATE_DIRS): - bottle.TEMPLATE_PATH.insert(0, d) - - if settings.DEBUG: - bottle.debug(True) - return settings, app diff --git a/zerobin/static/js/behavior.js b/zerobin/static/js/behavior.js index 4fb8145..55fea7a 100644 --- a/zerobin/static/js/behavior.js +++ b/zerobin/static/js/behavior.js @@ -18,8 +18,6 @@ setTimeout(function () { document.getElementById('content').focus() }, 100) - - const app = new Vue({ el: '#app', diff --git a/zerobin/utils.py b/zerobin/utils.py index 623e164..0f431d9 100644 --- a/zerobin/utils.py +++ b/zerobin/utils.py @@ -8,6 +8,13 @@ import hashlib import secrets from functools import partial +from pathlib import Path + +import bottle + +from appdirs import AppDirs + +import zerobin from zerobin import default_settings @@ -92,7 +99,7 @@ def as_unicode(obj): return str(obj) -def ensure_var_env(): +def ensure_app_context(data_dir=None, config_dir=None): """ Ensure all the variable things we generate are available. This will make sure we have: @@ -101,20 +108,39 @@ def ensure_var_env(): - a content dir - a secret key - an admin URL + + This function is idempotent if nothing touch the files it created. """ - settings.VAR_DIR.mkdir(exist_ok=True, parents=True) - settings.PASTE_FILES_ROOT = settings.VAR_DIR / "content" + app_dirs = AppDirs("0bin", "tygs") + + settings.DATA_DIR = Path(data_dir or app_dirs.user_data_dir).expanduser() + settings.DATA_DIR.mkdir(exist_ok=True, parents=True) + + settings.CONFIG_DIR = Path(config_dir or app_dirs.user_config_dir).expanduser() + settings.CONFIG_DIR.mkdir(exist_ok=True, parents=True) + + settings.STATIC_FILES_ROOT = zerobin.ROOT_DIR / "static" + + settings.PASTE_FILES_ROOT = settings.DATA_DIR / "pastes" settings.PASTE_FILES_ROOT.mkdir(exist_ok=True) - settings.SESSIONS_DIR = settings.VAR_DIR / "sessions" + + settings.SESSIONS_DIR = settings.DATA_DIR / "sessions" settings.SESSIONS_DIR.mkdir(exist_ok=True) - secret_key_file = settings.VAR_DIR / "secret_key" + bottle.TEMPLATE_PATH.insert(0, zerobin.ROOT_DIR / "views") + + CUSTOM_VIEWS_DIR = settings.CONFIG_DIR / "custom_views" + CUSTOM_VIEWS_DIR.mkdir(exist_ok=True) + + bottle.TEMPLATE_PATH.insert(0, CUSTOM_VIEWS_DIR) + + secret_key_file = settings.CONFIG_DIR / "secret_key" if not secret_key_file.is_file(): secret_key_file.write_text(secrets.token_urlsafe(64)) settings.SECRET_KEY = secret_key_file.read_text() - admin_password_file = settings.VAR_DIR / "admin_password" + admin_password_file = settings.CONFIG_DIR / "admin_password" if not secret_key_file.is_file(): admin_password_file.write_text( "No password set. Use the set_admin_passord command. Don't write this file by hand." @@ -124,6 +150,13 @@ def ensure_var_env(): payload = ("admin" + settings.SECRET_KEY).encode("ascii") settings.ADMIN_URL = "/admin/" + hashlib.sha256(payload).hexdigest() + "/" + settings_file = settings.CONFIG_DIR / "settings.py" + if not settings_file.is_file(): + default_config = (zerobin.ROOT_DIR / "default_settings.py").read_text() + settings_file.write_text(default_config) + + settings.update_with_file(settings_file) + def hash_password(password): return hashlib.scrypt( diff --git a/zerobin/views/base.tpl b/zerobin/views/base.tpl index 99e6214..a675a4b 100644 --- a/zerobin/views/base.tpl +++ b/zerobin/views/base.tpl @@ -12,7 +12,7 @@ - %if settings.COMPRESSED_STATIC_FILES: + %if not settings.DEBUG: %else: @@ -94,10 +94,11 @@ - - %if settings.COMPRESSED_STATIC_FILES: + + %if not settings.DEBUG: %else: + %end @@ -107,8 +108,8 @@ - %if settings.COMPRESSED_STATIC_FILES: - + %if not settings.DEBUG: + %else: diff --git a/zerobin/wsgi.py b/zerobin/wsgi.py new file mode 100644 index 0000000..33fb133 --- /dev/null +++ b/zerobin/wsgi.py @@ -0,0 +1,3 @@ +from zerobin.wsgi import setup_app + +settings, app = get_app()