mirror of
https://github.com/onionshare/onionshare.git
synced 2025-06-23 14:04:26 -04:00
Removed all of the Tails-specific code. If we use ephemeral hidden services, we no longer need to touch the filesystem, and hopefully no longer need root in Tails.
This commit is contained in:
parent
d50601f5bd
commit
5d37d9b7b2
7 changed files with 71 additions and 159 deletions
|
@ -23,6 +23,7 @@
|
||||||
"help_tails_port": "Tails only: port for opening firewall, starting hidden service",
|
"help_tails_port": "Tails only: port for opening firewall, starting hidden service",
|
||||||
"help_local_only": "Do not attempt to use tor: for development only",
|
"help_local_only": "Do not attempt to use tor: for development only",
|
||||||
"help_stay_open": "Keep hidden service running after download has finished",
|
"help_stay_open": "Keep hidden service running after download has finished",
|
||||||
|
"help_transparent_torification": "My system is transparently torified",
|
||||||
"help_debug": "Log errors to disk",
|
"help_debug": "Log errors to disk",
|
||||||
"help_filename": "List of files or folders to share",
|
"help_filename": "List of files or folders to share",
|
||||||
"gui_drag_and_drop": "Drag and drop\nfiles here",
|
"gui_drag_and_drop": "Drag and drop\nfiles here",
|
||||||
|
|
|
@ -27,10 +27,7 @@ sys.setdefaultencoding("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def get_platform():
|
def get_platform():
|
||||||
p = platform.system()
|
return platform.system()
|
||||||
if p == 'Linux' and platform.uname()[0:2] == ('Linux', 'amnesia'):
|
|
||||||
p = 'Tails'
|
|
||||||
return p
|
|
||||||
|
|
||||||
if get_platform() == 'Darwin':
|
if get_platform() == 'Darwin':
|
||||||
# this is hacky, but it ultimate ends up returning the absolute path to
|
# this is hacky, but it ultimate ends up returning the absolute path to
|
||||||
|
|
|
@ -29,10 +29,6 @@ class NoTor(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TailsError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HSDirError(Exception):
|
class HSDirError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -48,7 +44,7 @@ def hsdic2list(dic):
|
||||||
|
|
||||||
|
|
||||||
class OnionShare(object):
|
class OnionShare(object):
|
||||||
def __init__(self, debug=False, local_only=False, stay_open=False):
|
def __init__(self, debug=False, local_only=False, stay_open=False, transparent_torification=False):
|
||||||
self.port = None
|
self.port = None
|
||||||
self.controller = None
|
self.controller = None
|
||||||
self.hidserv_dir = None
|
self.hidserv_dir = None
|
||||||
|
@ -63,6 +59,9 @@ class OnionShare(object):
|
||||||
# automatically close when download is finished
|
# automatically close when download is finished
|
||||||
self.stay_open = stay_open
|
self.stay_open = stay_open
|
||||||
|
|
||||||
|
# traffic automatically goes through Tor
|
||||||
|
self.transparent_torification = transparent_torification
|
||||||
|
|
||||||
# files and dirs to delete on shutdown
|
# files and dirs to delete on shutdown
|
||||||
self.cleanup_filenames = []
|
self.cleanup_filenames = []
|
||||||
|
|
||||||
|
@ -100,92 +99,62 @@ class OnionShare(object):
|
||||||
self.port = tmpsock.getsockname()[1]
|
self.port = tmpsock.getsockname()[1]
|
||||||
tmpsock.close()
|
tmpsock.close()
|
||||||
|
|
||||||
def start_hidden_service(self, gui=False, tails_root=False):
|
def start_hidden_service(self, gui=False):
|
||||||
if not self.port:
|
if not self.port:
|
||||||
self.choose_port()
|
self.choose_port()
|
||||||
|
|
||||||
if helpers.get_platform() == 'Tails' and not tails_root:
|
if self.local_only:
|
||||||
# in Tails, start the hidden service in a root process
|
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
|
||||||
if gui:
|
return
|
||||||
args = ['/usr/bin/gksudo', '-D', 'OnionShare', '--', '/usr/bin/onionshare']
|
|
||||||
else:
|
|
||||||
args = ['/usr/bin/sudo', '--', '/usr/bin/onionshare']
|
|
||||||
print "Executing: {0:s}".format(args+[str(self.port)])
|
|
||||||
p = subprocess.Popen(args+[str(self.port)], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
||||||
stdout = p.stdout.read(22) # .onion URLs are 22 chars long
|
|
||||||
|
|
||||||
if stdout:
|
|
||||||
self.onion_host = stdout
|
|
||||||
print 'Got onion_host: {0:s}'.format(self.onion_host)
|
|
||||||
else:
|
|
||||||
if p.poll() == -1:
|
|
||||||
raise TailsError(o.stderr.read())
|
|
||||||
else:
|
|
||||||
raise TailsError(strings._("error_tails_unknown_root"))
|
|
||||||
|
|
||||||
|
# come up with a hidden service directory name
|
||||||
|
if helpers.get_platform() == 'Windows':
|
||||||
|
path = '{0:s}/onionshare'.format(os.environ['Temp'].replace('\\', '/'))
|
||||||
else:
|
else:
|
||||||
if self.local_only:
|
path = '/tmp/onionshare'
|
||||||
self.onion_host = '127.0.0.1:{0:d}'.format(self.port)
|
|
||||||
|
|
||||||
else:
|
try:
|
||||||
# come up with a hidden service directory name
|
if not os.path.exists(path):
|
||||||
if helpers.get_platform() == 'Tails':
|
os.makedirs(path, 0700)
|
||||||
# need to create HS directory in /var/lib/tor because of AppArmor rules included in Tails
|
except:
|
||||||
self.hidserv_dir = tempfile.mkdtemp(dir='/var/lib/tor')
|
raise HSDirError(strings._("error_hs_dir_cannot_create").format(path))
|
||||||
|
if not os.access(path, os.W_OK):
|
||||||
|
raise HSDirError(strings._("error_hs_dir_not_writable").format(path))
|
||||||
|
|
||||||
# change owner to debian-tor
|
self.hidserv_dir = tempfile.mkdtemp(dir=path)
|
||||||
import pwd
|
self.cleanup_filenames.append(self.hidserv_dir)
|
||||||
import grp
|
|
||||||
uid = pwd.getpwnam("debian-tor").pw_uid
|
|
||||||
gid = grp.getgrnam("debian-tor").gr_gid
|
|
||||||
os.chown(self.hidserv_dir, uid, gid)
|
|
||||||
else:
|
|
||||||
# in non-Tails linux, onionshare will create HS dir in /tmp/onionshare/*
|
|
||||||
path = '/tmp/onionshare'
|
|
||||||
try:
|
|
||||||
if not os.path.exists(path):
|
|
||||||
os.makedirs(path, 0700)
|
|
||||||
except:
|
|
||||||
raise HSDirError(strings._("error_hs_dir_cannot_create").format(path))
|
|
||||||
if not os.access(path, os.W_OK):
|
|
||||||
raise HSDirError(strings._("error_hs_dir_not_writable").format(path))
|
|
||||||
|
|
||||||
self.hidserv_dir = tempfile.mkdtemp(dir=path)
|
# connect to the tor controlport
|
||||||
self.cleanup_filenames.append(self.hidserv_dir)
|
self.controller = None
|
||||||
|
tor_control_ports = [9051, 9151]
|
||||||
|
for tor_control_port in tor_control_ports:
|
||||||
|
try:
|
||||||
|
self.controller = Controller.from_port(port=tor_control_port)
|
||||||
|
self.controller.authenticate()
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not self.controller:
|
||||||
|
raise NoTor(strings._("cant_connect_ctrlport").format(tor_control_ports))
|
||||||
|
|
||||||
# connect to the tor controlport
|
# set up hidden service
|
||||||
self.controller = None
|
hsdic = self.controller.get_conf_map('HiddenServiceOptions') or {
|
||||||
tor_control_ports = [9051, 9151]
|
'HiddenServiceDir': [], 'HiddenServicePort': []
|
||||||
for tor_control_port in tor_control_ports:
|
}
|
||||||
try:
|
if self.hidserv_dir in hsdic.get('HiddenServiceDir', []):
|
||||||
self.controller = Controller.from_port(port=tor_control_port)
|
# Maybe a stale service with the wrong local port
|
||||||
self.controller.authenticate()
|
dropme = hsdic['HiddenServiceDir'].index(self.hidserv_dir)
|
||||||
break
|
del hsdic['HiddenServiceDir'][dropme]
|
||||||
except:
|
del hsdic['HiddenServicePort'][dropme]
|
||||||
pass
|
hsdic['HiddenServiceDir'] = hsdic.get('HiddenServiceDir', [])+[self.hidserv_dir]
|
||||||
if not self.controller:
|
hsdic['HiddenServicePort'] = hsdic.get('HiddenServicePort', [])+[
|
||||||
raise NoTor(strings._("cant_connect_ctrlport").format(tor_control_ports))
|
'80 127.0.0.1:{0:d}'.format(self.port)]
|
||||||
|
|
||||||
# set up hidden service
|
self.controller.set_options(hsdic2list(hsdic))
|
||||||
if helpers.get_platform() == 'Windows':
|
|
||||||
self.hidserv_dir = self.hidserv_dir.replace('\\', '/')
|
|
||||||
hsdic = self.controller.get_conf_map('HiddenServiceOptions') or {
|
|
||||||
'HiddenServiceDir': [], 'HiddenServicePort': []
|
|
||||||
}
|
|
||||||
if self.hidserv_dir in hsdic.get('HiddenServiceDir', []):
|
|
||||||
# Maybe a stale service with the wrong local port
|
|
||||||
dropme = hsdic['HiddenServiceDir'].index(self.hidserv_dir)
|
|
||||||
del hsdic['HiddenServiceDir'][dropme]
|
|
||||||
del hsdic['HiddenServicePort'][dropme]
|
|
||||||
hsdic['HiddenServiceDir'] = hsdic.get('HiddenServiceDir', [])+[self.hidserv_dir]
|
|
||||||
hsdic['HiddenServicePort'] = hsdic.get('HiddenServicePort', [])+[
|
|
||||||
'80 127.0.0.1:{0:d}'.format(self.port)]
|
|
||||||
|
|
||||||
self.controller.set_options(hsdic2list(hsdic))
|
# figure out the .onion hostname
|
||||||
|
hostname_file = '{0:s}/hostname'.format(self.hidserv_dir)
|
||||||
# figure out the .onion hostname
|
self.onion_host = open(hostname_file, 'r').read().strip()
|
||||||
hostname_file = '{0:s}/hostname'.format(self.hidserv_dir)
|
|
||||||
self.onion_host = open(hostname_file, 'r').read().strip()
|
|
||||||
|
|
||||||
def wait_for_hs(self):
|
def wait_for_hs(self):
|
||||||
if self.local_only:
|
if self.local_only:
|
||||||
|
@ -199,9 +168,8 @@ class OnionShare(object):
|
||||||
sys.stdout.write('{0:s} '.format(strings._('wait_for_hs_trying')))
|
sys.stdout.write('{0:s} '.format(strings._('wait_for_hs_trying')))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
|
|
||||||
if helpers.get_platform() == 'Tails':
|
if self.transparent_torification:
|
||||||
# in Tails everything is proxies over Tor already
|
# no need to set the socks5 proxy
|
||||||
# so no need to set the socks5 proxy
|
|
||||||
urllib2.urlopen('http://{0:s}'.format(self.onion_host))
|
urllib2.urlopen('http://{0:s}'.format(self.onion_host))
|
||||||
else:
|
else:
|
||||||
tor_exists = False
|
tor_exists = False
|
||||||
|
@ -221,13 +189,13 @@ class OnionShare(object):
|
||||||
ready = True
|
ready = True
|
||||||
|
|
||||||
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_yup')))
|
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_yup')))
|
||||||
except socks.SOCKS5Error: # non-Tails error
|
except socks.SOCKS5Error:
|
||||||
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope')))
|
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope')))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except urllib2.HTTPError: # Tails error
|
except urllib2.HTTPError: # torification error
|
||||||
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope')))
|
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope')))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except httplib.BadStatusLine: # Tails (with bridge) error
|
except httplib.BadStatusLine: # torification (with bridge) error
|
||||||
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope')))
|
sys.stdout.write('{0:s}\n'.format(strings._('wait_for_hs_nope')))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
@ -235,46 +203,6 @@ class OnionShare(object):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def tails_root():
|
|
||||||
# if running in Tails and as root, do only the things that require root
|
|
||||||
if helpers.get_platform() == 'Tails' and helpers.is_root():
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
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:s}\n'.format(strings._("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
|
|
||||||
app = OnionShare()
|
|
||||||
app.choose_port()
|
|
||||||
app.port = port
|
|
||||||
app.start_hidden_service(False, True)
|
|
||||||
sys.stdout.write(app.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 main(cwd=None):
|
def main(cwd=None):
|
||||||
strings.load_strings()
|
strings.load_strings()
|
||||||
|
|
||||||
|
@ -283,12 +211,11 @@ def main(cwd=None):
|
||||||
if cwd:
|
if cwd:
|
||||||
os.chdir(cwd)
|
os.chdir(cwd)
|
||||||
|
|
||||||
tails_root()
|
|
||||||
|
|
||||||
# parse arguments
|
# parse arguments
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--local-only', action='store_true', dest='local_only', help=strings._("help_local_only"))
|
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('--stay-open', action='store_true', dest='stay_open', help=strings._("help_stay_open"))
|
||||||
|
parser.add_argument('--transparent', action='store_true', dest='transparent_torification', help=strings._("help_transparent_torification"))
|
||||||
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
parser.add_argument('--debug', action='store_true', dest='debug', help=strings._("help_debug"))
|
||||||
parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename'))
|
parser.add_argument('filename', metavar='filename', nargs='+', help=strings._('help_filename'))
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -300,6 +227,7 @@ def main(cwd=None):
|
||||||
local_only = bool(args.local_only)
|
local_only = bool(args.local_only)
|
||||||
debug = bool(args.debug)
|
debug = bool(args.debug)
|
||||||
stay_open = bool(args.stay_open)
|
stay_open = bool(args.stay_open)
|
||||||
|
transparent_torification = bool(args.transparent_torification)
|
||||||
|
|
||||||
# validation
|
# validation
|
||||||
valid = True
|
valid = True
|
||||||
|
@ -312,14 +240,12 @@ def main(cwd=None):
|
||||||
|
|
||||||
# start the onionshare app
|
# start the onionshare app
|
||||||
try:
|
try:
|
||||||
app = OnionShare(debug, local_only, stay_open)
|
app = OnionShare(debug, local_only, stay_open, transparent_torification)
|
||||||
app.choose_port()
|
app.choose_port()
|
||||||
print strings._("connecting_ctrlport").format(int(app.port))
|
print strings._("connecting_ctrlport").format(int(app.port))
|
||||||
app.start_hidden_service()
|
app.start_hidden_service()
|
||||||
except NoTor as e:
|
except NoTor as e:
|
||||||
sys.exit(e.args[0])
|
sys.exit(e.args[0])
|
||||||
except TailsError as e:
|
|
||||||
sys.exit(e.args[0])
|
|
||||||
except HSDirError as e:
|
except HSDirError as e:
|
||||||
sys.exit(e.args[0])
|
sys.exit(e.args[0])
|
||||||
|
|
||||||
|
@ -335,7 +261,7 @@ def main(cwd=None):
|
||||||
print ''
|
print ''
|
||||||
|
|
||||||
# start onionshare service in new thread
|
# start onionshare service in new thread
|
||||||
t = threading.Thread(target=web.start, args=(app.port, app.stay_open))
|
t = threading.Thread(target=web.start, args=(app.port, app.stay_open, app.transparent_torification))
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ def load_strings(default="en"):
|
||||||
p = helpers.get_platform()
|
p = helpers.get_platform()
|
||||||
|
|
||||||
# find locale dir
|
# find locale dir
|
||||||
if p == 'Linux' or p == 'Tails':
|
if p == 'Linux':
|
||||||
locale_dir = os.path.join(sys.prefix, 'share/onionshare/locale')
|
locale_dir = os.path.join(sys.prefix, 'share/onionshare/locale')
|
||||||
elif p == 'Darwin':
|
elif p == 'Darwin':
|
||||||
locale_dir = os.path.join(helpers.osx_resources_dir, 'locale')
|
locale_dir = os.path.join(helpers.osx_resources_dir, 'locale')
|
||||||
|
|
|
@ -83,16 +83,18 @@ slug = helpers.random_string(16)
|
||||||
download_count = 0
|
download_count = 0
|
||||||
|
|
||||||
stay_open = False
|
stay_open = False
|
||||||
|
|
||||||
|
|
||||||
def set_stay_open(new_stay_open):
|
def set_stay_open(new_stay_open):
|
||||||
global stay_open
|
global stay_open
|
||||||
stay_open = new_stay_open
|
stay_open = new_stay_open
|
||||||
|
|
||||||
|
|
||||||
def get_stay_open():
|
def get_stay_open():
|
||||||
return stay_open
|
return stay_open
|
||||||
|
|
||||||
|
transparent_torification = False
|
||||||
|
def set_transparent_torification(new_transparent_torification):
|
||||||
|
global transparent_torification
|
||||||
|
stay_open = new_transparent_torification
|
||||||
|
def get_transparent_torification():
|
||||||
|
return transparent_torification
|
||||||
|
|
||||||
def debug_mode():
|
def debug_mode():
|
||||||
import logging
|
import logging
|
||||||
|
@ -224,16 +226,15 @@ def shutdown(shutdown_slug_candidate):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def start(port, stay_open=False):
|
def start(port, stay_open=False, transparent_torification=False):
|
||||||
set_stay_open(stay_open)
|
set_stay_open(stay_open)
|
||||||
|
set_transparent_torification(transparent_torification)
|
||||||
app.run(port=port, threaded=True)
|
app.run(port=port, threaded=True)
|
||||||
|
|
||||||
|
|
||||||
def stop(port):
|
def stop(port):
|
||||||
# to stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
|
# to stop flask, load http://127.0.0.1:<port>/<shutdown_slug>/shutdown
|
||||||
if helpers.get_platform() == 'Tails':
|
if transparent_torification:
|
||||||
# in Tails everything is proxies over Tor, so we need to get lower level
|
|
||||||
# to connect not over the proxy
|
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
s = socket.socket()
|
s = socket.socket()
|
||||||
|
|
|
@ -39,7 +39,7 @@ from options import Options
|
||||||
class Application(QtGui.QApplication):
|
class Application(QtGui.QApplication):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
platform = helpers.get_platform()
|
platform = helpers.get_platform()
|
||||||
if platform == 'Tails' or platform == 'Linux':
|
if platform == 'Linux':
|
||||||
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
|
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
|
||||||
QtGui.QApplication.__init__(self, sys.argv)
|
QtGui.QApplication.__init__(self, sys.argv)
|
||||||
|
|
||||||
|
@ -128,11 +128,6 @@ class OnionShareGui(QtGui.QWidget):
|
||||||
self.server_status.stop_server()
|
self.server_status.stop_server()
|
||||||
self.status_bar.clearMessage()
|
self.status_bar.clearMessage()
|
||||||
return
|
return
|
||||||
except onionshare.TailsError as e:
|
|
||||||
alert(e.args[0], QtGui.QMessageBox.Warning)
|
|
||||||
self.server_status.stop_server()
|
|
||||||
self.status_bar.clearMessage()
|
|
||||||
return
|
|
||||||
|
|
||||||
# start onionshare service in new thread
|
# start onionshare service in new thread
|
||||||
t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open))
|
t = threading.Thread(target=web.start, args=(self.app.port, self.app.stay_open))
|
||||||
|
|
|
@ -21,14 +21,6 @@ from nose import with_setup
|
||||||
|
|
||||||
import test_helpers
|
import test_helpers
|
||||||
|
|
||||||
|
|
||||||
def test_get_platform_on_tails():
|
|
||||||
"""get_platform() returns 'Tails' when hostname is 'amnesia'"""
|
|
||||||
helpers.platform.uname = lambda: ('Linux', 'amnesia', '3.14-1-amd64',
|
|
||||||
'#1 SMP Debian 3.14.4-1 (2014-05-13)', 'x86_64', '')
|
|
||||||
assert helpers.get_platform() == 'Tails'
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_platform_returns_platform_system():
|
def test_get_platform_returns_platform_system():
|
||||||
"""get_platform() returns platform.system() when ONIONSHARE_PLATFORM is not defined"""
|
"""get_platform() returns platform.system() when ONIONSHARE_PLATFORM is not defined"""
|
||||||
helpers.platform.system = lambda: 'Sega Saturn'
|
helpers.platform.system = lambda: 'Sega Saturn'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue