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:
@ -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
|
||||
|
@ -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 ##############
|
||||
|
124
zerobin/paste.py
124
zerobin/paste.py
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -37,4 +37,4 @@
|
||||
</form>
|
||||
|
||||
|
||||
%rebase base settings=settings, pastes_count=pastes_count
|
||||
% rebase('base', settings=settings, pastes_count=pastes_count)
|
@ -113,8 +113,7 @@
|
||||
</div><!--/span-->
|
||||
|
||||
<div id='main' class="span10">
|
||||
|
||||
%include
|
||||
{{!base}}
|
||||
|
||||
</div><!--/span-->
|
||||
|
||||
|
@ -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)
|
||||
|
@ -25,4 +25,4 @@
|
||||
</form>
|
||||
|
||||
|
||||
%rebase base settings=settings, pastes_count=pastes_count
|
||||
% rebase("base", settings=settings, pastes_count=pastes_count)
|
||||
|
@ -84,4 +84,4 @@
|
||||
</div>
|
||||
|
||||
|
||||
%rebase base settings=settings, pastes_count=pastes_count
|
||||
% rebase("base", settings=settings, pastes_count=pastes_count)
|
||||
|
Reference in New Issue
Block a user