[Pkg-anonymity-tools] [onionshare] 26/140: Split the increasingly-sprawly onionshare module into different modules:
Ulrike Uhlig
u-guest at moszumanska.debian.org
Mon Sep 29 20:33:44 UTC 2014
This is an automated email from the git hooks/post-receive script.
u-guest pushed a commit to branch master
in repository onionshare.
commit 54a37ee28e1fd4c61e6cc18e2c7caf407b1cee24
Author: Micah Lee <micah at micahflee.com>
Date: Tue Aug 26 18:22:59 2014 -0700
Split the increasingly-sprawly onionshare module into different modules:
onionshare: the main business logic
helpers: helper function used in multiple modules
strings: handles all localized strings
web: the flask web server
---
onionshare/helpers.py | 66 +++++++
onionshare/onionshare.py | 439 ++++++++++++-----------------------------------
onionshare/strings.py | 22 +++
onionshare/web.py | 134 +++++++++++++++
4 files changed, 333 insertions(+), 328 deletions(-)
diff --git a/onionshare/helpers.py b/onionshare/helpers.py
new file mode 100644
index 0000000..83e04d7
--- /dev/null
+++ b/onionshare/helpers.py
@@ -0,0 +1,66 @@
+import os, inspect, hashlib, base64, hmac, platform
+from itertools import izip
+
+def get_platform():
+ p = platform.system()
+ if p == 'Linux' and platform.uname()[0:2] == ('Linux', 'amnesia'):
+ p = 'Tails'
+ return p
+
+def get_onionshare_dir():
+ if get_platform() == 'Darwin':
+ onionshare_dir = os.path.dirname(__file__)
+ else:
+ onionshare_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
+ return onionshare_dir
+
+def constant_time_compare(val1, val2):
+ _builtin_constant_time_compare = getattr(hmac, 'compare_digest', None)
+ if _builtin_constant_time_compare is not None:
+ return _builtin_constant_time_compare(val1, val2)
+
+ len_eq = len(val1) == len(val2)
+ if len_eq:
+ result = 0
+ left = val1
+ else:
+ result = 1
+ left = val2
+ for x, y in izip(bytearray(left), bytearray(val2)):
+ result |= x ^ y
+ return result == 0
+
+def random_string(num_bytes):
+ b = os.urandom(num_bytes)
+ h = hashlib.sha256(b).digest()[:16]
+ return base64.b32encode(h).lower().replace('=','')
+
+def human_readable_filesize(b):
+ thresh = 1024.0
+ if b < thresh:
+ return '{0} B'.format(b)
+ units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']
+ u = 0
+ b /= thresh
+ while b >= thresh:
+ b /= thresh
+ u += 1
+ return '{0} {1}'.format(round(b, 1), units[u])
+
+def is_root():
+ return os.geteuid() == 0
+
+def file_crunching(filename):
+ # calculate filehash, file size
+ BLOCKSIZE = 65536
+ hasher = hashlib.sha1()
+ with open(filename, 'rb') as f:
+ buf = f.read(BLOCKSIZE)
+ while len(buf) > 0:
+ hasher.update(buf)
+ buf = f.read(BLOCKSIZE)
+ filehash = hasher.hexdigest()
+ filesize = os.path.getsize(filename)
+ return filehash, filesize
+
+
diff --git a/onionshare/onionshare.py b/onionshare/onionshare.py
index def2bd9..f47a4c4 100644
--- a/onionshare/onionshare.py
+++ b/onionshare/onionshare.py
@@ -1,304 +1,111 @@
# -*- coding: utf-8 -*-
-import os, sys, subprocess, time, hashlib, platform, json, locale, socket, argparse, Queue, inspect, base64, mimetypes, hmac, shutil
-from itertools import izip
+import os, sys, subprocess, time, argparse, inspect, shutil, socket
from stem.control import Controller
from stem import SocketError
-from flask import Flask, Response, request, render_template_string, abort
+import strings, helpers, web
class NoTor(Exception): pass
-
-def constant_time_compare(val1, val2):
- _builtin_constant_time_compare = getattr(hmac, 'compare_digest', None)
- if _builtin_constant_time_compare is not None:
- return _builtin_constant_time_compare(val1, val2)
-
- len_eq = len(val1) == len(val2)
- if len_eq:
- result = 0
- left = val1
- else:
- result = 1
- left = val2
- for x, y in izip(bytearray(left), bytearray(val2)):
- result |= x ^ y
- return result == 0
-
-def random_string(num_bytes):
- b = os.urandom(num_bytes)
- h = hashlib.sha256(b).digest()[:16]
- return base64.b32encode(h).lower().replace('=','')
-
-def get_platform():
- p = platform.system()
- if p == 'Linux' and platform.uname()[0:2] == ('Linux', 'amnesia'):
- p = 'Tails'
- return p
-
-# information about the file
-filename = filesize = filehash = None
-def set_file_info(new_filename, new_filehash, new_filesize):
- global filename, filehash, filesize
- filename = new_filename
- filehash = new_filehash
- filesize = new_filesize
-
-# automatically close
-stay_open = False
-def set_stay_open(new_stay_open):
- global stay_open
- stay_open = new_stay_open
-
-def get_stay_open():
- return stay_open
-
-app = Flask(__name__)
-
-def debug_mode():
- import logging
- global app
-
- if platform.system() == 'Windows':
- temp_dir = os.environ['Temp'].replace('\\', '/')
- else:
- temp_dir = '/tmp/'
-
- log_handler = logging.FileHandler('{0}/onionshare_server.log'.format(temp_dir))
- log_handler.setLevel(logging.WARNING)
- app.logger.addHandler(log_handler)
-
-# get path of onioshare directory
-if get_platform() == 'Darwin':
- onionshare_dir = os.path.dirname(__file__)
-else:
- onionshare_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
-
-strings = {}
-slug = random_string(16)
-download_count = 0
-
-REQUEST_LOAD = 0
-REQUEST_DOWNLOAD = 1
-REQUEST_PROGRESS = 2
-REQUEST_OTHER = 3
-q = Queue.Queue()
-
-def add_request(type, path, data=None):
- global q
- q.put({
- 'type': type,
- 'path': path,
- 'data': data
- })
-
-cleanup_q = Queue.Queue()
-def register_cleanup_handler(directory):
- global cleanup_q
- def handler(signum = None, frame = None):
- shutil.rmtree(directory)
- cleanup_q.put(handler)
-
-def execute_cleanup_handlers():
- global cleanup_q
- try:
- while True:
- handler = cleanup_q.get(False)
- handler()
- except Queue.Empty:
- pass
-
-def human_readable_filesize(b):
- thresh = 1024.0
- if b < thresh:
- return '{0} B'.format(b)
- units = ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB']
- u = 0
- b /= thresh
- while b >= thresh:
- b /= thresh
- u += 1
- return '{0} {1}'.format(round(b, 1), units[u])
-
- at app.route("/<slug_candidate>")
-def index(slug_candidate):
- global filename, filesize, filehash, slug, strings, REQUEST_LOAD, onionshare_dir
-
- if not constant_time_compare(slug.encode('ascii'), slug_candidate.encode('ascii')):
- abort(404)
-
- add_request(REQUEST_LOAD, request.path)
- return render_template_string(
- open('{0}/index.html'.format(onionshare_dir)).read(),
- slug=slug,
- filename=os.path.basename(filename).decode("utf-8"),
- filehash=filehash,
- filesize=filesize,
- filesize_human=human_readable_filesize(filesize),
- strings=strings
- )
-
- at app.route("/<slug_candidate>/download")
-def download(slug_candidate):
- global filename, filesize, q, download_count
- global REQUEST_DOWNLOAD, REQUEST_PROGRESS
-
- if not constant_time_compare(slug.encode('ascii'), slug_candidate.encode('ascii')):
- abort(404)
-
- # each download has a unique id
- download_id = download_count
- download_count += 1
-
- # prepare some variables to use inside generate() function below
- # which is outsie of the request context
- shutdown_func = request.environ.get('werkzeug.server.shutdown')
- path = request.path
-
- # tell GUI the download started
- add_request(REQUEST_DOWNLOAD, path, { 'id':download_id })
-
- dirname = os.path.dirname(filename)
- basename = os.path.basename(filename)
-
- def generate():
- chunk_size = 102400 # 100kb
-
- fp = open(filename, 'rb')
- done = False
- while not done:
- chunk = fp.read(102400)
- if chunk == '':
- done = True
+class TailsError(Exception): pass
+
+class OnionShare(object):
+ def __init__(self, debug=False, local_only=False, stay_open=False):
+ # debug mode
+ if debug:
+ web.debug_mode()
+
+ # do not use tor -- for development
+ self.local_only = local_only
+
+ # automatically close when download is finished
+ self.stay_open = stay_open
+
+ # list of hidden service dirs to cleanup
+ self.hidserv_dirs = []
+
+ # choose a random port
+ self.choose_port()
+ self.local_host = "127.0.0.1:{0}".format(self.port)
+
+ def cleanup(self):
+ for d in self.hidserv_dirs:
+ shutil.rmtree(d)
+
+ def choose_port(self):
+ # let the OS choose a port
+ tmpsock = socket.socket()
+ tmpsock.bind(("127.0.0.1", 0))
+ self.port = tmpsock.getsockname()[1]
+ tmpsock.close()
+
+ def start_hidden_service(self):
+ if helpers.get_platform() == 'Tails':
+ # in Tails, start the hidden service in a root process
+ p = subprocess.Popen(['/usr/bin/sudo', '--', '/usr/bin/onionshare', str(app.port)], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ stdout = p.stdout.read(22) # .onion URLs are 22 chars long
+
+ if stdout:
+ self.onion_host = stdout
else:
- yield chunk
-
- # tell GUI the progress
- downloaded_bytes = fp.tell()
- percent = round((1.0 * downloaded_bytes / filesize) * 100, 2);
- sys.stdout.write("\r{0}, {1}% ".format(human_readable_filesize(downloaded_bytes), percent))
- sys.stdout.flush()
- add_request(REQUEST_PROGRESS, path, { 'id':download_id, 'bytes':downloaded_bytes })
-
- fp.close()
- sys.stdout.write("\n")
-
- # download is finished, close the server
- global stay_open
- if not stay_open:
- print translated("closing_automatically")
- if shutdown_func is None:
- raise RuntimeError('Not running with the Werkzeug Server')
- shutdown_func()
-
- r = Response(generate())
- r.headers.add('Content-Length', filesize)
- r.headers.add('Content-Disposition', 'attachment', filename=basename)
- # guess content type
- (content_type, _) = mimetypes.guess_type(basename, strict=False)
- if content_type is not None:
- r.headers.add('Content-Type', content_type)
- return r
-
- at app.errorhandler(404)
-def page_not_found(e):
- global REQUEST_OTHER, onionshare_dir
- add_request(REQUEST_OTHER, request.path)
- return render_template_string(open('{0}/404.html'.format(onionshare_dir)).read())
-
-def is_root():
- return os.geteuid() == 0
-
-def load_strings(default="en"):
- global strings
- try:
- translated = json.loads(open('{0}/strings.json'.format(os.getcwd())).read())
- except IOError:
- translated = json.loads(open('{0}/strings.json'.format(onionshare_dir)).read())
- strings = translated[default]
- lc, enc = locale.getdefaultlocale()
- if lc:
- lang = lc[:2]
- if lang in translated:
- # if a string doesn't exist, fallback to English
- for key in translated[default]:
- if key in translated[lang]:
- strings[key] = translated[lang][key]
- return strings
-
-def translated(k):
- return strings[k].encode("utf-8")
-
-def file_crunching(filename):
- # calculate filehash, file size
- BLOCKSIZE = 65536
- hasher = hashlib.sha1()
- with open(filename, 'rb') as f:
- buf = f.read(BLOCKSIZE)
- while len(buf) > 0:
- hasher.update(buf)
- buf = f.read(BLOCKSIZE)
- filehash = hasher.hexdigest()
- filesize = os.path.getsize(filename)
- return filehash, filesize
-
-def choose_port():
- # let the OS choose a port
- tmpsock = socket.socket()
- tmpsock.bind(("127.0.0.1", 0))
- port = tmpsock.getsockname()[1]
- tmpsock.close()
- return port
-
-def start_hidden_service(port):
- # come up with a hidden service directory name
- hidserv_dir_rand = random_string(8)
- if get_platform() == "Windows":
- if 'Temp' in os.environ:
- temp = os.environ['Temp'].replace('\\', '/')
- else:
- temp = 'C:/tmp'
- hidserv_dir = "{0}/onionshare_{1}".format(temp, hidserv_dir_rand)
- else:
- hidserv_dir = "/tmp/onionshare_{0}".format(hidserv_dir_rand)
-
- register_cleanup_handler(hidserv_dir)
-
- # connect to the tor controlport
- controlports = [9051, 9151]
- controller = False
- for controlport in controlports:
- try:
- controller = Controller.from_port(port=controlport)
- except SocketError:
- pass
- if not controller:
- raise NoTor(translated("cant_connect_ctrlport").format(controlports))
- controller.authenticate()
-
- # set up hidden service
- controller.set_options([
- ('HiddenServiceDir', hidserv_dir),
- ('HiddenServicePort', '80 127.0.0.1:{0}'.format(port))
- ])
+ if root_p.poll() == -1:
+ raise TailsError(o.stderr.read())
+ else:
+ raise TailsError(strings._("error_tails_unknown_root"))
- # figure out the .onion hostname
- hostname_file = '{0}/hostname'.format(hidserv_dir)
- onion_host = open(hostname_file, 'r').read().strip()
+ else:
+ if self.local_only:
+ self.onion_host = '127.0.0.1:{0}'.format(self.port)
- return onion_host
+ else:
+ print strings._("connecting_ctrlport").format(self.port)
+
+ # come up with a hidden service directory name
+ hidserv_dir_rand = helpers.random_string(8)
+ if helpers.get_platform() == "Windows":
+ if 'Temp' in os.environ:
+ temp = os.environ['Temp'].replace('\\', '/')
+ else:
+ temp = 'C:/tmp'
+ hidserv_dir = "{0}/onionshare_{1}".format(temp, hidserv_dir_rand)
+ else:
+ hidserv_dir = "/tmp/onionshare_{0}".format(hidserv_dir_rand)
+
+ self.hidserv_dirs.append(hidserv_dir)
+
+ # connect to the tor controlport
+ controlports = [9051, 9151]
+ controller = False
+ for controlport in controlports:
+ try:
+ controller = Controller.from_port(port=controlport)
+ except SocketError:
+ pass
+ if not controller:
+ raise NoTor(strings._("cant_connect_ctrlport").format(controlports))
+ controller.authenticate()
+
+ # set up hidden service
+ controller.set_options([
+ ('HiddenServiceDir', hidserv_dir),
+ ('HiddenServicePort', '80 127.0.0.1:{0}'.format(self.port))
+ ])
+
+ # figure out the .onion hostname
+ hostname_file = '{0}/hostname'.format(hidserv_dir)
+ self.onion_host = open(hostname_file, 'r').read().strip()
def tails_root():
# if running in Tails and as root, do only the things that require root
- if get_platform() == 'Tails' and is_root():
+ if helpers.get_platform() == 'Tails' and helpers.is_root():
parser = argparse.ArgumentParser()
- parser.add_argument('port', nargs=1, help=translated("help_tails_port"))
+ parser.add_argument('port', nargs=1, help=strings._("help_tails_port"))
args = parser.parse_args()
try:
port = int(args.port[0])
except ValueError:
- sys.stderr.write('{0}\n'.format(translated("error_tails_invalid_port")))
+ sys.stderr.write('{0}\n'.format(strings._("error_tails_invalid_port")))
sys.exit(-1)
# open hole in firewall
@@ -322,74 +129,50 @@ def tails_root():
time.sleep(1)
def main():
- load_strings()
+ strings.load_strings()
tails_root()
# parse arguments
parser = argparse.ArgumentParser()
- parser.add_argument('--local-only', action='store_true', dest='local_only', help=translated("help_local_only"))
- parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=translated("help_stay_open"))
- parser.add_argument('--debug', action='store_true', dest='debug', help=translated("help_debug"))
- parser.add_argument('filename', nargs=1, help=translated("help_filename"))
+ parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
+ parser.add_argument('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
+ parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
+ parser.add_argument('filename', nargs=1, help=strings._("help_filename"))
args = parser.parse_args()
filename = os.path.abspath(args.filename[0])
local_only = bool(args.local_only)
debug = bool(args.debug)
-
- if debug:
- debug_mode()
-
- global stay_open
stay_open = bool(args.stay_open)
if not (filename and os.path.isfile(filename)):
- sys.exit(translated("not_a_file").format(filename))
+ sys.exit(strings._("not_a_file").format(filename))
filename = os.path.abspath(filename)
- port = choose_port()
- local_host = "127.0.0.1:{0}".format(port)
-
- if get_platform() == 'Tails':
- # if this is tails, start the root process
- root_p = subprocess.Popen(['/usr/bin/sudo', '--', '/usr/bin/onionshare', str(port)], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
- stdout = root_p.stdout.read(22) # .onion URLs are 22 chars long
-
- if stdout:
- onion_host = stdout
- else:
- if root_p.poll() == -1:
- sys.exit(root_p.stderr.read())
- else:
- sys.exit(translated("error_tails_unknown_root"))
- else:
- # if not tails, start hidden service normally
- if not local_only:
- # try starting hidden service
- print translated("connecting_ctrlport").format(port)
- try:
- onion_host = start_hidden_service(port)
- except NoTor as e:
- sys.exit(e.args[0])
+ # start the onionshare app
+ try:
+ app = OnionShare(debug, local_only, stay_open)
+ app.start_hidden_service()
+ except NoTor as e:
+ sys.exit(e.args[0])
+ except TailsError as e:
+ sys.exit(e.args[0])
# startup
- print translated("calculating_sha1")
- filehash, filesize = file_crunching(filename)
- set_file_info(filename, filehash, filesize)
- print '\n' + translated("give_this_url")
- if local_only:
- print 'http://{0}/{1}'.format(local_host, slug)
- else:
- print 'http://{0}/{1}'.format(onion_host, slug)
+ print strings._("calculating_sha1")
+ filehash, filesize = helpers.file_crunching(filename)
+ web.set_file_info(filename, filehash, filesize)
+ print '\n' + strings._("give_this_url")
+ print 'http://{0}/{1}'.format(app.onion_host, web.slug)
print ''
- print translated("ctrlc_to_stop")
+ print strings._("ctrlc_to_stop")
# start the web server
- app.run(port=port)
+ web.start(app.port, app.stay_open)
print '\n'
# shutdown
- execute_cleanup_handlers()
+ app.cleanup()
if __name__ == '__main__':
main()
diff --git a/onionshare/strings.py b/onionshare/strings.py
new file mode 100644
index 0000000..3808f93
--- /dev/null
+++ b/onionshare/strings.py
@@ -0,0 +1,22 @@
+import json, locale
+import helpers
+
+strings = {}
+
+def load_strings(default="en"):
+ global strings
+ translated = json.loads(open('{0}/strings.json'.format(helpers.get_onionshare_dir())).read())
+ strings = translated[default]
+ lc, enc = locale.getdefaultlocale()
+ if lc:
+ lang = lc[:2]
+ if lang in translated:
+ # if a string doesn't exist, fallback to English
+ for key in translated[default]:
+ if key in translated[lang]:
+ strings[key] = translated[lang][key]
+
+def translated(k):
+ return strings[k].encode("utf-8")
+
+_ = translated
diff --git a/onionshare/web.py b/onionshare/web.py
new file mode 100644
index 0000000..9f16cdc
--- /dev/null
+++ b/onionshare/web.py
@@ -0,0 +1,134 @@
+import Queue, mimetypes, platform, os, sys
+from flask import Flask, Response, request, render_template_string, abort
+
+import strings, helpers
+
+app = Flask(__name__)
+
+# information about the file
+filename = filesize = filehash = None
+def set_file_info(new_filename, new_filehash, new_filesize):
+ global filename, filehash, filesize
+ filename = new_filename
+ filehash = new_filehash
+ filesize = new_filesize
+
+REQUEST_LOAD = 0
+REQUEST_DOWNLOAD = 1
+REQUEST_PROGRESS = 2
+REQUEST_OTHER = 3
+q = Queue.Queue()
+
+def add_request(type, path, data=None):
+ global q
+ q.put({
+ 'type': type,
+ 'path': path,
+ 'data': data
+ })
+
+slug = helpers.random_string(16)
+download_count = 0
+
+stay_open = False
+def set_stay_open(new_stay_open):
+ global stay_open
+ stay_open = new_stay_open
+def get_stay_open():
+ return stay_open
+
+def debug_mode():
+ import logging
+
+ if platform.system() == 'Windows':
+ temp_dir = os.environ['Temp'].replace('\\', '/')
+ else:
+ temp_dir = '/tmp/'
+
+ log_handler = logging.FileHandler('{0}/onionshare_server.log'.format(temp_dir))
+ log_handler.setLevel(logging.WARNING)
+ app.logger.addHandler(log_handler)
+
+ at app.route("/<slug_candidate>")
+def index(slug_candidate):
+ if not helpers.constant_time_compare(slug.encode('ascii'), slug_candidate.encode('ascii')):
+ abort(404)
+
+ add_request(REQUEST_LOAD, request.path)
+ return render_template_string(
+ open('{0}/index.html'.format(helpers.get_onionshare_dir())).read(),
+ slug=slug,
+ filename=os.path.basename(filename).decode("utf-8"),
+ filehash=filehash,
+ filesize=filesize,
+ filesize_human=helpers.human_readable_filesize(filesize),
+ strings=strings.strings
+ )
+
+ at app.route("/<slug_candidate>/download")
+def download(slug_candidate):
+ global download_count
+ if not helpers.constant_time_compare(slug.encode('ascii'), slug_candidate.encode('ascii')):
+ abort(404)
+
+ # each download has a unique id
+ download_id = download_count
+ download_count += 1
+
+ # prepare some variables to use inside generate() function below
+ # which is outsie of the request context
+ shutdown_func = request.environ.get('werkzeug.server.shutdown')
+ path = request.path
+
+ # tell GUI the download started
+ add_request(REQUEST_DOWNLOAD, path, { 'id':download_id })
+
+ dirname = os.path.dirname(filename)
+ basename = os.path.basename(filename)
+
+ def generate():
+ chunk_size = 102400 # 100kb
+
+ fp = open(filename, 'rb')
+ done = False
+ while not done:
+ chunk = fp.read(102400)
+ if chunk == '':
+ done = True
+ else:
+ yield chunk
+
+ # tell GUI the progress
+ downloaded_bytes = fp.tell()
+ percent = round((1.0 * downloaded_bytes / filesize) * 100, 2);
+ sys.stdout.write("\r{0}, {1}% ".format(helpers.human_readable_filesize(downloaded_bytes), percent))
+ sys.stdout.flush()
+ add_request(REQUEST_PROGRESS, path, { 'id':download_id, 'bytes':downloaded_bytes })
+
+ fp.close()
+ sys.stdout.write("\n")
+
+ # download is finished, close the server
+ if not stay_open:
+ print strings._("closing_automatically")
+ if shutdown_func is None:
+ raise RuntimeError('Not running with the Werkzeug Server')
+ shutdown_func()
+
+ r = Response(generate())
+ r.headers.add('Content-Length', filesize)
+ r.headers.add('Content-Disposition', 'attachment', filename=basename)
+ # guess content type
+ (content_type, _) = mimetypes.guess_type(basename, strict=False)
+ if content_type is not None:
+ r.headers.add('Content-Type', content_type)
+ return r
+
+ at app.errorhandler(404)
+def page_not_found(e):
+ add_request(REQUEST_OTHER, request.path)
+ return render_template_string(open('{0}/404.html'.format(helpers.get_onionshare_dir())).read())
+
+def start(port, stay_open=False):
+ set_stay_open(stay_open)
+ app.run(port=port)
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/collab-maint/onionshare.git
More information about the Pkg-anonymity-tools
mailing list