onionshare/onionshare/onionshare.py

386 lines
12 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
import os, sys, subprocess, time, hashlib, platform, json, locale, socket, argparse, Queue, inspect, base64, mimetypes, hmac
2014-05-20 19:44:00 +00:00
from random import randint
from functools import wraps
from itertools import izip
2014-05-20 23:02:33 +00:00
from stem.control import Controller
from stem import SocketError
from flask import Flask, Markup, Response, request, make_response, send_from_directory, render_template_string, abort
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
2014-06-05 16:11:21 -07:00
def random_string(num_bytes):
b = os.urandom(num_bytes)
h = hashlib.sha256(b).digest()[:16]
return base64.b32encode(h).lower().replace('=','')
2014-06-25 14:42:39 -07:00
def get_platform():
p = platform.system()
if p == 'Linux' and platform.uname()[0:2] == ('Linux', 'amnesia'):
p = 'Tails'
return p
2014-06-05 16:11:21 -07:00
# 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
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)
2014-06-25 14:42:39 -07:00
# 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
})
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])
@app.route("/<slug_candidate>")
def index(slug_candidate):
2014-06-24 15:08:39 -04:00
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),
filehash=filehash,
filesize=filesize,
filesize_human=human_readable_filesize(filesize),
strings=strings
)
@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
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)
2014-08-10 10:44:06 +02:00
# 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
2014-05-20 19:44:00 +00:00
@app.errorhandler(404)
def page_not_found(e):
2014-06-24 15:08:39 -04:00
global REQUEST_OTHER, onionshare_dir
2014-06-13 00:16:22 -04:00
add_request(REQUEST_OTHER, request.path)
return render_template_string(open('{0}/404.html'.format(onionshare_dir)).read())
2014-06-10 21:55:58 +00:00
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]
2014-06-05 16:11:21 -07:00
return strings
def translated(k):
return strings[k].encode("utf-8")
2014-06-05 16:11:21 -07:00
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)
2014-06-05 16:11:21 -07:00
return filehash, filesize
2014-06-05 16:11:21 -07:00
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()
2014-06-05 16:11:21 -07:00
return port
2014-05-20 19:44:00 +00:00
2014-06-05 16:11:21 -07:00
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)
2014-06-05 16:11:21 -07:00
# 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))
2014-06-05 16:11:21 -07:00
controller.authenticate()
# set up hidden service
controller.set_options([
('HiddenServiceDir', hidserv_dir),
2014-06-05 16:11:21 -07:00
('HiddenServicePort', '80 127.0.0.1:{0}'.format(port))
])
# figure out the .onion hostname
hostname_file = '{0}/hostname'.format(hidserv_dir)
onion_host = open(hostname_file, 'r').read().strip()
2014-06-05 16:11:21 -07:00
return onion_host
def tails_root():
# if running in Tails and as root, do only the things that require root
if get_platform() == 'Tails' and is_root():
parser = argparse.ArgumentParser()
parser.add_argument('port', nargs=1, help=translated("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.exit(-1)
# open hole in firewall
subprocess.call(['/sbin/iptables', '-I', 'OUTPUT', '-o', 'lo', '-p', 'tcp', '--dport', str(port), '-j', 'ACCEPT'])
# start hidden service
onion_host = start_hidden_service(port)
sys.stdout.write(onion_host)
sys.stdout.flush()
# close hole in firewall on shutdown
import signal
def handler(signum = None, frame = None):
subprocess.call(['/sbin/iptables', '-D', 'OUTPUT', '-o', 'lo', '-p', 'tcp', '--dport', str(port), '-j', 'ACCEPT'])
sys.exit()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT]:
signal.signal(sig, handler)
# stay open until killed
while True:
time.sleep(1)
def register_cleanup_handler(directory):
import signal
def handler(signum = None, frame = None):
subprocess.call(['rm','-r',directory])
sys.exit()
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGHUP, signal.SIGQUIT]:
signal.signal(sig, handler)
2014-06-05 16:11:21 -07:00
def main():
load_strings()
tails_root()
2014-06-10 21:55:58 +00:00
# parse arguments
2014-06-05 16:11:21 -07:00
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"))
2014-06-05 16:11:21 -07:00
args = parser.parse_args()
2014-06-05 16:11:21 -07:00
filename = os.path.abspath(args.filename[0])
local_only = bool(args.local_only)
debug = bool(args.debug)
if debug:
debug_mode()
2014-06-24 15:08:39 -04:00
global stay_open
stay_open = bool(args.stay_open)
2014-06-05 16:11:21 -07:00
if not (filename and os.path.isfile(filename)):
sys.exit(translated("not_a_file").format(filename))
2014-06-05 16:11:21 -07:00
filename = os.path.abspath(filename)
2014-06-05 16:11:21 -07:00
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/gksudo', '-D', 'OnionShare', '--', '/usr/bin/onionshare', str(port)], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
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])
2014-06-05 16:11:21 -07:00
# startup
print translated("calculating_sha1")
2014-06-05 16:11:21 -07:00
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)
2014-05-20 19:44:00 +00:00
print ''
print translated("ctrlc_to_stop")
2014-06-05 16:11:21 -07:00
2014-05-20 19:44:00 +00:00
# start the web server
2014-06-05 16:11:21 -07:00
app.run(port=port)
2014-05-20 19:44:00 +00:00
print '\n'
2014-06-05 16:11:21 -07:00
if __name__ == '__main__':
main()