1
0
mirror of https://github.com/Tygs/0bin.git synced 2023-08-10 21:13:00 +03:00

Porting zerobin to python 3

This commit is contained in:
sametmax
2015-05-10 19:19:02 +02:00
parent 391df055f9
commit 9b84122414
137 changed files with 22928 additions and 4370 deletions

View File

@ -1,7 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: ai ts=4 sts=4 et sw=4
# coding: utf-8
from default_settings import VERSION
from __future__ import absolute_import
from zerobin.default_settings import VERSION
__version__ = VERSION

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: ai ts=4 sts=4 et sw=4
# coding: utf-8
from __future__ import unicode_literals, absolute_import
######## NOT SETTINGS, JUST BOILER PLATE ##############

View File

@ -1,12 +1,15 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from __future__ import unicode_literals, absolute_import
import os
import hashlib
import locale
import base64
from datetime import datetime, timedelta
from utils import settings
from zerobin.utils import settings, to_ascii, as_unicode, safe_open as open
class Paste(object):
@ -18,9 +21,9 @@ class Paste(object):
DIR_CACHE = set()
DURATIONS = {
u'1_day': 24 * 3600,
u'1_month': 30 * 24 * 3600,
u'never': 365 * 24 * 3600 * 100,
'1_day': 24 * 3600,
'1_month': 30 * 24 * 3600,
'never': 365 * 24 * 3600 * 100,
}
@ -28,37 +31,33 @@ class Paste(object):
content=None, expiration=None):
self.content = content
self.expiration = expiration
if isinstance(self.content, unicode):
self.content = self.content.encode('utf8')
self.expiration = self.get_expiration(expiration)
if not uuid:
uuid = hashlib.sha1(self.content)\
.digest().encode('base64').rstrip('=\n').replace('/', '-')
if uuid_length: uuid = uuid[:uuid_length]
# generate the uuid from the decoded content by hashing it
# and turning it into base64, with some caracters strippped
uuid = hashlib.sha1(self.content.encode('utf8'))
uuid = base64.b64encode(uuid.digest()).decode()
uuid = uuid.rstrip('=\n').replace('/', '-')
if uuid_length:
uuid = uuid[:uuid_length]
self.uuid = uuid
def get_expiration(self, expiration):
"""
Return a tuple with the date at which the Paste will expire
Return a date at which the Paste will expire
or if it should be destroyed after first reading.
Do not modify the value if it's already a date object or
if it's burn_after_reading
"""
if (isinstance(expiration, datetime) or
'burn_after_reading' in str(expiration)):
return expiration
try:
return datetime.now() + timedelta(seconds=self.DURATIONS[expiration])
except KeyError:
return u'burn_after_reading'
return expiration
@classmethod
@ -93,17 +92,17 @@ class Paste(object):
given file.
"""
try:
paste = open(path)
uuid = os.path.basename(path)
expiration = paste.next().strip()
content = paste.next().strip()
if "burn_after_reading" not in str(expiration):
expiration = datetime.strptime(expiration, '%Y-%m-%d %H:%M:%S.%f')
with open(path) as paste:
uuid = os.path.basename(path)
expiration = next(paste).strip()
content = next(paste).strip()
if "burn_after_reading" not in expiration:
expiration = datetime.strptime(expiration, '%Y-%m-%d %H:%M:%S.%f')
except StopIteration:
raise TypeError(u'File %s is malformed' % path)
raise TypeError(to_ascii('File %s is malformed' % path))
except (IOError, OSError):
raise ValueError(u'Can not open paste from file %s' % path)
raise ValueError(to_ascii('Can not open paste from file %s' % path))
return Paste(uuid=uuid, expiration=expiration, content=content)
@ -119,54 +118,38 @@ class Paste(object):
def increment_counter(self):
"""
Increment pastes counter
Increment pastes counter.
It uses a lock file to prevent multi access to the file.
"""
# simple counter incrementation
# using lock file to prevent multi access to the file
# could be improved.
path = settings.PASTE_FILES_ROOT
counter_file = os.path.join(path, 'counter')
lock_file = os.path.join(path, 'counter.lock')
# TODO : change lock implementation to use the lockfile lib
# https://pypi.python.org/pypi/lockfile
# The current lock implementation sucks. It skips some increment, and
# still allows race conditions.
if not os.path.isfile(lock_file):
try:
#make lock file
flock = open(lock_file, "w")
flock.write('lock')
flock.close()
# init counter (first time)
if not os.path.isfile(counter_file):
fcounter = open(counter_file, "w")
fcounter.write('1')
fcounter.close()
# get counter value
fcounter = open(counter_file, "r")
counter_value = fcounter.read(50)
fcounter.close()
# Aquire lock file
with open(lock_file, "w") as flock:
flock.write('lock')
# Read the value from the counter
try:
counter_value = long(counter_value) + 1
except ValueError:
with open(counter_file, "r") as fcounter:
counter_value = int(fcounter.read(50)) + 1
except (ValueError, IOError, OSError):
counter_value = 1
# write new value to counter
fcounter = open(counter_file, "w")
fcounter.write(str(counter_value))
fcounter.close()
with open(counter_file, "w") as fcounter:
fcounter.write(str(counter_value))
#remove lock file
os.remove(lock_file)
finally:
if os.path.isfile(lock_file):
#remove lock file
os.remove(lock_file)
# remove lock file
os.remove(lock_file)
def save(self):
"""
@ -198,12 +181,15 @@ class Paste(object):
# add a timestamp to burn after reading to allow
# a quick period of time where you can redirect to the page without
# deleting the paste
if self.expiration == "burn_after_reading":
self.expiration = self.expiration + '#%s' % datetime.now()
if "burn_after_reading" == self.expiration:
expiration = self.expiration + '#%s' % datetime.now() # TODO: use UTC dates
expiration = self.expiration
else:
expiration = as_unicode(self.expiration)
# write the paste
with open(self.path, 'w') as f:
f.write(unicode(self.expiration) + '\n')
f.write(expiration + '\n')
f.write(self.content + '\n')
return self
@ -216,17 +202,13 @@ class Paste(object):
(must have option DISPLAY_COUNTER enabled for the pastes to be
be counted)
"""
try:
locale.setlocale(locale.LC_ALL, 'en_US')
except:
pass
counter_file = os.path.join(settings.PASTE_FILES_ROOT, 'counter')
try:
count = long(open(counter_file).read(50))
count = int(open(counter_file).read(50))
except (IOError, OSError):
count = 0
return locale.format("%d", long(count), grouping=True)
return '{0:,}'.format(count)
@property

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim: ai ts=4 sts=4 et sw=4
# coding: utf-8
from __future__ import unicode_literals, absolute_import, print_function
"""
Main script including controller, rooting, dependency management, and
@ -9,21 +10,30 @@
import os
import sys
import thread
import urlparse
try:
import thread
except ImportError:
import _thread as thread
try:
import urlparse
except ImportError:
import urllib.parse as urlparse
from datetime import datetime, timedelta
# add project dir and libs dir to the PYTHON PATH to ensure they are
# importable
from utils import settings, SettingsValidationError
from zerobin.utils import (settings, SettingsValidationError,
drop_privileges, dmerge)
import bottle
from bottle import (Bottle, run, static_file, view, request)
import clize
from paste import Paste
from utils import drop_privileges, dmerge
from zerobin.paste import Paste
app = Bottle()
@ -51,49 +61,46 @@ def create_paste():
try:
body = urlparse.parse_qs(request.body.read(int(settings.MAX_SIZE * 1.1)))
except ValueError:
return {'status': 'error',
'message': u"Wrong data payload."}
return {'status': 'error', 'message': "Wrong data payload."}
try:
content = unicode(''.join(body['content']), 'utf8')
content = "".join(x.decode('utf8') for x in body[b'content'])
except (UnicodeDecodeError, KeyError):
return {'status': 'error',
'message': u"Encoding error: the paste couldn't be saved."}
'message': "Encoding error: the paste couldn't be saved."}
if '{"iv":' not in content: # reject silently non encrypted content
return {'status': 'error',
'message': u"Wrong data payload."}
return {'status': 'error', 'message': "Wrong data payload."}
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 = body.get('expiration', [u'burn_after_reading'])[0]
paste = Paste(expiration=expiration, content=content,
uuid_length=settings.PASTE_ID_LENGTH)
paste.save()
# 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 0 < len(content) < settings.MAX_SIZE:
expiration = body.get(b'expiration', ['burn_after_reading'])[0]
paste = Paste(expiration=expiration.decode('utf8'), content=content,
uuid_length=settings.PASTE_ID_LENGTH)
paste.save()
# display counter
if settings.DISPLAY_COUNTER:
# display counter
if settings.DISPLAY_COUNTER:
#increment paste counter
paste.increment_counter()
#increment paste counter
paste.increment_counter()
# if refresh time elapsed pick up new counter value
now = datetime.now()
timeout = (GLOBAL_CONTEXT['refresh_counter']
+ timedelta(seconds=settings.REFRESH_COUNTER))
if timeout < now:
GLOBAL_CONTEXT['pastes_count'] = Paste.get_pastes_count()
GLOBAL_CONTEXT['refresh_counter'] = now
# if refresh time elapsed pick up new counter value
now = datetime.now()
timeout = (GLOBAL_CONTEXT['refresh_counter']
+ timedelta(seconds=settings.REFRESH_COUNTER))
if timeout < now:
GLOBAL_CONTEXT['pastes_count'] = Paste.get_pastes_count()
GLOBAL_CONTEXT['refresh_counter'] = now
return {'status': 'ok',
'paste': paste.uuid}
return {'status': 'ok', 'paste': paste.uuid}
return {'status': 'error',
'message': u"Serveur error: the paste couldn't be saved. "
u"Please try later."}
'message': "Serveur error: the paste couldn't be saved. "
"Please try later."}
@app.route('/paste/:paste_id')
@ -105,7 +112,7 @@ def display_paste(paste_id):
try:
paste = Paste.load(paste_id)
# Delete the paste if it expired:
if 'burn_after_reading' in str(paste.expiration):
if not isinstance(paste.expiration, datetime):
# 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
@ -179,7 +186,7 @@ def runserver(host='', port='', debug=None, user='', group='',
version=False, paste_id_length=None, server="cherrypy"):
if version:
print '0bin V%s' % settings.VERSION
print('0bin V%s' % settings.VERSION)
sys.exit(0)
settings.HOST = host or settings.HOST
@ -191,7 +198,7 @@ def runserver(host='', port='', debug=None, user='', group='',
try:
_, app = get_app(debug, settings_file, compressed_static, settings=settings)
except SettingsValidationError as err:
print >>sys.stderr, 'Configuration error: %s' % err.message
print('Configuration error: %s' % err.message, file=sys.stderr)
sys.exit(1)
thread.start_new_thread(drop_privileges, (settings.USER, settings.GROUP))

View File

@ -1,18 +1,23 @@
# -*- coding: utf-8 -*-
# coding: utf-8
from __future__ import print_function, unicode_literals, absolute_import
import time
import os
import glob
import tempfile
import sys
import codecs
import unicodedata
from functools import partial
import default_settings
from zerobin import default_settings
sys.path.append(default_settings.LIBS_DIR)
try:
from privilege import drop_privileges_permanently, coerce_user, coerce_group
from zerobin.privilege import drop_privileges_permanently, coerce_user, coerce_group
except (AttributeError):
pass # privilege does't work on several plateform
pass # privilege does't work on several plateform
try:
from runpy import run_path
@ -45,7 +50,7 @@ def drop_privileges(user=None, group=None, wait=5):
drop_privileges_permanently(user, group, ())
except Exception:
print "Failed to drop privileges. Running with current user."
print("Failed to drop privileges. Running with current user.")
def dmerge(*args):
@ -53,16 +58,14 @@ 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 SettingsValidationError(Exception): pass
class SettingsValidationError(Exception):
pass
class SettingsContainer(object):
@ -86,7 +89,7 @@ class SettingsContainer(object):
Update settings with values from the given mapping object.
(Taking only variable with uppercased name)
"""
for name, value in dict.iteritems():
for name, value in dict.items():
if name.isupper():
setattr(self, name, value)
return self
@ -121,3 +124,24 @@ class SettingsContainer(object):
settings = SettingsContainer()
def to_ascii(utext):
""" Take a unicode string and return ascii bytes.
Try to replace non ASCII char by similar ASCII char. If it can't,
replace it with "?".
"""
return unicodedata.normalize('NFKD', utext).encode('ascii', "replace")
# Make sure to always specify encoding when using open in Python 2 or 3
safe_open = partial(codecs.open, encoding="utf8")
def as_unicode(obj):
""" Return the unicode representation of an object """
try:
return unicode(obj)
except NameError:
return str(obj)

View File

@ -37,4 +37,4 @@
</form>
%rebase base settings=settings, pastes_count=pastes_count
% rebase('base', settings=settings, pastes_count=pastes_count)

View File

@ -113,8 +113,7 @@
</div><!--/span-->
<div id='main' class="span10">
%include
{{!base}}
</div><!--/span-->

View File

@ -4,7 +4,7 @@
<hr width="90%">
<dl>
<dl>
<dt>How does 0bin work?</dt>
<dd>
<p>A random key is generated and used to encrypt the paste, thanks to
@ -41,7 +41,7 @@
<dd>
<p>Read above.</p>
<p>0bin is not built, and does not aim, to protect user data - but rather the host.
If any user data is compromised, 0bin still provides the host with
If any user data is compromised, 0bin still provides the host with
plausible deniability (as they ignore the content of the pastes).</p>
<p>It would make no sense if the host was to compromise the encryption process
to read the data; in that case, they wouldn't have
@ -69,5 +69,4 @@
</div>
%rebase base settings=settings, pastes_count=pastes_count
% rebase("base", settings=settings, pastes_count=pastes_count)

View File

@ -25,4 +25,4 @@
</form>
%rebase base settings=settings, pastes_count=pastes_count
% rebase("base", settings=settings, pastes_count=pastes_count)

View File

@ -84,4 +84,4 @@
</div>
%rebase base settings=settings, pastes_count=pastes_count
% rebase("base", settings=settings, pastes_count=pastes_count)