mirror of
https://github.com/onionshare/onionshare.git
synced 2025-02-05 17:35:35 -05:00
386 lines
12 KiB
Python
386 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
import os, sys, subprocess, time, hashlib, platform, json, locale, socket, argparse, Queue, inspect, base64, mimetypes, hmac
|
|
from random import randint
|
|
from functools import wraps
|
|
from itertools import izip
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
})
|
|
|
|
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):
|
|
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)
|
|
# 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
|
|
|
|
@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))
|
|
])
|
|
|
|
# figure out the .onion hostname
|
|
hostname_file = '{0}/hostname'.format(hidserv_dir)
|
|
onion_host = open(hostname_file, 'r').read().strip()
|
|
|
|
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)
|
|
|
|
|
|
def main():
|
|
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"))
|
|
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))
|
|
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/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])
|
|
|
|
# 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 ''
|
|
print translated("ctrlc_to_stop")
|
|
|
|
# start the web server
|
|
app.run(port=port)
|
|
print '\n'
|
|
|
|
if __name__ == '__main__':
|
|
main()
|