diff --git a/settings.py b/docs/example_settings.py similarity index 53% rename from settings.py rename to docs/example_settings.py index dabf1bf..dea6f47 100644 --- a/settings.py +++ b/docs/example_settings.py @@ -3,22 +3,38 @@ # import default settings value from src/default_settings.py # you can refer to this file if you forgot what # settings is for and what it is set to by default -# DO NOT ALTER THIS LINE -from src.default_settings import * +# You probably do not want to alter this line +from zerobin.default_settings import * # debug will get you error message and auto reload # don't set this to True in production -DEBUG = False +DEBUG = True + +# 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 = os.path.join(ROOT_DIR, 'static') # absolute path where the paste files should be store # default in projectdirectory/static/content/ # use "/" even under Windows -PASTE_FILES_ROOT = os.path.join(STATIC_FILES_ROOT, 'content') +PASTE_FILES_ROOT = os.path.join(ROOT_DIR, 'static', 'content') + +# 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 a template, create a new dir, write the +# template with the same name as the one you want to override in it +# then add the dir path at the top of this tuple +TEMPLATE_DIRS = ( + os.path.join(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 -HOST = "127.0.0.1" +HOST = "0.0.0.0" PORT = "8000" # User and group the server should run as. Set to None if it should be the @@ -26,23 +42,16 @@ PORT = "8000" USER = None GROUP = None -# limit size of pasted text in bytes. Be carefull allowing too much -# size can slow down user's browser -MAX_SIZE = 1024 * 500 -MAX_SIZE_KB = int(math.ceil(MAX_SIZE / 1024.0)) - # Names/links to insert in the menu bar. # Any link with "mailto:" will be escaped to prevent spam MENU = ( - ('Home', '/'), # internal link + ('Home', '/'), # internal link. First link will be highlited ('Download 0bin', 'https://github.com/sametmax/0bin'), # external link ('Contact', 'mailto:your@email.com') # email ) -# this import a file named settings_local.py if it exists -# you may want to create such a file to have different settings -# on each machine -try: - from settings_local import * -except ImportError: - pass +# limit size of pasted text in bytes. Be carefull allowing too much size can slow down user's +# browser +MAX_SIZE = 1024 * 500 +MAX_SIZE_KB = int(math.ceil(MAX_SIZE / 1024.0)) + diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index faaaf79..0000000 --- a/src/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - - diff --git a/src/utils.py b/src/utils.py deleted file mode 100644 index a45f4c4..0000000 --- a/src/utils.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- - -import time -import os -import glob -import tempfile - - -try: - from privilege import drop_privileges_permanently, coerce_user, coerce_group -except (AttributeError): - pass # privilege does't work on several plateform - - -def drop_privileges(user=None, group=None, wait=5): - """ - Try to set the process user and group to another one. - If no group is provided, it's set to the same as the user. - You can wait for a certain time before doing so. - """ - if wait: - time.sleep(wait) - if user: - group = group or user - try: - user = coerce_user(user) - group = coerce_group(group) - - lock_files = glob.glob(os.path.join(tempfile.gettempdir(), - 'bottle.*.lock')) - for lock_file in lock_files: - os.chown(lock_file, user, group) - - drop_privileges_permanently(user, group, ()) - except Exception: - print "Failed to drop privileges. Running with current user." - - -def dmerge(*args): - """ - return new directionay being the sum of all merged dictionaries passed as arguments - """ - - dictionary = {} - - for arg in args: - dictionary.update(arg) - - return dictionary diff --git a/start.py b/start.py index c6026a8..f81277f 100755 --- a/start.py +++ b/start.py @@ -2,133 +2,6 @@ # -*- coding: utf-8 -*- # vim: ai ts=4 sts=4 et sw=4 -""" - Main script including controller, rooting, dependancy management, and - server run. -""" +from zerobin.routes import main -import sys -import os -import thread - -from datetime import datetime, timedelta - -# add project dir and libs dir to the PYTHON PATH to ensure they are -# importable -import settings -sys.path.insert(0, os.path.dirname(settings.ROOT_DIR)) -sys.path.append(os.path.join(settings.ROOT_DIR, 'libs')) - -import bottle -from bottle import (Bottle, run, static_file, view, request) - -import clize - - -from src.paste import Paste -from src.utils import drop_privileges, dmerge - - -app = Bottle() - -global_vars = { - 'settings': settings -} - - -@app.route('/') -@view('home') -def index(): - return global_vars - - -@app.route('/paste/create', method='POST') -def create_paste(): - - try: - content = unicode(request.forms.get('content', ''), 'utf8') - except UnicodeDecodeError: - return {'status': 'error', - 'message': u"Encoding error: the paste couldn't be saved."} - - if '{"iv":' not in content: # reject silently non encrypted content - return '' - - if content: - # check size of the paste. if more than settings return error without saving paste. - # prevent from unusual use of the system. - # need to be improved - if len(content) < settings.MAX_SIZE: - expiration = request.forms.get('expiration', u'burn_after_reading') - paste = Paste(expiration=expiration, content=content) - paste.save() - return {'status': 'ok', - 'paste': paste.uuid} - - return {'status': 'error', - 'message': u"Serveur error: the paste couldn't be saved. Please try later."} - - -@app.route('/paste/:paste_id') -@view('paste') -def display_paste(paste_id): - - now = datetime.now() - keep_alive = False - try: - paste = Paste.load(paste_id) - # Delete the paste if it expired: - if 'burn_after_reading' in str(paste.expiration): - # burn_after_reading contains the paste creation date - # if this read appends 10 seconds after the creation date - # we don't delete the paste because it means it's the redirection - # to the paste that happens during the paste creation - try: - keep_alive = paste.expiration.split('#')[1] - keep_alive = datetime.strptime(keep_alive, '%Y-%m-%d %H:%M:%S.%f') - keep_alive = now < keep_alive + timedelta(seconds=10) - except IndexError: - keep_alive = False - if not keep_alive: - paste.delete() - - elif paste.expiration < now: - paste.delete() - raise ValueError() - - except (TypeError, ValueError): - #abort(404, u"This paste doesn't exist or has expired") - return error404(ValueError) - - context = {'paste': paste, 'keep_alive': keep_alive} - return dmerge(context, global_vars) - - -@app.error(404) -@view('404') -def error404(code): - return global_vars - - -@clize.clize -def runserver(host=settings.HOST, port=settings.PORT, debug=settings.DEBUG, - serve_static=settings.DEBUG, user=settings.USER, - group=settings.GROUP): - - if serve_static: - @app.route('/static/') - def server_static(filename): - return static_file(filename, root=settings.STATIC_FILES_ROOT) - - thread.start_new_thread(drop_privileges, (user, group)) - - if debug: - bottle.debug(True) - run(app, host=host, port=port, reloader=True, server="cherrypy") - else: - run(app, host=host, port=port, server="cherrypy") - - - -if __name__ == "__main__": - clize.run(runserver) +main() \ No newline at end of file diff --git a/__init__.py b/zerobin/__init__.py similarity index 100% rename from __init__.py rename to zerobin/__init__.py diff --git a/src/default_settings.py b/zerobin/default_settings.py similarity index 60% rename from src/default_settings.py rename to zerobin/default_settings.py index b4a45cb..a7ed6ca 100644 --- a/src/default_settings.py +++ b/zerobin/default_settings.py @@ -2,21 +2,41 @@ # -*- coding: utf-8 -*- # vim: ai ts=4 sts=4 et sw=4 + +######## NOT SETTINGS, JUST BOILER PLATE ############## import os import math -ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -STATIC_FILES_ROOT = os.path.join(ROOT_DIR, 'static') +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +LIBS_DIR = os.path.join(os.path.dirname(ROOT_DIR), 'libs') + +######## END OF BOILER PLATE ############## # debug will get you error message 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 = os.path.join(ROOT_DIR, 'static') + # absolute path where the paste files should be store # default in projectdirectory/static/content/ # use "/" even under Windows PASTE_FILES_ROOT = os.path.join(STATIC_FILES_ROOT, 'content') +# 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 a template, create a new dir, write the +# template with the same name as the one you want to override in it +# then add the dir path at the top of this tuple +TEMPLATE_DIRS = ( + os.path.join(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 diff --git a/src/paste.py b/zerobin/paste.py similarity index 77% rename from src/paste.py rename to zerobin/paste.py index 3cbb86a..b52ace9 100644 --- a/src/paste.py +++ b/zerobin/paste.py @@ -2,11 +2,10 @@ import os import hashlib -import json from datetime import datetime, timedelta -import settings +from utils import settings class Paste(object): @@ -25,12 +24,10 @@ class Paste(object): def __init__(self, uuid=None, content=None, - expiration=None, - comments=None): + expiration=None): self.content = content self.expiration = expiration - self.comments = comments if isinstance(self.content, unicode): self.content = self.content.encode('utf8') @@ -95,7 +92,6 @@ class Paste(object): uuid = os.path.basename(path) expiration = paste.next().strip() content = paste.next().strip() - comments = paste.read()[:-1] # remove the last coma if "burn_after_reading" not in str(expiration): expiration = datetime.strptime(expiration, '%Y-%m-%d %H:%M:%S.%f') @@ -104,8 +100,7 @@ class Paste(object): except (IOError, OSError): raise ValueError(u'Can not open paste from file %s' % path) - return Paste(uuid=uuid, comments=comments, - expiration=expiration, content=content) + return Paste(uuid=uuid, expiration=expiration, content=content) @classmethod @@ -120,9 +115,6 @@ class Paste(object): def save(self): """ Save the content of this paste to a file. - - If comments are passed, they are expected to be serialized - already. """ head, tail = self.uuid[:2], self.uuid[2:4] @@ -157,8 +149,6 @@ class Paste(object): with open(self.path, 'w') as f: f.write(unicode(self.expiration) + '\n') f.write(self.content + '\n') - if self.comments: - f.write(self.comments) return self @@ -169,26 +159,3 @@ class Paste(object): """ os.remove(self.path) - - @classmethod - def add_comment_to(cls, uuid, **comment): - """ - Append a comment to the file of the paste with the given uuid. - The comment is serialized to json, and a comma is added at the - end of it. Then the result is appended to the paste file. - This way we can add sequencially all comments to the file by just - appending to it, and then extracting the comment by selecting - this big blob of text, adding [] around it and use it as a json list - with no extra processing. - """ - with open(cls.get_path(uuid), 'a') as f: - f.write(json.dumps(comment) + u',\n') - - - def add_comment(self, **comment): - """ - Append a comment to the file of this paste. - - Use add_comment_to() - """ - self.add_comment_to(self.uuid, **comment) diff --git a/zerobin/routes.py b/zerobin/routes.py new file mode 100644 index 0000000..bc9fa3f --- /dev/null +++ b/zerobin/routes.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# vim: ai ts=4 sts=4 et sw=4 + +""" + Main script including controller, rooting, dependancy management, and + server run. +""" + +import thread + +from datetime import datetime, timedelta + +# add project dir and libs dir to the PYTHON PATH to ensure they are +# importable +from utils import settings + +import bottle +from bottle import (Bottle, run, static_file, view, request) + +import clize + +from paste import Paste +from utils import drop_privileges, dmerge + + +app = Bottle() +GLOBAL_CONTEXT = { + 'settings': settings +} + + +@app.route('/') +@view('home') +def index(): + return GLOBAL_CONTEXT + + +@app.route('/paste/create', method='POST') +def create_paste(): + + try: + content = unicode(request.forms.get('content', ''), 'utf8') + except UnicodeDecodeError: + return {'status': 'error', + 'message': u"Encoding error: the paste couldn't be saved."} + + if '{"iv":' not in content: # reject silently non encrypted content + return '' + + if content: + # check size of the paste. if more than settings return error without saving paste. + # prevent from unusual use of the system. + # need to be improved + if len(content) < settings.MAX_SIZE: + expiration = request.forms.get('expiration', u'burn_after_reading') + paste = Paste(expiration=expiration, content=content) + paste.save() + return {'status': 'ok', + 'paste': paste.uuid} + + return {'status': 'error', + 'message': u"Serveur error: the paste couldn't be saved. Please try later."} + + +@app.route('/paste/:paste_id') +@view('paste') +def display_paste(paste_id): + + now = datetime.now() + keep_alive = False + try: + paste = Paste.load(paste_id) + # Delete the paste if it expired: + if 'burn_after_reading' in str(paste.expiration): + # burn_after_reading contains the paste creation date + # if this read appends 10 seconds after the creation date + # we don't delete the paste because it means it's the redirection + # to the paste that happens during the paste creation + try: + keep_alive = paste.expiration.split('#')[1] + keep_alive = datetime.strptime(keep_alive, '%Y-%m-%d %H:%M:%S.%f') + keep_alive = now < keep_alive + timedelta(seconds=10) + except IndexError: + keep_alive = False + if not keep_alive: + paste.delete() + + elif paste.expiration < now: + paste.delete() + raise ValueError() + + except (TypeError, ValueError): + #abort(404, u"This paste doesn't exist or has expired") + return error404(ValueError) + + context = {'paste': paste, 'keep_alive': keep_alive} + return dmerge(context, GLOBAL_CONTEXT) + + +@app.error(404) +@view('404') +def error404(code): + return GLOBAL_CONTEXT + + +@clize.clize +def runserver(host='', port='', debug=None, serve_static='', user='', + group='', settings_file=''): + + # merge the settings + if settings_file: + settings.update_with_file(settings_file) + + settings.HOST = host or settings.HOST + settings.PORT = port or settings.PORT + settings.DEBUG = debug if debug is not None else settings.DEBUG + settings.STATIC_FILES_ROOT = serve_static or settings.STATIC_FILES_ROOT + settings.USER = user or settings.USER + settings.GROUP = group or settings.GROUP + + # make sure the templates can be loaded + for d in reversed(settings.TEMPLATE_DIRS): + bottle.TEMPLATE_PATH.insert(0, d) + + if serve_static: + @app.route('/static/') + def server_static(filename): + return static_file(filename, root=settings.STATIC_FILES_ROOT) + + thread.start_new_thread(drop_privileges, (settings.USER, settings.GROUP)) + + if settings.DEBUG: + bottle.debug(True) + run(app, host=settings.HOST, port=settings.PORT, reloader=True, server="cherrypy") + else: + run(app, host=settings.HOST, port=settings.PORT, server="cherrypy") + + +def main(): + clize.run(runserver) diff --git a/static/css/bootstrap-responsive.css b/zerobin/static/css/bootstrap-responsive.css similarity index 100% rename from static/css/bootstrap-responsive.css rename to zerobin/static/css/bootstrap-responsive.css diff --git a/static/css/bootstrap-responsive.min.css b/zerobin/static/css/bootstrap-responsive.min.css similarity index 100% rename from static/css/bootstrap-responsive.min.css rename to zerobin/static/css/bootstrap-responsive.min.css diff --git a/static/css/bootstrap.css b/zerobin/static/css/bootstrap.css similarity index 100% rename from static/css/bootstrap.css rename to zerobin/static/css/bootstrap.css diff --git a/static/css/bootstrap.min.css b/zerobin/static/css/bootstrap.min.css similarity index 100% rename from static/css/bootstrap.min.css rename to zerobin/static/css/bootstrap.min.css diff --git a/static/css/prettify.css b/zerobin/static/css/prettify.css similarity index 100% rename from static/css/prettify.css rename to zerobin/static/css/prettify.css diff --git a/static/css/style.css b/zerobin/static/css/style.css similarity index 100% rename from static/css/style.css rename to zerobin/static/css/style.css diff --git a/static/css/sunburst.css b/zerobin/static/css/sunburst.css similarity index 100% rename from static/css/sunburst.css rename to zerobin/static/css/sunburst.css diff --git a/static/img/favicon.ico b/zerobin/static/img/favicon.ico similarity index 100% rename from static/img/favicon.ico rename to zerobin/static/img/favicon.ico diff --git a/static/img/glyphicons-halflings-white.png b/zerobin/static/img/glyphicons-halflings-white.png similarity index 100% rename from static/img/glyphicons-halflings-white.png rename to zerobin/static/img/glyphicons-halflings-white.png diff --git a/static/img/glyphicons-halflings.png b/zerobin/static/img/glyphicons-halflings.png similarity index 100% rename from static/img/glyphicons-halflings.png rename to zerobin/static/img/glyphicons-halflings.png diff --git a/static/img/gradient.png b/zerobin/static/img/gradient.png similarity index 100% rename from static/img/gradient.png rename to zerobin/static/img/gradient.png diff --git a/static/img/ico.png b/zerobin/static/img/ico.png similarity index 100% rename from static/img/ico.png rename to zerobin/static/img/ico.png diff --git a/static/js/ZeroClipboard.js b/zerobin/static/js/ZeroClipboard.js similarity index 100% rename from static/js/ZeroClipboard.js rename to zerobin/static/js/ZeroClipboard.js diff --git a/static/js/ZeroClipboard.swf b/zerobin/static/js/ZeroClipboard.swf similarity index 100% rename from static/js/ZeroClipboard.swf rename to zerobin/static/js/ZeroClipboard.swf diff --git a/static/js/behavior.js b/zerobin/static/js/behavior.js similarity index 100% rename from static/js/behavior.js rename to zerobin/static/js/behavior.js diff --git a/static/js/jquery-1.7.2.min.js b/zerobin/static/js/jquery-1.7.2.min.js similarity index 100% rename from static/js/jquery-1.7.2.min.js rename to zerobin/static/js/jquery-1.7.2.min.js diff --git a/static/js/jquery.elastic.source.js b/zerobin/static/js/jquery.elastic.source.js similarity index 100% rename from static/js/jquery.elastic.source.js rename to zerobin/static/js/jquery.elastic.source.js diff --git a/static/js/lzw.js b/zerobin/static/js/lzw.js similarity index 100% rename from static/js/lzw.js rename to zerobin/static/js/lzw.js diff --git a/static/js/prettify.min.js b/zerobin/static/js/prettify.min.js similarity index 100% rename from static/js/prettify.min.js rename to zerobin/static/js/prettify.min.js diff --git a/static/js/sjcl.js b/zerobin/static/js/sjcl.js similarity index 100% rename from static/js/sjcl.js rename to zerobin/static/js/sjcl.js diff --git a/static/js/vizhash.min.js b/zerobin/static/js/vizhash.min.js similarity index 100% rename from static/js/vizhash.min.js rename to zerobin/static/js/vizhash.min.js diff --git a/zerobin/utils.py b/zerobin/utils.py new file mode 100644 index 0000000..b11905a --- /dev/null +++ b/zerobin/utils.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- + +import time +import os +import glob +import tempfile +import sys + +import default_settings +sys.path.append(default_settings.LIBS_DIR) + +try: + from privilege import drop_privileges_permanently, coerce_user, coerce_group +except (AttributeError): + pass # privilege does't work on several plateform + + +def drop_privileges(user=None, group=None, wait=5): + """ + Try to set the process user and group to another one. + If no group is provided, it's set to the same as the user. + You can wait for a certain time before doing so. + """ + if wait: + time.sleep(wait) + if user: + group = group or user + try: + user = coerce_user(user) + group = coerce_group(group) + + lock_files = glob.glob(os.path.join(tempfile.gettempdir(), + 'bottle.*.lock')) + for lock_file in lock_files: + os.chown(lock_file, user, group) + + drop_privileges_permanently(user, group, ()) + except Exception: + print "Failed to drop privileges. Running with current user." + + +def dmerge(*args): + """ + Return new directionay being the sum of all merged dictionaries passed + as arguments + """ + + dictionary = {} + + for arg in args: + dictionary.update(arg) + + return dictionary + + + + +class SettingsContainer(object): + """ + Singleton containing the settings for the whole app + """ + + _instance = None + + def __new__(cls, *args, **kwargs): + + if not cls._instance: + cls._instance = super(SettingsContainer, cls).__new__(cls, *args, + **kwargs) + cls._instance.update_with_module(default_settings) + return cls._instance + + + def update_with_module(self, module): + """ + Update settings with values from the given module. + (Taking only variable with uppercased name) + """ + for name, value in module.__dict__.iteritems(): + if name.isupper(): + setattr(self, name, value) + return self + + + @classmethod + def from_module(cls, module): + """ + Create an instance of SettingsContainer with values based + on the one in the passed module. + """ + settings = cls() + settings.update_with_module(module) + return settings + + + def update_with_file(self, filepath): + """ + Update settings with values from the given module file. + User update_with_module() behind the scene + """ + sys.path.insert(0, os.path.dirname(filepath)) + module_name = os.path.splitext(os.path.basename(filepath))[0] + return self.update_with_module(__import__(module_name)) + + +settings = SettingsContainer() diff --git a/views/404.tpl b/zerobin/views/404.tpl similarity index 100% rename from views/404.tpl rename to zerobin/views/404.tpl diff --git a/views/base.tpl b/zerobin/views/base.tpl similarity index 90% rename from views/base.tpl rename to zerobin/views/base.tpl index a8ee95e..8af1f1c 100644 --- a/views/base.tpl +++ b/zerobin/views/base.tpl @@ -14,11 +14,24 @@ - + + + + diff --git a/views/home.tpl b/zerobin/views/home.tpl similarity index 100% rename from views/home.tpl rename to zerobin/views/home.tpl diff --git a/views/paste.tpl b/zerobin/views/paste.tpl similarity index 100% rename from views/paste.tpl rename to zerobin/views/paste.tpl